Support refactorings through code actions. (#359)

* Support refactorings through code actions.

- make LspCodeAction work on a selected range, to support extract method
or extract variable refactorings
- properly execute commands returned by code actions
- support workspace/applyEdit requests from server

* Fixes to match the Google VimScript Style Guide

* Refactor handling of requests from server.

Introduce an on_request option in lsp#client#start, that will be called
every time a request is received from a server.

* Use robust operator ==#

* Move apply_workspace_edit to separate file.
This commit is contained in:
Tomasz Zurkowski
2019-04-06 12:26:04 -04:00
committed by Prabir Shrestha
parent 6608aad006
commit a79fb04d36
5 changed files with 140 additions and 34 deletions

View File

@@ -351,6 +351,7 @@ function! s:ensure_start(buf, server_name, cb) abort
\ 'on_stderr': function('s:on_stderr', [a:server_name]), \ 'on_stderr': function('s:on_stderr', [a:server_name]),
\ 'on_exit': function('s:on_exit', [a:server_name]), \ 'on_exit': function('s:on_exit', [a:server_name]),
\ 'on_notification': function('s:on_notification', [a:server_name]), \ 'on_notification': function('s:on_notification', [a:server_name]),
\ 'on_request': function('s:on_request', [a:server_name]),
\ }) \ })
if l:lsp_id > 0 if l:lsp_id > 0
@@ -562,6 +563,13 @@ function! s:send_notification(server_name, data) abort
call lsp#client#send_notification(l:lsp_id, a:data) call lsp#client#send_notification(l:lsp_id, a:data)
endfunction endfunction
function! s:send_response(server_name, data) abort
let l:lsp_id = s:servers[a:server_name]['lsp_id']
let l:data = copy(a:data)
call lsp#log_verbose('--->', l:lsp_id, a:server_name, l:data)
call lsp#client#send_response(l:lsp_id, a:data)
endfunction
function! s:on_stderr(server_name, id, data, event) abort function! s:on_stderr(server_name, id, data, event) abort
call lsp#log_verbose('<---(stderr)', a:id, a:server_name, a:data) call lsp#log_verbose('<---(stderr)', a:id, a:server_name, a:data)
endfunction endfunction
@@ -604,6 +612,17 @@ function! s:on_notification(server_name, id, data, event) abort
endfor endfor
endfunction endfunction
function! s:on_request(server_name, id, request) abort
call lsp#log_verbose('<---', a:id, a:request)
if a:request['method'] ==# 'workspace/applyEdit'
call lsp#utils#workspace_edit#apply_workspace_edit(a:request['params']['edit'])
call s:send_response(a:server_name, { 'id': a:request['id'], 'result': { 'applied': v:true } })
else
" Error returned according to json-rpc specification.
call s:send_response(a:server_name, { 'id': a:request['id'], 'error': { 'code': -32601, 'message': 'Method not found' } })
endif
endfunction
function! s:handle_initialize(server_name, data) abort function! s:handle_initialize(server_name, data) abort
let l:response = a:data['response'] let l:response = a:data['response']
let l:server = s:servers[a:server_name] let l:server = s:servers[a:server_name]

View File

