]> git.madduck.net Git - etc/vim.git/blob - autoload/lsp/internal/document_hover/under_cursor.vim

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

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.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Squashed '.vim/bundle/vim-lsp/' content from commit 04428c92
[etc/vim.git] / autoload / lsp / internal / document_hover / under_cursor.vim
1 " https://microsoft.github.io/language-server-protocol/specification#textDocument_hover
2
3 let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown')
4 let s:MarkupContent = vital#lsp#import('VS.LSP.MarkupContent')
5 let s:FloatingWindow = vital#lsp#import('VS.Vim.Window.FloatingWindow')
6 let s:Window = vital#lsp#import('VS.Vim.Window')
7 let s:Buffer = vital#lsp#import('VS.Vim.Buffer')
8
9 " options - {
10 "   server - 'server_name'              " optional
11 "   ui - 'float' | 'preview'
12 " }
13 function! lsp#internal#document_hover#under_cursor#do(options) abort
14     let l:bufnr = bufnr('%')
15     let l:ui = get(a:options, 'ui', g:lsp_hover_ui)
16     if empty(l:ui)
17         let l:ui = s:FloatingWindow.is_available() ? 'float' : 'preview'
18     endif
19
20     if l:ui ==# 'float'
21         let l:doc_win = s:get_doc_win()
22         if l:doc_win.is_visible()
23             if bufnr('%') ==# l:doc_win.get_bufnr()
24                 call s:close_floating_window()
25             else
26                 call l:doc_win.enter()
27                 inoremap <buffer><silent> <Plug>(lsp-float-close) <ESC>:<C-u>call <SID>close_floating_window()<CR>
28                 nnoremap <buffer><silent> <Plug>(lsp-float-close) :<C-u>call <SID>close_floating_window()<CR>
29                 execute('doautocmd <nomodeline> User lsp_float_focused')
30                 if !hasmapto('<Plug>(lsp-float-close)')
31                     imap <silent> <buffer> <C-c> <Plug>(lsp-float-close)
32                     nmap  <silent> <buffer> <C-c> <Plug>(lsp-float-close)
33                 endif
34             endif
35             return
36         endif
37     endif
38
39     if has_key(a:options, 'server')
40         let l:servers = [a:options['server']]
41     else
42         let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_hover_provider(v:val)')
43     endif
44
45     if len(l:servers) == 0
46         let l:filetype = getbufvar(l:bufnr, '&filetype')
47         call lsp#utils#error('textDocument/hover not supported for ' . l:filetype)
48         return
49     endif
50
51     redraw | echo 'Retrieving hover ...'
52
53     call lsp#_new_command()
54
55     " TODO: ask user to select server for formatting if there are multiple servers
56     let l:request = {
57         \ 'method': 'textDocument/hover',
58         \ 'params': {
59         \   'textDocument': lsp#get_text_document_identifier(),
60         \   'position': lsp#get_position(),
61         \ },
62         \ }
63     call lsp#callbag#pipe(
64         \ lsp#callbag#fromList(l:servers),
65         \ lsp#callbag#flatMap({server->
66         \   lsp#request(server, l:request)
67         \ }),
68         \ lsp#callbag#tap({x->s:show_hover(l:ui, x['server_name'], x['request'], x['response'])}),
69         \ lsp#callbag#takeUntil(lsp#callbag#pipe(
70         \   lsp#stream(),
71         \   lsp#callbag#filter({x->has_key(x, 'command')}),
72         \ )),
73         \ lsp#callbag#subscribe(),
74         \ )
75 endfunction
76
77 function! lsp#internal#document_hover#under_cursor#getpreviewwinid() abort
78     if exists('s:doc_win')
79         return s:doc_win.get_winid()
80     endif
81     return v:null
82 endfunction
83
84 function! s:show_hover(ui, server_name, request, response) abort
85     if !has_key(a:response, 'result') || empty(a:response['result']) || 
86         \ empty(a:response['result']['contents'])
87         call lsp#utils#error('No hover information found in server - ' . a:server_name)
88         return
89     endif
90
91     echo ''
92
93     if s:FloatingWindow.is_available() && a:ui ==? 'float'
94         call s:show_floating_window(a:server_name, a:request, a:response)
95     else
96         call s:show_preview_window(a:server_name, a:request, a:response)
97     endif
98 endfunction
99
100 function! s:show_preview_window(server_name, request, response) abort
101     let l:contents = s:get_contents(a:response['result']['contents'])
102
103     " Ignore if contents is empty.
104     if empty(l:contents)
105         call lsp#utils#error('Empty contents for LspHover')
106         return
107     endif
108
109     let l:lines = lsp#utils#_split_by_eol(join(l:contents, "\n\n"))
110     let l:view = winsaveview()
111     let l:alternate=@#
112     silent! pclose
113     sp LspHoverPreview
114     execute 'resize '.min([len(l:lines), &previewheight])
115     set previewwindow
116     setlocal conceallevel=2
117     setlocal bufhidden=hide
118     setlocal nobuflisted
119     setlocal buftype=nofile
120     setlocal noswapfile
121     %d _
122     call setline(1, l:lines)
123     call s:Window.do(win_getid(), {->s:Markdown.apply()})
124     execute "normal \<c-w>p"
125     call winrestview(l:view)
126     let @#=l:alternate
127 endfunction
128
129 function! s:show_floating_window(server_name, request, response) abort
130     call s:close_floating_window()
131
132     let l:contents = s:get_contents(a:response['result']['contents'])
133
134     " Ignore if contents is empty.
135     if empty(l:contents)
136         return s:close_floating_window()
137     endif
138
139     " Update contents.
140     let l:doc_win = s:get_doc_win()
141     silent! call deletebufline(l:doc_win.get_bufnr(), 1, '$')
142     call setbufline(l:doc_win.get_bufnr(), 1, lsp#utils#_split_by_eol(join(l:contents, "\n\n")))
143
144     " Calculate layout.
145     if g:lsp_float_max_width >= 1
146         let l:maxwidth = g:lsp_float_max_width
147     elseif g:lsp_float_max_width == 0
148         let l:maxwidth = &columns
149     else
150         let l:maxwidth = float2nr(&columns * 0.4)
151     endif
152     let l:size = l:doc_win.get_size({
153         \   'maxwidth': l:maxwidth,
154         \   'maxheight': float2nr(&lines * 0.4),
155         \ })
156     let l:pos = s:compute_position(l:size)
157     if empty(l:pos)
158         call s:close_floating_window()
159         return
160     endif
161
162     execute printf('augroup vim_lsp_hover_close_on_move_%d', bufnr('%'))
163         " vint: -ProhibitAutocmdWithNoGroup
164         autocmd!
165         autocmd InsertEnter,BufLeave <buffer> call s:close_floating_window()
166         execute printf('autocmd CursorMoved <buffer> call s:close_floating_window_on_move(%s)', getcurpos())
167         " vint: +ProhibitAutocmdWithNoGroup
168     augroup END
169
170    " Show popupmenu and apply markdown syntax.
171     call l:doc_win.open({
172         \   'row': l:pos[0],
173         \   'col': l:pos[1],
174         \   'width': l:size.width,
175         \   'height': l:size.height,
176         \   'border': v:true,
177         \ })
178     call s:Window.do(l:doc_win.get_winid(), { -> s:Markdown.apply() })
179
180     " Format contents to fit window
181     call setbufvar(l:doc_win.get_bufnr(), '&textwidth', l:size.width)
182     call s:Window.do(l:doc_win.get_winid(), { -> s:format_window() })
183 endfunction
184
185 function! s:format_window() abort
186     global/^/normal! gqgq
187 endfunction
188
189 function! s:get_contents(contents) abort
190     if type(a:contents) == type('')
191         return [a:contents]
192     elseif type(a:contents) == type([])
193         let l:result = []
194         for l:content in a:contents
195             let l:result += s:get_contents(l:content)
196         endfor
197         return l:result
198     elseif type(a:contents) == type({})
199         if has_key(a:contents, 'value')
200             if has_key(a:contents, 'kind')
201                 if a:contents['kind'] ==? 'markdown'
202                     let l:detail = s:MarkupContent.normalize(a:contents['value'], {
203                     \   'compact': !g:lsp_preview_fixup_conceal
204                     \ })
205                     return [l:detail]
206                 else
207                     return [a:contents['value']]
208                 endif
209             elseif has_key(a:contents, 'language')
210                 let l:detail = s:MarkupContent.normalize(a:contents, {
211                 \     'compact': !g:lsp_preview_fixup_conceal
212                 \ })
213                 return [l:detail]
214             else
215                 return ''
216             endif
217         else
218             return ''
219         endif
220     else
221         return ''
222     endif
223 endfunction
224
225 function! s:close_floating_window() abort
226     call s:get_doc_win().close()
227 endfunction
228
229 function! s:close_floating_window_on_move(curpos) abort
230     if a:curpos != getcurpos() | call s:close_floating_window() | endif
231 endf
232
233 function! s:on_opened() abort
234     inoremap <buffer><silent> <Plug>(lsp-float-close) <ESC>:<C-u>call <SID>close_floating_window()<CR>
235     nnoremap <buffer><silent> <Plug>(lsp-float-close) :<C-u>call <SID>close_floating_window()<CR>
236     execute('doautocmd <nomodeline> User lsp_float_opened')
237     if !hasmapto('<Plug>(lsp-float-close)')
238         imap <silent> <buffer> <C-c> <Plug>(lsp-float-close)
239         nmap  <silent> <buffer> <C-c> <Plug>(lsp-float-close)
240     endif
241 endfunction
242
243 function! s:on_closed() abort
244     execute('doautocmd <nomodeline> User lsp_float_closed')
245 endfunction
246
247 function! s:get_doc_win() abort
248     if exists('s:doc_win')
249         return s:doc_win
250     endif
251
252     let s:doc_win = s:FloatingWindow.new({
253         \   'on_opened': function('s:on_opened'),
254         \   'on_closed': function('s:on_closed')
255         \ })
256     call s:doc_win.set_var('&wrap', 1)
257     call s:doc_win.set_var('&conceallevel', 2)
258     call s:doc_win.set_bufnr(s:Buffer.create())
259     call setbufvar(s:doc_win.get_bufnr(), '&buftype', 'nofile')
260     call setbufvar(s:doc_win.get_bufnr(), '&bufhidden', 'hide')
261     call setbufvar(s:doc_win.get_bufnr(), '&buflisted', 0)
262     call setbufvar(s:doc_win.get_bufnr(), '&swapfile', 0)
263     return s:doc_win
264 endfunction
265
266 function! s:compute_position(size) abort
267     let l:pos = screenpos(0, line('.'), col('.'))
268     if l:pos.row == 0 && l:pos.col == 0
269         " workaround for float position
270         let l:pos = {'curscol': wincol(), 'row': winline()}
271     endif
272     let l:pos = [l:pos.row + 1, l:pos.curscol + 1]
273     if l:pos[0] + a:size.height > &lines
274         let l:pos[0] = l:pos[0] - a:size.height - 3
275     endif
276     if l:pos[1] + a:size.width > &columns
277         let l:pos[1] = l:pos[1] - a:size.width - 3
278     endif
279     return l:pos
280 endfunction
281