Skip to content

Commit ed51a4b

Browse files
engalarako
authored andcommitted
feat(tui): command completion and fuzzy matching in cmdbar
1 parent 8171411 commit ed51a4b

2 files changed

Lines changed: 134 additions & 5 deletions

File tree

tui/cmdbar.go

Lines changed: 131 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,32 @@ import (
88
"github.com/charmbracelet/lipgloss"
99
)
1010

11+
var allCommands = []struct {
12+
name string
13+
desc string
14+
}{
15+
{"callers", "show callers of selected element"},
16+
{"callees", "show callees of selected element"},
17+
{"context", "show context of selected element"},
18+
{"impact", "show impact of selected element"},
19+
{"refs", "show references to selected element"},
20+
{"diagram", "open diagram in browser"},
21+
{"search", "full-text search: search <keyword>"},
22+
{"run", "run MDL file: run <file.mdl>"},
23+
{"check", "check MDL syntax: check <file.mdl>"},
24+
}
25+
1126
// CmdBar is the bottom command input bar activated by ":".
1227
type CmdBar struct {
13-
input textinput.Model
14-
visible bool
28+
input textinput.Model
29+
visible bool
30+
candidates []string
31+
selectedCandidate int
1532
}
1633

1734
func NewCmdBar() CmdBar {
1835
ti := textinput.New()
19-
ti.Placeholder = "command (run, check, callers, callees, context, impact, refs, diagram, search <kw>)"
36+
ti.Placeholder = "command (callers, callees, context, impact, refs, diagram, search, run, check)"
2037
ti.Prompt = ": "
2138
ti.CharLimit = 200
2239
return CmdBar{input: ti}
@@ -26,11 +43,15 @@ func (c *CmdBar) Show() {
2643
c.visible = true
2744
c.input.SetValue("")
2845
c.input.Focus()
46+
c.candidates = nil
47+
c.selectedCandidate = 0
2948
}
3049

3150
func (c *CmdBar) Hide() {
3251
c.visible = false
3352
c.input.Blur()
53+
c.candidates = nil
54+
c.selectedCandidate = 0
3455
}
3556

3657
func (c CmdBar) IsVisible() bool { return c.visible }
@@ -48,19 +69,124 @@ func (c CmdBar) Command() (verb string, rest string) {
4869
return verb, rest
4970
}
5071

72+
func (c *CmdBar) filterCandidates() {
73+
verb, _ := c.Command()
74+
if verb == "" {
75+
c.candidates = nil
76+
return
77+
}
78+
var result []string
79+
for _, cmd := range allCommands {
80+
if strings.HasPrefix(cmd.name, verb) || strings.Contains(cmd.name, verb) {
81+
result = append(result, cmd.name)
82+
}
83+
}
84+
// If exact match, no need to show candidates
85+
if len(result) == 1 && result[0] == verb {
86+
c.candidates = nil
87+
return
88+
}
89+
c.candidates = result
90+
if c.selectedCandidate >= len(c.candidates) {
91+
c.selectedCandidate = 0
92+
}
93+
}
94+
95+
// ApplyCompletion applies the selected candidate if one is highlighted and not yet complete.
96+
// Returns true if completion was applied (caller should not submit yet).
97+
func (c *CmdBar) ApplyCompletion() bool {
98+
if len(c.candidates) > 0 && c.selectedCandidate < len(c.candidates) {
99+
verb, _ := c.Command()
100+
candidate := c.candidates[c.selectedCandidate]
101+
if verb != candidate {
102+
c.input.SetValue(candidate + " ")
103+
c.input.CursorEnd()
104+
c.candidates = nil
105+
c.selectedCandidate = 0
106+
return true
107+
}
108+
}
109+
return false
110+
}
111+
51112
func (c CmdBar) Update(msg tea.Msg) (CmdBar, tea.Cmd) {
113+
if keyMsg, ok := msg.(tea.KeyMsg); ok {
114+
switch keyMsg.String() {
115+
case "tab":
116+
if len(c.candidates) > 0 {
117+
chosen := c.candidates[c.selectedCandidate]
118+
c.input.SetValue(chosen + " ")
119+
c.input.CursorEnd()
120+
c.candidates = nil
121+
c.selectedCandidate = 0
122+
}
123+
return c, nil
124+
case "up":
125+
if len(c.candidates) > 0 {
126+
c.selectedCandidate--
127+
if c.selectedCandidate < 0 {
128+
c.selectedCandidate = len(c.candidates) - 1
129+
}
130+
}
131+
return c, nil
132+
case "down":
133+
if len(c.candidates) > 0 {
134+
c.selectedCandidate++
135+
if c.selectedCandidate >= len(c.candidates) {
136+
c.selectedCandidate = 0
137+
}
138+
}
139+
return c, nil
140+
}
141+
}
52142
var cmd tea.Cmd
53143
c.input, cmd = c.input.Update(msg)
144+
c.filterCandidates()
54145
return c, cmd
55146
}
56147

57148
func (c CmdBar) View() string {
58149
if !c.visible {
59150
return lipgloss.NewStyle().
60151
Foreground(lipgloss.Color("240")).
61-
Render(" :run :check :callers :callees :context :impact :refs :diagram :search")
152+
Render(" :callers :callees :context :impact :refs :diagram :search :run :check")
62153
}
63-
return lipgloss.NewStyle().
154+
155+
inputLine := lipgloss.NewStyle().
64156
Bold(true).Foreground(lipgloss.Color("214")).
65157
Render(c.input.View())
158+
159+
if len(c.candidates) == 0 {
160+
return inputLine
161+
}
162+
163+
// Show up to 5 candidates
164+
maxShow := min(5, len(c.candidates))
165+
166+
normalStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
167+
highlightStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")).Bold(true)
168+
169+
var lines []string
170+
lines = append(lines, inputLine)
171+
for i := 0; i < maxShow; i++ {
172+
name := c.candidates[i]
173+
// Find description
174+
desc := ""
175+
for _, cmd := range allCommands {
176+
if cmd.name == name {
177+
desc = cmd.desc
178+
break
179+
}
180+
}
181+
entry := " " + name
182+
if desc != "" {
183+
entry += " " + desc
184+
}
185+
if i == c.selectedCandidate {
186+
lines = append(lines, highlightStyle.Render(entry))
187+
} else {
188+
lines = append(lines, normalStyle.Render(entry))
189+
}
190+
}
191+
return strings.Join(lines, "\n")
66192
}

tui/model.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
193193
m.cmdbar.Hide()
194194
return m, nil
195195
case "enter":
196+
if m.cmdbar.ApplyCompletion() {
197+
return m, nil
198+
}
196199
verb, rest := m.cmdbar.Command()
197200
m.cmdbar.Hide()
198201
return m, m.dispatchCommand(verb, rest)

0 commit comments

Comments
 (0)