3 # LSP completion related functions
6 import './buffer.vim' as buf
7 import './options.vim' as opt
8 import './textedit.vim'
10 import './codeaction.vim'
12 # per-filetype omni-completion enabled/disabled table
13 var ftypeOmniCtrlMap: dict<bool> = {}
15 var defaultKinds: dict<string> = {
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)
50 # Enables or disables omni-completion for filetype "fype"
51 export def OmniComplSet(ftype: string, enabled: bool)
52 ftypeOmniCtrlMap->extend({[ftype]: enabled})
55 # Map LSP complete item kind to a character
56 def LspCompleteItemKindChar(kind: number): string
57 var kindMap: list<string> = [
91 var kindName = kindMap[kind]
92 var kindValue = defaultKinds[kindName]
94 var lspOpts = opt.lspOptions
95 if lspOpts.customCompletionKinds &&
96 lspOpts.completionKinds->has_key(kindName)
97 kindValue = lspOpts.completionKinds[kindName]
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]\+')
118 # add completion from current buf
119 def CompletionFromBuffer(items: list<dict<any>>)
121 var start = reltime()
122 var timeout = opt.lspOptions.bufferCompletionTimeout
124 for line in getline(1, '$')
125 for word in line->split('\W\+')
126 if !words->has_key(word) && word->len() > 1
138 # Check every 200 lines if timeout is exceeded
139 if timeout > 0 && linenr % 200 == 0 &&
140 start->reltime()->reltimefloat() * 1000 > timeout
147 # process the 'textDocument/completion' reply from the LSP server
148 # Result: CompletionItem[] | CompletionList | null
149 export def CompletionReply(lspserver: dict<any>, cItems: any)
151 if lspserver.omniCompletePending
152 lspserver.completeItems = []
153 lspserver.omniCompletePending = false
158 lspserver.completeItemsIsIncomplete = false
160 var items: list<dict<any>>
161 if cItems->type() == v:t_list
165 lspserver.completeItemsIsIncomplete = cItems->get('isIncomplete', false)
168 var lspOpts = opt.lspOptions
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()
178 var start_col = start_idx + 1
180 if lspOpts.ultisnipsSupport
181 snippet.CompletionUltiSnips(prefix, items)
182 elseif lspOpts.vsnipSupport
183 snippet.CompletionVsnip(items)
186 if lspOpts.useBufferCompletion
187 CompletionFromBuffer(items)
190 var completeItems: list<dict<any>> = []
192 var d: dict<any> = {}
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
201 start_charcol = charidx(starttext, start_idx) + 1
203 start_charcol = chcol
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
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 : ]
218 d.word = textEdit.newText
220 elseif item->has_key('insertText')
221 d.word = item.insertText
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
234 # plain text completion
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
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()
249 # If the completion item text doesn't start with the current keyword
252 if filterText->stridx(prefix) != 0
262 if lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_ICASE
266 if item->has_key('kind')
267 # namespace CompletionItemKind
268 # map LSP kind to complete-item-kind
269 d.kind = LspCompleteItemKindChar(item.kind)
272 if lspserver.completionLazyDoc
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]
281 if item->has_key('documentation')
282 var itemDoc = item.documentation
283 if itemDoc->type() == v:t_string && !itemDoc->empty()
285 elseif itemDoc->type() == v:t_dict
286 && itemDoc.value->type() == v:t_string
287 d.info = itemDoc.value
292 # Score is used for sorting.
293 d.score = item->get('sortText')
295 d.score = item->get('label', '')
299 completeItems->add(d)
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)
308 if lspOpts.autoComplete && !lspserver.omniCompletePending
309 if completeItems->empty()
315 if m != 'i' && m != 'R' && m != 'Rv'
316 # If not in insert or replace mode, then don't start the completion
320 if completeItems->len() == 1
321 && getline('.')->matchstr($'\C{completeItems[0].word}\>') != ''
322 # only one complete match. No need to show the completion popup
326 completeItems->complete(start_col)
328 lspserver.completeItems = completeItems
329 lspserver.omniCompletePending = false
333 # process the completion documentation
334 def ShowCompletionDocumentation(cItem: any)
335 if cItem->empty() || cItem->type() != v:t_dict
339 # check if completion item is still selected
340 var cInfo = complete_info()
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
350 var infoText: list<string>
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"))
360 if cItem->has_key('documentation')
361 if !infoText->empty()
362 infoText->extend(['- - -'])
364 var cItemDoc = cItem.documentation
365 if cItemDoc->type() == v:t_dict
367 if cItemDoc.kind == 'plaintext'
368 infoText->extend(cItemDoc.value->split("\n"))
370 elseif cItemDoc.kind == 'markdown'
371 infoText->extend(cItemDoc.value->split("\n"))
374 util.ErrMsg($'Unsupported documentation type ({cItemDoc.kind})')
377 elseif cItemDoc->type() == v:t_string
378 infoText->extend(cItemDoc->split("\n"))
380 util.ErrMsg($'Unsupported documentation ({cItemDoc->string()})')
389 # check if completion item is changed in meantime
390 cInfo = complete_info()
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
400 # autoComplete or &omnifunc with &completeopt =~ 'popup'
401 var id = popup_findinfo()
403 var bufnr = id->winbufnr()
404 id->popup_settext(infoText)
405 infoKind->setbufvar(bufnr, '&ft')
408 # &omnifunc with &completeopt =~ 'preview'
412 bufnr()->deletebufline(1, '$')
415 exe $'setlocal ft={infoKind}'
417 catch /E441/ # No preview window
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)
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()
436 # first send all the changes in the current buffer to the LSP server
439 lspserver.omniCompletePending = true
440 lspserver.completeItems = []
441 # initiate a request to LSP server to get list of completions
442 lspserver.getCompletion(1, '')
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()
450 # Wait for the list of matches from the LSP server
451 var count: number = 0
452 while lspserver.omniCompletePending && count < 1000
460 if lspserver.omniCompletePending
464 var res: list<dict<any>> = lspserver.completeItems
466 var prefix = lspserver.omniCompleteKeyword
468 # Don't attempt to filter on the items, when "isIncomplete" is set
469 if prefix->empty() || lspserver.completeItemsIsIncomplete
473 var lspOpts = opt.lspOptions
474 if lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_FUZZY
475 return res->matchfuzzy(prefix, { key: 'word' })
478 if lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_ICASE
479 return res->filter((i, v) =>
480 v.word->tolower()->stridx(prefix->tolower()) == 0)
483 return res->filter((i, v) => v.word->stridx(prefix) == 0)
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
494 # Insert mode completion handler. Used when 24x7 completion is enabled
497 var lspserver: dict<any> = buf.CurbufGetServer('completion')
498 if lspserver->empty() || !lspserver.running || !lspserver.ready
502 var cur_col: number = charcol('.')
503 var line: string = getline('.')
505 if cur_col == 0 || line->empty()
509 # Trigger kind is 1 for 24x7 code complete or manual invocation
510 var triggerKind: number = 1
511 var triggerChar: string = ''
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])
521 # completion triggered by one of the trigger characters
523 triggerChar = trigChars[trigidx]
526 # first send all the changes in the current buffer to the LSP server
529 # initiate a request to LSP server to get list of completions
530 lspserver.getCompletion(triggerKind, triggerChar)
533 # Lazy complete documentation handler
535 var lspserver: dict<any> = buf.CurbufGetServerChecked('completion')
536 if lspserver->empty()
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)
545 ShowCompletionDocumentation(item.user_data)
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', {})
559 if cItem->type() != v:t_dict || !cItem->has_key('documentation')
560 \ || cItem.documentation->type() != v:t_dict
561 \ || cItem.documentation.kind != 'markdown'
565 var id = popup_findinfo()
567 var bnum = id->winbufnr()
568 setbufvar(bnum, '&ft', 'lspgfm')
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()
579 if v:completed_item->type() != v:t_dict
583 var completionData: any = v:completed_item->get('user_data', '')
584 if completionData->type() != v:t_dict
585 || !opt.lspOptions.completionTextEdit
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
593 completionData = lspserver.resolveCompletion(completionData, true)
594 if !completionData->get('additionalTextEdits', {})->empty()
595 textedit.ApplyTextEdits(bnr, completionData.additionalTextEdits)
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)
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
614 if !opt.lspOptions.autoComplete && !LspOmniComplEnabled(ftype)
615 # LSP auto/omni completion support is not enabled for this buffer
619 # buffer-local autocmds for completion
620 var acmds: list<dict<any>> = []
622 # set options for insert mode completion
623 if opt.lspOptions.autoComplete
624 if lspserver.completionLazyDoc
625 setbufvar(bnr, '&completeopt', 'menuone,popuphidden,noinsert,noselect')
627 setbufvar(bnr, '&completeopt', 'menuone,popup,noinsert,noselect')
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>"
636 # Trigger 24x7 insert mode completion when text is changed
637 acmds->add({bufnr: bnr,
638 event: 'TextChangedI',
639 group: 'LSPBufferAutocmds',
640 cmd: 'LspComplete()'})
643 if LspOmniComplEnabled(ftype)
644 setbufvar(bnr, '&omnifunc', 'g:LspOmniFunc')
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()'})
655 acmds->add({bufnr: bnr,
656 event: 'CompleteChanged',
657 group: 'LSPBufferAutocmds',
658 cmd: 'LspSetPopupFileType()'})
660 # Execute LSP server initiated text edits after completion
661 acmds->add({bufnr: bnr,
662 event: 'CompleteDone',
663 group: 'LSPBufferAutocmds',
664 cmd: $'LspCompleteDone({bnr})'})
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')
678 # vim: tabstop=8 shiftwidth=2 softtabstop=2