Add nvim floating window support (replaces #3314) (#3470)

* Add nvim floating window hover support
* Add configuration for float to replace preview
* preview#ShowFloating: qualify local variables
* Configure floating preview usecases individually

Also:
  * Extract floating preview to its own file.
  * Ignore 'stay_here' option. Moving into the floating preview window
    seems confusing at best.
  * Re-use existing floating preview window if it's still up.
  * Flush out floating preview documentation.

* Watch cursor position changes per window

Floating previews open a new window, so when that window is written to,
it moves briefly there at a different position than the original window.
This makes repeated positions detected when positions are tracked at a
s: level. Instead, we change the variable to window scoped, which only
fires a message if the cursor has changed from the last position in
*that window*.

* g:ale_floating_preview cleanup
* floating_preview: add ALEDetail tests
* Fix fecs test missing runtime call
* Add ALEHover floating preview tests

Co-authored-by: Jan-Grimo Sobez <jan-grimo.sobez@phys.chem.ethz.ch>
This commit is contained in:
Kevin Clark 2021-01-14 10:06:20 -08:00 committed by GitHub
parent 97ce2423b0
commit 39f393ef07
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 330 additions and 7 deletions

View file

@ -9,7 +9,6 @@ let g:ale_echo_delay = get(g:, 'ale_echo_delay', 10)
let g:ale_echo_msg_format = get(g:, 'ale_echo_msg_format', '%code: %%s') let g:ale_echo_msg_format = get(g:, 'ale_echo_msg_format', '%code: %%s')
let s:cursor_timer = -1 let s:cursor_timer = -1
let s:last_pos = [0, 0, 0]
function! ale#cursor#TruncatedEcho(original_message) abort function! ale#cursor#TruncatedEcho(original_message) abort
let l:message = a:original_message let l:message = a:original_message
@ -118,14 +117,18 @@ function! ale#cursor#EchoCursorWarningWithDelay() abort
let l:pos = getpos('.')[0:2] let l:pos = getpos('.')[0:2]
if !exists('w:last_pos')
let w:last_pos = [0, 0, 0]
endif
" Check the current buffer, line, and column number against the last " Check the current buffer, line, and column number against the last
" recorded position. If the position has actually changed, *then* " recorded position. If the position has actually changed, *then*
" we should echo something. Otherwise we can end up doing processing " we should echo something. Otherwise we can end up doing processing
" the echo message far too frequently. " the echo message far too frequently.
if l:pos != s:last_pos if l:pos != w:last_pos
let l:delay = ale#Var(l:buffer, 'echo_delay') let l:delay = ale#Var(l:buffer, 'echo_delay')
let s:last_pos = l:pos let w:last_pos = l:pos
let s:cursor_timer = timer_start( let s:cursor_timer = timer_start(
\ l:delay, \ l:delay,
\ function('ale#cursor#EchoCursorWarning') \ function('ale#cursor#EchoCursorWarning')
@ -139,11 +142,16 @@ function! s:ShowCursorDetailForItem(loc, options) abort
let s:last_detailed_line = line('.') let s:last_detailed_line = line('.')
let l:message = get(a:loc, 'detail', a:loc.text) let l:message = get(a:loc, 'detail', a:loc.text)
let l:lines = split(l:message, "\n") let l:lines = split(l:message, "\n")
call ale#preview#Show(l:lines, {'stay_here': l:stay_here})
" Clear the echo message if we manually displayed details. if g:ale_floating_preview || g:ale_detail_to_floating_preview
if !l:stay_here call ale#floating_preview#Show(l:lines)
execute 'echo' else
call ale#preview#Show(l:lines, {'stay_here': l:stay_here})
" Clear the echo message if we manually displayed details.
if !l:stay_here
execute 'echo'
endif
endif endif
endfunction endfunction

View file

@ -0,0 +1,91 @@
" Author: Jan-Grimo Sobez <jan-grimo.sobez@phys.chem.ethz.ch>
" Author: Kevin Clark <kevin.clark@gmail.com>
" Description: Floating preview window for showing whatever information in.
" Precondition: exists('*nvim_open_win')
function! ale#floating_preview#Show(lines, ...) abort
if !exists('*nvim_open_win')
execute 'echom ''Floating windows not supported in this vim instance.'''
return
endif
" Remove the close autocmd so it doesn't happen mid update
augroup ale_floating_preview_window
autocmd!
augroup END
let l:options = get(a:000, 0, {})
" Only create a new window if we need it
if !exists('w:preview') || index(nvim_list_wins(), w:preview['id']) is# -1
call s:Create(l:options)
else
call nvim_buf_set_option(w:preview['buffer'], 'modifiable', v:true)
endif
" Execute commands in window context
let l:parent_window = nvim_get_current_win()
call nvim_set_current_win(w:preview['id'])
for l:command in get(l:options, 'commands', [])
call execute(l:command)
endfor
call nvim_set_current_win(l:parent_window)
" Return to parent context on move
augroup ale_floating_preview_window
autocmd!
if g:ale_close_preview_on_insert
autocmd CursorMoved,TabLeave,WinLeave,InsertEnter <buffer> ++once call s:Close()
else
autocmd CursorMoved,TabLeave,WinLeave <buffer> ++once call s:Close()
endif
augroup END
let l:width = max(map(copy(a:lines), 'strdisplaywidth(v:val)'))
let l:height = min([len(a:lines), 10])
call nvim_win_set_width(w:preview['id'], l:width)
call nvim_win_set_height(w:preview['id'], l:height)
call nvim_buf_set_lines(w:preview['buffer'], 0, -1, v:false, a:lines)
call nvim_buf_set_option(w:preview['buffer'], 'modified', v:false)
call nvim_buf_set_option(w:preview['buffer'], 'modifiable', v:false)
endfunction
function! s:Create(options) abort
let l:buffer = nvim_create_buf(v:false, v:false)
let l:winid = nvim_open_win(l:buffer, v:false, {
\ 'relative': 'cursor',
\ 'row': 1,
\ 'col': 0,
\ 'width': 42,
\ 'height': 4,
\ 'style': 'minimal'
\ })
call nvim_buf_set_option(l:buffer, 'buftype', 'acwrite')
call nvim_buf_set_option(l:buffer, 'bufhidden', 'delete')
call nvim_buf_set_option(l:buffer, 'swapfile', v:false)
call nvim_buf_set_option(l:buffer, 'filetype', get(a:options, 'filetype', 'ale-preview'))
let w:preview = {'id': l:winid, 'buffer': l:buffer}
endfunction
function! s:Close() abort
if !exists('w:preview')
return
endif
call setbufvar(w:preview['buffer'], '&modified', 0)
if win_id2win(w:preview['id']) > 0
execute win_id2win(w:preview['id']).'wincmd c'
endif
unlet w:preview
endfunction

View file

@ -46,6 +46,10 @@ function! ale#hover#HandleTSServerResponse(conn_id, response) abort
call balloon_show(a:response.body.displayString) call balloon_show(a:response.body.displayString)
elseif get(l:options, 'truncated_echo', 0) elseif get(l:options, 'truncated_echo', 0)
call ale#cursor#TruncatedEcho(split(a:response.body.displayString, "\n")[0]) call ale#cursor#TruncatedEcho(split(a:response.body.displayString, "\n")[0])
elseif g:ale_hover_to_floating_preview || g:ale_floating_preview
call ale#floating_preview#Show(split(a:response.body.displayString, "\n"), {
\ 'filetype': 'ale-preview.message',
\})
elseif g:ale_hover_to_preview elseif g:ale_hover_to_preview
call ale#preview#Show(split(a:response.body.displayString, "\n"), { call ale#preview#Show(split(a:response.body.displayString, "\n"), {
\ 'filetype': 'ale-preview.message', \ 'filetype': 'ale-preview.message',
@ -226,6 +230,11 @@ function! ale#hover#HandleLSPResponse(conn_id, response) abort
call balloon_show(join(l:lines, "\n")) call balloon_show(join(l:lines, "\n"))
elseif get(l:options, 'truncated_echo', 0) elseif get(l:options, 'truncated_echo', 0)
call ale#cursor#TruncatedEcho(l:lines[0]) call ale#cursor#TruncatedEcho(l:lines[0])
elseif g:ale_hover_to_floating_preview || g:ale_floating_preview
call ale#floating_preview#Show(l:lines, {
\ 'filetype': 'ale-preview.message',
\ 'commands': l:commands,
\})
elseif g:ale_hover_to_preview elseif g:ale_hover_to_preview
call ale#preview#Show(l:lines, { call ale#preview#Show(l:lines, {
\ 'filetype': 'ale-preview.message', \ 'filetype': 'ale-preview.message',

