1package completions
2
3import (
4 "github.com/charmbracelet/bubbles/v2/key"
5 tea "github.com/charmbracelet/bubbletea/v2"
6 "github.com/charmbracelet/lipgloss/v2"
7 "github.com/opencode-ai/opencode/internal/tui/components/core/list"
8 "github.com/opencode-ai/opencode/internal/tui/styles"
9 "github.com/opencode-ai/opencode/internal/tui/theme"
10 "github.com/opencode-ai/opencode/internal/tui/util"
11)
12
13type Completion struct {
14 Title string // The title of the completion item
15 Value any // The value of the completion item
16}
17
18type OpenCompletionsMsg struct {
19 Completions []Completion
20 X int // X position for the completions popup
21 Y int // Y position for the completions popup
22}
23
24type FilterCompletionsMsg struct {
25 Query string // The query to filter completions
26}
27
28type CompletionsClosedMsg struct{}
29
30type CloseCompletionsMsg struct{}
31
32type SelectCompletionMsg struct {
33 Value any // The value of the selected completion item
34}
35
36type Completions interface {
37 util.Model
38 Open() bool
39 Query() string // Returns the current filter query
40 KeyMap() KeyMap
41 Position() (int, int) // Returns the X and Y position of the completions popup
42}
43
44type completionsCmp struct {
45 width int
46 height int // Height of the completions component`
47 x int // X position for the completions popup\
48 y int // Y position for the completions popup
49 open bool // Indicates if the completions are open
50 keyMap KeyMap
51
52 list list.ListModel
53 query string // The current filter query
54}
55
56func New() Completions {
57 completionsKeyMap := DefaultKeyMap()
58 keyMap := list.DefaultKeyMap()
59 keyMap.Up.SetEnabled(false)
60 keyMap.Down.SetEnabled(false)
61 keyMap.NDown.SetEnabled(false)
62 keyMap.NUp.SetEnabled(false)
63 keyMap.HalfPageDown.SetEnabled(false)
64 keyMap.HalfPageUp.SetEnabled(false)
65 keyMap.Home.SetEnabled(false)
66 keyMap.End.SetEnabled(false)
67 keyMap.UpOneItem = completionsKeyMap.Up
68 keyMap.DownOneItem = completionsKeyMap.Down
69
70 l := list.New(
71 list.WithReverse(true),
72 list.WithKeyMap(keyMap),
73 list.WithHideFilterInput(true),
74 )
75 return &completionsCmp{
76 width: 30,
77 height: 10,
78 list: l,
79 query: "",
80 keyMap: completionsKeyMap,
81 }
82}
83
84// Init implements Completions.
85func (c *completionsCmp) Init() tea.Cmd {
86 return tea.Sequence(
87 c.list.Init(),
88 c.list.SetSize(c.width, c.height),
89 )
90}
91
92// Update implements Completions.
93func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
94 switch msg := msg.(type) {
95 case tea.KeyPressMsg:
96 switch {
97 case key.Matches(msg, c.keyMap.Up):
98 u, cmd := c.list.Update(msg)
99 c.list = u.(list.ListModel)
100 return c, cmd
101
102 case key.Matches(msg, c.keyMap.Down):
103 d, cmd := c.list.Update(msg)
104 c.list = d.(list.ListModel)
105 return c, cmd
106 case key.Matches(msg, c.keyMap.Select):
107 selectedItemInx := c.list.SelectedIndex()
108 if selectedItemInx == list.NoSelection {
109 return c, nil // No item selected, do nothing
110 }
111 items := c.list.Items()
112 selectedItem := items[selectedItemInx].(CompletionItem).Value()
113 c.open = false // Close completions after selection
114 return c, util.CmdHandler(SelectCompletionMsg{
115 Value: selectedItem,
116 })
117 case key.Matches(msg, c.keyMap.Cancel):
118 if c.open {
119 c.open = false
120 return c, util.CmdHandler(CompletionsClosedMsg{})
121 }
122 }
123 case CloseCompletionsMsg:
124 c.open = false
125 c.query = ""
126 return c, tea.Batch(
127 c.list.SetItems([]util.Model{}),
128 util.CmdHandler(CompletionsClosedMsg{}),
129 )
130 case OpenCompletionsMsg:
131 c.open = true
132 c.query = ""
133 c.x = msg.X
134 c.y = msg.Y
135 items := []util.Model{}
136 for _, completion := range msg.Completions {
137 item := NewCompletionItem(completion.Title, completion.Value)
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 := theme.CurrentTheme()
175 return styles.BaseStyle().
176 Width(c.width).
177 Height(c.height).
178 Background(t.BackgroundSecondary())
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}