Skip to content

Commit d460637

Browse files
committed
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.
1 parent b68fdad commit d460637

File tree

9 files changed

+135
-14
lines changed

9 files changed

+135
-14
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 0.2.8-dev
1+
# 0.2.8
22

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

1315
# 0.2.7
1416

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ let g:lsc_auto_map = v:true " Use defaults
4646
let g:lsc_auto_map = {
4747
\ 'GoToDefinition': '<C-]>',
4848
\ 'FindReferences': 'gr',
49+
\ 'FindCodeActions': 'ga',
4950
\ 'ShowHover': 'K',
5051
\ 'Completion': 'completefunc',
5152
\}
@@ -118,3 +119,14 @@ While the cursor is on any identifier call `LSClientShowHover` (`K` if using the
118119
default mappings) to request hover text and show it in a preview window.
119120
Override the direction of the split by setting `g:lsc_preview_split_direction`
120121
to either `'below'` or `'above'`.
122+
123+
### Code Actions (experimental)
124+
125+
While this is still experimental it is opt-in. Add
126+
`let g:lsc_enable_apply_edit = v:true` to allow edits to files (since these are
127+
the most likely result of code actions). Call `LSClientFindCodeActions` (`ga` if
128+
using the default mappings) to look for code actions available at the cursor
129+
location.
130+
131+
Support is very limited for now. Edits can only be applied in the active buffer
132+
to prevent.

autoload/lsc/config.vim

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
let s:default_maps = {
22
\ 'GoToDefinition': '<C-]>',
33
\ 'FindReferences': 'gr',
4+
\ 'FindCodeActions': 'ga',
45
\ 'ShowHover': 'K',
56
\ 'Completion': 'completefunc',
67
\}
@@ -20,7 +21,12 @@ function! lsc#config#mapKeys() abort
2021
return
2122
endif
2223

23-
for command in ['GoToDefinition', 'FindReferences', 'ShowHover']
24+
for command in [
25+
\ 'GoToDefinition',
26+
\ 'FindReferences',
27+
\ 'ShowHover',
28+
\ 'FindCodeActions',
29+
\]
2430
if has_key(maps, command)
2531
execute 'nnoremap <buffer>'.maps[command].' :LSClient'.command.'<CR>'
2632
endif

autoload/lsc/dispatch.vim

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
" Handle messages received from the server.
2-
function! lsc#dispatch#message(message) abort
2+
function! lsc#dispatch#message(server, message) abort
33
if has_key(a:message, 'method')
44
if a:message['method'] ==? 'textDocument/publishDiagnostics'
55
let params = a:message['params']
@@ -11,6 +11,14 @@ function! lsc#dispatch#message(message) abort
1111
elseif a:message['method'] ==? 'window/logMessage'
1212
let params = a:message['params']
1313
call lsc#message#log(params['message'], params['type'])
14+
elseif a:message['method'] ==? 'workspace/applyEdit'
15+
let params = a:message['params']
16+
let applied = lsc#edit#apply(params)
17+
if has_key(a:message, 'id')
18+
let id = a:message['id']
19+
let response = {'applied': applied}
20+
call a:server.send(lsc#protocol#formatResponse(id, response))
21+
endif
1422
else
1523
echom 'Got notification: '.a:message['method'].
1624
\ ' params: '.string(a:message['params'])

autoload/lsc/edit.vim

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,26 @@ function! lsc#edit#findCodeActions() abort
1010
let find_actions_id = s:find_actions_id
1111
function! SelectAction(result) closure abort
1212
if !s:isFindActionsValid(old_pos, find_actions_id)
13-
echom 'CodeActions skipped'
13+
call lsc#message#show('Actions ignored')
1414
return
1515
endif
16-
if type(a:result) == v:t_none ||
17-
\ (type(a:result) == v:t_list && len(a:result) == 0)
16+
if type(a:result) != v:t_list || len(a:result) == 0
1817
call lsc#message#show('No actions available')
18+
return
19+
endif
20+
let choices = ['Choose an action:']
21+
let idx = 0
22+
while idx < len(a:result)
23+
call add(choices, string(idx+1).' - '.a:result[idx]['title'])
24+
let idx += 1
25+
endwhile
26+
let choice = inputlist(choices)
27+
if choice > 0
28+
call lsc#server#userCall('workspace/executeCommand',
29+
\ {'command': a:result[choice - 1]['command'],
30+
\ 'arguments': a:result[choice - 1]['arguments']},
31+
\ {_->0})
1932
endif
20-
for action in a:result
21-
echom 'I found an action: '.action['title']
22-
endfor
2333
endfunction
2434
call lsc#server#userCall('textDocument/codeAction',
2535
\ s:TextDocumentRangeParams(), function('SelectAction'))
@@ -39,3 +49,61 @@ function! s:isFindActionsValid(old_pos, find_actions_id) abort
3949
return a:find_actions_id == s:find_actions_id &&
4050
\ a:old_pos == getcurpos()
4151
endfunction
52+
53+
" Applies a workspace edit and returns `v:true` if it was successful.
54+
function! lsc#edit#apply(params) abort
55+
if !exists('g:lsc_enable_apply_edit')
56+
\ || !g:lsc_enable_apply_edit
57+
\ || !has_key(a:params.edit, 'changes')
58+
return v:false
59+
endif
60+
let changes = a:params.edit.changes
61+
" Only applying changes in open files for now
62+
for uri in keys(changes)
63+
if lsc#uri#documentPath(uri) != expand('%:p')
64+
call lsc#message#error('Can only apply edits in the current buffer')
65+
return v:false
66+
endif
67+
endfor
68+
for [uri, edits] in items(changes)
69+
for edit in edits
70+
" Expect edit is in current buffer
71+
call s:Apply(edit)
72+
endfor
73+
endfor
74+
return v:true
75+
endfunction
76+
77+
" Apply a `TextEdit` to the current buffer.
78+
function! s:Apply(edit) abort
79+
let old_paste = &paste
80+
set paste
81+
if s:IsEmptyRange(a:edit.range)
82+
let command = printf('%dG%d|i%s',
83+
\ a:edit.range.start.line + 1,
84+
\ a:edit.range.start.character + 1,
85+
\ a:edit.newText
86+
\)
87+
else
88+
" `back` handles end-exclusive range
89+
let back = 'h'
90+
if a:edit.range.end.character == 0
91+
let back = 'k$'
92+
endif
93+
let command = printf('%dG%d|v%dG%d|%sc%s',
94+
\ a:edit.range.start.line + 1,
95+
\ a:edit.range.start.character + 1,
96+
\ a:edit.range.end.line + 1,
97+
\ a:edit.range.end.character + 1,
98+
\ back,
99+
\ a:edit.newText
100+
\)
101+
endif
102+
execute 'normal!' command
103+
let &paste = old_paste
104+
endfunction
105+
106+
function! s:IsEmptyRange(range) abort
107+
return a:range.start.line == a:range.end.line &&
108+
\ a:range.start.character == a:range.end.character
109+
endfunction

