Skip to content

Commit 648f067

Browse files
authored
Add VS Code Insiders and VSCodium to Open In editor picker (#1392)
1 parent add5f34 commit 648f067

File tree

5 files changed

+229
-26
lines changed

5 files changed

+229
-26
lines changed

apps/server/src/open.test.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
4040
args: ["/tmp/workspace"],
4141
});
4242

43+
const vscodeInsidersLaunch = yield* resolveEditorLaunch(
44+
{ cwd: "/tmp/workspace", editor: "vscode-insiders" },
45+
"darwin",
46+
);
47+
assert.deepEqual(vscodeInsidersLaunch, {
48+
command: "code-insiders",
49+
args: ["/tmp/workspace"],
50+
});
51+
52+
const vscodiumLaunch = yield* resolveEditorLaunch(
53+
{ cwd: "/tmp/workspace", editor: "vscodium" },
54+
"darwin",
55+
);
56+
assert.deepEqual(vscodiumLaunch, {
57+
command: "codium",
58+
args: ["/tmp/workspace"],
59+
});
60+
4361
const zedLaunch = yield* resolveEditorLaunch(
4462
{ cwd: "/tmp/workspace", editor: "zed" },
4563
"darwin",
@@ -80,6 +98,24 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
8098
args: ["--goto", "/tmp/workspace/src/open.ts:71:5"],
8199
});
82100

101+
const vscodeInsidersLineAndColumn = yield* resolveEditorLaunch(
102+
{ cwd: "/tmp/workspace/src/open.ts:71:5", editor: "vscode-insiders" },
103+
"darwin",
104+
);
105+
assert.deepEqual(vscodeInsidersLineAndColumn, {
106+
command: "code-insiders",
107+
args: ["--goto", "/tmp/workspace/src/open.ts:71:5"],
108+
});
109+
110+
const vscodiumLineAndColumn = yield* resolveEditorLaunch(
111+
{ cwd: "/tmp/workspace/src/open.ts:71:5", editor: "vscodium" },
112+
"darwin",
113+
);
114+
assert.deepEqual(vscodiumLineAndColumn, {
115+
command: "codium",
116+
args: ["--goto", "/tmp/workspace/src/open.ts:71:5"],
117+
});
118+
83119
const zedLineAndColumn = yield* resolveEditorLaunch(
84120
{ cwd: "/tmp/workspace/src/open.ts:71:5", editor: "zed" },
85121
"darwin",
@@ -220,13 +256,14 @@ it.layer(NodeServices.layer)("resolveAvailableEditors", (it) => {
220256
const path = yield* Path.Path;
221257
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editors-" });
222258

223-
yield* fs.writeFileString(path.join(dir, "cursor.CMD"), "@echo off\r\n");
259+
yield* fs.writeFileString(path.join(dir, "code-insiders.CMD"), "@echo off\r\n");
260+
yield* fs.writeFileString(path.join(dir, "codium.CMD"), "@echo off\r\n");
224261
yield* fs.writeFileString(path.join(dir, "explorer.CMD"), "MZ");
225262
const editors = resolveAvailableEditors("win32", {
226263
PATH: dir,
227264
PATHEXT: ".COM;.EXE;.BAT;.CMD",
228265
});
229-
assert.deepEqual(editors, ["cursor", "file-manager"]);
266+
assert.deepEqual(editors, ["vscode-insiders", "vscodium", "file-manager"]);
230267
}),
231268
);
232269
});

