Skip to content

Commit

Permalink
Add an option to use a popup menu for code actions (#1361)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dotdash authored Oct 15, 2022
1 parent 08f0583 commit 06f2c12
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 29 deletions.
39 changes: 39 additions & 0 deletions autoload/lsp/internal/ui/popupmenu.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown')
let s:Window = vital#lsp#import('VS.Vim.Window')

function! lsp#internal#ui#popupmenu#open(opt) abort
let l:Callback = remove(a:opt, 'callback')
let l:items = remove(a:opt, 'items')

let l:items_with_shortcuts= map(l:items, {
\ idx, item -> ((idx < 9) ? '['.(idx+1).'] ' : '').item
\ })

function! Filter(id, key) abort closure
if a:key >= 1 && a:key <= len(l:items)
call popup_close(a:id, a:key)
elseif a:key ==# "\<C-j>"
call win_execute(a:id, 'normal! j')
elseif a:key ==# "\<C-k>"
call win_execute(a:id, 'normal! k')
else
return popup_filter_menu(a:id, a:key)
endif

return v:true
endfunction

let l:popup_opt = extend({
\ 'callback': funcref('s:callback', [l:Callback]),
\ 'filter': funcref('Filter'),
\ }, a:opt)

let l:winid = popup_menu(l:items_with_shortcuts, l:popup_opt)
call s:Window.do(l:winid, { -> s:Markdown.apply() })
execute('doautocmd <nomodeline> User lsp_float_opened')
endfunction

function! s:callback(callback, id, selected) abort
call a:callback(a:id, a:selected)
execute('doautocmd <nomodeline> User lsp_float_closed')
endfunction
6 changes: 3 additions & 3 deletions autoload/lsp/ui/vim.vim
Original file line number Diff line number Diff line change
Expand Up @@ -331,12 +331,12 @@ function! s:handle_text_edit(server, last_command_id, type, data) abort
redraw | echo 'Document formatted'
endfunction

function! lsp#ui#vim#code_action() abort
call lsp#ui#vim#code_action#do({
function! lsp#ui#vim#code_action(opts) abort
call lsp#ui#vim#code_action#do(extend({
\ 'sync': v:false,
\ 'selection': v:false,
\ 'query': '',
\ })
\ }, a:opts))
endfunction

function! lsp#ui#vim#code_lens() abort
Expand Down
41 changes: 33 additions & 8 deletions autoload/lsp/ui/vim/code_action.vim
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,17 @@ endfunction
" selection: v:true | v:false = Provide by CommandLine like `:'<,'>LspCodeAction`
" sync: v:true | v:false = Specify enable synchronous request. Example use case is `BufWritePre`
" query: string = Specify code action kind query. If query provided and then filtered code action is only one, invoke code action immediately.
" ui: 'float' | 'preview'
" }
"
function! lsp#ui#vim#code_action#do(option) abort
let l:selection = get(a:option, 'selection', v:false)
let l:sync = get(a:option, 'sync', v:false)
let l:query = get(a:option, 'query', '')
let l:ui = get(a:option, 'ui', g:lsp_code_action_ui)
if empty(l:ui)
let l:ui = lsp#utils#_has_popup_menu() ? 'float' : 'preview'
endif

let l:server_names = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_code_action_provider(v:val)')
if len(l:server_names) == 0
Expand Down Expand Up @@ -51,13 +56,13 @@ function! lsp#ui#vim#code_action#do(option) abort
\ },
\ },
\ 'sync': l:sync,
\ 'on_notification': function('s:handle_code_action', [l:ctx, l:server_name, l:command_id, l:sync, l:query, l:bufnr]),
\ 'on_notification': function('s:handle_code_action', [l:ui, l:ctx, l:server_name, l:command_id, l:sync, l:query, l:bufnr]),
\ })
endfor
echo 'Retrieving code actions ...'
endfunction

function! s:handle_code_action(ctx, server_name, command_id, sync, query, bufnr, data) abort
function! s:handle_code_action(ui, ctx, server_name, command_id, sync, query, bufnr, data) abort
" Ignore old request.
if a:command_id != lsp#_last_command()
return
Expand Down Expand Up @@ -130,14 +135,34 @@ function! s:handle_code_action(ctx, server_name, command_id, sync, query, bufnr,
endif
call add(l:items, { 'title': l:title, 'item': l:action })
endfor
call lsp#internal#ui#quickpick#open({
\ 'items': l:items,
\ 'key': 'title',
\ 'on_accept': funcref('s:accept_code_action', [a:sync, a:bufnr]),
\ })

