Improve code action (#663)

* Improve code action

* Add LspCodeActionSync

* Fix miss argument

* Fix for the review

* Add utils and tests

* Remove unused function
This commit is contained in:
hrsh7th
2020-01-10 01:35:39 +09:00
committed by Prabir Shrestha
parent 896abb2d69
commit 70234feca4
11 changed files with 324 additions and 143 deletions

View File

@@ -435,6 +435,14 @@ function! lsp#default_get_supported_capabilities(server_info) abort
\ 'valueSet': lsp#omni#get_completion_item_kinds() \ 'valueSet': lsp#omni#get_completion_item_kinds()
\ } \ }
\ }, \ },
\ 'codeAction': {
\ 'dynamicRegistration': v:false,
\ 'codeActionLiteralSupport': {
\ 'codeActionKind': {
\ 'valueSet': ['', 'quickfix', 'refactor', 'refactor.extract', 'refactor.inline', 'refactor.rewrite', 'source', 'source.organizeImports'],
\ }
\ }
\ },
\ 'declaration': { \ 'declaration': {
\ 'linkSupport' : v:true \ 'linkSupport' : v:true
\ }, \ },

View File

@@ -161,3 +161,16 @@ function! lsp#capabilities#get_signature_help_trigger_characters(server_name) ab
endif endif
return [] return []
endfunction endfunction
function! lsp#capabilities#get_code_action_kinds(server_name) abort
let l:capabilities = lsp#get_server_capabilities(a:server_name)
if !empty(l:capabilities) && has_key(l:capabilities, 'codeActionProvider')
if type(l:capabilities['codeActionProvider']) == type({})
if has_key(l:capabilities['codeActionProvider'], 'codeActionKinds') && type(l:capabilities['codeActionProvider']['codeActionKinds']) == type([])
return l:capabilities['codeActionProvider']['codeActionKinds']
endif
endif
endif
return []
endfunction

View File

