]> Sergey Matveev's repositories - vim-lsp.git/blob - autoload/lsp/completion.vim
Optimize dict lookup
[vim-lsp.git] / autoload / lsp / completion.vim
1 vim9script
2
3 # LSP completion related functions
4
5 import './util.vim'
6 import './buffer.vim' as buf
7 import './options.vim' as opt
8 import './textedit.vim'
9 import './snippet.vim'
10
11 # per-filetype omni-completion enabled/disabled table
12 var ftypeOmniCtrlMap: dict<bool> = {}
13
14 var defaultKinds: dict<string> = {
15   'Text':           't',
16   'Method':         'm',
17   'Function':       'f',
18   'Constructor':    'C',
19   'Field':          'F',
20   'Variable':       'v',
21   'Class':          'c',
22   'Interface':      'i',
23   'Module':         'M',
24   'Property':       'p',
25   'Unit':           'u',
26   'Value':          'V',
27   'Enum':           'e',
28   'Keyword':        'k',
29   'Snippet':        'S',
30   'Color':          'C',
31   'File':           'f',
32   'Reference':      'r',
33   'Folder':         'F',
34   'EnumMember':     'E',
35   'Constant':       'd',
36   'Struct':         's',
37   'Event':          'E',
38   'Operator':       'o',
39   'TypeParameter':  'T',
40   'Buffer':         'B',
41 }
42
43 # Returns true if omni-completion is enabled for filetype "ftype".
44 # Otherwise, returns false.
45 def LspOmniComplEnabled(ftype: string): bool
46   return ftypeOmniCtrlMap->get(ftype, false)
47 enddef
48
49 # Enables or disables omni-completion for filetype "fype"
50 export def OmniComplSet(ftype: string, enabled: bool)
51   ftypeOmniCtrlMap->extend({[ftype]: enabled})
52 enddef
53
54 # Map LSP complete item kind to a character
55 def LspCompleteItemKindChar(kind: number): string
56   var kindMap: list<string> = [
57     '',
58     'Text',
59     'Method',
60     'Function',
61     'Constructor',
62     'Field',
63     'Variable',
64     'Class',
65     'Interface',
66     'Module',
67     'Property',
68     'Unit',
69     'Value',
70     'Enum',
71     'Keyword',
72     'Snippet',
73     'Color',
74     'File',
75     'Reference',
76     'Folder',
77     'EnumMember',
78     'Constant',
79     'Struct',
80     'Event',
81     'Operator',
82     'TypeParameter',
83     'Buffer'
84   ]
85
86   if kind > 26
87     return ''
88   endif
89
90   var kindName = kindMap[kind]
91   var kindValue = defaultKinds[kindName]
92
93   if opt.lspOptions.customCompletionKinds && opt.lspOptions.completionKinds->has_key(kindName)
94     kindValue = opt.lspOptions.completionKinds[kindName]
95   endif
96
97   return kindValue
98 enddef
99
100 # Remove all the snippet placeholders from "str" and return the value.
101 # Based on a similar function in the vim-lsp plugin.
102 def MakeValidWord(str_arg: string): string
103   var str = str_arg->substitute('\$[0-9]\+\|\${\%(\\.\|[^}]\)\+}', '', 'g')
104   str = str->substitute('\\\(.\)', '\1', 'g')
105   var valid = str->matchstr('^[^"'' (<{\[\t\r\n]\+')
106   if valid->empty()
107     return str
108   endif
109   if valid =~ ':$'
110     return valid[: -2]
111   endif
112   return valid
113 enddef
114
115 # add completion from current buf
116 def CompletionFromBuffer(items: list<dict<any>>)
117   var words = {}
118   var start = reltime()
119   var timeout = opt.lspOptions.bufferCompletionTimeout
120   var linenr = 1
121   for line in getline(1, '$')
122     for word in line->split('\W\+')
123       if !words->has_key(word) && word->len() > 1
124         words[word] = 1
125         items->add({
126           label: word,
127           data: {
128             entryNames: [word],
129           },
130           kind: 26,
131           documentation: "",
132         })
133       endif
134     endfor
135     # Check every 200 lines if timeout is exceeded
136     if timeout > 0 && linenr % 200 == 0 && start->reltime()->reltimefloat() * 1000 > timeout
137       break
138     endif
139     linenr += 1
140   endfor
141 enddef
142
143 # process the 'textDocument/completion' reply from the LSP server
144 # Result: CompletionItem[] | CompletionList | null
145 export def CompletionReply(lspserver: dict<any>, cItems: any)
146   if cItems->empty()
147     if lspserver.omniCompletePending
148       lspserver.completeItems = []
149       lspserver.omniCompletePending = false
150     endif
151     return
152   endif
153
154   lspserver.completeItemsIsIncomplete = false
155
156   var items: list<dict<any>>
157   if cItems->type() == v:t_list
158     items = cItems
159   else
160     items = cItems.items
161     lspserver.completeItemsIsIncomplete = cItems->get('isIncomplete', false)
162   endif
163
164   # Get the keyword prefix before the current cursor column.
165   var chcol = charcol('.')
166   var starttext = chcol == 1 ? '' : getline('.')[ : chcol - 2]
167   var [prefix, start_idx, end_idx] = starttext->matchstrpos('\k*$')
168   if opt.lspOptions.completionMatcher == 'icase'
169     prefix = prefix->tolower()
170   endif
171
172   var start_col = start_idx + 1
173
174   if opt.lspOptions.ultisnipsSupport
175     snippet.CompletionUltiSnips(prefix, items)
176   elseif opt.lspOptions.vsnipSupport
177     snippet.CompletionVsnip(items)
178   endif
179
180   if opt.lspOptions.useBufferCompletion
181     CompletionFromBuffer(items)
182   endif
183
184   var completeItems: list<dict<any>> = []
185   for item in items
186     var d: dict<any> = {}
187
188     # TODO: Add proper support for item.textEdit.newText and item.textEdit.range
189     # Keep in mind that item.textEdit.range can start be way before the typed
190     # keyword.
191     if item->has_key('textEdit') && opt.lspOptions.completionMatcher != 'fuzzy'
192       var start_charcol: number
193       if prefix != ''
194         start_charcol = charidx(starttext, start_idx) + 1
195       else
196         start_charcol = chcol
197       endif
198       var textEdit = item.textEdit
199       var textEditStartCol =
200                 util.GetCharIdxWithoutCompChar(bufnr(), textEdit.range.start)
201       if textEditStartCol != start_charcol
202         var offset = start_charcol - textEditStartCol - 1
203         d.word = textEdit.newText[offset : ]
204       else
205         d.word = textEdit.newText
206       endif
207     elseif item->has_key('insertText')
208       d.word = item.insertText
209     else
210       d.word = item.label
211     endif
212
213     if item->get('insertTextFormat', 1) == 2
214       # snippet completion.  Needs a snippet plugin to expand the snippet.
215       # Remove all the snippet placeholders
216       d.word = MakeValidWord(d.word)
217     elseif !lspserver.completeItemsIsIncomplete || opt.lspOptions.useBufferCompletion
218       # Filter items only when "isIncomplete" is set (otherwise server would
219       #   have done the filtering) or when buffer completion is enabled
220
221       # plain text completion
222       if prefix != ''
223         # If the completion item text doesn't start with the current (case
224         # ignored) keyword prefix, skip it.
225         var filterText: string = item->get('filterText', d.word)
226         if opt.lspOptions.completionMatcher == 'icase'
227           if filterText->tolower()->stridx(prefix) != 0
228             continue
229           endif
230         # If the completion item text doesn't fuzzy match with the current
231         # keyword prefix, skip it.
232         elseif opt.lspOptions.completionMatcher == 'fuzzy'
233           if matchfuzzy([filterText], prefix)->empty()
234             continue
235           endif
236         # If the completion item text doesn't start with the current keyword
237         # prefix, skip it.
238         else
239           if filterText->stridx(prefix) != 0
240             continue
241           endif
242         endif
243       endif
244     endif
245
246     d.abbr = item.label
247     d.dup = 1
248
249     if opt.lspOptions.completionMatcher == 'icase'
250       d.icase = 1
251     endif
252
253     if item->has_key('kind')
254       # namespace CompletionItemKind
255       # map LSP kind to complete-item-kind
256       d.kind = LspCompleteItemKindChar(item.kind)
257     endif
258
259     if lspserver.completionLazyDoc
260       d.info = 'Lazy doc'
261     else
262       if item->has_key('detail') && item.detail != ''
263         # Solve a issue where if a server send a detail field
264         # with a "\n", on the menu will be everything joined with
265         # a "^@" separating it. (example: clangd)
266         d.menu = item.detail->split("\n")[0]
267       endif
268       if item->has_key('documentation')
269         if item.documentation->type() == v:t_string && item.documentation != ''
270           d.info = item.documentation
271         elseif item.documentation->type() == v:t_dict
272             && item.documentation.value->type() == v:t_string
273           d.info = item.documentation.value
274         endif
275       endif
276     endif
277
278     # Score is used for sorting.
279     d.score = item->get('sortText')
280     if d.score->empty()
281       d.score = item->get('label', '')
282     endif
283
284     d.user_data = item
285     completeItems->add(d)
286   endfor
287
288   if opt.lspOptions.completionMatcher != 'fuzzy'
289     # Lexographical sort (case-insensitive).
290     completeItems->sort((a, b) =>
291       a.score == b.score ? 0 : a.score >? b.score ? 1 : -1)
292   endif
293
294   if opt.lspOptions.autoComplete && !lspserver.omniCompletePending
295     if completeItems->empty()
296       # no matches
297       return
298     endif
299
300     var m = mode()
301     if m != 'i' && m != 'R' && m != 'Rv'
302       # If not in insert or replace mode, then don't start the completion
303       return
304     endif
305
306     if completeItems->len() == 1
307         && getline('.')->matchstr($'\C{completeItems[0].word}\>') != ''
308       # only one complete match. No need to show the completion popup
309       return
310     endif
311
312     completeItems->complete(start_col)
313   else
314     lspserver.completeItems = completeItems
315     lspserver.omniCompletePending = false
316   endif
317 enddef
318
319 # process the completion documentation
320 def ShowCompletionDocumentation(cItem: any)
321   if cItem->empty() || cItem->type() != v:t_dict
322     return
323   endif
324
325   # check if completion item is still selected
326   var cInfo = complete_info()
327   if cInfo->empty()
328       || !cInfo.pum_visible
329       || cInfo.selected == -1
330       || cInfo.items[cInfo.selected]->type() != v:t_dict
331       || cInfo.items[cInfo.selected].user_data->type() != v:t_dict
332       || cInfo.items[cInfo.selected].user_data.label != cItem.label
333     return
334   endif
335
336   var infoText: list<string>
337   var infoKind: string
338
339   if cItem->has_key('detail') && !cItem.detail->empty()
340     # Solve a issue where if a server send the detail field with "\n",
341     # on the completion popup, everything will be joined with "^@"
342     # (example: typescript-language-server)
343     infoText->extend(cItem.detail->split("\n"))
344   endif
345
346   if cItem->has_key('documentation')
347     if !infoText->empty()
348       infoText->extend(['- - -'])
349     endif
350     if cItem.documentation->type() == v:t_dict
351       # MarkupContent
352       if cItem.documentation.kind == 'plaintext'
353         infoText->extend(cItem.documentation.value->split("\n"))
354         infoKind = 'text'
355       elseif cItem.documentation.kind == 'markdown'
356         infoText->extend(cItem.documentation.value->split("\n"))
357         infoKind = 'lspgfm'
358       else
359         util.ErrMsg($'Unsupported documentation type ({cItem.documentation.kind})')
360         return
361       endif
362     elseif cItem.documentation->type() == v:t_string
363       infoText->extend(cItem.documentation->split("\n"))
364     else
365       util.ErrMsg($'Unsupported documentation ({cItem.documentation->string()})')
366       return
367     endif
368   endif
369
370   if infoText->empty()
371     return
372   endif
373
374   # check if completion item is changed in meantime
375   cInfo = complete_info()
376   if cInfo->empty()
377       || !cInfo.pum_visible
378       || cInfo.selected == -1
379       || cInfo.items[cInfo.selected]->type() != v:t_dict
380       || cInfo.items[cInfo.selected].user_data->type() != v:t_dict
381       || cInfo.items[cInfo.selected].user_data.label != cItem.label
382     return
383   endif
384
385   # autoComplete or &omnifunc with &completeopt =~ 'popup'
386   var id = popup_findinfo()
387   if id > 0
388     var bufnr = id->winbufnr()
389     id->popup_settext(infoText)
390     infoKind->setbufvar(bufnr, '&ft')
391     id->popup_show()
392   else
393     # &omnifunc with &completeopt =~ 'preview'
394     try
395       :wincmd P
396       :setlocal modifiable
397       bufnr()->deletebufline(1, '$')
398       infoText->append(0)
399       [1, 1]->cursor()
400       exe $'setlocal ft={infoKind}'
401       :wincmd p
402     catch /E441/ # No preview window
403     endtry
404   endif
405 enddef
406
407 # process the 'completionItem/resolve' reply from the LSP server
408 # Result: CompletionItem
409 export def CompletionResolveReply(lspserver: dict<any>, cItem: any)
410   ShowCompletionDocumentation(cItem)
411 enddef
412
413 # omni complete handler
414 def g:LspOmniFunc(findstart: number, base: string): any
415   var lspserver: dict<any> = buf.CurbufGetServerChecked('completion')
416   if lspserver->empty()
417     return -2
418   endif
419
420   if findstart
421     # first send all the changes in the current buffer to the LSP server
422     listener_flush()
423
424     lspserver.omniCompletePending = true
425     lspserver.completeItems = []
426     # initiate a request to LSP server to get list of completions
427     lspserver.getCompletion(1, '')
428
429     # locate the start of the word
430     var line = getline('.')
431     var start = charcol('.') - 1
432     var keyword: string = ''
433     while start > 0 && line[start - 1] =~ '\k'
434       keyword = line[start - 1] .. keyword
435       start -= 1
436     endwhile
437     lspserver.omniCompleteKeyword = keyword
438     return line->byteidx(start)
439   else
440     # Wait for the list of matches from the LSP server
441     var count: number = 0
442     while lspserver.omniCompletePending && count < 1000
443       if complete_check()
444         return v:none
445       endif
446       sleep 2m
447       count += 1
448     endwhile
449
450     if lspserver.omniCompletePending
451       return v:none
452     endif
453
454     var res: list<dict<any>> = lspserver.completeItems
455
456     var prefix = lspserver.omniCompleteKeyword
457
458     # Don't attempt to filter on the items, when "isIncomplete" is set
459     if prefix->empty() || lspserver.completeItemsIsIncomplete
460       return res
461     endif
462
463     if opt.lspOptions.completionMatcher == 'fuzzy'
464       return res->matchfuzzy(prefix, { key: 'word' })
465     endif
466
467     if opt.lspOptions.completionMatcher == 'icase'
468       return res->filter((i, v) => v.word->tolower()->stridx(prefix->tolower()) == 0)
469     endif
470
471     return res->filter((i, v) => v.word->stridx(prefix) == 0)
472   endif
473 enddef
474
475 # Insert mode completion handler. Used when 24x7 completion is enabled
476 # (default).
477 def LspComplete()
478   var lspserver: dict<any> = buf.CurbufGetServer('completion')
479   if lspserver->empty() || !lspserver.running || !lspserver.ready
480     return
481   endif
482
483   var cur_col: number = charcol('.')
484   var line: string = getline('.')
485
486   if cur_col == 0 || line->empty()
487     return
488   endif
489
490   # Trigger kind is 1 for 24x7 code complete or manual invocation
491   var triggerKind: number = 1
492   var triggerChar: string = ''
493
494   # If the character before the cursor is not a keyword character or is not
495   # one of the LSP completion trigger characters, then do nothing.
496   if line[cur_col - 2] !~ '\k'
497     var trigChars = lspserver.completionTriggerChars
498     var trigidx = trigChars->index(line[cur_col - 2])
499     if trigidx == -1
500       return
501     endif
502     # completion triggered by one of the trigger characters
503     triggerKind = 2
504     triggerChar = trigChars[trigidx]
505   endif
506
507   # first send all the changes in the current buffer to the LSP server
508   listener_flush()
509
510   # initiate a request to LSP server to get list of completions
511   lspserver.getCompletion(triggerKind, triggerChar)
512 enddef
513
514 # Lazy complete documentation handler
515 def LspResolve()
516   var lspserver: dict<any> = buf.CurbufGetServerChecked('completion')
517   if lspserver->empty()
518     return
519   endif
520
521   var item = v:event.completed_item
522   if item->has_key('user_data') && !item.user_data->empty()
523       if !item.user_data->has_key('documentation')
524         lspserver.resolveCompletion(item.user_data)
525       else
526         ShowCompletionDocumentation(item.user_data)
527       endif
528   endif
529 enddef
530
531 # If the completion popup documentation window displays "markdown" content,
532 # then set the 'filetype' to "lspgfm".
533 def LspSetPopupFileType()
534   var item = v:event.completed_item
535   if !item->has_key('user_data') || item.user_data->empty()
536     return
537   endif
538
539   var cItem = item.user_data
540   if cItem->type() != v:t_dict || !cItem->has_key('documentation')
541         \ || cItem.documentation->type() != v:t_dict
542         \ || cItem.documentation.kind != 'markdown'
543     return
544   endif
545
546   var id = popup_findinfo()
547   if id > 0
548     var bnum = id->winbufnr()
549     setbufvar(bnum, '&ft', 'lspgfm')
550   endif
551 enddef
552
553 # complete done handler (LSP server-initiated actions after completion)
554 def LspCompleteDone()
555   var lspserver: dict<any> = buf.CurbufGetServerChecked('completion')
556   if lspserver->empty()
557     return
558   endif
559
560   if v:completed_item->type() != v:t_dict
561     return
562   endif
563
564   var completionData: any = v:completed_item->get('user_data', '')
565   if completionData->type() != v:t_dict
566       || !completionData->has_key('additionalTextEdits')
567       || !opt.lspOptions.completionTextEdit
568     return
569   endif
570
571   var bnr: number = bufnr()
572   textedit.ApplyTextEdits(bnr, completionData.additionalTextEdits)
573 enddef
574
575 # Initialize buffer-local completion options and autocmds
576 export def BufferInit(lspserver: dict<any>, bnr: number, ftype: string)
577   if !lspserver.isCompletionProvider
578     # no support for completion
579     return
580   endif
581
582   if !opt.lspOptions.autoComplete && !LspOmniComplEnabled(ftype)
583     # LSP auto/omni completion support is not enabled for this buffer
584     return
585   endif
586
587   # buffer-local autocmds for completion
588   var acmds: list<dict<any>> = []
589
590   # set options for insert mode completion
591   if opt.lspOptions.autoComplete
592     if lspserver.completionLazyDoc
593       setbufvar(bnr, '&completeopt', 'menuone,popuphidden,noinsert,noselect')
594     else
595       setbufvar(bnr, '&completeopt', 'menuone,popup,noinsert,noselect')
596     endif
597     setbufvar(bnr, '&completepopup', 'width:80,highlight:Pmenu,align:item,border:off')
598     # <Enter> in insert mode stops completion and inserts a <Enter>
599     if !opt.lspOptions.noNewlineInCompletion
600       :inoremap <expr> <buffer> <CR> pumvisible() ? "\<C-Y>\<CR>" : "\<CR>"
601     endif
602
603     # Trigger 24x7 insert mode completion when text is changed
604     acmds->add({bufnr: bnr,
605                 event: 'TextChangedI',
606                 group: 'LSPBufferAutocmds',
607                 cmd: 'LspComplete()'})
608   else
609     setbufvar(bnr, '&omnifunc', 'g:LspOmniFunc')
610   endif
611
612   if lspserver.completionLazyDoc
613     # resolve additional documentation for a selected item
614     acmds->add({bufnr: bnr,
615                 event: 'CompleteChanged',
616                 group: 'LSPBufferAutocmds',
617                 cmd: 'LspResolve()'})
618   endif
619
620   acmds->add({bufnr: bnr,
621               event: 'CompleteChanged',
622               group: 'LSPBufferAutocmds',
623               cmd: 'LspSetPopupFileType()'})
624
625   # Execute LSP server initiated text edits after completion
626   acmds->add({bufnr: bnr,
627               event: 'CompleteDone',
628               group: 'LSPBufferAutocmds',
629               cmd: 'LspCompleteDone()'})
630
631   autocmd_add(acmds)
632 enddef
633
634 # vim: tabstop=8 shiftwidth=2 softtabstop=2