Skip to content

Commit e269ea3

Browse files
committed
Consolidate paginated search state into searchState struct
paginatedModel had 9 scattered fields handling server-side search (searching, searchLoading, searchInput, debounceSeq, hasSearchState, savedRows, savedIter, savedExhaust, limitReached was left as its own thing). Many of the recent fixes in this PR's history were races between these fields getting out of sync. Group them into a searchState struct with an optional *savedSearch sub-struct for the pre-search snapshot. restorePreSearchState and executeSearch become markedly simpler because "there is no saved state" is one nil check instead of four zeroed fields. Also consolidate a few trivial table tests (View, renderFooter, search input keys, fetch) into table-driven form. Co-authored-by: Isaac
1 parent 4981680 commit e269ea3

2 files changed

Lines changed: 296 additions & 292 deletions

File tree

libs/tableview/paginated.go

Lines changed: 55 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -64,20 +64,30 @@ type paginatedModel struct {
6464
widths []int
6565

6666
// Search
67-
searching bool
68-
searchLoading bool
69-
searchInput string
70-
debounceSeq int
71-
hasSearchState bool
72-
savedRows [][]string
73-
savedIter RowIterator
74-
savedExhaust bool
67+
search searchState
7568

7669
// Limits
7770
maxItems int
7871
limitReached bool
7972
}
8073

