Files
paseo/packages/desktop/src/features/editor-targets/registry.test.ts
T
Mohamed BoudraandGitHub f4509fe044 Open files in more installed editors (#2119)
* feat(desktop): open files in more installed editors

Detect bundled app commands as well as PATH launchers and preserve file positions through editor-specific launches.

* fix(desktop): preserve editor target compatibility

Keep existing file-manager preference ids, retain project context for file launches, and recognize Windows 64-bit IDE commands.
2026-07-16 14:39:55 +02:00

293 lines
9.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { listAvailableEditorTargets, openEditorTarget } from "./registry.js";
import type { EditorTargetIcon, EditorTargetRuntime } from "./target.js";
import { cursorTarget } from "./targets/cursor.js";
import { explorerTarget, fileManagerTarget, finderTarget } from "./targets/file-manager.js";
import { intellijIdeaTarget } from "./targets/intellij-idea.js";
import { pycharmTarget } from "./targets/pycharm.js";
import { vscodeTarget } from "./targets/vscode.js";
import { webstormTarget } from "./targets/webstorm.js";
import { zedTarget } from "./targets/zed.js";
interface RecordedLaunch {
command: string;
args: string[];
}
class FakeEditorTargets implements EditorTargetRuntime {
readonly launches: RecordedLaunch[] = [];
readonly openedPaths: string[] = [];
readonly revealedPaths: string[] = [];
readonly openedMacApplications: Array<{
applicationName: string;
paths: string[];
}> = [];
readonly env: NodeJS.ProcessEnv;
readonly platform: NodeJS.Platform;
private readonly paths = new Set<string>();
private readonly commands = new Map<string, string>();
private readonly macApplications = new Set<string>();
constructor(platform: NodeJS.Platform = "linux", env: NodeJS.ProcessEnv = {}) {
this.platform = platform;
this.env = env;
}
addPath(targetPath: string): void {
this.paths.add(targetPath);
}
installCommand(command: string, executable = `/bin/${command}`): void {
this.commands.set(command, executable);
}
installMacApplication(applicationName: string): void {
this.macApplications.add(applicationName);
}
pathExists(targetPath: string): boolean {
return this.paths.has(targetPath);
}
isAbsolutePath(targetPath: string): boolean {
return targetPath.startsWith("/") || /^[A-Z]:\//u.test(targetPath);
}
resolveCommand(commands: readonly string[]): string | null {
for (const command of commands) {
const executable = this.commands.get(command);
if (executable) return executable;
}
return null;
}
async spawnDetached(input: { command: string; args: readonly string[] }): Promise<void> {
this.launches.push({ command: input.command, args: [...input.args] });
}
async openPath(targetPath: string): Promise<void> {
this.openedPaths.push(targetPath);
}
revealPath(targetPath: string): void {
this.revealedPaths.push(targetPath);
}
async loadIcon(fileName: string): Promise<EditorTargetIcon> {
return { kind: "image", dataUrl: `data:image/png;base64,${fileName}` };
}
hasMacApplication(applicationName: string): boolean {
return this.macApplications.has(applicationName);
}
async openMacApplication(input: {
applicationName: string;
paths: readonly string[];
}): Promise<void> {
this.openedMacApplications.push({
applicationName: input.applicationName,
paths: [...input.paths],
});
}
}
describe("editor target registry", () => {
it("lists installed target implementations in registration order", async () => {
const runtime = new FakeEditorTargets();
runtime.installCommand("code");
runtime.installCommand("webstorm");
const targets = await listAvailableEditorTargets(runtime, [
cursorTarget,
vscodeTarget,
webstormTarget,
fileManagerTarget,
]);
expect(targets).toEqual([
{
id: "vscode",
label: "VS Code",
kind: "editor",
icon: { kind: "image", dataUrl: "data:image/png;base64,vscode.png" },
},
{
id: "webstorm",
label: "WebStorm",
kind: "editor",
icon: { kind: "image", dataUrl: "data:image/png;base64,webstorm.png" },
},
{
id: "file-manager",
label: "Files",
kind: "file-manager",
icon: { kind: "symbol", name: "folder" },
},
]);
});
it("opens a selected file at its position through the target implementation", async () => {
const runtime = new FakeEditorTargets();
runtime.installCommand("code");
runtime.addPath("/repo");
runtime.addPath("/repo/src/app.ts");
await openEditorTarget(
{
editorId: "vscode",
workspacePath: "/repo",
filePath: "/repo/src/app.ts",
line: 12,
column: 4,
},
runtime,
[vscodeTarget],
);
expect(runtime.launches).toEqual([
{
command: "/bin/code",
args: ["/repo", "--goto", "/repo/src/app.ts:12:4"],
},
]);
});
it("lets each target choose its own command and arguments", async () => {
const runtime = new FakeEditorTargets();
runtime.installCommand("zeditor");
runtime.installCommand("webstorm");
runtime.installCommand("idea");
await zedTarget.launch(
{ workspacePath: "/repo", filePath: "/repo/src/app.ts", line: 7, column: 2 },
runtime,
);
await webstormTarget.launch(
{ workspacePath: "/repo", filePath: "/repo/src/app.ts", line: 7, column: 2 },
runtime,
);
await intellijIdeaTarget.launch({ workspacePath: "/repo" }, runtime);
expect(runtime.launches).toEqual([
{ command: "/bin/zeditor", args: ["/repo", "/repo/src/app.ts:7:2"] },
{
command: "/bin/webstorm",
args: ["--line", "7", "--column", "2", "/repo", "/repo/src/app.ts"],
},
{ command: "/bin/idea", args: ["/repo"] },
]);
});
it("recognizes Windows 64-bit project IDE launchers", async () => {
const runtime = new FakeEditorTargets("win32");
runtime.installCommand("pycharm64", "C:/Tools/PyCharm/bin/pycharm64.exe");
expect(await pycharmTarget.isInstalled(runtime)).toBe(true);
await pycharmTarget.launch(
{ workspacePath: "C:/repo", filePath: "C:/repo/src/app.py", line: 6 },
runtime,
);
expect(runtime.launches).toEqual([
{
command: "C:/Tools/PyCharm/bin/pycharm64.exe",
args: ["--line", "6", "C:/repo", "C:/repo/src/app.py"],
},
]);
});
it("detects and launches the macOS application when the command is absent", async () => {
const runtime = new FakeEditorTargets("darwin");
runtime.installMacApplication("Cursor");
expect(await cursorTarget.isInstalled(runtime)).toBe(true);
await cursorTarget.launch({ workspacePath: "/repo", filePath: "/repo/src/app.ts" }, runtime);
expect(runtime.openedMacApplications).toEqual([
{
applicationName: "Cursor",
paths: ["/repo", "/repo/src/app.ts"],
},
]);
});
it("uses Cursor's bundled macOS command so file positions survive application detection", async () => {
const runtime = new FakeEditorTargets("darwin");
const bundledCommand = "/Applications/Cursor.app/Contents/Resources/app/bin/cursor";
runtime.installCommand(bundledCommand, bundledCommand);
expect(await cursorTarget.isInstalled(runtime)).toBe(true);
await cursorTarget.launch(
{ workspacePath: "/repo", filePath: "/repo/src/app.ts", line: 18, column: 3 },
runtime,
);
expect(runtime.launches).toEqual([
{
command: bundledCommand,
args: ["/repo", "--goto", "/repo/src/app.ts:18:3"],
},
]);
});
it("detects Cursor's installed Windows command when it is absent from PATH", async () => {
const runtime = new FakeEditorTargets("win32", {
LOCALAPPDATA: "C:/Users/me/AppData/Local",
});
const installedCommand =
"C:/Users/me/AppData/Local/Programs/cursor/resources/app/bin/cursor.cmd";
runtime.installCommand(installedCommand, installedCommand);
expect(await cursorTarget.isInstalled(runtime)).toBe(true);
await cursorTarget.launch(
{ workspacePath: "C:/repo", filePath: "C:/repo/src/app.ts", line: 9 },
runtime,
);
expect(runtime.launches).toEqual([
{
command: installedCommand,
args: ["C:/repo", "--goto", "C:/repo/src/app.ts:9"],
},
]);
});
it("delegates folder opening and file reveal to the system file manager", async () => {
const runtime = new FakeEditorTargets("win32");
expect(await explorerTarget.describe(runtime)).toEqual({
id: "explorer",
label: "Explorer",
kind: "file-manager",
icon: { kind: "symbol", name: "folder" },
});
await explorerTarget.launch({ workspacePath: "C:/repo" }, runtime);
await explorerTarget.launch(
{ workspacePath: "C:/repo", filePath: "C:/repo/src/app.ts" },
runtime,
);
expect(runtime.openedPaths).toEqual(["C:/repo"]);
expect(runtime.revealedPaths).toEqual(["C:/repo/src/app.ts"]);
});
it("keeps the platform file-manager ids used by stored preferences", async () => {
const macTargets = await listAvailableEditorTargets(new FakeEditorTargets("darwin"), [
finderTarget,
explorerTarget,
fileManagerTarget,
]);
const windowsTargets = await listAvailableEditorTargets(new FakeEditorTargets("win32"), [
finderTarget,
explorerTarget,
fileManagerTarget,
]);
expect(macTargets.map((target) => target.id)).toEqual(["finder"]);
expect(windowsTargets.map((target) => target.id)).toEqual(["explorer"]);
});
});