Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions src/client/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,23 +270,35 @@ export function moveTabToEdge(
const source = leafWithTab(node, tabId)
if (source === undefined) return state
const tab = source.tabs.find(candidate => candidate.id === tabId)!
let emptied = false
let splits = mapLeaf(node, source.id, (leaf) => {
leaf.tabs = leaf.tabs.filter(candidate => candidate.id !== tabId)
if (leaf.active === tabId) leaf.active = leaf.tabs[leaf.tabs.length - 1]?.id ?? null
if (leaf.tabs.length === 0) emptied = true
})
if (emptied) splits = removeLeafAt(splits, source.id)
if (zone === 'center') {
let emptied = false
let splits = mapLeaf(node, source.id, (leaf) => {
leaf.tabs = leaf.tabs.filter(candidate => candidate.id !== tabId)
if (leaf.active === tabId) leaf.active = leaf.tabs[leaf.tabs.length - 1]?.id ?? null
if (leaf.tabs.length === 0) emptied = true
})
if (emptied) splits = removeLeafAt(splits, source.id)
splits = mapLeaf(splits, toPane, (leaf) => {
leaf.tabs = [...leaf.tabs, tab]
leaf.active = tab.id
})
return { ...state, bottomSplits: splits, activePane: toPane }
}
// Edge zones split the target pane first, then lift the tab out of its
// source pane. Doing it the other way around breaks a self-drop: removing
// the tab empties the source leaf and removeLeafAt deletes it from the
// tree, so insertLeafAt targets an id that no longer exists and the
// dragged tab is silently discarded.
const dir = zone === 'left' || zone === 'right' ? 'row' : 'col'
const result = insertLeafAt(splits, toPane, dir, tab, zone === 'left' || zone === 'up')
return { ...state, bottomSplits: result.node, activePane: result.leafId }
const inserted = insertLeafAt(node, toPane, dir, tab, zone === 'left' || zone === 'up')
let emptied = false
let splits = mapLeaf(inserted.node, source.id, (leaf) => {
leaf.tabs = leaf.tabs.filter(candidate => candidate.id !== tabId)
if (leaf.active === tabId) leaf.active = leaf.tabs[leaf.tabs.length - 1]?.id ?? null
if (leaf.tabs.length === 0) emptied = true
})
if (emptied) splits = removeLeafAt(splits, source.id)
return { ...state, bottomSplits: splits, activePane: inserted.leafId }
}

/**
Expand Down
34 changes: 34 additions & 0 deletions tests/state.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,40 @@ describe('sidebar state', () => {
expect(after.tabs).toHaveLength(2)
})

it('dragging a pane\'s only tab onto its own edge splits in place instead of losing the tab', () => {
let s = state()
s = openTabInBottomPane(s, { id: 'git', type: 'git', title: 'Git' })
s = splitPane(s, 'col')
const split = s.bottomSplits as Extract<SplitNode, { kind: 'split' }>
const paneA = split.children[0] as { id: string; tabs: { id: string }[] }
const paneB = split.children[1] as { id: string }
const tabId = paneA.tabs[0]!.id
s = moveTab(s, paneA.id, tabId, paneB.id)
// paneB now holds the single tab; dropping it onto paneB's own edge
// used to empty paneB, delete its leaf, and discard the tab entirely.
s = moveTabToEdge(s, paneB.id, tabId, paneB.id, 'left')
const leaves = allLeaves(s.bottomSplits)
expect(leaves).toHaveLength(1)
expect(leaves[0]!.tabs.map(t => t.id)).toEqual([tabId])
expect(s.activePane).toBe(leaves[0]!.id)
})

it('dragging one of several tabs onto its own pane edge splits the pane with the dragged tab', () => {
let s = state()
s = openTabInBottomPane(s, { id: 'git', type: 'git', title: 'Git' })
s = openTabInBottomPane(s, { id: 't2', type: 'terminal', title: 'T2' })
const leaf = s.bottomSplits as { id: string; tabs: { id: string }[] }
const first = leaf.tabs[0]!.id
s = moveTabToEdge(s, leaf.id, first, leaf.id, 'right')
const leaves = allLeaves(s.bottomSplits)
expect(leaves).toHaveLength(2)
const dragged = leaves.find(candidate => candidate.tabs.some(t => t.id === first))
const kept = leaves.find(candidate => candidate !== dragged)
expect(dragged!.tabs.map(t => t.id)).toEqual([first])
expect(kept!.tabs.map(t => t.id)).toEqual([leaf.tabs[1]!.id])
expect(s.activePane).toBe(dragged!.id)
})

it('closing the last tab removes the pane (promotes the sibling)', () => {
let s = state()
s = openTabInBottomPane(s, { id: 'git', type: 'git', title: 'Git' })
Expand Down