Skip to content

Commit 06f2c12

Browse files
authored
Add an option to use a popup menu for code actions (#1361)
For code actions, some people might find it more convenient to use a popup menu that shows up at the cursor position, instead of using the quickpick window at the bottom of the screen.
1 parent 08f0583 commit 06f2c12

File tree

7 files changed

+116
-29
lines changed

7 files changed

+116
-29
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown')
2+
let s:Window = vital#lsp#import('VS.Vim.Window')
3+
4+
function! lsp#internal#ui#popupmenu#open(opt) abort
5+
let l:Callback = remove(a:opt, 'callback')
6+
let l:items = remove(a:opt, 'items')
7+
8+
let l:items_with_shortcuts= map(l:items, {
9+
\ idx, item -> ((idx < 9) ? '['.(idx+1).'] ' : '').item
10+
\ })
11+
12+
function! Filter(id, key) abort closure
13+
if a:key >= 1 && a:key <= len(l:items)
14+
call popup_close(a:id, a:key)
15+
elseif a:key ==# "\<C-j>"
16+
call win_execute(a:id, 'normal! j')
17+
elseif a:key ==# "\<C-k>"
18+
call win_execute(a:id, 'normal! k')
19+
else
20+
return popup_filter_menu(a:id, a:key)
21+
endif
22+
23+
return v:true
24+
endfunction
25+
26+
let l:popup_opt = extend({
27+
\ 'callback': funcref('s:callback', [l:Callback]),
28+
\ 'filter': funcref('Filter'),
29+
\ }, a:opt)
30+
31+
let l:winid = popup_menu(l:items_with_shortcuts, l:popup_opt)
32+
call s:Window.do(l:winid, { -> s:Markdown.apply() })
33+
execute('doautocmd <nomodeline> User lsp_float_opened')
34+
endfunction
35+
36+
function! s:callback(callback, id, selected) abort
37+
call a:callback(a:id, a:selected)
38+
execute('doautocmd <nomodeline> User lsp_float_closed')
39+
endfunction

autoload/lsp/ui/vim.vim

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,12 +331,12 @@ function! s:handle_text_edit(server, last_command_id, type, data) abort
331331
redraw | echo 'Document formatted'
332332
endfunction
333333

334-
function! lsp#ui#vim#code_action() abort
335-
call lsp#ui#vim#code_action#do({
334+
function! lsp#ui#vim#code_action(opts) abort
335+
call lsp#ui#vim#code_action#do(extend({
336336
\ 'sync': v:false,
337337
\ 'selection': v:false,
338338
\ 'query': '',
339-
\ })
339+
\ }, a:opts))
340340
endfunction
341341

342342
function! lsp#ui#vim#code_lens() abort

autoload/lsp/ui/vim/code_action.vim

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,17 @@ endfunction
1414
" selection: v:true | v:false = Provide by CommandLine like `:'<,'>LspCodeAction`
1515
" sync: v:true | v:false = Specify enable synchronous request. Example use case is `BufWritePre`
1616
" query: string = Specify code action kind query. If query provided and then filtered code action is only one, invoke code action immediately.
17+
" ui: 'float' | 'preview'
1718
" }
1819
"
1920
function! lsp#ui#vim#code_action#do(option) abort
2021
let l:selection = get(a:option, 'selection', v:false)
2122
let l:sync = get(a:option, 'sync', v:false)
2223
let l:query = get(a:option, 'query', '')
24+
let l:ui = get(a:option, 'ui', g:lsp_code_action_ui)
25+
if empty(l:ui)
26+
let l:ui = lsp#utils#_has_popup_menu() ? 'float' : 'preview'
27+
endif
2328

2429
let l:server_names = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_code_action_provider(v:val)')
2530
if len(l:server_names) == 0
@@ -51,13 +56,13 @@ function! lsp#ui#vim#code_action#do(option) abort
5156
\ },
5257
\ },
5358
\ 'sync': l:sync,
54-
\ 'on_notification': function('s:handle_code_action', [l:ctx, l:server_name, l:command_id, l:sync, l:query, l:bufnr]),
59+
\ 'on_notification': function('s:handle_code_action', [l:ui, l:ctx, l:server_name, l:command_id, l:sync, l:query, l:bufnr]),
5560
\ })
5661
endfor
5762
echo 'Retrieving code actions ...'
5863
endfunction
5964