@@ -82,7 +82,13 @@ function! s:on_stdout(id, data, event) abort
if exists('l:response') if exists('l:response')
" call appropriate callbacks " call appropriate callbacks
let l:on_notification_data = { 'response': l:response } let l:on_notification_data = { 'response': l:response }
if has_key(l:response, 'id') if has_key(l:response, 'method') && has_key(l:response, 'id')
" it is a request from a server
let l:request = l:response
if has_key(l:ctx['opts'], 'on_request')
call l:ctx['opts']['on_request'](a:id, l:request)
endif
elseif has_key(l:response, 'id')
" it is a request->response " it is a request->response
if !(type(l:response['id']) == type(0) || type(l:response['id']) == type('')) if !(type(l:response['id']) == type(0) || type(l:response['id']) == type(''))
" response['id'] can be number | string | null based on the spec " response['id'] can be number | string | null based on the spec
@@ -199,13 +205,14 @@ endfunction
let s:send_type_request = 1 let s:send_type_request = 1
let s:send_type_notification = 2 let s:send_type_notification = 2
function! s:lsp_send(id, opts, type) abort " opts = { method, params?, on_notification } let s:send_type_response = 3
function! s:lsp_send(id, opts, type) abort " opts = { id?, method?, result?, params?, on_notification }
let l:ctx = get(s:clients, a:id, {}) let l:ctx = get(s:clients, a:id, {})
if empty(l:ctx) if empty(l:ctx)
return -1 return -1
endif endif
let l:request = { 'jsonrpc': '2.0', 'method': a:opts['method'] } let l:request = { 'jsonrpc': '2.0' }
if (a:type == s:send_type_request) if (a:type == s:send_type_request)
let l:ctx['request_sequence'] = l:ctx['request_sequence'] + 1 let l:ctx['request_sequence'] = l:ctx['request_sequence'] + 1
@@ -216,9 +223,21 @@ function! s:lsp_send(id, opts, type) abort " opts = { method, params?, on_notifi
endif endif
endif endif
if has_key(a:opts, 'id')
let l:request['id'] = a:opts['id']
endif
if has_key(a:opts, 'method')
let l:request['method'] = a:opts['method']
endif
if has_key(a:opts, 'params') if has_key(a:opts, 'params')
let l:request['params'] = a:opts['params'] let l:request['params'] = a:opts['params']
endif endif
if has_key(a:opts, 'result')
let l:request['result'] = a:opts['result']
endif
if has_key(a:opts, 'error')
let l:request['error'] = a:opts['error']
endif
let l:json = json_encode(l:request) let l:json = json_encode(l:request)
let l:payload = 'Content-Length: ' . len(l:json) . "\r\n\r\n" . l:json let l:payload = 'Content-Length: ' . len(l:json) . "\r\n\r\n" . l:json
@@ -275,6 +294,10 @@ function! lsp#client#send_notification(client_id, opts) abort
return s:lsp_send(a:client_id, a:opts, s:send_type_notification) return s:lsp_send(a:client_id, a:opts, s:send_type_notification)
endfunction endfunction
function! lsp#client#send_response(client_id, opts) abort
return s:lsp_send(a:client_id, a:opts, s:send_type_response)
endfunction
function! lsp#client#get_last_request_id(client_id) abort function! lsp#client#get_last_request_id(client_id) abort
return s:lsp_get_last_request_id(a:client_id) return s:lsp_get_last_request_id(a:client_id)
endfunction endfunction

View File

