Add ALERename (tsserver & LSP), ALEOrganizeImports (tsserver) and auto import support (tsserver) (#2709)

This commit adds support for renaming symbols in tsserver and with LSP tools, and for organising imports with tsserver. Completion results for symbols that can be imported are now suggested if enabled for tsserver completion done via ALE.
This commit is contained in:
Jerko Steiner 2019-09-13 03:53:23 +07:00 committed by w0rp
parent b66d202c1d
commit 3e8c8d3ccb
20 changed files with 1633 additions and 17 deletions

View file

@ -189,6 +189,14 @@ completion manually with `<C-x><C-o>`.
set omnifunc=ale#completion#OmniFunc set omnifunc=ale#completion#OmniFunc
``` ```
When working with TypeScript files, ALE supports automatic imports from
external modules. This behavior is disabled by default and can be enabled by
setting:
```vim
let g:ale_completion_tsserver_autoimport = 1
```
See `:help ale-completion` for more information. See `:help ale-completion` for more information.
<a name="usage-go-to-definition"></a> <a name="usage-go-to-definition"></a>

View file

@ -0,0 +1,163 @@
" Author: Jerko Steiner <jerko.steiner@gmail.com>
" Description: Code action support for LSP / tsserver
function! ale#code_action#HandleCodeAction(code_action) abort
let l:current_buffer = bufnr('')
let l:changes = a:code_action.changes
for l:file_code_edit in l:changes
let l:buf = bufnr(l:file_code_edit.fileName)
if l:buf != -1 && l:buf != l:current_buffer && getbufvar(l:buf, '&mod')
call ale#util#Execute('echom ''Aborting action, file is unsaved''')
return
endif
endfor
for l:file_code_edit in l:changes
call ale#code_action#ApplyChanges(
\ l:file_code_edit.fileName, l:file_code_edit.textChanges)
endfor
endfunction
function! ale#code_action#ApplyChanges(filename, changes) abort
let l:current_buffer = bufnr('')
" The buffer is used to determine the fileformat, if available.
let l:buffer = bufnr(a:filename)
let l:is_current_buffer = l:buffer > 0 && l:buffer == l:current_buffer
if l:buffer > 0
let l:lines = getbufline(l:buffer, 1, '$')
else
let l:lines = readfile(a:filename, 'b')
endif
if l:is_current_buffer
let l:pos = getpos('.')[1:2]
else
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
for l:code_edit in a:changes
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: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
else
let l:end_line_len = len(l:lines[l:end_line - 1])
endif
let l:insertions = split(l:text, '\n', 1)
if l:line is 1
" Same logic as for column below. Vimscript's slice [:-1] will not
" be an empty list.
let l:start = []
else
let l:start = l:lines[: l:line - 2]
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.
let l:middle = [l:insertions[0]]
else
let l:middle = [l:lines[l:line - 1][: l:column - 2] . l:insertions[0]]
endif
call extend(l:middle, l:insertions[1:])
let l:middle[-1] .= l:lines[l:end_line - 1][l:end_column - 1 :]
let l:lines_before_change = len(l:lines)
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,
\ [l:line, l:column],
\ [l:end_line, l:end_column],
\ [l:current_line_offset, l:column_offset])
endfor
if l:lines[-1] is# ''
call remove(l:lines, -1)
endif
call ale#util#Writefile(l:buffer, l:lines, a:filename)
if l:is_current_buffer
call ale#util#Execute(':e!')
call setpos('.', [0, l:pos[0], l:pos[1], 0])
endif
endfunction
function! s:UpdateCursor(cursor, start, end, offset) abort
let l:cur_line = a:cursor[0]
let l:cur_column = a:cursor[1]
let l:line = a:start[0]
let l:column = a:start[1]
let l:end_line = a:end[0]
let l:end_column = a:end[1]
let l:line_offset = a:offset[0]
let l:column_offset = a:offset[1]
if l:end_line < l:cur_line
" both start and end lines are before the cursor. only line offset
" needs to be updated
let l:cur_line += l:line_offset
elseif l:end_line == l:cur_line
" end line is at the same location as cursor, which means
" l:line <= l:cur_line
if l:line < l:cur_line || l:column <= l:cur_column
" updates are happening either before or around the cursor
if l:end_column < l:cur_column
" updates are happening before the cursor, update the
" column offset for cursor
let l:cur_line += l:line_offset
let l:cur_column += l:column_offset
else
" updates are happening around the cursor, move the cursor
" to the end of the changes
let l:cur_line += l:line_offset
let l:cur_column = l:end_column + l:column_offset
endif
" else is not necessary, it means modifications are happening
" after the cursor so no cursor updates need to be done
endif
else
" end line is after the cursor
if l:line < l:cur_line || l:line == l:cur_line && l:column <= l:cur_column
" changes are happening around the cursor, move the cursor
" to the end of the changes
let l:cur_line = l:end_line + l:line_offset
let l:cur_column = l:end_column + l:column_offset
" else is not necesary, it means modifications are happening
" after the cursor so no cursor updates need to be done
endif
endif
return [l:cur_line, l:cur_column]
endfunction

View file

