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