дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
feat(app): install the openidea command on macOS, Windows and Linux (#2009)
Adds Install/Uninstall 'openidea' Command actions. macOS uses the bundle launcher, Windows a user PATH wrapper and named pipe, and Linux the executable/AppImage path with a PATH hint. Files, folders, positions and `diff` go to the running app or start it. Closes #1286 Co-authored-by: jaw52 <[email protected]>
Цей коміт міститься в:
1 джерело
25424465c0
коміт
0bfd4fa3cc
28 files changed
+1601
-42
No files matched your search
@@ -979,6 +979,10 @@ export const commands = {
|
||||
*/
|
||||
menuSetDisabled: (ids: string[]) => __TAURI_INVOKE<void>("menu_set_disabled", { ids }),
|
||||
appLogLocation: () => typedError<LogLocation, IdeError>(__TAURI_INVOKE("app_log_location")),
|
||||
/** Installs the `openidea` command for this user. */
|
||||
appCliInstall: () => typedError<CliInstallation, 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 })),
|
||||
/**
|
||||
@@ -2284,6 +2288,11 @@ export type ClearCachesResult = {
|
||||
rssAfterBytes: number | null,
|
||||
};
|
||||
|
||||
export type CliInstallation = {
|
||||
path: IpcPath,
|
||||
pathDirectory: IpcPath | null,
|
||||
};
|
||||
|
||||
export type CloseContext = {
|
||||
/** Quit is in progress (exit was already confirmed). */
|
||||
quitting: boolean,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { beforeEach, expect, it, vi } from "vitest";
|
||||
import { getContext } from "@/platform/context";
|
||||
import { provideNotificationService } from "@/workbench/contributions";
|
||||
|
||||
const { install, uninstall, notify } = vi.hoisted(() => ({ install: vi.fn(), uninstall: vi.fn(), notify: vi.fn() }));
|
||||
vi.mock("@/platform/ipc", () => ({
|
||||
call: (result: Promise<unknown>) => result,
|
||||
commands: { appCliInstall: install, appCliUninstall: uninstall },
|
||||
isCancelled: (error: unknown) => (error as { kind?: string })?.kind === "cancelled",
|
||||
}));
|
||||
|
||||
const { cliActions } = await import("../cliCommand");
|
||||
const run = (id: string) =>
|
||||
cliActions()
|
||||
.find((action) => action.id === id)!
|
||||
.run({ ctx: getContext() });
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
provideNotificationService({ notify, expire: () => {} });
|
||||
});
|
||||
|
||||
it("explains the Linux PATH step when the user bin is missing from PATH", async () => {
|
||||
install.mockResolvedValue({ path: "/home/user/.local/bin/openidea", pathDirectory: "/home/user/.local/bin" });
|
||||
await run("app.installCli");
|
||||
expect(notify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
level: "info",
|
||||
body: expect.stringContaining("Add /home/user/.local/bin to the beginning of PATH"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("tells users to open a new terminal after installation", async () => {
|
||||
install.mockResolvedValue({ path: "C:/Users/user/OpenIDEA/bin/openidea.cmd", pathDirectory: null });
|
||||
await run("app.installCli");
|
||||
expect(notify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ body: expect.stringContaining("Open a new terminal") }),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports an absent command on repeated uninstall", async () => {
|
||||
uninstall.mockResolvedValue([]);
|
||||
await run("app.uninstallCli");
|
||||
expect(notify).toHaveBeenCalledWith(expect.objectContaining({ title: "The openidea command is not installed" }));
|
||||
});
|
||||
|
||||
it("reports an install failure and stays quiet when an administrator prompt is cancelled", async () => {
|
||||
install.mockRejectedValueOnce(new Error("foreign command"));
|
||||
await run("app.installCli");
|
||||
expect(notify).toHaveBeenCalledWith(expect.objectContaining({ level: "error", body: "foreign command" }));
|
||||
notify.mockClear();
|
||||
install.mockRejectedValueOnce({ kind: "cancelled" });
|
||||
await run("app.installCli");
|
||||
expect(notify).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
/** Install / Uninstall the `openidea` shell command. */
|
||||
import type { Action } from "@/platform/actions";
|
||||
import { tx } 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 result = await call(commands.appCliInstall());
|
||||
const hint = result.pathDirectory
|
||||
? tx`Add ${result.pathDirectory} to the beginning of PATH, then open a new terminal.`
|
||||
: tx("Open a new terminal to use the openidea command.");
|
||||
notifications().notify({
|
||||
level: "info",
|
||||
title: tx("The openidea command is installed"),
|
||||
body: `${result.path}\n${hint}`,
|
||||
key: KEY,
|
||||
});
|
||||
} catch (e) {
|
||||
if (!isCancelled(e))
|
||||
notifications().notify({
|
||||
level: "error",
|
||||
title: tx("Cannot install the openidea command"),
|
||||
body: errorText(e),
|
||||
key: KEY,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function uninstallCli(): Promise<void> {
|
||||
try {
|
||||
const links = await call(commands.appCliUninstall());
|
||||
notifications().notify({
|
||||
level: "info",
|
||||
title: tx(links.length ? "The openidea command is uninstalled" : "The openidea command is not installed"),
|
||||
body: links.join("\n") || undefined,
|
||||
key: KEY,
|
||||
});
|
||||
} catch (e) {
|
||||
if (!isCancelled(e))
|
||||
notifications().notify({
|
||||
level: "error",
|
||||
title: tx("Cannot uninstall the openidea command"),
|
||||
body: errorText(e),
|
||||
key: KEY,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function cliActions(): Action[] {
|
||||
return [
|
||||
{
|
||||
id: "app.installCli",
|
||||
title: tx("Install 'openidea' Command"),
|
||||
category: tx("Tools"),
|
||||
keywords: ["cli", "shell", "terminal", "path", "command line", "launcher"],
|
||||
run: installCli,
|
||||
},
|
||||
{
|
||||
id: "app.uninstallCli",
|
||||
title: tx("Uninstall 'openidea' Command"),
|
||||
category: tx("Tools"),
|
||||
keywords: ["cli", "shell", "terminal", "path", "command line", "launcher"],
|
||||
run: uninstallCli,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -6,6 +6,7 @@ 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";
|
||||
@@ -31,6 +32,7 @@ export function activate(): () => void {
|
||||
const disposers = [
|
||||
installLocaleSync(),
|
||||
registerLocalizedActions(() => [...appActions(), ...windowActions()]),
|
||||
registerLocalizedActions(cliActions),
|
||||
registerOverlay({ id: "app.about", component: AboutOverlay }),
|
||||
registerOverlay({ id: "app.keymapReference", component: KeymapReferenceOverlay }),
|
||||
registerOverlay({ id: "app.invalidateCaches", component: InvalidateCachesOverlay }),
|
||||
|
||||
@@ -100,7 +100,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",
|
||||
|
||||
@@ -130,6 +130,15 @@
|
||||
"WAR Archive": "WAR Archive",
|
||||
"WAR (exploded)": "WAR (exploded)",
|
||||
"No project is currently open.": "No project is currently open.",
|
||||
"Install 'openidea' Command": "Install 'openidea' Command",
|
||||
"Uninstall 'openidea' Command": "Uninstall 'openidea' Command",
|
||||
"The openidea command is installed": "The openidea command is installed",
|
||||
"The openidea command is uninstalled": "The openidea command is uninstalled",
|
||||
"The openidea command is not installed": "The openidea command is not installed",
|
||||
"Cannot install the openidea command": "Cannot install the openidea command",
|
||||
"Cannot uninstall the openidea command": "Cannot uninstall the openidea command",
|
||||
"Open a new terminal to use the openidea command.": "Open a new terminal to use the openidea command.",
|
||||
"Add {path} to the beginning of PATH, then open a new terminal.": "Add {path} to the beginning of PATH, then open a new terminal.",
|
||||
"Default SDK used by modules, builds, and run configurations unless overridden at the module level.": "Default SDK used by modules, builds, and run configurations unless overridden at the module level.",
|
||||
"Project SDK": "Project SDK",
|
||||
"Default Java language level for source code assistance and compilation across project modules.": "Default Java language level for source code assistance and compilation across project modules.",
|
||||
|
||||
@@ -1715,6 +1715,15 @@
|
||||
"WAR Archive": "WAR 归档",
|
||||
"WAR (exploded)": "WAR(展开)",
|
||||
"No project is currently open.": "当前未打开任何项目。",
|
||||
"Install 'openidea' Command": "安装 'openidea' 命令",
|
||||
"Uninstall 'openidea' Command": "卸载 'openidea' 命令",
|
||||
"The openidea command is installed": "openidea 命令已安装",
|
||||
"The openidea command is uninstalled": "openidea 命令已卸载",
|
||||
"The openidea command is not installed": "openidea 命令未安装",
|
||||
"Cannot install the openidea command": "无法安装 openidea 命令",
|
||||
"Cannot uninstall the openidea command": "无法卸载 openidea 命令",
|
||||
"Open a new terminal to use the openidea command.": "请打开新终端以使用 openidea 命令。",
|
||||
"Add {path} to the beginning of PATH, then open a new terminal.": "请将 {path} 添加到 PATH 开头,然后打开新终端。",
|
||||
"Default SDK used by modules, builds, and run configurations unless overridden at the module level.": "模块、构建和运行配置默认使用的 SDK(除非在模块级别覆盖)。",
|
||||
"Project SDK": "项目 SDK",
|
||||
"Default Java language level for source code assistance and compilation across project modules.": "项目各模块用于源码辅助和编译的默认 Java 语言级别。",
|
||||
|
||||
@@ -100,7 +100,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": "使用块注释",
|
||||
|
||||
Reference in new issue
Заблокувати користувача