mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00: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.
This commit is contained in:
@@ -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
Block a user