Skip to content

Commit

Permalink
Add LSClientFindCodeActions
Browse files Browse the repository at this point in the history
This is very conservative for now. Edits must be enabled with a global
variable since they are risky, and they only will apply in the current
buffer.

- Allow choosing a code action following a call to `findCodeActions` and
  then call `workspace/executeCommand`
- Add dispatching for `workspace/applyEdit` to a function which selects
  the changed range and replaces the text.
- Take a server argument to dispatch so that requests can send back a
  resonse.
- Add mapping to `ga`. The default behavior is to print the ascii code
  of the character under the cursor.
- Update the client capabilities to indicate applyEdit if it is enabled.

Future Work:
- Add support for edits in other buffers.
- Allow a visual mode call which sends a range rather than a single
  character under the cursor.
  • Loading branch information
natebosch committed Dec 23, 2017
1 parent b68fdad commit d460637
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 14 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 0.2.8-dev
# 0.2.8

- Don't track files which are not `modifiable`.
- Bug Fix: Fix jumping from quickfix item to files under home directory.
Expand All @@ -9,6 +9,8 @@
- Add support for overriding the `params` for certain methods.
- Bug Fix: Correct paths on Windows.
- Bug Fix: Allow restarting a server which failed to start initially.
- Add experimental support for `textDocument/codeActions` and
`workspace/applyEdit`

# 0.2.7

Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ let g:lsc_auto_map = v:true " Use defaults
let g:lsc_auto_map = {
\ 'GoToDefinition': '<C-]>',
\ 'FindReferences': 'gr',
\ 'FindCodeActions': 'ga',
\ 'ShowHover': 'K',
\ 'Completion': 'completefunc',
\}
Expand Down Expand Up @@ -118,3 +119,14 @@ While the cursor is on any identifier call `LSClientShowHover` (`K` if using the
default mappings) to request hover text and show it in a preview window.
Override the direction of the split by setting `g:lsc_preview_split_direction`
to either `'below'` or `'above'`.

### Code Actions (experimental)

While this is still experimental it is opt-in. Add
`let g:lsc_enable_apply_edit = v:true` to allow edits to files (since these are
the most likely result of code actions). Call `LSClientFindCodeActions` (`ga` if
using the default mappings) to look for code actions available at the cursor
location.

Support is very limited for now. Edits can only be applied in the active buffer
to prevent.
8 changes: 7 additions & 1 deletion autoload/lsc/config.vim
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
let s:default_maps = {
\ 'GoToDefinition': '<C-]>',
\ 'FindReferences': 'gr',
\ 'FindCodeActions': 'ga',
\ 'ShowHover': 'K',
\ 'Completion': 'completefunc',
\}
Expand All @@ -20,7 +21,12 @@ function! lsc#config#mapKeys() abort
return
endif

for command in ['GoToDefinition', 'FindReferences', 'ShowHover']
for command in [
\ 'GoToDefinition',
\ 'FindReferences',
\ 'ShowHover',
\ 'FindCodeActions',
\]
if has_key(maps, command)
execute 'nnoremap <buffer>'.maps[command].' :LSClient'.command.'<CR>'
endif
Expand Down
10 changes: 9 additions & 1 deletion autoload/lsc/dispatch.vim
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
" Handle messages received from the server.
function! lsc#dispatch#message(message) abort
function! lsc#dispatch#message(server, message) abort
if has_key(a:message, 'method')
if a:message['method'] ==? 'textDocument/publishDiagnostics'
let params = a:message['params']
Expand All @@ -11,6 +11,14 @@ function! lsc#dispatch#message(message) abort
elseif a:message['method'] ==? 'window/logMessage'
let params = a:message['params']
call lsc#message#log(params['message'], params['type'])
elseif a:message['method'] ==? 'workspace/applyEdit'
let params = a:message['params']
let applied = lsc#edit#apply(params)
if has_key(a:message, 'id')
let id = a:message['id']
let response = {'applied': applied}
call a:server.send(lsc#protocol#formatResponse(id, response))
endif
else
echom 'Got notification: '.a:message['method'].
\ ' params: '.string(a:message['params'])
Expand Down
80 changes: 74 additions & 6 deletions autoload/lsc/edit.vim
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,26 @@ function! lsc#edit#findCodeActions() abort
let find_actions_id = s:find_actions_id
function! SelectAction(result) closure abort
if !s:isFindActionsValid(old_pos, find_actions_id)
echom 'CodeActions skipped'
call lsc#message#show('Actions ignored')
return
endif
if type(a:result) == v:t_none ||
\ (type(a:result) == v:t_list && len(a:result) == 0)
if type(a:result) != v:t_list || len(a:result) == 0
call lsc#message#show('No actions available')
return
endif
let choices = ['Choose an action:']
let idx = 0
while idx < len(a:result)
call add(choices, string(idx+1).' - '.a:result[idx]['title'])
let idx += 1
endwhile
let choice = inputlist(choices)
if choice > 0
call lsc#server#userCall('workspace/executeCommand',
\ {'command': a:result[choice - 1]['command'],
\ 'arguments': a:result[choice - 1]['arguments']},
\ {_->0})
endif
for action in a:result
echom 'I found an action: '.action['title']
endfor
endfunction
call lsc#server#userCall('textDocument/codeAction',
\ s:TextDocumentRangeParams(), function('SelectAction'))
Expand All @@ -39,3 +49,61 @@ function! s:isFindActionsValid(old_pos, find_actions_id) abort
return a:find_actions_id == s:find_actions_id &&
\ a:old_pos == getcurpos()
endfunction