@ -15,6 +15,7 @@ onoremap <silent> <Plug>(ale_show_completion_menu) <Nop>
let g:ale_completion_delay = get(g:, 'ale_completion_delay', 100) let g:ale_completion_delay = get(g:, 'ale_completion_delay', 100)
let g:ale_completion_excluded_words = get(g:, 'ale_completion_excluded_words', []) let g:ale_completion_excluded_words = get(g:, 'ale_completion_excluded_words', [])
let g:ale_completion_max_suggestions = get(g:, 'ale_completion_max_suggestions', 50) let g:ale_completion_max_suggestions = get(g:, 'ale_completion_max_suggestions', 50)
let g:ale_completion_tsserver_autoimport = get(g:, 'ale_completion_tsserver_autoimport', 0)
let s:timer_id = -1 let s:timer_id = -1
let s:last_done_pos = [] let s:last_done_pos = []
@ -296,7 +297,10 @@ function! ale#completion#ParseTSServerCompletions(response) abort
let l:names = [] let l:names = []
for l:suggestion in a:response.body for l:suggestion in a:response.body
call add(l:names, l:suggestion.name) call add(l:names, {
\ 'word': l:suggestion.name,
\ 'source': get(l:suggestion, 'source', ''),
\})
endfor endfor
return l:names return l:names
@ -330,13 +334,22 @@ function! ale#completion#ParseTSServerCompletionEntryDetails(response) abort
endif endif
" See :help complete-items " See :help complete-items
call add(l:results, { let l:result = {
\ 'word': l:suggestion.name, \ 'word': l:suggestion.name,
\ 'kind': l:kind, \ 'kind': l:kind,
\ 'icase': 1, \ 'icase': 1,
\ 'menu': join(l:displayParts, ''), \ 'menu': join(l:displayParts, ''),
\ 'dup': g:ale_completion_tsserver_autoimport,
\ 'info': join(l:documentationParts, ''), \ 'info': join(l:documentationParts, ''),
\}) \}
if has_key(l:suggestion, 'codeActions')
let l:result.user_data = json_encode({
\ 'codeActions': l:suggestion.codeActions,
\ })
endif
call add(l:results, l:result)
endfor endfor
let l:names = getbufvar(l:buffer, 'ale_tsserver_completion_names', []) let l:names = getbufvar(l:buffer, 'ale_tsserver_completion_names', [])
@ -345,12 +358,12 @@ function! ale#completion#ParseTSServerCompletionEntryDetails(response) abort
let l:names_with_details = map(copy(l:results), 'v:val.word') let l:names_with_details = map(copy(l:results), 'v:val.word')
let l:missing_names = filter( let l:missing_names = filter(
\ copy(l:names), \ copy(l:names),
\ 'index(l:names_with_details, v:val) < 0', \ 'index(l:names_with_details, v:val.word) < 0',
\) \)
for l:name in l:missing_names for l:name in l:missing_names
call add(l:results, { call add(l:results, {
\ 'word': l:name, \ 'word': l:name.word,
\ 'kind': 'v', \ 'kind': 'v',
\ 'icase': 1, \ 'icase': 1,
\ 'menu': '', \ 'menu': '',
@ -472,13 +485,22 @@ function! ale#completion#HandleTSServerResponse(conn_id, response) abort
call setbufvar(l:buffer, 'ale_tsserver_completion_names', l:names) call setbufvar(l:buffer, 'ale_tsserver_completion_names', l:names)
if !empty(l:names) if !empty(l:names)
let l:identifiers = []
for l:name in l:names
call add(l:identifiers, {
\ 'name': l:name.word,
\ 'source': get(l:name, 'source', ''),
\})
endfor
let b:ale_completion_info.request_id = ale#lsp#Send( let b:ale_completion_info.request_id = ale#lsp#Send(
\ b:ale_completion_info.conn_id, \ b:ale_completion_info.conn_id,
\ ale#lsp#tsserver_message#CompletionEntryDetails( \ ale#lsp#tsserver_message#CompletionEntryDetails(
\ l:buffer, \ l:buffer,
\ b:ale_completion_info.line, \ b:ale_completion_info.line,
\ b:ale_completion_info.column, \ b:ale_completion_info.column,
\ l:names, \ l:identifiers,
\ ), \ ),
\) \)
endif endif
@ -525,6 +547,7 @@ function! s:OnReady(linter, lsp_details) abort
\ b:ale_completion_info.line, \ b:ale_completion_info.line,
\ b:ale_completion_info.column, \ b:ale_completion_info.column,
\ b:ale_completion_info.prefix, \ b:ale_completion_info.prefix,
\ g:ale_completion_tsserver_autoimport,
\) \)
else else
" Send a message saying the buffer has changed first, otherwise " Send a message saying the buffer has changed first, otherwise
@ -692,6 +715,26 @@ function! ale#completion#Queue() abort
let s:timer_id = timer_start(g:ale_completion_delay, function('s:TimerHandler')) let s:timer_id = timer_start(g:ale_completion_delay, function('s:TimerHandler'))
endfunction endfunction
function! ale#completion#HandleUserData(completed_item) abort
let l:source = get(get(b:, 'ale_completion_info', {}), 'source', '')
if l:source isnot# 'ale-automatic' && l:source isnot# 'ale-manual'
return
endif
let l:user_data_json = get(a:completed_item, 'user_data', '')
if empty(l:user_data_json)
return
endif
let l:user_data = json_decode(l:user_data_json)
for l:code_action in get(l:user_data, 'codeActions', [])
call ale#code_action#HandleCodeAction(l:code_action)
endfor
endfunction
function! ale#completion#Done() abort function! ale#completion#Done() abort
silent! pclose silent! pclose
@ -700,6 +743,10 @@ function! ale#completion#Done() abort
let s:last_done_pos = getpos('.')[1:2] let s:last_done_pos = getpos('.')[1:2]
endfunction endfunction
augroup ALECompletionActions
autocmd CompleteDone * call ale#completion#HandleUserData(v:completed_item)
augroup END
function! s:Setup(enabled) abort function! s:Setup(enabled) abort
augroup ALECompletionGroup augroup ALECompletionGroup
autocmd! autocmd!

View file

@ -37,6 +37,7 @@ function! ale#lsp#Register(executable_or_address, project, init_options) abort
\ 'init_queue': [], \ 'init_queue': [],
\ 'capabilities': { \ 'capabilities': {
\ 'hover': 0, \ 'hover': 0,
\ 'rename': 0,
\ 'references': 0, \ 'references': 0,
\ 'completion': 0, \ 'completion': 0,
\ 'completion_trigger_characters': [], \ 'completion_trigger_characters': [],
@ -199,6 +200,10 @@ function! s:UpdateCapabilities(conn, capabilities) abort
let a:conn.capabilities.references = 1 let a:conn.capabilities.references = 1
endif endif
if get(a:capabilities, 'renameProvider') is v:true
let a:conn.capabilities.rename = 1
endif
if !empty(get(a:capabilities, 'completionProvider')) if !empty(get(a:capabilities, 'completionProvider'))
let a:conn.capabilities.completion = 1 let a:conn.capabilities.completion = 1
endif endif
@ -317,6 +322,7 @@ function! ale#lsp#MarkConnectionAsTsserver(conn_id) abort
let l:conn.capabilities.completion_trigger_characters = ['.'] let l:conn.capabilities.completion_trigger_characters = ['.']
let l:conn.capabilities.definition = 1 let l:conn.capabilities.definition = 1
let l:conn.capabilities.symbol_search = 1 let l:conn.capabilities.symbol_search = 1
let l:conn.capabilities.rename = 1
endfunction endfunction
function! s:SendInitMessage(conn) abort function! s:SendInitMessage(conn) abort

View file

@ -162,3 +162,13 @@ function! ale#lsp#message#DidChangeConfiguration(buffer, config) abort
\ 'settings': a:config, \ 'settings': a:config,
\}] \}]
endfunction endfunction
function! ale#lsp#message#Rename(buffer, line, column, new_name) abort
return [0, 'textDocument/rename', {
\ 'textDocument': {
\ 'uri': ale#path#ToURI(expand('#' . a:buffer . ':p')),
\ },
\ 'position': {'line': a:line - 1, 'character': a:column - 1},
\ 'newName': a:new_name,
\}]
endfunction

View file

@ -36,12 +36,14 @@ function! ale#lsp#tsserver_message#Geterr(buffer) abort
return [1, 'ts@geterr', {'files': [expand('#' . a:buffer . ':p')]}] return [1, 'ts@geterr', {'files': [expand('#' . a:buffer . ':p')]}]
endfunction endfunction
function! ale#lsp#tsserver_message#Completions(buffer, line, column, prefix) abort function! ale#lsp#tsserver_message#Completions(
\ buffer, line, column, prefix, include_external) abort
return [0, 'ts@completions', { return [0, 'ts@completions', {
\ 'line': a:line, \ 'line': a:line,
\ 'offset': a:column, \ 'offset': a:column,
\ 'file': expand('#' . a:buffer . ':p'), \ 'file': expand('#' . a:buffer . ':p'),
\ 'prefix': a:prefix, \ 'prefix': a:prefix,
\ 'includeExternalModuleExports': a:include_external,
\}] \}]
endfunction endfunction
@ -77,3 +79,27 @@ function! ale#lsp#tsserver_message#Quickinfo(buffer, line, column) abort
\ 'file': expand('#' . a:buffer . ':p'), \ 'file': expand('#' . a:buffer . ':p'),
\}] \}]
endfunction endfunction
function! ale#lsp#tsserver_message#Rename(
\ buffer, line, column, find_in_comments, find_in_strings) abort
return [0, 'ts@rename', {
\ 'line': a:line,
\ 'offset': a:column,
\ 'file': expand('#' . a:buffer . ':p'),
\ 'arguments': {
\ 'findInComments': a:find_in_comments,
\ 'findInStrings': a:find_in_strings,
\ }
\}]
endfunction
function! ale#lsp#tsserver_message#OrganizeImports(buffer) abort
return [0, 'ts@organizeImports', {
\ 'scope': {
\ 'type': 'file',
\ 'args': {
\ 'file': expand('#' . a:buffer . ':p'),
\ },
\ },
\}]
endfunction

