| author | |
| committer | |
| log | 20ef25037f455a570426ac2a93fcf499d1f91167 |
| tree | bfb6606e8eb27f6b6bb77aa7a3f0fc21c4421a0a |
| parent | 5735693d4acb949c2b0daf1219b4f62845115435 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>4 files changed, 31 insertions(+), 9 deletions(-)
apps/desktop/src/electron/ElectronShell.test.ts+9-3| ... | @@ -93,10 +93,16 @@ describe("ElectronShell", () => { | ... | @@ -93,10 +93,16 @@ describe("ElectronShell", () => { |
| 93 | openExternalMock.mockResolvedValue(undefined); | 93 | openExternalMock.mockResolvedValue(undefined); |
| 94 | 94 | ||
| 95 | const electronShell = yield* ElectronShell.ElectronShell; | 95 | const electronShell = yield* ElectronShell.ElectronShell; |
| 96 | const result = yield* electronShell.openExternal("zed://ssh/example.com/home/user/project"); | 96 | const results = yield* Effect.all([ |
| 97 | electronShell.openExternal("zed://ssh/example.com/home/user/project"), | ||
| 98 | electronShell.openExternal("zed://ssh/example.com/"), | ||
| 99 | ]); | ||
| 97 | 100 | ||
| 98 | assert.equal(result, true); | 101 | assert.deepEqual(results, [true, true]); |
| 99 | assert.deepEqual(openExternalMock.mock.calls, [["zed://ssh/example.com/home/user/project"]]); | 102 | assert.deepEqual(openExternalMock.mock.calls, [ |
| 103 | ["zed://ssh/example.com/home/user/project"], | ||
| 104 | ["zed://ssh/example.com/"], | ||
| 105 | ]); | ||
| 100 | }).pipe(Effect.provide(ElectronShell.layer)), | 106 | }).pipe(Effect.provide(ElectronShell.layer)), |
| 101 | ); | 107 | ); |
| 102 | 108 |
apps/desktop/src/electron/ElectronShell.ts+1-1| ... | @@ -36,7 +36,7 @@ const REMOTE_EDITOR_PROTOCOLS = new Set( | ... | @@ -36,7 +36,7 @@ const REMOTE_EDITOR_PROTOCOLS = new Set( |
| 36 | ); | 36 | ); |
| 37 | 37 | ||
| 38 | // Zed's host sits in the first path segment, so it needs its own userinfo ban. | 38 | // Zed's host sits in the first path segment, so it needs its own userinfo ban. |
| 39 | const ZED_SSH_PATHNAME = /^\/[^/@:]+\/.+$/; | 39 | const ZED_SSH_PATHNAME = /^\/[^/@:]+\/.*$/; |
| 40 | 40 | ||
| 41 | const isRemoteEditorUrl = (url: URL) => | 41 | const isRemoteEditorUrl = (url: URL) => |
| 42 | REMOTE_EDITOR_PROTOCOLS.has(url.protocol) && | 42 | REMOTE_EDITOR_PROTOCOLS.has(url.protocol) && |
apps/web/src/remoteOpen.test.ts+9| ... | @@ -151,6 +151,15 @@ describe("buildRemoteOpenUrl", () => { | ... | @@ -151,6 +151,15 @@ describe("buildRemoteOpenUrl", () => { |
| 151 | ).toBe("zed://ssh/sol.tail1234.ts.net/home/theo/code/my%20repo"); | 151 | ).toBe("zed://ssh/sol.tail1234.ts.net/home/theo/code/my%20repo"); |
| 152 | }); | 152 | }); |
| 153 | 153 | ||
| 154 | it("drops the Windows drive letter for Zed", () => { | ||
| 155 | expect( | ||
| 156 | buildRemoteOpenUrl({ editor: "zed", host: "sol", absolutePath: "C:\\Users\\theo" }), | ||
| 157 | ).toBe("zed://ssh/sol/Users/theo"); | ||
| 158 | expect(buildRemoteOpenUrl({ editor: "zed", host: "sol", absolutePath: "/C:/project" })).toBe( | ||
| 159 | "zed://ssh/sol/C%3A/project", | ||
| 160 | ); | ||
| 161 | }); | ||
| 162 | |||
| 154 | it("returns undefined for editors without remote support", () => { | 163 | it("returns undefined for editors without remote support", () => { |
| 155 | expect(buildRemoteOpenUrl({ editor: "idea", host: "sol", absolutePath: "/tmp/x" })).toBe( | 164 | expect(buildRemoteOpenUrl({ editor: "idea", host: "sol", absolutePath: "/tmp/x" })).toBe( |
| 156 | undefined, | 165 | undefined, |
packages/contracts/src/editor.ts+12-5| ... | @@ -91,7 +91,7 @@ export type LaunchEditorInput = typeof LaunchEditorInput.Type; | ... | @@ -91,7 +91,7 @@ export type LaunchEditorInput = typeof LaunchEditorInput.Type; |
| 91 | 91 | ||
| 92 | const remoteSchemeOf = (editor: EditorDefinition): string | undefined => editor.remoteScheme; | 92 | const remoteSchemeOf = (editor: EditorDefinition): string | undefined => editor.remoteScheme; |
| 93 | 93 | ||
| 94 | /** Editors that can open a remote workspace via `vscode-remote` deep links. */ | 94 | /** Editors that can open a remote workspace via an SSH deep link. */ |
| 95 | export const REMOTE_CAPABLE_EDITOR_IDS: ReadonlyArray<EditorId> = EDITORS.flatMap((editor) => | 95 | export const REMOTE_CAPABLE_EDITOR_IDS: ReadonlyArray<EditorId> = EDITORS.flatMap((editor) => |
| 96 | remoteSchemeOf(editor) !== undefined ? [editor.id] : [], | 96 | remoteSchemeOf(editor) !== undefined ? [editor.id] : [], |
| 97 | ); | 97 | ); |
| ... | @@ -119,11 +119,18 @@ export const buildRemoteOpenUrl = (input: { | ... | @@ -119,11 +119,18 @@ export const buildRemoteOpenUrl = (input: { |
| 119 | // Windows server paths (`C:\...`) appear as `/C:/...` in vscode-remote URIs. | 119 | // Windows server paths (`C:\...`) appear as `/C:/...` in vscode-remote URIs. |
| 120 | const posixPath = input.absolutePath.replaceAll("\\", "/"); | 120 | const posixPath = input.absolutePath.replaceAll("\\", "/"); |
| 121 | const rootedPath = posixPath.startsWith("/") ? posixPath : `/${posixPath}`; | 121 | const rootedPath = posixPath.startsWith("/") ? posixPath : `/${posixPath}`; |
| 122 | const encodedPath = rootedPath.split("/").map(encodeURIComponent).join("/"); | ||
| 123 | const encodedHost = encodeURIComponent(input.host); | 122 | const encodedHost = encodeURIComponent(input.host); |
| 124 | return input.editor === "zed" | 123 | if (input.editor === "zed") { |
| 125 | ? `${scheme}://ssh/${encodedHost}${encodedPath}` | 124 | // Zed's remote server resolves a rooted path on the system drive, so a |
| 126 | : `${scheme}://vscode-remote/ssh-remote+${encodedHost}${encodedPath}`; | 125 | // Windows `C:\Users\x` must become `/Users/x` (verified in #8938). Other |
| 126 | // drives are untested and kept as is rather than silently remapped, and a | ||
| 127 | // POSIX path that happens to start with `/C:` is left alone. | ||
| 128 | const zedPath = /^[Cc]:[\\/]/.test(input.absolutePath) ? rootedPath.slice(3) : rootedPath; | ||
| 129 | const encodedZedPath = zedPath.split("/").map(encodeURIComponent).join("/"); | ||
| 130 | return `${scheme}://ssh/${encodedHost}${encodedZedPath}`; | ||
| 131 | } | ||
| 132 | const encodedPath = rootedPath.split("/").map(encodeURIComponent).join("/"); | ||
| 133 | return `${scheme}://vscode-remote/ssh-remote+${encodedHost}${encodedPath}`; | ||
| 127 | }; | 134 | }; |
| 128 | 135 | ||
| 129 | /** | 136 | /** |