60-
function! s:handle_code_action(ctx, server_name, command_id, sync, query, bufnr, data) abort
65+
function! s:handle_code_action(ui, ctx, server_name, command_id, sync, query, bufnr, data) abort
6166
" Ignore old request.
6267
if a:command_id != lsp#_last_command()
6368
return
@@ -130,14 +135,34 @@ function! s:handle_code_action(ctx, server_name, command_id, sync, query, bufnr,
130135
endif
131136
call add(l:items, { 'title': l:title, 'item': l:action })
132137
endfor
133-
call lsp#internal#ui#quickpick#open({
134-
\ 'items': l:items,
135-
\ 'key': 'title',
136-
\ 'on_accept': funcref('s:accept_code_action', [a:sync, a:bufnr]),
137-
\ })
138+
139+
if lsp#utils#_has_popup_menu() && a:ui ==? 'float'
140+
call lsp#internal#ui#popupmenu#open({
141+
\ 'title': 'Code actions',
142+
\ 'items': mapnew(l:items, { idx, item -> item.title}),
143+
\ 'pos': 'topleft',
144+
\ 'line': 'cursor+1',
145+
\ 'col': 'cursor',
146+
\ 'callback': funcref('s:popup_accept_code_action', [a:sync, a:bufnr, l:items]),
147+
\ })
148+
else
149+
call lsp#internal#ui#quickpick#open({
150+
\ 'items': l:items,
151+
\ 'key': 'title',
152+
\ 'on_accept': funcref('s:quickpick_accept_code_action', [a:sync, a:bufnr]),
153+
\ })
154+
endif
155+
endfunction
156+
157+
function! s:popup_accept_code_action(sync, bufnr, items, id, selected, ...) abort
158+
if a:selected <= 0 | return | endif
159+
let l:item = a:items[a:selected - 1]['item']
160+
if s:handle_disabled_action(l:item) | return | endif
161+
call s:handle_one_code_action(l:item['server_name'], a:sync, a:bufnr, l:item['code_action'])
162+
execute('doautocmd <nomodeline> User lsp_float_closed')
138163
endfunction
139164

140-
function! s:accept_code_action(sync, bufnr, data, ...) abort
165+
function! s:quickpick_accept_code_action(sync, bufnr, data, ...) abort
141166
call lsp#internal#ui#quickpick#close()
142167
if empty(a:data['items']) | return | endif
143168
let l:selected = a:data['items'][0]['item']

autoload/lsp/utils.vim

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ function! lsp#utils#_has_highlights() abort
3434
return s:has_higlights
3535
endfunction
3636

37+
let s:has_popup_menu = exists('*popup_menu')
38+
function! lsp#utils#_has_popup_menu() abort
39+
return s:has_popup_menu
40+
endfunction
41+
3742
function! lsp#utils#is_file_uri(uri) abort
3843
return stridx(a:uri, 'file:///') == 0
3944
endfunction

autoload/lsp/utils/args.vim

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1-
function! lsp#utils#args#_parse(args, opt) abort
1+
function! lsp#utils#args#_parse(args, opt, remainder_key) abort
22
let l:result = {}
3+
let l:is_opts = v:true
4+
let l:remainder = []
35
for l:item in split(a:args, ' ')
6+
if l:item[:1] !=# '--'
7+
let l:is_opts = v:false
8+
endif
9+
10+
if l:is_opts == v:false
11+
call add(l:remainder, l:item)
12+
continue
13+
endif
14+
415
let l:parts = split(l:item, '=')
516
let l:key = l:parts[0]
617
let l:value = get(l:parts, 1, '')
718
let l:key = l:key[2:]
19+
820
if has_key(a:opt, l:key)
921
if has_key(a:opt[l:key], 'type')
1022
let l:type = a:opt[l:key]['type']
@@ -21,5 +33,10 @@ function! lsp#utils#args#_parse(args, opt) abort
2133
endif
2234
let l:result[l:key] = l:value
2335
endfor
36+
37+
if a:remainder_key != v:null
38+
let l:result[a:remainder_key] = join(l:remainder)
39+
endif
40+
2441
return l:result
2542
endfunction

