Skip to content

Commit 53ce2fe

Browse files
rhysdautozimu
authored andcommitted
Reimplement hover window with floating window for Neovim 0.4.0 or later (#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)
1 parent d31e492 commit 53ce2fe

File tree

5 files changed

+304
-22
lines changed

5 files changed

+304
-22
lines changed

autoload/LanguageClient.vim

+148
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ let s:TYPE = {
88
\ 'dict': type({}),
99
\ 'funcref': type(function('call'))
1010
\ }
11+
let s:FLOAT_WINDOW_AVAILABLE = has('nvim') && exists('*nvim_open_win')
1112

1213
function! s:AddPrefix(message) abort
1314
return '[LC] ' . a:message
@@ -261,6 +262,150 @@ function! s:GetVar(...) abort
261262
endif
262263
endfunction
263264

265+
function! s:ShouldUseFloatWindow() abort
266+
let use = s:GetVar('LanguageClient_useFloatingHover')
267+
return s:FLOAT_WINDOW_AVAILABLE && (use || use is v:null)
268+
endfunction
269+
270+
function! s:CloseFloatingHoverOnCursorMove(win_id, opened) abort
271+
if getpos('.') == a:opened
272+
" Just after opening floating window, CursorMoved event is run.
273+
" To avoid closing floating window immediately, check the cursor
274+
" was really moved
275+
return
276+
endif
277+
autocmd! plugin-LC-neovim-close-hover
278+
let winnr = win_id2win(a:win_id)
279+
if winnr == 0
280+
return
281+
endif
282+
execute winnr . 'wincmd c'
283+
endfunction
284+
285+
function! s:CloseFloatingHoverOnBufEnter(win_id, bufnr) abort
286+
let winnr = win_id2win(a:win_id)
287+
if winnr == 0
288+
" Float window was already closed
289+
autocmd! plugin-LC-neovim-close-hover
290+
return
291+
endif
292+
if winnr == winnr()
293+
" Cursor is moving into floating window. Do not close it
294+
return
295+
endif
296+
if bufnr('%') == a:bufnr
297+
" When current buffer opened hover window, it's not another buffer. Skipped
298+
return
299+
endif
300+
autocmd! plugin-LC-neovim-close-hover
301+
execute winnr . 'wincmd c'
302+
endfunction
303+
304+
" Open preview window. Window is open in:
305+
" - Floating window on Neovim (0.4.0 or later)
306+
" - Preview window on Neovim (0.3.0 or earlier) or Vim
307+
function! s:OpenHoverPreview(bufname, lines, filetype) abort
308+
" Use local variable since parameter is not modifiable
309+
let lines = a:lines
310+
let bufnr = bufnr('%')
311+
312+
let use_float_win = s:ShouldUseFloatWindow()
313+
if use_float_win
314+
let pos = getpos('.')
315+
316+
" Calculate width and height and give margin to lines
317+
let width = 0
318+
for index in range(len(lines))
319+
let line = lines[index]
320+
if line !=# ''
321+
" Give a left margin
322+
let line = ' ' . line
323+
endif
324+
let lw = strdisplaywidth(line)
325+
if lw > width
326+
let width = lw
327+
endif
328+
let lines[index] = line
329+
endfor
330+
331+
" Give margin
332+
let width += 1
333+
let lines = [''] + lines + ['']
334+
let height = len(lines)
335+
336+
" Calculate anchor
337+
" Prefer North, but if there is no space, fallback into South
338+
let bottom_line = line('w0') + winheight(0) - 1
339+
if pos[1] + height <= bottom_line
340+
let vert = 'N'
341+
let row = 1
342+
else
343+
let vert = 'S'
344+
let row = 0
345+
endif
346+
347+
" Prefer West, but if there is no space, fallback into East
348+
if pos[2] + width <= &columns
349+
let hor = 'W'
350+
let col = 0
351+
else
352+
let hor = 'E'
353+
let col = 1
354+
endif
355+
356+
let float_win_id = nvim_open_win(bufnr, v:true, {
357+
\ 'relative': 'cursor',
358+
\ 'anchor': vert . hor,
359+
\ 'row': row,
360+
\ 'col': col,
361+
\ 'width': width,
362+
\ 'height': height,
363+
\ })
364+
365+
execute 'noswapfile edit!' a:bufname
366+
367+
setlocal winhl=Normal:CursorLine
368+
else
369+
execute 'silent! noswapfile pedit!' a:bufname
370+
wincmd P
371+
endif
372+
373+
setlocal buftype=nofile nobuflisted bufhidden=wipe nonumber norelativenumber signcolumn=no
374+
375+
if a:filetype isnot v:null
376+
let &filetype = a:filetype
377+
endif
378+
379+
call setline(1, lines)
380+
setlocal nomodified nomodifiable
381+
382+
wincmd p
383+
384+
if use_float_win
385+
" Unlike preview window, :pclose does not close window. Instead, close
386+
" hover window automatically when cursor is moved.
387+
let call_after_move = printf('<SID>CloseFloatingHoverOnCursorMove(%d, %s)', float_win_id, string(pos))
388+
let call_on_bufenter = printf('<SID>CloseFloatingHoverOnBufEnter(%d, %d)', float_win_id, bufnr)
389+
augroup plugin-LC-neovim-close-hover
390+
execute 'autocmd CursorMoved,CursorMovedI,InsertEnter <buffer> call ' . call_after_move
391+
execute 'autocmd BufEnter * call ' . call_on_bufenter
392+
augroup END
393+
endif
394+
endfunction
395+
396+
function! s:MoveIntoHoverPreview() abort
397+
for bufnr in range(1, bufnr('$'))
398+
if bufname(bufnr) ==# '__LanguageClient__'
399+
let winnr = bufwinnr(bufnr)
400+
if winnr != -1
401+
execute winnr . 'wincmd w'
402+
endif
403+
return v:true
404+
endif
405+
endfor
406+
return v:false
407+
endfunction
408+
264409
let s:id = 1
265410
let s:handlers = {}
266411

@@ -529,6 +674,9 @@ function! LanguageClient#Notify(method, params) abort
529674
endfunction
530675

531676
function! LanguageClient#textDocument_hover(...) abort
677+
if s:ShouldUseFloatWindow() && s:MoveIntoHoverPreview()
678+
return
679+
endif
532680
let l:Callback = get(a:000, 1, v:null)
533681
let l:params = {
534682
\ 'filename': LSP#filename(),

autoload/health/LanguageClient.vim

+10-1
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,20 @@ function! s:checkBinary() abort
1515
\ l:path)
1616
endif
1717

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

22+
function! s:checkFloatingWindow() abort
23+
if !exists('*nvim_open_win')
24+
call health#report_info('Floating window is not supported. Preview window will be used for hover')
25+
return
26+
endif
27+
call health#report_ok('Floating window is supported and will be used for hover')
28+
endfunction
29+
2230
function! health#LanguageClient#check() abort
2331
call s:checkJobFeature()
2432
call s:checkBinary()
33+
call s:checkFloatingWindow()
2534
endfunction

doc/LanguageClient.txt

+16-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ accessed by regular quickfix/location list operations.
6363
To use the language server with Vim's formatting operator |gq|, set 'formatexpr': >
6464
set formatexpr=LanguageClient#textDocument_rangeFormatting_sync()
6565
<
66-
6766
==============================================================================
6867
2. Configuration *LanguageClientConfiguration*
6968

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

350+
2.26 g:LanguageClient_useFloatingHover *g:LanguageClient_useFloatingHover*
351+
352+
When the value is set to 1, |LanguageClient#textDocument_hover()| opens
353+
documentation in a floating window instead of preview.
354+
This variable is effective only when the floating window feature is
355+
supported.
356+
357+
Default: 1 when a floating window is supported, otherwise 0
358+
Valid Options: 1 | 0
359+
351360
==============================================================================
352361
3. Commands *LanguageClientCommands*
353362

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

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

409+
If you're using Neovim 0.4.0 or later, this function opens documentation in a
410+
floating window. The window is automatically closed when you move the cursor.
411+
Or calling this function again just after opening the floating window moves
412+
the cursor into the window. It is useful when documentation is longer and you
413+
need to scroll down or you want to yank some text in the documentation.
414+
400415
*LanguageClient#textDocument_definition()*
401416
*LanguageClient_textDocument_definition()*
402417
Signature: LanguageClient#textDocument_definition(...)

src/language_server_protocol.rs

+5-20
Original file line numberDiff line numberDiff line change
@@ -864,27 +864,12 @@ impl LanguageClient {
864864
D: ToDisplay + ?Sized,
865865
{
866866
let bufname = "__LanguageClient__";
867-
868-
let cmd = "silent! pedit! +setlocal\\ buftype=nofile\\ nobuflisted\\ noswapfile\\ nonumber";
869-
let cmd = if let Some(ref ft) = to_display.vim_filetype() {
870-
format!("{}\\ filetype={} {}", cmd, ft, bufname)
871-
} else {
872-
format!("{} {}", cmd, bufname)
873-
};
874-
self.vim()?.command(cmd)?;
875-
867+
let filetype = &to_display.vim_filetype();
876868
let lines = to_display.to_display();
877-
if self.get(|state| state.is_nvim)? {
878-
let bufnr: u64 = serde_json::from_value(self.vim()?.rpcclient.call("bufnr", bufname)?)?;
879-
self.vim()?
880-
.rpcclient
881-
.notify("nvim_buf_set_lines", json!([bufnr, 0, -1, 0, lines]))?;
882-
} else {
883-
self.vim()?
884-
.rpcclient
885-
.notify("setbufline", json!([bufname, 1, lines]))?;
886-
// TODO: removing existing bottom lines.
887-
}
869+
870+
self.vim()?
871+
.rpcclient
872+
.notify("s:OpenHoverPreview", json!([bufname, lines, filetype]))?;
888873

889874
Ok(())
890875
}

tests/LanguageClient_test.py

+125
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,128 @@ def test_languageClient_registerHandlers(nvim):
208208
# if b.name.startswith('term://')), None) is None)
209209

210210
# assertRetry(lambda: len(nvim.funcs.getqflist()) == 0)
211+
212+
213+
def _open_float_window(nvim):
214+
nvim.funcs.cursor(13, 19)
215+
pos = nvim.funcs.getpos('.')
216+
nvim.funcs.LanguageClient_textDocument_hover()
217+
time.sleep(1)
218+
return pos
219+
220+
221+
def test_textDocument_hover_float_window_closed_on_cursor_moved(nvim):
222+
if not nvim.funcs.exists("*nvim_open_win"):
223+
pytest.skip("Neovim 0.3.0 or earlier does not support floating window")
224+
225+
nvim.command("edit! {}".format(PATH_INDEXJS))
226+
time.sleep(1)
227+
228+
buf = nvim.current.buffer
229+
230+
pos = _open_float_window(nvim)
231+
232+
float_buf = next(
233+
b for b in nvim.buffers if b.name.endswith("__LanguageClient__"))
234+
235+
# Check if float window is open
236+
float_winnr = nvim.funcs.bufwinnr(float_buf.number)
237+
assert float_winnr > 0
238+
239+
# Check if cursor is not moved
240+
assert buf.number == nvim.current.buffer.number
241+
assert pos == nvim.funcs.getpos(".")
242+
243+
# Move cursor to left
244+
nvim.funcs.cursor(13, 17)
245+
246+
# Check float window buffer was closed by CursorMoved
247+
assert all(
248+
b for b in nvim.buffers if not b.name.endswith("__LanguageClient__"))
249+
250+
251+
def test_textDocument_hover_float_window_closed_on_entering_window(nvim):
252+
if not nvim.funcs.exists("*nvim_open_win"):
253+
pytest.skip("Neovim 0.3.0 or earlier does not support floating window")
254+
255+
nvim.command("edit! {}".format(PATH_INDEXJS))
256+
time.sleep(1)
257+
258+
win_id = nvim.funcs.win_getid()
259+
nvim.command("split")
260+
try:
261+
assert win_id != nvim.funcs.win_getid()
262+
263+
_open_float_window(nvim)
264+
assert win_id != nvim.funcs.win_getid()
265+
266+
# Move to another window
267+
nvim.funcs.win_gotoid(win_id)
268+
assert win_id == nvim.funcs.win_getid()
269+
270+
# Check float window buffer was closed by BufEnter
271+
assert all(
272+
b for b in nvim.buffers
273+
if not b.name.endswith("__LanguageClient__"))
274+
finally:
275+
nvim.command("close!")
276+
277+
278+
def test_textDocument_hover_float_window_closed_on_switching_to_buffer(nvim):
279+
if not nvim.funcs.exists("*nvim_open_win"):
280+
pytest.skip("Neovim 0.3.0 or earlier does not support floating window")
281+
282+
# Create a new buffer
283+
nvim.command("enew!")
284+
285+
another_bufnr = nvim.current.buffer.number
286+
287+
try:
288+
nvim.command("edit! {}".format(PATH_INDEXJS))
289+
time.sleep(1)
290+
291+
source_bufnr = nvim.current.buffer.number
292+
293+
_open_float_window(nvim)
294+
295+
float_buf = next(
296+
b for b in nvim.buffers if b.name.endswith("__LanguageClient__"))
297+
float_winnr = nvim.funcs.bufwinnr(float_buf.number)
298+
assert float_winnr > 0
299+
300+
assert nvim.current.buffer.number == source_bufnr
301+
302+
# Move to another buffer within the same window
303+
nvim.command("buffer {}".format(another_bufnr))
304+
assert nvim.current.buffer.number == another_bufnr
305+
306+
# Check float window buffer was closed by BufEnter
307+
assert all(
308+
b for b in nvim.buffers
309+
if not b.name.endswith("__LanguageClient__"))
310+
finally:
311+
nvim.command("bdelete! {}".format(another_bufnr))
312+
313+
314+
def test_textDocument_hover_float_window_move_cursor_into_window(nvim):
315+
if not nvim.funcs.exists("*nvim_open_win"):
316+
pytest.skip("Neovim 0.3.0 or earlier does not support floating window")
317+
318+
nvim.command("edit! {}".format(PATH_INDEXJS))
319+
time.sleep(1)
320+
321+
prev_bufnr = nvim.current.buffer.number
322+
323+
_open_float_window(nvim)
324+
325+
# Moves cursor into floating window
326+
nvim.funcs.LanguageClient_textDocument_hover()
327+
assert nvim.current.buffer.name.endswith("__LanguageClient__")
328+
329+
# Close the window
330+
nvim.command('close')
331+
assert nvim.current.buffer.number == prev_bufnr
332+
333+
# Check float window buffer was closed by :close in the window
334+
assert all(
335+
b for b in nvim.buffers if not b.name.endswith("__LanguageClient__"))

0 commit comments

Comments
 (0)