@@ -382,74 +382,6 @@ function! lsp#ui#vim#document_symbol() abort
echo 'Retrieving document symbols ...' echo 'Retrieving document symbols ...'
endfunction endfunction
" Returns currently selected range. If nothing is selected, returns empty
" dictionary.
"
" @returns
" Range - https://microsoft.github.io/language-server-protocol/specification#range
function! s:get_visual_selection_range() abort
" TODO: unify this method with s:get_visual_selection_pos()
let [l:line_start, l:column_start] = getpos("'<")[1:2]
let [l:line_end, l:column_end] = getpos("'>")[1:2]
call lsp#log([l:line_start, l:column_start, l:line_end, l:column_end])
if l:line_start == 0
return {}
endif
" For line selection, column_end is a very large number, so trim it to
" number of characters in this line.
if l:column_end - 1 > len(getline(l:line_end))
let l:column_end = len(getline(l:line_end)) + 1
endif
let l:char_start = lsp#utils#to_char('%', l:line_start, l:column_start)
let l:char_end = lsp#utils#to_char('%', l:line_end, l:column_end)
return {
\ 'start': { 'line': l:line_start - 1, 'character': l:char_start },
\ 'end': { 'line': l:line_end - 1, 'character': l:char_end },
\}
endfunction
" https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction
function! lsp#ui#vim#code_action() abort
let l:servers = filter(lsp#get_whitelisted_servers(), 'lsp#capabilities#has_code_action_provider(v:val)')
let l:command_id = lsp#_new_command()
let l:diagnostic = lsp#ui#vim#diagnostics#get_diagnostics_under_cursor()
if len(l:servers) == 0
call s:not_supported('Code action')
return
endif
let l:range = s:get_visual_selection_range()
if empty(l:range)
if empty(l:diagnostic)
echo 'No diagnostics found under the cursors'
return
else
let l:range = l:diagnostic['range']
let l:diagnostics = [l:diagnostic]
end
else
let l:diagnostics = []
endif
for l:server in l:servers
call lsp#send_request(l:server, {
\ 'method': 'textDocument/codeAction',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'range': l:range,
\ 'context': {
\ 'diagnostics' : l:diagnostics,
\ 'only': ['', 'quickfix', 'refactor', 'refactor.extract', 'refactor.inline', 'refactor.rewrite', 'source', 'source.organizeImports'],
\ },
\ },
\ 'on_notification': function('s:handle_code_action', [l:server, l:command_id, 'codeAction']),
\ })
endfor
echo 'Retrieving code actions ...'
endfunction
function! s:handle_symbol(server, last_command_id, type, data) abort function! s:handle_symbol(server, last_command_id, type, data) abort
if a:last_command_id != lsp#_last_command() if a:last_command_id != lsp#_last_command()
return return
@@ -582,37 +514,6 @@ function! s:handle_text_edit(server, last_command_id, type, data) abort
redraw | echo 'Document formatted' redraw | echo 'Document formatted'
endfunction endfunction
function! s:handle_code_action(server, last_command_id, type, data) abort
if lsp#client#is_error(a:data['response'])
call lsp#utils#error('Failed to '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response']))
return
endif
let l:codeActions = a:data['response']['result']
let l:index = 0
let l:choices = []
call lsp#log('s:handle_code_action', l:codeActions)
if len(l:codeActions) == 0
echo 'No code actions found'
return
endif
while l:index < len(l:codeActions)
call add(l:choices, string(l:index + 1) . ' - ' . l:codeActions[index]['title'])
let l:index += 1
endwhile
let l:choice = inputlist(l:choices)
if l:choice > 0 && l:choice <= l:index
call s:execute_command_or_code_action(a:server, l:codeActions[l:choice - 1])
endif
endfunction
function! s:handle_type_hierarchy(ctx, server, type, data) abort "ctx = {counter, list, last_command_id} function! s:handle_type_hierarchy(ctx, server, type, data) abort "ctx = {counter, list, last_command_id}
if a:ctx['last_command_id'] != lsp#_last_command() if a:ctx['last_command_id'] != lsp#_last_command()
return return
@@ -679,37 +580,3 @@ function! s:get_treeitem_for_tree_hierarchy(Callback, object) dict abort
call a:Callback('success', s:hierarchyitem_to_treeitem(a:object)) call a:Callback('success', s:hierarchyitem_to_treeitem(a:object))
endfunction endfunction
" @params
" server - string
" comand_or_code_action - Command | CodeAction
function! s:execute_command_or_code_action(server, command_or_code_action) abort
if has_key(a:command_or_code_action, 'command') && type(a:command_or_code_action['command']) == type('')
let l:command = a:command_or_code_action
call s:execute_command(a:server, l:command)
else
let l:code_action = a:command_or_code_action
if has_key(l:code_action, 'edit')
call lsp#utils#workspace_edit#apply_workspace_edit(a:command_or_code_action['edit'])
endif
if has_key(l:code_action, 'command')
call s:execute_command(a:server, l:code_action['command'])
endif
endif
endfunction
" Sends workspace/executeCommand with given command.
" @params
" server - string
" command - https://microsoft.github.io/language-server-protocol/specification#command
function! s:execute_command(server, command) abort
let l:params = {'command': a:command['command']}
if has_key(a:command, 'arguments')
let l:params['arguments'] = a:command['arguments']
endif
call lsp#send_request(a:server, {
\ 'method': 'workspace/executeCommand',
\ 'params': l:params,
\ })
endfunction

View File

