From 0163a812d173315f16bffa308418c853f510245e Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Fri, 30 Oct 2020 00:30:40 +0200 Subject: [PATCH 01/26] CodeFix implementation Relates issue https://github.com/dense-analysis/ale/issues/1466 --- autoload/ale/codefix.vim | 139 ++++++++++++++++++++++++++ autoload/ale/lsp.vim | 1 + autoload/ale/lsp/tsserver_message.vim | 13 +++ plugin/ale.vim | 3 + 4 files changed, 156 insertions(+) create mode 100644 autoload/ale/codefix.vim diff --git a/autoload/ale/codefix.vim b/autoload/ale/codefix.vim new file mode 100644 index 0000000000..ccde2e856f --- /dev/null +++ b/autoload/ale/codefix.vim @@ -0,0 +1,139 @@ +" Author: Dalius Dobravolskas +" Description: Code Fix support for tsserver + +function! s:message(message) abort + call ale#util#Execute('echom ' . string(a:message)) +endfunction + +function! ale#codefix#HandleTSServerResponse(conn_id, response) abort + if get(a:response, 'command', '') isnot# 'getCodeFixes' + return + endif + + if get(a:response, 'success', v:false) is v:false + let l:message = get(a:response, 'message', 'unknown') + call s:message('Error while getting code fixes. Reason: ' . l:message) + + return + endif + + if len(a:response.body) == 0 + call s:message('No code fixes available.') + + return + endif + + let l:code_fix_to_apply = 0 + + if len(a:response.body) == 1 + let l:code_fix_to_apply = 1 + else + let l:codefix_no = 1 + let l:codefixstring = '' + + for l:codefix in a:response.body + let l:codefixstring .= l:codefix_no . ") " . l:codefix.description . "\n" + let l:codefix_no += 1 + endfor + + let l:codefixstring .= 'Apply code fix: ' + + let l:code_fix_to_apply = ale#util#Input(l:codefixstring, '') + let l:code_fix_to_apply = str2nr(l:code_fix_to_apply) + + if l:code_fix_to_apply == 0 + return + endif + endif + + let l:changes = a:response.body[l:code_fix_to_apply - 1].changes + + call ale#code_action#HandleCodeAction({ + \ 'description': 'codefix', + \ 'changes': l:changes, + \}, v:false) +endfunction + +function! s:OnReady(line, column, linter, lsp_details) abort + let l:id = a:lsp_details.connection_id + + if a:linter.lsp isnot# 'tsserver' + call s:message('CodeFix currently only works with tsserver') + + return + endif + + if !ale#lsp#HasCapability(l:id, 'code_actions') + return + endif + + let l:buffer = a:lsp_details.buffer + + if !has_key(g:ale_buffer_info, l:buffer) + return + endif + + let l:nearest_error = v:null + let l:nearest_error_diff = -1 + + for l:error in get(g:ale_buffer_info[l:buffer], 'loclist', []) + if l:error.lnum == a:line + let l:diff = abs(l:error.col - a:column) + + if l:nearest_error_diff == -1 || l:diff < l:nearest_error_diff + let l:nearest_error_diff = l:diff + let l:nearest_error = l:error.code + endif + endif + endfor + + let l:Callback = function('ale#codefix#HandleTSServerResponse') + + call ale#lsp#RegisterCallback(l:id, l:Callback) + + if a:linter.lsp is# 'tsserver' + let l:message = ale#lsp#tsserver_message#GetCodeFixes( + \ l:buffer, + \ a:line, + \ a:column, + \ a:line, + \ a:column, + \ [l:nearest_error], + \) + endif + + let l:request_id = ale#lsp#Send(l:id, l:message) +endfunction + +function! s:ExecuteGetCodeFix(linter) abort + let l:buffer = bufnr('') + let [l:line, l:column] = getpos('.')[1:2] + + if a:linter.lsp isnot# 'tsserver' + let l:column = min([l:column, len(getline(l:line))]) + endif + + let l:Callback = function( + \ 's:OnReady', [l:line, l:column]) + call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback) +endfunction + +function! ale#codefix#Execute() abort + let l:lsp_linters = [] + + for l:linter in ale#linter#Get(&filetype) + if !empty(l:linter.lsp) + call add(l:lsp_linters, l:linter) + endif + endfor + + if empty(l:lsp_linters) + call s:message('No active LSPs') + + return + endif + + for l:lsp_linter in l:lsp_linters + call s:ExecuteGetCodeFix(l:lsp_linter) + endfor +endfunction diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index 7d99e9d2b4..75f0413ccd 100644 --- a/autoload/ale/lsp.vim +++ b/autoload/ale/lsp.vim @@ -350,6 +350,7 @@ function! ale#lsp#MarkConnectionAsTsserver(conn_id) abort let l:conn.capabilities.definition = 1 let l:conn.capabilities.symbol_search = 1 let l:conn.capabilities.rename = 1 + let l:conn.capabilities.code_actions = 1 endfunction function! s:SendInitMessage(conn) abort diff --git a/autoload/ale/lsp/tsserver_message.vim b/autoload/ale/lsp/tsserver_message.vim index b9fafaa06b..13465be5be 100644 --- a/autoload/ale/lsp/tsserver_message.vim +++ b/autoload/ale/lsp/tsserver_message.vim @@ -103,3 +103,16 @@ function! ale#lsp#tsserver_message#OrganizeImports(buffer) abort \ }, \}] endfunction + +function! ale#lsp#tsserver_message#GetCodeFixes(buffer, line, column, end_line, end_column, error_codes) abort + " The lines and columns are 1-based. + " The errors codes must be a list of tsserver error codes to fix. + return [0, 'ts@getCodeFixes', { + \ 'startLine': a:line, + \ 'startOffset': a:column, + \ 'endLine': a:end_line, + \ 'endOffset': a:end_column + 1, + \ 'file': expand('#' . a:buffer . ':p'), + \ 'errorCodes': a:error_codes, + \}] +endfunction diff --git a/plugin/ale.vim b/plugin/ale.vim index 32ec14ac5f..e35e4241b5 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -240,6 +240,8 @@ command! -bar ALEImport :call ale#completion#Import() " Rename symbols using tsserver and LSP command! -bar -bang ALERename :call ale#rename#Execute({'force_save': '' is# '!'}) +command! -bar ALECodeFix :call ale#codefix#Execute() + " Organize import statements using tsserver command! -bar ALEOrganizeImports :call ale#organize_imports#Execute() @@ -283,6 +285,7 @@ nnoremap (ale_documentation) :ALEDocumentation inoremap (ale_complete) :ALEComplete nnoremap (ale_import) :ALEImport nnoremap (ale_rename) :ALERename +nnoremap (ale_code_fix) :ALECodeFix nnoremap (ale_repeat_selection) :ALERepeatSelection " Set up autocmd groups now. From 278ccb9f925d4c25f95db97a7dd56cbc0bbb5c17 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Fri, 30 Oct 2020 12:38:56 +0200 Subject: [PATCH 02/26] Tests added. Minor changes. --- autoload/ale/codefix.vim | 6 +- test/test_codefix.vader | 308 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 311 insertions(+), 3 deletions(-) create mode 100644 test/test_codefix.vader diff --git a/autoload/ale/codefix.vim b/autoload/ale/codefix.vim index ccde2e856f..77cd5da8e0 100644 --- a/autoload/ale/codefix.vim +++ b/autoload/ale/codefix.vim @@ -29,14 +29,14 @@ function! ale#codefix#HandleTSServerResponse(conn_id, response) abort let l:code_fix_to_apply = 1 else let l:codefix_no = 1 - let l:codefixstring = '' + let l:codefixstring = "Code Fixes:\n" for l:codefix in a:response.body let l:codefixstring .= l:codefix_no . ") " . l:codefix.description . "\n" let l:codefix_no += 1 endfor - let l:codefixstring .= 'Apply code fix: ' + let l:codefixstring .= 'Type number and (empty cancels): ' let l:code_fix_to_apply = ale#util#Input(l:codefixstring, '') let l:code_fix_to_apply = str2nr(l:code_fix_to_apply) @@ -51,7 +51,7 @@ function! ale#codefix#HandleTSServerResponse(conn_id, response) abort call ale#code_action#HandleCodeAction({ \ 'description': 'codefix', \ 'changes': l:changes, - \}, v:false) + \}, {}) endfunction function! s:OnReady(line, column, linter, lsp_details) abort diff --git a/test/test_codefix.vader b/test/test_codefix.vader new file mode 100644 index 0000000000..feb2a35744 --- /dev/null +++ b/test/test_codefix.vader @@ -0,0 +1,308 @@ +Before: + call ale#test#SetDirectory('/testplugin/test') + call ale#test#SetFilename('dummy.txt') + Save g:ale_buffer_info + + let g:ale_buffer_info = {} + + let g:old_filename = expand('%:p') + let g:Callback = '' + let g:expr_list = [] + let g:message_list = [] + let g:handle_code_action_called = 0 + let g:code_actions = [] + let g:options = {} + let g:capability_checked = '' + let g:conn_id = v:null + let g:InitCallback = v:null + + runtime autoload/ale/lsp_linter.vim + runtime autoload/ale/lsp.vim + runtime autoload/ale/util.vim + runtime autoload/ale/codefix.vim + runtime autoload/ale/code_action.vim + + function! ale#lsp_linter#StartLSP(buffer, linter, Callback) abort + let g:conn_id = ale#lsp#Register('executable', '/foo/bar', {}) + call ale#lsp#MarkDocumentAsOpen(g:conn_id, a:buffer) + + if a:linter.lsp is# 'tsserver' + call ale#lsp#MarkConnectionAsTsserver(g:conn_id) + endif + + let l:details = { + \ 'command': 'foobar', + \ 'buffer': a:buffer, + \ 'connection_id': g:conn_id, + \ 'project_root': '/foo/bar', + \} + + let g:InitCallback = {-> ale#lsp_linter#OnInit(a:linter, l:details, a:Callback)} + endfunction + + function! ale#lsp#HasCapability(conn_id, capability) abort + let g:capability_checked = a:capability + + return 1 + 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#code_action#HandleCodeAction(code_action, options) abort + let g:handle_code_action_called = 1 + Assert !get(a:options, 'should_save') + call add(g:code_actions, a:code_action) + endfunction + + function! ale#util#Input(message, value) abort + return '2' + endfunction + +After: + Restore + + if g:conn_id isnot v:null + call ale#lsp#RemoveConnectionWithID(g:conn_id) + endif + + call ale#test#RestoreDirectory() + call ale#linter#Reset() + + unlet! g:capability_checked + unlet! g:InitCallback + unlet! g:old_filename + unlet! g:conn_id + unlet! g:Callback + unlet! g:message_list + unlet! g:expr_list + unlet! b:ale_linters + unlet! g:options + unlet! g:code_actions + unlet! g:handle_code_action_called + + runtime autoload/ale/lsp_linter.vim + runtime autoload/ale/lsp.vim + runtime autoload/ale/util.vim + runtime autoload/ale/codefix.vim + runtime autoload/ale/code_action.vim + +Execute(Failed codefix responses should be handled correctly): + call ale#codefix#HandleTSServerResponse( + \ 1, + \ {'command': 'getCodeFixes', 'request_seq': 3} + \) + AssertEqual g:handle_code_action_called, 0 + + + +Given typescript(Some typescript file): + foo + somelongerline + bazxyzxyzxyz + +Execute(Code actions from tsserver should be handled): + call ale#codefix#HandleTSServerResponse(1, { + \ 'command': 'getCodeFixes', + \ 'request_seq': 3, + \ 'success': v:true, + \ 'type': 'response', + \ 'body': [ + \ { + \ 'description': 'Import default "x" from module "./z"', + \ 'fixName': 'import', + \ 'changes': [ + \ { + \ 'fileName': "/foo/bar/file1.ts", + \ 'textChanges': [ + \ { + \ 'end': { + \ 'line': 2, + \ 'offset': 1, + \ }, + \ 'newText': 'import x from "./z";^@', + \ 'start': { + \ 'line': 2, + \ 'offset': 1, + \ } + \ } + \ ] + \ } + \ ] + \ } + \ ] + \}) + + AssertEqual g:handle_code_action_called, 1 + AssertEqual + \ [ + \ { + \ 'description': 'codefix', + \ 'changes': [ + \ { + \ 'fileName': "/foo/bar/file1.ts", + \ 'textChanges': [ + \ { + \ 'end': { + \ 'line': 2, + \ 'offset': 1 + \ }, + \ 'newText': 'import x from "./z";^@', + \ 'start': { + \ 'line': 2, + \ 'offset': 1 + \ } + \ } + \ ] + \ } + \ ] + \ } + \ ], + \ g:code_actions + +Execute(Code actions from tsserver should be handled with user input if there are more than one action): + call ale#codefix#HandleTSServerResponse(1, { + \ 'command': 'getCodeFixes', + \ 'request_seq': 3, + \ 'success': v:true, + \ 'type': 'response', + \ 'body': [ + \ { + \ 'description': 'Import default "x" from module "./z"', + \ 'fixName': 'import', + \ 'changes': [ + \ { + \ 'fileName': "/foo/bar/file1.ts", + \ 'textChanges': [ + \ { + \ 'end': { + \ 'line': 2, + \ 'offset': 1, + \ }, + \ 'newText': 'import x from "./z";^@', + \ 'start': { + \ 'line': 2, + \ 'offset': 1, + \ } + \ } + \ ] + \ } + \ ] + \ }, + \ { + \ 'description': 'Import default "x" from module "./y"', + \ 'fixName': 'import', + \ 'changes': [ + \ { + \ 'fileName': "/foo/bar/file1.ts", + \ 'textChanges': [ + \ { + \ 'end': { + \ 'line': 2, + \ 'offset': 1, + \ }, + \ 'newText': 'import x from "./y";^@', + \ 'start': { + \ 'line': 2, + \ 'offset': 1, + \ } + \ } + \ ] + \ } + \ ] + \ } + \ ] + \}) + + AssertEqual g:handle_code_action_called, 1 + AssertEqual + \ [ + \ { + \ 'description': 'codefix', + \ 'changes': [ + \ { + \ 'fileName': "/foo/bar/file1.ts", + \ 'textChanges': [ + \ { + \ 'end': { + \ 'line': 2, + \ 'offset': 1 + \ }, + \ 'newText': 'import x from "./y";^@', + \ 'start': { + \ 'line': 2, + \ 'offset': 1 + \ } + \ } + \ ] + \ } + \ ] + \ } + \ ], + \ g:code_actions + +Execute(Prints a tsserver error message when unsuccessful): + call ale#codefix#HandleTSServerResponse(1, { + \ 'command': 'getCodeFixes', + \ 'request_seq': 3, + \ 'success': v:false, + \ 'message': 'something is wrong', + \}) + + AssertEqual g:handle_code_action_called, 0 + AssertEqual ['echom ''Error while getting code fixes. Reason: something is wrong'''], g:expr_list + +Execute(Does nothing when where are no code fixes): + call ale#codefix#HandleTSServerResponse(1, { + \ 'command': 'getCodeFixes', + \ 'request_seq': 3, + \ 'success': v:true, + \ 'body': [] + \}) + + AssertEqual g:handle_code_action_called, 0 + AssertEqual ['echom ''No code fixes available.'''], g:expr_list + +Execute(tsserver codefix requests should be sent): + call ale#linter#Reset() + + runtime ale_linters/typescript/tsserver.vim + let g:ale_buffer_info = {bufnr(''): {'loclist': [{'lnum': 2, 'col': 5, 'code': 2304}]}} + call setpos('.', [bufnr(''), 2, 5, 0]) + + ALECodeFix + + " We shouldn't register the callback yet. + AssertEqual '''''', string(g:Callback) + + AssertEqual type(function('type')), type(g:InitCallback) + call g:InitCallback() + + AssertEqual 'code_actions', g:capability_checked + AssertEqual + \ 'function(''ale#codefix#HandleTSServerResponse'')', + \ string(g:Callback) + AssertEqual + \ [ + \ ale#lsp#tsserver_message#Change(bufnr('')), + \ [0, 'ts@getCodeFixes', { + \ 'startLine': 2, + \ 'startOffset': 5, + \ 'endLine': 2, + \ 'endOffset': 6, + \ 'file': expand('%:p'), + \ 'errorCodes': [2304], + \ }] + \ ], + \ g:message_list From 58ec8ae364aa08bdedaea8f7eee1824ad3f556f6 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Fri, 30 Oct 2020 12:45:08 +0200 Subject: [PATCH 03/26] ALECodeFix documented. --- doc/ale.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/ale.txt b/doc/ale.txt index eb8f027575..c68fd55368 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -3106,6 +3106,15 @@ ALERename *ALERename* in those files will not be updated. +ALECodeFix *ALECodeFix* + + Code fix using `tsserver`. + + The code fix will be attempted in location where the cursor is resting on + error that came from `tsserver`. If there are multiple code fixes suggested + then user will be asked to select one + + ALERepeatSelection *ALERepeatSelection* Repeat the last selection displayed in the preview window. From def3a2dbf38bbf99d57677fcd0b764bce0b499a7 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Sat, 31 Oct 2020 00:07:50 +0200 Subject: [PATCH 04/26] Vint problem fix. --- autoload/ale/codefix.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autoload/ale/codefix.vim b/autoload/ale/codefix.vim index 77cd5da8e0..711d79ede2 100644 --- a/autoload/ale/codefix.vim +++ b/autoload/ale/codefix.vim @@ -32,7 +32,7 @@ function! ale#codefix#HandleTSServerResponse(conn_id, response) abort let l:codefixstring = "Code Fixes:\n" for l:codefix in a:response.body - let l:codefixstring .= l:codefix_no . ") " . l:codefix.description . "\n" + let l:codefixstring .= l:codefix_no . ') ' . l:codefix.description . "\n" let l:codefix_no += 1 endfor From 70022075746a51948ae9ba738a799c4a77f763b3 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Sat, 31 Oct 2020 00:22:43 +0200 Subject: [PATCH 05/26] It seems this line must be added. --- autoload/ale/lsp.vim | 1 + 1 file changed, 1 insertion(+) diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index 75f0413ccd..53da74fbad 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 \ 'definition': 0, \ 'typeDefinition': 0, \ 'symbol_search': 0, + \ 'code_actions': 0, \ }, \} endif From 5f5f5778a77c286fbb955d2cec676f72587c5fa4 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Sat, 31 Oct 2020 23:38:07 +0200 Subject: [PATCH 06/26] Use only errors with code. There might be situations when the error is reported by multiple sources at the same location (e.g. eslint and tsserver). In that case first source might be missing `code`. --- autoload/ale/codefix.vim | 2 +- test/test_codefix.vader | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/autoload/ale/codefix.vim b/autoload/ale/codefix.vim index 711d79ede2..0223a5ee75 100644 --- a/autoload/ale/codefix.vim +++ b/autoload/ale/codefix.vim @@ -77,7 +77,7 @@ function! s:OnReady(line, column, linter, lsp_details) abort let l:nearest_error_diff = -1 for l:error in get(g:ale_buffer_info[l:buffer], 'loclist', []) - if l:error.lnum == a:line + if has_key(l:error, 'code') && l:error.lnum == a:line let l:diff = abs(l:error.col - a:column) if l:nearest_error_diff == -1 || l:diff < l:nearest_error_diff diff --git a/test/test_codefix.vader b/test/test_codefix.vader index feb2a35744..990109e4bd 100644 --- a/test/test_codefix.vader +++ b/test/test_codefix.vader @@ -306,3 +306,37 @@ Execute(tsserver codefix requests should be sent): \ }] \ ], \ g:message_list + + +Execute(tsserver codefix requests should be sent only for error with code): + call ale#linter#Reset() + + runtime ale_linters/typescript/tsserver.vim + let g:ale_buffer_info = {bufnr(''): {'loclist': [{'lnum': 2, 'col': 5}, {'lnum': 2, 'col': 5, 'code': 2304}]}} + call setpos('.', [bufnr(''), 2, 5, 0]) + + ALECodeFix + + " We shouldn't register the callback yet. + AssertEqual '''''', string(g:Callback) + + AssertEqual type(function('type')), type(g:InitCallback) + call g:InitCallback() + + AssertEqual 'code_actions', g:capability_checked + AssertEqual + \ 'function(''ale#codefix#HandleTSServerResponse'')', + \ string(g:Callback) + AssertEqual + \ [ + \ ale#lsp#tsserver_message#Change(bufnr('')), + \ [0, 'ts@getCodeFixes', { + \ 'startLine': 2, + \ 'startOffset': 5, + \ 'endLine': 2, + \ 'endOffset': 6, + \ 'file': expand('%:p'), + \ 'errorCodes': [2304], + \ }] + \ ], + \ g:message_list From e7c14ebfe42d1309320c1e4029781754889c01ec Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Wed, 4 Nov 2020 16:00:40 +0200 Subject: [PATCH 07/26] ALECodeFix renamed to ALECodeAction. This is done to accommodate the fact that in the future there will be more capabilities: refactorings from tsserver and LSP Code Actions. --- doc/ale.txt | 2 +- plugin/ale.vim | 4 ++-- test/test_codefix.vader | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/ale.txt b/doc/ale.txt index c68fd55368..bd77f53ec9 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -3106,7 +3106,7 @@ ALERename *ALERename* in those files will not be updated. -ALECodeFix *ALECodeFix* +ALECodeAction *ALECodeAction* Code fix using `tsserver`. diff --git a/plugin/ale.vim b/plugin/ale.vim index e35e4241b5..2cc3a1369d 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -240,7 +240,7 @@ command! -bar ALEImport :call ale#completion#Import() " Rename symbols using tsserver and LSP command! -bar -bang ALERename :call ale#rename#Execute({'force_save': '' is# '!'}) -command! -bar ALECodeFix :call ale#codefix#Execute() +command! -bar ALECodeAction :call ale#codefix#Execute() " Organize import statements using tsserver command! -bar ALEOrganizeImports :call ale#organize_imports#Execute() @@ -285,7 +285,7 @@ nnoremap (ale_documentation) :ALEDocumentation inoremap (ale_complete) :ALEComplete nnoremap (ale_import) :ALEImport nnoremap (ale_rename) :ALERename -nnoremap (ale_code_fix) :ALECodeFix +nnoremap (ale_code_action) :ALECodeAction nnoremap (ale_repeat_selection) :ALERepeatSelection " Set up autocmd groups now. diff --git a/test/test_codefix.vader b/test/test_codefix.vader index 990109e4bd..aefce914cd 100644 --- a/test/test_codefix.vader +++ b/test/test_codefix.vader @@ -281,7 +281,7 @@ Execute(tsserver codefix requests should be sent): let g:ale_buffer_info = {bufnr(''): {'loclist': [{'lnum': 2, 'col': 5, 'code': 2304}]}} call setpos('.', [bufnr(''), 2, 5, 0]) - ALECodeFix + ALECodeAction " We shouldn't register the callback yet. AssertEqual '''''', string(g:Callback) @@ -315,7 +315,7 @@ Execute(tsserver codefix requests should be sent only for error with code): let g:ale_buffer_info = {bufnr(''): {'loclist': [{'lnum': 2, 'col': 5}, {'lnum': 2, 'col': 5, 'code': 2304}]}} call setpos('.', [bufnr(''), 2, 5, 0]) - ALECodeFix + ALECodeAction " We shouldn't register the callback yet. AssertEqual '''''', string(g:Callback) From a762e32db890c231c8f7e5d0fa52b6f6226c9406 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Thu, 5 Nov 2020 00:33:07 +0200 Subject: [PATCH 08/26] tsserver refactors support added. --- autoload/ale/codefix.vim | 240 +++++++++++++++++++------- autoload/ale/lsp/tsserver_message.vim | 23 +++ doc/ale.txt | 13 +- plugin/ale.vim | 2 +- test/test_codefix.vader | 126 +++++++++++++- 5 files changed, 335 insertions(+), 69 deletions(-) diff --git a/autoload/ale/codefix.vim b/autoload/ale/codefix.vim index 0223a5ee75..f9a9c4e447 100644 --- a/autoload/ale/codefix.vim +++ b/autoload/ale/codefix.vim @@ -1,64 +1,154 @@ " Author: Dalius Dobravolskas " Description: Code Fix support for tsserver +let s:codefix_map = {} + +" Used to get the codefix map in tests. +function! ale#codefix#GetMap() abort + return deepcopy(s:codefix_map) +endfunction + +" Used to set the codefix map in tests. +function! ale#codefix#SetMap(map) abort + let s:codefix_map = a:map +endfunction + +function! ale#codefix#ClearLSPData() abort + let s:codefix_map = {} +endfunction + function! s:message(message) abort call ale#util#Execute('echom ' . string(a:message)) endfunction function! ale#codefix#HandleTSServerResponse(conn_id, response) abort - if get(a:response, 'command', '') isnot# 'getCodeFixes' + if !has_key(a:response, 'request_seq') + \ || !has_key(s:codefix_map, a:response.request_seq) return endif - if get(a:response, 'success', v:false) is v:false - let l:message = get(a:response, 'message', 'unknown') - call s:message('Error while getting code fixes. Reason: ' . l:message) + let l:location = remove(s:codefix_map, a:response.request_seq) - return - endif + if get(a:response, 'command', '') is'getCodeFixes' + if get(a:response, 'success', v:false) is v:false + let l:message = get(a:response, 'message', 'unknown') + call s:message('Error while getting code fixes. Reason: ' . l:message) - if len(a:response.body) == 0 - call s:message('No code fixes available.') + return + endif - return - endif + if len(a:response.body) == 0 + call s:message('No code fixes available.') - let l:code_fix_to_apply = 0 + return + endif - if len(a:response.body) == 1 - let l:code_fix_to_apply = 1 - else - let l:codefix_no = 1 - let l:codefixstring = "Code Fixes:\n" + let l:code_fix_to_apply = 0 + + if len(a:response.body) == 1 + let l:code_fix_to_apply = 1 + else + let l:codefix_no = 1 + let l:codefixstring = "Code Fixes:\n" + + for l:codefix in a:response.body + let l:codefixstring .= l:codefix_no . ') ' . l:codefix.description . "\n" + let l:codefix_no += 1 + endfor + + let l:codefixstring .= 'Type number and (empty cancels): ' + + let l:code_fix_to_apply = ale#util#Input(l:codefixstring, '') + let l:code_fix_to_apply = str2nr(l:code_fix_to_apply) + + if l:code_fix_to_apply == 0 + return + endif + endif + + let l:changes = a:response.body[l:code_fix_to_apply - 1].changes + + call ale#code_action#HandleCodeAction({ + \ 'description': 'codefix', + \ 'changes': l:changes, + \}, {}) + elseif get(a:response, 'command', '') is# 'getApplicableRefactors' + if get(a:response, 'success', v:false) is v:false + let l:message = get(a:response, 'message', 'unknown') + call s:message('Error while getting applicable refactors. Reason: ' . l:message) + + return + endif + + if len(a:response.body) == 0 + call s:message('No applicable refactors available.') + + return + endif - for l:codefix in a:response.body - let l:codefixstring .= l:codefix_no . ') ' . l:codefix.description . "\n" - let l:codefix_no += 1 + let l:refactors = [] + + for l:item in a:response.body + for l:action in l:item.actions + call add(l:refactors, { + \ 'name': l:action.description, + \ 'id': [l:item.name, l:action.name], + \}) + endfor + endfor + + let l:refactor_no = 1 + let l:refactorstring = "Applicable refactors:\n" + + for l:refactor in l:refactors + let l:refactorstring .= l:refactor_no . ') ' . l:refactor.name . "\n" + let l:refactor_no += 1 endfor - let l:codefixstring .= 'Type number and (empty cancels): ' + let l:refactorstring .= 'Type number and (empty cancels): ' - let l:code_fix_to_apply = ale#util#Input(l:codefixstring, '') - let l:code_fix_to_apply = str2nr(l:code_fix_to_apply) + let l:refactor_to_apply = ale#util#Input(l:refactorstring, '') + let l:refactor_to_apply = str2nr(l:refactor_to_apply) - if l:code_fix_to_apply == 0 + if l:refactor_to_apply == 0 return endif - endif - let l:changes = a:response.body[l:code_fix_to_apply - 1].changes + let l:id = l:refactors[l:refactor_to_apply - 1].id + + let l:message = ale#lsp#tsserver_message#GetEditsForRefactor( + \ l:location.buffer, + \ l:location.line, + \ l:location.column, + \ l:location.end_line, + \ l:location.end_column, + \ l:id[0], + \ l:id[1], + \) + + let l:request_id = ale#lsp#Send(l:location.connection_id, l:message) - call ale#code_action#HandleCodeAction({ - \ 'description': 'codefix', - \ 'changes': l:changes, - \}, {}) + let s:codefix_map[l:request_id] = l:location + elseif get(a:response, 'command', '') is# 'getEditsForRefactor' + if get(a:response, 'success', v:false) is v:false + let l:message = get(a:response, 'message', 'unknown') + call s:message('Error while getting edits for refactor. Reason: ' . l:message) + + return + endif + + call ale#code_action#HandleCodeAction({ + \ 'description': 'editsForRefactor', + \ 'changes': a:response.body.edits, + \}, {}) + endif endfunction -function! s:OnReady(line, column, linter, lsp_details) abort +function! s:OnReady(line, column, end_line, end_column, linter, lsp_details) abort let l:id = a:lsp_details.connection_id if a:linter.lsp isnot# 'tsserver' - call s:message('CodeFix currently only works with tsserver') + call s:message('ALECodeAction currently only works with tsserver') return endif @@ -69,56 +159,88 @@ function! s:OnReady(line, column, linter, lsp_details) abort let l:buffer = a:lsp_details.buffer - if !has_key(g:ale_buffer_info, l:buffer) - return - endif + if a:line == a:end_line && a:column == a:end_column + if !has_key(g:ale_buffer_info, l:buffer) + return + endif - let l:nearest_error = v:null - let l:nearest_error_diff = -1 + let l:nearest_error = v:null + let l:nearest_error_diff = -1 - for l:error in get(g:ale_buffer_info[l:buffer], 'loclist', []) - if has_key(l:error, 'code') && l:error.lnum == a:line - let l:diff = abs(l:error.col - a:column) + for l:error in get(g:ale_buffer_info[l:buffer], 'loclist', []) + if has_key(l:error, 'code') && l:error.lnum == a:line + let l:diff = abs(l:error.col - a:column) - if l:nearest_error_diff == -1 || l:diff < l:nearest_error_diff - let l:nearest_error_diff = l:diff - let l:nearest_error = l:error.code + if l:nearest_error_diff == -1 || l:diff < l:nearest_error_diff + let l:nearest_error_diff = l:diff + let l:nearest_error = l:error.code + endif endif + endfor + + if a:linter.lsp is# 'tsserver' + let l:message = ale#lsp#tsserver_message#GetCodeFixes( + \ l:buffer, + \ a:line, + \ a:column, + \ a:line, + \ a:column, + \ [l:nearest_error], + \) endif - endfor + else + if a:linter.lsp is# 'tsserver' + let l:message = ale#lsp#tsserver_message#GetApplicableRefactors( + \ l:buffer, + \ a:line, + \ a:column, + \ a:end_line, + \ a:end_column, + \) + endif + endif let l:Callback = function('ale#codefix#HandleTSServerResponse') call ale#lsp#RegisterCallback(l:id, l:Callback) - if a:linter.lsp is# 'tsserver' - let l:message = ale#lsp#tsserver_message#GetCodeFixes( - \ l:buffer, - \ a:line, - \ a:column, - \ a:line, - \ a:column, - \ [l:nearest_error], - \) - endif - let l:request_id = ale#lsp#Send(l:id, l:message) + + let s:codefix_map[l:request_id] = { + \ 'connection_id': l:id, + \ 'buffer': l:buffer, + \ 'line': a:line, + \ 'column': a:column, + \ 'end_line': a:end_line, + \ 'end_column': a:end_column, + \} endfunction -function! s:ExecuteGetCodeFix(linter) abort +function! s:ExecuteGetCodeFix(linter, range) abort let l:buffer = bufnr('') - let [l:line, l:column] = getpos('.')[1:2] + + if a:range == 0 + let [l:line, l:column] = getpos('.')[1:2] + let l:end_line = l:line + let l:end_column = l:column + else + let [l:line, l:column] = getpos("'<")[1:2] + let [l:end_line, l:end_column] = getpos("'>")[1:2] + + let l:column = min([l:column, len(getline(l:line))]) + let l:end_column = min([l:end_column, len(getline(l:end_line))]) + endif if a:linter.lsp isnot# 'tsserver' let l:column = min([l:column, len(getline(l:line))]) endif let l:Callback = function( - \ 's:OnReady', [l:line, l:column]) + \ 's:OnReady', [l:line, l:column, l:end_line, l:end_column]) call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback) endfunction -function! ale#codefix#Execute() abort +function! ale#codefix#Execute(range) abort let l:lsp_linters = [] for l:linter in ale#linter#Get(&filetype) @@ -134,6 +256,6 @@ function! ale#codefix#Execute() abort endif for l:lsp_linter in l:lsp_linters - call s:ExecuteGetCodeFix(l:lsp_linter) + call s:ExecuteGetCodeFix(l:lsp_linter, a:range) endfor endfunction diff --git a/autoload/ale/lsp/tsserver_message.vim b/autoload/ale/lsp/tsserver_message.vim index 13465be5be..3c1b47ed3c 100644 --- a/autoload/ale/lsp/tsserver_message.vim +++ b/autoload/ale/lsp/tsserver_message.vim @@ -116,3 +116,26 @@ function! ale#lsp#tsserver_message#GetCodeFixes(buffer, line, column, end_line, \ 'errorCodes': a:error_codes, \}] endfunction + +function! ale#lsp#tsserver_message#GetApplicableRefactors(buffer, line, column, end_line, end_column) abort + " The arguments for this request can also be just 'line' and 'offset' + return [0, 'ts@getApplicableRefactors', { + \ 'startLine': a:line, + \ 'startOffset': a:column, + \ 'endLine': a:end_line, + \ 'endOffset': a:end_column + 1, + \ 'file': expand('#' . a:buffer . ':p'), + \}] +endfunction + +function! ale#lsp#tsserver_message#GetEditsForRefactor(buffer, line, column, end_line, end_column, refactor, action) abort + return [0, 'ts@getEditsForRefactor', { + \ 'startLine': a:line, + \ 'startOffset': a:column, + \ 'endLine': a:end_line, + \ 'endOffset': a:end_column + 1, + \ 'file': expand('#' . a:buffer . ':p'), + \ 'refactor': a:refactor, + \ 'action': a:action, + \}] +endfunction diff --git a/doc/ale.txt b/doc/ale.txt index bd77f53ec9..4263f69808 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -3108,11 +3108,14 @@ ALERename *ALERename* ALECodeAction *ALECodeAction* - Code fix using `tsserver`. - - The code fix will be attempted in location where the cursor is resting on - error that came from `tsserver`. If there are multiple code fixes suggested - then user will be asked to select one + Code Actions support for `tsserver`. + + There are two different kind of actions supported for `tsserver`. If run in + normal mode then code fix will be attempted if there is error in that line. + If there are multiple code fixes available use will be shown input to choose + one. In visual mode `tsserver` will be queries for applicable refactors + (e.g. extract to constant or extract to function) and user will be given + choice to select the one he/she likes. ALERepeatSelection *ALERepeatSelection* diff --git a/plugin/ale.vim b/plugin/ale.vim index 2cc3a1369d..7472ea8cd5 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -240,7 +240,7 @@ command! -bar ALEImport :call ale#completion#Import() " Rename symbols using tsserver and LSP command! -bar -bang ALERename :call ale#rename#Execute({'force_save': '' is# '!'}) -command! -bar ALECodeAction :call ale#codefix#Execute() +command! -bar -range ALECodeAction :call ale#codefix#Execute() " Organize import statements using tsserver command! -bar ALEOrganizeImports :call ale#organize_imports#Execute() diff --git a/test/test_codefix.vader b/test/test_codefix.vader index aefce914cd..103ed68129 100644 --- a/test/test_codefix.vader +++ b/test/test_codefix.vader @@ -112,7 +112,8 @@ Given typescript(Some typescript file): somelongerline bazxyzxyzxyz -Execute(Code actions from tsserver should be handled): +Execute(getCodeFixes from tsserver should be handled): + call ale#codefix#SetMap({3: {}}) call ale#codefix#HandleTSServerResponse(1, { \ 'command': 'getCodeFixes', \ 'request_seq': 3, @@ -171,7 +172,8 @@ Execute(Code actions from tsserver should be handled): \ ], \ g:code_actions -Execute(Code actions from tsserver should be handled with user input if there are more than one action): +Execute(getCodeFixes from tsserver should be handled with user input if there are more than one action): + call ale#codefix#SetMap({3: {}}) call ale#codefix#HandleTSServerResponse(1, { \ 'command': 'getCodeFixes', \ 'request_seq': 3, @@ -252,7 +254,8 @@ Execute(Code actions from tsserver should be handled with user input if there ar \ ], \ g:code_actions -Execute(Prints a tsserver error message when unsuccessful): +Execute(Prints a tsserver error message when getCodeFixes unsuccessful): + call ale#codefix#SetMap({3: {}}) call ale#codefix#HandleTSServerResponse(1, { \ 'command': 'getCodeFixes', \ 'request_seq': 3, @@ -264,6 +267,7 @@ Execute(Prints a tsserver error message when unsuccessful): AssertEqual ['echom ''Error while getting code fixes. Reason: something is wrong'''], g:expr_list Execute(Does nothing when where are no code fixes): + call ale#codefix#SetMap({3: {}}) call ale#codefix#HandleTSServerResponse(1, { \ 'command': 'getCodeFixes', \ 'request_seq': 3, @@ -307,7 +311,6 @@ Execute(tsserver codefix requests should be sent): \ ], \ g:message_list - Execute(tsserver codefix requests should be sent only for error with code): call ale#linter#Reset() @@ -340,3 +343,118 @@ Execute(tsserver codefix requests should be sent only for error with code): \ }] \ ], \ g:message_list + +Execute(getApplicableRefactors from tsserver should be handled): + call ale#codefix#SetMap({3: { + \ 'buffer': expand('%:p'), + \ 'line': 1, + \ 'column': 2, + \ 'end_line': 3, + \ 'end_column': 4, + \ 'connection_id': 0, + \}}) + call ale#codefix#HandleTSServerResponse(1, + \ {'seq': 0, 'request_seq': 3, 'type': 'response', 'success': v:true, 'body': [{'actions': [{'description': 'Extract to constant in enclosing scope', 'name': 'constant_scope_0'}], 'description': 'Extract constant', 'name': 'Extract Symbol'}, {'actions': [{'description': 'Extract to function in module scope', 'name': 'function_scope_1'}], 'description': 'Extract function', 'name': 'Extract Symbol'}], 'command': 'getApplicableRefactors'}) + + AssertEqual + \ [ + \ [0, 'ts@getEditsForRefactor', { + \ 'startLine': 1, + \ 'startOffset': 2, + \ 'endLine': 3, + \ 'endOffset': 5, + \ 'file': expand('%:p'), + \ 'refactor': 'Extract Symbol', + \ 'action': 'function_scope_1', + \ }] + \ ], + \ g:message_list + +Execute(getApplicableRefactors should print error on failure): + call ale#codefix#SetMap({3: { + \ 'buffer': expand('%:p'), + \ 'line': 1, + \ 'column': 2, + \ 'end_line': 3, + \ 'end_column': 4, + \ 'connection_id': 0, + \}}) + call ale#codefix#HandleTSServerResponse(1, + \ {'seq': 0, 'request_seq': 3, 'type': 'response', 'success': v:false, 'message': 'oops', 'command': 'getApplicableRefactors'}) + + AssertEqual ['echom ''Error while getting applicable refactors. Reason: oops'''], g:expr_list + +Execute(getApplicableRefactors should do nothing if there are no refactors): + call ale#codefix#SetMap({3: { + \ 'buffer': expand('%:p'), + \ 'line': 1, + \ 'column': 2, + \ 'end_line': 3, + \ 'end_column': 4, + \ 'connection_id': 0, + \}}) + call ale#codefix#HandleTSServerResponse(1, + \ {'seq': 0, 'request_seq': 3, 'type': 'response', 'success': v:true, 'body': [], 'command': 'getApplicableRefactors'}) + + AssertEqual ['echom ''No applicable refactors available.'''], g:expr_list + +Execute(getEditsForRefactor from tsserver should be handled): + call ale#codefix#SetMap({3: {}}) + call ale#codefix#HandleTSServerResponse(1, + \{'seq': 0, 'request_seq': 3, 'type': 'response', 'success': v:true, 'body': {'edits': [{'fileName': '/foo/bar/file.ts', 'textChanges': [{'end': {'offset': 35, 'line': 9}, 'newText': 'newFunction(app);', 'start': {'offset': 3, 'line': 8}}, {'end': {'offset': 4, 'line': 19}, 'newText': '^@function newFunction(app: Router) {^@ app.use(booExpressCsrf());^@ app.use(booExpressRequireHttps);^@}^@', 'start': {'offset': 4, 'line': 19}}]}], 'renameLocation': {'offset': 3, 'line': 8}, 'renameFilename': '/foo/bar/file.ts'}, 'command': 'getEditsForRefactor' } + \) + + AssertEqual g:handle_code_action_called, 1 + AssertEqual + \ [ + \ { + \ 'description': 'editsForRefactor', + \ 'changes': [{'fileName': '/foo/bar/file.ts', 'textChanges': [{'end': {'offset': 35, 'line': 9}, 'newText': 'newFunction(app);', 'start': {'offset': 3, 'line': 8}}, {'end': {'offset': 4, 'line': 19}, 'newText': '^@function newFunction(app: Router) {^@ app.use(booExpressCsrf());^@ app.use(booExpressRequireHttps);^@}^@', 'start': {'offset': 4, 'line': 19}}]}], + \ } + \ ], + \ g:code_actions + +Execute(getEditsForRefactor should print error on failure): + call ale#codefix#SetMap({3: {}}) + call ale#codefix#HandleTSServerResponse(1, + \{'seq': 0, 'request_seq': 3, 'type': 'response', 'success': v:false, 'message': 'oops', 'command': 'getEditsForRefactor' } + \) + + AssertEqual ['echom ''Error while getting edits for refactor. Reason: oops'''], g:expr_list + +" TODO: I can't figure out how to run ALECodeAction on range +" in test function. Therefore I can't write properly working +" test. If somebody knows how to do that help is appreciated. +" +" Execute(tsserver getApplicableRefactors requests should be sent): +" call ale#linter#Reset() +" +" runtime ale_linters/typescript/tsserver.vim +" let g:ale_buffer_info = {bufnr(''): {'loclist': []}} +" call setpos('.', [bufnr(''), 2, 5, 0]) +" normal "v$" +" +" ALECodeAction +" +" " We shouldn't register the callback yet. +" AssertEqual '''''', string(g:Callback) +" +" AssertEqual type(function('type')), type(g:InitCallback) +" call g:InitCallback() +" +" AssertEqual 'code_actions', g:capability_checked +" AssertEqual +" \ 'function(''ale#codefix#HandleTSServerResponse'')', +" \ string(g:Callback) +" AssertEqual +" \ [ +" \ ale#lsp#tsserver_message#Change(bufnr('')), +" \ [0, 'ts@getApplicableRefactors', { +" \ 'startLine': 2, +" \ 'startOffset': 5, +" \ 'endLine': 2, +" \ 'endOffset': 15, +" \ 'file': expand('%:p'), +" \ }] +" \ ], +" \ g:message_list From 0abbe1b598c267fd48ec24ec608fdd9044dd8b8e Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Thu, 5 Nov 2020 01:02:42 +0200 Subject: [PATCH 09/26] Minor fix. --- autoload/ale/codefix.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autoload/ale/codefix.vim b/autoload/ale/codefix.vim index f9a9c4e447..29e27eca97 100644 --- a/autoload/ale/codefix.vim +++ b/autoload/ale/codefix.vim @@ -29,7 +29,7 @@ function! ale#codefix#HandleTSServerResponse(conn_id, response) abort let l:location = remove(s:codefix_map, a:response.request_seq) - if get(a:response, 'command', '') is'getCodeFixes' + if get(a:response, 'command', '') is# 'getCodeFixes' if get(a:response, 'success', v:false) is v:false let l:message = get(a:response, 'message', 'unknown') call s:message('Error while getting code fixes. Reason: ' . l:message) From ff6abacd3eee93281da53b84895242a5ec914c31 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Thu, 5 Nov 2020 01:13:09 +0200 Subject: [PATCH 10/26] Attempt to fix problem in tests. --- test/test_codefix.vader | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_codefix.vader b/test/test_codefix.vader index 103ed68129..1441bcadd7 100644 --- a/test/test_codefix.vader +++ b/test/test_codefix.vader @@ -285,7 +285,7 @@ Execute(tsserver codefix requests should be sent): let g:ale_buffer_info = {bufnr(''): {'loclist': [{'lnum': 2, 'col': 5, 'code': 2304}]}} call setpos('.', [bufnr(''), 2, 5, 0]) - ALECodeAction + execute "ALECodeAction" " We shouldn't register the callback yet. AssertEqual '''''', string(g:Callback) @@ -318,7 +318,7 @@ Execute(tsserver codefix requests should be sent only for error with code): let g:ale_buffer_info = {bufnr(''): {'loclist': [{'lnum': 2, 'col': 5}, {'lnum': 2, 'col': 5, 'code': 2304}]}} call setpos('.', [bufnr(''), 2, 5, 0]) - ALECodeAction + execute "ALECodeAction" " We shouldn't register the callback yet. AssertEqual '''''', string(g:Callback) @@ -434,7 +434,7 @@ Execute(getEditsForRefactor should print error on failure): " call setpos('.', [bufnr(''), 2, 5, 0]) " normal "v$" " -" ALECodeAction +" execute "ALECodeAction" " " " We shouldn't register the callback yet. " AssertEqual '''''', string(g:Callback) From bca2ec8c90b35cb99e1defbfd75aa1267df3632c Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Thu, 5 Nov 2020 01:20:15 +0200 Subject: [PATCH 11/26] Call function instead of command. We will lose one testing layer but I think it is acceptable in this situation. --- test/test_codefix.vader | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_codefix.vader b/test/test_codefix.vader index 1441bcadd7..0241c83090 100644 --- a/test/test_codefix.vader +++ b/test/test_codefix.vader @@ -285,7 +285,8 @@ Execute(tsserver codefix requests should be sent): let g:ale_buffer_info = {bufnr(''): {'loclist': [{'lnum': 2, 'col': 5, 'code': 2304}]}} call setpos('.', [bufnr(''), 2, 5, 0]) - execute "ALECodeAction" + " ALECodeAction + call ale#codefix#Execute(0) " We shouldn't register the callback yet. AssertEqual '''''', string(g:Callback) @@ -318,7 +319,8 @@ Execute(tsserver codefix requests should be sent only for error with code): let g:ale_buffer_info = {bufnr(''): {'loclist': [{'lnum': 2, 'col': 5}, {'lnum': 2, 'col': 5, 'code': 2304}]}} call setpos('.', [bufnr(''), 2, 5, 0]) - execute "ALECodeAction" + " ALECodeAction + call ale#codefix#Execute(0) " We shouldn't register the callback yet. AssertEqual '''''', string(g:Callback) From d40b4b962274af1b10cb187c011ddf776402e9dd Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Thu, 12 Nov 2020 00:59:32 +0200 Subject: [PATCH 12/26] Handling special case when new text is added after new line symbol. It seems edits assumes that lines includes end-of-line symbol. Meanwhile Vim's readfile command omits new lines (\n) and carriage return (\r) symbols if there are any. --- autoload/ale/code_action.vim | 6 ++++++ test/test_code_action_python.vader | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 test/test_code_action_python.vader diff --git a/autoload/ale/code_action.vim b/autoload/ale/code_action.vim index 42f4f26553..e8681c32d6 100644 --- a/autoload/ale/code_action.vim +++ b/autoload/ale/code_action.vim @@ -125,6 +125,12 @@ function! ale#code_action#ApplyChanges(filename, changes, should_save) abort let l:start = l:lines[: l:line - 2] endif + " Special case when text must be added after new line + if l:column > len(l:lines[l:line - 1]) + call extend(l:start, [l:lines[l:line - 1]]) + let l:column = 1 + endif + if l:column is 1 " We need to handle column 1 specially, because we can't slice an " empty string ending on index 0. diff --git a/test/test_code_action_python.vader b/test/test_code_action_python.vader new file mode 100644 index 0000000000..36c6a67f9d --- /dev/null +++ b/test/test_code_action_python.vader @@ -0,0 +1,23 @@ +Given python(An example Python file): + def main(): + a = 1 + c = a + 1 + +Execute(): + let g:changes = [ + \ {'end': {'offset': 7, 'line': 1}, 'newText': 'func_qtffgsv', 'start': {'offset': 5, 'line': 1}}, + \ {'end': {'offset': 9, 'line': 1}, 'newText': '', 'start': {'offset': 8, 'line': 1}}, + \ {'end': {'offset': 15, 'line': 3}, 'newText': " return c\n\n\ndef main():\n c = func_qtffgsvi()\n", 'start': {'offset': 15, 'line': 3}} + \] + + call ale#code_action#ApplyChanges(expand('%:p'), g:changes, 0) + +Expect(The changes should be applied correctly): + def func_qtffgsvi(): + a = 1 + c = a + 1 + return c + + + def main(): + c = func_qtffgsvi() From 747cea90daec5b2d8208106c90de6710fa4d9393 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Thu, 12 Nov 2020 01:13:27 +0200 Subject: [PATCH 13/26] Text fixed. --- test/test_code_action.vader | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/test_code_action.vader b/test/test_code_action.vader index 2e5d138152..7eabb75948 100644 --- a/test/test_code_action.vader +++ b/test/test_code_action.vader @@ -3,6 +3,9 @@ Before: let g:ale_enabled = 0 + " Enable fix end-of-line as tests below expect that + set fixeol + runtime autoload/ale/code_action.vim runtime autoload/ale/util.vim @@ -211,6 +214,7 @@ Execute(End of file can be modified): \) AssertEqual g:test.text + [ + \ '', \ 'type A: string', \ 'type B: number', \ '', From 5cfc6445c85035f27f855b75f8bf97c5721e8bbc Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Thu, 12 Nov 2020 16:34:12 +0200 Subject: [PATCH 14/26] ale#code_action#ApplyChanges simplified. Previously code was applied from top-to-bottom that required extra parameters to track progress and there was bug. I have changed code to bottom-to-top approach as that does not require those extra parameters and solved the bug. --- autoload/ale/code_action.vim | 42 +++++++++--------------------- test/test_code_action_python.vader | 36 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/autoload/ale/code_action.vim b/autoload/ale/code_action.vim index e8681c32d6..b76f79a85e 100644 --- a/autoload/ale/code_action.vim +++ b/autoload/ale/code_action.vim @@ -33,35 +33,35 @@ endfunction function! s:ChangeCmp(left, right) abort if a:left.start.line < a:right.start.line - return -1 + return 1 endif if a:left.start.line > a:right.start.line - return 1 + return -1 endif if a:left.start.offset < a:right.start.offset - return -1 + return 1 endif if a:left.start.offset > a:right.start.offset - return 1 + return -1 endif if a:left.end.line < a:right.end.line - return -1 + return 1 endif if a:left.end.line > a:right.end.line - return 1 + return -1 endif if a:left.end.offset < a:right.end.offset - return -1 + return 1 endif if a:left.end.offset > a:right.end.offset - return 1 + return -1 endif return 0 @@ -85,29 +85,14 @@ function! ale#code_action#ApplyChanges(filename, changes, should_save) abort let l:pos = [1, 1] endif - " We have to keep track of how many lines we have added, and offset - " changes accordingly. - let l:line_offset = 0 - let l:column_offset = 0 - let l:last_end_line = 0 - - " Changes have to be sorted so we apply them from top-to-bottom. + " Changes have to be sorted so we apply them from bottom-to-top for l:code_edit in sort(copy(a:changes), function('s:ChangeCmp')) - if l:code_edit.start.line isnot l:last_end_line - let l:column_offset = 0 - endif - - let l:line = l:code_edit.start.line + l:line_offset - let l:column = l:code_edit.start.offset + l:column_offset - let l:end_line = l:code_edit.end.line + l:line_offset - let l:end_column = l:code_edit.end.offset + l:column_offset + let l:line = l:code_edit.start.line + let l:column = l:code_edit.start.offset + let l:end_line = l:code_edit.end.line + let l:end_column = l:code_edit.end.offset let l:text = l:code_edit.newText - let l:cur_line = l:pos[0] - let l:cur_column = l:pos[1] - - let l:last_end_line = l:end_line - " Adjust the ends according to previous edits. if l:end_line > len(l:lines) let l:end_line_len = 0 @@ -146,7 +131,6 @@ function! ale#code_action#ApplyChanges(filename, changes, should_save) abort let l:lines = l:start + l:middle + l:lines[l:end_line :] let l:current_line_offset = len(l:lines) - l:lines_before_change - let l:line_offset += l:current_line_offset let l:column_offset = len(l:middle[-1]) - l:end_line_len let l:pos = s:UpdateCursor(l:pos, diff --git a/test/test_code_action_python.vader b/test/test_code_action_python.vader index 36c6a67f9d..fd30633de2 100644 --- a/test/test_code_action_python.vader +++ b/test/test_code_action_python.vader @@ -21,3 +21,39 @@ Expect(The changes should be applied correctly): def main(): c = func_qtffgsvi() + + +Given python(Second python example): + import sys + import exifread + + def main(): + with open(sys.argv[1], 'rb') as f: + exif = exifread.process_file(f) + dt = str(exif['Image DateTime']) + date = dt[:10].replace(':', '-') + +Execute(): + let g:changes = [ + \ {'end': {'offset': 16, 'line': 2}, 'newText': "\n\ndef func_ivlpdpao(f):\n exif = exifread.process_file(f)\n dt = str(exif['Image DateTime'])\n date = dt[:10].replace(':', '-')\n return date\n", 'start': {'offset': 16, 'line': 2}}, + \ {'end': {'offset': 32, 'line': 6}, 'newText': 'date = func', 'start': {'offset': 9, 'line': 6}}, + \ {'end': {'offset': 42, 'line': 8}, 'newText': "ivlpdpao(f)\n", 'start': {'offset': 33, 'line': 6}} + \] + + call ale#code_action#ApplyChanges(expand('%:p'), g:changes, 0) + +Expect(The changes should be applied correctly): + import sys + import exifread + + + def func_ivlpdpao(f): + exif = exifread.process_file(f) + dt = str(exif['Image DateTime']) + date = dt[:10].replace(':', '-') + return date + + + def main(): + with open(sys.argv[1], 'rb') as f: + date = func_ivlpdpao(f) From 3dbac7436bea2971926e608dc7d932338bdbccd8 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Mon, 9 Nov 2020 22:05:47 +0200 Subject: [PATCH 15/26] Initial attempt on LSP Code Actions. --- ale_linters/python/jedils.vim | 34 +++++++++++ autoload/ale/code_action.vim | 58 +++++++++++++++++++ autoload/ale/codefix.vim | 106 ++++++++++++++++++++++++---------- autoload/ale/lsp.vim | 8 +++ autoload/ale/lsp/message.vim | 15 +++++ autoload/ale/rename.vim | 56 +----------------- 6 files changed, 193 insertions(+), 84 deletions(-) create mode 100644 ale_linters/python/jedils.vim diff --git a/ale_linters/python/jedils.vim b/ale_linters/python/jedils.vim new file mode 100644 index 0000000000..eae5fb0756 --- /dev/null +++ b/ale_linters/python/jedils.vim @@ -0,0 +1,34 @@ +" Author: Dalius Dobravolskas +" Description: https://github.com/pappasam/jedi-language-server + +call ale#Set('python_jedils_executable', 'jedi-language-server') +call ale#Set('python_jedils_use_global', get(g:, 'ale_use_global_executables', 0)) +call ale#Set('python_jedils_auto_pipenv', 0) + +function! ale_linters#python#jedils#GetExecutable(buffer) abort + if (ale#Var(a:buffer, 'python_auto_pipenv') || ale#Var(a:buffer, 'python_jedils_auto_pipenv')) + \ && ale#python#PipenvPresent(a:buffer) + return 'pipenv' + endif + + return ale#python#FindExecutable(a:buffer, 'python_jedils', ['jedi-language-server']) +endfunction + +function! ale_linters#python#jedils#GetCommand(buffer) abort + let l:executable = ale_linters#python#jedils#GetExecutable(a:buffer) + + let l:exec_args = l:executable =~? 'pipenv$' + \ ? ' run jedi-language-server' + \ : '' + + return ale#Escape(l:executable) . l:exec_args +endfunction + +call ale#linter#Define('python', { +\ 'name': 'jedils', +\ 'lsp': 'stdio', +\ 'executable': function('ale_linters#python#jedils#GetExecutable'), +\ 'command': function('ale_linters#python#jedils#GetCommand'), +\ 'project_root': function('ale#python#FindProjectRoot'), +\ 'completion_filter': 'ale#completion#python#CompletionItemFilter', +\}) diff --git a/autoload/ale/code_action.vim b/autoload/ale/code_action.vim index b76f79a85e..4dbb2d0844 100644 --- a/autoload/ale/code_action.vim +++ b/autoload/ale/code_action.vim @@ -205,3 +205,61 @@ function! s:UpdateCursor(cursor, start, end, offset) abort return [l:cur_line, l:cur_column] endfunction + +function! ale#code_action#GetChanges(workspace_edit) abort + let l:changes = {} + + if has_key(a:workspace_edit, 'changes') && !empty(a:workspace_edit.changes) + return a:workspace_edit.changes + elseif has_key(a:workspace_edit, 'documentChanges') + let l:document_changes = [] + + if type(a:workspace_edit.documentChanges) is v:t_dict + \ && has_key(a:workspace_edit.documentChanges, 'edits') + call add(l:document_changes, a:workspace_edit.documentChanges) + elseif type(a:workspace_edit.documentChanges) is v:t_list + let l:document_changes = a:workspace_edit.documentChanges + endif + + for l:text_document_edit in l:document_changes + let l:filename = l:text_document_edit.textDocument.uri + let l:edits = l:text_document_edit.edits + let l:changes[l:filename] = l:edits + endfor + endif + + return l:changes +endfunction + +function! ale#code_action#BuildChangesList(changes_map) abort + let l:changes = [] + + for l:file_name in keys(a:changes_map) + let l:text_edits = a:changes_map[l:file_name] + let l:text_changes = [] + + for l:edit in l:text_edits + let l:range = l:edit.range + let l:new_text = l:edit.newText + + call add(l:text_changes, { + \ 'start': { + \ 'line': l:range.start.line + 1, + \ 'offset': l:range.start.character + 1, + \ }, + \ 'end': { + \ 'line': l:range.end.line + 1, + \ 'offset': l:range.end.character + 1, + \ }, + \ 'newText': l:new_text, + \}) + endfor + + call add(l:changes, { + \ 'fileName': ale#path#FromURI(l:file_name), + \ 'textChanges': l:text_changes, + \}) + endfor + + return l:changes +endfunction diff --git a/autoload/ale/codefix.vim b/autoload/ale/codefix.vim index 29e27eca97..57a77af1f0 100644 --- a/autoload/ale/codefix.vim +++ b/autoload/ale/codefix.vim @@ -144,14 +144,52 @@ function! ale#codefix#HandleTSServerResponse(conn_id, response) abort endif endfunction -function! s:OnReady(line, column, end_line, end_column, linter, lsp_details) abort - let l:id = a:lsp_details.connection_id +function! ale#codefix#HandleLSPResponse(conn_id, response) abort + if has_key(a:response, 'id') + \&& has_key(s:codefix_map, a:response.id) + let l:options = remove(s:codefix_map, a:response.id) - if a:linter.lsp isnot# 'tsserver' - call s:message('ALECodeAction currently only works with tsserver') + if !has_key(a:response, 'result') || type(a:response.result) != v:t_list + call s:message('No code actions received from server') - return + return + endif + + let l:codeaction_no = 1 + let l:codeactionstring = "Code Fixes:\n" + + for l:codeaction in a:response.result + let l:codeactionstring .= l:codeaction_no . ') ' . l:codeaction.title . "\n" + let l:codeaction_no += 1 + endfor + + let l:codeactionstring .= 'Type number and (empty cancels): ' + + let l:codeaction_to_apply = ale#util#Input(l:codeactionstring, '') + let l:codeaction_to_apply = str2nr(l:codeaction_to_apply) + + if l:codeaction_to_apply == 0 + return + endif + + let l:changes_map = ale#code_action#GetChanges(a:response.result[l:codeaction_to_apply - 1].edit) + + if empty(l:changes_map) + return + endif + + let l:changes = ale#code_action#BuildChangesList(l:changes_map) + + call ale#code_action#HandleCodeAction({ + \ 'description': 'codeaction', + \ 'changes': l:changes, + \}, {}) endif +endfunction + + +function! s:OnReady(line, column, end_line, end_column, linter, lsp_details) abort + let l:id = a:lsp_details.connection_id if !ale#lsp#HasCapability(l:id, 'code_actions') return @@ -159,26 +197,26 @@ function! s:OnReady(line, column, end_line, end_column, linter, lsp_details) abo let l:buffer = a:lsp_details.buffer - if a:line == a:end_line && a:column == a:end_column - if !has_key(g:ale_buffer_info, l:buffer) - return - endif + if a:linter.lsp is# 'tsserver' + if a:line == a:end_line && a:column == a:end_column + if !has_key(g:ale_buffer_info, l:buffer) + return + endif - let l:nearest_error = v:null - let l:nearest_error_diff = -1 + let l:nearest_error = v:null + let l:nearest_error_diff = -1 - for l:error in get(g:ale_buffer_info[l:buffer], 'loclist', []) - if has_key(l:error, 'code') && l:error.lnum == a:line - let l:diff = abs(l:error.col - a:column) + for l:error in get(g:ale_buffer_info[l:buffer], 'loclist', []) + if has_key(l:error, 'code') && l:error.lnum == a:line + let l:diff = abs(l:error.col - a:column) - if l:nearest_error_diff == -1 || l:diff < l:nearest_error_diff - let l:nearest_error_diff = l:diff - let l:nearest_error = l:error.code + if l:nearest_error_diff == -1 || l:diff < l:nearest_error_diff + let l:nearest_error_diff = l:diff + let l:nearest_error = l:error.code + endif endif - endif - endfor + endfor - if a:linter.lsp is# 'tsserver' let l:message = ale#lsp#tsserver_message#GetCodeFixes( \ l:buffer, \ a:line, @@ -187,9 +225,7 @@ function! s:OnReady(line, column, end_line, end_column, linter, lsp_details) abo \ a:column, \ [l:nearest_error], \) - endif - else - if a:linter.lsp is# 'tsserver' + else let l:message = ale#lsp#tsserver_message#GetApplicableRefactors( \ l:buffer, \ a:line, @@ -198,9 +234,23 @@ function! s:OnReady(line, column, end_line, end_column, linter, lsp_details) abo \ a:end_column, \) endif + else + " Send a message saying the buffer has changed first, otherwise + " completions won't know what text is nearby. + call ale#lsp#NotifyForChanges(l:id, l:buffer) + + let l:message = ale#lsp#message#CodeAction( + \ l:buffer, + \ a:line, + \ a:column, + \ a:end_line, + \ a:end_column, + \) endif - let l:Callback = function('ale#codefix#HandleTSServerResponse') + let l:Callback = a:linter.lsp is# 'tsserver' + \ ? function('ale#codefix#HandleTSServerResponse') + \ : function('ale#codefix#HandleLSPResponse') call ale#lsp#RegisterCallback(l:id, l:Callback) @@ -226,14 +276,10 @@ function! s:ExecuteGetCodeFix(linter, range) abort else let [l:line, l:column] = getpos("'<")[1:2] let [l:end_line, l:end_column] = getpos("'>")[1:2] - - let l:column = min([l:column, len(getline(l:line))]) - let l:end_column = min([l:end_column, len(getline(l:end_line))]) endif - if a:linter.lsp isnot# 'tsserver' - let l:column = min([l:column, len(getline(l:line))]) - endif + let l:column = min([l:column, len(getline(l:line))]) + let l:end_column = min([l:end_column, len(getline(l:end_line))]) let l:Callback = function( \ 's:OnReady', [l:line, l:column, l:end_line, l:end_column]) diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index 53da74fbad..cb0573aa69 100644 --- a/autoload/ale/lsp.vim +++ b/autoload/ale/lsp.vim @@ -220,6 +220,14 @@ function! s:UpdateCapabilities(conn, capabilities) abort let a:conn.capabilities.rename = 1 endif + if get(a:capabilities, 'codeActionProvider') is v:true + let a:conn.capabilities.code_actions = 1 + endif + + if type(get(a:capabilities, 'codeActionProvider')) is v:t_dict + let a:conn.capabilities.code_actions = 1 + endif + if !empty(get(a:capabilities, 'completionProvider')) let a:conn.capabilities.completion = 1 endif diff --git a/autoload/ale/lsp/message.vim b/autoload/ale/lsp/message.vim index 5b0cb8b7b8..48f068a80c 100644 --- a/autoload/ale/lsp/message.vim +++ b/autoload/ale/lsp/message.vim @@ -172,3 +172,18 @@ function! ale#lsp#message#Rename(buffer, line, column, new_name) abort \ 'newName': a:new_name, \}] endfunction + +function! ale#lsp#message#CodeAction(buffer, line, column, end_line, end_column) abort + return [0, 'textDocument/codeAction', { + \ 'textDocument': { + \ 'uri': ale#path#ToURI(expand('#' . a:buffer . ':p')), + \ }, + \ 'range': { + \ 'start': {'line': a:line - 1, 'character': a:column - 1}, + \ 'end': {'line': a:end_line - 1, 'character': a:end_column - 1}, + \ }, + \ 'context': { + \ 'diagnostics': [] + \ }, + \}] +endfunction diff --git a/autoload/ale/rename.vim b/autoload/ale/rename.vim index 8190411db2..0d074c245f 100644 --- a/autoload/ale/rename.vim +++ b/autoload/ale/rename.vim @@ -90,31 +90,6 @@ function! ale#rename#HandleTSServerResponse(conn_id, response) abort \) endfunction -function! s:getChanges(workspace_edit) abort - let l:changes = {} - - if has_key(a:workspace_edit, 'changes') && !empty(a:workspace_edit.changes) - return a:workspace_edit.changes - elseif has_key(a:workspace_edit, 'documentChanges') - let l:document_changes = [] - - if type(a:workspace_edit.documentChanges) is v:t_dict - \ && has_key(a:workspace_edit.documentChanges, 'edits') - call add(l:document_changes, a:workspace_edit.documentChanges) - elseif type(a:workspace_edit.documentChanges) is v:t_list - let l:document_changes = a:workspace_edit.documentChanges - endif - - for l:text_document_edit in l:document_changes - let l:filename = l:text_document_edit.textDocument.uri - let l:edits = l:text_document_edit.edits - let l:changes[l:filename] = l:edits - endfor - endif - - return l:changes -endfunction - function! ale#rename#HandleLSPResponse(conn_id, response) abort if has_key(a:response, 'id') \&& has_key(s:rename_map, a:response.id) @@ -126,7 +101,7 @@ function! ale#rename#HandleLSPResponse(conn_id, response) abort return endif - let l:changes_map = s:getChanges(a:response.result) + let l:changes_map = ale#code_action#GetChanges(a:response.result) if empty(l:changes_map) call s:message('No changes received from server') @@ -134,34 +109,7 @@ function! ale#rename#HandleLSPResponse(conn_id, response) abort return endif - let l:changes = [] - - for l:file_name in keys(l:changes_map) - let l:text_edits = l:changes_map[l:file_name] - let l:text_changes = [] - - for l:edit in l:text_edits - let l:range = l:edit.range - let l:new_text = l:edit.newText - - call add(l:text_changes, { - \ 'start': { - \ 'line': l:range.start.line + 1, - \ 'offset': l:range.start.character + 1, - \ }, - \ 'end': { - \ 'line': l:range.end.line + 1, - \ 'offset': l:range.end.character + 1, - \ }, - \ 'newText': l:new_text, - \}) - endfor - - call add(l:changes, { - \ 'fileName': ale#path#FromURI(l:file_name), - \ 'textChanges': l:text_changes, - \}) - endfor + let l:changes = ale#code_action#BuildChangesList(l:changes_map) call ale#code_action#HandleCodeAction( \ { From 2d74cded3955988a43a7a3e33109da2af440483f Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Tue, 10 Nov 2020 18:07:02 +0200 Subject: [PATCH 16/26] Minor fix. --- autoload/ale/codefix.vim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/autoload/ale/codefix.vim b/autoload/ale/codefix.vim index 57a77af1f0..e205fabec2 100644 --- a/autoload/ale/codefix.vim +++ b/autoload/ale/codefix.vim @@ -149,7 +149,9 @@ function! ale#codefix#HandleLSPResponse(conn_id, response) abort \&& has_key(s:codefix_map, a:response.id) let l:options = remove(s:codefix_map, a:response.id) - if !has_key(a:response, 'result') || type(a:response.result) != v:t_list + if !has_key(a:response, 'result') + \ || type(a:response.result) != v:t_list + \ || len(a:response.result) == 0 call s:message('No code actions received from server') return From e2839d2b0bdc6c833d2f8f0d9ffcefac911581f0 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Wed, 11 Nov 2020 00:43:49 +0200 Subject: [PATCH 17/26] workspace/executeCommand added. --- autoload/ale/codefix.vim | 51 ++++++++++++++++++++++++++++-------- autoload/ale/lsp/message.vim | 7 +++++ 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/autoload/ale/codefix.vim b/autoload/ale/codefix.vim index e205fabec2..1ac3275d26 100644 --- a/autoload/ale/codefix.vim +++ b/autoload/ale/codefix.vim @@ -145,9 +145,26 @@ function! ale#codefix#HandleTSServerResponse(conn_id, response) abort endfunction function! ale#codefix#HandleLSPResponse(conn_id, response) abort - if has_key(a:response, 'id') + if has_key(a:response, 'method') + \ && a:response.method == 'workspace/applyEdit' + \ && has_key(a:response, 'params') + let l:params = a:response.params + + let l:changes_map = ale#code_action#GetChanges(l:params.edit) + + if empty(l:changes_map) + return + endif + + let l:changes = ale#code_action#BuildChangesList(l:changes_map) + + call ale#code_action#HandleCodeAction({ + \ 'description': 'applyEdit', + \ 'changes': l:changes, + \}, {}) + elseif has_key(a:response, 'id') \&& has_key(s:codefix_map, a:response.id) - let l:options = remove(s:codefix_map, a:response.id) + let l:location = remove(s:codefix_map, a:response.id) if !has_key(a:response, 'result') \ || type(a:response.result) != v:t_list @@ -174,18 +191,30 @@ function! ale#codefix#HandleLSPResponse(conn_id, response) abort return endif - let l:changes_map = ale#code_action#GetChanges(a:response.result[l:codeaction_to_apply - 1].edit) + let l:item = a:response.result[l:codeaction_to_apply - 1] - if empty(l:changes_map) - return - endif + if has_key(l:item, 'command') + let l:command = l:item.command + let l:message = ale#lsp#message#ExecuteCommand( + \ l:command.command, + \ l:command.arguments, + \) - let l:changes = ale#code_action#BuildChangesList(l:changes_map) + let l:request_id = ale#lsp#Send(l:location.connection_id, l:message) + elseif has_key(l:item, 'edit') + let l:changes_map = ale#code_action#GetChanges(l:item.edit) - call ale#code_action#HandleCodeAction({ - \ 'description': 'codeaction', - \ 'changes': l:changes, - \}, {}) + if empty(l:changes_map) + return + endif + + let l:changes = ale#code_action#BuildChangesList(l:changes_map) + + call ale#code_action#HandleCodeAction({ + \ 'description': 'codeaction', + \ 'changes': l:changes, + \}, {}) + endif endif endfunction diff --git a/autoload/ale/lsp/message.vim b/autoload/ale/lsp/message.vim index 48f068a80c..0b2bc43b43 100644 --- a/autoload/ale/lsp/message.vim +++ b/autoload/ale/lsp/message.vim @@ -187,3 +187,10 @@ function! ale#lsp#message#CodeAction(buffer, line, column, end_line, end_column) \ }, \}] endfunction + +function! ale#lsp#message#ExecuteCommand(command, arguments) abort + return [0, 'workspace/executeCommand', { + \ 'command': a:command, + \ 'arguments': a:arguments, + \}] +endfunction From 8b6df354ddb583050eb507fed6c1e933b2b03636 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Wed, 11 Nov 2020 01:00:05 +0200 Subject: [PATCH 18/26] Minor improvement. --- autoload/ale/codefix.vim | 1 + 1 file changed, 1 insertion(+) diff --git a/autoload/ale/codefix.vim b/autoload/ale/codefix.vim index 1ac3275d26..a022aa5b97 100644 --- a/autoload/ale/codefix.vim +++ b/autoload/ale/codefix.vim @@ -194,6 +194,7 @@ function! ale#codefix#HandleLSPResponse(conn_id, response) abort let l:item = a:response.result[l:codeaction_to_apply - 1] if has_key(l:item, 'command') + \ && type(l:item.command) == v:t_dict let l:command = l:item.command let l:message = ale#lsp#message#ExecuteCommand( \ l:command.command, From 86a34f96ff09b349db20b8f9abee25765e639a4a Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Thu, 12 Nov 2020 12:34:54 +0200 Subject: [PATCH 19/26] Some null checks added. E.g.: jedi-language-server returns null in some cases. --- autoload/ale/completion.vim | 1 + autoload/ale/lsp/response.vim | 1 + 2 files changed, 2 insertions(+) diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim index efbf0fd53b..48e9bf7c8e 100644 --- a/autoload/ale/completion.vim +++ b/autoload/ale/completion.vim @@ -617,6 +617,7 @@ function! ale#completion#ParseLSPCompletions(response) abort let l:user_data = {'_ale_completion_item': 1} if has_key(l:item, 'additionalTextEdits') + \ && l:item.additionalTextEdits isnot v:null let l:text_changes = [] for l:edit in l:item.additionalTextEdits diff --git a/autoload/ale/lsp/response.vim b/autoload/ale/lsp/response.vim index 30da77e1ca..a4f809802e 100644 --- a/autoload/ale/lsp/response.vim +++ b/autoload/ale/lsp/response.vim @@ -56,6 +56,7 @@ function! ale#lsp#response#ReadDiagnostics(response) abort endif if has_key(l:diagnostic, 'relatedInformation') + \ && l:diagnostic.relatedInformation isnot v:null let l:related = deepcopy(l:diagnostic.relatedInformation) call map(l:related, {key, val -> \ ale#path#FromURI(val.location.uri) . From ce2f468f7cff367d38b8a20d8e79eedd7a7f9d0c Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Thu, 12 Nov 2020 17:16:36 +0200 Subject: [PATCH 20/26] Add last column to LSP Code Action message. --- autoload/ale/lsp/message.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autoload/ale/lsp/message.vim b/autoload/ale/lsp/message.vim index 0b2bc43b43..724fc04ebe 100644 --- a/autoload/ale/lsp/message.vim +++ b/autoload/ale/lsp/message.vim @@ -180,7 +180,7 @@ function! ale#lsp#message#CodeAction(buffer, line, column, end_line, end_column) \ }, \ 'range': { \ 'start': {'line': a:line - 1, 'character': a:column - 1}, - \ 'end': {'line': a:end_line - 1, 'character': a:end_column - 1}, + \ 'end': {'line': a:end_line - 1, 'character': a:end_column}, \ }, \ 'context': { \ 'diagnostics': [] From 0c7021ce7c6c3d7e0009601f543a998134cf26d2 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Thu, 12 Nov 2020 17:59:25 +0200 Subject: [PATCH 21/26] Pass diagnostics to LSP code action. Tested with typescript-language-server and it is working. --- autoload/ale/codefix.vim | 67 +++++++++++++++++++++++++++++++----- autoload/ale/lsp/message.vim | 4 +-- 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/autoload/ale/codefix.vim b/autoload/ale/codefix.vim index a022aa5b97..0a0adf0ed5 100644 --- a/autoload/ale/codefix.vim +++ b/autoload/ale/codefix.vim @@ -202,8 +202,14 @@ function! ale#codefix#HandleLSPResponse(conn_id, response) abort \) let l:request_id = ale#lsp#Send(l:location.connection_id, l:message) - elseif has_key(l:item, 'edit') - let l:changes_map = ale#code_action#GetChanges(l:item.edit) + elseif has_key(l:item, 'edit') || has_key(l:item, 'arguments') + if has_key(l:item, 'edit') + let l:topass = l:item.edit + else + let l:topass = l:item.arguments[0] + endif + + let l:changes_map = ale#code_action#GetChanges(l:topass) if empty(l:changes_map) return @@ -271,13 +277,56 @@ function! s:OnReady(line, column, end_line, end_column, linter, lsp_details) abo " completions won't know what text is nearby. call ale#lsp#NotifyForChanges(l:id, l:buffer) - let l:message = ale#lsp#message#CodeAction( - \ l:buffer, - \ a:line, - \ a:column, - \ a:end_line, - \ a:end_column, - \) + if a:line == a:end_line && a:column == a:end_column + if !has_key(g:ale_buffer_info, l:buffer) + return + endif + + let l:nearest_error = v:null + let l:nearest_error_diff = -1 + + for l:error in get(g:ale_buffer_info[l:buffer], 'loclist', []) + if has_key(l:error, 'code') && l:error.lnum == a:line + let l:diff = abs(l:error.col - a:column) + + if l:nearest_error_diff == -1 || l:diff < l:nearest_error_diff + let l:nearest_error_diff = l:diff + let l:nearest_error = l:error + endif + endif + endfor + + let l:diagnostics = [] + + if l:nearest_error isnot v:null + let l:diagnostics = [{ + \ 'code': l:nearest_error.code, + \ 'message': l:nearest_error.text, + \ 'range': { + \ 'start': { 'line': l:nearest_error.lnum - 1, 'character': l:nearest_error.col - 1 }, + \ 'end': { 'line': l:nearest_error.end_lnum - 1, 'character': l:nearest_error.end_col - 1 } + \} + \}] + endif + + let l:message = ale#lsp#message#CodeAction( + \ l:buffer, + \ a:line, + \ a:column, + \ a:end_line, + \ a:end_column, + \ l:diagnostics, + \) + else + let l:message = ale#lsp#message#CodeAction( + \ l:buffer, + \ a:line, + \ a:column, + \ a:end_line, + \ a:end_column, + \ [], + \) + endif endif let l:Callback = a:linter.lsp is# 'tsserver' diff --git a/autoload/ale/lsp/message.vim b/autoload/ale/lsp/message.vim index 724fc04ebe..38be4da67d 100644 --- a/autoload/ale/lsp/message.vim +++ b/autoload/ale/lsp/message.vim @@ -173,7 +173,7 @@ function! ale#lsp#message#Rename(buffer, line, column, new_name) abort \}] endfunction -function! ale#lsp#message#CodeAction(buffer, line, column, end_line, end_column) abort +function! ale#lsp#message#CodeAction(buffer, line, column, end_line, end_column, diagnostics) abort return [0, 'textDocument/codeAction', { \ 'textDocument': { \ 'uri': ale#path#ToURI(expand('#' . a:buffer . ':p')), @@ -183,7 +183,7 @@ function! ale#lsp#message#CodeAction(buffer, line, column, end_line, end_column) \ 'end': {'line': a:end_line - 1, 'character': a:end_column}, \ }, \ 'context': { - \ 'diagnostics': [] + \ 'diagnostics': a:diagnostics \ }, \}] endfunction From d3859201e9dd6a9c4c6f4c589b03b6221b208014 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Fri, 13 Nov 2020 01:12:31 +0200 Subject: [PATCH 22/26] Couple new tests added. --- test/test_codefix.vader | 86 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/test/test_codefix.vader b/test/test_codefix.vader index 0241c83090..6a9364648c 100644 --- a/test/test_codefix.vader +++ b/test/test_codefix.vader @@ -460,3 +460,89 @@ Execute(getEditsForRefactor should print error on failure): " \ }] " \ ], " \ g:message_list + + +Given python(Some python file): + def main(): + a = 1 + b = a + 2 + +Execute(LSP code action requests should be sent): + call ale#linter#Reset() + + runtime ale_linters/python/jedils.vim + let g:ale_buffer_info = {bufnr(''): {'loclist': [{'lnum': 2, 'col': 5, 'end_lnum': 2, 'end_col': 6, 'code': 2304, 'text': 'oops'}]}} + call setpos('.', [bufnr(''), 2, 5, 0]) + + " ALECodeAction + call ale#codefix#Execute(0) + + " We shouldn't register the callback yet. + AssertEqual '''''', string(g:Callback) + + AssertEqual type(function('type')), type(g:InitCallback) + call g:InitCallback() + + AssertEqual 'code_actions', g:capability_checked + AssertEqual + \ 'function(''ale#codefix#HandleLSPResponse'')', + \ string(g:Callback) + AssertEqual + \ [ + \ [1, 'workspace/didChangeConfiguration', {'settings': {'python': {}}}], + \ [1, 'textDocument/didChange', { + \ 'contentChanges': [{'text': "def main():\n a = 1\n b = a + 2\n"}], + \ 'textDocument': { + \ 'uri': ale#path#ToURI(expand('%:p')), + \ 'version': g:ale_lsp_next_version_id - 1, + \ }, + \ }], + \ [0, 'textDocument/codeAction', { + \ 'context': { + \ 'diagnostics': [{'range': {'end': {'character': 5, 'line': 1}, 'start': {'character': 4, 'line': 1}}, 'code': 2304, 'message': 'oops'}] + \ }, + \ 'range': {'end': {'character': 5, 'line': 1}, 'start': {'character': 4, 'line': 1}}, + \ 'textDocument': {'uri': ale#path#ToURI(expand('%:p'))} + \ }] + \ ], + \ g:message_list + +Execute(LSP code action requests should be sent only for error with code): + call ale#linter#Reset() + + runtime ale_linters/python/jedils.vim + let g:ale_buffer_info = {bufnr(''): {'loclist': [{'lnum': 2, 'col': 5, 'end_lnum': 2, 'end_col': 6, 'code': 2304, 'text': 'oops'}]}} + call setpos('.', [bufnr(''), 2, 5, 0]) + + " ALECodeAction + call ale#codefix#Execute(0) + + " We shouldn't register the callback yet. + AssertEqual '''''', string(g:Callback) + + AssertEqual type(function('type')), type(g:InitCallback) + call g:InitCallback() + + AssertEqual 'code_actions', g:capability_checked + AssertEqual + \ 'function(''ale#codefix#HandleLSPResponse'')', + \ string(g:Callback) + AssertEqual + \ [ + \ [1, 'workspace/didChangeConfiguration', {'settings': {'python': {}}}], + \ [1, 'textDocument/didChange', { + \ 'contentChanges': [{'text': "def main():\n a = 1\n b = a + 2\n"}], + \ 'textDocument': { + \ 'uri': ale#path#ToURI(expand('%:p')), + \ 'version': g:ale_lsp_next_version_id - 1, + \ }, + \ }], + \ [0, 'textDocument/codeAction', { + \ 'context': { + \ 'diagnostics': [{'range': {'end': {'character': 5, 'line': 1}, 'start': {'character': 4, 'line': 1}}, 'code': 2304, 'message': 'oops'}] + \ }, + \ 'range': {'end': {'character': 5, 'line': 1}, 'start': {'character': 4, 'line': 1}}, + \ 'textDocument': {'uri': ale#path#ToURI(expand('%:p'))} + \ }] + \ ], + \ g:message_list From a258a518a178c609472a53467e8dd5d00924a7fd Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Fri, 13 Nov 2020 17:38:37 +0200 Subject: [PATCH 23/26] Even more tests. --- test/test_codefix.vader | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/test_codefix.vader b/test/test_codefix.vader index 6a9364648c..836fd401ff 100644 --- a/test/test_codefix.vader +++ b/test/test_codefix.vader @@ -461,12 +461,46 @@ Execute(getEditsForRefactor should print error on failure): " \ ], " \ g:message_list +Execute(Failed LSP responses should be handled correctly): + call ale#codefix#HandleLSPResponse( + \ 1, + \ {'method': 'workspace/applyEdit', 'request_seq': 3} + \) + AssertEqual g:handle_code_action_called, 0 Given python(Some python file): def main(): a = 1 b = a + 2 +Execute("workspace/applyEdit" from LSP should be handled): + call ale#codefix#SetMap({3: {}}) + call ale#codefix#HandleLSPResponse(1, + \ {'id': 0, 'jsonrpc': '2.0', 'method': 'workspace/applyEdit', 'params': {'edit': {'changes': {'file:///foo/bar/file.ts': [{'range': {'end': {'character': 27, 'line': 7}, 'start': {'character': 27, 'line': 7}}, 'newText': ', Config'}, {'range': {'end': {'character': 12, 'line': 96}, 'start': {'character': 2, 'line': 94}}, 'newText': 'await newFunction(redis, imageKey, cover, config);'}, {'range': {'end': {'character': 2, 'line': 99}, 'start': {'character': 2, 'line': 99}}, 'newText': '^@async function newFunction(redis: IRedis, imageKey: string, cover: Buffer, config: Config) {^@ try {^@ await redis.set(imageKey, cover, ''ex'', parseInt(config.coverKeyTTL, 10));^@ }^@ catch { }^@}^@'}]}}}}) + + AssertEqual g:handle_code_action_called, 1 + AssertEqual + \ [{'description': 'applyEdit', 'changes': [{'fileName': '/foo/bar/file.ts', 'textChanges': [{'end': {'offset': 28, 'line': 8}, 'newText': ', Config', 'start': {'offset': 28, 'line': 8}}, {'end': {'offset': 13, 'line': 97}, 'newText': 'await newFunction(redis, imageKey, cover, config);', 'start': {'offset': 3, 'line': 95}}, {'end': {'offset': 3, 'line': 100}, 'newText': '^@async function newFunction(redis: IRedis, imageKey: string, cover: Buffer, config: Config) {^@ try {^@ await redis.set(imageKey, cover, ''ex'', parseInt(config.coverKeyTTL, 10));^@ }^@ catch { }^@}^@', 'start': {'offset': 3, 'line': 100}}]}]}], + \ g:code_actions + +Execute(getCodeFixes from tsserver should be handled with user input if there are more than one action): + call ale#codefix#SetMap({2: {}}) + call ale#codefix#HandleLSPResponse(1, + \ {'id': 2, 'jsonrpc': '2.0', 'result': [{'diagnostics': v:null, 'edit': {'changes': v:null, 'documentChanges': [{'edits': [{'range': {'end': {'character': 4, 'line': 2}, 'start': {'character': 4, 'line': 1}}, 'newText': ''}, {'range': {'end': {'character': 9, 'line': 2}, 'start': {'character': 8, 'line': 2}}, 'newText': '(1)'}], 'textDocument': {'uri': 'file:///foo/bar/test.py', 'version': v:null}}]}, 'kind': 'refactor.inline', 'title': 'Inline variable', 'command': v:null}, {'diagnostics': v:null, 'edit': {'changes': v:null, 'documentChanges': [{'edits': [{'range': {'end': {'character': 0, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': 'def func_bomdjnxh():^@ a = 1return a^@^@^@'}, {'range': {'end': {'character': 9, 'line': 1}, 'start': {'character': 8, 'line': 1}}, 'newText': 'func_bomdjnxh()^@'}], 'textDocument': {'uri': 'file:///foo/bar/test.py', 'version': v:null}}]}, 'kind': 'refactor.extract', 'title': 'Extract expression into function ''func_bomdjnxh''', 'command': v:null}]}) + + AssertEqual g:handle_code_action_called, 1 + AssertEqual + \ [{'description': 'codeaction', 'changes': [{'fileName': '/foo/bar/test.py', 'textChanges': [{'end': {'offset': 1, 'line': 1}, 'newText': 'def func_bomdjnxh():^@ a = 1return a^@^@^@', 'start': {'offset': 1, 'line': 1}}, {'end': {'offset': 10, 'line': 2}, 'newText': 'func_bomdjnxh()^@', 'start': {'offset': 9, 'line': 2}}]}]}], + \ g:code_actions + +Execute(Prints message when LSP code action returns no results): + call ale#codefix#SetMap({3: {}}) + call ale#codefix#HandleLSPResponse(1, + \ {'id': 3, 'jsonrpc': '2.0', 'result': []}) + + AssertEqual g:handle_code_action_called, 0 + AssertEqual ['echom ''No code actions received from server'''], g:expr_list + Execute(LSP code action requests should be sent): call ale#linter#Reset() From a626ca55814d2f68ed84995039edb3fba97f55b8 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Fri, 13 Nov 2020 18:01:28 +0200 Subject: [PATCH 24/26] Even more tests. --- test/test_codefix.vader | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/test/test_codefix.vader b/test/test_codefix.vader index 836fd401ff..63275d7f2e 100644 --- a/test/test_codefix.vader +++ b/test/test_codefix.vader @@ -483,7 +483,17 @@ Execute("workspace/applyEdit" from LSP should be handled): \ [{'description': 'applyEdit', 'changes': [{'fileName': '/foo/bar/file.ts', 'textChanges': [{'end': {'offset': 28, 'line': 8}, 'newText': ', Config', 'start': {'offset': 28, 'line': 8}}, {'end': {'offset': 13, 'line': 97}, 'newText': 'await newFunction(redis, imageKey, cover, config);', 'start': {'offset': 3, 'line': 95}}, {'end': {'offset': 3, 'line': 100}, 'newText': '^@async function newFunction(redis: IRedis, imageKey: string, cover: Buffer, config: Config) {^@ try {^@ await redis.set(imageKey, cover, ''ex'', parseInt(config.coverKeyTTL, 10));^@ }^@ catch { }^@}^@', 'start': {'offset': 3, 'line': 100}}]}]}], \ g:code_actions -Execute(getCodeFixes from tsserver should be handled with user input if there are more than one action): +Execute(Code Actions from LSP should be handled with user input if there are more than one action): + call ale#codefix#SetMap({2: {}}) + call ale#codefix#HandleLSPResponse(1, + \ {'id': 2, 'jsonrpc': '2.0', 'result': [{'title': 'fake for testing'}, {'arguments': [{'documentChanges': [{'edits': [{'range': {'end': {'character': 31, 'line': 2}, 'start': {'character': 31, 'line': 2}}, 'newText': ', createVideo'}], 'textDocument': {'uri': 'file:///foo/bar/file.ts', 'version': 1}}]}], 'title': 'Add ''createVideo'' to existing import declaration from "./video"', 'command': '_typescript.applyWorkspaceEdit'}]}) + + AssertEqual g:handle_code_action_called, 1 + AssertEqual + \ [{'description': 'codeaction', 'changes': [{'fileName': '/foo/bar/file.ts', 'textChanges': [{'end': {'offset': 32, 'line': 3}, 'newText': ', createVideo', 'start': {'offset': 32, 'line': 3}}]}]}], + \ g:code_actions + +Execute(Code Actions from LSP should be handled when returned with documentChanges): call ale#codefix#SetMap({2: {}}) call ale#codefix#HandleLSPResponse(1, \ {'id': 2, 'jsonrpc': '2.0', 'result': [{'diagnostics': v:null, 'edit': {'changes': v:null, 'documentChanges': [{'edits': [{'range': {'end': {'character': 4, 'line': 2}, 'start': {'character': 4, 'line': 1}}, 'newText': ''}, {'range': {'end': {'character': 9, 'line': 2}, 'start': {'character': 8, 'line': 2}}, 'newText': '(1)'}], 'textDocument': {'uri': 'file:///foo/bar/test.py', 'version': v:null}}]}, 'kind': 'refactor.inline', 'title': 'Inline variable', 'command': v:null}, {'diagnostics': v:null, 'edit': {'changes': v:null, 'documentChanges': [{'edits': [{'range': {'end': {'character': 0, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': 'def func_bomdjnxh():^@ a = 1return a^@^@^@'}, {'range': {'end': {'character': 9, 'line': 1}, 'start': {'character': 8, 'line': 1}}, 'newText': 'func_bomdjnxh()^@'}], 'textDocument': {'uri': 'file:///foo/bar/test.py', 'version': v:null}}]}, 'kind': 'refactor.extract', 'title': 'Extract expression into function ''func_bomdjnxh''', 'command': v:null}]}) @@ -493,6 +503,18 @@ Execute(getCodeFixes from tsserver should be handled with user input if there ar \ [{'description': 'codeaction', 'changes': [{'fileName': '/foo/bar/test.py', 'textChanges': [{'end': {'offset': 1, 'line': 1}, 'newText': 'def func_bomdjnxh():^@ a = 1return a^@^@^@', 'start': {'offset': 1, 'line': 1}}, {'end': {'offset': 10, 'line': 2}, 'newText': 'func_bomdjnxh()^@', 'start': {'offset': 9, 'line': 2}}]}]}], \ g:code_actions +Execute(LSP Code Actions handles command responses): + call ale#codefix#SetMap({3: { + \ 'connection_id': 0, + \}}) + call ale#codefix#HandleLSPResponse(1, + \ {'id': 3, 'jsonrpc': '2.0', 'result': [{'kind': 'refactor', 'title': 'Extract to inner function in function ''getVideo''', 'command': {'arguments': [{'file': '/foo/bar/file.ts', 'endOffset': 0, 'action': 'function_scope_0', 'startOffset': 1, 'startLine': 65, 'refactor': 'Extract Symbol', 'endLine': 68}], 'title': 'Extract to inner function in function ''getVideo''', 'command': '_typescript.applyRefactoring'}}, {'kind': 'refactor', 'title': 'Extract to function in module scope', 'command': {'arguments': [{'file': '/foo/bar/file.ts', 'endOffset': 0, 'action': 'function_scope_1', 'startOffset': 1, 'startLine': 65, 'refactor': 'Extract Symbol', 'endLine': 68}], 'title': 'Extract to function in module scope', 'command': '_typescript.applyRefactoring'}}]}) + + AssertEqual + \ [[0, 'workspace/executeCommand', {'arguments': [{'file': '/foo/bar/file.ts', 'action': 'function_scope_1', 'endOffset': 0, 'refactor': 'Extract Symbol', 'endLine': 68, 'startLine': 65, 'startOffset': 1}], 'command': '_typescript.applyRefactoring'}]], + \ g:message_list + + Execute(Prints message when LSP code action returns no results): call ale#codefix#SetMap({3: {}}) call ale#codefix#HandleLSPResponse(1, From 9faca44a4146508fb33578edd4e909479a1b7d03 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Sat, 14 Nov 2020 10:50:27 +0200 Subject: [PATCH 25/26] Tests fixed. --- test/lsp/test_other_initialize_message_handling.vader | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/lsp/test_other_initialize_message_handling.vader b/test/lsp/test_other_initialize_message_handling.vader index b6ef852a1a..f3b538435f 100644 --- a/test/lsp/test_other_initialize_message_handling.vader +++ b/test/lsp/test_other_initialize_message_handling.vader @@ -23,6 +23,7 @@ Before: \ 'completion_trigger_characters': [], \ 'definition': 0, \ 'symbol_search': 0, + \ 'code_actions': 0, \ }, \} @@ -102,6 +103,7 @@ Execute(Capabilities should bet set up correctly): \ 'definition': 1, \ 'symbol_search': 1, \ 'rename': 1, + \ 'code_actions': 1, \ }, \ b:conn.capabilities AssertEqual [[1, 'initialized', {}]], g:message_list @@ -125,7 +127,7 @@ Execute(Disabled capabilities should be recognised correctly): \ 'referencesProvider': v:false, \ 'textDocumentSync': 2, \ 'documentFormattingProvider': v:true, - \ 'codeActionProvider': v:true, + \ 'codeActionProvider': v:false, \ 'signatureHelpProvider': { \ 'triggerCharacters': ['(', ','], \ }, @@ -146,6 +148,7 @@ Execute(Disabled capabilities should be recognised correctly): \ 'definition': 0, \ 'symbol_search': 0, \ 'rename': 0, + \ 'code_actions': 0, \ }, \ b:conn.capabilities AssertEqual [[1, 'initialized', {}]], g:message_list @@ -197,6 +200,7 @@ Execute(Capabilities should be enabled when send as Dictionaries): \ 'typeDefinition': 1, \ 'symbol_search': 1, \ 'rename': 1, + \ 'code_actions': 1, \ }, \ b:conn.capabilities AssertEqual [[1, 'initialized', {}]], g:message_list From 8fe3cc7e06ac58c545b8e162fee20dad092cbb59 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Sat, 14 Nov 2020 10:54:17 +0200 Subject: [PATCH 26/26] One more test fixed. --- autoload/ale/codefix.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autoload/ale/codefix.vim b/autoload/ale/codefix.vim index 0a0adf0ed5..b58f5e4b36 100644 --- a/autoload/ale/codefix.vim +++ b/autoload/ale/codefix.vim @@ -146,7 +146,7 @@ endfunction function! ale#codefix#HandleLSPResponse(conn_id, response) abort if has_key(a:response, 'method') - \ && a:response.method == 'workspace/applyEdit' + \ && a:response.method is# 'workspace/applyEdit' \ && has_key(a:response, 'params') let l:params = a:response.params