дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +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
@@ -13,7 +13,13 @@ vi.mock("@/platform/tauriCore", async (original) => ({
|
||||
beforeEach(() => {
|
||||
useTasks.setState({ tasks: [] });
|
||||
useRemote.setState({
|
||||
session: { sshConfig: "s", root: "/remote", host: "host", commands: ["fs_copy", "cancel_request"] },
|
||||
session: {
|
||||
target: { kind: "ssh", config: "s" },
|
||||
root: "/remote",
|
||||
host: "host",
|
||||
os: "linux",
|
||||
commands: ["fs_copy", "cancel_request"],
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { MEMBER_PREFIX, type ProjectNode } from "./model";
|
||||
import {
|
||||
canPaste,
|
||||
checkEditingName,
|
||||
copySelection,
|
||||
importExternal,
|
||||
paste,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
import { IpcError } from "@/platform/ipc";
|
||||
import { closeFileRefactorDialog, pendingFileRefactorDialog } from "./FileRefactorDialogs";
|
||||
import { useRemote } from "@/platform/remote";
|
||||
import { usePlatform } from "@/platform/platformInfo";
|
||||
import { provideNotificationService } from "@/workbench/contributions";
|
||||
import { MemoryFs, setProjectFsForTests } from "./fs";
|
||||
import { initProjectTree, loadDir, setClipboard, setSelection, useProjectTree } from "./store";
|
||||
@@ -21,6 +23,30 @@ import { setOsClipboardForTests } from "./clipboard";
|
||||
let fs: MemoryFs;
|
||||
const notify = vi.fn();
|
||||
|
||||
describe("file names follow the execution host", () => {
|
||||
const platform = usePlatform.getState();
|
||||
afterEach(() => {
|
||||
usePlatform.setState(platform);
|
||||
useRemote.setState({ session: null });
|
||||
setProjectFsForTests(null);
|
||||
initProjectTree(null);
|
||||
});
|
||||
|
||||
it.each(["linux", "windows", "macos"])("validates names on a %s host from a Windows desktop", async (os) => {
|
||||
usePlatform.setState({ os: "windows" });
|
||||
useRemote.setState({
|
||||
session: { target: { kind: "ssh", config: "host" }, root: "/p", host: "host", os, commands: [] },
|
||||
});
|
||||
fs = new MemoryFs({ "/p/readme.txt": "" }, "/p");
|
||||
setProjectFsForTests(fs, "/p");
|
||||
initProjectTree("/p", "p");
|
||||
await loadDir("/p");
|
||||
useProjectTree.setState({ editing: { kind: "new", parent: "/p", nodeKind: "file" } });
|
||||
expect(checkEditingName("con").error !== null).toBe(os === "windows");
|
||||
expect(checkEditingName("README.txt").error !== null).toBe(os !== "linux");
|
||||
});
|
||||
});
|
||||
|
||||
const node = (path: string, dir = false): ProjectNode => ({
|
||||
key: path,
|
||||
path,
|
||||
@@ -102,7 +128,9 @@ describe("OS file imports", () => {
|
||||
setProjectFsForTests(fs, "/p");
|
||||
initProjectTree("/p", "p");
|
||||
await loadDir("/p");
|
||||
useRemote.setState({ session: { sshConfig: "ssh", root: "/p", host: "host", commands: ["fs_copy"] } });
|
||||
useRemote.setState({
|
||||
session: { target: { kind: "ssh", config: "ssh" }, root: "/p", host: "host", os: "linux", commands: ["fs_copy"] },
|
||||
});
|
||||
provideNotificationService({ notify });
|
||||
notify.mockClear();
|
||||
});
|
||||
@@ -292,7 +320,15 @@ describe("project clipboard ownership", () => {
|
||||
it.each(remoteCases)(
|
||||
"remote hand-off=$handedOff OS=$osList cut=$cut on $entry",
|
||||
async ({ handedOff, osList, cut, entry }) => {
|
||||
useRemote.setState({ session: { sshConfig: "ssh", root: "/p", host: "host", commands: ["fs_copy"] } });
|
||||
useRemote.setState({
|
||||
session: {
|
||||
target: { kind: "ssh", config: "ssh" },
|
||||
root: "/p",
|
||||
host: "host",
|
||||
os: "linux",
|
||||
commands: ["fs_copy"],
|
||||
},
|
||||
});
|
||||
const writeText = vi.fn(async () => {
|
||||
if (!handedOff) throw new Error("unavailable");
|
||||
});
|
||||
@@ -338,7 +374,9 @@ describe("project clipboard ownership", () => {
|
||||
);
|
||||
|
||||
it("writes multiple remote paths as newline-joined text", async () => {
|
||||
useRemote.setState({ session: { sshConfig: "ssh", root: "/p", host: "host", commands: ["fs_copy"] } });
|
||||
useRemote.setState({
|
||||
session: { target: { kind: "ssh", config: "ssh" }, root: "/p", host: "host", os: "linux", commands: ["fs_copy"] },
|
||||
});
|
||||
await fs.createFile("/p/second.txt");
|
||||
await loadDir("/p", true);
|
||||
setSelection(new Set(["/p/source.txt", "/p/second.txt"]), "/p/source.txt");
|
||||
@@ -350,7 +388,9 @@ describe("project clipboard ownership", () => {
|
||||
});
|
||||
|
||||
it("waits for a pending remote text write before reading file URLs", async () => {
|
||||
useRemote.setState({ session: { sshConfig: "ssh", root: "/p", host: "host", commands: ["fs_copy"] } });
|
||||
useRemote.setState({
|
||||
session: { target: { kind: "ssh", config: "ssh" }, root: "/p", host: "host", os: "linux", commands: ["fs_copy"] },
|
||||
});
|
||||
let finish!: () => void;
|
||||
vi.stubGlobal("navigator", {
|
||||
clipboard: {
|
||||
|
||||
@@ -39,17 +39,14 @@ import {
|
||||
useProjectTree,
|
||||
} from "./store";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { isRemoteWindow } from "@/platform/remote";
|
||||
import { hostOs, isRemoteWindow } from "@/platform/remote";
|
||||
import { usePlatform } from "@/platform/platformInfo";
|
||||
|
||||
/** Nodes that may be renamed, moved or deleted (not roots, groups or library sources). */
|
||||
const mutable = (n: ProjectNode) => !n.root && !n.placeholder && !n.readOnly;
|
||||
|
||||
const get = () => useProjectTree.getState();
|
||||
|
||||
const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
|
||||
const isWindows = /Win/i.test(ua);
|
||||
/** macOS and Windows file systems are case-insensitive by default. */
|
||||
const caseInsensitive = /Mac|Win/i.test(ua);
|
||||
const caseInsensitiveOs = (os: string) => os === "windows" || os === "macos";
|
||||
|
||||
function errorMessage(e: unknown): string {
|
||||
return e instanceof Error ? e.message : String(e);
|
||||
@@ -236,6 +233,9 @@ export function cancelEditing() {
|
||||
export function checkEditingName(name: string) {
|
||||
const { editing } = get();
|
||||
if (!editing) return { error: null };
|
||||
const os = hostOs();
|
||||
const isWindows = os === "windows";
|
||||
const caseInsensitive = caseInsensitiveOs(os);
|
||||
if (editing.kind === "new" && editing.pkg) {
|
||||
const { segments, error } = packageSegments(name, editing.pkg);
|
||||
if (!segments) return { error };
|
||||
@@ -645,6 +645,7 @@ async function pasteFromOsClipboard(paths: string[], target: string) {
|
||||
/** The same files, as the clipboard keeps them (`/`-separated, any order). */
|
||||
function sameFiles(files: string[], paths: string[] | undefined): boolean {
|
||||
if (!paths || files.length !== paths.length) return false;
|
||||
const caseInsensitive = caseInsensitiveOs(usePlatform.getState().os);
|
||||
const key = (list: string[]) =>
|
||||
list
|
||||
.map((p) => (caseInsensitive ? p.toLowerCase() : p))
|
||||
@@ -687,6 +688,7 @@ export async function transfer(
|
||||
if (!root) return false;
|
||||
const fs = projectFs(root);
|
||||
await loadDir(target);
|
||||
const caseInsensitive = caseInsensitiveOs(hostOs());
|
||||
const taken = new Set(siblingsOf(target).map((n) => (caseInsensitive ? n.toLowerCase() : n)));
|
||||
const hasName = (n: string) => taken.has(caseInsensitive ? n.toLowerCase() : n);
|
||||
const isTaken = (n: string) => hasName(n) || (isUnityAssetFolder(target) && hasName(`${n}.meta`));
|
||||
@@ -744,6 +746,7 @@ export async function transfer(
|
||||
async function moveInto(paths: string[], target: string): Promise<boolean> {
|
||||
paths = assetPaths(paths);
|
||||
await loadDir(target);
|
||||
const caseInsensitive = caseInsensitiveOs(hostOs());
|
||||
const taken = new Set(siblingsOf(target).map((n) => (caseInsensitive ? n.toLowerCase() : n)));
|
||||
const errors: string[] = [];
|
||||
const moves: { from: string; to: string }[] = [];
|
||||
|
||||
Reference in new issue
Заблокувати користувача