feat(plugins): support secondary split on [[contributes.views]] (#2185)

Plugin tool windows could only target the primary split of their dock, because the view was never registered as secondary. An optional secondary = true on [[contributes.views]], gated to plugin API 1.3, places a view in Left Bottom, Right Bottom or Bottom Right. Existing plugins stay where they are, and a user's saved layout wins.

Co-authored-by: dengliu <[email protected]>
Цей коміт міститься в:
Deng Liuтаdengliu authored and GitHub committed 2026-11-09 03:35:57 +08:00
1 джерело 067feac89c
коміт 3dae4ef729
5 files changed
+115 -4

No files matched your search

+43
Переглянути файл
@@ -5,6 +5,7 @@ import userEvent from "@testing-library/user-event";
import { setLocale, tx } from "@/platform/i18n";
import { setContext, useContextKeys } from "@/platform/context";
import { provideToolWindowService, useContributions } from "@/workbench/contributions";
import { loadToolWindowLayout, moveToolWindow, resetToolWindowLayout, slotOfId } from "@/workbench/toolwindows/store";
import { registerWindowView, revealWindowView, type WindowView } from "../views/viewWindows";
const disposers: (() => void)[] = [];
@@ -173,6 +174,48 @@ it("retains an opened tab's local state while switching tabs", async () => {
expect(input).toHaveValue("retain this draft");
});
it("keeps views primary unless secondary is explicitly true in every dock", () => {
for (const location of ["left", "right", "bottom"] as const) {
for (const secondary of [undefined, false, true]) {
for (const header of [undefined, "none"] as const) {
const id = `${location}-${secondary}-${header}`;
add("split", { id, location, secondary, header });
expect(useContributions.getState().toolWindows.get(`plugin.split.${id}`)?.secondary).toBe(secondary === true);
}
}
}
});
it("uses the first group member's split even when hidden", () => {
add("spring", { id: "routes", group: "framework", secondary: true, when: "plugin.spring.ready" });
add("spring", { id: "beans", group: "framework", secondary: false });
expect(useContributions.getState().toolWindows.get(id)?.secondary).toBe(true);
setContext("plugin.spring.ready", true);
expect(useContributions.getState().toolWindows.get(id)?.secondary).toBe(true);
});
it("preserves a user's moved placement across registration and layout reload", () => {
localStorage.clear();
resetToolWindowLayout();
loadToolWindowLayout("/plugin-split-test");
try {
const view = add("split", { id: "panel", location: "right", secondary: true });
const id = "plugin.split.panel";
expect(slotOfId(id)).toBe("right-split");
moveToolWindow(id, "right");
view.setIcon("folder");
loadToolWindowLayout("/plugin-split-test");
expect(slotOfId(id)).toBe("right");
moveToolWindow(id, "bottom-split");
view.setIcon("plugin");
loadToolWindowLayout("/plugin-split-test");
expect(slotOfId(id)).toBe("bottom-split");
} finally {
resetToolWindowLayout();
localStorage.clear();
}
});
it("discards a conditionally hidden tab and keeps it lazy when it becomes available again", async () => {
setContext("plugin.spring.ready", true);
add("spring", { id: "routes", title: "Routes", group: "framework" });
+6 -1
Переглянути файл
@@ -17,6 +17,8 @@ export interface WindowView {
id: string;
title: string;
location: ToolWindowAnchor;
secondary?: boolean;
header?: "default" | "none" | null;
group?: string | null;
when?: string | null;
}
@@ -105,7 +107,9 @@ function refresh(id: string, group: WindowGroup) {
members,
selected: selected ?? members[0]?.decl.id ?? null,
});
const signature = first && members.length ? JSON.stringify([first.decl.title, first.decl.location, first.icon]) : "";
const secondary = first?.decl.secondary === true;
const signature =
first && members.length ? JSON.stringify([first.decl.title, first.decl.location, secondary, first.icon]) : "";
if (signature === group.signature) return;
group.dispose();
group.signature = signature;
@@ -116,6 +120,7 @@ function refresh(id: string, group: WindowGroup) {
title: first.decl.title,
verbatim: true,
anchor: first.decl.location,
secondary,
icon: first.icon,
order: 900,
component: first.decl.group ? group.component : first.component,