View file

@ -646,6 +646,9 @@ problem will be displayed in a balloon instead of hover information.
Hover information can be displayed in the preview window instead by setting Hover information can be displayed in the preview window instead by setting
|g:ale_hover_to_preview| to `1`. |g:ale_hover_to_preview| to `1`.
When using Neovim, if |g:ale_hover_to_floating_preview| or |g:ale_floating_preview|
is set to 1, the hover information will show in a floating window.
For Vim 8.1+ terminals, mouse hovering is disabled by default. Enabling For Vim 8.1+ terminals, mouse hovering is disabled by default. Enabling
|balloonexpr| commands in terminals can cause scrolling issues in terminals, |balloonexpr| commands in terminals can cause scrolling issues in terminals,
so ALE will not attempt to show balloons unless |g:ale_set_balloons| is set to so ALE will not attempt to show balloons unless |g:ale_set_balloons| is set to
@ -954,6 +957,15 @@ g:ale_default_navigation *g:ale_default_navigation*
buffer, such as for |ALEFindReferences|, or |ALEGoToDefinition|. buffer, such as for |ALEFindReferences|, or |ALEGoToDefinition|.
g:ale_detail_to_floating_preview *g:ale_detail_to_floating_preview*
*b:ale_detail_to_floating_preview*
Type: |Number|
Default: `0`
When this option is set to `1`, Neovim will use a floating window for
ALEDetail output.
g:ale_disable_lsp *g:ale_disable_lsp* g:ale_disable_lsp *g:ale_disable_lsp*
*b:ale_disable_lsp* *b:ale_disable_lsp*
@ -1177,6 +1189,16 @@ g:ale_fix_on_save_ignore *g:ale_fix_on_save_ignore*
let g:ale_fix_on_save_ignore = [g:AddBar] let g:ale_fix_on_save_ignore = [g:AddBar]
< <
g:ale_floating_preview *g:ale_floating_preview*
Type: |Number|
Default: `0`
When set to `1`, Neovim will use a floating window for ale's preview window.
This is equivalent to setting |g:ale_hover_to_floating_preview| and
|g:ale_detail_to_floating_preview| to `1`.
g:ale_history_enabled *g:ale_history_enabled* g:ale_history_enabled *g:ale_history_enabled*
Type: |Number| Type: |Number|
@ -1235,6 +1257,14 @@ g:ale_hover_to_preview *g:ale_hover_to_preview*
instead of in balloons or the message line. instead of in balloons or the message line.
g:ale_hover_to_floating_preview *g:ale_hover_to_floating_preview*
*b:ale_hover_to_floating_preview*
Type: |Number|
Default: `0`
If set to `1`, Neovim will use floating windows for hover messages.
g:ale_keep_list_window_open *g:ale_keep_list_window_open* g:ale_keep_list_window_open *g:ale_keep_list_window_open*
*b:ale_keep_list_window_open* *b:ale_keep_list_window_open*
Type: |Number| Type: |Number|

