Implement LSP symbol search
This commit is contained in:
parent
20e4e3f9db
commit
4ef2c81e95
10 changed files with 349 additions and 2 deletions
13
README.md
13
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.
|
||||
|
||||
<a name="usage-symbol-search"></a>
|
||||
|
||||
### 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.
|
||||
|
||||
<a name="installation"></a>
|
||||
|
||||
## 3. Installation
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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': {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
109
autoload/ale/symbol.vim
Normal file
109
autoload/ale/symbol.vim
Normal file
|
@ -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
|
19
doc/ale.txt
19
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 `<Plug>(ale_hover)` is defined for this command.
|
||||
|
||||
|
||||
ALESymbolSearch `<query>` *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*
|
||||
|
||||
|
|
|
@ -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(<q-args>)
|
||||
|
||||
" <Plug> mappings for commands
|
||||
nnoremap <silent> <Plug>(ale_previous) :ALEPrevious<Return>
|
||||
nnoremap <silent> <Plug>(ale_previous_wrap) :ALEPreviousWrap<Return>
|
||||
|
|
|
@ -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
|
||||
\ [
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
173
test/test_symbol_search.vader
Normal file
173
test/test_symbol_search.vader
Normal file
|
@ -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()
|
Reference in a new issue