fix(picker): rest on the nearest row above when the cursor row stops matching
- commit
- 3bd98ac8fbb8406ab8ed56fc347b3930740e8c09
- author
- brice · · 2026-10-08 20:07 UTC
- parent
- ef274b7
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +35 -11
M internal/usecase/picker/model.go +20 -11
| @@ -253,21 +253,30 @@ func (m *Model) ReplaceLines(lines []protocol.PickerLine, cursor protocol.Picker | |||
| 253 | 253 | key, hadKey = cursor.Key, cursor.Key != "" | |
| 254 | 254 | } | |
| 255 | 255 | m.rebuild(key, hadKey, cursor.Index) | |
| 256 | - | if hadKey && !m.hasRow(key) { | |
| 257 | - | if current, ok := m.cursorKey(); !ok || current != key { | |
| 258 | - | // The selected row left the publication (for example it was | |
| 259 | - | // killed): rest on the nearest surviving row above it instead of | |
| 260 | - | // jumping back to the top. | |
| 261 | - | m.selectFirstKey(above) | |
| 262 | - | } | |
| 256 | + | if hadKey && !m.cursorRestsOn(key) { | |
| 257 | + | // The selected row left the publication (for example it was killed) | |
| 258 | + | // or stopped matching the query: rest on the nearest surviving row | |
| 259 | + | // above it instead of jumping elsewhere. | |
| 260 | + | m.selectFirstKey(above) | |
| 263 | 261 | } | |
| 264 | 262 | } | |
| 265 | 263 | ||
| 266 | - | // hasRow reports whether a row with this id is still published, drawn or not. | |
| 267 | - | func (m *Model) hasRow(id string) bool { | |
| 268 | - | for _, r := range m.rows { | |
| 264 | + | // cursorRestsOn reports whether the cursor is on the row with this id, or on | |
| 265 | + | // the collapsed section that hides it. | |
| 266 | + | func (m *Model) cursorRestsOn(id string) bool { | |
| 267 | + | current, ok := m.cursorKey() | |
| 268 | + | if !ok { | |
| 269 | + | return false | |
| 270 | + | } | |
| 271 | + | if current == id { | |
| 272 | + | return true | |
| 273 | + | } | |
| 274 | + | if !m.rows[m.selected].section() { | |
| 275 | + | return false | |
| 276 | + | } | |
| 277 | + | for idx, r := range m.rows { | |
| 269 | 278 | if r.id() == id { | |
| 270 | - | return true | |
| 279 | + | return m.sectionOf(idx) == m.selected | |
| 271 | 280 | } | |
| 272 | 281 | } | |
| 273 | 282 | return false | |
| @@ -253,21 +253,30 @@ func (m *Model) ReplaceLines(lines []protocol.PickerLine, cursor protocol.Picker | |||||
| 253 | key, hadKey = cursor.Key, cursor.Key != "" | 253 | key, hadKey = cursor.Key, cursor.Key != "" | ||
| 254 | } | 254 | } | ||
| 255 | m.rebuild(key, hadKey, cursor.Index) | 255 | m.rebuild(key, hadKey, cursor.Index) | ||
| 256 | - | if hadKey && !m.hasRow(key) { | 256 | + | if hadKey && !m.cursorRestsOn(key) { |
| 257 | - | if current, ok := m.cursorKey(); !ok || current != key { | 257 | + | // The selected row left the publication (for example it was killed) |
| 258 | - | // The selected row left the publication (for example it was | 258 | + | // or stopped matching the query: rest on the nearest surviving row |
| 259 | - | // killed): rest on the nearest surviving row above it instead of | 259 | + | // above it instead of jumping elsewhere. |
| 260 | - | // jumping back to the top. | 260 | + | m.selectFirstKey(above) |
| 261 | - | m.selectFirstKey(above) | |||
| 262 | - | } | |||
| 263 | } | 261 | } | ||
| 264 | } | 262 | } | ||
| 265 | 263 | ||||
| 266 | - | // hasRow reports whether a row with this id is still published, drawn or not. | 264 | + | // cursorRestsOn reports whether the cursor is on the row with this id, or on |
| 267 | - | func (m *Model) hasRow(id string) bool { | 265 | + | // the collapsed section that hides it. |
| 268 | - | for _, r := range m.rows { | 266 | + | func (m *Model) cursorRestsOn(id string) bool { |
| 267 | + | current, ok := m.cursorKey() | |||
| 268 | + | if !ok { | |||
| 269 | + | return false | |||
| 270 | + | } | |||
| 271 | + | if current == id { | |||
| 272 | + | return true | |||
| 273 | + | } | |||
| 274 | + | if !m.rows[m.selected].section() { | |||
| 275 | + | return false | |||
| 276 | + | } | |||
| 277 | + | for idx, r := range m.rows { | |||
| 269 | if r.id() == id { | 278 | if r.id() == id { | ||
| 270 | - | return true | 279 | + | return m.sectionOf(idx) == m.selected |
| 271 | } | 280 | } | ||
| 272 | } | 281 | } | ||
| 273 | return false | 282 | return false | ||
M internal/usecase/picker/model_test.go +15 -0
| @@ -567,6 +567,21 @@ func TestReplaceLinesPreviousRowRespectsSearch(t *testing.T) { | |||
| 567 | 567 | require.Equal(t, "a", selected.Key, "the hidden row above is skipped for the nearest matching one") | |
| 568 | 568 | } | |
| 569 | 569 | ||
| 570 | + | func TestReplaceLinesPreviousRowWhenTheCursorRowStopsMatching(t *testing.T) { | |
| 571 | + | m := New([]protocol.PickerLine{navLine("a", "alpha"), navLine("b", "beta"), navLine("c", "alps"), navLine("d", "altitude")}, Config{Cursor: protocol.PickerCursor{Key: "c", Index: -1}}) | |
| 572 | + | m.EnterSearch() | |
| 573 | + | for _, r := range "al" { | |
| 574 | + | m.InsertSearch(r) | |
| 575 | + | } | |
| 576 | + | m.Down() // alpha -> alps | |
| 577 | + | require.Equal(t, "c", mustSelectedKey(t, m)) | |
| 578 | + | ||
| 579 | + | // c is still published but no longer matches; the source hints the last row. | |
| 580 | + | m.ReplaceLines([]protocol.PickerLine{navLine("a", "alpha"), navLine("b", "beta"), navLine("c", "cliff"), navLine("d", "altitude")}, protocol.PickerCursor{Index: 3}) | |
| 581 | + | ||
| 582 | + | require.Equal(t, "a", mustSelectedKey(t, m), "the nearest matching row above wins over the source hint") | |
| 583 | + | } | |
| 584 | + | ||
| 570 | 585 | // drawnRows reports the tree prefix and label of every drawn row. | |
| 571 | 586 | func drawnRows(m *Model) []string { | |
| 572 | 587 | got := make([]string, 0, len(m.view)) | |
| @@ -567,6 +567,21 @@ func TestReplaceLinesPreviousRowRespectsSearch(t *testing.T) { | |||||
| 567 | require.Equal(t, "a", selected.Key, "the hidden row above is skipped for the nearest matching one") | 567 | require.Equal(t, "a", selected.Key, "the hidden row above is skipped for the nearest matching one") | ||
| 568 | } | 568 | } | ||
| 569 | 569 | ||||
| 570 | + | func TestReplaceLinesPreviousRowWhenTheCursorRowStopsMatching(t *testing.T) { | |||
| 571 | + | m := New([]protocol.PickerLine{navLine("a", "alpha"), navLine("b", "beta"), navLine("c", "alps"), navLine("d", "altitude")}, Config{Cursor: protocol.PickerCursor{Key: "c", Index: -1}}) | |||
| 572 | + | m.EnterSearch() | |||
| 573 | + | for _, r := range "al" { | |||
| 574 | + | m.InsertSearch(r) | |||
| 575 | + | } | |||
| 576 | + | m.Down() // alpha -> alps | |||
| 577 | + | require.Equal(t, "c", mustSelectedKey(t, m)) | |||
| 578 | + | ||||
| 579 | + | // c is still published but no longer matches; the source hints the last row. | |||
| 580 | + | m.ReplaceLines([]protocol.PickerLine{navLine("a", "alpha"), navLine("b", "beta"), navLine("c", "cliff"), navLine("d", "altitude")}, protocol.PickerCursor{Index: 3}) | |||
| 581 | + | ||||
| 582 | + | require.Equal(t, "a", mustSelectedKey(t, m), "the nearest matching row above wins over the source hint") | |||
| 583 | + | } | |||
| 584 | + | ||||
| 570 | // drawnRows reports the tree prefix and label of every drawn row. | 585 | // drawnRows reports the tree prefix and label of every drawn row. | ||
| 571 | func drawnRows(m *Model) []string { | 586 | func drawnRows(m *Model) []string { | ||
| 572 | got := make([]string, 0, len(m.view)) | 587 | got := make([]string, 0, len(m.view)) | ||