From d46063730c0841f5211afd1793788597c2a26dd1 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Sat, 23 Dec 2017 14:33:35 -0800 Subject: [PATCH] Add LSClientFindCodeActions 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. --- CHANGELOG.md | 4 +- README.md | 12 ++++++ autoload/lsc/config.vim | 8 +++- autoload/lsc/dispatch.vim | 10 ++++- autoload/lsc/edit.vim | 80 ++++++++++++++++++++++++++++++++++++--- autoload/lsc/protocol.vim | 6 ++- autoload/lsc/server.vim | 16 ++++++-- doc/lsc.txt | 12 ++++++ plugin/lsc.vim | 1 + 9 files changed, 135 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e8c72b..3559ea06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. @@ -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 diff --git a/README.md b/README.md index e5aa8d81..b0d34f1a 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ let g:lsc_auto_map = v:true " Use defaults let g:lsc_auto_map = { \ 'GoToDefinition': '', \ 'FindReferences': 'gr', + \ 'FindCodeActions': 'ga', \ 'ShowHover': 'K', \ 'Completion': 'completefunc', \} @@ -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. diff --git a/autoload/lsc/config.vim b/autoload/lsc/config.vim index 84582be6..87970051 100644 --- a/autoload/lsc/config.vim +++ b/autoload/lsc/config.vim @@ -1,6 +1,7 @@ let s:default_maps = { \ 'GoToDefinition': '', \ 'FindReferences': 'gr', + \ 'FindCodeActions': 'ga', \ 'ShowHover': 'K', \ 'Completion': 'completefunc', \} @@ -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 '.maps[command].' :LSClient'.command.'' endif diff --git a/autoload/lsc/dispatch.vim b/autoload/lsc/dispatch.vim index 0a443a14..baf62374 100644 --- a/autoload/lsc/dispatch.vim +++ b/autoload/lsc/dispatch.vim @@ -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'] @@ -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']) diff --git a/autoload/lsc/edit.vim b/autoload/lsc/edit.vim index 64a84457..125d3baf 100644 --- a/autoload/lsc/edit.vim +++ b/autoload/lsc/edit.vim @@ -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')) @@ -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 diff --git a/autoload/lsc/protocol.vim b/autoload/lsc/protocol.vim index b24a3990..7a77f0bc 100644 --- a/autoload/lsc/protocol.vim +++ b/autoload/lsc/protocol.vim @@ -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} @@ -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 diff --git a/autoload/lsc/server.vim b/autoload/lsc/server.vim index 3db7d601..728d56f1 100644 --- a/autoload/lsc/server.vim +++ b/autoload/lsc/server.vim @@ -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', @@ -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, @@ -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]] diff --git a/doc/lsc.txt b/doc/lsc.txt index f0cb2878..dc50622c 100644 --- a/doc/lsc.txt +++ b/doc/lsc.txt @@ -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 @@ -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* diff --git a/plugin/lsc.vim b/plugin/lsc.vim index 25f9e774..92218c12 100644 --- a/plugin/lsc.vim +++ b/plugin/lsc.vim @@ -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 IfEnabled('lsc#server#restart') command! LSClientDisable call lsc#server#disable() command! LSClientEnable call lsc#server#enable()