complete.go

  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}