diff --git a/packages/desktop/src/features/editor-targets/runtime.test.ts b/packages/desktop/src/features/editor-targets/runtime.test.ts index 6a2342e901..d6bd0d4b30 100644 --- a/packages/desktop/src/features/editor-targets/runtime.test.ts +++ b/packages/desktop/src/features/editor-targets/runtime.test.ts @@ -1,7 +1,9 @@ import type { SpawnOptions } from "node:child_process"; import { describe, expect, it } from "vitest"; +import { listAvailableEditorTargets, openEditorTarget } from "./registry.js"; import { createEditorTargetRuntime } from "./runtime.js"; +import { zedTarget } from "./targets/zed.js"; interface SpawnRecord { command: string; @@ -10,6 +12,23 @@ interface SpawnRecord { unrefed: boolean; } +function recordSpawns(records: SpawnRecord[]) { + return (command: string, args: string[], options: SpawnOptions) => { + const record = { command, args, options, unrefed: false }; + records.push(record); + const child = { + once(event: "error" | "spawn", handler: (error?: Error) => void) { + if (event === "spawn") queueMicrotask(() => handler()); + return child; + }, + unref() { + record.unrefed = true; + }, + }; + return child; + }; +} + describe("editor target runtime", () => { it("resolves command aliases and safely launches Windows command scripts", async () => { const records: SpawnRecord[] = []; @@ -20,20 +39,7 @@ describe("editor target runtime", () => { ELECTRON_RUN_AS_NODE: "1", }, pathExists: (targetPath) => targetPath === "C:/Program Files/Editors & Tools/bin/code.cmd", - spawn: (command, args, options) => { - const record = { command, args, options, unrefed: false }; - records.push(record); - const child = { - once(event: "error" | "spawn", handler: (error?: Error) => void) { - if (event === "spawn") queueMicrotask(() => handler()); - return child; - }, - unref() { - record.unrefed = true; - }, - }; - return child; - }, + spawn: recordSpawns(records), }); const command = runtime.resolveCommand(["missing", "code"]); @@ -57,4 +63,32 @@ describe("editor target runtime", () => { }, ]); }); + + it.each([ + ["system", "/var/lib/flatpak/exports/bin/dev.zed.Zed"], + ["user", "/home/dev/.local/share/flatpak/exports/bin/dev.zed.Zed"], + ])("finds and opens Zed from a %s Flatpak install on Linux", async (_scope, flatpakExport) => { + const records: SpawnRecord[] = []; + const existingPaths = new Set([flatpakExport, "/repo", "/repo/src/app.ts"]); + const runtime = createEditorTargetRuntime({ + platform: "linux", + env: { PATH: "/usr/local/bin:/usr/bin:/bin" }, + homeDirectory: "/home/dev", + pathExists: (targetPath) => existingPaths.has(targetPath), + spawn: recordSpawns(records), + loadIcon: async (fileName) => ({ kind: "image", dataUrl: fileName }), + }); + + const targets = await listAvailableEditorTargets(runtime, [zedTarget]); + expect(targets.map((target) => target.id)).toEqual(["zed"]); + + await openEditorTarget( + { editorId: "zed", workspacePath: "/repo", filePath: "/repo/src/app.ts", line: 3 }, + runtime, + [zedTarget], + ); + expect(records.map(({ command, args }) => ({ command, args }))).toEqual([ + { command: flatpakExport, args: ["/repo", "/repo/src/app.ts:3"] }, + ]); + }); }); diff --git a/packages/desktop/src/features/editor-targets/runtime.ts b/packages/desktop/src/features/editor-targets/runtime.ts index dcd5b514c4..c104c4b4dc 100644 --- a/packages/desktop/src/features/editor-targets/runtime.ts +++ b/packages/desktop/src/features/editor-targets/runtime.ts @@ -56,10 +56,28 @@ function isAbsolutePath(value: string, platform: NodeJS.Platform): boolean { return platform === "win32" ? win32.isAbsolute(value) : posix.isAbsolute(value); } +// Flatpak exports app launchers here, but does not put these directories on PATH. +function flatpakExportDirectories(env: NodeJS.ProcessEnv, homeDirectory: string): string[] { + const userDataDirectory = env.XDG_DATA_HOME || `${homeDirectory}/.local/share`; + return [`${userDataDirectory}/flatpak/exports/bin`, "/var/lib/flatpak/exports/bin"]; +} + +function commandSearchDirectories(input: { + env: NodeJS.ProcessEnv; + platform: NodeJS.Platform; + homeDirectory: string; +}): string[] { + const pathValue = input.env.PATH ?? input.env.Path ?? input.env.path ?? ""; + const pathDelimiter = input.platform === "win32" ? ";" : ":"; + const directories = pathValue.split(pathDelimiter).filter(Boolean); + if (input.platform !== "linux") return directories; + return [...directories, ...flatpakExportDirectories(input.env, input.homeDirectory)]; +} + function resolveExecutable( commands: readonly string[], input: { - env: NodeJS.ProcessEnv; + searchDirectories: readonly string[]; pathExists: (path: string) => boolean; platform: NodeJS.Platform; }, @@ -69,10 +87,7 @@ function resolveExecutable( return command; } - const pathValue = input.env.PATH ?? input.env.Path ?? input.env.path ?? ""; - const pathDelimiter = input.platform === "win32" ? ";" : ":"; - for (const directory of pathValue.split(pathDelimiter)) { - if (!directory) continue; + for (const directory of input.searchDirectories) { const candidate = `${directory}/${command}`; if (input.platform !== "win32") { if (input.pathExists(candidate)) return candidate; @@ -140,7 +155,12 @@ export function createEditorTargetRuntime( env, pathExists, isAbsolutePath: (targetPath) => isAbsolutePath(targetPath, platform), - resolveCommand: (commands) => resolveExecutable(commands, { env, pathExists, platform }), + resolveCommand: (commands) => + resolveExecutable(commands, { + searchDirectories: commandSearchDirectories({ env, platform, homeDirectory }), + pathExists, + platform, + }), async spawnDetached({ command, args }) { const commandScript = isWindowsCommandScript(command, platform); const launchCommand = commandScript ? escapeWindowsCmdValue(command) : command; diff --git a/packages/desktop/src/features/editor-targets/targets/zed.ts b/packages/desktop/src/features/editor-targets/targets/zed.ts index 40b13f258e..fac2cc8f51 100644 --- a/packages/desktop/src/features/editor-targets/targets/zed.ts +++ b/packages/desktop/src/features/editor-targets/targets/zed.ts @@ -1,6 +1,6 @@ import type { EditorTarget, EditorTargetLaunchInput } from "../target.js"; -const COMMANDS = ["zed", "zeditor"] as const; +const COMMANDS = ["zed", "zeditor", "dev.zed.Zed"] as const; function location(input: EditorTargetLaunchInput): string { if (!input.line) return input.filePath!;