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
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
|
local cache = require('cmp.utils.cache')
local char = require('cmp.utils.char')
local misc = require('cmp.utils.misc')
local str = require('cmp.utils.str')
local config = require('cmp.config')
local types = require('cmp.types')
local matcher = require('cmp.matcher')
---@class cmp.Entry
---@field public id integer
---@field public cache cmp.Cache
---@field public match_cache cmp.Cache
---@field public score integer
---@field public exact boolean
---@field public matches table
---@field public context cmp.Context
---@field public source cmp.Source
---@field public source_offset integer
---@field public source_insert_range lsp.Range
---@field public source_replace_range lsp.Range
---@field public completion_item lsp.CompletionItem
---@field public resolved_completion_item lsp.CompletionItem|nil
---@field public resolved_callbacks fun()[]
---@field public resolving boolean
---@field public confirmed boolean
local entry = {}
---Create new entry
---@param ctx cmp.Context
---@param source cmp.Source
---@param completion_item lsp.CompletionItem
---@return cmp.Entry
entry.new = function(ctx, source, completion_item)
local self = setmetatable({}, { __index = entry })
self.id = misc.id('entry.new')
self.cache = cache.new()
self.match_cache = cache.new()
self.score = 0
self.exact = false
self.matches = {}
self.context = ctx
self.source = source
self.source_offset = source.request_offset
self.source_insert_range = source:get_default_insert_range()
self.source_replace_range = source:get_default_replace_range()
self.completion_item = completion_item
self.resolved_completion_item = nil
self.resolved_callbacks = {}
self.resolving = false
self.confirmed = false
return self
end
---Make offset value
---@return integer
entry.get_offset = function(self)
return self.cache:ensure({ 'get_offset', self.resolved_completion_item and 1 or 0 }, function()
local offset = self.source_offset
if misc.safe(self:get_completion_item().textEdit) then
local range = misc.safe(self:get_completion_item().textEdit.insert) or misc.safe(self:get_completion_item().textEdit.range)
if range then
local c = misc.to_vimindex(self.context.cursor_line, range.start.character)
for idx = c, self.source_offset do
if not char.is_white(string.byte(self.context.cursor_line, idx)) then
offset = idx
break
end
end
end
else
-- NOTE
-- The VSCode does not implement this but it's useful if the server does not care about word patterns.
-- We should care about this performance.
local word = self:get_word()
for idx = self.source_offset - 1, self.source_offset - #word, -1 do
if char.is_semantic_index(self.context.cursor_line, idx) then
local c = string.byte(self.context.cursor_line, idx)
if char.is_white(c) then
break
end
local match = true
for i = 1, self.source_offset - idx do
local c1 = string.byte(word, i)
local c2 = string.byte(self.context.cursor_line, idx + i - 1)
if not c1 or not c2 or c1 ~= c2 then
match = false
break
end
end
if match then
offset = math.min(offset, idx)
end
end
end
end
return offset
end)
end
---Create word for vim.CompletedItem
---NOTE: This method doesn't clear the cache after completionItem/resolve.
---@return string
entry.get_word = function(self)
return self.cache:ensure({ 'get_word' }, function()
--NOTE: This is nvim-cmp specific implementation.
if misc.safe(self:get_completion_item().word) then
return self:get_completion_item().word
end
local word
if misc.safe(self:get_completion_item().textEdit) and not misc.empty(self:get_completion_item().textEdit.newText) then
word = str.trim(self:get_completion_item().textEdit.newText)
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = vim.lsp.util.parse_snippet(word)
end
local overwrite = self:get_overwrite()
if 0 < overwrite[2] or self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.get_word(word, string.byte(self.context.cursor_after_line, 1), overwrite[1] or 0)
end
elseif not misc.empty(self:get_completion_item().insertText) then
word = str.trim(self:get_completion_item().insertText)
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.get_word(vim.lsp.util.parse_snippet(word))
end
else
word = str.trim(self:get_completion_item().label)
end
return str.oneline(word)
end)
end
---Get overwrite information
---@return integer, integer
entry.get_overwrite = function(self)
return self.cache:ensure({ 'get_overwrite', self.resolved_completion_item and 1 or 0 }, function()
if misc.safe(self:get_completion_item().textEdit) then
local r = misc.safe(self:get_completion_item().textEdit.insert) or misc.safe(self:get_completion_item().textEdit.range)
if r then
local s = misc.to_vimindex(self.context.cursor_line, r.start.character)
local e = misc.to_vimindex(self.context.cursor_line, r['end'].character)
local before = self.context.cursor.col - s
local after = e - self.context.cursor.col
return { before, after }
end
end
return { 0, 0 }
end)
end
---Create filter text
---@return string
entry.get_filter_text = function(self)
return self.cache:ensure({ 'get_filter_text', self.resolved_completion_item and 1 or 0 }, function()
local word
if misc.safe(self:get_completion_item().filterText) then
word = self:get_completion_item().filterText
else
word = str.trim(self:get_completion_item().label)
end
return word
end)
end
---Get LSP's insert text
---@return string
entry.get_insert_text = function(self)
return self.cache:ensure({ 'get_insert_text', self.resolved_completion_item and 1 or 0 }, function()
local word
if misc.safe(self:get_completion_item().textEdit) then
word = str.trim(self:get_completion_item().textEdit.newText)
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}')
end
elseif misc.safe(self:get_completion_item().insertText) then
word = str.trim(self:get_completion_item().insertText)
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}')
end
else
word = str.trim(self:get_completion_item().label)
end
return word
end)
end
---Return the item is deprecated or not.
---@return boolean
entry.is_deprecated = function(self)
return self:get_completion_item().deprecated or vim.tbl_contains(self:get_completion_item().tags or {}, types.lsp.CompletionItemTag.Deprecated)
end
---Return view information.
---@param suggest_offset integer
---@param entries_buf integer The buffer this entry will be rendered into.
---@return { abbr: { text: string, bytes: integer, width: integer, hl_group: string }, kind: { text: string, bytes: integer, width: integer, hl_group: string }, menu: { text: string, bytes: integer, width: integer, hl_group: string } }
entry.get_view = function(self, suggest_offset, entries_buf)
local item = self:get_vim_item(suggest_offset)
return self.cache:ensure({ 'get_view', self.resolved_completion_item and 1 or 0, entries_buf }, function()
local view = {}
-- The result of vim.fn.strdisplaywidth depends on which buffer it was
-- called in because it reads the values of the option 'tabstop' when
-- rendering <Tab> characters.
vim.api.nvim_buf_call(entries_buf, function()
view.abbr = {}
view.abbr.text = item.abbr or ''
view.abbr.bytes = #view.abbr.text
view.abbr.width = vim.fn.strdisplaywidth(view.abbr.text)
view.abbr.hl_group = item.abbr_hl_group or (self:is_deprecated() and 'CmpItemAbbrDeprecated' or 'CmpItemAbbr')
view.kind = {}
view.kind.text = item.kind or ''
view.kind.bytes = #view.kind.text
view.kind.width = vim.fn.strdisplaywidth(view.kind.text)
view.kind.hl_group = item.kind_hl_group or ('CmpItemKind' .. (types.lsp.CompletionItemKind[self:get_kind()] or ''))
view.menu = {}
view.menu.text = item.menu or ''
view.menu.bytes = #view.menu.text
view.menu.width = vim.fn.strdisplaywidth(view.menu.text)
view.menu.hl_group = item.menu_hl_group or 'CmpItemMenu'
view.dup = item.dup
end)
return view
end)
end
---Make vim.CompletedItem
---@param suggest_offset integer
---@return vim.CompletedItem
entry.get_vim_item = function(self, suggest_offset)
return self.cache:ensure({ 'get_vim_item', suggest_offset, self.resolved_completion_item and 1 or 0 }, function()
local completion_item = self:get_completion_item()
local word = self:get_word()
local abbr = str.oneline(completion_item.label)
-- ~ indicator
local is_snippet = false
if #(misc.safe(completion_item.additionalTextEdits) or {}) > 0 then
is_snippet = true
elseif completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
is_snippet = self:get_insert_text() ~= word
elseif completion_item.kind == types.lsp.CompletionItemKind.Snippet then
is_snippet = true
end
if is_snippet then
abbr = abbr .. '~'
end
-- append delta text
if suggest_offset < self:get_offset() then
word = string.sub(self.context.cursor_before_line, suggest_offset, self:get_offset() - 1) .. word
end
-- labelDetails.
local menu = nil
if misc.safe(completion_item.labelDetails) then
menu = ''
if misc.safe(completion_item.labelDetails.detail) then
menu = menu .. completion_item.labelDetails.detail
end
if misc.safe(completion_item.labelDetails.description) then
menu = menu .. completion_item.labelDetails.description
end
end
-- remove duplicated string.
if self:get_offset() ~= self.context.cursor.col then
for i = 1, #word - 1 do
if str.has_prefix(self.context.cursor_after_line, string.sub(word, i, #word)) then
word = string.sub(word, 1, i - 1)
break
end
end
end
local cmp_opts = self:get_completion_item().cmp or {}
local vim_item = {
word = word,
abbr = abbr,
kind = cmp_opts.kind_text or types.lsp.CompletionItemKind[self:get_kind()] or types.lsp.CompletionItemKind[1],
kind_hl_group = cmp_opts.kind_hl_group,
menu = menu,
dup = self:get_completion_item().dup or 1,
}
if config.get().formatting.format then
vim_item = config.get().formatting.format(self, vim_item)
end
vim_item.word = str.oneline(vim_item.word or '')
vim_item.abbr = str.oneline(vim_item.abbr or '')
vim_item.kind = str.oneline(vim_item.kind or '')
vim_item.menu = str.oneline(vim_item.menu or '')
vim_item.equal = 1
vim_item.empty = 1
return vim_item
end)
end
---Get commit characters
---@return string[]
entry.get_commit_characters = function(self)
return misc.safe(self:get_completion_item().commitCharacters) or {}
end
---Return insert range
---@return lsp.Range|nil
entry.get_insert_range = function(self)
local insert_range
if misc.safe(self:get_completion_item().textEdit) then
if misc.safe(self:get_completion_item().textEdit.insert) then
insert_range = self:get_completion_item().textEdit.insert
else
insert_range = self:get_completion_item().textEdit.range
end
else
insert_range = {
start = {
line = self.context.cursor.row - 1,
character = math.min(misc.to_utfindex(self.context.cursor_line, self:get_offset()), self.source_insert_range.start.character),
},
['end'] = self.source_insert_range['end'],
}
end
return insert_range
end
---Return replace range
---@return lsp.Range|nil
entry.get_replace_range = function(self)
return self.cache:ensure({ 'get_replace_range', self.resolved_completion_item and 1 or 0 }, function()
local replace_range
if misc.safe(self:get_completion_item().textEdit) and misc.safe(self:get_completion_item().textEdit.replace) then
replace_range = self:get_completion_item().textEdit.replace
else
replace_range = {
start = {
line = self.source_replace_range.start.line,
character = math.min(misc.to_utfindex(self.context.cursor_line, self:get_offset()), self.source_replace_range.start.character),
},
['end'] = self.source_replace_range['end'],
}
end
return replace_range
end)
end
---Match line.
---@param input string
---@param matching_config cmp.MatchingConfig
---@return { score: integer, matches: table[] }
entry.match = function(self, input, matching_config)
return self.match_cache:ensure({
input,
self.resolved_completion_item and 1 or 0,
matching_config.disallow_fuzzy_matching and 1 or 0,
matching_config.disallow_partial_matching and 1 or 0,
matching_config.disallow_prefix_unmatching and 1 or 0,
}, function()
local option = {
disallow_fuzzy_matching = matching_config.disallow_fuzzy_matching,
disallow_partial_matching = matching_config.disallow_partial_matching,
disallow_prefix_unmatching = matching_config.disallow_prefix_unmatching,
synonyms = {
self:get_word(),
self:get_completion_item().label,
},
}
local score, matches, _
score, matches = matcher.match(input, self:get_filter_text(), option)
-- Support the language server that doesn't respect VSCode's behaviors.
if score == 0 then
if misc.safe(self:get_completion_item().textEdit) and not misc.empty(self:get_completion_item().textEdit.newText) then
local diff = self.source_offset - self:get_offset()
if diff > 0 then
local prefix = string.sub(self.context.cursor_line, self:get_offset(), self:get_offset() + diff)
local accept = nil
accept = accept or string.match(prefix, '^[^%a]+$')
accept = accept or string.find(self:get_completion_item().textEdit.newText, prefix, 1, true)
if accept then
score, matches = matcher.match(input, prefix .. self:get_filter_text(), option)
end
end
end
end
if self:get_filter_text() ~= self:get_completion_item().label then
_, matches = matcher.match(input, self:get_completion_item().label, { synonyms = { self:get_word() } })
end
return { score = score, matches = matches }
end)
end
---Get resolved completion item if possible.
---@return lsp.CompletionItem
entry.get_completion_item = function(self)
return self.cache:ensure({ 'get_completion_item', self.resolved_completion_item and 1 or 0 }, function()
if self.resolved_completion_item then
local completion_item = misc.copy(self.completion_item)
for k, v in pairs(self.resolved_completion_item) do
completion_item[k] = v or completion_item[k]
end
return completion_item
end
return self.completion_item
end)
end
---Create documentation
---@return string
entry.get_documentation = function(self)
local item = self:get_completion_item()
local documents = {}
-- detail
if misc.safe(item.detail) and item.detail ~= '' then
local ft = self.context.filetype
local dot_index = string.find(ft, '%.')
if dot_index ~= nil then
ft = string.sub(ft, 0, dot_index - 1)
end
table.insert(documents, {
kind = types.lsp.MarkupKind.Markdown,
value = ('```%s\n%s\n```'):format(ft, str.trim(item.detail)),
})
end
local documentation = item.documentation
if type(documentation) == 'string' and documentation ~= '' then
local value = str.trim(documentation)
if value ~= '' then
table.insert(documents, {
kind = types.lsp.MarkupKind.PlainText,
value = value,
})
end
elseif type(documentation) == 'table' and not misc.empty(documentation.value) then
local value = str.trim(documentation.value)
if value ~= '' then
table.insert(documents, {
kind = documentation.kind,
value = value,
})
end
end
return vim.lsp.util.convert_input_to_markdown_lines(documents)
end
---Get completion item kind
---@return lsp.CompletionItemKind
entry.get_kind = function(self)
return misc.safe(self:get_completion_item().kind) or types.lsp.CompletionItemKind.Text
end
---Execute completion item's command.
---@param callback fun()
entry.execute = function(self, callback)
self.source:execute(self:get_completion_item(), callback)
end
---Resolve completion item.
---@param callback fun()
entry.resolve = function(self, callback)
if self.resolved_completion_item then
return callback()
end
table.insert(self.resolved_callbacks, callback)
if not self.resolving then
self.resolving = true
self.source:resolve(self.completion_item, function(completion_item)
self.resolved_completion_item = misc.safe(completion_item) or self.completion_item
for _, c in ipairs(self.resolved_callbacks) do
c()
end
end)
end
end
return entry
|