3 # LSP completion related functions
6 import './buffer.vim' as buf
7 import './options.vim' as opt
8 import './textedit.vim'
11 # per-filetype omni-completion enabled/disabled table
12 var ftypeOmniCtrlMap: dict<bool> = {}
14 var defaultKinds: dict<string> = {
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)
49 # Enables or disables omni-completion for filetype "fype"
50 export def OmniComplSet(ftype: string, enabled: bool)
51 ftypeOmniCtrlMap->extend({[ftype]: enabled})
54 # Map LSP complete item kind to a character
55 def LspCompleteItemKindChar(kind: number): string
56 var kindMap: list<string> = [
90 var kindName = kindMap[kind]
91 var kindValue = defaultKinds[kindName]
93 var lspOpts = opt.lspOptions
94 if lspOpts.customCompletionKinds &&
95 lspOpts.completionKinds->has_key(kindName)
96 kindValue = lspOpts.completionKinds[kindName]
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]\+')
117 # add completion from current buf
118 def CompletionFromBuffer(items: list<dict<any>>)
120 var start = reltime()
121 var timeout = opt.lspOptions.bufferCompletionTimeout
123 for line in getline(1, '$')
124 for word in line->split('\W\+')
125 if !words->has_key(word) && word->len() > 1
137 # Check every 200 lines if timeout is exceeded
138 if timeout > 0 && linenr % 200 == 0 &&
139 start->reltime()->reltimefloat() * 1000 > timeout
146 # process the 'textDocument/completion' reply from the LSP server
147 # Result: CompletionItem[] | CompletionList | null
148 export def CompletionReply(lspserver: dict<any>, cItems: any)
150 if lspserver.omniCompletePending
151 lspserver.completeItems = []
152 lspserver.omniCompletePending = false
157 lspserver.completeItemsIsIncomplete = false
159 var items: list<dict<any>>
160 if cItems->type() == v:t_list
164 lspserver.completeItemsIsIncomplete = cItems->get('isIncomplete', false)
167 var lspOpts = opt.lspOptions
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()
177 var start_col = start_idx + 1
179 if lspOpts.ultisnipsSupport
180 snippet.CompletionUltiSnips(prefix, items)
181 elseif lspOpts.vsnipSupport
182 snippet.CompletionVsnip(items)
185 if lspOpts.useBufferCompletion
186 CompletionFromBuffer(items)
189 var completeItems: list<dict<any>> = []
191 var d: dict<any> = {}
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
200 start_charcol = charidx(starttext, start_idx) + 1
202 start_charcol = chcol
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
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 : ]
217 d.word = textEdit.newText
219 elseif item->has_key('insertText')
220 d.word = item.insertText
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
233 # plain text completion
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
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()
248 # If the completion item text doesn't start with the current keyword
251 if filterText->stridx(prefix) != 0
261 if lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_ICASE
265 if item->has_key('kind')
266 # namespace CompletionItemKind
267 # map LSP kind to complete-item-kind
268 d.kind = LspCompleteItemKindChar(item.kind)
271 if lspserver.completionLazyDoc
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]
280 if item->has_key('documentation')
281 var itemDoc = item.documentation
282 if itemDoc->type() == v:t_string && !itemDoc->empty()
284 elseif itemDoc->type() == v:t_dict
285 && itemDoc.value->type() == v:t_string
286 d.info = itemDoc.value
291 # Score is used for sorting.
292 d.score = item->get('sortText')
294 d.score = item->get('label', '')
298 completeItems->add(d)
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)
307 if lspOpts.autoComplete && !lspserver.omniCompletePending
308 if completeItems->empty()
314 if m != 'i' && m != 'R' && m != 'Rv'
315 # If not in insert or replace mode, then don't start the completion
319 if completeItems->len() == 1
320 && getline('.')->matchstr($'\C{completeItems[0].word}\>') != ''
321 # only one complete match. No need to show the completion popup
325 completeItems->complete(start_col)
327 lspserver.completeItems = completeItems
328 lspserver.omniCompletePending = false
332 # process the completion documentation
333 def ShowCompletionDocumentation(cItem: any)
334 if cItem->empty() || cItem->type() != v:t_dict
338 # check if completion item is still selected
339 var cInfo = complete_info()
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
349 var infoText: list<string>
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"))
359 if cItem->has_key('documentation')
360 if !infoText->empty()
361 infoText->extend(['- - -'])
363 var cItemDoc = cItem.documentation
364 if cItemDoc->type() == v:t_dict
366 if cItemDoc.kind == 'plaintext'
367 infoText->extend(cItemDoc.value->split("\n"))
369 elseif cItemDoc.kind == 'markdown'
370 infoText->extend(cItemDoc.value->split("\n"))
373 util.ErrMsg($'Unsupported documentation type ({cItemDoc.kind})')
376 elseif cItemDoc->type() == v:t_string
377 infoText->extend(cItemDoc->split("\n"))
379 util.ErrMsg($'Unsupported documentation ({cItemDoc->string()})')
388 # check if completion item is changed in meantime
389 cInfo = complete_info()
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
399 # autoComplete or &omnifunc with &completeopt =~ 'popup'
400 var id = popup_findinfo()
402 var bufnr = id->winbufnr()
403 id->popup_settext(infoText)
404 infoKind->setbufvar(bufnr, '&ft')
407 # &omnifunc with &completeopt =~ 'preview'
411 bufnr()->deletebufline(1, '$')
414 exe $'setlocal ft={infoKind}'
416 catch /E441/ # No preview window
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)
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()
435 # first send all the changes in the current buffer to the LSP server
438 lspserver.omniCompletePending = true
439 lspserver.completeItems = []
440 # initiate a request to LSP server to get list of completions
441 lspserver.getCompletion(1, '')
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
451 lspserver.omniCompleteKeyword = keyword
452 return line->byteidx(start)
454 # Wait for the list of matches from the LSP server
455 var count: number = 0
456 while lspserver.omniCompletePending && count < 1000
464 if lspserver.omniCompletePending
468 var res: list<dict<any>> = lspserver.completeItems
470 var prefix = lspserver.omniCompleteKeyword
472 # Don't attempt to filter on the items, when "isIncomplete" is set
473 if prefix->empty() || lspserver.completeItemsIsIncomplete
477 var lspOpts = opt.lspOptions
478 if lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_FUZZY
479 return res->matchfuzzy(prefix, { key: 'word' })
482 if lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_ICASE
483 return res->filter((i, v) =>
484 v.word->tolower()->stridx(prefix->tolower()) == 0)
487 return res->filter((i, v) => v.word->stridx(prefix) == 0)
491 # Insert mode completion handler. Used when 24x7 completion is enabled
494 var lspserver: dict<any> = buf.CurbufGetServer('completion')
495 if lspserver->empty() || !lspserver.running || !lspserver.ready
499 var cur_col: number = charcol('.')
500 var line: string = getline('.')
502 if cur_col == 0 || line->empty()
506 # Trigger kind is 1 for 24x7 code complete or manual invocation
507 var triggerKind: number = 1
508 var triggerChar: string = ''
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])
518 # completion triggered by one of the trigger characters
520 triggerChar = trigChars[trigidx]
523 # first send all the changes in the current buffer to the LSP server
526 # initiate a request to LSP server to get list of completions
527 lspserver.getCompletion(triggerKind, triggerChar)
530 # Lazy complete documentation handler
532 var lspserver: dict<any> = buf.CurbufGetServerChecked('completion')
533 if lspserver->empty()
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)
542 ShowCompletionDocumentation(item.user_data)
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', {})
556 if cItem->type() != v:t_dict || !cItem->has_key('documentation')
557 \ || cItem.documentation->type() != v:t_dict
558 \ || cItem.documentation.kind != 'markdown'
562 var id = popup_findinfo()
564 var bnum = id->winbufnr()
565 setbufvar(bnum, '&ft', 'lspgfm')
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()
576 if v:completed_item->type() != v:t_dict
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
587 textedit.ApplyTextEdits(bnr, completionData.additionalTextEdits)
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
597 if !opt.lspOptions.autoComplete && !LspOmniComplEnabled(ftype)
598 # LSP auto/omni completion support is not enabled for this buffer
602 # buffer-local autocmds for completion
603 var acmds: list<dict<any>> = []
605 # set options for insert mode completion
606 if opt.lspOptions.autoComplete
607 if lspserver.completionLazyDoc
608 setbufvar(bnr, '&completeopt', 'menuone,popuphidden,noinsert,noselect')
610 setbufvar(bnr, '&completeopt', 'menuone,popup,noinsert,noselect')
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>"
619 # Trigger 24x7 insert mode completion when text is changed
620 acmds->add({bufnr: bnr,
621 event: 'TextChangedI',
622 group: 'LSPBufferAutocmds',
623 cmd: 'LspComplete()'})
625 setbufvar(bnr, '&omnifunc', 'g:LspOmniFunc')
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()'})
636 acmds->add({bufnr: bnr,
637 event: 'CompleteChanged',
638 group: 'LSPBufferAutocmds',
639 cmd: 'LspSetPopupFileType()'})
641 # Execute LSP server initiated text edits after completion
642 acmds->add({bufnr: bnr,
643 event: 'CompleteDone',
644 group: 'LSPBufferAutocmds',
645 cmd: $'LspCompleteDone({bnr})'})
650 # vim: tabstop=8 shiftwidth=2 softtabstop=2