Skip to content

Commit 70234fe

Browse files
hrsh7thprabirshrestha
authored andcommitted
Improve code action (#663)
* Improve code action * Add LspCodeActionSync * Fix miss argument * Fix for the review * Add utils and tests * Remove unused function
1 parent 896abb2 commit 70234fe

File tree

11 files changed

+324
-143
lines changed

11 files changed

+324
-143
lines changed

autoload/lsp.vim

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,14 @@ function! lsp#default_get_supported_capabilities(server_info) abort
435435
\ 'valueSet': lsp#omni#get_completion_item_kinds()
436436
\ }
437437
\ },
438+
\ 'codeAction': {
439+
\ 'dynamicRegistration': v:false,
440+
\ 'codeActionLiteralSupport': {
441+
\ 'codeActionKind': {
442+
\ 'valueSet': ['', 'quickfix', 'refactor', 'refactor.extract', 'refactor.inline', 'refactor.rewrite', 'source', 'source.organizeImports'],
443+
\ }
444+
\ }
445+
\ },
438446
\ 'declaration': {
439447
\ 'linkSupport' : v:true
440448
\ },

autoload/lsp/capabilities.vim

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,16 @@ function! lsp#capabilities#get_signature_help_trigger_characters(server_name) ab
161161
endif
162162
return []
163163
endfunction
164+
165+
function! lsp#capabilities#get_code_action_kinds(server_name) abort
166+
let l:capabilities = lsp#get_server_capabilities(a:server_name)
167+
if !empty(l:capabilities) && has_key(l:capabilities, 'codeActionProvider')
168+
if type(l:capabilities['codeActionProvider']) == type({})
169+
if has_key(l:capabilities['codeActionProvider'], 'codeActionKinds') && type(l:capabilities['codeActionProvider']['codeActionKinds']) == type([])
170+
return l:capabilities['codeActionProvider']['codeActionKinds']
171+
endif
172+
endif
173+
endif
174+
return []
175+
endfunction
176+

autoload/lsp/ui/vim.vim

Lines changed: 0 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -382,74 +382,6 @@ function! lsp#ui#vim#document_symbol() abort
382382
echo 'Retrieving document symbols ...'
383383
endfunction
384384

385-
" Returns currently selected range. If nothing is selected, returns empty
386-
" dictionary.
387-
"
388-
" @returns
389-
" Range - https://microsoft.github.io/language-server-protocol/specification#range
390-
function! s:get_visual_selection_range() abort
391-
" TODO: unify this method with s:get_visual_selection_pos()
392-
let [l:line_start, l:column_start] = getpos("'<")[1:2]
393-
let [l:line_end, l:column_end] = getpos("'>")[1:2]
394-
call lsp#log([l:line_start, l:column_start, l:line_end, l:column_end])
395-
if l:line_start == 0
396-
return {}
397-
endif
398-
" For line selection, column_end is a very large number, so trim it to
399-
" number of characters in this line.
400-
if l:column_end - 1 > len(getline(l:line_end))
401-
let l:column_end = len(getline(l:line_end)) + 1
402-
endif
403-
let l:char_start = lsp#utils#to_char('%', l:line_start, l:column_start)
404-
let l:char_end = lsp#utils#to_char('%', l:line_end, l:column_end)
405-
return {
406-
\ 'start': { 'line': l:line_start - 1, 'character': l:char_start },
407-
\ 'end': { 'line': l:line_end - 1, 'character': l:char_end },
408-
\}
409-
endfunction
410-
411-
" https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction
412-
function! lsp#ui#vim#code_action() abort
413-
let l:servers = filter(lsp#get_whitelisted_servers(), 'lsp#capabilities#has_code_action_provider(v:val)')
414-
let l:command_id = lsp#_new_command()
415-
let l:diagnostic = lsp#ui#vim#diagnostics#get_diagnostics_under_cursor()
416-
417-
if len(l:servers) == 0
418-
call s:not_supported('Code action')
419-
return
420-
endif
421-
422-
let l:range = s:get_visual_selection_range()
423-
if empty(l:range)
424-
if empty(l:diagnostic)
425-
echo 'No diagnostics found under the cursors'
426-
return
427-
else
428-
let l:range = l:diagnostic['range']
429-
let l:diagnostics = [l:diagnostic]
430-
end
431-
else
432-
let l:diagnostics = []
433-
endif
434-
435-
for l:server in l:servers
436-
call lsp#send_request(l:server, {
437-
\ 'method': 'textDocument/codeAction',
438-
\ 'params': {
439-
\ 'textDocument': lsp#get_text_document_identifier(),
440-
\ 'range': l:range,
441-
\ 'context': {
442-
\ 'diagnostics' : l:diagnostics,
443-
\ 'only': ['', 'quickfix', 'refactor', 'refactor.extract', 'refactor.inline', 'refactor.rewrite', 'source', 'source.organizeImports'],
444-
\ },
445-
\ },
446-
\ 'on_notification': function('s:handle_code_action', [l:server, l:command_id, 'codeAction']),
447-
\ })
448-
endfor
449-
450-
echo 'Retrieving code actions ...'
451-
endfunction
452-
453385
function! s:handle_symbol(server, last_command_id, type, data) abort
454386
if a:last_command_id != lsp#_last_command()
455387
return
@@ -582,37 +514,6 @@ function! s:handle_text_edit(server, last_command_id, type, data) abort
582514
redraw | echo 'Document formatted'
583515
endfunction
584516

