Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions apps/desktop/src/electron/ElectronShell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,19 @@ describe("ElectronShell", () => {
openExternalMock.mockResolvedValue(undefined);

const electronShell = yield* ElectronShell.ElectronShell;
const result = yield* electronShell.openExternal(
const urls = [
"vscode://vscode-remote/ssh-remote+example.com/home/user/project",
"vscode://vscode-remote/ssh-remote+example.com/home/user/my%20file%20%231.json:1",
];
for (const url of urls) {
const result = yield* electronShell.openExternal(url);

assert.equal(result, true);
}
assert.deepEqual(
openExternalMock.mock.calls,
urls.map((url) => [url]),
);

assert.equal(result, true);
assert.deepEqual(openExternalMock.mock.calls, [
["vscode://vscode-remote/ssh-remote+example.com/home/user/project"],
]);
}).pipe(Effect.provide(ElectronShell.layer)),
);

Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/chat/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,8 @@ export const ChatHeader = memo(function ChatHeader({
environmentId={activeThreadEnvironmentId}
keybindings={keybindings}
availableEditors={availableEditors}
openInCwd={openInCwd}
openInPath={openInCwd}
pathKind="folder"
/>
)}
{activeProjectName && (
Expand Down
22 changes: 13 additions & 9 deletions apps/web/src/components/chat/OpenInPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,16 @@ export const OpenInPicker = memo(function OpenInPicker({
environmentId,
keybindings,
availableEditors,
openInCwd,
openInPath,
pathKind,
compact = false,
enableShortcut = true,
}: {
environmentId: EnvironmentId;
keybindings: ResolvedKeybindingsConfig;
availableEditors: ReadonlyArray<EditorId>;
openInCwd: string | null;
openInPath: string | null;
pathKind: "file" | "folder";
compact?: boolean;
enableShortcut?: boolean;
}) {
Expand All @@ -205,15 +207,16 @@ export const OpenInPicker = memo(function OpenInPicker({

const openInEditor = useCallback(
(editorId: EditorId | null) => {
if (!openInCwd) return;
if (!openInPath) return;
const editor = editorId ?? preferredEditor;
if (!editor) return;
if (remote.mode === "remote-unavailable") return;
if (remote.mode === "remote-links") {
const url = buildRemoteOpenUrl({
editor,
host: remote.host.host,
absolutePath: openInCwd,
absolutePath: openInPath,
pathKind,
});
if (url === undefined) return;
// Only record hint-seen/preferred when the shell actually accepted
Expand All @@ -228,7 +231,7 @@ export const OpenInPicker = memo(function OpenInPicker({
const result = openInEditorMutation({
environmentId,
input: {
cwd: openInCwd,
cwd: openInPath,
editor,
},
});
Expand All @@ -238,8 +241,9 @@ export const OpenInPicker = memo(function OpenInPicker({
[
environmentId,
markRemoteHintSeen,
openInCwd,
openInPath,
openInEditorMutation,
pathKind,
preferredEditor,
remote,
setPreferredEditor,
Expand All @@ -255,15 +259,15 @@ export const OpenInPicker = memo(function OpenInPicker({
if (!enableShortcut) return;
const handler = (e: globalThis.KeyboardEvent) => {
if (!isOpenFavoriteEditorShortcut(e, keybindings)) return;
if (!openInCwd) return;
if (!openInPath) return;
if (!preferredEditor) return;

e.preventDefault();
void openInEditor(preferredEditor);
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [enableShortcut, keybindings, openInCwd, openInEditor, preferredEditor]);
}, [enableShortcut, keybindings, openInPath, openInEditor, preferredEditor]);

return (
<Group aria-label="Open in editor">
Expand All @@ -272,7 +276,7 @@ export const OpenInPicker = memo(function OpenInPicker({
className="ps-[8.5px]"
size="xs"
variant="outline"
disabled={!preferredEditor || !openInCwd || remote.mode === "remote-unavailable"}
disabled={!preferredEditor || !openInPath || remote.mode === "remote-unavailable"}
onClick={() => openInEditor(preferredEditor)}
>
{primaryOption?.Icon && (
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/files/FilePreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1140,7 +1140,8 @@ export default function FilePreviewPanel({
environmentId={environmentId}
keybindings={keybindings}
availableEditors={availableEditors}
openInCwd={absolutePath}
openInPath={absolutePath}
pathKind="file"
compact
enableShortcut={false}
/>
Expand Down
53 changes: 45 additions & 8 deletions apps/web/src/remoteOpen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,31 +119,68 @@ describe("resolveRemoteOpenState", () => {
});

describe("buildRemoteOpenUrl", () => {
it.each([
["/home/user/.local/share/app/settings.json", "/home/user/.local/share/app/settings.json"],
["/tmp/README", "/tmp/README"],
["/tmp/my file #1?.json", "/tmp/my%20file%20%231%3F.json"],
["C:\\Users\\user\\settings.json", "/C%3A/Users/user/settings.json"],
["/tmp/project.code-workspace", "/tmp/project.code-workspace"],
])("opens %s as a remote file", (absolutePath, encodedPath) => {
expect(
buildRemoteOpenUrl({
editor: "vscode",
host: "sol",
absolutePath,
pathKind: "file",
}),
).toBe(`vscode://vscode-remote/ssh-remote+sol${encodedPath}:1`);
});

it("builds a vscode-remote deep link", () => {
expect(
buildRemoteOpenUrl({
editor: "vscode",
host: "sol.tail1234.ts.net",
absolutePath: "/home/theo/code/my repo",
pathKind: "folder",
}),
).toBe("vscode://vscode-remote/ssh-remote+sol.tail1234.ts.net/home/theo/code/my%20repo");
});

it("uses the fork's scheme", () => {
expect(buildRemoteOpenUrl({ editor: "cursor", host: "sol", absolutePath: "/tmp/x" })).toBe(
"cursor://vscode-remote/ssh-remote+sol/tmp/x",
);
it.each(["cursor", "vscode-insiders", "vscodium"] as const)("uses %s's scheme", (editor) => {
expect(
buildRemoteOpenUrl({ editor, host: "sol", absolutePath: "/tmp/x", pathKind: "file" }),
).toBe(`${editor}://vscode-remote/ssh-remote+sol/tmp/x:1`);
expect(
buildRemoteOpenUrl({ editor, host: "sol", absolutePath: "/tmp/x", pathKind: "folder" }),
).toBe(`${editor}://vscode-remote/ssh-remote+sol/tmp/x`);
});

it("keeps folders with file extensions as folders", () => {
expect(
buildRemoteOpenUrl({
editor: "vscode",
host: "sol",
absolutePath: "/tmp/project.json",
pathKind: "folder",
}),
).toBe("vscode://vscode-remote/ssh-remote+sol/tmp/project.json");
});

it("roots Windows paths", () => {
expect(
buildRemoteOpenUrl({ editor: "vscode", host: "sol", absolutePath: "C:\\Users\\theo" }),
buildRemoteOpenUrl({
editor: "vscode",
host: "sol",
absolutePath: "C:\\Users\\theo",
pathKind: "folder",
}),
).toBe("vscode://vscode-remote/ssh-remote+sol/C%3A/Users/theo");
});

it("returns undefined for editors without remote support", () => {
expect(buildRemoteOpenUrl({ editor: "zed", host: "sol", absolutePath: "/tmp/x" })).toBe(
undefined,
);
expect(
buildRemoteOpenUrl({ editor: "zed", host: "sol", absolutePath: "/tmp/x", pathKind: "file" }),
).toBe(undefined);
});
});
5 changes: 4 additions & 1 deletion packages/contracts/src/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export const buildRemoteOpenUrl = (input: {
readonly editor: EditorId;
readonly host: string;
readonly absolutePath: string;
readonly pathKind: "file" | "folder";
}): string | undefined => {
const scheme = remoteSchemeForEditor(input.editor);
if (scheme === undefined) {
Expand All @@ -112,7 +113,9 @@ export const buildRemoteOpenUrl = (input: {
const posixPath = input.absolutePath.replaceAll("\\", "/");
const rootedPath = posixPath.startsWith("/") ? posixPath : `/${posixPath}`;
const encodedPath = rootedPath.split("/").map(encodeURIComponent).join("/");
return `${scheme}://vscode-remote/ssh-remote+${encodeURIComponent(input.host)}${encodedPath}`;
// A :line suffix makes VS Code's remote URL handler open a file instead of a folder.
const position = input.pathKind === "file" ? ":1" : "";
return `${scheme}://vscode-remote/ssh-remote+${encodeURIComponent(input.host)}${encodedPath}${position}`;
};

/**
Expand Down
Loading