View file

@ -0,0 +1,59 @@
" Author: Jerko Steiner <jerko.steiner@gmail.com>
" Description: Organize imports support for tsserver
"
function! ale#organize_imports#HandleTSServerResponse(conn_id, response) abort
if get(a:response, 'command', '') isnot# 'organizeImports'
return
endif
if get(a:response, 'success', v:false) isnot v:true
return
endif
let l:file_code_edits = a:response.body
call ale#code_action#HandleCodeAction({
\ 'description': 'Organize Imports',
\ 'changes': l:file_code_edits,
\})
endfunction
function! s:OnReady(linter, lsp_details) abort
let l:id = a:lsp_details.connection_id
if a:linter.lsp isnot# 'tsserver'
call ale#util#Execute('echom ''OrganizeImports currently only works with tsserver''')
return
endif
let l:buffer = a:lsp_details.buffer
let l:Callback = function('ale#organize_imports#HandleTSServerResponse')
call ale#lsp#RegisterCallback(l:id, l:Callback)
let l:message = ale#lsp#tsserver_message#OrganizeImports(l:buffer)
let l:request_id = ale#lsp#Send(l:id, l:message)
endfunction
function! s:OrganizeImports(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')
call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback)
endfunction
function! ale#organize_imports#Execute() abort
for l:linter in ale#linter#Get(&filetype)
if !empty(l:linter.lsp)
call s:OrganizeImports(l:linter)
endif
endfor
endfunction

225
autoload/ale/rename.vim Normal file
View file

@ -0,0 +1,225 @@
" Author: Jerko Steiner <jerko.steiner@gmail.com>
" Description: Rename symbol support for LSP / tsserver
let s:rename_map = {}
" Used to get the rename map in tests.
function! ale#rename#GetMap() abort
return deepcopy(s:rename_map)
endfunction
" Used to set the rename map in tests.
function! ale#rename#SetMap(map) abort
let s:rename_map = a:map
endfunction
function! ale#rename#ClearLSPData() abort
let s:rename_map = {}
endfunction
let g:ale_rename_tsserver_find_in_comments = get(g:, 'ale_rename_tsserver_find_in_comments')
let g:ale_rename_tsserver_find_in_strings = get(g:, 'ale_rename_tsserver_find_in_strings')
function! s:message(message) abort
call ale#util#Execute('echom ' . string(a:message))
endfunction
function! ale#rename#HandleTSServerResponse(conn_id, response) abort
if get(a:response, 'command', '') isnot# 'rename'
return
endif
if !has_key(s:rename_map, a:response.request_seq)
return
endif
let l:old_name = s:rename_map[a:response.request_seq].old_name
let l:new_name = s:rename_map[a:response.request_seq].new_name
call remove(s:rename_map, a:response.request_seq)
if get(a:response, 'success', v:false) is v:false
let l:message = get(a:response, 'message', 'unknown')
call s:message('Error renaming "' . l:old_name . '" to: "' . l:new_name
\ . '". Reason: ' . l:message)
return
endif
let l:changes = []
for l:response_item in a:response.body.locs
let l:filename = l:response_item.file
let l:text_changes = []
for l:loc in l:response_item.locs
call add(l:text_changes, {
\ 'start': {
\ 'line': l:loc.start.line,
\ 'offset': l:loc.start.offset,
\ },
\ 'end': {
\ 'line': l:loc.end.line,
\ 'offset': l:loc.end.offset,
\ },
\ 'newText': l:new_name,
\})
endfor
call add(l:changes, {
\ 'fileName': l:filename,
\ 'textChanges': l:text_changes,
\})
endfor
if empty(l:changes)
call s:message('Error renaming "' . l:old_name . '" to: "' . l:new_name . '"')
return
endif
call ale#code_action#HandleCodeAction({
\ 'description': 'rename',
\ 'changes': 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)
call remove(s:rename_map, a:response.id)
if !has_key(a:response, 'result')
call s:message('No rename result received from server')
return
endif
let l:workspace_edit = a:response.result
if !has_key(l:workspace_edit, 'changes') || empty(l:workspace_edit.changes)
call s:message('No changes received from server')
return
endif
let l:changes = []
for l:file_name in keys(l:workspace_edit.changes)
let l:text_edits = l:workspace_edit.changes[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
call ale#code_action#HandleCodeAction({
\ 'description': 'rename',
\ 'changes': l:changes,
\})
endif
endfunction
function! s:OnReady(line, column, old_name, new_name, linter, lsp_details) abort
let l:id = a:lsp_details.connection_id
if !ale#lsp#HasCapability(l:id, 'rename')
return
endif
let l:buffer = a:lsp_details.buffer
let l:Callback = a:linter.lsp is# 'tsserver'
\ ? function('ale#rename#HandleTSServerResponse')
\ : function('ale#rename#HandleLSPResponse')
call ale#lsp#RegisterCallback(l:id, l:Callback)
if a:linter.lsp is# 'tsserver'
let l:message = ale#lsp#tsserver_message#Rename(
\ l:buffer,
\ a:line,
\ a:column,
\ g:ale_rename_tsserver_find_in_comments,
\ g:ale_rename_tsserver_find_in_strings,
\)
else
" Send a message saying the buffer has changed first, or the
" rename position probably won't make sense.
call ale#lsp#NotifyForChanges(l:id, l:buffer)
let l:message = ale#lsp#message#Rename(
\ l:buffer,
\ a:line,
\ a:column,
\ a:new_name
\)
endif
let l:request_id = ale#lsp#Send(l:id, l:message)
let s:rename_map[l:request_id] = {
\ 'new_name': a:new_name,
\ 'old_name': a:old_name,
\}
endfunction
function! s:ExecuteRename(linter, old_name, new_name) 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, a:old_name, a:new_name])
call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback)
endfunction
function! ale#rename#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
let l:old_name = expand('<cword>')
let l:new_name = ale#util#Input('New name: ', l:old_name)
if empty(l:new_name)
call s:message('New name cannot be empty!')
return
endif
for l:lsp_linter in l:lsp_linters
call s:ExecuteRename(l:lsp_linter, l:old_name, l:new_name)
endfor
endfunction

View file

@ -477,3 +477,6 @@ function! ale#util#FindItemAtCursor(buffer) abort
return [l:info, l:loc] return [l:info, l:loc]
endfunction endfunction
function! ale#util#Input(message, value) abort
return input(a:message, a:value)
endfunction

View file

