Remember user choices

There are 2 things we ask for input for:

- input variables
- exception breakpoints

It's irritating to have to repeat yourself when going through the
edit/debug loop.

Howver, cacheing has some quirks and disadvantages - they key one being
when to clear the cache. To resolve this we take two slightly different
approaches:

1. For input variables, we remember the choice of the user, but present
that only as the default, so they can just hit enter to accept it. We
already rememeber the choices for the length of the debug session (i.e.
across 'restart' calls).

2. For exception breakpoints, we remember the choices for as long as the
current session is running.

This allows users to hit the 'restart' button without being prompted at
all.

Meanwhile, we also remove the (broken) support for exception breakpoint
matchers and state the server default for exception breakpoint filters.
This commit is contained in:
Ben Jackson 2019-10-05 22:20:33 +01:00
commit b64946e34c
4 changed files with 69 additions and 50 deletions

View file

@ -43,7 +43,7 @@ class ProjectBreakpoints( object ):
# These are the user-entered breakpoints.
self._line_breakpoints = defaultdict( list )
self._func_breakpoints = []
self._exceptionBreakpoints = None
self._exception_breakpoints = None
# FIXME: Remove this. Remove breakpoints nonesense from code.py
self._breakpoints_handler = None
@ -66,13 +66,12 @@ class ProjectBreakpoints( object ):
def ConnectionClosed( self ):
self._breakpoints_handler = None
self._exceptionBreakpoints = None
self._server_capabilities = {}
self._connection = None
self.UpdateUI()
# for each breakpoint:
# clear its resolved status
# NOTE: we don't reset self._exception_breakpoints because we don't want to
# re-ask the user every time for the sane info.
def ListBreakpoints( self ):
@ -117,7 +116,6 @@ class ProjectBreakpoints( object ):
self._line_breakpoints = defaultdict( list )
self._func_breakpoints = []
self._exceptionBreakpoints = None
self.UpdateUI()
@ -250,16 +248,16 @@ class ProjectBreakpoints( object ):
}
)
if self._exceptionBreakpoints is None:
if self._exception_breakpoints is None:
self._SetUpExceptionBreakpoints()
if self._exceptionBreakpoints:
if self._exception_breakpoints:
awaiting = awaiting + 1
self._connection.DoRequest(
lambda msg: response_handler( None, None ),
{
'command': 'setExceptionBreakpoints',
'arguments': self._exceptionBreakpoints
'arguments': self._exception_breakpoints
}
)
@ -268,44 +266,36 @@ class ProjectBreakpoints( object ):
def _SetUpExceptionBreakpoints( self ):
exceptionBreakpointFilters = self._server_capabilities.get(
exception_breakpoint_filters = self._server_capabilities.get(
'exceptionBreakpointFilters',
[] )
if exceptionBreakpointFilters or not self._server_capabilities.get(
if exception_breakpoint_filters or not self._server_capabilities.get(
'supportsConfigurationDoneRequest' ):
exceptionFilters = []
if exceptionBreakpointFilters:
for f in exceptionBreakpointFilters:
response = utils.AskForInput(
"Enable exception filter '{}'? (Y/N)".format( f[ 'label' ] ) )
exception_filters = []
if exception_breakpoint_filters:
for f in exception_breakpoint_filters:
default_value = 'Y' if f.get( 'default' ) else 'N'
if response == 'Y':
exceptionFilters.append( f[ 'filter' ] )
elif not response and f.get( 'default' ):
exceptionFilters.append( f[ 'filter' ] )
result = utils.AskForInput(
"Break on {} (Y/N/default: {})? ".format( f[ 'label' ],
default_value ),
default_value )
self._exceptionBreakpoints = {
'filters': exceptionFilters
if result == 'Y':
exception_filters.append( f[ 'filter' ] )
elif not result and f.get( 'default' ):
exception_filters.append( f[ 'filter' ] )
self._exception_breakpoints = {
'filters': exception_filters
}
if self._server_capabilities.get( 'supportsExceptionOptions' ):
# FIXME Sigh. The python debug adapter requires this
# key to exist. Even though it is optional.
break_mode = utils.SelectFromList( 'When to break on exception?',
[ 'never',
'always',
'unhandled',
'userHandled' ] )
if not break_mode:
break_mode = 'unhandled'
path = [ { 'nagate': True, 'names': [ 'DO_NOT_MATCH' ] } ]
self._exceptionBreakpoints[ 'exceptionOptions' ] = [ {
'path': path,
'breakMode': break_mode
} ]
# TODO: There are more elaborate exception breakpoint options here, but
# we don't support them. It doesn't seem like any of the servers really
# pay any attention to them anyway.
self._exception_breakpoints[ 'exceptionOptions' ] = []
def _ShowBreakpoints( self ):
for file_name, line_breakpoints in self._line_breakpoints.items():

View file

@ -34,6 +34,9 @@ VIMSPECTOR_HOME = os.path.abspath( os.path.join( os.path.dirname( __file__ ),
'..',
'..' ) )
# cache of what the user entered for any option we ask them
USER_CHOICES = {}
class DebugSession( object ):
def __init__( self ):
@ -148,14 +151,26 @@ class DebugSession( object ):
}
self._variables.update(
utils.ParseVariables( adapter.get( 'variables', {} ),
self._variables ) )
self._variables,
USER_CHOICES ) )
self._variables.update(
utils.ParseVariables( configuration.get( 'variables', {} ),
self._variables ) )
self._variables,
USER_CHOICES ) )
# Pretend that vars passed to the launch command were typed in by the user
# (they may have been in theory)
# TODO: Is it right that we do this _after_ ParseVariables, rather than
# before ?
USER_CHOICES.update( launch_variables )
self._variables.update( launch_variables )
utils.ExpandReferencesInDict( configuration, self._variables )
utils.ExpandReferencesInDict( adapter, self._variables )
utils.ExpandReferencesInDict( configuration,
self._variables,
USER_CHOICES )
utils.ExpandReferencesInDict( adapter,
self._variables,
USER_CHOICES )
if not adapter:
utils.UserMessage( 'No adapter configured for {}'.format(

View file

@ -261,10 +261,16 @@ def SelectFromList( prompt, options ):
return None
def AskForInput( prompt ):
def AskForInput( prompt, default_value = None ):
if default_value is None:
default_option = ''
else:
default_option = ", '{}'".format( Escape( default_value ) )
with InputSave():
try:
return vim.eval( "input( '{0}' )".format( Escape( prompt ) ) )
return vim.eval( "input( '{}' {} )".format( Escape( prompt ),
default_option ) )
except KeyboardInterrupt:
return ''
@ -306,7 +312,7 @@ def IsCurrent( window, buf ):
# TODO: Should we just run the substitution on the whole JSON string instead?
# That woul dallow expansion in bool and number values, such as ports etc. ?
def ExpandReferencesInDict( obj, mapping, **kwargs ):
def ExpandReferencesInDict( obj, mapping, user_choices ):
def expand_refs_in_string( orig_s ):
s = os.path.expanduser( orig_s )
s = os.path.expandvars( s )
@ -318,13 +324,16 @@ def ExpandReferencesInDict( obj, mapping, **kwargs ):
++bug_catcher
try:
s = string.Template( s ).substitute( mapping, **kwargs )
s = string.Template( s ).substitute( mapping )
break
except KeyError as e:
# HACK: This is seemingly the only way to get the key. str( e ) returns
# the key surrounded by '' for unknowable reasons.
key = e.args[ 0 ]
mapping[ key ] = AskForInput( 'Enter value for {}: '.format( key ) )
default_value = user_choices.get( key, None )
mapping[ key ] = AskForInput( 'Enter value for {}: '.format( key ),
default_value )
user_choices[ key ] = mapping[ key ]
_logger.debug( "Value for %s not set in %s (from %s): set to %s",
key,
s,
@ -339,7 +348,7 @@ def ExpandReferencesInDict( obj, mapping, **kwargs ):
def expand_refs_in_object( obj ):
if isinstance( obj, dict ):
ExpandReferencesInDict( obj, mapping, **kwargs )
ExpandReferencesInDict( obj, mapping, user_choices )
elif isinstance( obj, list ):
for i, _ in enumerate( obj ):
# FIXME: We are assuming that it is a list of string, but could be a
@ -354,7 +363,7 @@ def ExpandReferencesInDict( obj, mapping, **kwargs ):
obj[ k ] = expand_refs_in_object( obj[ k ] )
def ParseVariables( variables_list, mapping, **kwargs ):
def ParseVariables( variables_list, mapping, user_choices ):
new_variables = {}
new_mapping = mapping.copy()
@ -371,7 +380,7 @@ def ParseVariables( variables_list, mapping, **kwargs ):
new_v = v.copy()
# Bit of a hack. Allows environment variables to be used.
ExpandReferencesInDict( new_v, new_mapping, **kwargs )
ExpandReferencesInDict( new_v, new_mapping, user_choices )
env = os.environ.copy()
env.update( new_v.get( 'env' ) or {} )