diff options
Diffstat (limited to 'autoload')
-rw-r--r-- | autoload/ale.vim | 2 | ||||
-rw-r--r-- | autoload/ale/code_action.vim | 217 | ||||
-rw-r--r-- | autoload/ale/codefix.vim | 484 | ||||
-rw-r--r-- | autoload/ale/completion.vim | 3 | ||||
-rw-r--r-- | autoload/ale/engine.vim | 83 | ||||
-rw-r--r-- | autoload/ale/fix/registry.vim | 5 | ||||
-rw-r--r-- | autoload/ale/fixers/luafmt.vim | 13 | ||||
-rw-r--r-- | autoload/ale/handlers/eslint.vim | 1 | ||||
-rw-r--r-- | autoload/ale/handlers/sh.vim | 22 | ||||
-rw-r--r-- | autoload/ale/handlers/shellcheck.vim | 9 | ||||
-rw-r--r-- | autoload/ale/lsp.vim | 10 | ||||
-rw-r--r-- | autoload/ale/lsp/message.vim | 22 | ||||
-rw-r--r-- | autoload/ale/lsp/response.vim | 1 | ||||
-rw-r--r-- | autoload/ale/lsp/tsserver_message.vim | 36 | ||||
-rw-r--r-- | autoload/ale/maven.vim | 51 | ||||
-rw-r--r-- | autoload/ale/organize_imports.vim | 11 | ||||
-rw-r--r-- | autoload/ale/rename.vim | 117 |
17 files changed, 924 insertions, 163 deletions
diff --git a/autoload/ale.vim b/autoload/ale.vim index 84003993..3f59a6a4 100644 --- a/autoload/ale.vim +++ b/autoload/ale.vim @@ -157,7 +157,7 @@ function! ale#Queue(delay, ...) abort endif endfunction -let s:current_ale_version = [2, 7, 0] +let s:current_ale_version = [3, 0, 0] " A function used to check for ALE features in files outside of the project. function! ale#Has(feature) abort diff --git a/autoload/ale/code_action.vim b/autoload/ale/code_action.vim index 359bc0d6..6b808b34 100644 --- a/autoload/ale/code_action.vim +++ b/autoload/ale/code_action.vim @@ -1,26 +1,33 @@ " Author: Jerko Steiner <jerko.steiner@gmail.com> " Description: Code action support for LSP / tsserver -function! ale#code_action#HandleCodeAction(code_action, should_save) abort +function! ale#code_action#HandleCodeAction(code_action, options) abort let l:current_buffer = bufnr('') let l:changes = a:code_action.changes + let l:should_save = get(a:options, 'should_save') + let l:force_save = get(a:options, 'force_save') + let l:safe_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''') + if !l:force_save + call ale#util#Execute('echom ''Aborting action, file is unsaved''') - return + return + endif + else + call add(l:safe_changes, l:file_code_edit) endif endfor - for l:file_code_edit in l:changes + for l:file_code_edit in l:safe_changes call ale#code_action#ApplyChanges( - \ l:file_code_edit.fileName, - \ l:file_code_edit.textChanges, - \ a:should_save, - \ ) + \ l:file_code_edit.fileName, + \ l:file_code_edit.textChanges, + \ l:should_save, + \) endfor endfunction @@ -78,29 +85,14 @@ function! ale#code_action#ApplyChanges(filename, changes, should_save) abort let l:pos = [1, 1] endif - " We have to keep track of how many lines we have added, and offset - " changes accordingly. - let l:line_offset = 0 - let l:column_offset = 0 - let l:last_end_line = 0 - - " Changes have to be sorted so we apply them from top-to-bottom. - for l:code_edit in sort(copy(a:changes), function('s:ChangeCmp')) - if l:code_edit.start.line isnot l:last_end_line - let l:column_offset = 0 - endif - - let l:line = l:code_edit.start.line + l:line_offset - let l:column = l:code_edit.start.offset + l:column_offset - let l:end_line = l:code_edit.end.line + l:line_offset - let l:end_column = l:code_edit.end.offset + l:column_offset + " Changes have to be sorted so we apply them from bottom-to-top + for l:code_edit in reverse(sort(copy(a:changes), function('s:ChangeCmp'))) + let l:line = l:code_edit.start.line + let l:column = l:code_edit.start.offset + let l:end_line = l:code_edit.end.line + let l:end_column = l:code_edit.end.offset let l:text = l:code_edit.newText - let l:cur_line = l:pos[0] - let l:cur_column = l:pos[1] - - let l:last_end_line = l:end_line - " Adjust the ends according to previous edits. if l:end_line > len(l:lines) let l:end_line_len = 0 @@ -118,6 +110,12 @@ function! ale#code_action#ApplyChanges(filename, changes, should_save) abort let l:start = l:lines[: l:line - 2] endif + " Special case when text must be added after new line + if l:column > len(l:lines[l:line - 1]) + call extend(l:start, [l:lines[l:line - 1]]) + let l:column = 1 + endif + if l:column is 1 " We need to handle column 1 specially, because we can't slice an " empty string ending on index 0. @@ -138,7 +136,6 @@ function! ale#code_action#ApplyChanges(filename, changes, should_save) abort let l:lines = l:start + l:middle + l:lines[l:end_line :] let l:current_line_offset = len(l:lines) - l:lines_before_change - let l:line_offset += l:current_line_offset let l:column_offset = len(l:middle[-1]) - l:end_line_len let l:pos = s:UpdateCursor(l:pos, @@ -213,3 +210,163 @@ function! s:UpdateCursor(cursor, start, end, offset) abort return [l:cur_line, l:cur_column] endfunction + +function! ale#code_action#GetChanges(workspace_edit) abort + let l:changes = {} + + if has_key(a:workspace_edit, 'changes') && !empty(a:workspace_edit.changes) + return a:workspace_edit.changes + elseif has_key(a:workspace_edit, 'documentChanges') + let l:document_changes = [] + + if type(a:workspace_edit.documentChanges) is v:t_dict + \ && has_key(a:workspace_edit.documentChanges, 'edits') + call add(l:document_changes, a:workspace_edit.documentChanges) + elseif type(a:workspace_edit.documentChanges) is v:t_list + let l:document_changes = a:workspace_edit.documentChanges + endif + + for l:text_document_edit in l:document_changes + let l:filename = l:text_document_edit.textDocument.uri + let l:edits = l:text_document_edit.edits + let l:changes[l:filename] = l:edits + endfor + endif + + return l:changes +endfunction + +function! ale#code_action#BuildChangesList(changes_map) abort + let l:changes = [] + + for l:file_name in keys(a:changes_map) + let l:text_edits = a:changes_map[l:file_name] + let l:text_changes = [] + + for l:edit in l:text_edits + let l:range = l:edit.range + let l:new_text = l:edit.newText + + call add(l:text_changes, { + \ 'start': { + \ 'line': l:range.start.line + 1, + \ 'offset': l:range.start.character + 1, + \ }, + \ 'end': { + \ 'line': l:range.end.line + 1, + \ 'offset': l:range.end.character + 1, + \ }, + \ 'newText': l:new_text, + \}) + endfor + + call add(l:changes, { + \ 'fileName': ale#path#FromURI(l:file_name), + \ 'textChanges': l:text_changes, + \}) + endfor + + return l:changes +endfunction + +function! s:EscapeMenuName(text) abort + return substitute(a:text, '\\\| \|\.\|&', '\\\0', 'g') +endfunction + +function! s:UpdateMenu(data, menu_items) abort + silent! aunmenu PopUp.Refactor\.\.\. + + if empty(a:data) + return + endif + + for [l:type, l:item] in a:menu_items + let l:name = l:type is# 'tsserver' ? l:item.name : l:item.title + let l:func_name = l:type is# 'tsserver' + \ ? 'ale#codefix#ApplyTSServerCodeAction' + \ : 'ale#codefix#ApplyLSPCodeAction' + + execute printf( + \ 'anoremenu <silent> PopUp.&Refactor\.\.\..%s' + \ . ' :call %s(%s, %s)<CR>', + \ s:EscapeMenuName(l:name), + \ l:func_name, + \ string(a:data), + \ string(l:item), + \) + endfor + + if empty(a:menu_items) + silent! anoremenu PopUp.Refactor\.\.\..(None) :silent + endif +endfunction + +function! s:GetCodeActions(linter, options) abort + let l:buffer = bufnr('') + let [l:line, l:column] = getpos('.')[1:2] + let l:column = min([l:column, len(getline(l:line))]) + + let l:location = { + \ 'buffer': l:buffer, + \ 'line': l:line, + \ 'column': l:column, + \ 'end_line': l:line, + \ 'end_column': l:column, + \} + let l:Callback = function('s:OnReady', [l:location, a:options]) + call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback) +endfunction + +function! ale#code_action#GetCodeActions(options) abort + silent! aunmenu PopUp.Rename + silent! aunmenu PopUp.Refactor\.\.\. + + " Only display the menu items if there's an LSP server. + let l:has_lsp = 0 + + for l:linter in ale#linter#Get(&filetype) + if !empty(l:linter.lsp) + let l:has_lsp = 1 + + break + endif + endfor + + if l:has_lsp + if !empty(expand('<cword>')) + silent! anoremenu <silent> PopUp.Rename :ALERename<CR> + endif + + silent! anoremenu <silent> PopUp.Refactor\.\.\..(None) :silent<CR> + + call ale#codefix#Execute( + \ mode() is# 'v' || mode() is# "\<C-V>", + \ function('s:UpdateMenu') + \) + endif +endfunction + +function! s:Setup(enabled) abort + augroup ALECodeActionsGroup + autocmd! + + if a:enabled + autocmd MenuPopup * :call ale#code_action#GetCodeActions({}) + endif + augroup END + + if !a:enabled + silent! augroup! ALECodeActionsGroup + + silent! aunmenu PopUp.Rename + silent! aunmenu PopUp.Refactor\.\.\. + endif +endfunction + +function! ale#code_action#EnablePopUpMenu() abort + call s:Setup(1) +endfunction + +function! ale#code_action#DisablePopUpMenu() abort + call s:Setup(0) +endfunction diff --git a/autoload/ale/codefix.vim b/autoload/ale/codefix.vim new file mode 100644 index 00000000..69bf36fa --- /dev/null +++ b/autoload/ale/codefix.vim @@ -0,0 +1,484 @@ +" Author: Dalius Dobravolskas <dalius.dobravolskas@gmail.com> +" Description: Code Fix support for tsserver and LSP servers + +let s:codefix_map = {} + +" Used to get the codefix map in tests. +function! ale#codefix#GetMap() abort + return deepcopy(s:codefix_map) +endfunction + +" Used to set the codefix map in tests. +function! ale#codefix#SetMap(map) abort + let s:codefix_map = a:map +endfunction + +function! ale#codefix#ClearLSPData() abort + let s:codefix_map = {} +endfunction + +function! s:message(message) abort + call ale#util#Execute('echom ' . string(a:message)) +endfunction + +function! ale#codefix#ApplyTSServerCodeAction(data, item) abort + if has_key(a:item, 'changes') + let l:changes = a:item.changes + + call ale#code_action#HandleCodeAction( + \ { + \ 'description': 'codefix', + \ 'changes': l:changes, + \ }, + \ {}, + \) + else + let l:message = ale#lsp#tsserver_message#GetEditsForRefactor( + \ a:data.buffer, + \ a:data.line, + \ a:data.column, + \ a:data.end_line, + \ a:data.end_column, + \ a:item.id[0], + \ a:item.id[1], + \) + + let l:request_id = ale#lsp#Send(a:data.connection_id, l:message) + + let s:codefix_map[l:request_id] = a:data + endif +endfunction + +function! ale#codefix#HandleTSServerResponse(conn_id, response) abort + if !has_key(a:response, 'request_seq') + \ || !has_key(s:codefix_map, a:response.request_seq) + return + endif + + let l:data = remove(s:codefix_map, a:response.request_seq) + let l:MenuCallback = get(l:data, 'menu_callback', v:null) + + if get(a:response, 'command', '') is# 'getCodeFixes' + if get(a:response, 'success', v:false) is v:false + \&& l:MenuCallback is v:null + let l:message = get(a:response, 'message', 'unknown') + call s:message('Error while getting code fixes. Reason: ' . l:message) + + return + endif + + let l:result = get(a:response, 'body', []) + call filter(l:result, 'has_key(v:val, ''changes'')') + + if l:MenuCallback isnot v:null + call l:MenuCallback( + \ l:data, + \ map(copy(l:result), '[''tsserver'', v:val]') + \) + + return + endif + + if len(l:result) == 0 + call s:message('No code fixes available.') + + return + endif + + let l:code_fix_to_apply = 0 + + if len(l:result) == 1 + let l:code_fix_to_apply = 1 + else + let l:codefix_no = 1 + let l:codefixstring = "Code Fixes:\n" + + for l:codefix in l:result + let l:codefixstring .= l:codefix_no . ') ' + \ . l:codefix.description . "\n" + let l:codefix_no += 1 + endfor + + let l:codefixstring .= 'Type number and <Enter> (empty cancels): ' + + let l:code_fix_to_apply = ale#util#Input(l:codefixstring, '') + let l:code_fix_to_apply = str2nr(l:code_fix_to_apply) + + if l:code_fix_to_apply == 0 + return + endif + endif + + call ale#codefix#ApplyTSServerCodeAction( + \ l:data, + \ l:result[l:code_fix_to_apply - 1], + \) + elseif get(a:response, 'command', '') is# 'getApplicableRefactors' + if get(a:response, 'success', v:false) is v:false + \&& l:MenuCallback is v:null + let l:message = get(a:response, 'message', 'unknown') + call s:message('Error while getting applicable refactors. Reason: ' . l:message) + + return + endif + + let l:result = get(a:response, 'body', []) + + if len(l:result) == 0 + call s:message('No applicable refactors available.') + + return + endif + + let l:refactors = [] + + for l:item in l:result + for l:action in l:item.actions + call add(l:refactors, { + \ 'name': l:action.description, + \ 'id': [l:item.name, l:action.name], + \}) + endfor + endfor + + if l:MenuCallback isnot v:null + call l:MenuCallback( + \ l:data, + \ map(copy(l:refactors), '[''tsserver'', v:val]') + \) + + return + endif + + let l:refactor_no = 1 + let l:refactorstring = "Applicable refactors:\n" + + for l:refactor in l:refactors + let l:refactorstring .= l:refactor_no . ') ' + \ . l:refactor.name . "\n" + let l:refactor_no += 1 + endfor + + let l:refactorstring .= 'Type number and <Enter> (empty cancels): ' + + let l:refactor_to_apply = ale#util#Input(l:refactorstring, '') + let l:refactor_to_apply = str2nr(l:refactor_to_apply) + + if l:refactor_to_apply == 0 + return + endif + + let l:id = l:refactors[l:refactor_to_apply - 1].id + + call ale#codefix#ApplyTSServerCodeAction( + \ l:data, + \ l:refactors[l:refactor_to_apply - 1], + \) + elseif get(a:response, 'command', '') is# 'getEditsForRefactor' + if get(a:response, 'success', v:false) is v:false + let l:message = get(a:response, 'message', 'unknown') + call s:message('Error while getting edits for refactor. Reason: ' . l:message) + + return + endif + + call ale#code_action#HandleCodeAction( + \ { + \ 'description': 'editsForRefactor', + \ 'changes': a:response.body.edits, + \ }, + \ {}, + \) + endif +endfunction + +function! ale#codefix#ApplyLSPCodeAction(data, item) abort + if has_key(a:item, 'command') + \&& type(a:item.command) == v:t_dict + let l:command = a:item.command + let l:message = ale#lsp#message#ExecuteCommand( + \ l:command.command, + \ l:command.arguments, + \) + + let l:request_id = ale#lsp#Send(a:data.connection_id, l:message) + elseif has_key(a:item, 'edit') || has_key(a:item, 'arguments') + if has_key(a:item, 'edit') + let l:topass = a:item.edit + else + let l:topass = a:item.arguments[0] + endif + + let l:changes_map = ale#code_action#GetChanges(l:topass) + + if empty(l:changes_map) + return + endif + + let l:changes = ale#code_action#BuildChangesList(l:changes_map) + + call ale#code_action#HandleCodeAction( + \ { + \ 'description': 'codeaction', + \ 'changes': l:changes, + \ }, + \ {}, + \) + endif +endfunction + +function! ale#codefix#HandleLSPResponse(conn_id, response) abort + if has_key(a:response, 'method') + \ && a:response.method is# 'workspace/applyEdit' + \ && has_key(a:response, 'params') + let l:params = a:response.params + + let l:changes_map = ale#code_action#GetChanges(l:params.edit) + + if empty(l:changes_map) + return + endif + + let l:changes = ale#code_action#BuildChangesList(l:changes_map) + + call ale#code_action#HandleCodeAction( + \ { + \ 'description': 'applyEdit', + \ 'changes': l:changes, + \ }, + \ {} + \) + elseif has_key(a:response, 'id') + \&& has_key(s:codefix_map, a:response.id) + let l:data = remove(s:codefix_map, a:response.id) + let l:MenuCallback = get(l:data, 'menu_callback', v:null) + + let l:result = get(a:response, 'result') + + if type(l:result) != v:t_list + let l:result = [] + endif + + " Send the results to the menu callback, if set. + if l:MenuCallback isnot v:null + call l:MenuCallback(map(copy(l:result), '[''lsp'', v:val]')) + + return + endif + + if len(l:result) == 0 + call s:message('No code actions received from server') + + return + endif + + let l:codeaction_no = 1 + let l:codeactionstring = "Code Fixes:\n" + + for l:codeaction in l:result + let l:codeactionstring .= l:codeaction_no . ') ' + \ . l:codeaction.title . "\n" + let l:codeaction_no += 1 + endfor + + let l:codeactionstring .= 'Type number and <Enter> (empty cancels): ' + + let l:codeaction_to_apply = ale#util#Input(l:codeactionstring, '') + let l:codeaction_to_apply = str2nr(l:codeaction_to_apply) + + if l:codeaction_to_apply == 0 + return + endif + + let l:item = l:result[l:codeaction_to_apply - 1] + + call ale#codefix#ApplyLSPCodeAction(l:data, l:item) + endif +endfunction + +function! s:FindError(buffer, line, column, end_line, end_column) abort + let l:nearest_error = v:null + + if a:line == a:end_line + \&& a:column == a:end_column + \&& has_key(g:ale_buffer_info, a:buffer) + let l:nearest_error_diff = -1 + + for l:error in get(g:ale_buffer_info[a:buffer], 'loclist', []) + if has_key(l:error, 'code') && l:error.lnum == a:line + let l:diff = abs(l:error.col - a:column) + + if l:nearest_error_diff == -1 || l:diff < l:nearest_error_diff + let l:nearest_error_diff = l:diff + let l:nearest_error = l:error + endif + endif + endfor + endif + + return l:nearest_error +endfunction + +function! s:OnReady( +\ line, +\ column, +\ end_line, +\ end_column, +\ MenuCallback, +\ linter, +\ lsp_details, +\) abort + let l:id = a:lsp_details.connection_id + + if !ale#lsp#HasCapability(l:id, 'code_actions') + return + endif + + let l:buffer = a:lsp_details.buffer + + if a:linter.lsp is# 'tsserver' + let l:nearest_error = + \ s:FindError(l:buffer, a:line, a:column, a:end_line, a:end_column) + + if l:nearest_error isnot v:null + let l:message = ale#lsp#tsserver_message#GetCodeFixes( + \ l:buffer, + \ a:line, + \ a:column, + \ a:line, + \ a:column, + \ [l:nearest_error.code], + \) + else + let l:message = ale#lsp#tsserver_message#GetApplicableRefactors( + \ l:buffer, + \ a:line, + \ a:column, + \ a:end_line, + \ a:end_column, + \) + endif + else + " Send a message saying the buffer has changed first, otherwise + " completions won't know what text is nearby. + call ale#lsp#NotifyForChanges(l:id, l:buffer) + + let l:diagnostics = [] + let l:nearest_error = + \ s:FindError(l:buffer, a:line, a:column, a:end_line, a:end_column) + + if l:nearest_error isnot v:null + let l:diagnostics = [ + \ { + \ 'code': l:nearest_error.code, + \ 'message': l:nearest_error.text, + \ 'range': { + \ 'start': { + \ 'line': l:nearest_error.lnum - 1, + \ 'character': l:nearest_error.col - 1, + \ }, + \ 'end': { + \ 'line': l:nearest_error.end_lnum - 1, + \ 'character': l:nearest_error.end_col, + \ }, + \ }, + \ }, + \] + endif + + let l:message = ale#lsp#message#CodeAction( + \ l:buffer, + \ a:line, + \ a:column, + \ a:end_line, + \ a:end_column, + \ l:diagnostics, + \) + endif + + let l:Callback = a:linter.lsp is# 'tsserver' + \ ? function('ale#codefix#HandleTSServerResponse') + \ : function('ale#codefix#HandleLSPResponse') + + call ale#lsp#RegisterCallback(l:id, l:Callback) + + let l:request_id = ale#lsp#Send(l:id, l:message) + + let s:codefix_map[l:request_id] = { + \ 'connection_id': l:id, + \ 'buffer': l:buffer, + \ 'line': a:line, + \ 'column': a:column, + \ 'end_line': a:end_line, + \ 'end_column': a:end_column, + \ 'menu_callback': a:MenuCallback, + \} +endfunction + +function! s:ExecuteGetCodeFix(linter, range, MenuCallback) abort + let l:buffer = bufnr('') + + if a:range == 0 + let [l:line, l:column] = getpos('.')[1:2] + let l:end_line = l:line + let l:end_column = l:column + + " Expand the range to cover the current word, if there is one. + let l:cword = expand('<cword>') + + if !empty(l:cword) + let l:search_pos = searchpos('\V' . l:cword, 'bn', l:line) + + if l:search_pos != [0, 0] + let l:column = l:search_pos[1] + let l:end_column = l:column + len(l:cword) - 1 + endif + endif + elseif mode() is# 'v' || mode() is# "\<C-V>" + " You need to get the start and end in a different way when you're in + " visual mode. + let [l:line, l:column] = getpos('v')[1:2] + let [l:end_line, l:end_column] = getpos('.')[1:2] + else + let [l:line, l:column] = getpos("'<")[1:2] + let [l:end_line, l:end_column] = getpos("'>")[1:2] + endif + + let l:column = min([l:column, len(getline(l:line))]) + let l:end_column = min([l:end_column, len(getline(l:end_line))]) + + let l:Callback = function( + \ 's:OnReady', [l:line, l:column, l:end_line, l:end_column, a:MenuCallback] + \) + + call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback) +endfunction + +function! ale#codefix#Execute(range, ...) abort + if a:0 > 1 + throw 'Too many arguments' + endif + + let l:MenuCallback = get(a:000, 0, v:null) + 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) + if l:MenuCallback is v:null + call s:message('No active LSPs') + else + call l:MenuCallback({}, []) + endif + + return + endif + + for l:lsp_linter in l:lsp_linters + call s:ExecuteGetCodeFix(l:lsp_linter, a:range, l:MenuCallback) + endfor +endfunction diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim index ecd93600..48e9bf7c 100644 --- a/autoload/ale/completion.vim +++ b/autoload/ale/completion.vim @@ -617,6 +617,7 @@ function! ale#completion#ParseLSPCompletions(response) abort let l:user_data = {'_ale_completion_item': 1} if has_key(l:item, 'additionalTextEdits') + \ && l:item.additionalTextEdits isnot v:null let l:text_changes = [] for l:edit in l:item.additionalTextEdits @@ -1006,7 +1007,7 @@ function! ale#completion#HandleUserData(completed_item) abort \|| l:source is# 'ale-import' \|| l:source is# 'ale-omnifunc' for l:code_action in get(l:user_data, 'code_actions', []) - call ale#code_action#HandleCodeAction(l:code_action, v:false) + call ale#code_action#HandleCodeAction(l:code_action, {}) endfor endif diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim index 63195d0f..3cafa25c 100644 --- a/autoload/ale/engine.vim +++ b/autoload/ale/engine.vim @@ -444,7 +444,7 @@ function! s:RunJob(command, options) abort return 1 endfunction -function! s:StopCurrentJobs(buffer, clear_lint_file_jobs) abort +function! s:StopCurrentJobs(buffer, clear_lint_file_jobs, linter_slots) abort let l:info = get(g:ale_buffer_info, a:buffer, {}) call ale#command#StopJobs(a:buffer, 'linter') @@ -453,13 +453,23 @@ function! s:StopCurrentJobs(buffer, clear_lint_file_jobs) abort call ale#command#StopJobs(a:buffer, 'file_linter') let l:info.active_linter_list = [] else + let l:lint_file_map = {} + + " Use a previously computed map of `lint_file` values to find + " linters that are used for linting files. + for [l:lint_file, l:linter] in a:linter_slots + if l:lint_file is 1 + let l:lint_file_map[l:linter.name] = 1 + endif + endfor + " Keep jobs for linting files when we're only linting buffers. - call filter(l:info.active_linter_list, 'get(v:val, ''lint_file'')') + call filter(l:info.active_linter_list, 'get(l:lint_file_map, v:val.name)') endif endfunction function! ale#engine#Stop(buffer) abort - call s:StopCurrentJobs(a:buffer, 1) + call s:StopCurrentJobs(a:buffer, 1, []) endfunction function! s:RemoveProblemsForDisabledLinters(buffer, linters) abort @@ -562,6 +572,22 @@ function! s:RunLinter(buffer, linter, lint_file) abort return 0 endfunction +function! s:GetLintFileSlots(buffer, linters) abort + let l:linter_slots = [] + + for l:linter in a:linters + let l:LintFile = l:linter.lint_file + + if type(l:LintFile) is v:t_func + let l:LintFile = l:LintFile(a:buffer) + endif + + call add(l:linter_slots, [l:LintFile, l:linter]) + endfor + + return l:linter_slots +endfunction + function! s:GetLintFileValues(slots, Callback) abort let l:deferred_list = [] let l:new_slots = [] @@ -595,12 +621,18 @@ endfunction function! s:RunLinters( \ buffer, +\ linters, \ slots, \ should_lint_file, \ new_buffer, -\ can_clear_results \) abort - let l:can_clear_results = a:can_clear_results + call s:StopCurrentJobs(a:buffer, a:should_lint_file, a:slots) + call s:RemoveProblemsForDisabledLinters(a:buffer, a:linters) + + " We can only clear the results if we aren't checking the buffer. + let l:can_clear_results = !ale#engine#IsCheckingBuffer(a:buffer) + + silent doautocmd <nomodeline> User ALELintPre for [l:lint_file, l:linter] in a:slots " Only run lint_file linters if we should. @@ -631,36 +663,19 @@ endfunction function! ale#engine#RunLinters(buffer, linters, should_lint_file) abort " Initialise the buffer information if needed. let l:new_buffer = ale#engine#InitBufferInfo(a:buffer) - call s:StopCurrentJobs(a:buffer, a:should_lint_file) - call s:RemoveProblemsForDisabledLinters(a:buffer, a:linters) - - " We can only clear the results if we aren't checking the buffer. - let l:can_clear_results = !ale#engine#IsCheckingBuffer(a:buffer) - - silent doautocmd <nomodeline> User ALELintPre - - " Handle `lint_file` callbacks first. - let l:linter_slots = [] - - for l:linter in a:linters - let l:LintFile = l:linter.lint_file - - if type(l:LintFile) is v:t_func - let l:LintFile = l:LintFile(a:buffer) - endif - call add(l:linter_slots, [l:LintFile, l:linter]) - endfor - - call s:GetLintFileValues(l:linter_slots, { - \ new_slots -> s:RunLinters( - \ a:buffer, - \ new_slots, - \ a:should_lint_file, - \ l:new_buffer, - \ l:can_clear_results, - \ ) - \}) + call s:GetLintFileValues( + \ s:GetLintFileSlots(a:buffer, a:linters), + \ { + \ slots -> s:RunLinters( + \ a:buffer, + \ a:linters, + \ slots, + \ a:should_lint_file, + \ l:new_buffer, + \ ) + \ } + \) endfunction " Clean up a buffer. diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim index d71668f2..9ea5331b 100644 --- a/autoload/ale/fix/registry.vim +++ b/autoload/ale/fix/registry.vim @@ -375,6 +375,11 @@ let s:default_registry = { \ 'suggested_filetypes': ['html', 'htmldjango'], \ 'description': 'Fix HTML files with html-beautify.', \ }, +\ 'luafmt': { +\ 'function': 'ale#fixers#luafmt#Fix', +\ 'suggested_filetypes': ['lua'], +\ 'description': 'Fix Lua files with luafmt.', +\ }, \ 'dhall': { \ 'function': 'ale#fixers#dhall#Fix', \ 'suggested_filetypes': ['dhall'], diff --git a/autoload/ale/fixers/luafmt.vim b/autoload/ale/fixers/luafmt.vim new file mode 100644 index 00000000..6cb9ef4a --- /dev/null +++ b/autoload/ale/fixers/luafmt.vim @@ -0,0 +1,13 @@ +call ale#Set('lua_luafmt_executable', 'luafmt') +call ale#Set('lua_luafmt_options', '') + +function! ale#fixers#luafmt#Fix(buffer) abort + let l:executable = ale#Var(a:buffer, 'lua_luafmt_executable') + let l:options = ale#Var(a:buffer, 'lua_luafmt_options') + + return { + \ 'command': ale#Escape(l:executable) + \ . (empty(l:options) ? '' : ' ' . l:options) + \ . ' --stdin', + \} +endfunction diff --git a/autoload/ale/handlers/eslint.vim b/autoload/ale/handlers/eslint.vim index e37d6902..b8610612 100644 --- a/autoload/ale/handlers/eslint.vim +++ b/autoload/ale/handlers/eslint.vim @@ -5,6 +5,7 @@ let s:executables = [ \ 'node_modules/.bin/eslint_d', \ 'node_modules/eslint/bin/eslint.js', \ 'node_modules/.bin/eslint', +\ '.yarn/sdks/eslint/bin/eslint', \] let s:sep = has('win32') ? '\' : '/' diff --git a/autoload/ale/handlers/sh.vim b/autoload/ale/handlers/sh.vim index 1e50cb89..6ed9fea3 100644 --- a/autoload/ale/handlers/sh.vim +++ b/autoload/ale/handlers/sh.vim @@ -1,18 +1,28 @@ " Author: w0rp <devw0rp@gmail.com> -" Get the shell type for a buffer, based on the hashbang line. function! ale#handlers#sh#GetShellType(buffer) abort - let l:bang_line = get(getbufline(a:buffer, 1), 0, '') + let l:shebang = get(getbufline(a:buffer, 1), 0, '') let l:command = '' - " Take the shell executable from the hashbang, if we can. - if l:bang_line[:1] is# '#!' + " Take the shell executable from the shebang, if we can. + if l:shebang[:1] is# '#!' " Remove options like -e, etc. - let l:command = substitute(l:bang_line, ' --\?[a-zA-Z0-9]\+', '', 'g') + let l:command = substitute(l:shebang, ' --\?[a-zA-Z0-9]\+', '', 'g') endif - " If we couldn't find a hashbang, try the filetype + " With no shebang line, attempt to use Vim's buffer-local variables. + if l:command is# '' + if getbufvar(a:buffer, 'is_bash', 0) + let l:command = 'bash' + elseif getbufvar(a:buffer, 'is_sh', 0) + let l:command = 'sh' + elseif getbufvar(a:buffer, 'is_kornshell', 0) + let l:command = 'ksh' + endif + endif + + " If we couldn't find a shebang, try the filetype if l:command is# '' let l:command = &filetype endif diff --git a/autoload/ale/handlers/shellcheck.vim b/autoload/ale/handlers/shellcheck.vim index b16280f0..351d6d3f 100644 --- a/autoload/ale/handlers/shellcheck.vim +++ b/autoload/ale/handlers/shellcheck.vim @@ -13,15 +13,6 @@ function! ale#handlers#shellcheck#GetDialectArgument(buffer) abort return l:shell_type endif - " If there's no hashbang, try using Vim's buffer variables. - if getbufvar(a:buffer, 'is_bash', 0) - return 'bash' - elseif getbufvar(a:buffer, 'is_sh', 0) - return 'sh' - elseif getbufvar(a:buffer, 'is_kornshell', 0) - return 'ksh' - endif - return '' endfunction diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index 7d99e9d2..cb0573aa 100644 --- a/autoload/ale/lsp.vim +++ b/autoload/ale/lsp.vim @@ -44,6 +44,7 @@ function! ale#lsp#Register(executable_or_address, project, init_options) abort \ 'definition': 0, \ 'typeDefinition': 0, \ 'symbol_search': 0, + \ 'code_actions': 0, \ }, \} endif @@ -219,6 +220,14 @@ function! s:UpdateCapabilities(conn, capabilities) abort let a:conn.capabilities.rename = 1 endif + if get(a:capabilities, 'codeActionProvider') is v:true + let a:conn.capabilities.code_actions = 1 + endif + + if type(get(a:capabilities, 'codeActionProvider')) is v:t_dict + let a:conn.capabilities.code_actions = 1 + endif + if !empty(get(a:capabilities, 'completionProvider')) let a:conn.capabilities.completion = 1 endif @@ -350,6 +359,7 @@ function! ale#lsp#MarkConnectionAsTsserver(conn_id) abort let l:conn.capabilities.definition = 1 let l:conn.capabilities.symbol_search = 1 let l:conn.capabilities.rename = 1 + let l:conn.capabilities.code_actions = 1 endfunction function! s:SendInitMessage(conn) abort diff --git a/autoload/ale/lsp/message.vim b/autoload/ale/lsp/message.vim index 5b0cb8b7..38be4da6 100644 --- a/autoload/ale/lsp/message.vim +++ b/autoload/ale/lsp/message.vim @@ -172,3 +172,25 @@ function! ale#lsp#message#Rename(buffer, line, column, new_name) abort \ 'newName': a:new_name, \}] endfunction + +function! ale#lsp#message#CodeAction(buffer, line, column, end_line, end_column, diagnostics) abort + return [0, 'textDocument/codeAction', { + \ 'textDocument': { + \ 'uri': ale#path#ToURI(expand('#' . a:buffer . ':p')), + \ }, + \ 'range': { + \ 'start': {'line': a:line - 1, 'character': a:column - 1}, + \ 'end': {'line': a:end_line - 1, 'character': a:end_column}, + \ }, + \ 'context': { + \ 'diagnostics': a:diagnostics + \ }, + \}] +endfunction + +function! ale#lsp#message#ExecuteCommand(command, arguments) abort + return [0, 'workspace/executeCommand', { + \ 'command': a:command, + \ 'arguments': a:arguments, + \}] +endfunction diff --git a/autoload/ale/lsp/response.vim b/autoload/ale/lsp/response.vim index 30da77e1..a4f80980 100644 --- a/autoload/ale/lsp/response.vim +++ b/autoload/ale/lsp/response.vim @@ -56,6 +56,7 @@ function! ale#lsp#response#ReadDiagnostics(response) abort endif if has_key(l:diagnostic, 'relatedInformation') + \ && l:diagnostic.relatedInformation isnot v:null let l:related = deepcopy(l:diagnostic.relatedInformation) call map(l:related, {key, val -> \ ale#path#FromURI(val.location.uri) . diff --git a/autoload/ale/lsp/tsserver_message.vim b/autoload/ale/lsp/tsserver_message.vim index b9fafaa0..3c1b47ed 100644 --- a/autoload/ale/lsp/tsserver_message.vim +++ b/autoload/ale/lsp/tsserver_message.vim @@ -103,3 +103,39 @@ function! ale#lsp#tsserver_message#OrganizeImports(buffer) abort \ }, \}] endfunction + +function! ale#lsp#tsserver_message#GetCodeFixes(buffer, line, column, end_line, end_column, error_codes) abort + " The lines and columns are 1-based. + " The errors codes must be a list of tsserver error codes to fix. + return [0, 'ts@getCodeFixes', { + \ 'startLine': a:line, + \ 'startOffset': a:column, + \ 'endLine': a:end_line, + \ 'endOffset': a:end_column + 1, + \ 'file': expand('#' . a:buffer . ':p'), + \ 'errorCodes': a:error_codes, + \}] +endfunction + +function! ale#lsp#tsserver_message#GetApplicableRefactors(buffer, line, column, end_line, end_column) abort + " The arguments for this request can also be just 'line' and 'offset' + return [0, 'ts@getApplicableRefactors', { + \ 'startLine': a:line, + \ 'startOffset': a:column, + \ 'endLine': a:end_line, + \ 'endOffset': a:end_column + 1, + \ 'file': expand('#' . a:buffer . ':p'), + \}] +endfunction + +function! ale#lsp#tsserver_message#GetEditsForRefactor(buffer, line, column, end_line, end_column, refactor, action) abort + return [0, 'ts@getEditsForRefactor', { + \ 'startLine': a:line, + \ 'startOffset': a:column, + \ 'endLine': a:end_line, + \ 'endOffset': a:end_column + 1, + \ 'file': expand('#' . a:buffer . ':p'), + \ 'refactor': a:refactor, + \ 'action': a:action, + \}] +endfunction diff --git a/autoload/ale/maven.vim b/autoload/ale/maven.vim new file mode 100644 index 00000000..745f8c93 --- /dev/null +++ b/autoload/ale/maven.vim @@ -0,0 +1,51 @@ +" Description: Functions for working with Maven projects. +" +" Given a buffer number, find a Maven project root. +function! ale#maven#FindProjectRoot(buffer) abort + let l:wrapper_path = ale#path#FindNearestFile(a:buffer, 'mvnw') + + if !empty(l:wrapper_path) + return fnamemodify(l:wrapper_path, ':h') + endif + + let l:pom_path = ale#path#FindNearestFile(a:buffer, 'pom.xml') + + if !empty(l:pom_path) + return fnamemodify(l:pom_path, ':h') + endif + + return '' +endfunction + + +" Given a buffer number, find the path to the executable. +" First search on the path for 'mvnw' (mvnw.cmd on Windows), if nothing is found, +" try the global command. Returns an empty string if cannot find the executable. +function! ale#maven#FindExecutable(buffer) abort + let l:wrapper_cmd = has('unix') ? 'mvnw' : 'mvnw.cmd' + let l:wrapper_path = ale#path#FindNearestFile(a:buffer, l:wrapper_cmd) + + if executable(l:wrapper_path) + return l:wrapper_path + endif + + if executable('mvn') + return 'mvn' + endif + + return '' +endfunction + +" Given a buffer number, build a command to print the classpath of the root +" project. Returns an empty string if cannot build the command. +function! ale#maven#BuildClasspathCommand(buffer) abort + let l:executable = ale#maven#FindExecutable(a:buffer) + let l:project_root = ale#maven#FindProjectRoot(a:buffer) + + if !empty(l:executable) && !empty(l:project_root) + return ale#path#CdString(l:project_root) + \ . l:executable . ' dependency:build-classpath' + endif + + return '' +endfunction diff --git a/autoload/ale/organize_imports.vim b/autoload/ale/organize_imports.vim index e89c832c..e2b1c0d2 100644 --- a/autoload/ale/organize_imports.vim +++ b/autoload/ale/organize_imports.vim @@ -12,10 +12,13 @@ function! ale#organize_imports#HandleTSServerResponse(conn_id, response) abort let l:file_code_edits = a:response.body - call ale#code_action#HandleCodeAction({ - \ 'description': 'Organize Imports', - \ 'changes': l:file_code_edits, - \}, v:false) + call ale#code_action#HandleCodeAction( + \ { + \ 'description': 'Organize Imports', + \ 'changes': l:file_code_edits, + \ }, + \ {} + \) endfunction function! s:OnReady(linter, lsp_details) abort diff --git a/autoload/ale/rename.vim b/autoload/ale/rename.vim index 64952e63..0d074c24 100644 --- a/autoload/ale/rename.vim +++ b/autoload/ale/rename.vim @@ -33,9 +33,10 @@ function! ale#rename#HandleTSServerResponse(conn_id, response) abort 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) + let l:options = remove(s:rename_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') @@ -77,41 +78,22 @@ function! ale#rename#HandleTSServerResponse(conn_id, response) abort return endif - call ale#code_action#HandleCodeAction({ - \ 'description': 'rename', - \ 'changes': l:changes, - \}, v:true) -endfunction - -function! s:getChanges(workspace_edit) abort - let l:changes = {} - - if has_key(a:workspace_edit, 'changes') && !empty(a:workspace_edit.changes) - return a:workspace_edit.changes - elseif has_key(a:workspace_edit, 'documentChanges') - let l:document_changes = [] - - if type(a:workspace_edit.documentChanges) is v:t_dict - \ && has_key(a:workspace_edit.documentChanges, 'edits') - call add(l:document_changes, a:workspace_edit.documentChanges) - elseif type(a:workspace_edit.documentChanges) is v:t_list - let l:document_changes = a:workspace_edit.documentChanges - endif - - for l:text_document_edit in l:document_changes - let l:filename = l:text_document_edit.textDocument.uri - let l:edits = l:text_document_edit.edits - let l:changes[l:filename] = l:edits - endfor - endif - - return l:changes + call ale#code_action#HandleCodeAction( + \ { + \ 'description': 'rename', + \ 'changes': l:changes, + \ }, + \ { + \ 'should_save': 1, + \ 'force_save': get(l:options, 'force_save'), + \ }, + \) 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) + let l:options = remove(s:rename_map, a:response.id) if !has_key(a:response, 'result') call s:message('No rename result received from server') @@ -119,7 +101,7 @@ function! ale#rename#HandleLSPResponse(conn_id, response) abort return endif - let l:changes_map = s:getChanges(a:response.result) + let l:changes_map = ale#code_action#GetChanges(a:response.result) if empty(l:changes_map) call s:message('No changes received from server') @@ -127,43 +109,22 @@ function! ale#rename#HandleLSPResponse(conn_id, response) abort return endif - let l:changes = [] - - for l:file_name in keys(l:changes_map) - let l:text_edits = l:changes_map[l:file_name] - let l:text_changes = [] - - for l:edit in l:text_edits - let l:range = l:edit.range - let l:new_text = l:edit.newText - - call add(l:text_changes, { - \ 'start': { - \ 'line': l:range.start.line + 1, - \ 'offset': l:range.start.character + 1, - \ }, - \ 'end': { - \ 'line': l:range.end.line + 1, - \ 'offset': l:range.end.character + 1, - \ }, - \ 'newText': l:new_text, - \}) - endfor - - call add(l:changes, { - \ 'fileName': ale#path#FromURI(l:file_name), - \ 'textChanges': l:text_changes, - \}) - endfor - - call ale#code_action#HandleCodeAction({ - \ 'description': 'rename', - \ 'changes': l:changes, - \}, v:true) + let l:changes = ale#code_action#BuildChangesList(l:changes_map) + + call ale#code_action#HandleCodeAction( + \ { + \ 'description': 'rename', + \ 'changes': l:changes, + \ }, + \ { + \ 'should_save': 1, + \ 'force_save': get(l:options, 'force_save'), + \ }, + \) endif endfunction -function! s:OnReady(line, column, old_name, new_name, linter, lsp_details) abort +function! s:OnReady(line, column, options, linter, lsp_details) abort let l:id = a:lsp_details.connection_id if !ale#lsp#HasCapability(l:id, 'rename') @@ -195,19 +156,16 @@ function! s:OnReady(line, column, old_name, new_name, linter, lsp_details) abort \ l:buffer, \ a:line, \ a:column, - \ a:new_name + \ a:options.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, - \} + let s:rename_map[l:request_id] = a:options endfunction -function! s:ExecuteRename(linter, old_name, new_name) abort +function! s:ExecuteRename(linter, options) abort let l:buffer = bufnr('') let [l:line, l:column] = getpos('.')[1:2] @@ -215,12 +173,11 @@ function! s:ExecuteRename(linter, old_name, new_name) abort 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]) + let l:Callback = function('s:OnReady', [l:line, l:column, a:options]) call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback) endfunction -function! ale#rename#Execute() abort +function! ale#rename#Execute(options) abort let l:lsp_linters = [] for l:linter in ale#linter#Get(&filetype) @@ -245,6 +202,10 @@ function! ale#rename#Execute() abort endif for l:lsp_linter in l:lsp_linters - call s:ExecuteRename(l:lsp_linter, l:old_name, l:new_name) + call s:ExecuteRename(l:lsp_linter, { + \ 'old_name': l:old_name, + \ 'new_name': l:new_name, + \ 'force_save': get(a:options, 'force_save') is 1, + \}) endfor endfunction |