authorgravatar for 96244394+AdemBenAbdallah@users.noreply.github.comAdem Ben Abdallah <96244394+AdemBenAbdallah@users.noreply.github.com> 2026-04-07 00:46:17+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-06 16:46:17-07:00
logafc392439345ef4f3056661cb21669da12efc233
treeb4cde8c6d4ab0427dc5d6c66e70ac3ebfba09da9
parent11d456f6c8404be6617b7299a9db9ad5538ee061
signature Signed by PGP key B5690EEEBB952194

Add Zed support to Open actions via editor command aliases (#1303)

Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: Julius Marminge <julius0216@outlook.com>

3 files changed, 105 insertions(+), 14 deletions(-)

apps/server/src/open.test.ts+68
......@@ -16,6 +16,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
1616 const antigravityLaunch = yield* resolveEditorLaunch(
1717 { cwd: "/tmp/workspace", editor: "antigravity" },
1818 "darwin",
19 { PATH: "" },
1920 );
2021 assert.deepEqual(antigravityLaunch, {
2122 command: "agy",
......@@ -25,6 +26,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
2526 const cursorLaunch = yield* resolveEditorLaunch(
2627 { cwd: "/tmp/workspace", editor: "cursor" },
2728 "darwin",
29 { PATH: "" },
2830 );
2931 assert.deepEqual(cursorLaunch, {
3032 command: "cursor",
......@@ -43,6 +45,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
4345 const vscodeLaunch = yield* resolveEditorLaunch(
4446 { cwd: "/tmp/workspace", editor: "vscode" },
4547 "darwin",
48 { PATH: "" },
4649 );
4750 assert.deepEqual(vscodeLaunch, {
4851 command: "code",
......@@ -70,6 +73,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
7073 const zedLaunch = yield* resolveEditorLaunch(
7174 { cwd: "/tmp/workspace", editor: "zed" },
7275 "darwin",
76 { PATH: "" },
7377 );
7478 assert.deepEqual(zedLaunch, {
7579 command: "zed",
......@@ -92,6 +96,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
9296 const lineOnly = yield* resolveEditorLaunch(
9397 { cwd: "/tmp/workspace/AGENTS.md:48", editor: "cursor" },
9498 "darwin",
99 { PATH: "" },
95100 );
96101 assert.deepEqual(lineOnly, {
97102 command: "cursor",
......@@ -101,6 +106,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
101106 const lineAndColumn = yield* resolveEditorLaunch(
102107 { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "cursor" },
103108 "darwin",
109 { PATH: "" },
104110 );
105111 assert.deepEqual(lineAndColumn, {
106112 command: "cursor",
......@@ -119,6 +125,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
119125 const vscodeLineAndColumn = yield* resolveEditorLaunch(
120126 { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "vscode" },
121127 "darwin",
128 { PATH: "" },
122129 );
123130 assert.deepEqual(vscodeLineAndColumn, {
124131 command: "code",
......@@ -146,6 +153,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
146153 const zedLineAndColumn = yield* resolveEditorLaunch(
147154 { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "zed" },
148155 "darwin",
156 { PATH: "" },
149157 );
150158 assert.deepEqual(zedLineAndColumn, {
151159 command: "zed",
......@@ -155,6 +163,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
155163 const zedLineOnly = yield* resolveEditorLaunch(
156164 { cwd: "/tmp/workspace/AGENTS.md:48", editor: "zed" },
157165 "darwin",
166 { PATH: "" },
158167 );
159168 assert.deepEqual(zedLineOnly, {
160169 command: "zed",
......@@ -181,11 +190,43 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
181190 }),
182191 );
183192
193 it.effect("falls back to zeditor when zed is not installed", () =>
194 Effect.gen(function* () {
195 const fs = yield* FileSystem.FileSystem;
196 const path = yield* Path.Path;
197 const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-open-test-" });
198 yield* fs.writeFileString(path.join(dir, "zeditor"), "#!/bin/sh\nexit 0\n");
199 yield* fs.chmod(path.join(dir, "zeditor"), 0o755);
200
201 const result = yield* resolveEditorLaunch({ cwd: "/tmp/workspace", editor: "zed" }, "linux", {
202 PATH: dir,
203 });
204
205 assert.deepEqual(result, {
206 command: "zeditor",
207 args: ["/tmp/workspace"],
208 });
209 }),
210 );
211
212 it.effect("falls back to the primary command when no alias is installed", () =>
213 Effect.gen(function* () {
214 const result = yield* resolveEditorLaunch({ cwd: "/tmp/workspace", editor: "zed" }, "linux", {
215 PATH: "",
216 });
217 assert.deepEqual(result, {
218 command: "zed",
219 args: ["/tmp/workspace"],
220 });
221 }),
222 );
223
184224 it.effect("maps file-manager editor to OS open commands", () =>
185225 Effect.gen(function* () {
186226 const launch1 = yield* resolveEditorLaunch(
187227 { cwd: "/tmp/workspace", editor: "file-manager" },
188228 "darwin",
229 { PATH: "" },
189230 );
190231 assert.deepEqual(launch1, {
191232 command: "open",
......@@ -195,6 +236,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
195236 const launch2 = yield* resolveEditorLaunch(
196237 { cwd: "C:\\workspace", editor: "file-manager" },
197238 "win32",
239 { PATH: "" },
198240 );
199241 assert.deepEqual(launch2, {
200242 command: "explorer",
......@@ -204,6 +246,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
204246 const launch3 = yield* resolveEditorLaunch(
205247 { cwd: "/tmp/workspace", editor: "file-manager" },
206248 "linux",
249 { PATH: "" },
207250 );
208251 assert.deepEqual(launch3, {
209252 command: "xdg-open",
......@@ -321,4 +364,29 @@ it.layer(NodeServices.layer)("resolveAvailableEditors", (it) => {
321364 assert.deepEqual(editors, ["trae", "vscode-insiders", "vscodium", "file-manager"]);
322365 }),
323366 );
367
368 it.effect("includes zed when only the zeditor command is installed", () =>
369 Effect.gen(function* () {
370 const fs = yield* FileSystem.FileSystem;
371 const path = yield* Path.Path;
372 const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editors-" });
373
374 yield* fs.writeFileString(path.join(dir, "zeditor"), "#!/bin/sh\nexit 0\n");
375 yield* fs.writeFileString(path.join(dir, "xdg-open"), "#!/bin/sh\nexit 0\n");
376 yield* fs.chmod(path.join(dir, "zeditor"), 0o755);
377 yield* fs.chmod(path.join(dir, "xdg-open"), 0o755);
378
379 const editors = resolveAvailableEditors("linux", {
380 PATH: dir,
381 });
382 assert.deepEqual(editors, ["zed", "file-manager"]);
383 }),
384 );
385
386 it("omits file-manager when the platform opener is unavailable", () => {
387 const editors = resolveAvailableEditors("linux", {
388 PATH: "",
389 });
390 assert.deepEqual(editors, []);
391 });
324392});
apps/server/src/open.ts+27-4
......@@ -75,6 +75,18 @@ function resolveCommandEditorArgs(
7575 }
7676}
7777
78function resolveAvailableCommand(
79 commands: ReadonlyArray<string>,
80 options: CommandAvailabilityOptions = {},
81): string | null {
82 for (const command of commands) {
83 if (isCommandAvailable(command, options)) {
84 return command;
85 }
86 }
87 return null;
88}
89
7890function fileManagerCommandForPlatform(platform: NodeJS.Platform): string {
7991 switch (platform) {
8092 case "darwin":
......@@ -198,8 +210,16 @@ export function resolveAvailableEditors(
198210 const available: EditorId[] = [];
199211
200212 for (const editor of EDITORS) {
201 const command = editor.command ?? fileManagerCommandForPlatform(platform);
202 if (isCommandAvailable(command, { platform, env })) {
213 if (editor.commands === null) {
214 const command = fileManagerCommandForPlatform(platform);
215 if (isCommandAvailable(command, { platform, env })) {
216 available.push(editor.id);
217 }
218 continue;
219 }
220
221 const command = resolveAvailableCommand(editor.commands, { platform, env });
222 if (command !== null) {
203223 available.push(editor.id);
204224 }
205225 }
......@@ -236,6 +256,7 @@ export class Open extends ServiceMap.Service<Open, OpenShape>()("t3/open") {}
236256export const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* (
237257 input: OpenInEditorInput,
238258 platform: NodeJS.Platform = process.platform,
259 env: NodeJS.ProcessEnv = process.env,
239260): Effect.fn.Return<EditorLaunch, OpenError> {
240261 yield* Effect.annotateCurrentSpan({
241262 "open.editor": input.editor,
......@@ -247,9 +268,11 @@ export const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* (
247268 return yield* new OpenError({ message: `Unknown editor: ${input.editor}` });
248269 }
249270
250 if (editorDef.command) {
271 if (editorDef.commands) {
272 const command =
273 resolveAvailableCommand(editorDef.commands, { platform, env }) ?? editorDef.commands[0];
251274 return {
252 command: editorDef.command,
275 command,
253276 args: resolveCommandEditorArgs(editorDef, input.cwd),
254277 };
255278 }
packages/contracts/src/editor.ts+10-10
......@@ -7,25 +7,25 @@ export type EditorLaunchStyle = typeof EditorLaunchStyle.Type;
77type EditorDefinition = {
88 readonly id: string;
99 readonly label: string;
10 readonly command: string | null;
10 readonly commands: readonly [string, ...string[]] | null;
1111 readonly launchStyle: EditorLaunchStyle;
1212};
1313
1414export const EDITORS = [
15 { id: "cursor", label: "Cursor", command: "cursor", launchStyle: "goto" },
16 { id: "trae", label: "Trae", command: "trae", launchStyle: "goto" },
17 { id: "vscode", label: "VS Code", command: "code", launchStyle: "goto" },
15 { id: "cursor", label: "Cursor", commands: ["cursor"], launchStyle: "goto" },
16 { id: "trae", label: "Trae", commands: ["trae"], launchStyle: "goto" },
17 { id: "vscode", label: "VS Code", commands: ["code"], launchStyle: "goto" },
1818 {
1919 id: "vscode-insiders",
2020 label: "VS Code Insiders",
21 command: "code-insiders",
21 commands: ["code-insiders"],
2222 launchStyle: "goto",
2323 },
24 { id: "vscodium", label: "VSCodium", command: "codium", launchStyle: "goto" },
25 { id: "zed", label: "Zed", command: "zed", launchStyle: "direct-path" },
26 { id: "antigravity", label: "Antigravity", command: "agy", launchStyle: "goto" },
27 { id: "idea", label: "IntelliJ IDEA", command: "idea", launchStyle: "line-column" },
28 { id: "file-manager", label: "File Manager", command: null, launchStyle: "direct-path" },
24 { id: "vscodium", label: "VSCodium", commands: ["codium"], launchStyle: "goto" },
25 { id: "zed", label: "Zed", commands: ["zed", "zeditor"], launchStyle: "direct-path" },
26 { id: "antigravity", label: "Antigravity", commands: ["agy"], launchStyle: "goto" },
27 { id: "idea", label: "IntelliJ IDEA", commands: ["idea"], launchStyle: "line-column" },
28 { id: "file-manager", label: "File Manager", commands: null, launchStyle: "direct-path" },
2929] as const satisfies ReadonlyArray<EditorDefinition>;
3030
3131export const EditorId = Schema.Literals(EDITORS.map((e) => e.id));