Skip to content

Commit 40825e3

Browse files
augustomeloprabirshrestha
authored andcommitted
[RFC|RDY] code actions (#162)
* WIP code actions It was addded the code actions funcionality https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction Processing the result is not quite right, because it is always applying the c (change) command, and it does not insert the new line (\n) character * WIP code actions It was addded the code actions funcionality https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction Processing the result is not quite right, because it is always applying the c (change) command, and it does not insert the new line (\n) character * Improved apply_text_edit Now the function apply_text_edit insert texts as well, instead of only replacing it. Fixed some typos and broken links * Added documentation and fixed code_action Now lsp#ui#vim#code_action uses the diagnostic range instead of the get_position * Fixed code action The code action had a problem when it needed to delete/replace some text, and when the code was folded it did not work since to execute do not seem to unfold the code * Changed copy to deepcopy Since we modifying nested objects we need to use deepcopy * Fixed preprocessing and added sort to textedits To perform an unfold it is necessary to check first if the line has fold level, if the line has foldlevel = 0 and you try to unfold it will cause and error. Since LSP specification does not guarantee that text edits are ordered, we needed to sort it so it will work on any valid LSP * Fixed text edits sort Removed stable_sort since it is not necessary Now it sorts the text edits in a reverse order and merge text edits that are inserts on the same place * Fixed codeAction deleting lines * Passing capability on initialization * Added buffer validation before setting the sign This validation is necessary since the documentation states that the file {fname} must be already be loaded in a buffer. * Changed msgpack_type to v:true Msgpack_type does not work on vim
1 parent 45f9d75 commit 40825e3

File tree

7 files changed

+315
-29
lines changed

7 files changed

+315
-29
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Refer to docs on configuring omnifunc or [asyncomplete.vim](https://github.com/p
5151

5252
| Command | Description|
5353
|--|--|
54+
|`:LspCodeAction`| Gets a list of possible commands that can be applied to a file so it can be fixed (quick fix) |
5455
|`:LspDocumentDiagnostics`| Get current document diagnostics information |
5556
|`:LspDefinition`| Go to definition |
5657
|`:LspDocumentFormat`| Format entire document |

autoload/lsp.vim

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,11 @@ function! s:ensure_init(buf, server_name, cb) abort
350350
if has_key(l:server_info, 'capabilities')
351351
let l:capabilities = l:server_info['capabilities']
352352
else
353-
let l:capabilities = {}
353+
let l:capabilities = {
354+
\ 'workspace': {
355+
\ 'applyEdit ': v:true
356+
\ }
357+
\ }
354358
endif
355359

356360
if has_key(l:server_info, 'initialization_options')

autoload/lsp/capabilities.vim

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ function! lsp#capabilities#has_implementation_provider(server_name) abort
3939
return s:has_bool_provider(a:server_name, 'implementationProvider')
4040
endfunction
4141

42+
function! lsp#capabilities#has_code_action_provider(server_name) abort
43+
return s:has_bool_provider(a:server_name, 'codeActionProvider')
44+
endfunction
45+
4246
function! lsp#capabilities#has_type_definition_provider(server_name) abort
4347
return s:has_bool_provider(a:server_name, 'typeDefinitionProvider')
4448
endfunction

autoload/lsp/ui/vim.vim

Lines changed: 296 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,39 @@ function! lsp#ui#vim#document_symbol() abort
259259
echo 'Retrieving document symbols ...'
260260
endfunction
261261

262+
" https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction
263+
function! lsp#ui#vim#code_action() abort
264+
let l:servers = filter(lsp#get_whitelisted_servers(), 'lsp#capabilities#has_code_action_provider(v:val)')
265+
let s:last_req_id = s:last_req_id + 1
266+
let s:diagnostics = lsp#ui#vim#diagnostics#get_diagnostics_under_cursor()
267+
268+
if len(l:servers) == 0
269+
call s:not_supported('Code action')
270+
return
271+
endif
272+
273+
if len(s:diagnostics) == 0
274+
echo 'No diagnostics found under the cursors'
275+
return
276+
endif
277+
278+
for l:server in l:servers
279+
call lsp#send_request(l:server, {
280+
\ 'method': 'textDocument/codeAction',
281+
\ 'params': {
282+
\ 'textDocument': lsp#get_text_document_identifier(),
283+
\ 'range': s:diagnostics['range'],
284+
\ 'context': {
285+
\ 'diagnostics' : [s:diagnostics],
286+
\ },
287+
\ },
288+
\ 'on_notification': function('s:handle_code_action', [l:server, s:last_req_id, 'codeAction']),
289+
\ })
290+
endfor
291+
292+
echo 'Retrieving code actions ...'
293+
endfunction
294+
262295
function! s:handle_symbol(server, last_req_id, type, data) abort
263296
if a:last_req_id != s:last_req_id
264297
return
@@ -344,8 +377,34 @@ function! s:handle_text_edit(server, last_req_id, type, data) abort
344377
echo 'Document formatted'
345378
endfunction
346379

380+
function! s:handle_code_action(server, last_req_id, type, data) abort
381+
let l:codeActions = a:data['response']['result']
382+
let l:index = 0
383+
let l:choices = []
384+
385+
call lsp#log('s:handle_code_action', l:codeActions)
386+
387+
if len(l:codeActions) == 0
388+
echo 'No code actions found'
389+
return
390+
endif
391+
392+
while l:index < len(l:codeActions)
393+
call add(l:choices, string(l:index + 1) . ' - ' . l:codeActions[index]['title'])
394+
395+
let l:index += 1
396+
endwhile
397+
398+
let l:choice = inputlist(l:choices)
399+
400+
if l:choice > 0 && l:choice <= l:index
401+
call lsp#log('s:handle_code_action', l:codeActions[l:choice - 1]['arguments'][0])
402+
call s:apply_workspace_edits(l:codeActions[l:choice - 1]['arguments'][0])
403+
endif
404+
endfunction
405+
347406
" @params
348-
" workspace_edits - https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#workspaceedit
407+
" workspace_edits - https://microsoft.github.io/language-server-protocol/specification#workspaceedit
349408
function! s:apply_workspace_edits(workspace_edits) abort
350409
if has_key(a:workspace_edits, 'changes')
351410
let l:cur_buffer = bufnr('%')
@@ -371,36 +430,246 @@ function! s:apply_workspace_edits(workspace_edits) abort
371430
endif
372431
endfunction
373432

374-
function! s:generate_move_cmd(line_pos, character_pos) abort
375-
let l:result = printf("%dG0", a:line_pos) " move the line and set to the cursor at the beggining
376-
if a:character_pos > 0
377-
let l:result .= printf("%dl", a:character_pos) " Move right until the character
433+
function! s:apply_text_edits(uri, text_edits) abort
434+
" https://microsoft.github.io/language-server-protocol/specification#textedit
435+
" The order in the array defines the order in which the inserted string
436+
" appear in the resulting text.
437+
"
438+
" The edits must be applied in the reverse order so the early edits will
439+
" not interfere with the position of later edits, they need to be applied
440+
" one at the time or put together as a single command.
441+
"
442+
" Example: {"range": {"end": {"character": 45, "line": 5}, "start":
443+
" {"character": 45, "line": 5}}, "newText": "\n"}, {"range": {"end":
444+
" {"character": 45, "line": 5}, "start": {"character": 45, "line": 5}},
445+
" "newText": "import javax.ws.rs.Consumes;"}]}}
446+
"
447+
" If we apply the \n first we will need adjust the line range of the next
448+
" command (so the import will be written on the next line) , but if we
449+
" write the import first and then the \n everything will be fine.
450+
" If you do not apply a command one at time, you will need to adjust the
451+
" range columns after which edit. You will get this (only one execution):
452+
"
453+
" execute 'keepjumps normal! 6G045laimport javax.ws.rs.Consumes;'" |
454+
" execute 'keepjumps normal! 6G045la\n'
455+
"
456+
" resulting in this:
457+
" import javax.servlet.http.HttpServletRequest;i
458+
" mport javax.ws.rs.Consumes;
459+
"
460+
" instead of this (multiple executions):
461+
" execute 'keepjumps normal! 6G045laimport javax.ws.rs.Consumes;'"
462+
" execute 'keepjumps normal! 6G045li\n'
463+
"
464+
" resulting in this:
465+
" import javax.servlet.http.HttpServletRequest;
466+
" import javax.ws.rs.Consumes;
467+
"
468+
"
469+
" The sort is also necessary since the LSP specification does not
470+
" guarantee that text edits are sorted.
471+
"
472+
" Example:
473+
" Initial text: "abcdef"
474+
" Edits:
475+
" ((0,0), (0, 1), "") - remove first character 'a'
476+
" ((0, 4), (0, 5), "") - remove fifth character 'e'
477+
" ((0, 2), (0, 3), "") - remove third character 'c'
478+
let l:text_edits = sort(deepcopy(a:text_edits), '<SID>sort_text_edit_desc')
479+
let l:i = 0
480+
481+
while l:i < len(l:text_edits)
482+
let l:merged_text_edit = s:merge_same_range(l:i, l:text_edits)
483+
let l:cmd = s:build_cmd(a:uri, l:merged_text_edit['merged'])
484+
485+
try
486+
let l:was_paste = &paste
487+
set paste
488+
execute l:cmd
489+
finally
490+
let &paste = l:was_paste
491+
endtry
492+
493+
let l:i = l:merged_text_edit['end_index']
494+
endwhile
495+
endfunction
496+
497+
" Merge the edits on the same range so we do not have to reverse the
498+
" text_edits that are inserts, also from the specification:
499+
" If multiple inserts have the same position, the order in the array
500+
" defines the order in which the inserted strings appear in the
501+
" resulting text
502+
function! s:merge_same_range(start_index, text_edits) abort
503+
let l:i = a:start_index + 1
504+
let l:merged = deepcopy(a:text_edits[a:start_index])
505+
506+
while l:i < len(a:text_edits) &&
507+
\ s:is_same_range(l:merged['range'], a:text_edits[l:i]['range'])
508+
509+
let l:merged['newText'] .= a:text_edits[l:i]['newText']
510+
let l:i += 1
511+
endwhile
512+
513+
return {'merged': l:merged, 'end_index': l:i}
514+
endfunction
515+
516+
function! s:is_same_range(range1, range2) abort
517+
return a:range1['start']['line'] == a:range2['start']['line'] &&
518+
\ a:range1['end']['line'] == a:range2['end']['line'] &&
519+
\ a:range1['start']['character'] == a:range2['start']['character'] &&
520+
\ a:range1['end']['character'] == a:range2['end']['character']
521+
endfunction
522+
523+
" https://microsoft.github.io/language-server-protocol/specification#textedit
524+
function! s:is_insert(range) abort
525+
return a:range['start']['line'] == a:range['end']['line'] &&
526+
\ a:range['start']['character'] == a:range['end']['character']
527+
endfunction
528+
529+
" Compares two text edits, based on the starting position of the range.
530+
" Assumes that edits have non-overlapping ranges.
531+
"
532+
" `text_edit1` and `text_edit2` are dictionaries and represent LSP TextEdit type.
533+
"
534+
" Returns 0 if both text edits starts at the same position (insert text),
535+
" positive value if `text_edit1` starts before `text_edit2` and negative value
536+
" otherwise.
537+
function! s:sort_text_edit_desc(text_edit1, text_edit2) abort
538+
if a:text_edit1['range']['start']['line'] != a:text_edit2['range']['start']['line']
539+
return a:text_edit2['range']['start']['line'] - a:text_edit1['range']['start']['line']
378540
endif
379-
return l:result
541+
542+
if a:text_edit1['range']['start']['character'] != a:text_edit2['range']['start']['character']
543+
return a:text_edit2['range']['start']['character'] - a:text_edit1['range']['start']['character']
544+
endif
545+
546+
return !s:is_insert(a:text_edit1['range']) ? -1 :
547+
\ s:is_insert(a:text_edit2['range']) ? 0 : 1
380548
endfunction
381549

382-
function! s:apply_text_edits(uri, text_edits) abort
550+
function! s:build_cmd(uri, text_edit) abort
383551
let l:path = lsp#utils#uri_to_path(a:uri)
384552
let l:buffer = bufnr(l:path)
385553
let l:cmd = 'keepjumps keepalt ' . (l:buffer !=# -1 ? 'b ' . l:buffer : 'edit ' . l:path)
386-
for l:text_edit in a:text_edits
387-
let l:start_line = l:text_edit['range']['start']['line'] + 1
388-
let l:start_character = l:text_edit['range']['start']['character']
389-
let l:end_line = l:text_edit['range']['end']['line'] + 1
390-
let l:end_character = l:text_edit['range']['end']['character'] - 1 " -1 since the last character is excluded
391-
let l:new_text = l:text_edit['newText']
392-
let l:sub_cmd = s:generate_move_cmd(l:start_line, l:start_character) " move to the first position
393-
let l:sub_cmd .= "v" " visual mode
394-
let l:sub_cmd .= s:generate_move_cmd(l:end_line, l:end_character) " move to the last position
395-
let l:sub_cmd .= printf("c%s", l:new_text) " change text
396-
let l:escaped_sub_cmd = substitute(l:sub_cmd, '''', '''''', 'g')
397-
let l:cmd = l:cmd . " | execute 'keepjumps normal! " . l:escaped_sub_cmd . "'"
398-
endfor
399-
call lsp#log('s:apply_text_edits', l:cmd)
400-
try
401-
set paste
402-
execute l:cmd
403-
finally
404-
set nopaste
405-
endtry
554+
let s:text_edit = deepcopy(a:text_edit)
555+
556+
let s:text_edit['range'] = s:parse_range(s:text_edit['range'])
557+
let l:sub_cmd = s:generate_sub_cmd(s:text_edit)
558+
let l:escaped_sub_cmd = substitute(l:sub_cmd, '''', '''''', 'g')
559+
let l:cmd = l:cmd . " | execute 'keepjumps normal! " . l:escaped_sub_cmd . "'"
560+
561+
call lsp#log('s:build_cmd', l:cmd)
562+
563+
return l:cmd
564+
endfunction
565+
566+
function! s:generate_sub_cmd(text_edit) abort
567+
if s:is_insert(a:text_edit['range'])
568+
return s:generate_sub_cmd_insert(a:text_edit)
569+
else
570+
return s:generate_sub_cmd_replace(a:text_edit)
571+
endif
572+
endfunction
573+
574+
function! s:generate_sub_cmd_insert(text_edit) abort
575+
let l:start_line = a:text_edit['range']['start']['line']
576+
let l:start_character = a:text_edit['range']['start']['character']
577+
let l:new_text = s:parse(a:text_edit['newText'])
578+
579+
let l:sub_cmd = s:preprocess_cmd(a:text_edit['range'])
580+
let l:sub_cmd .= s:generate_move_cmd(l:start_line, l:start_character)
581+
582+
if len(l:new_text) == 0
583+
let l:sub_cmd .= 'x'
584+
else
585+
if l:start_character >= len(getline(l:start_line))
586+
let l:sub_cmd .= 'a'
587+
else
588+
let l:sub_cmd .= 'i'
589+
endif
590+
endif
591+
592+
let l:sub_cmd .= printf('%s', l:new_text)
593+
594+
return l:sub_cmd
595+
endfunction
596+
597+
function! s:generate_sub_cmd_replace(text_edit) abort
598+
let l:start_line = a:text_edit['range']['start']['line']
599+
let l:start_character = a:text_edit['range']['start']['character']
600+
let l:end_line = a:text_edit['range']['end']['line']
601+
let l:end_character = a:text_edit['range']['end']['character']
602+
let l:new_text = a:text_edit['newText']
603+
604+
" This is necessary since you are removing lines, because when in normal
605+
" mode it cannot grab the last character + 1 (\n).
606+
if l:start_character >= len(getline(l:start_line)) && l:end_character == 0
607+
let l:start_line += 1
608+
let l:start_character = 0
609+
610+
endif
611+
612+
" Since the columns in vim is one-based index, this validation is necessary as
613+
" well
614+
if l:end_character == 0
615+
let l:end_line -= 1
616+
let l:end_character = len(getline(l:end_line))
617+
endif
618+
619+
let l:sub_cmd = s:preprocess_cmd(a:text_edit['range'])
620+
let l:sub_cmd .= s:generate_move_cmd(l:start_line, l:start_character) " move to the first position
621+
let l:sub_cmd .= 'v'
622+
let l:sub_cmd .= s:generate_move_cmd(l:end_line, l:end_character) " move to the last position
623+
624+
if len(l:new_text) == 0
625+
let l:sub_cmd .= 'x'
626+
else
627+
let l:sub_cmd .= 'c'
628+
let l:sub_cmd .= printf('%s', l:new_text) " change text
629+
endif
630+
631+
return l:sub_cmd
632+
endfunction
633+
634+
function! s:generate_move_cmd(line_pos, character_pos) abort
635+
let l:result = printf('%dG0', a:line_pos) " move the line and set to the cursor at the beginning
636+
if a:character_pos > 0
637+
let l:result .= printf('%dl', a:character_pos) " move right until the character
638+
endif
639+
return l:result
640+
endfunction
641+
642+
function! s:parse(text) abort
643+
" https://stackoverflow.com/questions/71417/why-is-r-a-newline-for-vim
644+
return substitute(a:text, '\(^\n|\n$\|\r\n\)', '\r', 'g')
645+
endfunction
646+
647+
function! s:preprocess_cmd(range) abort
648+
" preprocess by opening the folds, this is needed because the line you are
649+
" going might have a folding
650+
let l:preprocess = ''
651+
652+
if foldlevel(a:range['start']['line']) > 0
653+
let l:preprocess .= a:range['start']['line']
654+
let l:preprocess .= 'GzO'
655+
endif
656+
657+
if foldlevel(a:range['end']['line']) > 0
658+
let l:preprocess .= a:range['end']['line']
659+
let l:preprocess .= 'GzO'
660+
endif
661+
662+
return l:preprocess
663+
endfunction
664+
665+
" https://microsoft.github.io/language-server-protocol/specification#text-documents
666+
" Position in a text document expressed as zero-based line and zero-based
667+
" character offset, and since we are using the character as a offset position
668+
" we do not have to fix its position
669+
function! s:parse_range(range) abort
670+
let s:range = deepcopy(a:range)
671+
let s:range['start']['line'] = a:range['start']['line'] + 1
672+
let s:range['end']['line'] = a:range['end']['line'] + 1
673+
674+
return s:range
406675
endfunction

autoload/lsp/ui/vim/signs.vim

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,8 @@ endfunction
186186

187187
function! s:place_signs(server_name, path, diagnostics) abort
188188
let s:err_loc = []
189-
if !empty(a:diagnostics)
189+
190+
if !empty(a:diagnostics) && bufnr(a:path) >= 0
190191
for l:item in a:diagnostics
191192
let l:line = l:item['range']['start']['line'] + 1
192193

0 commit comments

Comments
 (0)