]> git.madduck.net Git - etc/vim.git/blob - autoload/ale/codefix.vim

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

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.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Squashed '.vim/bundle/ale/' content from commit 22185c4c
[etc/vim.git] / autoload / ale / codefix.vim
1 " Author: Dalius Dobravolskas <dalius.dobravolskas@gmail.com>
2 " Description: Code Fix support for tsserver and LSP servers
3
4 let s:codefix_map = {}
5
6 " Used to get the codefix map in tests.
7 function! ale#codefix#GetMap() abort
8     return deepcopy(s:codefix_map)
9 endfunction
10
11 " Used to set the codefix map in tests.
12 function! ale#codefix#SetMap(map) abort
13     let s:codefix_map = a:map
14 endfunction
15
16 function! ale#codefix#ClearLSPData() abort
17     let s:codefix_map = {}
18 endfunction
19
20 function! s:message(message) abort
21     call ale#util#Execute('echom ' . string(a:message))
22 endfunction
23
24 function! ale#codefix#ApplyTSServerCodeAction(data, item) abort
25     if has_key(a:item, 'changes')
26         let l:changes = a:item.changes
27
28         call ale#code_action#HandleCodeAction(
29         \   {
30         \       'description': 'codefix',
31         \       'changes': l:changes,
32         \   },
33         \   {},
34         \)
35     else
36         let l:message = ale#lsp#tsserver_message#GetEditsForRefactor(
37         \   a:data.buffer,
38         \   a:data.line,
39         \   a:data.column,
40         \   a:data.end_line,
41         \   a:data.end_column,
42         \   a:item.id[0],
43         \   a:item.id[1],
44         \)
45
46         let l:request_id = ale#lsp#Send(a:data.connection_id, l:message)
47
48         let s:codefix_map[l:request_id] = a:data
49     endif
50 endfunction
51
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)
55         return
56     endif
57
58     let l:data = remove(s:codefix_map, a:response.request_seq)
59     let l:MenuCallback = get(l:data, 'menu_callback', v:null)
60
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)
66
67             return
68         endif
69
70         let l:result = get(a:response, 'body', [])
71         call filter(l:result, 'has_key(v:val, ''changes'')')
72
73         if l:MenuCallback isnot v:null
74             call l:MenuCallback(
75             \   l:data,
76             \   map(copy(l:result), '[''tsserver'', v:val]')
77             \)
78
79             return
80         endif
81
82         if len(l:result) == 0
83             call s:message('No code fixes available.')
84
85             return
86         endif
87
88         let l:code_fix_to_apply = 0
89
90         if len(l:result) == 1
91             let l:code_fix_to_apply = 1
92         else
93             let l:codefix_no = 1
94             let l:codefixstring = "Code Fixes:\n"
95
96             for l:codefix in l:result
97                 let l:codefixstring .= l:codefix_no . ') '
98                 \   . l:codefix.description . "\n"
99                 let l:codefix_no += 1
100             endfor
101
102             let l:codefixstring .= 'Type number and <Enter> (empty cancels): '
103
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)
106
107             if l:code_fix_to_apply == 0
108                 return
109             endif
110         endif
111
112         call ale#codefix#ApplyTSServerCodeAction(
113         \   l:data,
114         \   l:result[l:code_fix_to_apply - 1],
115         \)
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)
121
122             return
123         endif
124
125         let l:result = get(a:response, 'body', [])
126
127         if len(l:result) == 0
128             call s:message('No applicable refactors available.')
129
130             return
131         endif
132
133         let l:refactors = []
134
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],
140                 \})
141             endfor
142         endfor
143
144         if l:MenuCallback isnot v:null
145             call l:MenuCallback(
146             \   l:data,
147             \   map(copy(l:refactors), '[''tsserver'', v:val]')
148             \)
149
150             return
151         endif
152
153         let l:refactor_no = 1
154         let l:refactorstring = "Applicable refactors:\n"
155
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
160         endfor
161
162         let l:refactorstring .= 'Type number and <Enter> (empty cancels): '
163
164         let l:refactor_to_apply = ale#util#Input(l:refactorstring, '')
165         let l:refactor_to_apply = str2nr(l:refactor_to_apply)
166
167         if l:refactor_to_apply == 0
168             return
169         endif
170
171         let l:id = l:refactors[l:refactor_to_apply - 1].id
172
173         call ale#codefix#ApplyTSServerCodeAction(
174         \   l:data,
175         \   l:refactors[l:refactor_to_apply - 1],
176         \)
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)
181
182             return
183         endif
184
185         call ale#code_action#HandleCodeAction(
186         \   {
187         \       'description': 'editsForRefactor',
188         \       'changes': a:response.body.edits,
189         \   },
190         \   {},
191         \)
192     endif
193 endfunction
194
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(
200         \   l:command.command,
201         \   l:command.arguments,
202         \)
203
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(
208         \   a:item.command,
209         \   a:item.arguments,
210         \)
211
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
216         else
217             let l:topass = a:item.arguments[0]
218         endif
219
220         let l:changes_map = ale#code_action#GetChanges(l:topass)
221
222         if empty(l:changes_map)
223             return
224         endif
225
226         let l:changes = ale#code_action#BuildChangesList(l:changes_map)
227
228         call ale#code_action#HandleCodeAction(
229         \   {
230         \       'description': 'codeaction',
231         \       'changes': l:changes,
232         \   },
233         \   {},
234         \)
235     endif
236 endfunction
237
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
243
244         let l:changes_map = ale#code_action#GetChanges(l:params.edit)
245
246         if empty(l:changes_map)
247             return
248         endif
249
250         let l:changes = ale#code_action#BuildChangesList(l:changes_map)
251
252         call ale#code_action#HandleCodeAction(
253         \   {
254         \       'description': 'applyEdit',
255         \       'changes': l:changes,
256         \   },
257         \   {}
258         \)
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)
263
264         let l:result = get(a:response, 'result')
265
266         if type(l:result) != v:t_list
267             let l:result = []
268         endif
269
270         " Send the results to the menu callback, if set.
271         if l:MenuCallback isnot v:null
272             call l:MenuCallback(
273             \   l:data,
274             \   map(copy(l:result), '[''lsp'', v:val]')
275             \)
276
277             return
278         endif
279
280         if len(l:result) == 0
281             call s:message('No code actions received from server')
282
283             return
284         endif
285
286         let l:codeaction_no = 1
287         let l:codeactionstring = "Code Fixes:\n"
288
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
293         endfor
294
295         let l:codeactionstring .= 'Type number and <Enter> (empty cancels): '
296
297         let l:codeaction_to_apply = ale#util#Input(l:codeactionstring, '')
298         let l:codeaction_to_apply = str2nr(l:codeaction_to_apply)
299
300         if l:codeaction_to_apply == 0
301             return
302         endif
303
304         let l:item = l:result[l:codeaction_to_apply - 1]
305
306         call ale#codefix#ApplyLSPCodeAction(l:data, l:item)
307     endif
308 endfunction
309
310 function! s:FindError(buffer, line, column, end_line, end_column, linter_name) abort
311     let l:nearest_error = v:null
312
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
317
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)
323
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
327                 endif
328             endif
329         endfor
330     endif
331
332     return l:nearest_error
333 endfunction
334
335 function! s:OnReady(
336 \   line,
337 \   column,
338 \   end_line,
339 \   end_column,
340 \   MenuCallback,
341 \   linter,
342 \   lsp_details,
343 \) abort
344     let l:id = a:lsp_details.connection_id
345
346     if !ale#lsp#HasCapability(l:id, 'code_actions')
347         return
348     endif
349
350     let l:buffer = a:lsp_details.buffer
351
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)
355
356         if l:nearest_error isnot v:null
357             let l:message = ale#lsp#tsserver_message#GetCodeFixes(
358             \   l:buffer,
359             \   a:line,
360             \   a:column,
361             \   a:line,
362             \   a:column,
363             \   [l:nearest_error.code],
364             \)
365         else
366             let l:message = ale#lsp#tsserver_message#GetApplicableRefactors(
367             \   l:buffer,
368             \   a:line,
369             \   a:column,
370             \   a:end_line,
371             \   a:end_column,
372             \)
373         endif
374     else
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)
378
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)
382
383         if l:nearest_error isnot v:null
384             let l:diagnostics = [
385             \   {
386             \       'code': l:nearest_error.code,
387             \       'message': l:nearest_error.text,
388             \       'range': {
389             \           'start': {
390             \               'line': l:nearest_error.lnum - 1,
391             \               'character': l:nearest_error.col - 1,
392             \           },
393             \           'end': {
394             \               'line': get(l:nearest_error, 'end_lnum', 1) - 1,
395             \               'character': get(l:nearest_error, 'end_col', 0)
396             \           },
397             \       },
398             \   },
399             \]
400         endif
401
402         let l:message = ale#lsp#message#CodeAction(
403         \   l:buffer,
404         \   a:line,
405         \   a:column,
406         \   a:end_line,
407         \   a:end_column,
408         \   l:diagnostics,
409         \)
410     endif
411
412     let l:Callback = a:linter.lsp is# 'tsserver'
413     \   ? function('ale#codefix#HandleTSServerResponse')
414     \   : function('ale#codefix#HandleLSPResponse')
415
416     call ale#lsp#RegisterCallback(l:id, l:Callback)
417
418     let l:request_id = ale#lsp#Send(l:id, l:message)
419
420     let s:codefix_map[l:request_id] = {
421     \   'connection_id': l:id,
422     \   'buffer': l:buffer,
423     \   'line': a:line,
424     \   'column': a:column,
425     \   'end_line': a:end_line,
426     \   'end_column': a:end_column,
427     \   'menu_callback': a:MenuCallback,
428     \}
429 endfunction
430
431 function! s:ExecuteGetCodeFix(linter, range, MenuCallback) abort
432     let l:buffer = bufnr('')
433
434     if a:range == 0
435         let [l:line, l:column] = getpos('.')[1:2]
436         let l:end_line = l:line
437         let l:end_column = l:column
438
439         " Expand the range to cover the current word, if there is one.
440         let l:cword = expand('<cword>')
441
442         if !empty(l:cword)
443             let l:search_pos = searchpos('\V' . l:cword, 'bn', l:line)
444
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
448             endif
449         endif
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
452         " visual mode.
453         let [l:line, l:column] = getpos('v')[1:2]
454         let [l:end_line, l:end_column] = getpos('.')[1:2]
455     else
456         let [l:line, l:column] = getpos("'<")[1:2]
457         let [l:end_line, l:end_column] = getpos("'>")[1:2]
458     endif
459
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))])
462
463     let l:Callback = function(
464     \ 's:OnReady', [l:line, l:column, l:end_line, l:end_column, a:MenuCallback]
465     \)
466
467     call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback)
468 endfunction
469
470 function! ale#codefix#Execute(range, ...) abort
471     if a:0 > 1
472         throw 'Too many arguments'
473     endif
474
475     let l:MenuCallback = get(a:000, 0, v:null)
476     let l:linters = ale#lsp_linter#GetEnabled(bufnr(''))
477
478     if empty(l:linters)
479         if l:MenuCallback is v:null
480             call s:message('No active LSPs')
481         else
482             call l:MenuCallback({}, [])
483         endif
484
485         return
486     endif
487
488     for l:linter in l:linters
489         call s:ExecuteGetCodeFix(l:linter, a:range, l:MenuCallback)
490     endfor
491 endfunction