From: Yegappan Lakshmanan Date: Sun, 13 Nov 2022 02:30:35 +0000 (-0800) Subject: Move completed related functions to the completion.vim file X-Git-Url: http://www.git.stargrave.org/?a=commitdiff_plain;h=425a4a85e0a4713f46fb9221d89b79af9389bba3;p=vim-lsp.git Move completed related functions to the completion.vim file --- diff --git a/autoload/lsp/buffer.vim b/autoload/lsp/buffer.vim index e0d18fa..207c538 100644 --- a/autoload/lsp/buffer.vim +++ b/autoload/lsp/buffer.vim @@ -2,6 +2,8 @@ vim9script # Functions for managing the per-buffer LSP server information +import './util.vim' + # Buffer number to LSP server map var bufnrToServer: dict> = {} @@ -29,4 +31,29 @@ export def BufHasLspServer(bnr: number): bool return bufnrToServer->has_key(bnr) enddef +# Returns the LSP server for the current buffer if it is running and is ready. +# Returns an empty dict if the server is not found or is not ready. +export def CurbufGetServerChecked(): dict + var fname: string = @% + if fname == '' + return {} + endif + + var lspserver: dict = CurbufGetServer() + if lspserver->empty() + util.ErrMsg($'Error: Language server for "{&filetype}" file type is not found') + return {} + endif + if !lspserver.running + util.ErrMsg($'Error: Language server for "{&filetype}" file type is not running') + return {} + endif + if !lspserver.ready + util.ErrMsg($'Error: Language server for "{&filetype}" file type is not ready') + return {} + endif + + return lspserver +enddef + # vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/autoload/lsp/completion.vim b/autoload/lsp/completion.vim new file mode 100644 index 0000000..1c5e702 --- /dev/null +++ b/autoload/lsp/completion.vim @@ -0,0 +1,448 @@ +vim9script + +# LSP completion related functions + +import './util.vim' +import './buffer.vim' as buf +import './options.vim' as opt +import './textedit.vim' + +# per-filetype omni-completion enabled/disabled table +var ftypeOmniCtrlMap: dict = {} + +# Returns true if omni-completion is enabled for filetype 'ftype'. +# Otherwise, returns false. +def LspOmniComplEnabled(ftype: string): bool + return ftypeOmniCtrlMap->get(ftype, v:false) +enddef + +# Enables or disables omni-completion for filetype 'fype' +export def OmniComplSet(ftype: string, enabled: bool) + ftypeOmniCtrlMap->extend({[ftype]: enabled}) +enddef + +# Map LSP complete item kind to a character +def LspCompleteItemKindChar(kind: number): string + var kindMap: list = ['', + 't', # Text + 'm', # Method + 'f', # Function + 'C', # Constructor + 'F', # Field + 'v', # Variable + 'c', # Class + 'i', # Interface + 'M', # Module + 'p', # Property + 'u', # Unit + 'V', # Value + 'e', # Enum + 'k', # Keyword + 'S', # Snippet + 'C', # Color + 'f', # File + 'r', # Reference + 'F', # Folder + 'E', # EnumMember + 'd', # Contant + 's', # Struct + 'E', # Event + 'o', # Operator + 'T' # TypeParameter + ] + if kind > 25 + return '' + endif + return kindMap[kind] +enddef + +# Remove all the snippet placeholders from 'str' and return the value. +# Based on a similar function in the vim-lsp plugin. +def MakeValidWord(str_arg: string): string + var str = substitute(str_arg, '\$[0-9]\+\|\${\%(\\.\|[^}]\)\+}', '', 'g') + str = substitute(str, '\\\(.\)', '\1', 'g') + var valid = matchstr(str, '^[^"'' (<{\[\t\r\n]\+') + if empty(valid) + return str + endif + if valid =~# ':$' + return valid[: -2] + endif + return valid +enddef + +# process the 'textDocument/completion' reply from the LSP server +# Result: CompletionItem[] | CompletionList | null +export def CompletionReply(lspserver: dict, cItems: any) + if cItems->empty() + return + endif + + var items: list> + if cItems->type() == v:t_list + items = cItems + else + items = cItems.items + endif + + var completeItems: list> = [] + for item in items + var d: dict = {} + if item->has_key('textEdit') && item.textEdit->has_key('newText') + d.word = item.textEdit.newText + elseif item->has_key('insertText') + d.word = item.insertText + else + d.word = item.label + endif + if item->get('insertTextFormat', 1) == 2 + # snippet completion. Needs a snippet plugin to expand the snippet. + # Remove all the snippet placeholders + d.word = MakeValidWord(d.word) + endif + d.abbr = item.label + d.dup = 1 + if item->has_key('kind') + # namespace CompletionItemKind + # map LSP kind to complete-item-kind + d.kind = LspCompleteItemKindChar(item.kind) + endif + if lspserver.completionLazyDoc + d.info = 'Lazy doc' + else + if item->has_key('detail') && item.detail != '' + # Solve a issue where if a server send a detail field + # with a "\n", on the menu will be everything joined with + # a "^@" separating it. (example: clangd) + d.menu = item.detail->split("\n")[0] + endif + if item->has_key('documentation') + if item.documentation->type() == v:t_string && item.documentation != '' + d.info = item.documentation + elseif item.documentation->type() == v:t_dict + && item.documentation.value->type() == v:t_string + d.info = item.documentation.value + endif + endif + endif + d.user_data = item + completeItems->add(d) + endfor + + if opt.lspOptions.autoComplete && !lspserver.omniCompletePending + if completeItems->empty() + # no matches + return + endif + + var m = mode() + if m != 'i' && m != 'R' && m != 'Rv' + # If not in insert or replace mode, then don't start the completion + return + endif + + if completeItems->len() == 1 + && getline('.')->matchstr(completeItems[0].word .. '\>') != '' + # only one complete match. No need to show the completion popup + return + endif + + var start_col: number = 0 + + # FIXME: The following doesn't work with typescript as one of the + # completion item has a start column that is before the special character. + # For example, when completing the methods for "str.", the dot is removed. + # + # # Find the start column for the completion. If any of the entries + # # returned by the LSP server has a starting position, then use that. + # for item in items + # if item->has_key('textEdit') + # start_col = item.textEdit.range.start.character + 1 + # break + # endif + # endfor + + # LSP server didn't return a starting position for completion, search + # backwards from the current cursor position for a non-keyword character. + if start_col == 0 + var line: string = getline('.') + var start = col('.') - 1 + while start > 0 && line[start - 1] =~ '\k' + start -= 1 + endwhile + start_col = start + 1 + endif + + complete(start_col, completeItems) + else + lspserver.completeItems = completeItems + lspserver.omniCompletePending = false + endif +enddef + +# process the 'completionItem/resolve' reply from the LSP server +# Result: CompletionItem +export def CompletionResolveReply(lspserver: dict, cItem: dict) + if cItem->empty() + return + endif + + # check if completion item is still selected + var cInfo = complete_info() + if cInfo->empty() + || !cInfo.pum_visible + || cInfo.selected == -1 + || cInfo.items[cInfo.selected].user_data.label != cItem.label + return + endif + + var infoText: list + var infoKind: string + + if cItem->has_key('detail') + # Solve a issue where if a server send the detail field with "\n", + # on the completion popup, everything will be joined with "^@" + # (example: typescript-language-server) + infoText->extend(cItem.detail->split("\n")) + endif + + if cItem->has_key('documentation') + if !infoText->empty() + infoText->extend(['- - -']) + endif + if cItem.documentation->type() == v:t_dict + # MarkupContent + if cItem.documentation.kind == 'plaintext' + infoText->extend(cItem.documentation.value->split("\n")) + infoKind = 'text' + elseif cItem.documentation.kind == 'markdown' + infoText->extend(cItem.documentation.value->split("\n")) + infoKind = 'lspgfm' + else + util.ErrMsg($'Error: Unsupported documentation type ({cItem.documentation.kind})') + return + endif + elseif cItem.documentation->type() == v:t_string + infoText->extend(cItem.documentation->split("\n")) + else + util.ErrMsg($'Error: Unsupported documentation ({cItem.documentation->string()})') + return + endif + endif + + if infoText->empty() + return + endif + + # check if completion item is changed in meantime + cInfo = complete_info() + if cInfo->empty() + || !cInfo.pum_visible + || cInfo.selected == -1 + || cInfo.items[cInfo.selected].user_data.label != cItem.label + return + endif + + var id = popup_findinfo() + if id > 0 + var bufnr = id->winbufnr() + id->popup_settext(infoText) + infoKind->setbufvar(bufnr, '&ft') + id->popup_show() + endif +enddef + +# omni complete handler +def g:LspOmniFunc(findstart: number, base: string): any + var lspserver: dict = buf.CurbufGetServerChecked() + if lspserver->empty() + return -2 + endif + + if findstart + # first send all the changes in the current buffer to the LSP server + listener_flush() + + lspserver.omniCompletePending = v:true + lspserver.completeItems = [] + # initiate a request to LSP server to get list of completions + lspserver.getCompletion(1, '') + + # locate the start of the word + var line = getline('.') + var start = charcol('.') - 1 + while start > 0 && line[start - 1] =~ '\k' + start -= 1 + endwhile + return start + else + # Wait for the list of matches from the LSP server + var count: number = 0 + while lspserver.omniCompletePending && count < 1000 + if complete_check() + return v:none + endif + sleep 2m + count += 1 + endwhile + + var res: list> = [] + for item in lspserver.completeItems + res->add(item) + endfor + return res->empty() ? v:none : res + endif +enddef + +# Insert mode completion handler. Used when 24x7 completion is enabled +# (default). +def LspComplete() + var lspserver: dict = buf.CurbufGetServer() + if lspserver->empty() || !lspserver.running || !lspserver.ready + return + endif + + var cur_col: number = col('.') + var line: string = getline('.') + + if cur_col == 0 || line->empty() + return + endif + + # Trigger kind is 1 for 24x7 code complete or manual invocation + var triggerKind: number = 1 + var triggerChar: string = '' + + # If the character before the cursor is not a keyword character or is not + # one of the LSP completion trigger characters, then do nothing. + if line[cur_col - 2] !~ '\k' + var trigidx = lspserver.completionTriggerChars->index(line[cur_col - 2]) + if trigidx == -1 + return + endif + # completion triggered by one of the trigger characters + triggerKind = 2 + triggerChar = lspserver.completionTriggerChars[trigidx] + endif + + # first send all the changes in the current buffer to the LSP server + listener_flush() + + # initiate a request to LSP server to get list of completions + lspserver.getCompletion(triggerKind, triggerChar) +enddef + +# Lazy complete documentation handler +def LspResolve() + var lspserver: dict = buf.CurbufGetServerChecked() + if lspserver->empty() + return + endif + + var item = v:event.completed_item + if item->has_key('user_data') && !empty(item.user_data) + lspserver.resolveCompletion(item.user_data) + endif +enddef + +# If the completion popup documentation window displays 'markdown' content, +# then set the 'filetype' to 'lspgfm'. +def LspSetFileType() + var item = v:event.completed_item + if !item->has_key('user_data') || empty(item.user_data) + return + endif + + var cItem = item.user_data + if !cItem->has_key('documentation') || cItem->type() != v:t_dict + || cItem.documentation.kind != 'markdown' + return + endif + + var id = popup_findinfo() + if id > 0 + var bnum = id->winbufnr() + setbufvar(bnum, '&ft', 'lspgfm') + endif +enddef + +# complete done handler (LSP server-initiated actions after completion) +def LspCompleteDone() + var lspserver: dict = buf.CurbufGetServerChecked() + if lspserver->empty() + return + endif + + if v:completed_item->type() != v:t_dict + return + endif + + var completionData: any = v:completed_item->get('user_data', '') + if completionData->type() != v:t_dict + || !completionData->has_key('additionalTextEdits') + return + endif + + var bnr: number = bufnr() + textedit.ApplyTextEdits(bnr, completionData.additionalTextEdits) +enddef + +# Add buffer-local autocmds for completion +def AddAutocmds(lspserver: dict, bnr: number) + var acmds: list> = [] + + # Insert-mode completion autocmds (if configured) + if opt.lspOptions.autoComplete + # Trigger 24x7 insert mode completion when text is changed + acmds->add({bufnr: bnr, + event: 'TextChangedI', + group: 'LSPBufferAutocmds', + cmd: 'LspComplete()'}) + if lspserver.completionLazyDoc + # resolve additional documentation for a selected item + acmds->add({bufnr: bnr, + event: 'CompleteChanged', + group: 'LSPBufferAutocmds', + cmd: 'LspResolve()'}) + endif + endif + + acmds->add({bufnr: bnr, + event: 'CompleteChanged', + group: 'LSPBufferAutocmds', + cmd: 'LspSetFileType()'}) + + # Execute LSP server initiated text edits after completion + acmds->add({bufnr: bnr, + event: 'CompleteDone', + group: 'LSPBufferAutocmds', + cmd: 'LspCompleteDone()'}) + + autocmd_add(acmds) +enddef + +# Initialize buffer-local completion options and autocmds +export def BufferInit(lspserver: dict, bnr: number, ftype: string) + # set options for insert mode completion + if opt.lspOptions.autoComplete + if lspserver.completionLazyDoc + setbufvar(bnr, '&completeopt', 'menuone,popuphidden,noinsert,noselect') + setbufvar(bnr, '&completepopup', 'width:80,highlight:Pmenu,align:menu,border:off') + else + setbufvar(bnr, '&completeopt', 'menuone,popup,noinsert,noselect') + setbufvar(bnr, '&completepopup', 'border:off') + endif + # in insert mode stops completion and inserts a + if !opt.lspOptions.noNewlineInCompletion + inoremap pumvisible() ? "\\" : "\" + endif + else + if LspOmniComplEnabled(ftype) + setbufvar(bnr, '&omnifunc', 'LspOmniFunc') + endif + endif + + AddAutocmds(lspserver, bnr) +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/autoload/lsp/lsp.vim b/autoload/lsp/lsp.vim index cfe7ec7..0f1a19c 100644 --- a/autoload/lsp/lsp.vim +++ b/autoload/lsp/lsp.vim @@ -11,6 +11,7 @@ import './options.vim' as opt import './lspserver.vim' as lserver import './util.vim' import './buffer.vim' as buf +import './completion.vim' import './textedit.vim' import './diag.vim' import './symbol.vim' @@ -24,9 +25,6 @@ var lspServers: list> = [] # filetype to LSP server map var ftypeServerMap: dict> = {} -# per-filetype omni-completion enabled/disabled table -var ftypeOmniCtrlMap: dict = {} - var lspInitializedOnce = false def LspInitOnce() @@ -53,47 +51,11 @@ def LspGetServer(ftype: string): dict return ftypeServerMap->get(ftype, {}) enddef -# Returns the LSP server for the current buffer if it is running and is ready. -# Returns an empty dict if the server is not found or is not ready. -def CurbufGetServerChecked(): dict - var fname: string = @% - if fname == '' - return {} - endif - - var lspserver: dict = buf.CurbufGetServer() - if lspserver->empty() - util.ErrMsg($'Error: Language server for "{&filetype}" file type is not found') - return {} - endif - if !lspserver.running - util.ErrMsg($'Error: Language server for "{&filetype}" file type is not running') - return {} - endif - if !lspserver.ready - util.ErrMsg($'Error: Language server for "{&filetype}" file type is not ready') - return {} - endif - - return lspserver -enddef - # Add a LSP server for a filetype def LspAddServer(ftype: string, lspsrv: dict) ftypeServerMap->extend({[ftype]: lspsrv}) enddef -# Returns true if omni-completion is enabled for filetype 'ftype'. -# Otherwise, returns false. -def LspOmniComplEnabled(ftype: string): bool - return ftypeOmniCtrlMap->get(ftype, v:false) -enddef - -# Enables or disables omni-completion for filetype 'fype' -def LspOmniComplSet(ftype: string, enabled: bool) - ftypeOmniCtrlMap->extend({[ftype]: enabled}) -enddef - # Enable/disable the logging of the language server protocol messages export def ServerDebug(arg: string) if arg !=? 'on' && arg !=? 'off' @@ -136,7 +98,7 @@ enddef # Go to a definition using "textDocument/definition" LSP request export def GotoDefinition(peek: bool, cmdmods: string) - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -146,7 +108,7 @@ enddef # Go to a declaration using "textDocument/declaration" LSP request export def GotoDeclaration(peek: bool, cmdmods: string) - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -156,7 +118,7 @@ enddef # Go to a type definition using "textDocument/typeDefinition" LSP request export def GotoTypedef(peek: bool, cmdmods: string) - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -166,7 +128,7 @@ enddef # Go to a implementation using "textDocument/implementation" LSP request export def GotoImplementation(peek: bool, cmdmods: string) - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -177,7 +139,7 @@ enddef # Switch source header using "textDocument/switchSourceHeader" LSP request # (Clangd specifc extension) export def SwitchSourceHeader() - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -188,7 +150,7 @@ enddef # Show the signature using "textDocument/signatureHelp" LSP method # Invoked from an insert-mode mapping, so return an empty string. def g:LspShowSignature(): string - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return '' endif @@ -277,33 +239,6 @@ def AddBufLocalAutocmds(lspserver: dict, bnr: number): void group: 'LSPBufferAutocmds', cmd: 'LspLeftInsertMode()'}) - # Insert-mode completion autocmds (if configured) - if opt.lspOptions.autoComplete - # Trigger 24x7 insert mode completion when text is changed - acmds->add({bufnr: bnr, - event: 'TextChangedI', - group: 'LSPBufferAutocmds', - cmd: 'LspComplete()'}) - if lspserver.completionLazyDoc - # resolve additional documentation for a selected item - acmds->add({bufnr: bnr, - event: 'CompleteChanged', - group: 'LSPBufferAutocmds', - cmd: 'LspResolve()'}) - endif - endif - - acmds->add({bufnr: bnr, - event: 'CompleteChanged', - group: 'LSPBufferAutocmds', - cmd: 'LspSetFileType()'}) - - # Execute LSP server initiated text edits after completion - acmds->add({bufnr: bnr, - event: 'CompleteDone', - group: 'LSPBufferAutocmds', - cmd: 'LspCompleteDone()'}) - # Auto highlight all the occurrences of the current keyword if opt.lspOptions.autoHighlight && lspserver.isDocumentHighlightProvider @@ -329,24 +264,7 @@ def BufferInit(bnr: number): void # add a listener to track changes to this buffer listener_add(Bufchange_listener, bnr) - # set options for insert mode completion - if opt.lspOptions.autoComplete - if lspserver.completionLazyDoc - setbufvar(bnr, '&completeopt', 'menuone,popuphidden,noinsert,noselect') - setbufvar(bnr, '&completepopup', 'width:80,highlight:Pmenu,align:menu,border:off') - else - setbufvar(bnr, '&completeopt', 'menuone,popup,noinsert,noselect') - setbufvar(bnr, '&completepopup', 'border:off') - endif - # in insert mode stops completion and inserts a - if !opt.lspOptions.noNewlineInCompletion - inoremap pumvisible() ? "\\" : "\" - endif - else - if LspOmniComplEnabled(ftype) - setbufvar(bnr, '&omnifunc', 'LspOmniFunc') - endif - endif + completion.BufferInit(lspserver, bnr, ftype) setbufvar(bnr, '&balloonexpr', 'g:LspDiagExpr()') @@ -432,7 +350,7 @@ enddef # Restart the LSP server for the current buffer export def RestartServer() - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -457,7 +375,7 @@ enddef # Add the LSP server for files with 'filetype' as "ftype". def AddServerForFiltype(lspserver: dict, ftype: string, omnicompl: bool) LspAddServer(ftype, lspserver) - LspOmniComplSet(ftype, omnicompl) + completion.OmniComplSet(ftype, omnicompl) # If a buffer of this file type is already present, then send it to the LSP # server now. @@ -551,7 +469,7 @@ export def ServerTraceSet(traceVal: string) return endif - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -562,7 +480,7 @@ enddef # Display the diagnostic messages from the LSP server for the current buffer # in a quickfix list export def ShowDiagnostics(): void - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -572,7 +490,7 @@ enddef # Show the diagnostic message for the current line export def LspShowCurrentDiag() - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -613,7 +531,7 @@ enddef # jump to the next/previous/first diagnostic message in the current buffer export def JumpToDiag(which: string): void - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -621,143 +539,6 @@ export def JumpToDiag(which: string): void diag.LspDiagsJump(lspserver, which) enddef -# Insert mode completion handler. Used when 24x7 completion is enabled -# (default). -def LspComplete() - var lspserver: dict = buf.CurbufGetServer() - if lspserver->empty() || !lspserver.running || !lspserver.ready - return - endif - - var cur_col: number = col('.') - var line: string = getline('.') - - if cur_col == 0 || line->empty() - return - endif - - # Trigger kind is 1 for 24x7 code complete or manual invocation - var triggerKind: number = 1 - var triggerChar: string = '' - - # If the character before the cursor is not a keyword character or is not - # one of the LSP completion trigger characters, then do nothing. - if line[cur_col - 2] !~ '\k' - var trigidx = lspserver.completionTriggerChars->index(line[cur_col - 2]) - if trigidx == -1 - return - endif - # completion triggered by one of the trigger characters - triggerKind = 2 - triggerChar = lspserver.completionTriggerChars[trigidx] - endif - - # first send all the changes in the current buffer to the LSP server - listener_flush() - - # initiate a request to LSP server to get list of completions - lspserver.getCompletion(triggerKind, triggerChar) - - return -enddef - -# Lazy complete documentation handler -def LspResolve() - var lspserver: dict = CurbufGetServerChecked() - if lspserver->empty() - return - endif - - var item = v:event.completed_item - if item->has_key('user_data') && !empty(item.user_data) - lspserver.resolveCompletion(item.user_data) - endif -enddef - -# If the completion popup documentation window displays 'markdown' content, -# then set the 'filetype' to 'lspgfm'. -def LspSetFileType() - var item = v:event.completed_item - if !item->has_key('user_data') || empty(item.user_data) - return - endif - - var cItem = item.user_data - if !cItem->has_key('documentation') || cItem->type() != v:t_dict - || cItem.documentation.kind != 'markdown' - return - endif - - var id = popup_findinfo() - if id > 0 - var bnum = id->winbufnr() - setbufvar(bnum, '&ft', 'lspgfm') - endif -enddef - -# omni complete handler -def g:LspOmniFunc(findstart: number, base: string): any - var lspserver: dict = CurbufGetServerChecked() - if lspserver->empty() - return -2 - endif - - if findstart - # first send all the changes in the current buffer to the LSP server - listener_flush() - - lspserver.omniCompletePending = v:true - lspserver.completeItems = [] - # initiate a request to LSP server to get list of completions - lspserver.getCompletion(1, '') - - # locate the start of the word - var line = getline('.') - var start = charcol('.') - 1 - while start > 0 && line[start - 1] =~ '\k' - start -= 1 - endwhile - return start - else - # Wait for the list of matches from the LSP server - var count: number = 0 - while lspserver.omniCompletePending && count < 1000 - if complete_check() - return v:none - endif - sleep 2m - count += 1 - endwhile - - var res: list> = [] - for item in lspserver.completeItems - res->add(item) - endfor - return res->empty() ? v:none : res - endif -enddef - -# complete done handler (LSP server-initiated actions after completion) -def LspCompleteDone() - var lspserver: dict = CurbufGetServerChecked() - if lspserver->empty() - return - endif - - if v:completed_item->type() != v:t_dict - return - endif - - var completionData: any = v:completed_item->get('user_data', '') - if completionData->type() != v:t_dict - || !completionData->has_key('additionalTextEdits') - return - endif - - var bnr: number = bufnr() - textedit.ApplyTextEdits(bnr, completionData.additionalTextEdits) -enddef - # Display the hover message from the LSP server for the current cursor # location export def Hover() @@ -771,7 +552,7 @@ enddef # show symbol references export def ShowReferences(peek: bool) - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -781,7 +562,7 @@ enddef # highlight all the places where a symbol is referenced def g:LspDocHighlight() - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -827,7 +608,7 @@ export def TextDocFormat(range_args: number, line1: number, line2: number) return endif - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -846,7 +627,7 @@ enddef # Display all the locations where the current symbol is called from. # Uses LSP "callHierarchy/incomingCalls" request export def IncomingCalls() - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -857,7 +638,7 @@ enddef # Display all the symbols used by the current symbol. # Uses LSP "callHierarchy/outgoingCalls" request export def OutgoingCalls() - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -868,7 +649,7 @@ enddef # Rename a symbol # Uses LSP "textDocument/rename" request export def Rename() - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -888,7 +669,7 @@ enddef # Perform a code action # Uses LSP "textDocument/codeAction" request export def CodeAction(line1: number, line2: number) - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -900,7 +681,7 @@ enddef # Perform a workspace wide symbol lookup # Uses LSP "workspace/symbol" request export def SymbolSearch(queryArg: string) - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -919,7 +700,7 @@ enddef # Display the list of workspace folders export def ListWorkspaceFolders() - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -929,7 +710,7 @@ enddef # Add a workspace folder. Default is to use the current folder. export def AddWorkspaceFolder(dirArg: string) - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -952,7 +733,7 @@ enddef # Remove a workspace folder. Default is to use the current folder. export def RemoveWorkspaceFolder(dirArg: string) - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -975,7 +756,7 @@ enddef # expand the previous selection or start a new selection export def SelectionExpand() - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -985,7 +766,7 @@ enddef # shrink the previous selection or start a new selection export def SelectionShrink() - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -995,7 +776,7 @@ enddef # fold the entire document export def FoldDocument() - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -1021,7 +802,7 @@ enddef # Display the LSP server capabilities export def ShowServerCapabilities() - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return endif @@ -1031,7 +812,7 @@ enddef # Function to use with the 'tagfunc' option. export def TagFunc(pat: string, flags: string, info: dict): any - var lspserver: dict = CurbufGetServerChecked() + var lspserver: dict = buf.CurbufGetServerChecked() if lspserver->empty() return v:null endif diff --git a/autoload/lsp/lspserver.vim b/autoload/lsp/lspserver.vim index a22d2f3..9b74739 100644 --- a/autoload/lsp/lspserver.vim +++ b/autoload/lsp/lspserver.vim @@ -11,6 +11,7 @@ import './diag.vim' import './selection.vim' import './symbol.vim' import './textedit.vim' +import './completion.vim' import './hover.vim' import './signature.vim' import './codeaction.vim' @@ -754,165 +755,6 @@ def GetLspTextDocPosition(): dict> position: GetLspPosition()} enddef -# Map LSP complete item kind to a character -def LspCompleteItemKindChar(kind: number): string - var kindMap: list = ['', - 't', # Text - 'm', # Method - 'f', # Function - 'C', # Constructor - 'F', # Field - 'v', # Variable - 'c', # Class - 'i', # Interface - 'M', # Module - 'p', # Property - 'u', # Unit - 'V', # Value - 'e', # Enum - 'k', # Keyword - 'S', # Snippet - 'C', # Color - 'f', # File - 'r', # Reference - 'F', # Folder - 'E', # EnumMember - 'd', # Contant - 's', # Struct - 'E', # Event - 'o', # Operator - 'T' # TypeParameter - ] - if kind > 25 - return '' - endif - return kindMap[kind] -enddef - -# Remove all the snippet placeholders from 'str' and return the value. -# Based on a similar function in the vim-lsp plugin. -def MakeValidWord(str_arg: string): string - var str = substitute(str_arg, '\$[0-9]\+\|\${\%(\\.\|[^}]\)\+}', '', 'g') - str = substitute(str, '\\\(.\)', '\1', 'g') - var valid = matchstr(str, '^[^"'' (<{\[\t\r\n]\+') - if empty(valid) - return str - endif - if valid =~# ':$' - return valid[: -2] - endif - return valid -enddef - -# process the 'textDocument/completion' reply from the LSP server -# Result: CompletionItem[] | CompletionList | null -def CompletionReply(lspserver: dict, cItems: any) - if cItems->empty() - return - endif - - var items: list> - if cItems->type() == v:t_list - items = cItems - else - items = cItems.items - endif - - var completeItems: list> = [] - for item in items - var d: dict = {} - if item->has_key('textEdit') && item.textEdit->has_key('newText') - d.word = item.textEdit.newText - elseif item->has_key('insertText') - d.word = item.insertText - else - d.word = item.label - endif - if item->get('insertTextFormat', 1) == 2 - # snippet completion. Needs a snippet plugin to expand the snippet. - # Remove all the snippet placeholders - d.word = MakeValidWord(d.word) - endif - d.abbr = item.label - d.dup = 1 - if item->has_key('kind') - # namespace CompletionItemKind - # map LSP kind to complete-item-kind - d.kind = LspCompleteItemKindChar(item.kind) - endif - if lspserver.completionLazyDoc - d.info = 'Lazy doc' - else - if item->has_key('detail') && item.detail != '' - # Solve a issue where if a server send a detail field - # with a "\n", on the menu will be everything joined with - # a "^@" separating it. (example: clangd) - d.menu = item.detail->split("\n")[0] - endif - if item->has_key('documentation') - if item.documentation->type() == v:t_string && item.documentation != '' - d.info = item.documentation - elseif item.documentation->type() == v:t_dict - && item.documentation.value->type() == v:t_string - d.info = item.documentation.value - endif - endif - endif - d.user_data = item - completeItems->add(d) - endfor - - if opt.lspOptions.autoComplete && !lspserver.omniCompletePending - if completeItems->empty() - # no matches - return - endif - - var m = mode() - if m != 'i' && m != 'R' && m != 'Rv' - # If not in insert or replace mode, then don't start the completion - return - endif - - if completeItems->len() == 1 - && getline('.')->matchstr(completeItems[0].word .. '\>') != '' - # only one complete match. No need to show the completion popup - return - endif - - var start_col: number = 0 - - # FIXME: The following doesn't work with typescript as one of the - # completion item has a start column that is before the special character. - # For example, when completing the methods for "str.", the dot is removed. - # - # # Find the start column for the completion. If any of the entries - # # returned by the LSP server has a starting position, then use that. - # for item in items - # if item->has_key('textEdit') - # start_col = item.textEdit.range.start.character + 1 - # break - # endif - # endfor - - # LSP server didn't return a starting position for completion, search - # backwards from the current cursor position for a non-keyword character. - if start_col == 0 - var line: string = getline('.') - var start = col('.') - 1 - while start > 0 && line[start - 1] =~ '\k' - start -= 1 - endwhile - start_col = start + 1 - endif - - complete(start_col, completeItems) - else - lspserver.completeItems = completeItems - lspserver.omniCompletePending = false - endif -enddef - # Get a list of completion items. # Request: "textDocument/completion" # Param: CompletionParams @@ -934,79 +776,8 @@ def GetCompletion(lspserver: dict, triggerKind_arg: number, triggerChar: st # interface CompletionContext params.context = {triggerKind: triggerKind_arg, triggerCharacter: triggerChar} - lspserver.rpc_a('textDocument/completion', params, CompletionReply) -enddef - -# process the 'completionItem/resolve' reply from the LSP server -# Result: CompletionItem -def CompletionResolveReply(lspserver: dict, cItem: dict) - if cItem->empty() - return - endif - - # check if completion item is still selected - var cInfo = complete_info() - if cInfo->empty() - || !cInfo.pum_visible - || cInfo.selected == -1 - || cInfo.items[cInfo.selected].user_data.label != cItem.label - return - endif - - var infoText: list - var infoKind: string - - if cItem->has_key('detail') - # Solve a issue where if a server send the detail field with "\n", - # on the completion popup, everything will be joined with "^@" - # (example: typescript-language-server) - infoText->extend(cItem.detail->split("\n")) - endif - - if cItem->has_key('documentation') - if !infoText->empty() - infoText->extend(['- - -']) - endif - if cItem.documentation->type() == v:t_dict - # MarkupContent - if cItem.documentation.kind == 'plaintext' - infoText->extend(cItem.documentation.value->split("\n")) - infoKind = 'text' - elseif cItem.documentation.kind == 'markdown' - infoText->extend(cItem.documentation.value->split("\n")) - infoKind = 'lspgfm' - else - util.ErrMsg($'Error: Unsupported documentation type ({cItem.documentation.kind})') - return - endif - elseif cItem.documentation->type() == v:t_string - infoText->extend(cItem.documentation->split("\n")) - else - util.ErrMsg($'Error: Unsupported documentation ({cItem.documentation->string()})') - return - endif - endif - - if infoText->empty() - return - endif - - # check if completion item is changed in meantime - cInfo = complete_info() - if cInfo->empty() - || !cInfo.pum_visible - || cInfo.selected == -1 - || cInfo.items[cInfo.selected].user_data.label != cItem.label - return - endif - - var id = popup_findinfo() - if id > 0 - var bufnr = id->winbufnr() - id->popup_settext(infoText) - infoKind->setbufvar(bufnr, '&ft') - id->popup_show() - endif + lspserver.rpc_a('textDocument/completion', params, + completion.CompletionReply) enddef # Get lazy properties for a completion item. @@ -1020,9 +791,8 @@ def ResolveCompletion(lspserver: dict, item: dict): void endif # interface CompletionItem - var params = item - - lspserver.rpc_a('completionItem/resolve', params, CompletionResolveReply) + lspserver.rpc_a('completionItem/resolve', item, + completion.CompletionResolveReply) enddef # Jump to or peek a symbol location.