Skip to content

Commit afc3924

Browse files
AdemBenAbdallahcodexjuliusmarminge
authored
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>
1 parent 11d456f commit afc3924

3 files changed

Lines changed: 105 additions & 14 deletions

File tree

apps/server/src/open.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,18 @@ function resolveCommandEditorArgs(
7575
}
7676
}
7777

78+
function 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+
7890
function 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") {}
236256
export 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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,25 @@ export type EditorLaunchStyle = typeof EditorLaunchStyle.Type;
77
type 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

1414
export 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

3131
export const EditorId = Schema.Literals(EDITORS.map((e) => e.id));

0 commit comments

Comments
 (0)