if lsp#utils#_has_popup_menu() && a:ui ==? 'float'
call lsp#internal#ui#popupmenu#open({
\ 'title': 'Code actions',
\ 'items': mapnew(l:items, { idx, item -> item.title}),
\ 'pos': 'topleft',
\ 'line': 'cursor+1',
\ 'col': 'cursor',
\ 'callback': funcref('s:popup_accept_code_action', [a:sync, a:bufnr, l:items]),
\ })
else
call lsp#internal#ui#quickpick#open({
\ 'items': l:items,
\ 'key': 'title',
\ 'on_accept': funcref('s:quickpick_accept_code_action', [a:sync, a:bufnr]),
\ })
endif
endfunction

function! s:popup_accept_code_action(sync, bufnr, items, id, selected, ...) abort
if a:selected <= 0 | return | endif
let l:item = a:items[a:selected - 1]['item']
if s:handle_disabled_action(l:item) | return | endif
call s:handle_one_code_action(l:item['server_name'], a:sync, a:bufnr, l:item['code_action'])
execute('doautocmd <nomodeline> User lsp_float_closed')
endfunction

function! s:accept_code_action(sync, bufnr, data, ...) abort
function! s:quickpick_accept_code_action(sync, bufnr, data, ...) abort
call lsp#internal#ui#quickpick#close()
if empty(a:data['items']) | return | endif
let l:selected = a:data['items'][0]['item']
Expand Down
5 changes: 5 additions & 0 deletions autoload/lsp/utils.vim
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ function! lsp#utils#_has_highlights() abort
return s:has_higlights
endfunction

let s:has_popup_menu = exists('*popup_menu')
function! lsp#utils#_has_popup_menu() abort
return s:has_popup_menu
endfunction

function! lsp#utils#is_file_uri(uri) abort
return stridx(a:uri, 'file:///') == 0
endfunction
Expand Down
19 changes: 18 additions & 1 deletion autoload/lsp/utils/args.vim
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
function! lsp#utils#args#_parse(args, opt) abort
function! lsp#utils#args#_parse(args, opt, remainder_key) abort
let l:result = {}
let l:is_opts = v:true
let l:remainder = []
for l:item in split(a:args, ' ')
if l:item[:1] !=# '--'
let l:is_opts = v:false
endif

if l:is_opts == v:false
call add(l:remainder, l:item)
continue
endif

let l:parts = split(l:item, '=')
let l:key = l:parts[0]
let l:value = get(l:parts, 1, '')
let l:key = l:key[2:]

if has_key(a:opt, l:key)
if has_key(a:opt[l:key], 'type')
let l:type = a:opt[l:key]['type']
Expand All @@ -21,5 +33,10 @@ function! lsp#utils#args#_parse(args, opt) abort
endif
let l:result[l:key] = l:value
endfor

if a:remainder_key != v:null
let l:result[a:remainder_key] = join(l:remainder)
endif

return l:result
endfunction
4 changes: 2 additions & 2 deletions doc/vim-lsp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1561,15 +1561,15 @@ LspCallHierarchyOutgoing *:LspCallHierarchyOutgoing*

Find outgoing call hierarchy for the symbol under cursor.

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

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

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

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

