diff --git a/apps/server/src/open.test.ts b/apps/server/src/open.test.ts index c612922fea..76b14c8597 100644 --- a/apps/server/src/open.test.ts +++ b/apps/server/src/open.test.ts @@ -75,10 +75,19 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { command: "zed", args: ["/tmp/workspace"], }); + + const ideaLaunch = yield* resolveEditorLaunch( + { cwd: "/tmp/workspace", editor: "idea" }, + "darwin", + ); + assert.deepEqual(ideaLaunch, { + command: "idea", + args: ["/tmp/workspace"], + }); }), ); - it.effect("uses --goto when editor supports line/column suffixes", () => + it.effect("applies launch-style-specific navigation arguments", () => Effect.gen(function* () { const lineOnly = yield* resolveEditorLaunch( { cwd: "/tmp/workspace/AGENTS.md:48", editor: "cursor" }, @@ -142,6 +151,33 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { command: "zed", args: ["/tmp/workspace/src/open.ts:71:5"], }); + + const zedLineOnly = yield* resolveEditorLaunch( + { cwd: "/tmp/workspace/AGENTS.md:48", editor: "zed" }, + "darwin", + ); + assert.deepEqual(zedLineOnly, { + command: "zed", + args: ["/tmp/workspace/AGENTS.md:48"], + }); + + const ideaLineOnly = yield* resolveEditorLaunch( + { cwd: "/tmp/workspace/AGENTS.md:48", editor: "idea" }, + "darwin", + ); + assert.deepEqual(ideaLineOnly, { + command: "idea", + args: ["--line", "48", "/tmp/workspace/AGENTS.md"], + }); + + const ideaLineAndColumn = yield* resolveEditorLaunch( + { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "idea" }, + "darwin", + ); + assert.deepEqual(ideaLineAndColumn, { + command: "idea", + args: ["--line", "71", "--column", "5", "/tmp/workspace/src/open.ts"], + }); }), ); diff --git a/apps/server/src/open.ts b/apps/server/src/open.ts index 212db36c01..cdacda7c09 100644 --- a/apps/server/src/open.ts +++ b/apps/server/src/open.ts @@ -34,10 +34,45 @@ interface CommandAvailabilityOptions { readonly env?: NodeJS.ProcessEnv; } -const LINE_COLUMN_SUFFIX_PATTERN = /:\d+(?::\d+)?$/; +const TARGET_WITH_POSITION_PATTERN = /^(.*?):(\d+)(?::(\d+))?$/; + +function parseTargetPathAndPosition(target: string): { + path: string; + line: string | undefined; + column: string | undefined; +} | null { + const match = TARGET_WITH_POSITION_PATTERN.exec(target); + if (!match?.[1] || !match[2]) { + return null; + } -function shouldUseGotoFlag(editor: (typeof EDITORS)[number], target: string): boolean { - return editor.supportsGoto && LINE_COLUMN_SUFFIX_PATTERN.test(target); + return { + path: match[1], + line: match[2], + column: match[3], + }; +} + +function resolveCommandEditorArgs( + editor: (typeof EDITORS)[number], + target: string, +): ReadonlyArray { + const parsedTarget = parseTargetPathAndPosition(target); + + switch (editor.launchStyle) { + case "direct-path": + return [target]; + case "goto": + return parsedTarget ? ["--goto", target] : [target]; + case "line-column": { + if (!parsedTarget) { + return [target]; + } + + const { path, line, column } = parsedTarget; + return [...(line ? ["--line", line] : []), ...(column ? ["--column", column] : []), path]; + } + } } function fileManagerCommandForPlatform(platform: NodeJS.Platform): string { @@ -208,9 +243,10 @@ export const resolveEditorLaunch = Effect.fnUntraced(function* ( } if (editorDef.command) { - return shouldUseGotoFlag(editorDef, input.cwd) - ? { command: editorDef.command, args: ["--goto", input.cwd] } - : { command: editorDef.command, args: [input.cwd] }; + return { + command: editorDef.command, + args: resolveCommandEditorArgs(editorDef, input.cwd), + }; } if (editorDef.id !== "file-manager") { diff --git a/apps/web/src/components/Icons.tsx b/apps/web/src/components/Icons.tsx index 3f4844af80..2e95b54e25 100644 --- a/apps/web/src/components/Icons.tsx +++ b/apps/web/src/components/Icons.tsx @@ -310,6 +310,107 @@ export const AntigravityIcon: Icon = (props) => ( ); +export const IntelliJIdeaIcon: Icon = (props) => { + const id = useId(); + const gradientAId = `${id}-idea-a`; + const gradientBId = `${id}-idea-b`; + const gradientCId = `${id}-idea-c`; + const gradientDId = `${id}-idea-d`; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + export const OpenCodeIcon: Icon = (props) => ( diff --git a/apps/web/src/components/chat/OpenInPicker.tsx b/apps/web/src/components/chat/OpenInPicker.tsx index bb5362439e..703bfadaa3 100644 --- a/apps/web/src/components/chat/OpenInPicker.tsx +++ b/apps/web/src/components/chat/OpenInPicker.tsx @@ -6,7 +6,15 @@ import { ChevronDownIcon, FolderClosedIcon } from "lucide-react"; import { Button } from "../ui/button"; import { Group, GroupSeparator } from "../ui/group"; import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "../ui/menu"; -import { AntigravityIcon, CursorIcon, Icon, TraeIcon, VisualStudioCode, Zed } from "../Icons"; +import { + AntigravityIcon, + CursorIcon, + Icon, + TraeIcon, + IntelliJIdeaIcon, + VisualStudioCode, + Zed, +} from "../Icons"; import { isMacPlatform, isWindowsPlatform } from "~/lib/utils"; import { readNativeApi } from "~/nativeApi"; @@ -47,6 +55,11 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray; export const EditorId = Schema.Literals(EDITORS.map((e) => e.id)); export type EditorId = typeof EditorId.Type;