@ -376,6 +376,10 @@ The |ALEComplete| command can be used to show completion suggestions manually,
even when |g:ale_completion_enabled| is set to `0`. For manually requesting even when |g:ale_completion_enabled| is set to `0`. For manually requesting
completion information with Deoplete, consult Deoplete's documentation. completion information with Deoplete, consult Deoplete's documentation.
When working with TypeScript files, ALE by can support automatic imports
from external modules. This behavior can be enabled by setting the
|g:ale_completion_tsserver_autoimport| variable to `1`.
*ale-completion-completeopt-bug* *ale-completion-completeopt-bug*
ALE Automatic completion implementation replaces |completeopt| before opening ALE Automatic completion implementation replaces |completeopt| before opening
@ -597,6 +601,16 @@ b:ale_completion_enabled *b:ale_completion_enabled*
See |ale-completion| See |ale-completion|
g:ale_completion_tsserver_autoimport *g:ale_completion_tsserver_autoimport*
Type: Number
Default: `0`
When this option is set to `0`, ALE will not try to automatically import
completion results from external modules. It can be enabled by setting it
to `1`.
g:ale_completion_excluded_words *g:ale_completion_excluded_words* g:ale_completion_excluded_words *g:ale_completion_excluded_words*
*b:ale_completion_excluded_words* *b:ale_completion_excluded_words*
Type: |List| Type: |List|
@ -1317,6 +1331,27 @@ g:ale_pattern_options_enabled *g:ale_pattern_options_enabled*
will not set buffer variables per |g:ale_pattern_options|. will not set buffer variables per |g:ale_pattern_options|.
g:ale_rename_tsserver_find_in_comments *g:ale_rename_tsserver_find_in_comments*
Type: |Number|
Default: `0`
If enabled, this option will tell tsserver to find and replace text in
comments when calling |ALERename|. It can be enabled by settings the value
to `1`.
g:ale_rename_tsserver_find_in_strings *g:ale_rename_tsserver_find_in_strings*
Type: |Number|
Default: `0`
If enabled, this option will tell tsserver to find and replace text in
strings when calling |ALERename|. It can be enabled by settings the value to
`1`.
g:ale_set_balloons *g:ale_set_balloons* g:ale_set_balloons *g:ale_set_balloons*
*b:ale_set_balloons* *b:ale_set_balloons*
@ -2543,6 +2578,18 @@ ALEHover *ALEHover*
A plug mapping `<Plug>(ale_hover)` is defined for this command. A plug mapping `<Plug>(ale_hover)` is defined for this command.
ALEOrganizeImports *ALEOrganizeImports*
Organize imports using tsserver. Currently not implemented for LSPs.
ALERename *ALERename*
Rename a symbol using TypeScript server or Language Server.
The user will be prompted for a new name.
ALESymbolSearch `<query>` *ALESymbolSearch* ALESymbolSearch `<query>` *ALESymbolSearch*
Search for symbols in the workspace, taken from any available LSP linters. Search for symbols in the workspace, taken from any available LSP linters.

View file

@ -221,6 +221,12 @@ command! -nargs=1 ALESymbolSearch :call ale#symbol#Search(<q-args>)
command! -bar ALEComplete :call ale#completion#GetCompletions('ale-manual') command! -bar ALEComplete :call ale#completion#GetCompletions('ale-manual')
" Rename symbols using tsserver and LSP
command! -bar ALERename :call ale#rename#Execute()
" Organize import statements using tsserver
command! -bar ALEOrganizeImports :call ale#organize_imports#Execute()
" <Plug> mappings for commands " <Plug> mappings for commands
nnoremap <silent> <Plug>(ale_previous) :ALEPrevious<Return> nnoremap <silent> <Plug>(ale_previous) :ALEPrevious<Return>
nnoremap <silent> <Plug>(ale_previous_wrap) :ALEPreviousWrap<Return> nnoremap <silent> <Plug>(ale_previous_wrap) :ALEPreviousWrap<Return>
@ -259,6 +265,7 @@ nnoremap <silent> <Plug>(ale_find_references) :ALEFindReferences<Return>
nnoremap <silent> <Plug>(ale_hover) :ALEHover<Return> nnoremap <silent> <Plug>(ale_hover) :ALEHover<Return>
nnoremap <silent> <Plug>(ale_documentation) :ALEDocumentation<Return> nnoremap <silent> <Plug>(ale_documentation) :ALEDocumentation<Return>
inoremap <silent> <Plug>(ale_complete) <C-\><C-O>:ALEComplete<Return> inoremap <silent> <Plug>(ale_complete) <C-\><C-O>:ALEComplete<Return>
nnoremap <silent> <Plug>(ale_rename) :ALERename<Return>
" Set up autocmd groups now. " Set up autocmd groups now.
call ale#events#Init() call ale#events#Init()

View file

@ -47,6 +47,14 @@ Before:
AssertEqual a:expect_success, g:get_completions_called AssertEqual a:expect_success, g:get_completions_called
endfunction endfunction
let g:handle_code_action_called = 0
function! MockHandleCodeAction() abort
" delfunction! ale#code_action#HandleCodeAction
function! ale#code_action#HandleCodeAction(action) abort
let g:handle_code_action_called += 1
endfunction
endfunction
After: After:
Restore Restore
@ -54,6 +62,7 @@ After:
unlet! g:output unlet! g:output
unlet! g:fake_mode unlet! g:fake_mode
unlet! g:get_completions_called unlet! g:get_completions_called
unlet! g:handle_code_action_called
unlet! b:ale_old_omnifunc unlet! b:ale_old_omnifunc
unlet! b:ale_old_completeopt unlet! b:ale_old_completeopt
unlet! b:ale_completion_info unlet! b:ale_completion_info
@ -61,6 +70,8 @@ After:
unlet! b:ale_complete_done_time unlet! b:ale_complete_done_time
delfunction CheckCompletionCalled delfunction CheckCompletionCalled
delfunction ale#code_action#HandleCodeAction
delfunction MockHandleCodeAction
if exists('*CompleteCallback') if exists('*CompleteCallback')
delfunction CompleteCallback delfunction CompleteCallback
@ -77,6 +88,7 @@ After:
endfunction endfunction
runtime autoload/ale/completion.vim runtime autoload/ale/completion.vim
runtime autoload/ale/code_action.vim
runtime autoload/ale/util.vim runtime autoload/ale/util.vim
Execute(ale#completion#GetCompletions should be called when the cursor position stays the same): Execute(ale#completion#GetCompletions should be called when the cursor position stays the same):
@ -385,3 +397,44 @@ Execute(Running the normal mode <Plug> keybind should reset the settings):
AssertEqual 'menu', &l:completeopt AssertEqual 'menu', &l:completeopt
Assert !has_key(b:, 'ale_old_omnifunc') Assert !has_key(b:, 'ale_old_omnifunc')
Assert !has_key(b:, 'ale_old_completeopt') Assert !has_key(b:, 'ale_old_completeopt')
Execute(HandleUserData should call ale#code_action#HandleCodeAction):
let b:ale_completion_info = {'source': 'ale-manual'}
call MockHandleCodeAction()
call ale#completion#HandleUserData({})
AssertEqual g:handle_code_action_called, 0
call ale#completion#HandleUserData({
\ 'user_data': ''
\})
AssertEqual g:handle_code_action_called, 0
call ale#completion#HandleUserData({
\ 'user_data': '{}'
\})
AssertEqual g:handle_code_action_called, 0
call ale#completion#HandleUserData({
\ 'user_data': '{"codeActions": []}'
\})
AssertEqual g:handle_code_action_called, 0
call ale#completion#HandleUserData({
\ 'user_data': '{"codeActions": [{"description":"", "changes": []}]}'
\})
AssertEqual g:handle_code_action_called, 1
let b:ale_completion_info = {'source': 'ale-automatic'}
call ale#completion#HandleUserData({
\ 'user_data': '{"codeActions": [{"description":"", "changes": []}]}'
\})
AssertEqual g:handle_code_action_called, 2
Execute(ale#code_action#HandleCodeAction should not be called when when source is not ALE):
call MockHandleCodeAction()
let b:ale_completion_info = {'source': 'syntastic'}
call ale#completion#HandleUserData({
\ 'user_data': '{"codeActions": [{"description":"", "changes": []}]}'
\})
AssertEqual g:handle_code_action_called, 0

View file

