| author | |
| committer | |
| log | 1b272fd70992ac349791dd6b6c084ef1d042d700 |
| tree | 3679c8cdb4cc5c887eaef6a71ee6680ecaa9e592 |
| parent | 9013c07f6d679813777462466e9051e4ffc27e03 |
| signature | Signed by PGP key B5690EEEBB952194 |
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) => { | ... | @@ -75,10 +75,19 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { |
| 75 | command: "zed", | 75 | command: "zed", |
| 76 | args: ["/tmp/workspace"], | 76 | args: ["/tmp/workspace"], |
| 77 | }); | 77 | }); |
| 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 | }); | ||
| 78 | }), | 87 | }), |
| 79 | ); | 88 | ); |
| 80 | 89 | ||
| 81 | it.effect("uses --goto when editor supports line/column suffixes", () => | 90 | it.effect("applies launch-style-specific navigation arguments", () => |
| 82 | Effect.gen(function* () { | 91 | Effect.gen(function* () { |
| 83 | const lineOnly = yield* resolveEditorLaunch( | 92 | const lineOnly = yield* resolveEditorLaunch( |
| 84 | { cwd: "/tmp/workspace/AGENTS.md:48", editor: "cursor" }, | 93 | { cwd: "/tmp/workspace/AGENTS.md:48", editor: "cursor" }, |
| ... | @@ -142,6 +151,33 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { | ... | @@ -142,6 +151,33 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { |
| 142 | command: "zed", | 151 | command: "zed", |
| 143 | args: ["/tmp/workspace/src/open.ts:71:5"], | 152 | args: ["/tmp/workspace/src/open.ts:71:5"], |
| 144 | }); | 153 | }); |
| 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 | }); | ||
| 145 | }), | 181 | }), |
| 146 | ); | 182 | ); |
| 147 | 183 |
apps/server/src/open.ts+42-6| ... | @@ -34,10 +34,45 @@ interface CommandAvailabilityOptions { | ... | @@ -34,10 +34,45 @@ interface CommandAvailabilityOptions { |
| 34 | readonly env?: NodeJS.ProcessEnv; | 34 | readonly env?: NodeJS.ProcessEnv; |
| 35 | } | 35 | } |
| 36 | 36 | ||
| 37 | const LINE_COLUMN_SUFFIX_PATTERN = /:\d+(?::\d+)?$/; | 37 | const TARGET_WITH_POSITION_PATTERN = /^(.*?):(\d+)(?::(\d+))?$/; |
| 38 | |||
| 39 | function 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 | } | ||
| 38 | 48 | ||
| 39 | function shouldUseGotoFlag(editor: (typeof EDITORS)[number], target: string): boolean { | 49 | return { |
| 40 | return editor.supportsGoto && LINE_COLUMN_SUFFIX_PATTERN.test(target); | 50 | path: match[1], |
| 51 | line: match[2], | ||
| 52 | column: match[3], | ||
| 53 | }; | ||
| 54 | } | ||
| 55 | |||
| 56 | function 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 | } | ||
| 41 | } | 76 | } |
| 42 | 77 | ||
| 43 | function fileManagerCommandForPlatform(platform: NodeJS.Platform): string { | 78 | function fileManagerCommandForPlatform(platform: NodeJS.Platform): string { |
| ... | @@ -208,9 +243,10 @@ export const resolveEditorLaunch = Effect.fnUntraced(function* ( | ... | @@ -208,9 +243,10 @@ export const resolveEditorLaunch = Effect.fnUntraced(function* ( |
| 208 | } | 243 | } |
| 209 | 244 | ||
| 210 | if (editorDef.command) { | 245 | if (editorDef.command) { |
| 211 | return shouldUseGotoFlag(editorDef, input.cwd) | 246 | return { |
| 212 | ? { command: editorDef.command, args: ["--goto", input.cwd] } | 247 | command: editorDef.command, |
| 213 | : { command: editorDef.command, args: [input.cwd] }; | 248 | args: resolveCommandEditorArgs(editorDef, input.cwd), |
| 249 | }; | ||
| 214 | } | 250 | } |
| 215 | 251 | ||
| 216 | if (editorDef.id !== "file-manager") { | 252 | if (editorDef.id !== "file-manager") { |
apps/web/src/components/Icons.tsx+101| ... | @@ -310,6 +310,107 @@ export const AntigravityIcon: Icon = (props) => ( | ... | @@ -310,6 +310,107 @@ export const AntigravityIcon: Icon = (props) => ( |
| 310 | </svg> | 310 | </svg> |
| 311 | ); | 311 | ); |
| 312 | 312 | ||
| 313 | export 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 | |||
| 313 | export const OpenCodeIcon: Icon = (props) => ( | 414 | export const OpenCodeIcon: Icon = (props) => ( |
| 314 | <svg {...props} viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg"> | 415 | <svg {...props} viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 315 | <g clipPath="url(#opencode__clip0_1311_94969)"> | 416 | <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"; | ... | @@ -6,7 +6,15 @@ import { ChevronDownIcon, FolderClosedIcon } from "lucide-react"; |
| 6 | import { Button } from "../ui/button"; | 6 | import { Button } from "../ui/button"; |
| 7 | import { Group, GroupSeparator } from "../ui/group"; | 7 | import { Group, GroupSeparator } from "../ui/group"; |
| 8 | import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "../ui/menu"; | 8 | import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "../ui/menu"; |
| 9 | import { AntigravityIcon, CursorIcon, Icon, TraeIcon, VisualStudioCode, Zed } from "../Icons"; | 9 | import { |
| 10 | AntigravityIcon, | ||
| 11 | CursorIcon, | ||
| 12 | Icon, | ||
| 13 | TraeIcon, | ||
| 14 | IntelliJIdeaIcon, | ||
| 15 | VisualStudioCode, | ||
| 16 | Zed, | ||
| 17 | } from "../Icons"; | ||
| 10 | import { isMacPlatform, isWindowsPlatform } from "~/lib/utils"; | 18 | import { isMacPlatform, isWindowsPlatform } from "~/lib/utils"; |
| 11 | import { readNativeApi } from "~/nativeApi"; | 19 | import { readNativeApi } from "~/nativeApi"; |
| 12 | 20 | ||
| ... | @@ -47,6 +55,11 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray<Editor | ... | @@ -47,6 +55,11 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray<Editor |
| 47 | Icon: AntigravityIcon, | 55 | Icon: AntigravityIcon, |
| 48 | value: "antigravity", | 56 | value: "antigravity", |
| 49 | }, | 57 | }, |
| 58 | { | ||
| 59 | label: "IntelliJ IDEA", | ||
| 60 | Icon: IntelliJIdeaIcon, | ||
| 61 | value: "idea", | ||
| 62 | }, | ||
| 50 | { | 63 | { |
| 51 | label: isMacPlatform(platform) | 64 | label: isMacPlatform(platform) |
| 52 | ? "Finder" | 65 | ? "Finder" |
packages/contracts/src/editor.ts+20-9| ... | @@ -1,21 +1,32 @@ | ... | @@ -1,21 +1,32 @@ |
| 1 | import { Schema } from "effect"; | 1 | import { Schema } from "effect"; |
| 2 | import { TrimmedNonEmptyString } from "./baseSchemas"; | 2 | import { TrimmedNonEmptyString } from "./baseSchemas"; |
| 3 | 3 | ||
| 4 | export const EditorLaunchStyle = Schema.Literals(["direct-path", "goto", "line-column"]); | ||
| 5 | export type EditorLaunchStyle = typeof EditorLaunchStyle.Type; | ||
| 6 | |||
| 7 | type EditorDefinition = { | ||
| 8 | readonly id: string; | ||
| 9 | readonly label: string; | ||
| 10 | readonly command: string | null; | ||
| 11 | readonly launchStyle: EditorLaunchStyle; | ||
| 12 | }; | ||
| 13 | |||
| 4 | export const EDITORS = [ | 14 | export const EDITORS = [ |
| 5 | { id: "cursor", label: "Cursor", command: "cursor", supportsGoto: true }, | 15 | { id: "cursor", label: "Cursor", command: "cursor", launchStyle: "goto" }, |
| 6 | { id: "trae", label: "Trae", command: "trae", supportsGoto: true }, | 16 | { id: "trae", label: "Trae", command: "trae", launchStyle: "goto" }, |
| 7 | { id: "vscode", label: "VS Code", command: "code", supportsGoto: true }, | 17 | { id: "vscode", label: "VS Code", command: "code", launchStyle: "goto" }, |
| 8 | { | 18 | { |
| 9 | id: "vscode-insiders", | 19 | id: "vscode-insiders", |
| 10 | label: "VS Code Insiders", | 20 | label: "VS Code Insiders", |
| 11 | command: "code-insiders", | 21 | command: "code-insiders", |
| 12 | supportsGoto: true, | 22 | launchStyle: "goto", |
| 13 | }, | 23 | }, |
| 14 | { id: "vscodium", label: "VSCodium", command: "codium", supportsGoto: true }, | 24 | { id: "vscodium", label: "VSCodium", command: "codium", launchStyle: "goto" }, |
| 15 | { id: "zed", label: "Zed", command: "zed", supportsGoto: false }, | 25 | { id: "zed", label: "Zed", command: "zed", launchStyle: "direct-path" }, |
| 16 | { id: "antigravity", label: "Antigravity", command: "agy", supportsGoto: false }, | 26 | { id: "antigravity", label: "Antigravity", command: "agy", launchStyle: "goto" }, |
| 17 | { id: "file-manager", label: "File Manager", command: null, supportsGoto: false }, | 27 | { id: "idea", label: "IntelliJ IDEA", command: "idea", launchStyle: "line-column" }, |
| 18 | ] as const; | 28 | { id: "file-manager", label: "File Manager", command: null, launchStyle: "direct-path" }, |
| 29 | ] as const satisfies ReadonlyArray<EditorDefinition>; | ||
| 19 | 30 | ||
| 20 | export const EditorId = Schema.Literals(EDITORS.map((e) => e.id)); | 31 | export const EditorId = Schema.Literals(EDITORS.map((e) => e.id)); |
| 21 | export type EditorId = typeof EditorId.Type; | 32 | export type EditorId = typeof EditorId.Type; |