apps/server/src/open.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,8 @@ interface CommandAvailabilityOptions {
3939

4040
const LINE_COLUMN_SUFFIX_PATTERN = /:\d+(?::\d+)?$/;
4141

42-
function shouldUseGotoFlag(editorId: EditorId, target: string): boolean {
43-
return (
44-
(editorId === "cursor" || editorId === "vscode") && LINE_COLUMN_SUFFIX_PATTERN.test(target)
45-
);
42+
function shouldUseGotoFlag(editor: (typeof EDITORS)[number], target: string): boolean {
43+
return editor.supportsGoto && LINE_COLUMN_SUFFIX_PATTERN.test(target);
4644
}
4745

4846
function fileManagerCommandForPlatform(platform: NodeJS.Platform): string {
@@ -213,7 +211,7 @@ export const resolveEditorLaunch = Effect.fnUntraced(function* (
213211
}
214212

215213
if (editorDef.command) {
216-
return shouldUseGotoFlag(editorDef.id, input.cwd)
214+
return shouldUseGotoFlag(editorDef, input.cwd)
217215
? { command: editorDef.command, args: ["--goto", input.cwd] }
218216
: { command: editorDef.command, args: [input.cwd] };
219217
}

apps/web/src/components/ChatView.browser.tsx

Lines changed: 165 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,25 @@ function withProjectScripts(
356356
};
357357
}
358358

359+
function setDraftThreadWithoutWorktree(): void {
360+
useComposerDraftStore.setState({
361+
draftThreadsByThreadId: {
362+
[THREAD_ID]: {
363+
projectId: PROJECT_ID,
364+
createdAt: NOW_ISO,
365+
runtimeMode: "full-access",
366+
interactionMode: "default",
367+
branch: null,
368+
worktreePath: null,
369+
envMode: "local",
370+
},
371+
},
372+
projectDraftThreadIdByProjectId: {
373+
[PROJECT_ID]: THREAD_ID,
374+
},
375+
});
376+
}
377+
359378
function createSnapshotWithLongProposedPlan(): OrchestrationReadModel {
360379
const snapshot = createSnapshotForTargetUser({
361380
targetMessageId: "msg-user-plan-target" as MessageId,
@@ -1011,30 +1030,162 @@ describe("ChatView timeline estimator parity (full app)", () => {
10111030
);
10121031

10131032
it("opens the project cwd for draft threads without a worktree path", async () => {
1014-
useComposerDraftStore.setState({
1015-
draftThreadsByThreadId: {
1016-
[THREAD_ID]: {
1017-
projectId: PROJECT_ID,
1018-
createdAt: NOW_ISO,
1019-
runtimeMode: "full-access",
1020-
interactionMode: "default",
1021-
branch: null,
1022-
worktreePath: null,
1023-
envMode: "local",
1033+
setDraftThreadWithoutWorktree();
1034+
1035+
const mounted = await mountChatView({
1036+
viewport: DEFAULT_VIEWPORT,
1037+
snapshot: createDraftOnlySnapshot(),
1038+
configureFixture: (nextFixture) => {
1039+
nextFixture.serverConfig = {
1040+
...nextFixture.serverConfig,
1041+
availableEditors: ["vscode"],
1042+
};
1043+
},
1044+
});
1045+
1046+
try {
1047+
const openButton = await waitForElement(
1048+
() =>
1049+
Array.from(document.querySelectorAll("button")).find(
1050+
(button) => button.textContent?.trim() === "Open",
1051+
) as HTMLButtonElement | null,
1052+
"Unable to find Open button.",
1053+
);
1054+
openButton.click();
1055+
1056+
await vi.waitFor(
1057+
() => {
1058+
const openRequest = wsRequests.find(
1059+
(request) => request._tag === WS_METHODS.shellOpenInEditor,
1060+
);
1061+
expect(openRequest).toMatchObject({
1062+
_tag: WS_METHODS.shellOpenInEditor,
1063+
cwd: "/repo/project",
1064+
editor: "vscode",
1065+
});
10241066
},
1067+
{ timeout: 8_000, interval: 16 },
1068+
);
1069+
} finally {
1070+
await mounted.cleanup();
1071+
}
1072+
});
1073+
1074+
it("opens the project cwd with VS Code Insiders when it is the only available editor", async () => {
1075+
setDraftThreadWithoutWorktree();
1076+
1077+
const mounted = await mountChatView({
1078+
viewport: DEFAULT_VIEWPORT,
1079+
snapshot: createDraftOnlySnapshot(),
1080+
configureFixture: (nextFixture) => {
1081+
nextFixture.serverConfig = {
1082+
...nextFixture.serverConfig,
1083+
availableEditors: ["vscode-insiders"],
1084+
};
10251085
},
1026-
projectDraftThreadIdByProjectId: {
1027-
[PROJECT_ID]: THREAD_ID,
1086+
});
1087+
1088+
try {
1089+
const openButton = await waitForElement(
1090+
() =>
1091+
Array.from(document.querySelectorAll("button")).find(
1092+
(button) => button.textContent?.trim() === "Open",
1093+
) as HTMLButtonElement | null,
1094+
"Unable to find Open button.",
1095+
);
1096+
openButton.click();
1097+
1098+
await vi.waitFor(
1099+
() => {
1100+
const openRequest = wsRequests.find(
1101+
(request) => request._tag === WS_METHODS.shellOpenInEditor,
1102+
);
1103+
expect(openRequest).toMatchObject({
1104+
_tag: WS_METHODS.shellOpenInEditor,
1105+
cwd: "/repo/project",
1106+
editor: "vscode-insiders",
1107+
});
1108+
},
1109+
{ timeout: 8_000, interval: 16 },
1110+
);
1111+
} finally {
1112+
await mounted.cleanup();
1113+
}
1114+
});
1115+
1116+
it("filters the open picker menu and opens VSCodium from the menu", async () => {
1117+
setDraftThreadWithoutWorktree();
1118+
1119+
const mounted = await mountChatView({
1120+
viewport: DEFAULT_VIEWPORT,
1121+
snapshot: createDraftOnlySnapshot(),
1122+
configureFixture: (nextFixture) => {
1123+
nextFixture.serverConfig = {
1124+
...nextFixture.serverConfig,
1125+
availableEditors: ["vscode-insiders", "vscodium"],
1126+
};
10281127
},
10291128
});
10301129

1130+
try {
1131+
const menuButton = await waitForElement(
1132+
() => document.querySelector('button[aria-label="Copy options"]'),
1133+
"Unable to find Open picker button.",
1134+
);
1135+
(menuButton as HTMLButtonElement).click();
1136+
1137+
await waitForElement(
1138+
() =>
1139+
Array.from(document.querySelectorAll('[data-slot="menu-item"]')).find((item) =>
1140+
item.textContent?.includes("VS Code Insiders"),
1141+
) ?? null,
1142+
"Unable to find VS Code Insiders menu item.",
1143+
);
1144+
1145+
expect(
1146+
Array.from(document.querySelectorAll('[data-slot="menu-item"]')).some((item) =>
1147+
item.textContent?.includes("Zed"),
1148+
),
1149+
).toBe(false);
1150+
1151+
const vscodiumItem = await waitForElement(
1152+
() =>
1153+
Array.from(document.querySelectorAll('[data-slot="menu-item"]')).find((item) =>
1154+
item.textContent?.includes("VSCodium"),
1155+
) ?? null,
1156+
"Unable to find VSCodium menu item.",
1157+
);
1158+
(vscodiumItem as HTMLElement).click();
1159+
1160+
await vi.waitFor(
1161+
() => {
1162+
const openRequest = wsRequests.find(
1163+
(request) => request._tag === WS_METHODS.shellOpenInEditor,
1164+
);
1165+
expect(openRequest).toMatchObject({
1166+
_tag: WS_METHODS.shellOpenInEditor,
1167+
cwd: "/repo/project",
1168+
editor: "vscodium",
1169+
});
1170+
},
1171+
{ timeout: 8_000, interval: 16 },
1172+
);
1173+
} finally {
1174+
await mounted.cleanup();
1175+
}
1176+
});
1177+
1178+
it("falls back to the first installed editor when the stored favorite is unavailable", async () => {
1179+
localStorage.setItem("t3code:last-editor", "vscodium");
1180+
setDraftThreadWithoutWorktree();
1181+
10311182
const mounted = await mountChatView({
10321183
viewport: DEFAULT_VIEWPORT,
10331184
snapshot: createDraftOnlySnapshot(),
10341185
configureFixture: (nextFixture) => {
10351186
nextFixture.serverConfig = {
10361187
...nextFixture.serverConfig,
1037-
availableEditors: ["vscode"],
1188+
availableEditors: ["vscode-insiders"],
10381189
};
10391190
},
10401191
});
@@ -1057,7 +1208,7 @@ describe("ChatView timeline estimator parity (full app)", () => {
10571208
expect(openRequest).toMatchObject({
10581209
_tag: WS_METHODS.shellOpenInEditor,
10591210
cwd: "/repo/project",
1060-
editor: "vscode",
1211+
editor: "vscode-insiders",
10611212
});
10621213
},
10631214
{ timeout: 8_000, interval: 16 },

apps/web/src/components/chat/OpenInPicker.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray<Editor
2222
Icon: VisualStudioCode,
2323
value: "vscode",
2424
},
25+
{
26+
label: "VS Code Insiders",
27+
Icon: VisualStudioCode,
28+
value: "vscode-insiders",
29+
},
30+
{
31+
label: "VSCodium",
32+
Icon: VisualStudioCode,
33+
value: "vscodium",
34+
},
2535
{
2636
label: "Zed",
2737
Icon: Zed,

packages/contracts/src/editor.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@ import { Schema } from "effect";
22
import { TrimmedNonEmptyString } from "./baseSchemas";
33

44
export const EDITORS = [
5-
{ id: "cursor", label: "Cursor", command: "cursor" },
6-
{ id: "vscode", label: "VS Code", command: "code" },
7-
{ id: "zed", label: "Zed", command: "zed" },
8-
{ id: "antigravity", label: "Antigravity", command: "agy" },
9-
{ id: "file-manager", label: "File Manager", command: null },
5+
{ id: "cursor", label: "Cursor", command: "cursor", supportsGoto: true },
6+
{ id: "vscode", label: "VS Code", command: "code", supportsGoto: true },
7+
{
8+
id: "vscode-insiders",
9+
label: "VS Code Insiders",
10+
command: "code-insiders",
11+
supportsGoto: true,
12+
},
13+
{ id: "vscodium", label: "VSCodium", command: "codium", supportsGoto: true },
14+
{ id: "zed", label: "Zed", command: "zed", supportsGoto: false },
15+
{ id: "antigravity", label: "Antigravity", command: "agy", supportsGoto: false },
16+
{ id: "file-manager", label: "File Manager", command: null, supportsGoto: false },
1017
] as const;
1118

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

0 commit comments

Comments
 (0)