Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions apps/server/src/open.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
const antigravityLaunch = yield* resolveEditorLaunch(
{ cwd: "/tmp/workspace", editor: "antigravity" },
"darwin",
{ PATH: "" },
);
assert.deepEqual(antigravityLaunch, {
command: "agy",
Expand All @@ -25,6 +26,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
const cursorLaunch = yield* resolveEditorLaunch(
{ cwd: "/tmp/workspace", editor: "cursor" },
"darwin",
{ PATH: "" },
);
assert.deepEqual(cursorLaunch, {
command: "cursor",
Expand All @@ -43,6 +45,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
const vscodeLaunch = yield* resolveEditorLaunch(
{ cwd: "/tmp/workspace", editor: "vscode" },
"darwin",
{ PATH: "" },
);
assert.deepEqual(vscodeLaunch, {
command: "code",
Expand Down Expand Up @@ -70,6 +73,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
const zedLaunch = yield* resolveEditorLaunch(
{ cwd: "/tmp/workspace", editor: "zed" },
"darwin",
{ PATH: "" },
);
assert.deepEqual(zedLaunch, {
command: "zed",
Expand All @@ -92,6 +96,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
const lineOnly = yield* resolveEditorLaunch(
{ cwd: "/tmp/workspace/AGENTS.md:48", editor: "cursor" },
"darwin",
{ PATH: "" },
);
assert.deepEqual(lineOnly, {
command: "cursor",
Expand All @@ -101,6 +106,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
const lineAndColumn = yield* resolveEditorLaunch(
{ cwd: "/tmp/workspace/src/open.ts:71:5", editor: "cursor" },
"darwin",
{ PATH: "" },
);
assert.deepEqual(lineAndColumn, {
command: "cursor",
Expand All @@ -119,6 +125,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
const vscodeLineAndColumn = yield* resolveEditorLaunch(
{ cwd: "/tmp/workspace/src/open.ts:71:5", editor: "vscode" },
"darwin",
{ PATH: "" },
);
assert.deepEqual(vscodeLineAndColumn, {
command: "code",
Expand Down Expand Up @@ -146,6 +153,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
const zedLineAndColumn = yield* resolveEditorLaunch(
{ cwd: "/tmp/workspace/src/open.ts:71:5", editor: "zed" },
"darwin",
{ PATH: "" },
);
assert.deepEqual(zedLineAndColumn, {
command: "zed",
Expand All @@ -155,6 +163,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
const zedLineOnly = yield* resolveEditorLaunch(
{ cwd: "/tmp/workspace/AGENTS.md:48", editor: "zed" },
"darwin",
{ PATH: "" },
);
assert.deepEqual(zedLineOnly, {
command: "zed",
Expand All @@ -181,11 +190,43 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
}),
);

it.effect("falls back to zeditor when zed is not installed", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-open-test-" });
yield* fs.writeFileString(path.join(dir, "zeditor"), "#!/bin/sh\nexit 0\n");
yield* fs.chmod(path.join(dir, "zeditor"), 0o755);

const result = yield* resolveEditorLaunch({ cwd: "/tmp/workspace", editor: "zed" }, "linux", {
PATH: dir,
});

assert.deepEqual(result, {
command: "zeditor",
args: ["/tmp/workspace"],
});
}),
);

it.effect("falls back to the primary command when no alias is installed", () =>
Effect.gen(function* () {
const result = yield* resolveEditorLaunch({ cwd: "/tmp/workspace", editor: "zed" }, "linux", {
PATH: "",
});
assert.deepEqual(result, {
command: "zed",
args: ["/tmp/workspace"],
});
}),
);

it.effect("maps file-manager editor to OS open commands", () =>
Effect.gen(function* () {
const launch1 = yield* resolveEditorLaunch(
{ cwd: "/tmp/workspace", editor: "file-manager" },
"darwin",
{ PATH: "" },
);
assert.deepEqual(launch1, {
command: "open",
Expand All @@ -195,6 +236,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
const launch2 = yield* resolveEditorLaunch(
{ cwd: "C:\\workspace", editor: "file-manager" },
"win32",
{ PATH: "" },
);
assert.deepEqual(launch2, {
command: "explorer",
Expand All @@ -204,6 +246,7 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
const launch3 = yield* resolveEditorLaunch(
{ cwd: "/tmp/workspace", editor: "file-manager" },
"linux",
{ PATH: "" },
);
assert.deepEqual(launch3, {
command: "xdg-open",
Expand Down Expand Up @@ -321,4 +364,29 @@ it.layer(NodeServices.layer)("resolveAvailableEditors", (it) => {
assert.deepEqual(editors, ["trae", "vscode-insiders", "vscodium", "file-manager"]);
}),
);

it.effect("includes zed when only the zeditor command is installed", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editors-" });

yield* fs.writeFileString(path.join(dir, "zeditor"), "#!/bin/sh\nexit 0\n");
yield* fs.writeFileString(path.join(dir, "xdg-open"), "#!/bin/sh\nexit 0\n");
yield* fs.chmod(path.join(dir, "zeditor"), 0o755);
yield* fs.chmod(path.join(dir, "xdg-open"), 0o755);

const editors = resolveAvailableEditors("linux", {
PATH: dir,
});
assert.deepEqual(editors, ["zed", "file-manager"]);
}),
);

it("omits file-manager when the platform opener is unavailable", () => {
const editors = resolveAvailableEditors("linux", {
PATH: "",
});
assert.deepEqual(editors, []);
});
});
31 changes: 27 additions & 4 deletions apps/server/src/open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ function resolveCommandEditorArgs(
}
}

function resolveAvailableCommand(
commands: ReadonlyArray<string>,
options: CommandAvailabilityOptions = {},
): string | null {
for (const command of commands) {
if (isCommandAvailable(command, options)) {
return command;
}
}
return null;
}

function fileManagerCommandForPlatform(platform: NodeJS.Platform): string {
switch (platform) {
case "darwin":
Expand Down Expand Up @@ -198,8 +210,16 @@ export function resolveAvailableEditors(
const available: EditorId[] = [];

for (const editor of EDITORS) {
const command = editor.command ?? fileManagerCommandForPlatform(platform);
if (isCommandAvailable(command, { platform, env })) {
if (editor.commands === null) {
const command = fileManagerCommandForPlatform(platform);
if (isCommandAvailable(command, { platform, env })) {
available.push(editor.id);
}
continue;
}

const command = resolveAvailableCommand(editor.commands, { platform, env });
if (command !== null) {
available.push(editor.id);
}
}
Expand Down Expand Up @@ -236,6 +256,7 @@ export class Open extends ServiceMap.Service<Open, OpenShape>()("t3/open") {}
export const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* (
input: OpenInEditorInput,
platform: NodeJS.Platform = process.platform,
env: NodeJS.ProcessEnv = process.env,
): Effect.fn.Return<EditorLaunch, OpenError> {
yield* Effect.annotateCurrentSpan({
"open.editor": input.editor,
Expand All @@ -247,9 +268,11 @@ export const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* (
return yield* new OpenError({ message: `Unknown editor: ${input.editor}` });
}

if (editorDef.command) {
if (editorDef.commands) {
const command =
resolveAvailableCommand(editorDef.commands, { platform, env }) ?? editorDef.commands[0];
return {
command: editorDef.command,
command,
args: resolveCommandEditorArgs(editorDef, input.cwd),
};
}
Expand Down
20 changes: 10 additions & 10 deletions packages/contracts/src/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,25 @@ export type EditorLaunchStyle = typeof EditorLaunchStyle.Type;
type EditorDefinition = {
readonly id: string;
readonly label: string;
readonly command: string | null;
readonly commands: readonly [string, ...string[]] | null;
readonly launchStyle: EditorLaunchStyle;
};

export const EDITORS = [
{ id: "cursor", label: "Cursor", command: "cursor", launchStyle: "goto" },
{ id: "trae", label: "Trae", command: "trae", launchStyle: "goto" },
{ id: "vscode", label: "VS Code", command: "code", launchStyle: "goto" },
{ id: "cursor", label: "Cursor", commands: ["cursor"], launchStyle: "goto" },
{ id: "trae", label: "Trae", commands: ["trae"], launchStyle: "goto" },
{ id: "vscode", label: "VS Code", commands: ["code"], launchStyle: "goto" },
{
id: "vscode-insiders",
label: "VS Code Insiders",
command: "code-insiders",
commands: ["code-insiders"],
launchStyle: "goto",
},
{ id: "vscodium", label: "VSCodium", command: "codium", launchStyle: "goto" },
{ id: "zed", label: "Zed", command: "zed", launchStyle: "direct-path" },
{ id: "antigravity", label: "Antigravity", command: "agy", launchStyle: "goto" },
{ id: "idea", label: "IntelliJ IDEA", command: "idea", launchStyle: "line-column" },
{ id: "file-manager", label: "File Manager", command: null, launchStyle: "direct-path" },
{ id: "vscodium", label: "VSCodium", commands: ["codium"], launchStyle: "goto" },
{ id: "zed", label: "Zed", commands: ["zed", "zeditor"], launchStyle: "direct-path" },
{ id: "antigravity", label: "Antigravity", commands: ["agy"], launchStyle: "goto" },
{ id: "idea", label: "IntelliJ IDEA", commands: ["idea"], launchStyle: "line-column" },
{ id: "file-manager", label: "File Manager", commands: null, launchStyle: "direct-path" },
] as const satisfies ReadonlyArray<EditorDefinition>;

export const EditorId = Schema.Literals(EDITORS.map((e) => e.id));
Expand Down
Loading