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.
This commit is contained in:
Mohamed Boudra
2026-07-16 14:39:55 +02:00
committed by GitHub
parent 3e8dce7d7c
commit f4509fe044
50 changed files with 1665 additions and 1051 deletions
@@ -2,19 +2,23 @@ import { useQuery } from "@tanstack/react-query";
import { getDesktopHost, type DesktopEditorBridge } from "@/desktop/host";
export type DesktopOpenTargetKind = "editor" | "file-manager";
export type DesktopOpenMode = "open" | "reveal";
export type DesktopOpenTargetIcon =
| { kind: "image"; dataUrl: string }
| { kind: "symbol"; name: "folder" | "terminal" };
export interface DesktopOpenTarget {
id: string;
label: string;
kind: DesktopOpenTargetKind;
icon: DesktopOpenTargetIcon;
}
export interface OpenDesktopTargetInput {
editorId: string;
path: string;
cwd?: string;
mode?: DesktopOpenMode;
workspacePath: string;
filePath?: string;
line?: number;
column?: number;
}
interface AvailableDesktopEditorBridge {
@@ -1,11 +0,0 @@
import { describe, expect, it } from "vitest";
import { isKnownEditorTargetId } from "./editor-targets";
describe("editor target ids", () => {
it("recognizes built-ins without typing custom ids out of the system", () => {
const customEditorId: string = "script:open-in-nvim";
expect(isKnownEditorTargetId("vscode")).toBe(true);
expect(isKnownEditorTargetId(customEditorId)).toBe(false);
});
});
@@ -1,16 +0,0 @@
export type EditorTargetId = string;
const KNOWN_EDITOR_TARGET_IDS: ReadonlySet<string> = new Set([
"cursor",
"vscode",
"webstorm",
"zed",
"antigravity",
"finder",
"explorer",
"file-manager",
]);
export function isKnownEditorTargetId(editorId: EditorTargetId): boolean {
return KNOWN_EDITOR_TARGET_IDS.has(editorId);
}
@@ -2,8 +2,18 @@ import { describe, expect, it } from "vitest";
import { planWorkspaceOpenTargets } from "./open-target-planner";
const desktopTargets = [
{ id: "vscode", label: "VS Code", kind: "editor" as const },
{ id: "finder", label: "Finder", kind: "file-manager" as const },
{
id: "vscode",
label: "VS Code",
kind: "editor" as const,
icon: { kind: "symbol" as const, name: "terminal" as const },
},
{
id: "finder",
label: "Finder",
kind: "file-manager" as const,
icon: { kind: "symbol" as const, name: "folder" as const },
},
];
const checkoutStatus = {
@@ -25,7 +35,12 @@ describe("planWorkspaceOpenTargets", () => {
expect(targets[0]).toMatchObject({
source: "desktop",
id: "vscode",
openInput: { editorId: "vscode", path: "/repo/src/app.ts", cwd: "/repo" },
openInput: {
editorId: "vscode",
workspacePath: "/repo",
filePath: "/repo/src/app.ts",
line: 3,
},
});
});
@@ -41,7 +56,11 @@ describe("planWorkspaceOpenTargets", () => {
expect(targets[1]).toMatchObject({
source: "desktop",
id: "finder",
openInput: { editorId: "finder", path: "/repo/src/app.ts", mode: "reveal" },
openInput: {
editorId: "finder",
workspacePath: "/repo",
filePath: "/repo/src/app.ts",
},
});
});
@@ -56,12 +75,12 @@ describe("planWorkspaceOpenTargets", () => {
expect(targets[0]).toMatchObject({
source: "desktop",
id: "vscode",
openInput: { editorId: "vscode", path: "/repo" },
openInput: { editorId: "vscode", workspacePath: "/repo" },
});
expect(targets[1]).toMatchObject({
source: "desktop",
id: "finder",
openInput: { editorId: "finder", path: "/repo" },
openInput: { editorId: "finder", workspacePath: "/repo" },
});
});
@@ -69,7 +88,14 @@ describe("planWorkspaceOpenTargets", () => {
const targets = planWorkspaceOpenTargets({
workspaceDirectory: "/repo",
activeFile: { path: "src/app.ts" },
desktopTargets: [{ id: "script:open-in-nvim", label: "Open in Neovim", kind: "editor" }],
desktopTargets: [
{
id: "script:open-in-nvim",
label: "Open in Neovim",
kind: "editor",
icon: { kind: "symbol", name: "terminal" },
},
],
canUseDesktopBridge: true,
isLocalExecution: true,
});
@@ -80,10 +106,11 @@ describe("planWorkspaceOpenTargets", () => {
id: "script:open-in-nvim",
label: "Open in Neovim",
editorId: "script:open-in-nvim",
icon: { kind: "symbol", name: "terminal" },
openInput: {
editorId: "script:open-in-nvim",
path: "/repo/src/app.ts",
cwd: "/repo",
workspacePath: "/repo",
filePath: "/repo/src/app.ts",
},
},
]);
@@ -17,6 +17,7 @@ export interface PlannedDesktopOpenTarget {
id: string;
label: string;
editorId: string;
icon: DesktopOpenTarget["icon"];
openInput: OpenDesktopTargetInput;
}
@@ -58,6 +59,7 @@ function resolveActiveFileForOpenTargets(
function planDesktopOpenTargets(input: {
workspaceDirectory: string;
activeFile?: WorkspaceFileLocation | null;
resolvedFile: ResolvedWorkspaceFilePaths | null;
desktopTargets: readonly DesktopOpenTarget[];
canUseDesktopBridge: boolean;
@@ -74,20 +76,8 @@ function planDesktopOpenTargets(input: {
id: target.id,
label: target.label,
editorId: target.id,
openInput: { editorId: target.id, path: input.workspaceDirectory },
};
}
if (target.kind === "editor") {
return {
source: "desktop",
id: target.id,
label: target.label,
editorId: target.id,
openInput: {
editorId: target.id,
path: input.resolvedFile.absolutePath,
cwd: input.workspaceDirectory,
},
icon: target.icon,
openInput: { editorId: target.id, workspacePath: input.workspaceDirectory },
};
}
return {
@@ -95,10 +85,12 @@ function planDesktopOpenTargets(input: {
id: target.id,
label: target.label,
editorId: target.id,
icon: target.icon,
openInput: {
editorId: target.id,
path: input.resolvedFile.absolutePath,
mode: "reveal",
workspacePath: input.workspaceDirectory,
filePath: input.resolvedFile.absolutePath,
...(input.activeFile?.lineStart ? { line: input.activeFile.lineStart } : {}),
},
};
});