дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
feat(editor): Open tabs in the opposite group, flip splitter orientation and unsplit all
Adds three split actions to the editor tab menu, the Window › Editor Tabs menu and Find Action: Open in Opposite Group keeps the tab and opens a copy in the neighbouring group (splitting when there is none), Change Splitter Orientation flips the split holding the current group, and Unsplit All closes every other group.
Цей коміт міститься в:
1 джерело
af3ed649b7
коміт
86d64c1463
9 files changed
+131
No files matched your search
@@ -781,6 +781,9 @@ pub fn spec(os: Os) -> Vec<Entry> {
|
||||
act("editor.splitDown", "Split Down"),
|
||||
act("editor.unsplit", "Unsplit"),
|
||||
act("editor.moveToOppositeGroup", "Move to Opposite Group"),
|
||||
act("editor.openInOppositeGroup", "Open in Opposite Group"),
|
||||
act("editor.changeSplitOrientation", "Change Splitter Orientation"),
|
||||
act("editor.unsplitAll", "Unsplit All"),
|
||||
act("editor.nextSplitter", "Go to Next Splitter"),
|
||||
act("editor.prevSplitter", "Go to Previous Splitter"),
|
||||
],
|
||||
|
||||
@@ -904,6 +904,30 @@ export function registerEditorActions(): () => void {
|
||||
when: "editorSplit",
|
||||
run: () => ctrl.unsplit(useTabs.getState().activePane),
|
||||
},
|
||||
{
|
||||
id: "editor.unsplitAll",
|
||||
title: "Unsplit All",
|
||||
category: "Window",
|
||||
when: "editorSplit",
|
||||
run: () => ctrl.unsplitAll(useTabs.getState().activePane),
|
||||
},
|
||||
{
|
||||
id: "editor.changeSplitOrientation",
|
||||
title: "Change Splitter Orientation",
|
||||
category: "Window",
|
||||
when: "editorSplit",
|
||||
run: () => ctrl.changeSplitOrientation(useTabs.getState().activePane),
|
||||
},
|
||||
{
|
||||
id: "editor.openInOppositeGroup",
|
||||
title: "Open in Opposite Group",
|
||||
category: "Window",
|
||||
when: TABS_WHEN,
|
||||
run: () => {
|
||||
const t = activePaneTab();
|
||||
if (t) ctrl.openInOpposite(t.paneId, t.tabId);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "editor.copyPath",
|
||||
title: "Copy Path",
|
||||
|
||||
@@ -65,6 +65,23 @@ describe("EditorController", () => {
|
||||
expect(openFilePaths(useTabs.getState())).toEqual(["/p/a.ts", "/p/b.ts"]);
|
||||
});
|
||||
|
||||
it("opens a copy of a tab in the opposite group, splitting when there is none", async () => {
|
||||
ctrl = new EditorController(createMemoryBackend({ "/p/a.ts": "", "/p/b.ts": "" }));
|
||||
ctrl.start();
|
||||
await ctrl.openFile("/p/a.ts", { focus: false });
|
||||
await ctrl.openFile("/p/b.ts", { focus: false });
|
||||
const paths = (paneId: string) => useTabs.getState().panes[paneId]!.tabs.map((t) => t.input.path);
|
||||
const first = useTabs.getState().activePane;
|
||||
const tabA = useTabs.getState().panes[first]!.tabs[0]!.id;
|
||||
ctrl.openInOpposite(first, tabA);
|
||||
const second = useTabs.getState().activePane;
|
||||
expect(second).not.toBe(first);
|
||||
expect(paths(second)).toEqual(["/p/a.ts"]);
|
||||
ctrl.openInOpposite(first, useTabs.getState().panes[first]!.tabs[1]!.id);
|
||||
expect(paths(second)).toEqual(["/p/a.ts", "/p/b.ts"]);
|
||||
expect(paths(first)).toEqual(["/p/a.ts", "/p/b.ts"]);
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
@@ -31,6 +31,7 @@ import { fileIconName, showContextMenu, type MenuItem } from "@/ui";
|
||||
import { usePlatform } from "@/platform/platformInfo";
|
||||
import {
|
||||
activateTab,
|
||||
changeSplitOrientation,
|
||||
closeAll,
|
||||
closeKey,
|
||||
closeOthers,
|
||||
@@ -57,6 +58,7 @@ import {
|
||||
splitPane,
|
||||
tabOptions,
|
||||
togglePin,
|
||||
unsplitAll,
|
||||
type SplitEdge,
|
||||
type Tab,
|
||||
type TabsState,
|
||||
@@ -1009,6 +1011,30 @@ export class EditorController implements SlotHost {
|
||||
unsplit(paneId: string) {
|
||||
this.apply((s) => removePane(s, paneId));
|
||||
}
|
||||
unsplitAll(paneId: string) {
|
||||
this.apply((s) => unsplitAll(s, paneId));
|
||||
this.focus();
|
||||
}
|
||||
changeSplitOrientation(paneId: string) {
|
||||
this.apply((s) => changeSplitOrientation(s, paneId));
|
||||
}
|
||||
/** IDEA "Open in Opposite Group": the tab stays and a copy opens next door (or in a new split). */
|
||||
openInOpposite(paneId: string, tabId: string) {
|
||||
const s = useTabs.getState();
|
||||
const tab = s.panes[paneId]?.tabs.find((t) => t.id === tabId);
|
||||
if (!tab) return;
|
||||
const target = neighbourPane(s, paneId);
|
||||
if (!target) {
|
||||
this.activate(paneId, tabId, false);
|
||||
return this.split(paneId, "row");
|
||||
}
|
||||
const r = openTab(s, target, tab.input);
|
||||
useTabs.setState(r.state, true);
|
||||
void this.ensureSlot(r.tabId, { from: this.slots.get(tabId) }).then((slot) => {
|
||||
if (slot?.view) slot.focus();
|
||||
else if (slot) slot.wantsFocus = true;
|
||||
});
|
||||
}
|
||||
|
||||
closeActiveTab() {
|
||||
const s = useTabs.getState();
|
||||
@@ -1183,7 +1209,10 @@ export class EditorController implements SlotHost {
|
||||
...(split
|
||||
? ([
|
||||
{ label: "Move to Opposite Group", run: () => this.moveToOpposite(paneId, tab.id) },
|
||||
{ label: "Open in Opposite Group", run: () => this.openInOpposite(paneId, tab.id) },
|
||||
{ label: "Change Splitter Orientation", run: () => this.changeSplitOrientation(paneId) },
|
||||
{ label: "Unsplit", run: () => this.unsplit(paneId) },
|
||||
{ label: "Unsplit All", run: () => this.unsplitAll(paneId) },
|
||||
] as MenuItem[])
|
||||
: []),
|
||||
{ kind: "separator" },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
changeSplitOrientation,
|
||||
closeAll,
|
||||
closeKey,
|
||||
closeOthers,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
splitPane,
|
||||
tabOptions,
|
||||
togglePin,
|
||||
unsplitAll,
|
||||
type TabsState,
|
||||
} from "./tabs";
|
||||
import { NavigationHistory } from "./navHistory";
|
||||
@@ -168,6 +170,29 @@ describe("tab model", () => {
|
||||
expect(s.layout).toEqual({ kind: "pane", paneId: split.paneId });
|
||||
});
|
||||
|
||||
it("flips the orientation of a split and unsplits everything", () => {
|
||||
let s = open(initialTabsState(), "a");
|
||||
const first = s.activePane;
|
||||
const right = splitPane(s, first, "row");
|
||||
const below = splitPane(right.state, right.paneId, "column");
|
||||
s = below.state;
|
||||
const dirs = (st: TabsState) =>
|
||||
st.layout.kind === "split"
|
||||
? [st.layout.dir, ...st.layout.children.map((c) => (c.kind === "split" ? c.dir : "pane"))]
|
||||
: [];
|
||||
expect(dirs(s)).toEqual(["row", "pane", "column"]);
|
||||
// The nested split turns side by side and merges into the outer row.
|
||||
s = changeSplitOrientation(s, below.paneId);
|
||||
expect(dirs(s)).toEqual(["row", "pane", "pane", "pane"]);
|
||||
s = changeSplitOrientation(s, first);
|
||||
expect(dirs(s)).toEqual(["column", "pane", "pane", "pane"]);
|
||||
expect(changeSplitOrientation(open(initialTabsState(), "x"), "nope").layout.kind).toBe("pane");
|
||||
s = unsplitAll(s, right.paneId);
|
||||
expect(paneIds(s)).toEqual([right.paneId]);
|
||||
expect(s.activePane).toBe(right.paneId);
|
||||
expect(labels(s)).toEqual(["a"]);
|
||||
});
|
||||
|
||||
it("closes a key everywhere and cycles tabs", () => {
|
||||
let s = initialTabsState();
|
||||
s = open(s, "a");
|
||||
|
||||
@@ -437,6 +437,30 @@ export function removePane(s: TabsState, paneId: string): TabsState {
|
||||
return { ...s, layout, panes, activePane };
|
||||
}
|
||||
|
||||
/** Flips the split that directly holds `paneId` between side by side and stacked. */
|
||||
export function changeSplitOrientation(s: TabsState, paneId: string): TabsState {
|
||||
let changed = false;
|
||||
const flipped = mapLayout(s.layout, (n) => {
|
||||
if (n.kind !== "split" || !n.children.some((c) => c.kind === "pane" && c.paneId === paneId)) return n;
|
||||
changed = true;
|
||||
const dir = n.dir === "row" ? "column" : "row";
|
||||
return { ...n, dir, children: n.children.flatMap((c) => (c.kind === "split" && c.dir === dir ? c.children : [c])) };
|
||||
})!;
|
||||
// The parent flattens a flipped split that now runs its way; the flipped one absorbs its children.
|
||||
return changed ? { ...s, layout: flipped } : s;
|
||||
}
|
||||
|
||||
/** Removes every pane except `keepPane`. */
|
||||
export function unsplitAll(s: TabsState, keepPane: string): TabsState {
|
||||
if (!s.panes[keepPane]) return s;
|
||||
return {
|
||||
...s,
|
||||
layout: { kind: "pane", paneId: keepPane },
|
||||
panes: { [keepPane]: s.panes[keepPane] },
|
||||
activePane: keepPane,
|
||||
};
|
||||
}
|
||||
|
||||
/** The pane right of / below `paneId`, if any (for "open to the side"). */
|
||||
export function neighbourPane(s: TabsState, paneId: string): string | null {
|
||||
const order = paneIds(s);
|
||||
|
||||
@@ -398,6 +398,9 @@
|
||||
"menu.editor.closeAllTabs": "Close All Tabs",
|
||||
"menu.editor.reopenClosedTab": "Reopen Closed Tab",
|
||||
"menu.editor.moveToOppositeGroup": "Move to Opposite Group",
|
||||
"menu.editor.openInOppositeGroup": "Open in Opposite Group",
|
||||
"menu.editor.changeSplitOrientation": "Change Splitter Orientation",
|
||||
"menu.editor.unsplitAll": "Unsplit All",
|
||||
"menu.navigate.showFilePath": "File Path",
|
||||
"menu.navigate.customRegion": "Custom Folding…",
|
||||
"menu.code.showErrorDescription": "Error Description",
|
||||
|
||||
@@ -225,6 +225,7 @@
|
||||
"Caret should be positioned at the name of the element to be refactored": "光标应位于要重构的元素名称上",
|
||||
"Cascade": "级联",
|
||||
"Case sensitive": "区分大小写",
|
||||
"Change Splitter Orientation": "更改拆分方向",
|
||||
"Change Signature…": "更改签名…",
|
||||
"Change actions": "更改操作",
|
||||
"Change font size with Command+Mouse Wheel": "使用 Command+鼠标滚轮更改字体大小",
|
||||
@@ -1271,6 +1272,7 @@
|
||||
"Open in Browser": "在浏览器中打开",
|
||||
"Open in Find Tool Window": "在“查找”工具窗口中打开",
|
||||
"Open in Find Window": "在查找窗口中打开",
|
||||
"Open in Opposite Group": "在相对组中打开",
|
||||
"Open in New Window": "在新窗口中打开",
|
||||
"Open in Terminal": "在终端中打开",
|
||||
"Open in editor": "在编辑器中打开",
|
||||
@@ -2154,6 +2156,7 @@
|
||||
"Unshelve Changes": "取消搁置更改",
|
||||
"Unshelve…": "取消搁置…",
|
||||
"Unsplit": "取消拆分",
|
||||
"Unsplit All": "全部取消拆分",
|
||||
"Unstage": "取消暂存",
|
||||
"Unstash Changes…": "取消储藏更改…",
|
||||
"Update": "更新",
|
||||
|
||||
@@ -398,6 +398,9 @@
|
||||
"menu.editor.closeAllTabs": "关闭所有标签页",
|
||||
"menu.editor.reopenClosedTab": "重新打开已关闭的标签页",
|
||||
"menu.editor.moveToOppositeGroup": "移动到另一组",
|
||||
"menu.editor.openInOppositeGroup": "在相对组中打开",
|
||||
"menu.editor.changeSplitOrientation": "更改拆分方向",
|
||||
"menu.editor.unsplitAll": "全部取消拆分",
|
||||
"menu.navigate.showFilePath": "文件路径",
|
||||
"menu.navigate.customRegion": "自定义折叠…",
|
||||
"menu.code.showErrorDescription": "错误描述",
|
||||
|
||||
Reference in new issue
Заблокувати користувача