]> git.madduck.net Git - etc/vim.git/blob - autoload/ale/hover.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/ale/' content from commit 22185c4c
[etc/vim.git] / autoload / ale / hover.vim
1 " Author: w0rp <devw0rp@gmail.com>
2 " Description: Hover support for LSP linters.
3
4 let s:hover_map = {}
5
6 " Used to get the hover map in tests.
7 function! ale#hover#GetMap() abort
8     return deepcopy(s:hover_map)
9 endfunction
10
11 " Used to set the hover map in tests.
12 function! ale#hover#SetMap(map) abort
13     let s:hover_map = a:map
14 endfunction
15
16 function! ale#hover#ClearLSPData() abort
17     let s:hover_map = {}
18 endfunction
19
20 function! ale#hover#HandleTSServerResponse(conn_id, response) abort
21     if get(a:response, 'command', '') is# 'quickinfo'
22     \&& has_key(s:hover_map, a:response.request_seq)
23         let l:options = remove(s:hover_map, a:response.request_seq)
24
25         if get(a:response, 'success', v:false) is v:true
26         \&& get(a:response, 'body', v:null) isnot v:null
27             let l:set_balloons = ale#Var(l:options.buffer, 'set_balloons')
28
29             " If we pass the show_documentation flag, we should show the full
30             " documentation, and always in the preview window.
31             if get(l:options, 'show_documentation', 0)
32                 let l:documentation = get(a:response.body, 'documentation', '')
33
34                 " displayString is not included here, because it can be very
35                 " noisy and run on for many lines for complex types. A less
36                 " verbose alternative may be nice in future.
37                 if !empty(l:documentation)
38                     call ale#preview#Show(split(l:documentation, "\n"), {
39                     \   'filetype': 'ale-preview.message',
40                     \   'stay_here': 1,
41                     \})
42                 endif
43             elseif get(l:options, 'hover_from_balloonexpr', 0)
44             \&& exists('*balloon_show')
45             \&& (l:set_balloons is 1 || l:set_balloons is# 'hover')
46                 call balloon_show(a:response.body.displayString)
47             elseif get(l:options, 'truncated_echo', 0)
48                 if !empty(a:response.body.displayString)
49                     call ale#cursor#TruncatedEcho(a:response.body.displayString)
50                 endif
51             elseif g:ale_hover_to_floating_preview || g:ale_floating_preview
52                 call ale#floating_preview#Show(split(a:response.body.displayString, "\n"), {
53                 \   'filetype': 'ale-preview.message',
54                 \})
55             elseif g:ale_hover_to_preview
56                 call ale#preview#Show(split(a:response.body.displayString, "\n"), {
57                 \   'filetype': 'ale-preview.message',
58                 \   'stay_here': 1,
59                 \})
60             else
61                 call ale#util#ShowMessage(a:response.body.displayString)
62             endif
63         endif
64     endif
65 endfunction
66
67 " Convert a language name to another one.
68 " The language name could be an empty string or v:null
69 function! s:ConvertLanguageName(language) abort
70     return a:language
71 endfunction
72
73 " Cache syntax file (non-)existence to avoid calling globpath repeatedly.
74 let s:syntax_file_exists_cache = {}
75
76 function! s:SyntaxFileExists(syntax_file) abort
77     if !has_key(s:syntax_file_exists_cache, a:syntax_file)
78         let s:syntax_file_exists_cache[a:syntax_file] =
79         \   !empty(globpath(&runtimepath, a:syntax_file))
80     endif
81
82     return s:syntax_file_exists_cache[a:syntax_file]
83 endfunction
84
85 function! ale#hover#ParseLSPResult(contents) abort
86     let l:includes = {}
87     let l:highlights = []
88     let l:lines = []
89     let l:list = type(a:contents) is v:t_list ? a:contents : [a:contents]
90     let l:region_index = 0
91
92     for l:item in l:list
93         if !empty(l:lines)
94             call add(l:lines, '')
95         endif
96
97         if type(l:item) is v:t_dict && has_key(l:item, 'kind')
98             if l:item.kind is# 'markdown'
99                 " Handle markdown values as we handle strings below.
100                 let l:item = get(l:item, 'value', '')
101             elseif l:item.kind is# 'plaintext'
102                 " We shouldn't try to parse plaintext as markdown.
103                 " Pass the lines on and skip parsing them.
104                 call extend(l:lines, split(get(l:item, 'value', ''), "\n"))
105
106                 continue
107             endif
108         endif
109
110         let l:marked_list = []
111
112         " If the item is a string, then we should parse it as Markdown text.
113         if type(l:item) is v:t_string
114             let l:fence_language = v:null
115             let l:fence_lines = []
116
117             for l:line in split(l:item, "\n")
118                 if l:fence_language is v:null
119                     " Look for the start of a code fence. (```python, etc.)
120                     let l:match = matchlist(l:line, '^``` *\([^ ]\+\)\? *$')
121
122                     if !empty(l:match)
123                         let l:fence_language = len(l:match) > 1 ? l:match[1] : 'text'
124
125                         if !empty(l:marked_list)
126                             call add(l:fence_lines, '')
127                         endif
128                     else
129                         if !empty(l:marked_list)
130                         \&& l:marked_list[-1][0] isnot v:null
131                             call add(l:marked_list, [v:null, ['']])
132                         endif
133
134                         call add(l:marked_list, [v:null, [l:line]])
135                     endif
136                 elseif l:line =~# '^```$'
137                     " When we hit the end of a code fence, pass the fenced
138                     " lines on to the next steps below.
139                     call add(l:marked_list, [l:fence_language, l:fence_lines])
140                     let l:fence_language = v:null
141                     let l:fence_lines = []
142                 else
143                     " Gather lines inside of a code fence.
144                     call add(l:fence_lines, l:line)
145                 endif
146             endfor
147         " If the result from the LSP server is a {language: ..., value: ...}
148         " Dictionary, then that should be interpreted as if it was:
149         "
150         " ```${language}
151         " ${value}
152         " ```
153         elseif type(l:item) is v:t_dict
154         \&& has_key(l:item, 'language')
155         \&& type(l:item.language) is v:t_string
156         \&& has_key(l:item, 'value')
157         \&& type(l:item.value) is v:t_string
158             call add(
159             \   l:marked_list,
160             \   [l:item.language, split(l:item.value, "\n")],
161             \)
162         endif
163
164         for [l:language, l:marked_lines] in l:marked_list
165             if l:language is v:null
166                 " NOTE: We could handle other Markdown formatting here.
167                 call map(
168                 \   l:marked_lines,
169                 \   'substitute(v:val, ''\\_'', ''_'', ''g'')',
170                 \)
171             else
172                 let l:language = s:ConvertLanguageName(l:language)
173
174                 if !empty(l:language)
175                     let l:syntax_file = printf('syntax/%s.vim', l:language)
176
177                     if s:SyntaxFileExists(l:syntax_file)
178                         let l:includes[l:language] = l:syntax_file
179                     endif
180
181                     let l:start = len(l:lines) + 1
182                     let l:end = l:start + len(l:marked_lines)
183                     let l:region_index += 1
184
185                     call add(l:highlights, 'syntax region'
186                     \   . ' ALE_hover_' . l:region_index
187                     \   . ' start=/\%' . l:start . 'l/'
188                     \   . ' end=/\%' . l:end . 'l/'
189                     \   . ' contains=@ALE_hover_' . l:language
190                     \)
191                 endif
192             endif
193
194             call extend(l:lines, l:marked_lines)
195         endfor
196     endfor
197
198     let l:include_commands = []
199
200     for [l:language, l:lang_path] in sort(items(l:includes))
201         call add(l:include_commands, 'unlet! b:current_syntax')
202         call add(
203         \   l:include_commands,
204         \   printf('syntax include @ALE_hover_%s %s', l:language, l:lang_path),
205         \)
206     endfor
207
208     return [l:include_commands + l:highlights, l:lines]
209 endfunction
210
211 function! ale#hover#HandleLSPResponse(conn_id, response) abort
212     if has_key(a:response, 'id')
213     \&& has_key(s:hover_map, a:response.id)
214         let l:options = remove(s:hover_map, a:response.id)
215
216         " If the call did __not__ come from balloonexpr...
217         if !get(l:options, 'hover_from_balloonexpr', 0)
218             let l:buffer = bufnr('')
219             let [l:line, l:column] = getpos('.')[1:2]
220             let l:end = len(getline(l:line))
221
222             if l:buffer isnot l:options.buffer
223             \|| l:line isnot l:options.line
224             \|| min([l:column, l:end]) isnot min([l:options.column, l:end])
225                 " ... Cancel display the message if the cursor has moved.
226                 return
227             endif
228         endif
229
230         " The result can be a Dictionary item, a List of the same, or null.
231         let l:result = get(a:response, 'result', v:null)
232
233         if l:result is v:null
234             return
235         endif
236
237         let [l:commands, l:lines] = ale#hover#ParseLSPResult(l:result.contents)
238
239         if !empty(l:lines)
240             let l:set_balloons = ale#Var(l:options.buffer, 'set_balloons')
241
242             if get(l:options, 'hover_from_balloonexpr', 0)
243             \&& exists('*balloon_show')
244             \&& (l:set_balloons is 1 || l:set_balloons is# 'hover')
245                 call balloon_show(join(l:lines, "\n"))
246             elseif get(l:options, 'truncated_echo', 0)
247                 if type(l:lines[0]) is# v:t_list
248                     call ale#cursor#TruncatedEcho(join(l:lines[0], '\n'))
249                 else
250                     call ale#cursor#TruncatedEcho(l:lines[0])
251                 endif
252             elseif g:ale_hover_to_floating_preview || g:ale_floating_preview
253                 call ale#floating_preview#Show(l:lines, {
254                 \   'filetype': 'ale-preview.message',
255                 \   'commands': l:commands,
256                 \})
257             elseif g:ale_hover_to_preview
258                 call ale#preview#Show(l:lines, {
259                 \   'filetype': 'ale-preview.message',
260                 \   'stay_here': 1,
261                 \   'commands': l:commands,
262                 \})
263             else
264                 call ale#util#ShowMessage(join(l:lines, "\n"), {
265                 \   'commands': l:commands,
266                 \})
267             endif
268         endif
269     endif
270 endfunction
271
272 function! s:OnReady(line, column, opt, linter, lsp_details) abort
273     let l:id = a:lsp_details.connection_id
274
275     if !ale#lsp#HasCapability(l:id, 'hover')
276         return
277     endif
278
279     let l:buffer = a:lsp_details.buffer
280
281     let l:Callback = a:linter.lsp is# 'tsserver'
282     \   ? function('ale#hover#HandleTSServerResponse')
283     \   : function('ale#hover#HandleLSPResponse')
284     call ale#lsp#RegisterCallback(l:id, l:Callback)
285
286     if a:linter.lsp is# 'tsserver'
287         let l:column = a:column
288
289         let l:message = ale#lsp#tsserver_message#Quickinfo(
290         \   l:buffer,
291         \   a:line,
292         \   l:column
293         \)
294     else
295         " Send a message saying the buffer has changed first, or the
296         " hover position probably won't make sense.
297         call ale#lsp#NotifyForChanges(l:id, l:buffer)
298
299         let l:column = max([
300         \   min([a:column, len(getbufline(l:buffer, a:line)[0])]),
301         \   1,
302         \])
303
304         let l:message = ale#lsp#message#Hover(l:buffer, a:line, l:column)
305     endif
306
307     let l:request_id = ale#lsp#Send(l:id, l:message)
308
309     let s:hover_map[l:request_id] = {
310     \   'buffer': l:buffer,
311     \   'line': a:line,
312     \   'column': l:column,
313     \   'hover_from_balloonexpr': get(a:opt, 'called_from_balloonexpr', 0),
314     \   'show_documentation': get(a:opt, 'show_documentation', 0),
315     \   'truncated_echo': get(a:opt, 'truncated_echo', 0),
316     \}
317 endfunction
318
319 " Obtain Hover information for the specified position
320 " Pass optional arguments in the dictionary opt.
321 " Currently, only one key/value is useful:
322 "   - called_from_balloonexpr, this flag marks if we want the result from this
323 "     ale#hover#Show to display in a balloon if possible
324 "
325 " Currently, the callbacks displays the info from hover :
326 " - in the balloon if opt.called_from_balloonexpr and balloon_show is detected
327 " - as status message otherwise
328 function! ale#hover#Show(buffer, line, col, opt) abort
329     let l:show_documentation = get(a:opt, 'show_documentation', 0)
330     let l:Callback = function('s:OnReady', [a:line, a:col, a:opt])
331
332     for l:linter in ale#lsp_linter#GetEnabled(a:buffer)
333         " Only tsserver supports documentation requests at the moment.
334         if !l:show_documentation || l:linter.lsp is# 'tsserver'
335             call ale#lsp_linter#StartLSP(a:buffer, l:linter, l:Callback)
336         endif
337     endfor
338 endfunction
339
340 let s:last_pos = [0, 0, 0]
341
342 " This function implements the :ALEHover command.
343 function! ale#hover#ShowAtCursor() abort
344     let l:buffer = bufnr('')
345     let l:pos = getpos('.')
346
347     call ale#hover#Show(l:buffer, l:pos[1], l:pos[2], {})
348 endfunction
349
350 function! ale#hover#ShowTruncatedMessageAtCursor() abort
351     let l:buffer = bufnr('')
352     let l:pos = getpos('.')[0:2]
353
354     if !getbufvar(l:buffer, 'ale_enabled', 1)
355         return
356     endif
357
358     if l:pos != s:last_pos
359         let s:last_pos = l:pos
360         let [l:info, l:loc] = ale#util#FindItemAtCursor(l:buffer)
361
362         if empty(l:loc)
363             call ale#hover#Show(
364             \   l:buffer,
365             \   l:pos[1],
366             \   l:pos[2],
367             \   {'truncated_echo': 1},
368             \)
369         endif
370     endif
371 endfunction
372
373 " This function implements the :ALEDocumentation command.
374 function! ale#hover#ShowDocumentationAtCursor() abort
375     let l:buffer = bufnr('')
376     let l:pos = getpos('.')
377     let l:options = {'show_documentation': 1}
378
379     call ale#hover#Show(l:buffer, l:pos[1], l:pos[2], l:options)
380 endfunction