дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(remote): open WSL projects through a WSL transport (#989)
* perf(vfs): list directories off the main thread and reuse the repository lookup Opening a folder on a network share (\\wsl.localhost\<distro>\...) is slow because every listing ran on the main thread and walked up to the repository root and ran a repository discovery again. - fs_read_dir runs on the blocking pool. - GitignoreStack::cached keeps the repository root and the repo-level rules for a few seconds. An edit of .git/info/exclude is still seen at once (its mtime and size are checked) and every .gitignore / .ignore is read fresh. Refs #916 * feat(remote): open WSL projects through a WSL transport A folder of a WSL distro (\\wsl.localhost\<distro>\... or \\wsl$\...) now opens as a remote project whose openidea-server runs inside the distro, so the terminal, SDK discovery, run, language servers and git are native Linux. - RemoteTarget (ssh | wsl) replaces the SSH configuration id in the remote launch, connection, session and recents; old recents are read as SSH and a WSL target never inherits the legacy trust migration. - remote_wsl: probe the distro, stream the server into ~/.openidea/server and start it with `wsl.exe -d <distro> --exec <bin> --stdio`. Distro names are validated and every command goes through --exec with positional arguments. - wsl_distros / wsl_resolve commands; File > Open, the CLI and recents route WSL folders to the distro and focus the window that already has them. - Remote Development dialog: SSH/WSL choice with the detected distros; the status bar shows `WSL: <distro>` / `SSH: <host>`. - ide-jsonrpc: skip the NUL left at the start of a line by UTF-16 output of wsl.exe, which made the first message of the server disappear. Not run on Windows yet: only a stand-in for wsl.exe is tested. Refs #925, #327, #721 * fix(remote): satisfy clippy in WSL output decoding * style(run): format run types * fix: regenerate bindings and drop untranslated WSL catalog entry * test(i18n): list WSL as an untranslated product name * fix(run): pick build wrappers and shell quoting by the execution host A remote window runs commands on its Linux or macOS server, so a Windows desktop no longer selects gradlew.bat, mvnw.cmd or Windows quoting for it. * fix(welcome): open legacy WSL UNC recents and sessions through the distro Recents and the restored session called the local openProject for a \\wsl.localhost folder saved before WSL projects, which fails with OS error 123. They now resolve it like the other entry points. * fix(windows): keep WSL files out of remote project launches path_launch tested the WSL share before is_dir, so a first-instance CLI launch with a WSL file opened a remote project instead of LightEdit. * fix(remote): run-command OS comes from the remote host, not an assumption The server's OS was dropped after connect, so a restored session had none and isWindows() assumed every remote host is Unix. Keep it on the connection and the session, and resolve it through hostOs(). --------- Co-authored-by: NguyenBang <[email protected]> Co-authored-by: NguyenBang <[email protected]> Co-authored-by: l0ng-ai <[email protected]>
Цей коміт міститься в:
47 files changed
+1512
-220
No files matched your search
@@ -6,11 +6,13 @@ const projectCurrent = vi.fn();
|
||||
const windowOpenProject = vi.fn();
|
||||
const windowOpenFiles = vi.fn();
|
||||
const windowTakeLaunch = vi.fn();
|
||||
const wslResolve = vi.fn();
|
||||
const cancelClose = vi.fn();
|
||||
const notify = vi.fn();
|
||||
vi.mock("@/platform/remote", async (original) => ({
|
||||
...(await original<typeof import("@/platform/remote")>()),
|
||||
isRemoteWindow: vi.fn(() => false),
|
||||
connectRemote: vi.fn(async () => {}),
|
||||
}));
|
||||
// The backend answers with the global settings, here: whatever the test set up.
|
||||
const lightEditOpen = vi.fn(async (_path: string) => ({ status: "ok", data: useProject.getState().settings ?? {} }));
|
||||
@@ -34,6 +36,7 @@ vi.mock("@/platform/ipc", async (orig) => {
|
||||
windowOpenProject: (...a: unknown[]) => windowOpenProject(...a),
|
||||
windowOpenFiles: (...a: unknown[]) => windowOpenFiles(...a),
|
||||
windowTakeLaunch: () => windowTakeLaunch(),
|
||||
wslResolve: (p: string) => wslResolve(p),
|
||||
lightEditOpen: (p: string) => lightEditOpen(p),
|
||||
fsReadDir: (p: string) => fsReadDir(p),
|
||||
},
|
||||
@@ -55,7 +58,7 @@ const {
|
||||
} = await import("../windows");
|
||||
const { useProject } = await import("@/platform/project");
|
||||
const { provideEditorService, provideNotificationService } = await import("@/workbench/contributions");
|
||||
const { isRemoteWindow } = await import("@/platform/remote");
|
||||
const { isRemoteWindow, connectRemote } = await import("@/platform/remote");
|
||||
const { IpcError } = await import("@/platform/ipc");
|
||||
provideNotificationService({ notify });
|
||||
|
||||
@@ -77,7 +80,9 @@ const ok = (data: unknown) => ({ status: "ok", data });
|
||||
const projectState = (root: string) => ({ project: { root, name: root, trusted: true, isGit: false, settings: {} } });
|
||||
|
||||
beforeEach(() => {
|
||||
for (const f of [windowOpenProject, windowOpenFiles, windowTakeLaunch]) f.mockReset();
|
||||
for (const f of [windowOpenProject, windowOpenFiles, windowTakeLaunch, wslResolve]) f.mockReset();
|
||||
wslResolve.mockResolvedValue(null);
|
||||
vi.mocked(connectRemote).mockClear();
|
||||
for (const f of [lightEditOpen, fsReadDir, openProject]) f.mockClear();
|
||||
cancelClose.mockClear();
|
||||
notify.mockClear();
|
||||
@@ -95,6 +100,18 @@ describe("openProjectInWindow", () => {
|
||||
expect(openProject).toHaveBeenCalledWith("/p/a");
|
||||
});
|
||||
|
||||
it("connects to the distribution when the folder is on the WSL share", async () => {
|
||||
const [distro, linuxPath] = ["Debian", "/srv/app"];
|
||||
const share = `\\\\wsl.localhost\\${distro}${linuxPath.replaceAll("/", "\\")}`;
|
||||
const target = { kind: "wsl", distro };
|
||||
wslResolve.mockResolvedValue({ target, path: linuxPath });
|
||||
windowOpenProject.mockResolvedValue(ok({ kind: "current" }));
|
||||
await openProjectInWindow(share, false);
|
||||
expect(wslResolve).toHaveBeenCalledWith(share);
|
||||
expect(connectRemote).toHaveBeenCalledWith(target, linuxPath);
|
||||
expect(openProject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("asks for a new window when this one already has a project", async () => {
|
||||
useProject.setState(projectState("/p/b") as never);
|
||||
windowOpenProject.mockResolvedValue(ok({ kind: "opened", label: "project-1" }));
|
||||
|
||||
@@ -53,7 +53,10 @@ async function openHere(path: string): Promise<boolean> {
|
||||
if (!isRemoteWindow() && useProject.getState().project?.root === path) return true;
|
||||
if (!(await confirmLeaveProject())) return false;
|
||||
try {
|
||||
await openProject(path);
|
||||
// A folder of a WSL distro runs there, not as a network folder.
|
||||
const wsl = await commands.wslResolve(path).catch(() => null);
|
||||
if (wsl) await connectRemote(wsl.target, wsl.path);
|
||||
else await openProject(path);
|
||||
return true;
|
||||
} catch (e) {
|
||||
maybeEditorService()?.cancelClose?.();
|
||||
@@ -219,8 +222,8 @@ export async function consumeLaunch(): Promise<void> {
|
||||
if (await openHere(launch.project)) await openInEditor(files, positions);
|
||||
} else if (files.length) await openFilesHere(files, positions);
|
||||
else if (launch?.remote) {
|
||||
const { sshConfig, path } = launch.remote;
|
||||
await connectRemote(sshConfig, path).catch((e) =>
|
||||
const { target, path } = launch.remote;
|
||||
await connectRemote(target, path).catch((e) =>
|
||||
notifications().notify({
|
||||
level: "error",
|
||||
title: t("app.window.openFailed", { name: basename(path) }),
|
||||
|
||||
Reference in new issue
Заблокувати користувача