vimspector/python3/vimspector/variables.py
Wayne Bowie f81cc126c3 Format fix
2020-07-14 14:04:31 -05:00

502 lines
16 KiB
Python

# vimspector - A multi-language debugging system for Vim
# Copyright 2018 Ben Jackson
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import abc
import vim
import logging
from functools import partial
import typing
from vimspector import utils
class Expandable:
"""Base for anything which might contain a hierarchy of values represented by
a 'variablesReference' to be resolved by the 'variables' request. Records the
current state expanded/collapsed. Implementations just implement
VariablesReference to get the variables."""
def __init__( self ):
self.variables: typing.List[ 'Variable' ] = None
# None is Falsy and represents collapsed _by default_. WHen set to False,
# this means the user explicitly collapsed it. When True, the user expanded
# it (or we expanded it by default).
self.expanded: bool = None
def IsCollapsedByUser( self ):
return self.expanded is False
def IsExpandedByUser( self ):
return self.expanded is True
def ShouldDrawDrillDown( self ):
return self.IsExpandedByUser() and self.variables is not None
def IsExpandable( self ):
return self.VariablesReference() > 0
@abc.abstractmethod
def VariablesReference( self ):
assert False
class Scope( Expandable ):
"""Holds an expandable scope (a DAP scope dict), with expand/collapse state"""
def __init__( self, scope: dict ):
super().__init__()
self.scope = scope
def VariablesReference( self ):
return self.scope.get( 'variablesReference', 0 )
def Update( self, scope ):
self.scope = scope
class WatchResult( Expandable ):
"""Holds the result of a Watch expression with expand/collapse."""
def __init__( self, result: dict ):
super().__init__()
self.result = result
# A new watch result is marked as changed
self.changed = True
def VariablesReference( self ):
return self.result.get( 'variablesReference', 0 )
def Update( self, result ):
self.changed = False
if self.result[ 'result' ] != result[ 'result' ]:
self.changed = True
self.result = result
class Variable( Expandable ):
"""Holds one level of an expanded value tree. Also itself expandable."""
def __init__( self, variable: dict ):
super().__init__()
self.variable = variable
# A new variable appearing is marked as changed
self.changed = True
def VariablesReference( self ):
return self.variable.get( 'variablesReference', 0 )
def Update( self, variable ):
self.changed = False
if self.variable[ 'value' ] != variable[ 'value' ]:
self.changed = True
self.variable = variable
class Watch:
"""Holds a user watch expression (DAP request) and the result (WatchResult)"""
def __init__( self, expression: dict ):
self.result: WatchResult
self.expression = expression
self.result = None
class View:
lines: typing.Dict[ int, Expandable ]
draw: typing.Callable
def __init__( self, win, lines, draw ):
self.lines = lines
self.draw = draw
self.buf = win.buffer
utils.SetUpUIWindow( win )
class VariablesView( object ):
def __init__( self, connection, variables_win, watches_win ):
self._logger = logging.getLogger( __name__ )
utils.SetUpLogging( self._logger )
self._connection = connection
self._current_syntax = ''
# Set up the "Variables" buffer in the variables_win
self._scopes: typing.List[ Scope ] = []
self._vars = View( variables_win, {}, self._DrawScopes )
utils.SetUpHiddenBuffer( self._vars.buf, 'vimspector.Variables' )
with utils.LetCurrentWindow( variables_win ):
vim.command(
'nnoremap <buffer> <CR> :call vimspector#ExpandVariable()<CR>' )
# Set up the "Watches" buffer in the watches_win (and create a WinBar in
# there)
self._watches: typing.List[ Watch ] = []
self._watch = View( watches_win, {}, self._DrawWatches )
utils.SetUpPromptBuffer( self._watch.buf,
'vimspector.Watches',
'Expression: ',
'vimspector#AddWatchPrompt' )
with utils.LetCurrentWindow( watches_win ):
vim.command(
'nnoremap <buffer> <CR> :call vimspector#ExpandVariable()<CR>' )
vim.command(
'nnoremap <buffer> <DEL> :call vimspector#DeleteWatch()<CR>' )
vim.command( 'nnoremenu 1.1 WinBar.New '
':call vimspector#AddWatch()<CR>' )
vim.command( 'nnoremenu 1.2 WinBar.Expand/Collapse '
':call vimspector#ExpandVariable()<CR>' )
vim.command( 'nnoremenu 1.3 WinBar.Delete '
':call vimspector#DeleteWatch()<CR>' )
# Set the (global!) balloon expr if supported
has_balloon = int( vim.eval( "has( 'balloon_eval' )" ) )
has_balloon_term = int( vim.eval( "has( 'balloon_eval_term' )" ) )
self._oldoptions = {}
if has_balloon or has_balloon_term:
self._oldoptions = {
'balloonexpr': vim.options[ 'balloonexpr' ],
'balloondelay': vim.options[ 'balloondelay' ],
}
vim.options[ 'balloonexpr' ] = 'vimspector#internal#balloon#BalloonExpr()'
vim.options[ 'balloondelay' ] = 250
if has_balloon:
self._oldoptions[ 'ballooneval' ] = vim.options[ 'ballooneval' ]
vim.options[ 'ballooneval' ] = True
if has_balloon_term:
self._oldoptions[ 'balloonevalterm' ] = vim.options[ 'balloonevalterm' ]
vim.options[ 'balloonevalterm' ] = True
self._is_term = not bool( int( vim.eval( "has( 'gui_running' )" ) ) )
def Clear( self ):
with utils.ModifiableScratchBuffer( self._vars.buf ):
utils.ClearBuffer( self._vars.buf )
with utils.ModifiableScratchBuffer( self._watch.buf ):
utils.ClearBuffer( self._watch.buf )
self._current_syntax = ''
def ConnectionUp( self, connection ):
self._connection = connection
def ConnectionClosed( self ):
self.Clear()
self._connection = None
def Reset( self ):
for k, v in self._oldoptions.items():
vim.options[ k ] = v
utils.CleanUpHiddenBuffer( self._vars.buf )
utils.CleanUpHiddenBuffer( self._watch.buf )
def LoadScopes( self, frame ):
def scopes_consumer( message ):
new_scopes = []
for scope_body in message[ 'body' ][ 'scopes' ]:
# Find it in the scopes list
found = False
for index, s in enumerate( self._scopes ):
if s.scope[ 'name' ] == scope_body[ 'name' ]:
found = True
scope = s
break
if not found:
scope = Scope( scope_body )
else:
scope.Update( scope_body )
new_scopes.append( scope )
if not scope.scope.get( 'expensive' ) and not scope.IsCollapsedByUser():
# Expand any non-expensive scope which is not manually collapsed
scope.expanded = True
if scope.IsExpandedByUser():
self._connection.DoRequest( partial( self._ConsumeVariables,
self._DrawScopes,
scope ), {
'command': 'variables',
'arguments': {
'variablesReference': scope.VariablesReference(),
},
} )
self._scopes = new_scopes
self._DrawScopes()
self._connection.DoRequest( scopes_consumer, {
'command': 'scopes',
'arguments': {
'frameId': frame[ 'id' ]
},
} )
def AddWatch( self, frame, expression ):
watch = {
'expression': expression,
'context': 'watch',
}
if frame:
watch[ 'frameId' ] = frame[ 'id' ]
self._watches.append( Watch( watch ) )
self.EvaluateWatches()
def DeleteWatch( self ):
if vim.current.buffer != self._watch.buf:
utils.UserMessage( 'Not a watch buffer' )
return
current_line = vim.current.window.cursor[ 0 ]
best_index = -1
for index, watch in enumerate( self._watches ):
if ( watch.line is not None
and watch.line <= current_line
and watch.line > best_index ):
best_index = index
if best_index >= 0:
del self._watches[ best_index ]
utils.UserMessage( 'Deleted' )
self._DrawWatches()
return
utils.UserMessage( 'No watch found' )
def EvaluateWatches( self ):
for watch in self._watches:
self._connection.DoRequest( partial( self._UpdateWatchExpression,
watch ), {
'command': 'evaluate',
'arguments': watch.expression,
} )
def _UpdateWatchExpression( self, watch: Watch, message: dict ):
if watch.result is not None:
watch.result.Update( message[ 'body' ] )
else:
watch.result = WatchResult( message[ 'body' ] )
if ( watch.result.IsExpandable() and
watch.result.IsExpandedByUser() ):
self._connection.DoRequest( partial( self._ConsumeVariables,
self._watch.draw,
watch.result ), {
'command': 'variables',
'arguments': {
'variablesReference': watch.result.VariablesReference(),
},
} )
self._DrawWatches()
def ExpandVariable( self ):
if vim.current.buffer == self._vars.buf:
view = self._vars
elif vim.current.buffer == self._watch.buf:
view = self._watch
else:
return
current_line = vim.current.window.cursor[ 0 ]
if current_line not in view.lines:
return
variable = view.lines[ current_line ]
if variable.expanded:
# Collapse
variable.expanded = False
view.draw()
return
if not variable.IsExpandable():
return
variable.expanded = True
self._connection.DoRequest( partial( self._ConsumeVariables,
view.draw,
variable ), {
'command': 'variables',
'arguments': {
'variablesReference': variable.VariablesReference()
},
} )
def _DrawVariables( self, view, variables, indent ):
assert indent > 0
for variable in variables:
line = utils.AppendToBuffer(
view.buf,
'{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.IsExpandedByUser() ) else '-',
name = variable.variable[ 'name' ],
type_ = variable.variable.get( 'type', '<unknown type>' ),
value = variable.variable.get( 'value',
'<unknown value>' ) ).split( '\n' ) )
view.lines[ line ] = variable
if variable.ShouldDrawDrillDown():
self._DrawVariables( view, variable.variables, indent + 2 )
def _DrawScopes( self ):
# FIXME: The drawing is dumb and draws from scratch every time. This is
# simple and works and makes sure the line-map is always correct.
# However it is pretty inefficient.
self._vars.lines.clear()
with utils.RestoreCursorPosition():
with utils.ModifiableScratchBuffer( self._vars.buf ):
utils.ClearBuffer( self._vars.buf )
for scope in self._scopes:
self._DrawScope( 0, scope )
def _DrawWatches( self ):
# FIXME: The drawing is dumb and draws from scratch every time. This is
# simple and works and makes sure the line-map is always correct.
# However it is pretty inefficient.
self._watch.lines.clear()
with utils.RestoreCursorPosition():
with utils.ModifiableScratchBuffer( self._watch.buf ):
utils.ClearBuffer( self._watch.buf )
utils.AppendToBuffer( self._watch.buf, 'Watches: ----' )
for watch in self._watches:
line = utils.AppendToBuffer( self._watch.buf,
'Expression: '
+ watch.expression[ 'expression' ] )
watch.line = line
self._DrawWatchResult( 2, watch )
def _DrawScope( self, indent, scope ):
icon = '+' if scope.IsExpandable() and not scope.IsExpandedByUser() else '-'
line = utils.AppendToBuffer( self._vars.buf,
'{0}{1} Scope: {2}'.format(
' ' * indent,
icon,
scope.scope[ 'name' ] ) )
self._vars.lines[ line ] = scope
if scope.ShouldDrawDrillDown():
indent += 2
self._DrawVariables( self._vars, scope.variables, indent )
def _DrawWatchResult( self, indent, watch ):
if not watch.result:
return
assert indent > 0
icon = '+' if ( watch.result.IsExpandable() and
not watch.result.IsExpandedByUser() ) else '-'
line = '{indent}{marker}{icon} Result: {result}'.format(
# We borrow 1 space of indent to draw the change marker
indent = ' ' * ( indent - 1 ),
marker = '*' if watch.result.changed else ' ',
icon = icon,
result = watch.result.result.get( 'result', '<unknown>' ) )
line = utils.AppendToBuffer( self._watch.buf, line.split( '\n' ) )
self._watch.lines[ line ] = watch.result
if watch.result.ShouldDrawDrillDown():
indent = 4
self._DrawVariables( self._watch, watch.result.variables, indent )
def _ConsumeVariables( self, draw, parent, message ):
new_variables = []
for variable_body in message[ 'body' ][ 'variables' ]:
if parent.variables is None:
parent.variables = []
# Find the variable in parent
found = False
for index, v in enumerate( parent.variables ):
if v.variable[ 'name' ] == variable_body[ 'name' ]:
variable = v
found = True
break
if not found:
variable = Variable( variable_body )
else:
variable.Update( variable_body )
new_variables.append( variable )
if variable.IsExpandable() and variable.IsExpandedByUser():
self._connection.DoRequest( partial( self._ConsumeVariables,
draw,
variable ), {
'command': 'variables',
'arguments': {
'variablesReference': variable.VariablesReference()
},
} )
parent.variables = new_variables
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 ):
self._current_syntax = utils.SetSyntax( self._current_syntax,
syntax,
self._vars.buf,
self._watch.buf )
# vim: sw=2