History

Add VS Code Insiders and VSCodium to Open In editor picker (#1392)

Nassim Najjar committed 7 months ago

Commit 648f0671 parent add5f345 files · +229 −26Browse files

Unified · Split

5 files
  1. apps/server/src/open.test.ts +39 −2
  2. apps/server/src/open.ts +3 −5
  3. apps/web/src/components/ChatView.browser.tsx +165 −14
  4. apps/web/src/components/chat/OpenInPicker.tsx +10 −0
  5. packages/contracts/src/editor.ts +12 −5
apps/server/src/open.test.ts+39−2
@@ -40,6 +40,24 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { · 39 unmodified lines
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) => { · 34 unmodified lines
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) => { · 134 unmodified lines
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+3−5
@@ -39,10 +39,8 @@ interface CommandAvailabilityOptions { · 38 unmodified lines
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* ( · 164 unmodified lines
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+165−14
@@ -356,6 +356,25 @@ function withProjectScripts( · 355 unmodified lines
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)", () => { · 649 unmodified lines
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)", () => { · 16 unmodified lines
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+10−0
@@ -22,6 +22,16 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray<Editor · 21 unmodified lines
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+12−5
@@ -2,11 +2,18 @@ import { Schema } from "effect"; · 1 unmodified line
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));