diff --git a/src/editors/DocumentSlotView.tsx b/src/editors/DocumentSlotView.tsx index 8ecccf7..3e32bc9 100644 --- a/src/editors/DocumentSlotView.tsx +++ b/src/editors/DocumentSlotView.tsx @@ -42,7 +42,12 @@ export function DocumentSlotView({ ); } return ( + // Key by document id so switching tabs to a different document remounts a + // fresh editor instead of reusing one bound to the previous document. The + // shared editor view binds its content on mount only, so without this a + // tab switch can leave the previous file's content showing. handleAuxClose(e, tab.tabId)} onClick={() => onSelect(tab.tabId)} - className={`group flex shrink-0 cursor-pointer items-center gap-1 rounded px-2 py-1 text-xs ${ + className={`group flex shrink-0 cursor-pointer items-center gap-1 rounded px-2 py-1 text-xs transition-none ${ isActive ? 'bg-accent/15 text-foreground' : 'text-foreground/60 hover:bg-border/30' diff --git a/src/workspaces/tiling/tabsModel.test.ts b/src/workspaces/tiling/tabsModel.test.ts index 9c31f11..fddfea2 100644 --- a/src/workspaces/tiling/tabsModel.test.ts +++ b/src/workspaces/tiling/tabsModel.test.ts @@ -11,6 +11,7 @@ import { getActiveSlot, getActiveTabId, getTabs, + planOpen, setActiveTab, setTabSlot, type TileTabsMap, @@ -146,6 +147,45 @@ describe('findTabForRequest', () => { }); }); +describe('planOpen', () => { + const makeId = () => 'tab-new'; + + it('focuses an existing tab without creating a new one', () => { + const map = tileWithDocs('tile-1', ['a', 'b'], 'a'); + const plan = planOpen(map, 'tile-1', request('b'), makeId); + expect(plan.alreadyOpen).toBe(true); + expect(plan.tabId).toBe('tab-b'); + expect(getActiveTabId(plan.map, 'tile-1')).toBe('tab-b'); + expect(getTabs(plan.map, 'tile-1')).toHaveLength(2); + }); + + it('reuses the active empty start tab', () => { + const map = ensureTile({}, 'tile-1', 'tab-1'); + const plan = planOpen(map, 'tile-1', request('a'), makeId); + expect(plan.alreadyOpen).toBe(false); + expect(plan.tabId).toBe('tab-1'); + expect(getTabs(plan.map, 'tile-1')).toHaveLength(1); + }); + + it('appends a new tab when the active tab holds a document', () => { + const map = tileWithDocs('tile-1', ['a'], 'a'); + const plan = planOpen(map, 'tile-1', request('b'), makeId); + expect(plan.alreadyOpen).toBe(false); + expect(plan.tabId).toBe('tab-new'); + expect(getTabs(plan.map, 'tile-1').map((t) => t.tabId)).toEqual([ + 'tab-a', + 'tab-new', + ]); + expect(getActiveTabId(plan.map, 'tile-1')).toBe('tab-new'); + }); + + it('creates the tile when it does not exist yet', () => { + const plan = planOpen({}, 'tile-1', request('a'), makeId); + expect(plan.tabId).toBe('tab-new'); + expect(getActiveTabId(plan.map, 'tile-1')).toBe('tab-new'); + }); +}); + describe('findEmptyTab', () => { it('finds an empty tab when one exists', () => { const map = addTab( diff --git a/src/workspaces/tiling/tabsModel.ts b/src/workspaces/tiling/tabsModel.ts index 0786c5c..5e01f3b 100644 --- a/src/workspaces/tiling/tabsModel.ts +++ b/src/workspaces/tiling/tabsModel.ts @@ -155,6 +155,47 @@ export function findEmptyTab(map: TileTabsMap, tileId: string): string | null { return tile.tabs.find((t) => t.slot.kind === 'empty')?.tabId ?? null; } +export type OpenPlan = { + map: TileTabsMap; + /** The tab the document should open into (or that was focused). */ + tabId: string; + /** True when the document was already open and we only changed focus. */ + alreadyOpen: boolean; +}; + +/** + * Decide where an open lands in a tile: + * 1. Focus an existing tab already showing the same document (no reopen). + * 2. Otherwise reuse the active empty "start" tab if there is one. + * 3. Otherwise append a new tab (creating the tile if absent). + * + * `makeTabId` is only invoked when a new tab is actually created, so callers' + * id counters do not advance on focus/reuse. + */ +export function planOpen( + map: TileTabsMap, + tileId: string, + request: OpenDocRequest, + makeTabId: () => string, +): OpenPlan { + const existing = findTabForRequest(map, tileId, request); + if (existing) { + return { + map: setActiveTab(map, tileId, existing), + tabId: existing, + alreadyOpen: true, + }; + } + + const activeTabId = getActiveTabId(map, tileId); + if (activeTabId !== null && getActiveSlot(map, tileId).kind === 'empty') { + return { map, tabId: activeTabId, alreadyOpen: false }; + } + + const tabId = makeTabId(); + return { map: addTab(map, tileId, tabId), tabId, alreadyOpen: false }; +} + export type CloseTabResult = { map: TileTabsMap; /** Tab ids whose document handles the caller must release. */ diff --git a/src/workspaces/tiling/useTilingDocumentSlots.ts b/src/workspaces/tiling/useTilingDocumentSlots.ts index 13d3d59..33f028f 100644 --- a/src/workspaces/tiling/useTilingDocumentSlots.ts +++ b/src/workspaces/tiling/useTilingDocumentSlots.ts @@ -77,15 +77,17 @@ export function useTilingDocumentSlots({ const request = openRequest; const tileId = activeTileId; - // Open into the active tile's active tab. (Focus-or-append routing arrives - // in a later phase, once the tab strip exists.) - let map = tileTabsRef.current; - if (!map[tileId]) { - map = tabsModel.ensureTile(map, tileId, makeTabId()); - setTileTabs(map); - } - const tabId = tabsModel.getActiveTabId(map, tileId); - if (!tabId) return; + // Focus an existing tab for this document, reuse the empty start tab, or + // append a new tab — all decided by tabsModel.planOpen. + const plan = tabsModel.planOpen( + tileTabsRef.current, + tileId, + request, + makeTabId, + ); + setTileTabs(plan.map); + if (plan.alreadyOpen) return; + const tabId = plan.tabId; const setSlot = (slot: DocumentSlotState) => setTileTabs((prev) => tabsModel.setTabSlot(prev, tileId, tabId, slot));