дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
feat(remote): reopen a local project in its dev container (#1843)
Refs #1083 (step B). A local project with a devcontainer.json can be reopened in its dev container (File › Reopen in Container, Find Action): the Dev Containers CLI brings the container up and the window connects to openidea-server inside it, as for WSL, so terminal, language servers, run, debug and git work in the container. The project's identity stays the local folder, so reopening from Recent Projects finds the same container. Missing CLI gives install guidance; trust of the local project carries over. Pure CLI output parsing lives in ide-remote; a failed connection no longer closes the current backend early, saved locations keep the container configuration, and Docker context precedence follows the CLI. Co-authored-by: andreabasile <[email protected]>
Цей коміт міститься в:
1 джерело
7479b7ad9d
коміт
60d7dcdca0
29 files changed
+908
-69
No files matched your search
+16
-6
@@ -1328,8 +1328,9 @@ export const commands = {
|
||||
/** Browse before connecting, using the desktop's SSH configuration even in a remote window. */
|
||||
remoteBootstrapList: (ssh: string, path: string) => typedError<RemoteEntry[], IdeError>(__TAURI_INVOKE("remote_bootstrap_list", { ssh, path })),
|
||||
/**
|
||||
* File › Remote Development: opens `path` on `target` (an SSH host or a WSL distribution)
|
||||
* in this window (installs / starts the server as needed).
|
||||
* File › Remote Development: opens `path` on `target` (an SSH host, a WSL distribution, or a
|
||||
* dev container, whose project folder is the container's own) in this window (installs /
|
||||
* starts the server as needed).
|
||||
*/
|
||||
remoteConnect: (target: RemoteTarget, path: string, onProgress: Channel<RemoteProgress>) => typedError<RemoteProject_Serialize, IdeError>(__TAURI_INVOKE("remote_connect", { target, path, onProgress })),
|
||||
/** The remote session of this window (a settings window: its parent's), if any. */
|
||||
@@ -6663,7 +6664,7 @@ export type RemoteEntry = {
|
||||
permissions: string,
|
||||
};
|
||||
|
||||
/** A project folder on an SSH host or in a WSL distribution. */
|
||||
/** A project folder on an SSH host, in a WSL distribution or in a dev container. */
|
||||
export type RemoteLaunch = {
|
||||
target: RemoteTarget,
|
||||
path: string,
|
||||
@@ -6738,14 +6739,21 @@ export type RemoteSession = {
|
||||
};
|
||||
|
||||
/** Where [`remote_connect`] is (`fraction`: 0..1 of a download / upload). */
|
||||
export type RemoteStage = "connecting" | "checking" | "downloading" | "uploading" | "starting" | "opening";
|
||||
export type RemoteStage = "connecting" |
|
||||
/** `devcontainer up`: building or starting the container. */
|
||||
"container" | "checking" | "downloading" | "uploading" | "starting" | "opening";
|
||||
|
||||
/** Where a remote project lives and how the server there is reached. */
|
||||
export type RemoteTarget =
|
||||
/** The host of SSH configuration `config`. */
|
||||
{ kind: "ssh"; config: string } |
|
||||
/** A WSL distribution on this machine. */
|
||||
{ kind: "wsl"; distro: string };
|
||||
{ kind: "wsl"; distro: string } |
|
||||
/**
|
||||
* The dev container of local folder `folder`, built and started with the Dev Containers CLI;
|
||||
* `config` is its `devcontainer.json`, relative to the folder (empty = the default one).
|
||||
*/
|
||||
{ kind: "devContainer"; folder: string; config: string };
|
||||
|
||||
export type Renamed = {
|
||||
edit: WorkspaceEdit,
|
||||
@@ -7947,7 +7955,9 @@ export type ToolInfo = {
|
||||
|
||||
export type ToolKind = "languageServer" | "debugger" |
|
||||
/** Loaded into another tool's process, e.g. a jdtls agent. */
|
||||
"plugin" | "formatter";
|
||||
"plugin" | "formatter" |
|
||||
/** A command the IDE itself runs, e.g. the Dev Containers CLI. */
|
||||
"cli";
|
||||
|
||||
export type ToolSettings = {
|
||||
/** Executable to launch instead of the managed / `PATH` one; empty = auto-detect. */
|
||||
|
||||
@@ -4,9 +4,15 @@ import type { RemoteSession } from "@/platform/ipc";
|
||||
import { useRemote } from "@/platform/remote";
|
||||
import { BarButton } from "@/ui";
|
||||
|
||||
const BADGE: Record<RemoteSession["target"]["kind"], string> = {
|
||||
ssh: "SSH",
|
||||
wsl: "WSL",
|
||||
devContainer: "Dev Container",
|
||||
};
|
||||
|
||||
/** Status bar label of a remote window: where its project runs. */
|
||||
export function remoteBadge(session: Pick<RemoteSession, "target" | "host">): string {
|
||||
return `${session.target.kind === "wsl" ? "WSL" : "SSH"}: ${session.host}`;
|
||||
return `${BADGE[session.target.kind]}: ${session.host}`;
|
||||
}
|
||||
|
||||
export function RemoteStatus() {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { beforeEach, expect, it, vi } from "vitest";
|
||||
import { IpcError } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { useRemote } from "@/platform/remote";
|
||||
|
||||
const mocks = vi.hoisted(() => ({ open: vi.fn(), notify: vi.fn() }));
|
||||
vi.mock("@/workbench/projectWindow", () => ({ openRemoteProject: mocks.open }));
|
||||
vi.mock("@/workbench/contributions", () => ({ notifications: () => ({ notify: mocks.notify }) }));
|
||||
|
||||
import { reopenInContainer } from "./container";
|
||||
|
||||
const project = (root: string) => ({ root, name: "app", trusted: true }) as never;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useRemote.setState({ session: null });
|
||||
useProject.setState({ project: project("/Users/me/app") });
|
||||
});
|
||||
|
||||
it("opens the local project's dev container", async () => {
|
||||
mocks.open.mockResolvedValue(true);
|
||||
await reopenInContainer();
|
||||
expect(mocks.open).toHaveBeenCalledWith(
|
||||
{ kind: "devContainer", folder: "/Users/me/app", config: "" },
|
||||
"/Users/me/app",
|
||||
);
|
||||
expect(mocks.notify).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing in a remote window or without a project", async () => {
|
||||
useRemote.setState({
|
||||
session: { target: { kind: "ssh", config: "a" }, root: "/srv/app", host: "a", os: "linux", commands: [] },
|
||||
});
|
||||
await reopenInContainer();
|
||||
useRemote.setState({ session: null });
|
||||
useProject.setState({ project: null });
|
||||
await reopenInContainer();
|
||||
expect(mocks.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports why the container could not be opened, but not a cancel", async () => {
|
||||
mocks.open.mockRejectedValueOnce(new Error("The Dev Containers CLI is not installed."));
|
||||
await reopenInContainer();
|
||||
expect(mocks.notify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ level: "error", body: "The Dev Containers CLI is not installed." }),
|
||||
);
|
||||
mocks.notify.mockClear();
|
||||
mocks.open.mockRejectedValueOnce(new IpcError({ kind: "cancelled", message: "Project opening cancelled" }));
|
||||
await reopenInContainer();
|
||||
expect(mocks.notify).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
/** Reopen in Container: the local project's dev container, opened as a remote project. */
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { isCancelled } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { isRemoteWindow } from "@/platform/remote";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { openRemoteProject } from "@/workbench/projectWindow";
|
||||
|
||||
/**
|
||||
* Builds or starts the dev container of this window's local project (its `devcontainer.json`)
|
||||
* and opens the project there. The folder must be trusted: building it runs the project's commands.
|
||||
*/
|
||||
export async function reopenInContainer(): Promise<void> {
|
||||
const project = useProject.getState().project;
|
||||
if (!project || isRemoteWindow()) return;
|
||||
try {
|
||||
await openRemoteProject({ kind: "devContainer", folder: project.root, config: "" }, project.root);
|
||||
} catch (e) {
|
||||
if (isCancelled(e)) return;
|
||||
notifications().notify({
|
||||
group: "remote",
|
||||
level: "error",
|
||||
title: tx("Could not reopen the project in a container"),
|
||||
body: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Remote development: a window whose project lives on an SSH host or in a WSL distribution. The backend
|
||||
* (`openidea-server`) runs there and this window routes its commands to it
|
||||
* (platform/remote.ts).
|
||||
* Remote development: a window whose project lives on an SSH host, in a WSL distribution or in a
|
||||
* dev container. The backend (`openidea-server`) runs there and this window routes its commands
|
||||
* to it (platform/remote.ts).
|
||||
*/
|
||||
import { registerActions } from "@/platform/actions";
|
||||
import { disconnectRemote, useRemote } from "@/platform/remote";
|
||||
@@ -9,6 +9,7 @@ import { lazyComponent } from "@/ui";
|
||||
import { maybeEditorService, registerOverlay, registerStatusItem, registerToolWindow } from "@/workbench/contributions";
|
||||
import { confirmLeaveProject } from "@/platform/project";
|
||||
import { ConnectHost } from "./ConnectDialog";
|
||||
import { reopenInContainer } from "./container";
|
||||
import { reloadEnvironment } from "./environment";
|
||||
import { RemoteStatus } from "./RemoteStatus";
|
||||
import { closeConnectDialog, openConnectDialog } from "./model";
|
||||
@@ -80,6 +81,17 @@ export function activate(): () => void {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "remote.reopenInContainer",
|
||||
title: "Reopen in Container",
|
||||
description:
|
||||
"Builds or starts the project's dev container (devcontainer.json) with the Dev Containers CLI and opens the project in it.",
|
||||
category: "File",
|
||||
icon: "folder",
|
||||
keywords: ["devcontainer", "dev container", "docker", "container", "remote"],
|
||||
when: "projectOpen",
|
||||
run: () => reopenInContainer(),
|
||||
},
|
||||
{
|
||||
id: "remote.reloadEnvironment",
|
||||
title: "Reload Environment",
|
||||
|
||||
@@ -39,6 +39,8 @@ export function stageLabel(stage: RemoteStage): string {
|
||||
switch (stage) {
|
||||
case "connecting":
|
||||
return "Connecting to the host…";
|
||||
case "container":
|
||||
return "Starting the dev container…";
|
||||
case "checking":
|
||||
return "Checking the IDE backend on the host…";
|
||||
case "downloading":
|
||||
|
||||
@@ -14,7 +14,7 @@ vi.mock("@/platform/ipc", async (importOriginal) => ({
|
||||
}));
|
||||
|
||||
import { PortsView } from "./PortsView";
|
||||
import { parsePort, usePorts } from "./ports";
|
||||
import { forwardPort, parsePort, usePorts } from "./ports";
|
||||
|
||||
ipc.remoteForwardedPorts.mockImplementation(async () => [...forwarded]);
|
||||
ipc.remoteForwardPort.mockImplementation(async (remotePort: number) => {
|
||||
@@ -86,3 +86,20 @@ describe("restoring forwarded ports", () => {
|
||||
useRemote.setState({ session: null });
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't forward ports of a dev container yet", async () => {
|
||||
const { useRemote } = await import("@/platform/remote");
|
||||
ipc.remoteForwardPort.mockClear();
|
||||
useRemote.setState({
|
||||
session: {
|
||||
target: { kind: "devContainer", folder: "/w/app", config: "" },
|
||||
root: "/workspaces/app",
|
||||
host: "app",
|
||||
os: "linux",
|
||||
commands: [],
|
||||
},
|
||||
});
|
||||
expect(await forwardPort(8080)).toBeNull();
|
||||
expect(ipc.remoteForwardPort).not.toHaveBeenCalled();
|
||||
useRemote.setState({ session: null });
|
||||
});
|
||||
@@ -53,11 +53,15 @@ export function parsePort(text: string): number | null {
|
||||
}
|
||||
|
||||
export async function forwardPort(port: number): Promise<ForwardedPort | null> {
|
||||
if (useRemote.getState().session?.target.kind === "wsl") {
|
||||
const kind = useRemote.getState().session?.target.kind;
|
||||
if (kind === "wsl" || kind === "devContainer") {
|
||||
notifications().notify({
|
||||
group: "remote",
|
||||
level: "error",
|
||||
title: tx("Forwarding ports is not supported in a WSL project yet"),
|
||||
title:
|
||||
kind === "wsl"
|
||||
? tx("Forwarding ports is not supported in a WSL project yet")
|
||||
: tx("Forwarding ports is not supported in a dev container yet"),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ describe("remote folder browser", () => {
|
||||
|
||||
const ssh = (config: string) => ({ kind: "ssh", config }) as const;
|
||||
const wsl = (distro: string) => ({ kind: "wsl", distro }) as const;
|
||||
const container = (folder: string) => ({ kind: "devContainer", folder, config: "" }) as const;
|
||||
|
||||
describe("remote project locations", () => {
|
||||
it("round-trip the configuration id and the path", () => {
|
||||
@@ -66,6 +67,20 @@ describe("remote project locations", () => {
|
||||
expect(parseRemoteLocation("wsl://Ubuntu")).toBeNull();
|
||||
});
|
||||
|
||||
it("round-trip a dev container's local folder and the path in the container", () => {
|
||||
const loc = remoteLocation(container("/Users/me/my app"), "/workspaces/my app");
|
||||
expect(loc).toBe("devcontainer://%2FUsers%2Fme%2Fmy%20app/workspaces/my app");
|
||||
expect(parseRemoteLocation(loc)).toEqual({ target: container("/Users/me/my app"), path: "/workspaces/my app" });
|
||||
});
|
||||
|
||||
it("round-trips container configurations and Windows folders with reserved characters", () => {
|
||||
const target = { ...container("C:\\work\\my app"), config: ".devcontainer/a:b?c/devcontainer.json" };
|
||||
expect(parseRemoteLocation(remoteLocation(target, "/workspaces/app"))).toEqual({
|
||||
target,
|
||||
path: "/workspaces/app",
|
||||
});
|
||||
});
|
||||
|
||||
it("remembers recent remote projects most recent first, once each", () => {
|
||||
localStorage.clear();
|
||||
rememberRemote({ target: ssh("a"), path: "/p", name: "p", host: "me@a", openedAtMs: 1 });
|
||||
@@ -85,6 +100,19 @@ describe("remote project locations", () => {
|
||||
expect(recentRemotes().map((r) => r.target)).toEqual([ssh("dev")]);
|
||||
});
|
||||
|
||||
it("keeps the dev containers of two configurations of a folder apart", () => {
|
||||
localStorage.clear();
|
||||
const python = { ...container("/w/app"), config: ".devcontainer/python/devcontainer.json" };
|
||||
rememberRemote({ target: container("/w/app"), path: "/workspaces/app", name: "app", host: "app", openedAtMs: 1 });
|
||||
rememberRemote({ target: python, path: "/workspaces/app", name: "app", host: "app", openedAtMs: 2 });
|
||||
expect(recentRemotes()).toHaveLength(2);
|
||||
const saved = remoteLocation(python, "/workspaces/app");
|
||||
expect(saved).not.toBe(remoteLocation(container("/w/app"), "/workspaces/app"));
|
||||
expect(parseRemoteLocation(saved)).toEqual({ target: python, path: "/workspaces/app" });
|
||||
forgetRemote(parseRemoteLocation(saved)!.target, "/workspaces/app");
|
||||
expect(recentRemotes().map((r) => r.target)).toEqual([container("/w/app")]);
|
||||
});
|
||||
|
||||
it("reads recents saved before WSL support, which carried `sshConfig`", () => {
|
||||
localStorage.clear();
|
||||
localStorage.setItem(
|
||||
@@ -123,4 +151,5 @@ it("migrates a snapshot taken before WSL support, and never trusts a WSL distrib
|
||||
it("labels the status bar with the transport and where the project runs", () => {
|
||||
expect(remoteBadge({ target: wsl("Ubuntu"), host: "Ubuntu" })).toBe("WSL: Ubuntu");
|
||||
expect(remoteBadge({ target: ssh("a"), host: "me@a:22" })).toBe("SSH: me@a:22");
|
||||
expect(remoteBadge({ target: container("/w/app"), host: "app" })).toBe("Dev Container: app");
|
||||
});
|
||||
@@ -40,6 +40,7 @@ const KIND_LABEL: Record<ToolInfo["kind"], string> = {
|
||||
debugger: "Debugger",
|
||||
plugin: "Plugin",
|
||||
formatter: "Formatter",
|
||||
cli: "Command-line tool",
|
||||
};
|
||||
|
||||
export function filterTools(tools: readonly ToolInfo[], query: string, filter: Filter): ToolInfo[] {
|
||||
|
||||
@@ -172,6 +172,7 @@
|
||||
"menu.file.newProjectsSettings": "Settings for New Projects…",
|
||||
"menu.file.toggleReadOnly": "Make File Read-only / Writable",
|
||||
"menu.file.closeProject": "Close Project",
|
||||
"menu.remote.reopenInContainer": "Reopen in Container",
|
||||
"menu.remote.reloadEnvironment": "Reload Environment",
|
||||
"menu.file.new": "New…",
|
||||
"menu.file.newScratchFile": "New Scratch File…",
|
||||
|
||||
@@ -201,5 +201,12 @@
|
||||
"Only Python can be debugged in containers": "Only Python can be debugged in containers",
|
||||
"Docker connections come from the Services tool window. Each run gets a new container, removed when it ends. Python configurations can be debugged there; coverage and profiling stay on this machine.": "Docker connections come from the Services tool window. Each run gets a new container, removed when it ends. Python configurations can be debugged there; coverage and profiling stay on this machine.",
|
||||
"Use Project JDK": "Use Project JDK",
|
||||
"JDK used for Maven sync, builds, goals and tests. An explicit JAVA_HOME in a run configuration takes precedence.": "JDK used for Maven sync, builds, goals and tests. An explicit JAVA_HOME in a run configuration takes precedence."
|
||||
"JDK used for Maven sync, builds, goals and tests. An explicit JAVA_HOME in a run configuration takes precedence.": "JDK used for Maven sync, builds, goals and tests. An explicit JAVA_HOME in a run configuration takes precedence.",
|
||||
"Starting the dev container…": "Starting the dev container…",
|
||||
"Command-line tool": "Command-line tool",
|
||||
"Forwarding ports is not supported in a dev container yet": "Forwarding ports is not supported in a dev container yet",
|
||||
"Could not reopen the project in a container": "Could not reopen the project in a container",
|
||||
"Reopen in Container": "Reopen in Container",
|
||||
"Builds or starts the project's dev container (devcontainer.json) with the Dev Containers CLI and opens the project in it.": "Builds or starts the project's dev container (devcontainer.json) with the Dev Containers CLI and opens the project in it.",
|
||||
"devcontainer up exited with {status}": "devcontainer up exited with {status}"
|
||||
}
|
||||
@@ -6541,6 +6541,12 @@
|
||||
"Where updates are downloaded from. If it can't be reached, the other source is tried.": "从哪里下载更新。无法连接时会改用另一个源。",
|
||||
"CNB (China mirror)": "CNB(国内镜像)",
|
||||
"Remote connection cancelled": "远程连接已取消",
|
||||
"Starting the dev container…": "正在启动开发容器…",
|
||||
"Command-line tool": "命令行工具",
|
||||
"Forwarding ports is not supported in a dev container yet": "开发容器暂不支持端口转发",
|
||||
"Could not reopen the project in a container": "无法在容器中重新打开项目",
|
||||
"Reopen in Container": "在容器中重新打开",
|
||||
"Builds or starts the project's dev container (devcontainer.json) with the Dev Containers CLI and opens the project in it.": "使用 Dev Containers CLI 构建或启动项目的开发容器(devcontainer.json),并在其中打开项目。",
|
||||
"Xdebug is not loaded in PHP {version}": "PHP {version} 未加载 Xdebug",
|
||||
"{php} runs without Xdebug, so breakpoints are skipped. Install Xdebug 3 for this PHP or choose another PHP interpreter.": "{php} 运行时没有 Xdebug,断点会被跳过。请为此 PHP 安装 Xdebug 3,或选择其他 PHP 解释器。",
|
||||
"Open PHP Settings": "打开 PHP 设置",
|
||||
@@ -6579,5 +6585,6 @@
|
||||
"The container exited before debugpy was ready. Check the Run console for errors.": "容器在 debugpy 就绪前已退出。请检查运行控制台中的错误。",
|
||||
"debugpy did not become ready within 120 seconds. Check the Run console and the container's Python interpreter.": "debugpy 未在 120 秒内就绪。请检查运行控制台和容器的 Python 解释器。",
|
||||
"Use Project JDK": "使用项目 SDK",
|
||||
"JDK used for Maven sync, builds, goals and tests. An explicit JAVA_HOME in a run configuration takes precedence.": "用于 Maven 同步、构建、目标和测试的 JDK。运行配置中显式设置的 JAVA_HOME 优先。"
|
||||
"JDK used for Maven sync, builds, goals and tests. An explicit JAVA_HOME in a run configuration takes precedence.": "用于 Maven 同步、构建、目标和测试的 JDK。运行配置中显式设置的 JAVA_HOME 优先。",
|
||||
"devcontainer up exited with {status}": "devcontainer up 已退出,状态为 {status}"
|
||||
}
|
||||
@@ -172,6 +172,7 @@
|
||||
"menu.file.newProjectsSettings": "新项目的设置…",
|
||||
"menu.file.toggleReadOnly": "将文件设为只读/可写",
|
||||
"menu.file.closeProject": "关闭项目",
|
||||
"menu.remote.reopenInContainer": "在容器中重新打开",
|
||||
"menu.remote.reloadEnvironment": "重新加载环境",
|
||||
"menu.file.new": "新建…",
|
||||
"menu.file.newScratchFile": "新建临时文件…",
|
||||
|
||||
@@ -154,7 +154,10 @@ export const useRemoteProgress = create<{ progress: RemoteProgress | null; locat
|
||||
location: null,
|
||||
}));
|
||||
|
||||
/** Opens `path` of `target` (an SSH host or a WSL distribution) in this window. */
|
||||
/**
|
||||
* Opens `path` of `target` (an SSH host or a WSL distribution) in this window. A dev container's
|
||||
* project folder is the one in the container: `path` is then only a label.
|
||||
*/
|
||||
export async function connectRemote(target: RemoteTarget, path: string) {
|
||||
useProject.setState({ opening: true });
|
||||
const location = remoteLocation(target, path);
|
||||
@@ -214,23 +217,35 @@ export async function disconnectRemote() {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Remote project locations: `ssh://<configuration id>/<absolute path>` or
|
||||
// `wsl://<distribution>/<absolute path>`, what the session and the recent projects
|
||||
// remember for a remote window.
|
||||
// Remote project locations: `ssh://<configuration id>/<absolute path>`,
|
||||
// `wsl://<distribution>/<absolute path>` or `devcontainer://<local folder>[:<config>]/<container path>`,
|
||||
// what the session and the recent projects remember for a remote window.
|
||||
|
||||
const SCHEMES = { ssh: "ssh://", wsl: "wsl://" } as const;
|
||||
const SCHEMES = { ssh: "ssh://", wsl: "wsl://", devContainer: "devcontainer://" } as const;
|
||||
|
||||
/** The SSH configuration id or the WSL distribution name. */
|
||||
/** The SSH configuration id, the WSL distribution name or the dev container's local folder. */
|
||||
export function targetName(target: RemoteTarget): string {
|
||||
return target.kind === "ssh" ? target.config : target.distro;
|
||||
switch (target.kind) {
|
||||
case "ssh":
|
||||
return target.config;
|
||||
case "wsl":
|
||||
return target.distro;
|
||||
case "devContainer":
|
||||
return target.folder;
|
||||
}
|
||||
}
|
||||
|
||||
export function sameTarget(a: RemoteTarget, b: RemoteTarget): boolean {
|
||||
return a.kind === b.kind && targetName(a) === targetName(b);
|
||||
return (
|
||||
a.kind === b.kind &&
|
||||
targetName(a) === targetName(b) &&
|
||||
(a.kind !== "devContainer" || b.kind !== "devContainer" || a.config === b.config)
|
||||
);
|
||||
}
|
||||
|
||||
export function remoteLocation(target: RemoteTarget, path: string): string {
|
||||
return `${SCHEMES[target.kind]}${encodeURIComponent(targetName(target))}${path.startsWith("/") ? "" : "/"}${path}`;
|
||||
const config = target.kind === "devContainer" && target.config ? `:${encodeURIComponent(target.config)}` : "";
|
||||
return `${SCHEMES[target.kind]}${encodeURIComponent(targetName(target))}${config}${path.startsWith("/") ? "" : "/"}${path}`;
|
||||
}
|
||||
|
||||
export function parseRemoteLocation(location: string): { target: RemoteTarget; path: string } | null {
|
||||
@@ -239,8 +254,12 @@ export function parseRemoteLocation(location: string): { target: RemoteTarget; p
|
||||
const rest = location.slice(SCHEMES[kind].length);
|
||||
const slash = rest.indexOf("/");
|
||||
if (slash <= 0) return null;
|
||||
const name = decodeURIComponent(rest.slice(0, slash));
|
||||
const target: RemoteTarget = kind === "ssh" ? { kind, config: name } : { kind, distro: name };
|
||||
const authority = rest.slice(0, slash);
|
||||
const colon = kind === "devContainer" ? authority.indexOf(":") : -1;
|
||||
const name = decodeURIComponent(colon < 0 ? authority : authority.slice(0, colon));
|
||||
const config = colon < 0 ? "" : decodeURIComponent(authority.slice(colon + 1));
|
||||
const target: RemoteTarget =
|
||||
kind === "ssh" ? { kind, config: name } : kind === "wsl" ? { kind, distro: name } : { kind, folder: name, config };
|
||||
return { target, path: rest.slice(slash) };
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Заблокувати користувача