Skip to content

Commit

Permalink
Reimplement hover window with floating window for Neovim 0.4.0 or lat…
Browse files Browse the repository at this point in the history
…er (#767)

* Reimplement hover window with floating window for Neovim 0.4.0 or later

* Add test for hover window using floating window

* Close floating window when entering other window on WinEnter

* Add test for closing floating window on WinEnter

* Fix checking enough space for floating window

* Use BufEnter instead of WinEnter

since WinEnter cannot close floating hover when the current buffer
switches to another buffer within the same window.

* Add test where entering another buffer closes floating hover

* Follow the API change of nvim_open_win()

Note: the API was changed at
neovim/neovim@96edbe7

* Move cursor into floating hover when hover is already open

* Add health check for floating window

* Describe the behavior of floating window support in document

* Add g:LanguageClient_useFloatingHover

* Add test for moving the cursor into floating window

* Add LanguageClient#openHoverInSeparateWindow()

to reopen floating hover in separate preview window

* Rename LanguageClient#openHoverInSeparateWindow to LanguageClient#reopenHoverInSeparateWindow

* Give left margin only when line is not empty

* Revert "Add LanguageClient#reopenHoverInSeparateWindow()"

This reverts commit 18640c2.
This reverts commit f40a1c3.

* Prefer s:GetVar() to get()

Addresses review comment #767 (comment)

* Move description of float window behavior to function doc section

Addresses review comments #767 (comment)
  • Loading branch information
rhysd authored and autozimu committed Mar 26, 2019
1 parent d31e492 commit 53ce2fe
Show file tree
Hide file tree
Showing 5 changed files with 304 additions and 22 deletions.
148 changes: 148 additions & 0 deletions autoload/LanguageClient.vim
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ let s:TYPE = {
\ 'dict': type({}),
\ 'funcref': type(function('call'))
\ }
let s:FLOAT_WINDOW_AVAILABLE = has('nvim') && exists('*nvim_open_win')

function! s:AddPrefix(message) abort
return '[LC] ' . a:message
Expand Down Expand Up @@ -261,6 +262,150 @@ function! s:GetVar(...) abort
endif
endfunction

function! s:ShouldUseFloatWindow() abort
let use = s:GetVar('LanguageClient_useFloatingHover')
return s:FLOAT_WINDOW_AVAILABLE && (use || use is v:null)
endfunction

function! s:CloseFloatingHoverOnCursorMove(win_id, opened) abort
if getpos('.') == a:opened
" Just after opening floating window, CursorMoved event is run.
" To avoid closing floating window immediately, check the cursor
" was really moved
return
endif
autocmd! plugin-LC-neovim-close-hover
let winnr = win_id2win(a:win_id)
if winnr == 0
return
endif
execute winnr . 'wincmd c'
endfunction

function! s:CloseFloatingHoverOnBufEnter(win_id, bufnr) abort
let winnr = win_id2win(a:win_id)
if winnr == 0
" Float window was already closed
autocmd! plugin-LC-neovim-close-hover
return
endif
if winnr == winnr()
" Cursor is moving into floating window. Do not close it
return
endif
if bufnr('%') == a:bufnr
" When current buffer opened hover window, it's not another buffer. Skipped
return
endif
autocmd! plugin-LC-neovim-close-hover
execute winnr . 'wincmd c'
endfunction

" Open preview window. Window is open in:
" - Floating window on Neovim (0.4.0 or later)
" - Preview window on Neovim (0.3.0 or earlier) or Vim
function! s:OpenHoverPreview(bufname, lines, filetype) abort
" Use local variable since parameter is not modifiable
let lines = a:lines
let bufnr = bufnr('%')

let use_float_win = s:ShouldUseFloatWindow()
if use_float_win
let pos = getpos('.')

" Calculate width and height and give margin to lines
let width = 0
for index in range(len(lines))
let line = lines[index]
if line !=# ''
" Give a left margin
let line = ' ' . line
endif
let lw = strdisplaywidth(line)
if lw > width
let width = lw
endif
let lines[index] = line
endfor

" Give margin
let width += 1
let lines = [''] + lines + ['']
let height = len(lines)

" Calculate anchor
" Prefer North, but if there is no space, fallback into South
let bottom_line = line('w0') + winheight(0) - 1
if pos[1] + height <= bottom_line
let vert = 'N'
let row = 1
else
let vert = 'S'
let row = 0
endif

" Prefer West, but if there is no space, fallback into East
if pos[2] + width <= &columns
let hor = 'W'
let col = 0
else
let hor = 'E'
let col = 1
endif

let float_win_id = nvim_open_win(bufnr, v:true, {
\ 'relative': 'cursor',
\ 'anchor': vert . hor,
\ 'row': row,
\ 'col': col,
\ 'width': width,
\ 'height': height,
\ })

execute 'noswapfile edit!' a:bufname

setlocal winhl=Normal:CursorLine
else
execute 'silent! noswapfile pedit!' a:bufname
wincmd P
endif

setlocal buftype=nofile nobuflisted bufhidden=wipe nonumber norelativenumber signcolumn=no
if a:filetype isnot v:null
let &filetype = a:filetype
endif

call setline(1, lines)
setlocal nomodified nomodifiable

wincmd p

if use_float_win
" Unlike preview window, :pclose does not close window. Instead, close
" hover window automatically when cursor is moved.
let call_after_move = printf('<SID>CloseFloatingHoverOnCursorMove(%d, %s)', float_win_id, string(pos))
let call_on_bufenter = printf('<SID>CloseFloatingHoverOnBufEnter(%d, %d)', float_win_id, bufnr)
augroup plugin-LC-neovim-close-hover
execute 'autocmd CursorMoved,CursorMovedI,InsertEnter <buffer> call ' . call_after_move
execute 'autocmd BufEnter * call ' . call_on_bufenter
augroup END
endif
endfunction

function! s:MoveIntoHoverPreview() abort
for bufnr in range(1, bufnr('$'))
if bufname(bufnr) ==# '__LanguageClient__'
let winnr = bufwinnr(bufnr)
if winnr != -1
execute winnr . 'wincmd w'
endif
return v:true
endif
endfor
return v:false
endfunction

let s:id = 1
let s:handlers = {}

Expand Down Expand Up @@ -529,6 +674,9 @@ function! LanguageClient#Notify(method, params) abort
endfunction

function! LanguageClient#textDocument_hover(...) abort
if s:ShouldUseFloatWindow() && s:MoveIntoHoverPreview()
return
endif
let l:Callback = get(a:000, 1, v:null)
let l:params = {
\ 'filename': LSP#filename(),
Expand Down
11 changes: 10 additions & 1 deletion autoload/health/LanguageClient.vim
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,20 @@ function! s:checkBinary() abort
\ l:path)
endif

let output = system([l:path, '--version'])
let output = substitute(system([l:path, '--version']), '\n$', '', '')
call health#report_ok(output)
endfunction

function! s:checkFloatingWindow() abort
if !exists('*nvim_open_win')
call health#report_info('Floating window is not supported. Preview window will be used for hover')
return
endif
call health#report_ok('Floating window is supported and will be used for hover')
endfunction

function! health#LanguageClient#check() abort
call s:checkJobFeature()
call s:checkBinary()
call s:checkFloatingWindow()
endfunction
17 changes: 16 additions & 1 deletion doc/LanguageClient.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ accessed by regular quickfix/location list operations.
To use the language server with Vim's formatting operator |gq|, set 'formatexpr': >
set formatexpr=LanguageClient#textDocument_rangeFormatting_sync()
<

==============================================================================
2. Configuration *LanguageClientConfiguration*

Expand Down Expand Up @@ -348,6 +347,16 @@ Specify whether to use virtual text to display diagnostics.
Default: 1 whenever virtual text is supported.
Valid Options: 1 | 0

2.26 g:LanguageClient_useFloatingHover *g:LanguageClient_useFloatingHover*

When the value is set to 1, |LanguageClient#textDocument_hover()| opens
documentation in a floating window instead of preview.
This variable is effective only when the floating window feature is
supported.

Default: 1 when a floating window is supported, otherwise 0
Valid Options: 1 | 0

==============================================================================
3. Commands *LanguageClientCommands*

Expand Down Expand Up @@ -397,6 +406,12 @@ Signature: LanguageClient#textDocument_hover(...)

Show type info (and short doc) of identifier under cursor.

If you're using Neovim 0.4.0 or later, this function opens documentation in a
floating window. The window is automatically closed when you move the cursor.
Or calling this function again just after opening the floating window moves
the cursor into the window. It is useful when documentation is longer and you
need to scroll down or you want to yank some text in the documentation.

*LanguageClient#textDocument_definition()*
*LanguageClient_textDocument_definition()*
Signature: LanguageClient#textDocument_definition(...)
Expand Down
25 changes: 5 additions & 20 deletions src/language_server_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -864,27 +864,12 @@ impl LanguageClient {
D: ToDisplay + ?Sized,
{
let bufname = "__LanguageClient__";

let cmd = "silent! pedit! +setlocal\\ buftype=nofile\\ nobuflisted\\ noswapfile\\ nonumber";
let cmd = if let Some(ref ft) = to_display.vim_filetype() {
format!("{}\\ filetype={} {}", cmd, ft, bufname)
} else {
format!("{} {}", cmd, bufname)
};
self.vim()?.command(cmd)?;

let filetype = &to_display.vim_filetype();
let lines = to_display.to_display();
if self.get(|state| state.is_nvim)? {
let bufnr: u64 = serde_json::from_value(self.vim()?.rpcclient.call("bufnr", bufname)?)?;
self.vim()?
.rpcclient
.notify("nvim_buf_set_lines", json!([bufnr, 0, -1, 0, lines]))?;
} else {
self.vim()?
.rpcclient
.notify("setbufline", json!([bufname, 1, lines]))?;
// TODO: removing existing bottom lines.
}

self.vim()?
.rpcclient
.notify("s:OpenHoverPreview", json!([bufname, lines, filetype]))?;

Ok(())
}
Expand Down
125 changes: 125 additions & 0 deletions tests/LanguageClient_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,128 @@ def test_languageClient_registerHandlers(nvim):
# if b.name.startswith('term://')), None) is None)

# assertRetry(lambda: len(nvim.funcs.getqflist()) == 0)


def _open_float_window(nvim):
nvim.funcs.cursor(13, 19)
pos = nvim.funcs.getpos('.')
nvim.funcs.LanguageClient_textDocument_hover()
time.sleep(1)
return pos


def test_textDocument_hover_float_window_closed_on_cursor_moved(nvim):
if not nvim.funcs.exists("*nvim_open_win"):
pytest.skip("Neovim 0.3.0 or earlier does not support floating window")

nvim.command("edit! {}".format(PATH_INDEXJS))
time.sleep(1)

buf = nvim.current.buffer

pos = _open_float_window(nvim)

float_buf = next(
b for b in nvim.buffers if b.name.endswith("__LanguageClient__"))

# Check if float window is open
float_winnr = nvim.funcs.bufwinnr(float_buf.number)
assert float_winnr > 0

# Check if cursor is not moved
assert buf.number == nvim.current.buffer.number
assert pos == nvim.funcs.getpos(".")

# Move cursor to left
nvim.funcs.cursor(13, 17)

# Check float window buffer was closed by CursorMoved
assert all(
b for b in nvim.buffers if not b.name.endswith("__LanguageClient__"))


def test_textDocument_hover_float_window_closed_on_entering_window(nvim):
if not nvim.funcs.exists("*nvim_open_win"):
pytest.skip("Neovim 0.3.0 or earlier does not support floating window")

nvim.command("edit! {}".format(PATH_INDEXJS))
time.sleep(1)

win_id = nvim.funcs.win_getid()
nvim.command("split")
try:
assert win_id != nvim.funcs.win_getid()

_open_float_window(nvim)
assert win_id != nvim.funcs.win_getid()

# Move to another window
nvim.funcs.win_gotoid(win_id)
assert win_id == nvim.funcs.win_getid()

# Check float window buffer was closed by BufEnter
assert all(
b for b in nvim.buffers
if not b.name.endswith("__LanguageClient__"))
finally:
nvim.command("close!")


def test_textDocument_hover_float_window_closed_on_switching_to_buffer(nvim):
if not nvim.funcs.exists("*nvim_open_win"):
pytest.skip("Neovim 0.3.0 or earlier does not support floating window")

# Create a new buffer
nvim.command("enew!")

another_bufnr = nvim.current.buffer.number

try:
nvim.command("edit! {}".format(PATH_INDEXJS))
time.sleep(1)

source_bufnr = nvim.current.buffer.number

_open_float_window(nvim)

float_buf = next(
b for b in nvim.buffers if b.name.endswith("__LanguageClient__"))
float_winnr = nvim.funcs.bufwinnr(float_buf.number)
assert float_winnr > 0

assert nvim.current.buffer.number == source_bufnr

# Move to another buffer within the same window
nvim.command("buffer {}".format(another_bufnr))
assert nvim.current.buffer.number == another_bufnr

# Check float window buffer was closed by BufEnter
assert all(
b for b in nvim.buffers
if not b.name.endswith("__LanguageClient__"))
finally:
nvim.command("bdelete! {}".format(another_bufnr))


def test_textDocument_hover_float_window_move_cursor_into_window(nvim):
if not nvim.funcs.exists("*nvim_open_win"):
pytest.skip("Neovim 0.3.0 or earlier does not support floating window")

nvim.command("edit! {}".format(PATH_INDEXJS))
time.sleep(1)

prev_bufnr = nvim.current.buffer.number

_open_float_window(nvim)

# Moves cursor into floating window
nvim.funcs.LanguageClient_textDocument_hover()
assert nvim.current.buffer.name.endswith("__LanguageClient__")

# Close the window
nvim.command('close')
assert nvim.current.buffer.number == prev_bufnr

# Check float window buffer was closed by :close in the window
assert all(
b for b in nvim.buffers if not b.name.endswith("__LanguageClient__"))

0 comments on commit 53ce2fe

Please sign in to comment.