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