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: 30,
74 height: 10,
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.KeyPressMsg:
93 switch {
94 case key.Matches(msg, c.keyMap.Up):
95 u, cmd := c.list.Update(msg)
96 c.list = u.(list.ListModel)
97 return c, cmd
98
99 case key.Matches(msg, c.keyMap.Down):
100 d, cmd := c.list.Update(msg)
101 c.list = d.(list.ListModel)
102 return c, cmd
103 case key.Matches(msg, c.keyMap.Select):
104 selectedItemInx := c.list.SelectedIndex()
105 if selectedItemInx == list.NoSelection {
106 return c, nil // No item selected, do nothing
107 }
108 items := c.list.Items()
109 selectedItem := items[selectedItemInx].(CompletionItem).Value()
110 c.open = false // Close completions after selection
111 return c, util.CmdHandler(SelectCompletionMsg{
112 Value: selectedItem,
113 })
114 case key.Matches(msg, c.keyMap.Cancel):
115 if c.open {
116 c.open = false
117 return c, util.CmdHandler(CompletionsClosedMsg{})
118 }
119 }
120 case CloseCompletionsMsg:
121 c.open = false
122 c.query = ""
123 return c, tea.Batch(
124 c.list.SetItems([]util.Model{}),
125 util.CmdHandler(CompletionsClosedMsg{}),
126 )
127 case OpenCompletionsMsg:
128 c.open = true
129 c.query = ""
130 c.x = msg.X
131 c.y = msg.Y
132 items := []util.Model{}
133 t := styles.CurrentTheme()
134 for _, completion := range msg.Completions {
135 item := NewCompletionItem(completion.Title, completion.Value, WithBackgroundColor(t.BgSubtle))
136 items = append(items, item)
137 }
138 c.height = max(min(10, len(items)), 1) // Ensure at least 1 item height
139 cmds := []tea.Cmd{
140 c.list.SetSize(c.width, c.height),
141 c.list.SetItems(items),
142 }
143 return c, tea.Batch(cmds...)
144 case FilterCompletionsMsg:
145 c.query = msg.Query
146 if !c.open {
147 return c, nil // If completions are not open, do nothing
148 }
149 cmd := c.list.Filter(msg.Query)
150 c.height = max(min(10, len(c.list.Items())), 1)
151 return c, tea.Batch(
152 cmd,
153 c.list.SetSize(c.width, c.height),
154 )
155 }
156 return c, nil
157}
158
159// View implements Completions.
160func (c *completionsCmp) View() string {
161 if len(c.list.Items()) == 0 {
162 return c.style().Render("No completions found")
163 }
164
165 return c.style().Render(c.list.View())
166}
167
168func (c *completionsCmp) style() lipgloss.Style {
169 t := styles.CurrentTheme()
170 return t.S().Base.
171 Width(c.width).
172 Height(c.height).
173 Background(t.BgSubtle)
174}
175
176func (c *completionsCmp) Open() bool {
177 return c.open
178}
179
180func (c *completionsCmp) Query() string {
181 return c.query
182}
183
184func (c *completionsCmp) KeyMap() KeyMap {
185 return c.keyMap
186}
187
188func (c *completionsCmp) Position() (int, int) {
189 return c.x, c.y - c.height
190}