Skip to content

Commit 8bca7cb

Browse files
committed
fix: invalidate pageStartIndices on filter change in targetHeight mode
1 parent 88d0435 commit 8bca7cb

3 files changed

Lines changed: 125 additions & 0 deletions

File tree

table/options.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,10 @@ func (m Model) WithFilterInput(input textinput.Model) Model {
440440
m.filterTextInput = input
441441
m.visibleRowCacheUpdated = false
442442

443+
if m.targetHeight != 0 {
444+
m.pageStartIndices = nil
445+
}
446+
443447
return m
444448
}
445449

@@ -455,6 +459,10 @@ func (m Model) WithFilterInputValue(value string) Model {
455459
m.filterTextInput.Blur()
456460
m.visibleRowCacheUpdated = false
457461

462+
if m.targetHeight != 0 {
463+
m.pageStartIndices = nil
464+
}
465+
458466
return m
459467
}
460468

table/target_height_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"strings"
66
"testing"
77

8+
"charm.land/bubbles/v2/textinput"
9+
tea "charm.land/bubbletea/v2"
810
"github.com/stretchr/testify/assert"
911
"github.com/stretchr/testify/require"
1012
)
@@ -448,6 +450,113 @@ func TestTargetHeightPageMapInvalidatedByOptions(t *testing.T) {
448450
}
449451
}
450452

453+
// TestTargetHeightFilterReducesRowsBelowOnePage is a regression test for a
454+
// panic caused by pageStartIndices not being invalidated when a filter is
455+
// applied in targetHeight mode. The page map is built by pointer-receiver
456+
// navigation calls (e.g. pageDown) and cached on the model. A subsequent
457+
// filter that shrinks visible rows leaves the stale map in place; ensurePageMap
458+
// then skips rebuilding, VisibleIndices returns an endRowIndex beyond the
459+
// filtered slice, and renderRow panics with an index out of range.
460+
func TestTargetHeightFilterReducesRowsBelowOnePage(t *testing.T) {
461+
// targetHeight=10 gives 4 rows per page (metaHeight=5, 1 border line).
462+
// 8 rows spans two pages; a navigation call seeds the stale page map;
463+
// the filter then reduces visible rows to 1 (< one page).
464+
rows := make([]Row, 8)
465+
for i := range rows {
466+
rows[i] = NewRow(RowData{
467+
"id": i + 1,
468+
"content": fmt.Sprintf("item-%d", i+1),
469+
})
470+
}
471+
472+
model := New([]Column{
473+
NewColumn("id", "ID", 3),
474+
NewColumn("content", "Content", 20).WithFiltered(true),
475+
}).
476+
WithRows(rows).
477+
WithTargetHeight(10).
478+
Filtered(true)
479+
480+
// pageDown is a pointer receiver: it calls ensurePageMap on the model
481+
// directly, seeding pageStartIndices for 8 unfiltered rows ([0, 4]).
482+
model.pageDown()
483+
484+
// Applying the filter sets visibleRowCacheUpdated=false but (before the
485+
// fix) leaves pageStartIndices=[0,4] intact.
486+
model = model.WithFilterInputValue("item-3")
487+
488+
// View must not panic even though only 1 row is visible.
489+
assert.NotPanics(t, func() {
490+
_ = model.View()
491+
})
492+
}
493+
494+
// TestTargetHeightFilterViaWithFilterInput ensures View does not panic when
495+
// WithFilterInput reduces visible rows below one page in targetHeight mode.
496+
func TestTargetHeightFilterViaWithFilterInput(t *testing.T) {
497+
rows := make([]Row, 8)
498+
for i := range rows {
499+
rows[i] = NewRow(RowData{"id": i + 1, "content": fmt.Sprintf("item-%d", i+1)})
500+
}
501+
502+
model := New([]Column{
503+
NewColumn("id", "ID", 3),
504+
NewColumn("content", "Content", 20).WithFiltered(true),
505+
}).WithRows(rows).WithTargetHeight(10).Filtered(true)
506+
507+
model.pageDown()
508+
509+
input := textinput.New()
510+
input.SetValue("item-3")
511+
model = model.WithFilterInput(input)
512+
513+
assert.NotPanics(t, func() { _ = model.View() })
514+
}
515+
516+
// TestTargetHeightFilterViaUpdateFilterTextInput ensures View does not panic
517+
// when typing in the filter input reduces visible rows below one page in
518+
// targetHeight mode.
519+
func TestTargetHeightFilterViaUpdateFilterTextInput(t *testing.T) {
520+
rows := make([]Row, 8)
521+
for i := range rows {
522+
rows[i] = NewRow(RowData{"id": i + 1, "content": fmt.Sprintf("item-%d", i+1)})
523+
}
524+
525+
model := New([]Column{
526+
NewColumn("id", "ID", 3),
527+
NewColumn("content", "Content", 20).WithFiltered(true),
528+
}).WithRows(rows).WithTargetHeight(10).Filtered(true)
529+
530+
model.pageDown()
531+
532+
model.filterTextInput.SetValue("item-3")
533+
model, _ = model.updateFilterTextInput(tea.KeyPressMsg{})
534+
535+
assert.NotPanics(t, func() { _ = model.View() })
536+
}
537+
538+
// TestTargetHeightFilterClearKey ensures View does not panic after the filter
539+
// is cleared via the FilterClear key in targetHeight mode.
540+
func TestTargetHeightFilterClearKey(t *testing.T) {
541+
rows := make([]Row, 8)
542+
for i := range rows {
543+
rows[i] = NewRow(RowData{"id": i + 1, "content": fmt.Sprintf("item-%d", i+1)})
544+
}
545+
546+
model := New([]Column{
547+
NewColumn("id", "ID", 3),
548+
NewColumn("content", "Content", 20).WithFiltered(true),
549+
}).WithRows(rows).WithTargetHeight(10).Filtered(true)
550+
551+
model.filterTextInput.SetValue("item-3")
552+
model.visibleRowCacheUpdated = false
553+
model.pageDown()
554+
555+
model.handleKeypress(tea.KeyPressMsg{Code: tea.KeyEscape})
556+
557+
assert.NotPanics(t, func() { _ = model.View() })
558+
}
559+
451560
// TestTargetHeightWithCurrentPageAboveMax checks that WithCurrentPage clamps
452561
// values above MaxPages to the last page in targetHeight mode.
453562
func TestTargetHeightWithCurrentPageAboveMax(t *testing.T) {

table/update.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ func (m Model) updateFilterTextInput(msg tea.Msg) (Model, tea.Cmd) {
6363
m.pageFirst()
6464
m.visibleRowCacheUpdated = false
6565

66+
if m.targetHeight != 0 {
67+
m.pageStartIndices = nil
68+
}
69+
6670
return m, cmd
6771
}
6872

@@ -108,6 +112,10 @@ func (m *Model) handleKeypress(msg tea.KeyPressMsg) {
108112
if key.Matches(msg, m.keyMap.FilterClear) {
109113
m.visibleRowCacheUpdated = false
110114
m.filterTextInput.Reset()
115+
116+
if m.targetHeight != 0 {
117+
m.pageStartIndices = nil
118+
}
111119
}
112120

113121
if key.Matches(msg, m.keyMap.ScrollRight) {

0 commit comments

Comments
 (0)