diff --git a/autoload/lsp.vim b/autoload/lsp.vim index 79b2a4568..5434df9c6 100644 --- a/autoload/lsp.vim +++ b/autoload/lsp.vim @@ -26,6 +26,8 @@ augroup _lsp_silent_ autocmd User lsp_server_init silent autocmd User lsp_server_exit silent autocmd User lsp_complete_done silent + autocmd User lsp_float_opened silent + autocmd User lsp_float_closed silent augroup END function! lsp#log_verbose(...) abort diff --git a/autoload/lsp/ui/vim/output.vim b/autoload/lsp/ui/vim/output.vim index 159c97372..3d7efd907 100644 --- a/autoload/lsp/ui/vim/output.vim +++ b/autoload/lsp/ui/vim/output.vim @@ -1,18 +1,206 @@ +let s:supports_floating = exists('*nvim_open_win') || has('patch-8.1.1517') +let s:winid = v:false +let s:prevwin = v:false +let s:preview_data = v:false + +function! lsp#ui#vim#output#closepreview() abort + if win_getid() == s:winid + " Don't close if window got focus + return + endif + "closing floats in vim8.1 must use popup_close() (nvim could use nvim_win_close but pclose + "works) + if s:supports_floating && s:winid && g:lsp_preview_float && !has('nvim') + call popup_close(s:winid) + else + pclose + endif + let s:winid = v:false + let s:preview_data = v:false + augroup lsp_float_preview_close + augroup end + autocmd! lsp_float_preview_close CursorMoved,CursorMovedI,VimResized * + doautocmd User lsp_float_closed +endfunction + +function! lsp#ui#vim#output#focuspreview() abort + " This does not work for vim8.1 popup but will work for nvim and old preview + if s:winid + if win_getid() != s:winid + let s:prevwin = win_getid() + call win_gotoid(s:winid) + elseif s:prevwin + " Temporarily disable hooks + " TODO: remove this when closing logic is able to distinguish different move directions + autocmd! lsp_float_preview_close CursorMoved,CursorMovedI,VimResized * + call win_gotoid(s:prevwin) + call s:add_float_closing_hooks() + let s:prevwin = v:false + endif + endif +endfunction + +function! s:bufwidth() abort + let width = winwidth(0) + let numberwidth = max([&numberwidth, strlen(line('$'))+1]) + let numwidth = (&number || &relativenumber)? numberwidth : 0 + let foldwidth = &foldcolumn + + if &signcolumn ==? 'yes' + let signwidth = 2 + elseif &signcolumn ==? 'auto' + let signs = execute(printf('sign place buffer=%d', bufnr(''))) + let signs = split(signs, "\n") + let signwidth = len(signs)>2? 2: 0 + else + let signwidth = 0 + endif + return width - numwidth - foldwidth - signwidth +endfunction + + +function! s:get_float_positioning(height, width) abort + let l:height = a:height + let l:width = a:width + " For a start show it below/above the cursor + " TODO: add option to configure it 'docked' at the bottom/top/right + let l:y = winline() + if l:y + l:height >= winheight(0) + " Float does not fit + if l:y - 2 > l:height + " Fits above + let l:y = winline() - l:height -1 + elseif l:y - 2 > winheight(0) - l:y + " Take space above cursor + let l:y = 1 + let l:height = winline()-2 + else + " Take space below cursor + let l:height = winheight(0) -l:y + endif + endif + let l:col = col('.') + " Positioning is not window but screen relative + let l:opts = { + \ 'relative': 'win', + \ 'row': l:y, + \ 'col': l:col, + \ 'width': l:width, + \ 'height': l:height, + \ } + return l:opts +endfunction + +function! lsp#ui#vim#output#floatingpreview(data) abort + if has('nvim') + let l:buf = nvim_create_buf(v:false, v:true) + call setbufvar(l:buf, '&signcolumn', 'no') + + " Try to get as much pace right-bolow the cursor, but at least 10x10 + let l:width = max([s:bufwidth(), 10]) + let l:height = max([&lines - winline() + 1, 10]) + + let l:opts = s:get_float_positioning(l:height, l:width) + + let s:winid = nvim_open_win(buf, v:true, l:opts) + call nvim_win_set_option(s:winid, 'winhl', 'Normal:Pmenu,NormalNC:Pmenu') + call nvim_win_set_option(s:winid, 'foldenable', v:false) + call nvim_win_set_option(s:winid, 'wrap', v:true) + call nvim_win_set_option(s:winid, 'statusline', '') + call nvim_win_set_option(s:winid, 'number', v:false) + call nvim_win_set_option(s:winid, 'relativenumber', v:false) + call nvim_win_set_option(s:winid, 'cursorline', v:false) + " Enable closing the preview with esc, but map only in the scratch buffer + nmap :pclose + else + let s:winid = popup_atcursor('...', { + \ 'moved': 'any', + \ 'border': [1, 1, 1, 1], + \}) + endif + return s:winid +endfunction + +function! s:setcontent(lines, ft) abort + if s:supports_floating && g:lsp_preview_float && !has('nvim') + " vim popup + call setbufline(winbufnr(s:winid), 1, a:lines) + let l:lightline_toggle = v:false + if exists('#lightline') && !has('nvim') + " Lightline does not work in popups but does not recognize it yet. + " It is ugly to have an check for an other plugin here, better fix lightline... + let l:lightline_toggle = v:true + call lightline#disable() + endif + call win_execute(s:winid, 'setlocal filetype=' . a:ft . '.lsp-hover') + if l:lightline_toggle + call lightline#enable() + endif + else + " nvim floating + call setline(1, a:lines) + setlocal readonly nomodifiable + let &l:filetype = a:ft . '.lsp-hover' + endif +endfunction + +function! s:adjust_float_placement(bufferlines, maxwidth) abort + if has('nvim') + let l:win_config = {} + let l:height = min([winheight(s:winid), a:bufferlines]) + let l:width = min([winwidth(s:winid), a:maxwidth]) + let l:win_config = s:get_float_positioning(l:height, l:width) + call nvim_win_set_config(s:winid, l:win_config ) + endif +endfunction + +function! s:add_float_closing_hooks() abort + if g:lsp_preview_autoclose + augroup lsp_float_preview_close + autocmd! lsp_float_preview_close CursorMoved,CursorMovedI,VimResized * + autocmd CursorMoved,CursorMovedI,VimResized * call lsp#ui#vim#output#closepreview() + augroup END + endif +endfunction + +function! lsp#ui#vim#output#getpreviewwinid() abort + return s:winid +endfunction + +function! s:open_preview(data) abort + if s:supports_floating && g:lsp_preview_float + let l:winid = lsp#ui#vim#output#floatingpreview(a:data) + else + execute &previewheight.'new' + let l:winid = win_getid() + endif + return l:winid +endfunction + function! lsp#ui#vim#output#preview(data) abort + if s:winid && type(s:preview_data) == type(a:data) + \ && s:preview_data == a:data + \ && type(g:lsp_preview_doubletap) == 3 + \ && len(g:lsp_preview_doubletap) >= 1 + \ && type(g:lsp_preview_doubletap[0]) == 2 + echo '' + return call(g:lsp_preview_doubletap[0], []) + endif " Close any previously opened preview window pclose let l:current_window_id = win_getid() - execute &previewheight.'new' - - let l:ft = s:append(a:data) - " Delete first empty line - 0delete _ + let s:winid = s:open_preview(a:data) - setlocal readonly nomodifiable + let s:preview_data = a:data + let l:lines = [] + let l:ft = s:append(a:data, l:lines) + call s:setcontent(l:lines, l:ft) - let &l:filetype = l:ft . '.lsp-hover' + " Get size information while still having the buffer active + let l:bufferlines = line('$') + let l:maxwidth = max(map(getline(1, '$'), 'strdisplaywidth(v:val)')) if g:lsp_preview_keep_focus " restore focus to the previous window @@ -21,28 +209,35 @@ function! lsp#ui#vim#output#preview(data) abort echo '' + if s:supports_floating && s:winid && g:lsp_preview_float + if has('nvim') + call s:adjust_float_placement(l:bufferlines, l:maxwidth) + call s:add_float_closing_hooks() + endif + doautocmd User lsp_float_opened + endif return '' endfunction -function! s:append(data) abort +function! s:append(data, lines) abort if type(a:data) == type([]) for l:entry in a:data - call s:append(entry) + call s:append(entry, a:lines) endfor return 'markdown' elseif type(a:data) == type('') - silent put =a:data + call extend(a:lines, split(a:data, "\n")) return 'markdown' elseif type(a:data) == type({}) && has_key(a:data, 'language') - silent put ='```'.a:data.language - silent put =a:data.value - silent put ='```' + call add(a:lines, '```'.a:data.language) + call extend(a:lines, split(a:data.value, '\n')) + call add(a:lines, '```') return 'markdown' elseif type(a:data) == type({}) && has_key(a:data, 'kind') - silent put =a:data.value + call add(a:lines, a:data.value) return a:data.kind ==? 'plaintext' ? 'text' : a:data.kind endif diff --git a/doc/vim-lsp.txt b/doc/vim-lsp.txt index 176f0ac86..5125c926e 100644 --- a/doc/vim-lsp.txt +++ b/doc/vim-lsp.txt @@ -13,6 +13,9 @@ CONTENTS *vim-lsp-contents* g:lsp_diagnostics_enabled |g:lsp_diagnostics_enabled| g:lsp_auto_enable |g:lsp_auto_enable| g:lsp_preview_keep_focus |g:lsp_preview_keep_focus| + g:lsp_preview_float |g:lsp_preview_float| + g:lsp_preview_autoclose |g:lsp_preview_autoclose| + g:lsp_preview_doubletap |g:lsp_preview_doubletap| g:lsp_insert_text_enabled |g:lsp_insert_text_enabled| g:lsp_text_edit_enabled |g:lsp_text_edit_enabled| g:lsp_diagnostics_echo_cursor |g:lsp_diagnostics_echo_cursor| @@ -53,6 +56,9 @@ CONTENTS *vim-lsp-contents* Autocommands |vim-lsp-autocommands| lsp_complete_done |lsp_complete_done| Mappings |vim-lsp-mappings| + (lsp-preview-close) |(lsp-preview-close)| + (lsp-preview-focus) |(lsp-preview-focus)| + Autocomplete |vim-lsp-autocomplete| omnifunc |vim-lsp-omnifunc| asyncomplete.vim |vim-lsp-asyncomplete| @@ -154,6 +160,68 @@ g:lsp_preview_keep_focus *g:lsp_preview_keep_focus* * |preview-window| can be closed using the default vim mapping - ``. +g:lsp_preview_float *g:lsp_preview_float* + Type: |Number| + Default: `1` + + If set and nvim_win_open() or popup_create is available, hover information + are shown in a floating window as |preview-window| at the cursor position. + The |preview-window| is closed automatically on cursor moves, unless it is + focused. While focused it may be closed with . + After opening an autocmd User event lsp_float_opened is issued, as well as + and lsp_float_closed upon closing. This can be used to alter the preview + window (using lsp#ui#vim#output#getpreviewwinid() to get the window id), or + setup custom bindings while a preview is open. + This feature requires neovim 0.4.0 (current master) or + Vim8.1 with has('patch-8.1.1517'). + + Example: + " Opens preview windows as floating + let g:lsp_preview_float = 1 + + " Opens preview windows as normal windows + let g:lsp_preview_float = 0 + + " Close preview window with + autocmd User lsp_float_opened nmap + \ (lsp-preview-close) + autocmd User lsp_float_closed nunmap + +g:lsp_preview_autoclose *g:lsp_preview_autoclose* + Type: |Number| + Default: `1` + + Indicates if an opened floating preview shall be automatically closed upon + movement of the cursor. If set to 1, the window will close automatically if + the cursor is moved and the preview is not focused. If set to 0, it will + remain open until explicitly closed (e.g. with (lsp-preview-close), + or when focused). + + Example: + " Preview closes on cursor move + let g:lsp_preview_autoclose = 1 + + " Preview remains open and waits for an explicit call + let g:lsp_preview_autoclose = 0 + +g:lsp_preview_doubletap *g:lsp_preview_doubletap* + Type: |List| + Default: `[function('lsp#ui#vim#output#focuspreview')]` + + When preview is called twice with the same data while the preview is still + open, the function in `lsp_preview_doubletap` is called instead. To disable + this and just "refresh" the preview, set to ´0´. + + Example: + " Focus preview on repeated preview (does not work for vim8.1 popups) + let g:lsp_preview_doubletap = [function('lsp#ui#vim#output#focuspreview')] + + " Closes the preview window on the second call to preview + let g:lsp_preview_doubletap = [function('lsp#ui#vim#output#closepreview')] + + " Disables double tap feature; refreshes the preview on consecutive taps + let g:lsp_preview_doubletap = 0 + g:lsp_insert_text_enabled *g:lsp_insert_text_enabled* Type: |Number| Default: `1` @@ -566,6 +634,9 @@ Gets the hover information and displays it in the |preview-window|. * |preview-window| can be closed using the default vim mapping - ``. * To control the default focus of |preview-window| for |LspHover| configure |g:lsp_preview_keep_focus|. + * If using neovim with nvim_win_open() available, |g:lsp_preview_float| can be + set to enable a floating preview at the cursor which is closed automatically + on cursormove if not focused and can be closed with if focused. LspNextError *LspNextError* @@ -640,6 +711,8 @@ Available plug mappings are following: (lsp-hover) (lsp-next-error) (lsp-next-reference) + (lsp-preview-close) + (lsp-preview-focus) (lsp-previous-error) (lsp-previous-reference) (lsp-references) @@ -653,6 +726,16 @@ Available plug mappings are following: See also |vim-lsp-commands| +(lsp-preview-close) *(lsp-preview-close)* + +Closes an opened preview window + +(lsp-preview-focus) *(lsp-preview-focus)* + +Transfers focus to an opened preview window or back to the previous window if +focus is already on the preview window. + + =============================================================================== Autocomplete *vim-lsp-autocomplete* diff --git a/ftplugin/lsp-hover.vim b/ftplugin/lsp-hover.vim index 53559a7cc..67557341c 100644 --- a/ftplugin/lsp-hover.vim +++ b/ftplugin/lsp-hover.vim @@ -1,6 +1,11 @@ " No usual did_ftplugin header here as we NEED to run this always -setlocal previewwindow buftype=nofile bufhidden=wipe noswapfile nobuflisted +if has('patch-8.1.1517') && g:lsp_preview_float && !has('nvim') + " Can not set buftype or popup_close will fail with 'not a popup window' + setlocal previewwindow bufhidden=wipe noswapfile nobuflisted +else + setlocal previewwindow buftype=nofile bufhidden=wipe noswapfile nobuflisted +endif setlocal nocursorline nofoldenable if has('syntax') diff --git a/plugin/lsp.vim b/plugin/lsp.vim index 65ae41bae..f3c746ae2 100644 --- a/plugin/lsp.vim +++ b/plugin/lsp.vim @@ -25,6 +25,9 @@ let g:lsp_use_event_queue = get(g:, 'lsp_use_event_queue', has('nvim') || has('p let g:lsp_insert_text_enabled= get(g:, 'lsp_insert_text_enabled', 1) let g:lsp_text_edit_enabled = get(g:, 'lsp_text_edit_enabled', has('patch-8.0.1493')) let g:lsp_highlight_references_enabled = get(g:, 'lsp_highlight_references_enabled', 1) +let g:lsp_preview_float = get(g:, 'lsp_preview_float', 1) +let g:lsp_preview_autoclose = get(g:, 'lsp_preview_autoclose', 1) +let g:lsp_preview_doubletap = get(g:, 'lsp_preview_doubletap', [function('lsp#ui#vim#output#focuspreview')]) let g:lsp_get_vim_completion_item = get(g:, 'lsp_get_vim_completion_item', [function('lsp#omni#default_get_vim_completion_item')]) let g:lsp_get_supported_capabilities = get(g:, 'lsp_get_supported_capabilities', [function('lsp#default_get_supported_capabilities')]) @@ -64,6 +67,8 @@ nnoremap (lsp-definition) :call lsp#ui#vim#definition() nnoremap (lsp-document-symbol) :call lsp#ui#vim#document_symbol() nnoremap (lsp-document-diagnostics) :call lsp#ui#vim#diagnostics#document_diagnostics() nnoremap (lsp-hover) :call lsp#ui#vim#hover#get_hover_under_cursor() +nnoremap (lsp-preview-close) :call lsp#ui#vim#output#closepreview() +nnoremap (lsp-preview-focus) :call lsp#ui#vim#output#focuspreview() nnoremap (lsp-next-error) :call lsp#ui#vim#diagnostics#next_error() nnoremap (lsp-previous-error) :call lsp#ui#vim#diagnostics#previous_error() nnoremap (lsp-references) :call lsp#ui#vim#references()