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 t := styles.CurrentTheme()
137 for _, completion := range msg.Completions {
138 item := NewCompletionItem(completion.Title, completion.Value, WithBackgroundColor(t.BgSubtle))
139 items = append(items, item)
140 }
141 c.height = max(min(10, len(items)), 1) // Ensure at least 1 item height
142 cmds := []tea.Cmd{
143 c.list.SetSize(c.width, c.height),
144 c.list.SetItems(items),
145 }
146 return c, tea.Batch(cmds...)
147 case FilterCompletionsMsg:
148 c.query = msg.Query
149 if !c.open {
150 return c, nil // If completions are not open, do nothing
151 }
152 cmd := c.list.Filter(msg.Query)
153 c.height = max(min(10, len(c.list.Items())), 1)
154 return c, tea.Batch(
155 cmd,
156 c.list.SetSize(c.width, c.height),
157 )
158 }
159 return c, nil
160}
161
162// View implements Completions.
163func (c *completionsCmp) View() tea.View {
164 if len(c.list.Items()) == 0 {
165 return tea.NewView(c.style().Render("No completions found"))
166 }
167
168 view := tea.NewView(
169 c.style().Render(c.list.View().String()),
170 )
171 return view
172}
173
174func (c *completionsCmp) style() lipgloss.Style {
175 t := theme.CurrentTheme()
176 return styles.BaseStyle().
177 Width(c.width).
178 Height(c.height).
179 Background(t.BackgroundSecondary())
180}
181
182func (c *completionsCmp) Open() bool {
183 return c.open
184}
185
186func (c *completionsCmp) Query() string {
187 return c.query
188}
189
190func (c *completionsCmp) KeyMap() KeyMap {
191 return c.keyMap
192}
193
194func (c *completionsCmp) Position() (int, int) {
195 return c.x, c.y - c.height
196}