From 4ef2c81e95529d4175ba8149fbe42e856a36ab10 Mon Sep 17 00:00:00 2001 From: w0rp Date: Wed, 31 Oct 2018 16:13:22 +0000 Subject: [PATCH] Implement LSP symbol search --- README.md | 13 ++ autoload/ale/lsp.vim | 6 + autoload/ale/lsp/message.vim | 6 + autoload/ale/preview.vim | 5 +- autoload/ale/symbol.vim | 109 +++++++++++ doc/ale.txt | 19 ++ plugin/ale.vim | 3 + test/lsp/test_lsp_client_messages.vader | 11 ++ ...st_other_initialize_message_handling.vader | 6 +- test/test_symbol_search.vader | 173 ++++++++++++++++++ 10 files changed, 349 insertions(+), 2 deletions(-) create mode 100644 autoload/ale/symbol.vim create mode 100644 test/test_symbol_search.vader diff --git a/README.md b/README.md index de01c102..9442e7f1 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ other content at [w0rp.com](https://w0rp.com). 4. [Go To Definition](#usage-go-to-definition) 5. [Find References](#usage-find-references) 6. [Hovering](#usage-hover) + 7. [Symbol Search](#usage-symbol-search) 3. [Installation](#installation) 1. [Installation with Vim package management](#standard-installation) 2. [Installation with Pathogen](#installation-with-pathogen) @@ -321,6 +322,18 @@ and needs to be configured for Vim 8.1+ in terminals. See `:help ale-hover` for more information. + + +### 2.vii Symbol Search + +ALE supports searching for workspace symbols via Language Server Protocol +linters with the `ALESymbolSearch` command. + +Search queries can be performed to find functions, types, and more which are +similar to a given query string. + +See `:help ale-symbol-search` for more information. + ## 3. Installation diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index 196cbe80..b7908e74 100644 --- a/autoload/ale/lsp.vim +++ b/autoload/ale/lsp.vim @@ -41,6 +41,7 @@ function! ale#lsp#Register(executable_or_address, project, init_options) abort \ 'completion': 0, \ 'completion_trigger_characters': [], \ 'definition': 0, + \ 'symbol_search': 0, \ }, \} endif @@ -203,6 +204,10 @@ function! s:UpdateCapabilities(conn, capabilities) abort if get(a:capabilities, 'definitionProvider') is v:true let a:conn.capabilities.definition = 1 endif + + if get(a:capabilities, 'workspaceSymbolProvider') is v:true + let a:conn.capabilities.symbol_search = 1 + endif endfunction function! ale#lsp#HandleInitResponse(conn, response) abort @@ -285,6 +290,7 @@ function! ale#lsp#MarkConnectionAsTsserver(conn_id) abort let l:conn.capabilities.completion = 1 let l:conn.capabilities.completion_trigger_characters = ['.'] let l:conn.capabilities.definition = 1 + let l:conn.capabilities.symbol_search = 1 endfunction " Start a program for LSP servers. diff --git a/autoload/ale/lsp/message.vim b/autoload/ale/lsp/message.vim index 9ed41ac4..9fffb83a 100644 --- a/autoload/ale/lsp/message.vim +++ b/autoload/ale/lsp/message.vim @@ -130,6 +130,12 @@ function! ale#lsp#message#References(buffer, line, column) abort \}] endfunction +function! ale#lsp#message#Symbol(query) abort + return [0, 'workspace/symbol', { + \ 'query': a:query, + \}] +endfunction + function! ale#lsp#message#Hover(buffer, line, column) abort return [0, 'textDocument/hover', { \ 'textDocument': { diff --git a/autoload/ale/preview.vim b/autoload/ale/preview.vim index 180a37d0..1f50e0ad 100644 --- a/autoload/ale/preview.vim +++ b/autoload/ale/preview.vim @@ -46,11 +46,14 @@ function! ale#preview#ShowSelection(item_list) abort " Create lines to display to users. for l:item in a:item_list + let l:match = get(l:item, 'match', '') + call add( \ l:lines, \ l:item.filename \ . ':' . l:item.line - \ . ':' . l:item.column, + \ . ':' . l:item.column + \ . (!empty(l:match) ? ' ' . l:match : ''), \) endfor diff --git a/autoload/ale/symbol.vim b/autoload/ale/symbol.vim new file mode 100644 index 00000000..5180cb86 --- /dev/null +++ b/autoload/ale/symbol.vim @@ -0,0 +1,109 @@ +let s:symbol_map = {} + +" Used to get the symbol map in tests. +function! ale#symbol#GetMap() abort + return deepcopy(s:symbol_map) +endfunction + +" Used to set the symbol map in tests. +function! ale#symbol#SetMap(map) abort + let s:symbol_map = a:map +endfunction + +function! ale#symbol#ClearLSPData() abort + let s:symbol_map = {} +endfunction + +function! ale#symbol#HandleLSPResponse(conn_id, response) abort + if has_key(a:response, 'id') + \&& has_key(s:symbol_map, a:response.id) + let l:options = remove(s:symbol_map, a:response.id) + + let l:result = get(a:response, 'result', v:null) + let l:item_list = [] + + if type(l:result) is v:t_list + " Each item looks like this: + " { + " 'name': 'foo', + " 'kind': 123, + " 'deprecated': v:false, + " 'location': { + " 'uri': 'file://...', + " 'range': { + " 'start': {'line': 0, 'character': 0}, + " 'end': {'line': 0, 'character': 0}, + " }, + " }, + " 'containerName': 'SomeContainer', + " } + for l:response_item in l:result + let l:location = l:response_item.location + + call add(l:item_list, { + \ 'filename': ale#path#FromURI(l:location.uri), + \ 'line': l:location.range.start.line + 1, + \ 'column': l:location.range.start.character + 1, + \ 'match': l:response_item.name, + \}) + endfor + endif + + if empty(l:item_list) + call ale#util#Execute('echom ''No symbols found.''') + else + call ale#preview#ShowSelection(l:item_list) + endif + endif +endfunction + +function! s:OnReady(linter, lsp_details, query, ...) abort + let l:buffer = a:lsp_details.buffer + + " If we already made a request, stop here. + if getbufvar(l:buffer, 'ale_symbol_request_made', 0) + return + endif + + let l:id = a:lsp_details.connection_id + + let l:Callback = function('ale#symbol#HandleLSPResponse') + call ale#lsp#RegisterCallback(l:id, l:Callback) + + let l:message = ale#lsp#message#Symbol(a:query) + let l:request_id = ale#lsp#Send(l:id, l:message) + + call setbufvar(l:buffer, 'ale_symbol_request_made', 1) + let s:symbol_map[l:request_id] = { + \ 'buffer': l:buffer, + \} +endfunction + +function! s:Search(linter, buffer, query) abort + let l:lsp_details = ale#lsp_linter#StartLSP(a:buffer, a:linter) + + if !empty(l:lsp_details) + call ale#lsp#WaitForCapability( + \ l:lsp_details.connection_id, + \ 'symbol_search', + \ function('s:OnReady', [a:linter, l:lsp_details, a:query]), + \) + endif +endfunction + +function! ale#symbol#Search(query) abort + if type(a:query) isnot v:t_string || empty(a:query) + throw 'A non-empty string must be provided!' + endif + + let l:buffer = bufnr('') + + " Set a flag so we only make one request. + call setbufvar(l:buffer, 'ale_symbol_request_made', 0) + + for l:linter in ale#linter#Get(getbufvar(l:buffer, '&filetype')) + if !empty(l:linter.lsp) && l:linter.lsp isnot# 'tsserver' + call s:Search(l:linter, l:buffer, a:query) + endif + endfor +endfunction diff --git a/doc/ale.txt b/doc/ale.txt index 21fab16c..6340091c 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -15,6 +15,8 @@ CONTENTS *ale-contents* 5.1 Completion........................|ale-completion| 5.2 Go To Definition..................|ale-go-to-definition| 5.3 Find References...................|ale-find-references| + 5.4 Hovering..........................|ale-hover| + 5.5 Symbol Search.....................|ale-symbol-search| 6. Global Options.......................|ale-options| 6.1 Highlights........................|ale-highlights| 6.2 Options for write-good Linter.....|ale-write-good-options| @@ -857,6 +859,15 @@ settings. For example: > set ttymouse=xterm < +------------------------------------------------------------------------------- +5.5 Symbol Search *ale-symbol-search* + +ALE supports searching for workspace symbols via LSP linters. The following +commands are supported: + +|ALESymbolSearch| - Search for symbols in the workspace. + + =============================================================================== 6. Global Options *ale-options* @@ -2147,6 +2158,14 @@ ALEHover *ALEHover* A plug mapping `(ale_hover)` is defined for this command. + +ALESymbolSearch `` *ALESymbolSearch* + + Search for symbols in the workspace, taken from any available LSP linters. + + The arguments provided to this command will be used as a search query for + finding symbols in the workspace, such as functions, types, etc. + *:ALELint* ALELint *ALELint* diff --git a/plugin/ale.vim b/plugin/ale.vim index 41da7c74..7ef19775 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -194,6 +194,9 @@ command! -bar ALEFindReferences :call ale#references#Find() command! -bar ALEHover :call ale#hover#Show(bufnr(''), getcurpos()[1], \ getcurpos()[2], {}) +" Search for appearances of a symbol, such as a type name or function name. +command! -nargs=1 ALESymbolSearch :call ale#symbol#Search() + " mappings for commands nnoremap (ale_previous) :ALEPrevious nnoremap (ale_previous_wrap) :ALEPreviousWrap diff --git a/test/lsp/test_lsp_client_messages.vader b/test/lsp/test_lsp_client_messages.vader index d4abaad9..71768ce5 100644 --- a/test/lsp/test_lsp_client_messages.vader +++ b/test/lsp/test_lsp_client_messages.vader @@ -161,6 +161,17 @@ Execute(ale#lsp#message#References() should return correct messages): \ ], \ ale#lsp#message#References(bufnr(''), 12, 34) +Execute(ale#lsp#message#Symbol() should return correct messages): + AssertEqual + \ [ + \ 0, + \ 'workspace/symbol', + \ { + \ 'query': 'foobar', + \ } + \ ], + \ ale#lsp#message#Symbol('foobar') + Execute(ale#lsp#message#Hover() should return correct messages): AssertEqual \ [ diff --git a/test/lsp/test_other_initialize_message_handling.vader b/test/lsp/test_other_initialize_message_handling.vader index e29f3358..072f8c4b 100644 --- a/test/lsp/test_other_initialize_message_handling.vader +++ b/test/lsp/test_other_initialize_message_handling.vader @@ -16,6 +16,7 @@ Before: \ 'completion': 0, \ 'completion_trigger_characters': [], \ 'definition': 0, + \ 'symbol_search': 0, \ }, \} @@ -67,7 +68,8 @@ Execute(Capabilities should bet set up correctly): \ }, \ 'definitionProvider': v:true, \ 'experimental': {}, - \ 'documentHighlightProvider': v:true + \ 'documentHighlightProvider': v:true, + \ 'workspaceSymbolProvider': v:true \ }, \ }, \}) @@ -80,6 +82,7 @@ Execute(Capabilities should bet set up correctly): \ 'references': 1, \ 'hover': 1, \ 'definition': 1, + \ 'symbol_search': 1, \ }, \ b:conn.capabilities @@ -121,6 +124,7 @@ Execute(Disabled capabilities should be recognised correctly): \ 'references': 0, \ 'hover': 0, \ 'definition': 0, + \ 'symbol_search': 0, \ }, \ b:conn.capabilities diff --git a/test/test_symbol_search.vader b/test/test_symbol_search.vader new file mode 100644 index 00000000..d8b7a4a6 --- /dev/null +++ b/test/test_symbol_search.vader @@ -0,0 +1,173 @@ +Before: + call ale#test#SetDirectory('/testplugin/test') + call ale#test#SetFilename('dummy.txt') + + let g:Callback = '' + let g:expr_list = [] + let g:message_list = [] + let g:preview_called = 0 + let g:item_list = [] + let g:capability_checked = '' + let g:conn_id = v:null + let g:WaitCallback = v:null + + runtime autoload/ale/lsp_linter.vim + runtime autoload/ale/lsp.vim + runtime autoload/ale/util.vim + runtime autoload/ale/preview.vim + + function! ale#lsp_linter#StartLSP(buffer, linter) abort + let g:conn_id = ale#lsp#Register('executable', '/foo/bar', {}) + call ale#lsp#MarkDocumentAsOpen(g:conn_id, a:buffer) + + return { + \ 'buffer': a:buffer, + \ 'connection_id': g:conn_id, + \ 'project_root': '/foo/bar', + \ 'language_id': 'python', + \} + endfunction + + function! ale#lsp#WaitForCapability(conn_id, capability, callback) abort + let g:capability_checked = a:capability + let g:WaitCallback = a:callback + endfunction + + function! ale#lsp#RegisterCallback(conn_id, callback) abort + let g:Callback = a:callback + endfunction + + function! ale#lsp#Send(conn_id, message) abort + call add(g:message_list, a:message) + + return 42 + endfunction + + function! ale#util#Execute(expr) abort + call add(g:expr_list, a:expr) + endfunction + + function! ale#preview#ShowSelection(item_list) abort + let g:preview_called = 1 + let g:item_list = a:item_list + endfunction + +After: + call ale#test#RestoreDirectory() + call ale#linter#Reset() + + unlet! g:capability_checked + unlet! g:WaitCallback + unlet! g:conn_id + unlet! g:Callback + unlet! g:message_list + unlet! g:expr_list + unlet! b:ale_linters + unlet! g:item_list + unlet! g:preview_called + + runtime autoload/ale/lsp_linter.vim + runtime autoload/ale/lsp.vim + runtime autoload/ale/util.vim + runtime autoload/ale/preview.vim + +Execute(Other messages for the LSP handler should be ignored): + call ale#symbol#HandleLSPResponse(1, {'command': 'foo'}) + +Execute(Failed symbol responses should be handled correctly): + call ale#symbol#SetMap({3: {}}) + call ale#symbol#HandleLSPResponse(1, {'id': 3}) + AssertEqual {}, ale#symbol#GetMap() + +Execute(LSP symbol responses should be handled): + call ale#symbol#SetMap({3: {}}) + call ale#symbol#HandleLSPResponse( + \ 1, + \ { + \ 'id': 3, + \ 'result': [ + \ { + \ 'name': 'foo', + \ 'location': { + \ 'uri': ale#path#ToURI(ale#path#Simplify(g:dir . '/completion_dummy_file')), + \ 'range': { + \ 'start': {'line': 2, 'character': 7}, + \ }, + \ }, + \ }, + \ { + \ 'name': 'foobar', + \ 'location': { + \ 'uri': ale#path#ToURI(ale#path#Simplify(g:dir . '/other_file')), + \ 'range': { + \ 'start': {'line': 7, 'character': 15}, + \ }, + \ }, + \ }, + \ ], + \ } + \) + + AssertEqual + \ [ + \ { + \ 'filename': ale#path#Simplify(g:dir . '/completion_dummy_file'), + \ 'line': 3, + \ 'column': 8, + \ 'match': 'foo', + \ }, + \ { + \ 'filename': ale#path#Simplify(g:dir . '/other_file'), + \ 'line': 8, + \ 'column': 16, + \ 'match': 'foobar', + \ }, + \ ], + \ g:item_list + AssertEqual {}, ale#symbol#GetMap() + +Execute(Preview windows should not be opened for empty LSP symbol responses): + call ale#symbol#SetMap({3: {}}) + call ale#symbol#HandleLSPResponse( + \ 1, + \ { + \ 'id': 3, + \ 'result': [ + \ ], + \ } + \) + + Assert !g:preview_called + AssertEqual {}, ale#symbol#GetMap() + AssertEqual ['echom ''No symbols found.'''], g:expr_list + +Given python(Some Python file): + foo + somelongerline + bazxyzxyzxyz + +Execute(LSP symbol requests should be sent): + runtime ale_linters/python/pyls.vim + let b:ale_linters = ['pyls'] + call setpos('.', [bufnr(''), 1, 5, 0]) + + ALESymbolSearch foo bar + + " We shouldn't register the callback yet. + AssertEqual '''''', string(g:Callback) + + AssertEqual 'symbol_search', g:capability_checked + AssertEqual type(function('type')), type(g:WaitCallback) + call call(g:WaitCallback, [g:conn_id, '/foo/bar']) + + AssertEqual + \ 'function(''ale#symbol#HandleLSPResponse'')', + \ string(g:Callback) + + AssertEqual + \ [ + \ [0, 'workspace/symbol', {'query': 'foo bar'}], + \ ], + \ g:message_list + + AssertEqual {'42': {'buffer': bufnr('')}}, ale#symbol#GetMap()