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