| 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 | 75 | command: "zed", |
| 76 | 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 | 91 | Effect.gen(function* () { |
| 83 | 92 | const lineOnly = yield* resolveEditorLaunch( |
| 84 | 93 | { cwd: "/tmp/workspace/AGENTS.md:48", editor: "cursor" }, |
| ... | ... | @@ -142,6 +151,33 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { |
| 142 | 151 | command: "zed", |
| 143 | 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 | 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 { | |
| 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 | ||
| 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 | 78 | function fileManagerCommandForPlatform(platform: NodeJS.Platform): string { |
| ... | ... | @@ -208,9 +243,10 @@ export const resolveEditorLaunch = Effect.fnUntraced(function* ( |
| 208 | 243 | } |
| 209 | 244 | |
| 210 | 245 | 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 | }; | |
| 214 | 250 | } |
| 215 | 251 | |
| 216 | 252 | if (editorDef.id !== "file-manager") { |
apps/web/src/components/Icons.tsx+101| ... | ... | @@ -310,6 +310,107 @@ export const AntigravityIcon: Icon = (props) => ( |
| 310 | 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 | 414 | export const OpenCodeIcon: Icon = (props) => ( |
| 314 | 415 | <svg {...props} viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 315 | 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 | 6 | import { Button } from "../ui/button"; |
| 7 | 7 | import { Group, GroupSeparator } from "../ui/group"; |
| 8 | 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 | 18 | import { isMacPlatform, isWindowsPlatform } from "~/lib/utils"; |
| 11 | 19 | import { readNativeApi } from "~/nativeApi"; |
| 12 | 20 | |
| ... | ... | @@ -47,6 +55,11 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray<Editor |
| 47 | 55 | Icon: AntigravityIcon, |
| 48 | 56 | value: "antigravity", |
| 49 | 57 | }, |
| 58 | { | |
| 59 | label: "IntelliJ IDEA", | |
| 60 | Icon: IntelliJIdeaIcon, | |
| 61 | value: "idea", | |
| 62 | }, | |
| 50 | 63 | { |
| 51 | 64 | label: isMacPlatform(platform) |
| 52 | 65 | ? "Finder" |
packages/contracts/src/editor.ts+20-9| ... | ... | @@ -1,21 +1,32 @@ |
| 1 | 1 | import { Schema } from "effect"; |
| 2 | 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 | 14 | export 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" }, | |
| 8 | 18 | { |
| 9 | 19 | id: "vscode-insiders", |
| 10 | 20 | label: "VS Code Insiders", |
| 11 | 21 | command: "code-insiders", |
| 12 | supportsGoto: true, | |
| 22 | launchStyle: "goto", | |
| 13 | 23 | }, |
| 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>; | |
| 19 | 30 | |
| 20 | 31 | export const EditorId = Schema.Literals(EDITORS.map((e) => e.id)); |
| 21 | 32 | export type EditorId = typeof EditorId.Type; |