turbo-editors/turbo-corepublic Fork 0
28d59854361aeda8541d853093e732126f3d7bff
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

completion.go · 268 lines · 6.9 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 17h ago1package app
2
3import (
4 "strings"
5
6 "github.com/gdamore/tcell/v2"
7
8 "codeberg.org/turbo-editors/turbo-core/lsp"
9 "codeberg.org/turbo-editors/turbo-core/theme"
10 "codeberg.org/turbo-editors/turbo-core/ui"
11)
12
13// Completion popup geometry. It is deliberately small: a list that covers half
14// the file hides the very code the completion is about.
15const (
16 completionMaxRows = 8
17 completionMinWidth = 24
18 completionMaxWidth = 60
19)
20
21// CompletionBox is the list of suggestions that pops up under the cursor.
22//
23// It filters as the user types, so the language server is asked once and the
24// narrowing that follows costs nothing.
25type CompletionBox struct {
26 ui.Box
27 items []lsp.CompletionItem
28 matches []int // indexes into items, in the order they are shown
29 selected int
30 top int
31 prefix string
32 visible bool
33
34 // OnAccept is called with the chosen item.
35 OnAccept func(lsp.CompletionItem)
36}
37
38// Visible reports whether the popup is showing.
39func (c *CompletionBox) Visible() bool { return c.visible }
40
41// Show opens the popup with a set of suggestions, filtered by prefix, anchored
42// so that its top-left corner is just below the cursor.
43//
44// It closes again straight away if nothing matches, because a popup showing
45// nothing is worse than no popup.
46func (c *CompletionBox) Show(items []lsp.CompletionItem, prefix string, cursorX, cursorY int, screen ui.Rect) {
47 c.items = items
48 c.prefix = prefix
49 c.selected = 0
50 c.top = 0
51 c.refilter()
52
53 if len(c.matches) == 0 {
54 c.Hide()
55 return
56 }
57 c.visible = true
58 c.SetBounds(c.placeUnder(cursorX, cursorY, screen))
59}
60
61// Hide closes the popup.
62func (c *CompletionBox) Hide() {
63 c.visible = false
64 c.items = nil
65 c.matches = nil
66 c.prefix = ""
67}
68
69// SetPrefix narrows the list as the user keeps typing, and closes it once
70// nothing matches any more.
71func (c *CompletionBox) SetPrefix(prefix string) {
72 if !c.visible {
73 return
74 }
75 c.prefix = prefix
76 c.selected = 0
77 c.top = 0
78 c.refilter()
79
80 if len(c.matches) == 0 {
81 c.Hide()
82 }
83}
84
85// refilter recomputes which items match the prefix.
86//
87// Matching is case-insensitive on the prefix, which is what makes typing
88// "prin" find "Println" without having to guess the case first.
89func (c *CompletionBox) refilter() {
90 c.matches = c.matches[:0]
91 wanted := strings.ToLower(c.prefix)
92
93 for i, item := range c.items {
94 if strings.HasPrefix(strings.ToLower(item.Label), wanted) {
95 c.matches = append(c.matches, i)
96 }
97 }
98}
99
100// placeUnder returns where the popup should sit: under the cursor if it fits,
101// above it if it does not, and always inside the screen.
102func (c *CompletionBox) placeUnder(cursorX, cursorY int, screen ui.Rect) ui.Rect {
103 height := min(len(c.matches), completionMaxRows) + 2
104 width := min(max(c.widestEntry()+4, completionMinWidth), completionMaxWidth)
105
106 bounds := ui.Rect{X: cursorX - 1, Y: cursorY + 1, W: width, H: height}
107 if bounds.Bottom() > screen.Bottom() {
108 bounds.Y = cursorY - height
109 }
110 return bounds.ClampInto(screen)
111}
112
113// widestEntry returns the width of the longest line the popup will show.
114func (c *CompletionBox) widestEntry() int {
115 widest := 0
116 for _, index := range c.matches {
117 widest = max(widest, len([]rune(c.entryText(c.items[index]))))
118 }
119 return widest
120}
121
122// entryText is one line of the popup: the label, then its kind.
123func (c *CompletionBox) entryText(item lsp.CompletionItem) string {
124 if item.Kind == 0 {
125 return item.Label
126 }
127 return item.Label + " " + item.Kind.String()
128}
129
130// Selected returns the highlighted item and whether there is one.
131func (c *CompletionBox) Selected() (lsp.CompletionItem, bool) {
132 if !c.visible || c.selected >= len(c.matches) {
133 return lsp.CompletionItem{}, false
134 }
135 return c.items[c.matches[c.selected]], true
136}
137
138// Count returns how many suggestions are currently shown.
139func (c *CompletionBox) Count() int { return len(c.matches) }
140
141// Matches returns the suggestions currently shown, in the order they appear.
142func (c *CompletionBox) Matches() []lsp.CompletionItem {
143 items := make([]lsp.CompletionItem, 0, len(c.matches))
144 for _, index := range c.matches {
145 items = append(items, c.items[index])
146 }
147 return items
148}
149
150// Accept chooses the highlighted item and closes the popup.
151func (c *CompletionBox) Accept() {
152 item, ok := c.Selected()
153 c.Hide()
154 if ok && c.OnAccept != nil {
155 c.OnAccept(item)
156 }
157}
158
159// move walks the list, clamping at either end rather than wrapping: a list
160// that wraps makes it easy to sail past the entry you wanted.
161func (c *CompletionBox) move(by int) {
162 if len(c.matches) == 0 {
163 return
164 }
165 c.selected = min(max(c.selected+by, 0), len(c.matches)-1)
166
167 rows := min(len(c.matches), completionMaxRows)
168 c.top = min(c.top, c.selected)
169 if c.selected >= c.top+rows {
170 c.top = c.selected - rows + 1
171 }
172}
173
174// Draw paints the popup, framed and shadowed like every other floating thing.
175func (c *CompletionBox) Draw(p *ui.Painter, th *theme.Theme) {
176 if !c.visible {
177 return
178 }
179
180 bounds := c.Bounds()
181 ui.DrawShadow(p, bounds, th.Style(theme.KeyShadow))
182
183 panel := p.Sub(bounds)
184 panel.Clear(th.Style(theme.KeyCompletionItem))
185 ui.DrawFrame(panel, panel.Size(), ui.FrameSingle, th.Style(theme.KeyCompletionFrame))
186
187 rows := min(panel.Size().H-2, len(c.matches)-c.top)
188 for row := range rows {
189 c.drawEntry(panel, th, row)
190 }
191}
192
193// drawEntry paints one line of the popup.
194func (c *CompletionBox) drawEntry(p *ui.Painter, th *theme.Theme, row int) {
195 index := c.matches[c.top+row]
196 item := c.items[index]
197 width := p.Size().W - 2
198
199 style := th.Style(theme.KeyCompletionItem)
200 if c.top+row == c.selected {
201 style = th.Style(theme.KeyCompletionSelected)
202 }
203
204 p.HLine(1, row+1, width, ' ', style)
205 end := p.TextLimited(1, row+1, width, item.Label, style)
206
207 if item.Kind != 0 {
208 tag := " " + item.Kind.String()
209 x := p.Size().W - 1 - len([]rune(tag))
210 if x > end {
211 p.Text(x, row+1, tag, th.Style(theme.KeyCompletionDetail))
212 }
213 }
214}
215
216// HandleKey works the popup and reports whether it consumed the key.
217//
218// Anything it does not use — a printable character, most notably — falls
219// through to the editor, which is what lets the user keep typing while the
220// list narrows itself.
221func (c *CompletionBox) HandleKey(ev *tcell.EventKey) bool {
222 if !c.visible {
223 return false
224 }
225
226 switch ev.Key() {
227 case tcell.KeyUp:
228 c.move(-1)
229 case tcell.KeyDown:
230 c.move(+1)
231 case tcell.KeyPgUp:
232 c.move(-completionMaxRows)
233 case tcell.KeyPgDn:
234 c.move(+completionMaxRows)
235 case tcell.KeyEnter, tcell.KeyTab:
236 c.Accept()
237 case tcell.KeyEscape:
238 c.Hide()
239 default:
240 return false
241 }
242 return true
243}
244
245// HandleMouse selects and accepts entries with the pointer.
246func (c *CompletionBox) HandleMouse(ev *tcell.EventMouse) bool {
247 if !c.visible {
248 return false
249 }
250
251 x, y := ev.Position()
252 if !c.Bounds().Contains(x, y) {
253 if ev.Buttons() == tcell.Button1 {
254 c.Hide() // a click elsewhere puts the list away
255 }
256 return false
257 }
258
259 index := c.top + y - c.Bounds().Y - 1
260 if index < 0 || index >= len(c.matches) {
261 return true
262 }
263 c.selected = index
264 if ev.Buttons() == tcell.Button1 {
265 c.Accept()
266 }
267 return true
268}