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