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 return c, util.CmdHandler(CloseCompletionsMsg{})
126 }
127 case CloseCompletionsMsg:
128 c.open = false
129 c.query = ""
130 return c, tea.Batch(
131 c.list.SetItems([]util.Model{}),
132 util.CmdHandler(CompletionsClosedMsg{}),
133 )
134 case OpenCompletionsMsg:
135 c.open = true
136 c.query = ""
137 c.x = msg.X
138 c.y = msg.Y
139 items := []util.Model{}
140 t := styles.CurrentTheme()
141 for _, completion := range msg.Completions {
142 item := NewCompletionItem(completion.Title, completion.Value, WithBackgroundColor(t.BgSubtle))
143 items = append(items, item)
144 }
145 c.height = max(min(c.height, len(items)), 1) // Ensure at least 1 item height
146 cmds := []tea.Cmd{
147 c.list.SetSize(c.width, c.height),
148 c.list.SetItems(items),
149 }
150 return c, tea.Batch(cmds...)
151 case FilterCompletionsMsg:
152 c.query = msg.Query
153 if !c.open {
154 return c, nil // If completions are not open, do nothing
155 }
156 var cmds []tea.Cmd
157 cmds = append(cmds, c.list.Filter(msg.Query))
158 itemsLen := len(c.list.Items())
159 c.height = max(min(maxCompletionsHeight, itemsLen), 1)
160 cmds = append(cmds, c.list.SetSize(c.width, c.height))
161 return c, tea.Batch(cmds...)
162 }
163 return c, nil
164}
165
166// View implements Completions.
167func (c *completionsCmp) View() string {
168 if !c.open {
169 return ""
170 }
171 if len(c.list.Items()) == 0 {
172 return c.style().Render("No completions found")
173 }
174
175 return c.style().Render(c.list.View())
176}
177
178func (c *completionsCmp) style() lipgloss.Style {
179 t := styles.CurrentTheme()
180 return t.S().Base.
181 Width(c.width).
182 Height(c.height).
183 Background(t.BgSubtle)
184}
185
186func (c *completionsCmp) Open() bool {
187 return c.open
188}
189
190func (c *completionsCmp) Query() string {
191 return c.query
192}
193
194func (c *completionsCmp) KeyMap() KeyMap {
195 return c.keyMap
196}
197
198func (c *completionsCmp) Position() (int, int) {
199 return c.x, c.y - c.height
200}
201
202func (c *completionsCmp) Width() int {
203 return c.width
204}
205
206func (c *completionsCmp) Height() int {
207 return c.height
208}