@ -116,7 +116,13 @@ Execute(The right message should be sent for the initial tsserver request):
\ string(g:Callback) \ string(g:Callback)
" We should send the right message. " We should send the right message.
AssertEqual AssertEqual
\ [[0, 'ts@completions', {'file': expand('%:p'), 'line': 1, 'offset': 3, 'prefix': 'fo'}]], \ [[0, 'ts@completions', {
\ 'file': expand('%:p'),
\ 'line': 1,
\ 'offset': 3,
\ 'prefix': 'fo',
\ 'includeExternalModuleExports': g:ale_completion_tsserver_autoimport,
\ }]],
\ g:message_list \ g:message_list
" We should set up the completion info correctly. " We should set up the completion info correctly.
AssertEqual AssertEqual
@ -151,7 +157,7 @@ Execute(The right message sent to the tsserver LSP when the first completion mes
\ 'body': [ \ 'body': [
\ {'name': 'Baz'}, \ {'name': 'Baz'},
\ {'name': 'dingDong'}, \ {'name': 'dingDong'},
\ {'name': 'Foo'}, \ {'name': 'Foo', 'source': '/path/to/foo.ts'},
\ {'name': 'FooBar'}, \ {'name': 'FooBar'},
\ {'name': 'frazzle'}, \ {'name': 'frazzle'},
\ {'name': 'FFS'}, \ {'name': 'FFS'},
@ -160,8 +166,16 @@ Execute(The right message sent to the tsserver LSP when the first completion mes
" We should save the names we got in the buffer, as TSServer doesn't return " We should save the names we got in the buffer, as TSServer doesn't return
" details for every name. " details for every name.
AssertEqual AssertEqual [{
\ ['Foo', 'FooBar', 'frazzle'], \ 'word': 'Foo',
\ 'source': '/path/to/foo.ts',
\ }, {
\ 'word': 'FooBar',
\ 'source': '',
\ }, {
\ 'word': 'frazzle',
\ 'source': '',
\}],
\ get(b:, 'ale_tsserver_completion_names', []) \ get(b:, 'ale_tsserver_completion_names', [])
" The entry details messages should have been sent. " The entry details messages should have been sent.
@ -171,7 +185,16 @@ Execute(The right message sent to the tsserver LSP when the first completion mes
\ 'ts@completionEntryDetails', \ 'ts@completionEntryDetails',
\ { \ {
\ 'file': expand('%:p'), \ 'file': expand('%:p'),
\ 'entryNames': ['Foo', 'FooBar', 'frazzle'], \ 'entryNames': [{
\ 'name': 'Foo',
\ 'source': '/path/to/foo.ts',
\ }, {
\ 'name': 'FooBar',
\ 'source': '',
\ }, {
\ 'name': 'frazzle',
\ 'source': '',
\ }],
\ 'offset': 1, \ 'offset': 1,
\ 'line': 1, \ 'line': 1,
\ }, \ },

View file

@ -6,10 +6,24 @@ Execute(TypeScript completions responses should be parsed correctly):
\ ale#completion#ParseTSServerCompletions({ \ ale#completion#ParseTSServerCompletions({
\ 'body': [], \ 'body': [],
\}) \})
AssertEqual ['foo', 'bar', 'baz'], AssertEqual
\ [
\ {
\ 'word': 'foo',
\ 'source': '/path/to/foo.ts',
\ },
\ {
\ 'word': 'bar',
\ 'source': '',
\ },
\ {
\ 'word': 'baz',
\ 'source': '',
\ }
\ ],
\ ale#completion#ParseTSServerCompletions({ \ ale#completion#ParseTSServerCompletions({
\ 'body': [ \ 'body': [
\ {'name': 'foo'}, \ {'name': 'foo', 'source': '/path/to/foo.ts'},
\ {'name': 'bar'}, \ {'name': 'bar'},
\ {'name': 'baz'}, \ {'name': 'baz'},
\ ], \ ],
@ -24,6 +38,7 @@ Execute(TypeScript completion details responses should be parsed correctly):
\ 'info': '', \ 'info': '',
\ 'kind': 'f', \ 'kind': 'f',
\ 'icase': 1, \ 'icase': 1,
\ 'dup': g:ale_completion_tsserver_autoimport,
\ }, \ },
\ { \ {
\ 'word': 'def', \ 'word': 'def',
@ -31,6 +46,7 @@ Execute(TypeScript completion details responses should be parsed correctly):
\ 'info': 'foo bar baz', \ 'info': 'foo bar baz',
\ 'kind': 'f', \ 'kind': 'f',
\ 'icase': 1, \ 'icase': 1,
\ 'dup': g:ale_completion_tsserver_autoimport,
\ }, \ },
\ { \ {
\ 'word': 'ghi', \ 'word': 'ghi',
@ -38,6 +54,7 @@ Execute(TypeScript completion details responses should be parsed correctly):
\ 'info': '', \ 'info': '',
\ 'kind': 'f', \ 'kind': 'f',
\ 'icase': 1, \ 'icase': 1,
\ 'dup': g:ale_completion_tsserver_autoimport,
\ }, \ },
\ ], \ ],
\ ale#completion#ParseTSServerCompletionEntryDetails({ \ ale#completion#ParseTSServerCompletionEntryDetails({
@ -96,7 +113,10 @@ Execute(TypeScript completion details responses should be parsed correctly):
\}) \})
Execute(Entries without details should be included in the responses): Execute(Entries without details should be included in the responses):
let b:ale_tsserver_completion_names = ['xyz'] let b:ale_tsserver_completion_names = [{
\ 'word': 'xyz',
\ 'source': '/path/to/xyz.ts',
\ }]
AssertEqual AssertEqual
\ [ \ [
@ -106,6 +126,13 @@ Execute(Entries without details should be included in the responses):
\ 'info': '', \ 'info': '',
\ 'kind': 'f', \ 'kind': 'f',
\ 'icase': 1, \ 'icase': 1,
\ 'user_data': json_encode({
\ 'codeActions': [{
\ 'description': 'abc action',
\ 'changes': [],
\ }],
\ }),
\ 'dup': g:ale_completion_tsserver_autoimport,
\ }, \ },
\ { \ {
\ 'word': 'def', \ 'word': 'def',
@ -113,6 +140,7 @@ Execute(Entries without details should be included in the responses):
\ 'info': 'foo bar baz', \ 'info': 'foo bar baz',
\ 'kind': 'f', \ 'kind': 'f',
\ 'icase': 1, \ 'icase': 1,
\ 'dup': g:ale_completion_tsserver_autoimport,
\ }, \ },
\ { \ {
\ 'word': 'xyz', \ 'word': 'xyz',
@ -139,6 +167,10 @@ Execute(Entries without details should be included in the responses):
\ {'text': ' '}, \ {'text': ' '},
\ {'text': 'number'}, \ {'text': 'number'},
\ ], \ ],
\ 'codeActions': [{
\ 'description': 'abc action',
\ 'changes': [],
\ }],
\ }, \ },
\ { \ {
\ 'name': 'def', \ 'name': 'def',

View file

@ -275,9 +275,10 @@ Execute(ale#lsp#tsserver_message#Completions() should return correct messages):
\ 'line': 347, \ 'line': 347,
\ 'offset': 12, \ 'offset': 12,
\ 'prefix': 'abc', \ 'prefix': 'abc',
\ 'includeExternalModuleExports': 1,
\ } \ }
\ ], \ ],
\ ale#lsp#tsserver_message#Completions(bufnr(''), 347, 12, 'abc') \ ale#lsp#tsserver_message#Completions(bufnr(''), 347, 12, 'abc', 1)
Execute(ale#lsp#tsserver_message#CompletionEntryDetails() should return correct messages): Execute(ale#lsp#tsserver_message#CompletionEntryDetails() should return correct messages):
AssertEqual AssertEqual

View file

@ -17,6 +17,7 @@ Before:
\ 'init_queue': [], \ 'init_queue': [],
\ 'capabilities': { \ 'capabilities': {
\ 'hover': 0, \ 'hover': 0,
\ 'rename': 0,
\ 'references': 0, \ 'references': 0,
\ 'completion': 0, \ 'completion': 0,
\ 'completion_trigger_characters': [], \ 'completion_trigger_characters': [],
@ -100,6 +101,7 @@ Execute(Capabilities should bet set up correctly):
\ 'hover': 1, \ 'hover': 1,
\ 'definition': 1, \ 'definition': 1,
\ 'symbol_search': 1, \ 'symbol_search': 1,
\ 'rename': 1,
\ }, \ },
\ b:conn.capabilities \ b:conn.capabilities
AssertEqual [[1, 'initialized', {}]], g:message_list AssertEqual [[1, 'initialized', {}]], g:message_list
@ -110,7 +112,7 @@ Execute(Disabled capabilities should be recognised correctly):
\ 'id': 1, \ 'id': 1,
\ 'result': { \ 'result': {
\ 'capabilities': { \ 'capabilities': {
\ 'renameProvider': v:true, \ 'renameProvider': v:false,
\ 'executeCommandProvider': { \ 'executeCommandProvider': {
\ 'commands': [], \ 'commands': [],
\ }, \ },
@ -143,6 +145,7 @@ Execute(Disabled capabilities should be recognised correctly):
\ 'hover': 0, \ 'hover': 0,
\ 'definition': 0, \ 'definition': 0,
\ 'symbol_search': 0, \ 'symbol_search': 0,
\ 'rename': 0,
\ }, \ },
\ b:conn.capabilities \ b:conn.capabilities
AssertEqual [[1, 'initialized', {}]], g:message_list AssertEqual [[1, 'initialized', {}]], g:message_list

View file

@ -188,6 +188,10 @@ Execute (ALECleanupGroup should include the right commands):
\], CheckAutocmd('ALECleanupGroup') \], CheckAutocmd('ALECleanupGroup')
endif endif
Execute(ALECompletionActions should always be set up):
AssertEqual [
\ 'CompleteDone * call ale#completion#HandleUserData(v:completed_item)',
\], CheckAutocmd('ALECompletionActions')
Execute(Enabling completion should set up autocmd events correctly): Execute(Enabling completion should set up autocmd events correctly):
let g:ale_completion_enabled = 0 let g:ale_completion_enabled = 0

334
test/test_code_action.vader Normal file
View file

@ -0,0 +1,334 @@
Before:
runtime autoload/ale/code_action.vim
runtime autoload/ale/util.vim
let g:file1 = tempname()
let g:file2 = tempname()
let g:test = {}
let g:test.create_change = {line, offset, end_line, end_offset, value ->
\{
\ 'changes': [{
\ 'fileName': g:file1,
\ 'textChanges': [{
\ 'start': {
\ 'line': line,
\ 'offset': offset,
\ },
\ 'end': {
\ 'line': end_line,
\ 'offset': end_offset,
\ },
\ 'newText': value,
\ }],
\ }]
\}}
function! WriteFileAndEdit() abort
let g:test.text = [
\ 'class Name {',
\ ' value: string',
\ '}',
\]
call writefile(g:test.text, g:file1, 'S')
execute 'edit ' . g:file1
endfunction!
After:
" Close the extra buffers if we opened it.
if bufnr(g:file1) != -1
execute ':bp | :bd ' . bufnr(g:file1)
endif
if bufnr(g:file2) != -1
execute ':bp | :bd ' . bufnr(g:file2)
endif
if filereadable(g:file1)
call delete(g:file1)
endif
if filereadable(g:file2)
call delete(g:file2)
endif
unlet g:file1
unlet g:file2
unlet g:test
delfunction WriteFileAndEdit
runtime autoload/ale/code_action.vim
runtime autoload/ale/util.vim
Execute(It should modify and save multiple files):
call writefile([
\ 'class Name {',
\ ' value: string',
\ '}',
\ '',
\ 'class B {',
\ ' constructor(readonly a: Name) {}',
\ '}'
\], g:file1, 'S')
call writefile([
\ 'import A from "A"',
\ 'import {',
\ ' B,',
\ ' C,',
\ '} from "module"',
\ 'import D from "D"',
\], g:file2, 'S')
call ale#code_action#HandleCodeAction({
\ 'changes': [{
\ 'fileName': g:file1,
\ 'textChanges': [{
\ 'start': {
\ 'line': 1,
\ 'offset': 7,
\ },
\ 'end': {
\ 'line': 1,
\ 'offset': 11,
\ },
\ 'newText': 'Value',
\ }, {
\ 'start': {
\ 'line': 6,
\ 'offset': 27,
\ },
\ 'end': {
\ 'line': 6,
\ 'offset': 31,
\ },
\ 'newText': 'Value',
\ }],
\ }, {
\ 'fileName': g:file2,
\ 'textChanges': [{
\ 'start': {
\ 'line': 2,
\ 'offset': 1,
\ },
\ 'end': {
\ 'line': 6,
\ 'offset': 1,
\ },
\ 'newText': "import {A, B} from 'module'\n\n",
\ }]
\ }],
\})
AssertEqual [
\ 'class Value {',
\ ' value: string',
\ '}',
\ '',
\ 'class B {',
\ ' constructor(readonly a: Value) {}',
\ '}',
\ '',
\], readfile(g:file1, 'b')
AssertEqual [
\ 'import A from "A"',
\ 'import {A, B} from ''module''',
\ '',
\ 'import D from "D"',
\ '',
\], readfile(g:file2, 'b')
Execute(Beginning of file can be modified):
let g:test.text = [
\ 'class Name {',
\ ' value: string',
\ '}',
\]
call writefile(g:test.text, g:file1, 'S')
call ale#code_action#HandleCodeAction({
\ 'changes': [{
\ 'fileName': g:file1,
\ 'textChanges': [{
\ 'start': {
\ 'line': 1,
\ 'offset': 1,
\ },
\ 'end': {
\ 'line': 1,
\ 'offset': 1,
\ },
\ 'newText': "type A: string\ntype B: number\n",
\ }],
\ }]
\})
AssertEqual [
\ 'type A: string',
\ 'type B: number',
\] + g:test.text + [''], readfile(g:file1, 'b')
Execute(End of file can be modified):
let g:test.text = [
\ 'class Name {',
\ ' value: string',
\ '}',
\]
call writefile(g:test.text, g:file1, 'S')
call ale#code_action#HandleCodeAction({
\ 'changes': [{
\ 'fileName': g:file1,
\ 'textChanges': [{
\ 'start': {
\ 'line': 4,
\ 'offset': 1,
\ },
\ 'end': {
\ 'line': 4,
\ 'offset': 1,
\ },
\ 'newText': "type A: string\ntype B: number\n",
\ }],
\ }]
\})
AssertEqual g:test.text + [
\ 'type A: string',
\ 'type B: number',
\ '',
\], readfile(g:file1, 'b')
Execute(Current buffer contents will be reloaded):
let g:test.text = [
\ 'class Name {',
\ ' value: string',
\ '}',
\]
call writefile(g:test.text, g:file1, 'S')
execute 'edit ' . g:file1
let g:test.buffer = bufnr(g:file1)
call ale#code_action#HandleCodeAction({
\ 'changes': [{
\ 'fileName': g:file1,
\ 'textChanges': [{
\ 'start': {
\ 'line': 1,
\ 'offset': 1,
\ },
\ 'end': {
\ 'line': 1,
\ 'offset': 1,
\ },
\ 'newText': "type A: string\ntype B: number\n",
\ }],
\ }]
\})
AssertEqual [
\ 'type A: string',
\ 'type B: number',
\] + g:test.text + [''], readfile(g:file1, 'b')
AssertEqual [
\ 'type A: string',
\ 'type B: number',
\] + g:test.text, getbufline(g:test.buffer, 1, '$')
# Tests for cursor repositioning. In comments `=` designates change range, and
# `C` cursor position
# C ===
Execute(Cursor will not move when it is before text change):
call WriteFileAndEdit()
let g:test.changes = g:test.create_change(2, 3, 2, 8, 'value2')
call setpos('.', [0, 1, 1, 0])
call ale#code_action#HandleCodeAction(g:test.changes)
AssertEqual [1, 1], getpos('.')[1:2]
call setpos('.', [0, 2, 2, 0])
call ale#code_action#HandleCodeAction(g:test.changes)
AssertEqual [2, 2], getpos('.')[1:2]
# ====C====
Execute(Cursor column will move to the change end when cursor between start/end):
let g:test.changes = g:test.create_change(2, 3, 2, 8, 'value2')
for r in range(3, 8)
call WriteFileAndEdit()
call setpos('.', [0, 2, r, 0])
AssertEqual ' value: string', getline('.')
call ale#code_action#HandleCodeAction(g:test.changes)
AssertEqual ' value2: string', getline('.')
AssertEqual [2, 9], getpos('.')[1:2]
endfor
# ====C
Execute(Cursor column will move back when new text is shorter):
call WriteFileAndEdit()
call setpos('.', [0, 2, 8, 0])
AssertEqual ' value: string', getline('.')
call ale#code_action#HandleCodeAction(g:test.create_change(2, 3, 2, 8, 'val'))
AssertEqual ' val: string', getline('.')
AssertEqual [2, 6], getpos('.')[1:2]
# ==== C
Execute(Cursor column will move forward when new text is longer):
call WriteFileAndEdit()
call setpos('.', [0, 2, 8, 0])
AssertEqual ' value: string', getline('.')
call ale#code_action#HandleCodeAction(g:test.create_change(2, 3, 2, 8, 'longValue'))
AssertEqual ' longValue: string', getline('.')
AssertEqual [2, 12], getpos('.')[1:2]
# =========
# =
# C
Execute(Cursor line will move when updates are happening on lines above):
call WriteFileAndEdit()
call setpos('.', [0, 3, 1, 0])
AssertEqual '}', getline('.')
call ale#code_action#HandleCodeAction(g:test.create_change(1, 1, 2, 1, "test\ntest\n"))
AssertEqual '}', getline('.')
AssertEqual [4, 1], getpos('.')[1:2]
# =========
# =C
Execute(Cursor line and column will move when change on lines above and just before cursor column):
call WriteFileAndEdit()
call setpos('.', [0, 2, 2, 0])
AssertEqual ' value: string', getline('.')
call ale#code_action#HandleCodeAction(g:test.create_change(1, 1, 2, 1, "test\ntest\n123"))
AssertEqual '123 value: string', getline('.')
AssertEqual [3, 5], getpos('.')[1:2]
# =========
# ======C==
# =
Execute(Cursor line and column will move at the end of changes):
call WriteFileAndEdit()
call setpos('.', [0, 2, 10, 0])
AssertEqual ' value: string', getline('.')
call ale#code_action#HandleCodeAction(g:test.create_change(1, 1, 3, 1, "test\n"))
AssertEqual '}', getline('.')
AssertEqual [2, 1], getpos('.')[1:2]
# C ==
# ===
Execute(Cursor will not move when changes happening on lines >= cursor, but after cursor):
call WriteFileAndEdit()
call setpos('.', [0, 2, 3, 0])
AssertEqual ' value: string', getline('.')
call ale#code_action#HandleCodeAction(g:test.create_change(2, 10, 3, 1, "number\n"))
AssertEqual ' value: number', getline('.')
AssertEqual [2, 3], getpos('.')[1:2]

