mirror of
https://github.com/dense-analysis/ale.git
synced 2026-03-07 23:52:02 +08:00
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.
498 lines
14 KiB
VimL
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
|