feat(terminal): reorder terminal tabs by dragging them (#836)

The Terminal tool window's tab strip uses the drag and drop Tabs already supports for editor tabs, with its own drag type so terminal and editor tabs don't mix; dropping moves the session tab to the insertion point.

Fixes #775

Co-authored-by: kevinh458 <[email protected]>
Цей коміт міститься в:
kevinh authored and GitHub committed 2026-11-02 20:21:14 +08:00
1 джерело 91c15ff31b
коміт df9a9e0be0
3 files changed
+40

No files matched your search

+10
Переглянути файл
@@ -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<typeof tabTitle>[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={
<div className={styles.tabActions}>
<IconButton icon="plus" size="small" action="terminal.new" title="New Tab" />
+14
Переглянути файл
@@ -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<T extends { id: string }>(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) => ({
+16
Переглянути файл
@@ -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("");