1package dialog
2
3import (
4 "github.com/charmbracelet/bubbles/key"
5 "github.com/charmbracelet/bubbles/textarea"
6 tea "github.com/charmbracelet/bubbletea"
7 "github.com/charmbracelet/lipgloss"
8 "github.com/opencode-ai/opencode/internal/logging"
9 utilComponents "github.com/opencode-ai/opencode/internal/tui/components/util"
10 "github.com/opencode-ai/opencode/internal/tui/layout"
11 "github.com/opencode-ai/opencode/internal/tui/styles"
12 "github.com/opencode-ai/opencode/internal/tui/theme"
13 "github.com/opencode-ai/opencode/internal/tui/util"
14)
15
16type CompletionItem struct {
17 title string
18 Title string
19 Value string
20}
21
22type CompletionItemI interface {
23 utilComponents.SimpleListItem
24 GetValue() string
25 DisplayValue() string
26}
27
28func (ci *CompletionItem) Render(selected bool, width int) string {
29 t := theme.CurrentTheme()
30 baseStyle := styles.BaseStyle()
31
32 itemStyle := baseStyle.
33 Width(width).
34 Padding(0, 1)
35
36 if selected {
37 itemStyle = itemStyle.
38 Background(t.Background()).
39 Foreground(t.Primary()).
40 Bold(true)
41 }
42
43 title := itemStyle.Render(
44 ci.GetValue(),
45 )
46
47 return title
48}
49
50func (ci *CompletionItem) DisplayValue() string {
51 return ci.Title
52}
53
54func (ci *CompletionItem) GetValue() string {
55 return ci.Value
56}
57
58func NewCompletionItem(completionItem CompletionItem) CompletionItemI {
59 return &completionItem
60}
61
62type CompletionProvider interface {
63 GetId() string
64 GetEntry() CompletionItemI
65 GetChildEntries(query string) ([]CompletionItemI, error)
66}
67
68type CompletionSelectedMsg struct {
69 SearchString string
70 CompletionValue string
71}
72
73type CompletionDialogCompleteItemMsg struct {
74 Value string
75}
76
77type CompletionDialogCloseMsg struct{}
78
79type CompletionDialog interface {
80 tea.Model
81 layout.Bindings
82 SetWidth(width int)
83}
84
85type completionDialogCmp struct {
86 query string
87 completionProvider CompletionProvider
88 width int
89 height int
90 pseudoSearchTextArea textarea.Model
91 listView utilComponents.SimpleList[CompletionItemI]
92}
93
94type completionDialogKeyMap struct {
95 Complete key.Binding
96 Cancel key.Binding
97}
98
99var completionDialogKeys = completionDialogKeyMap{
100 Complete: key.NewBinding(
101 key.WithKeys("tab", "enter"),
102 ),
103 Cancel: key.NewBinding(
104 key.WithKeys(" ", "esc", "backspace"),
105 ),
106}
107
108func (c *completionDialogCmp) Init() tea.Cmd {
109 return nil
110}
111
112func (c *completionDialogCmp) complete(item CompletionItemI) tea.Cmd {
113 value := c.pseudoSearchTextArea.Value()
114
115 if value == "" {
116 return nil
117 }
118
119 return tea.Batch(
120 util.CmdHandler(CompletionSelectedMsg{
121 SearchString: value,
122 CompletionValue: item.GetValue(),
123 }),
124 c.close(),
125 )
126}
127
128func (c *completionDialogCmp) close() tea.Cmd {
129 c.listView.SetItems([]CompletionItemI{})
130 c.pseudoSearchTextArea.Reset()
131 c.pseudoSearchTextArea.Blur()
132
133 return util.CmdHandler(CompletionDialogCloseMsg{})
134}
135
136func (c *completionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
137 var cmds []tea.Cmd
138 switch msg := msg.(type) {
139 case tea.KeyMsg:
140 if c.pseudoSearchTextArea.Focused() {
141
142 if !key.Matches(msg, completionDialogKeys.Complete) {
143
144 var cmd tea.Cmd
145 c.pseudoSearchTextArea, cmd = c.pseudoSearchTextArea.Update(msg)
146 cmds = append(cmds, cmd)
147
148 var query string
149 query = c.pseudoSearchTextArea.Value()
150 if query != "" {
151 query = query[1:]
152 }
153
154 if query != c.query {
155 logging.Info("Query", query)
156 items, err := c.completionProvider.GetChildEntries(query)
157 if err != nil {
158 logging.Error("Failed to get child entries", err)
159 }
160
161 c.listView.SetItems(items)
162 c.query = query
163 }
164
165 u, cmd := c.listView.Update(msg)
166 c.listView = u.(utilComponents.SimpleList[CompletionItemI])
167
168 cmds = append(cmds, cmd)
169 }
170
171 switch {
172 case key.Matches(msg, completionDialogKeys.Complete):
173 item, i := c.listView.GetSelectedItem()
174 if i == -1 {
175 return c, nil
176 }
177
178 cmd := c.complete(item)
179
180 return c, cmd
181 case key.Matches(msg, completionDialogKeys.Cancel):
182 // Only close on backspace when there are no characters left
183 if msg.String() != "backspace" || len(c.pseudoSearchTextArea.Value()) <= 0 {
184 return c, c.close()
185 }
186 }
187
188 return c, tea.Batch(cmds...)
189 } else {
190 items, err := c.completionProvider.GetChildEntries("")
191 if err != nil {
192 logging.Error("Failed to get child entries", err)
193 }
194
195 c.listView.SetItems(items)
196 c.pseudoSearchTextArea.SetValue(msg.String())
197 return c, c.pseudoSearchTextArea.Focus()
198 }
199 case tea.WindowSizeMsg:
200 c.width = msg.Width
201 c.height = msg.Height
202 }
203
204 return c, tea.Batch(cmds...)
205}
206
207func (c *completionDialogCmp) View() string {
208 t := theme.CurrentTheme()
209 baseStyle := styles.BaseStyle()
210
211 maxWidth := 40
212
213 completions := c.listView.GetItems()
214
215 for _, cmd := range completions {
216 title := cmd.DisplayValue()
217 if len(title) > maxWidth-4 {
218 maxWidth = len(title) + 4
219 }
220 }
221
222 c.listView.SetMaxWidth(maxWidth)
223
224 return baseStyle.Padding(0, 0).
225 Border(lipgloss.NormalBorder()).
226 BorderBottom(false).
227 BorderRight(false).
228 BorderLeft(false).
229 BorderBackground(t.Background()).
230 BorderForeground(t.TextMuted()).
231 Width(c.width).
232 Render(c.listView.View())
233}
234
235func (c *completionDialogCmp) SetWidth(width int) {
236 c.width = width
237}
238
239func (c *completionDialogCmp) BindingKeys() []key.Binding {
240 return layout.KeyMapToSlice(completionDialogKeys)
241}
242
243func NewCompletionDialogCmp(completionProvider CompletionProvider) CompletionDialog {
244 ti := textarea.New()
245
246 items, err := completionProvider.GetChildEntries("")
247 if err != nil {
248 logging.Error("Failed to get child entries", err)
249 }
250
251 li := utilComponents.NewSimpleList(
252 items,
253 7,
254 "No file matches found",
255 false,
256 )
257
258 return &completionDialogCmp{
259 query: "",
260 completionProvider: completionProvider,
261 pseudoSearchTextArea: ti,
262 listView: li,
263 }
264}