585-
function! s:handle_code_action(server, last_command_id, type, data) abort
586-
if lsp#client#is_error(a:data['response'])
587-
call lsp#utils#error('Failed to '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response']))
588-
return
589-
endif
590-
591-
let l:codeActions = a:data['response']['result']
592-
593-
let l:index = 0
594-
let l:choices = []
595-
596-
call lsp#log('s:handle_code_action', l:codeActions)
597-
598-
if len(l:codeActions) == 0
599-
echo 'No code actions found'
600-
return
601-
endif
602-
603-
while l:index < len(l:codeActions)
604-
call add(l:choices, string(l:index + 1) . ' - ' . l:codeActions[index]['title'])
605-
606-
let l:index += 1
607-
endwhile
608-
609-
let l:choice = inputlist(l:choices)
610-
611-
if l:choice > 0 && l:choice <= l:index
612-
call s:execute_command_or_code_action(a:server, l:codeActions[l:choice - 1])
613-
endif
614-
endfunction
615-
616517
function! s:handle_type_hierarchy(ctx, server, type, data) abort "ctx = {counter, list, last_command_id}
617518
if a:ctx['last_command_id'] != lsp#_last_command()
618519
return
@@ -679,37 +580,3 @@ function! s:get_treeitem_for_tree_hierarchy(Callback, object) dict abort
679580
call a:Callback('success', s:hierarchyitem_to_treeitem(a:object))
680581
endfunction
681582

