authorgravatar for 1054346243@qq.comc1yde3 <1054346243@qq.com> 2026-04-03 07:57:09+08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-02 23:57:09+00:00
log1b272fd70992ac349791dd6b6c084ef1d042d700
tree3679c8cdb4cc5c887eaef6a71ee6680ecaa9e592
parent9013c07f6d679813777462466e9051e4ffc27e03
signature Signed by PGP key B5690EEEBB952194

feat: support IntelliJ IDEA open-in launch (#1673)


5 files changed, 214 insertions(+), 17 deletions(-)

apps/server/src/open.test.ts+37-1
......@@ -75,10 +75,19 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
7575 command: "zed",
7676 args: ["/tmp/workspace"],
7777 });
78
79 const ideaLaunch = yield* resolveEditorLaunch(
80 { cwd: "/tmp/workspace", editor: "idea" },
81 "darwin",
82 );
83 assert.deepEqual(ideaLaunch, {
84 command: "idea",
85 args: ["/tmp/workspace"],
86 });
7887 }),
7988 );
8089
81 it.effect("uses --goto when editor supports line/column suffixes", () =>
90 it.effect("applies launch-style-specific navigation arguments", () =>
8291 Effect.gen(function* () {
8392 const lineOnly = yield* resolveEditorLaunch(
8493 { cwd: "/tmp/workspace/AGENTS.md:48", editor: "cursor" },
......@@ -142,6 +151,33 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
142151 command: "zed",
143152 args: ["/tmp/workspace/src/open.ts:71:5"],
144153 });
154
155 const zedLineOnly = yield* resolveEditorLaunch(
156 { cwd: "/tmp/workspace/AGENTS.md:48", editor: "zed" },
157 "darwin",
158 );
159 assert.deepEqual(zedLineOnly, {
160 command: "zed",
161 args: ["/tmp/workspace/AGENTS.md:48"],
162 });
163
164 const ideaLineOnly = yield* resolveEditorLaunch(
165 { cwd: "/tmp/workspace/AGENTS.md:48", editor: "idea" },
166 "darwin",
167 );
168 assert.deepEqual(ideaLineOnly, {
169 command: "idea",
170 args: ["--line", "48", "/tmp/workspace/AGENTS.md"],
171 });
172
173 const ideaLineAndColumn = yield* resolveEditorLaunch(
174 { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "idea" },
175 "darwin",
176 );
177 assert.deepEqual(ideaLineAndColumn, {
178 command: "idea",
179 args: ["--line", "71", "--column", "5", "/tmp/workspace/src/open.ts"],
180 });
145181 }),
146182 );
147183
apps/server/src/open.ts+42-6
......@@ -34,10 +34,45 @@ interface CommandAvailabilityOptions {
3434 readonly env?: NodeJS.ProcessEnv;
3535}
3636
37const LINE_COLUMN_SUFFIX_PATTERN = /:\d+(?::\d+)?$/;
37const TARGET_WITH_POSITION_PATTERN = /^(.*?):(\d+)(?::(\d+))?$/;
38
39function parseTargetPathAndPosition(target: string): {
40 path: string;
41 line: string | undefined;
42 column: string | undefined;
43} | null {
44 const match = TARGET_WITH_POSITION_PATTERN.exec(target);
45 if (!match?.[1] || !match[2]) {
46 return null;
47 }
3848
39function shouldUseGotoFlag(editor: (typeof EDITORS)[number], target: string): boolean {
40 return editor.supportsGoto && LINE_COLUMN_SUFFIX_PATTERN.test(target);
49 return {
50 path: match[1],
51 line: match[2],
52 column: match[3],
53 };
54}
55
56function resolveCommandEditorArgs(
57 editor: (typeof EDITORS)[number],
58 target: string,
59): ReadonlyArray<string> {
60 const parsedTarget = parseTargetPathAndPosition(target);
61
62 switch (editor.launchStyle) {
63 case "direct-path":
64 return [target];
65 case "goto":
66 return parsedTarget ? ["--goto", target] : [target];
67 case "line-column": {
68 if (!parsedTarget) {
69 return [target];
70 }
71
72 const { path, line, column } = parsedTarget;
73 return [...(line ? ["--line", line] : []), ...(column ? ["--column", column] : []), path];
74 }
75 }
4176}
4277
4378function fileManagerCommandForPlatform(platform: NodeJS.Platform): string {
......@@ -208,9 +243,10 @@ export const resolveEditorLaunch = Effect.fnUntraced(function* (
208243 }
209244
210245 if (editorDef.command) {
211 return shouldUseGotoFlag(editorDef, input.cwd)
212 ? { command: editorDef.command, args: ["--goto", input.cwd] }
213 : { command: editorDef.command, args: [input.cwd] };
246 return {
247 command: editorDef.command,
248 args: resolveCommandEditorArgs(editorDef, input.cwd),
249 };
214250 }
215251
216252 if (editorDef.id !== "file-manager") {
apps/web/src/components/Icons.tsx+101
......@@ -310,6 +310,107 @@ export const AntigravityIcon: Icon = (props) => (
310310 </svg>
311311);
312312
313export const IntelliJIdeaIcon: Icon = (props) => {
314 const id = useId();
315 const gradientAId = `${id}-idea-a`;
316 const gradientBId = `${id}-idea-b`;
317 const gradientCId = `${id}-idea-c`;
318 const gradientDId = `${id}-idea-d`;
319
320 return (
321 <svg {...props} viewBox="0 0 70 70" fill="none">
322 <defs>
323 <linearGradient
324 id={gradientAId}
325 x1="0.7898"
326 y1="40.0893"
327 x2="33.3172"
328 y2="40.0893"
329 gradientUnits="userSpaceOnUse"
330 >
331 <stop offset="0.2581" stopColor="#F97A12" />
332 <stop offset="0.4591" stopColor="#B07B58" />
333 <stop offset="0.7241" stopColor="#577BAE" />
334 <stop offset="0.9105" stopColor="#1E7CE5" />
335 <stop offset="1" stopColor="#087CFA" />
336 </linearGradient>
337 <linearGradient
338 id={gradientBId}
339 x1="25.7674"
340 y1="24.88"
341 x2="79.424"
342 y2="54.57"
343 gradientUnits="userSpaceOnUse"
344 >
345 <stop stopColor="#F97A12" />
346 <stop offset="0.07179946" stopColor="#CB7A3E" />
347 <stop offset="0.1541" stopColor="#9E7B6A" />
348 <stop offset="0.242" stopColor="#757B91" />
349 <stop offset="0.3344" stopColor="#537BB1" />
350 <stop offset="0.4324" stopColor="#387CCC" />
351 <stop offset="0.5381" stopColor="#237CE0" />
352 <stop offset="0.6552" stopColor="#147CEF" />
353 <stop offset="0.7925" stopColor="#0B7CF7" />
354 <stop offset="1" stopColor="#087CFA" />
355 </linearGradient>
356 <linearGradient
357 id={gradientCId}
358 x1="63.2277"
359 y1="42.9153"
360 x2="48.2903"
361 y2="-1.7191"
362 gradientUnits="userSpaceOnUse"
363 >
364 <stop stopColor="#FE315D" />
365 <stop offset="0.07840246" stopColor="#CB417E" />
366 <stop offset="0.1601" stopColor="#9E4E9B" />
367 <stop offset="0.2474" stopColor="#755BB4" />
368 <stop offset="0.3392" stopColor="#5365CA" />
369 <stop offset="0.4365" stopColor="#386DDB" />
370 <stop offset="0.5414" stopColor="#2374E9" />
371 <stop offset="0.6576" stopColor="#1478F3" />
372 <stop offset="0.794" stopColor="#0B7BF8" />
373 <stop offset="1" stopColor="#087CFA" />
374 </linearGradient>
375 <linearGradient
376 id={gradientDId}
377 x1="10.7204"
378 y1="16.473"
379 x2="55.5237"
380 y2="90.58"
381 gradientUnits="userSpaceOnUse"
382 >
383 <stop stopColor="#FE315D" />
384 <stop offset="0.04023279" stopColor="#F63462" />
385 <stop offset="0.1037" stopColor="#DF3A71" />
386 <stop offset="0.1667" stopColor="#C24383" />
387 <stop offset="0.2912" stopColor="#AD4A91" />
388 <stop offset="0.5498" stopColor="#755BB4" />
389 <stop offset="0.9175" stopColor="#1D76ED" />
390 <stop offset="1" stopColor="#087CFA" />
391 </linearGradient>
392 </defs>
393 <polygon points="17.7,54.6 0.8,41.2 9.2,25.6 33.3,35" fill={`url(#${gradientAId})`} />
394 <path
395 d="M70 18.7 68.7 59.2 41.8 70 25.6 59.6 49.3 35 38.9 12.3 48.2 1.1Z"
396 fill={`url(#${gradientBId})`}
397 />
398 <polygon points="70,18.7 48.7,43.9 38.9,12.3 48.2,1.1" fill={`url(#${gradientCId})`} />
399 <path
400 d="M33.7 58.1 5.6 68.3 10.1 52.5 16 33.1 0 27.7 10.1 0 32.1 2.7 53.7 27.4Z"
401 fill={`url(#${gradientDId})`}
402 />
403 <rect x="13.7" y="13.5" width="43.2" height="43.2" fill="#000" />
404 <rect x="17.7" y="48.6" width="16.2" height="2.7" fill="#fff" />
405 <path d="M29.4 22.4v-3.3h-9v3.3h2.6v11.3h-2.6V37h9v-3.3h-2.5V22.4h2.5Z" fill="#fff" />
406 <path
407 d="M38 37.3c-1.4 0-2.6-.3-3.5-.8-.9-.5-1.7-1.2-2.3-1.9l2.5-2.8c.5.6 1 1 1.5 1.3.5.3 1.1.5 1.7.5.7 0 1.3-.2 1.8-.7.4-.5.6-1.2.6-2.3V19.1h4v11.7c0 1.1-.1 2-.4 2.8-.3.8-.7 1.4-1.3 2-.5.5-1.2 1-2 1.2-.8.3-1.6.5-2.6.5Z"
408 fill="#fff"
409 />
410 </svg>
411 );
412};
413
313414export const OpenCodeIcon: Icon = (props) => (
314415 <svg {...props} viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
315416 <g clipPath="url(#opencode__clip0_1311_94969)">
apps/web/src/components/chat/OpenInPicker.tsx+14-1
......@@ -6,7 +6,15 @@ import { ChevronDownIcon, FolderClosedIcon } from "lucide-react";
66import { Button } from "../ui/button";
77import { Group, GroupSeparator } from "../ui/group";
88import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "../ui/menu";
9import { AntigravityIcon, CursorIcon, Icon, TraeIcon, VisualStudioCode, Zed } from "../Icons";
9import {
10 AntigravityIcon,
11 CursorIcon,
12 Icon,
13 TraeIcon,
14 IntelliJIdeaIcon,
15 VisualStudioCode,
16 Zed,
17} from "../Icons";
1018import { isMacPlatform, isWindowsPlatform } from "~/lib/utils";
1119import { readNativeApi } from "~/nativeApi";
1220
......@@ -47,6 +55,11 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray<Editor
4755 Icon: AntigravityIcon,
4856 value: "antigravity",
4957 },
58 {
59 label: "IntelliJ IDEA",
60 Icon: IntelliJIdeaIcon,
61 value: "idea",
62 },
5063 {
5164 label: isMacPlatform(platform)
5265 ? "Finder"
packages/contracts/src/editor.ts+20-9
......@@ -1,21 +1,32 @@
11import { Schema } from "effect";
22import { TrimmedNonEmptyString } from "./baseSchemas";
33
4export const EditorLaunchStyle = Schema.Literals(["direct-path", "goto", "line-column"]);
5export type EditorLaunchStyle = typeof EditorLaunchStyle.Type;
6
7type EditorDefinition = {
8 readonly id: string;
9 readonly label: string;
10 readonly command: string | null;
11 readonly launchStyle: EditorLaunchStyle;
12};
13
414export const EDITORS = [
5 { id: "cursor", label: "Cursor", command: "cursor", supportsGoto: true },
6 { id: "trae", label: "Trae", command: "trae", supportsGoto: true },
7 { id: "vscode", label: "VS Code", command: "code", supportsGoto: true },
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" },
818 {
919 id: "vscode-insiders",
1020 label: "VS Code Insiders",
1121 command: "code-insiders",
12 supportsGoto: true,
22 launchStyle: "goto",
1323 },
14 { id: "vscodium", label: "VSCodium", command: "codium", supportsGoto: true },
15 { id: "zed", label: "Zed", command: "zed", supportsGoto: false },
16 { id: "antigravity", label: "Antigravity", command: "agy", supportsGoto: false },
17 { id: "file-manager", label: "File Manager", command: null, supportsGoto: false },
18] as const;
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" },
29] as const satisfies ReadonlyArray<EditorDefinition>;
1930
2031export const EditorId = Schema.Literals(EDITORS.map((e) => e.id));
2132export type EditorId = typeof EditorId.Type;