@@ -259,6 +259,39 @@ function! lsp#ui#vim#document_symbol() abort
259
259
echo ' Retrieving document symbols ...'
260
260
endfunction
261
261
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
+
262
295
function ! s: handle_symbol (server, last_req_id, type , data) abort
263
296
if a: last_req_id != s: last_req_id
264
297
return
@@ -344,8 +377,34 @@ function! s:handle_text_edit(server, last_req_id, type, data) abort
344
377
echo ' Document formatted'
345
378
endfunction
346
379
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
+
347
406
" @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
349
408
function ! s: apply_workspace_edits (workspace_edits) abort
350
409
if has_key (a: workspace_edits , ' changes' )
351
410
let l: cur_buffer = bufnr (' %' )
@@ -371,36 +430,246 @@ function! s:apply_workspace_edits(workspace_edits) abort
371
430
endif
372
431
endfunction
373
432
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' ]
378
540
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
380
548
endfunction
381
549
382
- function ! s: apply_text_edits (uri, text_edits ) abort
550
+ function ! s: build_cmd (uri, text_edit ) abort
383
551
let l: path = lsp#utils#uri_to_path (a: uri )
384
552
let l: buffer = bufnr (l: path )
385
553
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
406
675
endfunction
0 commit comments