682-
" @params
683-
" server - string
684-
" comand_or_code_action - Command | CodeAction
685-
function! s:execute_command_or_code_action(server, command_or_code_action) abort
686-
if has_key(a:command_or_code_action, 'command') && type(a:command_or_code_action['command']) == type('')
687-
let l:command = a:command_or_code_action
688-
call s:execute_command(a:server, l:command)
689-
else
690-
let l:code_action = a:command_or_code_action
691-
if has_key(l:code_action, 'edit')
692-
call lsp#utils#workspace_edit#apply_workspace_edit(a:command_or_code_action['edit'])
693-
endif
694-
if has_key(l:code_action, 'command')
695-
call s:execute_command(a:server, l:code_action['command'])
696-
endif
697-
endif
698-
endfunction
699-
700-
" Sends workspace/executeCommand with given command.
701-
" @params
702-
" server - string
703-
" command - https://microsoft.github.io/language-server-protocol/specification#command
704-
function! s:execute_command(server, command) abort
705-
let l:params = {'command': a:command['command']}
706-
if has_key(a:command, 'arguments')
707-
let l:params['arguments'] = a:command['arguments']
708-
endif
709-
call lsp#send_request(a:server, {
710-
\ 'method': 'workspace/executeCommand',
711-
\ 'params': l:params,
712-
\ })
713-
endfunction
714-
715-
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
" vint: -ProhibitUnusedVariable
2+
3+
function! lsp#ui#vim#code_action#complete(input, command, len) abort
4+
let l:server_names = filter(lsp#get_whitelisted_servers(), 'lsp#capabilities#has_code_action_provider(v:val)')
5+
let l:kinds = []
6+
for l:server_name in l:server_names
7+
let l:kinds += lsp#capabilities#get_code_action_kinds(l:server_name)
8+
endfor
9+
return filter(copy(l:kinds), { _, kind -> kind =~ '^' . a:input })
10+
endfunction
11+
12+
"
13+
" @param option = {
14+
" selection: v:true | v:false = Provide by CommandLine like `:'<,'>LspCodeAction`
15+
" sync: v:true | v:false = Specify enable synchronous request. Example use case is `BufWritePre`
16+
" query: string = Specify code action kind query. If query provided and then filtered code action is only one, invoke code action immediately.
17+
" }
18+
"
19+
function! lsp#ui#vim#code_action#do(option) abort
20+
let l:selection = get(a:option, 'selection', v:false)
21+
let l:sync = get(a:option, 'sync', v:false)
22+
let l:query = get(a:option, 'query', '')
23+
24+
let l:server_names = filter(lsp#get_whitelisted_servers(), 'lsp#capabilities#has_code_action_provider(v:val)')
25+
if len(l:server_names) == 0
26+
return lsp#utils#error('Code action not supported for ' . &filetype)
27+
endif
28+
29+
if l:selection
30+
let l:range = lsp#utils#range#_get_recent_visual_range()
31+
else
32+
let l:range = lsp#utils#range#_get_current_line_range()
33+
endif
34+
35+
let l:command_id = lsp#_new_command()
36+
for l:server_name in l:server_names
37+
let l:diagnostic = lsp#ui#vim#diagnostics#get_diagnostics_under_cursor(l:server_name)
38+
call lsp#send_request(l:server_name, {
39+
\ 'method': 'textDocument/codeAction',
40+
\ 'params': {
41+
\ 'textDocument': lsp#get_text_document_identifier(),
42+
\ 'range': empty(l:diagnostic) || l:selection ? l:range : l:diagnostic['range'],
43+
\ 'context': {
44+
\ 'diagnostics' : empty(l:diagnostic) ? [] : [l:diagnostic],
45+
\ 'only': ['', 'quickfix', 'refactor', 'refactor.extract', 'refactor.inline', 'refactor.rewrite', 'source', 'source.organizeImports'],
46+
\ },
47+
\ },
48+
\ 'sync': l:sync,
49+
\ 'on_notification': function('s:handle_code_action', [l:server_name, l:command_id, l:sync, l:query]),
50+
\ })
51+
endfor
52+
echo 'Retrieving code actions ...'
53+
endfunction
54+
55+
function! s:handle_code_action(server_name, command_id, sync, query, data) abort
56+
" Ignore old request.
57+
if a:command_id != lsp#_last_command()
58+
return
59+
endif
60+
61+
" Check response error.
62+
if lsp#client#is_error(a:data['response'])
63+
call lsp#utils#error('Failed to CodeAction for ' . a:server_name . ': ' . lsp#client#error_message(a:data['response']))
64+
return
65+
endif
66+
67+
" Check code actions.
68+
let l:code_actions = a:data['response']['result']
69+
call lsp#log('s:handle_code_action', l:code_actions)
70+
if len(l:code_actions) == 0
71+
echo 'No code actions found'
72+
return
73+
endif
74+
75+
" Filter code actions.
76+
if !empty(a:query)
77+
let l:code_actions = filter(l:code_actions, { _, action -> get(action, 'kind', '') =~# '^' . a:query })
78+
endif
79+
80+
" Prompt to choose code actions when empty query provided.
81+
let l:index = 1
82+
if len(l:code_actions) > 1 || empty(a:query)
83+
let l:index = inputlist(map(copy(l:code_actions), { i, action ->
84+
\ printf('%s - %s', i + 1, action['title'])
85+
\ }))
86+
endif
87+
88+
" Execute code action.
89+
if 0 < l:index && l:index <= len(l:code_actions)
90+
call s:handle_one_code_action(a:server_name, a:sync, l:code_actions[l:index - 1])
91+
endif
92+
endfunction
93+
94+
function! s:handle_one_code_action(server_name, sync, command_or_code_action) abort
95+
" has WorkspaceEdit.
96+
if has_key(a:command_or_code_action, 'edit')
97+
call lsp#utils#workspace_edit#apply_workspace_edit(a:command_or_code_action['edit'])
98+
99+
" Command.
100+
elseif has_key(a:command_or_code_action, 'command') && type(a:command_or_code_action['command']) == type('')
101+
call lsp#send_request(a:server_name, {
102+
\ 'method': 'workspace/executeCommand',
103+
\ 'params': a:command_or_code_action,
104+
\ 'sync': a:sync
105+
\ })
106+
107+
" has Command.
108+
elseif has_key(a:command_or_code_action, 'command') && type(a:command_or_code_action['command']) == type({})
109+
call lsp#send_request(a:server_name, {
110+
\ 'method': 'workspace/executeCommand',
111+
\ 'params': a:command_or_code_action['command'],
112+
\ 'sync': a:sync
113+
\ })
114+
endif
115+
endfunction
116+

