1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
|
--- Manages most of the state for the completion list such that downstream consumers can be mostly stateless
--- @class (exact) blink.cmp.CompletionList
--- @field config blink.cmp.CompletionListConfig
--- @field show_emitter blink.cmp.EventEmitter<blink.cmp.CompletionListShowEvent>
--- @field hide_emitter blink.cmp.EventEmitter<blink.cmp.CompletionListHideEvent>
--- @field select_emitter blink.cmp.EventEmitter<blink.cmp.CompletionListSelectEvent>
--- @field accept_emitter blink.cmp.EventEmitter<blink.cmp.CompletionListAcceptEvent>
---
--- @field context? blink.cmp.Context
--- @field items blink.cmp.CompletionItem[]
--- @field selected_item_idx? number
--- @field preview_undo? { text_edit: lsp.TextEdit, cursor: integer[]?}
---
--- @field show fun(context: blink.cmp.Context, items: table<string, blink.cmp.CompletionItem[]>)
--- @field fuzzy fun(context: blink.cmp.Context, items: table<string, blink.cmp.CompletionItem[]>): blink.cmp.CompletionItem[]
--- @field hide fun()
---
--- @field get_selected_item fun(): blink.cmp.CompletionItem?
--- @field get_selection_mode fun(context: blink.cmp.Context): { preselect: boolean, auto_insert: boolean }
--- @field get_item_idx_in_list fun(item?: blink.cmp.CompletionItem): number?
--- @field select fun(idx?: number, opts?: { auto_insert?: boolean, undo_preview?: boolean, is_explicit_selection?: boolean })
--- @field select_next fun(opts?: blink.cmp.CompletionListSelectOpts)
--- @field select_prev fun(opts?: blink.cmp.CompletionListSelectOpts)
---
--- @field undo_preview fun()
--- @field apply_preview fun(item: blink.cmp.CompletionItem)
--- @field accept fun(opts?: blink.cmp.CompletionListAcceptOpts): boolean Applies the currently selected item, returning true if it succeeded
--- @class blink.cmp.CompletionListSelectOpts
--- @field auto_insert? boolean When `true`, inserts the completion item automatically when selecting it
--- @class blink.cmp.CompletionListSelectAndAcceptOpts
--- @field callback? fun() Called after the item is accepted
--- @class blink.cmp.CompletionListAcceptOpts : blink.cmp.CompletionListSelectAndAcceptOpts
--- @field index? number The index of the item to accept, if not provided, the currently selected item will be accepted
--- @class blink.cmp.CompletionListShowEvent
--- @field items blink.cmp.CompletionItem[]
--- @field context blink.cmp.Context
--- @class blink.cmp.CompletionListHideEvent
--- @field context blink.cmp.Context
--- @class blink.cmp.CompletionListSelectEvent
--- @field idx? number
--- @field item? blink.cmp.CompletionItem
--- @field items blink.cmp.CompletionItem[]
--- @field context blink.cmp.Context
--- @class blink.cmp.CompletionListAcceptEvent
--- @field item blink.cmp.CompletionItem
--- @field context blink.cmp.Context
--- @type blink.cmp.CompletionList
--- @diagnostic disable-next-line: missing-fields
local list = {
select_emitter = require('blink.cmp.lib.event_emitter').new('select', 'BlinkCmpListSelect'),
accept_emitter = require('blink.cmp.lib.event_emitter').new('accept', 'BlinkCmpAccept'),
show_emitter = require('blink.cmp.lib.event_emitter').new('show', 'BlinkCmpShow'),
hide_emitter = require('blink.cmp.lib.event_emitter').new('hide', 'BlinkCmpHide'),
config = require('blink.cmp.config').completion.list,
context = nil,
items = {},
is_explicitly_selected = false,
preview_undo = nil,
}
---------- State ----------
function list.show(context, items_by_source)
-- reset state for new context
local is_new_context = not list.context or list.context.id ~= context.id
if is_new_context then
list.preview_undo = nil
list.is_explicitly_selected = false
end
-- if the keyword changed, the list is no longer explicitly selected
local bounds_equal = list.context ~= nil
and list.context.bounds.start_col == context.bounds.start_col
and list.context.bounds.length == context.bounds.length
if not bounds_equal then list.is_explicitly_selected = false end
local previous_selected_item = list.get_selected_item()
-- update the context/list and emit
list.context = context
list.items = list.fuzzy(context, items_by_source)
if #list.items == 0 then
list.hide_emitter:emit({ context = context })
else
list.show_emitter:emit({ items = list.items, context = context })
end
-- maintain the selection if the user selected an item
local previous_item_idx = list.get_item_idx_in_list(previous_selected_item)
if list.is_explicitly_selected and previous_item_idx ~= nil and previous_item_idx <= 10 then
list.select(previous_item_idx, { auto_insert = false, undo_preview = false })
-- otherwise, use the default selection
else
list.select(
list.get_selection_mode(list.context).preselect and 1 or nil,
{ auto_insert = false, undo_preview = false, is_explicit_selection = false }
)
end
end
function list.fuzzy(context, items_by_source)
local fuzzy = require('blink.cmp.fuzzy')
local filtered_items = fuzzy.fuzzy(
context.get_line(),
context.get_cursor()[2],
items_by_source,
require('blink.cmp.config').completion.keyword.range
)
-- apply the per source max_items
filtered_items = require('blink.cmp.sources.lib').apply_max_items_for_completions(context, filtered_items)
-- apply the global max_items
return require('blink.cmp.lib.utils').slice(filtered_items, 1, list.config.max_items)
end
function list.hide() list.hide_emitter:emit({ context = list.context }) end
---------- Selection ----------
function list.get_selected_item() return list.items[list.selected_item_idx] end
function list.get_selection_mode(context)
assert(context ~= nil, 'Context must be set before getting selection mode')
local preselect = list.config.selection.preselect
if type(preselect) == 'function' then preselect = preselect(context) end
--- @cast preselect boolean
local auto_insert = list.config.selection.auto_insert
if type(auto_insert) == 'function' then auto_insert = auto_insert(context) end
--- @cast auto_insert boolean
return { preselect = preselect, auto_insert = auto_insert }
end
function list.get_item_idx_in_list(item)
if item == nil then return end
return require('blink.cmp.lib.utils').find_idx(list.items, function(i) return i.label == item.label end)
end
function list.select(idx, opts)
opts = opts or {}
local item = list.items[idx]
local auto_insert = opts.auto_insert
if auto_insert == nil then auto_insert = list.get_selection_mode(list.context).auto_insert end
require('blink.cmp.completion.trigger').suppress_events_for_callback(function()
if opts.undo_preview ~= false then list.undo_preview() end
if auto_insert and item ~= nil then list.apply_preview(item) end
end)
--- @diagnostic disable-next-line: assign-type-mismatch
list.is_explicitly_selected = opts.is_explicit_selection == nil and true or opts.is_explicit_selection
list.selected_item_idx = idx
list.select_emitter:emit({ idx = idx, item = item, items = list.items, context = list.context })
end
function list.select_next(opts)
if #list.items == 0 or list.context == nil then return end
-- haven't selected anything yet, select the first item
if list.selected_item_idx == nil then return list.select(1, opts) end
-- end of the list
if list.selected_item_idx == #list.items then
-- cycling around has been disabled, ignore
if not list.config.cycle.from_bottom then return end
-- preselect is not enabled, we go back to no selection
if not list.get_selection_mode(list.context).preselect then return list.select(nil, opts) end
-- otherwise, we cycle around
return list.select(1, opts)
end
-- typical case, select the next item
list.select(list.selected_item_idx + 1, opts)
end
function list.select_prev(opts)
if #list.items == 0 or list.context == nil then return end
-- haven't selected anything yet, select the last item
if list.selected_item_idx == nil then return list.select(#list.items, opts) end
-- start of the list
if list.selected_item_idx == 1 then
-- cycling around has been disabled, ignore
if not list.config.cycle.from_top then return end
-- auto_insert is enabled, we go back to no selection
if list.get_selection_mode(list.context).auto_insert then return list.select(nil, opts) end
-- otherwise, we cycle around
return list.select(#list.items, opts)
end
-- typical case, select the previous item
list.select(list.selected_item_idx - 1, opts)
end
---------- Preview ----------
function list.undo_preview()
if list.preview_undo == nil then return end
require('blink.cmp.lib.text_edits').apply({ list.preview_undo.text_edit })
if list.preview_undo.cursor then
require('blink.cmp.completion.trigger.context').set_cursor(list.preview_undo.cursor)
end
list.preview_undo = nil
end
function list.apply_preview(item)
-- undo the previous preview if it exists
list.undo_preview()
-- apply the new preview
local undo_text_edit, undo_cursor = require('blink.cmp.completion.accept.preview')(item)
list.preview_undo = { text_edit = undo_text_edit, cursor = undo_cursor }
end
---------- Accept ----------
function list.accept(opts)
opts = opts or {}
local item = list.items[opts.index or list.selected_item_idx]
if item == nil then return false end
list.undo_preview()
local accept = require('blink.cmp.completion.accept')
accept(list.context, item, function()
list.accept_emitter:emit({ item = item, context = list.context })
if opts.callback then opts.callback() end
end)
return true
end
return list
|