diff --git a/src/features/terminal/TerminalView.tsx b/src/features/terminal/TerminalView.tsx index 12d5b0ae9..37ec86b0d 100644 --- a/src/features/terminal/TerminalView.tsx +++ b/src/features/terminal/TerminalView.tsx @@ -16,9 +16,11 @@ import { Tabs, type MenuItem, type TabItem, + type TabsDnd, } from "@/ui"; import { getSession, + moveTab, newTab, publishFocus, renameTab, @@ -36,6 +38,13 @@ import styles from "./Terminal.module.css"; const TAB_PREFIX = "openidea-terminal"; +/** Dragging a tab reorders the sessions, like IDEA's terminal tabs. */ +const TAB_DND: TabsDnd = { + type: "application/x-openidea-terminal-tab", + payload: (id) => id, + onDrop: (id, index) => moveTab(id, index), +}; + /** Full title when the label is trimmed, then the working directory. */ function tabTooltip(tab: TerminalTab, sessions: Parameters[1]): string | undefined { const full = tabFullTitle(tab, sessions); @@ -336,6 +345,7 @@ export function TerminalTabs() { onClose={(id) => void onClose(id)} onContextMenu={tabMenu} panelIdPrefix={TAB_PREFIX} + dnd={TAB_DND} trailing={
diff --git a/src/features/terminal/store.ts b/src/features/terminal/store.ts index 463bfb6b8..16c02e9b2 100644 --- a/src/features/terminal/store.ts +++ b/src/features/terminal/store.ts @@ -205,6 +205,20 @@ export function selectTab(id: string) { useTerminals.setState({ activeTab: id }); } +/** `tabs` with tab `id` moved to insertion `index`, an index into `tabs` before the move. */ +export function reorderTabs(tabs: readonly T[], id: string, index: number): T[] { + const from = tabs.findIndex((t) => t.id === id); + if (from < 0) return [...tabs]; + const rest = tabs.filter((t) => t.id !== id); + rest.splice(Math.max(0, Math.min(index > from ? index - 1 : index, rest.length)), 0, tabs[from]!); + return rest; +} + +/** Drag & drop in the tab strip. */ +export function moveTab(id: string, index: number) { + useTerminals.setState((s) => ({ tabs: reorderTabs(s.tabs, id, index) })); +} + export function renameTab(id: string, name: string) { const trimmed = name.trim(); useTerminals.setState((s) => ({ diff --git a/src/features/terminal/terminal.test.ts b/src/features/terminal/terminal.test.ts index 548cb5ed5..1a5edf3ce 100644 --- a/src/features/terminal/terminal.test.ts +++ b/src/features/terminal/terminal.test.ts @@ -8,6 +8,7 @@ import { layoutPanes, nextTabName, removeFromLayout, + reorderTabs, splitLayout, tabFullTitle, tabTitle, @@ -230,6 +231,21 @@ describe("tab names", () => { }); }); +describe("tab order", () => { + const ids = (tabs: { id: string }[]) => tabs.map((t) => t.id).join(""); + const tabs = [{ id: "a" }, { id: "b" }, { id: "c" }]; + + it("moves a dragged tab to the insertion index", () => { + expect(ids(reorderTabs(tabs, "a", 3))).toBe("bca"); + expect(ids(reorderTabs(tabs, "a", 2))).toBe("bac"); + expect(ids(reorderTabs(tabs, "c", 0))).toBe("cab"); + expect(ids(reorderTabs(tabs, "b", 1))).toBe("abc"); + expect(ids(reorderTabs(tabs, "b", 2))).toBe("abc"); + }); + + it("ignores unknown tabs", () => expect(ids(reorderTabs(tabs, "x", 0))).toBe("abc")); +}); + describe("terminal find", () => { it("formats the match counter", () => { expect(matchCounter(null, "x")).toBe("");