Merge pull request #350 from puremourning/nvim-float-window

Replace vim balloons with popups
This commit is contained in:
mergify[bot] 2021-02-21 18:44:21 +00:00 committed by GitHub
commit e70b8f37a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 784 additions and 113 deletions

View file

@ -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)
<!-- Added by: ben, at: Sat 9 Jan 2021 13:13:28 GMT -->
<!-- Added by: ben, at: Sun 21 Feb 2021 16:59:12 GMT -->
<!--te-->
@ -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 `<Plug>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 `<Plug>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 <Leader>di <Plug>VimspectorBalloonEval
" for visual mode, the visually selected text
xmap <Leader>di <Plug>VimspectorBalloonEval
```
## Windows differences
@ -653,6 +664,7 @@ features to set your own mappings. To that end, Vimspector defines the following
* `<Plug>VimspectorStepInto`
* `<Plug>VimspectorStepOut`
* `<Plug>VimspectorRunToCursor`
* `<Plug>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 `<Plug>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 <Leader>di <Plug>VimspectorBalloonEval
" for visual mode, the visually selected text
xmap <Leader>di <Plug>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
`<Plug>VimspectorBalloonEval` to manually trigger the popup.
* Use regular nagivation keys (`j`, `k`) to chose the current selection; `<Esc>`
(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

View file

@ -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( '<cexpr>' )
endif
return py3eval( '_vimspector_session.ShowEvalBalloon('
\ . ' int( vim.eval( "winnr()" ) ), "'
\ . expr
\ . '", 0 )' )
endfunction
" Boilerplate {{{
let &cpoptions=s:save_cpo

View file

@ -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 ==# "\<Esc>"
call vimspector#internal#balloon#Close()
return 0
endif
if index( [ "\<leftmouse>", "\<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 ==? "\<CR>"
" 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( [ "\<LeftMouse>", "\<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 <silent> <buffer> <CR>
\ <cmd>call vimspector#ExpandVariable()<CR>
nnoremap <silent> <buffer> <Esc>
\ <cmd>quit<CR>
nnoremap <silent> <buffer> <2-LeftMouse>
\ <cmd>call vimspector#ExpandVariable()<CR>
" Close the popup whenever we leave this window
augroup vimspector#internal#balloon#nvim_float
autocmd!
autocmd WinLeave <buffer>
\ :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

View file

@ -60,6 +60,13 @@ nnoremap <silent> <Plug>VimspectorStepOut
nnoremap <silent> <Plug>VimspectorRunToCursor
\ :<c-u>call vimspector#RunToCursor()<CR>
" Eval for normal mode
nnoremap <silent> <Plug>VimspectorBalloonEval
\ :<c-u>call vimspector#ShowEvalBalloon( 0 )<CR>
" And for visual modes
xnoremap <silent> <Plug>VimspectorBalloonEval
\ :<c-u>call vimspector#ShowEvalBalloon( 1 )<CR>
if s:mappings ==# 'VISUAL_STUDIO'
nmap <F5> <Plug>VimspectorContinue
nmap <S-F5> <Plug>VimspectorStop

View file

@ -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():

View file

@ -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',

View file

@ -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 <silent> <buffer> <CR> '
':<C-u>call vimspector#ExpandVariable()<CR>' )
@ -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', '<unknown>' )
)
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',
'<unknown>' ) ).split( '\n' ) )
value = variable.variable.get( 'value', '<unknown>' )
)
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', '<unknown>' ) )
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', '<unknown>' ),
'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

View file

@ -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( "\\\<F9>argc==0\<CR>\<CR>", 'xt' )
" Add the conditional breakpoint (note , is the mapleader)
call feedkeys( ",\<F9>argc==0\<CR>\<CR>", '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( "\\\<F9>\<CR>3\<CR>", 'xt' )
" Add the conditional breakpoint (3 times) (note , is the mapleader)
call feedkeys( ",\<F9>\<CR>3\<CR>", 'xt' )
call vimspector#test#signs#AssertSignGroupSingletonAtLine(
\ 'VimspectorBP',
\ 14,

View file

@ -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( "\\\<F9>argc==0\<CR>\<CR>", 'xt' )
" Add the conditional breakpoint (, is mapleader)
call feedkeys( ",\<F9>argc==0\<CR>\<CR>", '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( "\\\<F9>\<CR>3\<CR>", 'xt' )
" Add the conditional breakpoint (3 times) (, is mapleader)
call feedkeys( ",\<F9>\<CR>3\<CR>", 'xt' )
call vimspector#test#signs#AssertSignGroupSingletonAtLine(
\ 'VimspectorBP',
\ 14,

View file

@ -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( "\\\<F8>", 'xt' )
call feedkeys( ",\<F8>", 'xt' )
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 9, 1 )
call WaitForAssert( {->
\ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 9 )

View file

@ -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 <buffer> <Leader>d <Plug>VimspectorBalloonEval
nmap <buffer> <Leader>d <Plug>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( "\<Esc>", '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( "\<Esc>", 'xt' )
call WaitForAssert( {->
\ assert_equal( v:none, g:vimspector_session_windows.eval )
\ } )
" Get back to normal mode
call feedkeys( "\<Esc>", '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( "\<Esc>", '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 <buffer> <Leader>d <Plug>VimspectorBalloonEval
nmap <buffer> <Leader>d <Plug>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\<CR>", '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( "\<CR>", '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( "\<Esc>", 'xt' )
call WaitForAssert( {->
\ assert_equal( v:none, g:vimspector_session_windows.eval )
\ } )
call vimspector#test#setup#Reset()
%bwipe!
endfunction

View file

@ -1,8 +1,10 @@
let g:vimspector_test_plugin_path = expand( '<sfile>:p:h:h' )
set mouse=a
set noequalalways
let mapleader = ','
let maplocalleader = "\<Space>"
let &rtp = &rtp . ',' . g:vimspector_test_plugin_path
let &runtimepath = &runtimepath . ',' . g:vimspector_test_plugin_path
filetype plugin indent on
syntax enable