summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md8
-rw-r--r--autoload/ale/code_action.vim163
-rw-r--r--autoload/ale/completion.vim59
-rw-r--r--autoload/ale/lsp.vim6
-rw-r--r--autoload/ale/lsp/message.vim10
-rw-r--r--autoload/ale/lsp/tsserver_message.vim28
-rw-r--r--autoload/ale/organize_imports.vim59
-rw-r--r--autoload/ale/rename.vim225
-rw-r--r--autoload/ale/util.vim3
-rw-r--r--doc/ale.txt47
-rw-r--r--plugin/ale.vim7
-rw-r--r--test/completion/test_completion_events.vader53
-rw-r--r--test/completion/test_lsp_completion_messages.vader33
-rw-r--r--test/completion/test_tsserver_completion_parsing.vader38
-rw-r--r--test/lsp/test_lsp_client_messages.vader3
-rw-r--r--test/lsp/test_other_initialize_message_handling.vader5
-rw-r--r--test/test_autocmd_commands.vader4
-rw-r--r--test/test_code_action.vader334
-rw-r--r--test/test_organize_imports.vader171
-rw-r--r--test/test_rename.vader394
20 files changed, 1633 insertions, 17 deletions
diff --git a/README.md b/README.md
index 81c372c3..1488d756 100644
--- a/README.md
+++ b/README.md
@@ -189,6 +189,14 @@ completion manually with `<C-x><C-o>`.
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.
<a name="usage-go-to-definition"></a>
diff --git a/autoload/ale/code_action.vim b/autoload/ale/code_action.vim
new file mode 100644
index 00000000..0af1bb70
--- /dev/null
+++ b/autoload/ale/code_action.vim
@@ -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
diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim
index ebf32909..94e85916 100644
--- a/autoload/ale/completion.vim
+++ b/autoload/ale/completion.vim
@@ -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_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_tsserver_autoimport = get(g:, 'ale_completion_tsserver_autoimport', 0)
let s:timer_id = -1
let s:last_done_pos = []
@@ -296,7 +297,10 @@ function! ale#completion#ParseTSServerCompletions(response) abort
let l:names = []
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
return l:names
@@ -330,13 +334,22 @@ function! ale#completion#ParseTSServerCompletionEntryDetails(response) abort
endif
" See :help complete-items
- call add(l:results, {
+ let l:result = {
\ 'word': l:suggestion.name,
\ 'kind': l:kind,
\ 'icase': 1,
\ 'menu': join(l:displayParts, ''),
+ \ 'dup': g:ale_completion_tsserver_autoimport,
\ '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
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:missing_names = filter(
\ 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
call add(l:results, {
- \ 'word': l:name,
+ \ 'word': l:name.word,
\ 'kind': 'v',
\ 'icase': 1,
\ 'menu': '',
@@ -472,13 +485,22 @@ function! ale#completion#HandleTSServerResponse(conn_id, response) abort
call setbufvar(l:buffer, 'ale_tsserver_completion_names', 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(
\ b:ale_completion_info.conn_id,
\ ale#lsp#tsserver_message#CompletionEntryDetails(
\ l:buffer,
\ b:ale_completion_info.line,
\ b:ale_completion_info.column,
- \ l:names,
+ \ l:identifiers,
\ ),
\)
endif
@@ -525,6 +547,7 @@ function! s:OnReady(linter, lsp_details) abort
\ b:ale_completion_info.line,
\ b:ale_completion_info.column,
\ b:ale_completion_info.prefix,
+ \ g:ale_completion_tsserver_autoimport,
\)
else
" 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'))
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
silent! pclose
@@ -700,6 +743,10 @@ function! ale#completion#Done() abort
let s:last_done_pos = getpos('.')[1:2]
endfunction
+augroup ALECompletionActions
+ autocmd CompleteDone * call ale#completion#HandleUserData(v:completed_item)
+augroup END
+
function! s:Setup(enabled) abort
augroup ALECompletionGroup
autocmd!
diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim
index 017096cd..2509174e 100644
--- a/autoload/ale/lsp.vim
+++ b/autoload/ale/lsp.vim
@@ -37,6 +37,7 @@ function! ale#lsp#Register(executable_or_address, project, init_options) abort
\ 'init_queue': [],
\ 'capabilities': {
\ 'hover': 0,
+ \ 'rename': 0,
\ 'references': 0,
\ 'completion': 0,
\ 'completion_trigger_characters': [],
@@ -199,6 +200,10 @@ function! s:UpdateCapabilities(conn, capabilities) abort
let a:conn.capabilities.references = 1
endif
+ if get(a:capabilities, 'renameProvider') is v:true
+ let a:conn.capabilities.rename = 1
+ endif
+
if !empty(get(a:capabilities, 'completionProvider'))
let a:conn.capabilities.completion = 1
endif
@@ -317,6 +322,7 @@ function! ale#lsp#MarkConnectionAsTsserver(conn_id) abort
let l:conn.capabilities.completion_trigger_characters = ['.']
let l:conn.capabilities.definition = 1
let l:conn.capabilities.symbol_search = 1
+ let l:conn.capabilities.rename = 1
endfunction
function! s:SendInitMessage(conn) abort
diff --git a/autoload/ale/lsp/message.vim b/autoload/ale/lsp/message.vim
index b6b14a22..5b0cb8b7 100644
--- a/autoload/ale/lsp/message.vim
+++ b/autoload/ale/lsp/message.vim
@@ -162,3 +162,13 @@ function! ale#lsp#message#DidChangeConfiguration(buffer, config) abort
\ 'settings': a:config,
\}]
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
diff --git a/autoload/ale/lsp/tsserver_message.vim b/autoload/ale/lsp/tsserver_message.vim
index d6919516..b9fafaa0 100644
--- a/autoload/ale/lsp/tsserver_message.vim
+++ b/autoload/ale/lsp/tsserver_message.vim
@@ -36,12 +36,14 @@ function! ale#lsp#tsserver_message#Geterr(buffer) abort
return [1, 'ts@geterr', {'files': [expand('#' . a:buffer . ':p')]}]
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', {
\ 'line': a:line,
\ 'offset': a:column,
\ 'file': expand('#' . a:buffer . ':p'),
\ 'prefix': a:prefix,
+ \ 'includeExternalModuleExports': a:include_external,
\}]
endfunction
@@ -77,3 +79,27 @@ function! ale#lsp#tsserver_message#Quickinfo(buffer, line, column) abort
\ 'file': expand('#' . a:buffer . ':p'),
\}]
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
diff --git a/autoload/ale/organize_imports.vim b/autoload/ale/organize_imports.vim
new file mode 100644
index 00000000..bc9b829e
--- /dev/null
+++ b/autoload/ale/organize_imports.vim
@@ -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
diff --git a/autoload/ale/rename.vim b/autoload/ale/rename.vim
new file mode 100644
index 00000000..02b7b579
--- /dev/null
+++ b/autoload/ale/rename.vim
@@ -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
diff --git a/autoload/ale/util.vim b/autoload/ale/util.vim
index e7563608..99cd856a 100644
--- a/autoload/ale/util.vim
+++ b/autoload/ale/util.vim
@@ -477,3 +477,6 @@ function! ale#util#FindItemAtCursor(buffer) abort
return [l:info, l:loc]
endfunction
+function! ale#util#Input(message, value) abort
+ return input(a:message, a:value)
+endfunction
diff --git a/doc/ale.txt b/doc/ale.txt
index 43e124a2..aa8dbafe 100644
--- a/doc/ale.txt
+++ b/doc/ale.txt
@@ -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
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 Automatic completion implementation replaces |completeopt| before opening
@@ -597,6 +601,16 @@ b:ale_completion_enabled *b:ale_completion_enabled*
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*
*b:ale_completion_excluded_words*
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|.
+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*
*b:ale_set_balloons*
@@ -2543,6 +2578,18 @@ ALEHover *ALEHover*
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*
Search for symbols in the workspace, taken from any available LSP linters.
diff --git a/plugin/ale.vim b/plugin/ale.vim
index 6262a7c4..1912a9c0 100644
--- a/plugin/ale.vim
+++ b/plugin/ale.vim
@@ -221,6 +221,12 @@ command! -nargs=1 ALESymbolSearch :call ale#symbol#Search(<q-args>)
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
nnoremap <silent> <Plug>(ale_previous) :ALEPrevious<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_documentation) :ALEDocumentation<Return>
inoremap <silent> <Plug>(ale_complete) <C-\><C-O>:ALEComplete<Return>
+nnoremap <silent> <Plug>(ale_rename) :ALERename<Return>
" Set up autocmd groups now.
call ale#events#Init()
diff --git a/test/completion/test_completion_events.vader b/test/completion/test_completion_events.vader
index 5672f8e5..e06ac98b 100644
--- a/test/completion/test_completion_events.vader
+++ b/test/completion/test_completion_events.vader
@@ -47,6 +47,14 @@ Before:
AssertEqual a:expect_success, g:get_completions_called
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:
Restore
@@ -54,6 +62,7 @@ After:
unlet! g:output
unlet! g:fake_mode
unlet! g:get_completions_called
+ unlet! g:handle_code_action_called
unlet! b:ale_old_omnifunc
unlet! b:ale_old_completeopt
unlet! b:ale_completion_info
@@ -61,6 +70,8 @@ After:
unlet! b:ale_complete_done_time
delfunction CheckCompletionCalled
+ delfunction ale#code_action#HandleCodeAction
+ delfunction MockHandleCodeAction
if exists('*CompleteCallback')
delfunction CompleteCallback
@@ -77,6 +88,7 @@ After:
endfunction
runtime autoload/ale/completion.vim
+ runtime autoload/ale/code_action.vim
runtime autoload/ale/util.vim
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
Assert !has_key(b:, 'ale_old_omnifunc')
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
diff --git a/test/completion/test_lsp_completion_messages.vader b/test/completion/test_lsp_completion_messages.vader
index 6bd241a8..b997ac86 100644
--- a/test/completion/test_lsp_completion_messages.vader
+++ b/test/completion/test_lsp_completion_messages.vader
@@ -116,7 +116,13 @@ Execute(The right message should be sent for the initial tsserver request):
\ string(g:Callback)
" We should send the right message.
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
" We should set up the completion info correctly.
AssertEqual
@@ -151,7 +157,7 @@ Execute(The right message sent to the tsserver LSP when the first completion mes
\ 'body': [
\ {'name': 'Baz'},
\ {'name': 'dingDong'},
- \ {'name': 'Foo'},
+ \ {'name': 'Foo', 'source': '/path/to/foo.ts'},
\ {'name': 'FooBar'},
\ {'name': 'frazzle'},
\ {'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
" details for every name.
- AssertEqual
- \ ['Foo', 'FooBar', 'frazzle'],
+ AssertEqual [{
+ \ 'word': 'Foo',
+ \ 'source': '/path/to/foo.ts',
+ \ }, {
+ \ 'word': 'FooBar',
+ \ 'source': '',
+ \ }, {
+ \ 'word': 'frazzle',
+ \ 'source': '',
+ \}],
\ get(b:, 'ale_tsserver_completion_names', [])
" 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',
\ {
\ 'file': expand('%:p'),
- \ 'entryNames': ['Foo', 'FooBar', 'frazzle'],
+ \ 'entryNames': [{
+ \ 'name': 'Foo',
+ \ 'source': '/path/to/foo.ts',
+ \ }, {
+ \ 'name': 'FooBar',
+ \ 'source': '',
+ \ }, {
+ \ 'name': 'frazzle',
+ \ 'source': '',
+ \ }],
\ 'offset': 1,
\ 'line': 1,
\ },
diff --git a/test/completion/test_tsserver_completion_parsing.vader b/test/completion/test_tsserver_completion_parsing.vader
index dbc4f9e2..02f287a9 100644
--- a/test/completion/test_tsserver_completion_parsing.vader
+++ b/test/completion/test_tsserver_completion_parsing.vader
@@ -6,10 +6,24 @@ Execute(TypeScript completions responses should be parsed correctly):
\ ale#completion#ParseTSServerCompletions({
\ 'body': [],
\})
- AssertEqual ['foo', 'bar', 'baz'],
+ AssertEqual
+ \ [
+ \ {
+ \ 'word': 'foo',
+ \ 'source': '/path/to/foo.ts',
+ \ },
+ \ {
+ \ 'word': 'bar',
+ \ 'source': '',
+ \ },
+ \ {
+ \ 'word': 'baz',
+ \ 'source': '',
+ \ }
+ \ ],
\ ale#completion#ParseTSServerCompletions({
\ 'body': [
- \ {'name': 'foo'},
+ \ {'name': 'foo', 'source': '/path/to/foo.ts'},
\ {'name': 'bar'},
\ {'name': 'baz'},
\ ],
@@ -24,6 +38,7 @@ Execute(TypeScript completion details responses should be parsed correctly):
\ 'info': '',
\ 'kind': 'f',
\ 'icase': 1,
+ \ 'dup': g:ale_completion_tsserver_autoimport,
\ },
\ {
\ 'word': 'def',
@@ -31,6 +46,7 @@ Execute(TypeScript completion details responses should be parsed correctly):
\ 'info': 'foo bar baz',
\ 'kind': 'f',
\ 'icase': 1,
+ \ 'dup': g:ale_completion_tsserver_autoimport,
\ },
\ {
\ 'word': 'ghi',
@@ -38,6 +54,7 @@ Execute(TypeScript completion details responses should be parsed correctly):
\ 'info': '',
\ 'kind': 'f',
\ 'icase': 1,
+ \ 'dup': g:ale_completion_tsserver_autoimport,
\ },
\ ],
\ 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):
- let b:ale_tsserver_completion_names = ['xyz']
+ let b:ale_tsserver_completion_names = [{
+ \ 'word': 'xyz',
+ \ 'source': '/path/to/xyz.ts',
+ \ }]
AssertEqual
\ [
@@ -106,6 +126,13 @@ Execute(Entries without details should be included in the responses):
\ 'info': '',
\ 'kind': 'f',
\ 'icase': 1,
+ \ 'user_data': json_encode({
+ \ 'codeActions': [{
+ \ 'description': 'abc action',
+ \ 'changes': [],
+ \ }],
+ \ }),
+ \ 'dup': g:ale_completion_tsserver_autoimport,
\ },
\ {
\ 'word': 'def',
@@ -113,6 +140,7 @@ Execute(Entries without details should be included in the responses):
\ 'info': 'foo bar baz',
\ 'kind': 'f',
\ 'icase': 1,
+ \ 'dup': g:ale_completion_tsserver_autoimport,
\ },
\ {
\ 'word': 'xyz',
@@ -139,6 +167,10 @@ Execute(Entries without details should be included in the responses):
\ {'text': ' '},
\ {'text': 'number'},
\ ],
+ \ 'codeActions': [{
+ \ 'description': 'abc action',
+ \ 'changes': [],
+ \ }],
\ },
\ {
\ 'name': 'def',
diff --git a/test/lsp/test_lsp_client_messages.vader b/test/lsp/test_lsp_client_messages.vader
index 90a20832..bc91bf68 100644
--- a/test/lsp/test_lsp_client_messages.vader
+++ b/test/lsp/test_lsp_client_messages.vader
@@ -275,9 +275,10 @@ Execute(ale#lsp#tsserver_message#Completions() should return correct messages):
\ 'line': 347,
\ 'offset': 12,
\ '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):
AssertEqual
diff --git a/test/lsp/test_other_initialize_message_handling.vader b/test/lsp/test_other_initialize_message_handling.vader
index 0372765d..6473e283 100644
--- a/test/lsp/test_other_initialize_message_handling.vader
+++ b/test/lsp/test_other_initialize_message_handling.vader
@@ -17,6 +17,7 @@ Before:
\ 'init_queue': [],
\ 'capabilities': {
\ 'hover': 0,
+ \ 'rename': 0,
\ 'references': 0,
\ 'completion': 0,
\ 'completion_trigger_characters': [],
@@ -100,6 +101,7 @@ Execute(Capabilities should bet set up correctly):
\ 'hover': 1,
\ 'definition': 1,
\ 'symbol_search': 1,
+ \ 'rename': 1,
\ },
\ b:conn.capabilities
AssertEqual [[1, 'initialized', {}]], g:message_list
@@ -110,7 +112,7 @@ Execute(Disabled capabilities should be recognised correctly):
\ 'id': 1,
\ 'result': {
\ 'capabilities': {
- \ 'renameProvider': v:true,
+ \ 'renameProvider': v:false,
\ 'executeCommandProvider': {
\ 'commands': [],
\ },
@@ -143,6 +145,7 @@ Execute(Disabled capabilities should be recognised correctly):
\ 'hover': 0,
\ 'definition': 0,
\ 'symbol_search': 0,
+ \ 'rename': 0,
\ },
\ b:conn.capabilities
AssertEqual [[1, 'initialized', {}]], g:message_list
diff --git a/test/test_autocmd_commands.vader b/test/test_autocmd_commands.vader
index 241e7d3e..355b4c77 100644
--- a/test/test_autocmd_commands.vader
+++ b/test/test_autocmd_commands.vader
@@ -188,6 +188,10 @@ Execute (ALECleanupGroup should include the right commands):
\], CheckAutocmd('ALECleanupGroup')
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):
let g:ale_completion_enabled = 0
diff --git a/test/test_code_action.vader b/test/test_code_action.vader
new file mode 100644
index 00000000..ffaca630
--- /dev/null
+++ b/test/test_code_action.vader
@@ -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]
diff --git a/test/test_organize_imports.vader b/test/test_organize_imports.vader
new file mode 100644
index 00000000..137326a9
--- /dev/null
+++ b/test/test_organize_imports.vader
@@ -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
diff --git a/test/test_rename.vader b/test/test_rename.vader
new file mode 100644
index 00000000..98e3ef30
--- /dev/null
+++ b/test/test_rename.vader
@@ -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()