diff --git a/autoload/ale/definition.vim b/autoload/ale/definition.vim index 8ca03651..fd6cd2e1 100644 --- a/autoload/ale/definition.vim +++ b/autoload/ale/definition.vim @@ -121,6 +121,12 @@ function! s:OnReady(line, column, options, capability, linter, lsp_details) abor \ a:line, \ a:column \) + elseif a:capability is# 'implementation' + let l:message = ale#lsp#tsserver_message#Implementation( + \ l:buffer, + \ a:line, + \ a:column + \) endif else " Send a message saying the buffer has changed first, or the @@ -134,6 +140,8 @@ function! s:OnReady(line, column, options, capability, linter, lsp_details) abor let l:message = ale#lsp#message#Definition(l:buffer, a:line, a:column) elseif a:capability is# 'typeDefinition' let l:message = ale#lsp#message#TypeDefinition(l:buffer, a:line, a:column) + elseif a:capability is# 'implementation' + let l:message = ale#lsp#message#Implementation(l:buffer, a:line, a:column) else " XXX: log here? return @@ -175,6 +183,14 @@ function! ale#definition#GoToType(options) abort endfor endfunction +function! ale#definition#GoToImpl(options) abort + for l:linter in ale#linter#Get(&filetype) + if !empty(l:linter.lsp) + call s:GoToLSPDefinition(l:linter, a:options, 'implementation') + endif + endfor +endfunction + function! ale#definition#GoToCommandHandler(command, ...) abort let l:options = {} @@ -200,6 +216,8 @@ function! ale#definition#GoToCommandHandler(command, ...) abort if a:command is# 'type' call ale#definition#GoToType(l:options) + elseif a:command is# 'implementation' + call ale#definition#GoToImpl(l:options) else call ale#definition#GoTo(l:options) endif diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index acdcdac3..daaed6fa 100644 --- a/autoload/ale/lsp.vim +++ b/autoload/ale/lsp.vim @@ -44,6 +44,7 @@ function! ale#lsp#Register(executable_or_address, project, init_options) abort \ 'completion_trigger_characters': [], \ 'definition': 0, \ 'typeDefinition': 0, + \ 'implementation': 0, \ 'symbol_search': 0, \ 'code_actions': 0, \ 'did_save': 0, @@ -259,6 +260,14 @@ function! s:UpdateCapabilities(conn, capabilities) abort let a:conn.capabilities.typeDefinition = 1 endif + if get(a:capabilities, 'implementationProvider') is v:true + let a:conn.capabilities.implementation = 1 + endif + + if type(get(a:capabilities, 'implementationProvider')) is v:t_dict + let a:conn.capabilities.implementation = 1 + endif + if get(a:capabilities, 'workspaceSymbolProvider') is v:true let a:conn.capabilities.symbol_search = 1 endif @@ -379,6 +388,7 @@ function! ale#lsp#MarkConnectionAsTsserver(conn_id) abort let l:conn.capabilities.completion_trigger_characters = ['.'] let l:conn.capabilities.definition = 1 let l:conn.capabilities.typeDefinition = 1 + let l:conn.capabilities.implementation = 1 let l:conn.capabilities.symbol_search = 1 let l:conn.capabilities.rename = 1 let l:conn.capabilities.filerename = 1 @@ -438,6 +448,10 @@ function! s:SendInitMessage(conn) abort \ 'typeDefinition': { \ 'dynamicRegistration': v:false, \ }, + \ 'implementation': { + \ 'dynamicRegistration': v:false, + \ 'linkSupport': v:false, + \ }, \ 'publishDiagnostics': { \ 'relatedInformation': v:true, \ }, diff --git a/autoload/ale/lsp/message.vim b/autoload/ale/lsp/message.vim index e5eeac82..c2238dec 100644 --- a/autoload/ale/lsp/message.vim +++ b/autoload/ale/lsp/message.vim @@ -139,6 +139,15 @@ function! ale#lsp#message#TypeDefinition(buffer, line, column) abort \}] endfunction +function! ale#lsp#message#Implementation(buffer, line, column) abort + return [0, 'textDocument/implementation', { + \ 'textDocument': { + \ 'uri': ale#util#ToURI(expand('#' . a:buffer . ':p')), + \ }, + \ 'position': {'line': a:line - 1, 'character': a:column - 1}, + \}] +endfunction + function! ale#lsp#message#References(buffer, line, column) abort return [0, 'textDocument/references', { \ 'textDocument': { diff --git a/autoload/ale/lsp/tsserver_message.vim b/autoload/ale/lsp/tsserver_message.vim index 7c59feee..02e57899 100644 --- a/autoload/ale/lsp/tsserver_message.vim +++ b/autoload/ale/lsp/tsserver_message.vim @@ -72,6 +72,14 @@ function! ale#lsp#tsserver_message#TypeDefinition(buffer, line, column) abort \}] endfunction +function! ale#lsp#tsserver_message#Implementation(buffer, line, column) abort + return [0, 'ts@implementation', { + \ 'line': a:line, + \ 'offset': a:column, + \ 'file': expand('#' . a:buffer . ':p'), + \}] +endfunction + function! ale#lsp#tsserver_message#References(buffer, line, column) abort return [0, 'ts@references', { \ 'line': a:line, diff --git a/doc/ale.txt b/doc/ale.txt index 8b8e1dca..3e6ea02b 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -17,10 +17,11 @@ CONTENTS *ale-contents* 5.1 Completion........................|ale-completion| 5.2 Go To Definition..................|ale-go-to-definition| 5.3 Go To Type Definition.............|ale-go-to-type-definition| - 5.4 Find References...................|ale-find-references| - 5.5 Hovering..........................|ale-hover| - 5.6 Symbol Search.....................|ale-symbol-search| - 5.7 Refactoring: Rename, Actions......|ale-refactor| + 5.4 Go To Implementation..............|ale-go-to-type-implementation| + 5.5 Find References...................|ale-find-references| + 5.6 Hovering..........................|ale-hover| + 5.7 Symbol Search.....................|ale-symbol-search| + 5.8 Refactoring: Rename, Actions......|ale-refactor| 6. Global Options.......................|ale-options| 6.1 Highlights........................|ale-highlights| 7. Linter/Fixer Options.................|ale-integration-options| @@ -637,14 +638,23 @@ documentation for the command for configuring how the location will be displayed. ------------------------------------------------------------------------------- -5.4 Find References *ale-find-references* +5.4 Go To Implementation *ale-go-to-implementation* + +ALE supports jumping to the files and locations where symbols are implemented +through any enabled LSP linters. The locations ALE will jump to depend on the +information returned by LSP servers. The |ALEGoToImplementation| command will +jump to the implementation of symbols under the cursor. See the documentation +for the command for configuring how the location will be displayed. + +------------------------------------------------------------------------------- +5.5 Find References *ale-find-references* ALE supports finding references for symbols though any enabled LSP linters with the |ALEFindReferences| command. See the documentation for the command for a full list of options. ------------------------------------------------------------------------------- -5.5 Hovering *ale-hover* +5.6 Hovering *ale-hover* ALE supports "hover" information for printing brief information about symbols at the cursor taken from LSP linters. The following commands are supported: @@ -686,14 +696,14 @@ Documentation for symbols at the cursor can be retrieved using the |ALEDocumentation| command. This command is only available for `tsserver`. ------------------------------------------------------------------------------- -5.6 Symbol Search *ale-symbol-search* +5.7 Symbol Search *ale-symbol-search* ALE supports searching for workspace symbols via LSP linters with the |ALESymbolSearch| command. See the documentation for the command for a full list of options. ------------------------------------------------------------------------------- -5.7 Refactoring: Rename, Actions *ale-refactor* +5.8 Refactoring: Rename, Actions *ale-refactor* ALE supports renaming symbols in code such as variables or class names with the |ALERename| command. @@ -3410,6 +3420,33 @@ ALEGoToTypeDefinition *ALEGoToTypeDefinition* `(ale_go_to_type_definition_in_vsplit)` - `:ALEGoToTypeDefinition -vsplit` +ALEGoToImplementation *ALEGoToImplementation* + + This works similar to |ALEGoToDefinition| but instead jumps to the + implementation of symbol under the cursor. ALE will jump to a definition if + an LSP server provides a location to jump to. Otherwise, ALE will do nothing. + + The locations opened in different ways using the following variations. + + `:ALEGoToImplementation -tab` - Open the location in a new tab. + `:ALEGoToImplementation -split` - Open the location in a horizontal split. + `:ALEGoToImplementation -vsplit` - Open the location in a vertical split. + + The default method used for navigating to a new location can be changed + by modifying |g:ale_default_navigation|. + + You can jump back to the position you were at before going to the definition + of something with jump motions like CTRL-O. See |jump-motions|. + + The following Plug mappings are defined for this command, which correspond + to the following commands. + + `(ale_go_to_implementation)` - `:ALEGoToImplementation` + `(ale_go_to_implementation_in_tab)` - `:ALEGoToImplementation -tab` + `(ale_go_to_implementation_in_split)` - `:ALEGoToImplementation -split` + `(ale_go_to_implementation_in_vsplit)` - `:ALEGoToImplementation -vsplit` + + ALEHover *ALEHover* Print brief information about the symbol under the cursor, taken from any diff --git a/plugin/ale.vim b/plugin/ale.vim index fb016651..8d829ddf 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -252,6 +252,9 @@ command! -bar -nargs=* ALEGoToDefinition :call ale#definition#GoToCommandHandler " Go to type definition for tsserver and LSP command! -bar -nargs=* ALEGoToTypeDefinition :call ale#definition#GoToCommandHandler('type', ) +" Go to implementation for tsserver and LSP +command! -bar -nargs=* ALEGoToImplementation :call ale#definition#GoToCommandHandler('implementation', ) + " Repeat a previous selection in the preview window command! -bar ALERepeatSelection :call ale#preview#RepeatSelection() @@ -319,6 +322,9 @@ nnoremap (ale_go_to_type_definition) :ALEGoToTypeDefinition (ale_go_to_type_definition_in_tab) :ALEGoToTypeDefinition -tab nnoremap (ale_go_to_type_definition_in_split) :ALEGoToTypeDefinition -split nnoremap (ale_go_to_type_definition_in_vsplit) :ALEGoToTypeDefinitionIn -vsplit +nnoremap (ale_go_to_implementation_in_tab) :ALEGoToImplementation -tab +nnoremap (ale_go_to_implementation_in_split) :ALEGoToImplementation -split +nnoremap (ale_go_to_implementation_in_vsplit) :ALEGoToImplementation -vsplit nnoremap (ale_find_references) :ALEFindReferences nnoremap (ale_hover) :ALEHover nnoremap (ale_documentation) :ALEDocumentation diff --git a/test/lsp/test_lsp_client_messages.vader b/test/lsp/test_lsp_client_messages.vader index 65fdaa67..4d897b51 100644 --- a/test/lsp/test_lsp_client_messages.vader +++ b/test/lsp/test_lsp_client_messages.vader @@ -175,6 +175,20 @@ Execute(ale#lsp#message#TypeDefinition() should return correct messages): \ ], \ ale#lsp#message#TypeDefinition(bufnr(''), 12, 34) +Execute(ale#lsp#message#Implementation() should return correct messages): + AssertEqual + \ [ + \ 0, + \ 'textDocument/implementation', + \ { + \ 'textDocument': { + \ 'uri': ale#path#ToFileURI(g:dir . '/foo/bar.ts'), + \ }, + \ 'position': {'line': 11, 'character': 33}, + \ } + \ ], + \ ale#lsp#message#Implementation(bufnr(''), 12, 34) + Execute(ale#lsp#message#References() should return correct messages): AssertEqual \ [ @@ -335,6 +349,19 @@ Execute(ale#lsp#tsserver_message#TypeDefinition() should return correct messages \ ], \ ale#lsp#tsserver_message#TypeDefinition(bufnr(''), 347, 12) +Execute(ale#lsp#tsserver_message#Implementation() should return correct messages): + AssertEqual + \ [ + \ 0, + \ 'ts@implementation', + \ { + \ 'file': ale#path#Simplify(g:dir . '/foo/bar.ts'), + \ 'line': 347, + \ 'offset': 12, + \ } + \ ], + \ ale#lsp#tsserver_message#Implementation(bufnr(''), 347, 12) + Execute(ale#lsp#tsserver_message#References() should return correct messages): AssertEqual \ [ diff --git a/test/lsp/test_lsp_startup.vader b/test/lsp/test_lsp_startup.vader index 1f169266..7417dbcb 100644 --- a/test/lsp/test_lsp_startup.vader +++ b/test/lsp/test_lsp_startup.vader @@ -189,6 +189,10 @@ Before: \ 'typeDefinition': { \ 'dynamicRegistration': v:false, \ }, + \ 'implementation': { + \ 'dynamicRegistration': v:false, + \ 'linkSupport': v:false, + \ }, \ 'publishDiagnostics': { \ 'relatedInformation': v:true, \ }, diff --git a/test/lsp/test_other_initialize_message_handling.vader b/test/lsp/test_other_initialize_message_handling.vader index f3b53843..6711c746 100644 --- a/test/lsp/test_other_initialize_message_handling.vader +++ b/test/lsp/test_other_initialize_message_handling.vader @@ -182,6 +182,7 @@ Execute(Capabilities should be enabled when send as Dictionaries): \ }, \ 'definitionProvider': {}, \ 'typeDefinitionProvider': {}, + \ 'implementationProvider': {}, \ 'experimental': {}, \ 'documentHighlightProvider': v:true, \ 'workspaceSymbolProvider': {} @@ -198,6 +199,7 @@ Execute(Capabilities should be enabled when send as Dictionaries): \ 'hover': 1, \ 'definition': 1, \ 'typeDefinition': 1, + \ 'implementation': 1, \ 'symbol_search': 1, \ 'rename': 1, \ 'code_actions': 1, diff --git a/test/test_go_to_definition.vader b/test/test_go_to_definition.vader index fd2519b8..726de551 100644 --- a/test/test_go_to_definition.vader +++ b/test/test_go_to_definition.vader @@ -266,6 +266,30 @@ Execute(tsserver type definition requests should be sent): \ g:message_list AssertEqual {'42': {'open_in': 'current-buffer'}}, ale#definition#GetMap() +Execute(tsserver implementation requests should be sent): + runtime ale_linters/typescript/tsserver.vim + call setpos('.', [bufnr(''), 2, 5, 0]) + + ALEGoToImplementation + + " We shouldn't register the callback yet. + AssertEqual '''''', string(g:Callback) + + AssertEqual type(function('type')), type(g:InitCallback) + call g:InitCallback() + + AssertEqual 'implementation', g:capability_checked + AssertEqual + \ 'function(''ale#definition#HandleTSServerResponse'')', + \ string(g:Callback) + AssertEqual + \ [ + \ ale#lsp#tsserver_message#Change(bufnr('')), + \ [0, 'ts@implementation', {'file': expand('%:p'), 'line': 2, 'offset': 5}] + \ ], + \ g:message_list + AssertEqual {'42': {'open_in': 'current-buffer'}}, ale#definition#GetMap() + Execute(tsserver tab definition requests should be sent): runtime ale_linters/typescript/tsserver.vim call setpos('.', [bufnr(''), 2, 5, 0]) @@ -520,6 +544,42 @@ Execute(LSP type definition requests should be sent): AssertEqual {'42': {'open_in': 'current-buffer'}}, ale#definition#GetMap() +Execute(LSP implementation requests should be sent): + runtime ale_linters/python/pylsp.vim + let b:ale_linters = ['pylsp'] + call setpos('.', [bufnr(''), 1, 5, 0]) + + ALEGoToImplementation + + " We shouldn't register the callback yet. + AssertEqual '''''', string(g:Callback) + + AssertEqual type(function('type')), type(g:InitCallback) + call g:InitCallback() + + AssertEqual 'implementation', g:capability_checked + AssertEqual + \ 'function(''ale#definition#HandleLSPResponse'')', + \ string(g:Callback) + + AssertEqual + \ [ + \ [1, 'textDocument/didChange', { + \ 'textDocument': { + \ 'uri': ale#path#ToFileURI(expand('%:p')), + \ 'version': g:ale_lsp_next_version_id - 1, + \ }, + \ 'contentChanges': [{'text': join(getline(1, '$'), "\n") . "\n"}] + \ }], + \ [0, 'textDocument/implementation', { + \ 'textDocument': {'uri': ale#path#ToFileURI(expand('%:p'))}, + \ 'position': {'line': 0, 'character': 2}, + \ }], + \ ], + \ g:message_list + + AssertEqual {'42': {'open_in': 'current-buffer'}}, ale#definition#GetMap() + Execute(LSP tab definition requests should be sent): runtime ale_linters/python/pylsp.vim let b:ale_linters = ['pylsp'] @@ -591,3 +651,39 @@ Execute(LSP tab type definition requests should be sent): \ g:message_list AssertEqual {'42': {'open_in': 'tab'}}, ale#definition#GetMap() + +Execute(LSP tab implementation requests should be sent): + runtime ale_linters/python/pylsp.vim + let b:ale_linters = ['pylsp'] + call setpos('.', [bufnr(''), 1, 5, 0]) + + ALEGoToImplementation -tab + + " We shouldn't register the callback yet. + AssertEqual '''''', string(g:Callback) + + AssertEqual type(function('type')), type(g:InitCallback) + call g:InitCallback() + + AssertEqual 'implementation', g:capability_checked + AssertEqual + \ 'function(''ale#definition#HandleLSPResponse'')', + \ string(g:Callback) + + AssertEqual + \ [ + \ [1, 'textDocument/didChange', { + \ 'textDocument': { + \ 'uri': ale#path#ToFileURI(expand('%:p')), + \ 'version': g:ale_lsp_next_version_id - 1, + \ }, + \ 'contentChanges': [{'text': join(getline(1, '$'), "\n") . "\n"}] + \ }], + \ [0, 'textDocument/implementation', { + \ 'textDocument': {'uri': ale#path#ToFileURI(expand('%:p'))}, + \ 'position': {'line': 0, 'character': 2}, + \ }], + \ ], + \ g:message_list + + AssertEqual {'42': {'open_in': 'tab'}}, ale#definition#GetMap()