detached efa1a9f

fix(core): restore pushed cascade columns by first window, in order

A pushed column is found by its first window being one of its remembered
windows, so a window that left it no longer brings its new column back.
Mixed width-cycle and maximize pushes keep the original neighbor order.
commit
efa1a9f404336dc913cc711e6f2027c44c983d8b
author
brice · · 2026-10-16 16:57 UTC
parent
0e73719
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +33 -10
M internal/core/cascade.go +10 -10
@@ -351,7 +351,8 @@ func (w *Workspace) shiftBand(lo, hi, dir, at int) bool {
351351 }
352352
353353 // columnIDs lists the windows of each column, its identity for
354-// restoreBand: any of them still together finds the column.
354+// restoreBand: a column whose first window is one of them is that column,
355+// even after its first window closed or the stack changed order.
355356 func columnIDs(cols []Column) [][]WindowID {
356357 ids := make([][]WindowID, len(cols))
357358 for i, c := range cols {
@@ -375,10 +376,11 @@ func (w *Workspace) isolateBand() {
375376 left := slices.Clone(bands[b][:p])
376377 right := slices.Clone(bands[b][p+1:])
377378 c := bands[b][p]
378- // Earlier pushes (cycle-column-width) stay remembered, farther out:
379- // pushedUp ends nearest, pushedDown starts nearest.
380- c.pushedUp = append(c.pushedUp, columnIDs(left)...)
381- c.pushedDown = append(columnIDs(right), c.pushedDown...)
379+ // Earlier pushes (cycle-column-width) stay remembered and were nearer
380+ // at first, so the new ones go past them: pushedUp ends nearest,
381+ // pushedDown starts nearest.
382+ c.pushedUp = append(columnIDs(left), c.pushedUp...)
383+ c.pushedDown = append(c.pushedDown, columnIDs(right)...)
382384 bands[b] = []Column{c}
383385 if len(right) > 0 {
384386 bands = w.sendBand(bands, b, 1, 0, right)
@@ -400,10 +402,10 @@ func (w *Workspace) pushNeighbor() {
400402 id, _ := w.Focused()
401403 c := &w.Columns[w.Focus]
402404 if w.Focus < last {
403- c.pushedDown = append(columnIDs(w.Columns[w.Focus+1:w.Focus+2]), c.pushedDown...)
405+ c.pushedDown = append(c.pushedDown, columnIDs(w.Columns[w.Focus+1:w.Focus+2])...)
404406 w.shiftBand(w.Focus+1, w.Focus+1, 1, 0)
405407 } else {
406- c.pushedUp = append(c.pushedUp, columnIDs(w.Columns[w.Focus-1:w.Focus])...)
408+ c.pushedUp = append(columnIDs(w.Columns[w.Focus-1:w.Focus]), c.pushedUp...)
407409 w.shiftBand(w.Focus-1, w.Focus-1, -1, max(w.MaxColumns, 1))
408410 }
409411 w.Focus = w.columnOf(id)
@@ -441,9 +443,7 @@ func (w *Workspace) restoreBand() bool {
441443 if rev {
442444 e = ids[len(ids)-1-k]
443445 }
444- i := slices.IndexFunc(bands[nb], func(c Column) bool {
445- return slices.ContainsFunc(c.Windows, func(v WindowID) bool { return slices.Contains(e, v) })
446- })
446+ i := slices.IndexFunc(bands[nb], func(c Column) bool { return slices.Contains(e, c.Windows[0]) })
447447 if i < 0 {
448448 continue
449449 }
@@ -351,7 +351,8 @@ func (w *Workspace) shiftBand(lo, hi, dir, at int) bool {
351 }351 }
352 352
353 // columnIDs lists the windows of each column, its identity for353 // columnIDs lists the windows of each column, its identity for
354-// restoreBand: any of them still together finds the column.354+// restoreBand: a column whose first window is one of them is that column,
355+// even after its first window closed or the stack changed order.
355 func columnIDs(cols []Column) [][]WindowID {356 func columnIDs(cols []Column) [][]WindowID {
356 ids := make([][]WindowID, len(cols))357 ids := make([][]WindowID, len(cols))
357 for i, c := range cols {358 for i, c := range cols {
@@ -375,10 +376,11 @@ func (w *Workspace) isolateBand() {
375 left := slices.Clone(bands[b][:p])376 left := slices.Clone(bands[b][:p])
376 right := slices.Clone(bands[b][p+1:])377 right := slices.Clone(bands[b][p+1:])
377 c := bands[b][p]378 c := bands[b][p]
378- // Earlier pushes (cycle-column-width) stay remembered, farther out:379+ // Earlier pushes (cycle-column-width) stay remembered and were nearer
379- // pushedUp ends nearest, pushedDown starts nearest.380+ // at first, so the new ones go past them: pushedUp ends nearest,
380- c.pushedUp = append(c.pushedUp, columnIDs(left)...)381+ // pushedDown starts nearest.
381- c.pushedDown = append(columnIDs(right), c.pushedDown...)382+ c.pushedUp = append(columnIDs(left), c.pushedUp...)
383+ c.pushedDown = append(c.pushedDown, columnIDs(right)...)
382 bands[b] = []Column{c}384 bands[b] = []Column{c}
383 if len(right) > 0 {385 if len(right) > 0 {
384 bands = w.sendBand(bands, b, 1, 0, right)386 bands = w.sendBand(bands, b, 1, 0, right)
@@ -400,10 +402,10 @@ func (w *Workspace) pushNeighbor() {
400 id, _ := w.Focused()402 id, _ := w.Focused()
401 c := &w.Columns[w.Focus]403 c := &w.Columns[w.Focus]
402 if w.Focus < last {404 if w.Focus < last {
403- c.pushedDown = append(columnIDs(w.Columns[w.Focus+1:w.Focus+2]), c.pushedDown...)405+ c.pushedDown = append(c.pushedDown, columnIDs(w.Columns[w.Focus+1:w.Focus+2])...)
404 w.shiftBand(w.Focus+1, w.Focus+1, 1, 0)406 w.shiftBand(w.Focus+1, w.Focus+1, 1, 0)
405 } else {407 } else {
406- c.pushedUp = append(c.pushedUp, columnIDs(w.Columns[w.Focus-1:w.Focus])...)408+ c.pushedUp = append(columnIDs(w.Columns[w.Focus-1:w.Focus]), c.pushedUp...)
407 w.shiftBand(w.Focus-1, w.Focus-1, -1, max(w.MaxColumns, 1))409 w.shiftBand(w.Focus-1, w.Focus-1, -1, max(w.MaxColumns, 1))
408 }410 }
409 w.Focus = w.columnOf(id)411 w.Focus = w.columnOf(id)
@@ -441,9 +443,7 @@ func (w *Workspace) restoreBand() bool {
441 if rev {443 if rev {
442 e = ids[len(ids)-1-k]444 e = ids[len(ids)-1-k]
443 }445 }
444- i := slices.IndexFunc(bands[nb], func(c Column) bool {446+ i := slices.IndexFunc(bands[nb], func(c Column) bool { return slices.Contains(e, c.Windows[0]) })
445- return slices.ContainsFunc(c.Windows, func(v WindowID) bool { return slices.Contains(e, v) })
446- })
447 if i < 0 {447 if i < 0 {
448 continue448 continue
449 }449 }
M internal/core/cascade_bands_test.go +23 -0
@@ -201,6 +201,29 @@ func TestCascadeRestoreBandMemory(t *testing.T) {
201201 w.ToggleFullWidth() // {1 2 3}
202202 wantBands(t, w, []WindowID{1, 2, 3})
203203 })
204+ t.Run("cycle then maximize from either end keeps the order", func(t *testing.T) {
205+ for _, focus := range []WindowID{1, 3} {
206+ _, w := cascadeMovesCore(t, 3, 3, false)
207+ w.FocusID(focus)
208+ w.Apply(ActionCycleColumnWidth)
209+ w.ToggleFullWidth()
210+ w.ToggleFullWidth()
211+ wantBands(t, w, []WindowID{1, 2, 3})
212+ }
213+ })
214+ t.Run("a window that left the pushed column does not bring its new column", func(t *testing.T) {
215+ _, w := cascadeMovesCore(t, 5, 4, false) // {1 2 3 4} {5}
216+ w.FocusID(4)
217+ w.ConsumeOrExpel(-1) // {1 2 3/4} {5}
218+ w.FocusID(2)
219+ w.ToggleFullWidth() // {1} {2*} {3/4 5}
220+ w.stackWindow(4, 5) // {1} {2*} {3 5/4}
221+ w.FocusID(5)
222+ w.MoveColumn(-1) // {1} {2*} {5/4 3}
223+ w.FocusID(2)
224+ w.ToggleFullWidth()
225+ wantBands(t, w, []WindowID{1, 2, 3}, []WindowID{5})
226+ })
204227 t.Run("first window of a pushed column closed", func(t *testing.T) {
205228 _, w := cascadeMovesCore(t, 4, 3, false) // {1 2 3} {4}
206229 w.FocusID(4)
@@ -201,6 +201,29 @@ func TestCascadeRestoreBandMemory(t *testing.T) {
201 w.ToggleFullWidth() // {1 2 3}201 w.ToggleFullWidth() // {1 2 3}
202 wantBands(t, w, []WindowID{1, 2, 3})202 wantBands(t, w, []WindowID{1, 2, 3})
203 })203 })
204+ t.Run("cycle then maximize from either end keeps the order", func(t *testing.T) {
205+ for _, focus := range []WindowID{1, 3} {
206+ _, w := cascadeMovesCore(t, 3, 3, false)
207+ w.FocusID(focus)
208+ w.Apply(ActionCycleColumnWidth)
209+ w.ToggleFullWidth()
210+ w.ToggleFullWidth()
211+ wantBands(t, w, []WindowID{1, 2, 3})
212+ }
213+ })
214+ t.Run("a window that left the pushed column does not bring its new column", func(t *testing.T) {
215+ _, w := cascadeMovesCore(t, 5, 4, false) // {1 2 3 4} {5}
216+ w.FocusID(4)
217+ w.ConsumeOrExpel(-1) // {1 2 3/4} {5}
218+ w.FocusID(2)
219+ w.ToggleFullWidth() // {1} {2*} {3/4 5}
220+ w.stackWindow(4, 5) // {1} {2*} {3 5/4}
221+ w.FocusID(5)
222+ w.MoveColumn(-1) // {1} {2*} {5/4 3}
223+ w.FocusID(2)
224+ w.ToggleFullWidth()
225+ wantBands(t, w, []WindowID{1, 2, 3}, []WindowID{5})
226+ })
204 t.Run("first window of a pushed column closed", func(t *testing.T) {227 t.Run("first window of a pushed column closed", func(t *testing.T) {
205 _, w := cascadeMovesCore(t, 4, 3, false) // {1 2 3} {4}228 _, w := cascadeMovesCore(t, 4, 3, false) // {1 2 3} {4}
206 w.FocusID(4)229 w.FocusID(4)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close