View file

@ -0,0 +1,171 @@
Before:
call ale#test#SetDirectory('/testplugin/test')
call ale#test#SetFilename('dummy.txt')
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/organize_imports.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) abort
let g:handle_code_action_called = 1
call add(g:code_actions, a:code_action)
endfunction
After:
if g:conn_id isnot v:null
call ale#lsp#RemoveConnectionWithID(g:conn_id)
endif
call ale#references#SetMap({})
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/organize_imports.vim
runtime autoload/ale/code_action.vim
Execute(Other messages for the tsserver handler should be ignored):
call ale#organize_imports#HandleTSServerResponse(1, {'command': 'foo'})
AssertEqual g:handle_code_action_called, 0
Execute(Failed organizeImports responses should be handled correctly):
call ale#organize_imports#HandleTSServerResponse(
\ 1,
\ {'command': 'organizeImports', 'request_seq': 3}
\)
AssertEqual g:handle_code_action_called, 0
Execute(Code actions from tsserver should be handled):
call ale#organize_imports#HandleTSServerResponse(1, {
\ 'command': 'organizeImports',
\ 'request_seq': 3,
\ 'success': v:true,
\ 'body': [],
\})
AssertEqual g:handle_code_action_called, 1
AssertEqual [{
\ 'description': 'Organize Imports',
\ 'changes': [],
\}], g:code_actions
Given typescript(Some typescript file):
foo
somelongerline
bazxyzxyzxyz
Execute(tsserver organize imports requests should be sent):
call ale#linter#Reset()
runtime ale_linters/typescript/tsserver.vim
ALEOrganizeImports
" We shouldn't register the callback yet.
AssertEqual '''''', string(g:Callback)
AssertEqual type(function('type')), type(g:InitCallback)
call g:InitCallback()
AssertEqual
\ 'function(''ale#organize_imports#HandleTSServerResponse'')',
\ string(g:Callback)
AssertEqual
\ [
\ ale#lsp#tsserver_message#Change(bufnr('')),
\ [0, 'ts@organizeImports', {
\ 'scope': {
\ 'type': 'file',
\ 'args': {
\ 'file': expand('%:p'),
\ },
\ },
\ }]
\ ],
\ g:message_list
Given python(Some Python file):
foo
somelongerline
bazxyzxyzxyz
Execute(Should result in error message):
call ale#linter#Reset()
runtime ale_linters/python/pyls.vim
let b:ale_linters = ['pyls']
ALEOrganizeImports
" We shouldn't register the callback yet.
AssertEqual '''''', string(g:Callback)
AssertEqual type(function('type')), type(g:InitCallback)
call g:InitCallback()
AssertEqual [
\ 'echom ''OrganizeImports currently only works with tsserver''',
\], g:expr_list

