summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--autoload/ale/hover.vim182
-rw-r--r--autoload/ale/preview.vim4
-rw-r--r--autoload/ale/util.vim15
-rw-r--r--test/test_hover.vader38
-rw-r--r--test/test_hover_parsing.vader173
5 files changed, 360 insertions, 52 deletions
diff --git a/autoload/ale/hover.vim b/autoload/ale/hover.vim
index a6462227..fe4e5da7 100644
--- a/autoload/ale/hover.vim
+++ b/autoload/ale/hover.vim
@@ -56,6 +56,137 @@ function! ale#hover#HandleTSServerResponse(conn_id, response) abort
endif
endfunction
+" Convert a language name to another one.
+" The language name could be an empty string or v:null
+function! s:ConvertLanguageName(language) abort
+ return a:language
+endfunction
+
+function! ale#hover#ParseLSPResult(contents) abort
+ let l:includes = {}
+ let l:highlights = []
+ let l:lines = []
+ let l:list = type(a:contents) is v:t_list ? a:contents : [a:contents]
+ let l:region_index = 0
+
+ for l:item in l:list
+ if !empty(l:lines)
+ call add(l:lines, '')
+ endif
+
+ if type(l:item) is v:t_dict && has_key(l:item, 'kind')
+ if l:item.kind is# 'markdown'
+ " Handle markdown values as we handle strings below.
+ let l:item = get(l:item, 'value', '')
+ elseif l:item.kind is# 'plaintext'
+ " We shouldn't try to parse plaintext as markdown.
+ " Pass the lines on and skip parsing them.
+ call extend(l:lines, split(get(l:item, 'value', ''), "\n"))
+
+ continue
+ endif
+ endif
+
+ let l:marked_list = []
+
+ " If the item is a string, then we should parse it as Markdown text.
+ if type(l:item) is v:t_string
+ let l:fence_language = v:null
+ let l:fence_lines = []
+
+ for l:line in split(l:item, "\n")
+ if l:fence_language is v:null
+ " Look for the start of a code fence. (```python, etc.)
+ let l:match = matchlist(l:line, '^```\(.*\)$')
+
+ if !empty(l:match)
+ let l:fence_language = l:match[1]
+
+ if !empty(l:marked_list)
+ call add(l:fence_lines, '')
+ endif
+ else
+ if !empty(l:marked_list)
+ \&& l:marked_list[-1][0] isnot v:null
+ call add(l:marked_list, [v:null, ['']])
+ endif
+
+ call add(l:marked_list, [v:null, [l:line]])
+ endif
+ elseif l:line =~# '^```$'
+ " When we hit the end of a code fence, pass the fenced
+ " lines on to the next steps below.
+ call add(l:marked_list, [l:fence_language, l:fence_lines])
+ let l:fence_language = v:null
+ let l:fence_lines = []
+ else
+ " Gather lines inside of a code fence.
+ call add(l:fence_lines, l:line)
+ endif
+ endfor
+ " If the result from the LSP server is a {language: ..., value: ...}
+ " Dictionary, then that should be interpreted as if it was:
+ "
+ " ```${language}
+ " ${value}
+ " ```
+ elseif type(l:item) is v:t_dict
+ \&& has_key(l:item, 'language')
+ \&& type(l:item.language) is v:t_string
+ \&& has_key(l:item, 'value')
+ \&& type(l:item.value) is v:t_string
+ call add(
+ \ l:marked_list,
+ \ [l:item.language, split(l:item.value, "\n")],
+ \)
+ endif
+
+ for [l:language, l:marked_lines] in l:marked_list
+ if l:language is v:null
+ " NOTE: We could handle other Markdown formatting here.
+ call map(
+ \ l:marked_lines,
+ \ 'substitute(v:val, ''\\_'', ''_'', ''g'')',
+ \)
+ else
+ let l:language = s:ConvertLanguageName(l:language)
+
+ if !empty(l:language)
+ let l:includes[l:language] = printf(
+ \ 'syntax/%s.vim',
+ \ l:language,
+ \)
+
+ let l:start = len(l:lines) + 1
+ let l:end = l:start + len(l:marked_lines)
+ let l:region_index += 1
+
+ call add(l:highlights, 'syntax region'
+ \ . ' ALE_hover_' . l:region_index
+ \ . ' start=/\%' . l:start . 'l/'
+ \ . ' end=/\%' . l:end . 'l/'
+ \ . ' contains=@ALE_hover_' . l:language
+ \)
+ endif
+ endif
+
+ call extend(l:lines, l:marked_lines)
+ endfor
+ endfor
+
+ let l:include_commands = []
+
+ for [l:language, l:lang_path] in sort(items(l:includes))
+ call add(l:include_commands, 'unlet! b:current_syntax')
+ call add(
+ \ l:include_commands,
+ \ printf('syntax include @ALE_hover_%s %s', l:language, l:lang_path),
+ \)
+ endfor
+
+ return [l:include_commands + l:highlights, l:lines]
+endfunction
+
function! ale#hover#HandleLSPResponse(conn_id, response) abort
if has_key(a:response, 'id')
\&& has_key(s:hover_map, a:response.id)
@@ -82,40 +213,25 @@ function! ale#hover#HandleLSPResponse(conn_id, response) abort
return
endif
- let l:result = l:result.contents
+ let [l:commands, l:lines] = ale#hover#ParseLSPResult(l:result.contents)
- if type(l:result) is v:t_string
- " The result can be just a string.
- let l:result = [l:result]
- endif
-
- if type(l:result) is v:t_dict
- " If the result is an object, then it's markup content.
- let l:result = has_key(l:result, 'value') ? [l:result.value] : []
- endif
-
- if type(l:result) is v:t_list
- " Replace objects with text values.
- call filter(l:result, '!(type(v:val) is v:t_dict && !has_key(v:val, ''value''))')
- call map(l:result, 'type(v:val) is v:t_string ? v:val : v:val.value')
- let l:str = join(l:result, "\n")
- let l:str = substitute(l:str, '^\s*\(.\{-}\)\s*$', '\1', '')
-
- if !empty(l:str)
- if get(l:options, 'hover_from_balloonexpr', 0)
- \&& exists('*balloon_show')
- \&& ale#Var(l:options.buffer, 'set_balloons')
- call balloon_show(l:str)
- elseif get(l:options, 'truncated_echo', 0)
- call ale#cursor#TruncatedEcho(split(l:str, "\n")[0])
- elseif g:ale_hover_to_preview
- call ale#preview#Show(split(l:str, "\n"), {
- \ 'filetype': 'ale-preview.message',
- \ 'stay_here': 1,
- \})
- else
- call ale#util#ShowMessage(l:str)
- endif
+ if !empty(l:lines)
+ if get(l:options, 'hover_from_balloonexpr', 0)
+ \&& exists('*balloon_show')
+ \&& ale#Var(l:options.buffer, 'set_balloons')
+ call balloon_show(join(l:lines, "\n"))
+ elseif get(l:options, 'truncated_echo', 0)
+ call ale#cursor#TruncatedEcho(l:lines[0])
+ elseif g:ale_hover_to_preview
+ call ale#preview#Show(l:lines, {
+ \ 'filetype': 'ale-preview.message',
+ \ 'stay_here': 1,
+ \ 'commands': l:commands,
+ \})
+ else
+ call ale#util#ShowMessage(join(l:lines, "\n"), {
+ \ 'commands': l:commands,
+ \})
endif
endif
endif
diff --git a/autoload/ale/preview.vim b/autoload/ale/preview.vim
index 7902ec63..faf45cb0 100644
--- a/autoload/ale/preview.vim
+++ b/autoload/ale/preview.vim
@@ -31,6 +31,10 @@ function! ale#preview#Show(lines, ...) abort
setlocal readonly
let &l:filetype = get(l:options, 'filetype', 'ale-preview')
+ for l:command in get(l:options, 'commands', [])
+ call execute(l:command)
+ endfor
+
if get(l:options, 'stay_here')
wincmd p
endif
diff --git a/autoload/ale/util.vim b/autoload/ale/util.vim
index ee62af28..bb9c1961 100644
--- a/autoload/ale/util.vim
+++ b/autoload/ale/util.vim
@@ -16,7 +16,9 @@ endfunction
" Vim 8 does not support echoing long messages from asynchronous callbacks,
" but NeoVim does. Small messages can be echoed in Vim 8, and larger messages
" have to be shown in preview windows.
-function! ale#util#ShowMessage(string) abort
+function! ale#util#ShowMessage(string, ...) abort
+ let l:options = get(a:000, 0, {})
+
if !has('nvim')
call ale#preview#CloseIfTypeMatches('ale-preview.message')
endif
@@ -25,10 +27,13 @@ function! ale#util#ShowMessage(string) abort
if has('nvim') || (a:string !~? "\n" && len(a:string) < &columns)
execute 'echo a:string'
else
- call ale#preview#Show(split(a:string, "\n"), {
- \ 'filetype': 'ale-preview.message',
- \ 'stay_here': 1,
- \})
+ call ale#preview#Show(split(a:string, "\n"), extend(
+ \ {
+ \ 'filetype': 'ale-preview.message',
+ \ 'stay_here': 1,
+ \ },
+ \ l:options,
+ \))
endif
endfunction
diff --git a/test/test_hover.vader b/test/test_hover.vader
index 8b4cf7bd..9689cda2 100644
--- a/test/test_hover.vader
+++ b/test/test_hover.vader
@@ -5,7 +5,7 @@ Before:
let g:Callback = 0
let g:message_list = []
let g:item_list = []
- let g:echo_list = []
+ let g:show_message_arg_list = []
runtime autoload/ale/linter.vim
runtime autoload/ale/lsp.vim
@@ -27,8 +27,8 @@ Before:
return 42
endfunction
- function! ale#util#ShowMessage(string) abort
- call add(g:echo_list, a:string)
+ function! ale#util#ShowMessage(string, ...) abort
+ call add(g:show_message_arg_list, [a:string] + a:000)
endfunction
function! HandleValidLSPResult(result) abort
@@ -58,7 +58,7 @@ After:
unlet! g:Callback
unlet! g:message_list
unlet! b:ale_linters
- unlet! g:echo_list
+ unlet! g:show_message_arg_list
delfunction HandleValidLSPResult
@@ -112,31 +112,31 @@ Execute(tsserver quickinfo displayString values should be displayed):
\ }
\)
- AssertEqual ['foo bar'], g:echo_list
+ AssertEqual [['foo bar']], g:show_message_arg_list
AssertEqual {}, ale#hover#GetMap()
Execute(LSP hover responses with just a string should be handled):
call HandleValidLSPResult({'contents': 'foobar'})
- AssertEqual ['foobar'], g:echo_list
+ AssertEqual [['foobar', {'commands': []}]], g:show_message_arg_list
AssertEqual {}, ale#hover#GetMap()
Execute(LSP hover null responses should be handled):
call HandleValidLSPResult(v:null)
- AssertEqual [], g:echo_list
+ AssertEqual [], g:show_message_arg_list
AssertEqual {}, ale#hover#GetMap()
Execute(LSP hover responses with markup content should be handled):
- call HandleValidLSPResult({'contents': {'kind': 'something', 'value': 'markup'}})
+ call HandleValidLSPResult({'contents': {'kind': 'markdown', 'value': 'markup'}})
- AssertEqual ['markup'], g:echo_list
+ AssertEqual [['markup', {'commands': []}]], g:show_message_arg_list
AssertEqual {}, ale#hover#GetMap()
Execute(LSP hover responses with markup content missing values should be handled):
- call HandleValidLSPResult({'contents': {'kind': 'something'}})
+ call HandleValidLSPResult({'contents': {'kind': 'markdown'}})
- AssertEqual [], g:echo_list
+ AssertEqual [], g:show_message_arg_list
AssertEqual {}, ale#hover#GetMap()
Execute(LSP hover response with lists of strings should be handled):
@@ -145,17 +145,27 @@ Execute(LSP hover response with lists of strings should be handled):
\ "bar\n",
\]})
- AssertEqual ["foo\n\nbar\n"], g:echo_list
+ AssertEqual [["foo\n\nbar", {'commands': []}]], g:show_message_arg_list
AssertEqual {}, ale#hover#GetMap()
Execute(LSP hover response with lists of strings and marked strings should be handled):
call HandleValidLSPResult({'contents': [
\ {'language': 'rust', 'value': 'foo'},
- \ {'language': 'foobar'},
\ "bar\n",
\]})
- AssertEqual ["foo\nbar\n"], g:echo_list
+ AssertEqual [
+ \ [
+ \ "foo\n\nbar",
+ \ {
+ \ 'commands': [
+ \ 'unlet! b:current_syntax',
+ \ 'syntax include @ALE_hover_rust syntax/rust.vim',
+ \ 'syntax region ALE_hover_1 start=/\%1l/ end=/\%2l/ contains=@ALE_hover_rust',
+ \ ],
+ \ },
+ \ ],
+ \], g:show_message_arg_list
AssertEqual {}, ale#hover#GetMap()
Execute(tsserver responses for documentation requests should be handled):
diff --git a/test/test_hover_parsing.vader b/test/test_hover_parsing.vader
new file mode 100644
index 00000000..4129c26a
--- /dev/null
+++ b/test/test_hover_parsing.vader
@@ -0,0 +1,173 @@
+Execute(Invalid results should be handled):
+ AssertEqual [[], []], ale#hover#ParseLSPResult(0)
+ AssertEqual [[], []], ale#hover#ParseLSPResult([0])
+ AssertEqual [[], []], ale#hover#ParseLSPResult('')
+ AssertEqual [[], []], ale#hover#ParseLSPResult({})
+ AssertEqual [[], []], ale#hover#ParseLSPResult([{}])
+ AssertEqual [[], []], ale#hover#ParseLSPResult([''])
+ AssertEqual [[], []], ale#hover#ParseLSPResult({'value': ''})
+ AssertEqual [[], []], ale#hover#ParseLSPResult([{'value': ''}])
+ AssertEqual [[], []], ale#hover#ParseLSPResult({'kind': 'markdown'})
+ AssertEqual [[], []], ale#hover#ParseLSPResult({'kind': 'plaintext'})
+ AssertEqual [[], []], ale#hover#ParseLSPResult({'kind': 'x', 'value': 'xxx'})
+
+Execute(A string with a code fence should be handled):
+ AssertEqual
+ \ [
+ \ [
+ \ 'unlet! b:current_syntax',
+ \ 'syntax include @ALE_hover_python syntax/python.vim',
+ \ 'syntax region ALE_hover_1 start=/\%1l/ end=/\%3l/ contains=@ALE_hover_python',
+ \ ],
+ \ [
+ \ 'def foo():',
+ \ ' pass',
+ \ ],
+ \ ],
+ \ ale#hover#ParseLSPResult(join([
+ \ '```python',
+ \ 'def foo():',
+ \ ' pass',
+ \ '```',
+ \ ], "\n"))
+
+ AssertEqual
+ \ [
+ \ [
+ \ 'unlet! b:current_syntax',
+ \ 'syntax include @ALE_hover_python syntax/python.vim',
+ \ 'unlet! b:current_syntax',
+ \ 'syntax include @ALE_hover_typescript syntax/typescript.vim',
+ \ 'syntax region ALE_hover_1 start=/\%1l/ end=/\%3l/ contains=@ALE_hover_python',
+ \ 'syntax region ALE_hover_2 start=/\%5l/ end=/\%8l/ contains=@ALE_hover_python',
+ \ 'syntax region ALE_hover_3 start=/\%8l/ end=/\%10l/ contains=@ALE_hover_typescript',
+ \ ],
+ \ [
+ \ 'def foo():',
+ \ ' pass',
+ \ '',
+ \ 'middle line',
+ \ '',
+ \ 'def bar():',
+ \ ' pass',
+ \ '',
+ \ 'const baz = () => undefined',
+ \ ],
+ \ ],
+ \ ale#hover#ParseLSPResult(join([
+ \ '```python',
+ \ 'def foo():',
+ \ ' pass',
+ \ '```',
+ \ 'middle line',
+ \ '```python',
+ \ 'def bar():',
+ \ ' pass',
+ \ '```',
+ \ '```typescript',
+ \ 'const baz = () => undefined',
+ \ '```',
+ \ ], "\n"))
+
+Execute(Multiple strings with fences should be handled):
+ AssertEqual
+ \ [
+ \ [
+ \ 'unlet! b:current_syntax',
+ \ 'syntax include @ALE_hover_python syntax/python.vim',
+ \ 'unlet! b:current_syntax',
+ \ 'syntax include @ALE_hover_typescript syntax/typescript.vim',
+ \ 'syntax region ALE_hover_1 start=/\%1l/ end=/\%3l/ contains=@ALE_hover_python',
+ \ 'syntax region ALE_hover_2 start=/\%5l/ end=/\%8l/ contains=@ALE_hover_python',
+ \ 'syntax region ALE_hover_3 start=/\%8l/ end=/\%10l/ contains=@ALE_hover_typescript',
+ \ ],
+ \ [
+ \ 'def foo():',
+ \ ' pass',
+ \ '',
+ \ 'middle line',
+ \ '',
+ \ 'def bar():',
+ \ ' pass',
+ \ '',
+ \ 'const baz = () => undefined',
+ \ ],
+ \ ],
+ \ ale#hover#ParseLSPResult([
+ \ join([
+ \ '```python',
+ \ 'def foo():',
+ \ ' pass',
+ \ '```',
+ \ ], "\n"),
+ \ join([
+ \ 'middle line',
+ \ '```python',
+ \ 'def bar():',
+ \ ' pass',
+ \ '```',
+ \ '```typescript',
+ \ 'const baz = () => undefined',
+ \ '```',
+ \ ], "\n"),
+ \ ])
+
+Execute(Objects with kinds should be handled):
+ AssertEqual
+ \ [
+ \ [
+ \ 'unlet! b:current_syntax',
+ \ 'syntax include @ALE_hover_python syntax/python.vim',
+ \ 'syntax region ALE_hover_1 start=/\%1l/ end=/\%3l/ contains=@ALE_hover_python',
+ \ ],
+ \ [
+ \ 'def foo():',
+ \ ' pass',
+ \ '',
+ \ '```typescript',
+ \ 'const baz = () => undefined',
+ \ '```',
+ \ ],
+ \ ],
+ \ ale#hover#ParseLSPResult([
+ \ {
+ \ 'kind': 'markdown',
+ \ 'value': join([
+ \ '```python',
+ \ 'def foo():',
+ \ ' pass',
+ \ '```',
+ \ ], "\n"),
+ \ },
+ \ {
+ \ 'kind': 'plaintext',
+ \ 'value': join([
+ \ '```typescript',
+ \ 'const baz = () => undefined',
+ \ '```',
+ \ ], "\n"),
+ \ },
+ \ ])
+
+Execute(Simple markdown formatting should be handled):
+ AssertEqual
+ \ [
+ \ [
+ \ 'unlet! b:current_syntax',
+ \ 'syntax include @ALE_hover_python syntax/python.vim',
+ \ 'syntax region ALE_hover_1 start=/\%1l/ end=/\%3l/ contains=@ALE_hover_python',
+ \ ],
+ \ [
+ \ 'def foo():',
+ \ ' pass',
+ \ '',
+ \ 'formatted _ line _',
+ \ ],
+ \ ],
+ \ ale#hover#ParseLSPResult(join([
+ \ '```python',
+ \ 'def foo():',
+ \ ' pass',
+ \ '```',
+ \ 'formatted \_ line \_',
+ \ ], "\n"))