From 9d36e83fb05eb6d9e5507502c6f23a7718d0bc33 Mon Sep 17 00:00:00 2001 From: Yegappan Lakshmanan Date: Sat, 29 Oct 2022 12:35:21 -0700 Subject: [PATCH] Use callback functions for async RPC requests to simplify the code --- autoload/lsp/diag.vim | 32 +-- autoload/lsp/handlers.vim | 440 +---------------------------------- autoload/lsp/lsp.vim | 9 +- autoload/lsp/lspserver.vim | 454 ++++++++++++++++++++++++++++--------- autoload/lsp/symbol.vim | 89 ++++++++ plugin/lsp.vim | 3 + test/unit_tests.vim | 28 +-- 7 files changed, 480 insertions(+), 575 deletions(-) diff --git a/autoload/lsp/diag.vim b/autoload/lsp/diag.vim index acd64a5..85870c9 100644 --- a/autoload/lsp/diag.vim +++ b/autoload/lsp/diag.vim @@ -112,26 +112,28 @@ enddef # get the count of error in the current buffer export def DiagsGetErrorCount(lspserver: dict): dict - var res = {'Error': 0, 'Warn': 0, 'Info': 0, 'Hint': 0} + var errCount = 0 + var warnCount = 0 + var infoCount = 0 + var hintCount = 0 var bnr: number = bufnr() if lspserver.diagsMap->has_key(bnr) - for item in lspserver.diagsMap[bnr]->values() - if item->has_key('severity') - if item.severity == 1 - res.Error = res.Error + 1 - elseif item.severity == 2 - res.Warn = res.Warn + 1 - elseif item.severity == 3 - res.Info = res.Info + 1 - elseif item.severity == 4 - res.Hint = res.Hint + 1 - endif - endif - endfor + for item in lspserver.diagsMap[bnr]->values() + var severity = item->get('severity', -1) + if severity == 1 + errCount += 1 + elseif severity == 2 + warnCount += 1 + elseif severity == 3 + infoCount += 1 + elseif severity == 4 + hintCount += 1 + endif + endfor endif - return res + return {'Error': errCount, 'Warn': warnCount, 'Info': infoCount, 'Hint': hintCount} enddef # Map the LSP DiagnosticSeverity to a quickfix type character diff --git a/autoload/lsp/handlers.vim b/autoload/lsp/handlers.vim index 6d19b77..0c9a704 100644 --- a/autoload/lsp/handlers.vim +++ b/autoload/lsp/handlers.vim @@ -10,448 +10,10 @@ import './diag.vim' import './outline.vim' import './textedit.vim' import './symbol.vim' -import './codeaction.vim' -import './callhierarchy.vim' as callhier - -# process the 'initialize' method reply from the LSP server -# Result: InitializeResult -def ProcessInitializeReply(lspserver: dict, req: dict, reply: dict): void - if reply.result->empty() - return - endif - - var caps: dict = reply.result.capabilities - lspserver.caps = caps - - if opt.lspOptions.autoComplete && caps->has_key('completionProvider') - lspserver.completionTriggerChars = caps.completionProvider->get('triggerCharacters', []) - lspserver.completionLazyDoc = - lspserver.caps.completionProvider->has_key('resolveProvider') - && lspserver.caps.completionProvider.resolveProvider - endif - - # send a "initialized" notification to server - lspserver.sendInitializedNotif() - lspserver.ready = true - if exists($'#User#LspServerReady{lspserver.name}') - exe $'doautocmd User LspServerReady{lspserver.name}' - endif - - # if the outline window is opened, then request the symbols for the current - # buffer - if bufwinid('LSP-Outline') != -1 - lspserver.getDocSymbols(@%) - endif -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 - -# process the 'textDocument/completion' reply from the LSP server -# Result: CompletionItem[] | CompletionList | null -def ProcessCompletionReply(lspserver: dict, req: dict, reply: dict): void - if reply.result->empty() - return - endif - - var items: list> - if reply.result->type() == v:t_list - items = reply.result - else - items = reply.result.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 - 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') - # 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 -def ProcessResolveReply(lspserver: dict, req: dict, reply: dict): void - if reply.result->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 != reply.result.label - return - endif - - var infoText: list - var infoKind: string - - if reply.result->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(reply.result.detail->split("\n")) - endif - - if reply.result->has_key('documentation') - if !infoText->empty() - infoText->extend(['- - -']) - endif - if reply.result.documentation->type() == v:t_dict - # MarkupContent - if reply.result.documentation.kind == 'plaintext' - infoText->extend(reply.result.documentation.value->split("\n")) - infoKind = 'text' - elseif reply.result.documentation.kind == 'markdown' - infoText->extend(reply.result.documentation.value->split("\n")) - infoKind = 'markdown' - else - util.ErrMsg($'Error: Unsupported documentation type ({reply.result.documentation.kind})') - return - endif - elseif reply.result.documentation->type() == v:t_string - infoText->extend(reply.result.documentation->split("\n")) - else - util.ErrMsg($'Error: Unsupported documentation ({reply.result.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 != reply.result.label - return - endif - - var id = popup_findinfo() - if id > 0 - var bufnr = id->winbufnr() - infoKind->setbufvar(bufnr, '&ft') - if infoKind == 'markdown' - 3->setwinvar(id, '&conceallevel') - else - 0->setwinvar(id, '&conceallevel') - endif - id->popup_settext(infoText) - id->popup_show() - endif -enddef - -# process the 'textDocument/documentHighlight' reply from the LSP server -# Result: DocumentHighlight[] | null -def ProcessDocHighlightReply(lspserver: dict, req: dict, reply: dict): void - if reply.result->empty() - return - endif - - var fname: string = util.LspUriToFile(req.params.textDocument.uri) - var bnr = fname->bufnr() - - for docHL in reply.result - var kind: number = docHL->get('kind', 1) - var propName: string - if kind == 2 - # Read-access - propName = 'LspReadRef' - elseif kind == 3 - # Write-access - propName = 'LspWriteRef' - else - # textual reference - propName = 'LspTextRef' - endif - prop_add(docHL.range.start.line + 1, - util.GetLineByteFromPos(bnr, docHL.range.start) + 1, - {end_lnum: docHL.range.end.line + 1, - end_col: util.GetLineByteFromPos(bnr, docHL.range.end) + 1, - bufnr: bnr, - type: propName}) - endfor -enddef - -# process SymbolInformation[] -def ProcessSymbolInfoTable(symbolInfoTable: list>, - symbolTypeTable: dict>>, - symbolLineTable: list>) - var fname: string - var symbolType: string - var name: string - var r: dict> - var symInfo: dict - - for syminfo in symbolInfoTable - fname = util.LspUriToFile(syminfo.location.uri) - symbolType = symbol.SymbolKindToName(syminfo.kind) - name = syminfo.name - if syminfo->has_key('containerName') - if syminfo.containerName != '' - name ..= $' [{syminfo.containerName}]' - endif - endif - r = syminfo.location.range - - if !symbolTypeTable->has_key(symbolType) - symbolTypeTable[symbolType] = [] - endif - symInfo = {name: name, range: r} - symbolTypeTable[symbolType]->add(symInfo) - symbolLineTable->add(symInfo) - endfor -enddef - -# process DocumentSymbol[] -def ProcessDocSymbolTable(docSymbolTable: list>, - symbolTypeTable: dict>>, - symbolLineTable: list>) - var symbolType: string - var name: string - var r: dict> - var symInfo: dict - var symbolDetail: string - var childSymbols: dict>> - - for syminfo in docSymbolTable - name = syminfo.name - symbolType = symbol.SymbolKindToName(syminfo.kind) - r = syminfo.range - if syminfo->has_key('detail') - symbolDetail = syminfo.detail - endif - if !symbolTypeTable->has_key(symbolType) - symbolTypeTable[symbolType] = [] - endif - childSymbols = {} - if syminfo->has_key('children') - ProcessDocSymbolTable(syminfo.children, childSymbols, symbolLineTable) - endif - symInfo = {name: name, range: r, detail: symbolDetail, - children: childSymbols} - symbolTypeTable[symbolType]->add(symInfo) - symbolLineTable->add(symInfo) - endfor -enddef - -# process the 'textDocument/documentSymbol' reply from the LSP server -# Open a symbols window and display the symbols as a tree -# Result: DocumentSymbol[] | SymbolInformation[] | null -def ProcessDocSymbolReply(lspserver: dict, req: dict, reply: dict): void - var fname: string - var symbolTypeTable: dict>> = {} - var symbolLineTable: list> = [] - - if req.params.textDocument.uri != '' - fname = util.LspUriToFile(req.params.textDocument.uri) - endif - - if reply.result->empty() - # No symbols defined for this file. Clear the outline window. - outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable) - return - endif - - if reply.result[0]->has_key('location') - # SymbolInformation[] - ProcessSymbolInfoTable(reply.result, symbolTypeTable, symbolLineTable) - else - # DocumentSymbol[] - ProcessDocSymbolTable(reply.result, symbolTypeTable, symbolLineTable) - endif - - # sort the symbols by line number - symbolLineTable->sort((a, b) => a.range.start.line - b.range.start.line) - outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable) -enddef - -# process the 'textDocument/codeAction' reply from the LSP server -# Result: (Command | CodeAction)[] | null -def ProcessCodeActionReply(lspserver: dict, req: dict, reply: dict) - if reply.result->empty() - # no action can be performed - util.WarnMsg('No code action is available') - return - endif - - codeaction.ApplyCodeAction(lspserver, reply.result) -enddef - -# Reply: 'textDocument/foldingRange' -# Result: FoldingRange[] | null -def ProcessFoldingRangeReply(lspserver: dict, req: dict, reply: dict) - if reply.result->empty() - return - endif - - # result: FoldingRange[] - var end_lnum: number - var last_lnum: number = line('$') - for foldRange in reply.result - end_lnum = foldRange.endLine + 1 - if end_lnum < foldRange.startLine + 2 - end_lnum = foldRange.startLine + 2 - endif - exe $':{foldRange.startLine + 2}, {end_lnum}fold' - # Open all the folds, otherwise the subsequently created folds are not - # correct. - :silent! foldopen! - endfor - - if &foldcolumn == 0 - :setlocal foldcolumn=2 - endif -enddef - -# process the 'workspace/executeCommand' reply from the LSP server -# Result: any | null -def ProcessWorkspaceExecuteReply(lspserver: dict, req: dict, reply: dict) - if reply.result->empty() - return - endif - - # Nothing to do for the reply -enddef # Process various reply messages from the LSP server export def ProcessReply(lspserver: dict, req: dict, reply: dict): void - var lsp_reply_handlers: dict = - { - 'initialize': ProcessInitializeReply, - 'textDocument/completion': ProcessCompletionReply, - 'completionItem/resolve': ProcessResolveReply, - 'textDocument/documentHighlight': ProcessDocHighlightReply, - 'textDocument/documentSymbol': ProcessDocSymbolReply, - 'textDocument/codeAction': ProcessCodeActionReply, - 'textDocument/foldingRange': ProcessFoldingRangeReply, - 'workspace/executeCommand': ProcessWorkspaceExecuteReply, - } - - if lsp_reply_handlers->has_key(req.method) - lsp_reply_handlers[req.method](lspserver, req, reply) - else - util.ErrMsg($'Error: Unsupported reply received from LSP server: {reply->string()} for request: {req->string()}') - endif + util.ErrMsg($'Error: Unsupported reply received from LSP server: {reply->string()} for request: {req->string()}') enddef # process a diagnostic notification message from the LSP server diff --git a/autoload/lsp/lsp.vim b/autoload/lsp/lsp.vim index 373ff05..919df56 100644 --- a/autoload/lsp/lsp.vim +++ b/autoload/lsp/lsp.vim @@ -61,15 +61,15 @@ def CurbufGetServerChecked(): dict var lspserver: dict = buf.CurbufGetServer() if lspserver->empty() - util.ErrMsg($'Error: LSP server for "{fname}" is not found') + util.ErrMsg($'Error: Language server not found for "{&filetype}" file type') return {} endif if !lspserver.running - util.ErrMsg($'Error: LSP server for "{fname}" is not running') + util.ErrMsg($'Error: Language server not running for "{&filetype}" file type') return {} endif if !lspserver.ready - util.ErrMsg($'Error: LSP server for "{fname}" is not ready') + util.ErrMsg($'Error: Language server not ready for "{&filetype}" file type') return {} endif @@ -312,8 +312,7 @@ def BufferInit(bnr: number): void exe $'autocmd InsertLeave call LspLeftInsertMode()' if opt.lspOptions.autoHighlight && - lspserver.caps->has_key('documentHighlightProvider') - && lspserver.caps.documentHighlightProvider + lspserver.caps->get('documentHighlightProvider', false) # Highlight all the occurrences of the current keyword exe $'autocmd CursorMoved call LspDocHighlightClear() | call LspDocHighlight()' endif diff --git a/autoload/lsp/lspserver.vim b/autoload/lsp/lspserver.vim index 6ef13f6..de7b22c 100644 --- a/autoload/lsp/lspserver.vim +++ b/autoload/lsp/lspserver.vim @@ -13,6 +13,7 @@ import './symbol.vim' import './textedit.vim' import './hover.vim' import './signature.vim' +import './codeaction.vim' import './callhierarchy.vim' as callhier # LSP server standard output handler @@ -81,12 +82,39 @@ def StartServer(lspserver: dict): number return 0 enddef +# process the 'initialize' method reply from the LSP server +# Result: InitializeResult +def ServerInitReply(lspserver: dict, initResult: dict): void + if initResult->empty() + return + endif + + var caps: dict = initResult.capabilities + lspserver.caps = caps + + if opt.lspOptions.autoComplete && caps->has_key('completionProvider') + lspserver.completionTriggerChars = caps.completionProvider->get('triggerCharacters', []) + lspserver.completionLazyDoc = + lspserver.caps.completionProvider->get('resolveProvider', false) + endif + + # send a "initialized" notification to server + lspserver.sendInitializedNotif() + lspserver.ready = true + if exists($'#User#LspServerReady{lspserver.name}') + exe $'doautocmd User LspServerReady{lspserver.name}' + endif + + # if the outline window is opened, then request the symbols for the current + # buffer + if bufwinid('LSP-Outline') != -1 + lspserver.getDocSymbols(@%) + endif +enddef + # Request: 'initialize' # Param: InitializeParams -# def InitServer(lspserver: dict) - var req = lspserver.createRequest('initialize') - # client capabilities (ClientCapabilities) var clientCaps: dict = { workspace: { @@ -134,12 +162,8 @@ def InitServer(lspserver: dict) if !empty(lspserver.initializationOptions) initparams.initializationOptions = lspserver.initializationOptions endif - req.params->extend(initparams) - lspserver.sendMessage(req) - if lspserver.syncInit - lspserver.waitForResponse(req) - endif + lspserver.rpc_a('initialize', initparams, ServerInitReply) enddef # Send a "initialized" LSP notification @@ -486,12 +510,151 @@ 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 + +# 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 + 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') + # 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 def GetCompletion(lspserver: dict, triggerKind_arg: number, triggerChar: string): void # Check whether LSP server supports completion - if !lspserver.caps->has_key('completionProvider') + if !lspserver.caps->get('completionProvider', false) util.ErrMsg("Error: LSP server does not support completion") return endif @@ -501,18 +664,89 @@ def GetCompletion(lspserver: dict, triggerKind_arg: number, triggerChar: st return endif - var req = lspserver.createRequest('textDocument/completion') - # interface CompletionParams # interface TextDocumentPositionParams - req.params = GetLspTextDocPosition() + var params = GetLspTextDocPosition() # interface CompletionContext - req.params.context = {triggerKind: triggerKind_arg, triggerCharacter: triggerChar} + params.context = {triggerKind: triggerKind_arg, triggerCharacter: triggerChar} - lspserver.sendMessage(req) - if get(g:, 'LSPTest') - # When running LSP tests, make this a synchronous call - lspserver.waitForResponse(req) + 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 = 'markdown' + 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() + infoKind->setbufvar(bufnr, '&ft') + if infoKind == 'markdown' + 3->setwinvar(id, '&conceallevel') + else + 0->setwinvar(id, '&conceallevel') + endif + id->popup_settext(infoText) + id->popup_show() endif enddef @@ -521,23 +755,16 @@ enddef # Param: CompletionItem def ResolveCompletion(lspserver: dict, item: dict): void # Check whether LSP server supports completion item resolve - if !lspserver.caps->has_key('completionProvider') - || !lspserver.caps.completionProvider->has_key('resolveProvider') - || !lspserver.caps.completionProvider.resolveProvider + if !lspserver.caps->get('completionProvider', false) + || !lspserver.caps.completionProvider->get('resolveProvider', false) util.ErrMsg("Error: LSP server does not support completion item resolve") return endif - var req = lspserver.createRequest('completionItem/resolve') - # interface CompletionItem - req.params = item + var params = item - lspserver.sendMessage(req) - if get(g:, 'LSPTest') - # When running LSP tests, make this a synchronous call - lspserver.waitForResponse(req) - endif + lspserver.rpc_a('completionItem/resolve', params, CompletionResolveReply) enddef # Jump to or peek a symbol location. @@ -593,8 +820,7 @@ enddef # Param: DefinitionParams def GotoDefinition(lspserver: dict, peek: bool) # Check whether LSP server supports jumping to a definition - if !lspserver.caps->has_key('definitionProvider') - || !lspserver.caps.definitionProvider + if !lspserver.caps->get('definitionProvider', false) util.ErrMsg("Error: Jumping to a symbol definition is not supported") return endif @@ -608,8 +834,7 @@ enddef # Param: DeclarationParams def GotoDeclaration(lspserver: dict, peek: bool): void # Check whether LSP server supports jumping to a declaration - if !lspserver.caps->has_key('declarationProvider') - || !lspserver.caps.declarationProvider + if !lspserver.caps->get('declarationProvider', false) util.ErrMsg("Error: Jumping to a symbol declaration is not supported") return endif @@ -623,8 +848,7 @@ enddef # Param: TypeDefinitionParams def GotoTypeDef(lspserver: dict, peek: bool): void # Check whether LSP server supports jumping to a type definition - if !lspserver.caps->has_key('typeDefinitionProvider') - || !lspserver.caps.typeDefinitionProvider + if !lspserver.caps->get('typeDefinitionProvider', false) util.ErrMsg("Error: Jumping to a symbol type definition is not supported") return endif @@ -638,8 +862,7 @@ enddef # Param: ImplementationParams def GotoImplementation(lspserver: dict, peek: bool): void # Check whether LSP server supports jumping to a implementation - if !lspserver.caps->has_key('implementationProvider') - || !lspserver.caps.implementationProvider + if !lspserver.caps->get('implementationProvider', false) util.ErrMsg("Error: Jumping to a symbol implementation is not supported") return endif @@ -677,7 +900,7 @@ enddef # Param: SignatureHelpParams def ShowSignature(lspserver: dict): void # Check whether LSP server supports signature help - if !lspserver.caps->has_key('signatureHelpProvider') + if !lspserver.caps->get('signatureHelpProvider', false) util.ErrMsg("Error: LSP server does not support signature help") return endif @@ -710,8 +933,7 @@ enddef # Param: HoverParams def ShowHoverInfo(lspserver: dict): void # Check whether LSP server supports getting hover information - if !lspserver.caps->has_key('hoverProvider') - || !lspserver.caps.hoverProvider + if !lspserver.caps->get('hoverProvider', false) return endif @@ -725,8 +947,7 @@ enddef # Param: ReferenceParams def ShowReferences(lspserver: dict, peek: bool): void # Check whether LSP server supports getting reference information - if !lspserver.caps->has_key('referencesProvider') - || !lspserver.caps.referencesProvider + if !lspserver.caps->get('referencesProvider', false) util.ErrMsg("Error: LSP server does not support showing references") return endif @@ -751,46 +972,65 @@ def ShowReferences(lspserver: dict, peek: bool): void symbol.ShowReferences(lspserver, reply.result, peek) enddef +# process the 'textDocument/documentHighlight' reply from the LSP server +# Result: DocumentHighlight[] | null +def DocHighlightReply(bnr: number, lspserver: dict, docHighlightReply: any): void + if docHighlightReply->empty() + return + endif + + for docHL in docHighlightReply + var kind: number = docHL->get('kind', 1) + var propName: string + if kind == 2 + # Read-access + propName = 'LspReadRef' + elseif kind == 3 + # Write-access + propName = 'LspWriteRef' + else + # textual reference + propName = 'LspTextRef' + endif + prop_add(docHL.range.start.line + 1, + util.GetLineByteFromPos(bnr, docHL.range.start) + 1, + {end_lnum: docHL.range.end.line + 1, + end_col: util.GetLineByteFromPos(bnr, docHL.range.end) + 1, + bufnr: bnr, + type: propName}) + endfor +enddef + # Request: "textDocument/documentHighlight" # Param: DocumentHighlightParams def DocHighlight(lspserver: dict): void # Check whether LSP server supports getting highlight information - if !lspserver.caps->has_key('documentHighlightProvider') - || !lspserver.caps.documentHighlightProvider + if !lspserver.caps->get('documentHighlightProvider', false) util.ErrMsg("Error: LSP server does not support document highlight") return endif - var req = lspserver.createRequest('textDocument/documentHighlight') # interface DocumentHighlightParams # interface TextDocumentPositionParams - req.params->extend(GetLspTextDocPosition()) - lspserver.sendMessage(req) - if get(g:, 'LSPTest') - # When running LSP tests, make this a synchronous call - lspserver.waitForResponse(req) - endif + var params = GetLspTextDocPosition() + lspserver.rpc_a('textDocument/documentHighlight', params, + function('DocHighlightReply', [bufnr()])) enddef # Request: "textDocument/documentSymbol" # Param: DocumentSymbolParams def GetDocSymbols(lspserver: dict, fname: string): void # Check whether LSP server supports getting document symbol information - if !lspserver.caps->has_key('documentSymbolProvider') - || !lspserver.caps.documentSymbolProvider + if !lspserver.caps->get('documentSymbolProvider', false) util.ErrMsg("Error: LSP server does not support getting list of symbols") return endif - var req = lspserver.createRequest('textDocument/documentSymbol') # interface DocumentSymbolParams # interface TextDocumentIdentifier - req.params->extend({textDocument: {uri: util.LspFileToUri(fname)}}) - lspserver.sendMessage(req) - if get(g:, 'LSPTest') - # When running LSP tests, make this a synchronous call - lspserver.waitForResponse(req) - endif + var params = {textDocument: {uri: util.LspFileToUri(fname)}} + lspserver.rpc_a('textDocument/documentSymbol', params, + function(symbol.DocSymbolReply, [fname])) enddef # Request: "textDocument/formatting" @@ -801,8 +1041,7 @@ enddef def TextDocFormat(lspserver: dict, fname: string, rangeFormat: bool, start_lnum: number, end_lnum: number) # Check whether LSP server supports formatting documents - if !lspserver.caps->has_key('documentFormattingProvider') - || !lspserver.caps.documentFormattingProvider + if !lspserver.caps->get('documentFormattingProvider', false) util.ErrMsg("Error: LSP server does not support formatting documents") return endif @@ -884,8 +1123,7 @@ enddef # Request: "callHierarchy/incomingCalls" def IncomingCalls(lspserver: dict, fname: string) # Check whether LSP server supports call hierarchy - if !lspserver.caps->has_key('callHierarchyProvider') - || !lspserver.caps.callHierarchyProvider + if !lspserver.caps->get('callHierarchyProvider', false) util.ErrMsg("Error: LSP server does not support call hierarchy") return endif @@ -912,8 +1150,7 @@ enddef # Request: "callHierarchy/outgoingCalls" def OutgoingCalls(lspserver: dict, fname: string) # Check whether LSP server supports call hierarchy - if !lspserver.caps->has_key('callHierarchyProvider') - || !lspserver.caps.callHierarchyProvider + if !lspserver.caps->get('callHierarchyProvider', false) util.ErrMsg("Error: LSP server does not support call hierarchy") return endif @@ -941,8 +1178,7 @@ enddef # Param: RenameParams def RenameSymbol(lspserver: dict, newName: string) # Check whether LSP server supports rename operation - if !lspserver.caps->has_key('renameProvider') - || !lspserver.caps.renameProvider + if !lspserver.caps->get('renameProvider', false) util.ErrMsg("Error: LSP server does not support rename operation") return endif @@ -969,34 +1205,37 @@ enddef # Param: CodeActionParams def CodeAction(lspserver: dict, fname_arg: string) # Check whether LSP server supports code action operation - if !lspserver.caps->has_key('codeActionProvider') - || !lspserver.caps.codeActionProvider + if !lspserver.caps->get('codeActionProvider', false) util.ErrMsg("Error: LSP server does not support code action operation") return endif - var req = lspserver.createRequest('textDocument/codeAction') - # interface CodeActionParams + var params: dict = {} var fname: string = fnamemodify(fname_arg, ':p') var bnr: number = fname_arg->bufnr() var r: dict> = { start: {line: line('.') - 1, character: charcol('.') - 1}, end: {line: line('.') - 1, character: charcol('.') - 1}} - req.params->extend({textDocument: {uri: util.LspFileToUri(fname)}, range: r}) + params->extend({textDocument: {uri: util.LspFileToUri(fname)}, range: r}) var d: list> = [] var lnum = line('.') var diagInfo: dict = diag.GetDiagByLine(lspserver, bnr, lnum) if !diagInfo->empty() d->add(diagInfo) endif - req.params->extend({context: {diagnostics: d}}) + params->extend({context: {diagnostics: d}}) - lspserver.sendMessage(req) - if get(g:, 'LSPTest') - # When running LSP tests, make this a synchronous call - lspserver.waitForResponse(req) + var reply = lspserver.rpc('textDocument/codeAction', params) + + # Result: (Command | CodeAction)[] | null + if reply->empty() || reply.result->empty() + # no action can be performed + util.WarnMsg('No code action is available') + return endif + + codeaction.ApplyCodeAction(lspserver, reply.result) enddef # List project-wide symbols matching query string @@ -1004,8 +1243,7 @@ enddef # Param: WorkspaceSymbolParams def WorkspaceQuerySymbols(lspserver: dict, query: string) # Check whether the LSP server supports listing workspace symbols - if !lspserver.caps->has_key('workspaceSymbolProvider') - || !lspserver.caps.workspaceSymbolProvider + if !lspserver.caps->get('workspaceSymbolProvider', false) util.ErrMsg("Error: LSP server does not support listing workspace symbols") return endif @@ -1080,8 +1318,7 @@ enddef # Param: SelectionRangeParams def SelectionRange(lspserver: dict, fname: string) # Check whether LSP server supports selection ranges - if !lspserver.caps->has_key('selectionRangeProvider') - || !lspserver.caps.selectionRangeProvider + if !lspserver.caps->get('selectionRangeProvider', false) util.ErrMsg("Error: LSP server does not support selection ranges") return endif @@ -1107,8 +1344,7 @@ enddef # Expand the previous selection or start a new one def SelectionExpand(lspserver: dict) # Check whether LSP server supports selection ranges - if !lspserver.caps->has_key('selectionRangeProvider') - || !lspserver.caps.selectionRangeProvider + if !lspserver.caps->get('selectionRangeProvider', false) util.ErrMsg("Error: LSP server does not support selection ranges") return endif @@ -1119,8 +1355,7 @@ enddef # Shrink the previous selection or start a new one def SelectionShrink(lspserver: dict) # Check whether LSP server supports selection ranges - if !lspserver.caps->has_key('selectionRangeProvider') - || !lspserver.caps.selectionRangeProvider + if !lspserver.caps->get('selectionRangeProvider', false) util.ErrMsg("Error: LSP server does not support selection ranges") return endif @@ -1133,34 +1368,50 @@ enddef # Param: FoldingRangeParams def FoldRange(lspserver: dict, fname: string) # Check whether LSP server supports fold ranges - if !lspserver.caps->has_key('foldingRangeProvider') - || !lspserver.caps.foldingRangeProvider + if !lspserver.caps->get('foldingRangeProvider', false) util.ErrMsg("Error: LSP server does not support folding") return endif - var req = lspserver.createRequest('textDocument/foldingRange') # interface FoldingRangeParams # interface TextDocumentIdentifier - req.params->extend({textDocument: {uri: util.LspFileToUri(fname)}}) - lspserver.sendMessage(req) - if get(g:, 'LSPTest') - # When running LSP tests, make this a synchronous call - lspserver.waitForResponse(req) + var params = {textDocument: {uri: util.LspFileToUri(fname)}} + var reply = lspserver.rpc('textDocument/foldingRange', params) + if reply->empty() || reply.result->empty() + return + endif + + # result: FoldingRange[] + var end_lnum: number + var last_lnum: number = line('$') + for foldRange in reply.result + end_lnum = foldRange.endLine + 1 + if end_lnum < foldRange.startLine + 2 + end_lnum = foldRange.startLine + 2 + endif + exe $':{foldRange.startLine + 2}, {end_lnum}fold' + # Open all the folds, otherwise the subsequently created folds are not + # correct. + :silent! foldopen! + endfor + + if &foldcolumn == 0 + :setlocal foldcolumn=2 endif enddef +# process the 'workspace/executeCommand' reply from the LSP server +# Result: any | null +def WorkspaceExecuteReply(lspserver: dict, execReply: any) + # Nothing to do for the reply +enddef + # Request the LSP server to execute a command # Request: workspace/executeCommand # Params: ExecuteCommandParams def ExecuteCommand(lspserver: dict, cmd: dict) - var req = lspserver.createRequest('workspace/executeCommand') - req.params->extend(cmd) - lspserver.sendMessage(req) - if get(g:, 'LSPTest') - # When running LSP tests, make this a synchronous call - lspserver.waitForResponse(req) - endif + var params = cmd + lspserver.rpc_a('workspace/executeCommand', params, WorkspaceExecuteReply) enddef # Display the LSP server capabilities (received during the initialization @@ -1179,8 +1430,7 @@ enddef # symbol definition or the symbol is not defined. def TagFunc(lspserver: dict, pat: string, flags: string, info: dict): any # Check whether LSP server supports getting the location of a definition - if !lspserver.caps->has_key('definitionProvider') - || !lspserver.caps.definitionProvider + if !lspserver.caps->get('definitionProvider', false) return null endif diff --git a/autoload/lsp/symbol.vim b/autoload/lsp/symbol.vim index c0ded0a..b5e4886 100644 --- a/autoload/lsp/symbol.vim +++ b/autoload/lsp/symbol.vim @@ -8,6 +8,7 @@ vim9script import './options.vim' as opt import './util.vim' +import './outline.vim' # Handle keys pressed when the workspace symbol popup menu is displayed def FilterSymbols(lspserver: dict, popupID: number, key: string): bool @@ -351,4 +352,92 @@ export def TagFunc(lspserver: dict, return retval enddef +# process SymbolInformation[] +def ProcessSymbolInfoTable(symbolInfoTable: list>, + symbolTypeTable: dict>>, + symbolLineTable: list>) + var fname: string + var symbolType: string + var name: string + var r: dict> + var symInfo: dict + + for syminfo in symbolInfoTable + fname = util.LspUriToFile(syminfo.location.uri) + symbolType = SymbolKindToName(syminfo.kind) + name = syminfo.name + if syminfo->has_key('containerName') + if syminfo.containerName != '' + name ..= $' [{syminfo.containerName}]' + endif + endif + r = syminfo.location.range + + if !symbolTypeTable->has_key(symbolType) + symbolTypeTable[symbolType] = [] + endif + symInfo = {name: name, range: r} + symbolTypeTable[symbolType]->add(symInfo) + symbolLineTable->add(symInfo) + endfor +enddef + +# process DocumentSymbol[] +def ProcessDocSymbolTable(docSymbolTable: list>, + symbolTypeTable: dict>>, + symbolLineTable: list>) + var symbolType: string + var name: string + var r: dict> + var symInfo: dict + var symbolDetail: string + var childSymbols: dict>> + + for syminfo in docSymbolTable + name = syminfo.name + symbolType = SymbolKindToName(syminfo.kind) + r = syminfo.range + if syminfo->has_key('detail') + symbolDetail = syminfo.detail + endif + if !symbolTypeTable->has_key(symbolType) + symbolTypeTable[symbolType] = [] + endif + childSymbols = {} + if syminfo->has_key('children') + ProcessDocSymbolTable(syminfo.children, childSymbols, symbolLineTable) + endif + symInfo = {name: name, range: r, detail: symbolDetail, + children: childSymbols} + symbolTypeTable[symbolType]->add(symInfo) + symbolLineTable->add(symInfo) + endfor +enddef + +# process the 'textDocument/documentSymbol' reply from the LSP server +# Open a symbols window and display the symbols as a tree +# Result: DocumentSymbol[] | SymbolInformation[] | null +export def DocSymbolReply(fname: string, lspserver: dict, docsymbol: any) + var symbolTypeTable: dict>> = {} + var symbolLineTable: list> = [] + + if docsymbol->empty() + # No symbols defined for this file. Clear the outline window. + outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable) + return + endif + + if docsymbol[0]->has_key('location') + # SymbolInformation[] + ProcessSymbolInfoTable(docsymbol, symbolTypeTable, symbolLineTable) + else + # DocumentSymbol[] + ProcessDocSymbolTable(docsymbol, symbolTypeTable, symbolLineTable) + endif + + # sort the symbols by line number + symbolLineTable->sort((a, b) => a.range.start.line - b.range.start.line) + outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable) +enddef + # vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/plugin/lsp.vim b/plugin/lsp.vim index c9a15a7..d1d54b3 100644 --- a/plugin/lsp.vim +++ b/plugin/lsp.vim @@ -18,10 +18,13 @@ def g:LspAddServer(serverList: list>) lsp.AddServer(serverList) enddef +# Returns true if the language server for the current buffer is initialized +# and ready to accept requests. def g:LspServerReady(): bool return lsp.ServerReady() enddef +# Returns true if the language server for 'ftype' file type is running def g:LspServerRunning(ftype: string): bool return lsp.ServerRunning(ftype) enddef diff --git a/test/unit_tests.vim b/test/unit_tests.vim index f59c671..aae2a6e 100644 --- a/test/unit_tests.vim +++ b/test/unit_tests.vim @@ -173,8 +173,8 @@ def Test_LspFormat() assert_equal('', execute('LspFormat')) # file without an LSP server - edit a.b - assert_equal(['Error: LSP server for "a.b" is not found'], + edit a.raku + assert_equal(['Error: Language server not found for "raku" file type'], execute('LspFormat')->split("\n")) :%bw! @@ -224,8 +224,8 @@ def Test_LspShowReferences() assert_equal('', execute('LspShowReferences')) # file without an LSP server - edit a.b - assert_equal(['Error: LSP server for "a.b" is not found'], + edit a.raku + assert_equal(['Error: Language server not found for "raku" file type'], execute('LspShowReferences')->split("\n")) :%bw! @@ -342,8 +342,8 @@ def Test_LspCodeAction() assert_equal('', execute('LspCodeAction')) # file without an LSP server - edit a.b - assert_equal(['Error: LSP server for "a.b" is not found'], + edit a.raku + assert_equal(['Error: Language server not found for "raku" file type'], execute('LspCodeAction')->split("\n")) :%bw! @@ -393,8 +393,8 @@ def Test_LspRename() assert_equal('', execute('LspRename')) # file without an LSP server - edit a.b - assert_equal(['Error: LSP server for "a.b" is not found'], + edit a.raku + assert_equal(['Error: Language server not found for "raku" file type'], execute('LspRename')->split("\n")) :%bw! @@ -493,8 +493,8 @@ def Test_LspSelection() assert_equal('', execute('LspSelectionExpand')) # file without an LSP server - edit a.b - assert_equal(['Error: LSP server for "a.b" is not found'], + edit a.raku + assert_equal(['Error: Language server not found for "raku" file type'], execute('LspSelectionExpand')->split("\n")) :%bw! @@ -573,12 +573,12 @@ def Test_LspGotoDefinition() assert_equal('', execute('LspGotoImpl')) # file without an LSP server - edit a.b - assert_equal(['Error: LSP server for "a.b" is not found'], + edit a.raku + assert_equal(['Error: Language server not found for "raku" file type'], execute('LspGotoDefinition')->split("\n")) - assert_equal(['Error: LSP server for "a.b" is not found'], + assert_equal(['Error: Language server not found for "raku" file type'], execute('LspGotoDeclaration')->split("\n")) - assert_equal(['Error: LSP server for "a.b" is not found'], + assert_equal(['Error: Language server not found for "raku" file type'], execute('LspGotoImpl')->split("\n")) :%bw! -- 2.48.1