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: Jerko Steiner <jerko.steiner@gmail.com>
2 " Description: Code action support for LSP / tsserver
4 function! ale#code_action#ReloadBuffer() abort
5 let l:buffer = bufnr('')
7 execute 'augroup ALECodeActionReloadGroup' . l:buffer
11 silent! execute 'augroup! ALECodeActionReloadGroup' . l:buffer
13 call ale#util#Execute(':e!')
16 function! ale#code_action#HandleCodeAction(code_action, options) abort
17 let l:current_buffer = bufnr('')
18 let l:changes = a:code_action.changes
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,
29 function! s:ChangeCmp(left, right) abort
30 if a:left.start.line < a:right.start.line
34 if a:left.start.line > a:right.start.line
38 if a:left.start.offset < a:right.start.offset
42 if a:left.start.offset > a:right.start.offset
46 if a:left.end.line < a:right.end.line
50 if a:left.end.line > a:right.end.line
54 if a:left.end.offset < a:right.end.offset
58 if a:left.end.offset > a:right.end.offset
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')
69 let l:orig_buffer = bufnr('')
71 " The buffer is used to determine the fileformat, if available.
72 let l:buffer = bufnr(a:filename)
74 if l:buffer != l:orig_buffer
75 call ale#util#Execute('silent edit ' . a:filename)
76 let l:buffer = bufnr('')
79 let l:lines = getbufline(l:buffer, 1, '$')
81 " Add empty line if there's trailing newline, like readfile() does.
82 if getbufvar(l:buffer, '&eol')
86 let l:pos = getpos('.')[1:2]
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
96 let l:insertions = split(l:text, '\n', 1)
99 let l:column = l:column > 0 ? l:column : 1
100 let l:end_column = l:end_column > 0 ? l:end_column : 1
104 let [l:line, l:column] = [1, 1]
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]
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]
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]
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]]
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 :]
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 :]
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
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])
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)
161 call setbufvar(l:buffer, '&eol', 0)
164 call ale#util#SetBufferContents(l:buffer, l:lines)
166 call ale#lsp#NotifyForChanges(l:conn_id, l:buffer)
169 call ale#util#Execute('silent w!')
172 call setpos('.', [0, l:pos[0], l:pos[1], 0])
174 if l:orig_buffer != l:buffer && bufexists(l:orig_buffer)
175 call ale#util#Execute('silent buf ' . string(l:orig_buffer))
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]
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
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
209 " else is not necessary, it means modifications are happening
210 " after the cursor so no cursor updates need to be done
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
224 return [l:cur_line, l:cur_column]
227 function! ale#code_action#GetChanges(workspace_edit) abort
228 if a:workspace_edit is v:null
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 = []
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
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
256 function! ale#code_action#BuildChangesList(changes_map) abort
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 = []
263 for l:edit in l:text_edits
264 let l:range = l:edit.range
265 let l:new_text = l:edit.newText
267 call add(l:text_changes, {
269 \ 'line': l:range.start.line + 1,
270 \ 'offset': l:range.start.character + 1,
273 \ 'line': l:range.end.line + 1,
274 \ 'offset': l:range.end.character + 1,
276 \ 'newText': l:new_text,
280 call add(l:changes, {
281 \ 'fileName': ale#util#ToResource(l:file_name),
282 \ 'textChanges': l:text_changes,
289 function! s:EscapeMenuName(text) abort
290 return substitute(a:text, '\\\| \|\.\|&', '\\\0', 'g')
293 function! s:UpdateMenu(data, menu_items) abort
294 silent! aunmenu PopUp.Refactor\.\.\.
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'
307 \ 'anoremenu <silent> PopUp.&Refactor\.\.\..%s'
308 \ . ' :call %s(%s, %s)<CR>',
309 \ s:EscapeMenuName(l:name),
316 if empty(a:menu_items)
317 silent! anoremenu PopUp.Refactor\.\.\..(None) :silent
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))])
327 \ 'buffer': l:buffer,
329 \ 'column': l:column,
330 \ 'end_line': l:line,
331 \ 'end_column': l:column,
333 let l:Callback = function('s:OnReady', [l:location, a:options])
334 call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback)
337 function! ale#code_action#GetCodeActions(options) abort
338 silent! aunmenu PopUp.Rename
339 silent! aunmenu PopUp.Refactor\.\.\.
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>
347 silent! anoremenu <silent> PopUp.Refactor\.\.\..(None) :silent<CR>
349 call ale#codefix#Execute(
350 \ mode() is# 'v' || mode() is# "\<C-V>",
351 \ function('s:UpdateMenu')
356 function! s:Setup(enabled) abort
357 augroup ALECodeActionsGroup
361 autocmd MenuPopup * :call ale#code_action#GetCodeActions({})
366 silent! augroup! ALECodeActionsGroup
368 silent! aunmenu PopUp.Rename
369 silent! aunmenu PopUp.Refactor\.\.\.
373 function! ale#code_action#EnablePopUpMenu() abort
377 function! ale#code_action#DisablePopUpMenu() abort