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
18 changes: 17 additions & 1 deletion list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,13 +438,29 @@ func (m *Model) InsertItem(index int, item Item) tea.Cmd {
// RemoveItem removes an item at the given index. If the index is out of bounds
// this will be a no-op. O(n) complexity, which probably won't matter in the
// case of a TUI.
//
// When a filter is active, index refers to the position in the filtered list.
// The corresponding item is removed from both the filtered list and the
// underlying unfiltered list.
func (m *Model) RemoveItem(index int) {
m.items = removeItemFromSlice(m.items, index)
if m.filterState != Unfiltered {
if index >= len(m.filteredItems) {
return // noop
}
globalIndex := m.filteredItems[index].index
m.items = removeItemFromSlice(m.items, globalIndex)
m.filteredItems = removeFilterMatchFromSlice(m.filteredItems, index)
// Keep stored global indices consistent after the removal.
for i := range m.filteredItems {
if m.filteredItems[i].index > globalIndex {
m.filteredItems[i].index--
}
}
if len(m.filteredItems) == 0 {
m.resetFiltering()
}
} else {
m.items = removeItemFromSlice(m.items, index)
}
m.updatePagination()
}
Expand Down
51 changes: 51 additions & 0 deletions list/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,54 @@ func TestSetFilterState(t *testing.T) {
t.Fatalf("Error: expected view to contain '%s'", expected)
}
}

func TestRemoveItemWhileFiltered(t *testing.T) {
// items: foo(0), bar(1), baz(2)
// filter "ba" → filtered: bar(global 1), baz(global 2)
items := []Item{item("foo"), item("bar"), item("baz")}
list := New(items, itemDelegate{}, 10, 10)
list.SetFilterText("ba")

// Sanity: filtered list is [bar, baz]
if got := list.VisibleItems(); !reflect.DeepEqual(got, []Item{item("bar"), item("baz")}) {
t.Fatalf("setup: expected filtered [bar baz], got %v", got)
}

// RemoveItem(0) should remove "bar" (filtered index 0 → global index 1)
list.RemoveItem(0)

// Filtered list should now be [baz]
if got := list.VisibleItems(); !reflect.DeepEqual(got, []Item{item("baz")}) {
t.Fatalf("filtered view after remove: expected [baz], got %v", got)
}

// Reset filter — global list should be [foo, baz]
list.ResetFilter()
if got := list.Items(); !reflect.DeepEqual(got, []Item{item("foo"), item("baz")}) {
t.Fatalf("global list after remove+reset: expected [foo baz], got %v", got)
}
}

func TestRemoveItemWhileFilteredUpdatesIndices(t *testing.T) {
// items: foo(0), bar(1), baz(2)
// filter "ba" → filtered: bar(global 1), baz(global 2)
// Remove bar (filtered index 0, global index 1).
// After removal baz should still be reachable (global index now 1).
items := []Item{item("foo"), item("bar"), item("baz")}
list := New(items, itemDelegate{}, 10, 10)
list.SetFilterText("ba")
list.RemoveItem(0)

// baz is the only remaining filtered item; remove it too
list.RemoveItem(0)

// filter should have been reset (no filtered items left)
if list.FilterState() != Unfiltered {
t.Fatalf("expected filter to reset when no items remain, got state %v", list.FilterState())
}

// Only "foo" should remain
if got := list.Items(); !reflect.DeepEqual(got, []Item{item("foo")}) {
t.Fatalf("global list after removing all filtered items: expected [foo], got %v", got)
}
}