Files
ale/autoload/ale/codefix.vim
Tomáš Janoušek efa563826d codefix: Fix code actions that return Command[] directly instead of CodeAction[] (#3929)
According to
https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_codeAction,
the response to textDocument/codeAction is:

    (Command | CodeAction)[] | null

and the code only handled the case where it was a CodeAction that either
specified an edit or a command, but didn't handle a direct Command.

Note that the specification also says that both can be specified and
then the edit is applied first, then the command. Furthermore, there
seems to be some hacky code handling arguments directly, which I suspect
is non-standard and only works with a specific LSP server that happens
to pass the edits in the arguments unmodified.
2021-10-07 11:29:00 +09:00

498 lines
14 KiB
VimL

" Author: Dalius Dobravolskas <dalius.dobravolskas@gmail.com>
" Description: Code Fix support for tsserver and LSP servers
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#ApplyTSServerCodeAction(data, item) abort
if has_key(a:item, 'changes')
let l:changes = a:item.changes
call ale#code_action#HandleCodeAction(
\ {
\ 'description': 'codefix',
\ 'changes': l:changes,
\ },
\ {},
\)
else
let l:message = ale#lsp#tsserver_message#GetEditsForRefactor(
\ a:data.buffer,
\ a:data.line,
\ a:data.column,
\ a:data.end_line,
\ a:data.end_column,
\ a:item.id[0],
\ a:item.id[1],
\)
let l:request_id = ale#lsp#Send(a:data.connection_id, l:message)
let s:codefix_map[l:request_id] = a:data
endif
endfunction
function! ale#codefix#HandleTSServerResponse(conn_id, response) abort
if !has_key(a:response, 'request_seq')
\ || !has_key(s:codefix_map, a:response.request_seq)
return
endif
let l:data = remove(s:codefix_map, a:response.request_seq)
let l:MenuCallback = get(l:data, 'menu_callback', v:null)
if get(a:response, 'command', '') is# 'getCodeFixes'
if get(a:response, 'success', v:false) is v:false
\&& l:MenuCallback is v:null
let l:message = get(a:response, 'message', 'unknown')
call s:message('Error while getting code fixes. Reason: ' . l:message)
return
endif
let l:result = get(a:response, 'body', [])
call filter(l:result, 'has_key(v:val, ''changes'')')
if l:MenuCallback isnot v:null
call l:MenuCallback(
\ l:data,
\ map(copy(l:result), '[''tsserver'', v:val]')
\)
return
endif
if len(l:result) == 0
call s:message('No code fixes available.')
return
endif
let l:code_fix_to_apply = 0
if len(l:result) == 1
let l:code_fix_to_apply = 1
else
let l:codefix_no = 1
let l:codefixstring = "Code Fixes:\n"
for l:codefix in l:result
let l:codefixstring .= l:codefix_no . ') '
\ . l:codefix.description . "\n"
let l:codefix_no += 1
endfor
let l:codefixstring .= 'Type number and <Enter> (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
call ale#codefix#ApplyTSServerCodeAction(
\ l:data,
\ l:result[l:code_fix_to_apply - 1],
\)
elseif get(a:response, 'command', '') is# 'getApplicableRefactors'
if get(a:response, 'success', v:false) is v:false
\&& l:MenuCallback is v:null
let l:message = get(a:response, 'message', 'unknown')
call s:message('Error while getting applicable refactors. Reason: ' . l:message)
return
endif
let l:result = get(a:response, 'body', [])
if len(l:result) == 0
call s:message('No applicable refactors available.')
return
endif
let l:refactors = []
for l:item in l:result
for l:action in l:item.actions
call add(l:refactors, {
\ 'name': l:action.description,
\ 'id': [l:item.name, l:action.name],
\})
endfor
endfor
if l:MenuCallback isnot v:null
call l:MenuCallback(
\ l:data,
\ map(copy(l:refactors), '[''tsserver'', v:val]')
\)
return
endif
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:refactorstring .= 'Type number and <Enter> (empty cancels): '
let l:refactor_to_apply = ale#util#Input(l:refactorstring, '')
let l:refactor_to_apply = str2nr(l:refactor_to_apply)
if l:refactor_to_apply == 0
return
endif
let l:id = l:refactors[l:refactor_to_apply - 1].id
call ale#codefix#ApplyTSServerCodeAction(
\ l:data,
\ l:refactors[l:refactor_to_apply - 1],
\)
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! ale#codefix#ApplyLSPCodeAction(data, item) abort
if has_key(a:item, 'command')
\&& type(a:item.command) == v:t_dict
let l:command = a:item.command
let l:message = ale#lsp#message#ExecuteCommand(
\ l:command.command,
\ l:command.arguments,
\)
let l:request_id = ale#lsp#Send(a:data.connection_id, l:message)
elseif has_key(a:item, 'command') && has_key(a:item, 'arguments')
\&& type(a:item.command) == v:t_string
let l:message = ale#lsp#message#ExecuteCommand(
\ a:item.command,
\ a:item.arguments,
\)
let l:request_id = ale#lsp#Send(a:data.connection_id, l:message)
elseif has_key(a:item, 'edit') || has_key(a:item, 'arguments')
if has_key(a:item, 'edit')
let l:topass = a:item.edit
else
let l:topass = a:item.arguments[0]
endif
let l:changes_map = ale#code_action#GetChanges(l:topass)
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! ale#codefix#HandleLSPResponse(conn_id, response) abort
if has_key(a:response, 'method')
\ && a:response.method is# '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:data = remove(s:codefix_map, a:response.id)
let l:MenuCallback = get(l:data, 'menu_callback', v:null)
let l:result = get(a:response, 'result')
if type(l:result) != v:t_list
let l:result = []
endif
" Send the results to the menu callback, if set.
if l:MenuCallback isnot v:null
call l:MenuCallback(
\ l:data,
\ map(copy(l:result), '[''lsp'', v:val]')
\)
return
endif
if len(l:result) == 0
call s:message('No code actions received from server')
return
endif
let l:codeaction_no = 1
let l:codeactionstring = "Code Fixes:\n"
for l:codeaction in l:result
let l:codeactionstring .= l:codeaction_no . ') '
\ . l:codeaction.title . "\n"
let l:codeaction_no += 1
endfor
let l:codeactionstring .= 'Type number and <Enter> (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:item = l:result[l:codeaction_to_apply - 1]
call ale#codefix#ApplyLSPCodeAction(l:data, l:item)
endif
endfunction
function! s:FindError(buffer, line, column, end_line, end_column, linter_name) abort
let l:nearest_error = v:null
if a:line == a:end_line
\&& a:column == a:end_column
\&& has_key(g:ale_buffer_info, a:buffer)
let l:nearest_error_diff = -1
for l:error in get(g:ale_buffer_info[a:buffer], 'loclist', [])
if has_key(l:error, 'code')
\ && (a:linter_name is v:null || l:error.linter_name is# a:linter_name)
\ && 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
endif
return l:nearest_error
endfunction
function! s:OnReady(
\ line,
\ column,
\ end_line,
\ end_column,
\ MenuCallback,
\ linter,
\ lsp_details,
\) abort
let l:id = a:lsp_details.connection_id
if !ale#lsp#HasCapability(l:id, 'code_actions')
return
endif
let l:buffer = a:lsp_details.buffer
if a:linter.lsp is# 'tsserver'
let l:nearest_error =
\ s:FindError(l:buffer, a:line, a:column, a:end_line, a:end_column, a:linter.lsp)
if l:nearest_error isnot v:null
let l:message = ale#lsp#tsserver_message#GetCodeFixes(
\ l:buffer,
\ a:line,
\ a:column,
\ a:line,
\ a:column,
\ [l:nearest_error.code],
\)
else
let l:message = ale#lsp#tsserver_message#GetApplicableRefactors(
\ l:buffer,
\ a:line,
\ a:column,
\ a:end_line,
\ 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:diagnostics = []
let l:nearest_error =
\ s:FindError(l:buffer, a:line, a:column, a:end_line, a:end_column, v:null)
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,
\ },
\ },
\ },
\]
endif
let l:message = ale#lsp#message#CodeAction(
\ l:buffer,
\ a:line,
\ a:column,
\ a:end_line,
\ a:end_column,
\ l:diagnostics,
\)
endif
let l:Callback = a:linter.lsp is# 'tsserver'
\ ? function('ale#codefix#HandleTSServerResponse')
\ : function('ale#codefix#HandleLSPResponse')
call ale#lsp#RegisterCallback(l:id, l:Callback)
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,
\ 'menu_callback': a:MenuCallback,
\}
endfunction
function! s:ExecuteGetCodeFix(linter, range, MenuCallback) abort
let l:buffer = bufnr('')
if a:range == 0
let [l:line, l:column] = getpos('.')[1:2]
let l:end_line = l:line
let l:end_column = l:column
" Expand the range to cover the current word, if there is one.
let l:cword = expand('<cword>')
if !empty(l:cword)
let l:search_pos = searchpos('\V' . l:cword, 'bn', l:line)
if l:search_pos != [0, 0]
let l:column = l:search_pos[1]
let l:end_column = l:column + len(l:cword) - 1
endif
endif
elseif mode() is# 'v' || mode() is# "\<C-V>"
" You need to get the start and end in a different way when you're in
" visual mode.
let [l:line, l:column] = getpos('v')[1:2]
let [l:end_line, l:end_column] = getpos('.')[1:2]
else
let [l:line, l:column] = getpos("'<")[1:2]
let [l:end_line, l:end_column] = getpos("'>")[1:2]
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, a:MenuCallback]
\)
call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback)
endfunction
function! ale#codefix#Execute(range, ...) abort
if a:0 > 1
throw 'Too many arguments'
endif
let l:MenuCallback = get(a:000, 0, v:null)
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)
if l:MenuCallback is v:null
call s:message('No active LSPs')
else
call l:MenuCallback({}, [])
endif
return
endif
for l:lsp_linter in l:lsp_linters
call s:ExecuteGetCodeFix(l:lsp_linter, a:range, l:MenuCallback)
endfor
endfunction