" Applies a workspace edit and returns `v:true` if it was successful.
function! lsc#edit#apply(params) abort
if !exists('g:lsc_enable_apply_edit')
\ || !g:lsc_enable_apply_edit
\ || !has_key(a:params.edit, 'changes')
return v:false
endif
let changes = a:params.edit.changes
" Only applying changes in open files for now
for uri in keys(changes)
if lsc#uri#documentPath(uri) != expand('%:p')
call lsc#message#error('Can only apply edits in the current buffer')
return v:false
endif
endfor
for [uri, edits] in items(changes)
for edit in edits
" Expect edit is in current buffer
call s:Apply(edit)
endfor
endfor
return v:true
endfunction

" Apply a `TextEdit` to the current buffer.
function! s:Apply(edit) abort
let old_paste = &paste
set paste
if s:IsEmptyRange(a:edit.range)
let command = printf('%dG%d|i%s',
\ a:edit.range.start.line + 1,
\ a:edit.range.start.character + 1,
\ a:edit.newText
\)
else
" `back` handles end-exclusive range
let back = 'h'
if a:edit.range.end.character == 0
let back = 'k$'
endif
let command = printf('%dG%d|v%dG%d|%sc%s',
\ a:edit.range.start.line + 1,
\ a:edit.range.start.character + 1,
\ a:edit.range.end.line + 1,
\ a:edit.range.end.character + 1,
\ back,
\ a:edit.newText
\)
endif
execute 'normal!' command
let &paste = old_paste
endfunction

function! s:IsEmptyRange(range) abort
return a:range.start.line == a:range.end.line &&
\ a:range.start.character == a:range.end.character
endfunction
6 changes: 5 additions & 1 deletion autoload/lsc/protocol.vim
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ function! lsc#protocol#formatNotification(method, params) abort
return s:Format(a:method, a:params, v:null)
endfunction

" Create a dictionary for the response to a call.
function! lsc#protocol#formatResponse(id, result) abort
return {'id': a:id, 'result': a:result}
endfunction

function! s:Format(method, params, id) abort
let message = {'method': a:method}
Expand Down Expand Up @@ -69,7 +73,7 @@ function! s:consumeMessage(server) abort
if exists('l:content')
call lsc#util#shift(a:server.messages, 10, content)
try
call lsc#dispatch#message(content)
call lsc#dispatch#message(a:server, content)
catch
call lsc#message#error('Error dispatching message: '.string(v:exception))
endtry
Expand Down
16 changes: 12 additions & 4 deletions autoload/lsc/server.vim
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ function! s:Start(server) abort
endif
let params = {'processId': getpid(),
\ 'rootUri': lsc#uri#documentUri(getcwd()),
\ 'capabilities': s:client_capabilities,
\ 'capabilities': s:ClientCapabilities(),
\ 'trace': trace_level
\}
call lsc#server#call(&filetype, 'initialize',
Expand Down Expand Up @@ -224,9 +224,16 @@ function! lsc#server#callback(channel, message) abort
call lsc#protocol#consumeMessage(server_info)
endfunction

" Supports no workspace capabilities - missing value means no support
let s:client_capabilities = {
\ 'workspace': {},
" Missing value means no support
function! s:ClientCapabilities() abort
let applyEdit = v:false
if exists('g:lsc_enable_apply_edit') && g:lsc_enable_apply_edit
let applyEdit = v:true
endif
return {
\ 'workspace': {
\ 'applyEdit': applyEdit,
\ },
\ 'textDocument': {
\ 'synchronization': {
\ 'willSave': v:false,
Expand All @@ -239,6 +246,7 @@ let s:client_capabilities = {
\ 'definition': {'dynamicRegistration': v:false},
\ }
\}
endfunction

function! lsc#server#filetypeActive(filetype) abort
let server = s:servers[g:lsc_servers_by_filetype[a:filetype]]
Expand Down
12 changes: 12 additions & 0 deletions doc/lsc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ If the preview window is not visible it will |split| the window and size it no
bigger than |previewheight|. Override the direction of the split by setting
|g:lsc_preview_split_direction| to either |above| or |below|.

*:LSClientFindCodeActions*
Check for available actions at the cursor's position and display them in a
menu. Sends a "textDocument/codeAction" request to get the available choices,
and if one is selected sends a "workspace/executeCommand". Typically actions
end up triggering workspace edits so this command is likely only useful if
|g:lsc_enable_apply_edit| is set to `v:true`.

*:LSClientRestartServer*
Sends requests to the server for the current filetype to "shutdown" and
"exit", and after the process exits, restarts it. If the server is
Expand Down Expand Up @@ -194,6 +201,11 @@ edits which simultaneously change content near the beginning and end of the
buffer can cause large changes to be sent, but in most cases the messages will
be smaller than with full syncs.

*lsc-configure-edits*
*g:lsc_enable_apply_edit*
By default the client will not modify any buffer in response to
`workspace/applyEdit` calls. To enable edits set to `v:true`.

AUTOCMDS *lsc-autocmds*

*autocmd-LSCAutocomplete*
Expand Down
1 change: 1 addition & 0 deletions plugin/lsc.vim
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ endif
command! LSClientGoToDefinition call lsc#reference#goToDefinition()
command! LSClientFindReferences call lsc#reference#findReferences()
command! LSClientShowHover call lsc#reference#hover()
command! LSClientFindCodeActions call lsc#edit#findCodeActions()
command! LSClientRestartServer call <SID>IfEnabled('lsc#server#restart')
command! LSClientDisable call lsc#server#disable()
command! LSClientEnable call lsc#server#enable()
Expand Down

0 comments on commit d460637

Please sign in to comment.