diff --git a/README.md b/README.md index 8e7a036..b3088b7 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ For detailed explanatin of the `.vimspector.json` format, see the * [Run to Cursor](#run-to-cursor) * [Stepping](#stepping) * [Variables and scopes](#variables-and-scopes) + * [Variable or selection hover evaluation](#variable-or-selection-hover-evaluation) * [Watches](#watches) * [Watch autocompletion](#watch-autocompletion) * [Stack Traces](#stack-traces) @@ -89,7 +90,7 @@ For detailed explanatin of the `.vimspector.json` format, see the * [Example](#example) * [FAQ](#faq) - + @@ -102,7 +103,7 @@ language that Visual Studio Code supports (but see caveats). The [Vimspector website][website] has an overview of the UI, along with basic instructions for configuration and setup. -But for now, here's a (rather old) screenshot of Vimsepctor debugging Vim: +But for now, here's a (rather old) screenshot of Vimspector debugging Vim: ![vimspector-vim-screenshot](https://puremourning.github.io/vimspector-web/img/vimspector-overview.png) @@ -124,7 +125,7 @@ And a couple of brief demos: - locals and globals display - watch expressions with autocompletion - call stack display and navigation -- variable value display hover +- hierarchical variable value display popup (see `VimspectorBalloonEval`) - interactive debug console with autocompletion - launch debugee within Vim's embedded terminal - logging/stdout display @@ -230,8 +231,8 @@ Why such a new vim ? Well 2 reasons: if you hit them. Why is neovim experimental? Because the author doesn't use neovim regularly, and -there are no regression tests for vimspector in neovim, so it's likely to break -frequently. Issue reports are handled on best-efforts basis, and PRs are +there are no regression tests for vimspector in neovim, so it may break +occasionally. Issue reports are handled on best-efforts basis, and PRs are welcome to fix bugs. See also the next section descibing differences for neovim vs vim. @@ -249,7 +250,8 @@ neovim doesn't implement some features Vimspector relies on: the output window's current output. * Prompt Buffers - used to send commands in the Console and add Watches. (*Note*: prompt buffers are available in neovim nightly) -* Balloons - used to display the values of variables when debugging. +* Balloons - this allows for the variable evaluation popup to be displayed when + hovering the mouse. See below for how to create a keyboard mapping instead. Workarounds are in place as follows: @@ -258,9 +260,18 @@ Workarounds are in place as follows: [`:VimspectorReset`](#closing-debugger) * Prompt Buffers - There are [`:VimspectorEval`](#console) and [`:VimspectorWatch`](#watches) +* Balloons - There is the `VimspectorBalloonEval` mapping. There is no +default mapping for this, so I recommend something like this to get variable +display in a popup: -There is no workaroud for the lack of balloons; you'll just have to use -`:VimspectorEval` or `:VimspectorWatch`, or switch to Vim. +```viml +" mnemonic 'di' = 'debug inspect' (pick your own, if you prefer!) + +" for normal mode - the word under the cursor +nmap di VimspectorBalloonEval +" for visual mode, the visually selected text +xmap di VimspectorBalloonEval +``` ## Windows differences @@ -653,6 +664,7 @@ features to set your own mappings. To that end, Vimspector defines the following * `VimspectorStepInto` * `VimspectorStepOut` * `VimspectorRunToCursor` +* `VimspectorBalloonEval` These map roughly 1-1 with the API functions below. @@ -715,6 +727,18 @@ let g:vimspector_enable_mappings = 'HUMAN' | `F11` | Step Into | `vimspector#StepInto()` | | `F12` | Step out of current function scope | `vimspector#StepOut()` | +In addition, I recommend adding a mapping to `VimspectorBalloonEval`, in +normal and visual modes, for example: + +```viml +" mnemonic 'di' = 'debug inspect' (pick your own, if you prefer!) + +" for normal mode - the word under the cursor +nmap di VimspectorBalloonEval +" for visual mode, the visually selected text +xmap di VimspectorBalloonEval +``` + # Usage and API This section defines detailed usage instructions, organised by feature. For most @@ -890,6 +914,20 @@ breakpoint when it is hit. Scopes and variables are represented by the buffer `vimspector.Variables`. +## Variable or selection hover evaluation + +All rules for `Variables and scopes` apply plus the following: + +* With mouse enabled, hover over a variable and get the value it evaluates to. +* Use your mouse to perform a visual selection of an expression (e.g. `a + b`) + and get its result. +* Make a normal mode (`nmap`) and visual mode (`xmap`) mapping to + `VimspectorBalloonEval` to manually trigger the popup. +* Use regular nagivation keys (`j`, `k`) to chose the current selection; `` + (or leave the tooltip window) to close the tooltip. + +![variable eval hover](https://puremourning.github.io/vimspector-web/img/vimspector-variable-eval-hover.png) + ## Watches The watch window is used to inspect variables and expressions. Expressions are diff --git a/autoload/vimspector.vim b/autoload/vimspector.vim index 35ecd03..d4bcfa7 100644 --- a/autoload/vimspector.vim +++ b/autoload/vimspector.vim @@ -523,6 +523,22 @@ function! vimspector#OnBufferCreated( file_name ) abort py3 _vimspector_session.RefreshSigns( vim.eval( 'a:file_name' ) ) 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, '\n' ) + else + let expr = expand( '' ) + endif + + return py3eval( '_vimspector_session.ShowEvalBalloon(' + \ . ' int( vim.eval( "winnr()" ) ), "' + \ . expr + \ . '", 0 )' ) +endfunction + " Boilerplate {{{ let &cpoptions=s:save_cpo diff --git a/autoload/vimspector/internal/balloon.vim b/autoload/vimspector/internal/balloon.vim index 360ed08..5f25f3a 100644 --- a/autoload/vimspector/internal/balloon.vim +++ b/autoload/vimspector/internal/balloon.vim @@ -19,16 +19,293 @@ let s:save_cpo = &cpoptions set cpoptions&vim " }}} -" Returns: py.ShowBalloon( winnr, expresssion ) -function! vimspector#internal#balloon#BalloonExpr() abort - " winnr + 1 because for *no good reason* winnr is 0 based here unlike - " everywhere else - " int() because for *no good reason* winnr is a string. - return py3eval('_vimspector_session.ShowBalloon(' - \ . 'int( vim.eval( "v:beval_winnr" ) ) + 1,' - \ . 'vim.eval( "v:beval_text" ) )' ) +scriptencoding utf-8 + +let s:popup_win_id = 0 +let s:nvim_border_win_id = 0 +" +" tooltip dimensions +let s:min_width = 1 +let s:min_height = 1 +let s:max_width = 80 +let s:max_height = 20 + +let s:is_neovim = has( 'nvim' ) + + +" 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#CreateTooltip( is_hover, ... ) abort + let body = [] + if a:0 > 0 + let body = a:1 + endif + + if s:popup_win_id != 0 + call vimspector#internal#balloon#Close() + endif + + if s:is_neovim + call s:CreateNeovimTooltip( body ) + else + let config = { + \ 'wrap': 0, + \ 'filtermode': 'n', + \ 'maxwidth': s:max_width, + \ 'maxheight': s:max_height, + \ 'minwidth': s:min_width, + \ 'minheight': s:min_height, + \ 'scrollbar': 1, + \ 'border': [], + \ 'padding': [ 0, 1, 0, 1], + \ 'drag': 1, + \ 'resize': 1, + \ 'close': 'button', + \ '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' ] = [ '─', '│', '─', '│', '╭', '╮', '┛', '╰' ] + endif + + if a:is_hover + 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:popup_win_id = popup_atcursor( body, config ) + endif + + endif + + 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 05245cd..6691a2a 100644 --- a/plugin/vimspector.vim +++ b/plugin/vimspector.vim @@ -60,6 +60,13 @@ nnoremap VimspectorStepOut nnoremap VimspectorRunToCursor \ :call vimspector#RunToCursor() +" Eval for normal mode +nnoremap VimspectorBalloonEval + \ :call vimspector#ShowEvalBalloon( 0 ) +" And for visual modes +xnoremap VimspectorBalloonEval + \ :call vimspector#ShowEvalBalloon( 1 ) + if s:mappings ==# 'VISUAL_STUDIO' nmap VimspectorContinue nmap VimspectorStop diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index ea72651..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 ): - self._variablesView.ExpandVariable() + def ExpandVariable( self, buf = None, line_num = None ): + self._variablesView.ExpandVariable( buf, line_num ) @IfConnected() def AddWatch( self, expression ): @@ -538,13 +538,13 @@ class DebugSession( object ): def DeleteWatch( self ): self._variablesView.DeleteWatch() + @IfConnected() - def ShowBalloon( self, winnr, expression ): - """Proxy: ballonexpr -> variables.ShowBallon""" + def ShowEvalBalloon( self, winnr, expression, is_hover ): frame = self._stackTraceView.GetCurrentFrame() # Check if RIP is in a frame if frame is None: - self._logger.debug( 'Balloon: Not in a stack frame' ) + self._logger.debug( 'Tooltip: Not in a stack frame' ) return '' # Check if cursor in code window @@ -555,7 +555,11 @@ class DebugSession( object ): return '' # Return variable aware function - return self._variablesView.ShowBalloon( frame, expression ) + return self._variablesView.VariableEval( frame, expression, is_hover ) + + + def CleanUpTooltip( self ): + return self._variablesView.CleanUpTooltip() @IfConnected() def ExpandFrameOrThread( self ): @@ -679,6 +683,7 @@ class DebugSession( object ): 'variables': utils.WindowID( vars_window, self._uiTab ), 'watches': utils.WindowID( watch_window, self._uiTab ), 'output': utils.WindowID( output_window, self._uiTab ), + 'eval': None # this is going to be updated every time eval popup is opened } with utils.RestoreCursorPosition(): with utils.RestoreCurrentWindow(): diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index e621d33..4022315 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -634,15 +634,19 @@ def ParseVariables( variables_list, return new_variables -def DisplayBaloon( is_term, display ): +def DisplayBalloon( is_term, display, is_hover = False ): if not is_term: - display = '\n'.join( display ) # To enable the Windows GUI to display the balloon correctly # Refer https://github.com/vim/vim/issues/1512#issuecomment-492070685 - vim.eval( "balloon_show( '' )" ) + display = '\n'.join( display ) - vim.eval( "balloon_show( {0} )".format( - json.dumps( display ) ) ) + created_win_id = int( vim.eval( + "vimspector#internal#balloon#CreateTooltip({}, {})".format( + int( is_hover ), json.dumps( display ) + ) + ) ) + + return created_win_id def GetBufferFilepath( buf ): @@ -719,6 +723,28 @@ def GetBufferFiletypes( buf ): return ft.split( '.' ) +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 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 + + def DisplaySplash( api_prefix, splash, text ): if splash: return Call( f'vimspector#internal#{api_prefix}popup#UpdateSplash', diff --git a/python3/vimspector/variables.py b/python3/vimspector/variables.py index 4028722..74626ac 100644 --- a/python3/vimspector/variables.py +++ b/python3/vimspector/variables.py @@ -117,17 +117,36 @@ class Watch: self.expression = expression self.result = None + @staticmethod + def New( frame, expression, context ): + watch = { + 'expression': expression, + 'context': context, + } + if frame: + watch[ 'frameId' ] = frame[ 'id' ] + + return Watch( watch ) + class View: lines: typing.Dict[ int, Expandable ] draw: typing.Callable + syntax: str def __init__( self, win, lines, draw ): self.lines = lines self.draw = draw - self.buf = win.buffer + self.syntax = None + if win is not None: + self.buf = win.buffer + utils.SetUpUIWindow( win ) - utils.SetUpUIWindow( win ) + +class BufView( View ): + def __init__( self, buf, lines, draw ): + super().__init__( None, lines, draw ) + self.buf = buf class VariablesView( object ): @@ -138,6 +157,9 @@ class VariablesView( object ): self._connection = None self._current_syntax = '' + self._variable_eval: Scope = None + self._variable_eval_view: View = None + def AddExpandMappings(): vim.command( 'nnoremap ' ':call vimspector#ExpandVariable()' ) @@ -183,7 +205,9 @@ class VariablesView( object ): 'balloonexpr': vim.options[ 'balloonexpr' ], 'balloondelay': vim.options[ 'balloondelay' ], } - vim.options[ 'balloonexpr' ] = 'vimspector#internal#balloon#BalloonExpr()' + vim.options[ 'balloonexpr' ] = ( "vimspector#internal#" + "balloon#HoverTooltip()" ) + vim.options[ 'balloondelay' ] = 250 if has_balloon: @@ -201,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 ): @@ -216,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 ): @@ -267,15 +294,98 @@ class VariablesView( object ): }, } ) - def AddWatch( self, frame, expression ): - watch = { - 'expression': expression, - 'context': 'watch', - } - if frame: - watch[ 'frameId' ] = frame[ 'id' ] + def _DrawBalloonEval( self ): + watch = self._variable_eval + view = self._variable_eval_view - self._watches.append( Watch( watch ) ) + with utils.RestoreCursorPosition(): + with utils.ModifiableScratchBuffer( view.buf ): + utils.ClearBuffer( view.buf ) + # FIXME: This probably doesn't work reliably + view.syntax = utils.SetSyntax( None, + self._current_syntax, + view.buf ) + + self._DrawWatchResult( view, + 0, + watch, + is_short = True ) + + vim.eval( "vimspector#internal#balloon#ResizeTooltip()" ) + + 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 + + def VariableEval( self, frame, expression, is_hover ): + """Callback to display variable under cursor `:h ballonexpr`""" + if not self._connection: + return '' + + def handler( message ): + + watch = self._variable_eval + if watch.result is None: + watch.result = WatchResult( message[ 'body' ] ) + else: + watch.result.Update( message[ 'body' ] ) + + popup_win_id = utils.DisplayBalloon( self._is_term, [], is_hover ) + # record the global eval window id + vim.vars[ 'vimspector_session_windows' ][ 'eval' ] = int( popup_win_id ) + popup_bufnr = int( vim.eval( "winbufnr({})".format( popup_win_id ) ) ) + + # 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(): + # Always expand the first level + watch.result.expanded = Expandable.EXPANDED_BY_US + + if watch.result.IsExpanded(): + self._connection.DoRequest( partial( self._ConsumeVariables, + self._variable_eval_view.draw, + watch.result ), { + 'command': 'variables', + 'arguments': { + 'variablesReference': watch.result.VariablesReference(), + }, + } ) + + self._DrawBalloonEval() + + def failure_handler( reason, message ): + display = [ reason ] + float_win_id = utils.DisplayBalloon( self._is_term, display, is_hover ) + # record the global eval window id + vim.vars[ 'vimspector_session_windows' ][ 'eval' ] = int( float_win_id ) + + self._variable_eval = Watch.New( frame, + expression, + 'hover' ) + + # Send async request + self._connection.DoRequest( handler, { + 'command': 'evaluate', + 'arguments': self._variable_eval.expression, + }, failure_handler ) + + # Return working (meanwhile) + return '' + + def AddWatch( self, frame, expression ): + self._watches.append( Watch.New( frame, expression, 'watch' ) ) self.EvaluateWatches() def DeleteWatch( self ): @@ -338,19 +448,27 @@ class VariablesView( object ): watch.result = WatchFailure( reason ) self._DrawWatches() - def ExpandVariable( self ): - 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 + and buf == self._variable_eval_view.buf ): + view = self._variable_eval_view else: return - current_line = vim.current.window.cursor[ 0 ] - 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 @@ -371,25 +489,40 @@ class VariablesView( object ): }, } ) - def _DrawVariables( self, view, variables, indent ): + def _DrawVariables( self, view, variables, indent, is_short = False ): assert indent > 0 for variable in variables: - line = utils.AppendToBuffer( - view.buf, - '{indent}{marker}{icon} {name} ({type_}): {value}'.format( + text = '' + if is_short: + text = '{indent}{icon} {name}: {value}'.format( + # We borrow 1 space of indent to draw the change marker + indent = ' ' * ( indent - 1 ), + icon = '+' if ( variable.IsExpandable() + and not variable.IsExpanded() ) else '-', + name = variable.variable.get( 'name', '' ), + value = variable.variable.get( 'value', '' ) + ) + else: + text = '{indent}{marker}{icon} {name} ({type_}): {value}'.format( # We borrow 1 space of indent to draw the change marker indent = ' ' * ( indent - 1 ), marker = '*' if variable.changed else ' ', icon = '+' if ( variable.IsExpandable() and not variable.IsExpanded() ) else '-', - name = variable.variable[ 'name' ], + name = variable.variable.get( 'name', '' ), type_ = variable.variable.get( 'type', '' ), - value = variable.variable.get( 'value', - '' ) ).split( '\n' ) ) + value = variable.variable.get( 'value', '' ) + ) + + line = utils.AppendToBuffer( + view.buf, + text.split( '\n' ) + ) + view.lines[ line ] = variable if variable.ShouldDrawDrillDown(): - self._DrawVariables( view, variable.variables, indent + 2 ) + self._DrawVariables( view, variable.variables, indent + 2, is_short ) def _DrawScopes( self ): # FIXME: The drawing is dumb and draws from scratch every time. This is @@ -416,7 +549,7 @@ class VariablesView( object ): 'Expression: ' + watch.expression[ 'expression' ] ) watch.line = line - self._DrawWatchResult( 2, watch ) + self._DrawWatchResult( self._watch, 2, watch ) def _DrawScope( self, indent, scope ): icon = '+' if scope.IsExpandable() and not scope.IsExpanded() else '-' @@ -432,27 +565,36 @@ class VariablesView( object ): indent += 2 self._DrawVariables( self._vars, scope.variables, indent ) - def _DrawWatchResult( self, indent, watch ): + def _DrawWatchResult( self, view, indent, watch, is_short = False ): if not watch.result: return - assert indent > 0 - icon = '+' if ( watch.result.IsExpandable() and - not watch.result.IsExpanded() ) else '-' + assert is_short or indent > 0 - line = '{indent}{marker}{icon} Result: {result}'.format( + if is_short: + # The first result is always expanded in a hover (short format) + icon = '' + marker = '' + leader = '' + else: + icon = '+' if ( watch.result.IsExpandable() and + not watch.result.IsExpanded() ) else '-' + marker = '*' if watch.result.changed else ' ' + leader = ' Result: ' + + line = '{indent}{marker}{icon}{leader}{result}'.format( # We borrow 1 space of indent to draw the change marker indent = ' ' * ( indent - 1 ), - marker = '*' if watch.result.changed else ' ', + marker = marker, icon = icon, + leader = leader, result = watch.result.result.get( 'result', '' ) ) - line = utils.AppendToBuffer( self._watch.buf, line.split( '\n' ) ) - self._watch.lines[ line ] = watch.result + line = utils.AppendToBuffer( view.buf, line.split( '\n' ) ) + view.lines[ line ] = watch.result if watch.result.ShouldDrawDrillDown(): - indent = 4 - self._DrawVariables( self._watch, watch.result.variables, indent ) + self._DrawVariables( view, watch.result.variables, indent + 2, is_short ) def _ConsumeVariables( self, draw, parent, message ): new_variables = [] @@ -467,7 +609,6 @@ class VariablesView( object ): variable = v found = True break - if not found: variable = Variable( variable_body ) else: @@ -489,47 +630,10 @@ class VariablesView( object ): draw() - def ShowBalloon( self, frame, expression ): - """Callback to display variable under cursor `:h ballonexpr`""" - if not self._connection: - return '' - - def handler( message ): - # TODO: this result count be expandable, but we have no way to allow the - # user to interact with the balloon to expand it, unless we use a popup - # instead, but even then we don't really want to trap the cursor. - body = message[ 'body' ] - result = body[ 'result' ] - if result is None: - result = 'null' - display = [ - 'Type: ' + body.get( 'type', '' ), - 'Value: ' + result - ] - utils.DisplayBaloon( self._is_term, display ) - - def failure_handler( reason, message ): - display = [ reason ] - utils.DisplayBaloon( self._is_term, display ) - - # Send async request - self._connection.DoRequest( handler, { - 'command': 'evaluate', - 'arguments': { - 'expression': expression, - 'frameId': frame[ 'id' ], - 'context': 'hover', - } - }, failure_handler ) - - # Return working (meanwhile) - return '...' - - def SetSyntax( self, syntax ): + # TODO: Switch to View.syntax self._current_syntax = utils.SetSyntax( self._current_syntax, syntax, self._vars.buf, self._watch.buf ) - # vim: sw=2 diff --git a/tests/breakpoints.test.vim b/tests/breakpoints.test.vim index 2e3eef8..877c1cf 100644 --- a/tests/breakpoints.test.vim +++ b/tests/breakpoints.test.vim @@ -280,8 +280,8 @@ function! Test_Conditional_Line_Breakpoint() call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 16, 1 ) call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 16 ) - " Add the conditional breakpoint - call feedkeys( "\\\argc==0\\", 'xt' ) + " Add the conditional breakpoint (note , is the mapleader) + call feedkeys( ",\argc==0\\", 'xt' ) call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', \ 16, \ 'vimspectorBPCond', @@ -360,8 +360,8 @@ function! Test_Conditional_Line_Breakpoint_Hit() exe 'edit' fn call setpos( '.', [ 0, 14, 1 ] ) - " Add the conditional breakpoint (3 times) - call feedkeys( "\\\\3\", 'xt' ) + " Add the conditional breakpoint (3 times) (note , is the mapleader) + call feedkeys( ",\\3\", 'xt' ) call vimspector#test#signs#AssertSignGroupSingletonAtLine( \ 'VimspectorBP', \ 14, diff --git a/tests/breakpoints_doublewidth.test.vim b/tests/breakpoints_doublewidth.test.vim index c646c3e..4bc0571 100644 --- a/tests/breakpoints_doublewidth.test.vim +++ b/tests/breakpoints_doublewidth.test.vim @@ -293,8 +293,8 @@ function! Test_Conditional_Line_Breakpoint() call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 16, 1 ) call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 16 ) - " Add the conditional breakpoint - call feedkeys( "\\\argc==0\\", 'xt' ) + " Add the conditional breakpoint (, is mapleader) + call feedkeys( ",\argc==0\\", 'xt' ) call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', \ 16, \ 'vimspectorBPCond', @@ -370,8 +370,8 @@ function! Test_Conditional_Line_Breakpoint_Hit() exe 'edit' fn call setpos( '.', [ 0, 14, 1 ] ) - " Add the conditional breakpoint (3 times) - call feedkeys( "\\\\3\", 'xt' ) + " Add the conditional breakpoint (3 times) (, is mapleader) + call feedkeys( ",\\3\", 'xt' ) call vimspector#test#signs#AssertSignGroupSingletonAtLine( \ 'VimspectorBP', \ 14, diff --git a/tests/mappings.test.vim b/tests/mappings.test.vim index fcd19fe..0f9995d 100644 --- a/tests/mappings.test.vim +++ b/tests/mappings.test.vim @@ -106,9 +106,9 @@ function! Test_Use_Mappings_HUMAN() \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 16 ) \ } ) - " Run to cursor + " Run to cursor (note , is the mapleader) call cursor( 9, 1 ) - call feedkeys( "\\\", 'xt' ) + call feedkeys( ",\", 'xt' ) call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 9, 1 ) call WaitForAssert( {-> \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 9 ) diff --git a/tests/variables.test.vim b/tests/variables.test.vim index 9152875..a43fafd 100644 --- a/tests/variables.test.vim +++ b/tests/variables.test.vim @@ -591,3 +591,199 @@ function! Test_EvaluateFailure() call vimspector#test#setup#Reset() %bwipe! endfunction + +function! Test_VariableEval() + let fn = 'testdata/cpp/simple/struct.cpp' + call s:StartDebugging( #{ fn: fn, line: 24, col: 1, launch: #{ + \ configuration: 'run-to-breakpoint' + \ } } ) + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 26, 1 ) + + " leader is , + xmap d VimspectorBalloonEval + nmap d VimspectorBalloonEval + + "evaluate the prev line + call setpos( '.', [ 0, 24, 8 ] ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 24, 8 ) + call feedkeys( ',d', 'xt' ) + + call WaitForAssert( {-> + \ assert_notequal( v:none, g:vimspector_session_windows.eval ) + \ } ) + + call WaitForAssert( {-> + \ AssertMatchist( + \ [ + \ '{...}', + \ ' - i: 0', + \ ' - c: 0 ''\\0\{1,3}''', + \ ' - fffff: 0', + \ ' + another_test: ', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.eval ), + \ 1, + \ '$' ) + \ ) + \ } ) + + "Close + call feedkeys( "\", 'xt' ) + + call WaitForAssert( {-> + \ assert_equal( v:none, g:vimspector_session_windows.eval ) + \ } ) + + " test selection + call setpos( '.', [ 0, 24, 8 ] ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 24, 8 ) + + call feedkeys( 'viw,d', 'xt' ) + + call WaitForAssert( {-> + \ assert_notequal( v:none, g:vimspector_session_windows.eval ) + \ } ) + + call WaitForAssert( {-> + \ AssertMatchist( + \ [ + \ '{...}', + \ ' - i: 0', + \ ' - c: 0 ''\\0\{1,3}''', + \ ' - fffff: 0', + \ ' + another_test: ', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.eval ), + \ 1, + \ '$' ) + \ ) + \ } ) + + "Close + call feedkeys( "\", 'xt' ) + + call WaitForAssert( {-> + \ assert_equal( v:none, g:vimspector_session_windows.eval ) + \ } ) + + " Get back to normal mode + call feedkeys( "\", 'xt' ) + + " Evaluation error + call setpos( '.', [ 0, 25, 1 ] ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 25, 1 ) + call feedkeys( ',d', 'xt' ) + + call WaitForAssert( {-> + \ assert_notequal( v:none, g:vimspector_session_windows.eval ) + \ } ) + + call WaitForAssert( {-> + \ AssertMatchist( + \ [ + \ 'Evaluation error', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.eval ), + \ 1, + \ '$' ) + \ ) + \ } ) + + "Close + call feedkeys( "\", 'xt' ) + + call WaitForAssert( {-> + \ assert_equal( v:none, g:vimspector_session_windows.eval ) + \ } ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction + +function! Test_VariableEvalExpand() + let fn = 'testdata/cpp/simple/struct.cpp' + call s:StartDebugging( #{ fn: fn, line: 24, col: 1, launch: #{ + \ configuration: 'run-to-breakpoint' + \ } } ) + + call vimspector#StepOver() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 26, 1 ) + + " leader is , + xmap d VimspectorBalloonEval + nmap d VimspectorBalloonEval + + "evaluate the prev line + call setpos( '.', [ 0, 24, 8 ] ) + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( fn, 24, 8 ) + call feedkeys( ',d', 'xt' ) + + call WaitForAssert( {-> + \ assert_notequal( v:none, g:vimspector_session_windows.eval ) + \ } ) + + call WaitForAssert( {-> + \ AssertMatchist( + \ [ + \ '{...}', + \ ' - i: 0', + \ ' - c: 0 ''\\0\{1,3}''', + \ ' - fffff: 0', + \ ' + another_test: ', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.eval ), + \ 1, + \ '$' ) + \ ) + \ } ) + + " Expand + call feedkeys( "jjjj\", 'xt' ) + + call WaitForAssert( {-> + \ AssertMatchist( + \ [ + \ '{...}', + \ ' - i: 0', + \ ' - c: 0 ''\\0\{1,3}''', + \ ' - fffff: 0', + \ ' - another_test: ', + \ ' - choo: 0 ''\\0\{1,3}''', + \ ' + ints: ' + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.eval ), + \ 1, + \ '$' ) + \ ) + \ } ) + + "Collapse + call feedkeys( "\", 'xt' ) + + call WaitForAssert( {-> + \ AssertMatchist( + \ [ + \ '{...}', + \ ' - i: 0', + \ ' - c: 0 ''\\0\{1,3}''', + \ ' - fffff: 0', + \ ' + another_test: ', + \ ], + \ getbufline( winbufnr( g:vimspector_session_windows.eval ), + \ 1, + \ '$' ) + \ ) + \ } ) + + "Close + call feedkeys( "\", 'xt' ) + + call WaitForAssert( {-> + \ assert_equal( v:none, g:vimspector_session_windows.eval ) + \ } ) + + call vimspector#test#setup#Reset() + %bwipe! +endfunction diff --git a/tests/vimrc b/tests/vimrc index e362824..c65efa8 100644 --- a/tests/vimrc +++ b/tests/vimrc @@ -1,8 +1,10 @@ let g:vimspector_test_plugin_path = expand( ':p:h:h' ) set mouse=a set noequalalways +let mapleader = ',' +let maplocalleader = "\" -let &rtp = &rtp . ',' . g:vimspector_test_plugin_path +let &runtimepath = &runtimepath . ',' . g:vimspector_test_plugin_path filetype plugin indent on syntax enable