| ... | @@ -17,6 +17,12 @@ type EditorDefinition = { | ... | @@ -17,6 +17,12 @@ type EditorDefinition = { |
| 17 | * `zed://ssh/<host><path>` shape. | 17 | * `zed://ssh/<host><path>` shape. |
| 18 | */ | 18 | */ |
| 19 | readonly remoteScheme?: string; | 19 | readonly remoteScheme?: string; |
| | 20 | /** |
| | 21 | * JetBrains product code (`IU`, `PY`, ...). JetBrains IDEs open remote |
| | 22 | * projects through the Toolbox App's `jetbrains://gateway/ssh/...` link, |
| | 23 | * which uses the code to choose the IDE backend. |
| | 24 | */ |
| | 25 | readonly jetbrainsProductCode?: string; |
| 20 | }; | 26 | }; |
| 21 | | 27 | |
| 22 | export const EDITORS = [ | 28 | export const EDITORS = [ |
| ... | @@ -67,18 +73,90 @@ export const EDITORS = [ | ... | @@ -67,18 +73,90 @@ export const EDITORS = [ |
| 67 | commands: ["antigravity-ide", "agy-ide"], | 73 | commands: ["antigravity-ide", "agy-ide"], |
| 68 | launchStyle: "goto", | 74 | launchStyle: "goto", |
| 69 | }, | 75 | }, |
| 70 | { id: "idea", label: "IntelliJ IDEA", commands: ["idea"], launchStyle: "line-column" }, | 76 | { |
| 71 | { id: "aqua", label: "Aqua", commands: ["aqua"], launchStyle: "line-column" }, | 77 | id: "idea", |
| 72 | { id: "clion", label: "CLion", commands: ["clion"], launchStyle: "line-column" }, | 78 | label: "IntelliJ IDEA", |
| 73 | { id: "datagrip", label: "DataGrip", commands: ["datagrip"], launchStyle: "line-column" }, | 79 | commands: ["idea"], |
| 74 | { id: "dataspell", label: "DataSpell", commands: ["dataspell"], launchStyle: "line-column" }, | 80 | launchStyle: "line-column", |
| 75 | { id: "goland", label: "GoLand", commands: ["goland"], launchStyle: "line-column" }, | 81 | jetbrainsProductCode: "IU", |
| 76 | { id: "phpstorm", label: "PhpStorm", commands: ["phpstorm"], launchStyle: "line-column" }, | 82 | }, |
| 77 | { id: "pycharm", label: "PyCharm", commands: ["pycharm"], launchStyle: "line-column" }, | 83 | { |
| 78 | { id: "rider", label: "Rider", commands: ["rider"], launchStyle: "line-column" }, | 84 | id: "aqua", |
| 79 | { id: "rubymine", label: "RubyMine", commands: ["rubymine"], launchStyle: "line-column" }, | 85 | label: "Aqua", |
| 80 | { id: "rustrover", label: "RustRover", commands: ["rustrover"], launchStyle: "line-column" }, | 86 | commands: ["aqua"], |
| 81 | { id: "webstorm", label: "WebStorm", commands: ["webstorm"], launchStyle: "line-column" }, | 87 | launchStyle: "line-column", |
| | 88 | jetbrainsProductCode: "QA", |
| | 89 | }, |
| | 90 | { |
| | 91 | id: "clion", |
| | 92 | label: "CLion", |
| | 93 | commands: ["clion"], |
| | 94 | launchStyle: "line-column", |
| | 95 | jetbrainsProductCode: "CL", |
| | 96 | }, |
| | 97 | { |
| | 98 | id: "datagrip", |
| | 99 | label: "DataGrip", |
| | 100 | commands: ["datagrip"], |
| | 101 | launchStyle: "line-column", |
| | 102 | jetbrainsProductCode: "DB", |
| | 103 | }, |
| | 104 | { |
| | 105 | id: "dataspell", |
| | 106 | label: "DataSpell", |
| | 107 | commands: ["dataspell"], |
| | 108 | launchStyle: "line-column", |
| | 109 | jetbrainsProductCode: "DS", |
| | 110 | }, |
| | 111 | { |
| | 112 | id: "goland", |
| | 113 | label: "GoLand", |
| | 114 | commands: ["goland"], |
| | 115 | launchStyle: "line-column", |
| | 116 | jetbrainsProductCode: "GO", |
| | 117 | }, |
| | 118 | { |
| | 119 | id: "phpstorm", |
| | 120 | label: "PhpStorm", |
| | 121 | commands: ["phpstorm"], |
| | 122 | launchStyle: "line-column", |
| | 123 | jetbrainsProductCode: "PS", |
| | 124 | }, |
| | 125 | { |
| | 126 | id: "pycharm", |
| | 127 | label: "PyCharm", |
| | 128 | commands: ["pycharm"], |
| | 129 | launchStyle: "line-column", |
| | 130 | jetbrainsProductCode: "PY", |
| | 131 | }, |
| | 132 | { |
| | 133 | id: "rider", |
| | 134 | label: "Rider", |
| | 135 | commands: ["rider"], |
| | 136 | launchStyle: "line-column", |
| | 137 | jetbrainsProductCode: "RD", |
| | 138 | }, |
| | 139 | { |
| | 140 | id: "rubymine", |
| | 141 | label: "RubyMine", |
| | 142 | commands: ["rubymine"], |
| | 143 | launchStyle: "line-column", |
| | 144 | jetbrainsProductCode: "RM", |
| | 145 | }, |
| | 146 | { |
| | 147 | id: "rustrover", |
| | 148 | label: "RustRover", |
| | 149 | commands: ["rustrover"], |
| | 150 | launchStyle: "line-column", |
| | 151 | jetbrainsProductCode: "RR", |
| | 152 | }, |
| | 153 | { |
| | 154 | id: "webstorm", |
| | 155 | label: "WebStorm", |
| | 156 | commands: ["webstorm"], |
| | 157 | launchStyle: "line-column", |
| | 158 | jetbrainsProductCode: "WS", |
| | 159 | }, |
| 82 | { id: "file-manager", label: "File Manager", commands: null, launchStyle: "direct-path" }, | 160 | { id: "file-manager", label: "File Manager", commands: null, launchStyle: "direct-path" }, |
| 83 | ] as const satisfies ReadonlyArray<EditorDefinition>; | 161 | ] as const satisfies ReadonlyArray<EditorDefinition>; |
| 84 | | 162 | |
| ... | @@ -98,7 +176,8 @@ export const LaunchEditorInput = Schema.Struct({ | ... | @@ -98,7 +176,8 @@ export const LaunchEditorInput = Schema.Struct({ |
| 98 | }); | 176 | }); |
| 99 | export type LaunchEditorInput = typeof LaunchEditorInput.Type; | 177 | export type LaunchEditorInput = typeof LaunchEditorInput.Type; |
| 100 | | 178 | |
| 101 | const remoteSchemeOf = (editor: EditorDefinition): string | undefined => editor.remoteScheme; | 179 | const remoteSchemeOf = (editor: EditorDefinition): string | undefined => |
| | 180 | editor.remoteScheme ?? (editor.jetbrainsProductCode === undefined ? undefined : "jetbrains"); |
| 102 | | 181 | |
| 103 | /** Editors that can open a remote workspace via an SSH deep link. */ | 182 | /** Editors that can open a remote workspace via an SSH deep link. */ |
| 104 | export const REMOTE_CAPABLE_EDITOR_IDS: ReadonlyArray<EditorId> = EDITORS.flatMap((editor) => | 183 | export const REMOTE_CAPABLE_EDITOR_IDS: ReadonlyArray<EditorId> = EDITORS.flatMap((editor) => |
| ... | @@ -112,9 +191,10 @@ export const remoteSchemeForEditor = (id: EditorId): string | undefined => { | ... | @@ -112,9 +191,10 @@ export const remoteSchemeForEditor = (id: EditorId): string | undefined => { |
| 112 | | 191 | |
| 113 | /** | 192 | /** |
| 114 | * Builds a `<scheme>://vscode-remote/ssh-remote+<host><path>` deep link (Zed | 193 | * Builds a `<scheme>://vscode-remote/ssh-remote+<host><path>` deep link (Zed |
| 115 | * takes `zed://ssh/<host><path>`) that opens `absolutePath` on `host` in the | 194 | * takes `zed://ssh/<host><path>`, JetBrains IDEs a Toolbox App |
| 116 | * local editor over SSH. Returns undefined for editors without remote | 195 | * `jetbrains://gateway/ssh/environment?...` link) that opens `absolutePath` on |
| 117 | * deep-link support. | 196 | * `host` in the local editor over SSH. Returns undefined for editors without |
| | 197 | * remote deep-link support. |
| 118 | */ | 198 | */ |
| 119 | export const buildRemoteOpenUrl = (input: { | 199 | export const buildRemoteOpenUrl = (input: { |
| 120 | readonly editor: EditorId; | 200 | readonly editor: EditorId; |
| ... | @@ -125,6 +205,18 @@ export const buildRemoteOpenUrl = (input: { | ... | @@ -125,6 +205,18 @@ export const buildRemoteOpenUrl = (input: { |
| 125 | if (scheme === undefined) { | 205 | if (scheme === undefined) { |
| 126 | return undefined; | 206 | return undefined; |
| 127 | } | 207 | } |
| | 208 | const editor = EDITORS.find((candidate) => candidate.id === input.editor); |
| | 209 | if (editor !== undefined && "jetbrainsProductCode" in editor) { |
| | 210 | // Like the VS Code link, no user or port: the SSH config entry for `host` |
| | 211 | // supplies them. A bare product code lets Toolbox pick the backend build. |
| | 212 | const params = new URLSearchParams({ |
| | 213 | h: input.host, |
| | 214 | launchIde: "true", |
| | 215 | ideHint: editor.jetbrainsProductCode, |
| | 216 | projectHint: input.absolutePath.replaceAll("\\", "/"), |
| | 217 | }); |
| | 218 | return `${scheme}://gateway/ssh/environment?${params.toString()}`; |
| | 219 | } |
| 128 | // Windows server paths (`C:\...`) appear as `/C:/...` in vscode-remote URIs. | 220 | // Windows server paths (`C:\...`) appear as `/C:/...` in vscode-remote URIs. |
| 129 | const posixPath = input.absolutePath.replaceAll("\\", "/"); | 221 | const posixPath = input.absolutePath.replaceAll("\\", "/"); |
| 130 | const rootedPath = posixPath.startsWith("/") ? posixPath : `/${posixPath}`; | 222 | const rootedPath = posixPath.startsWith("/") ? posixPath : `/${posixPath}`; |