From 044804ca20b75fceddea01d193401eb57e02bd25 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 8 Jul 2020 21:22:28 +0100 Subject: [PATCH 1/7] Calculate variables on-demand; add an unused-local-port variable-function --- python3/vimspector/debug_session.py | 40 +++++++++++------- python3/vimspector/utils.py | 63 +++++++++++++++++++++-------- 2 files changed, 71 insertions(+), 32 deletions(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index e3e1be4..244a0c4 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -177,43 +177,53 @@ class DebugSession( object ): return [ '', '' ] return os.path.splitext( p ) - self._variables = { + variables = { 'dollar': '$', # HACK. Hote '$$' also works. 'workspaceRoot': self._workspace_root, 'workspaceFolder': self._workspace_root, - 'gadgetDir': install.GetGadgetDir( VIMSPECTOR_HOME, install.GetOS() ), 'file': current_file, - 'relativeFile': relpath( current_file, self._workspace_root ), - 'fileBasename': os.path.basename( current_file ), + } + + calculus = { + 'gadgetDir': lambda: install.GetGadgetDir( VIMSPECTOR_HOME, + install.GetOS() ), + 'relativeFile': lambda: relpath( current_file, + self._workspace_root ), + 'fileBasename': lambda: os.path.basename( current_file ), 'fileBasenameNoExtension': - splitext( os.path.basename( current_file ) )[ 0 ], - 'fileDirname': os.path.dirname( current_file ), - 'fileExtname': splitext( os.path.basename( current_file ) )[ 1 ], + lambda: splitext( os.path.basename( current_file ) )[ 0 ], + 'fileDirname': lambda: os.path.dirname( current_file ), + 'fileExtname': lambda: splitext( os.path.basename( current_file ) )[ 1 ], # NOTE: this is the window-local cwd for the current window, *not* Vim's # working directory. - 'cwd': os.getcwd(), + 'cwd': os.getcwd, + 'unusedLocalPort': utils.GetUnusedLocalPort, } # Pretend that vars passed to the launch command were typed in by the user # (they may have been in theory) USER_CHOICES.update( launch_variables ) - self._variables.update( launch_variables ) + variables.update( launch_variables ) - self._variables.update( + variables.update( utils.ParseVariables( adapter.get( 'variables', {} ), - self._variables, + variables, + calculus, USER_CHOICES ) ) - self._variables.update( + variables.update( utils.ParseVariables( configuration.get( 'variables', {} ), - self._variables, + variables, + calculus, USER_CHOICES ) ) utils.ExpandReferencesInDict( configuration, - self._variables, + variables, + calculus, USER_CHOICES ) utils.ExpandReferencesInDict( adapter, - self._variables, + variables, + calculus, USER_CHOICES ) if not adapter: diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index a12f3dc..d00c8d7 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -345,9 +345,9 @@ def IsCurrent( window, buf ): return vim.current.window == window and vim.current.window.buffer == buf -def ExpandReferencesInObject( obj, mapping, user_choices ): +def ExpandReferencesInObject( obj, mapping, calculus, user_choices ): if isinstance( obj, dict ): - ExpandReferencesInDict( obj, mapping, user_choices ) + ExpandReferencesInDict( obj, mapping, calculus, user_choices ) elif isinstance( obj, list ): j_offset = 0 obj_copy = list( obj ) @@ -360,6 +360,7 @@ def ExpandReferencesInObject( obj, mapping, user_choices ): # *${something} - expand list in place value = ExpandReferencesInString( obj_copy[ i ][ 1: ], mapping, + calculus, user_choices ) obj.pop( j ) j_offset -= 1 @@ -369,14 +370,18 @@ def ExpandReferencesInObject( obj, mapping, user_choices ): else: obj[ j ] = ExpandReferencesInObject( obj_copy[ i ], mapping, + calculus, user_choices ) elif isinstance( obj, str ): - obj = ExpandReferencesInString( obj, mapping, user_choices ) + obj = ExpandReferencesInString( obj, mapping, calculus, user_choices ) return obj -def ExpandReferencesInString( orig_s, mapping, user_choices ): +def ExpandReferencesInString( orig_s, + mapping, + calculus, + user_choices ): s = os.path.expanduser( orig_s ) s = os.path.expandvars( s ) @@ -393,15 +398,19 @@ def ExpandReferencesInString( orig_s, mapping, user_choices ): # 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 ] - 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, - orig_s, - mapping[ key ] ) + + if key in calculus: + mapping[ key ] = calculus[ key ]() + else: + 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, + orig_s, + mapping[ key ] ) except ValueError as e: UserMessage( 'Invalid $ in string {}: {}'.format( s, e ), persist = True ) @@ -412,12 +421,18 @@ def ExpandReferencesInString( orig_s, mapping, user_choices ): # 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, user_choices ): +def ExpandReferencesInDict( obj, mapping, calculus, user_choices ): for k in obj.keys(): - obj[ k ] = ExpandReferencesInObject( obj[ k ], mapping, user_choices ) + obj[ k ] = ExpandReferencesInObject( obj[ k ], + mapping, + calculus, + user_choices ) -def ParseVariables( variables_list, mapping, user_choices ): +def ParseVariables( variables_list, + mapping, + calculus, + user_choices ): new_variables = {} new_mapping = mapping.copy() @@ -431,7 +446,10 @@ def ParseVariables( variables_list, mapping, user_choices ): if 'shell' in v: new_v = v.copy() # Bit of a hack. Allows environment variables to be used. - ExpandReferencesInDict( new_v, new_mapping, user_choices ) + ExpandReferencesInDict( new_v, + new_mapping, + calculus, + user_choices ) env = os.environ.copy() env.update( new_v.get( 'env' ) or {} ) @@ -455,6 +473,7 @@ def ParseVariables( variables_list, mapping, user_choices ): else: new_variables[ n ] = ExpandReferencesInObject( v, mapping, + calculus, user_choices ) return new_variables @@ -575,3 +594,13 @@ def GetVimspectorBase(): return base.decode( 'utf-8' ) else: return base + + +def GetUnusedLocalPort(): + import socket + sock = socket.socket() + # This tells the OS to give us any free port in the range [1024 - 65535] + sock.bind( ( '', 0 ) ) + port = sock.getsockname()[ 1 ] + sock.close() + return port From 81712b124febc8890a02d511c60c12650b967f02 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 9 Jul 2020 13:08:15 +0100 Subject: [PATCH 2/7] Fix traceback when (sometimes?) using the watch window --- python3/vimspector/variables.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python3/vimspector/variables.py b/python3/vimspector/variables.py index bb5b252..fe44880 100644 --- a/python3/vimspector/variables.py +++ b/python3/vimspector/variables.py @@ -230,7 +230,7 @@ class VariablesView( object ): scope ), { 'command': 'variables', 'arguments': { - 'variablesReference': scope.scope[ 'variablesReference' ] + 'variablesReference': scope.VariablesReference(), }, } ) @@ -295,10 +295,10 @@ class VariablesView( object ): watch.result.IsExpandedByUser() ): self._connection.DoRequest( partial( self._ConsumeVariables, self._watch.draw, - watch.result.result ), { + watch.result ), { 'command': 'variables', 'arguments': { - 'variablesReference': watch.result.result[ 'variablesReference' ] + 'variablesReference': watch.result.VariablesReference(), }, } ) From a647b659830030c4ab23b0b3f1007bdafc487579 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 9 Jul 2020 13:36:52 +0100 Subject: [PATCH 3/7] Use an unused local port for CodeLLDB --- install_gadget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install_gadget.py b/install_gadget.py index 627152e..2c3a6ba 100755 --- a/install_gadget.py +++ b/install_gadget.py @@ -478,9 +478,9 @@ GADGETS = { 'type': 'CodeLLDB', "command": [ "${gadgetDir}/CodeLLDB/adapter/codelldb", - "--port", "${port}" + "--port", "${unusedLocalPort}" ], - "port": "${port}", + "port": "${unusedLocalPort}", "configuration": { "type": "lldb", "name": "lldb", From fe8d7251a4406096428da3c8a4ded6f3f853b626 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 9 Jul 2020 18:07:58 +0100 Subject: [PATCH 4/7] Add unusedLocalPort to docs --- docs/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration.md b/docs/configuration.md index 6c82584..98be766 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -364,6 +364,7 @@ The following variables are provided: * `${fileDirname}` - the current opened file's dirname * `${fileExtname}` - the current opened file's extension * `${cwd}` - the current working directory of the active window on launch +* `${unusedLocalPort}` - an unused local TCP port ## Remote Debugging Support From 9baba5afabe1fd2893c4bbfe61f3953a11192bde Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 9 Jul 2020 18:19:11 +0100 Subject: [PATCH 5/7] Document --basedir and ClearBreakpoints() and Exception Breakpoints --- README.md | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a7befe..e060e6a 100644 --- a/README.md +++ b/README.md @@ -307,6 +307,30 @@ should be): ./install_gadget.py --all --disable-tcl ``` +If you want to just add a new adapter without destroying the exisitng ones, add +`--update-gadget-config`, as in: + +```bash +$ ./install_gadget.py --enable-tcl +$ ./install_gadget.py --enable-rust --update-gadget-config +$ ./install_gadget.py --enable-java --update-gadget-config +``` + +If you want to maintain `configurations` outside of the vimspector repository +(this can be useful if you have custom gadgets or global configurations), +you can tell the installer to use a different basedir, then set +`g:vimspector_base_dir` to point to that directory, for example: + +```bash +$ ./install_gadget.py --basedir $HOME/.vim/vimspector-config --all --force-all +``` + +Then add this to your `.vimrc`: + +```viml +let g:vimspector_base_dir=expand( '$HOME/.vim/vimspector-config' ) +``` + See `--help` for more info. ## Manual gadget installation @@ -571,7 +595,7 @@ This would start the `Run Test` configuration with `${Test}` set to `'Name of the test'` and Vimspector would _not_ prompt the user to enter or confirm these things. -See [this issue](https://github.com/puremourning/vimspector/issues/97) for +See [our YouCompleteMe integration guide](#usage-with-youcompleteme) for another example where it can be used to specify the port to connect the [java debugger](#java---partially-supported) @@ -599,6 +623,24 @@ whatever dialect the debugger understands when evaluating expressions. When using the `` mapping, the user is prompted to enter these expressions in a command line (with history). +### Exception breakpoints + +When starting debugging, you may be asekd a few questions about how to handle +exceptoins. These are "exception breakpoints" and vimspector remembers your +choices while Vim is still running. + +Typically you can accept the defaults (just keep pressing ``!) as most debug +adapter defaults are sane, but if you want to break on, say `uncaught exception` +then answer `Y` to that (for example). + +You can configure your choices in the `.vimspector.json`. See +[the configuration guide][vimspector-ref-exception] for details on that. + +### Clear breakpoints + +* Use `vimspector#ClearBreakpoints()` + to clear all breakpoints including the memory of exception breakpoint choices. + ## Stepping * Step in/out, finish, continue, pause etc. using the WinBar, or mappings. From a4abe511c78a713ec7b7b7b1f0197bee402779f8 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 9 Jul 2020 18:20:40 +0100 Subject: [PATCH 6/7] Update contents links --- README.md | 4 +++- docs/configuration.md | 56 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e060e6a..15e9872 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ For a tutorial and usage overview, take a look at the * [Launch and attach by PID:](#launch-and-attach-by-pid) * [Launch with options](#launch-with-options) * [Breakpoints](#breakpoints) + * [Exception breakpoints](#exception-breakpoints) + * [Clear breakpoints](#clear-breakpoints) * [Stepping](#stepping) * [Variables and scopes](#variables-and-scopes) * [Watches](#watches) @@ -66,7 +68,7 @@ For a tutorial and usage overview, take a look at the * [Motivation](#motivation) * [License](#license) - + diff --git a/docs/configuration.md b/docs/configuration.md index 98be766..14f5047 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -11,15 +11,21 @@ for Vimspector. * [Debug adapter configuration](#debug-adapter-configuration) * [Debug profile configuration](#debug-profile-configuration) * [Replacements and variables](#replacements-and-variables) + * [The splat operator](#the-splat-operator) * [Configuration Format](#configuration-format) * [Files and locations](#files-and-locations) * [Adapter configurations](#adapter-configurations) * [Debug configurations](#debug-configurations) * [Exception breakpionts](#exception-breakpionts) * [Predefined Variables](#predefined-variables) + * [Remote Debugging Support](#remote-debugging-support) + * [Python (debugpy) Example](#python-debugpy-example) + * [C-family (gdbserver) Example](#c-family-gdbserver-example) + * [Docker Example](#docker-example) + * [Appendix: Configuration file format](#appendix-configuration-file-format) * [Appendix: Editor configuration](#appendix-editor-configuration) - + @@ -330,7 +336,7 @@ the configured response is empty string, the debug adapter default will be used. Referring to the above example, the following tells the debug adapter to use the default value for `caught` exceptoins and to break on `uncaught` exception: -``` +```json { "configurations": { "example-debug-configuration": { @@ -344,6 +350,52 @@ default value for `caught` exceptoins and to break on `uncaught` exception: ... ``` +The keys in the `exception` mapping are what Vimspector includes in the prompt. +For example, when prompted with the following: + +``` +cpp_throw: Break on C++: on throw (Y/N/default: Y)? +``` + +The exception breakpoint "type" is `cpp_throw` and the default is `Y`. + +Similarly: + +``` +cpp_catch: Break on C++: on catch (Y/N/default: N)? +``` + +The exception breakpoint "type" is `cpp_catch` and the default is `N`. + +Use the following to set the values in config and not get asked: + +```json + "configurations": { + "example-debug-configuration": { + "adapter": "example-adapter-name", + "breakpoints": { + "exception": { + "cpp_throw": "Y", + "cpp_catch": "Y" + } + }, +``` + +To just accept the defaults for these exception breakpoint types, don't specify +a value, as in : + +```json + "configurations": { + "example-debug-configuration": { + "adapter": "example-adapter-name", + "breakpoints": { + "exception": { + "cpp_throw": "", + "cpp_catch": "" + } + }, +``` + ## Predefined Variables The following variables are provided: From 9f6caadc40a8449f748c458efe9e46e8b4ca31f3 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 9 Jul 2020 18:57:28 +0100 Subject: [PATCH 7/7] Pre-calculate the gadgetDir, as this is likely used every time --- python3/vimspector/debug_session.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 244a0c4..50b2b00 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -181,12 +181,11 @@ class DebugSession( object ): 'dollar': '$', # HACK. Hote '$$' also works. 'workspaceRoot': self._workspace_root, 'workspaceFolder': self._workspace_root, + 'gadgetDir': install.GetGadgetDir( VIMSPECTOR_HOME, install.GetOS() ), 'file': current_file, } calculus = { - 'gadgetDir': lambda: install.GetGadgetDir( VIMSPECTOR_HOME, - install.GetOS() ), 'relativeFile': lambda: relpath( current_file, self._workspace_root ), 'fileBasename': lambda: os.path.basename( current_file ),