394
test/test_rename.vader Normal file
View file

@ -0,0 +1,394 @@
Before:
call ale#test#SetDirectory('/testplugin/test')
call ale#test#SetFilename('dummy.txt')
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/rename.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) abort
let g:handle_code_action_called = 1
call add(g:code_actions, a:code_action)
endfunction
function! ale#util#Input(message, value) abort
return 'a-new-name'
endfunction
call ale#rename#SetMap({
\ 3: {
\ 'old_name': 'oldName',
\ 'new_name': 'aNewName',
\ },
\})
After:
if g:conn_id isnot v:null
call ale#lsp#RemoveConnectionWithID(g:conn_id)
endif
call ale#rename#SetMap({})
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/rename.vim
runtime autoload/ale/code_action.vim
Execute(Other messages for the tsserver handler should be ignored):
call ale#rename#HandleTSServerResponse(1, {'command': 'foo'})
AssertEqual g:handle_code_action_called, 0
Execute(Failed rename responses should be handled correctly):
call ale#rename#SetMap({3: {'old_name': 'oldName', 'new_name': 'a-test'}})
call ale#rename#HandleTSServerResponse(
\ 1,
\ {'command': 'rename', '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#rename#HandleTSServerResponse(1, {
\ 'command': 'rename',
\ 'request_seq': 3,
\ 'success': v:true,
\ 'body': {
\ 'locs': [
\ {
\ 'file': '/foo/bar/file1.ts',
\ 'locs': [
\ {
\ 'start': {
\ 'line': 1,
\ 'offset': 2,
\ },
\ 'end': {
\ 'line': 3,
\ 'offset': 4,
\ },
\ },
\ ],
\ },
\ {
\ 'file': '/foo/bar/file2.ts',
\ 'locs': [
\ {
\ 'start': {
\ 'line': 10,
\ 'offset': 20,
\ },
\ 'end': {
\ 'line': 30,
\ 'offset': 40,
\ },
\ },
\ ],
\ },
\ ]
\ },
\})
AssertEqual
\ [
\ {
\ 'description': 'rename',
\ 'changes': [
\ {
\ 'fileName': '/foo/bar/file1.ts',
\ 'textChanges': [
\ {
\ 'start': {
\ 'line': 1,
\ 'offset': 2,
\ },
\ 'end': {
\ 'line': 3,
\ 'offset': 4,
\ },
\ 'newText': 'aNewName',
\ },
\ ],
\ },
\ {
\ 'fileName': '/foo/bar/file2.ts',
\ 'textChanges': [
\ {
\ 'start': {
\ 'line': 10,
\ 'offset': 20,
\ },
\ 'end': {
\ 'line': 30,
\ 'offset': 40,
\ },
\ 'newText': 'aNewName',
\ },
\ ],
\ },
\ ],
\ }
\ ],
\ g:code_actions
Execute(HandleTSServerResponse does nothing when no data in rename_map):
call ale#rename#HandleTSServerResponse(1, {
\ 'command': 'rename',
\ 'request_seq': -9,
\ 'success': v:true,
\ 'body': {}
\})
AssertEqual g:handle_code_action_called, 0
Execute(Prints a tsserver error message when unsuccessful):
call ale#rename#HandleTSServerResponse(1, {
\ 'command': 'rename',
\ 'request_seq': 3,
\ 'success': v:false,
\ 'message': 'This symbol cannot be renamed',
\})
AssertEqual g:handle_code_action_called, 0
AssertEqual ['echom ''Error renaming "oldName" to: "aNewName". ' .
\ 'Reason: This symbol cannot be renamed'''], g:expr_list
Execute(Does nothing when no changes):
call ale#rename#HandleTSServerResponse(1, {
\ 'command': 'rename',
\ 'request_seq': 3,
\ 'success': v:true,
\ 'body': {
\ 'locs': []
\ }
\})
AssertEqual g:handle_code_action_called, 0
AssertEqual ['echom ''Error renaming "oldName" to: "aNewName"'''], g:expr_list
Execute(tsserver rename requests should be sent):
call ale#rename#SetMap({})
call ale#linter#Reset()
runtime ale_linters/typescript/tsserver.vim
call setpos('.', [bufnr(''), 2, 5, 0])
ALERename
" We shouldn't register the callback yet.
AssertEqual '''''', string(g:Callback)
AssertEqual type(function('type')), type(g:InitCallback)
call g:InitCallback()
AssertEqual 'rename', g:capability_checked
AssertEqual
\ 'function(''ale#rename#HandleTSServerResponse'')',
\ string(g:Callback)
AssertEqual
\ [
\ ale#lsp#tsserver_message#Change(bufnr('')),
\ [0, 'ts@rename', {
\ 'file': expand('%:p'),
\ 'line': 2,
\ 'offset': 5,
\ 'arguments': {
\ 'findInComments': g:ale_rename_tsserver_find_in_comments,
\ 'findInStrings': g:ale_rename_tsserver_find_in_strings,
\ },
\ }]
\ ],
\ g:message_list
AssertEqual {'42': {'old_name': 'somelongerline', 'new_name': 'a-new-name'}},
\ ale#rename#GetMap()
Given python(Some Python file):
foo
somelongerline
bazxyzxyzxyz
Execute(Code actions from LSP should be handled):
call ale#rename#HandleLSPResponse(1, {
\ 'id': 3,
\ 'result': {
\ 'changes': {
\ 'file:///foo/bar/file1.ts': [
\ {
\ 'range': {
\ 'start': {
\ 'line': 1,
\ 'character': 2,
\ },
\ 'end': {
\ 'line': 3,
\ 'character': 4,
\ },
\ },
\ 'newText': 'bla123'
\ },
\ ],
\ },
\ },
\})
AssertEqual
\ [
\ {
\ 'description': 'rename',
\ 'changes': [
\ {
\ 'fileName': '/foo/bar/file1.ts',
\ 'textChanges': [
\ {
\ 'start': {
\ 'line': 2,
\ 'offset': 3,
\ },
\ 'end': {
\ 'line': 4,
\ 'offset': 5,
\ },
\ 'newText': 'bla123',
\ },
\ ],
\ },
\ ],
\ }
\ ],
\ g:code_actions
Execute(LSP should perform no action when no result):
call ale#rename#HandleLSPResponse(1, {
\ 'id': 3,
\})
AssertEqual g:handle_code_action_called, 0
AssertEqual ['echom ''No rename result received from server'''], g:expr_list
Execute(LSP should perform no action when no changes):
call ale#rename#HandleLSPResponse(1, {
\ 'id': 3,
\ 'result': {},
\})
AssertEqual g:handle_code_action_called, 0
AssertEqual ['echom ''No changes received from server'''], g:expr_list
Execute(LSP should perform no action when changes is empty):
call ale#rename#HandleLSPResponse(1, {
\ 'id': 3,
\ 'result': {
\ 'changes': [],
\ },
\})
AssertEqual g:handle_code_action_called, 0
AssertEqual ['echom ''No changes received from server'''], g:expr_list
Execute(LSP rename requests should be sent):
call ale#rename#SetMap({})
runtime ale_linters/python/pyls.vim
let b:ale_linters = ['pyls']
call setpos('.', [bufnr(''), 1, 5, 0])
ALERename
" We shouldn't register the callback yet.
AssertEqual '''''', string(g:Callback)
AssertEqual type(function('type')), type(g:InitCallback)
call g:InitCallback()
AssertEqual 'rename', g:capability_checked
AssertEqual
\ 'function(''ale#rename#HandleLSPResponse'')',
\ string(g:Callback)
AssertEqual
\ [
\ [1, 'textDocument/didChange', {
\ 'textDocument': {
\ 'uri': ale#path#ToURI(expand('%:p')),
\ 'version': g:ale_lsp_next_version_id - 1,
\ },
\ 'contentChanges': [{'text': join(getline(1, '$'), "\n") . "\n"}]
\ }],
\ [0, 'textDocument/rename', {
\ 'textDocument': {'uri': ale#path#ToURI(expand('%:p'))},
\ 'position': {'line': 0, 'character': 2},
\ 'newName': 'a-new-name',
\ }],
\ ],
\ g:message_list
AssertEqual {'42': {'old_name': 'foo', 'new_name': 'a-new-name'}},
\ ale#rename#GetMap()