autoload/lsp/ui/vim/diagnostics.vim

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ endfunction
5555
"
5656
" Note: Consider renaming this method (s/diagnostics/diagnostic) to make
5757
" it clear that it returns just one diagnostic, not a list.
58-
function! lsp#ui#vim#diagnostics#get_diagnostics_under_cursor() abort
59-
let l:diagnostics = s:get_all_buffer_diagnostics()
58+
function! lsp#ui#vim#diagnostics#get_diagnostics_under_cursor(...) abort
59+
let l:target_server_name = get(a:000, 0, '')
60+
61+
let l:diagnostics = s:get_all_buffer_diagnostics(l:target_server_name)
6062
if !len(l:diagnostics)
6163
return
6264
endif
@@ -127,8 +129,8 @@ function! s:next_diagnostic(diagnostics) abort
127129
let l:view['col'] = l:next_col
128130
let l:view['topline'] = 1
129131
let l:height = winheight(0)
130-
let totalnum = line('$')
131-
if totalnum > l:height
132+
let l:totalnum = line('$')
133+
if l:totalnum > l:height
132134
let l:half = l:height / 2
133135
if l:totalnum - l:half < l:view['lnum']
134136
let l:view['topline'] = l:totalnum - l:height + 1
@@ -186,8 +188,8 @@ function! s:previous_diagnostic(diagnostics) abort
186188
let l:view['col'] = l:next_col
187189
let l:view['topline'] = 1
188190
let l:height = winheight(0)
189-
let totalnum = line('$')
190-
if totalnum > l:height
191+
let l:totalnum = line('$')
192+
if l:totalnum > l:height
191193
let l:half = l:height / 2
192194
if l:totalnum - l:half < l:view['lnum']
193195
let l:view['topline'] = l:totalnum - l:height + 1
@@ -215,7 +217,9 @@ function! s:get_diagnostics(uri) abort
215217
endfunction
216218

217219
" Get diagnostics for the current buffer URI from all servers
218-
function! s:get_all_buffer_diagnostics() abort
220+
function! s:get_all_buffer_diagnostics(...) abort
221+
let l:target_server_name = get(a:000, 0, '')
222+
219223
let l:uri = lsp#utils#get_buffer_uri()
220224

221225
let [l:has_diagnostics, l:diagnostics] = s:get_diagnostics(l:uri)
@@ -225,7 +229,9 @@ function! s:get_all_buffer_diagnostics() abort
225229

226230
let l:all_diagnostics = []
227231
for [l:server_name, l:data] in items(l:diagnostics)
228-
call extend(l:all_diagnostics, l:data['response']['params']['diagnostics'])
232+
if empty(l:target_server_name) || l:server_name ==# l:target_server_name
233+
call extend(l:all_diagnostics, l:data['response']['params']['diagnostics'])
234+
endif
229235
endfor
230236

231237
return l:all_diagnostics

autoload/lsp/utils/position.vim

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,22 @@ function! s:to_col(expr, lnum, char) abort
1717
return strlen(strcharpart(l:linestr, 0, a:char)) + 1
1818
endfunction
1919

20+
" The inverse version of `s:to_col`.
21+
" Convert [lnum, col] to LSP's `Position`.
22+
function! s:to_char(expr, lnum, col) abort
23+
let l:lines = getbufline(a:expr, a:lnum)
24+
if l:lines == []
25+
if type(a:expr) != v:t_string || !filereadable(a:expr)
26+
" invalid a:expr
27+
return a:col - 1
28+
endif
29+
" a:expr is a file that is not yet loaded as a buffer
30+
let l:lines = readfile(a:expr, '', a:lnum)
31+
endif
32+
let l:linestr = l:lines[-1]
33+
return strchars(strpart(l:linestr, 0, a:col - 1))
34+
endfunction
35+
2036
" @param expr = see :help bufname()
2137
" @param position = {
2238
" 'line': 1,
@@ -32,3 +48,17 @@ function! lsp#utils#position#_lsp_to_vim(expr, position) abort
3248
let l:col = s:to_col(a:expr, l:line, l:char)
3349
return [l:line, l:col]
3450
endfunction
51+
52+
" @param expr = :help bufname()
53+
" @param pos = [lnum, col]
54+
" @returns {
55+
" 'line': line,
56+
" 'character': character
57+
" }
58+
function! lsp#utils#position#_vim_to_lsp(expr, pos) abort
59+
return {
60+
\ 'line': a:pos[0] - 1,
61+
\ 'character': s:to_char(a:expr, a:pos[0], a:pos[1])
62+
\ }
63+
endfunction
64+

0 commit comments

Comments
 (0)