fix(editor): keep a pinned duplicate open on close and ignore setActiveFile for a missing file (#28120)

* fix(editor): keep a pinned clean sibling open when closing its same-owner twin

Closing a document also closes clean same-owner duplicate records; a record
whose tab is pinned is now left open with its tab.

* fix(editor): ignore setActiveFile for an id with no open file

A stale tab or pane id could make a missing file active and persist it as the
workspace's active file.
This commit is contained in:
BearMett
2026-10-12 23:42:06 -07:00
committed by GitHub
parent d26b3b83a7
commit 52e88e4469
4 changed files with 144 additions and 0 deletions
@@ -0,0 +1,79 @@
import type { StoreApi } from 'zustand/vanilla'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { AppState } from '../types'
import type { OpenFile } from './editor'
import { createEditorTabsStore } from './editor-slice-test-harness'
const live = vi.hoisted((): { store: StoreApi<AppState> | null } => ({ store: null }))
vi.mock('@/store', () => ({
useAppStore: {
getState: () => {
if (!live.store) {
throw new Error('No test store')
}
return live.store.getState()
}
}
}))
let store: StoreApi<AppState>
let file: OpenFile
beforeEach(() => {
store = createEditorTabsStore()
live.store = store
store.getState().openFile({
filePath: '/repo/note.md',
relativePath: 'note.md',
worktreeId: 'wt-1',
language: 'markdown',
mode: 'edit'
})
const opened = store.getState().openFiles[0]
if (!opened) {
throw new Error('Document did not open')
}
file = opened
})
afterEach(() => {
live.store = null
})
// A clean same-owner twin of `file`, shown in its own tab.
function addPinnedSibling(): OpenFile {
const sibling = { ...file, id: 'duplicate-record' }
store.setState({ openFiles: [...store.getState().openFiles, sibling] })
store.getState().createUnifiedTab(sibling.worktreeId, 'editor', {
id: 'duplicate-tab',
entityId: sibling.id,
label: 'note.md',
activate: false
})
store.getState().pinTab('duplicate-tab')
return sibling
}
describe('case 7: closing a document with a pinned clean sibling', () => {
it('premise: the sibling tab is pinned and both records are open', () => {
addPinnedSibling()
const tabs = store.getState().unifiedTabsByWorktree['wt-1']
expect(tabs.find((tab) => tab.id === 'duplicate-tab')?.isPinned).toBe(true)
expect(store.getState().openFiles.map((open) => open.id)).toEqual([file.id, 'duplicate-record'])
})
it('keeps the pinned sibling record open', () => {
const sibling = addPinnedSibling()
store.getState().closeFile(file.id)
expect(store.getState().openFiles).toEqual([sibling])
})
it('keeps the pinned sibling tab and closes only the named tab', () => {
addPinnedSibling()
store.getState().closeFile(file.id)
const tabs = store.getState().unifiedTabsByWorktree['wt-1'] ?? []
expect(tabs.map((tab) => [tab.id, tab.entityId])).toEqual([
['duplicate-tab', 'duplicate-record']
])
})
})
@@ -0,0 +1,59 @@
import type { StoreApi } from 'zustand/vanilla'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { AppState } from '../types'
import { createEditorTabsStore } from './editor-slice-test-harness'
const live = vi.hoisted((): { store: StoreApi<AppState> | null } => ({ store: null }))
vi.mock('@/store', () => ({
useAppStore: {
getState: () => {
if (!live.store) {
throw new Error('No test store')
}
return live.store.getState()
}
}
}))
const GHOST_ID = 'editor:ghost-pane'
let store: StoreApi<AppState>
let fileId: string
beforeEach(() => {
store = createEditorTabsStore()
live.store = store
store.getState().openFile({
filePath: '/repo/note.md',
relativePath: 'note.md',
worktreeId: 'wt-1',
language: 'markdown',
mode: 'edit'
})
const opened = store.getState().openFiles[0]
if (!opened) {
throw new Error('Document did not open')
}
fileId = opened.id
})
afterEach(() => {
live.store = null
})
describe('setActiveFile with an id that has no open file', () => {
it('premise: the open document is active', () => {
expect(store.getState().activeFileId).toBe(fileId)
expect(store.getState().activeFileIdByWorktree['wt-1']).toBe(fileId)
})
it('leaves the in-memory and per-workspace active file unchanged', () => {
store.getState().setActiveFile(GHOST_ID)
expect(store.getState().activeFileId).toBe(fileId)
expect(store.getState().activeFileIdByWorktree['wt-1']).toBe(fileId)
})
it('leaves the per-workspace active file, which is what persists, unchanged', () => {
store.getState().setActiveFile(GHOST_ID, 'wt-1')
expect(store.getState().activeFileIdByWorktree['wt-1']).toBe(fileId)
})
})
@@ -88,6 +88,9 @@ export function createCloseFileAction(
preClose.readOnly !== true &&
file.readOnly !== true &&
isSameOwnedDocument(file, preClose) &&
!get().unifiedTabsByWorktree[file.worktreeId]?.some(
(tab) => tab.entityId === file.id && tab.isPinned
) &&
!file.isDirty &&
!(file.id in get().editorDrafts))
)
@@ -21,6 +21,9 @@ export function createOpenFileMutations(
> {
return {
setActiveFile: (fileId, targetWorktreeId) => {
if (!get().openFiles.some((file) => file.id === fileId)) {
return
}
set((s) => {
const file = s.openFiles.find((f) => f.id === fileId)
const worktreeId = file?.worktreeId