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.
This commit is contained in:
Dalius Dobravolskas 2021-12-17 01:09:26 +02:00 committed by GitHub
parent e4ec2e4dc7
commit 5b792c7641
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 385 additions and 2 deletions

View File

@ -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 ALE supports renaming symbols in symbols in code such as variables or class
names with the `ALERename` command. 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 `ALECodeAction` will execute actions on the cursor or applied to a visual
range selection, such as automatically fixing errors. range selection, such as automatically fixing errors.

133
autoload/ale/filerename.vim Normal file
View File

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

View File

@ -38,6 +38,7 @@ function! ale#lsp#Register(executable_or_address, project, init_options) abort
\ 'capabilities': { \ 'capabilities': {
\ 'hover': 0, \ 'hover': 0,
\ 'rename': 0, \ 'rename': 0,
\ 'filerename': 0,
\ 'references': 0, \ 'references': 0,
\ 'completion': 0, \ 'completion': 0,
\ 'completion_trigger_characters': [], \ 'completion_trigger_characters': [],
@ -380,6 +381,7 @@ function! ale#lsp#MarkConnectionAsTsserver(conn_id) abort
let l:conn.capabilities.typeDefinition = 1 let l:conn.capabilities.typeDefinition = 1
let l:conn.capabilities.symbol_search = 1 let l:conn.capabilities.symbol_search = 1
let l:conn.capabilities.rename = 1 let l:conn.capabilities.rename = 1
let l:conn.capabilities.filerename = 1
let l:conn.capabilities.code_actions = 1 let l:conn.capabilities.code_actions = 1
endfunction endfunction

View File

@ -101,6 +101,14 @@ function! ale#lsp#tsserver_message#Rename(
\}] \}]
endfunction 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 function! ale#lsp#tsserver_message#OrganizeImports(buffer) abort
return [0, 'ts@organizeImports', { return [0, 'ts@organizeImports', {
\ 'scope': { \ 'scope': {

View File

@ -491,8 +491,12 @@ 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 function! ale#util#Input(message, value, ...) abort
return input(a:message, a:value) if a:0 > 0
return input(a:message, a:value, a:1)
else
return input(a:message, a:value)
endif
endfunction endfunction
function! ale#util#HasBuflineApi() abort function! ale#util#HasBuflineApi() abort

View File

@ -686,6 +686,8 @@ for a full list of options.
ALE supports renaming symbols in code such as variables or class names with ALE supports renaming symbols in code such as variables or class names with
the |ALERename| command. 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 |ALECodeAction| will execute actions on the cursor or applied to a visual
range selection, such as automatically fixing errors. 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 The symbol where the cursor is resting will be the symbol renamed, and a
prompt will open to request a new name. prompt will open to request a new name.
ALEFileRename *ALEFileRename*
Rename a file and fix imports using `tsserver`.
ALECodeAction *ALECodeAction* ALECodeAction *ALECodeAction*

View File

@ -270,6 +270,9 @@ command! -bar ALEImport :call ale#completion#Import()
" Rename symbols using tsserver and LSP " Rename symbols using tsserver and LSP
command! -bar -bang ALERename :call ale#rename#Execute() 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. " Apply code actions to a range.
command! -bar -range ALECodeAction :call ale#codefix#Execute(<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> inoremap <silent> <Plug>(ale_complete) <C-\><C-O>:ALEComplete<Return>
nnoremap <silent> <Plug>(ale_import) :ALEImport<Return> nnoremap <silent> <Plug>(ale_import) :ALEImport<Return>
nnoremap <silent> <Plug>(ale_rename) :ALERename<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_code_action) :ALECodeAction<Return>
nnoremap <silent> <Plug>(ale_repeat_selection) :ALERepeatSelection<Return> nnoremap <silent> <Plug>(ale_repeat_selection) :ALERepeatSelection<Return>

224
test/test_filerename.vader Normal file
View File

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