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