From 53ce2fee7d24dd9262ea783908e2edebc71635dc Mon Sep 17 00:00:00 2001 From: Linda_pp Date: Tue, 26 Mar 2019 13:13:07 +0900 Subject: [PATCH] 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 https://github.com/neovim/neovim/commit/96edbe7b1d6144f3cf9b720d61182ee31858b478 * 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 18640c29eec80cd1013bf29587928d43e4a43dc2. This reverts commit f40a1c3b50e119e05c447c7e61b149e9ab8bf70d. * Prefer s:GetVar() to get() Addresses review comment https://github.com/autozimu/LanguageClient-neovim/pull/767#discussion_r268271396 * Move description of float window behavior to function doc section Addresses review comments https://github.com/autozimu/LanguageClient-neovim/pull/767#discussion_r268271396 --- autoload/LanguageClient.vim | 148 +++++++++++++++++++++++++++++ autoload/health/LanguageClient.vim | 11 ++- doc/LanguageClient.txt | 17 +++- src/language_server_protocol.rs | 25 +---- tests/LanguageClient_test.py | 125 ++++++++++++++++++++++++ 5 files changed, 304 insertions(+), 22 deletions(-) diff --git a/autoload/LanguageClient.vim b/autoload/LanguageClient.vim index d367bb9e..992ff0eb 100644 --- a/autoload/LanguageClient.vim +++ b/autoload/LanguageClient.vim @@ -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 @@ -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('CloseFloatingHoverOnCursorMove(%d, %s)', float_win_id, string(pos)) + let call_on_bufenter = printf('CloseFloatingHoverOnBufEnter(%d, %d)', float_win_id, bufnr) + augroup plugin-LC-neovim-close-hover + execute 'autocmd CursorMoved,CursorMovedI,InsertEnter 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 = {} @@ -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(), diff --git a/autoload/health/LanguageClient.vim b/autoload/health/LanguageClient.vim index 598c1f50..4cc5e7e3 100644 --- a/autoload/health/LanguageClient.vim +++ b/autoload/health/LanguageClient.vim @@ -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 diff --git a/doc/LanguageClient.txt b/doc/LanguageClient.txt index b19ce815..3cd84dac 100644 --- a/doc/LanguageClient.txt +++ b/doc/LanguageClient.txt @@ -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* @@ -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* @@ -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(...) diff --git a/src/language_server_protocol.rs b/src/language_server_protocol.rs index 9821d08e..d0bf91a3 100644 --- a/src/language_server_protocol.rs +++ b/src/language_server_protocol.rs @@ -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(()) } diff --git a/tests/LanguageClient_test.py b/tests/LanguageClient_test.py index fb2b58c1..9273615a 100644 --- a/tests/LanguageClient_test.py +++ b/tests/LanguageClient_test.py @@ -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__"))