feat(app): an openidea shell command that opens files and folders in the running app (macOS)

The bundle ships Contents/Resources/bin/openidea, a script that runs the app
executable in CLI mode. It sends its targets (--line/--column, or
path:line:column) to the running app over a per-identifier Unix socket, or
starts the app and waits for it. Install/Uninstall 'openidea' Command in the
app menu links it onto PATH.

Restoring a project's editor session no longer counts as the user picking a
tab, so a file opened with the project keeps its caret and activation.

Closes #1286

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Цей коміт міститься в:
jawтаClaude Opus 5.5 authored and l0ng-ai committed 2026-11-07 21:57:29 +08:00
1 джерело 90cca59462
коміт be0a6b4359
22 files changed
+916 -8

No files matched your search

+4
Переглянути файл
@@ -952,6 +952,10 @@ export const commands = {
*/
menuSetDisabled: (ids: string[]) => __TAURI_INVOKE<void>("menu_set_disabled", { ids }),
appLogLocation: () => typedError<LogLocation, IdeError>(__TAURI_INVOKE("app_log_location")),
/** Links the `openidea` command onto `PATH` (macOS app bundle only); returns the link. */
appCliInstall: () => typedError<IpcPath, IdeError>(__TAURI_INVOKE("app_cli_install")),
/** Removes the installed `openidea` command; returns the links removed. */
appCliUninstall: () => typedError<IpcPath[], IdeError>(__TAURI_INVOKE("app_cli_uninstall")),
appBuildInfo: () => __TAURI_INVOKE<BuildInfo>("app_build_info"),
appCheckUpdate: (source: UpdateSource) => typedError<UpdateCheck, IdeError>(__TAURI_INVOKE("app_check_update", { source })),
/**
+20
Переглянути файл
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { registerFileEditorProvider } from "@/workbench/contributions";
import { EditorController, openFilePaths, useTabs } from "./controller";
import { createMemoryBackend } from "./backend";
import { storeSession } from "./model/session";
import { initialTabsState } from "./model/tabs";
import { useProject } from "@/platform/project";
@@ -63,6 +64,25 @@ function tabFor(path: string) {
}
describe("file open order", () => {
it("keeps the caret and activation of a file opened while the project's session restores", async () => {
const before = setup();
before.restoreSession("/p");
await before.openFile("/p/b.ts", { focus: false });
before.stop();
useTabs.setState(initialTabsState(), true);
try {
const controller = setup();
const open = controller.openFile("/p/a.ts", { selection: { line: 1, col: 2 }, focus: false });
controller.restoreSession("/p");
await open;
expect(openFilePaths(useTabs.getState()).sort()).toEqual(["/p/a.ts", "/p/b.ts"]);
expect(activePath()).toBe("/p/a.ts");
expect(controller.slotFor(tabFor("/p/a.ts")!.id)?.caret()).toMatchObject({ line: 1, col: 2 });
} finally {
storeSession("/p", null);
}
});
it.each([1, 30])("keeps the newer file active after an earlier probe (tab limit: %s)", async (tabLimit) => {
useProject.setState({ settings: { editor: { tabs: { tabLimit } } } as never });
const { started, response } = delayProvider();
+55
Переглянути файл
@@ -0,0 +1,55 @@
/** Install / Uninstall the `openidea` shell command (macOS app bundle only). */
import type { Action } from "@/platform/actions";
import { t } from "@/platform/i18n";
import { call, commands, isCancelled } from "@/platform/ipc";
import { notifications } from "@/workbench/contributions";
const KEY = "app.cli";
function errorText(e: unknown) {
return e instanceof Error ? e.message : String(e);
}
async function installCli(): Promise<void> {
try {
const link = await call(commands.appCliInstall());
notifications().notify({ level: "info", title: t("app.cli.installed"), body: link, key: KEY });
} catch (e) {
if (!isCancelled(e))
notifications().notify({ level: "error", title: t("app.cli.installFailed"), body: errorText(e), key: KEY });
}
}
async function uninstallCli(): Promise<void> {
try {
const links = await call(commands.appCliUninstall());
notifications().notify({
level: "info",
title: t(links.length ? "app.cli.uninstalled" : "app.cli.notInstalled"),
body: links.join("\n") || undefined,
key: KEY,
});
} catch (e) {
if (!isCancelled(e))
notifications().notify({ level: "error", title: t("app.cli.uninstallFailed"), body: errorText(e), key: KEY });
}
}
export function cliActions(): Action[] {
return [
{
id: "app.installCli",
title: t("app.action.installCli"),
category: t("app.category.tools"),
keywords: ["cli", "shell", "terminal", "path", "command line", "launcher"],
run: installCli,
},
{
id: "app.uninstallCli",
title: t("app.action.uninstallCli"),
category: t("app.category.tools"),
keywords: ["cli", "shell", "terminal", "path", "command line", "launcher"],
run: uninstallCli,
},
];
}
+3 -1
Переглянути файл
@@ -6,10 +6,11 @@ import { registerOverlay, registerStatusItem } from "@/workbench/contributions";
import { AboutOverlay } from "./AboutDialog";
import { appActions, updateActions } from "./appActions";
import { checkForUpdatesOnStartup } from "./updates";
import { cliActions } from "./cliCommand";
import { installCloseGuard } from "./closeGuard";
import { TerminationOverlay } from "./termination";
import { InvalidateCachesOverlay, registerInvalidateCachesActions } from "./InvalidateCaches";
import { buildInfo } from "./host";
import { buildInfo, guessOs } from "./host";
import { KeymapReferenceOverlay } from "./KeymapReference";
import { LightEditStatus } from "./LightEditStatus";
import { installLocaleSync, registerLocalizedActions } from "./locale";
@@ -31,6 +32,7 @@ export function activate(): () => void {
const disposers = [
installLocaleSync(),
registerLocalizedActions(() => [...appActions(), ...windowActions()]),
registerLocalizedActions(() => (guessOs() === "macos" ? cliActions() : [])),
registerOverlay({ id: "app.about", component: AboutOverlay }),
registerOverlay({ id: "app.keymapReference", component: KeymapReferenceOverlay }),
registerOverlay({ id: "app.invalidateCaches", component: InvalidateCachesOverlay }),
+10
Переглянути файл
@@ -17,6 +17,7 @@
"app.action.checkForUpdates": "Check for Updates…",
"app.action.copy": "Copy",
"app.action.cut": "Cut",
"app.action.installCli": "Install 'openidea' Command",
"app.action.keymapReference": "Keymap Reference",
"app.action.lightEditOpenProject": "Open Containing Folder as Project",
"app.action.newWindow": "New Window",
@@ -32,11 +33,18 @@
"app.action.showLog": "{os, select, macos {Show Log in Finder} windows {Show Log in Explorer} other {Show Log in Files}}",
"app.action.toggleFullScreen": "Toggle Full Screen",
"app.action.undo": "Undo",
"app.action.uninstallCli": "Uninstall 'openidea' Command",
"app.category.edit": "Edit",
"app.category.file": "File",
"app.category.help": "Help",
"app.category.tools": "Tools",
"app.category.view": "View",
"app.category.window": "Window",
"app.cli.installFailed": "Cannot install the openidea command",
"app.cli.installed": "The openidea command is installed",
"app.cli.notInstalled": "The openidea command is not installed",
"app.cli.uninstallFailed": "Cannot uninstall the openidea command",
"app.cli.uninstalled": "The openidea command is uninstalled",
"app.exit.body": "Are you sure you want to exit OpenIDEA?",
"app.exit.confirm": "Exit",
"app.exit.dontAsk": "Don't ask again",
@@ -100,7 +108,9 @@
"menu.app": "OpenIDEA",
"menu.app.about": "About OpenIDEA",
"menu.app.checkForUpdates": "Check for Updates…",
"menu.app.installCli": "Install 'openidea' Command",
"menu.app.quit": "{os, select, macos {Quit OpenIDEA} other {Exit}}",
"menu.app.uninstallCli": "Uninstall 'openidea' Command",
"menu.appearance": "Appearance",
"menu.code": "Code",
"menu.code.commentBlock": "Comment with Block Comment",
+10
Переглянути файл
@@ -17,6 +17,7 @@
"app.action.checkForUpdates": "检查更新…",
"app.action.copy": "复制",
"app.action.cut": "剪切",
"app.action.installCli": "安装 'openidea' 命令",
"app.action.keymapReference": "快捷键参考",
"app.action.lightEditOpenProject": "将所在文件夹作为项目打开",
"app.action.newWindow": "新建窗口",
@@ -32,11 +33,18 @@
"app.action.showLog": "{os, select, macos {在访达中显示日志} windows {在资源管理器中显示日志} other {在文件管理器中显示日志}}",
"app.action.toggleFullScreen": "切换全屏",
"app.action.undo": "撤销",
"app.action.uninstallCli": "卸载 'openidea' 命令",
"app.category.edit": "编辑",
"app.category.file": "文件",
"app.category.help": "帮助",
"app.category.tools": "工具",
"app.category.view": "视图",
"app.category.window": "窗口",
"app.cli.installFailed": "无法安装 openidea 命令",
"app.cli.installed": "openidea 命令已安装",
"app.cli.notInstalled": "openidea 命令未安装",
"app.cli.uninstallFailed": "无法卸载 openidea 命令",
"app.cli.uninstalled": "openidea 命令已卸载",
"app.exit.body": "确定要退出 OpenIDEA 吗?",
"app.exit.confirm": "退出",
"app.exit.dontAsk": "不再询问",
@@ -100,7 +108,9 @@
"menu.app": "OpenIDEA",
"menu.app.about": "关于 OpenIDEA",
"menu.app.checkForUpdates": "检查更新…",
"menu.app.installCli": "安装 'openidea' 命令",
"menu.app.quit": "{os, select, macos {退出 OpenIDEA} other {退出}}",
"menu.app.uninstallCli": "卸载 'openidea' 命令",
"menu.appearance": "外观",
"menu.code": "代码",
"menu.code.commentBlock": "使用块注释",