дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
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
Цей коміт міститься в:
1 джерело
5bab140968
коміт
01defdefe4
88 files changed
+2689
-151
No files matched your search
@@ -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();
|
||||
|
||||
@@ -262,6 +262,15 @@ export class EditorController implements SlotHost {
|
||||
prevRoot = root;
|
||||
prevLight = s.lightEdit;
|
||||
this.saveSession();
|
||||
if (
|
||||
root &&
|
||||
previous.project &&
|
||||
!previous.project.workspace &&
|
||||
s.project?.workspace?.projects[0] === previous.project.root
|
||||
) {
|
||||
const session = loadSession(previous.project.root);
|
||||
if (session) storeSession(root, session);
|
||||
}
|
||||
this.sessionRoot = null;
|
||||
const generation = ++this.projectGeneration;
|
||||
const documents = customDocuments(useTabs.getState(), useContributions.getState().editorKinds);
|
||||
|
||||
@@ -40,6 +40,7 @@ export function activate(): void | (() => void) {
|
||||
activePath: () => ctrl.activePath(),
|
||||
activeCursor: () => ctrl.activeCursor(),
|
||||
saveAll: () => ctrl.saveAll(),
|
||||
dirtyDocumentIds: () => [...ctrl.manager.byId.values()].filter((m) => m.getMeta().dirty).map((m) => m.doc),
|
||||
focus: () => ctrl.focus(),
|
||||
openPaths: () => ctrl.openPaths(),
|
||||
openTabs: () => ctrl.openTabs(),
|
||||
|
||||
@@ -4,7 +4,7 @@ import clsx from "clsx";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { shortcutLabel } from "@/platform/keymap";
|
||||
import { basename, dirname, relativePath, useProject } from "@/platform/project";
|
||||
import { basename, dirname, projectFolder, relativePath, useProject } from "@/platform/project";
|
||||
import type { LineEnding, OutlineItem } from "@/platform/ipc";
|
||||
import { BarButton, Icon, Menu, badgeKind, fileIconName } from "@/ui";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
@@ -56,7 +56,8 @@ interface Crumb {
|
||||
*/
|
||||
export function Breadcrumbs() {
|
||||
const path = useEditorStatus((s) => s.status?.path ?? null);
|
||||
const project = useProject((s) => s.project);
|
||||
const currentProject = useProject((s) => s.project);
|
||||
const project = projectFolder(currentProject, path ?? "");
|
||||
const outline = useOutline((s) => (path ? s[path] : undefined));
|
||||
const chain = useCaretSymbols(path, outline) ?? NO_SYMBOLS;
|
||||
const [popup, setPopup] = useState<{ nav: NavSegment; anchor: HTMLElement } | null>(null);
|
||||
|
||||
@@ -74,3 +74,21 @@ describe("caret-driven status widgets", () => {
|
||||
expect(commits).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows the member and its directories in workspace breadcrumbs", async () => {
|
||||
const { useProject } = await import("@/platform/project");
|
||||
useProject.setState({
|
||||
project: {
|
||||
root: "/workspace",
|
||||
name: "Workspace",
|
||||
settings: {},
|
||||
isGit: false,
|
||||
workspace: { version: 1, name: "Workspace", projects: ["/a", "/b"] },
|
||||
},
|
||||
});
|
||||
useEditorStatus.setState({ status: { ...caret(0, 0), path: "/b/src/file.ts" } });
|
||||
const view = render(<Breadcrumbs />);
|
||||
expect(screen.getByRole("navigation", { name: "Breadcrumbs" }).textContent).toBe("bsrcfile.ts");
|
||||
view.unmount();
|
||||
useProject.setState({ project: null });
|
||||
});
|
||||
Reference in new issue
Заблокувати користувача