]> git.madduck.net Git - etc/vim.git/blob - autoload/ale/code_action.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 / code_action.vim
1 " Author: Jerko Steiner <jerko.steiner@gmail.com>
2 " Description: Code action support for LSP / tsserver
3
4 function! ale#code_action#ReloadBuffer() abort
5     let l:buffer = bufnr('')
6
7     execute 'augroup ALECodeActionReloadGroup' . l:buffer
8         autocmd!
9     augroup END
10
11     silent! execute 'augroup! ALECodeActionReloadGroup' . l:buffer
12
13     call ale#util#Execute(':e!')
14 endfunction
15
16 function! ale#code_action#HandleCodeAction(code_action, options) abort
17     let l:current_buffer = bufnr('')
18     let l:changes = a:code_action.changes
19
20     for l:file_code_edit in l:changes
21         call ale#code_action#ApplyChanges(
22         \   l:file_code_edit.fileName,
23         \   l:file_code_edit.textChanges,
24         \   a:options,
25         \)
26     endfor
27 endfunction
28
29 function! s:ChangeCmp(left, right) abort
30     if a:left.start.line < a:right.start.line
31         return -1
32     endif
33
34     if a:left.start.line > a:right.start.line
35         return 1
36     endif
37
38     if a:left.start.offset < a:right.start.offset
39         return -1
40     endif
41
42     if a:left.start.offset > a:right.start.offset
43         return 1
44     endif
45
46     if a:left.end.line < a:right.end.line
47         return -1
48     endif
49
50     if a:left.end.line > a:right.end.line
51         return 1
52     endif
53
54     if a:left.end.offset < a:right.end.offset
55         return -1
56     endif
57
58     if a:left.end.offset > a:right.end.offset
59         return 1
60     endif
61
62     return 0
63 endfunction
64
65 function! ale#code_action#ApplyChanges(filename, changes, options) abort
66     let l:should_save = get(a:options, 'should_save')
67     let l:conn_id = get(a:options, 'conn_id')
68
69     let l:orig_buffer = bufnr('')
70
71     " The buffer is used to determine the fileformat, if available.
72     let l:buffer = bufnr(a:filename)
73
74     if l:buffer != l:orig_buffer
75         call ale#util#Execute('silent edit ' . a:filename)
76         let l:buffer = bufnr('')
77     endif
78
79     let l:lines = getbufline(l:buffer, 1, '$')
80
81     " Add empty line if there's trailing newline, like readfile() does.
82     if getbufvar(l:buffer, '&eol')
83         let l:lines += ['']
84     endif
85
86     let l:pos = getpos('.')[1:2]
87
88     " Changes have to be sorted so we apply them from bottom-to-top
89     for l:code_edit in reverse(sort(copy(a:changes), function('s:ChangeCmp')))
90         let l:line = l:code_edit.start.line
91         let l:column = l:code_edit.start.offset
92         let l:end_line = l:code_edit.end.line
93         let l:end_column = l:code_edit.end.offset
94         let l:text = l:code_edit.newText
95
96         let l:insertions = split(l:text, '\n', 1)
97
98         " Fix invalid columns
99         let l:column = l:column > 0 ? l:column : 1
100         let l:end_column = l:end_column > 0 ? l:end_column : 1
101
102         " Clamp start to BOF
103         if l:line < 1
104             let [l:line, l:column] = [1, 1]
105         endif
106
107         " Clamp start to EOF
108         if l:line > len(l:lines) || l:line == len(l:lines) && l:column > len(l:lines[-1]) + 1
109             let [l:line, l:column] = [len(l:lines), len(l:lines[-1]) + 1]
110         " Special case when start is after EOL
111         elseif l:line < len(l:lines) && l:column > len(l:lines[l:line - 1]) + 1
112             let [l:line, l:column] = [l:line + 1, 1]
113         endif
114
115         " Adjust end: clamp if invalid and/or adjust if we moved start
116         if l:end_line < l:line || l:end_line == l:line && l:end_column < l:column
117             let [l:end_line, l:end_column] = [l:line, l:column]
118         endif
119
120         " Clamp end to EOF
121         if l:end_line > len(l:lines) || l:end_line == len(l:lines) && l:end_column > len(l:lines[-1]) + 1
122             let [l:end_line, l:end_column] = [len(l:lines), len(l:lines[-1]) + 1]
123         " Special case when end is after EOL
124         elseif l:end_line < len(l:lines) && l:end_column > len(l:lines[l:end_line - 1]) + 1
125             let [l:end_line, l:end_column] = [l:end_line + 1, 1]
126         endif
127
128         " Careful, [:-1] is not an empty list
129         let l:start = l:line is 1 ? [] : l:lines[: l:line - 2]
130         let l:middle = l:column is 1 ? [''] : [l:lines[l:line - 1][: l:column - 2]]
131
132         let l:middle[-1] .= l:insertions[0]
133         let l:middle     += l:insertions[1:]
134         let l:middle[-1] .= l:lines[l:end_line - 1][l:end_column - 1 :]
135
136         let l:end_line_len = len(l:lines[l:end_line - 1])
137         let l:lines_before_change = len(l:lines)
138         let l:lines = l:start + l:middle + l:lines[l:end_line :]
139
140         let l:current_line_offset = len(l:lines) - l:lines_before_change
141         let l:column_offset = len(l:middle[-1]) - l:end_line_len
142
143         " Keep cursor where it was (if outside of changes) or move it after
144         " the changed text (if inside), but don't touch it when the change
145         " spans the entire buffer, in which case we have no clue and it's
146         " better to not do anything.
147         if l:line isnot 1 || l:column isnot 1
148         \|| l:end_line < l:lines_before_change
149         \|| l:end_line == l:lines_before_change && l:end_column <= l:end_line_len
150             let l:pos = s:UpdateCursor(l:pos,
151             \ [l:line, l:column],
152             \ [l:end_line, l:end_column],
153             \ [l:current_line_offset, l:column_offset])
154         endif
155     endfor
156
157     " Make sure to add a trailing newline if and only if it should be added.
158     if l:lines[-1] is# '' && getbufvar(l:buffer, '&eol')
159         call remove(l:lines, -1)
160     else
161         call setbufvar(l:buffer, '&eol', 0)
162     endif
163
164     call ale#util#SetBufferContents(l:buffer, l:lines)
165
166     call ale#lsp#NotifyForChanges(l:conn_id, l:buffer)
167
168     if l:should_save
169         call ale#util#Execute('silent w!')
170     endif
171
172     call setpos('.', [0, l:pos[0], l:pos[1], 0])
173
174     if l:orig_buffer != l:buffer && bufexists(l:orig_buffer)
175         call ale#util#Execute('silent buf ' . string(l:orig_buffer))
176     endif
177 endfunction
178
179 function! s:UpdateCursor(cursor, start, end, offset) abort
180     let l:cur_line = a:cursor[0]
181     let l:cur_column = a:cursor[1]
182     let l:line = a:start[0]
183     let l:column = a:start[1]
184     let l:end_line = a:end[0]
185     let l:end_column = a:end[1]
186     let l:line_offset = a:offset[0]
187     let l:column_offset = a:offset[1]
188
189     if l:end_line < l:cur_line
190         " both start and end lines are before the cursor. only line offset
191         " needs to be updated
192         let l:cur_line += l:line_offset
193     elseif l:end_line == l:cur_line
194         " end line is at the same location as cursor, which means
195         " l:line <= l:cur_line
196         if l:line < l:cur_line || l:column <= l:cur_column
197             " updates are happening either before or around the cursor
198             if l:end_column < l:cur_column
199                 " updates are happening before the cursor, update the
200                 " column offset for cursor
201                 let l:cur_line += l:line_offset
202                 let l:cur_column += l:column_offset
203             else
204                 " updates are happening around the cursor, move the cursor
205                 " to the end of the changes
206                 let l:cur_line += l:line_offset
207                 let l:cur_column = l:end_column + l:column_offset
208             endif
209         " else is not necessary, it means modifications are happening
210         " after the cursor so no cursor updates need to be done
211         endif
212     else
213         " end line is after the cursor
214         if l:line < l:cur_line || l:line == l:cur_line && l:column <= l:cur_column
215             " changes are happening around the cursor, move the cursor
216             " to the end of the changes
217             let l:cur_line = l:end_line + l:line_offset
218             let l:cur_column = l:end_column + l:column_offset
219         " else is not necessary, it means modifications are happening
220         " after the cursor so no cursor updates need to be done
221         endif
222     endif
223
224     return [l:cur_line, l:cur_column]
225 endfunction
226
227 function! ale#code_action#GetChanges(workspace_edit) abort
228     if a:workspace_edit is v:null
229         return {}
230     endif
231
232     let l:changes = {}
233
234     if has_key(a:workspace_edit, 'changes') && !empty(a:workspace_edit.changes)
235         return a:workspace_edit.changes
236     elseif has_key(a:workspace_edit, 'documentChanges')
237         let l:document_changes = []
238
239         if type(a:workspace_edit.documentChanges) is v:t_dict
240         \ && has_key(a:workspace_edit.documentChanges, 'edits')
241             call add(l:document_changes, a:workspace_edit.documentChanges)
242         elseif type(a:workspace_edit.documentChanges) is v:t_list
243             let l:document_changes = a:workspace_edit.documentChanges
244         endif
245
246         for l:text_document_edit in l:document_changes
247             let l:filename = l:text_document_edit.textDocument.uri
248             let l:edits = l:text_document_edit.edits
249             let l:changes[l:filename] = l:edits
250         endfor
251     endif
252
253     return l:changes
254 endfunction
255
256 function! ale#code_action#BuildChangesList(changes_map) abort
257     let l:changes = []
258
259     for l:file_name in keys(a:changes_map)
260         let l:text_edits = a:changes_map[l:file_name]
261         let l:text_changes = []
262
263         for l:edit in l:text_edits
264             let l:range = l:edit.range
265             let l:new_text = l:edit.newText
266
267             call add(l:text_changes, {
268             \ 'start': {
269             \   'line': l:range.start.line + 1,
270             \   'offset': l:range.start.character + 1,
271             \ },
272             \ 'end': {
273             \   'line': l:range.end.line + 1,
274             \   'offset': l:range.end.character + 1,
275             \ },
276             \ 'newText': l:new_text,
277             \})
278         endfor
279
280         call add(l:changes, {
281         \   'fileName': ale#util#ToResource(l:file_name),
282         \   'textChanges': l:text_changes,
283         \})
284     endfor
285
286     return l:changes
287 endfunction
288
289 function! s:EscapeMenuName(text) abort
290     return substitute(a:text, '\\\| \|\.\|&', '\\\0', 'g')
291 endfunction
292
293 function! s:UpdateMenu(data, menu_items) abort
294     silent! aunmenu PopUp.Refactor\.\.\.
295
296     if empty(a:data)
297         return
298     endif
299
300     for [l:type, l:item] in a:menu_items
301         let l:name = l:type is# 'tsserver' ? l:item.name : l:item.title
302         let l:func_name = l:type is# 'tsserver'
303         \   ? 'ale#codefix#ApplyTSServerCodeAction'
304         \   : 'ale#codefix#ApplyLSPCodeAction'
305
306         execute printf(
307         \   'anoremenu <silent> PopUp.&Refactor\.\.\..%s'
308         \       . ' :call %s(%s, %s)<CR>',
309         \   s:EscapeMenuName(l:name),
310         \   l:func_name,
311         \   string(a:data),
312         \   string(l:item),
313         \)
314     endfor
315
316     if empty(a:menu_items)
317         silent! anoremenu PopUp.Refactor\.\.\..(None) :silent
318     endif
319 endfunction
320
321 function! s:GetCodeActions(linter, options) abort
322     let l:buffer = bufnr('')
323     let [l:line, l:column] = getpos('.')[1:2]
324     let l:column = min([l:column, len(getline(l:line))])
325
326     let l:location = {
327     \   'buffer': l:buffer,
328     \   'line': l:line,
329     \   'column': l:column,
330     \   'end_line': l:line,
331     \   'end_column': l:column,
332     \}
333     let l:Callback = function('s:OnReady', [l:location, a:options])
334     call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback)
335 endfunction
336
337 function! ale#code_action#GetCodeActions(options) abort
338     silent! aunmenu PopUp.Rename
339     silent! aunmenu PopUp.Refactor\.\.\.
340
341     " Only display the menu items if there's an LSP server.
342     if len(ale#lsp_linter#GetEnabled(bufnr(''))) > 0
343         if !empty(expand('<cword>'))
344             silent! anoremenu <silent> PopUp.Rename :ALERename<CR>
345         endif
346
347         silent! anoremenu <silent> PopUp.Refactor\.\.\..(None) :silent<CR>
348
349         call ale#codefix#Execute(
350         \   mode() is# 'v' || mode() is# "\<C-V>",
351         \   function('s:UpdateMenu')
352         \)
353     endif
354 endfunction
355
356 function! s:Setup(enabled) abort
357     augroup ALECodeActionsGroup
358         autocmd!
359
360         if a:enabled
361             autocmd MenuPopup * :call ale#code_action#GetCodeActions({})
362         endif
363     augroup END
364
365     if !a:enabled
366         silent! augroup! ALECodeActionsGroup
367
368         silent! aunmenu PopUp.Rename
369         silent! aunmenu PopUp.Refactor\.\.\.
370     endif
371 endfunction
372
373 function! ale#code_action#EnablePopUpMenu() abort
374     call s:Setup(1)
375 endfunction
376
377 function! ale#code_action#DisablePopUpMenu() abort
378     call s:Setup(0)
379 endfunction