From 97da893637254590715bdd3f0726f6cd23999a8d Mon Sep 17 00:00:00 2001 From: Yegappan Lakshmanan Date: Mon, 17 Oct 2022 21:26:44 -0700 Subject: [PATCH] Use sync RPC for workspace symbol search --- README.md | 2 +- autoload/lsp/handlers.vim | 80 +------------------------------------ autoload/lsp/lsp.vim | 6 +-- autoload/lsp/lspserver.vim | 19 +++++---- autoload/lsp/symbol.vim | 82 +++++++++++++++++++++++++++++++++++++- doc/lsp.txt | 8 ++-- test/unit_tests.vim | 38 ++++++++++++++++++ 7 files changed, 140 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index 8a94468..edc5ac1 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The following language server protocol (LSP) features are supported: * Display code outline * Rename symbol * Display type and documentation on hover -* Inlay hints +* Signature help * Code action * Formatting code * Folding code diff --git a/autoload/lsp/handlers.vim b/autoload/lsp/handlers.vim index bdb2a5b..48cf696 100644 --- a/autoload/lsp/handlers.vim +++ b/autoload/lsp/handlers.vim @@ -284,19 +284,6 @@ def ProcessDocHighlightReply(lspserver: dict, req: dict, reply: dict = ['', 'File', 'Module', 'Namespace', 'Package', - 'Class', 'Method', 'Property', 'Field', 'Constructor', 'Enum', - 'Interface', 'Function', 'Variable', 'Constant', 'String', 'Number', - 'Boolean', 'Array', 'Object', 'Key', 'Null', 'EnumMember', 'Struct', - 'Event', 'Operator', 'TypeParameter'] - if symkind > 26 - return '' - endif - return symbolMap[symkind] -enddef - # process SymbolInformation[] def ProcessSymbolInfoTable(symbolInfoTable: list>, symbolTypeTable: dict>>, @@ -309,7 +296,7 @@ def ProcessSymbolInfoTable(symbolInfoTable: list>, for symbol in symbolInfoTable fname = util.LspUriToFile(symbol.location.uri) - symbolType = LspSymbolKindToName(symbol.kind) + symbolType = symbol.SymbolKindToName(symbol.kind) name = symbol.name if symbol->has_key('containerName') if symbol.containerName != '' @@ -340,7 +327,7 @@ def ProcessDocSymbolTable(docSymbolTable: list>, for symbol in docSymbolTable name = symbol.name - symbolType = LspSymbolKindToName(symbol.kind) + symbolType = symbol.SymbolKindToName(symbol.kind) r = symbol.range if symbol->has_key('detail') symbolDetail = symbol.detail @@ -438,68 +425,6 @@ def ProcessWorkspaceExecuteReply(lspserver: dict, req: dict, reply: di # Nothing to do for the reply enddef -# Convert a file name () format. -# Make sure the popup does't occupy the entire screen by reducing the width. -def MakeMenuName(popupWidth: number, fname: string): string - var filename: string = fname->fnamemodify(':t') - var flen: number = filename->len() - var dirname: string = fname->fnamemodify(':h') - - if fname->len() > popupWidth && flen < popupWidth - # keep the full file name and reduce directory name length - # keep some characters at the beginning and end (equally). - # 6 spaces are used for "..." and " ()" - var dirsz = (popupWidth - flen - 6) / 2 - dirname = dirname[: dirsz] .. '...' .. dirname[-dirsz : ] - endif - var str: string = filename - if dirname != '.' - str ..= $' ({dirname}/)' - endif - return str -enddef - -# process the 'workspace/symbol' reply from the LSP server -# Result: SymbolInformation[] | null -def ProcessWorkspaceSymbolReply(lspserver: dict, req: dict, reply: dict) - var symbols: list> = [] - var symbolType: string - var fileName: string - var r: dict> - var symName: string - - if reply.result->empty() - return - endif - - for symbol in reply.result - if !symbol->has_key('location') - # ignore entries without location information - continue - endif - - # interface SymbolInformation - fileName = util.LspUriToFile(symbol.location.uri) - r = symbol.location.range - - symName = symbol.name - if symbol->has_key('containerName') && symbol.containerName != '' - symName = $'{symbol.containerName}::{symName}' - endif - symName ..= $' [{LspSymbolKindToName(symbol.kind)}]' - symName ..= ' ' .. MakeMenuName( - lspserver.workspaceSymbolPopup->popup_getpos().core_width, - fileName) - - symbols->add({name: symName, - file: fileName, - pos: r.start}) - endfor - symbols->setwinvar(lspserver.workspaceSymbolPopup, 'LspSymbolTable') - lspserver.workspaceSymbolPopup->popup_settext( - symbols->copy()->mapnew('v:val.name')) -enddef - # Process various reply messages from the LSP server export def ProcessReply(lspserver: dict, req: dict, reply: dict): void var lsp_reply_handlers: dict = @@ -513,7 +438,6 @@ export def ProcessReply(lspserver: dict, req: dict, reply: dict): 'textDocument/codeAction': ProcessCodeActionReply, 'textDocument/foldingRange': ProcessFoldingRangeReply, 'workspace/executeCommand': ProcessWorkspaceExecuteReply, - 'workspace/symbol': ProcessWorkspaceSymbolReply, } if lsp_reply_handlers->has_key(req.method) diff --git a/autoload/lsp/lsp.vim b/autoload/lsp/lsp.vim index 199a915..6fd28e2 100644 --- a/autoload/lsp/lsp.vim +++ b/autoload/lsp/lsp.vim @@ -764,11 +764,7 @@ export def SymbolSearch(queryArg: string) endif redraw! - symbol.ShowSymbolMenu(lspserver, query) - - if !lspserver.workspaceQuery(query) - lspserver.workspaceSymbolPopup->popup_close() - endif + lspserver.workspaceQuery(query) enddef # Display the list of workspace folders diff --git a/autoload/lsp/lspserver.vim b/autoload/lsp/lspserver.vim index 82a9e7f..e4fc267 100644 --- a/autoload/lsp/lspserver.vim +++ b/autoload/lsp/lspserver.vim @@ -917,19 +917,24 @@ enddef # List project-wide symbols matching query string # Request: "workspace/symbol" # Param: WorkspaceSymbolParams -def WorkspaceQuerySymbols(lspserver: dict, query: string): bool +def WorkspaceQuerySymbols(lspserver: dict, query: string) # Check whether the LSP server supports listing workspace symbols if !lspserver.caps->has_key('workspaceSymbolProvider') || !lspserver.caps.workspaceSymbolProvider util.ErrMsg("Error: LSP server does not support listing workspace symbols") - return false + return endif - var req = lspserver.createRequest('workspace/symbol') - req.params->extend({query: query}) - lspserver.sendMessage(req) + # Param: WorkspaceSymbolParams + var param = {} + param.query = query + var reply = lspserver.rpc('workspace/symbol', param) + if reply->empty() || reply.result->empty() + util.WarnMsg($'Error: Symbol "{query}" is not found') + return + endif - return true + symbol.WorkspaceSymbolPopup(lspserver, query, reply.result) enddef # Add a workspace folder to the LSP server. @@ -1099,7 +1104,7 @@ export def NewLspServer(path: string, args: list, isSync: bool, initiali completionTriggerChars: [], signaturePopup: -1, diagsMap: {}, - workspaceSymbolPopup: 0, + workspaceSymbolPopup: -1, workspaceSymbolQuery: '', callHierarchyType: '', selection: {} diff --git a/autoload/lsp/symbol.vim b/autoload/lsp/symbol.vim index 57e9809..ae293dc 100644 --- a/autoload/lsp/symbol.vim +++ b/autoload/lsp/symbol.vim @@ -103,6 +103,9 @@ def JumpToWorkspaceSymbol(popupID: number, result: number): void else winList[0]->win_gotoid() endif + # Set the previous cursor location mark. Instead of using setpos(), m' is + # used so that the current location is added to the jump list. + normal m' setcursorcharpos(symTbl[result - 1].pos.line + 1, symTbl[result - 1].pos.character + 1) catch @@ -111,7 +114,7 @@ def JumpToWorkspaceSymbol(popupID: number, result: number): void enddef # display a list of symbols from the workspace -export def ShowSymbolMenu(lspserver: dict, query: string) +def ShowSymbolMenu(lspserver: dict, query: string) # Create the popup menu var lnum = &lines - &cmdheight - 2 - 10 var popupAttr = { @@ -138,6 +141,83 @@ export def ShowSymbolMenu(lspserver: dict, query: string) echo $'Symbol: {query}' enddef +# Convert a file name to () format. +# Make sure the popup does't occupy the entire screen by reducing the width. +def MakeMenuName(popupWidth: number, fname: string): string + var filename: string = fname->fnamemodify(':t') + var flen: number = filename->len() + var dirname: string = fname->fnamemodify(':h') + + if fname->len() > popupWidth && flen < popupWidth + # keep the full file name and reduce directory name length + # keep some characters at the beginning and end (equally). + # 6 spaces are used for "..." and " ()" + var dirsz = (popupWidth - flen - 6) / 2 + dirname = dirname[: dirsz] .. '...' .. dirname[-dirsz : ] + endif + var str: string = filename + if dirname != '.' + str ..= $' ({dirname}/)' + endif + return str +enddef + +# process the 'workspace/symbol' reply from the LSP server +# Result: SymbolInformation[] | null +export def WorkspaceSymbolPopup(lspserver: dict, query: string, + symInfo: list>) + var symbols: list> = [] + var symbolType: string + var fileName: string + var r: dict> + var symName: string + + # Create a symbol popup menu if it is not present + if lspserver.workspaceSymbolPopup->winbufnr() == -1 + ShowSymbolMenu(lspserver, query) + endif + + for symbol in symInfo + if !symbol->has_key('location') + # ignore entries without location information + continue + endif + + # interface SymbolInformation + fileName = util.LspUriToFile(symbol.location.uri) + r = symbol.location.range + + symName = symbol.name + if symbol->has_key('containerName') && symbol.containerName != '' + symName = $'{symbol.containerName}::{symName}' + endif + symName ..= $' [{SymbolKindToName(symbol.kind)}]' + symName ..= ' ' .. MakeMenuName( + lspserver.workspaceSymbolPopup->popup_getpos().core_width, + fileName) + + symbols->add({name: symName, + file: fileName, + pos: r.start}) + endfor + symbols->setwinvar(lspserver.workspaceSymbolPopup, 'LspSymbolTable') + lspserver.workspaceSymbolPopup->popup_settext( + symbols->copy()->mapnew('v:val.name')) +enddef + +# map the LSP symbol kind number to string +export def SymbolKindToName(symkind: number): string + var symbolMap: list = ['', 'File', 'Module', 'Namespace', 'Package', + 'Class', 'Method', 'Property', 'Field', 'Constructor', 'Enum', + 'Interface', 'Function', 'Variable', 'Constant', 'String', 'Number', + 'Boolean', 'Array', 'Object', 'Key', 'Null', 'EnumMember', 'Struct', + 'Event', 'Operator', 'TypeParameter'] + if symkind > 26 + return '' + endif + return symbolMap[symkind] +enddef + # Display or peek symbol references in a location list export def ShowReferences(lspserver: dict, refs: list>, peekSymbol: bool) if refs->empty() diff --git a/doc/lsp.txt b/doc/lsp.txt index 3d07de7..22152a2 100644 --- a/doc/lsp.txt +++ b/doc/lsp.txt @@ -472,9 +472,11 @@ diagnostic messages, you can add the following line to your .vimrc file: enter the symbol name (the keyword under the cursor is used as the default). A popup window is opened with the list of matching symbols. You can enter a few - characters to narrow down the list of matches. You can - close the popup menu by pressing the escape key or by - pressing CTRL-C. + characters to narrow down the list of matches. The + displayed symbol name can be erased by pressing + or and a new symbol search pattern + can be entered. You can close the popup menu by + pressing the escape key or by pressing CTRL-C. In the popup menu, the following keys can be used: diff --git a/test/unit_tests.vim b/test/unit_tests.vim index 3abc4e5..54cf016 100644 --- a/test/unit_tests.vim +++ b/test/unit_tests.vim @@ -672,6 +672,44 @@ def Test_LspShowSignature() :%bw! enddef +# Test for :LspSymbolSearch +def Test_LspSymbolSearch() + silent! edit Xtest.c + sleep 200m + var lines: list =<< trim END + void lsptest_funcA() + { + } + + void lsptest_funcB() + { + } + + void lsptest_funcC() + { + } + END + setline(1, lines) + :sleep 1 + + cursor(1, 1) + feedkeys(":LspSymbolSearch lsptest_funcB\\", "xt") + assert_equal([5, 6], [line('.'), col('.')]) + + cursor(1, 1) + feedkeys(":LspSymbolSearch lsptest_func\\\\", "xt") + assert_equal([9, 6], [line('.'), col('.')]) + + cursor(1, 1) + feedkeys(":LspSymbolSearch lsptest_funcA\\B\", "xt") + assert_equal([5, 6], [line('.'), col('.')]) + + var output = execute(':LspSymbolSearch lsptest_nonexist')->split("\n") + assert_equal(['Error: Symbol "lsptest_nonexist" is not found'], output) + + :%bw! +enddef + # Test for :LspIncomingCalls def Test_LspIncomingCalls() silent! edit Xtest.c -- 2.48.1