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 if opt.lspOptions.customCompletionKinds
94 && opt.lspOptions.completionKinds->has_key(kindName)
95 kindValue = opt.lspOptions.completionKinds[kindName]
101 # Remove all the snippet placeholders from "str" and return the value.
102 # Based on a similar function in the vim-lsp plugin.
103 def MakeValidWord(str_arg: string): string
104 var str = str_arg->substitute('\$[0-9]\+\|\${\%(\\.\|[^}]\)\+}', '', 'g')
105 str = str->substitute('\\\(.\)', '\1', 'g')
106 var valid = str->matchstr('^[^"'' (<{\[\t\r\n]\+')
116 # add completion from current buf
117 def CompletionFromBuffer(items: list<dict<any>>)
119 var start = reltime()
120 var timeout = opt.lspOptions.bufferCompletionTimeout
122 for line in getline(1, '$')
123 for word in line->split('\W\+')
124 if !words->has_key(word) && word->len() > 1
136 # Check every 200 lines if timeout is exceeded
137 if timeout > 0 && linenr % 200 == 0 && start->reltime()->reltimefloat() * 1000 > timeout
144 # process the 'textDocument/completion' reply from the LSP server
145 # Result: CompletionItem[] | CompletionList | null
146 export def CompletionReply(lspserver: dict<any>, cItems: any)
148 if lspserver.omniCompletePending
149 lspserver.completeItems = []
150 lspserver.omniCompletePending = false
155 lspserver.completeItemsIsIncomplete = false
157 var items: list<dict<any>>
158 if cItems->type() == v:t_list
162 lspserver.completeItemsIsIncomplete = cItems->get('isIncomplete', false)
165 # Get the keyword prefix before the current cursor column.
166 var chcol = charcol('.')
167 var starttext = chcol == 1 ? '' : getline('.')[ : chcol - 2]
168 var [prefix, start_idx, end_idx] = starttext->matchstrpos('\k*$')
169 if opt.lspOptions.completionMatcher == 'icase'
170 prefix = prefix->tolower()
173 var start_col = start_idx + 1
175 if opt.lspOptions.ultisnipsSupport
176 snippet.CompletionUltiSnips(prefix, items)
177 elseif opt.lspOptions.vsnipSupport
178 snippet.CompletionVsnip(items)
181 if opt.lspOptions.useBufferCompletion
182 CompletionFromBuffer(items)
185 var completeItems: list<dict<any>> = []
187 var d: dict<any> = {}
189 # TODO: Add proper support for item.textEdit.newText and item.textEdit.range
190 # Keep in mind that item.textEdit.range can start be way before the typed
192 if item->has_key('textEdit') && opt.lspOptions.completionMatcher != 'fuzzy'
193 var start_charcol: number
195 start_charcol = charidx(starttext, start_idx) + 1
197 start_charcol = chcol
199 var textEdit = item.textEdit
200 var textEditStartCol =
201 util.GetCharIdxWithoutCompChar(bufnr(), textEdit.range.start)
202 if textEditStartCol != start_charcol
203 var offset = start_charcol - textEditStartCol - 1
204 d.word = textEdit.newText[offset : ]
206 d.word = textEdit.newText
208 elseif item->has_key('insertText')
209 d.word = item.insertText
214 if item->get('insertTextFormat', 1) == 2
215 # snippet completion. Needs a snippet plugin to expand the snippet.
216 # Remove all the snippet placeholders
217 d.word = MakeValidWord(d.word)
218 elseif !lspserver.completeItemsIsIncomplete || opt.lspOptions.useBufferCompletion
219 # Filter items only when "isIncomplete" is set (otherwise server would
220 # have done the filtering) or when buffer completion is enabled
222 # plain text completion
224 # If the completion item text doesn't start with the current (case
225 # ignored) keyword prefix, skip it.
226 var filterText: string = item->get('filterText', d.word)
227 if opt.lspOptions.completionMatcher == 'icase'
228 if filterText->tolower()->stridx(prefix) != 0
231 # If the completion item text doesn't fuzzy match with the current
232 # keyword prefix, skip it.
233 elseif opt.lspOptions.completionMatcher == 'fuzzy'
234 if matchfuzzy([filterText], prefix)->empty()
237 # If the completion item text doesn't start with the current keyword
240 if filterText->stridx(prefix) != 0
250 if opt.lspOptions.completionMatcher == 'icase'
254 if item->has_key('kind')
255 # namespace CompletionItemKind
256 # map LSP kind to complete-item-kind
257 d.kind = LspCompleteItemKindChar(item.kind)
260 if lspserver.completionLazyDoc
263 if item->has_key('detail') && item.detail != ''
264 # Solve a issue where if a server send a detail field
265 # with a "\n", on the menu will be everything joined with
266 # a "^@" separating it. (example: clangd)
267 d.menu = item.detail->split("\n")[0]
269 if item->has_key('documentation')
270 if item.documentation->type() == v:t_string && item.documentation != ''
271 d.info = item.documentation
272 elseif item.documentation->type() == v:t_dict
273 && item.documentation.value->type() == v:t_string
274 d.info = item.documentation.value
279 # Score is used for sorting.
280 d.score = item->get('sortText')
282 d.score = item->get('label', '')
286 completeItems->add(d)
289 if opt.lspOptions.completionMatcher != 'fuzzy'
290 # Lexographical sort (case-insensitive).
291 completeItems->sort((a, b) =>
292 a.score == b.score ? 0 : a.score >? b.score ? 1 : -1)
295 if opt.lspOptions.autoComplete && !lspserver.omniCompletePending
296 if completeItems->empty()
302 if m != 'i' && m != 'R' && m != 'Rv'
303 # If not in insert or replace mode, then don't start the completion
307 if completeItems->len() == 1
308 && getline('.')->matchstr($'\C{completeItems[0].word}\>') != ''
309 # only one complete match. No need to show the completion popup
313 completeItems->complete(start_col)
315 lspserver.completeItems = completeItems
316 lspserver.omniCompletePending = false
320 # process the completion documentation
321 def ShowCompletionDocumentation(cItem: any)
322 if cItem->empty() || cItem->type() != v:t_dict
326 # check if completion item is still selected
327 var cInfo = complete_info()
329 || !cInfo.pum_visible
330 || cInfo.selected == -1
331 || cInfo.items[cInfo.selected]->type() != v:t_dict
332 || cInfo.items[cInfo.selected].user_data->type() != v:t_dict
333 || cInfo.items[cInfo.selected].user_data.label != cItem.label
337 var infoText: list<string>
340 if cItem->has_key('detail') && !cItem.detail->empty()
341 # Solve a issue where if a server send the detail field with "\n",
342 # on the completion popup, everything will be joined with "^@"
343 # (example: typescript-language-server)
344 infoText->extend(cItem.detail->split("\n"))
347 if cItem->has_key('documentation')
348 if !infoText->empty()
349 infoText->extend(['- - -'])
351 if cItem.documentation->type() == v:t_dict
353 if cItem.documentation.kind == 'plaintext'
354 infoText->extend(cItem.documentation.value->split("\n"))
356 elseif cItem.documentation.kind == 'markdown'
357 infoText->extend(cItem.documentation.value->split("\n"))
360 util.ErrMsg($'Unsupported documentation type ({cItem.documentation.kind})')
363 elseif cItem.documentation->type() == v:t_string
364 infoText->extend(cItem.documentation->split("\n"))
366 util.ErrMsg($'Unsupported documentation ({cItem.documentation->string()})')
375 # check if completion item is changed in meantime
376 cInfo = complete_info()
378 || !cInfo.pum_visible
379 || cInfo.selected == -1
380 || cInfo.items[cInfo.selected]->type() != v:t_dict
381 || cInfo.items[cInfo.selected].user_data->type() != v:t_dict
382 || cInfo.items[cInfo.selected].user_data.label != cItem.label
386 # autoComplete or &omnifunc with &completeopt =~ 'popup'
387 var id = popup_findinfo()
389 var bufnr = id->winbufnr()
390 id->popup_settext(infoText)
391 infoKind->setbufvar(bufnr, '&ft')
394 # &omnifunc with &completeopt =~ 'preview'
398 bufnr()->deletebufline(1, '$')
401 exe $'setlocal ft={infoKind}'
403 catch /E441/ # No preview window
408 # process the 'completionItem/resolve' reply from the LSP server
409 # Result: CompletionItem
410 export def CompletionResolveReply(lspserver: dict<any>, cItem: any)
411 ShowCompletionDocumentation(cItem)
414 # omni complete handler
415 def g:LspOmniFunc(findstart: number, base: string): any
416 var lspserver: dict<any> = buf.CurbufGetServerChecked('completion')
417 if lspserver->empty()
422 # first send all the changes in the current buffer to the LSP server
425 lspserver.omniCompletePending = true
426 lspserver.completeItems = []
427 # initiate a request to LSP server to get list of completions
428 lspserver.getCompletion(1, '')
430 # locate the start of the word
431 var line = getline('.')
432 var start = charcol('.') - 1
433 var keyword: string = ''
434 while start > 0 && line[start - 1] =~ '\k'
435 keyword = line[start - 1] .. keyword
438 lspserver.omniCompleteKeyword = keyword
439 return line->byteidx(start)
441 # Wait for the list of matches from the LSP server
442 var count: number = 0
443 while lspserver.omniCompletePending && count < 1000
451 if lspserver.omniCompletePending
455 var res: list<dict<any>> = lspserver.completeItems
457 var prefix = lspserver.omniCompleteKeyword
459 # Don't attempt to filter on the items, when "isIncomplete" is set
460 if prefix->empty() || lspserver.completeItemsIsIncomplete
464 if opt.lspOptions.completionMatcher == 'fuzzy'
465 return res->matchfuzzy(prefix, { key: 'word' })
468 if opt.lspOptions.completionMatcher == 'icase'
469 return res->filter((i, v) => v.word->tolower()->stridx(prefix->tolower()) == 0)
472 return res->filter((i, v) => v.word->stridx(prefix) == 0)
476 # Insert mode completion handler. Used when 24x7 completion is enabled
479 var lspserver: dict<any> = buf.CurbufGetServer('completion')
480 if lspserver->empty() || !lspserver.running || !lspserver.ready
484 var cur_col: number = charcol('.')
485 var line: string = getline('.')
487 if cur_col == 0 || line->empty()
491 # Trigger kind is 1 for 24x7 code complete or manual invocation
492 var triggerKind: number = 1
493 var triggerChar: string = ''
495 # If the character before the cursor is not a keyword character or is not
496 # one of the LSP completion trigger characters, then do nothing.
497 if line[cur_col - 2] !~ '\k'
498 var trigChars = lspserver.completionTriggerChars
499 var trigidx = trigChars->index(line[cur_col - 2])
503 # completion triggered by one of the trigger characters
505 triggerChar = trigChars[trigidx]
508 # first send all the changes in the current buffer to the LSP server
511 # initiate a request to LSP server to get list of completions
512 lspserver.getCompletion(triggerKind, triggerChar)
515 # Lazy complete documentation handler
517 var lspserver: dict<any> = buf.CurbufGetServerChecked('completion')
518 if lspserver->empty()
522 var item = v:event.completed_item
523 if item->has_key('user_data') && !item.user_data->empty()
524 if !item.user_data->has_key('documentation')
525 lspserver.resolveCompletion(item.user_data)
527 ShowCompletionDocumentation(item.user_data)
532 # If the completion popup documentation window displays "markdown" content,
533 # then set the 'filetype' to "lspgfm".
534 def LspSetPopupFileType()
535 var item = v:event.completed_item
536 var cItem = item->get('user_data', {})
541 if cItem->type() != v:t_dict || !cItem->has_key('documentation')
542 \ || cItem.documentation->type() != v:t_dict
543 \ || cItem.documentation.kind != 'markdown'
547 var id = popup_findinfo()
549 var bnum = id->winbufnr()
550 setbufvar(bnum, '&ft', 'lspgfm')
554 # complete done handler (LSP server-initiated actions after completion)
555 def LspCompleteDone()
556 var lspserver: dict<any> = buf.CurbufGetServerChecked('completion')
557 if lspserver->empty()
561 if v:completed_item->type() != v:t_dict
565 var completionData: any = v:completed_item->get('user_data', '')
566 if completionData->type() != v:t_dict
567 || !completionData->has_key('additionalTextEdits')
568 || !opt.lspOptions.completionTextEdit
572 var bnr: number = bufnr()
573 textedit.ApplyTextEdits(bnr, completionData.additionalTextEdits)
576 # Initialize buffer-local completion options and autocmds
577 export def BufferInit(lspserver: dict<any>, bnr: number, ftype: string)
578 if !lspserver.isCompletionProvider
579 # no support for completion
583 if !opt.lspOptions.autoComplete && !LspOmniComplEnabled(ftype)
584 # LSP auto/omni completion support is not enabled for this buffer
588 # buffer-local autocmds for completion
589 var acmds: list<dict<any>> = []
591 # set options for insert mode completion
592 if opt.lspOptions.autoComplete
593 if lspserver.completionLazyDoc
594 setbufvar(bnr, '&completeopt', 'menuone,popuphidden,noinsert,noselect')
596 setbufvar(bnr, '&completeopt', 'menuone,popup,noinsert,noselect')
598 setbufvar(bnr, '&completepopup', 'width:80,highlight:Pmenu,align:item,border:off')
599 # <Enter> in insert mode stops completion and inserts a <Enter>
600 if !opt.lspOptions.noNewlineInCompletion
601 :inoremap <expr> <buffer> <CR> pumvisible() ? "\<C-Y>\<CR>" : "\<CR>"
604 # Trigger 24x7 insert mode completion when text is changed
605 acmds->add({bufnr: bnr,
606 event: 'TextChangedI',
607 group: 'LSPBufferAutocmds',
608 cmd: 'LspComplete()'})
610 setbufvar(bnr, '&omnifunc', 'g:LspOmniFunc')
613 if lspserver.completionLazyDoc
614 # resolve additional documentation for a selected item
615 acmds->add({bufnr: bnr,
616 event: 'CompleteChanged',
617 group: 'LSPBufferAutocmds',
618 cmd: 'LspResolve()'})
621 acmds->add({bufnr: bnr,
622 event: 'CompleteChanged',
623 group: 'LSPBufferAutocmds',
624 cmd: 'LspSetPopupFileType()'})
626 # Execute LSP server initiated text edits after completion
627 acmds->add({bufnr: bnr,
628 event: 'CompleteDone',
629 group: 'LSPBufferAutocmds',
630 cmd: 'LspCompleteDone()'})
635 # vim: tabstop=8 shiftwidth=2 softtabstop=2