feat: multi-project workspaces and project window groups (#448)

Workspaces open several local project folders in one IDE session (File › New / Open / Manage Workspace…, or Attach when opening a project, which creates a workspace automatically for a plain project window). Member folders are the Project view roots and take part in indexing, Find in Files, Git, language servers, run configurations and builds; agent tasks choose which member they work in. Window › Merge All Project Windows groups project windows into one window with project tabs (switch, reorder, close, move to a new window), restored on reopen.

Closes #241
Closes #198
Closes #286
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-30 22:45:42 +08:00
1 джерело 5bab140968
коміт 01defdefe4
88 files changed
+2689 -151

No files matched your search

+26
Переглянути файл
@@ -170,6 +170,32 @@ describe("EditorController", () => {
expect(pane().tabs.map((t) => t.input.path)).toEqual(["/p/a.ts", "/p/c.ts"]);
});
it("carries the plain project's editor tabs into its automatic workspace", async () => {
const settings = {};
useProject.setState({
project: { root: "/attach-a", name: "A", isGit: false, settings },
settings,
lightEdit: false,
});
ctrl = new EditorController(createMemoryBackend({ "/attach-a/one.txt": "one", "/attach-a/two.txt": "two" }));
ctrl.start();
await ctrl.openFile("/attach-a/one.txt", { focus: false, preview: false });
await ctrl.openFile("/attach-a/two.txt", { focus: false, preview: false });
useProject.setState({
project: {
root: "/attach-workspace",
name: "A",
isGit: false,
settings,
workspace: { version: 1, name: "A", projects: ["/attach-a", "/attach-b"] },
},
});
await vi.waitFor(() =>
expect(openFilePaths(useTabs.getState())).toEqual(["/attach-a/one.txt", "/attach-a/two.txt"]),
);
useProject.setState({ project: null });
});
it("reopens the LightEdit files the next time a window switches to LightEdit", async () => {
ctrl = new EditorController(createMemoryBackend({ "/x/a.txt": "a", "/y/b.md": "b" }));
ctrl.start();