Keep file opens in editor split groups (#3232)
This commit is contained in:
parent
2c55de5589
commit
73e17b947c
|
|
@ -428,6 +428,99 @@ describe('createEditorSlice floating editor activation', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('createEditorSlice split-group editor routing', () => {
|
||||
function openSourceFile(
|
||||
store: StoreApi<AppState>,
|
||||
filePath: string,
|
||||
options?: Parameters<AppState['openFile']>[1]
|
||||
): void {
|
||||
store.getState().openFile(
|
||||
{
|
||||
filePath,
|
||||
relativePath: filePath.replace('/repo/', ''),
|
||||
worktreeId: 'wt-1',
|
||||
language: 'typescript',
|
||||
mode: 'edit'
|
||||
},
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
function seedTerminalAndEditorGroups(store: StoreApi<AppState>): {
|
||||
terminalTabId: string
|
||||
terminalGroupId: string
|
||||
editorGroupId: string
|
||||
} {
|
||||
const terminalTab = store.getState().createUnifiedTab('wt-1', 'terminal', {
|
||||
id: 'terminal-tab',
|
||||
entityId: 'terminal-tab',
|
||||
label: 'Agent'
|
||||
})
|
||||
const terminalGroup = store.getState().groupsByWorktree['wt-1']?.[0]
|
||||
if (!terminalGroup) {
|
||||
throw new Error('Expected terminal group')
|
||||
}
|
||||
const terminalGroupId = terminalGroup.id
|
||||
const editorGroupId = store.getState().createEmptySplitGroup('wt-1', terminalGroupId, 'right')
|
||||
if (!editorGroupId) {
|
||||
throw new Error('Expected split editor group')
|
||||
}
|
||||
openSourceFile(store, '/repo/seed.ts', { targetGroupId: editorGroupId })
|
||||
store.setState({
|
||||
activeGroupIdByWorktree: { 'wt-1': terminalGroupId },
|
||||
activeTabType: 'terminal',
|
||||
activeTabTypeByWorktree: { 'wt-1': 'terminal' }
|
||||
} as Partial<AppState>)
|
||||
return { terminalTabId: terminalTab.id, terminalGroupId, editorGroupId }
|
||||
}
|
||||
|
||||
function findUnifiedTabByEntity(store: StoreApi<AppState>, entityId: string) {
|
||||
return store.getState().unifiedTabsByWorktree['wt-1']?.find((tab) => tab.entityId === entityId)
|
||||
}
|
||||
|
||||
it('routes implicit file opens to an existing visible editor group', () => {
|
||||
const store = createEditorTabsStore()
|
||||
const { terminalTabId, terminalGroupId, editorGroupId } = seedTerminalAndEditorGroups(store)
|
||||
|
||||
openSourceFile(store, '/repo/next.ts')
|
||||
|
||||
const openedTab = findUnifiedTabByEntity(store, '/repo/next.ts')
|
||||
const terminalGroup = store
|
||||
.getState()
|
||||
.groupsByWorktree['wt-1'].find((group) => group.id === terminalGroupId)
|
||||
const editorGroup = store
|
||||
.getState()
|
||||
.groupsByWorktree['wt-1'].find((group) => group.id === editorGroupId)
|
||||
expect(openedTab?.groupId).toBe(editorGroupId)
|
||||
expect(editorGroup?.activeTabId).toBe(openedTab?.id)
|
||||
expect(terminalGroup?.activeTabId).toBe(terminalTabId)
|
||||
})
|
||||
|
||||
it('uses editor-recent groups when no inactive group is currently showing an editor', () => {
|
||||
const store = createEditorTabsStore()
|
||||
const { editorGroupId } = seedTerminalAndEditorGroups(store)
|
||||
store.getState().createUnifiedTab('wt-1', 'browser', {
|
||||
id: 'browser-tab',
|
||||
entityId: 'browser-tab',
|
||||
label: 'Browser',
|
||||
targetGroupId: editorGroupId
|
||||
})
|
||||
|
||||
openSourceFile(store, '/repo/recent-target.ts')
|
||||
|
||||
expect(findUnifiedTabByEntity(store, '/repo/recent-target.ts')?.groupId).toBe(editorGroupId)
|
||||
})
|
||||
|
||||
it('keeps explicit target groups ahead of default editor routing', () => {
|
||||
const store = createEditorTabsStore()
|
||||
const { terminalGroupId } = seedTerminalAndEditorGroups(store)
|
||||
|
||||
openSourceFile(store, '/repo/explicit.ts', { targetGroupId: terminalGroupId })
|
||||
|
||||
expect(findUnifiedTabByEntity(store, '/repo/explicit.ts')?.groupId).toBe(terminalGroupId)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createEditorSlice untitled cleanup routing', () => {
|
||||
const runtimeEnvironmentCallMock = vi.fn()
|
||||
const runtimeEnvironmentTransportCallMock = vi.fn()
|
||||
|
|
|
|||
|
|
@ -545,10 +545,7 @@ function openWorkspaceEditorItem(
|
|||
isPreview?: boolean,
|
||||
targetGroupId?: string
|
||||
): string {
|
||||
const resolvedGroupId =
|
||||
targetGroupId ??
|
||||
state.activeGroupIdByWorktree?.[worktreeId] ??
|
||||
state.groupsByWorktree?.[worktreeId]?.[0]?.id
|
||||
const resolvedGroupId = resolveEditorOpenTargetGroupId(state, worktreeId, targetGroupId)
|
||||
if (resolvedGroupId) {
|
||||
const existing = state.findTabForEntityInGroup?.(
|
||||
worktreeId,
|
||||
|
|
@ -570,6 +567,82 @@ function openWorkspaceEditorItem(
|
|||
return created?.id ?? fileId
|
||||
}
|
||||
|
||||
function isEditorTabContentType(contentType: Tab['contentType']): boolean {
|
||||
return contentType === 'editor' || contentType === 'diff' || contentType === 'conflict-review'
|
||||
}
|
||||
|
||||
function getGroupActiveTab(group: TabGroup, tabsById: Map<string, Tab>): Tab | null {
|
||||
return group.activeTabId ? (tabsById.get(group.activeTabId) ?? null) : null
|
||||
}
|
||||
|
||||
function getMostRecentEditorTabForGroup(group: TabGroup, tabsById: Map<string, Tab>): Tab | null {
|
||||
const seen = new Set<string>()
|
||||
const candidateIdLists = [group.recentTabIds ?? [], group.tabOrder]
|
||||
for (const candidateIds of candidateIdLists) {
|
||||
for (let index = candidateIds.length - 1; index >= 0; index -= 1) {
|
||||
const tabId = candidateIds[index]
|
||||
if (!tabId || seen.has(tabId)) {
|
||||
continue
|
||||
}
|
||||
seen.add(tabId)
|
||||
const tab = tabsById.get(tabId)
|
||||
if (tab?.groupId === group.id && isEditorTabContentType(tab.contentType)) {
|
||||
return tab
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function resolveEditorOpenTargetGroupId(
|
||||
state: Pick<AppState, 'activeGroupIdByWorktree' | 'groupsByWorktree' | 'unifiedTabsByWorktree'>,
|
||||
worktreeId: string,
|
||||
explicitTargetGroupId?: string
|
||||
): string | undefined {
|
||||
if (explicitTargetGroupId) {
|
||||
return explicitTargetGroupId
|
||||
}
|
||||
|
||||
const groups = state.groupsByWorktree?.[worktreeId] ?? []
|
||||
if (groups.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const fallbackGroup = groups[0]
|
||||
if (!fallbackGroup) {
|
||||
return undefined
|
||||
}
|
||||
const tabsById = new Map(
|
||||
(state.unifiedTabsByWorktree?.[worktreeId] ?? []).map((tab) => [tab.id, tab])
|
||||
)
|
||||
const activeGroup =
|
||||
groups.find((group) => group.id === state.activeGroupIdByWorktree?.[worktreeId]) ??
|
||||
fallbackGroup
|
||||
const activeTab = getGroupActiveTab(activeGroup, tabsById)
|
||||
if (!activeTab || isEditorTabContentType(activeTab.contentType)) {
|
||||
return activeGroup.id
|
||||
}
|
||||
|
||||
// Why: file explorer opens should reuse an existing editor pane when the
|
||||
// focused pane is an agent terminal, instead of turning that terminal pane
|
||||
// into an editor tab.
|
||||
const visibleEditorGroup = groups.find((group) => {
|
||||
if (group.id === activeGroup.id) {
|
||||
return false
|
||||
}
|
||||
const groupActiveTab = getGroupActiveTab(group, tabsById)
|
||||
return groupActiveTab ? isEditorTabContentType(groupActiveTab.contentType) : false
|
||||
})
|
||||
if (visibleEditorGroup) {
|
||||
return visibleEditorGroup.id
|
||||
}
|
||||
|
||||
const recentEditorGroup = groups.find(
|
||||
(group) => group.id !== activeGroup.id && getMostRecentEditorTabForGroup(group, tabsById)
|
||||
)
|
||||
return recentEditorGroup?.id ?? activeGroup.id
|
||||
}
|
||||
|
||||
function buildEditorActiveResult(
|
||||
state: Pick<EditorSlice, 'activeFileIdByWorktree' | 'activeTabTypeByWorktree'>,
|
||||
worktreeId: string,
|
||||
|
|
@ -1232,10 +1305,8 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
|||
// scoped to that group. Opening as preview in group B must not evict a
|
||||
// preview tab belonging to group A (split tab groups).
|
||||
const targetGroupId =
|
||||
options?.targetGroupId ??
|
||||
s.activeGroupIdByWorktree?.[worktreeId] ??
|
||||
s.groupsByWorktree?.[worktreeId]?.[0]?.id ??
|
||||
undefined
|
||||
resolveEditorOpenTargetGroupId(s, worktreeId, options?.targetGroupId) ?? undefined
|
||||
editorItemTargetGroupId = targetGroupId
|
||||
const previewTabByEntity = new Map<string, string>()
|
||||
if (targetGroupId) {
|
||||
const tabsForWorktree = s.unifiedTabsByWorktree?.[worktreeId] ?? []
|
||||
|
|
|
|||
Loading…
Reference in New Issue