doc/vim-lsp.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,15 +1561,15 @@ LspCallHierarchyOutgoing *:LspCallHierarchyOutgoing*
15611561

15621562
Find outgoing call hierarchy for the symbol under cursor.
15631563

1564-
LspCodeAction [{CodeActionKind}] *:LspCodeAction*
1564+
LspCodeAction [--ui=float|preview] [{CodeActionKind}] *:LspCodeAction*
15651565

15661566
Gets a list of possible commands that can be applied to a file so it can be
15671567
fixed (quick fix).
15681568

15691569
If the optional {CodeActionKind} specified, will invoke code action
15701570
immediately when matched code action is one only.
15711571

1572-
LspCodeActionSync [{CodeActionKind}] *:LspCodeActionSync*
1572+
LspCodeActionSync [--ui=float|preview] [{CodeActionKind}] *:LspCodeActionSync*
15731573

15741574
Same as |:LspCodeAction| but synchronous. Useful when running |:autocmd|
15751575
commands such as organize imports before save.

plugin/lsp.vim

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ let g:lsp_work_done_progress_enabled = get(g:, 'lsp_work_done_progress_enabled',
7474
let g:lsp_untitled_buffer_enabled = get(g:, 'lsp_untitled_buffer_enabled', 1)
7575
let g:lsp_inlay_hints_enabled = get(g:, 'lsp_inlay_hints_enabled', 0)
7676
let g:lsp_inlay_hints_delay = get(g:, 'lsp_inlay_hints_delay', 350)
77+
let g:lsp_code_action_ui = get(g:, 'lsp_code_action_ui', 'preview')
7778

7879
let g:lsp_get_supported_capabilities = get(g:, 'lsp_get_supported_capabilities', [function('lsp#default_get_supported_capabilities')])
7980

@@ -89,16 +90,14 @@ endif
8990
command! LspAddTreeCallHierarchyIncoming call lsp#ui#vim#add_tree_call_hierarchy_incoming()
9091
command! LspCallHierarchyIncoming call lsp#ui#vim#call_hierarchy_incoming({})
9192
command! LspCallHierarchyOutgoing call lsp#ui#vim#call_hierarchy_outgoing()
92-
command! -range -nargs=* -complete=customlist,lsp#ui#vim#code_action#complete LspCodeAction call lsp#ui#vim#code_action#do({
93-
\ 'sync': v:false,
94-
\ 'selection': <range> != 0,
95-
\ 'query': '<args>'
96-
\ })
97-
command! -range -nargs=* -complete=customlist,lsp#ui#vim#code_action#complete LspCodeActionSync call lsp#ui#vim#code_action#do({
98-
\ 'sync': v:true,
99-
\ 'selection': <range> != 0,
100-
\ 'query': '<args>'
101-
\ })
93+
command! -range -nargs=* -complete=customlist,lsp#ui#vim#code_action#complete LspCodeAction call lsp#ui#vim#code_action#do(
94+
\ extend({ 'sync': v:false, 'selection': <range> != 0 }, lsp#utils#args#_parse(<q-args>, {
95+
\ 'ui': { 'type': type('') },
96+
\ }, 'query')))
97+
command! -range -nargs=* -complete=customlist,lsp#ui#vim#code_action#complete LspCodeActionSync call lsp#ui#vim#code_action#do(
98+
\ extend({ 'sync': v:true, 'selection': <range> != 0 }, lsp#utils#args#_parse(<q-args>, {
99+
\ 'ui': { 'type': type('') },
100+
\ }, 'query')))
102101
command! LspCodeLens call lsp#ui#vim#code_lens#do({})
103102
command! LspDeclaration call lsp#ui#vim#declaration(0, <q-mods>)
104103
command! LspPeekDeclaration call lsp#ui#vim#declaration(1)
@@ -109,11 +108,11 @@ command! LspDocumentSymbolSearch call lsp#internal#document_symbol#search#do({})
109108
command! -nargs=? LspDocumentDiagnostics call lsp#internal#diagnostics#document_diagnostics_command#do(
110109
\ extend({}, lsp#utils#args#_parse(<q-args>, {
111110
\ 'buffers': {'type': type('')},
112-
\ })))
111+
\ }, v:null)))
113112
command! -nargs=? -complete=customlist,lsp#utils#empty_complete LspHover call lsp#internal#document_hover#under_cursor#do(
114113
\ extend({}, lsp#utils#args#_parse(<q-args>, {
115114
\ 'ui': { 'type': type('') },
116-
\ })))
115+
\ }, v:null)))
117116
command! -nargs=* LspNextError call lsp#internal#diagnostics#movement#_next_error(<f-args>)
118117
command! -nargs=* LspPreviousError call lsp#internal#diagnostics#movement#_previous_error(<f-args>)
119118
command! -nargs=* LspNextWarning call lsp#internal#diagnostics#movement#_next_warning(<f-args>)
@@ -132,13 +131,13 @@ command! -range -nargs=? LspDocumentFormatSync call lsp#internal#document_format
132131
\ extend({'bufnr': bufnr('%'), 'sync': 1 }, lsp#utils#args#_parse(<q-args>, {
133132
\ 'timeout': {'type': type(0)},
134133
\ 'sleep': {'type': type(0)},
135-
\ })))
134+
\ }, v:null)))
136135
command! -range LspDocumentRangeFormat call lsp#internal#document_range_formatting#format({ 'bufnr': bufnr('%') })
137136
command! -range -nargs=? LspDocumentRangeFormatSync call lsp#internal#document_range_formatting#format(
138137
\ extend({'bufnr': bufnr('%'), 'sync': 1 }, lsp#utils#args#_parse(<q-args>, {
139138
\ 'timeout': {'type': type(0)},
140139
\ 'sleep': {'type': type(0)},
141-
\ })))
140+
\ }, v:null)))
142141
command! LspImplementation call lsp#ui#vim#implementation(0, <q-mods>)
143142
command! LspPeekImplementation call lsp#ui#vim#implementation(1)
144143
command! -nargs=0 LspStatus call lsp#print_server_status()
@@ -153,7 +152,9 @@ command! -nargs=0 LspSemanticTokenModifiers echo lsp#internal#semantic#get_token
153152

