feat(palette): show and search visible command fields
- commit
- 916eeb25bed6c92e4b55019dbd26040c44dd86ab
- author
- brice · · 2026-07-09 23:27 UTC
- committer
- brice · · 2026-07-09 23:28 UTC
- parent
- 31971ae
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +51 -12
M internal/usecase/palette/fuzzy.go +1 -1
| @@ -63,7 +63,7 @@ func score(cmd command.Command, query string, order int) (Match, bool) { | |||
| 63 | 63 | if positions, ok := subsequencePositions([]rune(codeLower), []rune(qLower)); ok { | |
| 64 | 64 | return Match{Command: cmd, Positions: positions, rank: 2, span: positions[len(positions)-1] - positions[0] + 1, first: positions[0], order: order}, true | |
| 65 | 65 | } | |
| 66 | - | text := strings.ToLower(cmd.Name + " " + cmd.Desc) | |
| 66 | + | text := strings.ToLower(cmd.Desc) | |
| 67 | 67 | if positions, ok := subsequencePositions([]rune(text), []rune(qLower)); ok { | |
| 68 | 68 | return Match{Command: cmd, rank: 3, span: positions[len(positions)-1] - positions[0] + 1, first: positions[0], order: order}, true | |
| 69 | 69 | } | |
| @@ -63,7 +63,7 @@ func score(cmd command.Command, query string, order int) (Match, bool) { | |||||
| 63 | if positions, ok := subsequencePositions([]rune(codeLower), []rune(qLower)); ok { | 63 | if positions, ok := subsequencePositions([]rune(codeLower), []rune(qLower)); ok { | ||
| 64 | return Match{Command: cmd, Positions: positions, rank: 2, span: positions[len(positions)-1] - positions[0] + 1, first: positions[0], order: order}, true | 64 | return Match{Command: cmd, Positions: positions, rank: 2, span: positions[len(positions)-1] - positions[0] + 1, first: positions[0], order: order}, true | ||
| 65 | } | 65 | } | ||
| 66 | - | text := strings.ToLower(cmd.Name + " " + cmd.Desc) | 66 | + | text := strings.ToLower(cmd.Desc) |
| 67 | if positions, ok := subsequencePositions([]rune(text), []rune(qLower)); ok { | 67 | if positions, ok := subsequencePositions([]rune(text), []rune(qLower)); ok { | ||
| 68 | return Match{Command: cmd, rank: 3, span: positions[len(positions)-1] - positions[0] + 1, first: positions[0], order: order}, true | 68 | return Match{Command: cmd, rank: 3, span: positions[len(positions)-1] - positions[0] + 1, first: positions[0], order: order}, true | ||
| 69 | } | 69 | } | ||
M internal/usecase/palette/fuzzy_test.go +19 -3
| @@ -19,16 +19,32 @@ func codes(matches []Match) []string { | |||
| 19 | 19 | return out | |
| 20 | 20 | } | |
| 21 | 21 | ||
| 22 | - | func TestFuzzyOrdersScoringTiers(t *testing.T) { | |
| 22 | + | func TestFuzzyOrdersVisibleFieldScoringTiers(t *testing.T) { | |
| 23 | 23 | matches := Fuzzy([]command.Command{ | |
| 24 | - | cmd("ZQQ", "alpha beta", "name subsequence"), | |
| 24 | + | cmd("ZQQ", "unused", "alpha beta"), | |
| 25 | 25 | cmd("XAB", "unused", "subsequence code"), | |
| 26 | 26 | cmd("ABX", "unused", "prefix code"), | |
| 27 | 27 | cmd("AB", "unused", "exact code"), | |
| 28 | 28 | }, "ab") | |
| 29 | 29 | ||
| 30 | 30 | require.Equal(t, []string{"AB", "ABX", "XAB", "ZQQ"}, codes(matches)) | |
| 31 | - | require.Empty(t, matches[3].Positions, "name/description matches do not expose code highlight positions") | |
| 31 | + | require.Empty(t, matches[3].Positions, "description matches do not expose code highlight positions") | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | func TestFuzzyDoesNotMatchCommandNames(t *testing.T) { | |
| 35 | + | matches := Fuzzy([]command.Command{ | |
| 36 | + | cmd("CPY", "Create Alpha Beta", "Enter copy mode"), | |
| 37 | + | cmd("ZQQ", "unused", "Alpha Beta tools"), | |
| 38 | + | }, "ab") | |
| 39 | + | ||
| 40 | + | require.Equal(t, []string{"ZQQ"}, codes(matches)) | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func TestFuzzyDescriptionMatchingIsCaseInsensitiveWithoutCodeHighlights(t *testing.T) { | |
| 44 | + | matches := Fuzzy([]command.Command{cmd("CPY", "unused", "Enter COPY mode")}, "copy") | |
| 45 | + | ||
| 46 | + | require.Len(t, matches, 1) | |
| 47 | + | require.Empty(t, matches[0].Positions) | |
| 32 | 48 | } | |
| 33 | 49 | ||
| 34 | 50 | func TestFuzzyTieBreaksBySpanFirstThenCode(t *testing.T) { | |
| @@ -19,16 +19,32 @@ func codes(matches []Match) []string { | |||||
| 19 | return out | 19 | return out | ||
| 20 | } | 20 | } | ||
| 21 | 21 | ||||
| 22 | - | func TestFuzzyOrdersScoringTiers(t *testing.T) { | 22 | + | func TestFuzzyOrdersVisibleFieldScoringTiers(t *testing.T) { |
| 23 | matches := Fuzzy([]command.Command{ | 23 | matches := Fuzzy([]command.Command{ | ||
| 24 | - | cmd("ZQQ", "alpha beta", "name subsequence"), | 24 | + | cmd("ZQQ", "unused", "alpha beta"), |
| 25 | cmd("XAB", "unused", "subsequence code"), | 25 | cmd("XAB", "unused", "subsequence code"), | ||
| 26 | cmd("ABX", "unused", "prefix code"), | 26 | cmd("ABX", "unused", "prefix code"), | ||
| 27 | cmd("AB", "unused", "exact code"), | 27 | cmd("AB", "unused", "exact code"), | ||
| 28 | }, "ab") | 28 | }, "ab") | ||
| 29 | 29 | ||||
| 30 | require.Equal(t, []string{"AB", "ABX", "XAB", "ZQQ"}, codes(matches)) | 30 | require.Equal(t, []string{"AB", "ABX", "XAB", "ZQQ"}, codes(matches)) | ||
| 31 | - | require.Empty(t, matches[3].Positions, "name/description matches do not expose code highlight positions") | 31 | + | require.Empty(t, matches[3].Positions, "description matches do not expose code highlight positions") |
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | func TestFuzzyDoesNotMatchCommandNames(t *testing.T) { | |||
| 35 | + | matches := Fuzzy([]command.Command{ | |||
| 36 | + | cmd("CPY", "Create Alpha Beta", "Enter copy mode"), | |||
| 37 | + | cmd("ZQQ", "unused", "Alpha Beta tools"), | |||
| 38 | + | }, "ab") | |||
| 39 | + | ||||
| 40 | + | require.Equal(t, []string{"ZQQ"}, codes(matches)) | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func TestFuzzyDescriptionMatchingIsCaseInsensitiveWithoutCodeHighlights(t *testing.T) { | |||
| 44 | + | matches := Fuzzy([]command.Command{cmd("CPY", "unused", "Enter COPY mode")}, "copy") | |||
| 45 | + | ||||
| 46 | + | require.Len(t, matches, 1) | |||
| 47 | + | require.Empty(t, matches[0].Positions) | |||
| 32 | } | 48 | } | ||
| 33 | 49 | ||||
| 34 | func TestFuzzyTieBreaksBySpanFirstThenCode(t *testing.T) { | 50 | func TestFuzzyTieBreaksBySpanFirstThenCode(t *testing.T) { | ||
M internal/usecase/palette/model.go +0 -1
| @@ -174,7 +174,6 @@ func (m *Model) Render(inner domain.Size, styles RenderStyles) renderer.Frame { | |||
| 174 | 174 | frame.Set(x, y+1, renderer.Cell{Rune: ' ', Style: style}) | |
| 175 | 175 | x++ | |
| 176 | 176 | } | |
| 177 | - | x = ui.DrawText(frame, x, y+1, frame.Width, match.Command.Name+" — ", style) | |
| 178 | 177 | ui.DrawText(frame, x, y+1, frame.Width, match.Command.Desc, mergePaletteDescStyle(style, desc)) | |
| 179 | 178 | } | |
| 180 | 179 | return frame | |
| @@ -174,7 +174,6 @@ func (m *Model) Render(inner domain.Size, styles RenderStyles) renderer.Frame { | |||||
| 174 | frame.Set(x, y+1, renderer.Cell{Rune: ' ', Style: style}) | 174 | frame.Set(x, y+1, renderer.Cell{Rune: ' ', Style: style}) | ||
| 175 | x++ | 175 | x++ | ||
| 176 | } | 176 | } | ||
| 177 | - | x = ui.DrawText(frame, x, y+1, frame.Width, match.Command.Name+" — ", style) | |||
| 178 | ui.DrawText(frame, x, y+1, frame.Width, match.Command.Desc, mergePaletteDescStyle(style, desc)) | 177 | ui.DrawText(frame, x, y+1, frame.Width, match.Command.Desc, mergePaletteDescStyle(style, desc)) | ||
| 179 | } | 178 | } | ||
| 180 | return frame | 179 | return frame | ||
M internal/usecase/palette/model_test.go +31 -7
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package palette | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "fmt" | |
| 4 | 5 | "testing" | |
| 5 | 6 | ||
| 6 | 7 | "github.com/bnema/vev/internal/domain" | |
| @@ -66,7 +67,7 @@ func TestModelMatchesDeepCopiesPositions(t *testing.T) { | |||
| 66 | 67 | require.Equal(t, []int{0}, fresh[0].Positions) | |
| 67 | 68 | } | |
| 68 | 69 | ||
| 69 | - | func TestRenderDrawsInputCaretSelectedLineAndCodeHighlight(t *testing.T) { | |
| 70 | + | func TestRenderDrawsOnlyCodeAndDescriptionWithStyles(t *testing.T) { | |
| 70 | 71 | m := New([]command.Command{ | |
| 71 | 72 | cmd("CPY", "Copy", "Enter copy mode"), | |
| 72 | 73 | cmd("CNT", "New", "Create tab"), | |
| @@ -81,15 +82,38 @@ func TestRenderDrawsInputCaretSelectedLineAndCodeHighlight(t *testing.T) { | |||
| 81 | 82 | require.Equal(t, 'y', frame.At(3, 0).Rune) | |
| 82 | 83 | require.True(t, frame.At(4, 0).Style.Inverse, "caret is reverse-video after query") | |
| 83 | 84 | ||
| 84 | - | require.Equal(t, 'C', frame.At(0, 1).Rune) | |
| 85 | - | require.Equal(t, 'P', frame.At(1, 1).Rune) | |
| 86 | - | require.Equal(t, 'Y', frame.At(2, 1).Rune) | |
| 85 | + | require.Equal(t, "CPY Enter copy mode ", frameRow(frame, 1)) | |
| 86 | + | require.NotContains(t, frameRow(frame, 1), "Copy") | |
| 87 | + | require.NotContains(t, frameRow(frame, 1), "—") | |
| 87 | 88 | require.True(t, frame.At(0, 1).Style.Inverse, "selected line is inverse") | |
| 88 | 89 | require.True(t, frame.At(5, 1).Style.Inverse, "selected line padding/text remains inverse") | |
| 89 | 90 | require.True(t, frame.At(0, 1).Style.Bold, "command code C is bold") | |
| 90 | 91 | require.True(t, frame.At(1, 1).Style.Bold, "command code P is bold") | |
| 91 | 92 | require.True(t, frame.At(2, 1).Style.Bold, "command code Y is bold") | |
| 92 | - | require.True(t, frame.At(13, 1).Style.Italic, "description is italic") | |
| 93 | + | require.True(t, frame.At(4, 1).Style.Italic, "description is italic") | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | func TestRenderSafelyClipsVisibleFieldsAtNarrowWidths(t *testing.T) { | |
| 97 | + | m := New([]command.Command{cmd("CPY", "Copy", "Enter copy mode")}) | |
| 98 | + | want := []rune("CPY Enter copy mode") | |
| 99 | + | ||
| 100 | + | for _, cols := range []int{0, 1, 3, 4, 7} { | |
| 101 | + | t.Run(fmt.Sprintf("cols_%d", cols), func(t *testing.T) { | |
| 102 | + | frame := m.Render(domain.Size{Cols: cols, Rows: 2}, DefaultRenderStyles()) | |
| 103 | + | require.Equal(t, cols, frame.Width) | |
| 104 | + | if cols > 0 { | |
| 105 | + | require.Equal(t, want[:min(cols, len(want))], []rune(frameRow(frame, 1))) | |
| 106 | + | } | |
| 107 | + | }) | |
| 108 | + | } | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | func frameRow(frame renderer.Frame, y int) string { | |
| 112 | + | row := make([]rune, frame.Width) | |
| 113 | + | for x := range frame.Width { | |
| 114 | + | row[x] = frame.At(x, y).Rune | |
| 115 | + | } | |
| 116 | + | return string(row) | |
| 93 | 117 | } | |
| 94 | 118 | ||
| 95 | 119 | func TestRenderUsesConfiguredStyles(t *testing.T) { | |
| @@ -128,7 +152,7 @@ func TestRenderUsesConfiguredStyles(t *testing.T) { | |||
| 128 | 152 | styles.Description = muted | |
| 129 | 153 | return styles | |
| 130 | 154 | }, | |
| 131 | - | x: 13, | |
| 155 | + | x: 4, | |
| 132 | 156 | assert: func(t *testing.T, style renderer.Style) { | |
| 133 | 157 | require.True(t, style.Italic) | |
| 134 | 158 | require.True(t, style.HasForegroundRGB) | |
| @@ -143,7 +167,7 @@ func TestRenderUsesConfiguredStyles(t *testing.T) { | |||
| 143 | 167 | styles.Description.Italic = false | |
| 144 | 168 | return styles | |
| 145 | 169 | }, | |
| 146 | - | x: 13, | |
| 170 | + | x: 4, | |
| 147 | 171 | assert: func(t *testing.T, style renderer.Style) { | |
| 148 | 172 | require.False(t, style.Italic) | |
| 149 | 173 | }, | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package palette | 1 | package palette | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "fmt" | |||
| 4 | "testing" | 5 | "testing" | ||
| 5 | 6 | ||||
| 6 | "github.com/bnema/vev/internal/domain" | 7 | "github.com/bnema/vev/internal/domain" | ||
| @@ -66,7 +67,7 @@ func TestModelMatchesDeepCopiesPositions(t *testing.T) { | |||||
| 66 | require.Equal(t, []int{0}, fresh[0].Positions) | 67 | require.Equal(t, []int{0}, fresh[0].Positions) | ||
| 67 | } | 68 | } | ||
| 68 | 69 | ||||
| 69 | - | func TestRenderDrawsInputCaretSelectedLineAndCodeHighlight(t *testing.T) { | 70 | + | func TestRenderDrawsOnlyCodeAndDescriptionWithStyles(t *testing.T) { |
| 70 | m := New([]command.Command{ | 71 | m := New([]command.Command{ | ||
| 71 | cmd("CPY", "Copy", "Enter copy mode"), | 72 | cmd("CPY", "Copy", "Enter copy mode"), | ||
| 72 | cmd("CNT", "New", "Create tab"), | 73 | cmd("CNT", "New", "Create tab"), | ||
| @@ -81,15 +82,38 @@ func TestRenderDrawsInputCaretSelectedLineAndCodeHighlight(t *testing.T) { | |||||
| 81 | require.Equal(t, 'y', frame.At(3, 0).Rune) | 82 | require.Equal(t, 'y', frame.At(3, 0).Rune) | ||
| 82 | require.True(t, frame.At(4, 0).Style.Inverse, "caret is reverse-video after query") | 83 | require.True(t, frame.At(4, 0).Style.Inverse, "caret is reverse-video after query") | ||
| 83 | 84 | ||||
| 84 | - | require.Equal(t, 'C', frame.At(0, 1).Rune) | 85 | + | require.Equal(t, "CPY Enter copy mode ", frameRow(frame, 1)) |
| 85 | - | require.Equal(t, 'P', frame.At(1, 1).Rune) | 86 | + | require.NotContains(t, frameRow(frame, 1), "Copy") |
| 86 | - | require.Equal(t, 'Y', frame.At(2, 1).Rune) | 87 | + | require.NotContains(t, frameRow(frame, 1), "—") |
| 87 | require.True(t, frame.At(0, 1).Style.Inverse, "selected line is inverse") | 88 | require.True(t, frame.At(0, 1).Style.Inverse, "selected line is inverse") | ||
| 88 | require.True(t, frame.At(5, 1).Style.Inverse, "selected line padding/text remains inverse") | 89 | require.True(t, frame.At(5, 1).Style.Inverse, "selected line padding/text remains inverse") | ||
| 89 | require.True(t, frame.At(0, 1).Style.Bold, "command code C is bold") | 90 | require.True(t, frame.At(0, 1).Style.Bold, "command code C is bold") | ||
| 90 | require.True(t, frame.At(1, 1).Style.Bold, "command code P is bold") | 91 | require.True(t, frame.At(1, 1).Style.Bold, "command code P is bold") | ||
| 91 | require.True(t, frame.At(2, 1).Style.Bold, "command code Y is bold") | 92 | require.True(t, frame.At(2, 1).Style.Bold, "command code Y is bold") | ||
| 92 | - | require.True(t, frame.At(13, 1).Style.Italic, "description is italic") | 93 | + | require.True(t, frame.At(4, 1).Style.Italic, "description is italic") |
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | func TestRenderSafelyClipsVisibleFieldsAtNarrowWidths(t *testing.T) { | |||
| 97 | + | m := New([]command.Command{cmd("CPY", "Copy", "Enter copy mode")}) | |||
| 98 | + | want := []rune("CPY Enter copy mode") | |||
| 99 | + | ||||
| 100 | + | for _, cols := range []int{0, 1, 3, 4, 7} { | |||
| 101 | + | t.Run(fmt.Sprintf("cols_%d", cols), func(t *testing.T) { | |||
| 102 | + | frame := m.Render(domain.Size{Cols: cols, Rows: 2}, DefaultRenderStyles()) | |||
| 103 | + | require.Equal(t, cols, frame.Width) | |||
| 104 | + | if cols > 0 { | |||
| 105 | + | require.Equal(t, want[:min(cols, len(want))], []rune(frameRow(frame, 1))) | |||
| 106 | + | } | |||
| 107 | + | }) | |||
| 108 | + | } | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | func frameRow(frame renderer.Frame, y int) string { | |||
| 112 | + | row := make([]rune, frame.Width) | |||
| 113 | + | for x := range frame.Width { | |||
| 114 | + | row[x] = frame.At(x, y).Rune | |||
| 115 | + | } | |||
| 116 | + | return string(row) | |||
| 93 | } | 117 | } | ||
| 94 | 118 | ||||
| 95 | func TestRenderUsesConfiguredStyles(t *testing.T) { | 119 | func TestRenderUsesConfiguredStyles(t *testing.T) { | ||
| @@ -128,7 +152,7 @@ func TestRenderUsesConfiguredStyles(t *testing.T) { | |||||
| 128 | styles.Description = muted | 152 | styles.Description = muted | ||
| 129 | return styles | 153 | return styles | ||
| 130 | }, | 154 | }, | ||
| 131 | - | x: 13, | 155 | + | x: 4, |
| 132 | assert: func(t *testing.T, style renderer.Style) { | 156 | assert: func(t *testing.T, style renderer.Style) { | ||
| 133 | require.True(t, style.Italic) | 157 | require.True(t, style.Italic) | ||
| 134 | require.True(t, style.HasForegroundRGB) | 158 | require.True(t, style.HasForegroundRGB) | ||
| @@ -143,7 +167,7 @@ func TestRenderUsesConfiguredStyles(t *testing.T) { | |||||
| 143 | styles.Description.Italic = false | 167 | styles.Description.Italic = false | ||
| 144 | return styles | 168 | return styles | ||
| 145 | }, | 169 | }, | ||
| 146 | - | x: 13, | 170 | + | x: 4, |
| 147 | assert: func(t *testing.T, style renderer.Style) { | 171 | assert: func(t *testing.T, style renderer.Style) { | ||
| 148 | require.False(t, style.Italic) | 172 | require.False(t, style.Italic) | ||
| 149 | }, | 173 | }, | ||