detached d589837

fix(palette): prioritize exact argument command

commit
d589837f425ad5e5f13fcdf6e5544a3a9a4f4696
author
brice · · 2026-07-14 12:08 UTC
parent
ba310a2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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() {
180180 }
181181
182182 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
186186 }
187+ if i > 0 {
188+ copy(m.matches[1:i+1], m.matches[:i])
189+ m.matches[0] = match
190+ }
191+ return
187192 }
188193 m.matches = append([]Match{{Command: cmd}}, m.matches...)
189194 }
@@ -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- return185+ 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) {
6767 require.Equal(t, []int{0}, fresh[0].Positions)
6868 }
6969
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+
70141 func TestRenderDrawsOnlyCodeAndDescriptionWithStyles(t *testing.T) {
71142 m := New([]command.Command{
72143 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"),

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close