fix(palette): prioritize exact argument command
- commit
- d589837f425ad5e5f13fcdf6e5544a3a9a4f4696
- author
- brice · · 2026-07-14 12:08 UTC
- parent
- ba310a2
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +79 -3
M internal/usecase/palette/model.go +8 -3
| @@ -180,10 +180,15 @@ func (m *Model) refresh() { | |||
| 180 | 180 | } | |
| 181 | 181 | ||
| 182 | 182 | func (m *Model) prependMatch(cmd command.Command) { | |
| 183 | - | for _, match := range m.matches { | |
| 184 | - | if match.Command.Code == cmd.Code { | |
| 185 | - | return | |
| 183 | + | for i, match := range m.matches { | |
| 184 | + | if match.Command.Code != cmd.Code { | |
| 185 | + | continue | |
| 186 | 186 | } | |
| 187 | + | if i > 0 { | |
| 188 | + | copy(m.matches[1:i+1], m.matches[:i]) | |
| 189 | + | m.matches[0] = match | |
| 190 | + | } | |
| 191 | + | return | |
| 187 | 192 | } | |
| 188 | 193 | m.matches = append([]Match{{Command: cmd}}, m.matches...) | |
| 189 | 194 | } | |
| @@ -180,10 +180,15 @@ func (m *Model) refresh() { | |||||
| 180 | } | 180 | } | ||
| 181 | 181 | ||||
| 182 | func (m *Model) prependMatch(cmd command.Command) { | 182 | func (m *Model) prependMatch(cmd command.Command) { | ||
| 183 | - | for _, match := range m.matches { | 183 | + | for i, match := range m.matches { |
| 184 | - | if match.Command.Code == cmd.Code { | 184 | + | if match.Command.Code != cmd.Code { |
| 185 | - | return | 185 | + | continue |
| 186 | } | 186 | } | ||
| 187 | + | if i > 0 { | |||
| 188 | + | copy(m.matches[1:i+1], m.matches[:i]) | |||
| 189 | + | m.matches[0] = match | |||
| 190 | + | } | |||
| 191 | + | return | |||
| 187 | } | 192 | } | ||
| 188 | m.matches = append([]Match{{Command: cmd}}, m.matches...) | 193 | m.matches = append([]Match{{Command: cmd}}, m.matches...) | ||
| 189 | } | 194 | } | ||
M internal/usecase/palette/model_test.go +71 -0
| @@ -67,6 +67,77 @@ func TestModelMatchesDeepCopiesPositions(t *testing.T) { | |||
| 67 | 67 | require.Equal(t, []int{0}, fresh[0].Positions) | |
| 68 | 68 | } | |
| 69 | 69 | ||
| 70 | + | func TestModelExactArgumentMatchMovesExistingFuzzyMatchToFront(t *testing.T) { | |
| 71 | + | zzz := cmd("ZZZ", "", "ZZZ 1") | |
| 72 | + | zzz.Arguments = command.ArgumentsRequired | |
| 73 | + | commands := []command.Command{ | |
| 74 | + | cmd("AAA", "", "ZZZ 1"), | |
| 75 | + | zzz, | |
| 76 | + | } | |
| 77 | + | want := Fuzzy(commands, "ZZZ 1")[1] | |
| 78 | + | m := New(commands) | |
| 79 | + | ||
| 80 | + | for _, r := range "ZZZ 1" { | |
| 81 | + | m.Insert(r) | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | matches := m.Matches() | |
| 85 | + | require.Len(t, matches, 2) | |
| 86 | + | require.Equal(t, "ZZZ", matches[0].Command.Code) | |
| 87 | + | require.Equal(t, want, matches[0], "the existing fuzzy match must retain its metadata") | |
| 88 | + | selected, ok := m.Selected() | |
| 89 | + | require.True(t, ok) | |
| 90 | + | require.Equal(t, "ZZZ", selected.Code) | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | func TestModelExactArgumentMatchKeepsAbsentAndFirstBehavior(t *testing.T) { | |
| 94 | + | for _, tt := range []struct { | |
| 95 | + | name string | |
| 96 | + | commands func(zzz command.Command) []command.Command | |
| 97 | + | want Match | |
| 98 | + | wantLen int | |
| 99 | + | }{ | |
| 100 | + | { | |
| 101 | + | name: "absent fuzzy match is prepended", | |
| 102 | + | commands: func(zzz command.Command) []command.Command { | |
| 103 | + | return []command.Command{cmd("AAA", "", "ZZZ 1"), zzz} | |
| 104 | + | }, | |
| 105 | + | want: Match{Command: command.Command{ | |
| 106 | + | Code: "ZZZ", | |
| 107 | + | Desc: "unmatched", | |
| 108 | + | Arguments: command.ArgumentsRequired, | |
| 109 | + | }}, | |
| 110 | + | wantLen: 2, | |
| 111 | + | }, | |
| 112 | + | { | |
| 113 | + | name: "first fuzzy match is unchanged", | |
| 114 | + | commands: func(zzz command.Command) []command.Command { | |
| 115 | + | zzz.Desc = "ZZZ 1" | |
| 116 | + | return []command.Command{zzz, cmd("ZZZZ", "", "ZZZ 1")} | |
| 117 | + | }, | |
| 118 | + | want: func() Match { | |
| 119 | + | zzz := cmd("ZZZ", "", "ZZZ 1") | |
| 120 | + | zzz.Arguments = command.ArgumentsRequired | |
| 121 | + | return Fuzzy([]command.Command{zzz, cmd("ZZZZ", "", "ZZZ 1")}, "ZZZ 1")[0] | |
| 122 | + | }(), | |
| 123 | + | wantLen: 2, | |
| 124 | + | }, | |
| 125 | + | } { | |
| 126 | + | t.Run(tt.name, func(t *testing.T) { | |
| 127 | + | zzz := cmd("ZZZ", "", "unmatched") | |
| 128 | + | zzz.Arguments = command.ArgumentsRequired | |
| 129 | + | m := New(tt.commands(zzz)) | |
| 130 | + | for _, r := range "ZZZ 1" { | |
| 131 | + | m.Insert(r) | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | matches := m.Matches() | |
| 135 | + | require.Len(t, matches, tt.wantLen) | |
| 136 | + | require.Equal(t, tt.want, matches[0]) | |
| 137 | + | }) | |
| 138 | + | } | |
| 139 | + | } | |
| 140 | + | ||
| 70 | 141 | func TestRenderDrawsOnlyCodeAndDescriptionWithStyles(t *testing.T) { | |
| 71 | 142 | m := New([]command.Command{ | |
| 72 | 143 | cmd("CPY", "Copy", "Enter copy mode"), | |
| @@ -67,6 +67,77 @@ func TestModelMatchesDeepCopiesPositions(t *testing.T) { | |||||
| 67 | require.Equal(t, []int{0}, fresh[0].Positions) | 67 | require.Equal(t, []int{0}, fresh[0].Positions) | ||
| 68 | } | 68 | } | ||
| 69 | 69 | ||||
| 70 | + | func TestModelExactArgumentMatchMovesExistingFuzzyMatchToFront(t *testing.T) { | |||
| 71 | + | zzz := cmd("ZZZ", "", "ZZZ 1") | |||
| 72 | + | zzz.Arguments = command.ArgumentsRequired | |||
| 73 | + | commands := []command.Command{ | |||
| 74 | + | cmd("AAA", "", "ZZZ 1"), | |||
| 75 | + | zzz, | |||
| 76 | + | } | |||
| 77 | + | want := Fuzzy(commands, "ZZZ 1")[1] | |||
| 78 | + | m := New(commands) | |||
| 79 | + | ||||
| 80 | + | for _, r := range "ZZZ 1" { | |||
| 81 | + | m.Insert(r) | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | matches := m.Matches() | |||
| 85 | + | require.Len(t, matches, 2) | |||
| 86 | + | require.Equal(t, "ZZZ", matches[0].Command.Code) | |||
| 87 | + | require.Equal(t, want, matches[0], "the existing fuzzy match must retain its metadata") | |||
| 88 | + | selected, ok := m.Selected() | |||
| 89 | + | require.True(t, ok) | |||
| 90 | + | require.Equal(t, "ZZZ", selected.Code) | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | func TestModelExactArgumentMatchKeepsAbsentAndFirstBehavior(t *testing.T) { | |||
| 94 | + | for _, tt := range []struct { | |||
| 95 | + | name string | |||
| 96 | + | commands func(zzz command.Command) []command.Command | |||
| 97 | + | want Match | |||
| 98 | + | wantLen int | |||
| 99 | + | }{ | |||
| 100 | + | { | |||
| 101 | + | name: "absent fuzzy match is prepended", | |||
| 102 | + | commands: func(zzz command.Command) []command.Command { | |||
| 103 | + | return []command.Command{cmd("AAA", "", "ZZZ 1"), zzz} | |||
| 104 | + | }, | |||
| 105 | + | want: Match{Command: command.Command{ | |||
| 106 | + | Code: "ZZZ", | |||
| 107 | + | Desc: "unmatched", | |||
| 108 | + | Arguments: command.ArgumentsRequired, | |||
| 109 | + | }}, | |||
| 110 | + | wantLen: 2, | |||
| 111 | + | }, | |||
| 112 | + | { | |||
| 113 | + | name: "first fuzzy match is unchanged", | |||
| 114 | + | commands: func(zzz command.Command) []command.Command { | |||
| 115 | + | zzz.Desc = "ZZZ 1" | |||
| 116 | + | return []command.Command{zzz, cmd("ZZZZ", "", "ZZZ 1")} | |||
| 117 | + | }, | |||
| 118 | + | want: func() Match { | |||
| 119 | + | zzz := cmd("ZZZ", "", "ZZZ 1") | |||
| 120 | + | zzz.Arguments = command.ArgumentsRequired | |||
| 121 | + | return Fuzzy([]command.Command{zzz, cmd("ZZZZ", "", "ZZZ 1")}, "ZZZ 1")[0] | |||
| 122 | + | }(), | |||
| 123 | + | wantLen: 2, | |||
| 124 | + | }, | |||
| 125 | + | } { | |||
| 126 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 127 | + | zzz := cmd("ZZZ", "", "unmatched") | |||
| 128 | + | zzz.Arguments = command.ArgumentsRequired | |||
| 129 | + | m := New(tt.commands(zzz)) | |||
| 130 | + | for _, r := range "ZZZ 1" { | |||
| 131 | + | m.Insert(r) | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | matches := m.Matches() | |||
| 135 | + | require.Len(t, matches, tt.wantLen) | |||
| 136 | + | require.Equal(t, tt.want, matches[0]) | |||
| 137 | + | }) | |||
| 138 | + | } | |||
| 139 | + | } | |||
| 140 | + | ||||
| 70 | func TestRenderDrawsOnlyCodeAndDescriptionWithStyles(t *testing.T) { | 141 | func TestRenderDrawsOnlyCodeAndDescriptionWithStyles(t *testing.T) { | ||
| 71 | m := New([]command.Command{ | 142 | m := New([]command.Command{ | ||
| 72 | cmd("CPY", "Copy", "Enter copy mode"), | 143 | cmd("CPY", "Copy", "Enter copy mode"), | ||