completions.go

  1package completions
  2
  3import (
  4	"github.com/charmbracelet/bubbles/v2/key"
  5	tea "github.com/charmbracelet/bubbletea/v2"
  6	"github.com/charmbracelet/lipgloss/v2"
  7	"github.com/opencode-ai/opencode/internal/tui/components/core/list"
  8	"github.com/opencode-ai/opencode/internal/tui/styles"
  9	"github.com/opencode-ai/opencode/internal/tui/theme"
 10	"github.com/opencode-ai/opencode/internal/tui/util"
 11)
 12
 13type Completion struct {
 14	Title string // The title of the completion item
 15	Value any    // The value of the completion item
 16}
 17
 18type OpenCompletionsMsg struct {
 19	Completions []Completion
 20	X           int // X position for the completions popup
 21	Y           int // Y position for the completions popup
 22}
 23
 24type FilterCompletionsMsg struct {
 25	Query string // The query to filter completions
 26}
 27
 28type CompletionsClosedMsg struct{}
 29
 30type CloseCompletionsMsg struct{}
 31
 32type SelectCompletionMsg struct {
 33	Value any // The value of the selected completion item
 34}
 35
 36type Completions interface {
 37	util.Model
 38	Open() bool
 39	Query() string // Returns the current filter query
 40	KeyMap() KeyMap
 41	Position() (int, int) // Returns the X and Y position of the completions popup
 42}
 43
 44type completionsCmp struct {
 45	width  int
 46	height int  // Height of the completions component`
 47	x      int  // X position for the completions popup\
 48	y      int  // Y position for the completions popup
 49	open   bool // Indicates if the completions are open
 50	keyMap KeyMap
 51
 52	list  list.ListModel
 53	query string // The current filter query
 54}
 55
 56func New() Completions {
 57	completionsKeyMap := DefaultKeyMap()
 58	keyMap := list.DefaultKeyMap()
 59	keyMap.Up.SetEnabled(false)
 60	keyMap.Down.SetEnabled(false)
 61	keyMap.NDown.SetEnabled(false)
 62	keyMap.NUp.SetEnabled(false)
 63	keyMap.HalfPageDown.SetEnabled(false)
 64	keyMap.HalfPageUp.SetEnabled(false)
 65	keyMap.Home.SetEnabled(false)
 66	keyMap.End.SetEnabled(false)
 67	keyMap.UpOneItem = completionsKeyMap.Up
 68	keyMap.DownOneItem = completionsKeyMap.Down
 69
 70	l := list.New(
 71		list.WithReverse(true),
 72		list.WithKeyMap(keyMap),
 73		list.WithHideFilterInput(true),
 74	)
 75	return &completionsCmp{
 76		width:  30,
 77		height: 10,
 78		list:   l,
 79		query:  "",
 80		keyMap: completionsKeyMap,
 81	}
 82}
 83
 84// Init implements Completions.
 85func (c *completionsCmp) Init() tea.Cmd {
 86	return tea.Sequence(
 87		c.list.Init(),
 88		c.list.SetSize(c.width, c.height),
 89	)
 90}
 91
 92// Update implements Completions.
 93func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 94	switch msg := msg.(type) {
 95	case tea.KeyPressMsg:
 96		switch {
 97		case key.Matches(msg, c.keyMap.Up):
 98			u, cmd := c.list.Update(msg)
 99			c.list = u.(list.ListModel)
100			return c, cmd
101
102		case key.Matches(msg, c.keyMap.Down):
103			d, cmd := c.list.Update(msg)
104			c.list = d.(list.ListModel)
105			return c, cmd
106		case key.Matches(msg, c.keyMap.Select):
107			selectedItemInx := c.list.SelectedIndex()
108			if selectedItemInx == list.NoSelection {
109				return c, nil // No item selected, do nothing
110			}
111			items := c.list.Items()
112			selectedItem := items[selectedItemInx].(CompletionItem).Value()
113			c.open = false // Close completions after selection
114			return c, util.CmdHandler(SelectCompletionMsg{
115				Value: selectedItem,
116			})
117		case key.Matches(msg, c.keyMap.Cancel):
118			if c.open {
119				c.open = false
120				return c, util.CmdHandler(CompletionsClosedMsg{})
121			}
122		}
123	case CloseCompletionsMsg:
124		c.open = false
125		c.query = ""
126		return c, tea.Batch(
127			c.list.SetItems([]util.Model{}),
128			util.CmdHandler(CompletionsClosedMsg{}),
129		)
130	case OpenCompletionsMsg:
131		c.open = true
132		c.query = ""
133		c.x = msg.X
134		c.y = msg.Y
135		items := []util.Model{}
136		t := styles.CurrentTheme()
137		for _, completion := range msg.Completions {
138			item := NewCompletionItem(completion.Title, completion.Value, WithBackgroundColor(t.BgSubtle))
139			items = append(items, item)
140		}
141		c.height = max(min(10, len(items)), 1) // Ensure at least 1 item height
142		cmds := []tea.Cmd{
143			c.list.SetSize(c.width, c.height),
144			c.list.SetItems(items),
145		}
146		return c, tea.Batch(cmds...)
147	case FilterCompletionsMsg:
148		c.query = msg.Query
149		if !c.open {
150			return c, nil // If completions are not open, do nothing
151		}
152		cmd := c.list.Filter(msg.Query)
153		c.height = max(min(10, len(c.list.Items())), 1)
154		return c, tea.Batch(
155			cmd,
156			c.list.SetSize(c.width, c.height),
157		)
158	}
159	return c, nil
160}
161
162// View implements Completions.
163func (c *completionsCmp) View() tea.View {
164	if len(c.list.Items()) == 0 {
165		return tea.NewView(c.style().Render("No completions found"))
166	}
167
168	view := tea.NewView(
169		c.style().Render(c.list.View().String()),
170	)
171	return view
172}
173
174func (c *completionsCmp) style() lipgloss.Style {
175	t := theme.CurrentTheme()
176	return styles.BaseStyle().
177		Width(c.width).
178		Height(c.height).
179		Background(t.BackgroundSecondary())
180}
181
182func (c *completionsCmp) Open() bool {
183	return c.open
184}
185
186func (c *completionsCmp) Query() string {
187	return c.query
188}
189
190func (c *completionsCmp) KeyMap() KeyMap {
191	return c.keyMap
192}
193
194func (c *completionsCmp) Position() (int, int) {
195	return c.x, c.y - c.height
196}