completions.go

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