autoload/lsc/protocol.vim

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ function! lsc#protocol#formatNotification(method, params) abort
2323
return s:Format(a:method, a:params, v:null)
2424
endfunction
2525

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

2731
function! s:Format(method, params, id) abort
2832
let message = {'method': a:method}
@@ -69,7 +73,7 @@ function! s:consumeMessage(server) abort
6973
if exists('l:content')
7074
call lsc#util#shift(a:server.messages, 10, content)
7175
try
72-
call lsc#dispatch#message(content)
76+
call lsc#dispatch#message(a:server, content)
7377
catch
7478
call lsc#message#error('Error dispatching message: '.string(v:exception))
7579
endtry

autoload/lsc/server.vim

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ function! s:Start(server) abort
134134
endif
135135
let params = {'processId': getpid(),
136136
\ 'rootUri': lsc#uri#documentUri(getcwd()),
137-
\ 'capabilities': s:client_capabilities,
137+
\ 'capabilities': s:ClientCapabilities(),
138138
\ 'trace': trace_level
139139
\}
140140
call lsc#server#call(&filetype, 'initialize',
@@ -224,9 +224,16 @@ function! lsc#server#callback(channel, message) abort
224224
call lsc#protocol#consumeMessage(server_info)
225225
endfunction
226226

227-
" Supports no workspace capabilities - missing value means no support
228-
let s:client_capabilities = {
229-
\ 'workspace': {},
227+
" Missing value means no support
228+
function! s:ClientCapabilities() abort
229+
let applyEdit = v:false
230+
if exists('g:lsc_enable_apply_edit') && g:lsc_enable_apply_edit
231+
let applyEdit = v:true
232+
endif
233+
return {
234+
\ 'workspace': {
235+
\ 'applyEdit': applyEdit,
236+
\ },
230237
\ 'textDocument': {
231238
\ 'synchronization': {
232239
\ 'willSave': v:false,
@@ -239,6 +246,7 @@ let s:client_capabilities = {
239246
\ 'definition': {'dynamicRegistration': v:false},
240247
\ }
241248
\}
249+
endfunction
242250

243251
function! lsc#server#filetypeActive(filetype) abort
244252
let server = s:servers[g:lsc_servers_by_filetype[a:filetype]]

doc/lsc.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ If the preview window is not visible it will |split| the window and size it no
7575
bigger than |previewheight|. Override the direction of the split by setting
7676
|g:lsc_preview_split_direction| to either |above| or |below|.
7777

78+
*:LSClientFindCodeActions*
79+
Check for available actions at the cursor's position and display them in a
80+
menu. Sends a "textDocument/codeAction" request to get the available choices,
81+
and if one is selected sends a "workspace/executeCommand". Typically actions
82+
end up triggering workspace edits so this command is likely only useful if
83+
|g:lsc_enable_apply_edit| is set to `v:true`.
84+
7885
*:LSClientRestartServer*
7986
Sends requests to the server for the current filetype to "shutdown" and
8087
"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
194201
buffer can cause large changes to be sent, but in most cases the messages will
195202
be smaller than with full syncs.
196203

204+
*lsc-configure-edits*
205+
*g:lsc_enable_apply_edit*
206+
By default the client will not modify any buffer in response to
207+
`workspace/applyEdit` calls. To enable edits set to `v:true`.
208+
197209
AUTOCMDS *lsc-autocmds*
198210

199211
*autocmd-LSCAutocomplete*

plugin/lsc.vim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ endif
1414
command! LSClientGoToDefinition call lsc#reference#goToDefinition()
1515
command! LSClientFindReferences call lsc#reference#findReferences()
1616
command! LSClientShowHover call lsc#reference#hover()
17+
command! LSClientFindCodeActions call lsc#edit#findCodeActions()
1718
command! LSClientRestartServer call <SID>IfEnabled('lsc#server#restart')
1819
command! LSClientDisable call lsc#server#disable()
1920
command! LSClientEnable call lsc#server#enable()

0 commit comments

Comments
 (0)