From b6048fc5c68681863fe6a03a4ce5bbf113ace8ef Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 10 Aug 2020 21:42:00 +0100 Subject: [PATCH 1/6] Refactor launch to split out the various parts --- python3/vimspector/debug_session.py | 212 +++--------------------- python3/vimspector/launch.py | 242 ++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+), 188 deletions(-) create mode 100644 python3/vimspector/launch.py diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index ea72651..ed304e5 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import glob import json import logging import os @@ -32,15 +31,12 @@ from vimspector import ( breakpoints, variables, settings, terminal, - installer ) -from vimspector.vendor.json_minify import minify + installer, + launch ) # We cache this once, and don't allow it to change (FIXME?) VIMSPECTOR_HOME = utils.GetVimspectorBase() -# cache of what the user entered for any option we ask them -USER_CHOICES = {} - class DebugSession( object ): def __init__( self, api_prefix ): @@ -83,21 +79,8 @@ class DebugSession( object ): def GetConfigurations( self, adapters ): current_file = utils.GetBufferFilepath( vim.current.buffer ) filetypes = utils.GetBufferFiletypes( vim.current.buffer ) - configurations = {} + return launch.GetConfigurations( adapters, current_file, filetypes ) - for launch_config_file in PathsToAllConfigFiles( VIMSPECTOR_HOME, - current_file, - filetypes ): - self._logger.debug( f'Reading configurations from: {launch_config_file}' ) - if not launch_config_file or not os.path.exists( launch_config_file ): - continue - - with open( launch_config_file, 'r' ) as f: - database = json.loads( minify( f.read() ) ) - configurations.update( database.get( 'configurations' ) or {} ) - adapters.update( database.get( 'adapters' ) or {} ) - - return launch_config_file, configurations def Start( self, launch_variables = None ): # We mutate launch_variables, so don't mutate the default argument. @@ -112,8 +95,11 @@ class DebugSession( object ): self._launch_config = None current_file = utils.GetBufferFilepath( vim.current.buffer ) - adapters = {} - launch_config_file, configurations = self.GetConfigurations( adapters ) + filetypes = utils.GetBufferFiletypes( vim.current.buffer ) + adapters = launch.GetAdapters( current_file, filetypes ) + launch_config_file, configurations = launch.GetConfigurations( adapters, + current_file, + filetypes ) if not configurations: utils.UserMessage( 'Unable to find any debug configurations. ' @@ -121,152 +107,28 @@ class DebugSession( object ): 'application.' ) return - glob.glob( install.GetGadgetDir( VIMSPECTOR_HOME ) ) - for gadget_config_file in PathsToAllGadgetConfigs( VIMSPECTOR_HOME, - current_file ): - self._logger.debug( f'Reading gadget config: {gadget_config_file}' ) - if not gadget_config_file or not os.path.exists( gadget_config_file ): - continue - - with open( gadget_config_file, 'r' ) as f: - a = json.loads( minify( f.read() ) ).get( 'adapters' ) or {} - adapters.update( a ) - - if 'configuration' in launch_variables: - configuration_name = launch_variables.pop( 'configuration' ) - elif ( len( configurations ) == 1 and - next( iter( configurations.values() ) ).get( "autoselect", True ) ): - configuration_name = next( iter( configurations.keys() ) ) - else: - # Find a single configuration with 'default' True and autoselect not False - defaults = { n: c for n, c in configurations.items() - if c.get( 'default', False ) is True - and c.get( 'autoselect', True ) is not False } - - if len( defaults ) == 1: - configuration_name = next( iter( defaults.keys() ) ) - else: - configuration_name = utils.SelectFromList( - 'Which launch configuration?', - sorted( configurations.keys() ) ) - - if not configuration_name or configuration_name not in configurations: - return - if launch_config_file: self._workspace_root = os.path.dirname( launch_config_file ) else: self._workspace_root = os.path.dirname( current_file ) - configuration = configurations[ configuration_name ] - adapter = configuration.get( 'adapter' ) - if isinstance( adapter, str ): - adapter_dict = adapters.get( adapter ) - - if adapter_dict is None: - suggested_gadgets = installer.FindGadgetForAdapter( adapter ) - if suggested_gadgets: - response = utils.AskForInput( - f"The specified adapter '{adapter}' is not " - "installed. Would you like to install the following gadgets? ", - ' '.join( suggested_gadgets ) ) - if response: - new_launch_variables = dict( launch_variables ) - new_launch_variables[ 'configuration' ] = configuration_name - - installer.RunInstaller( - self._api_prefix, - False, # Don't leave open - *shlex.split( response ), - then = lambda: self.Start( new_launch_variables ) ) - return - elif response is None: - return - - utils.UserMessage( f"The specified adapter '{adapter}' is not " - "available. Did you forget to run " - "'install_gadget.py'?", - persist = True, - error = True ) - return - - adapter = adapter_dict - - # Additional vars as defined by VSCode: - # - # ${workspaceFolder} - the path of the folder opened in VS Code - # ${workspaceFolderBasename} - the name of the folder opened in VS Code - # without any slashes (/) - # ${file} - the current opened file - # ${relativeFile} - the current opened file relative to workspaceFolder - # ${fileBasename} - the current opened file's basename - # ${fileBasenameNoExtension} - the current opened file's basename with no - # file extension - # ${fileDirname} - the current opened file's dirname - # ${fileExtname} - the current opened file's extension - # ${cwd} - the task runner's current working directory on startup - # ${lineNumber} - the current selected line number in the active file - # ${selectedText} - the current selected text in the active file - # ${execPath} - the path to the running VS Code executable - - def relpath( p, relative_to ): - if not p: - return '' - return os.path.relpath( p, relative_to ) - - def splitext( p ): - if not p: - return [ '', '' ] - return os.path.splitext( p ) - - variables = { - 'dollar': '$', # HACK. Hote '$$' also works. - 'workspaceRoot': self._workspace_root, - 'workspaceFolder': self._workspace_root, - 'gadgetDir': install.GetGadgetDir( VIMSPECTOR_HOME ), - 'file': current_file, - } - - calculus = { - 'relativeFile': lambda: relpath( current_file, - self._workspace_root ), - 'fileBasename': lambda: os.path.basename( current_file ), - 'fileBasenameNoExtension': - 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, - '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 ) - variables.update( launch_variables ) - + configuration_name, configuration = launch.SelectConfiguration( + launch_variables, + configurations ) + adapter = launch.SelectAdapter( self._api_prefix, + configuration_name, + configuration, + adapters, + launch_variables, + self ) + if not adapter: + return try: - variables.update( - utils.ParseVariables( adapter.get( 'variables', {} ), - variables, - calculus, - USER_CHOICES ) ) - variables.update( - utils.ParseVariables( configuration.get( 'variables', {} ), - variables, - calculus, - USER_CHOICES ) ) - - - utils.ExpandReferencesInDict( configuration, - variables, - calculus, - USER_CHOICES ) - utils.ExpandReferencesInDict( adapter, - variables, - calculus, - USER_CHOICES ) + launch.ResolveConfiguration( adapter, + configuration, + launch_variables, + self._workspace_root, + current_file ) except KeyboardInterrupt: self._Reset() return @@ -1277,29 +1139,3 @@ class DebugSession( object ): def AddFunctionBreakpoint( self, function, options ): return self._breakpoints.AddFunctionBreakpoint( function, options ) - - -def PathsToAllGadgetConfigs( vimspector_base, current_file ): - yield install.GetGadgetConfigFile( vimspector_base ) - for p in sorted( glob.glob( - os.path.join( install.GetGadgetConfigDir( vimspector_base ), - '*.json' ) ) ): - yield p - - yield utils.PathToConfigFile( '.gadgets.json', - os.path.dirname( current_file ) ) - - -def PathsToAllConfigFiles( vimspector_base, current_file, filetypes ): - for ft in filetypes + [ '_all' ]: - for p in sorted( glob.glob( - os.path.join( install.GetConfigDirForFiletype( vimspector_base, ft ), - '*.json' ) ) ): - yield p - - for ft in filetypes: - yield utils.PathToConfigFile( f'.vimspector.{ft}.json', - os.path.dirname( current_file ) ) - - yield utils.PathToConfigFile( '.vimspector.json', - os.path.dirname( current_file ) ) diff --git a/python3/vimspector/launch.py b/python3/vimspector/launch.py new file mode 100644 index 0000000..c5d0616 --- /dev/null +++ b/python3/vimspector/launch.py @@ -0,0 +1,242 @@ +# vimspector - A multi-language debugging system for Vim +# Copyright 2020 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 os +import logging +import json +import glob +import shlex + +from vimspector import install, installer, utils +from vimspector.vendor.json_minify import minify + +_logger = logging.getLogger( __name__ ) +utils.SetUpLogging( _logger ) + +# We cache this once, and don't allow it to change (FIXME?) +VIMSPECTOR_HOME = utils.GetVimspectorBase() + +# cache of what the user entered for any option we ask them +USER_CHOICES = {} + + + +def GetAdapters( current_file, filetypes ): + adapters = {} + for gadget_config_file in PathsToAllGadgetConfigs( VIMSPECTOR_HOME, + current_file ): + _logger.debug( f'Reading gadget config: {gadget_config_file}' ) + if not gadget_config_file or not os.path.exists( gadget_config_file ): + continue + + with open( gadget_config_file, 'r' ) as f: + a = json.loads( minify( f.read() ) ).get( 'adapters' ) or {} + adapters.update( a ) + + return adapters + + +def PathsToAllGadgetConfigs( vimspector_base, current_file ): + yield install.GetGadgetConfigFile( vimspector_base ) + for p in sorted( glob.glob( + os.path.join( install.GetGadgetConfigDir( vimspector_base ), + '*.json' ) ) ): + yield p + + yield utils.PathToConfigFile( '.gadgets.json', + os.path.dirname( current_file ) ) + + +def GetConfigurations( adapters, current_file, filetypes ): + configurations = {} + for launch_config_file in PathsToAllConfigFiles( VIMSPECTOR_HOME, + current_file, + filetypes ): + _logger.debug( f'Reading configurations from: {launch_config_file}' ) + if not launch_config_file or not os.path.exists( launch_config_file ): + continue + + with open( launch_config_file, 'r' ) as f: + database = json.loads( minify( f.read() ) ) + configurations.update( database.get( 'configurations' ) or {} ) + adapters.update( database.get( 'adapters' ) or {} ) + + # We return the last config file inspected which is the most specific one + # (i.e. the project-local one) + return launch_config_file, configurations + + +def PathsToAllConfigFiles( vimspector_base, current_file, filetypes ): + for ft in filetypes + [ '_all' ]: + for p in sorted( glob.glob( + os.path.join( install.GetConfigDirForFiletype( vimspector_base, ft ), + '*.json' ) ) ): + yield p + + for ft in filetypes: + yield utils.PathToConfigFile( f'.vimspector.{ft}.json', + os.path.dirname( current_file ) ) + + yield utils.PathToConfigFile( '.vimspector.json', + os.path.dirname( current_file ) ) + + +def SelectConfiguration( launch_variables, configurations ): + if 'configuration' in launch_variables: + configuration_name = launch_variables.pop( 'configuration' ) + elif ( len( configurations ) == 1 and + next( iter( configurations.values() ) ).get( "autoselect", True ) ): + configuration_name = next( iter( configurations.keys() ) ) + else: + # Find a single configuration with 'default' True and autoselect not False + defaults = { n: c for n, c in configurations.items() + if c.get( 'default', False ) is True + and c.get( 'autoselect', True ) is not False } + + if len( defaults ) == 1: + configuration_name = next( iter( defaults.keys() ) ) + else: + configuration_name = utils.SelectFromList( + 'Which launch configuration?', + sorted( configurations.keys() ) ) + + if not configuration_name or configuration_name not in configurations: + return None, None + + configuration = configurations[ configuration_name ] + + return configuration_name, configuration + + +def SelectAdapter( api_prefix, + configuration_name, + configuration, + adapters, + launch_variables, + debug_session ): + adapter = configuration.get( 'adapter' ) + + if isinstance( adapter, str ): + adapter_dict = adapters.get( adapter ) + + if adapter_dict is None: + suggested_gadgets = installer.FindGadgetForAdapter( adapter ) + if suggested_gadgets: + response = utils.AskForInput( + f"The specified adapter '{adapter}' is not " + "installed. Would you like to install the following gadgets? ", + ' '.join( suggested_gadgets ) ) + if response: + new_launch_variables = dict( launch_variables ) + new_launch_variables[ 'configuration' ] = configuration_name + + installer.RunInstaller( + api_prefix, + False, # Don't leave open + *shlex.split( response ), + then = debug_session.Start( new_launch_variables ) ) + return + elif response is None: + return None + + utils.UserMessage( f"The specified adapter '{adapter}' is not " + "available. Did you forget to run " + "'install_gadget.py'?", + persist = True, + error = True ) + return None + + adapter = adapter_dict + + return adapter + + +def ResolveConfiguration( adapter, + configuration, + launch_variables, + workspace_root, + current_file ): + # Additional vars as defined by VSCode: + # + # ${workspaceFolder} - the path of the folder opened in VS Code + # ${workspaceFolderBasename} - the name of the folder opened in VS Code + # without any slashes (/) + # ${file} - the current opened file + # ${relativeFile} - the current opened file relative to workspaceFolder + # ${fileBasename} - the current opened file's basename + # ${fileBasenameNoExtension} - the current opened file's basename with no + # file extension + # ${fileDirname} - the current opened file's dirname + # ${fileExtname} - the current opened file's extension + # ${cwd} - the task runner's current working directory on startup + # ${lineNumber} - the current selected line number in the active file + # ${selectedText} - the current selected text in the active file + # ${execPath} - the path to the running VS Code executable + def relpath( p, relative_to ): + if not p: + return '' + return os.path.relpath( p, relative_to ) + + def splitext( p ): + if not p: + return [ '', '' ] + return os.path.splitext( p ) + + variables = { + 'dollar': '$', # HACK. Hote '$$' also works. + 'workspaceRoot': workspace_root, + 'workspaceFolder': workspace_root, + 'gadgetDir': install.GetGadgetDir( VIMSPECTOR_HOME ), + 'file': current_file, + } + + calculus = { + 'relativeFile': lambda: relpath( current_file, workspace_root ), + 'fileBasename': lambda: os.path.basename( current_file ), + 'fileBasenameNoExtension': + 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, + '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 ) + variables.update( launch_variables ) + + variables.update( + utils.ParseVariables( adapter.get( 'variables', {} ), + variables, + calculus, + USER_CHOICES ) ) + variables.update( + utils.ParseVariables( configuration.get( 'variables', {} ), + variables, + calculus, + USER_CHOICES ) ) + + + utils.ExpandReferencesInDict( configuration, + variables, + calculus, + USER_CHOICES ) + utils.ExpandReferencesInDict( adapter, + variables, + calculus, + USER_CHOICES ) From acd0f31573569fb1683811764fba3b5bae696cca Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 23 Aug 2020 14:49:36 +0100 Subject: [PATCH 2/6] WIP: Guided config for c++ This adds some templates to the gadget config, organised by "category" (human-readable) and presented in a menu. The configuration thus created is just run normally through the variable replacements. Also: - Fix default value replacements which come from the calculus - Add default-value _lists_. This uses select-from-list UI. Useful for enumerated values. Allow specifying a default. --- autoload/vimspector.vim | 2 +- python3/vimspector/debug_session.py | 35 ++++++++------ python3/vimspector/gadgets.py | 71 +++++++++++++++++++++++++++++ python3/vimspector/launch.py | 58 ++++++++++++++++++++--- python3/vimspector/utils.py | 54 +++++++++++++++++----- 5 files changed, 186 insertions(+), 34 deletions(-) diff --git a/autoload/vimspector.vim b/autoload/vimspector.vim index 35ecd03..f19bd78 100644 --- a/autoload/vimspector.vim +++ b/autoload/vimspector.vim @@ -296,7 +296,7 @@ function! vimspector#GetConfigurations() abort return endif let configurations = py3eval( - \ 'list( _vimspector_session.GetConfigurations( {} )[ 1 ].keys() )' + \ 'list( _vimspector_session.GetConfigurations().keys() )' \ . ' if _vimspector_session else []' ) return configurations endfunction diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index ed304e5..e664121 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -76,10 +76,10 @@ class DebugSession( object ): self._server_capabilities = {} self.ClearTemporaryBreakpoints() - def GetConfigurations( self, adapters ): + def GetConfigurations( self ): current_file = utils.GetBufferFilepath( vim.current.buffer ) filetypes = utils.GetBufferFiletypes( vim.current.buffer ) - return launch.GetConfigurations( adapters, current_file, filetypes ) + return launch.GetConfigurations( {}, current_file, filetypes )[ 1 ] def Start( self, launch_variables = None ): @@ -96,33 +96,40 @@ class DebugSession( object ): current_file = utils.GetBufferFilepath( vim.current.buffer ) filetypes = utils.GetBufferFiletypes( vim.current.buffer ) - adapters = launch.GetAdapters( current_file, filetypes ) + adapters = launch.GetAdapters( current_file ) launch_config_file, configurations = launch.GetConfigurations( adapters, current_file, filetypes ) - if not configurations: - utils.UserMessage( 'Unable to find any debug configurations. ' - 'You need to tell vimspector how to launch your ' - 'application.' ) - return - if launch_config_file: self._workspace_root = os.path.dirname( launch_config_file ) else: self._workspace_root = os.path.dirname( current_file ) - configuration_name, configuration = launch.SelectConfiguration( - launch_variables, - configurations ) + + if not configurations: + configuration_name, configuration = launch.SuggestConfiguration( + filetypes ) + else: + configuration_name, configuration = launch.SelectConfiguration( + launch_variables, + configurations ) + + if not configuration: + utils.UserMessage( 'Unable to find any debug configurations. ' + 'You need to tell vimspector how to launch your ' + 'application.' ) + return + adapter = launch.SelectAdapter( self._api_prefix, + self, configuration_name, configuration, adapters, - launch_variables, - self ) + launch_variables ) if not adapter: return + try: launch.ResolveConfiguration( adapter, configuration, diff --git a/python3/vimspector/gadgets.py b/python3/vimspector/gadgets.py index 0271069..a98fb8b 100644 --- a/python3/vimspector/gadgets.py +++ b/python3/vimspector/gadgets.py @@ -49,6 +49,40 @@ GADGETS = { } }, }, + 'templates': [ + { + 'description': 'gdb/lldb local debugging with vscode-cpptools', + 'filetypes': ( 'c', 'cpp', 'objc', 'rust' ), + 'configurations': [ + { + 'description': 'Launch local process', + 'launch_configuration': { + 'adapter': 'vscode-cpptools', + 'configuration': { + 'request': 'launch', + 'program': '${Binary:${fileBasenameNoExtension\\}}', + 'args': [ '*${CommandLineArguments}' ], + 'cwd': '${WorkingDir:${fileDirname\\}}', + 'externalConsole#json': '${UseExternalConsole:true\nfalse}', + 'stopAtEntry#json': '${StopAtEntry:true\nfalse}', + 'MIMode': '${Debugger:gdb\nlldb}', + } + } + }, + { + 'description': 'Attach to local process by PID', + 'launch_configuration': { + 'adapter': 'vscode-cpptools', + 'configuration': { + 'request': 'attach', + 'program': '${Binary:${fileBasenameNoExtension\\}}', + 'MIMode': '${Debugger:gdb\nlldb}', + } + } + } + ] + } + ] }, 'linux': { 'file_name': 'cpptools-linux.vsix', @@ -452,6 +486,42 @@ GADGETS = { }, 'all': { 'version': 'v1.5.3', + 'templates': [ + { + 'description': 'LLDB local debugging with CodeLLDB', + 'filetypes': ( 'c', 'cpp', 'objc', 'rust' ), + 'configurations': [ + { + 'description': 'Launch local process', + 'launch_configuration': { + 'adapter': 'CodeLLDB', + 'configuration': { + 'request': 'launch', + 'program': '${Binary:${fileBasenameNoExtension\\}}', + 'args': [ '*${CommandLineArguments}' ], + 'cwd': '${WorkingDir:${fileDirname\\}}', + 'terminal': '${Console:none\nintegrated\nexternal}', + 'stopOnEntry#json': '${StopOnEntry:true\nfalse}', + 'expressions': '${ExpressionType:native\nsimple\npython}' + } + } + }, + { + 'description': 'Attach to local process by PID', + 'launch_configuration': { + 'adapter': 'CodeLLDB', + 'configuration': { + 'request': 'attach', + 'program': '${Binary:${fileBasenameNoExtension\\}}', + 'pid#json': "${pid}", + 'stopOnEntry#json': '${StopOnEntry:true\nfalse}', + 'expressions': '${ExpressionType:native\nsimple\npython}' + } + } + } + ] + } + ] }, 'macos': { 'file_name': 'codelldb-x86_64-darwin.vsix', @@ -486,6 +556,7 @@ GADGETS = { 'name': 'CodeLLDB', 'type': 'CodeLLDB', "command": [ + # FIXME: This probably doesn't work on windows. "${gadgetDir}/CodeLLDB/adapter/codelldb", "--port", "${unusedLocalPort}" ], diff --git a/python3/vimspector/launch.py b/python3/vimspector/launch.py index c5d0616..2903a7f 100644 --- a/python3/vimspector/launch.py +++ b/python3/vimspector/launch.py @@ -19,7 +19,7 @@ import json import glob import shlex -from vimspector import install, installer, utils +from vimspector import install, installer, utils, gadgets from vimspector.vendor.json_minify import minify _logger = logging.getLogger( __name__ ) @@ -33,7 +33,7 @@ USER_CHOICES = {} -def GetAdapters( current_file, filetypes ): +def GetAdapters( current_file ): adapters = {} for gadget_config_file in PathsToAllGadgetConfigs( VIMSPECTOR_HOME, current_file ): @@ -120,13 +120,57 @@ def SelectConfiguration( launch_variables, configurations ): return configuration_name, configuration +def SuggestConfiguration( filetypes ): + nothing = None, None + templates = [] + filetypes = set( filetypes ) + + for gadget_name, gadget in gadgets.GADGETS.items(): + spec = {} + spec.update( gadget.get( 'all', {} ) ) + spec.update( gadget.get( install.GetOS(), {} ) ) + + for template in spec.get( 'templates', [] ): + if filetypes.intersection( template.get( 'filetypes', set() ) ): + templates.append( template ) + + if not templates: + return nothing + + template_idx = utils.SelectFromList( + 'No debug configurations were found for this project, ' + 'Would you like to use one of the following templates?', + [ t[ 'description' ] for t in templates ], + ret = 'index' ) + + if template_idx is None: + return nothing + + template = templates[ template_idx ] + + config_index = utils.SelectFromList( + 'Which configuration?', + [ c[ 'description' ] for c in template[ 'configurations' ] ], + ret = 'index' ) + + if config_index is None: + return nothing + + configuration = template[ 'configurations' ][ config_index ] + configuration_name = utils.AskForInput( 'Give the config a name: ', + configuration[ 'description' ] ) + + + return configuration_name, configuration[ 'launch_configuration' ] + + def SelectAdapter( api_prefix, + debug_session, configuration_name, configuration, adapters, - launch_variables, - debug_session ): - adapter = configuration.get( 'adapter' ) + launch_variables ): + adapter = configuration.get( 'adapter' ) if isinstance( adapter, str ): adapter_dict = adapters.get( adapter ) @@ -136,7 +180,7 @@ def SelectAdapter( api_prefix, if suggested_gadgets: response = utils.AskForInput( f"The specified adapter '{adapter}' is not " - "installed. Would you like to install the following gadgets? ", + "installed. Would you like to install the following gadgets? ", ' '.join( suggested_gadgets ) ) if response: new_launch_variables = dict( launch_variables ) @@ -147,7 +191,7 @@ def SelectAdapter( api_prefix, False, # Don't leave open *shlex.split( response ), then = debug_session.Start( new_launch_variables ) ) - return + return None elif response is None: return None diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index 5ab9872..581411a 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -333,25 +333,41 @@ def UserMessage( msg, persist=False, error=False ): @contextlib.contextmanager -def InputSave(): +def AskingForUserInput(): vim.eval( 'inputsave()' ) try: yield finally: vim.eval( 'inputrestore()' ) + # Clear the command line so that subsequent + vim.command( 'redraw' ) -def SelectFromList( prompt, options ): - with InputSave(): +def SelectFromList( prompt, + options, + ret = 'text', + default_index = None ): + with AskingForUserInput(): display_options = [ prompt ] - display_options.extend( [ '{0}: {1}'.format( i + 1, v ) - for i, v in enumerate( options ) ] ) + display_options.extend( [ + '{0}{1}: {2}'.format( '*' if i == default_index else ' ', + i + 1, + v ) + for i, v in enumerate( options ) + ] ) try: selection = int( vim.eval( 'inputlist( ' + json.dumps( display_options ) + ' )' ) ) - 1 if selection < 0 or selection >= len( options ): - return None - return options[ selection ] + if default_index is not None: + selection = default_index + else: + return None + + if ret == 'text': + return options[ selection ] + else: + return selection except ( KeyboardInterrupt, vim.error ): return None @@ -362,7 +378,7 @@ def AskForInput( prompt, default_value = None ): else: default_option = ", '{}'".format( Escape( default_value ) ) - with InputSave(): + with AskingForUserInput(): try: return vim.eval( "input( '{}' {} )".format( Escape( prompt ), default_option ) ) @@ -531,11 +547,25 @@ def ExpandReferencesInString( orig_s, if default_value is None and e.default_value is not None: try: default_value = _Substitute( e.default_value, mapping ) - except MissingSubstitution: - default_value = e.default_value + except MissingSubstitution as f: + if f.name in calculus: + default_value = calculus[ f.name ]() + else: + default_value = e.default_value - mapping[ key ] = AskForInput( 'Enter value for {}: '.format( key ), - default_value ) + value_list = None + if default_value: + default_value_list = default_value.split( '\n' ) + if len( default_value_list ) > 1: + value_list = default_value_list + + if value_list: + mapping[ key ] = SelectFromList( f'Select value for { key }: ', + default_value_list, + default_index = 0 ) + else: + mapping[ key ] = AskForInput( 'Enter value for {}: '.format( key ), + default_value ) if mapping[ key ] is None: raise KeyboardInterrupt From 7a02f6139f73653a86f56ce466b8f22b0cde00e6 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 31 Dec 2020 20:33:42 +0000 Subject: [PATCH 3/6] Same generated configurations on reset --- python3/vimspector/debug_session.py | 5 +++- python3/vimspector/launch.py | 41 ++++++++++++++++++++++++++++- python3/vimspector/utils.py | 11 ++++++-- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index e664121..c2deb08 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -31,7 +31,6 @@ from vimspector import ( breakpoints, variables, settings, terminal, - installer, launch ) # We cache this once, and don't allow it to change (FIXME?) @@ -109,6 +108,7 @@ class DebugSession( object ): if not configurations: configuration_name, configuration = launch.SuggestConfiguration( + current_file, filetypes ) else: configuration_name, configuration = launch.SelectConfiguration( @@ -267,6 +267,9 @@ class DebugSession( object ): def _Reset( self ): self._logger.info( "Debugging complete." ) + + launch.SaveConfiguration( self._configuration ) + if self._uiTab: self._logger.debug( "Clearing down UI" ) diff --git a/python3/vimspector/launch.py b/python3/vimspector/launch.py index 2903a7f..c4f0af4 100644 --- a/python3/vimspector/launch.py +++ b/python3/vimspector/launch.py @@ -120,7 +120,7 @@ def SelectConfiguration( launch_variables, configurations ): return configuration_name, configuration -def SuggestConfiguration( filetypes ): +def SuggestConfiguration( current_file, filetypes ): nothing = None, None templates = [] filetypes = set( filetypes ) @@ -160,10 +160,49 @@ def SuggestConfiguration( filetypes ): configuration_name = utils.AskForInput( 'Give the config a name: ', configuration[ 'description' ] ) + configuration[ 'launch_configuration' ][ 'generated' ] = { + 'name': configuration_name, + 'path': os.path.join( os.path.dirname( current_file ), '.vimspector.json' ), + } return configuration_name, configuration[ 'launch_configuration' ] +def SaveConfiguration( configuration ): + gen = configuration.pop( 'generated', None ) + if not gen: + return + + config_path = utils.AskForInput( + f'Would you like to save the configuration named "{ gen[ "name" ] }"' + '? Enter the path to save to (ctrl-c to cancel): ', + gen[ 'path' ] ) + + if not config_path: + return + + os.makedirs( os.path.dirname( config_path ), exist_ok = True ) + current_contents = {} + + if os.path.exists( config_path ): + if utils.Confirm( 'File exists, overwrite? (NOTE: comments and ' + 'formatting in the existing file will be LOST!!)', + '&Yes\n&No' ) == 1: + with open( config_path, 'r' ) as f: + current_contents = json.loads( minify( f.read() ) ) + + # TODO: how much of configuration is mangled at this point ? + # TODO: how about the defaulted arguments? All the refs are replaced at this + # point? + current_contents.setdefault( 'configurations', {} ) + current_contents[ 'configurations' ][ gen[ 'name' ] ] = configuration + + with open( config_path, 'w' ) as f: + json.dump( current_contents, f, indent=2 ) + + utils.UserMessage( f'Wrote { config_path }.', persist = True ) + + def SelectAdapter( api_prefix, debug_session, configuration_name, diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index 581411a..ab5d2ca 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -386,6 +386,14 @@ def AskForInput( prompt, default_value = None ): return None +def Confirm( msg, *args ): + with AskingForUserInput(): + try: + return int( Call( 'confirm', msg, *args ) ) + except ( KeyboardInterrupt, vim.error ): + return 0 + + def AppendToBuffer( buf, line_or_lines, modified=False ): line = 1 try: @@ -600,8 +608,6 @@ def CoerceType( mapping: typing.Dict[ str, typing.Any ], key: str ): mapping[ key ] = DICT_TYPES[ new_type ]( value ) -# 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, calculus, user_choices ): for k in list( obj.keys() ): obj[ k ] = ExpandReferencesInObject( obj[ k ], @@ -817,6 +823,7 @@ def WindowID( window, tab=None ): return int( Call( 'win_getid', window.number, tab.number ) ) +@memoize def UseWinBar(): # Buggy neovim doesn't render correctly when the WinBar is defined: # https://github.com/neovim/neovim/issues/12689 From 6126ac172409cb91fadb5081d01a979ed17ea958 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 31 Dec 2020 20:37:43 +0000 Subject: [PATCH 4/6] none terminal doesn't work in CodeLLDB --- python3/vimspector/gadgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python3/vimspector/gadgets.py b/python3/vimspector/gadgets.py index a98fb8b..a2a603d 100644 --- a/python3/vimspector/gadgets.py +++ b/python3/vimspector/gadgets.py @@ -500,7 +500,7 @@ GADGETS = { 'program': '${Binary:${fileBasenameNoExtension\\}}', 'args': [ '*${CommandLineArguments}' ], 'cwd': '${WorkingDir:${fileDirname\\}}', - 'terminal': '${Console:none\nintegrated\nexternal}', + 'terminal': '${Console:integrated\nexternal}', 'stopOnEntry#json': '${StopOnEntry:true\nfalse}', 'expressions': '${ExpressionType:native\nsimple\npython}' } From 2fa6142c8f8acc80b088eec6ecb08677e9b01aff Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 31 Dec 2020 20:55:35 +0000 Subject: [PATCH 5/6] FixUp: save config --- python3/vimspector/launch.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/python3/vimspector/launch.py b/python3/vimspector/launch.py index c4f0af4..6eada0d 100644 --- a/python3/vimspector/launch.py +++ b/python3/vimspector/launch.py @@ -173,10 +173,13 @@ def SaveConfiguration( configuration ): if not gen: return - config_path = utils.AskForInput( - f'Would you like to save the configuration named "{ gen[ "name" ] }"' - '? Enter the path to save to (ctrl-c to cancel): ', - gen[ 'path' ] ) + if utils.Confirm( + f'Would you like to save the configuration named "{ gen[ "name" ] }"', + '&Yes\n&No' ) != 1: + return + + config_path = utils.AskForInput( 'Enter the path to save to: ', + gen[ 'path' ] ) if not config_path: return @@ -185,7 +188,7 @@ def SaveConfiguration( configuration ): current_contents = {} if os.path.exists( config_path ): - if utils.Confirm( 'File exists, overwrite? (NOTE: comments and ' + if utils.Confirm( 'File exists, overwrite?\n(NOTE: comments and ' 'formatting in the existing file will be LOST!!)', '&Yes\n&No' ) == 1: with open( config_path, 'r' ) as f: From 7e55563c8fef720e90cea3c5c2b8fda39ff2cb39 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 31 Dec 2020 21:43:24 +0000 Subject: [PATCH 6/6] FixUp: make sure that we copy templates rather than end up updating them --- python3/vimspector/launch.py | 21 ++++++++++++++------- python3/vimspector/utils.py | 5 +---- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/python3/vimspector/launch.py b/python3/vimspector/launch.py index 6eada0d..5bf4108 100644 --- a/python3/vimspector/launch.py +++ b/python3/vimspector/launch.py @@ -18,6 +18,7 @@ import logging import json import glob import shlex +import copy from vimspector import install, installer, utils, gadgets from vimspector.vendor.json_minify import minify @@ -132,7 +133,10 @@ def SuggestConfiguration( current_file, filetypes ): for template in spec.get( 'templates', [] ): if filetypes.intersection( template.get( 'filetypes', set() ) ): - templates.append( template ) + # We _must_ copy the template as we end up taking bits out of it to + # assign to the resulting config. Python... references... mutabiliy.. + # fun. + templates.append( copy.deepcopy( template ) ) if not templates: return nothing @@ -188,11 +192,14 @@ def SaveConfiguration( configuration ): current_contents = {} if os.path.exists( config_path ): - if utils.Confirm( 'File exists, overwrite?\n(NOTE: comments and ' - 'formatting in the existing file will be LOST!!)', - '&Yes\n&No' ) == 1: - with open( config_path, 'r' ) as f: - current_contents = json.loads( minify( f.read() ) ) + if utils.Confirm( 'File exists, merge with this configuration?\n' + '(NOTE: comments and formatting in the existing file ' + 'will be LOST!!)', + '&Yes\n&No' ) != 1: + return + + with open( config_path, 'r' ) as f: + current_contents = json.loads( minify( f.read() ) ) # TODO: how much of configuration is mangled at this point ? # TODO: how about the defaulted arguments? All the refs are replaced at this @@ -203,7 +210,7 @@ def SaveConfiguration( configuration ): with open( config_path, 'w' ) as f: json.dump( current_contents, f, indent=2 ) - utils.UserMessage( f'Wrote { config_path }.', persist = True ) + utils.UserMessage( f'Wrote { config_path }', persist = True ) def SelectAdapter( api_prefix, diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index ab5d2ca..0aba76b 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -536,10 +536,7 @@ def ExpandReferencesInString( orig_s, # Parse any variables passed in in mapping, and ask for any that weren't, # storing the result in mapping - bug_catcher = 0 - while bug_catcher < 100: - ++bug_catcher - + while True: try: s = _Substitute( s, mapping ) break