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