@@ -0,0 +1,116 @@
" vint: -ProhibitUnusedVariable
function! lsp#ui#vim#code_action#complete(input, command, len) abort
let l:server_names = filter(lsp#get_whitelisted_servers(), 'lsp#capabilities#has_code_action_provider(v:val)')
let l:kinds = []
for l:server_name in l:server_names
let l:kinds += lsp#capabilities#get_code_action_kinds(l:server_name)
endfor
return filter(copy(l:kinds), { _, kind -> kind =~ '^' . a:input })
endfunction
"
" @param option = {
" selection: v:true | v:false = Provide by CommandLine like `:'<,'>LspCodeAction`
" sync: v:true | v:false = Specify enable synchronous request. Example use case is `BufWritePre`
" query: string = Specify code action kind query. If query provided and then filtered code action is only one, invoke code action immediately.
" }
"
function! lsp#ui#vim#code_action#do(option) abort
let l:selection = get(a:option, 'selection', v:false)
let l:sync = get(a:option, 'sync', v:false)
let l:query = get(a:option, 'query', '')
let l:server_names = filter(lsp#get_whitelisted_servers(), 'lsp#capabilities#has_code_action_provider(v:val)')
if len(l:server_names) == 0
return lsp#utils#error('Code action not supported for ' . &filetype)
endif
if l:selection
let l:range = lsp#utils#range#_get_recent_visual_range()
else
let l:range = lsp#utils#range#_get_current_line_range()
endif
let l:command_id = lsp#_new_command()
for l:server_name in l:server_names
let l:diagnostic = lsp#ui#vim#diagnostics#get_diagnostics_under_cursor(l:server_name)
call lsp#send_request(l:server_name, {
\ 'method': 'textDocument/codeAction',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'range': empty(l:diagnostic) || l:selection ? l:range : l:diagnostic['range'],
\ 'context': {
\ 'diagnostics' : empty(l:diagnostic) ? [] : [l:diagnostic],
\ 'only': ['', 'quickfix', 'refactor', 'refactor.extract', 'refactor.inline', 'refactor.rewrite', 'source', 'source.organizeImports'],
\ },
\ },
\ 'sync': l:sync,
\ 'on_notification': function('s:handle_code_action', [l:server_name, l:command_id, l:sync, l:query]),
\ })
endfor
echo 'Retrieving code actions ...'
endfunction
function! s:handle_code_action(server_name, command_id, sync, query, data) abort
" Ignore old request.
if a:command_id != lsp#_last_command()
return
endif
" Check response error.
if lsp#client#is_error(a:data['response'])
call lsp#utils#error('Failed to CodeAction for ' . a:server_name . ': ' . lsp#client#error_message(a:data['response']))
return
endif
" Check code actions.
let l:code_actions = a:data['response']['result']
call lsp#log('s:handle_code_action', l:code_actions)
if len(l:code_actions) == 0
echo 'No code actions found'
return
endif
" Filter code actions.
if !empty(a:query)
let l:code_actions = filter(l:code_actions, { _, action -> get(action, 'kind', '') =~# '^' . a:query })
endif
" Prompt to choose code actions when empty query provided.
let l:index = 1
if len(l:code_actions) > 1 || empty(a:query)
let l:index = inputlist(map(copy(l:code_actions), { i, action ->
\ printf('%s - %s', i + 1, action['title'])
\ }))
endif
" Execute code action.
if 0 < l:index && l:index <= len(l:code_actions)
call s:handle_one_code_action(a:server_name, a:sync, l:code_actions[l:index - 1])
endif
endfunction
function! s:handle_one_code_action(server_name, sync, command_or_code_action) abort
" has WorkspaceEdit.
if has_key(a:command_or_code_action, 'edit')
call lsp#utils#workspace_edit#apply_workspace_edit(a:command_or_code_action['edit'])
" Command.
elseif has_key(a:command_or_code_action, 'command') && type(a:command_or_code_action['command']) == type('')
call lsp#send_request(a:server_name, {
\ 'method': 'workspace/executeCommand',
\ 'params': a:command_or_code_action,
\ 'sync': a:sync
\ })
" has Command.
elseif has_key(a:command_or_code_action, 'command') && type(a:command_or_code_action['command']) == type({})
call lsp#send_request(a:server_name, {
\ 'method': 'workspace/executeCommand',
\ 'params': a:command_or_code_action['command'],
\ 'sync': a:sync
\ })
endif
endfunction

View File