Same as |:LspCodeAction| but synchronous. Useful when running |:autocmd|
commands such as organize imports before save.
Expand Down
31 changes: 16 additions & 15 deletions plugin/lsp.vim
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ let g:lsp_work_done_progress_enabled = get(g:, 'lsp_work_done_progress_enabled',
let g:lsp_untitled_buffer_enabled = get(g:, 'lsp_untitled_buffer_enabled', 1)
let g:lsp_inlay_hints_enabled = get(g:, 'lsp_inlay_hints_enabled', 0)
let g:lsp_inlay_hints_delay = get(g:, 'lsp_inlay_hints_delay', 350)
let g:lsp_code_action_ui = get(g:, 'lsp_code_action_ui', 'preview')

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

Expand All @@ -89,16 +90,14 @@ endif
command! LspAddTreeCallHierarchyIncoming call lsp#ui#vim#add_tree_call_hierarchy_incoming()
command! LspCallHierarchyIncoming call lsp#ui#vim#call_hierarchy_incoming({})
command! LspCallHierarchyOutgoing call lsp#ui#vim#call_hierarchy_outgoing()
command! -range -nargs=* -complete=customlist,lsp#ui#vim#code_action#complete LspCodeAction call lsp#ui#vim#code_action#do({
\ 'sync': v:false,
\ 'selection': <range> != 0,
\ 'query': '<args>'
\ })
command! -range -nargs=* -complete=customlist,lsp#ui#vim#code_action#complete LspCodeActionSync call lsp#ui#vim#code_action#do({
\ 'sync': v:true,
\ 'selection': <range> != 0,
\ 'query': '<args>'
\ })
command! -range -nargs=* -complete=customlist,lsp#ui#vim#code_action#complete LspCodeAction call lsp#ui#vim#code_action#do(
\ extend({ 'sync': v:false, 'selection': <range> != 0 }, lsp#utils#args#_parse(<q-args>, {
\ 'ui': { 'type': type('') },
\ }, 'query')))
command! -range -nargs=* -complete=customlist,lsp#ui#vim#code_action#complete LspCodeActionSync call lsp#ui#vim#code_action#do(
\ extend({ 'sync': v:true, 'selection': <range> != 0 }, lsp#utils#args#_parse(<q-args>, {
\ 'ui': { 'type': type('') },
\ }, 'query')))
command! LspCodeLens call lsp#ui#vim#code_lens#do({})
command! LspDeclaration call lsp#ui#vim#declaration(0, <q-mods>)
command! LspPeekDeclaration call lsp#ui#vim#declaration(1)
Expand All @@ -109,11 +108,11 @@ command! LspDocumentSymbolSearch call lsp#internal#document_symbol#search#do({})
command! -nargs=? LspDocumentDiagnostics call lsp#internal#diagnostics#document_diagnostics_command#do(
\ extend({}, lsp#utils#args#_parse(<q-args>, {
\ 'buffers': {'type': type('')},
\ })))
\ }, v:null)))
command! -nargs=? -complete=customlist,lsp#utils#empty_complete LspHover call lsp#internal#document_hover#under_cursor#do(
\ extend({}, lsp#utils#args#_parse(<q-args>, {
\ 'ui': { 'type': type('') },
\ })))
\ }, v:null)))
command! -nargs=* LspNextError call lsp#internal#diagnostics#movement#_next_error(<f-args>)
command! -nargs=* LspPreviousError call lsp#internal#diagnostics#movement#_previous_error(<f-args>)
command! -nargs=* LspNextWarning call lsp#internal#diagnostics#movement#_next_warning(<f-args>)
Expand All @@ -132,13 +131,13 @@ command! -range -nargs=? LspDocumentFormatSync call lsp#internal#document_format
\ extend({'bufnr': bufnr('%'), 'sync': 1 }, lsp#utils#args#_parse(<q-args>, {
\ 'timeout': {'type': type(0)},
\ 'sleep': {'type': type(0)},
\ })))
\ }, v:null)))
command! -range LspDocumentRangeFormat call lsp#internal#document_range_formatting#format({ 'bufnr': bufnr('%') })
command! -range -nargs=? LspDocumentRangeFormatSync call lsp#internal#document_range_formatting#format(
\ extend({'bufnr': bufnr('%'), 'sync': 1 }, lsp#utils#args#_parse(<q-args>, {
\ 'timeout': {'type': type(0)},
\ 'sleep': {'type': type(0)},
\ })))
\ }, v:null)))
command! LspImplementation call lsp#ui#vim#implementation(0, <q-mods>)
command! LspPeekImplementation call lsp#ui#vim#implementation(1)
command! -nargs=0 LspStatus call lsp#print_server_status()
Expand All @@ -153,7 +152,9 @@ command! -nargs=0 LspSemanticTokenModifiers echo lsp#internal#semantic#get_token

nnoremap <silent> <plug>(lsp-call-hierarchy-incoming) :<c-u>call lsp#ui#vim#call_hierarchy_incoming({})<cr>
nnoremap <silent> <plug>(lsp-call-hierarchy-outgoing) :<c-u>call lsp#ui#vim#call_hierarchy_outgoing()<cr>
nnoremap <silent> <plug>(lsp-code-action) :<c-u>call lsp#ui#vim#code_action()<cr>
nnoremap <silent> <plug>(lsp-code-action) :<c-u>call lsp#ui#vim#code_action({})<cr>
nnoremap <silent> <plug>(lsp-code-action-float) :<c-u>call lsp#ui#vim#code_action({ 'ui': 'float' })<cr>
nnoremap <silent> <plug>(lsp-code-action-preview) :<c-u>call lsp#ui#vim#code_action({ 'ui': 'preview' })<cr>
nnoremap <silent> <plug>(lsp-code-lens) :<c-u>call lsp#ui#vim#code_lens()<cr>
nnoremap <silent> <plug>(lsp-declaration) :<c-u>call lsp#ui#vim#declaration(0)<cr>
nnoremap <silent> <plug>(lsp-peek-declaration) :<c-u>call lsp#ui#vim#declaration(1)<cr>
Expand Down

0 comments on commit 06f2c12

Please sign in to comment.