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
53 changes: 45 additions & 8 deletions filter/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"slices"
"strconv"
"strings"

"github.com/charmbracelet/bubbles/help"
Expand Down Expand Up @@ -84,7 +85,14 @@ func (o Options) Run() error {
}

if o.SelectIfOne && len(matches) == 1 {
tty.Println(matches[0].Str)
if o.OutputIndexes {
idx := o.findIndex(matches[0].Str, filteringChoices)
if idx >= 0 {
tty.Println(strconv.Itoa(idx))
}
} else {
tty.Println(matches[0].Str)
}
return nil
}

Expand Down Expand Up @@ -157,18 +165,47 @@ func (o Options) Run() error {
// than 1 or if flag --no-limit is passed, hence there is
// no need to further checks
if len(m.selected) > 0 {
o.checkSelected(m)
o.checkSelected(m, filteringChoices)
} else if len(m.matches) > m.cursor && m.cursor >= 0 {
tty.Println(m.matches[m.cursor].Str)
if o.OutputIndexes {
idx := o.findIndex(m.matches[m.cursor].Str, filteringChoices)
if idx >= 0 {
tty.Println(strconv.Itoa(idx))
}
} else {
tty.Println(m.matches[m.cursor].Str)
}
}

return nil
}

func (o Options) checkSelected(m model) {
out := []string{}
for k := range m.selected {
out = append(out, k)
func (o Options) checkSelected(m model, filteringChoices []string) {
if o.OutputIndexes {
// For each selected item, find all indexes in filteringChoices (handles duplicates)
indexes := make([]string, 0, len(m.selected))
for k := range m.selected {
for i, choice := range filteringChoices {
if choice == k {
indexes = append(indexes, strconv.Itoa(i))
}
Comment on lines +188 to +191
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nested loop in checkSelected has O(n*m) complexity where n is the number of selected items and m is the total number of choices. For multiple selections with many choices, this could be inefficient. Consider using the existing findIndex helper method which provides the same functionality but with clearer intent: idx := o.findIndex(k, filteringChoices); if idx >= 0 { indexes = append(indexes, strconv.Itoa(idx)) }

Suggested change
for i, choice := range filteringChoices {
if choice == k {
indexes = append(indexes, strconv.Itoa(i))
}
idx := o.findIndex(k, filteringChoices)
if idx >= 0 {
indexes = append(indexes, strconv.Itoa(idx))

Copilot uses AI. Check for mistakes.
}
}
Comment on lines +185 to +193
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states this 'handles duplicates', but the implementation will output multiple indexes for duplicate values in filteringChoices. Since m.selected is a map with unique keys, if the same value appears multiple times in the original choices list, all matching indexes will be output. This behavior may be unexpected - if a user selects one item that appears at positions 2 and 5, both indexes will be returned. The single-selection path at lines 89-92 and 171-174 uses findIndex which only returns the first occurrence. Consider documenting this behavior difference or making it consistent.

Suggested change
// For each selected item, find all indexes in filteringChoices (handles duplicates)
indexes := make([]string, 0, len(m.selected))
for k := range m.selected {
for i, choice := range filteringChoices {
if choice == k {
indexes = append(indexes, strconv.Itoa(i))
}
}
}
// For each selected item, find the first index in filteringChoices (consistent with single-selection)
indexes := make([]string, 0, len(m.selected))
for k := range m.selected {
idx := o.findIndex(k, filteringChoices)
if idx >= 0 {
indexes = append(indexes, strconv.Itoa(idx))
}
}

Copilot uses AI. Check for mistakes.
tty.Println(strings.Join(indexes, o.OutputDelimiter))
} else {
out := []string{}
for k := range m.selected {
out = append(out, k)
}
tty.Println(strings.Join(out, o.OutputDelimiter))
}
}

func (o Options) findIndex(value string, choices []string) int {
for i, choice := range choices {
if choice == value {
return i
}
}
tty.Println(strings.Join(out, o.OutputDelimiter))
return -1 // Value not found in choices
}
26 changes: 26 additions & 0 deletions filter/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ import (
"github.com/charmbracelet/x/ansi"
)

func TestFindIndex(t *testing.T) {
opts := Options{}
choices := []string{"apple", "banana", "cherry", "date"}

tests := []struct {
name string
value string
expected int
}{
{"first item", "apple", 0},
{"second item", "banana", 1},
{"third item", "cherry", 2},
{"last item", "date", 3},
{"not found", "grape", -1},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := opts.findIndex(tt.value, choices)
if result != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, result)
}
})
}
}

func TestMatchedRanges(t *testing.T) {
for name, tt := range map[string]struct {
in []int
Expand Down
1 change: 1 addition & 0 deletions filter/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type Options struct {
OutputDelimiter string `help:"Option delimiter when writing to STDOUT" default:"\n" env:"GUM_FILTER_OUTPUT_DELIMITER"`
StripANSI bool `help:"Strip ANSI sequences when reading from STDIN" default:"true" negatable:"" env:"GUM_FILTER_STRIP_ANSI"`
Padding string `help:"Padding" default:"${defaultPadding}" group:"Style Flags" env:"GUM_FILTER_PADDING"`
OutputIndexes bool `help:"Output 0-based indexes of selected items instead of values" default:"false" env:"GUM_FILTER_OUTPUT_INDEXES"`

// Deprecated: use [FuzzySort]. This will be removed at some point.
Sort bool `help:"Sort fuzzy results by their scores" default:"true" env:"GUM_FILTER_FUZZY_SORT" negatable:"" hidden:""`
Expand Down
Loading