154153
nnoremap <silent> <plug>(lsp-call-hierarchy-incoming) :<c-u>call lsp#ui#vim#call_hierarchy_incoming({})<cr>
155154
nnoremap <silent> <plug>(lsp-call-hierarchy-outgoing) :<c-u>call lsp#ui#vim#call_hierarchy_outgoing()<cr>
156-
nnoremap <silent> <plug>(lsp-code-action) :<c-u>call lsp#ui#vim#code_action()<cr>
155+
nnoremap <silent> <plug>(lsp-code-action) :<c-u>call lsp#ui#vim#code_action({})<cr>
156+
nnoremap <silent> <plug>(lsp-code-action-float) :<c-u>call lsp#ui#vim#code_action({ 'ui': 'float' })<cr>
157+
nnoremap <silent> <plug>(lsp-code-action-preview) :<c-u>call lsp#ui#vim#code_action({ 'ui': 'preview' })<cr>
157158
nnoremap <silent> <plug>(lsp-code-lens) :<c-u>call lsp#ui#vim#code_lens()<cr>
158159
nnoremap <silent> <plug>(lsp-declaration) :<c-u>call lsp#ui#vim#declaration(0)<cr>
159160
nnoremap <silent> <plug>(lsp-peek-declaration) :<c-u>call lsp#ui#vim#declaration(1)<cr>

0 commit comments

Comments
 (0)