@@ -331,20 +331,52 @@ 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
return {
\ 'start': { 'line': l:line_start - 1, 'character': l:column_start - 1 },
\ 'end': { 'line': l:line_end - 1, 'character': l:column_end - 1 },
\}
endfunction
" https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction " https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction
function! lsp#ui#vim#code_action() abort 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:servers = filter(lsp#get_whitelisted_servers(), 'lsp#capabilities#has_code_action_provider(v:val)')
let s:last_req_id = s:last_req_id + 1 let s:last_req_id = s:last_req_id + 1
let s:diagnostics = lsp#ui#vim#diagnostics#get_diagnostics_under_cursor() let l:diagnostic = lsp#ui#vim#diagnostics#get_diagnostics_under_cursor()
if len(l:servers) == 0 if len(l:servers) == 0
call s:not_supported('Code action') call s:not_supported('Code action')
return return
endif endif
if len(s:diagnostics) == 0 let l:range = s:get_visual_selection_range()
echo 'No diagnostics found under the cursors' if empty(l:range)
return 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 endif
for l:server in l:servers for l:server in l:servers
@@ -352,9 +384,9 @@ function! lsp#ui#vim#code_action() abort
\ 'method': 'textDocument/codeAction', \ 'method': 'textDocument/codeAction',
\ 'params': { \ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(), \ 'textDocument': lsp#get_text_document_identifier(),
\ 'range': s:diagnostics['range'], \ 'range': l:range,
\ 'context': { \ 'context': {
\ 'diagnostics' : [s:diagnostics], \ 'diagnostics' : l:diagnostics,
\ }, \ },
\ }, \ },
\ 'on_notification': function('s:handle_code_action', [l:server, s:last_req_id, 'codeAction']), \ 'on_notification': function('s:handle_code_action', [l:server, s:last_req_id, 'codeAction']),
@@ -459,7 +491,7 @@ function! s:handle_workspace_edit(server, last_req_id, type, data) abort
return return
endif endif
call s:apply_workspace_edits(a:data['response']['result']) call lsp#utils#workspace_edit#apply_workspace_edit(a:data['response']['result'])
echo 'Renamed' echo 'Renamed'
endfunction endfunction
@@ -486,6 +518,7 @@ function! s:handle_code_action(server, last_req_id, type, data) abort
endif endif
let l:codeActions = a:data['response']['result'] let l:codeActions = a:data['response']['result']
let l:index = 0 let l:index = 0
let l:choices = [] let l:choices = []
@@ -505,35 +538,41 @@ function! s:handle_code_action(server, last_req_id, type, data) abort
let l:choice = inputlist(l:choices) let l:choice = inputlist(l:choices)
if l:choice > 0 && l:choice <= l:index if l:choice > 0 && l:choice <= l:index
call lsp#log('s:handle_code_action', l:codeActions[l:choice - 1]['arguments'][0]) call s:execute_command_or_code_action(a:server, l:codeActions[l:choice - 1])
call s:apply_workspace_edits(l:codeActions[l:choice - 1]['arguments'][0])
endif endif
endfunction endfunction
" @params " @params
" workspace_edits - https://microsoft.github.io/language-server-protocol/specification#workspaceedit " server - string
function! s:apply_workspace_edits(workspace_edits) abort " comand_or_code_action - Command | CodeAction
if has_key(a:workspace_edits, 'changes') function! s:execute_command_or_code_action(server, command_or_code_action) abort
let l:cur_buffer = bufnr('%') if has_key(a:command_or_code_action, 'command') && type(a:command_or_code_action['command']) == type('')
let l:view = winsaveview() let l:command = a:command_or_code_action
for [l:uri, l:text_edits] in items(a:workspace_edits['changes']) call s:execute_command(a:server, l:command)
call lsp#utils#text_edit#apply_text_edits(l:uri, l:text_edits) else
endfor let l:code_action = a:command_or_code_action
if l:cur_buffer !=# bufnr('%') if has_key(l:code_action, 'edit')
execute 'keepjumps keepalt b ' . l:cur_buffer call lsp#utils#workspace_edit#apply_workspace_edit(a:command_or_code_action['edit'])
endif endif
call winrestview(l:view) if has_key(l:code_action, 'command')
endif call s:execute_command(a:server, l:code_action['command'])
if has_key(a:workspace_edits, 'documentChanges')
let l:cur_buffer = bufnr('%')
let l:view = winsaveview()
for l:text_document_edit in a:workspace_edits['documentChanges']
call lsp#utils#text_edit#apply_text_edits(l:text_document_edit['textDocument']['uri'], l:text_document_edit['edits'])
endfor
if l:cur_buffer !=# bufnr('%')
execute 'keepjumps keepalt b ' . l:cur_buffer
endif endif
call winrestview(l:view)
endif endif
endfunction 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,25 @@
" Applies WorkspaceEdit changes.
function! lsp#utils#workspace_edit#apply_workspace_edit(workspace_edit) abort
if has_key(a:workspace_edit, 'changes')
let l:cur_buffer = bufnr('%')
let l:view = winsaveview()
for [l:uri, l:text_edits] in items(a:workspace_edit['changes'])
call lsp#utils#text_edit#apply_text_edits(l:uri, l:text_edits)
endfor
if l:cur_buffer !=# bufnr('%')
execute 'keepjumps keepalt b ' . l:cur_buffer
endif
call winrestview(l:view)
endif
if has_key(a:workspace_edit, 'documentChanges')
let l:cur_buffer = bufnr('%')
let l:view = winsaveview()
for l:text_document_edit in a:workspace_edit['documentChanges']
call lsp#utils#text_edit#apply_text_edits(l:text_document_edit['textDocument']['uri'], l:text_document_edit['edits'])
endfor
if l:cur_buffer !=# bufnr('%')
execute 'keepjumps keepalt b ' . l:cur_buffer
endif
call winrestview(l:view)
endif
endfunction

View File

@@ -31,7 +31,7 @@ if g:lsp_auto_enable
augroup END augroup END
endif endif
command! LspCodeAction call lsp#ui#vim#code_action() command! -range LspCodeAction call lsp#ui#vim#code_action()
command! LspDeclaration call lsp#ui#vim#declaration() command! LspDeclaration call lsp#ui#vim#declaration()
command! LspDefinition call lsp#ui#vim#definition() command! LspDefinition call lsp#ui#vim#definition()
command! LspDocumentSymbol call lsp#ui#vim#document_symbol() command! LspDocumentSymbol call lsp#ui#vim#document_symbol()