]> git.madduck.net Git - etc/vim.git/blob - autoload/lsp/internal/completion/documentation.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 / completion / documentation.vim
1 " https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
2 let s:enabled = 0
3
4 let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown')
5 let s:MarkupContent = vital#lsp#import('VS.LSP.MarkupContent')
6 let s:FloatingWindow = vital#lsp#import('VS.Vim.Window.FloatingWindow')
7 let s:Window = vital#lsp#import('VS.Vim.Window')
8 let s:Buffer = vital#lsp#import('VS.Vim.Buffer')
9
10 function! lsp#internal#completion#documentation#_enable() abort
11     " don't even bother registering if the feature is disabled
12     if !g:lsp_completion_documentation_enabled | return | endif
13
14     if !s:FloatingWindow.is_available() | return | endif
15     if !exists('##CompleteChanged') | return | endif
16
17     if s:enabled | return | endif
18     let s:enabled = 1
19
20     let s:Dispose = lsp#callbag#pipe(
21         \ lsp#callbag#merge(
22         \   lsp#callbag#pipe(
23         \       lsp#callbag#fromEvent('CompleteChanged'),
24         \       lsp#callbag#filter({_->g:lsp_completion_documentation_enabled}),
25         \       lsp#callbag#map({->copy(v:event)}),
26         \       lsp#callbag#debounceTime(g:lsp_completion_documentation_delay),
27         \       lsp#callbag#switchMap({event->
28         \           lsp#callbag#pipe(
29         \               s:resolve_completion(event),
30         \               lsp#callbag#tap({managed_user_data->s:show_floating_window(event, managed_user_data)}),
31         \               lsp#callbag#takeUntil(lsp#callbag#fromEvent('CompleteDone'))
32         \           )
33         \       })
34         \   ),
35         \   lsp#callbag#pipe(
36         \       lsp#callbag#fromEvent('CompleteDone'),
37         \       lsp#callbag#tap({_->s:close_floating_window(v:false)}),
38         \   )
39         \ ),
40         \ lsp#callbag#subscribe(),
41         \ )
42 endfunction
43
44 function! s:resolve_completion(event) abort
45     let l:managed_user_data = lsp#omni#get_managed_user_data_from_completed_item(a:event['completed_item'])
46     if empty(l:managed_user_data)
47         return lsp#callbag#of({})
48     endif
49
50     let l:completion_item = l:managed_user_data['completion_item']
51
52     if has_key(l:completion_item, 'documentation')
53         return lsp#callbag#of(l:managed_user_data)
54     elseif lsp#capabilities#has_completion_resolve_provider(l:managed_user_data['server_name'])
55         return lsp#callbag#pipe(
56             \ lsp#request(l:managed_user_data['server_name'], {
57             \   'method': 'completionItem/resolve',
58             \   'params': l:completion_item,
59             \ }),
60             \ lsp#callbag#map({x->{
61             \   'server_name': l:managed_user_data['server_name'],
62             \   'completion_item': x['response']['result'],
63             \   'complete_position': l:managed_user_data['complete_position'],
64             \ }})
65             \ )
66     else
67         return lsp#callbag#of(l:managed_user_data)
68     endif
69 endfunction
70
71 function! s:show_floating_window(event, managed_user_data) abort
72     if empty(a:managed_user_data) || !pumvisible()
73         call s:close_floating_window(v:true)
74         return
75     endif
76     let l:completion_item = a:managed_user_data['completion_item']
77
78     let l:contents = []
79
80     " Add detail field if provided.
81     if type(get(l:completion_item, 'detail', v:null)) == type('')
82         if !empty(l:completion_item.detail)
83             let l:detail = s:MarkupContent.normalize({
84             \     'language': &filetype,
85             \     'value': l:completion_item['detail'],
86             \ }, {
87             \     'compact': !g:lsp_preview_fixup_conceal
88             \ })
89             let l:contents += [l:detail]
90         endif
91     endif
92
93     " Add documentation filed if provided.
94     let l:documentation = s:MarkupContent.normalize(get(l:completion_item, 'documentation', ''), {
95     \     'compact': !g:lsp_preview_fixup_conceal
96     \ })
97     if !empty(l:documentation)
98         let l:contents += [l:documentation]
99     endif
100
101     " Ignore if contents is empty.
102     if empty(l:contents)
103         return s:close_floating_window(v:true)
104     endif
105
106     " Update contents.
107     let l:doc_win = s:get_doc_win()
108     call deletebufline(l:doc_win.get_bufnr(), 1, '$')
109     call setbufline(l:doc_win.get_bufnr(), 1, lsp#utils#_split_by_eol(join(l:contents, "\n\n")))
110
111     " Calculate layout.
112     if g:lsp_float_max_width >= 1
113         let l:maxwidth = g:lsp_float_max_width
114     elseif g:lsp_float_max_width == 0
115         let l:maxwidth = &columns
116     else
117         let l:maxwidth = float2nr(&columns * 0.4)
118     endif
119     let l:size = l:doc_win.get_size({
120     \     'maxwidth': l:maxwidth,
121     \     'maxheight': float2nr(&lines * 0.4),
122     \ })
123     let l:margin_right = &columns - 1 - (float2nr(a:event.col) + float2nr(a:event.width) + 1 + (a:event.scrollbar ? 1 : 0))
124     let l:margin_left = float2nr(a:event.col) - 3
125     if l:size.width < l:margin_right
126       " do nothing
127     elseif l:margin_left <= l:margin_right
128       let l:size.width = l:margin_right
129     else
130       let l:size.width = l:margin_left
131     endif
132     let l:pos = s:compute_position(a:event, l:size)
133     if empty(l:pos)
134         call s:close_floating_window(v:true)
135         return
136     endif
137
138     " Show popupmenu and apply markdown syntax.
139     call l:doc_win.open({
140     \     'row': l:pos[0] + 1,
141     \     'col': l:pos[1] + 1,
142     \     'width': l:size.width,
143     \     'height': l:size.height,
144     \     'border': v:true,
145     \     'topline': 1,
146     \ })
147     call s:Window.do(l:doc_win.get_winid(), { -> s:Markdown.apply() })
148 endfunction
149
150 function! s:close_floating_window(force) abort
151     " Ignore `CompleteDone` if it occurred by `complete()` because in this case, the popup menu will re-appear immediately.
152     let l:ctx = {}
153     function! l:ctx.callback(force) abort
154         if !pumvisible() || a:force
155             call s:get_doc_win().close()
156         endif
157     endfunction
158     call timer_start(1, { -> l:ctx.callback(a:force) })
159 endfunction
160
161 function! s:compute_position(event, size) abort
162     let l:col_if_right = a:event.col + a:event.width + 1 + (a:event.scrollbar ? 1 : 0)
163     let l:col_if_left = a:event.col - a:size.width - 2
164
165     if a:size.width >= (&columns - l:col_if_right)
166         let l:col = l:col_if_left
167     else
168         let l:col = l:col_if_right
169     endif
170
171     if l:col <= 0
172         return []
173     endif
174     if &columns <= l:col + a:size.width
175         return []
176     endif
177
178     return [a:event.row, l:col]
179 endfunction
180
181 function! s:get_doc_win() abort
182     if exists('s:doc_win')
183         return s:doc_win
184     endif
185
186     let s:doc_win = s:FloatingWindow.new({
187     \   'on_opened': { -> execute('doautocmd <nomodeline> User lsp_float_opened') },
188     \   'on_closed': { -> execute('doautocmd <nomodeline> User lsp_float_closed') }
189     \ })
190     call s:doc_win.set_var('&wrap', 1)
191     call s:doc_win.set_var('&conceallevel', 2)
192     noautocmd silent let l:bufnr = s:Buffer.create()
193     call s:doc_win.set_bufnr(l:bufnr)
194     call setbufvar(s:doc_win.get_bufnr(), '&buftype', 'nofile')
195     call setbufvar(s:doc_win.get_bufnr(), '&bufhidden', 'hide')
196     call setbufvar(s:doc_win.get_bufnr(), '&buflisted', 0)
197     call setbufvar(s:doc_win.get_bufnr(), '&swapfile', 0)
198     return s:doc_win
199 endfunction
200
201 function! lsp#internal#completion#documentation#_disable() abort
202     if !s:enabled | return | endif
203     if exists('s:Dispose')
204         call s:Dispose()
205         unlet s:Dispose
206     endif
207 endfunction