@@ -55,8 +55,10 @@ endfunction
" "
" Note: Consider renaming this method (s/diagnostics/diagnostic) to make " Note: Consider renaming this method (s/diagnostics/diagnostic) to make
" it clear that it returns just one diagnostic, not a list. " it clear that it returns just one diagnostic, not a list.
function! lsp#ui#vim#diagnostics#get_diagnostics_under_cursor() abort function! lsp#ui#vim#diagnostics#get_diagnostics_under_cursor(...) abort
let l:diagnostics = s:get_all_buffer_diagnostics() let l:target_server_name = get(a:000, 0, '')
let l:diagnostics = s:get_all_buffer_diagnostics(l:target_server_name)
if !len(l:diagnostics) if !len(l:diagnostics)
return return
endif endif
@@ -127,8 +129,8 @@ function! s:next_diagnostic(diagnostics) abort
let l:view['col'] = l:next_col let l:view['col'] = l:next_col
let l:view['topline'] = 1 let l:view['topline'] = 1
let l:height = winheight(0) let l:height = winheight(0)
let totalnum = line('$') let l:totalnum = line('$')
if totalnum > l:height if l:totalnum > l:height
let l:half = l:height / 2 let l:half = l:height / 2
if l:totalnum - l:half < l:view['lnum'] if l:totalnum - l:half < l:view['lnum']
let l:view['topline'] = l:totalnum - l:height + 1 let l:view['topline'] = l:totalnum - l:height + 1
@@ -186,8 +188,8 @@ function! s:previous_diagnostic(diagnostics) abort
let l:view['col'] = l:next_col let l:view['col'] = l:next_col
let l:view['topline'] = 1 let l:view['topline'] = 1
let l:height = winheight(0) let l:height = winheight(0)
let totalnum = line('$') let l:totalnum = line('$')
if totalnum > l:height if l:totalnum > l:height
let l:half = l:height / 2 let l:half = l:height / 2
if l:totalnum - l:half < l:view['lnum'] if l:totalnum - l:half < l:view['lnum']
let l:view['topline'] = l:totalnum - l:height + 1 let l:view['topline'] = l:totalnum - l:height + 1
@@ -215,7 +217,9 @@ function! s:get_diagnostics(uri) abort
endfunction endfunction
" Get diagnostics for the current buffer URI from all servers " Get diagnostics for the current buffer URI from all servers
function! s:get_all_buffer_diagnostics() abort function! s:get_all_buffer_diagnostics(...) abort
let l:target_server_name = get(a:000, 0, '')
let l:uri = lsp#utils#get_buffer_uri() let l:uri = lsp#utils#get_buffer_uri()
let [l:has_diagnostics, l:diagnostics] = s:get_diagnostics(l:uri) let [l:has_diagnostics, l:diagnostics] = s:get_diagnostics(l:uri)
@@ -225,7 +229,9 @@ function! s:get_all_buffer_diagnostics() abort
let l:all_diagnostics = [] let l:all_diagnostics = []
for [l:server_name, l:data] in items(l:diagnostics) for [l:server_name, l:data] in items(l:diagnostics)
call extend(l:all_diagnostics, l:data['response']['params']['diagnostics']) if empty(l:target_server_name) || l:server_name ==# l:target_server_name
call extend(l:all_diagnostics, l:data['response']['params']['diagnostics'])
endif
endfor endfor
return l:all_diagnostics return l:all_diagnostics

View File

