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