fix(lightedit): merge the saved session with launched tabs instead of replacing them (#2229) (#2290)

Цей коміт міститься в:
arookieofc authored and GitHub committed 2026-11-09 15:22:18 +08:00
1 джерело e77ee39047
коміт b12fabd472
3 files changed
+90 -1

No files matched your search

+4 -1
Переглянути файл
@@ -56,6 +56,7 @@ import {
enforceTabLimit,
findTab,
initialTabsState,
mergeSessionTabs,
moveTab,
moveTabToNewSplit,
neighbourPane,
@@ -531,7 +532,9 @@ export class EditorController implements SlotHost {
this.restoredTabs = new Set(Object.values(state.panes).flatMap((p) => p.tabs.map((t) => t.id)));
this.restoring = true;
try {
useTabs.setState(state, true);
// A restore landing after tabs were already opened (e.g. a launched file
// admitted while the transition was still in flight) must not wipe them.
useTabs.setState((current) => mergeSessionTabs(current, state), true);
} finally {
this.restoring = false;
}
+63
Переглянути файл
@@ -0,0 +1,63 @@
import { afterEach, describe, expect, it } from "vitest";
import { EditorController, LIGHT_EDIT_SESSION, openFilePaths, useTabs } from "./controller";
import { createMemoryBackend } from "./backend";
import { initialTabsState } from "./model/tabs";
import { storeSession } from "./model/session";
import { useProject } from "@/platform/project";
let ctrl: EditorController | null = null;
const initialSettings = useProject.getState().settings;
afterEach(() => {
ctrl?.stop();
ctrl = null;
useTabs.setState(initialTabsState(), true);
useProject.setState({ project: null, lightEdit: false, settings: initialSettings } as never);
try {
localStorage.removeItem(`openidea.editorSession.v1:${LIGHT_EDIT_SESSION}`);
} catch {
/* ignore */
}
});
function activePath() {
const state = useTabs.getState();
const pane = state.panes[state.activePane]!;
return pane.tabs.find((tab) => tab.id === pane.active)?.input.path;
}
function seedLightEditSession() {
storeSession(LIGHT_EDIT_SESSION, {
version: 1,
layout: { kind: "pane", tabs: [{ path: "/p/a.ts" }], active: 0 },
activePane: 0,
files: {},
});
}
describe("lightedit launch (#2229)", () => {
it("admits the launched file after the saved session restores", async () => {
seedLightEditSession();
ctrl = new EditorController(createMemoryBackend({ "/p/a.ts": "a", "/p/b.ts": "b" }));
ctrl.start();
// openFilesHere: window has neither project nor LightEdit -> openLightEdit(b) then openInEditor([b]).
useProject.setState({ project: null, lightEdit: true });
await ctrl.openFile("/p/b.ts", { focus: true });
await new Promise((r) => setTimeout(r, 50));
expect(openFilePaths(useTabs.getState()).sort()).toEqual(["/p/a.ts", "/p/b.ts"]);
expect(activePath()).toBe("/p/b.ts");
});
it("merges a late saved-session restore with the launched tab instead of replacing it", async () => {
seedLightEditSession();
ctrl = new EditorController(createMemoryBackend({ "/p/a.ts": "a", "/p/b.ts": "b" }));
ctrl.start();
useProject.setState({ project: null, lightEdit: true });
await ctrl.openFile("/p/b.ts", { focus: true });
// A stale/second transition restore landing after the admit (#2229).
ctrl.restoreSession(LIGHT_EDIT_SESSION);
await new Promise((r) => setTimeout(r, 50));
expect(openFilePaths(useTabs.getState()).sort()).toEqual(["/p/a.ts", "/p/b.ts"]);
expect(activePath()).toBe("/p/b.ts");
});
});
+23
Переглянути файл
@@ -93,6 +93,29 @@ export function openKeys(s: TabsState): Set<string> {
return keys;
}
/**
* Folds restored tabs into the current tabs without losing anything open: tabs
* already open stay (with their active tab), restored tabs missing by key are
* appended to the active pane. An empty window takes the restored state as-is,
* keeping its saved layout and active tab.
*/
export function mergeSessionTabs(current: TabsState, restored: TabsState): TabsState {
const target = current.panes[current.activePane];
if (!target || !Object.values(current.panes).some((p) => p.tabs.length)) return restored;
const seen = openKeys(current);
const additions: Tab[] = [];
for (const pane of Object.values(restored.panes))
for (const tab of pane.tabs) {
if (seen.has(tab.input.key)) continue;
seen.add(tab.input.key);
additions.push(tab);
}
if (!additions.length) return current;
const tabs = [...target.tabs, ...additions];
const mru = [...target.mru, ...additions.map((t) => t.id).filter((id) => !target.mru.includes(id))];
return withPane(current, { ...target, tabs, mru });
}
function withPane(s: TabsState, pane: Pane): TabsState {
return { ...s, panes: { ...s.panes, [pane.id]: pane } };
}