@@ -17,6 +17,22 @@ function! s:to_col(expr, lnum, char) abort
return strlen(strcharpart(l:linestr, 0, a:char)) + 1 return strlen(strcharpart(l:linestr, 0, a:char)) + 1
endfunction endfunction
" The inverse version of `s:to_col`.
" Convert [lnum, col] to LSP's `Position`.
function! s:to_char(expr, lnum, col) abort
let l:lines = getbufline(a:expr, a:lnum)
if l:lines == []
if type(a:expr) != v:t_string || !filereadable(a:expr)
" invalid a:expr
return a:col - 1
endif
" a:expr is a file that is not yet loaded as a buffer
let l:lines = readfile(a:expr, '', a:lnum)
endif
let l:linestr = l:lines[-1]
return strchars(strpart(l:linestr, 0, a:col - 1))
endfunction
" @param expr = see :help bufname() " @param expr = see :help bufname()
" @param position = { " @param position = {
" 'line': 1, " 'line': 1,
@@ -32,3 +48,17 @@ function! lsp#utils#position#_lsp_to_vim(expr, position) abort
let l:col = s:to_col(a:expr, l:line, l:char) let l:col = s:to_col(a:expr, l:line, l:char)
return [l:line, l:col] return [l:line, l:col]
endfunction endfunction
" @param expr = :help bufname()
" @param pos = [lnum, col]
" @returns {
" 'line': line,
" 'character': character
" }
function! lsp#utils#position#_vim_to_lsp(expr, pos) abort
return {
\ 'line': a:pos[0] - 1,
\ 'character': s:to_char(a:expr, a:pos[0], a:pos[1])
\ }
endfunction

View File

@@ -0,0 +1,31 @@
"
" Returns recent visual-mode range.
"
function! lsp#utils#range#_get_recent_visual_range() abort
let l:start_pos = getpos("'<")[1 : 2]
let l:end_pos = getpos("'>")[1 : 2]
let l:end_pos[1] += 1 " To exclusive
" Fix line selection.
let l:end_line = getline(l:end_pos[0])
if l:end_pos[1] > strlen(l:end_line)
let l:end_pos[1] = strlen(l:end_line) + 1
endif
let l:range = {}
let l:range['start'] = lsp#utils#position#_vim_to_lsp('%', l:start_pos)
let l:range['end'] = lsp#utils#position#_vim_to_lsp('%', l:end_pos)
return l:range
endfunction
"
" Returns current line range.
"
function! lsp#utils#range#_get_current_line_range() abort
let l:pos = getpos('.')[1 : 2]
let l:range = {}
let l:range['start'] = lsp#utils#position#_vim_to_lsp('%', l:pos)
let l:range['end'] = lsp#utils#position#_vim_to_lsp('%', [l:pos[0], l:pos[1] + strlen(getline(l:pos[0])) + 1])
return l:range
endfunction

View File

@@ -53,6 +53,7 @@ CONTENTS *vim-lsp-contents*
lsp#get_buffer_first_error_line() |lsp#get_buffer_first_error_line()| lsp#get_buffer_first_error_line() |lsp#get_buffer_first_error_line()|
Commands |vim-lsp-commands| Commands |vim-lsp-commands|
LspCodeAction |:LspCodeAction| LspCodeAction |:LspCodeAction|
LspCodeActionSync |:LspCodeActionSync|
LspDocumentDiagnostics |:LspDocumentDiagnostics| LspDocumentDiagnostics |:LspDocumentDiagnostics|
LspDeclaration |:LspDeclaration| LspDeclaration |:LspDeclaration|
LspDefinition |:LspDefinition| LspDefinition |:LspDefinition|
@@ -914,11 +915,23 @@ Get line number of first error in current buffer.
============================================================================== ==============================================================================
Commands *vim-lsp-commands* Commands *vim-lsp-commands*
LspCodeAction *:LspCodeAction* LspCodeAction [{CodeActionKind}] *:LspCodeAction*
Gets a list of possible commands that can be applied to a file so it can be Gets a list of possible commands that can be applied to a file so it can be
fixed (quick fix). fixed (quick fix).
If the optional {CodeActionKind} specified, will invoke code action
immediately when matched code action is one only.
LspCodeActionSync [{CodeActionKind}] *:LspCodeActionSync*
Same as |:LspCodeAction| but synchronous. Useful when running |:autocmd|
commands such as organize imports before save.
Example: >
autocmd BufWritePre <buffer>
\ call execute('LspCodeActionSync source.organizeImports')
LspDocumentDiagnostics *:LspDocumentDiagnostics* LspDocumentDiagnostics *:LspDocumentDiagnostics*
Gets the current document diagnostics. Gets the current document diagnostics.

View File

@@ -53,7 +53,16 @@ if g:lsp_auto_enable
augroup END augroup END
endif endif
command! -range LspCodeAction call lsp#ui#vim#code_action() command! -range -nargs=* -complete=customlist,lsp#ui#vim#code_action#complete LspCodeAction call lsp#ui#vim#code_action#do({
\ 'sync': v:false,
\ 'selection': <range> != 0,
\ 'query': '<args>'
\ })
command! -range -nargs=* -complete=customlist,lsp#ui#vim#code_action#complete LspCodeActionSync call lsp#ui#vim#code_action#do({
\ 'sync': v:true,
\ 'selection': <range> != 0,
\ 'query': '<args>'
\ })
command! LspDeclaration call lsp#ui#vim#declaration(0) command! LspDeclaration call lsp#ui#vim#declaration(0)
command! LspPeekDeclaration call lsp#ui#vim#declaration(1) command! LspPeekDeclaration call lsp#ui#vim#declaration(1)
command! LspDefinition call lsp#ui#vim#definition(0) command! LspDefinition call lsp#ui#vim#definition(0)

