complete.go

  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}