Compare commits

...
Author SHA1 Message Date
paseo-bot[bot] e66773b7d0 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
2026-09-29 23:23:14 +02:00
3 changed files with 75 additions and 21 deletions
@@ -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"] },
]);
});
});
@@ -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;
@@ -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!;