let s:use_vim_popup = has('patch-8.1.1517') && g:lsp_preview_float && !has('nvim') let s:use_nvim_float = exists('*nvim_open_win') && g:lsp_preview_float && has('nvim') let s:use_preview = !s:use_vim_popup && !s:use_nvim_float function! s:import_modules() abort if exists('s:Markdown') | return | endif let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown') let s:MarkupContent = vital#lsp#import('VS.LSP.MarkupContent') let s:Window = vital#lsp#import('VS.Vim.Window') let s:Text = vital#lsp#import('VS.LSP.Text') endfunction let s:winid = v:false let s:prevwin = v:false let s:preview_data = v:false function! s:vim_popup_closed(...) abort let s:preview_data = v:false endfunction function! lsp#ui#vim#output#closepreview() abort if win_getid() ==# s:winid " Don't close if window got focus return endif if s:winid == v:false return endif "closing floats in vim8.1 must use popup_close() "nvim must use nvim_win_close. pclose is not reliable and does not always work if s:use_vim_popup && s:winid call popup_close(s:winid) elseif s:use_nvim_float && s:winid silent! call nvim_win_close(s:winid, 0) 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 if s:is_cmdwin() return endif " 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 l:width = winwidth(0) let l:numberwidth = max([&numberwidth, strlen(line('$'))+1]) let l:numwidth = (&number || &relativenumber)? l:numberwidth : 0 let l:foldwidth = &foldcolumn if &signcolumn ==? 'yes' let l:signwidth = 2 elseif &signcolumn ==? 'auto' let l:signs = execute(printf('sign place buffer=%d', bufnr(''))) let l:signs = split(l:signs, "\n") let l:signwidth = len(l:signs)>2? 2: 0 else let l:signwidth = 0 endif return l:width - l:numwidth - l:foldwidth - l:signwidth endfunction function! s:get_float_positioning(height, width) abort let l:height = a:height let l:width = a:width " TODO: add option to configure it 'docked' at the bottom/top/right " NOTE: screencol() and screenrow() start from (1,1) " but the popup window co-ordinates start from (0,0) " Very convenient! " For a simple single-line 'tooltip', the following " two lines are enough to determine the position let l:col = screencol() let l:row = screenrow() let l:height = min([l:height, max([&lines - &cmdheight - l:row, &previewheight])]) let l:style = 'minimal' let l:border = 'double' " Positioning is not window but screen relative let l:opts = { \ 'relative': 'editor', \ 'row': l:row, \ 'col': l:col, \ 'width': l:width, \ 'height': l:height, \ 'style': l:style, \ 'border': l:border, \ } return l:opts endfunction function! lsp#ui#vim#output#floatingpreview(data) abort if s:use_nvim_float let l:buf = nvim_create_buf(v:false, v:true) " Try to get as much space around the cursor, but at least 10x10 let l:width = max([s:bufwidth(), 10]) let l:height = max([&lines - winline() + 1, winline() - 1, 10]) if g:lsp_preview_max_height > 0 let l:height = min([g:lsp_preview_max_height, l:height]) endif let l:opts = s:get_float_positioning(l:height, l:width) let s:winid = nvim_open_win(l:buf, v:false, 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) call nvim_win_set_option(s:winid, 'cursorcolumn', v:false) call nvim_win_set_option(s:winid, 'colorcolumn', '') call nvim_win_set_option(s:winid, 'signcolumn', 'no') " Enable closing the preview with esc, but map only in the scratch buffer call nvim_buf_set_keymap(l:buf, 'n', '', ':pclose', {'silent': v:true}) elseif s:use_vim_popup let l:options = { \ 'moved': 'any', \ 'border': [1, 1, 1, 1], \ 'callback': function('s:vim_popup_closed') \ } if g:lsp_preview_max_width > 0 let l:options['maxwidth'] = g:lsp_preview_max_width endif if g:lsp_preview_max_height > 0 let l:options['maxheight'] = g:lsp_preview_max_height endif let s:winid = popup_atcursor('...', l:options) endif return s:winid endfunction function! lsp#ui#vim#output#setcontent(winid, lines, ft) abort if s:use_vim_popup " vim popup call setbufline(winbufnr(a:winid), 1, a:lines) call setbufvar(winbufnr(a:winid), '&filetype', a:ft . '.lsp-hover') elseif s:use_nvim_float " nvim floating call nvim_buf_set_lines(winbufnr(a:winid), 0, -1, v:false, a:lines) call nvim_buf_set_option(winbufnr(a:winid), 'readonly', v:true) call nvim_buf_set_option(winbufnr(a:winid), 'modifiable', v:false) call nvim_buf_set_option(winbufnr(a:winid), 'filetype', a:ft.'.lsp-hover') call nvim_win_set_cursor(a:winid, [1, 0]) elseif s:use_preview " preview window call setbufline(winbufnr(a:winid), 1, a:lines) call setbufvar(winbufnr(a:winid), '&filetype', a:ft . '.lsp-hover') endif endfunction function! lsp#ui#vim#output#adjust_float_placement(bufferlines, maxwidth) abort if s:use_nvim_float 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:use_vim_popup || s:use_nvim_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! s:set_cursor(current_window_id, options) abort if !has_key(a:options, 'cursor') return endif if s:use_nvim_float " Neovim floats " Go back to the preview window to set the cursor call win_gotoid(s:winid) let l:old_scrolloff = &scrolloff let &scrolloff = 0 call nvim_win_set_cursor(s:winid, [a:options['cursor']['line'], a:options['cursor']['col']]) call s:align_preview(a:options) " Finally, go back to the original window call win_gotoid(a:current_window_id) let &scrolloff = l:old_scrolloff elseif s:use_vim_popup " Vim popups function! AlignVimPopup(timer) closure abort call s:align_preview(a:options) endfunction call timer_start(0, function('AlignVimPopup')) else " Preview " Don't use 'scrolloff', it might mess up the cursor's position let &l:scrolloff = 0 call cursor(a:options['cursor']['line'], a:options['cursor']['col']) call s:align_preview(a:options) endif endfunction function! s:align_preview(options) abort if !has_key(a:options, 'cursor') || \ !has_key(a:options['cursor'], 'align') return endif let l:align = a:options['cursor']['align'] if s:use_vim_popup " Vim popups let l:pos = popup_getpos(s:winid) let l:below = winline() < winheight(0) / 2 if l:below let l:height = min([l:pos['core_height'], winheight(0) - winline() - 2]) else let l:height = min([l:pos['core_height'], winline() - 3]) endif let l:width = l:pos['core_width'] let l:options = { \ 'minwidth': l:width, \ 'maxwidth': l:width, \ 'minheight': l:height, \ 'maxheight': l:height, \ 'pos': l:below ? 'topleft' : 'botleft', \ 'line': l:below ? 'cursor+1' : 'cursor-1' \ } if l:align ==? 'top' let l:options['firstline'] = a:options['cursor']['line'] elseif l:align ==? 'center' let l:options['firstline'] = a:options['cursor']['line'] - (l:height - 1) / 2 elseif l:align ==? 'bottom' let l:options['firstline'] = a:options['cursor']['line'] - l:height + 1 endif call popup_setoptions(s:winid, l:options) redraw! else " Preview and Neovim floats if l:align ==? 'top' normal! zt elseif l:align ==? 'center' normal! zz elseif l:align ==? 'bottom' normal! zb endif endif endfunction function! lsp#ui#vim#output#get_size_info(winid) abort " Get size information while still having the buffer active let l:buffer = winbufnr(a:winid) let l:maxwidth = max(map(getbufline(l:buffer, 1, '$'), 'strdisplaywidth(v:val)')) let l:bufferlines = 0 if g:lsp_preview_max_width > 0 let l:maxwidth = min([g:lsp_preview_max_width, l:maxwidth]) " Determine, for each line, how many "virtual" lines it spans, and add " these together for all lines in the buffer for l:line in getbufline(l:buffer, 1, '$') let l:num_lines = str2nr(string(ceil(strdisplaywidth(l:line) * 1.0 / g:lsp_preview_max_width))) let l:bufferlines += max([l:num_lines, 1]) endfor else if s:use_vim_popup let l:bufferlines = line('$', a:winid) elseif s:use_nvim_float let l:bufferlines = nvim_buf_line_count(winbufnr(a:winid)) endif endif return [l:bufferlines, l:maxwidth] endfunction function! lsp#ui#vim#output#float_supported() abort return s:use_vim_popup || s:use_nvim_float endfunction function! lsp#ui#vim#output#preview(server, data, options) abort if s:is_cmdwin() return endif 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 \ && index(['i', 's'], mode()[0]) ==# -1 echo '' return call(g:lsp_preview_doubletap[0], []) endif " Close any previously opened preview window call lsp#ui#vim#output#closepreview() let l:current_window_id = win_getid() let s:winid = s:open_preview(a:data) let s:preview_data = a:data let l:lines = [] let l:syntax_lines = [] let l:ft = lsp#ui#vim#output#append(a:data, l:lines, l:syntax_lines) if has_key(a:options, 'filetype') let l:ft = a:options['filetype'] endif let l:do_conceal = g:lsp_hover_conceal let l:server_info = a:server !=# '' ? lsp#get_server_info(a:server) : {} let l:config = get(l:server_info, 'config', {}) let l:do_conceal = get(l:config, 'hover_conceal', l:do_conceal) call setbufvar(winbufnr(s:winid), 'lsp_syntax_highlights', l:syntax_lines) call setbufvar(winbufnr(s:winid), 'lsp_do_conceal', l:do_conceal) call lsp#ui#vim#output#setcontent(s:winid, l:lines, l:ft) let [l:bufferlines, l:maxwidth] = lsp#ui#vim#output#get_size_info(s:winid) if s:use_preview " Set statusline if has_key(a:options, 'statusline') let &l:statusline = a:options['statusline'] endif call s:set_cursor(l:current_window_id, a:options) endif " Go to the previous window to adjust positioning call win_gotoid(l:current_window_id) echo '' if s:winid && (s:use_vim_popup || s:use_nvim_float) if s:use_nvim_float " Neovim floats call lsp#ui#vim#output#adjust_float_placement(l:bufferlines, l:maxwidth) call s:set_cursor(l:current_window_id, a:options) call s:add_float_closing_hooks() elseif s:use_vim_popup " Vim popups call s:set_cursor(l:current_window_id, a:options) endif doautocmd User lsp_float_opened endif if l:ft ==? 'markdown' call s:import_modules() call s:Window.do(s:winid, {->s:Markdown.apply()}) endif if !g:lsp_preview_keep_focus " set the focus to the preview window call win_gotoid(s:winid) endif return '' endfunction function! s:escape_string_for_display(str) abort return substitute(substitute(a:str, '\r\n', '\n', 'g'), '\r', '\n', 'g') endfunction function! lsp#ui#vim#output#append(data, lines, syntax_lines) abort if type(a:data) == type([]) for l:entry in a:data call lsp#ui#vim#output#append(l:entry, a:lines, a:syntax_lines) endfor return 'markdown' elseif type(a:data) ==# type('') call extend(a:lines, split(s:escape_string_for_display(a:data), "\n", v:true)) return 'markdown' elseif type(a:data) ==# type({}) && has_key(a:data, 'language') let l:new_lines = split(s:escape_string_for_display(a:data.value), '\n') let l:i = 1 while l:i <= len(l:new_lines) call add(a:syntax_lines, { 'line': len(a:lines) + l:i, 'language': a:data.language }) let l:i += 1 endwhile call extend(a:lines, l:new_lines) return 'markdown' elseif type(a:data) ==# type({}) && has_key(a:data, 'kind') if a:data.kind ==? 'markdown' call s:import_modules() let l:detail = s:MarkupContent.normalize(a:data.value, { \ 'compact': !g:lsp_preview_fixup_conceal \ }) call extend(a:lines, s:Text.split_by_eol(l:detail)) else call extend(a:lines, split(s:escape_string_for_display(a:data.value), '\n', v:true)) endif return a:data.kind ==? 'plaintext' ? 'text' : a:data.kind endif endfunction function! s:is_cmdwin() abort return getcmdwintype() !=# '' endfunction