Files
openidea/src/workbench/projectWindow.ts
T
b0f65769c0 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]>
2026-11-05 15:37:19 +08:00

78 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** Where a project opens when this window already has one: System Settings › Project › "Open project in". */
import { t } from "@/platform/i18n";
import { call, commands, type RemoteTarget } from "@/platform/ipc";
import { confirmLeaveProject, useProject } from "@/platform/project";
import { connectRemote, isRemoteWindow } from "@/platform/remote";
import { ask } from "./messages";
import { maybeEditorService } from "./contributions";
/** "Open project in": `new` | `same` | `attach` | `ask`. */
export function openPolicy(): "ask" | "new" | "same" | "attach" {
const v = useProject.getState().settings?.general?.openProjectIn;
return v === "new" || v === "same" || v === "attach" ? v : "ask";
}
/**
* The window to open project `name` in: this one when it has no project (or LightEdit session), else
* per "Open project in", asking when set to (and remembering "Don't ask again").
* `null`: cancelled.
*/
export async function chooseProjectWindow(name: string, allowAttach = true): Promise<"new" | "same" | "attach" | null> {
const { project, lightEdit } = useProject.getState();
if (!project && !lightEdit) return "same";
const canAttach = allowAttach && !!project && !isRemoteWindow();
const policy = openPolicy();
const mode = policy === "attach" && !canAttach ? "ask" : policy;
if (mode !== "ask") return mode;
const r = await ask({
title: t("app.window.askTitle"),
width: canAttach ? 640 : undefined,
body: t("app.window.askBody", { name }),
buttons: [
{ id: "cancel", label: t("common.cancel") },
...(canAttach ? [{ id: "attach", label: t("app.window.askAttach") }] : []),
{ id: "new", label: t("app.window.askNew") },
{ id: "same", label: t("app.window.askThis"), primary: true },
],
checkbox: t("app.window.askRemember"),
});
if (r.button !== "same" && r.button !== "new" && r.button !== "attach") return null;
if (r.checked) {
try {
useProject.setState({
settings: await call(commands.settingsSet("global", "general.openProjectIn", JSON.stringify(r.button))),
});
} catch {
/* the choice still applies to this open */
}
}
return r.button;
}
/**
* Opens folder `path` of `target` (an SSH host or a WSL distribution): focuses the
* window that has it, else opens it in a new window or (`here`) this one per
* "Open project in". `false`: cancelled.
*/
export async function openRemoteProject(
target: RemoteTarget,
path: string,
here: () => Promise<unknown> = () => connectRemote(target, path),
): Promise<boolean> {
if ((await call(commands.windowOpenRemote(target, path, false))).kind === "focused") return true;
const mode = await chooseProjectWindow(path.replace(/\/+$/, "").split("/").pop() || path, false);
if (!mode) return false;
if (mode === "new") {
await call(commands.windowOpenRemote(target, path, true));
return true;
}
if (!(await confirmLeaveProject())) return false;
try {
await here();
return true;
} catch (e) {
maybeEditorService()?.cancelClose?.();
throw e;
}
}