1package dialog
2
3import (
4 "github.com/charmbracelet/bubbles/v2/key"
5 "github.com/charmbracelet/bubbles/v2/textarea"
6 tea "github.com/charmbracelet/bubbletea/v2"
7 "github.com/charmbracelet/lipgloss/v2"
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 util.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.KeyPressMsg:
140 if c.pseudoSearchTextArea.Focused() {
141 if !key.Matches(msg, completionDialogKeys.Complete) {
142 var cmd tea.Cmd
143 c.pseudoSearchTextArea, cmd = c.pseudoSearchTextArea.Update(msg)
144 cmds = append(cmds, cmd)
145
146 var query string
147 query = c.pseudoSearchTextArea.Value()
148 if query != "" {
149 query = query[1:]
150 }
151
152 if query != c.query {
153 logging.Info("Query", query)
154 items, err := c.completionProvider.GetChildEntries(query)
155 if err != nil {
156 logging.Error("Failed to get child entries", err)
157 }
158
159 c.listView.SetItems(items)
160 c.query = query
161 }
162
163 u, cmd := c.listView.Update(msg)
164 c.listView = u.(utilComponents.SimpleList[CompletionItemI])
165
166 cmds = append(cmds, cmd)
167 }
168
169 switch {
170 case key.Matches(msg, completionDialogKeys.Complete):
171 item, i := c.listView.GetSelectedItem()
172 if i == -1 {
173 return c, nil
174 }
175
176 cmd := c.complete(item)
177
178 return c, cmd
179 case key.Matches(msg, completionDialogKeys.Cancel):
180 // Only close on backspace when there are no characters left
181 if msg.String() != "backspace" || len(c.pseudoSearchTextArea.Value()) <= 0 {
182 return c, c.close()
183 }
184 }
185
186 return c, tea.Batch(cmds...)
187 } else {
188 items, err := c.completionProvider.GetChildEntries("")
189 if err != nil {
190 logging.Error("Failed to get child entries", err)
191 }
192
193 c.listView.SetItems(items)
194 c.pseudoSearchTextArea.SetValue(msg.String())
195 return c, c.pseudoSearchTextArea.Focus()
196 }
197 case tea.WindowSizeMsg:
198 c.width = msg.Width
199 c.height = msg.Height
200 }
201
202 return c, tea.Batch(cmds...)
203}
204
205func (c *completionDialogCmp) View() string {
206 t := theme.CurrentTheme()
207 baseStyle := styles.BaseStyle()
208
209 maxWidth := 40
210
211 completions := c.listView.GetItems()
212
213 for _, cmd := range completions {
214 title := cmd.DisplayValue()
215 if len(title) > maxWidth-4 {
216 maxWidth = len(title) + 4
217 }
218 }
219
220 c.listView.SetMaxWidth(maxWidth)
221
222 return baseStyle.Padding(0, 0).
223 Border(lipgloss.NormalBorder()).
224 BorderBottom(false).
225 BorderRight(false).
226 BorderLeft(false).
227 BorderBackground(t.Background()).
228 BorderForeground(t.TextMuted()).
229 Width(c.width).
230 Render(c.listView.View())
231}
232
233func (c *completionDialogCmp) SetWidth(width int) {
234 c.width = width
235}
236
237func (c *completionDialogCmp) BindingKeys() []key.Binding {
238 return layout.KeyMapToSlice(completionDialogKeys)
239}
240
241func NewCompletionDialogCmp(completionProvider CompletionProvider) CompletionDialog {
242 ti := textarea.New()
243
244 items, err := completionProvider.GetChildEntries("")
245 if err != nil {
246 logging.Error("Failed to get child entries", err)
247 }
248
249 li := utilComponents.NewSimpleList(
250 items,
251 7,
252 "No file matches found",
253 false,
254 )
255
256 return &completionDialogCmp{
257 query: "",
258 completionProvider: completionProvider,
259 pseudoSearchTextArea: ti,
260 listView: li,
261 }
262}