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}