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 " Author: w0rp <devw0rp@gmail.com>
2 " Description: Hover support for LSP linters.
6 " Used to get the hover map in tests.
7 function! ale#hover#GetMap() abort
8 return deepcopy(s:hover_map)
11 " Used to set the hover map in tests.
12 function! ale#hover#SetMap(map) abort
13 let s:hover_map = a:map
16 function! ale#hover#ClearLSPData() abort
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)
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')
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', '')
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',
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)
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',
55 elseif g:ale_hover_to_preview
56 call ale#preview#Show(split(a:response.body.displayString, "\n"), {
57 \ 'filetype': 'ale-preview.message',
61 call ale#util#ShowMessage(a:response.body.displayString)
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
73 " Cache syntax file (non-)existence to avoid calling globpath repeatedly.
74 let s:syntax_file_exists_cache = {}
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))
82 return s:syntax_file_exists_cache[a:syntax_file]
85 function! ale#hover#ParseLSPResult(contents) abort
89 let l:list = type(a:contents) is v:t_list ? a:contents : [a:contents]
90 let l:region_index = 0
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"))
110 let l:marked_list = []
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 = []
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, '^``` *\([^ ]\+\)\? *$')
123 let l:fence_language = len(l:match) > 1 ? l:match[1] : 'text'
125 if !empty(l:marked_list)
126 call add(l:fence_lines, '')
129 if !empty(l:marked_list)
130 \&& l:marked_list[-1][0] isnot v:null
131 call add(l:marked_list, [v:null, ['']])
134 call add(l:marked_list, [v:null, [l:line]])
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 = []
143 " Gather lines inside of a code fence.
144 call add(l:fence_lines, l:line)
147 " If the result from the LSP server is a {language: ..., value: ...}
148 " Dictionary, then that should be interpreted as if it was:
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
160 \ [l:item.language, split(l:item.value, "\n")],
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.
169 \ 'substitute(v:val, ''\\_'', ''_'', ''g'')',
172 let l:language = s:ConvertLanguageName(l:language)
174 if !empty(l:language)
175 let l:syntax_file = printf('syntax/%s.vim', l:language)
177 if s:SyntaxFileExists(l:syntax_file)
178 let l:includes[l:language] = l:syntax_file
181 let l:start = len(l:lines) + 1
182 let l:end = l:start + len(l:marked_lines)
183 let l:region_index += 1
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
194 call extend(l:lines, l:marked_lines)
198 let l:include_commands = []
200 for [l:language, l:lang_path] in sort(items(l:includes))
201 call add(l:include_commands, 'unlet! b:current_syntax')
203 \ l:include_commands,
204 \ printf('syntax include @ALE_hover_%s %s', l:language, l:lang_path),
208 return [l:include_commands + l:highlights, l:lines]
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)
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))
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.
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)
233 if l:result is v:null
237 let [l:commands, l:lines] = ale#hover#ParseLSPResult(l:result.contents)
240 let l:set_balloons = ale#Var(l:options.buffer, 'set_balloons')
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'))
250 call ale#cursor#TruncatedEcho(l:lines[0])
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,
257 elseif g:ale_hover_to_preview
258 call ale#preview#Show(l:lines, {
259 \ 'filetype': 'ale-preview.message',
261 \ 'commands': l:commands,
264 call ale#util#ShowMessage(join(l:lines, "\n"), {
265 \ 'commands': l:commands,
272 function! s:OnReady(line, column, opt, linter, lsp_details) abort
273 let l:id = a:lsp_details.connection_id
275 if !ale#lsp#HasCapability(l:id, 'hover')
279 let l:buffer = a:lsp_details.buffer
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)
286 if a:linter.lsp is# 'tsserver'
287 let l:column = a:column
289 let l:message = ale#lsp#tsserver_message#Quickinfo(
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)
300 \ min([a:column, len(getbufline(l:buffer, a:line)[0])]),
304 let l:message = ale#lsp#message#Hover(l:buffer, a:line, l:column)
307 let l:request_id = ale#lsp#Send(l:id, l:message)
309 let s:hover_map[l:request_id] = {
310 \ 'buffer': l:buffer,
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),
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
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])
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)
340 let s:last_pos = [0, 0, 0]
342 " This function implements the :ALEHover command.
343 function! ale#hover#ShowAtCursor() abort
344 let l:buffer = bufnr('')
345 let l:pos = getpos('.')
347 call ale#hover#Show(l:buffer, l:pos[1], l:pos[2], {})
350 function! ale#hover#ShowTruncatedMessageAtCursor() abort
351 let l:buffer = bufnr('')
352 let l:pos = getpos('.')[0:2]
354 if !getbufvar(l:buffer, 'ale_enabled', 1)
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)
367 \ {'truncated_echo': 1},
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}
379 call ale#hover#Show(l:buffer, l:pos[1], l:pos[2], l:options)