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:
@@ -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 } : {}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user