@@ -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 ":".
1227type CmdBar struct {
13- input textinput.Model
14- visible bool
28+ input textinput.Model
29+ visible bool
30+ candidates []string
31+ selectedCandidate int
1532}
1633
1734func 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
3150func (c * CmdBar ) Hide () {
3251 c .visible = false
3352 c .input .Blur ()
53+ c .candidates = nil
54+ c .selectedCandidate = 0
3455}
3556
3657func (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+
51112func (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
57148func (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}
0 commit comments