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