summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDalius Dobravolskas <dalius.dobravolskas@gmail.com>2021-12-17 01:09:26 +0200
committerGitHub <noreply@github.com>2021-12-17 08:09:26 +0900
commit5b792c764196aeb45eb6025c6c1c9727084c2feb (patch)
treedf96221664bac924f584db5e25d5deb736e44a6a
parente4ec2e4dc74fc24d57fd74770d2aa277425dde7c (diff)
downloadale-5b792c764196aeb45eb6025c6c1c9727084c2feb.zip
ALEFileRename command added. (#4012)
* ALEFileRename command added. This command renames file and uses tsserver `getEditsForFileRename` to fix import paths in Typescript files. * ale#util#Input fix * Even more fixes. * Linting error fix.
-rw-r--r--README.md3
-rw-r--r--autoload/ale/filerename.vim133
-rw-r--r--autoload/ale/lsp.vim2
-rw-r--r--autoload/ale/lsp/tsserver_message.vim8
-rw-r--r--autoload/ale/util.vim8
-rw-r--r--doc/ale.txt5
-rw-r--r--plugin/ale.vim4
-rw-r--r--test/test_filerename.vader224
8 files changed, 385 insertions, 2 deletions
diff --git a/README.md b/README.md
index 697f2d87..430ab645 100644
--- a/README.md
+++ b/README.md
@@ -263,6 +263,9 @@ See `:help ale-symbol-search` for more information.
ALE supports renaming symbols in symbols in code such as variables or class
names with the `ALERename` command.
+`ALEFileRename` will rename file and fix import paths (tsserver
+only).
+
`ALECodeAction` will execute actions on the cursor or applied to a visual
range selection, such as automatically fixing errors.
diff --git a/autoload/ale/filerename.vim b/autoload/ale/filerename.vim
new file mode 100644
index 00000000..ec20d279
--- /dev/null
+++ b/autoload/ale/filerename.vim
@@ -0,0 +1,133 @@
+" Author: Dalius Dobravolskas <dalius.dobravolskas@gmail.com>
+" Description: Rename file support for tsserver
+
+let s:filerename_map = {}
+
+" Used to get the rename map in tests.
+function! ale#filerename#GetMap() abort
+ return deepcopy(s:filerename_map)
+endfunction
+
+" Used to set the rename map in tests.
+function! ale#filerename#SetMap(map) abort
+ let s:filerename_map = a:map
+endfunction
+
+function! ale#filerename#ClearLSPData() abort
+ let s:filerename_map = {}
+endfunction
+
+function! s:message(message) abort
+ call ale#util#Execute('echom ' . string(a:message))
+endfunction
+
+function! ale#filerename#HandleTSServerResponse(conn_id, response) abort
+ if get(a:response, 'command', '') isnot# 'getEditsForFileRename'
+ return
+ endif
+
+ if !has_key(s:filerename_map, a:response.request_seq)
+ return
+ endif
+
+ let l:options = remove(s:filerename_map, a:response.request_seq)
+
+ let l:old_name = l:options.old_name
+ let l:new_name = l:options.new_name
+
+ if get(a:response, 'success', v:false) is v:false
+ let l:message = get(a:response, 'message', 'unknown')
+ call s:message('Error renaming file "' . l:old_name . '" to "' . l:new_name
+ \ . '". Reason: ' . l:message)
+
+ return
+ endif
+
+ let l:changes = a:response.body
+
+ if empty(l:changes)
+ call s:message('No changes while renaming "' . l:old_name . '" to "' . l:new_name . '"')
+ else
+ call ale#code_action#HandleCodeAction(
+ \ {
+ \ 'description': 'filerename',
+ \ 'changes': l:changes,
+ \ },
+ \ {
+ \ 'should_save': 1,
+ \ },
+ \)
+ endif
+
+ silent! noautocmd execute 'saveas ' . l:new_name
+ call delete(l:old_name)
+endfunction
+
+function! s:OnReady(options, linter, lsp_details) abort
+ let l:id = a:lsp_details.connection_id
+
+ if !ale#lsp#HasCapability(l:id, 'filerename')
+ return
+ endif
+
+ let l:buffer = a:lsp_details.buffer
+
+ let l:Callback = function('ale#filerename#HandleTSServerResponse')
+
+ call ale#lsp#RegisterCallback(l:id, l:Callback)
+
+ let l:message = ale#lsp#tsserver_message#GetEditsForFileRename(
+ \ a:options.old_name,
+ \ a:options.new_name,
+ \)
+
+ let l:request_id = ale#lsp#Send(l:id, l:message)
+
+ let s:filerename_map[l:request_id] = a:options
+endfunction
+
+function! s:ExecuteFileRename(linter, options) abort
+ let l:buffer = bufnr('')
+
+ let l:Callback = function('s:OnReady', [a:options])
+ call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback)
+endfunction
+
+function! ale#filerename#Execute() abort
+ let l:lsp_linters = []
+
+ for l:linter in ale#linter#Get(&filetype)
+ if l:linter.lsp is# 'tsserver'
+ call add(l:lsp_linters, l:linter)
+ endif
+ endfor
+
+ if empty(l:lsp_linters)
+ call s:message('No active tsserver LSPs')
+
+ return
+ endif
+
+ let l:buffer = bufnr('')
+ let l:old_name = expand('#' . l:buffer . ':p')
+ let l:new_name = ale#util#Input('New file name: ', l:old_name, 'file')
+
+ if l:old_name is# l:new_name
+ call s:message('New file name matches old file name')
+
+ return
+ endif
+
+ 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:ExecuteFileRename(l:lsp_linter, {
+ \ 'old_name': l:old_name,
+ \ 'new_name': l:new_name,
+ \})
+ endfor
+endfunction
diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim
index 75d81525..02723f56 100644
--- a/autoload/ale/lsp.vim
+++ b/autoload/ale/lsp.vim
@@ -38,6 +38,7 @@ function! ale#lsp#Register(executable_or_address, project, init_options) abort
\ 'capabilities': {
\ 'hover': 0,
\ 'rename': 0,
+ \ 'filerename': 0,
\ 'references': 0,
\ 'completion': 0,
\ 'completion_trigger_characters': [],
@@ -380,6 +381,7 @@ function! ale#lsp#MarkConnectionAsTsserver(conn_id) abort
let l:conn.capabilities.typeDefinition = 1
let l:conn.capabilities.symbol_search = 1
let l:conn.capabilities.rename = 1
+ let l:conn.capabilities.filerename = 1
let l:conn.capabilities.code_actions = 1
endfunction
diff --git a/autoload/ale/lsp/tsserver_message.vim b/autoload/ale/lsp/tsserver_message.vim
index 00213a75..7c59feee 100644
--- a/autoload/ale/lsp/tsserver_message.vim
+++ b/autoload/ale/lsp/tsserver_message.vim
@@ -101,6 +101,14 @@ function! ale#lsp#tsserver_message#Rename(
\}]
endfunction
+function! ale#lsp#tsserver_message#GetEditsForFileRename(
+\ oldFilePath, newFilePath) abort
+ return [0, 'ts@getEditsForFileRename', {
+ \ 'oldFilePath': a:oldFilePath,
+ \ 'newFilePath': a:newFilePath,
+ \}]
+endfunction
+
function! ale#lsp#tsserver_message#OrganizeImports(buffer) abort
return [0, 'ts@organizeImports', {
\ 'scope': {
diff --git a/autoload/ale/util.vim b/autoload/ale/util.vim
index ec9383ac..c90229b4 100644
--- a/autoload/ale/util.vim
+++ b/autoload/ale/util.vim
@@ -491,8 +491,12 @@ 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)
+function! ale#util#Input(message, value, ...) abort
+ if a:0 > 0
+ return input(a:message, a:value, a:1)
+ else
+ return input(a:message, a:value)
+ endif
endfunction
function! ale#util#HasBuflineApi() abort
diff --git a/doc/ale.txt b/doc/ale.txt
index dda7a712..8d98d511 100644
--- a/doc/ale.txt
+++ b/doc/ale.txt
@@ -686,6 +686,8 @@ for a full list of options.
ALE supports renaming symbols in code such as variables or class names with
the |ALERename| command.
+`ALEFileRename` will rename file and fix import paths (tsserver only).
+
|ALECodeAction| will execute actions on the cursor or applied to a visual
range selection, such as automatically fixing errors.
@@ -3368,6 +3370,9 @@ ALERename *ALERename*
The symbol where the cursor is resting will be the symbol renamed, and a
prompt will open to request a new name.
+ALEFileRename *ALEFileRename*
+
+ Rename a file and fix imports using `tsserver`.
ALECodeAction *ALECodeAction*
diff --git a/plugin/ale.vim b/plugin/ale.vim
index d19824b1..423a743a 100644
--- a/plugin/ale.vim
+++ b/plugin/ale.vim
@@ -270,6 +270,9 @@ command! -bar ALEImport :call ale#completion#Import()
" Rename symbols using tsserver and LSP
command! -bar -bang ALERename :call ale#rename#Execute()
+" Rename file using tsserver
+command! -bar -bang ALEFileRename :call ale#filerename#Execute()
+
" Apply code actions to a range.
command! -bar -range ALECodeAction :call ale#codefix#Execute(<range>)
@@ -316,6 +319,7 @@ nnoremap <silent> <Plug>(ale_documentation) :ALEDocumentation<Return>
inoremap <silent> <Plug>(ale_complete) <C-\><C-O>:ALEComplete<Return>
nnoremap <silent> <Plug>(ale_import) :ALEImport<Return>
nnoremap <silent> <Plug>(ale_rename) :ALERename<Return>
+nnoremap <silent> <Plug>(ale_filerename) :ALEFileRename<Return>
nnoremap <silent> <Plug>(ale_code_action) :ALECodeAction<Return>
nnoremap <silent> <Plug>(ale_repeat_selection) :ALERepeatSelection<Return>
diff --git a/test/test_filerename.vader b/test/test_filerename.vader
new file mode 100644
index 00000000..c91b3556
--- /dev/null
+++ b/test/test_filerename.vader
@@ -0,0 +1,224 @@
+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/filerename.vim
+ runtime autoload/ale/code_action.vim
+
+ function! ale#lsp_linter#StartLSP(buffer, linter, Callback) abort
+ let g:conn_id = ale#lsp#Register('executable', '/foo/bar', {})
+ call ale#lsp#MarkDocumentAsOpen(g:conn_id, a:buffer)
+
+ if a:linter.lsp is# 'tsserver'
+ call ale#lsp#MarkConnectionAsTsserver(g:conn_id)
+ endif
+
+ let l:details = {
+ \ 'command': 'foobar',
+ \ 'buffer': a:buffer,
+ \ 'connection_id': g:conn_id,
+ \ 'project_root': '/foo/bar',
+ \}
+
+ let g:InitCallback = {-> ale#lsp_linter#OnInit(a:linter, l:details, a:Callback)}
+ endfunction
+
+ function! ale#lsp#HasCapability(conn_id, capability) abort
+ let g:capability_checked = a:capability
+
+ return 1
+ endfunction
+
+ function! ale#lsp#RegisterCallback(conn_id, callback) abort
+ let g:Callback = a:callback
+ endfunction
+
+ function! ale#lsp#Send(conn_id, message) abort
+ call add(g:message_list, a:message)
+
+ return 42
+ endfunction
+
+ function! ale#util#Execute(expr) abort
+ call add(g:expr_list, a:expr)
+ endfunction
+
+ function! ale#code_action#HandleCodeAction(code_action, options) abort
+ let g:handle_code_action_called = 1
+ Assert get(a:options, 'should_save')
+ call add(g:code_actions, a:code_action)
+ endfunction
+
+ function! ale#util#Input(message, value, completion) abort
+ return 'a-new-name'
+ endfunction
+
+ call ale#filerename#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#filerename#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/filerename.vim
+ runtime autoload/ale/code_action.vim
+
+Execute(Other messages for the tsserver handler should be ignored):
+ call ale#filerename#HandleTSServerResponse(1, {'command': 'foo'})
+ AssertEqual g:handle_code_action_called, 0
+
+Execute(Failed file rename responses should be handled correctly):
+ call ale#filerename#SetMap({3: {'old_name': 'oldName', 'new_name': 'a-test'}})
+ call ale#filerename#HandleTSServerResponse(
+ \ 1,
+ \ {'command': 'getEditsForFileRename', '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#filerename#HandleTSServerResponse(1, {
+ \ 'command': 'getEditsForFileRename',
+ \ 'seq': 0,
+ \ 'request_seq': 3,
+ \ 'type': 'response',
+ \ 'success': v:true,
+ \ 'body': [
+ \ {
+ \ 'fileName': '/foo/bar/file1.tsx',
+ \ 'textChanges': [
+ \ {
+ \ 'end': {'offset': 55, 'line': 22},
+ \ 'newText': './file2',
+ \ 'start': {'offset': 34, 'line': 22},
+ \ }
+ \ ]
+ \ }
+ \ ],
+ \})
+
+ AssertEqual
+ \ [
+ \ {
+ \ 'description': 'filerename',
+ \ 'changes': [
+ \ {
+ \ 'fileName': '/foo/bar/file1.tsx',
+ \ 'textChanges': [
+ \ {
+ \ 'end': {'offset': 55, 'line': 22},
+ \ 'newText': './file2',
+ \ 'start': {'offset': 34, 'line': 22},
+ \ }
+ \ ]
+ \ }
+ \ ],
+ \ }
+ \ ],
+ \ g:code_actions
+
+Execute(HandleTSServerResponse does nothing when no data in filerename_map):
+ call ale#filerename#HandleTSServerResponse(1, {
+ \ 'command': 'getEditsForFileRename',
+ \ 'request_seq': -9,
+ \ 'success': v:true,
+ \ 'body': {}
+ \})
+
+ AssertEqual g:handle_code_action_called, 0
+
+Execute(Prints a tsserver error message when unsuccessful):
+ call ale#filerename#HandleTSServerResponse(1, {
+ \ 'command': 'getEditsForFileRename',
+ \ 'request_seq': 3,
+ \ 'success': v:false,
+ \ 'message': 'This file cannot be renamed',
+ \})
+
+ AssertEqual g:handle_code_action_called, 0
+ AssertEqual ['echom ''Error renaming file "oldName" to "aNewName". ' .
+ \ 'Reason: This file cannot be renamed'''], g:expr_list
+
+Execute(Does nothing when no changes):
+ call ale#filerename#HandleTSServerResponse(1, {
+ \ 'command': 'getEditsForFileRename',
+ \ 'request_seq': 3,
+ \ 'success': v:true,
+ \ 'body': [],
+ \})
+
+ AssertEqual g:handle_code_action_called, 0
+ AssertEqual ['echom ''No changes while renaming "oldName" to "aNewName"'''], g:expr_list
+
+Execute(tsserver file rename requests should be sent):
+ call ale#filerename#SetMap({})
+ call ale#linter#Reset()
+
+ runtime ale_linters/typescript/tsserver.vim
+ call setpos('.', [bufnr(''), 2, 5, 0])
+
+ ALEFileRename
+
+ " We shouldn't register the callback yet.
+ AssertEqual '''''', string(g:Callback)
+
+ AssertEqual type(function('type')), type(g:InitCallback)
+ call g:InitCallback()
+
+ AssertEqual 'filerename', g:capability_checked
+ AssertEqual
+ \ 'function(''ale#filerename#HandleTSServerResponse'')',
+ \ string(g:Callback)
+ AssertEqual
+ \ [
+ \ ale#lsp#tsserver_message#Change(bufnr('')),
+ \ [0, 'ts@getEditsForFileRename', {
+ \ 'oldFilePath': expand('%:p'),
+ \ 'newFilePath': 'a-new-name',
+ \ }]
+ \ ],
+ \ g:message_list
+ AssertEqual {'42': {'old_name': expand('%:p'), 'new_name': 'a-new-name'}},
+ \ ale#filerename#GetMap()