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 && opt.lspOptions.completionKinds->has_key(kindName)
94 kindValue = opt.lspOptions.completionKinds[kindName]
100 # Remove all the snippet placeholders from "str" and return the value.
101 # Based on a similar function in the vim-lsp plugin.
102 def MakeValidWord(str_arg: string): string
103 var str = str_arg->substitute('\$[0-9]\+\|\${\%(\\.\|[^}]\)\+}', '', 'g')
104 str = str->substitute('\\\(.\)', '\1', 'g')
105 var valid = str->matchstr('^[^"'' (<{\[\t\r\n]\+')
115 # add completion from current buf
116 def CompletionFromBuffer(items: list<dict<any>>)
118 var start = reltime()
120 for line in getline(1, '$')
121 for word in line->split('\W\+')
122 if !words->has_key(word) && word->len() > 1
134 # Check every 200 lines if timeout is exceeded
135 if opt.lspOptions.bufferCompletionTimeout > 0 && linenr % 200 == 0 &&
136 start->reltime()->reltimefloat() * 1000 > opt.lspOptions.bufferCompletionTimeout
143 # process the 'textDocument/completion' reply from the LSP server
144 # Result: CompletionItem[] | CompletionList | null
145 export def CompletionReply(lspserver: dict<any>, cItems: any)
147 if lspserver.omniCompletePending
148 lspserver.completeItems = []
149 lspserver.omniCompletePending = false
154 lspserver.completeItemsIsIncomplete = false
156 var items: list<dict<any>>
157 if cItems->type() == v:t_list
161 lspserver.completeItemsIsIncomplete = cItems->get('isIncomplete', false)
164 # Get the keyword prefix before the current cursor column.
165 var chcol = charcol('.')
166 var starttext = chcol == 1 ? '' : getline('.')[ : chcol - 2]
167 var [prefix, start_idx, end_idx] = starttext->matchstrpos('\k*$')
168 if opt.lspOptions.completionMatcher == 'icase'
169 prefix = prefix->tolower()
172 var start_col = start_idx + 1
174 if opt.lspOptions.ultisnipsSupport
175 snippet.CompletionUltiSnips(prefix, items)
176 elseif opt.lspOptions.vsnipSupport
177 snippet.CompletionVsnip(items)
180 if opt.lspOptions.useBufferCompletion
181 CompletionFromBuffer(items)
184 var completeItems: list<dict<any>> = []
186 var d: dict<any> = {}
188 # TODO: Add proper support for item.textEdit.newText and item.textEdit.range
189 # Keep in mind that item.textEdit.range can start be way before the typed
191 if item->has_key('textEdit') && opt.lspOptions.completionMatcher != 'fuzzy'
192 var start_charcol: number
194 start_charcol = charidx(starttext, start_idx) + 1
196 start_charcol = chcol
198 var textEdit = item.textEdit
199 var textEditStartCol =
200 util.GetCharIdxWithoutCompChar(bufnr(), textEdit.range.start)
201 if textEditStartCol != start_charcol
202 var offset = start_charcol - textEditStartCol - 1
203 d.word = textEdit.newText[offset : ]
205 d.word = textEdit.newText
207 elseif item->has_key('insertText')
208 d.word = item.insertText
213 if item->get('insertTextFormat', 1) == 2
214 # snippet completion. Needs a snippet plugin to expand the snippet.
215 # Remove all the snippet placeholders
216 d.word = MakeValidWord(d.word)
217 elseif !lspserver.completeItemsIsIncomplete || opt.lspOptions.useBufferCompletion
218 # Filter items only when "isIncomplete" is set (otherwise server would
219 # have done the filtering) or when buffer completion is enabled
221 # plain text completion
223 # If the completion item text doesn't start with the current (case
224 # ignored) keyword prefix, skip it.
225 var filterText: string = item->get('filterText', d.word)
226 if opt.lspOptions.completionMatcher == 'icase'
227 if filterText->tolower()->stridx(prefix) != 0
230 # If the completion item text doesn't fuzzy match with the current
231 # keyword prefix, skip it.
232 elseif opt.lspOptions.completionMatcher == 'fuzzy'
233 if matchfuzzy([filterText], prefix)->empty()
236 # If the completion item text doesn't start with the current keyword
239 if filterText->stridx(prefix) != 0
249 if opt.lspOptions.completionMatcher == 'icase'
253 if item->has_key('kind')
254 # namespace CompletionItemKind
255 # map LSP kind to complete-item-kind
256 d.kind = LspCompleteItemKindChar(item.kind)
259 if lspserver.completionLazyDoc
262 if item->has_key('detail') && item.detail != ''
263 # Solve a issue where if a server send a detail field
264 # with a "\n", on the menu will be everything joined with
265 # a "^@" separating it. (example: clangd)
266 d.menu = item.detail->split("\n")[0]
268 if item->has_key('documentation')
269 if item.documentation->type() == v:t_string && item.documentation != ''
270 d.info = item.documentation
271 elseif item.documentation->type() == v:t_dict
272 && item.documentation.value->type() == v:t_string
273 d.info = item.documentation.value
278 # Score is used for sorting.
279 d.score = item->get('sortText')
281 d.score = item->get('label', '')
285 completeItems->add(d)
288 if opt.lspOptions.completionMatcher != 'fuzzy'
289 # Lexographical sort (case-insensitive).
290 completeItems->sort((a, b) =>
291 a.score == b.score ? 0 : a.score >? b.score ? 1 : -1)
294 if opt.lspOptions.autoComplete && !lspserver.omniCompletePending
295 if completeItems->empty()
301 if m != 'i' && m != 'R' && m != 'Rv'
302 # If not in insert or replace mode, then don't start the completion
306 if completeItems->len() == 1
307 && getline('.')->matchstr($'\C{completeItems[0].word}\>') != ''
308 # only one complete match. No need to show the completion popup
312 completeItems->complete(start_col)
314 lspserver.completeItems = completeItems
315 lspserver.omniCompletePending = false
319 # process the completion documentation
320 def ShowCompletionDocumentation(cItem: any)
321 if cItem->empty() || cItem->type() != v:t_dict
325 # check if completion item is still selected
326 var cInfo = complete_info()
328 || !cInfo.pum_visible
329 || cInfo.selected == -1
330 || cInfo.items[cInfo.selected]->type() != v:t_dict
331 || cInfo.items[cInfo.selected].user_data->type() != v:t_dict
332 || cInfo.items[cInfo.selected].user_data.label != cItem.label
336 var infoText: list<string>
339 if cItem->has_key('detail') && !cItem.detail->empty()
340 # Solve a issue where if a server send the detail field with "\n",
341 # on the completion popup, everything will be joined with "^@"
342 # (example: typescript-language-server)
343 infoText->extend(cItem.detail->split("\n"))
346 if cItem->has_key('documentation')
347 if !infoText->empty()
348 infoText->extend(['- - -'])
350 if cItem.documentation->type() == v:t_dict
352 if cItem.documentation.kind == 'plaintext'
353 infoText->extend(cItem.documentation.value->split("\n"))
355 elseif cItem.documentation.kind == 'markdown'
356 infoText->extend(cItem.documentation.value->split("\n"))
359 util.ErrMsg($'Unsupported documentation type ({cItem.documentation.kind})')
362 elseif cItem.documentation->type() == v:t_string
363 infoText->extend(cItem.documentation->split("\n"))
365 util.ErrMsg($'Unsupported documentation ({cItem.documentation->string()})')
374 # check if completion item is changed in meantime
375 cInfo = complete_info()
377 || !cInfo.pum_visible
378 || cInfo.selected == -1
379 || cInfo.items[cInfo.selected]->type() != v:t_dict
380 || cInfo.items[cInfo.selected].user_data->type() != v:t_dict
381 || cInfo.items[cInfo.selected].user_data.label != cItem.label
385 # autoComplete or &omnifunc with &completeopt =~ 'popup'
386 var id = popup_findinfo()
388 var bufnr = id->winbufnr()
389 id->popup_settext(infoText)
390 infoKind->setbufvar(bufnr, '&ft')
393 # &omnifunc with &completeopt =~ 'preview'
397 bufnr()->deletebufline(1, '$')
400 exe $'setlocal ft={infoKind}'
402 catch /E441/ # No preview window
407 # process the 'completionItem/resolve' reply from the LSP server
408 # Result: CompletionItem
409 export def CompletionResolveReply(lspserver: dict<any>, cItem: any)
410 ShowCompletionDocumentation(cItem)
413 # omni complete handler
414 def g:LspOmniFunc(findstart: number, base: string): any
415 var lspserver: dict<any> = buf.CurbufGetServerChecked('completion')
416 if lspserver->empty()
421 # first send all the changes in the current buffer to the LSP server
424 lspserver.omniCompletePending = true
425 lspserver.completeItems = []
426 # initiate a request to LSP server to get list of completions
427 lspserver.getCompletion(1, '')
429 # locate the start of the word
430 var line = getline('.')
431 var start = charcol('.') - 1
432 var keyword: string = ''
433 while start > 0 && line[start - 1] =~ '\k'
434 keyword = line[start - 1] .. keyword
437 lspserver.omniCompleteKeyword = keyword
438 return line->byteidx(start)
440 # Wait for the list of matches from the LSP server
441 var count: number = 0
442 while lspserver.omniCompletePending && count < 1000
450 if lspserver.omniCompletePending
454 var res: list<dict<any>> = lspserver.completeItems
456 var prefix = lspserver.omniCompleteKeyword
458 # Don't attempt to filter on the items, when "isIncomplete" is set
459 if prefix->empty() || lspserver.completeItemsIsIncomplete
463 if opt.lspOptions.completionMatcher == 'fuzzy'
464 return res->matchfuzzy(prefix, { key: 'word' })
467 if opt.lspOptions.completionMatcher == 'icase'
468 return res->filter((i, v) => v.word->tolower()->stridx(prefix->tolower()) == 0)
471 return res->filter((i, v) => v.word->stridx(prefix) == 0)
475 # Insert mode completion handler. Used when 24x7 completion is enabled
478 var lspserver: dict<any> = buf.CurbufGetServer('completion')
479 if lspserver->empty() || !lspserver.running || !lspserver.ready
483 var cur_col: number = charcol('.')
484 var line: string = getline('.')
486 if cur_col == 0 || line->empty()
490 # Trigger kind is 1 for 24x7 code complete or manual invocation
491 var triggerKind: number = 1
492 var triggerChar: string = ''
494 # If the character before the cursor is not a keyword character or is not
495 # one of the LSP completion trigger characters, then do nothing.
496 if line[cur_col - 2] !~ '\k'
497 var trigChars = lspserver.completionTriggerChars
498 var trigidx = trigChars->index(line[cur_col - 2])
502 # completion triggered by one of the trigger characters
504 triggerChar = trigChars[trigidx]
507 # first send all the changes in the current buffer to the LSP server
510 # initiate a request to LSP server to get list of completions
511 lspserver.getCompletion(triggerKind, triggerChar)
514 # Lazy complete documentation handler
516 var lspserver: dict<any> = buf.CurbufGetServerChecked('completion')
517 if lspserver->empty()
521 var item = v:event.completed_item
522 if item->has_key('user_data') && !item.user_data->empty()
523 if !item.user_data->has_key('documentation')
524 lspserver.resolveCompletion(item.user_data)
526 ShowCompletionDocumentation(item.user_data)
531 # If the completion popup documentation window displays "markdown" content,
532 # then set the 'filetype' to "lspgfm".
533 def LspSetPopupFileType()
534 var item = v:event.completed_item
535 if !item->has_key('user_data') || item.user_data->empty()
539 var cItem = item.user_data
540 if cItem->type() != v:t_dict || !cItem->has_key('documentation')
541 \ || cItem.documentation->type() != v:t_dict
542 \ || cItem.documentation.kind != 'markdown'
546 var id = popup_findinfo()
548 var bnum = id->winbufnr()
549 setbufvar(bnum, '&ft', 'lspgfm')
553 # complete done handler (LSP server-initiated actions after completion)
554 def LspCompleteDone()
555 var lspserver: dict<any> = buf.CurbufGetServerChecked('completion')
556 if lspserver->empty()
560 if v:completed_item->type() != v:t_dict
564 var completionData: any = v:completed_item->get('user_data', '')
565 if completionData->type() != v:t_dict
566 || !completionData->has_key('additionalTextEdits')
567 || !opt.lspOptions.completionTextEdit
571 var bnr: number = bufnr()
572 textedit.ApplyTextEdits(bnr, completionData.additionalTextEdits)
575 # Initialize buffer-local completion options and autocmds
576 export def BufferInit(lspserver: dict<any>, bnr: number, ftype: string)
577 if !lspserver.isCompletionProvider
578 # no support for completion
582 if !opt.lspOptions.autoComplete && !LspOmniComplEnabled(ftype)
583 # LSP auto/omni completion support is not enabled for this buffer
587 # buffer-local autocmds for completion
588 var acmds: list<dict<any>> = []
590 # set options for insert mode completion
591 if opt.lspOptions.autoComplete
592 if lspserver.completionLazyDoc
593 setbufvar(bnr, '&completeopt', 'menuone,popuphidden,noinsert,noselect')
595 setbufvar(bnr, '&completeopt', 'menuone,popup,noinsert,noselect')
597 setbufvar(bnr, '&completepopup', 'width:80,highlight:Pmenu,align:item,border:off')
598 # <Enter> in insert mode stops completion and inserts a <Enter>
599 if !opt.lspOptions.noNewlineInCompletion
600 :inoremap <expr> <buffer> <CR> pumvisible() ? "\<C-Y>\<CR>" : "\<CR>"
603 # Trigger 24x7 insert mode completion when text is changed
604 acmds->add({bufnr: bnr,
605 event: 'TextChangedI',
606 group: 'LSPBufferAutocmds',
607 cmd: 'LspComplete()'})
609 setbufvar(bnr, '&omnifunc', 'g:LspOmniFunc')
612 if lspserver.completionLazyDoc
613 # resolve additional documentation for a selected item
614 acmds->add({bufnr: bnr,
615 event: 'CompleteChanged',
616 group: 'LSPBufferAutocmds',
617 cmd: 'LspResolve()'})
620 acmds->add({bufnr: bnr,
621 event: 'CompleteChanged',
622 group: 'LSPBufferAutocmds',
623 cmd: 'LspSetPopupFileType()'})
625 # Execute LSP server initiated text edits after completion
626 acmds->add({bufnr: bnr,
627 event: 'CompleteDone',
628 group: 'LSPBufferAutocmds',
629 cmd: 'LspCompleteDone()'})
634 # vim: tabstop=8 shiftwidth=2 softtabstop=2