дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
* 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]>
157 рядки
6.2 KiB
TypeScript
157 рядки
6.2 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
|
import type { RunConfig } from "@/platform/ipc";
|
|
import { useProject } from "@/platform/project";
|
|
import { useRemote } from "@/platform/remote";
|
|
import { validateSettings } from "@/features/settings/model/schema";
|
|
import { runServiceProvider } from "@/features/run/services";
|
|
import { useRun } from "@/features/run/store";
|
|
import { registerServiceProvider } from "@/workbench/contributions";
|
|
import { ServicesTreeView } from "./ServicesTree";
|
|
import { useServicesTree } from "./tree";
|
|
|
|
const config = (id: string, kind: string): RunConfig => ({
|
|
id,
|
|
name: id,
|
|
kind,
|
|
command: "java",
|
|
args: [],
|
|
cwd: null,
|
|
env: {},
|
|
detected: true,
|
|
isTest: false,
|
|
debugAdapter: null,
|
|
});
|
|
function project(root: string) {
|
|
useProject.setState({ project: { root, name: "app", trusted: true, isGit: false, settings: validateSettings({}) } });
|
|
}
|
|
let dispose: Array<() => void>;
|
|
const dockerRoots = [{ key: "docker:one", label: "Docker", icon: "file-docker" }];
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
project("/services");
|
|
useRemote.setState({ session: null });
|
|
useRun.setState({
|
|
configs: [
|
|
config("Gateway", "spring-boot"),
|
|
config("Portal", "spring-boot"),
|
|
config("Web", "npm"),
|
|
config("Worker", "npm"),
|
|
],
|
|
sessions: [],
|
|
});
|
|
localStorage.setItem("openidea.services.runTypes:/services", '["spring-boot","npm"]');
|
|
useServicesTree.setState({
|
|
roots: {},
|
|
selectedKey: null,
|
|
selectedKeys: new Set(),
|
|
expanded: new Set(["run:type:spring-boot", "run:type:npm"]),
|
|
});
|
|
dispose = [
|
|
registerServiceProvider(runServiceProvider),
|
|
registerServiceProvider({
|
|
id: "docker",
|
|
order: 10,
|
|
useRoots: () => dockerRoots,
|
|
}),
|
|
];
|
|
});
|
|
afterEach(() => {
|
|
dispose.forEach((f) => f());
|
|
useProject.setState({ project: null });
|
|
useRemote.setState({ session: null });
|
|
});
|
|
const tree = () => screen.getByRole("tree", { name: "Services" });
|
|
const names = () =>
|
|
within(tree())
|
|
.getAllByRole("treeitem")
|
|
.map((el) => el.textContent);
|
|
const select = (text: string) => fireEvent.mouseDown(within(tree()).getByText(text).closest('[role="treeitem"]')!);
|
|
|
|
describe("service reordering integration", () => {
|
|
it("moves detected configurations with accessible controls and retains order after reopening", () => {
|
|
const original = useRun.getState().configs;
|
|
const { unmount } = render(<ServicesTreeView />);
|
|
select("Portal");
|
|
fireEvent.click(screen.getByRole("button", { name: "Move Up" }));
|
|
expect(names()).toEqual(["Spring Boot", "Portal", "Gateway", "npm", "Web", "Worker", "Docker"]);
|
|
expect(useServicesTree.getState().selectedKey).toBe("run:config:Portal");
|
|
expect(useRun.getState().configs).toBe(original);
|
|
expect(screen.getByRole("button", { name: "Move Up" })).toBeDisabled();
|
|
unmount();
|
|
render(<ServicesTreeView />);
|
|
expect(names()).toEqual(["Spring Boot", "Portal", "Gateway", "npm", "Web", "Worker", "Docker"]);
|
|
});
|
|
|
|
it("uses the same ordering for npm without enabling groups or Docker", () => {
|
|
render(<ServicesTreeView />);
|
|
select("Worker");
|
|
fireEvent.click(screen.getByRole("button", { name: "Move Up" }));
|
|
expect(names()).toEqual(["Spring Boot", "Gateway", "Portal", "npm", "Worker", "Web", "Docker"]);
|
|
select("Docker");
|
|
expect(screen.queryByRole("button", { name: "Move Up" })).toBeNull();
|
|
select("Spring Boot");
|
|
expect(screen.queryByRole("button", { name: "Move Up" })).toBeNull();
|
|
});
|
|
|
|
it("preserves multi-selection without offering single-row moves", () => {
|
|
render(<ServicesTreeView />);
|
|
select("Portal");
|
|
fireEvent.mouseDown(within(tree()).getByText("Gateway").closest('[role="treeitem"]')!, { metaKey: true });
|
|
expect(useServicesTree.getState().selectedKeys.size).toBe(2);
|
|
expect(screen.queryByRole("button", { name: "Move Up" })).toBeNull();
|
|
expect(screen.queryByRole("button", { name: "Move Down" })).toBeNull();
|
|
expect(names()).toEqual(["Spring Boot", "Gateway", "Portal", "npm", "Web", "Worker", "Docker"]);
|
|
});
|
|
|
|
it("cancels an active drag when the project changes", () => {
|
|
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(function (this: HTMLElement) {
|
|
return new DOMRect(0, Number(this.dataset.row ?? 0) * 24, 400, this.dataset.row === undefined ? 600 : 24);
|
|
});
|
|
render(<ServicesTreeView />);
|
|
const source = within(tree()).getByText("Portal").closest('[role="treeitem"]')!;
|
|
const target = within(tree()).getByText("Gateway").closest('[role="treeitem"]')!;
|
|
Object.defineProperty(document, "elementFromPoint", { configurable: true, value: () => target });
|
|
const pointer = (el: Element | Window, type: string, y: number) =>
|
|
fireEvent(
|
|
el,
|
|
new MouseEvent(type, {
|
|
bubbles: true,
|
|
button: 0,
|
|
buttons: type === "pointerup" ? 0 : 1,
|
|
clientX: 70,
|
|
clientY: y,
|
|
}),
|
|
);
|
|
pointer(source, "pointerdown", 60);
|
|
pointer(window, "pointermove", 26);
|
|
expect(document.querySelector('[aria-hidden="true"][role="treeitem"]')).not.toBeNull();
|
|
act(() => project("/other"));
|
|
pointer(window, "pointerup", 26);
|
|
expect(document.querySelector('[aria-hidden="true"][role="treeitem"]')).toBeNull();
|
|
expect(localStorage.getItem("openidea.services.runOrder:/other")).toBeNull();
|
|
expect(localStorage.getItem("openidea.services.runOrder:/services")).toBeNull();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("isolates local and remote projects that share the same root", () => {
|
|
render(<ServicesTreeView />);
|
|
select("Portal");
|
|
fireEvent.click(screen.getByRole("button", { name: "Move Up" }));
|
|
act(() =>
|
|
useRemote.setState({
|
|
session: {
|
|
root: "/services",
|
|
target: { kind: "ssh", config: "host" },
|
|
host: "host",
|
|
os: "linux",
|
|
commands: [],
|
|
},
|
|
}),
|
|
);
|
|
expect(names()).toEqual(["Spring Boot", "Gateway", "Portal", "npm", "Web", "Worker", "Docker"]);
|
|
act(() => useRemote.setState({ session: null }));
|
|
expect(names()).toEqual(["Spring Boot", "Portal", "Gateway", "npm", "Web", "Worker", "Docker"]);
|
|
});
|
|
});
|