View File

@@ -35,4 +35,31 @@ Describe lsp#utils#position
End End
Describe lsp#utils#position#_vim_to_lsp
It should return the character-index from the given byte-index on a buffer
call setline(1, ['a β c', 'δ', ''])
Assert Equals({ 'line': 0, 'character': 0 }, lsp#utils#position#_vim_to_lsp('%', [1, 1]))
Assert Equals({ 'line': 0, 'character': 1 }, lsp#utils#position#_vim_to_lsp('%', [1, 2]))
Assert Equals({ 'line': 0, 'character': 2 }, lsp#utils#position#_vim_to_lsp('%', [1, 3]))
Assert Equals({ 'line': 0, 'character': 3 }, lsp#utils#position#_vim_to_lsp('%', [1, 5]))
Assert Equals({ 'line': 0, 'character': 4 }, lsp#utils#position#_vim_to_lsp('%', [1, 6]))
Assert Equals({ 'line': 1, 'character': 0 }, lsp#utils#position#_vim_to_lsp('%', [2, 1]))
Assert Equals({ 'line': 1, 'character': 1 }, lsp#utils#position#_vim_to_lsp('%', [2, 3]))
Assert Equals({ 'line': 2, 'character': 0 }, lsp#utils#position#_vim_to_lsp('%', [3, 1]))
End
It should return the character-index from the given byte-index in an unloaded file
Assert Equals({ 'line': 0, 'character': 0 }, lsp#utils#position#_vim_to_lsp('./test/testfiles/multibyte.txt', [1, 1]))
Assert Equals({ 'line': 0, 'character': 1 }, lsp#utils#position#_vim_to_lsp('./test/testfiles/multibyte.txt', [1, 2]))
Assert Equals({ 'line': 0, 'character': 2 }, lsp#utils#position#_vim_to_lsp('./test/testfiles/multibyte.txt', [1, 3]))
Assert Equals({ 'line': 0, 'character': 3 }, lsp#utils#position#_vim_to_lsp('./test/testfiles/multibyte.txt', [1, 5]))
Assert Equals({ 'line': 0, 'character': 4 }, lsp#utils#position#_vim_to_lsp('./test/testfiles/multibyte.txt', [1, 6]))
Assert Equals({ 'line': 1, 'character': 0 }, lsp#utils#position#_vim_to_lsp('./test/testfiles/multibyte.txt', [2, 1]))
Assert Equals({ 'line': 1, 'character': 1 }, lsp#utils#position#_vim_to_lsp('./test/testfiles/multibyte.txt', [2, 3]))
Assert Equals({ 'line': 2, 'character': 0 }, lsp#utils#position#_vim_to_lsp('./test/testfiles/multibyte.txt', [3, 1]))
End
End
End End

View File

@@ -0,0 +1,61 @@
Describe lsp#utils#range
Before each
% delete _
End
Describe lsp#utils#range#_get_recent_visual_range
It should return single line visual selection
call setline(1, ['あいうえお'])
normal! gg0llvly
Assert Equals(lsp#utils#range#_get_recent_visual_range(), {
\ 'start': {
\ 'line': 0,
\ 'character': 2
\ },
\ 'end': {
\ 'line': 0,
\ 'character': 4
\ }
\ })
End
It should return multi line visual selection
call setline(1, ['あいうえお', 'かきくけこ'])
normal! gg0llvjly
Assert Equals(lsp#utils#range#_get_recent_visual_range(), {
\ 'start': {
\ 'line': 0,
\ 'character': 2
\ },
\ 'end': {
\ 'line': 1,
\ 'character': 4
\ }
\ })
End
End
Describe lsp#utils#range#_get_current_line_range
It should return current line range
call setline(1, ['あいうえお', 'かきくけこ', 'さしすせそ'])
call cursor(2, 1)
Assert Equals(lsp#utils#range#_get_current_line_range(), {
\ 'start': {
\ 'line': 1,
\ 'character': 0
\ },
\ 'end': {
\ 'line': 1,
\ 'character': 5
\ }
\ })
End
End
End