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