mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-15 00:08:15 +00:00
feat(toolwindows): Pin / Unpin Tab pins a tool window's selected tab (#2807)
Pin / Unpin Tab only acted on editor tabs and was disabled while a tool window had focus, so a shortcut couldn't pin a Find or Usages tab. With a tool window focused it now toggles the pin of that window's selected content tab, through the same content tab registry Close Tool Window Tab uses; Find and Usages tabs support it. Closes #2786
This commit is contained in:
@@ -75,6 +75,8 @@ import { useProject } from "@/platform/project";
|
||||
import { call, commands } from "@/platform/ipc";
|
||||
import { editorPrefs } from "@/platform/editorSettings";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { togglePinSelectedContent } from "@/workbench/toolwindows/content";
|
||||
import { useToolWindowLayout } from "@/workbench/toolwindows/store";
|
||||
import { toggleReadOnly } from "./fileAttributes";
|
||||
import { openNavBarPopup } from "./view/NavBarPopup";
|
||||
import {
|
||||
@@ -1057,8 +1059,11 @@ export function registerEditorActions(): () => void {
|
||||
title: "Pin / Unpin Tab",
|
||||
category: "Window",
|
||||
icon: "pin",
|
||||
when: TABS_WHEN,
|
||||
// IDEA: in a focused tool window, its selected content tab (Find, Usages).
|
||||
when: "editorTabsOpen || toolWindowFocus",
|
||||
run: () => {
|
||||
const tw = useToolWindowLayout.getState().focused;
|
||||
if (tw) return void togglePinSelectedContent(tw);
|
||||
const t = activePaneTab();
|
||||
if (t) ctrl.togglePin(t.paneId, t.tabId);
|
||||
},
|
||||
|
||||
@@ -90,6 +90,10 @@ export function UsagesTabs() {
|
||||
activeId={String(search.id)}
|
||||
onSelect={(id) => useUsages.setState({ active: Number(id) })}
|
||||
onClose={(id) => closeUsageTab(Number(id))}
|
||||
onTogglePin={(id) => {
|
||||
const t = tabs.find((x) => x.id === Number(id));
|
||||
if (t) pinUsageTab(t.id, !t.pinned);
|
||||
}}
|
||||
onContextMenu={(id, e) => {
|
||||
const t = tabs.find((x) => x.id === Number(id));
|
||||
if (!t) return;
|
||||
|
||||
@@ -180,6 +180,10 @@ export function FindTabs() {
|
||||
activeId={String(tab.id)}
|
||||
onSelect={(id) => useFindWindow.setState({ active: Number(id) })}
|
||||
onClose={(id) => closeFindTab(Number(id))}
|
||||
onTogglePin={(id) => {
|
||||
const t = tabs.find((x) => x.id === Number(id));
|
||||
if (t) pinFindTab(t.id, !t.pinned);
|
||||
}}
|
||||
onContextMenu={(id, e) => {
|
||||
const t = tabs.find((x) => x.id === Number(id));
|
||||
if (!t) return;
|
||||
|
||||
@@ -171,6 +171,22 @@ describe("Tabs interactions", () => {
|
||||
expect(handles).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("lets a container pin the selected tab when the strip supports pinning", () => {
|
||||
const handles: TabStripHandle[] = [];
|
||||
const register = (h: TabStripHandle) => (handles.push(h), () => void handles.splice(handles.indexOf(h), 1));
|
||||
const onTogglePin = vi.fn();
|
||||
const ui = (pin?: (id: string) => void) => (
|
||||
<TabStripRegistry.Provider value={register}>
|
||||
<Tabs items={items} activeId="b" onSelect={() => {}} onTogglePin={pin} />
|
||||
</TabStripRegistry.Provider>
|
||||
);
|
||||
const { rerender } = render(ui(onTogglePin));
|
||||
expect(handles[0]!.togglePinSelected()).toBe(true);
|
||||
expect(onTogglePin).toHaveBeenCalledWith("b");
|
||||
rerender(ui());
|
||||
expect(handles[0]!.togglePinSelected()).toBe(false);
|
||||
});
|
||||
|
||||
it("a vertical strip moves with ↑/↓, shows actions above the tabs and has no overflow dropdown", () => {
|
||||
const onSelect = vi.fn();
|
||||
render(
|
||||
|
||||
+13
-3
@@ -92,9 +92,10 @@ function previewOrder(ids: readonly string[], id: string, index: number): Map<st
|
||||
return new Map(order.map((it, i) => [it, i]));
|
||||
}
|
||||
|
||||
/** A tab strip as seen from its container: close the selected tab if it can be closed. */
|
||||
/** A tab strip as seen from its container: close or pin the selected tab when it can be. */
|
||||
export interface TabStripHandle {
|
||||
closeSelected(): boolean;
|
||||
togglePinSelected(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,6 +111,8 @@ export interface TabsProps {
|
||||
onClose?: (id: string) => void;
|
||||
/** Alt+click on a close button (IDEA: close all other tabs). */
|
||||
onCloseOthers?: (id: string) => void;
|
||||
/** Pin / Unpin Tab on the selected tab, from the container (a tool window's content tabs). */
|
||||
onTogglePin?: (id: string) => void;
|
||||
/** Shift+left-click a tab closes it (editor tabs; plain clicks still select). */
|
||||
closeOnShiftClick?: boolean;
|
||||
onDoubleClick?: (id: string) => void;
|
||||
@@ -138,6 +141,7 @@ export function Tabs({
|
||||
onSelect,
|
||||
onClose,
|
||||
onCloseOthers,
|
||||
onTogglePin,
|
||||
closeOnShiftClick,
|
||||
onDoubleClick,
|
||||
onContextMenu,
|
||||
@@ -162,9 +166,9 @@ export function Tabs({
|
||||
const [dragging, setDragging] = useState<string | null>(null);
|
||||
|
||||
const register = useContext(TabStripRegistry);
|
||||
const latest = useRef({ items, activeId, onClose });
|
||||
const latest = useRef({ items, activeId, onClose, onTogglePin });
|
||||
useLayoutEffect(() => {
|
||||
latest.current = { items, activeId, onClose };
|
||||
latest.current = { items, activeId, onClose, onTogglePin };
|
||||
});
|
||||
useEffect(
|
||||
() =>
|
||||
@@ -176,6 +180,12 @@ export function Tabs({
|
||||
onClose(item.id);
|
||||
return true;
|
||||
},
|
||||
togglePinSelected: () => {
|
||||
const { items, activeId, onTogglePin } = latest.current;
|
||||
if (!onTogglePin || !items.some((it) => it.id === activeId)) return false;
|
||||
onTogglePin(activeId!);
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
[register],
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { closeSelectedContent, contentTabRegistry } from "./content";
|
||||
import { closeSelectedContent, contentTabRegistry, togglePinSelectedContent } from "./content";
|
||||
|
||||
describe("tool window content tabs", () => {
|
||||
it("closes the selected tab of the first strip that can, else reports false", () => {
|
||||
const closed: string[] = [];
|
||||
const register = contentTabRegistry("run");
|
||||
expect(contentTabRegistry("run")).toBe(register);
|
||||
const disposeA = register({ closeSelected: () => false });
|
||||
const disposeB = register({ closeSelected: () => (closed.push("b"), true) });
|
||||
const disposeA = register({ closeSelected: () => false, togglePinSelected: () => false });
|
||||
const disposeB = register({ closeSelected: () => (closed.push("b"), true), togglePinSelected: () => false });
|
||||
expect(closeSelectedContent("run")).toBe(true);
|
||||
expect(closed).toEqual(["b"]);
|
||||
disposeB();
|
||||
@@ -15,4 +15,16 @@ describe("tool window content tabs", () => {
|
||||
disposeA();
|
||||
expect(closeSelectedContent("other")).toBe(false);
|
||||
});
|
||||
|
||||
it("pins the selected tab of the first strip that can, else reports false", () => {
|
||||
const pinned: string[] = [];
|
||||
const register = contentTabRegistry("find");
|
||||
const disposeA = register({ closeSelected: () => false, togglePinSelected: () => false });
|
||||
const disposeB = register({ closeSelected: () => false, togglePinSelected: () => (pinned.push("b"), true) });
|
||||
expect(togglePinSelectedContent("find")).toBe(true);
|
||||
expect(pinned).toEqual(["b"]);
|
||||
disposeB();
|
||||
expect(togglePinSelectedContent("find")).toBe(false);
|
||||
disposeA();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,12 @@ export function contentTabRegistry(id: string): (handle: TabStripHandle) => () =
|
||||
return fn;
|
||||
}
|
||||
|
||||
/** Pins or unpins the selected content tab of tool window `id`; false when it has none that can be pinned. */
|
||||
export function togglePinSelectedContent(id: string): boolean {
|
||||
for (const strip of strips.get(id) ?? []) if (strip.togglePinSelected()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Closes the selected content tab of tool window `id`; false when it has none that can be closed. */
|
||||
export function closeSelectedContent(id: string): boolean {
|
||||
for (const strip of strips.get(id) ?? []) if (strip.closeSelected()) return true;
|
||||
|
||||
Reference in New Issue
Block a user