74+
// searchState groups the server-side search / debounce state.
75+
// When a search replaces the original iterator, saved holds the
76+
// pre-search snapshot so it can be restored on cancel/clear.
77+
type searchState struct {
78+
active bool
79+
loading bool
80+
input string
81+
debounceSeq int
82+
saved *savedSearch
83+
}
84+
85+
type savedSearch struct {
86+
rows [][]string
87+
iter RowIterator
88+
exhausted bool
89+
}
90+
8191
// Err returns the error recorded during data fetching, if any.
8292
func (m paginatedModel) Err() error {
8393
return m.err
@@ -166,7 +176,7 @@ func (m paginatedModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
166176
switch msg := msg.(type) {
167177
case tea.WindowSizeMsg:
168178
fh := footerHeight
169-
if m.searching {
179+
if m.search.active {
170180
fh = searchFooterHeight
171181
}
172182
if !m.ready {
@@ -197,9 +207,9 @@ func (m paginatedModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
197207
m.err = nil
198208

199209
isFirstBatch := len(m.rows) == 0
200-
if m.searchLoading {
210+
if m.search.loading {
201211
m.rows = msg.rows
202-
m.searchLoading = false
212+
m.search.loading = false
203213
isFirstBatch = true
204214
} else {
205215
m.rows = append(m.rows, msg.rows...)
@@ -224,13 +234,13 @@ func (m paginatedModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
224234
return m, nil
225235

226236
case searchDebounceMsg:
227-
if msg.seq != m.debounceSeq || !m.searching {
237+
if msg.seq != m.search.debounceSeq || !m.search.active {
228238
return m, nil
229239
}
230-
return m.executeSearch(m.searchInput)
240+
return m.executeSearch(m.search.input)
231241

232242
case tea.KeyMsg:
233-
if m.searching {
243+
if m.search.active {
234244
return m.updateSearch(msg)
235245
}
236246
return m.updateNormal(msg)
@@ -301,8 +311,8 @@ func (m paginatedModel) updateNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
301311
return m, tea.Quit
302312
case "/":
303313
if m.cfg.Search != nil {
304-
m.searching = true
305-
m.searchInput = ""
314+
m.search.active = true
315+
m.search.input = ""
306316
// Shrink viewport by one row to make room for the search input bar.
307317
m.viewport.Height--
308318
return m, nil
@@ -352,7 +362,7 @@ func (m *paginatedModel) moveCursor(delta int) {
352362
}
353363

354364
func maybeFetch(m paginatedModel) (paginatedModel, tea.Cmd) {
355-
if m.loading || m.exhausted || m.searching {
365+
if m.loading || m.exhausted || m.search.active {
356366
return m, nil
357367
}
358368
if len(m.rows)-m.cursor <= fetchThresholdFromBottom {
@@ -364,8 +374,8 @@ func maybeFetch(m paginatedModel) (paginatedModel, tea.Cmd) {
364374

365375
// scheduleSearchDebounce returns a command that sends a searchDebounceMsg after the delay.
366376
func (m *paginatedModel) scheduleSearchDebounce() tea.Cmd {
367-
m.debounceSeq++
368-
seq := m.debounceSeq
377+
m.search.debounceSeq++
378+
seq := m.search.debounceSeq
369379
return tea.Tick(searchDebounceDelay, func(_ time.Time) tea.Msg {
370380
return searchDebounceMsg{seq: seq}
371381
})
@@ -375,20 +385,17 @@ func (m *paginatedModel) scheduleSearchDebounce() tea.Cmd {
375385
// loading so that maybeFetch is unblocked. Safe to call even when there is
376386
// no saved search state.
377387
func (m *paginatedModel) restorePreSearchState() {
378-
if m.hasSearchState {
388+
if m.search.saved != nil {
379389
// Bump generation to discard any in-flight search fetch, since we're
380390
// switching back to the original iterator.
381391
m.fetchGeneration++
382-
m.rows = m.savedRows
383-
m.rowIter = m.savedIter
384-
m.exhausted = m.savedExhaust
385-
m.hasSearchState = false
386-
m.savedRows = nil
387-
m.savedIter = nil
388-
m.savedExhaust = false
392+
m.rows = m.search.saved.rows
393+
m.rowIter = m.search.saved.iter
394+
m.exhausted = m.search.saved.exhausted
395+
m.search.saved = nil
389396
m.limitReached = false
390397
m.loading = false
391-
m.searchLoading = false
398+
m.search.loading = false
392399
}
393400
m.cursor = 0
394401
if m.ready {
@@ -406,18 +413,19 @@ func (m paginatedModel) executeSearch(query string) (tea.Model, tea.Cmd) {
406413
return m, nil
407414
}
408415

409-
if !m.hasSearchState {
410-
m.hasSearchState = true
411-
m.savedRows = m.rows
412-
m.savedIter = m.rowIter
413-
m.savedExhaust = m.exhausted
416+
if m.search.saved == nil {
417+
m.search.saved = &savedSearch{
418+
rows: m.rows,
419+
iter: m.rowIter,
420+
exhausted: m.exhausted,
421+
}
414422
}
415423

416424
m.fetchGeneration++
417425
m.exhausted = false
418426
m.limitReached = false
419427
m.loading = true
420-
m.searchLoading = true
428+
m.search.loading = true
421429
m.cursor = 0
422430
m.rowIter = m.makeSearchIter(query)
423431
return m, m.makeFetchCmd(m)
@@ -426,33 +434,33 @@ func (m paginatedModel) executeSearch(query string) (tea.Model, tea.Cmd) {
426434
func (m paginatedModel) updateSearch(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
427435
switch msg.String() {
428436
case "enter":
429-
m.searching = false
437+
m.search.active = false
430438
// Restore viewport height now that search bar is hidden.
431439
m.viewport.Height++
432440
// Execute final search immediately (bypass debounce).
433-
return m.executeSearch(m.searchInput)
441+
return m.executeSearch(m.search.input)
434442
case "ctrl+c":
435443
return m, tea.Quit
436444
case "esc":
437-
m.searching = false
438-
m.searchInput = ""
445+
m.search.active = false
446+
m.search.input = ""
439447
// Restore viewport height now that search bar is hidden.
440448
m.viewport.Height++
441449
m.restorePreSearchState()
442450
return m, nil
443451
case "backspace":
444-
if len(m.searchInput) > 0 {
445-
_, size := utf8.DecodeLastRuneInString(m.searchInput)
446-
m.searchInput = m.searchInput[:len(m.searchInput)-size]
452+
if len(m.search.input) > 0 {
453+
_, size := utf8.DecodeLastRuneInString(m.search.input)
454+
m.search.input = m.search.input[:len(m.search.input)-size]
447455
}
448456
return m, m.scheduleSearchDebounce()
449457
default:
450458
if msg.Type == tea.KeyRunes {
451-
m.searchInput += msg.String()
459+
m.search.input += msg.String()
452460
return m, m.scheduleSearchDebounce()
453461
}
454462
if msg.Type == tea.KeySpace {
455-
m.searchInput += " "
463+
m.search.input += " "
456464
return m, m.scheduleSearchDebounce()
457465
}
458466
return m, nil
@@ -464,7 +472,7 @@ func (m paginatedModel) View() string {
464472
return "Loading..."
465473
}
466474
if len(m.rows) == 0 && m.loading {
467-
if m.searchLoading {
475+
if m.search.loading {
468476
return "Searching..."
469477
}
470478
return "Fetching results..."
@@ -484,18 +492,18 @@ func (m paginatedModel) View() string {
484492
}
485493

486494
func (m paginatedModel) renderFooter() string {
487-
if m.searching {
495+
if m.search.active {
488496
placeholder := ""
489497
if m.cfg.Search != nil {
490498
placeholder = m.cfg.Search.Placeholder
491499
}
492-
input := m.searchInput
500+
input := m.search.input
493501
if input == "" && placeholder != "" {
494502
input = footerStyle.Render(placeholder)
495503
}
496504
prompt := searchStyle.Render("/ " + input + "█")
497505
status := fmt.Sprintf("%d rows loaded", len(m.rows))
498-
if m.searchLoading {
506+
if m.search.loading {
499507
status = "Searching..."
500508
}
501509
return footerStyle.Render(status) + "\n" + prompt

0 commit comments

Comments
 (0)