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) * [Run to Cursor](#run-to-cursor)
* [Stepping](#stepping) * [Stepping](#stepping)
* [Variables and scopes](#variables-and-scopes) * [Variables and scopes](#variables-and-scopes)
* [Variable or selection hover evaluation](#variable-or-selection-hover-evaluation)
* [Watches](#watches) * [Watches](#watches)
* [Watch autocompletion](#watch-autocompletion) * [Watch autocompletion](#watch-autocompletion)
* [Stack Traces](#stack-traces) * [Stack Traces](#stack-traces)
@ -89,7 +90,7 @@ For detailed explanatin of the `.vimspector.json` format, see the
* [Example](#example) * [Example](#example)
* [FAQ](#faq) * [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--> <!--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 The [Vimspector website][website] has an overview of the UI, along with basic
instructions for configuration and setup. 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) ![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 - locals and globals display
- watch expressions with autocompletion - watch expressions with autocompletion
- call stack display and navigation - call stack display and navigation
- variable value display hover - hierarchical variable value display popup (see `<Plug>VimspectorBalloonEval`)
- interactive debug console with autocompletion - interactive debug console with autocompletion
- launch debugee within Vim's embedded terminal - launch debugee within Vim's embedded terminal
- logging/stdout display - logging/stdout display
@ -230,8 +231,8 @@ Why such a new vim ? Well 2 reasons:
if you hit them. if you hit them.
Why is neovim experimental? Because the author doesn't use neovim regularly, and 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 there are no regression tests for vimspector in neovim, so it may break
frequently. Issue reports are handled on best-efforts basis, and PRs are 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 welcome to fix bugs. See also the next section descibing differences for neovim
vs vim. vs vim.
@ -249,7 +250,8 @@ neovim doesn't implement some features Vimspector relies on:
the output window's current output. the output window's current output.
* Prompt Buffers - used to send commands in the Console and add Watches. * Prompt Buffers - used to send commands in the Console and add Watches.
(*Note*: prompt buffers are available in neovim nightly) (*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: Workarounds are in place as follows:
@ -258,9 +260,18 @@ Workarounds are in place as follows:
[`:VimspectorReset`](#closing-debugger) [`:VimspectorReset`](#closing-debugger)
* Prompt Buffers - There are [`:VimspectorEval`](#console) * Prompt Buffers - There are [`:VimspectorEval`](#console)
and [`:VimspectorWatch`](#watches) 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 ```viml
`:VimspectorEval` or `:VimspectorWatch`, or switch to Vim. " 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 ## Windows differences
@ -653,6 +664,7 @@ features to set your own mappings. To that end, Vimspector defines the following
* `<Plug>VimspectorStepInto` * `<Plug>VimspectorStepInto`
* `<Plug>VimspectorStepOut` * `<Plug>VimspectorStepOut`
* `<Plug>VimspectorRunToCursor` * `<Plug>VimspectorRunToCursor`
* `<Plug>VimspectorBalloonEval`
These map roughly 1-1 with the API functions below. 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()` | | `F11` | Step Into | `vimspector#StepInto()` |
| `F12` | Step out of current function scope | `vimspector#StepOut()` | | `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 # Usage and API
This section defines detailed usage instructions, organised by feature. For most 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`. 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 ## Watches
The watch window is used to inspect variables and expressions. Expressions are 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' ) ) py3 _vimspector_session.RefreshSigns( vim.eval( 'a:file_name' ) )
endfunction 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 {{{ " Boilerplate {{{
let &cpoptions=s:save_cpo let &cpoptions=s:save_cpo

View file

@ -19,16 +19,293 @@ let s:save_cpo = &cpoptions
set cpoptions&vim set cpoptions&vim
" }}} " }}}
" Returns: py.ShowBalloon( winnr, expresssion ) scriptencoding utf-8
function! vimspector#internal#balloon#BalloonExpr() abort
" winnr + 1 because for *no good reason* winnr is 0 based here unlike let s:popup_win_id = 0
" everywhere else let s:nvim_border_win_id = 0
" int() because for *no good reason* winnr is a string. "
return py3eval('_vimspector_session.ShowBalloon(' " tooltip dimensions
\ . 'int( vim.eval( "v:beval_winnr" ) ) + 1,' let s:min_width = 1
\ . 'vim.eval( "v:beval_text" ) )' ) 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 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 {{{ " Boilerplate {{{
let &cpoptions=s:save_cpo let &cpoptions=s:save_cpo
unlet s:save_cpo unlet s:save_cpo

View file

@ -60,6 +60,13 @@ nnoremap <silent> <Plug>VimspectorStepOut
nnoremap <silent> <Plug>VimspectorRunToCursor nnoremap <silent> <Plug>VimspectorRunToCursor
\ :<c-u>call vimspector#RunToCursor()<CR> \ :<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' if s:mappings ==# 'VISUAL_STUDIO'
nmap <F5> <Plug>VimspectorContinue nmap <F5> <Plug>VimspectorContinue
nmap <S-F5> <Plug>VimspectorStop nmap <S-F5> <Plug>VimspectorStop

View file

@ -520,8 +520,8 @@ class DebugSession( object ):
self._stackTraceView.SetCurrentThread() self._stackTraceView.SetCurrentThread()
@IfConnected() @IfConnected()
def ExpandVariable( self ): def ExpandVariable( self, buf = None, line_num = None ):
self._variablesView.ExpandVariable() self._variablesView.ExpandVariable( buf, line_num )
@IfConnected() @IfConnected()
def AddWatch( self, expression ): def AddWatch( self, expression ):
@ -538,13 +538,13 @@ class DebugSession( object ):
def DeleteWatch( self ): def DeleteWatch( self ):
self._variablesView.DeleteWatch() self._variablesView.DeleteWatch()
@IfConnected() @IfConnected()
def ShowBalloon( self, winnr, expression ): def ShowEvalBalloon( self, winnr, expression, is_hover ):
"""Proxy: ballonexpr -> variables.ShowBallon"""
frame = self._stackTraceView.GetCurrentFrame() frame = self._stackTraceView.GetCurrentFrame()
# Check if RIP is in a frame # Check if RIP is in a frame
if frame is None: if frame is None:
self._logger.debug( 'Balloon: Not in a stack frame' ) self._logger.debug( 'Tooltip: Not in a stack frame' )
return '' return ''
# Check if cursor in code window # Check if cursor in code window
@ -555,7 +555,11 @@ class DebugSession( object ):
return '' return ''
# Return variable aware function # 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() @IfConnected()
def ExpandFrameOrThread( self ): def ExpandFrameOrThread( self ):
@ -679,6 +683,7 @@ class DebugSession( object ):
'variables': utils.WindowID( vars_window, self._uiTab ), 'variables': utils.WindowID( vars_window, self._uiTab ),
'watches': utils.WindowID( watch_window, self._uiTab ), 'watches': utils.WindowID( watch_window, self._uiTab ),
'output': utils.WindowID( output_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.RestoreCursorPosition():
with utils.RestoreCurrentWindow(): with utils.RestoreCurrentWindow():

View file

@ -634,15 +634,19 @@ def ParseVariables( variables_list,
return new_variables return new_variables
def DisplayBaloon( is_term, display ): def DisplayBalloon( is_term, display, is_hover = False ):
if not is_term: if not is_term:
display = '\n'.join( display )
# To enable the Windows GUI to display the balloon correctly # To enable the Windows GUI to display the balloon correctly
# Refer https://github.com/vim/vim/issues/1512#issuecomment-492070685 # Refer https://github.com/vim/vim/issues/1512#issuecomment-492070685
vim.eval( "balloon_show( '' )" ) display = '\n'.join( display )
vim.eval( "balloon_show( {0} )".format( created_win_id = int( vim.eval(
json.dumps( display ) ) ) "vimspector#internal#balloon#CreateTooltip({}, {})".format(
int( is_hover ), json.dumps( display )
)
) )
return created_win_id
def GetBufferFilepath( buf ): def GetBufferFilepath( buf ):
@ -719,6 +723,28 @@ def GetBufferFiletypes( buf ):
return ft.split( '.' ) 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 ): def DisplaySplash( api_prefix, splash, text ):
if splash: if splash:
return Call( f'vimspector#internal#{api_prefix}popup#UpdateSplash', return Call( f'vimspector#internal#{api_prefix}popup#UpdateSplash',

View file

@ -117,17 +117,36 @@ class Watch:
self.expression = expression self.expression = expression
self.result = None 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: class View:
lines: typing.Dict[ int, Expandable ] lines: typing.Dict[ int, Expandable ]
draw: typing.Callable draw: typing.Callable
syntax: str
def __init__( self, win, lines, draw ): def __init__( self, win, lines, draw ):
self.lines = lines self.lines = lines
self.draw = draw 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 ): class VariablesView( object ):
@ -138,6 +157,9 @@ class VariablesView( object ):
self._connection = None self._connection = None
self._current_syntax = '' self._current_syntax = ''
self._variable_eval: Scope = None
self._variable_eval_view: View = None
def AddExpandMappings(): def AddExpandMappings():
vim.command( 'nnoremap <silent> <buffer> <CR> ' vim.command( 'nnoremap <silent> <buffer> <CR> '
':<C-u>call vimspector#ExpandVariable()<CR>' ) ':<C-u>call vimspector#ExpandVariable()<CR>' )
@ -183,7 +205,9 @@ class VariablesView( object ):
'balloonexpr': vim.options[ 'balloonexpr' ], 'balloonexpr': vim.options[ 'balloonexpr' ],
'balloondelay': vim.options[ 'balloondelay' ], 'balloondelay': vim.options[ 'balloondelay' ],
} }
vim.options[ 'balloonexpr' ] = 'vimspector#internal#balloon#BalloonExpr()' vim.options[ 'balloonexpr' ] = ( "vimspector#internal#"
"balloon#HoverTooltip()" )
vim.options[ 'balloondelay' ] = 250 vim.options[ 'balloondelay' ] = 250
if has_balloon: if has_balloon:
@ -201,6 +225,7 @@ class VariablesView( object ):
utils.ClearBuffer( self._vars.buf ) utils.ClearBuffer( self._vars.buf )
with utils.ModifiableScratchBuffer( self._watch.buf ): with utils.ModifiableScratchBuffer( self._watch.buf ):
utils.ClearBuffer( self._watch.buf ) utils.ClearBuffer( self._watch.buf )
self.ClearTooltip()
self._current_syntax = '' self._current_syntax = ''
def ConnectionUp( self, connection ): def ConnectionUp( self, connection ):
@ -216,6 +241,8 @@ class VariablesView( object ):
utils.CleanUpHiddenBuffer( self._vars.buf ) utils.CleanUpHiddenBuffer( self._vars.buf )
utils.CleanUpHiddenBuffer( self._watch.buf ) utils.CleanUpHiddenBuffer( self._watch.buf )
self.ClearTooltip()
def LoadScopes( self, frame ): def LoadScopes( self, frame ):
def scopes_consumer( message ): def scopes_consumer( message ):
@ -267,15 +294,98 @@ class VariablesView( object ):
}, },
} ) } )
def AddWatch( self, frame, expression ): def _DrawBalloonEval( self ):
watch = { watch = self._variable_eval
'expression': expression, view = self._variable_eval_view
'context': 'watch',
}
if frame:
watch[ 'frameId' ] = frame[ 'id' ]
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() self.EvaluateWatches()
def DeleteWatch( self ): def DeleteWatch( self ):
@ -338,19 +448,27 @@ class VariablesView( object ):
watch.result = WatchFailure( reason ) watch.result = WatchFailure( reason )
self._DrawWatches() self._DrawWatches()
def ExpandVariable( self ): def ExpandVariable( self, buf = None, line_num = None ):
if vim.current.buffer == self._vars.buf: 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 view = self._vars
elif vim.current.buffer == self._watch.buf: elif buf == self._watch.buf:
view = self._watch view = self._watch
elif ( self._variable_eval_view is not None
and buf == self._variable_eval_view.buf ):
view = self._variable_eval_view
else: else:
return return
current_line = vim.current.window.cursor[ 0 ] if line_num not in view.lines:
if current_line not in view.lines:
return return
variable = view.lines[ current_line ] variable = view.lines[ line_num ]
if variable.IsExpanded(): if variable.IsExpanded():
# Collapse # 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 assert indent > 0
for variable in variables: for variable in variables:
line = utils.AppendToBuffer( text = ''
view.buf, if is_short:
'{indent}{marker}{icon} {name} ({type_}): {value}'.format( 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 # We borrow 1 space of indent to draw the change marker
indent = ' ' * ( indent - 1 ), indent = ' ' * ( indent - 1 ),
marker = '*' if variable.changed else ' ', marker = '*' if variable.changed else ' ',
icon = '+' if ( variable.IsExpandable() icon = '+' if ( variable.IsExpandable()
and not variable.IsExpanded() ) else '-', and not variable.IsExpanded() ) else '-',
name = variable.variable[ 'name' ], name = variable.variable.get( 'name', '' ),
type_ = variable.variable.get( 'type', '' ), type_ = variable.variable.get( 'type', '' ),
value = variable.variable.get( 'value', value = variable.variable.get( 'value', '<unknown>' )
'<unknown>' ) ).split( '\n' ) ) )
line = utils.AppendToBuffer(
view.buf,
text.split( '\n' )
)
view.lines[ line ] = variable view.lines[ line ] = variable
if variable.ShouldDrawDrillDown(): if variable.ShouldDrawDrillDown():
self._DrawVariables( view, variable.variables, indent + 2 ) self._DrawVariables( view, variable.variables, indent + 2, is_short )
def _DrawScopes( self ): def _DrawScopes( self ):
# FIXME: The drawing is dumb and draws from scratch every time. This is # FIXME: The drawing is dumb and draws from scratch every time. This is
@ -416,7 +549,7 @@ class VariablesView( object ):
'Expression: ' 'Expression: '
+ watch.expression[ 'expression' ] ) + watch.expression[ 'expression' ] )
watch.line = line watch.line = line
self._DrawWatchResult( 2, watch ) self._DrawWatchResult( self._watch, 2, watch )
def _DrawScope( self, indent, scope ): def _DrawScope( self, indent, scope ):
icon = '+' if scope.IsExpandable() and not scope.IsExpanded() else '-' icon = '+' if scope.IsExpandable() and not scope.IsExpanded() else '-'
@ -432,27 +565,36 @@ class VariablesView( object ):
indent += 2 indent += 2
self._DrawVariables( self._vars, scope.variables, indent ) 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: if not watch.result:
return return
assert indent > 0 assert is_short or indent > 0
icon = '+' if ( watch.result.IsExpandable() and
not watch.result.IsExpanded() ) else '-'
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 # We borrow 1 space of indent to draw the change marker
indent = ' ' * ( indent - 1 ), indent = ' ' * ( indent - 1 ),
marker = '*' if watch.result.changed else ' ', marker = marker,
icon = icon, icon = icon,
leader = leader,
result = watch.result.result.get( 'result', '<unknown>' ) ) result = watch.result.result.get( 'result', '<unknown>' ) )
line = utils.AppendToBuffer( self._watch.buf, line.split( '\n' ) ) line = utils.AppendToBuffer( view.buf, line.split( '\n' ) )
self._watch.lines[ line ] = watch.result view.lines[ line ] = watch.result
if watch.result.ShouldDrawDrillDown(): if watch.result.ShouldDrawDrillDown():
indent = 4 self._DrawVariables( view, watch.result.variables, indent + 2, is_short )
self._DrawVariables( self._watch, watch.result.variables, indent )
def _ConsumeVariables( self, draw, parent, message ): def _ConsumeVariables( self, draw, parent, message ):
new_variables = [] new_variables = []
@ -467,7 +609,6 @@ class VariablesView( object ):
variable = v variable = v
found = True found = True
break break
if not found: if not found:
variable = Variable( variable_body ) variable = Variable( variable_body )
else: else:
@ -489,47 +630,10 @@ class VariablesView( object ):
draw() 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 ): def SetSyntax( self, syntax ):
# TODO: Switch to View.syntax
self._current_syntax = utils.SetSyntax( self._current_syntax, self._current_syntax = utils.SetSyntax( self._current_syntax,
syntax, syntax,
self._vars.buf, self._vars.buf,
self._watch.buf ) self._watch.buf )
# vim: sw=2 # 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#AssertCursorIsAtLineInBuffer( 'simple.cpp', 16, 1 )
call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 16 ) call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 16 )
" Add the conditional breakpoint " Add the conditional breakpoint (note , is the mapleader)
call feedkeys( "\\\<F9>argc==0\<CR>\<CR>", 'xt' ) call feedkeys( ",\<F9>argc==0\<CR>\<CR>", 'xt' )
call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP',
\ 16, \ 16,
\ 'vimspectorBPCond', \ 'vimspectorBPCond',
@ -360,8 +360,8 @@ function! Test_Conditional_Line_Breakpoint_Hit()
exe 'edit' fn exe 'edit' fn
call setpos( '.', [ 0, 14, 1 ] ) call setpos( '.', [ 0, 14, 1 ] )
" Add the conditional breakpoint (3 times) " Add the conditional breakpoint (3 times) (note , is the mapleader)
call feedkeys( "\\\<F9>\<CR>3\<CR>", 'xt' ) call feedkeys( ",\<F9>\<CR>3\<CR>", 'xt' )
call vimspector#test#signs#AssertSignGroupSingletonAtLine( call vimspector#test#signs#AssertSignGroupSingletonAtLine(
\ 'VimspectorBP', \ 'VimspectorBP',
\ 14, \ 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#AssertCursorIsAtLineInBuffer( 'simple.cpp', 16, 1 )
call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 16 ) call vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 16 )
" Add the conditional breakpoint " Add the conditional breakpoint (, is mapleader)
call feedkeys( "\\\<F9>argc==0\<CR>\<CR>", 'xt' ) call feedkeys( ",\<F9>argc==0\<CR>\<CR>", 'xt' )
call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP', call vimspector#test#signs#AssertSignGroupSingletonAtLine( 'VimspectorBP',
\ 16, \ 16,
\ 'vimspectorBPCond', \ 'vimspectorBPCond',
@ -370,8 +370,8 @@ function! Test_Conditional_Line_Breakpoint_Hit()
exe 'edit' fn exe 'edit' fn
call setpos( '.', [ 0, 14, 1 ] ) call setpos( '.', [ 0, 14, 1 ] )
" Add the conditional breakpoint (3 times) " Add the conditional breakpoint (3 times) (, is mapleader)
call feedkeys( "\\\<F9>\<CR>3\<CR>", 'xt' ) call feedkeys( ",\<F9>\<CR>3\<CR>", 'xt' )
call vimspector#test#signs#AssertSignGroupSingletonAtLine( call vimspector#test#signs#AssertSignGroupSingletonAtLine(
\ 'VimspectorBP', \ 'VimspectorBP',
\ 14, \ 14,

View file

@ -106,9 +106,9 @@ function! Test_Use_Mappings_HUMAN()
\ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 16 ) \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 16 )
\ } ) \ } )
" Run to cursor " Run to cursor (note , is the mapleader)
call cursor( 9, 1 ) call cursor( 9, 1 )
call feedkeys( "\\\<F8>", 'xt' ) call feedkeys( ",\<F8>", 'xt' )
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 9, 1 ) call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'simple.cpp', 9, 1 )
call WaitForAssert( {-> call WaitForAssert( {->
\ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 9 ) \ vimspector#test#signs#AssertPCIsAtLineInBuffer( 'simple.cpp', 9 )

View file

@ -591,3 +591,199 @@ function! Test_EvaluateFailure()
call vimspector#test#setup#Reset() call vimspector#test#setup#Reset()
%bwipe! %bwipe!
endfunction 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' ) let g:vimspector_test_plugin_path = expand( '<sfile>:p:h:h' )
set mouse=a set mouse=a
set noequalalways 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 filetype plugin indent on
syntax enable syntax enable