From cc84e159329fbed068754b0bb6d3237f0ce5d14d Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 21 Feb 2021 00:34:51 +0000 Subject: [PATCH] Tidy up, refactor and fix some bugs --- autoload/vimspector.vim | 13 +- autoload/vimspector/internal/balloon.vim | 411 +++++++++++++---------- plugin/vimspector.vim | 7 +- python3/vimspector/debug_session.py | 8 +- python3/vimspector/utils.py | 17 +- python3/vimspector/variables.py | 61 ++-- 6 files changed, 307 insertions(+), 210 deletions(-) diff --git a/autoload/vimspector.vim b/autoload/vimspector.vim index 9ac7ff1..d4bcfa7 100644 --- a/autoload/vimspector.vim +++ b/autoload/vimspector.vim @@ -525,13 +525,18 @@ endfunction function! vimspector#ShowEvalBalloon( is_visual ) abort if a:is_visual - let expr = py3eval( '__import__( "vimspector", fromlist = [ "utils" ] ).utils.GetVisualSelection( int( vim.eval( "winbufnr( winnr() )" ) ) )' ) - let expr = join(expr) + let expr = py3eval( '__import__( "vimspector", fromlist = [ "utils" ] )' + \ . '.utils.GetVisualSelection(' + \ . ' int( vim.eval( "winbufnr( winnr() )" ) ) )' ) + let expr = join( expr, '\n' ) else - let expr = expand('') + let expr = expand( '' ) endif - return py3eval( '_vimspector_session.ShowEvalBalloon( int( vim.eval( "winnr()" ) ), "'.expr.'", 0 )' ) + return py3eval( '_vimspector_session.ShowEvalBalloon(' + \ . ' int( vim.eval( "winnr()" ) ), "' + \ . expr + \ . '", 0 )' ) endfunction diff --git a/autoload/vimspector/internal/balloon.vim b/autoload/vimspector/internal/balloon.vim index 16ae4c8..5f25f3a 100644 --- a/autoload/vimspector/internal/balloon.vim +++ b/autoload/vimspector/internal/balloon.vim @@ -21,12 +21,8 @@ set cpoptions&vim scriptencoding utf-8 -function! vimspector#internal#balloon#HoverTooltip() abort - return py3eval('_vimspector_session.ShowEvalBalloon(int( vim.eval( "v:beval_winnr" ) ) + 1 ,vim.eval( "v:beval_text"), 1)') -endfunction - -let s:float_win = 0 -let s:nvim_related_win = 0 +let s:popup_win_id = 0 +let s:nvim_border_win_id = 0 " " tooltip dimensions let s:min_width = 1 @@ -34,173 +30,31 @@ let s:min_height = 1 let s:max_width = 80 let s:max_height = 20 -function! vimspector#internal#balloon#MouseFilter(winid, key) abort - if index(["\", "\<2-leftmouse>"], a:key) < 0 - return 0 - endif +let s:is_neovim = has( 'nvim' ) - let handled = 0 - let mouse_coords = getmousepos() - " close the popup if mouse is clicked outside the window - if mouse_coords['winid'] != a:winid - call vimspector#internal#balloon#Close() - return 0 - endif - - " place the cursor according to the click - call win_execute(a:winid, ':call cursor('.mouse_coords['line'].', '.mouse_coords['column'].')') - - " expand the variable if we got double click - if a:key ==? "\<2-leftmouse>" - " forward line number to python, since vim does not allow us to focus - " the correct window - call py3eval('_vimspector_session.ExpandVariable('.line('.', a:winid).')') - let handled = 1 - endif - - return handled +" This is used as the balloonexpr in vim to show the Tooltip at the hover +" position +function! vimspector#internal#balloon#HoverTooltip() abort + return py3eval( '_vimspector_session.ShowEvalBalloon(' + \ . ' int( vim.eval( "v:beval_winnr" ) ) + 1,' + \ . ' vim.eval( "v:beval_text"),' + \ . ' 1 )' ) endfunction -function! vimspector#internal#balloon#CursorFilter(winid, key) abort - if a:key ==? "\" - " forward line number to python, since vim does not allow us to focus - " the correct window - call py3eval('_vimspector_session.ExpandVariable('.line('.', a:winid).')') - return 1 - elseif index( [ "\", "\<2-LeftMouse>" ], a:key ) >= 0 - return vimspector#internal#balloon#MouseFilter( a:winid, a:key ) - endif - - return popup_filter_menu( a:winid, a:key ) -endfunction - -function! vimspector#internal#balloon#Close() abort - if has('nvim') - call nvim_win_close(s:float_win, v:true) - call nvim_win_close(s:nvim_related_win, v:true) - - call vimspector#internal#balloon#CloseCallback() - else - call popup_close(s:float_win) - endif - -endfunction - -function! vimspector#internal#balloon#CloseCallback( ... ) abort - let s:float_win = 0 - let s:nvim_related_win = 0 - return py3eval('_vimspector_session._CleanUpTooltip()') -endfunction - -function! vimspector#internal#balloon#nvim_generate_border(width, height) abort - let top = '╭' . repeat('─',a:width + 2) . '╮' - let mid = '│' . repeat(' ',a:width + 2) . '│' - let bot = '╰' . repeat('─',a:width + 2) . '╯' - let lines = [top] + repeat([mid], a:height) + [bot] - - return lines -endfunction - -function! vimspector#internal#balloon#nvim_resize_tooltip() abort - if !has('nvim') || s:float_win <= 0 || s:nvim_related_win <= 0 - return - endif - - noa call win_gotoid(s:float_win) - let buf_lines = getline(1, '$') - - let width = s:min_width - let height = min([max([s:min_height, len(buf_lines)]), s:max_height]) - - " calculate the longest line - for l in buf_lines - let width = max([width, len(l)]) - endfor - - let width = min([width, s:max_width]) - - let opts = { - \ 'width': width, - \ 'height': height, - \ } - " resize the content window - call nvim_win_set_config(s:float_win, opts) - - " resize the border window - let opts['width'] = width + 4 - let opts['height'] = height + 2 - call nvim_win_set_config(s:nvim_related_win, opts) - call nvim_buf_set_lines(nvim_win_get_buf(s:nvim_related_win), 0, -1, v:true, vimspector#internal#balloon#nvim_generate_border(width, height)) - -endfunction - -function! vimspector#internal#balloon#CreateTooltip(is_hover, ...) abort +function! vimspector#internal#balloon#CreateTooltip( is_hover, ... ) abort let body = [] if a:0 > 0 let body = a:1 endif - if has('nvim') - " generate border for the float window by creating a background buffer and - " overlaying the content buffer - " see https://github.com/neovim/neovim/issues/9718#issuecomment-546603628 - let buf_id = nvim_create_buf(v:false, v:true) - call nvim_buf_set_lines(buf_id, 0, -1, v:true, vimspector#internal#balloon#nvim_generate_border(s:max_width, s:max_height)) - - " default the dimensions for now. they can be easily overwritten later - let opts = { - \ 'relative': 'cursor', - \ 'width': s:max_width + 2, - \ 'height': s:max_height + 2, - \ 'col': 0, - \ 'row': 1, - \ 'anchor': 'NW', - \ 'style': 'minimal' - \ } - " this is the border window - let s:nvim_related_win = nvim_open_win(buf_id, 0, opts) - call nvim_win_set_option(s:nvim_related_win, 'signcolumn', 'no') - call nvim_win_set_option(s:nvim_related_win, 'relativenumber', v:false) - call nvim_win_set_option(s:nvim_related_win, 'number', v:false) - - " when calculating where to display the content window, we need to account - " for the border - let opts.row += 1 - let opts.height -= 2 - let opts.col += 2 - let opts.width -= 4 - - " create the content window - let buf_id = nvim_create_buf(v:false, v:true) - call nvim_buf_set_lines(buf_id, 0, -1, v:true, body) - call nvim_buf_set_option(buf_id, 'modifiable', v:false) - let s:float_win = nvim_open_win(buf_id, v:false, opts) - - call nvim_win_set_option(s:float_win, 'wrap', v:false) - call nvim_win_set_option(s:float_win, 'cursorline', v:true) - call nvim_win_set_option(s:float_win, 'signcolumn', 'no') - call nvim_win_set_option(s:float_win, 'relativenumber', v:false) - call nvim_win_set_option(s:float_win, 'number', v:false) - - noautocmd call win_gotoid(s:float_win) - - nnoremap :call vimspector#ExpandVariable() - nnoremap :quit - nnoremap <2-LeftMouse>:call vimspector#ExpandVariable() - - " make sure we clean up the float after it loses focus - augroup vimspector#internal#balloon#nvim_float - autocmd! - autocmd BufLeave * :call vimspector#internal#balloon#Close() | autocmd! vimspector#internal#balloon#nvim_float - augroup END + if s:popup_win_id != 0 + call vimspector#internal#balloon#Close() + endif + if s:is_neovim + call s:CreateNeovimTooltip( body ) else - - if s:float_win != 0 - call vimspector#internal#balloon#Close() - endif - let config = { \ 'wrap': 0, \ 'filtermode': 'n', @@ -217,26 +71,241 @@ function! vimspector#internal#balloon#CreateTooltip(is_hover, ...) abort \ 'callback': 'vimspector#internal#balloon#CloseCallback' \ } + " When ambiwidth is single, use prettier characters for the border. This + " would look silly when ambiwidth is double. if &ambiwidth ==# 'single' && &encoding ==? 'utf-8' - let config['borderchars'] = [ '─', '│', '─', '│', '╭', '╮', '┛', '╰' ] + let config[ 'borderchars' ] = [ '─', '│', '─', '│', '╭', '╮', '┛', '╰' ] endif if a:is_hover - let config['filter'] = 'vimspector#internal#balloon#MouseFilter' - let config['mousemoved'] = [0, 0, 0] - let s:float_win = popup_beval(body, config) + let config[ 'filter' ] = 'vimspector#internal#balloon#MouseFilter' + let config[ 'mousemoved' ] = [ 0, 0, 0 ] + let s:popup_win_id = popup_beval( body, config ) else - let config['filter'] = 'vimspector#internal#balloon#CursorFilter' - let config['moved'] = 'any' - let config['cursorline'] = 1 - let s:float_win = popup_atcursor(body, config) + let config[ 'filter' ] = 'vimspector#internal#balloon#CursorFilter' + let config[ 'moved' ] = 'any' + let config[ 'cursorline' ] = 1 + let s:popup_win_id = popup_atcursor( body, config ) endif endif - return s:float_win + return s:popup_win_id endfunction +" Filters for vim {{{ +function! vimspector#internal#balloon#MouseFilter( winid, key ) abort + if a:key ==# "\" + call vimspector#internal#balloon#Close() + return 0 + endif + + if index( [ "\", "\<2-leftmouse>" ], a:key ) < 0 + return 0 + endif + + let handled = 0 + let mouse_coords = getmousepos() + + " close the popup if mouse is clicked outside the window + if mouse_coords[ 'winid' ] != a:winid + call vimspector#internal#balloon#Close() + return 0 + endif + + " place the cursor according to the click + call win_execute( a:winid, + \ ':call cursor( ' + \ . mouse_coords[ 'line' ] + \ . ', ' + \ . mouse_coords[ 'column' ] + \ . ' )' ) + + " expand the variable if we got double click + if a:key ==? "\<2-leftmouse>" + " forward line number to python, since vim does not allow us to focus + " the correct window + call py3eval( '_vimspector_session.ExpandVariable(' + \ . 'buf = vim.buffers[ ' . winbufnr( a:winid ) . ' ],' + \ . 'line_num = ' . line( '.', a:winid ) + \ . ')' ) + let handled = 1 + endif + + return handled +endfunction + +function! vimspector#internal#balloon#CursorFilter( winid, key ) abort + if a:key ==? "\" + " forward line number to python, since vim does not allow us to focus + " the correct window + call py3eval( '_vimspector_session.ExpandVariable(' + \ . 'buf = vim.buffers[ ' . winbufnr( a:winid ) . ' ],' + \ . 'line_num = ' . line( '.', a:winid ) + \ . ')' ) + return 1 + elseif index( [ "\", "\<2-LeftMouse>" ], a:key ) >= 0 + return vimspector#internal#balloon#MouseFilter( a:winid, a:key ) + endif + + return popup_filter_menu( a:winid, a:key ) +endfunction + +" }}} + +" Closing {{{ + +function! vimspector#internal#balloon#CloseCallback( ... ) abort + let s:popup_win_id = 0 + let s:nvim_border_win_id = 0 + return py3eval( '_vimspector_session.CleanUpTooltip()' ) +endfunction + +function! vimspector#internal#balloon#Close() abort + if s:is_neovim + call nvim_win_close( s:popup_win_id, v:true ) + call nvim_win_close( s:nvim_border_win_id, v:true ) + + call vimspector#internal#balloon#CloseCallback() + else + call popup_close(s:popup_win_id) + endif +endfunction + +" }}} + +" Neovim pollyfill {{{ + +function! vimspector#internal#balloon#ResizeTooltip() abort + if !s:is_neovim + " Vim does this for us + return + endif + + if s:popup_win_id <= 0 || s:nvim_border_win_id <= 0 + " nothing to resize + return + endif + + noautocmd call win_gotoid( s:popup_win_id ) + let buf_lines = getline( 1, '$' ) + + let width = s:min_width + let height = min( [ max( [ s:min_height, len( buf_lines ) ] ), + \ s:max_height ] ) + + " calculate the longest line + for l in buf_lines + let width = max( [ width, len( l ) ] ) + endfor + + let width = min( [ width, s:max_width ] ) + + let opts = { + \ 'width': width, + \ 'height': height, + \ } + + " resize the content window + call nvim_win_set_config( s:popup_win_id, opts ) + + " resize the border window + let opts[ 'width' ] = width + 4 + let opts[ 'height' ] = height + 2 + + call nvim_win_set_config( s:nvim_border_win_id, opts ) + call nvim_buf_set_lines( nvim_win_get_buf( s:nvim_border_win_id ), + \ 0, + \ -1, + \ v:true, + \ s:GenerateBorder( width, height ) ) +endfunction + +" neovim doesn't have the border support, so we have to make our own. +" FIXME: This will likely break if the user has `ambiwidth=2` +function! s:GenerateBorder( width, height ) abort + + let top = '╭' . repeat('─',a:width + 2) . '╮' + let mid = '│' . repeat(' ',a:width + 2) . '│' + let bot = '╰' . repeat('─',a:width + 2) . '╯' + let lines = [ top ] + repeat( [ mid ], a:height ) + [ bot ] + + return lines +endfunction + +function! s:CreateNeovimTooltip( body ) abort + " generate border for the float window by creating a background buffer and + " overlaying the content buffer + " see https://github.com/neovim/neovim/issues/9718#issuecomment-546603628 + let buf_id = nvim_create_buf( v:false, v:true ) + call nvim_buf_set_lines( buf_id, + \ 0, + \ -1, + \ v:true, + \ s:GenerateBorder( s:max_width, s:max_height ) ) + + " default the dimensions initially, then we'll calculate the real size and + " resize it. + let opts = { + \ 'relative': 'cursor', + \ 'width': s:max_width + 2, + \ 'height': s:max_height + 2, + \ 'col': 0, + \ 'row': 1, + \ 'anchor': 'NW', + \ 'style': 'minimal' + \ } + + " this is the border window + let s:nvim_border_win_id = nvim_open_win( buf_id, 0, opts ) + call nvim_win_set_option( s:nvim_border_win_id, 'signcolumn', 'no' ) + call nvim_win_set_option( s:nvim_border_win_id, 'relativenumber', v:false ) + call nvim_win_set_option( s:nvim_border_win_id, 'number', v:false ) + + " when calculating where to display the content window, we need to account + " for the border + let opts.row += 1 + let opts.height -= 2 + let opts.col += 2 + let opts.width -= 4 + + " create the content window + let buf_id = nvim_create_buf( v:false, v:true ) + call nvim_buf_set_lines( buf_id, 0, -1, v:true, a:body ) + call nvim_buf_set_option( buf_id, 'modifiable', v:false ) + let s:popup_win_id = nvim_open_win( buf_id, v:false, opts ) + + call nvim_win_set_option( s:popup_win_id, 'wrap', v:false ) + call nvim_win_set_option( s:popup_win_id, 'cursorline', v:true ) + call nvim_win_set_option( s:popup_win_id, 'signcolumn', 'no' ) + call nvim_win_set_option( s:popup_win_id, 'relativenumber', v:false ) + call nvim_win_set_option( s:popup_win_id, 'number', v:false ) + + " Move the cursor into the popup window, as this is the only way we can + " interract with the popup in neovim + noautocmd call win_gotoid( s:popup_win_id ) + + nnoremap + \ call vimspector#ExpandVariable() + nnoremap + \ quit + nnoremap <2-LeftMouse> + \ call vimspector#ExpandVariable() + + " Close the popup whenever we leave this window + augroup vimspector#internal#balloon#nvim_float + autocmd! + autocmd WinLeave + \ :call vimspector#internal#balloon#Close() + \ | autocmd! vimspector#internal#balloon#nvim_float + augroup END + + call vimspector#internal#balloon#ResizeTooltip() +endfunction + +" }}} + + " Boilerplate {{{ let &cpoptions=s:save_cpo unlet s:save_cpo diff --git a/plugin/vimspector.vim b/plugin/vimspector.vim index 6465c21..6691a2a 100644 --- a/plugin/vimspector.vim +++ b/plugin/vimspector.vim @@ -60,11 +60,12 @@ nnoremap VimspectorStepOut nnoremap VimspectorRunToCursor \ :call vimspector#RunToCursor() +" Eval for normal mode nnoremap VimspectorBalloonEval - \ :call vimspector#ShowEvalBalloon(0) - + \ :call vimspector#ShowEvalBalloon( 0 ) +" And for visual modes xnoremap VimspectorBalloonEval - \ :call vimspector#ShowEvalBalloon(1) + \ :call vimspector#ShowEvalBalloon( 1 ) if s:mappings ==# 'VISUAL_STUDIO' nmap VimspectorContinue diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index c91caa4..36e6feb 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -520,8 +520,8 @@ class DebugSession( object ): self._stackTraceView.SetCurrentThread() @IfConnected() - def ExpandVariable( self, lineNum = -1 ): - self._variablesView.ExpandVariable( lineNum ) + def ExpandVariable( self, buf = None, line_num = None ): + self._variablesView.ExpandVariable( buf, line_num ) @IfConnected() def AddWatch( self, expression ): @@ -558,8 +558,8 @@ class DebugSession( object ): return self._variablesView.VariableEval( frame, expression, is_hover ) - def _CleanUpTooltip( self ): - return self._variablesView._CleanUpTooltip() + def CleanUpTooltip( self ): + return self._variablesView.CleanUpTooltip() @IfConnected() def ExpandFrameOrThread( self ): diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index 8010fb9..e2f96b0 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -645,8 +645,6 @@ def DisplayBalloon( is_term, display, is_hover = False ): ) ) ) - vim.eval( "vimspector#internal#balloon#nvim_resize_tooltip()" ) - return created_win_id @@ -728,11 +726,20 @@ def GetVisualSelection( bufnr ): start_line, start_col = vim.current.buffer.mark( "<" ) end_line, end_col = vim.current.buffer.mark( ">" ) + # lines are 1 based, but columns are 0 based - # don't as me why... - lines = vim.buffers[ bufnr ][ start_line - 1 : end_line ] - lines[ 0 ] = lines[ 0 ][ start_col : ] + # don't ask me why... + start_line -= 1 + end_line -= 1 + + lines = vim.buffers[ bufnr ][ start_line : end_line + 1 ] + # Do end first, in case it's on the same line as start (as doing start first + # would change the offset) lines[ -1 ] = lines[ -1 ][ : end_col + 1 ] + lines[ 0 ] = lines[ 0 ][ start_col : ] + + _logger.debug( f'Visual selection: { lines } from ' + f'{ start_line }/{ start_col } -> { end_line }/{ end_col }' ) return lines diff --git a/python3/vimspector/variables.py b/python3/vimspector/variables.py index 7890e1b..74626ac 100644 --- a/python3/vimspector/variables.py +++ b/python3/vimspector/variables.py @@ -143,6 +143,12 @@ class View: utils.SetUpUIWindow( win ) +class BufView( View ): + def __init__( self, buf, lines, draw ): + super().__init__( None, lines, draw ) + self.buf = buf + + class VariablesView( object ): def __init__( self, variables_win, watches_win ): self._logger = logging.getLogger( __name__ ) @@ -219,6 +225,7 @@ class VariablesView( object ): utils.ClearBuffer( self._vars.buf ) with utils.ModifiableScratchBuffer( self._watch.buf ): utils.ClearBuffer( self._watch.buf ) + self.ClearTooltip() self._current_syntax = '' def ConnectionUp( self, connection ): @@ -234,6 +241,8 @@ class VariablesView( object ): utils.CleanUpHiddenBuffer( self._vars.buf ) utils.CleanUpHiddenBuffer( self._watch.buf ) + self.ClearTooltip() + def LoadScopes( self, frame ): def scopes_consumer( message ): @@ -302,9 +311,14 @@ class VariablesView( object ): watch, is_short = True ) - vim.eval( "vimspector#internal#balloon#nvim_resize_tooltip()" ) + vim.eval( "vimspector#internal#balloon#ResizeTooltip()" ) - def _CleanUpTooltip( self ) : + def ClearTooltip( self ): + # This will actually end up calling CleanUpTooltip via the popup close + # callback + vim.eval( 'vimspector#internal#balloon#Close()' ) + + def CleanUpTooltip( self ) : # remove reference to old tooltip window self._variable_eval_view = None vim.vars[ 'vimspector_session_windows' ][ 'eval' ] = None @@ -322,22 +336,17 @@ class VariablesView( object ): else: watch.result.Update( message[ 'body' ] ) - float_win_id = utils.DisplayBalloon( self._is_term, [], is_hover ) + popup_win_id = utils.DisplayBalloon( self._is_term, [], is_hover ) # record the global eval window id - vim.vars[ 'vimspector_session_windows' ][ 'eval' ] = int( float_win_id ) - float_buf_nr = int( vim.eval( "winbufnr({})".format( float_win_id ) ) ) + vim.vars[ 'vimspector_session_windows' ][ 'eval' ] = int( popup_win_id ) + popup_bufnr = int( vim.eval( "winbufnr({})".format( popup_win_id ) ) ) - # since vim's popup cant be focused there is no way - # to get a reference to its window - # we will emulate python's window object ourselves - self._variable_eval_view = View( - type( - '__vim__window__', - ( object, ), - { 'options': {}, 'buffer': vim.buffers[ float_buf_nr ] } - ), - {}, - self._DrawBalloonEval + # We don't need to do any UI window setup here, as it's already done as + # part of the popup creation, so just pass the buffer to the View instance + self._variable_eval_view = BufView( + vim.buffers[ popup_bufnr ], + {}, + self._DrawBalloonEval ) if watch.result.IsExpandable(): @@ -439,21 +448,27 @@ class VariablesView( object ): watch.result = WatchFailure( reason ) self._DrawWatches() - def ExpandVariable( self, lineNum = -1 ): - if vim.current.buffer == self._vars.buf: + def ExpandVariable( self, buf = None, line_num = None ): + if buf is None: + buf = vim.current.buffer + + if line_num is None: + line_num = vim.current.window.cursor[ 0 ] + + if buf == self._vars.buf: view = self._vars - elif vim.current.buffer == self._watch.buf: + elif buf == self._watch.buf: view = self._watch - elif self._variable_eval_view is not None: + elif ( self._variable_eval_view is not None + and buf == self._variable_eval_view.buf ): view = self._variable_eval_view else: return - current_line = vim.current.window.cursor[ 0 ] if lineNum <= 0 else lineNum - if current_line not in view.lines: + if line_num not in view.lines: return - variable = view.lines[ current_line ] + variable = view.lines[ line_num ] if variable.IsExpanded(): # Collapse