mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user