All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
1 " Author: Dalius Dobravolskas <dalius.dobravolskas@gmail.com>
2 " Description: Code Fix support for tsserver and LSP servers
6 " Used to get the codefix map in tests.
7 function! ale#codefix#GetMap() abort
8 return deepcopy(s:codefix_map)
11 " Used to set the codefix map in tests.
12 function! ale#codefix#SetMap(map) abort
13 let s:codefix_map = a:map
16 function! ale#codefix#ClearLSPData() abort
17 let s:codefix_map = {}
20 function! s:message(message) abort
21 call ale#util#Execute('echom ' . string(a:message))
24 function! ale#codefix#ApplyTSServerCodeAction(data, item) abort
25 if has_key(a:item, 'changes')
26 let l:changes = a:item.changes
28 call ale#code_action#HandleCodeAction(
30 \ 'description': 'codefix',
31 \ 'changes': l:changes,
36 let l:message = ale#lsp#tsserver_message#GetEditsForRefactor(
46 let l:request_id = ale#lsp#Send(a:data.connection_id, l:message)
48 let s:codefix_map[l:request_id] = a:data
52 function! ale#codefix#HandleTSServerResponse(conn_id, response) abort
53 if !has_key(a:response, 'request_seq')
54 \ || !has_key(s:codefix_map, a:response.request_seq)
58 let l:data = remove(s:codefix_map, a:response.request_seq)
59 let l:MenuCallback = get(l:data, 'menu_callback', v:null)
61 if get(a:response, 'command', '') is# 'getCodeFixes'
62 if get(a:response, 'success', v:false) is v:false
63 \&& l:MenuCallback is v:null
64 let l:message = get(a:response, 'message', 'unknown')
65 call s:message('Error while getting code fixes. Reason: ' . l:message)
70 let l:result = get(a:response, 'body', [])
71 call filter(l:result, 'has_key(v:val, ''changes'')')
73 if l:MenuCallback isnot v:null
76 \ map(copy(l:result), '[''tsserver'', v:val]')
83 call s:message('No code fixes available.')
88 let l:code_fix_to_apply = 0
91 let l:code_fix_to_apply = 1
94 let l:codefixstring = "Code Fixes:\n"
96 for l:codefix in l:result
97 let l:codefixstring .= l:codefix_no . ') '
98 \ . l:codefix.description . "\n"
102 let l:codefixstring .= 'Type number and <Enter> (empty cancels): '
104 let l:code_fix_to_apply = ale#util#Input(l:codefixstring, '')
105 let l:code_fix_to_apply = str2nr(l:code_fix_to_apply)
107 if l:code_fix_to_apply == 0
112 call ale#codefix#ApplyTSServerCodeAction(
114 \ l:result[l:code_fix_to_apply - 1],
116 elseif get(a:response, 'command', '') is# 'getApplicableRefactors'
117 if get(a:response, 'success', v:false) is v:false
118 \&& l:MenuCallback is v:null
119 let l:message = get(a:response, 'message', 'unknown')
120 call s:message('Error while getting applicable refactors. Reason: ' . l:message)
125 let l:result = get(a:response, 'body', [])
127 if len(l:result) == 0
128 call s:message('No applicable refactors available.')
135 for l:item in l:result
136 for l:action in l:item.actions
137 call add(l:refactors, {
138 \ 'name': l:action.description,
139 \ 'id': [l:item.name, l:action.name],
144 if l:MenuCallback isnot v:null
147 \ map(copy(l:refactors), '[''tsserver'', v:val]')
153 let l:refactor_no = 1
154 let l:refactorstring = "Applicable refactors:\n"
156 for l:refactor in l:refactors
157 let l:refactorstring .= l:refactor_no . ') '
158 \ . l:refactor.name . "\n"
159 let l:refactor_no += 1
162 let l:refactorstring .= 'Type number and <Enter> (empty cancels): '
164 let l:refactor_to_apply = ale#util#Input(l:refactorstring, '')
165 let l:refactor_to_apply = str2nr(l:refactor_to_apply)
167 if l:refactor_to_apply == 0
171 let l:id = l:refactors[l:refactor_to_apply - 1].id
173 call ale#codefix#ApplyTSServerCodeAction(
175 \ l:refactors[l:refactor_to_apply - 1],
177 elseif get(a:response, 'command', '') is# 'getEditsForRefactor'
178 if get(a:response, 'success', v:false) is v:false
179 let l:message = get(a:response, 'message', 'unknown')
180 call s:message('Error while getting edits for refactor. Reason: ' . l:message)
185 call ale#code_action#HandleCodeAction(
187 \ 'description': 'editsForRefactor',
188 \ 'changes': a:response.body.edits,
195 function! ale#codefix#ApplyLSPCodeAction(data, item) abort
196 if has_key(a:item, 'command')
197 \&& type(a:item.command) == v:t_dict
198 let l:command = a:item.command
199 let l:message = ale#lsp#message#ExecuteCommand(
201 \ l:command.arguments,
204 let l:request_id = ale#lsp#Send(a:data.connection_id, l:message)
205 elseif has_key(a:item, 'command') && has_key(a:item, 'arguments')
206 \&& type(a:item.command) == v:t_string
207 let l:message = ale#lsp#message#ExecuteCommand(
212 let l:request_id = ale#lsp#Send(a:data.connection_id, l:message)
213 elseif has_key(a:item, 'edit') || has_key(a:item, 'arguments')
214 if has_key(a:item, 'edit')
215 let l:topass = a:item.edit
217 let l:topass = a:item.arguments[0]
220 let l:changes_map = ale#code_action#GetChanges(l:topass)
222 if empty(l:changes_map)
226 let l:changes = ale#code_action#BuildChangesList(l:changes_map)
228 call ale#code_action#HandleCodeAction(
230 \ 'description': 'codeaction',
231 \ 'changes': l:changes,
238 function! ale#codefix#HandleLSPResponse(conn_id, response) abort
239 if has_key(a:response, 'method')
240 \ && a:response.method is# 'workspace/applyEdit'
241 \ && has_key(a:response, 'params')
242 let l:params = a:response.params
244 let l:changes_map = ale#code_action#GetChanges(l:params.edit)
246 if empty(l:changes_map)
250 let l:changes = ale#code_action#BuildChangesList(l:changes_map)
252 call ale#code_action#HandleCodeAction(
254 \ 'description': 'applyEdit',
255 \ 'changes': l:changes,
259 elseif has_key(a:response, 'id')
260 \&& has_key(s:codefix_map, a:response.id)
261 let l:data = remove(s:codefix_map, a:response.id)
262 let l:MenuCallback = get(l:data, 'menu_callback', v:null)
264 let l:result = get(a:response, 'result')
266 if type(l:result) != v:t_list
270 " Send the results to the menu callback, if set.
271 if l:MenuCallback isnot v:null
274 \ map(copy(l:result), '[''lsp'', v:val]')
280 if len(l:result) == 0
281 call s:message('No code actions received from server')
286 let l:codeaction_no = 1
287 let l:codeactionstring = "Code Fixes:\n"
289 for l:codeaction in l:result
290 let l:codeactionstring .= l:codeaction_no . ') '
291 \ . l:codeaction.title . "\n"
292 let l:codeaction_no += 1
295 let l:codeactionstring .= 'Type number and <Enter> (empty cancels): '
297 let l:codeaction_to_apply = ale#util#Input(l:codeactionstring, '')
298 let l:codeaction_to_apply = str2nr(l:codeaction_to_apply)
300 if l:codeaction_to_apply == 0
304 let l:item = l:result[l:codeaction_to_apply - 1]
306 call ale#codefix#ApplyLSPCodeAction(l:data, l:item)
310 function! s:FindError(buffer, line, column, end_line, end_column, linter_name) abort
311 let l:nearest_error = v:null
313 if a:line == a:end_line
314 \&& a:column == a:end_column
315 \&& has_key(g:ale_buffer_info, a:buffer)
316 let l:nearest_error_diff = -1
318 for l:error in get(g:ale_buffer_info[a:buffer], 'loclist', [])
319 if has_key(l:error, 'code')
320 \ && (a:linter_name is v:null || l:error.linter_name is# a:linter_name)
321 \ && l:error.lnum == a:line
322 let l:diff = abs(l:error.col - a:column)
324 if l:nearest_error_diff == -1 || l:diff < l:nearest_error_diff
325 let l:nearest_error_diff = l:diff
326 let l:nearest_error = l:error
332 return l:nearest_error
344 let l:id = a:lsp_details.connection_id
346 if !ale#lsp#HasCapability(l:id, 'code_actions')
350 let l:buffer = a:lsp_details.buffer
352 if a:linter.lsp is# 'tsserver'
353 let l:nearest_error =
354 \ s:FindError(l:buffer, a:line, a:column, a:end_line, a:end_column, a:linter.lsp)
356 if l:nearest_error isnot v:null
357 let l:message = ale#lsp#tsserver_message#GetCodeFixes(
363 \ [l:nearest_error.code],
366 let l:message = ale#lsp#tsserver_message#GetApplicableRefactors(
375 " Send a message saying the buffer has changed first, otherwise
376 " completions won't know what text is nearby.
377 call ale#lsp#NotifyForChanges(l:id, l:buffer)
379 let l:diagnostics = []
380 let l:nearest_error =
381 \ s:FindError(l:buffer, a:line, a:column, a:end_line, a:end_column, v:null)
383 if l:nearest_error isnot v:null
384 let l:diagnostics = [
386 \ 'code': l:nearest_error.code,
387 \ 'message': l:nearest_error.text,
390 \ 'line': l:nearest_error.lnum - 1,
391 \ 'character': l:nearest_error.col - 1,
394 \ 'line': get(l:nearest_error, 'end_lnum', 1) - 1,
395 \ 'character': get(l:nearest_error, 'end_col', 0)
402 let l:message = ale#lsp#message#CodeAction(
412 let l:Callback = a:linter.lsp is# 'tsserver'
413 \ ? function('ale#codefix#HandleTSServerResponse')
414 \ : function('ale#codefix#HandleLSPResponse')
416 call ale#lsp#RegisterCallback(l:id, l:Callback)
418 let l:request_id = ale#lsp#Send(l:id, l:message)
420 let s:codefix_map[l:request_id] = {
421 \ 'connection_id': l:id,
422 \ 'buffer': l:buffer,
424 \ 'column': a:column,
425 \ 'end_line': a:end_line,
426 \ 'end_column': a:end_column,
427 \ 'menu_callback': a:MenuCallback,
431 function! s:ExecuteGetCodeFix(linter, range, MenuCallback) abort
432 let l:buffer = bufnr('')
435 let [l:line, l:column] = getpos('.')[1:2]
436 let l:end_line = l:line
437 let l:end_column = l:column
439 " Expand the range to cover the current word, if there is one.
440 let l:cword = expand('<cword>')
443 let l:search_pos = searchpos('\V' . l:cword, 'bn', l:line)
445 if l:search_pos != [0, 0]
446 let l:column = l:search_pos[1]
447 let l:end_column = l:column + len(l:cword) - 1
450 elseif mode() is# 'v' || mode() is# "\<C-V>"
451 " You need to get the start and end in a different way when you're in
453 let [l:line, l:column] = getpos('v')[1:2]
454 let [l:end_line, l:end_column] = getpos('.')[1:2]
456 let [l:line, l:column] = getpos("'<")[1:2]
457 let [l:end_line, l:end_column] = getpos("'>")[1:2]
460 let l:column = max([min([l:column, len(getline(l:line))]), 1])
461 let l:end_column = min([l:end_column, len(getline(l:end_line))])
463 let l:Callback = function(
464 \ 's:OnReady', [l:line, l:column, l:end_line, l:end_column, a:MenuCallback]
467 call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback)
470 function! ale#codefix#Execute(range, ...) abort
472 throw 'Too many arguments'
475 let l:MenuCallback = get(a:000, 0, v:null)
476 let l:linters = ale#lsp_linter#GetEnabled(bufnr(''))
479 if l:MenuCallback is v:null
480 call s:message('No active LSPs')
482 call l:MenuCallback({}, [])
488 for l:linter in l:linters
489 call s:ExecuteGetCodeFix(l:linter, a:range, l:MenuCallback)