From b6048fc5c68681863fe6a03a4ce5bbf113ace8ef Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 10 Aug 2020 21:42:00 +0100 Subject: [PATCH] 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 )