From e66773b7d043854afb78cdaea00bac4d5fcc525a Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 23:23:14 +0200 Subject: [PATCH] fix(desktop): find Zed installed as a Flatpak on Linux Flatpak puts app launchers in its exports/bin directories but does not add them to PATH, so Zed installed from Flathub never appeared in the Open in editor list. Search the user and system Flatpak export directories after PATH on Linux, and look for Zed's Flathub launcher. Closes #5458 --- .../features/editor-targets/runtime.test.ts | 62 ++++++++++++++----- .../src/features/editor-targets/runtime.ts | 32 ++++++++-- .../features/editor-targets/targets/zed.ts | 2 +- 3 files changed, 75 insertions(+), 21 deletions(-) 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!;