All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
1 let s:use_vim_popup = has('patch-8.1.1517') && g:lsp_preview_float && !has('nvim')
2 let s:use_nvim_float = exists('*nvim_open_win') && g:lsp_preview_float && has('nvim')
3 let s:use_preview = !s:use_vim_popup && !s:use_nvim_float
5 function! s:import_modules() abort
6 if exists('s:Markdown') | return | endif
7 let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown')
8 let s:MarkupContent = vital#lsp#import('VS.LSP.MarkupContent')
9 let s:Window = vital#lsp#import('VS.Vim.Window')
10 let s:Text = vital#lsp#import('VS.LSP.Text')
14 let s:prevwin = v:false
15 let s:preview_data = v:false
17 function! s:vim_popup_closed(...) abort
18 let s:preview_data = v:false
21 function! lsp#ui#vim#output#closepreview() abort
22 if win_getid() ==# s:winid
23 " Don't close if window got focus
31 "closing floats in vim8.1 must use popup_close()
32 "nvim must use nvim_win_close. pclose is not reliable and does not always work
33 if s:use_vim_popup && s:winid
34 call popup_close(s:winid)
35 elseif s:use_nvim_float && s:winid
36 silent! call nvim_win_close(s:winid, 0)
41 let s:preview_data = v:false
42 augroup lsp_float_preview_close
44 autocmd! lsp_float_preview_close CursorMoved,CursorMovedI,VimResized *
45 doautocmd <nomodeline> User lsp_float_closed
48 function! lsp#ui#vim#output#focuspreview() abort
53 " This does not work for vim8.1 popup but will work for nvim and old preview
55 if win_getid() !=# s:winid
56 let s:prevwin = win_getid()
57 call win_gotoid(s:winid)
59 " Temporarily disable hooks
60 " TODO: remove this when closing logic is able to distinguish different move directions
61 autocmd! lsp_float_preview_close CursorMoved,CursorMovedI,VimResized *
62 call win_gotoid(s:prevwin)
63 call s:add_float_closing_hooks()
64 let s:prevwin = v:false
69 function! s:bufwidth() abort
70 let l:width = winwidth(0)
71 let l:numberwidth = max([&numberwidth, strlen(line('$'))+1])
72 let l:numwidth = (&number || &relativenumber)? l:numberwidth : 0
73 let l:foldwidth = &foldcolumn
75 if &signcolumn ==? 'yes'
77 elseif &signcolumn ==? 'auto'
78 let l:signs = execute(printf('sign place buffer=%d', bufnr('')))
79 let l:signs = split(l:signs, "\n")
80 let l:signwidth = len(l:signs)>2? 2: 0
84 return l:width - l:numwidth - l:foldwidth - l:signwidth
88 function! s:get_float_positioning(height, width) abort
89 let l:height = a:height
91 " TODO: add option to configure it 'docked' at the bottom/top/right
93 " NOTE: screencol() and screenrow() start from (1,1)
94 " but the popup window co-ordinates start from (0,0)
96 " For a simple single-line 'tooltip', the following
97 " two lines are enough to determine the position
99 let l:col = screencol()
100 let l:row = screenrow()
102 let l:height = min([l:height, max([&lines - &cmdheight - l:row, &previewheight])])
104 let l:style = 'minimal'
105 let l:border = 'double'
106 " Positioning is not window but screen relative
108 \ 'relative': 'editor',
112 \ 'height': l:height,
114 \ 'border': l:border,
119 function! lsp#ui#vim#output#floatingpreview(data) abort
121 let l:buf = nvim_create_buf(v:false, v:true)
123 " Try to get as much space around the cursor, but at least 10x10
124 let l:width = max([s:bufwidth(), 10])
125 let l:height = max([&lines - winline() + 1, winline() - 1, 10])
127 if g:lsp_preview_max_height > 0
128 let l:height = min([g:lsp_preview_max_height, l:height])
131 let l:opts = s:get_float_positioning(l:height, l:width)
133 let s:winid = nvim_open_win(l:buf, v:false, l:opts)
134 call nvim_win_set_option(s:winid, 'winhl', 'Normal:Pmenu,NormalNC:Pmenu')
135 call nvim_win_set_option(s:winid, 'foldenable', v:false)
136 call nvim_win_set_option(s:winid, 'wrap', v:true)
137 call nvim_win_set_option(s:winid, 'statusline', '')
138 call nvim_win_set_option(s:winid, 'number', v:false)
139 call nvim_win_set_option(s:winid, 'relativenumber', v:false)
140 call nvim_win_set_option(s:winid, 'cursorline', v:false)
141 call nvim_win_set_option(s:winid, 'cursorcolumn', v:false)
142 call nvim_win_set_option(s:winid, 'colorcolumn', '')
143 call nvim_win_set_option(s:winid, 'signcolumn', 'no')
144 " Enable closing the preview with esc, but map only in the scratch buffer
145 call nvim_buf_set_keymap(l:buf, 'n', '<esc>', ':pclose<cr>', {'silent': v:true})
146 elseif s:use_vim_popup
149 \ 'border': [1, 1, 1, 1],
150 \ 'callback': function('s:vim_popup_closed')
153 if g:lsp_preview_max_width > 0
154 let l:options['maxwidth'] = g:lsp_preview_max_width
157 if g:lsp_preview_max_height > 0
158 let l:options['maxheight'] = g:lsp_preview_max_height
161 let s:winid = popup_atcursor('...', l:options)
166 function! lsp#ui#vim#output#setcontent(winid, lines, ft) abort
169 call setbufline(winbufnr(a:winid), 1, a:lines)
170 call setbufvar(winbufnr(a:winid), '&filetype', a:ft . '.lsp-hover')
171 elseif s:use_nvim_float
173 call nvim_buf_set_lines(winbufnr(a:winid), 0, -1, v:false, a:lines)
174 call nvim_buf_set_option(winbufnr(a:winid), 'readonly', v:true)
175 call nvim_buf_set_option(winbufnr(a:winid), 'modifiable', v:false)
176 call nvim_buf_set_option(winbufnr(a:winid), 'filetype', a:ft.'.lsp-hover')
177 call nvim_win_set_cursor(a:winid, [1, 0])
180 call setbufline(winbufnr(a:winid), 1, a:lines)
181 call setbufvar(winbufnr(a:winid), '&filetype', a:ft . '.lsp-hover')
185 function! lsp#ui#vim#output#adjust_float_placement(bufferlines, maxwidth) abort
187 let l:win_config = {}
188 let l:height = min([winheight(s:winid), a:bufferlines])
189 let l:width = min([winwidth(s:winid), a:maxwidth])
190 let l:win_config = s:get_float_positioning(l:height, l:width)
191 call nvim_win_set_config(s:winid, l:win_config )
195 function! s:add_float_closing_hooks() abort
196 if g:lsp_preview_autoclose
197 augroup lsp_float_preview_close
198 autocmd! lsp_float_preview_close CursorMoved,CursorMovedI,VimResized *
199 autocmd CursorMoved,CursorMovedI,VimResized * call lsp#ui#vim#output#closepreview()
204 function! lsp#ui#vim#output#getpreviewwinid() abort
208 function! s:open_preview(data) abort
209 if s:use_vim_popup || s:use_nvim_float
210 let l:winid = lsp#ui#vim#output#floatingpreview(a:data)
212 execute &previewheight.'new'
213 let l:winid = win_getid()
218 function! s:set_cursor(current_window_id, options) abort
219 if !has_key(a:options, 'cursor')
225 " Go back to the preview window to set the cursor
226 call win_gotoid(s:winid)
227 let l:old_scrolloff = &scrolloff
230 call nvim_win_set_cursor(s:winid, [a:options['cursor']['line'], a:options['cursor']['col']])
231 call s:align_preview(a:options)
233 " Finally, go back to the original window
234 call win_gotoid(a:current_window_id)
236 let &scrolloff = l:old_scrolloff
237 elseif s:use_vim_popup
239 function! AlignVimPopup(timer) closure abort
240 call s:align_preview(a:options)
242 call timer_start(0, function('AlignVimPopup'))
245 " Don't use 'scrolloff', it might mess up the cursor's position
247 call cursor(a:options['cursor']['line'], a:options['cursor']['col'])
248 call s:align_preview(a:options)
252 function! s:align_preview(options) abort
253 if !has_key(a:options, 'cursor') ||
254 \ !has_key(a:options['cursor'], 'align')
258 let l:align = a:options['cursor']['align']
262 let l:pos = popup_getpos(s:winid)
263 let l:below = winline() < winheight(0) / 2
265 let l:height = min([l:pos['core_height'], winheight(0) - winline() - 2])
267 let l:height = min([l:pos['core_height'], winline() - 3])
269 let l:width = l:pos['core_width']
272 \ 'minwidth': l:width,
273 \ 'maxwidth': l:width,
274 \ 'minheight': l:height,
275 \ 'maxheight': l:height,
276 \ 'pos': l:below ? 'topleft' : 'botleft',
277 \ 'line': l:below ? 'cursor+1' : 'cursor-1'
281 let l:options['firstline'] = a:options['cursor']['line']
282 elseif l:align ==? 'center'
283 let l:options['firstline'] = a:options['cursor']['line'] - (l:height - 1) / 2
284 elseif l:align ==? 'bottom'
285 let l:options['firstline'] = a:options['cursor']['line'] - l:height + 1
288 call popup_setoptions(s:winid, l:options)
291 " Preview and Neovim floats
294 elseif l:align ==? 'center'
296 elseif l:align ==? 'bottom'
302 function! lsp#ui#vim#output#get_size_info(winid) abort
303 " Get size information while still having the buffer active
304 let l:buffer = winbufnr(a:winid)
305 let l:maxwidth = max(map(getbufline(l:buffer, 1, '$'), 'strdisplaywidth(v:val)'))
306 let l:bufferlines = 0
307 if g:lsp_preview_max_width > 0
308 let l:maxwidth = min([g:lsp_preview_max_width, l:maxwidth])
310 " Determine, for each line, how many "virtual" lines it spans, and add
311 " these together for all lines in the buffer
312 for l:line in getbufline(l:buffer, 1, '$')
313 let l:num_lines = str2nr(string(ceil(strdisplaywidth(l:line) * 1.0 / g:lsp_preview_max_width)))
314 let l:bufferlines += max([l:num_lines, 1])
318 let l:bufferlines = line('$', a:winid)
319 elseif s:use_nvim_float
320 let l:bufferlines = nvim_buf_line_count(winbufnr(a:winid))
324 return [l:bufferlines, l:maxwidth]
327 function! lsp#ui#vim#output#float_supported() abort
328 return s:use_vim_popup || s:use_nvim_float
331 function! lsp#ui#vim#output#preview(server, data, options) abort
336 if s:winid && type(s:preview_data) ==# type(a:data)
337 \ && s:preview_data ==# a:data
338 \ && type(g:lsp_preview_doubletap) ==# 3
339 \ && len(g:lsp_preview_doubletap) >= 1
340 \ && type(g:lsp_preview_doubletap[0]) ==# 2
341 \ && index(['i', 's'], mode()[0]) ==# -1
343 return call(g:lsp_preview_doubletap[0], [])
345 " Close any previously opened preview window
346 call lsp#ui#vim#output#closepreview()
348 let l:current_window_id = win_getid()
350 let s:winid = s:open_preview(a:data)
352 let s:preview_data = a:data
354 let l:syntax_lines = []
355 let l:ft = lsp#ui#vim#output#append(a:data, l:lines, l:syntax_lines)
357 if has_key(a:options, 'filetype')
358 let l:ft = a:options['filetype']
361 let l:do_conceal = g:lsp_hover_conceal
362 let l:server_info = a:server !=# '' ? lsp#get_server_info(a:server) : {}
363 let l:config = get(l:server_info, 'config', {})
364 let l:do_conceal = get(l:config, 'hover_conceal', l:do_conceal)
366 call setbufvar(winbufnr(s:winid), 'lsp_syntax_highlights', l:syntax_lines)
367 call setbufvar(winbufnr(s:winid), 'lsp_do_conceal', l:do_conceal)
368 call lsp#ui#vim#output#setcontent(s:winid, l:lines, l:ft)
370 let [l:bufferlines, l:maxwidth] = lsp#ui#vim#output#get_size_info(s:winid)
374 if has_key(a:options, 'statusline')
375 let &l:statusline = a:options['statusline']
378 call s:set_cursor(l:current_window_id, a:options)
381 " Go to the previous window to adjust positioning
382 call win_gotoid(l:current_window_id)
386 if s:winid && (s:use_vim_popup || s:use_nvim_float)
389 call lsp#ui#vim#output#adjust_float_placement(l:bufferlines, l:maxwidth)
390 call s:set_cursor(l:current_window_id, a:options)
391 call s:add_float_closing_hooks()
392 elseif s:use_vim_popup
394 call s:set_cursor(l:current_window_id, a:options)
396 doautocmd <nomodeline> User lsp_float_opened
399 if l:ft ==? 'markdown'
400 call s:import_modules()
401 call s:Window.do(s:winid, {->s:Markdown.apply()})
404 if !g:lsp_preview_keep_focus
405 " set the focus to the preview window
406 call win_gotoid(s:winid)
411 function! s:escape_string_for_display(str) abort
412 return substitute(substitute(a:str, '\r\n', '\n', 'g'), '\r', '\n', 'g')
415 function! lsp#ui#vim#output#append(data, lines, syntax_lines) abort
416 if type(a:data) == type([])
417 for l:entry in a:data
418 call lsp#ui#vim#output#append(l:entry, a:lines, a:syntax_lines)
422 elseif type(a:data) ==# type('')
423 call extend(a:lines, split(s:escape_string_for_display(a:data), "\n", v:true))
425 elseif type(a:data) ==# type({}) && has_key(a:data, 'language')
426 let l:new_lines = split(s:escape_string_for_display(a:data.value), '\n')
429 while l:i <= len(l:new_lines)
430 call add(a:syntax_lines, { 'line': len(a:lines) + l:i, 'language': a:data.language })
434 call extend(a:lines, l:new_lines)
436 elseif type(a:data) ==# type({}) && has_key(a:data, 'kind')
437 if a:data.kind ==? 'markdown'
438 call s:import_modules()
439 let l:detail = s:MarkupContent.normalize(a:data.value, {
440 \ 'compact': !g:lsp_preview_fixup_conceal
442 call extend(a:lines, s:Text.split_by_eol(l:detail))
444 call extend(a:lines, split(s:escape_string_for_display(a:data.value), '\n', v:true))
446 return a:data.kind ==? 'plaintext' ? 'text' : a:data.kind
450 function! s:is_cmdwin() abort
451 return getcmdwintype() !=# ''