Support for LSP/tsserver Code Actions (#3437)

* Added tsserver and LSP code action support.
* tsserver refactors support added.
* Handling special case when new text is added after new line symbol.
* ale#code_action#ApplyChanges simplified.
* Initial attempt on LSP Code Actions.
* workspace/executeCommand added.
* Some null checks added.
* Add last column to LSP Code Action message.
* Pass diagnostics to LSP code action.

Previously ApplyChanges 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.

Tested with typescript-language-server and it is working.
This commit is contained in:
Dalius Dobravolskas 2020-11-14 12:15:17 +02:00 committed by GitHub
parent 1ec573bf0d
commit 01800a23ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1260 additions and 86 deletions

View File

@ -0,0 +1,34 @@
" Author: Dalius Dobravolskas <dalius.dobravolskas@gmail.com>
" 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',
\})

View File

@ -33,37 +33,37 @@ 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
endif
if a:left.end.offset < a:right.end.offset
return -1
endif
if a:left.end.offset > a:right.end.offset
if a:left.end.offset < a:right.end.offset
return 1
endif
if a:left.end.offset > a:right.end.offset
return -1
endif
return 0
endfunction
@ -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
@ -125,6 +110,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.
@ -140,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,
@ -215,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

388
autoload/ale/codefix.vim Normal file
View File

@ -0,0 +1,388 @@
" Author: Dalius Dobravolskas <dalius.dobravolskas@gmail.com>
" 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 !has_key(a:response, 'request_seq')
\ || !has_key(s:codefix_map, a:response.request_seq)
return
endif
let l:location = remove(s:codefix_map, a:response.request_seq)
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)
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 = "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 <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
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
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: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
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)
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! 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:location = remove(s:codefix_map, a:response.id)
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
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 <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 = 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,
\ l:command.arguments,
\)
let l:request_id = ale#lsp#Send(l:location.connection_id, l:message)
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
endif
let l:changes = ale#code_action#BuildChangesList(l:changes_map)
call ale#code_action#HandleCodeAction({
\ 'description': 'codeaction',
\ 'changes': l:changes,
\}, {})
endif
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
endif
let l:buffer = a:lsp_details.buffer
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
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
endif
endif
endfor
let l:message = ale#lsp#tsserver_message#GetCodeFixes(
\ l:buffer,
\ a:line,
\ a:column,
\ a:line,
\ a:column,
\ [l:nearest_error],
\)
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)
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'
\ ? 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,
\}
endfunction
function! s:ExecuteGetCodeFix(linter, range) 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
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])
call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback)
endfunction
function! ale#codefix#Execute(range) 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, a:range)
endfor
endfunction

View File

@ -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

View File

@ -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
@ -219,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
@ -350,6 +359,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

View File

@ -172,3 +172,25 @@ 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, diagnostics) 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},
\ },
\ 'context': {
\ 'diagnostics': a:diagnostics
\ },
\}]
endfunction
function! ale#lsp#message#ExecuteCommand(command, arguments) abort
return [0, 'workspace/executeCommand', {
\ 'command': a:command,
\ 'arguments': a:arguments,
\}]
endfunction

View File

@ -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) .

View File

@ -103,3 +103,39 @@ 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
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

View File

@ -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(
\ {

View File

@ -3106,6 +3106,18 @@ ALERename *ALERename*
in those files will not be updated.
ALECodeAction *ALECodeAction*
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*
Repeat the last selection displayed in the preview window.

View File

@ -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': '<bang>' is# '!'})
command! -bar -range ALECodeAction :call ale#codefix#Execute(<range>)
" Organize import statements using tsserver
command! -bar ALEOrganizeImports :call ale#organize_imports#Execute()
@ -283,6 +285,7 @@ nnoremap <silent> <Plug>(ale_documentation) :ALEDocumentation<Return>
inoremap <silent> <Plug>(ale_complete) <C-\><C-O>:ALEComplete<Return>
nnoremap <silent> <Plug>(ale_import) :ALEImport<Return>
nnoremap <silent> <Plug>(ale_rename) :ALERename<Return>
nnoremap <silent> <Plug>(ale_code_action) :ALECodeAction<Return>
nnoremap <silent> <Plug>(ale_repeat_selection) :ALERepeatSelection<Return>
" Set up autocmd groups now.

View File

@ -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

View File

@ -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',
\ '',

View File

@ -0,0 +1,59 @@
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()
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)

604
test/test_codefix.vader Normal file
View File

@ -0,0 +1,604 @@
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(getCodeFixes from tsserver should be handled):
call ale#codefix#SetMap({3: {}})
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(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,
\ '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 getCodeFixes unsuccessful):
call ale#codefix#SetMap({3: {}})
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#SetMap({3: {}})
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])
" 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#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
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])
" 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#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
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$"
"
" execute "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
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(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}]})
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(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,
\ {'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()
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