View file

@ -138,6 +138,15 @@ let g:ale_set_balloons = get(g:, 'ale_set_balloons', has('balloon_eval') && has(
" Use preview window for hover messages. " Use preview window for hover messages.
let g:ale_hover_to_preview = get(g:, 'ale_hover_to_preview', 0) let g:ale_hover_to_preview = get(g:, 'ale_hover_to_preview', 0)
" Float preview windows in Neovim
let g:ale_floating_preview = get(g:, 'ale_floating_preview', 0)
" Hovers use floating windows in Neovim
let g:ale_hover_to_floating_preview = get(g:, 'ale_hover_to_floating_preview', 0)
" Detail uses floating windows in Neovim
let g:ale_detail_to_floating_preview = get(g:, 'ale_detail_to_floating_preview', 0)
" This flag can be set to 0 to disable warnings for trailing whitespace " This flag can be set to 0 to disable warnings for trailing whitespace
let g:ale_warn_about_trailing_whitespace = get(g:, 'ale_warn_about_trailing_whitespace', 1) let g:ale_warn_about_trailing_whitespace = get(g:, 'ale_warn_about_trailing_whitespace', 1)
" This flag can be set to 0 to disable warnings for trailing blank lines " This flag can be set to 0 to disable warnings for trailing blank lines

View file

@ -1,5 +1,6 @@
Before: Before:
call ale#assert#SetUpLinterTest('javascript', 'fecs') call ale#assert#SetUpLinterTest('javascript', 'fecs')
runtime autoload/ale/handlers/fecs.vim
After: After:
call ale#assert#TearDownLinterTest() call ale#assert#TearDownLinterTest()

View file

@ -0,0 +1,92 @@
Before:
let g:ale_floating_preview = 0
let g:ale_hover_to_floating_preview = 0
let g:ale_detail_to_floating_preview = 0
runtime autoload/ale/floating_preview.vim
let g:floated_lines = []
let g:floating_preview_show_called = 0
" Stub out so we can track the call
function! ale#floating_preview#Show(lines, ...) abort
let g:floating_preview_show_called = 1
let g:floated_lines = a:lines
endfunction
let g:ale_buffer_info = {
\ bufnr('%'): {
\ 'loclist': [
\ {
\ 'lnum': 1,
\ 'col': 10,
\ 'bufnr': bufnr('%'),
\ 'vcol': 0,
\ 'linter_name': 'notalinter',
\ 'nr': -1,
\ 'type': 'E',
\ 'code': 'semi',
\ 'text': "Missing semicolon.\r",
\ 'detail': "Every statement should end with a semicolon\nsecond line",
\ },
\ ],
\ }
\}
call ale#linter#Reset()
call ale#linter#PreventLoading('javascript')
After:
Restore
let g:ale_floating_preview = 0
let g:ale_hover_to_floating_preview = 0
let g:ale_detail_to_floating_preview = 0
call cursor(1, 1)
let g:ale_buffer_info = {}
" Close the preview window if it's open.
if &filetype is# 'ale-preview'
noautocmd :q!
endif
call ale#linter#Reset()
Given javascript(A file with warnings/errors):
var x = 3 + 12345678
var x = 5*2 + parseInt("10");
// comment
Execute(Floating preview is used with ALEDetail when g:ale_floating_preview set):
let g:ale_floating_preview = 1
call cursor(1, 10)
ALEDetail
let expected = ["Every statement should end with a semicolon", "second line"]
AssertEqual 1, g:floating_preview_show_called
AssertEqual expected, g:floated_lines
Execute(Floating preview is used with ALEDetail when g:ale_detail_to_floating_preview set):
let g:ale_detail_to_floating_preview = 1
call cursor(1, 10)
ALEDetail
let expected = ["Every statement should end with a semicolon", "second line"]
AssertEqual 1, g:floating_preview_show_called
AssertEqual expected, g:floated_lines
Execute(Floating preview is not used with ALEDetail by default):
call cursor(1, 10)
ALEDetail
AssertEqual 0, g:floating_preview_show_called

View file

@ -7,9 +7,25 @@ Before:
let g:item_list = [] let g:item_list = []
let g:show_message_arg_list = [] let g:show_message_arg_list = []
let g:ale_floating_preview = 0
let g:ale_hover_to_floating_preview = 0
let g:ale_detail_to_floating_preview = 0
runtime autoload/ale/linter.vim runtime autoload/ale/linter.vim
runtime autoload/ale/lsp.vim runtime autoload/ale/lsp.vim
runtime autoload/ale/lsp_linter.vim
runtime autoload/ale/util.vim runtime autoload/ale/util.vim
runtime autoload/ale/floating_preview.vim
runtime autoload/ale/hover.vim
let g:floated_lines = []
let g:floating_preview_show_called = 0
" Stub out so we can track the call
function! ale#floating_preview#Show(lines, ...) abort
let g:floating_preview_show_called = 1
let g:floated_lines = a:lines
endfunction
function! ale#lsp_linter#StartLSP(buffer, linter, callback) abort function! ale#lsp_linter#StartLSP(buffer, linter, callback) abort
let g:Callback = a:callback let g:Callback = a:callback
@ -50,6 +66,7 @@ Before:
\) \)
endfunction endfunction
After: After:
call ale#hover#SetMap({}) call ale#hover#SetMap({})
call ale#test#RestoreDirectory() call ale#test#RestoreDirectory()
@ -65,6 +82,7 @@ After:
runtime autoload/ale/lsp_linter.vim runtime autoload/ale/lsp_linter.vim
runtime autoload/ale/lsp.vim runtime autoload/ale/lsp.vim
runtime autoload/ale/util.vim runtime autoload/ale/util.vim
runtime autoload/ale/floating_preview.vim
Given python(Some Python file): Given python(Some Python file):
foo foo
@ -168,6 +186,28 @@ Execute(LSP hover response with lists of strings and marked strings should be ha
\], g:show_message_arg_list \], g:show_message_arg_list
AssertEqual {}, ale#hover#GetMap() AssertEqual {}, ale#hover#GetMap()
Execute(LSP hover with ale_floating_preview should float):
let g:ale_floating_preview = 1
call HandleValidLSPResult({'contents': "the message\ncontinuing"})
AssertEqual 1, g:floating_preview_show_called
AssertEqual ["the message", "continuing"], g:floated_lines
Execute(LSP hover ale_hover_to_floating_preview should float):
let g:ale_hover_to_floating_preview = 1
call HandleValidLSPResult({'contents': "the message\ncontinuing"})
AssertEqual 1, g:floating_preview_show_called
AssertEqual ["the message", "continuing"], g:floated_lines
Execute(LSP hover by default should not float):
call HandleValidLSPResult({'contents': "the message\ncontinuing"})
AssertEqual 0, g:floating_preview_show_called
Execute(tsserver responses for documentation requests should be handled): Execute(tsserver responses for documentation requests should be handled):
call ale#hover#SetMap({3: {'show_documentation': 1, 'buffer': bufnr('')}}) call ale#hover#SetMap({3: {'show_documentation': 1, 'buffer': bufnr('')}})
@ -187,3 +227,46 @@ Execute(tsserver responses for documentation requests should be handled):
" The preview window should show the text. " The preview window should show the text.
AssertEqual ['foo is a very good method'], ale#test#GetPreviewWindowText() AssertEqual ['foo is a very good method'], ale#test#GetPreviewWindowText()
silent! pclose silent! pclose
Execute(hover with show_documentation should be in the preview window, not floating):
let g:ale_hover_to_floating_preview = 1
let g:ale_floating_preview = 1
call ale#hover#SetMap({3: {'show_documentation': 1, 'buffer': bufnr('')}})
call ale#hover#HandleTSServerResponse(
\ 1,
\ {
\ 'command': 'quickinfo',
\ 'request_seq': 3,
\ 'success': v:true,
\ 'body': {
\ 'documentation': 'foo is a very good method',
\ 'displayString': 'foo bar ',
\ },
\ }
\)
let expected = ["Every statement should end with a semicolon", "second line"]
AssertEqual 0, g:floating_preview_show_called
Execute(TSServer hover without show_documentation and ale_floating_preview should float):
let g:ale_floating_preview = 1
call ale#hover#SetMap({3: {'buffer': bufnr('')}})
call ale#hover#HandleTSServerResponse(
\ 1,
\ {
\ 'command': 'quickinfo',
\ 'request_seq': 3,
\ 'success': v:true,
\ 'body': {
\ 'displayString': "the message\ncontinuing",
\ },
\ }
\)
AssertEqual 1, g:floating_preview_show_called
AssertEqual ["the message", "continuing"], g:floated_lines