3 # LSP completion related functions
6 import './buffer.vim' as buf
7 import './options.vim' as opt
8 import './textedit.vim'
10 # per-filetype omni-completion enabled/disabled table
11 var ftypeOmniCtrlMap: dict<bool> = {}
13 var defaultKinds: dict<string> = {
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)
48 # Enables or disables omni-completion for filetype "fype"
49 export def OmniComplSet(ftype: string, enabled: bool)
50 ftypeOmniCtrlMap->extend({[ftype]: enabled})
53 # Map LSP complete item kind to a character
54 def LspCompleteItemKindChar(kind: number): string
55 var kindMap: list<string> = [
89 var kindName = kindMap[kind]
90 var kindValue = defaultKinds[kindName]
92 if opt.lspOptions.customCompletionKinds && opt.lspOptions.completionKinds->has_key(kindName)
93 kindValue = opt.lspOptions.completionKinds[kindName]
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]\+')
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"
123 if line->empty() || line[0 : 6] == "snippet"
126 restxt = restxt .. line .. "\n"
135 documentation: restxt,
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}*$'
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))
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)
163 filterText: item.word,
166 newText: user_data.vsnip.snippet->join("\n"),
179 entryNames: [item.word],
184 value: documentation->join("\n"),
191 # add completion from current buf
192 def CompletionFromBuffer(items: list<dict<any>>)
194 for line in getline(1, '$')
195 for word in line->split('\W\+')
196 if !words->has_key(word) && word->len() > 1
211 # process the 'textDocument/completion' reply from the LSP server
212 # Result: CompletionItem[] | CompletionList | null
213 export def CompletionReply(lspserver: dict<any>, cItems: any)
215 if lspserver.omniCompletePending
216 lspserver.completeItems = []
217 lspserver.omniCompletePending = false
222 lspserver.completeItemsIsIncomplete = false
224 var items: list<dict<any>>
225 if cItems->type() == v:t_list
229 lspserver.completeItemsIsIncomplete = cItems->get('isIncomplete', false)
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()
240 var start_col = start_idx + 1
242 if opt.lspOptions.ultisnipsSupport
243 CompletionUltiSnips(prefix, items)
244 elseif opt.lspOptions.vsnipSupport
245 CompletionVsnip(items)
248 if opt.lspOptions.useBufferCompletion
249 CompletionFromBuffer(items)
252 var completeItems: list<dict<any>> = []
254 var d: dict<any> = {}
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
259 if item->has_key('textEdit') && opt.lspOptions.completionMatcher != 'fuzzy'
260 var start_charcol: number
262 start_charcol = charidx(starttext, start_idx) + 1
264 start_charcol = chcol
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 : ]
273 d.word = textEdit.newText
275 elseif item->has_key('insertText')
276 d.word = item.insertText
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
289 # plain text completion
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
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()
304 # If the completion item text doesn't start with the current keyword
307 if filterText->stridx(prefix) != 0
317 if opt.lspOptions.completionMatcher == 'icase'
321 if item->has_key('kind')
322 # namespace CompletionItemKind
323 # map LSP kind to complete-item-kind
324 d.kind = LspCompleteItemKindChar(item.kind)
327 if lspserver.completionLazyDoc
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]
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
346 # Score is used for sorting.
347 d.score = item->get('sortText')
349 d.score = item->get('label', '')
353 completeItems->add(d)
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)
362 if opt.lspOptions.autoComplete && !lspserver.omniCompletePending
363 if completeItems->empty()
369 if m != 'i' && m != 'R' && m != 'Rv'
370 # If not in insert or replace mode, then don't start the completion
374 if completeItems->len() == 1
375 && getline('.')->matchstr($'\C{completeItems[0].word}\>') != ''
376 # only one complete match. No need to show the completion popup
380 completeItems->complete(start_col)
382 lspserver.completeItems = completeItems
383 lspserver.omniCompletePending = false
387 # process the completion documentation
388 def ShowCompletionDocumentation(cItem: any)
389 if cItem->empty() || cItem->type() != v:t_dict
393 # check if completion item is still selected
394 var cInfo = complete_info()
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
404 var infoText: list<string>
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"))
414 if cItem->has_key('documentation')
415 if !infoText->empty()
416 infoText->extend(['- - -'])
418 if cItem.documentation->type() == v:t_dict
420 if cItem.documentation.kind == 'plaintext'
421 infoText->extend(cItem.documentation.value->split("\n"))
423 elseif cItem.documentation.kind == 'markdown'
424 infoText->extend(cItem.documentation.value->split("\n"))
427 util.ErrMsg($'Unsupported documentation type ({cItem.documentation.kind})')
430 elseif cItem.documentation->type() == v:t_string
431 infoText->extend(cItem.documentation->split("\n"))
433 util.ErrMsg($'Unsupported documentation ({cItem.documentation->string()})')
442 # check if completion item is changed in meantime
443 cInfo = complete_info()
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
453 # autoComplete or &omnifunc with &completeopt =~ 'popup'
454 var id = popup_findinfo()
456 var bufnr = id->winbufnr()
457 id->popup_settext(infoText)
458 infoKind->setbufvar(bufnr, '&ft')
461 # &omnifunc with &completeopt =~ 'preview'
465 bufnr()->deletebufline(1, '$')
468 exe $'setlocal ft={infoKind}'
470 catch /E441/ # No preview window
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)
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()
489 # first send all the changes in the current buffer to the LSP server
492 lspserver.omniCompletePending = true
493 lspserver.completeItems = []
494 # initiate a request to LSP server to get list of completions
495 lspserver.getCompletion(1, '')
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
505 lspserver.omniCompleteKeyword = keyword
506 return line->byteidx(start)
508 # Wait for the list of matches from the LSP server
509 var count: number = 0
510 while lspserver.omniCompletePending && count < 1000
518 if lspserver.omniCompletePending
522 var res: list<dict<any>> = lspserver.completeItems
524 var prefix = lspserver.omniCompleteKeyword
526 # Don't attempt to filter on the items, when "isIncomplete" is set
527 if prefix->empty() || lspserver.completeItemsIsIncomplete
531 if opt.lspOptions.completionMatcher == 'fuzzy'
532 return res->matchfuzzy(prefix, { key: 'word' })
535 if opt.lspOptions.completionMatcher == 'icase'
536 return res->filter((i, v) => v.word->tolower()->stridx(prefix->tolower()) == 0)
539 return res->filter((i, v) => v.word->stridx(prefix) == 0)
543 # Insert mode completion handler. Used when 24x7 completion is enabled
546 var lspserver: dict<any> = buf.CurbufGetServer('completion')
547 if lspserver->empty() || !lspserver.running || !lspserver.ready
551 var cur_col: number = charcol('.')
552 var line: string = getline('.')
554 if cur_col == 0 || line->empty()
558 # Trigger kind is 1 for 24x7 code complete or manual invocation
559 var triggerKind: number = 1
560 var triggerChar: string = ''
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])
570 # completion triggered by one of the trigger characters
572 triggerChar = trigChars[trigidx]
575 # first send all the changes in the current buffer to the LSP server
578 # initiate a request to LSP server to get list of completions
579 lspserver.getCompletion(triggerKind, triggerChar)
582 # Lazy complete documentation handler
584 var lspserver: dict<any> = buf.CurbufGetServerChecked('completion')
585 if lspserver->empty()
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)
594 ShowCompletionDocumentation(item.user_data)
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()
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'
614 var id = popup_findinfo()
616 var bnum = id->winbufnr()
617 setbufvar(bnum, '&ft', 'lspgfm')
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()
628 if v:completed_item->type() != v:t_dict
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
639 var bnr: number = bufnr()
640 textedit.ApplyTextEdits(bnr, completionData.additionalTextEdits)
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
650 if !opt.lspOptions.autoComplete && !LspOmniComplEnabled(ftype)
651 # LSP auto/omni completion support is not enabled for this buffer
655 # buffer-local autocmds for completion
656 var acmds: list<dict<any>> = []
658 # set options for insert mode completion
659 if opt.lspOptions.autoComplete
660 if lspserver.completionLazyDoc
661 setbufvar(bnr, '&completeopt', 'menuone,popuphidden,noinsert,noselect')
663 setbufvar(bnr, '&completeopt', 'menuone,popup,noinsert,noselect')
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>"
671 # Trigger 24x7 insert mode completion when text is changed
672 acmds->add({bufnr: bnr,
673 event: 'TextChangedI',
674 group: 'LSPBufferAutocmds',
675 cmd: 'LspComplete()'})
677 setbufvar(bnr, '&omnifunc', 'g:LspOmniFunc')
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()'})
688 acmds->add({bufnr: bnr,
689 event: 'CompleteChanged',
690 group: 'LSPBufferAutocmds',
691 cmd: 'LspSetPopupFileType()'})
693 # Execute LSP server initiated text edits after completion
694 acmds->add({bufnr: bnr,
695 event: 'CompleteDone',
696 group: 'LSPBufferAutocmds',
697 cmd: 'LspCompleteDone()'})
702 # vim: tabstop=8 shiftwidth=2 softtabstop=2