From c3c25030cdb5f9ff48ca5a65c3f08ae759c14c02 Mon Sep 17 00:00:00 2001 From: Jordan Walsh Date: Fri, 22 Dec 2023 14:03:47 -0500 Subject: [PATCH 01/43] Added workdir support for docker exec --- docs/schema/vimspector.schema.json | 4 + python3/vimspector/debug_session.py | 4201 ++++++++++++++------------- 2 files changed, 2113 insertions(+), 2092 deletions(-) diff --git a/docs/schema/vimspector.schema.json b/docs/schema/vimspector.schema.json index 7b319637a..23f006482 100644 --- a/docs/schema/vimspector.schema.json +++ b/docs/schema/vimspector.schema.json @@ -186,6 +186,10 @@ "type": "array", "items": { "type": "string" }, "description": "A single command to execute for remote-launch. Like runCommands but for a single command." + }, + "workdir": { + "type": "string", + "description": "For containers. The value passed to docker exec for the working directory." } } } diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 0ede8984a..d9a19f722 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -47,2228 +47,2245 @@ class DebugSession( object ): - child_sessions: typing.List[ "DebugSession" ] - - def CurrentSession(): - def decorator( fct ): - @functools.wraps( fct ) - def wrapper( self: "DebugSession", *args, **kwargs ): - active_session = self - if self._stackTraceView: - active_session = self._stackTraceView.GetCurrentSession() - if active_session is not None: - return fct( active_session, *args, **kwargs ) - return fct( self, *args, **kwargs ) - return wrapper - return decorator - - def ParentOnly( otherwise=None ): - def decorator( fct ): - @functools.wraps( fct ) - def wrapper( self: "DebugSession", *args, **kwargs ): - if self.parent_session: - return otherwise - return fct( self, *args, **kwargs ) - return wrapper - return decorator - - def IfConnected( otherwise=None ): - def decorator( fct ): - """Decorator, call fct if self._connected else echo warning""" - @functools.wraps( fct ) - def wrapper( self: "DebugSession", *args, **kwargs ): - if not self._connection: - utils.UserMessage( - 'Vimspector not connected, start a debug session first', - persist=False, - error=True ) - return otherwise - return fct( self, *args, **kwargs ) - return wrapper - return decorator - - def RequiresUI( otherwise=None ): - """Decorator, call fct if self._connected else echo warning""" - def decorator( fct ): - @functools.wraps( fct ) - def wrapper( self, *args, **kwargs ): - if not self.HasUI(): - utils.UserMessage( - 'Vimspector is not active', - persist=False, - error=True ) - return otherwise - return fct( self, *args, **kwargs ) - return wrapper - return decorator - - - def __init__( self, - session_id, - session_manager, - api_prefix, - session_name = None, - parent_session: "DebugSession" = None ): - self.session_id = session_id - self.manager = session_manager - self.name = session_name - self.parent_session = parent_session - self.child_sessions = [] - - if parent_session: - parent_session.child_sessions.append( self ) - - self._logger = logging.getLogger( __name__ + '.' + str( session_id ) ) - utils.SetUpLogging( self._logger, session_id ) - - self._api_prefix = api_prefix - - self._render_emitter = utils.EventEmitter() - - self._logger.info( "**** INITIALISING NEW VIMSPECTOR SESSION FOR ID " - f"{session_id } ****" ) - self._logger.info( "API is: {}".format( api_prefix ) ) - self._logger.info( 'VIMSPECTOR_HOME = %s', VIMSPECTOR_HOME ) - self._logger.info( 'gadgetDir = %s', - install.GetGadgetDir( VIMSPECTOR_HOME ) ) - - self._uiTab = None - - self._logView: output.OutputView = None - self._stackTraceView: stack_trace.StackTraceView = None - self._variablesView: variables.VariablesView = None - self._outputView: output.DAPOutputView = None - self._codeView: code.CodeView = None - self._disassemblyView: disassembly.DisassemblyView = None - - if parent_session: - self._breakpoints = parent_session._breakpoints - else: - self._breakpoints = breakpoints.ProjectBreakpoints( - session_id, - self._render_emitter, - self._IsPCPresentAt, - self._disassemblyView ) - utils.SetSessionWindows( {} ) + child_sessions: typing.List[ "DebugSession" ] + + def CurrentSession(): + def decorator( fct ): + @functools.wraps( fct ) + def wrapper( self: "DebugSession", *args, **kwargs ): + active_session = self + if self._stackTraceView: + active_session = self._stackTraceView.GetCurrentSession() + if active_session is not None: + return fct( active_session, *args, **kwargs ) + return fct( self, *args, **kwargs ) + return wrapper + return decorator + + def ParentOnly( otherwise=None ): + def decorator( fct ): + @functools.wraps( fct ) + def wrapper( self: "DebugSession", *args, **kwargs ): + if self.parent_session: + return otherwise + return fct( self, *args, **kwargs ) + return wrapper + return decorator + + def IfConnected( otherwise=None ): + def decorator( fct ): + """Decorator, call fct if self._connected else echo warning""" + @functools.wraps( fct ) + def wrapper( self: "DebugSession", *args, **kwargs ): + if not self._connection: + utils.UserMessage( + 'Vimspector not connected, start a debug session first', + persist=False, + error=True ) + return otherwise + return fct( self, *args, **kwargs ) + return wrapper + return decorator + + def RequiresUI( otherwise=None ): + """Decorator, call fct if self._connected else echo warning""" + def decorator( fct ): + @functools.wraps( fct ) + def wrapper( self, *args, **kwargs ): + if not self.HasUI(): + utils.UserMessage( + 'Vimspector is not active', + persist=False, + error=True ) + return otherwise + return fct( self, *args, **kwargs ) + return wrapper + return decorator + + + def __init__( self, + session_id, + session_manager, + api_prefix, + session_name = None, + parent_session: "DebugSession" = None ): + self.session_id = session_id + self.manager = session_manager + self.name = session_name + self.parent_session = parent_session + self.child_sessions = [] + + if parent_session: + parent_session.child_sessions.append( self ) + + self._logger = logging.getLogger( __name__ + '.' + str( session_id ) ) + utils.SetUpLogging( self._logger, session_id ) + + self._api_prefix = api_prefix + + self._render_emitter = utils.EventEmitter() + + self._logger.info( "**** INITIALISING NEW VIMSPECTOR SESSION FOR ID " + f"{session_id } ****" ) + self._logger.info( "API is: {}".format( api_prefix ) ) + self._logger.info( 'VIMSPECTOR_HOME = %s', VIMSPECTOR_HOME ) + self._logger.info( 'gadgetDir = %s', + install.GetGadgetDir( VIMSPECTOR_HOME ) ) + + self._uiTab = None + + self._logView: output.OutputView = None + self._stackTraceView: stack_trace.StackTraceView = None + self._variablesView: variables.VariablesView = None + self._outputView: output.DAPOutputView = None + self._codeView: code.CodeView = None + self._disassemblyView: disassembly.DisassemblyView = None + + if parent_session: + self._breakpoints = parent_session._breakpoints + else: + self._breakpoints = breakpoints.ProjectBreakpoints( + session_id, + self._render_emitter, + self._IsPCPresentAt, + self._disassemblyView ) + utils.SetSessionWindows( {} ) - self._saved_variables_data = None + self._saved_variables_data = None - self._splash_screen = None - self._remote_term = None - self._adapter_term = None + self._splash_screen = None + self._remote_term = None + self._adapter_term = None - self._run_on_server_exit = None + self._run_on_server_exit = None - self._configuration = None - self._adapter = None - self._launch_config = None + self._configuration = None + self._adapter = None + self._launch_config = None - self._ResetServerState() + self._ResetServerState() - def _ResetServerState( self ): - self._connection = None - self._init_complete = False - self._launch_complete = False - self._on_init_complete_handlers = [] - self._server_capabilities = {} - self._breakpoints.ClearTemporaryBreakpoints() + def _ResetServerState( self ): + self._connection = None + self._init_complete = False + self._launch_complete = False + self._on_init_complete_handlers = [] + self._server_capabilities = {} + self._breakpoints.ClearTemporaryBreakpoints() - def GetConfigurations( self, adapters ): - current_file = utils.GetBufferFilepath( vim.current.buffer ) - filetypes = utils.GetBufferFiletypes( vim.current.buffer ) - configurations = settings.Dict( 'configurations' ) + def GetConfigurations( self, adapters ): + current_file = utils.GetBufferFilepath( vim.current.buffer ) + filetypes = utils.GetBufferFiletypes( vim.current.buffer ) + configurations = settings.Dict( 'configurations' ) - 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 + 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 {} ) + 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 {} ) - filetype_configurations = configurations - if filetypes: - # filter out any configurations that have a 'filetypes' list set and it - # doesn't contain one of the current filetypes - filetype_configurations = { - k: c for k, c in configurations.items() if 'filetypes' not in c or any( - ft in c[ 'filetypes' ] for ft in filetypes - ) - } + filetype_configurations = configurations + if filetypes: + # filter out any configurations that have a 'filetypes' list set and it + # doesn't contain one of the current filetypes + filetype_configurations = { + k: c for k, c in configurations.items() if 'filetypes' not in c or any( + ft in c[ 'filetypes' ] for ft in filetypes + ) + } - return launch_config_file, filetype_configurations, configurations + return launch_config_file, filetype_configurations, configurations - def Name( self ): - return self.name if self.name else "Unnamed-" + str( self.session_id ) + def Name( self ): + return self.name if self.name else "Unnamed-" + str( self.session_id ) - def DisplayName( self ): - return self.Name() + ' (' + str( self.session_id ) + ')' + def DisplayName( self ): + return self.Name() + ' (' + str( self.session_id ) + ')' - @ParentOnly() - def Start( self, - force_choose = False, - launch_variables = None, - adhoc_configurations = None ): - # We mutate launch_variables, so don't mutate the default argument. - # https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments - if launch_variables is None: - launch_variables = {} + @ParentOnly() + def Start( self, + force_choose = False, + launch_variables = None, + adhoc_configurations = None ): + # We mutate launch_variables, so don't mutate the default argument. + # https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments + if launch_variables is None: + launch_variables = {} - self._logger.info( "User requested start debug session with %s", - launch_variables ) + self._logger.info( "User requested start debug session with %s", + launch_variables ) - current_file = utils.GetBufferFilepath( vim.current.buffer ) - adapters = settings.Dict( 'adapters' ) + current_file = utils.GetBufferFilepath( vim.current.buffer ) + adapters = settings.Dict( 'adapters' ) - launch_config_file = None - configurations = None - if adhoc_configurations: - configurations = adhoc_configurations - else: - ( launch_config_file, - configurations, - all_configurations ) = self.GetConfigurations( adapters ) - - if not configurations: - utils.UserMessage( 'Unable to find any debug configurations. ' - 'You need to tell vimspector how to launch your ' - '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 force_choose: - # Always display the menu - configuration_name = utils.SelectFromList( - 'Which launch configuration?', - sorted( configurations.keys() ) ) - 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 ) - and c.get( 'autoselect', True ) } - - 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 self.name is None: - self.name = configuration_name - - if launch_config_file: - self._workspace_root = os.path.dirname( launch_config_file ) - else: - self._workspace_root = os.path.dirname( current_file ) - - try: - configuration = configurations[ configuration_name ] - except KeyError: - # Maybe the specified one by name that's not for this filetype? Let's try - # that one... - configuration = all_configurations[ configuration_name ] - - current_configuration_name = configuration_name - while 'extends' in configuration: - base_configuration_name = configuration.pop( 'extends' ) - base_configuration = all_configurations.get( base_configuration_name ) - if base_configuration is None: - raise RuntimeError( f"The adapter { current_configuration_name } " - f"extends configuration { base_configuration_name }" - ", but this does not exist" ) - - core_utils.override( base_configuration, configuration ) - current_configuration_name = base_configuration_name - configuration = base_configuration - - - 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: + launch_config_file = None + configurations = None + if adhoc_configurations: + configurations = adhoc_configurations + else: + ( launch_config_file, + configurations, + all_configurations ) = self.GetConfigurations( adapters ) + + if not configurations: + utils.UserMessage( 'Unable to find any debug configurations. ' + 'You need to tell vimspector how to launch your ' + 'application.' ) return - utils.UserMessage( f"The specified adapter '{adapter}' is not " - "available. Did you forget to run " - "'VimspectorInstall'?", - persist = True, - error = True ) - return - - adapter = adapter_dict - - if not adapter: - utils.UserMessage( 'No adapter configured for {}'.format( - configuration_name ), - persist=True ) - return - - # Pull in anything from the base(s) - # FIXME: this is copypasta from above, but sharing the code is a little icky - # due to the way it returns from this method (maybe use an exception?) - while 'extends' in adapter: - base_adapter_name = adapter.pop( 'extends' ) - base_adapter = adapters.get( base_adapter_name ) - - if base_adapter is None: - suggested_gadgets = installer.FindGadgetForAdapter( base_adapter_name ) - if suggested_gadgets: - response = utils.AskForInput( - f"The specified base adapter '{base_adapter_name}' 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 ) ) + 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 force_choose: + # Always display the menu + configuration_name = utils.SelectFromList( + 'Which launch configuration?', + sorted( configurations.keys() ) ) + 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 ) + and c.get( 'autoselect', True ) } + + 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 - elif response is None: + + if self.name is None: + self.name = configuration_name + + if launch_config_file: + self._workspace_root = os.path.dirname( launch_config_file ) + else: + self._workspace_root = os.path.dirname( current_file ) + + try: + configuration = configurations[ configuration_name ] + except KeyError: + # Maybe the specified one by name that's not for this filetype? Let's try + # that one... + configuration = all_configurations[ configuration_name ] + + current_configuration_name = configuration_name + while 'extends' in configuration: + base_configuration_name = configuration.pop( 'extends' ) + base_configuration = all_configurations.get( + base_configuration_name ) + if base_configuration is None: + raise RuntimeError( f"The adapter { current_configuration_name } " + f"extends configuration { base_configuration_name }" + ", but this does not exist" ) + + core_utils.override( base_configuration, configuration ) + current_configuration_name = base_configuration_name + configuration = base_configuration + + + 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 " + "'VimspectorInstall'?", + persist = True, + error = True ) + return + + adapter = adapter_dict + + if not adapter: + utils.UserMessage( 'No adapter configured for {}'.format( + configuration_name ), + persist=True ) return - utils.UserMessage( f"The specified base adapter '{base_adapter_name}' " - "is not available. Did you forget to run " - "'VimspectorInstall'?", - persist = True, - error = True ) - return - - core_utils.override( base_adapter, adapter ) - adapter = base_adapter - - # 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 = { - 'relativeFileDirname': lambda: os.path.dirname( relpath( current_file, - self._workspace_root ) ), - '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, - - # The following, starting with uppercase letters, are 'functions' taking - # arguments. - 'SelectProcess': _SelectProcess, - 'PickProcess': _SelectProcess, - } - - # 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 ) - - try: - variables.update( - utils.ParseVariables( adapter.pop( 'variables', {} ), - variables, - calculus, - USER_CHOICES ) ) - variables.update( - utils.ParseVariables( configuration.pop( 'variables', {} ), - variables, - calculus, - USER_CHOICES ) ) - - - utils.ExpandReferencesInDict( configuration, + # Pull in anything from the base(s) + # FIXME: this is copypasta from above, but sharing the code is a little icky + # due to the way it returns from this method (maybe use an exception?) + while 'extends' in adapter: + base_adapter_name = adapter.pop( 'extends' ) + base_adapter = adapters.get( base_adapter_name ) + + if base_adapter is None: + suggested_gadgets = installer.FindGadgetForAdapter( + base_adapter_name ) + if suggested_gadgets: + response = utils.AskForInput( + f"The specified base adapter '{base_adapter_name}' 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 base adapter '{base_adapter_name}' " + "is not available. Did you forget to run " + "'VimspectorInstall'?", + persist = True, + error = True ) + return + + core_utils.override( base_adapter, adapter ) + adapter = base_adapter + + # 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 = { + 'relativeFileDirname': lambda: os.path.dirname( relpath( current_file, + self._workspace_root ) ), + '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, + + # The following, starting with uppercase letters, are 'functions' taking + # arguments. + 'SelectProcess': _SelectProcess, + 'PickProcess': _SelectProcess, + } + + # 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 ) + + try: + variables.update( + utils.ParseVariables( adapter.pop( 'variables', {} ), variables, calculus, - USER_CHOICES ) - utils.ExpandReferencesInDict( adapter, + USER_CHOICES ) ) + variables.update( + utils.ParseVariables( configuration.pop( 'variables', {} ), variables, calculus, - USER_CHOICES ) - except KeyboardInterrupt: - self._Reset() - return + USER_CHOICES ) ) + + + utils.ExpandReferencesInDict( configuration, + variables, + calculus, + USER_CHOICES ) + utils.ExpandReferencesInDict( adapter, + variables, + calculus, + USER_CHOICES ) + except KeyboardInterrupt: + self._Reset() + return + + self._StartWithConfiguration( configuration, adapter ) + + def _StartWithConfiguration( self, configuration, adapter ): + def start(): + self._configuration = configuration + self._adapter = adapter + self._launch_config = None + + self._logger.info( 'Configuration: %s', + json.dumps( self._configuration ) ) + self._logger.info( 'Adapter: %s', + json.dumps( self._adapter ) ) - self._StartWithConfiguration( configuration, adapter ) - def _StartWithConfiguration( self, configuration, adapter ): - def start(): - self._configuration = configuration - self._adapter = adapter - self._launch_config = None + if self.parent_session: + # use the parent session's stuff + self._uiTab = self.parent_session._uiTab + self._stackTraceView = self.parent_session._stackTraceView + self._variablesView = self.parent_session._variablesView + self._outputView = self.parent_session._outputView + self._disassemblyView = self.parent_session._disassemblyView + self._codeView = self.parent_session._codeView - self._logger.info( 'Configuration: %s', - json.dumps( self._configuration ) ) - self._logger.info( 'Adapter: %s', - json.dumps( self._adapter ) ) + elif not self._uiTab: + self._SetUpUI() + else: + with utils.NoAutocommands(): + vim.current.tabpage = self._uiTab + self._stackTraceView.AddSession( self ) + self._Prepare() + if not self._StartDebugAdapter(): + self._logger.info( + "Failed to launch or attach to the debug adapter" ) + return - if self.parent_session: - # use the parent session's stuff - self._uiTab = self.parent_session._uiTab - self._stackTraceView = self.parent_session._stackTraceView - self._variablesView = self.parent_session._variablesView - self._outputView = self.parent_session._outputView - self._disassemblyView = self.parent_session._disassemblyView - self._codeView = self.parent_session._codeView + self._Initialise() - elif not self._uiTab: - self._SetUpUI() - else: - with utils.NoAutocommands(): - vim.current.tabpage = self._uiTab + if self._saved_variables_data: + self._variablesView.Load( self._saved_variables_data ) - self._stackTraceView.AddSession( self ) - self._Prepare() - if not self._StartDebugAdapter(): - self._logger.info( "Failed to launch or attach to the debug adapter" ) - return + if self._connection: + self._logger.debug( "Stop debug adapter with callback: start" ) + self.StopAllSessions( interactive = False, then = start ) + return - self._Initialise() + start() - if self._saved_variables_data: - self._variablesView.Load( self._saved_variables_data ) + @ParentOnly() + def Restart( self ): + if self._configuration is None or self._adapter is None: + return self.Start() - if self._connection: - self._logger.debug( "Stop debug adapter with callback: start" ) - self.StopAllSessions( interactive = False, then = start ) - return + self._StartWithConfiguration( self._configuration, self._adapter ) - start() + def Connection( self ): + return self._connection - @ParentOnly() - def Restart( self ): - if self._configuration is None or self._adapter is None: - return self.Start() + def HasUI( self ): + return self._uiTab and self._uiTab.valid - self._StartWithConfiguration( self._configuration, self._adapter ) + def IsUITab( self, tab_number ): + return self.HasUI() and self._uiTab.number == tab_number - def Connection( self ): - return self._connection + @ParentOnly() + def SwitchTo( self ): + if self.HasUI(): + vim.current.tabpage = self._uiTab - def HasUI( self ): - return self._uiTab and self._uiTab.valid + self._breakpoints.UpdateUI() - def IsUITab( self, tab_number ): - return self.HasUI() and self._uiTab.number == tab_number - @ParentOnly() - def SwitchTo( self ): - if self.HasUI(): - vim.current.tabpage = self._uiTab + @ParentOnly() + def SwitchFrom( self ): + self._breakpoints.ClearUI() - self._breakpoints.UpdateUI() + def OnChannelData( self, data ): + if self._connection is None: + # Should _not_ happen, but maybe possible due to races or vim bufs? + return - @ParentOnly() - def SwitchFrom( self ): - self._breakpoints.ClearUI() + self._connection.OnData( data ) - def OnChannelData( self, data ): - if self._connection is None: - # Should _not_ happen, but maybe possible due to races or vim bufs? - return + def OnServerStderr( self, data ): + if self._outputView: + self._outputView.Print( 'server', data ) - self._connection.OnData( data ) + def OnRequestTimeout( self, timer_id ): + self._connection.OnRequestTimeout( timer_id ) - def OnServerStderr( self, data ): - if self._outputView: - self._outputView.Print( 'server', data ) + def OnChannelClosed( self ): + # TODO: Not called + self._connection = None - def OnRequestTimeout( self, timer_id ): - self._connection.OnRequestTimeout( timer_id ) + def _StopNextSession( self, terminateDebuggee, then ): + if self.child_sessions: + c = self.child_sessions.pop() + c._StopNextSession( terminateDebuggee, + then = lambda: self._StopNextSession( + terminateDebuggee, + then ) ) + elif self._connection: + self._StopDebugAdapter( terminateDebuggee, callback = then ) + elif then: + then() - def OnChannelClosed( self ): - # TODO: Not called - self._connection = None + def StopAllSessions( self, interactive = False, then = None ): + if not interactive: + self._StopNextSession( None, then ) + elif not self._server_capabilities.get( 'supportTerminateDebuggee' ): + self._StopNextSession( None, then ) + elif not self._stackTraceView.AnyThreadsRunning(): + self._StopNextSession( None, then ) + else: + self._ConfirmTerminateDebugee( + lambda terminateDebuggee: self._StopNextSession( terminateDebuggee, + then ) ) + + @ParentOnly() + @IfConnected() + def Stop( self, interactive = False ): + self._logger.debug( "Stop debug adapter with no callback" ) + self.StopAllSessions( interactive = False ) + + @ParentOnly() + def Destroy( self ): + """Call when the vimspector session will be removed and never used again""" + if self._connection is not None: + raise RuntimeError( + "Can't destroy a session with a live connection" ) + + if self.HasUI(): + raise RuntimeError( "Can't destroy a session with an active UI" ) + + self.ClearBreakpoints() + self._ResetUI() + + + @ParentOnly() + def Reset( self, interactive = False ): + # We reset all of the child sessions in turn + self._logger.debug( "Stop debug adapter with callback: _Reset" ) + self.StopAllSessions( interactive, self._Reset ) + + + def _IsPCPresentAt( self, file_path, line ): + return self._codeView and self._codeView.IsPCPresentAt( file_path, line ) + + + def _ResetUI( self ): + if not self.parent_session: + if self._stackTraceView: + self._stackTraceView.Reset() + if self._variablesView: + self._variablesView.Reset() + if self._outputView: + self._outputView.Reset() + if self._logView: + self._logView.Reset() + if self._codeView: + self._codeView.Reset() + if self._disassemblyView: + self._disassemblyView.Reset() + + self._breakpoints.RemoveConnection( self._connection ) + self._stackTraceView = None + self._variablesView = None + self._outputView = None + self._codeView = None + self._disassemblyView = None + self._remote_term = None + self._uiTab = None + if self.parent_session: + self.manager.DestroySession( self ) - def _StopNextSession( self, terminateDebuggee, then ): - if self.child_sessions: - c = self.child_sessions.pop() - c._StopNextSession( terminateDebuggee, - then = lambda: self._StopNextSession( - terminateDebuggee, - then ) ) - elif self._connection: - self._StopDebugAdapter( terminateDebuggee, callback = then ) - elif then: - then() - def StopAllSessions( self, interactive = False, then = None ): - if not interactive: - self._StopNextSession( None, then ) - elif not self._server_capabilities.get( 'supportTerminateDebuggee' ): - self._StopNextSession( None, then ) - elif not self._stackTraceView.AnyThreadsRunning(): - self._StopNextSession( None, then ) - else: - self._ConfirmTerminateDebugee( - lambda terminateDebuggee: self._StopNextSession( terminateDebuggee, - then ) ) - - @ParentOnly() - @IfConnected() - def Stop( self, interactive = False ): - self._logger.debug( "Stop debug adapter with no callback" ) - self.StopAllSessions( interactive = False ) - - @ParentOnly() - def Destroy( self ): - """Call when the vimspector session will be removed and never used again""" - if self._connection is not None: - raise RuntimeError( "Can't destroy a session with a live connection" ) - - if self.HasUI(): - raise RuntimeError( "Can't destroy a session with an active UI" ) - - self.ClearBreakpoints() - self._ResetUI() - - - @ParentOnly() - def Reset( self, interactive = False ): - # We reset all of the child sessions in turn - self._logger.debug( "Stop debug adapter with callback: _Reset" ) - self.StopAllSessions( interactive, self._Reset ) - - - def _IsPCPresentAt( self, file_path, line ): - return self._codeView and self._codeView.IsPCPresentAt( file_path, line ) - - - def _ResetUI( self ): - if not self.parent_session: - if self._stackTraceView: - self._stackTraceView.Reset() - if self._variablesView: - self._variablesView.Reset() - if self._outputView: - self._outputView.Reset() - if self._logView: - self._logView.Reset() - if self._codeView: - self._codeView.Reset() - if self._disassemblyView: - self._disassemblyView.Reset() - - self._breakpoints.RemoveConnection( self._connection ) - self._stackTraceView = None - self._variablesView = None - self._outputView = None - self._codeView = None - self._disassemblyView = None - self._remote_term = None - self._uiTab = None - - if self.parent_session: - self.manager.DestroySession( self ) - - - def _Reset( self ): - if self.parent_session: - self._ResetUI() - return - - vim.vars[ 'vimspector_resetting' ] = 1 - self._logger.info( "Debugging complete." ) - - if self.HasUI(): - self._logger.debug( "Clearing down UI" ) - with utils.NoAutocommands(): - vim.current.tabpage = self._uiTab - self._splash_screen = utils.HideSplash( self._api_prefix, - self._splash_screen ) - self._ResetUI() - vim.command( 'tabclose!' ) - else: - self._ResetUI() - - self._breakpoints.SetDisassemblyManager( None ) - utils.SetSessionWindows( { - 'breakpoints': vim.vars[ 'vimspector_session_windows' ].get( - 'breakpoints' ) - } ) - vim.command( 'doautocmd User VimspectorDebugEnded' ) - - vim.vars[ 'vimspector_resetting' ] = 0 - - # make sure that we're displaying signs in any still-open buffers - self._breakpoints.UpdateUI() - - @ParentOnly( False ) - def ReadSessionFile( self, session_file: str = None ): - if session_file is None: - session_file = self._DetectSessionFile( invent_one_if_not_found = False ) - - if session_file is None: - utils.UserMessage( f"No { settings.Get( 'session_file_name' ) } file " - "found. Specify a file with :VimspectorLoadSession " - "", - persist = True, - error = True ) - return False - - try: - with open( session_file, 'r' ) as f: - session_data = json.load( f ) - - USER_CHOICES.update( - session_data.get( 'session', {} ).get( 'user_choices', {} ) ) - - self._breakpoints.Load( session_data.get( 'breakpoints' ) ) - - # We might not _have_ a self._variablesView yet so we need a - # mechanism where we save this for later and reload when it's ready - variables_data = session_data.get( 'variables', {} ) - if self._variablesView: - self._variablesView.Load( variables_data ) - else: - self._saved_variables_data = variables_data - - utils.UserMessage( f"Loaded session file { session_file }", - persist=True ) - return True - except OSError: - self._logger.exception( f"Invalid session file { session_file }" ) - utils.UserMessage( f"Session file { session_file } not found", - persist=True, - error=True ) - return False - except json.JSONDecodeError: - self._logger.exception( f"Invalid session file { session_file }" ) - utils.UserMessage( "The session file could not be read", - persist = True, - error = True ) - return False - - - @ParentOnly( False ) - def WriteSessionFile( self, session_file: str = None ): - if session_file is None: - session_file = self._DetectSessionFile( invent_one_if_not_found = True ) - elif os.path.isdir( session_file ): - session_file = self._DetectSessionFile( invent_one_if_not_found = True, - in_directory = session_file ) - - - try: - with open( session_file, 'w' ) as f: - f.write( json.dumps( { - 'breakpoints': self._breakpoints.Save(), - 'session': { - 'user_choices': USER_CHOICES, - }, - 'variables': self._variablesView.Save() if self._variablesView else {} - } ) ) - - utils.UserMessage( f"Wrote { session_file }" ) - return True - except OSError: - self._logger.exception( f"Unable to write session file { session_file }" ) - utils.UserMessage( "The session file could not be read", - persist = True, - error = True ) - return False - - - def _DetectSessionFile( self, - invent_one_if_not_found: bool, - in_directory: str = None ): - session_file_name = settings.Get( 'session_file_name' ) - - if in_directory: - # If a dir was supplied, read from there - write_directory = in_directory - file_path = os.path.join( in_directory, session_file_name ) - if not os.path.exists( file_path ): - file_path = None - else: - # Otherwise, search based on the current file, and write based on CWD - current_file = utils.GetBufferFilepath( vim.current.buffer ) - write_directory = os.getcwd() - # Search from the path of the file we're editing. But note that if we - # invent a file, we always use CWD as that's more like what would be - # expected. - file_path = utils.PathToConfigFile( session_file_name, - os.path.dirname( current_file ) ) - - - if file_path: - return file_path - - if invent_one_if_not_found: - return os.path.join( write_directory, session_file_name ) - - return None - - - @CurrentSession() - @IfConnected() - def StepOver( self, **kwargs ): - if self._stackTraceView.GetCurrentThreadId() is None: - return - - arguments = { - 'threadId': self._stackTraceView.GetCurrentThreadId(), - 'granularity': self._CurrentSteppingGranularity(), - } - arguments.update( kwargs ) - - if not self._server_capabilities.get( 'supportsSteppingGranularity' ): - arguments.pop( 'granularity' ) - - self._connection.DoRequest( None, { - 'command': 'next', - 'arguments': arguments, - } ) - - # TODO: WHy is this different from StepInto and StepOut - self._stackTraceView.OnContinued( self ) - self.ClearCurrentPC() - - @CurrentSession() - @IfConnected() - def StepInto( self, **kwargs ): - threadId = self._stackTraceView.GetCurrentThreadId() - if threadId is None: - return - - def handler( *_ ): - self._stackTraceView.OnContinued( self, { 'threadId': threadId } ) - self.ClearCurrentPC() - - arguments = { - 'threadId': threadId, - 'granularity': self._CurrentSteppingGranularity(), - } - arguments.update( kwargs ) - self._connection.DoRequest( handler, { - 'command': 'stepIn', - 'arguments': arguments, - } ) - - @CurrentSession() - @IfConnected() - def StepOut( self, **kwargs ): - threadId = self._stackTraceView.GetCurrentThreadId() - if threadId is None: - return - - def handler( *_ ): - self._stackTraceView.OnContinued( self, { 'threadId': threadId } ) - self.ClearCurrentPC() - - arguments = { - 'threadId': threadId, - 'granularity': self._CurrentSteppingGranularity(), - } - arguments.update( kwargs ) - self._connection.DoRequest( handler, { - 'command': 'stepOut', - 'arguments': arguments, - } ) - - def _CurrentSteppingGranularity( self ): - if self._disassemblyView and self._disassemblyView.IsCurrent(): - return 'instruction' - - return 'statement' - - @CurrentSession() - def Continue( self ): - if not self._connection: - self.Start() - return - - threadId = self._stackTraceView.GetCurrentThreadId() - if threadId is None: - utils.UserMessage( 'No current thread', persist = True ) - return - - def handler( msg ): - self._stackTraceView.OnContinued( self, { + def _Reset( self ): + if self.parent_session: + self._ResetUI() + return + + vim.vars[ 'vimspector_resetting' ] = 1 + self._logger.info( "Debugging complete." ) + + if self.HasUI(): + self._logger.debug( "Clearing down UI" ) + with utils.NoAutocommands(): + vim.current.tabpage = self._uiTab + self._splash_screen = utils.HideSplash( self._api_prefix, + self._splash_screen ) + self._ResetUI() + vim.command( 'tabclose!' ) + else: + self._ResetUI() + + self._breakpoints.SetDisassemblyManager( None ) + utils.SetSessionWindows( { + 'breakpoints': vim.vars[ 'vimspector_session_windows' ].get( + 'breakpoints' ) + } ) + vim.command( 'doautocmd User VimspectorDebugEnded' ) + + vim.vars[ 'vimspector_resetting' ] = 0 + + # make sure that we're displaying signs in any still-open buffers + self._breakpoints.UpdateUI() + + @ParentOnly( False ) + def ReadSessionFile( self, session_file: str = None ): + if session_file is None: + session_file = self._DetectSessionFile( + invent_one_if_not_found = False ) + + if session_file is None: + utils.UserMessage( f"No { settings.Get( 'session_file_name' ) } file " + "found. Specify a file with :VimspectorLoadSession " + "", + persist = True, + error = True ) + return False + + try: + with open( session_file, 'r' ) as f: + session_data = json.load( f ) + + USER_CHOICES.update( + session_data.get( 'session', {} ).get( 'user_choices', {} ) ) + + self._breakpoints.Load( session_data.get( 'breakpoints' ) ) + + # We might not _have_ a self._variablesView yet so we need a + # mechanism where we save this for later and reload when it's ready + variables_data = session_data.get( 'variables', {} ) + if self._variablesView: + self._variablesView.Load( variables_data ) + else: + self._saved_variables_data = variables_data + + utils.UserMessage( f"Loaded session file { session_file }", + persist=True ) + return True + except OSError: + self._logger.exception( f"Invalid session file { session_file }" ) + utils.UserMessage( f"Session file { session_file } not found", + persist=True, + error=True ) + return False + except json.JSONDecodeError: + self._logger.exception( f"Invalid session file { session_file }" ) + utils.UserMessage( "The session file could not be read", + persist = True, + error = True ) + return False + + + @ParentOnly( False ) + def WriteSessionFile( self, session_file: str = None ): + if session_file is None: + session_file = self._DetectSessionFile( + invent_one_if_not_found = True ) + elif os.path.isdir( session_file ): + session_file = self._DetectSessionFile( invent_one_if_not_found = True, + in_directory = session_file ) + + + try: + with open( session_file, 'w' ) as f: + f.write( json.dumps( { + 'breakpoints': self._breakpoints.Save(), + 'session': { + 'user_choices': USER_CHOICES, + }, + 'variables': self._variablesView.Save() if self._variablesView else {} + } ) ) + + utils.UserMessage( f"Wrote { session_file }" ) + return True + except OSError: + self._logger.exception( + f"Unable to write session file { session_file }" ) + utils.UserMessage( "The session file could not be read", + persist = True, + error = True ) + return False + + + def _DetectSessionFile( self, + invent_one_if_not_found: bool, + in_directory: str = None ): + session_file_name = settings.Get( 'session_file_name' ) + + if in_directory: + # If a dir was supplied, read from there + write_directory = in_directory + file_path = os.path.join( in_directory, session_file_name ) + if not os.path.exists( file_path ): + file_path = None + else: + # Otherwise, search based on the current file, and write based on CWD + current_file = utils.GetBufferFilepath( vim.current.buffer ) + write_directory = os.getcwd() + # Search from the path of the file we're editing. But note that if we + # invent a file, we always use CWD as that's more like what would be + # expected. + file_path = utils.PathToConfigFile( session_file_name, + os.path.dirname( current_file ) ) + + + if file_path: + return file_path + + if invent_one_if_not_found: + return os.path.join( write_directory, session_file_name ) + + return None + + + @CurrentSession() + @IfConnected() + def StepOver( self, **kwargs ): + if self._stackTraceView.GetCurrentThreadId() is None: + return + + arguments = { + 'threadId': self._stackTraceView.GetCurrentThreadId(), + 'granularity': self._CurrentSteppingGranularity(), + } + arguments.update( kwargs ) + + if not self._server_capabilities.get( 'supportsSteppingGranularity' ): + arguments.pop( 'granularity' ) + + self._connection.DoRequest( None, { + 'command': 'next', + 'arguments': arguments, + } ) + + # TODO: WHy is this different from StepInto and StepOut + self._stackTraceView.OnContinued( self ) + self.ClearCurrentPC() + + @CurrentSession() + @IfConnected() + def StepInto( self, **kwargs ): + threadId = self._stackTraceView.GetCurrentThreadId() + if threadId is None: + return + + def handler( *_ ): + self._stackTraceView.OnContinued( self, { 'threadId': threadId } ) + self.ClearCurrentPC() + + arguments = { 'threadId': threadId, - 'allThreadsContinued': ( msg.get( 'body' ) or {} ).get( - 'allThreadsContinued', - True ) + 'granularity': self._CurrentSteppingGranularity(), + } + arguments.update( kwargs ) + self._connection.DoRequest( handler, { + 'command': 'stepIn', + 'arguments': arguments, } ) - self.ClearCurrentPC() - - self._connection.DoRequest( handler, { - 'command': 'continue', - 'arguments': { - 'threadId': threadId, - }, - } ) - - @CurrentSession() - @IfConnected() - def Pause( self ): - if self._stackTraceView.GetCurrentThreadId() is None: - utils.UserMessage( 'No current thread', persist = True ) - return - - self._connection.DoRequest( None, { - 'command': 'pause', - 'arguments': { - 'threadId': self._stackTraceView.GetCurrentThreadId(), - }, - } ) - - @IfConnected() - def PauseContinueThread( self ): - self._stackTraceView.PauseContinueThread() - - @CurrentSession() - @IfConnected() - def SetCurrentThread( self ): - self._stackTraceView.SetCurrentThread() - - @CurrentSession() - @IfConnected() - def ExpandVariable( self, buf = None, line_num = None ): - self._variablesView.ExpandVariable( buf, line_num ) - - @CurrentSession() - @IfConnected() - def SetVariableValue( self, new_value = None, buf = None, line_num = None ): - if not self._server_capabilities.get( 'supportsSetVariable' ): - return - self._variablesView.SetVariableValue( new_value, buf, line_num ) - - @ParentOnly() - def ReadMemory( self, length = None, offset = None ): - # We use the parent session because the actual connection is returned from - # the variables view (and might not be our self._connection) at least in - # theory. - if not self._server_capabilities.get( 'supportsReadMemoryRequest' ): - utils.UserMessage( "Server does not support memory request", - error = True ) - return - - connection: debug_adapter_connection.DebugAdapterConnection - connection, memoryReference = self._variablesView.GetMemoryReference() - if memoryReference is None or connection is None: - utils.UserMessage( "Cannot find memory reference for that", - error = True ) - return - - if length is None: - length = utils.AskForInput( 'How much data to display? ', - default_value = '1024' ) - - try: - length = int( length ) - except ValueError: - return - - if offset is None: - offset = utils.AskForInput( 'Location offset? ', - default_value = '0' ) - - try: - offset = int( offset ) - except ValueError: - return - - - def handler( msg ): - self._codeView.ShowMemory( connection.GetSessionId(), - memoryReference, - length, - offset, - msg ) - - connection.DoRequest( handler, { - 'command': 'readMemory', - 'arguments': { - 'memoryReference': memoryReference, - 'count': int( length ), - 'offset': int( offset ) - } - } ) - - - @CurrentSession() - @IfConnected() - @RequiresUI() - def ShowDisassembly( self ): - if self._disassemblyView and self._disassemblyView.WindowIsValid(): - return - - if not self._codeView or not self._codeView._window.valid: - return - - if not self._stackTraceView: - return - - if not self._server_capabilities.get( 'supportsDisassembleRequest', False ): - utils.UserMessage( "Sorry, server doesn't support that" ) - return - - with utils.LetCurrentWindow( self._codeView._window ): - vim.command( f'rightbelow { settings.Int( "disassembly_height" ) }new' ) - self._disassemblyView = disassembly.DisassemblyView( - vim.current.window, - self._api_prefix, - self._render_emitter ) - - self._breakpoints.SetDisassemblyManager( self._disassemblyView ) - - utils.UpdateSessionWindows( { - 'disassembly': utils.WindowID( vim.current.window, self._uiTab ) - } ) - - self._disassemblyView.SetCurrentFrame( - self._connection, - self._stackTraceView.GetCurrentFrame(), - True ) - - - def OnDisassemblyWindowScrolled( self, win_id ): - if self._disassemblyView: - self._disassemblyView.OnWindowScrolled( win_id ) - - - @CurrentSession() - @IfConnected() - def AddWatch( self, expression ): - self._variablesView.AddWatch( self._connection, - self._stackTraceView.GetCurrentFrame(), - expression ) - - @CurrentSession() - @IfConnected() - def EvaluateConsole( self, expression, verbose ): - self._outputView.Evaluate( self._connection, - self._stackTraceView.GetCurrentFrame(), - expression, - verbose ) - - @CurrentSession() - @IfConnected() - def DeleteWatch( self ): - self._variablesView.DeleteWatch() - - - @CurrentSession() - @IfConnected() - def HoverEvalTooltip( self, winnr, bufnr, lnum, expression, is_hover ): - frame = self._stackTraceView.GetCurrentFrame() - # Check if RIP is in a frame - if frame is None: - self._logger.debug( 'Tooltip: Not in a stack frame' ) - return '' - - # Check if cursor in code window - if winnr == int( self._codeView._window.number ): - return self._variablesView.HoverEvalTooltip( self._connection, - frame, - expression, - is_hover ) - - return self._variablesView.HoverVarWinTooltip( bufnr, - lnum, - is_hover ) - # Return variable aware function - - - @CurrentSession() - def CleanUpTooltip( self ): - return self._variablesView.CleanUpTooltip() - - @IfConnected() - def ExpandFrameOrThread( self ): - self._stackTraceView.ExpandFrameOrThread() - - @IfConnected() - def UpFrame( self ): - self._stackTraceView.UpFrame() - - @IfConnected() - def DownFrame( self ): - self._stackTraceView.DownFrame() - - def ToggleLog( self ): - if self.HasUI(): - return self.ShowOutput( 'Vimspector' ) - - if self._logView and self._logView.WindowIsValid(): - self._logView.Reset() - self._logView = None - return - - if self._logView: - self._logView.Reset() - - # TODO: The UI code is too scattered. Re-organise into a UI class that - # just deals with these things like window layout and custmisattion. - vim.command( f'botright { settings.Int( "bottombar_height" ) }new' ) - win = vim.current.window - self._logView = output.OutputView( win, self._api_prefix ) - self._logView.AddLogFileView() - self._logView.ShowOutput( 'Vimspector' ) - - @RequiresUI() - def ShowOutput( self, category ): - if not self._outputView.WindowIsValid(): - # TODO: The UI code is too scattered. Re-organise into a UI class that - # just deals with these things like window layout and custmisattion. - # currently, this class and the CodeView share some responsibility for - # this and poking into each View class to check its window is valid also - # feels wrong. - with utils.LetCurrentTabpage( self._uiTab ): - vim.command( f'botright { settings.Int( "bottombar_height" ) }new' ) - self._outputView.UseWindow( vim.current.window ) - utils.UpdateSessionWindows( { - 'output': utils.WindowID( vim.current.window, self._uiTab ) + + @CurrentSession() + @IfConnected() + def StepOut( self, **kwargs ): + threadId = self._stackTraceView.GetCurrentThreadId() + if threadId is None: + return + + def handler( *_ ): + self._stackTraceView.OnContinued( self, { 'threadId': threadId } ) + self.ClearCurrentPC() + + arguments = { + 'threadId': threadId, + 'granularity': self._CurrentSteppingGranularity(), + } + arguments.update( kwargs ) + self._connection.DoRequest( handler, { + 'command': 'stepOut', + 'arguments': arguments, } ) - self._outputView.ShowOutput( category ) - - @RequiresUI( otherwise=[] ) - def GetOutputBuffers( self ): - return self._outputView.GetCategories() - - @CurrentSession() - @IfConnected( otherwise=[] ) - def GetCompletionsSync( self, text_line, column_in_bytes ): - if not self._server_capabilities.get( 'supportsCompletionsRequest' ): - return [] - - response = self._connection.DoRequestSync( { - 'command': 'completions', - 'arguments': { - 'frameId': self._stackTraceView.GetCurrentFrame()[ 'id' ], - # TODO: encoding ? bytes/codepoints - 'text': text_line, - 'column': column_in_bytes - } - } ) - # TODO: - # - start / length - # - sortText - return response[ 'body' ][ 'targets' ] - - - @CurrentSession() - @IfConnected( otherwise=[] ) - def GetCommandLineCompletions( self, ArgLead, prev_non_keyword_char ): - items = [] - for candidate in self.GetCompletionsSync( ArgLead, prev_non_keyword_char ): - label = candidate.get( 'text', candidate[ 'label' ] ) - start = prev_non_keyword_char - 1 - if 'start' in candidate and 'length' in candidate: - start = candidate[ 'start' ] - items.append( ArgLead[ 0 : start ] + label ) - - return items - - - @ParentOnly() - def RefreshSigns( self ): - if self._connection: - self._codeView.Refresh() - self._breakpoints.Refresh() - - - @ParentOnly() - def _SetUpUI( self ): - vim.command( '$tab split' ) - - # Switch to this session now that we've made it visible. Note that the - # TabEnter autocmd does trigger when the above is run, but that's before the - # following line assigns the tab to this session, so when we try to find - # this session by tab number, it's not found. So we have to manually switch - # to it when creating a new tab. - utils.Call( 'vimspector#internal#state#SwitchToSession', - self.session_id ) - - self._uiTab = vim.current.tabpage - - mode = settings.Get( 'ui_mode' ) - - if mode == 'auto': - # Go vertical if there isn't enough horizontal space for at least: - # the left bar width - # + the code min width - # + the terminal min width - # + enough space for a sign column and number column? - min_width = ( settings.Int( 'sidebar_width' ) - + 1 + 2 + 3 - + settings.Int( 'code_minwidth' ) - + 1 + settings.Int( 'terminal_minwidth' ) ) - - min_height = ( settings.Int( 'code_minheight' ) + 1 + - settings.Int( 'topbar_height' ) + 1 + - settings.Int( 'bottombar_height' ) + 1 + - 2 ) - - mode = ( 'vertical' - if vim.options[ 'columns' ] < min_width - else 'horizontal' ) - - if vim.options[ 'lines' ] < min_height: - mode = 'horizontal' - - self._logger.debug( 'min_width/height: %s/%s, actual: %s/%s - result: %s', - min_width, - min_height, - vim.options[ 'columns' ], - vim.options[ 'lines' ], - mode ) - - if mode == 'vertical': - self._SetUpUIVertical() - else: - self._SetUpUIHorizontal() - - - def _SetUpUIHorizontal( self ): - # Code window - code_window = vim.current.window - self._codeView = code.CodeView( self.session_id, - code_window, - self._api_prefix, - self._render_emitter, - self._breakpoints.IsBreakpointPresentAt ) - - # Call stack - vim.command( - f'topleft vertical { settings.Int( "sidebar_width" ) }new' ) - stack_trace_window = vim.current.window - one_third = int( vim.eval( 'winheight( 0 )' ) ) / 3 - self._stackTraceView = stack_trace.StackTraceView( self.session_id, - stack_trace_window ) - - # Watches - vim.command( 'leftabove new' ) - watch_window = vim.current.window - - # Variables - vim.command( 'leftabove new' ) - vars_window = vim.current.window - - with utils.LetCurrentWindow( vars_window ): - vim.command( f'{ one_third }wincmd _' ) - with utils.LetCurrentWindow( watch_window ): - vim.command( f'{ one_third }wincmd _' ) - with utils.LetCurrentWindow( stack_trace_window ): - vim.command( f'{ one_third }wincmd _' ) - - self._variablesView = variables.VariablesView( self.session_id, - vars_window, - watch_window ) - - # Output/logging - vim.current.window = code_window - vim.command( f'rightbelow { settings.Int( "bottombar_height" ) }new' ) - output_window = vim.current.window - self._outputView = output.DAPOutputView( output_window, - self._api_prefix, - session_id = self.session_id ) - - utils.SetSessionWindows( { - 'mode': 'horizontal', - 'tabpage': self._uiTab.number, - 'code': utils.WindowID( code_window, self._uiTab ), - 'stack_trace': utils.WindowID( stack_trace_window, self._uiTab ), - 'variables': utils.WindowID( vars_window, self._uiTab ), - 'watches': utils.WindowID( watch_window, self._uiTab ), - 'output': utils.WindowID( output_window, self._uiTab ), - 'eval': None, # updated every time eval popup is opened - 'breakpoints': vim.vars[ 'vimspector_session_windows' ].get( - 'breakpoints' ) # same as above, but for breakpoints - } ) - with utils.RestoreCursorPosition(): - with utils.RestoreCurrentWindow(): - with utils.RestoreCurrentBuffer( vim.current.window ): - vim.command( 'doautocmd User VimspectorUICreated' ) - - - def _SetUpUIVertical( self ): - # Code window - code_window = vim.current.window - self._codeView = code.CodeView( self.session_id, - code_window, - self._api_prefix, - self._render_emitter, - self._breakpoints.IsBreakpointPresentAt ) - - # Call stack - vim.command( - f'topleft { settings.Int( "topbar_height" ) }new' ) - stack_trace_window = vim.current.window - one_third = int( vim.eval( 'winwidth( 0 )' ) ) / 3 - self._stackTraceView = stack_trace.StackTraceView( self.session_id, - stack_trace_window ) - - - # Watches - vim.command( 'leftabove vertical new' ) - watch_window = vim.current.window - - # Variables - vim.command( 'leftabove vertical new' ) - vars_window = vim.current.window - - - with utils.LetCurrentWindow( vars_window ): - vim.command( f'{ one_third }wincmd |' ) - with utils.LetCurrentWindow( watch_window ): - vim.command( f'{ one_third }wincmd |' ) - with utils.LetCurrentWindow( stack_trace_window ): - vim.command( f'{ one_third }wincmd |' ) - - self._variablesView = variables.VariablesView( self.session_id, - vars_window, - watch_window ) - - - # Output/logging - vim.current.window = code_window - vim.command( f'rightbelow { settings.Int( "bottombar_height" ) }new' ) - output_window = vim.current.window - self._outputView = output.DAPOutputView( output_window, - self._api_prefix, - session_id = self.session_id ) - - utils.SetSessionWindows( { - 'mode': 'vertical', - 'tabpage': self._uiTab.number, - 'code': utils.WindowID( code_window, self._uiTab ), - 'stack_trace': utils.WindowID( stack_trace_window, self._uiTab ), - 'variables': utils.WindowID( vars_window, self._uiTab ), - 'watches': utils.WindowID( watch_window, self._uiTab ), - 'output': utils.WindowID( output_window, self._uiTab ), - 'eval': None, # updated every time eval popup is opened - 'breakpoints': vim.vars[ 'vimspector_session_windows' ].get( - 'breakpoints' ) # same as above, but for breakpoints - } ) - with utils.RestoreCursorPosition(): - with utils.RestoreCurrentWindow(): - with utils.RestoreCurrentBuffer( vim.current.window ): - vim.command( 'doautocmd User VimspectorUICreated' ) - - - @RequiresUI() - def ClearCurrentFrame( self ): - self.SetCurrentFrame( None ) - - - def ClearCurrentPC( self ): - self._codeView.SetCurrentFrame( None, False ) - if self._disassemblyView: - self._disassemblyView.SetCurrentFrame( None, None, False ) - - - @RequiresUI() - def SetCurrentFrame( self, frame, reason = '' ): - if not frame: - self._variablesView.Clear() - - target = self._codeView - if self._disassemblyView and self._disassemblyView.IsCurrent(): - target = self._disassemblyView - - if not self._codeView.SetCurrentFrame( frame, - target == self._codeView ): - return False - - if self._disassemblyView: - self._disassemblyView.SetCurrentFrame( self._connection, - frame, - target == self._disassemblyView ) - - # the codeView.SetCurrentFrame already checked the frame was valid and - # countained a valid source - assert frame - if self._codeView.current_syntax not in ( 'ON', 'OFF' ): - self._variablesView.SetSyntax( self._codeView.current_syntax ) - self._stackTraceView.SetSyntax( self._codeView.current_syntax ) - else: - self._variablesView.SetSyntax( None ) - self._stackTraceView.SetSyntax( None ) - - self._variablesView.LoadScopes( self._connection, frame ) - self._variablesView.EvaluateWatches( self._connection, frame ) - - if reason == 'stopped': - self._breakpoints.ClearTemporaryBreakpoint( frame[ 'source' ][ 'path' ], - frame[ 'line' ] ) - - return True - - def _StartDebugAdapter( self ): - self._splash_screen = utils.DisplaySplash( - self._api_prefix, - self._splash_screen, - f"Starting debug adapter for session {self.DisplayName()}..." ) - - if self._connection: - utils.UserMessage( 'The connection is already created. Please try again', - persist = True ) - return False - - self._logger.info( 'Starting debug adapter with: %s', - json.dumps( self._adapter ) ) - - self._init_complete = False - self._launch_complete = False - self._run_on_server_exit = None - - self._connection_type = 'job' - if 'port' in self._adapter: - self._connection_type = 'channel' - - if self._adapter[ 'port' ] == 'ask': - port = utils.AskForInput( 'Enter port to connect to: ' ) - if port is None: - self._Reset() - return False - self._adapter[ 'port' ] = port - - self._connection_type = self._api_prefix + self._connection_type - self._logger.debug( f"Connection Type: { self._connection_type }" ) - - self._adapter[ 'env' ] = self._adapter.get( 'env', {} ) - - if 'cwd' in self._configuration: - self._adapter[ 'cwd' ] = self._configuration[ 'cwd' ] - elif 'cwd' not in self._adapter: - self._adapter[ 'cwd' ] = os.getcwd() - - vim.vars[ '_vimspector_adapter_spec' ] = self._adapter - - # if the debug adapter is lame and requires a terminal or has any - # input/output on stdio, then launch it that way - if self._adapter.get( 'tty', False ): - if 'port' not in self._adapter: - utils.UserMessage( "Invalid adapter configuration. When using a tty, " - "communication must use socket. Add the 'port' to " - "the adapter config." ) - return False - - if 'command' not in self._adapter: - utils.UserMessage( "Invalid adapter configuration. When using a tty, " - "a command must be supplied. Add the 'command' to " - "the adapter config." ) - return False - - command = self._adapter[ 'command' ] - if isinstance( command, str ): - command = shlex.split( command ) - - self._adapter_term = terminal.LaunchTerminal( - self._api_prefix, - { - 'args': command, - 'cwd': self._adapter[ 'cwd' ], - 'env': self._adapter[ 'env' ], + def _CurrentSteppingGranularity( self ): + if self._disassemblyView and self._disassemblyView.IsCurrent(): + return 'instruction' + + return 'statement' + + @CurrentSession() + def Continue( self ): + if not self._connection: + self.Start() + return + + threadId = self._stackTraceView.GetCurrentThreadId() + if threadId is None: + utils.UserMessage( 'No current thread', persist = True ) + return + + def handler( msg ): + self._stackTraceView.OnContinued( self, { + 'threadId': threadId, + 'allThreadsContinued': ( msg.get( 'body' ) or {} ).get( + 'allThreadsContinued', + True ) + } ) + self.ClearCurrentPC() + + self._connection.DoRequest( handler, { + 'command': 'continue', + 'arguments': { + 'threadId': threadId, }, - self._codeView._window, - self._adapter_term ) - - if not vim.eval( "vimspector#internal#{}#StartDebugSession( " - " {}," - " g:_vimspector_adapter_spec " - ")".format( self._connection_type, - self.session_id ) ): - self._logger.error( "Unable to start debug server" ) - self._splash_screen = utils.DisplaySplash( - self._api_prefix, - self._splash_screen, - [ - "Unable to start or connect to debug adapter", - "", - "Check :messages and :VimspectorToggleLog for more information.", - "", - ":VimspectorReset to close down vimspector", - ] ) - return False - else: - handlers = [ self ] - if 'custom_handler' in self._adapter: - spec = self._adapter[ 'custom_handler' ] - if isinstance( spec, dict ): - module = spec[ 'module' ] - cls = spec[ 'class' ] - else: - module, cls = spec.rsplit( '.', 1 ) + } ) + + @CurrentSession() + @IfConnected() + def Pause( self ): + if self._stackTraceView.GetCurrentThreadId() is None: + utils.UserMessage( 'No current thread', persist = True ) + return + + self._connection.DoRequest( None, { + 'command': 'pause', + 'arguments': { + 'threadId': self._stackTraceView.GetCurrentThreadId(), + }, + } ) + + @IfConnected() + def PauseContinueThread( self ): + self._stackTraceView.PauseContinueThread() + + @CurrentSession() + @IfConnected() + def SetCurrentThread( self ): + self._stackTraceView.SetCurrentThread() + + @CurrentSession() + @IfConnected() + def ExpandVariable( self, buf = None, line_num = None ): + self._variablesView.ExpandVariable( buf, line_num ) + + @CurrentSession() + @IfConnected() + def SetVariableValue( self, new_value = None, buf = None, line_num = None ): + if not self._server_capabilities.get( 'supportsSetVariable' ): + return + self._variablesView.SetVariableValue( new_value, buf, line_num ) + + @ParentOnly() + def ReadMemory( self, length = None, offset = None ): + # We use the parent session because the actual connection is returned from + # the variables view (and might not be our self._connection) at least in + # theory. + if not self._server_capabilities.get( 'supportsReadMemoryRequest' ): + utils.UserMessage( "Server does not support memory request", + error = True ) + return + + connection: debug_adapter_connection.DebugAdapterConnection + connection, memoryReference = self._variablesView.GetMemoryReference() + if memoryReference is None or connection is None: + utils.UserMessage( "Cannot find memory reference for that", + error = True ) + return + + if length is None: + length = utils.AskForInput( 'How much data to display? ', + default_value = '1024' ) try: - CustomHandler = getattr( importlib.import_module( module ), cls ) - handlers = [ CustomHandler( self ), self ] - except ImportError: - self._logger.exception( "Unable to load custom adapter %s", - spec ) - - self._connection = debug_adapter_connection.DebugAdapterConnection( - handlers = handlers, - session_id = self.session_id, - send_func = lambda msg: utils.Call( - "vimspector#internal#{}#Send".format( self._connection_type ), - self.session_id, - msg ), - sync_timeout = self._adapter.get( 'sync_timeout' ), - async_timeout = self._adapter.get( 'async_timeout' ) ) - - self._logger.info( 'Debug Adapter Started' ) - return True - - def _StopDebugAdapter( self, terminateDebuggee, callback ): - arguments = {} - - if terminateDebuggee is not None: - arguments[ 'terminateDebuggee' ] = terminateDebuggee - - self._splash_screen = utils.DisplaySplash( - self._api_prefix, - self._splash_screen, - f"Shutting down debug adapter for session {self.DisplayName()}..." ) - - def handler( *args ): - self._splash_screen = utils.HideSplash( self._api_prefix, - self._splash_screen ) - - if callback: - self._logger.debug( "Setting server exit handler before disconnect" ) - assert not self._run_on_server_exit - self._run_on_server_exit = callback - - vim.eval( 'vimspector#internal#{}#StopDebugSession( {} )'.format( - self._connection_type, - self.session_id ) ) - - self._connection.DoRequest( - handler, - { - 'command': 'disconnect', - 'arguments': arguments, - }, - failure_handler = handler, - timeout = self._connection.sync_timeout ) - - - def _ConfirmTerminateDebugee( self, then ): - def handle_choice( choice ): - terminateDebuggee = None - if choice == 1: - # yes - terminateDebuggee = True - elif choice == 2: - # no - terminateDebuggee = False - elif choice <= 0: - # Abort - return - # Else, use server default - - then( terminateDebuggee ) - - utils.Confirm( self._api_prefix, - "Terminate debuggee?", - handle_choice, - default_value = 3, - options = [ '(Y)es', '(N)o', '(D)efault' ], - keys = [ 'y', 'n', 'd' ] ) - - def _PrepareAttach( self, adapter_config, launch_config ): - attach_config = adapter_config.get( 'attach' ) - - if not attach_config: - return - - if 'remote' in attach_config: - # FIXME: We almost want this to feed-back variables to be expanded later, - # e.g. expand variables when we use them, not all at once. This would - # remove the whole %PID% hack. - remote = attach_config[ 'remote' ] - remote_exec_cmd = self._GetRemoteExecCommand( remote ) - - # FIXME: Why does this not use self._GetCommands ? - pid_cmd = remote_exec_cmd + remote[ 'pidCommand' ] - - self._logger.debug( 'Getting PID: %s', pid_cmd ) - pid = subprocess.check_output( pid_cmd ).decode( 'utf-8' ).strip() - self._logger.debug( 'Got PID: %s', pid ) - - if not pid: - # FIXME: We should raise an exception here or something - utils.UserMessage( 'Unable to get PID', persist = True ) - return - - if 'initCompleteCommand' in remote: - initcmd = remote_exec_cmd + remote[ 'initCompleteCommand' ][ : ] - for index, item in enumerate( initcmd ): - initcmd[ index ] = item.replace( '%PID%', pid ) - - self._on_init_complete_handlers.append( - lambda: subprocess.check_call( initcmd ) ) - - commands = self._GetCommands( remote, 'attach' ) - - for command in commands: - cmd = remote_exec_cmd + command - - for index, item in enumerate( cmd ): - cmd[ index ] = item.replace( '%PID%', pid ) - - self._logger.debug( 'Running remote app: %s', cmd ) - self._remote_term = terminal.LaunchTerminal( - self._api_prefix, - { - 'args': cmd, - 'cwd': os.getcwd() - }, - self._codeView._window, - self._remote_term ) - else: - if attach_config[ 'pidSelect' ] == 'ask': - prop = attach_config[ 'pidProperty' ] - if prop not in launch_config: - # NOTE: We require that any custom picker process handles no-arguments - # as well as any arguments supplied in the config. - pid = _SelectProcess() - if pid is None: + length = int( length ) + except ValueError: return - launch_config[ prop ] = pid - return - elif attach_config[ 'pidSelect' ] == 'none': - return - - raise ValueError( 'Unrecognised pidSelect {0}'.format( - attach_config[ 'pidSelect' ] ) ) - - if 'delay' in attach_config: - utils.UserMessage( f"Waiting ( { attach_config[ 'delay' ] } )..." ) - vim.command( f'sleep { attach_config[ "delay" ] }' ) - - - def _PrepareLaunch( self, command_line, adapter_config, launch_config ): - run_config = adapter_config.get( 'launch', {} ) - - if 'remote' in run_config: - remote = run_config[ 'remote' ] - remote_exec_cmd = self._GetRemoteExecCommand( remote ) - commands = self._GetCommands( remote, 'run' ) - - for index, command in enumerate( commands ): - cmd = remote_exec_cmd + command[ : ] - full_cmd = [] - for item in cmd: - if isinstance( command_line, list ): - if item == '%CMD%': - full_cmd.extend( command_line ) - else: - full_cmd.append( item ) - else: - full_cmd.append( item.replace( '%CMD%', command_line ) ) - - self._logger.debug( 'Running remote app: %s', full_cmd ) - self._remote_term = terminal.LaunchTerminal( - self._api_prefix, - { - 'args': full_cmd, - 'cwd': os.getcwd() - }, - self._codeView._window, - self._remote_term ) - if 'delay' in run_config: - utils.UserMessage( f"Waiting ( {run_config[ 'delay' ]} )..." ) - vim.command( f'sleep { run_config[ "delay" ] }' ) + if offset is None: + offset = utils.AskForInput( 'Location offset? ', + default_value = '0' ) + try: + offset = int( offset ) + except ValueError: + return - def _GetSSHCommand( self, remote ): - ssh_config = remote.get( 'ssh', {} ) - ssh = ssh_config.get( 'cmd', [ 'ssh' ] ) + ssh_config.get( 'args', [] ) + def handler( msg ): + self._codeView.ShowMemory( connection.GetSessionId(), + memoryReference, + length, + offset, + msg ) + + connection.DoRequest( handler, { + 'command': 'readMemory', + 'arguments': { + 'memoryReference': memoryReference, + 'count': int( length ), + 'offset': int( offset ) + } + } ) - if 'account' in remote: - ssh.append( remote[ 'account' ] + '@' + remote[ 'host' ] ) - else: - ssh.append( remote[ 'host' ] ) - return ssh + @CurrentSession() + @IfConnected() + @RequiresUI() + def ShowDisassembly( self ): + if self._disassemblyView and self._disassemblyView.WindowIsValid(): + return - def _GetShellCommand( self ): - return [] + if not self._codeView or not self._codeView._window.valid: + return - def _GetDockerCommand( self, remote ): - docker = [ 'docker', 'exec', '-t' ] - docker.append( remote[ 'container' ] ) - return docker + if not self._stackTraceView: + return - def _GetRemoteExecCommand( self, remote ): - is_ssh_cmd = any( key in remote for key in [ 'ssh', - 'host', - 'account', ] ) - is_docker_cmd = 'container' in remote + if not self._server_capabilities.get( 'supportsDisassembleRequest', False ): + utils.UserMessage( "Sorry, server doesn't support that" ) + return + + with utils.LetCurrentWindow( self._codeView._window ): + vim.command( + f'rightbelow { settings.Int( "disassembly_height" ) }new' ) + self._disassemblyView = disassembly.DisassemblyView( + vim.current.window, + self._api_prefix, + self._render_emitter ) + + self._breakpoints.SetDisassemblyManager( self._disassemblyView ) + + utils.UpdateSessionWindows( { + 'disassembly': utils.WindowID( vim.current.window, self._uiTab ) + } ) + + self._disassemblyView.SetCurrentFrame( + self._connection, + self._stackTraceView.GetCurrentFrame(), + True ) + + + def OnDisassemblyWindowScrolled( self, win_id ): + if self._disassemblyView: + self._disassemblyView.OnWindowScrolled( win_id ) + + + @CurrentSession() + @IfConnected() + def AddWatch( self, expression ): + self._variablesView.AddWatch( self._connection, + self._stackTraceView.GetCurrentFrame(), + expression ) + + @CurrentSession() + @IfConnected() + def EvaluateConsole( self, expression, verbose ): + self._outputView.Evaluate( self._connection, + self._stackTraceView.GetCurrentFrame(), + expression, + verbose ) + + @CurrentSession() + @IfConnected() + def DeleteWatch( self ): + self._variablesView.DeleteWatch() + + + @CurrentSession() + @IfConnected() + def HoverEvalTooltip( self, winnr, bufnr, lnum, expression, is_hover ): + frame = self._stackTraceView.GetCurrentFrame() + # Check if RIP is in a frame + if frame is None: + self._logger.debug( 'Tooltip: Not in a stack frame' ) + return '' + + # Check if cursor in code window + if winnr == int( self._codeView._window.number ): + return self._variablesView.HoverEvalTooltip( self._connection, + frame, + expression, + is_hover ) + + return self._variablesView.HoverVarWinTooltip( bufnr, + lnum, + is_hover ) + # Return variable aware function + + + @CurrentSession() + def CleanUpTooltip( self ): + return self._variablesView.CleanUpTooltip() + + @IfConnected() + def ExpandFrameOrThread( self ): + self._stackTraceView.ExpandFrameOrThread() + + @IfConnected() + def UpFrame( self ): + self._stackTraceView.UpFrame() + + @IfConnected() + def DownFrame( self ): + self._stackTraceView.DownFrame() + + def ToggleLog( self ): + if self.HasUI(): + return self.ShowOutput( 'Vimspector' ) + + if self._logView and self._logView.WindowIsValid(): + self._logView.Reset() + self._logView = None + return + + if self._logView: + self._logView.Reset() + + # TODO: The UI code is too scattered. Re-organise into a UI class that + # just deals with these things like window layout and custmisattion. + vim.command( f'botright { settings.Int( "bottombar_height" ) }new' ) + win = vim.current.window + self._logView = output.OutputView( win, self._api_prefix ) + self._logView.AddLogFileView() + self._logView.ShowOutput( 'Vimspector' ) + + @RequiresUI() + def ShowOutput( self, category ): + if not self._outputView.WindowIsValid(): + # TODO: The UI code is too scattered. Re-organise into a UI class that + # just deals with these things like window layout and custmisattion. + # currently, this class and the CodeView share some responsibility for + # this and poking into each View class to check its window is valid also + # feels wrong. + with utils.LetCurrentTabpage( self._uiTab ): + vim.command( + f'botright { settings.Int( "bottombar_height" ) }new' ) + self._outputView.UseWindow( vim.current.window ) + utils.UpdateSessionWindows( { + 'output': utils.WindowID( vim.current.window, self._uiTab ) + } ) + + self._outputView.ShowOutput( category ) + + @RequiresUI( otherwise=[] ) + def GetOutputBuffers( self ): + return self._outputView.GetCategories() + + @CurrentSession() + @IfConnected( otherwise=[] ) + def GetCompletionsSync( self, text_line, column_in_bytes ): + if not self._server_capabilities.get( 'supportsCompletionsRequest' ): + return [] + + response = self._connection.DoRequestSync( { + 'command': 'completions', + 'arguments': { + 'frameId': self._stackTraceView.GetCurrentFrame()[ 'id' ], + # TODO: encoding ? bytes/codepoints + 'text': text_line, + 'column': column_in_bytes + } + } ) + # TODO: + # - start / length + # - sortText + return response[ 'body' ][ 'targets' ] + + + @CurrentSession() + @IfConnected( otherwise=[] ) + def GetCommandLineCompletions( self, ArgLead, prev_non_keyword_char ): + items = [] + for candidate in self.GetCompletionsSync( ArgLead, prev_non_keyword_char ): + label = candidate.get( 'text', candidate[ 'label' ] ) + start = prev_non_keyword_char - 1 + if 'start' in candidate and 'length' in candidate: + start = candidate[ 'start' ] + items.append( ArgLead[ 0 : start ] + label ) + + return items + + + @ParentOnly() + def RefreshSigns( self ): + if self._connection: + self._codeView.Refresh() + self._breakpoints.Refresh() + + + @ParentOnly() + def _SetUpUI( self ): + vim.command( '$tab split' ) + + # Switch to this session now that we've made it visible. Note that the + # TabEnter autocmd does trigger when the above is run, but that's before the + # following line assigns the tab to this session, so when we try to find + # this session by tab number, it's not found. So we have to manually switch + # to it when creating a new tab. + utils.Call( 'vimspector#internal#state#SwitchToSession', + self.session_id ) + + self._uiTab = vim.current.tabpage + + mode = settings.Get( 'ui_mode' ) + + if mode == 'auto': + # Go vertical if there isn't enough horizontal space for at least: + # the left bar width + # + the code min width + # + the terminal min width + # + enough space for a sign column and number column? + min_width = ( settings.Int( 'sidebar_width' ) + + 1 + 2 + 3 + + settings.Int( 'code_minwidth' ) + + 1 + settings.Int( 'terminal_minwidth' ) ) + + min_height = ( settings.Int( 'code_minheight' ) + 1 + + settings.Int( 'topbar_height' ) + 1 + + settings.Int( 'bottombar_height' ) + 1 + + 2 ) + + mode = ( 'vertical' + if vim.options[ 'columns' ] < min_width + else 'horizontal' ) + + if vim.options[ 'lines' ] < min_height: + mode = 'horizontal' + + self._logger.debug( 'min_width/height: %s/%s, actual: %s/%s - result: %s', + min_width, + min_height, + vim.options[ 'columns' ], + vim.options[ 'lines' ], + mode ) + + if mode == 'vertical': + self._SetUpUIVertical() + else: + self._SetUpUIHorizontal() + + + def _SetUpUIHorizontal( self ): + # Code window + code_window = vim.current.window + self._codeView = code.CodeView( self.session_id, + code_window, + self._api_prefix, + self._render_emitter, + self._breakpoints.IsBreakpointPresentAt ) + + # Call stack + vim.command( + f'topleft vertical { settings.Int( "sidebar_width" ) }new' ) + stack_trace_window = vim.current.window + one_third = int( vim.eval( 'winheight( 0 )' ) ) / 3 + self._stackTraceView = stack_trace.StackTraceView( self.session_id, + stack_trace_window ) + + # Watches + vim.command( 'leftabove new' ) + watch_window = vim.current.window + + # Variables + vim.command( 'leftabove new' ) + vars_window = vim.current.window + + with utils.LetCurrentWindow( vars_window ): + vim.command( f'{ one_third }wincmd _' ) + with utils.LetCurrentWindow( watch_window ): + vim.command( f'{ one_third }wincmd _' ) + with utils.LetCurrentWindow( stack_trace_window ): + vim.command( f'{ one_third }wincmd _' ) + + self._variablesView = variables.VariablesView( self.session_id, + vars_window, + watch_window ) + + # Output/logging + vim.current.window = code_window + vim.command( f'rightbelow { settings.Int( "bottombar_height" ) }new' ) + output_window = vim.current.window + self._outputView = output.DAPOutputView( output_window, + self._api_prefix, + session_id = self.session_id ) + + utils.SetSessionWindows( { + 'mode': 'horizontal', + 'tabpage': self._uiTab.number, + 'code': utils.WindowID( code_window, self._uiTab ), + 'stack_trace': utils.WindowID( stack_trace_window, self._uiTab ), + 'variables': utils.WindowID( vars_window, self._uiTab ), + 'watches': utils.WindowID( watch_window, self._uiTab ), + 'output': utils.WindowID( output_window, self._uiTab ), + 'eval': None, # updated every time eval popup is opened + 'breakpoints': vim.vars[ 'vimspector_session_windows' ].get( + 'breakpoints' ) # same as above, but for breakpoints + } ) + with utils.RestoreCursorPosition(): + with utils.RestoreCurrentWindow(): + with utils.RestoreCurrentBuffer( vim.current.window ): + vim.command( 'doautocmd User VimspectorUICreated' ) + + + def _SetUpUIVertical( self ): + # Code window + code_window = vim.current.window + self._codeView = code.CodeView( self.session_id, + code_window, + self._api_prefix, + self._render_emitter, + self._breakpoints.IsBreakpointPresentAt ) + + # Call stack + vim.command( + f'topleft { settings.Int( "topbar_height" ) }new' ) + stack_trace_window = vim.current.window + one_third = int( vim.eval( 'winwidth( 0 )' ) ) / 3 + self._stackTraceView = stack_trace.StackTraceView( self.session_id, + stack_trace_window ) + + + # Watches + vim.command( 'leftabove vertical new' ) + watch_window = vim.current.window + + # Variables + vim.command( 'leftabove vertical new' ) + vars_window = vim.current.window + + + with utils.LetCurrentWindow( vars_window ): + vim.command( f'{ one_third }wincmd |' ) + with utils.LetCurrentWindow( watch_window ): + vim.command( f'{ one_third }wincmd |' ) + with utils.LetCurrentWindow( stack_trace_window ): + vim.command( f'{ one_third }wincmd |' ) + + self._variablesView = variables.VariablesView( self.session_id, + vars_window, + watch_window ) + + + # Output/logging + vim.current.window = code_window + vim.command( f'rightbelow { settings.Int( "bottombar_height" ) }new' ) + output_window = vim.current.window + self._outputView = output.DAPOutputView( output_window, + self._api_prefix, + session_id = self.session_id ) + + utils.SetSessionWindows( { + 'mode': 'vertical', + 'tabpage': self._uiTab.number, + 'code': utils.WindowID( code_window, self._uiTab ), + 'stack_trace': utils.WindowID( stack_trace_window, self._uiTab ), + 'variables': utils.WindowID( vars_window, self._uiTab ), + 'watches': utils.WindowID( watch_window, self._uiTab ), + 'output': utils.WindowID( output_window, self._uiTab ), + 'eval': None, # updated every time eval popup is opened + 'breakpoints': vim.vars[ 'vimspector_session_windows' ].get( + 'breakpoints' ) # same as above, but for breakpoints + } ) + with utils.RestoreCursorPosition(): + with utils.RestoreCurrentWindow(): + with utils.RestoreCurrentBuffer( vim.current.window ): + vim.command( 'doautocmd User VimspectorUICreated' ) + + + @RequiresUI() + def ClearCurrentFrame( self ): + self.SetCurrentFrame( None ) + + + def ClearCurrentPC( self ): + self._codeView.SetCurrentFrame( None, False ) + if self._disassemblyView: + self._disassemblyView.SetCurrentFrame( None, None, False ) + + + @RequiresUI() + def SetCurrentFrame( self, frame, reason = '' ): + if not frame: + self._variablesView.Clear() + + target = self._codeView + if self._disassemblyView and self._disassemblyView.IsCurrent(): + target = self._disassemblyView + + if not self._codeView.SetCurrentFrame( frame, + target == self._codeView ): + return False + + if self._disassemblyView: + self._disassemblyView.SetCurrentFrame( self._connection, + frame, + target == self._disassemblyView ) + + # the codeView.SetCurrentFrame already checked the frame was valid and + # countained a valid source + assert frame + if self._codeView.current_syntax not in ( 'ON', 'OFF' ): + self._variablesView.SetSyntax( self._codeView.current_syntax ) + self._stackTraceView.SetSyntax( self._codeView.current_syntax ) + else: + self._variablesView.SetSyntax( None ) + self._stackTraceView.SetSyntax( None ) + + self._variablesView.LoadScopes( self._connection, frame ) + self._variablesView.EvaluateWatches( self._connection, frame ) + + if reason == 'stopped': + self._breakpoints.ClearTemporaryBreakpoint( frame[ 'source' ][ 'path' ], + frame[ 'line' ] ) + + return True + + def _StartDebugAdapter( self ): + self._splash_screen = utils.DisplaySplash( + self._api_prefix, + self._splash_screen, + f"Starting debug adapter for session {self.DisplayName()}..." ) + + if self._connection: + utils.UserMessage( 'The connection is already created. Please try again', + persist = True ) + return False + + self._logger.info( 'Starting debug adapter with: %s', + json.dumps( self._adapter ) ) + + self._init_complete = False + self._launch_complete = False + self._run_on_server_exit = None + + self._connection_type = 'job' + if 'port' in self._adapter: + self._connection_type = 'channel' + + if self._adapter[ 'port' ] == 'ask': + port = utils.AskForInput( 'Enter port to connect to: ' ) + if port is None: + self._Reset() + return False + self._adapter[ 'port' ] = port + + self._connection_type = self._api_prefix + self._connection_type + self._logger.debug( f"Connection Type: { self._connection_type }" ) + + self._adapter[ 'env' ] = self._adapter.get( 'env', {} ) + + if 'cwd' in self._configuration: + self._adapter[ 'cwd' ] = self._configuration[ 'cwd' ] + elif 'cwd' not in self._adapter: + self._adapter[ 'cwd' ] = os.getcwd() + + vim.vars[ '_vimspector_adapter_spec' ] = self._adapter + + # if the debug adapter is lame and requires a terminal or has any + # input/output on stdio, then launch it that way + if self._adapter.get( 'tty', False ): + if 'port' not in self._adapter: + utils.UserMessage( "Invalid adapter configuration. When using a tty, " + "communication must use socket. Add the 'port' to " + "the adapter config." ) + return False + + if 'command' not in self._adapter: + utils.UserMessage( "Invalid adapter configuration. When using a tty, " + "a command must be supplied. Add the 'command' to " + "the adapter config." ) + return False + + command = self._adapter[ 'command' ] + if isinstance( command, str ): + command = shlex.split( command ) + + self._adapter_term = terminal.LaunchTerminal( + self._api_prefix, + { + 'args': command, + 'cwd': self._adapter[ 'cwd' ], + 'env': self._adapter[ 'env' ], + }, + self._codeView._window, + self._adapter_term ) + + if not vim.eval( "vimspector#internal#{}#StartDebugSession( " + " {}," + " g:_vimspector_adapter_spec " + ")".format( self._connection_type, + self.session_id ) ): + self._logger.error( "Unable to start debug server" ) + self._splash_screen = utils.DisplaySplash( + self._api_prefix, + self._splash_screen, + [ + "Unable to start or connect to debug adapter", + "", + "Check :messages and :VimspectorToggleLog for more information.", + "", + ":VimspectorReset to close down vimspector", + ] ) + return False + else: + handlers = [ self ] + if 'custom_handler' in self._adapter: + spec = self._adapter[ 'custom_handler' ] + if isinstance( spec, dict ): + module = spec[ 'module' ] + cls = spec[ 'class' ] + else: + module, cls = spec.rsplit( '.', 1 ) + + try: + CustomHandler = getattr( + importlib.import_module( module ), cls ) + handlers = [ CustomHandler( self ), self ] + except ImportError: + self._logger.exception( "Unable to load custom adapter %s", + spec ) + + self._connection = debug_adapter_connection.DebugAdapterConnection( + handlers = handlers, + session_id = self.session_id, + send_func = lambda msg: utils.Call( + "vimspector#internal#{}#Send".format( self._connection_type ), + self.session_id, + msg ), + sync_timeout = self._adapter.get( 'sync_timeout' ), + async_timeout = self._adapter.get( 'async_timeout' ) ) + + self._logger.info( 'Debug Adapter Started' ) + return True + + def _StopDebugAdapter( self, terminateDebuggee, callback ): + arguments = {} + + if terminateDebuggee is not None: + arguments[ 'terminateDebuggee' ] = terminateDebuggee + + self._splash_screen = utils.DisplaySplash( + self._api_prefix, + self._splash_screen, + f"Shutting down debug adapter for session {self.DisplayName()}..." ) + + def handler( *args ): + self._splash_screen = utils.HideSplash( self._api_prefix, + self._splash_screen ) + + if callback: + self._logger.debug( + "Setting server exit handler before disconnect" ) + assert not self._run_on_server_exit + self._run_on_server_exit = callback + + vim.eval( 'vimspector#internal#{}#StopDebugSession( {} )'.format( + self._connection_type, + self.session_id ) ) - if is_ssh_cmd: - return self._GetSSHCommand( remote ) - elif is_docker_cmd: - return self._GetDockerCommand( remote ) - else: - # if it's neither docker nor ssh, run locally - return self._GetShellCommand() - - - def _GetCommands( self, remote, pfx ): - commands = remote.get( pfx + 'Commands', None ) - - if isinstance( commands, list ): - return commands - elif commands is not None: - raise ValueError( "Invalid commands; must be list" ) - - command = remote[ pfx + 'Command' ] - - if isinstance( command, str ): - command = shlex.split( command ) - - if not isinstance( command, list ): - raise ValueError( "Invalid command; must be list/string" ) - - if not command: - raise ValueError( 'Could not determine commands for ' + pfx ) - - return [ command ] - - def _Initialise( self ): - self._splash_screen = utils.DisplaySplash( - self._api_prefix, - self._splash_screen, - f"Initializing debug session {self.DisplayName()}..." ) - - # For a good explanation as to why this sequence is the way it is, see - # https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522 - # - # In short, we do what VSCode does: - # 1. Send the initialize request and wait for the reply - # 2a. When we receive the initialize reply, send the launch/attach request - # 2b. When we receive the initialized notification, send the breakpoints - # - if supportsConfigurationDoneRequest, send it - # - else, send the empty exception breakpoints request - # 3. When we have received both the receive the launch/attach reply *and* - # the connfiguration done reply (or, if we didn't send one, a response to - # the empty exception breakpoints request), we request threads - # 4. The threads response triggers things like scopes and triggers setting - # the current frame. - # - def handle_initialize_response( msg ): - self._server_capabilities = msg.get( 'body' ) or {} - # TODO/FIXME: We assume that the capabilities are the same for all - # connections. We should fix this when we split the server bp - # representation out? - if not self.parent_session: - self._breakpoints.SetServerCapabilities( self._server_capabilities ) - self._Launch() - - self._connection.DoRequest( handle_initialize_response, { - 'command': 'initialize', - 'arguments': { - 'adapterID': self._adapter.get( 'name', 'adapter' ), - 'clientID': 'vimspector', - 'clientName': 'vimspector', - 'linesStartAt1': True, - 'columnsStartAt1': True, - 'locale': 'en_GB', - 'pathFormat': 'path', - 'supportsVariableType': True, - 'supportsVariablePaging': False, - 'supportsRunInTerminalRequest': True, - 'supportsMemoryReferences': True, - 'supportsStartDebuggingRequest': True - }, - } ) - - - def OnFailure( self, reason, request, message ): - msg = "Request for '{}' failed: {}\nResponse: {}".format( request, - reason, - message ) - self._outputView.Print( 'server', msg ) - - - def _Prepare( self ): - self._on_init_complete_handlers = [] - - self._logger.debug( "LAUNCH!" ) - if self._launch_config is None: - self._launch_config = {} - # TODO: Should we use core_utils.override for this? That would strictly be - # a change in behaviour as dicts in the specific configuration would merge - # with dicts in the adapter, where before they would overlay - self._launch_config.update( self._adapter.get( 'configuration', {} ) ) - self._launch_config.update( self._configuration[ 'configuration' ] ) - - request = self._configuration.get( - 'remote-request', - self._launch_config.get( 'request', 'launch' ) ) - - if request == "attach": - self._splash_screen = utils.DisplaySplash( - self._api_prefix, - self._splash_screen, - f"Attaching to debuggee {self.DisplayName()}..." ) - - self._PrepareAttach( self._adapter, self._launch_config ) - elif request == "launch": - self._splash_screen = utils.DisplaySplash( - self._api_prefix, - self._splash_screen, - f"Launching debuggee {self.DisplayName()}..." ) - - # FIXME: This cmdLine hack is not fun. - self._PrepareLaunch( self._configuration.get( 'remote-cmdLine', [] ), - self._adapter, - self._launch_config ) - - # FIXME: name is mandatory. Forcefully add it (we should really use the - # _actual_ name, but that isn't actually remembered at this point) - if 'name' not in self._launch_config: - self._launch_config[ 'name' ] = 'test' - - - def _Launch( self ): - def failure_handler( reason, msg ): - text = [ - f'Initialize for session {self.DisplayName()} Failed', - '' ] + reason.splitlines() + [ - '', 'Use :VimspectorReset to close' ] - self._logger.info( "Launch failed: %s", '\n'.join( text ) ) - self._splash_screen = utils.DisplaySplash( self._api_prefix, - self._splash_screen, - text ) - - self._connection.DoRequest( - lambda msg: self._OnLaunchComplete(), - { - 'command': self._launch_config[ 'request' ], - 'arguments': self._launch_config - }, - failure_handler ) - - - def _OnLaunchComplete( self ): - self._launch_complete = True - self._LoadThreadsIfReady() - - def _OnInitializeComplete( self ): - self._init_complete = True - self._LoadThreadsIfReady() - - def _LoadThreadsIfReady( self ): - # NOTE: You might think we should only load threads on a stopped event, - # but the spec is clear: - # - # After a successful launch or attach the development tool requests the - # baseline of currently existing threads with the threads request and - # then starts to listen for thread events to detect new or terminated - # threads. - # - # Of course, specs are basically guidelines. MS's own cpptools simply - # doesn't respond top threads request when attaching via gdbserver. At - # least it would appear that way. - # - # As it turns out this is due to a bug in gdbserver which means that - # attachment doesn't work due to sending the signal to the process group - # leader rather than the process. The workaround is to manually SIGTRAP the - # PID. - # - if self._launch_complete and self._init_complete: - self._splash_screen = utils.HideSplash( self._api_prefix, - self._splash_screen ) - - for h in self._on_init_complete_handlers: - h() - self._on_init_complete_handlers = [] - - self._stackTraceView.LoadThreads( self, True ) - - - @CurrentSession() - @IfConnected() - @RequiresUI() - def PrintDebugInfo( self ): - def Line(): - return ( "--------------------------------------------------------------" - "------------------" ) - - def Pretty( obj ): - if obj is None: - return [ "None" ] - return [ Line() ] + json.dumps( obj, indent=2 ).splitlines() + [ Line() ] - - - debugInfo = [ - "Vimspector Debug Info", - Line(), - f"ConnectionType: { self._connection_type }", - "Adapter: " ] + Pretty( self._adapter ) + [ - "Configuration: " ] + Pretty( self._configuration ) + [ - f"API Prefix: { self._api_prefix }", - f"Launch/Init: { self._launch_complete } / { self._init_complete }", - f"Workspace Root: { self._workspace_root }", - "Launch Config: " ] + Pretty( self._launch_config ) + [ - "Server Capabilities: " ] + Pretty( self._server_capabilities ) + [ - "Line Breakpoints: " ] + Pretty( self._breakpoints._line_breakpoints ) + [ - "Func Breakpoints: " ] + Pretty( self._breakpoints._func_breakpoints ) + [ - "Ex Breakpoints: " ] + Pretty( self._breakpoints._exception_breakpoints ) - - self._outputView.ClearCategory( 'DebugInfo' ) - self._outputView.Print( "DebugInfo", debugInfo ) - self.ShowOutput( "DebugInfo" ) - - - def OnEvent_loadedSource( self, msg ): - pass - - - def OnEvent_capabilities( self, msg ): - self._server_capabilities.update( - ( msg.get( 'body' ) or {} ).get( 'capabilities' ) or {} ) - - - def OnEvent_initialized( self, message ): - def OnBreakpointsDone(): - self._breakpoints.Refresh() - if self._server_capabilities.get( 'supportsConfigurationDoneRequest' ): self._connection.DoRequest( - lambda msg: self._OnInitializeComplete(), + handler, { - 'command': 'configurationDone', - } - ) - else: - self._OnInitializeComplete() + 'command': 'disconnect', + 'arguments': arguments, + }, + failure_handler = handler, + timeout = self._connection.sync_timeout ) + + + def _ConfirmTerminateDebugee( self, then ): + def handle_choice( choice ): + terminateDebuggee = None + if choice == 1: + # yes + terminateDebuggee = True + elif choice == 2: + # no + terminateDebuggee = False + elif choice <= 0: + # Abort + return + # Else, use server default + + then( terminateDebuggee ) + + utils.Confirm( self._api_prefix, + "Terminate debuggee?", + handle_choice, + default_value = 3, + options = [ '(Y)es', '(N)o', '(D)efault' ], + keys = [ 'y', 'n', 'd' ] ) + + def _PrepareAttach( self, adapter_config, launch_config ): + attach_config = adapter_config.get( 'attach' ) + + if not attach_config: + return - self._breakpoints.SetConfiguredBreakpoints( - self._configuration.get( 'breakpoints', {} ) ) - self._breakpoints.AddConnection( self._connection ) - self._breakpoints.UpdateUI( OnBreakpointsDone ) + if 'remote' in attach_config: + # FIXME: We almost want this to feed-back variables to be expanded later, + # e.g. expand variables when we use them, not all at once. This would + # remove the whole %PID% hack. + remote = attach_config[ 'remote' ] + remote_exec_cmd = self._GetRemoteExecCommand( remote ) + + # FIXME: Why does this not use self._GetCommands ? + pid_cmd = remote_exec_cmd + remote[ 'pidCommand' ] + + self._logger.debug( 'Getting PID: %s', pid_cmd ) + pid = subprocess.check_output( pid_cmd ).decode( 'utf-8' ).strip() + self._logger.debug( 'Got PID: %s', pid ) + + if not pid: + # FIXME: We should raise an exception here or something + utils.UserMessage( 'Unable to get PID', persist = True ) + return + + if 'initCompleteCommand' in remote: + initcmd = remote_exec_cmd + remote[ 'initCompleteCommand' ][ : ] + for index, item in enumerate( initcmd ): + initcmd[ index ] = item.replace( '%PID%', pid ) + + self._on_init_complete_handlers.append( + lambda: subprocess.check_call( initcmd ) ) + + commands = self._GetCommands( remote, 'attach' ) + + for command in commands: + cmd = remote_exec_cmd + command + + for index, item in enumerate( cmd ): + cmd[ index ] = item.replace( '%PID%', pid ) + + self._logger.debug( 'Running remote app: %s', cmd ) + self._remote_term = terminal.LaunchTerminal( + self._api_prefix, + { + 'args': cmd, + 'cwd': os.getcwd() + }, + self._codeView._window, + self._remote_term ) + else: + if attach_config[ 'pidSelect' ] == 'ask': + prop = attach_config[ 'pidProperty' ] + if prop not in launch_config: + # NOTE: We require that any custom picker process handles no-arguments + # as well as any arguments supplied in the config. + pid = _SelectProcess() + if pid is None: + return + launch_config[ prop ] = pid + return + elif attach_config[ 'pidSelect' ] == 'none': + return + + raise ValueError( 'Unrecognised pidSelect {0}'.format( + attach_config[ 'pidSelect' ] ) ) + + if 'delay' in attach_config: + utils.UserMessage( f"Waiting ( { attach_config[ 'delay' ] } )..." ) + vim.command( f'sleep { attach_config[ "delay" ] }' ) + + + def _PrepareLaunch( self, command_line, adapter_config, launch_config ): + run_config = adapter_config.get( 'launch', {} ) + + if 'remote' in run_config: + remote = run_config[ 'remote' ] + remote_exec_cmd = self._GetRemoteExecCommand( remote ) + commands = self._GetCommands( remote, 'run' ) + + for index, command in enumerate( commands ): + cmd = remote_exec_cmd + command[ : ] + full_cmd = [] + for item in cmd: + if isinstance( command_line, list ): + if item == '%CMD%': + full_cmd.extend( command_line ) + else: + full_cmd.append( item ) + else: + full_cmd.append( item.replace( '%CMD%', command_line ) ) + + self._logger.debug( 'Running remote app: %s', full_cmd ) + self._remote_term = terminal.LaunchTerminal( + self._api_prefix, + { + 'args': full_cmd, + 'cwd': os.getcwd() + }, + self._codeView._window, + self._remote_term ) + + if 'delay' in run_config: + utils.UserMessage( f"Waiting ( {run_config[ 'delay' ]} )..." ) + vim.command( f'sleep { run_config[ "delay" ] }' ) + + + + def _GetSSHCommand( self, remote ): + ssh_config = remote.get( 'ssh', {} ) + ssh = ssh_config.get( 'cmd', [ 'ssh' ] ) + ssh_config.get( 'args', [] ) + + if 'account' in remote: + ssh.append( remote[ 'account' ] + '@' + remote[ 'host' ] ) + else: + ssh.append( remote[ 'host' ] ) + + return ssh + + def _GetShellCommand( self ): + return [] + + def _GetDockerCommand( self, remote ): + docker = [ 'docker', 'exec', '-t' ] + if 'workdir' in remote: + docker.extend(["-w", remote['workdir']]) + docker.append( remote[ 'container' ] ) + return docker + + def _GetRemoteExecCommand( self, remote ): + is_ssh_cmd = any( key in remote for key in [ 'ssh', + 'host', + 'account', ] ) + is_docker_cmd = 'container' in remote + + if is_ssh_cmd: + return self._GetSSHCommand( remote ) + elif is_docker_cmd: + return self._GetDockerCommand( remote ) + else: + # if it's neither docker nor ssh, run locally + return self._GetShellCommand() - def OnEvent_thread( self, message ): - self._stackTraceView.OnThreadEvent( self, message[ 'body' ] ) + def _GetCommands( self, remote, pfx ): + commands = remote.get( pfx + 'Commands', None ) + if isinstance( commands, list ): + return commands + elif commands is not None: + raise ValueError( "Invalid commands; must be list" ) - def OnEvent_breakpoint( self, message ): - reason = message[ 'body' ][ 'reason' ] - bp = message[ 'body' ][ 'breakpoint' ] - if reason == 'changed': - self._breakpoints.UpdatePostedBreakpoint( self._connection, bp ) - elif reason == 'new': - self._breakpoints.AddPostedBreakpoint( self._connection, bp ) - elif reason == 'removed': - self._breakpoints.DeletePostedBreakpoint( self._connection, bp ) - else: - utils.UserMessage( - 'Unrecognised breakpoint event (undocumented): {0}'.format( reason ), - persist = True ) - - def OnRequest_runInTerminal( self, message ): - params = message[ 'arguments' ] - - if not params.get( 'cwd' ) : - params[ 'cwd' ] = self._workspace_root - self._logger.debug( 'Defaulting working directory to %s', - params[ 'cwd' ] ) - - term_id = self._codeView.LaunchTerminal( params ) - - response = { - 'processId': int( utils.Call( - 'vimspector#internal#{}term#GetPID'.format( self._api_prefix ), - term_id ) ) - } - - self._connection.DoResponse( message, None, response ) - - def OnEvent_terminated( self, message ): - # The debugging _session_ has terminated. This does not mean that the - # debuggee has terminated (that's the exited event). - # - # We will handle this when the server actually exists. - # - # FIXME we should always wait for this event before disconnecting closing - # any socket connection - # self._stackTraceView.OnTerminated( self ) - self.SetCurrentFrame( None ) - - - def OnEvent_exited( self, message ): - utils.UserMessage( 'The debuggee exited with status code: {}'.format( - message[ 'body' ][ 'exitCode' ] ) ) - self._stackTraceView.OnExited( self, message ) - self.ClearCurrentPC() - - - def OnRequest_startDebugging( self, message ): - self._DoStartDebuggingRequest( message, - message[ 'arguments' ][ 'request' ], - message[ 'arguments' ][ 'configuration' ], - self._adapter ) - - def _DoStartDebuggingRequest( self, - message, - request_type, - launch_arguments, - adapter, - session_name = None ): - - session = self.manager.NewSession( - session_name = session_name or launch_arguments.get( 'name' ), - parent_session = self ) - - # Inject the launch config (HACK!). This will actually mean that the - # configuration passed below is ignored. - session._launch_config = launch_arguments - session._launch_config[ 'request' ] = request_type - - # FIXME: We probably do need to add a StartWithLauncArguments and somehow - # tell the new session that it shoud not support "Restart" requests ? - # - # In fact, what even would Reset do... ? - session._StartWithConfiguration( { 'configuration': launch_arguments }, - adapter ) - - self._connection.DoResponse( message, None, {} ) - - def OnEvent_process( self, message ): - utils.UserMessage( 'debuggee was started: {}'.format( - message[ 'body' ][ 'name' ] ) ) - - def OnEvent_module( self, message ): - pass - - def OnEvent_continued( self, message ): - self._stackTraceView.OnContinued( self, message[ 'body' ] ) - self.ClearCurrentPC() - - @ParentOnly() - def Clear( self ): - self._codeView.Clear() - if self._disassemblyView: - self._disassemblyView.Clear() - self._stackTraceView.Clear() - self._variablesView.Clear() - - def OnServerExit( self, status ): - self._logger.info( "The server has terminated with status %s", - status ) - - if self._connection is not None: - # Can be None if the server dies _before_ StartDebugSession vim function - # returns - self._connection.Reset() - - self._stackTraceView.ConnectionClosed( self ) - self._breakpoints.ConnectionClosed( self._connection ) - self._variablesView.ConnectionClosed( self._connection ) - if self._disassemblyView: - self._disassemblyView.ConnectionClosed( self._connection ) - - self.Clear() - self._ResetServerState() - - if self._run_on_server_exit: - self._logger.debug( "Running server exit handler" ) - callback = self._run_on_server_exit - self._run_on_server_exit = None - callback() - else: - self._logger.debug( "No server exit handler" ) + command = remote[ pfx + 'Command' ] - def OnEvent_output( self, message ): - if self._outputView: - self._outputView.OnOutput( message[ 'body' ] ) + if isinstance( command, str ): + command = shlex.split( command ) - def OnEvent_stopped( self, message ): - event = message[ 'body' ] - reason = event.get( 'reason' ) or '' - description = event.get( 'description' ) - text = event.get( 'text' ) + if not isinstance( command, list ): + raise ValueError( "Invalid command; must be list/string" ) - if description: - explanation = description + '(' + reason + ')' - else: - explanation = reason + if not command: + raise ValueError( 'Could not determine commands for ' + pfx ) + + return [ command ] + + def _Initialise( self ): + self._splash_screen = utils.DisplaySplash( + self._api_prefix, + self._splash_screen, + f"Initializing debug session {self.DisplayName()}..." ) + + # For a good explanation as to why this sequence is the way it is, see + # https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522 + # + # In short, we do what VSCode does: + # 1. Send the initialize request and wait for the reply + # 2a. When we receive the initialize reply, send the launch/attach request + # 2b. When we receive the initialized notification, send the breakpoints + # - if supportsConfigurationDoneRequest, send it + # - else, send the empty exception breakpoints request + # 3. When we have received both the receive the launch/attach reply *and* + # the connfiguration done reply (or, if we didn't send one, a response to + # the empty exception breakpoints request), we request threads + # 4. The threads response triggers things like scopes and triggers setting + # the current frame. + # + def handle_initialize_response( msg ): + self._server_capabilities = msg.get( 'body' ) or {} + # TODO/FIXME: We assume that the capabilities are the same for all + # connections. We should fix this when we split the server bp + # representation out? + if not self.parent_session: + self._breakpoints.SetServerCapabilities( + self._server_capabilities ) + self._Launch() + + self._connection.DoRequest( handle_initialize_response, { + 'command': 'initialize', + 'arguments': { + 'adapterID': self._adapter.get( 'name', 'adapter' ), + 'clientID': 'vimspector', + 'clientName': 'vimspector', + 'linesStartAt1': True, + 'columnsStartAt1': True, + 'locale': 'en_GB', + 'pathFormat': 'path', + 'supportsVariableType': True, + 'supportsVariablePaging': False, + 'supportsRunInTerminalRequest': True, + 'supportsMemoryReferences': True, + 'supportsStartDebuggingRequest': True + }, + } ) - if text: - explanation += ': ' + text - msg = 'Paused in thread {0} due to {1}'.format( - event.get( 'threadId', '' ), - explanation ) - utils.UserMessage( msg ) + def OnFailure( self, reason, request, message ): + msg = "Request for '{}' failed: {}\nResponse: {}".format( request, + reason, + message ) + self._outputView.Print( 'server', msg ) - if self._outputView: - self._outputView.Print( 'server', msg ) - self._stackTraceView.OnStopped( self, event ) + def _Prepare( self ): + self._on_init_complete_handlers = [] - def BreakpointsAsQuickFix( self ): - return self._breakpoints.BreakpointsAsQuickFix() + self._logger.debug( "LAUNCH!" ) + if self._launch_config is None: + self._launch_config = {} + # TODO: Should we use core_utils.override for this? That would strictly be + # a change in behaviour as dicts in the specific configuration would merge + # with dicts in the adapter, where before they would overlay + self._launch_config.update( + self._adapter.get( 'configuration', {} ) ) + self._launch_config.update( self._configuration[ 'configuration' ] ) - def ListBreakpoints( self ): - self._breakpoints.ToggleBreakpointsView() + request = self._configuration.get( + 'remote-request', + self._launch_config.get( 'request', 'launch' ) ) - def ToggleBreakpointViewBreakpoint( self ): - self._breakpoints.ToggleBreakpointViewBreakpoint() + if request == "attach": + self._splash_screen = utils.DisplaySplash( + self._api_prefix, + self._splash_screen, + f"Attaching to debuggee {self.DisplayName()}..." ) - def ToggleAllBreakpointsViewBreakpoint( self ): - self._breakpoints.ToggleAllBreakpointsViewBreakpoint() + self._PrepareAttach( self._adapter, self._launch_config ) + elif request == "launch": + self._splash_screen = utils.DisplaySplash( + self._api_prefix, + self._splash_screen, + f"Launching debuggee {self.DisplayName()}..." ) + + # FIXME: This cmdLine hack is not fun. + self._PrepareLaunch( self._configuration.get( 'remote-cmdLine', [] ), + self._adapter, + self._launch_config ) + + # FIXME: name is mandatory. Forcefully add it (we should really use the + # _actual_ name, but that isn't actually remembered at this point) + if 'name' not in self._launch_config: + self._launch_config[ 'name' ] = 'test' + + + def _Launch( self ): + def failure_handler( reason, msg ): + text = [ + f'Initialize for session {self.DisplayName()} Failed', + '' ] + reason.splitlines() + [ + '', 'Use :VimspectorReset to close' ] + self._logger.info( "Launch failed: %s", '\n'.join( text ) ) + self._splash_screen = utils.DisplaySplash( self._api_prefix, + self._splash_screen, + text ) - def DeleteBreakpointViewBreakpoint( self ): - self._breakpoints.ClearBreakpointViewBreakpoint() + self._connection.DoRequest( + lambda msg: self._OnLaunchComplete(), + { + 'command': self._launch_config[ 'request' ], + 'arguments': self._launch_config + }, + failure_handler ) + + + def _OnLaunchComplete( self ): + self._launch_complete = True + self._LoadThreadsIfReady() + + def _OnInitializeComplete( self ): + self._init_complete = True + self._LoadThreadsIfReady() + + def _LoadThreadsIfReady( self ): + # NOTE: You might think we should only load threads on a stopped event, + # but the spec is clear: + # + # After a successful launch or attach the development tool requests the + # baseline of currently existing threads with the threads request and + # then starts to listen for thread events to detect new or terminated + # threads. + # + # Of course, specs are basically guidelines. MS's own cpptools simply + # doesn't respond top threads request when attaching via gdbserver. At + # least it would appear that way. + # + # As it turns out this is due to a bug in gdbserver which means that + # attachment doesn't work due to sending the signal to the process group + # leader rather than the process. The workaround is to manually SIGTRAP the + # PID. + # + if self._launch_complete and self._init_complete: + self._splash_screen = utils.HideSplash( self._api_prefix, + self._splash_screen ) + + for h in self._on_init_complete_handlers: + h() + self._on_init_complete_handlers = [] + + self._stackTraceView.LoadThreads( self, True ) + + + @CurrentSession() + @IfConnected() + @RequiresUI() + def PrintDebugInfo( self ): + def Line(): + return ( "--------------------------------------------------------------" + "------------------" ) + + def Pretty( obj ): + if obj is None: + return [ "None" ] + return [ Line() ] + json.dumps( obj, indent=2 ).splitlines() + [ Line() ] + + + debugInfo = [ + "Vimspector Debug Info", + Line(), + f"ConnectionType: { self._connection_type }", + "Adapter: " ] + Pretty( self._adapter ) + [ + "Configuration: " ] + Pretty( self._configuration ) + [ + f"API Prefix: { self._api_prefix }", + f"Launch/Init: { self._launch_complete } / { self._init_complete }", + f"Workspace Root: { self._workspace_root }", + "Launch Config: " ] + Pretty( self._launch_config ) + [ + "Server Capabilities: " ] + Pretty( self._server_capabilities ) + [ + "Line Breakpoints: " ] + Pretty( self._breakpoints._line_breakpoints ) + [ + "Func Breakpoints: " ] + Pretty( self._breakpoints._func_breakpoints ) + [ + "Ex Breakpoints: " ] + Pretty( self._breakpoints._exception_breakpoints ) + + self._outputView.ClearCategory( 'DebugInfo' ) + self._outputView.Print( "DebugInfo", debugInfo ) + self.ShowOutput( "DebugInfo" ) + + + def OnEvent_loadedSource( self, msg ): + pass + + + def OnEvent_capabilities( self, msg ): + self._server_capabilities.update( + ( msg.get( 'body' ) or {} ).get( 'capabilities' ) or {} ) + + + def OnEvent_initialized( self, message ): + def OnBreakpointsDone(): + self._breakpoints.Refresh() + if self._server_capabilities.get( 'supportsConfigurationDoneRequest' ): + self._connection.DoRequest( + lambda msg: self._OnInitializeComplete(), + { + 'command': 'configurationDone', + } + ) + else: + self._OnInitializeComplete() - def JumpToBreakpointViewBreakpoint( self ): - self._breakpoints.JumpToBreakpointViewBreakpoint() + self._breakpoints.SetConfiguredBreakpoints( + self._configuration.get( 'breakpoints', {} ) ) + self._breakpoints.AddConnection( self._connection ) + self._breakpoints.UpdateUI( OnBreakpointsDone ) - def EditBreakpointOptionsViewBreakpoint( self ): - self._breakpoints.EditBreakpointOptionsViewBreakpoint() - def JumpToNextBreakpoint( self ): - self._breakpoints.JumpToNextBreakpoint() + def OnEvent_thread( self, message ): + self._stackTraceView.OnThreadEvent( self, message[ 'body' ] ) - def JumpToPreviousBreakpoint( self ): - self._breakpoints.JumpToPreviousBreakpoint() - def JumpToProgramCounter( self ): - self._stackTraceView.JumpToProgramCounter() + def OnEvent_breakpoint( self, message ): + reason = message[ 'body' ][ 'reason' ] + bp = message[ 'body' ][ 'breakpoint' ] + if reason == 'changed': + self._breakpoints.UpdatePostedBreakpoint( self._connection, bp ) + elif reason == 'new': + self._breakpoints.AddPostedBreakpoint( self._connection, bp ) + elif reason == 'removed': + self._breakpoints.DeletePostedBreakpoint( self._connection, bp ) + else: + utils.UserMessage( + 'Unrecognised breakpoint event (undocumented): {0}'.format( + reason ), + persist = True ) + + def OnRequest_runInTerminal( self, message ): + params = message[ 'arguments' ] + + if not params.get( 'cwd' ) : + params[ 'cwd' ] = self._workspace_root + self._logger.debug( 'Defaulting working directory to %s', + params[ 'cwd' ] ) + + term_id = self._codeView.LaunchTerminal( params ) + + response = { + 'processId': int( utils.Call( + 'vimspector#internal#{}term#GetPID'.format( self._api_prefix ), + term_id ) ) + } + + self._connection.DoResponse( message, None, response ) + + def OnEvent_terminated( self, message ): + # The debugging _session_ has terminated. This does not mean that the + # debuggee has terminated (that's the exited event). + # + # We will handle this when the server actually exists. + # + # FIXME we should always wait for this event before disconnecting closing + # any socket connection + # self._stackTraceView.OnTerminated( self ) + self.SetCurrentFrame( None ) + + + def OnEvent_exited( self, message ): + utils.UserMessage( 'The debuggee exited with status code: {}'.format( + message[ 'body' ][ 'exitCode' ] ) ) + self._stackTraceView.OnExited( self, message ) + self.ClearCurrentPC() + + + def OnRequest_startDebugging( self, message ): + self._DoStartDebuggingRequest( message, + message[ 'arguments' ][ 'request' ], + message[ 'arguments' ][ 'configuration' ], + self._adapter ) + + def _DoStartDebuggingRequest( self, + message, + request_type, + launch_arguments, + adapter, + session_name = None ): + + session = self.manager.NewSession( + session_name = session_name or launch_arguments.get( 'name' ), + parent_session = self ) + + # Inject the launch config (HACK!). This will actually mean that the + # configuration passed below is ignored. + session._launch_config = launch_arguments + session._launch_config[ 'request' ] = request_type + + # FIXME: We probably do need to add a StartWithLauncArguments and somehow + # tell the new session that it shoud not support "Restart" requests ? + # + # In fact, what even would Reset do... ? + session._StartWithConfiguration( { 'configuration': launch_arguments }, + adapter ) + + self._connection.DoResponse( message, None, {} ) + + def OnEvent_process( self, message ): + utils.UserMessage( 'debuggee was started: {}'.format( + message[ 'body' ][ 'name' ] ) ) + + def OnEvent_module( self, message ): + pass + + def OnEvent_continued( self, message ): + self._stackTraceView.OnContinued( self, message[ 'body' ] ) + self.ClearCurrentPC() + + @ParentOnly() + def Clear( self ): + self._codeView.Clear() + if self._disassemblyView: + self._disassemblyView.Clear() + self._stackTraceView.Clear() + self._variablesView.Clear() + + def OnServerExit( self, status ): + self._logger.info( "The server has terminated with status %s", + status ) + + if self._connection is not None: + # Can be None if the server dies _before_ StartDebugSession vim function + # returns + self._connection.Reset() + + self._stackTraceView.ConnectionClosed( self ) + self._breakpoints.ConnectionClosed( self._connection ) + self._variablesView.ConnectionClosed( self._connection ) + if self._disassemblyView: + self._disassemblyView.ConnectionClosed( self._connection ) + + self.Clear() + self._ResetServerState() + + if self._run_on_server_exit: + self._logger.debug( "Running server exit handler" ) + callback = self._run_on_server_exit + self._run_on_server_exit = None + callback() + else: + self._logger.debug( "No server exit handler" ) - def ToggleBreakpoint( self, options ): - return self._breakpoints.ToggleBreakpoint( options ) + def OnEvent_output( self, message ): + if self._outputView: + self._outputView.OnOutput( message[ 'body' ] ) + def OnEvent_stopped( self, message ): + event = message[ 'body' ] + reason = event.get( 'reason' ) or '' + description = event.get( 'description' ) + text = event.get( 'text' ) - def RunTo( self, file_name, line ): - self._breakpoints.ClearTemporaryBreakpoints() - self._breakpoints.AddTemporaryLineBreakpoint( file_name, - line, - { 'temporary': True }, - lambda: self.Continue() ) + if description: + explanation = description + '(' + reason + ')' + else: + explanation = reason - @CurrentSession() - @IfConnected() - def GoTo( self, file_name, line ): - def failure_handler( reason, *args ): - utils.UserMessage( f"Can't jump to location: {reason}", error=True ) + if text: + explanation += ': ' + text - def handle_targets( msg ): - targets = msg.get( 'body', {} ).get( 'targets', [] ) - if not targets: - failure_handler( "No targets" ) - return + msg = 'Paused in thread {0} due to {1}'.format( + event.get( 'threadId', '' ), + explanation ) + utils.UserMessage( msg ) - if len( targets ) == 1: - target_selected = 0 - else: - target_selected = utils.SelectFromList( "Which target?", [ - t[ 'label' ] for t in targets - ], ret = 'index' ) + if self._outputView: + self._outputView.Print( 'server', msg ) - if target_selected is None: - return + self._stackTraceView.OnStopped( self, event ) - self._connection.DoRequest( None, { - 'command': 'goto', - 'arguments': { - 'threadId': self._stackTraceView.GetCurrentThreadId(), - 'targetId': targets[ target_selected ][ 'id' ] - }, - }, failure_handler ) + def BreakpointsAsQuickFix( self ): + return self._breakpoints.BreakpointsAsQuickFix() - if not self._server_capabilities.get( 'supportsGotoTargetsRequest', False ): - failure_handler( "Server doesn't support it" ) - return + def ListBreakpoints( self ): + self._breakpoints.ToggleBreakpointsView() - self._connection.DoRequest( handle_targets, { - 'command': 'gotoTargets', - 'arguments': { - 'source': { - 'path': utils.NormalizePath( file_name ) - }, - 'line': line - }, - }, failure_handler ) + def ToggleBreakpointViewBreakpoint( self ): + self._breakpoints.ToggleBreakpointViewBreakpoint() + def ToggleAllBreakpointsViewBreakpoint( self ): + self._breakpoints.ToggleAllBreakpointsViewBreakpoint() - def SetLineBreakpoint( self, file_name, line_num, options, then = None ): - return self._breakpoints.SetLineBreakpoint( file_name, - line_num, - options, - then ) + def DeleteBreakpointViewBreakpoint( self ): + self._breakpoints.ClearBreakpointViewBreakpoint() - def ClearLineBreakpoint( self, file_name, line_num ): - return self._breakpoints.ClearLineBreakpoint( file_name, line_num ) + def JumpToBreakpointViewBreakpoint( self ): + self._breakpoints.JumpToBreakpointViewBreakpoint() - def ClearBreakpoints( self ): - return self._breakpoints.ClearBreakpoints() + def EditBreakpointOptionsViewBreakpoint( self ): + self._breakpoints.EditBreakpointOptionsViewBreakpoint() - def ResetExceptionBreakpoints( self ): - return self._breakpoints.ResetExceptionBreakpoints() + def JumpToNextBreakpoint( self ): + self._breakpoints.JumpToNextBreakpoint() - def AddFunctionBreakpoint( self, function, options ): - return self._breakpoints.AddFunctionBreakpoint( function, options ) + def JumpToPreviousBreakpoint( self ): + self._breakpoints.JumpToPreviousBreakpoint() + def JumpToProgramCounter( self ): + self._stackTraceView.JumpToProgramCounter() -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 + def ToggleBreakpoint( self, options ): + return self._breakpoints.ToggleBreakpoint( options ) - yield utils.PathToConfigFile( '.gadgets.json', - os.path.dirname( current_file ) ) + def RunTo( self, file_name, line ): + self._breakpoints.ClearTemporaryBreakpoints() + self._breakpoints.AddTemporaryLineBreakpoint( file_name, + line, + { 'temporary': True }, + lambda: self.Continue() ) -def PathsToAllConfigFiles( vimspector_base, current_file, filetypes ): - for ft in filetypes + [ '_all' ]: + @CurrentSession() + @IfConnected() + def GoTo( self, file_name, line ): + def failure_handler( reason, *args ): + utils.UserMessage( f"Can't jump to location: {reason}", error=True ) + + def handle_targets( msg ): + targets = msg.get( 'body', {} ).get( 'targets', [] ) + if not targets: + failure_handler( "No targets" ) + return + + if len( targets ) == 1: + target_selected = 0 + else: + target_selected = utils.SelectFromList( "Which target?", [ + t[ 'label' ] for t in targets + ], ret = 'index' ) + + if target_selected is None: + return + + self._connection.DoRequest( None, { + 'command': 'goto', + 'arguments': { + 'threadId': self._stackTraceView.GetCurrentThreadId(), + 'targetId': targets[ target_selected ][ 'id' ] + }, + }, failure_handler ) + + if not self._server_capabilities.get( 'supportsGotoTargetsRequest', False ): + failure_handler( "Server doesn't support it" ) + return + + self._connection.DoRequest( handle_targets, { + 'command': 'gotoTargets', + 'arguments': { + 'source': { + 'path': utils.NormalizePath( file_name ) + }, + 'line': line + }, + }, failure_handler ) + + + def SetLineBreakpoint( self, file_name, line_num, options, then = None ): + return self._breakpoints.SetLineBreakpoint( file_name, + line_num, + options, + then ) + + def ClearLineBreakpoint( self, file_name, line_num ): + return self._breakpoints.ClearLineBreakpoint( file_name, line_num ) + + def ClearBreakpoints( self ): + return self._breakpoints.ClearBreakpoints() + + def ResetExceptionBreakpoints( self ): + return self._breakpoints.ResetExceptionBreakpoints() + + 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.GetConfigDirForFiletype( vimspector_base, ft ), + os.path.join( install.GetGadgetConfigDir( vimspector_base ), '*.json' ) ) ): - yield p + yield p - for ft in filetypes: - yield utils.PathToConfigFile( f'.vimspector.{ft}.json', + yield utils.PathToConfigFile( '.gadgets.json', os.path.dirname( current_file ) ) - yield utils.PathToConfigFile( '.vimspector.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 ) ) def _SelectProcess( *args ): - value = 0 - - custom_picker = settings.Get( 'custom_process_picker_func' ) - if custom_picker: - try: - value = utils.Call( custom_picker, *args ) - except vim.error: - pass - else: - # Use the built-in one - vimspector_process_list: str = None - try: - try: - vimspector_process_list = installer.FindExecutable( - 'vimspector_process_list' ) - except installer.MissingExecutable: - vimspector_process_list = installer.FindExecutable( - 'vimspector_process_list', - [ os.path.join( install.GetSupportDir(), - 'vimspector_process_list' ) ] ) - except installer.MissingExecutable: - pass - - default_pid = None - if vimspector_process_list: - output = subprocess.check_output( - ( vimspector_process_list, ) + args ).decode( 'utf-8' ) - # if there's only one entry, use it as the default value for input. - lines = output.splitlines() - if len( lines ) == 2: - default_pid = lines[ -1 ].split()[ 0 ] - utils.UserMessage( lines ) - - value = utils.AskForInput( 'Enter Process ID: ', - default_value = default_pid ) - - if value: - try: - return int( value ) - except ValueError: - return 0 - - return 0 + value = 0 + + custom_picker = settings.Get( 'custom_process_picker_func' ) + if custom_picker: + try: + value = utils.Call( custom_picker, *args ) + except vim.error: + pass + else: + # Use the built-in one + vimspector_process_list: str = None + try: + try: + vimspector_process_list = installer.FindExecutable( + 'vimspector_process_list' ) + except installer.MissingExecutable: + vimspector_process_list = installer.FindExecutable( + 'vimspector_process_list', + [ os.path.join( install.GetSupportDir(), + 'vimspector_process_list' ) ] ) + except installer.MissingExecutable: + pass + + default_pid = None + if vimspector_process_list: + output = subprocess.check_output( + ( vimspector_process_list, ) + args ).decode( 'utf-8' ) + # if there's only one entry, use it as the default value for input. + lines = output.splitlines() + if len( lines ) == 2: + default_pid = lines[ -1 ].split()[ 0 ] + utils.UserMessage( lines ) + + value = utils.AskForInput( 'Enter Process ID: ', + default_value = default_pid ) + + if value: + try: + return int( value ) + except ValueError: + return 0 + + return 0 From 4377cf12119dd548c2a3f999ad72d4fd6e5e691b Mon Sep 17 00:00:00 2001 From: Jordan Walsh Date: Fri, 22 Dec 2023 14:15:49 -0500 Subject: [PATCH 02/43] debugging --- python3/vimspector/debug_session.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index d9a19f722..06b3ce636 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -1696,6 +1696,7 @@ def _GetShellCommand( self ): def _GetDockerCommand( self, remote ): docker = [ 'docker', 'exec', '-t' ] + self._logger.info(f"remote is: {remote}") if 'workdir' in remote: docker.extend(["-w", remote['workdir']]) docker.append( remote[ 'container' ] ) From c64775af9e687c8b3d8f399838e7c2b996917ac0 Mon Sep 17 00:00:00 2001 From: Jordan Walsh Date: Fri, 22 Dec 2023 14:18:03 -0500 Subject: [PATCH 03/43] Revert "debugging" This reverts commit 4377cf12119dd548c2a3f999ad72d4fd6e5e691b. --- python3/vimspector/debug_session.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 06b3ce636..d9a19f722 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -1696,7 +1696,6 @@ def _GetShellCommand( self ): def _GetDockerCommand( self, remote ): docker = [ 'docker', 'exec', '-t' ] - self._logger.info(f"remote is: {remote}") if 'workdir' in remote: docker.extend(["-w", remote['workdir']]) docker.append( remote[ 'container' ] ) From 8af4f54961a5022d0d950439b4f122a43d242695 Mon Sep 17 00:00:00 2001 From: Jordan Walsh Date: Fri, 22 Dec 2023 14:48:54 -0500 Subject: [PATCH 04/43] Revert "Added workdir support for docker exec" This reverts commit c3c25030cdb5f9ff48ca5a65c3f08ae759c14c02. --- docs/schema/vimspector.schema.json | 4 - python3/vimspector/debug_session.py | 4201 +++++++++++++-------------- 2 files changed, 2092 insertions(+), 2113 deletions(-) diff --git a/docs/schema/vimspector.schema.json b/docs/schema/vimspector.schema.json index 23f006482..7b319637a 100644 --- a/docs/schema/vimspector.schema.json +++ b/docs/schema/vimspector.schema.json @@ -186,10 +186,6 @@ "type": "array", "items": { "type": "string" }, "description": "A single command to execute for remote-launch. Like runCommands but for a single command." - }, - "workdir": { - "type": "string", - "description": "For containers. The value passed to docker exec for the working directory." } } } diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index d9a19f722..0ede8984a 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -47,2245 +47,2228 @@ class DebugSession( object ): - child_sessions: typing.List[ "DebugSession" ] - - def CurrentSession(): - def decorator( fct ): - @functools.wraps( fct ) - def wrapper( self: "DebugSession", *args, **kwargs ): - active_session = self - if self._stackTraceView: - active_session = self._stackTraceView.GetCurrentSession() - if active_session is not None: - return fct( active_session, *args, **kwargs ) - return fct( self, *args, **kwargs ) - return wrapper - return decorator - - def ParentOnly( otherwise=None ): - def decorator( fct ): - @functools.wraps( fct ) - def wrapper( self: "DebugSession", *args, **kwargs ): - if self.parent_session: - return otherwise - return fct( self, *args, **kwargs ) - return wrapper - return decorator - - def IfConnected( otherwise=None ): - def decorator( fct ): - """Decorator, call fct if self._connected else echo warning""" - @functools.wraps( fct ) - def wrapper( self: "DebugSession", *args, **kwargs ): - if not self._connection: - utils.UserMessage( - 'Vimspector not connected, start a debug session first', - persist=False, - error=True ) - return otherwise - return fct( self, *args, **kwargs ) - return wrapper - return decorator - - def RequiresUI( otherwise=None ): - """Decorator, call fct if self._connected else echo warning""" - def decorator( fct ): - @functools.wraps( fct ) - def wrapper( self, *args, **kwargs ): - if not self.HasUI(): - utils.UserMessage( - 'Vimspector is not active', - persist=False, - error=True ) - return otherwise - return fct( self, *args, **kwargs ) - return wrapper - return decorator - - - def __init__( self, - session_id, - session_manager, - api_prefix, - session_name = None, - parent_session: "DebugSession" = None ): - self.session_id = session_id - self.manager = session_manager - self.name = session_name - self.parent_session = parent_session - self.child_sessions = [] - - if parent_session: - parent_session.child_sessions.append( self ) - - self._logger = logging.getLogger( __name__ + '.' + str( session_id ) ) - utils.SetUpLogging( self._logger, session_id ) - - self._api_prefix = api_prefix - - self._render_emitter = utils.EventEmitter() - - self._logger.info( "**** INITIALISING NEW VIMSPECTOR SESSION FOR ID " - f"{session_id } ****" ) - self._logger.info( "API is: {}".format( api_prefix ) ) - self._logger.info( 'VIMSPECTOR_HOME = %s', VIMSPECTOR_HOME ) - self._logger.info( 'gadgetDir = %s', - install.GetGadgetDir( VIMSPECTOR_HOME ) ) - - self._uiTab = None - - self._logView: output.OutputView = None - self._stackTraceView: stack_trace.StackTraceView = None - self._variablesView: variables.VariablesView = None - self._outputView: output.DAPOutputView = None - self._codeView: code.CodeView = None - self._disassemblyView: disassembly.DisassemblyView = None - - if parent_session: - self._breakpoints = parent_session._breakpoints - else: - self._breakpoints = breakpoints.ProjectBreakpoints( - session_id, - self._render_emitter, - self._IsPCPresentAt, - self._disassemblyView ) - utils.SetSessionWindows( {} ) + child_sessions: typing.List[ "DebugSession" ] + + def CurrentSession(): + def decorator( fct ): + @functools.wraps( fct ) + def wrapper( self: "DebugSession", *args, **kwargs ): + active_session = self + if self._stackTraceView: + active_session = self._stackTraceView.GetCurrentSession() + if active_session is not None: + return fct( active_session, *args, **kwargs ) + return fct( self, *args, **kwargs ) + return wrapper + return decorator + + def ParentOnly( otherwise=None ): + def decorator( fct ): + @functools.wraps( fct ) + def wrapper( self: "DebugSession", *args, **kwargs ): + if self.parent_session: + return otherwise + return fct( self, *args, **kwargs ) + return wrapper + return decorator + + def IfConnected( otherwise=None ): + def decorator( fct ): + """Decorator, call fct if self._connected else echo warning""" + @functools.wraps( fct ) + def wrapper( self: "DebugSession", *args, **kwargs ): + if not self._connection: + utils.UserMessage( + 'Vimspector not connected, start a debug session first', + persist=False, + error=True ) + return otherwise + return fct( self, *args, **kwargs ) + return wrapper + return decorator + + def RequiresUI( otherwise=None ): + """Decorator, call fct if self._connected else echo warning""" + def decorator( fct ): + @functools.wraps( fct ) + def wrapper( self, *args, **kwargs ): + if not self.HasUI(): + utils.UserMessage( + 'Vimspector is not active', + persist=False, + error=True ) + return otherwise + return fct( self, *args, **kwargs ) + return wrapper + return decorator + + + def __init__( self, + session_id, + session_manager, + api_prefix, + session_name = None, + parent_session: "DebugSession" = None ): + self.session_id = session_id + self.manager = session_manager + self.name = session_name + self.parent_session = parent_session + self.child_sessions = [] + + if parent_session: + parent_session.child_sessions.append( self ) + + self._logger = logging.getLogger( __name__ + '.' + str( session_id ) ) + utils.SetUpLogging( self._logger, session_id ) + + self._api_prefix = api_prefix + + self._render_emitter = utils.EventEmitter() + + self._logger.info( "**** INITIALISING NEW VIMSPECTOR SESSION FOR ID " + f"{session_id } ****" ) + self._logger.info( "API is: {}".format( api_prefix ) ) + self._logger.info( 'VIMSPECTOR_HOME = %s', VIMSPECTOR_HOME ) + self._logger.info( 'gadgetDir = %s', + install.GetGadgetDir( VIMSPECTOR_HOME ) ) + + self._uiTab = None + + self._logView: output.OutputView = None + self._stackTraceView: stack_trace.StackTraceView = None + self._variablesView: variables.VariablesView = None + self._outputView: output.DAPOutputView = None + self._codeView: code.CodeView = None + self._disassemblyView: disassembly.DisassemblyView = None + + if parent_session: + self._breakpoints = parent_session._breakpoints + else: + self._breakpoints = breakpoints.ProjectBreakpoints( + session_id, + self._render_emitter, + self._IsPCPresentAt, + self._disassemblyView ) + utils.SetSessionWindows( {} ) - self._saved_variables_data = None + self._saved_variables_data = None - self._splash_screen = None - self._remote_term = None - self._adapter_term = None + self._splash_screen = None + self._remote_term = None + self._adapter_term = None - self._run_on_server_exit = None + self._run_on_server_exit = None - self._configuration = None - self._adapter = None - self._launch_config = None + self._configuration = None + self._adapter = None + self._launch_config = None - self._ResetServerState() + self._ResetServerState() - def _ResetServerState( self ): - self._connection = None - self._init_complete = False - self._launch_complete = False - self._on_init_complete_handlers = [] - self._server_capabilities = {} - self._breakpoints.ClearTemporaryBreakpoints() + def _ResetServerState( self ): + self._connection = None + self._init_complete = False + self._launch_complete = False + self._on_init_complete_handlers = [] + self._server_capabilities = {} + self._breakpoints.ClearTemporaryBreakpoints() - def GetConfigurations( self, adapters ): - current_file = utils.GetBufferFilepath( vim.current.buffer ) - filetypes = utils.GetBufferFiletypes( vim.current.buffer ) - configurations = settings.Dict( 'configurations' ) + def GetConfigurations( self, adapters ): + current_file = utils.GetBufferFilepath( vim.current.buffer ) + filetypes = utils.GetBufferFiletypes( vim.current.buffer ) + configurations = settings.Dict( 'configurations' ) - 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 + 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 {} ) + 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 {} ) - filetype_configurations = configurations - if filetypes: - # filter out any configurations that have a 'filetypes' list set and it - # doesn't contain one of the current filetypes - filetype_configurations = { - k: c for k, c in configurations.items() if 'filetypes' not in c or any( - ft in c[ 'filetypes' ] for ft in filetypes - ) - } + filetype_configurations = configurations + if filetypes: + # filter out any configurations that have a 'filetypes' list set and it + # doesn't contain one of the current filetypes + filetype_configurations = { + k: c for k, c in configurations.items() if 'filetypes' not in c or any( + ft in c[ 'filetypes' ] for ft in filetypes + ) + } - return launch_config_file, filetype_configurations, configurations + return launch_config_file, filetype_configurations, configurations - def Name( self ): - return self.name if self.name else "Unnamed-" + str( self.session_id ) + def Name( self ): + return self.name if self.name else "Unnamed-" + str( self.session_id ) - def DisplayName( self ): - return self.Name() + ' (' + str( self.session_id ) + ')' + def DisplayName( self ): + return self.Name() + ' (' + str( self.session_id ) + ')' - @ParentOnly() - def Start( self, - force_choose = False, - launch_variables = None, - adhoc_configurations = None ): - # We mutate launch_variables, so don't mutate the default argument. - # https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments - if launch_variables is None: - launch_variables = {} + @ParentOnly() + def Start( self, + force_choose = False, + launch_variables = None, + adhoc_configurations = None ): + # We mutate launch_variables, so don't mutate the default argument. + # https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments + if launch_variables is None: + launch_variables = {} - self._logger.info( "User requested start debug session with %s", - launch_variables ) + self._logger.info( "User requested start debug session with %s", + launch_variables ) - current_file = utils.GetBufferFilepath( vim.current.buffer ) - adapters = settings.Dict( 'adapters' ) + current_file = utils.GetBufferFilepath( vim.current.buffer ) + adapters = settings.Dict( 'adapters' ) - launch_config_file = None - configurations = None - if adhoc_configurations: - configurations = adhoc_configurations - else: - ( launch_config_file, - configurations, - all_configurations ) = self.GetConfigurations( adapters ) - - if not configurations: - utils.UserMessage( 'Unable to find any debug configurations. ' - 'You need to tell vimspector how to launch your ' - 'application.' ) + launch_config_file = None + configurations = None + if adhoc_configurations: + configurations = adhoc_configurations + else: + ( launch_config_file, + configurations, + all_configurations ) = self.GetConfigurations( adapters ) + + if not configurations: + utils.UserMessage( 'Unable to find any debug configurations. ' + 'You need to tell vimspector how to launch your ' + '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 force_choose: + # Always display the menu + configuration_name = utils.SelectFromList( + 'Which launch configuration?', + sorted( configurations.keys() ) ) + 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 ) + and c.get( 'autoselect', True ) } + + 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 self.name is None: + self.name = configuration_name + + if launch_config_file: + self._workspace_root = os.path.dirname( launch_config_file ) + else: + self._workspace_root = os.path.dirname( current_file ) + + try: + configuration = configurations[ configuration_name ] + except KeyError: + # Maybe the specified one by name that's not for this filetype? Let's try + # that one... + configuration = all_configurations[ configuration_name ] + + current_configuration_name = configuration_name + while 'extends' in configuration: + base_configuration_name = configuration.pop( 'extends' ) + base_configuration = all_configurations.get( base_configuration_name ) + if base_configuration is None: + raise RuntimeError( f"The adapter { current_configuration_name } " + f"extends configuration { base_configuration_name }" + ", but this does not exist" ) + + core_utils.override( base_configuration, configuration ) + current_configuration_name = base_configuration_name + configuration = base_configuration + + + 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 - - 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 force_choose: - # Always display the menu - configuration_name = utils.SelectFromList( - 'Which launch configuration?', - sorted( configurations.keys() ) ) - 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 ) - and c.get( 'autoselect', True ) } - - 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: + elif response is None: return - if self.name is None: - self.name = configuration_name - - if launch_config_file: - self._workspace_root = os.path.dirname( launch_config_file ) - else: - self._workspace_root = os.path.dirname( current_file ) - - try: - configuration = configurations[ configuration_name ] - except KeyError: - # Maybe the specified one by name that's not for this filetype? Let's try - # that one... - configuration = all_configurations[ configuration_name ] - - current_configuration_name = configuration_name - while 'extends' in configuration: - base_configuration_name = configuration.pop( 'extends' ) - base_configuration = all_configurations.get( - base_configuration_name ) - if base_configuration is None: - raise RuntimeError( f"The adapter { current_configuration_name } " - f"extends configuration { base_configuration_name }" - ", but this does not exist" ) - - core_utils.override( base_configuration, configuration ) - current_configuration_name = base_configuration_name - configuration = base_configuration - - - 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 " - "'VimspectorInstall'?", - persist = True, - error = True ) - return - - adapter = adapter_dict - - if not adapter: - utils.UserMessage( 'No adapter configured for {}'.format( - configuration_name ), - persist=True ) + utils.UserMessage( f"The specified adapter '{adapter}' is not " + "available. Did you forget to run " + "'VimspectorInstall'?", + persist = True, + error = True ) + return + + adapter = adapter_dict + + if not adapter: + utils.UserMessage( 'No adapter configured for {}'.format( + configuration_name ), + persist=True ) + return + + # Pull in anything from the base(s) + # FIXME: this is copypasta from above, but sharing the code is a little icky + # due to the way it returns from this method (maybe use an exception?) + while 'extends' in adapter: + base_adapter_name = adapter.pop( 'extends' ) + base_adapter = adapters.get( base_adapter_name ) + + if base_adapter is None: + suggested_gadgets = installer.FindGadgetForAdapter( base_adapter_name ) + if suggested_gadgets: + response = utils.AskForInput( + f"The specified base adapter '{base_adapter_name}' 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 - # Pull in anything from the base(s) - # FIXME: this is copypasta from above, but sharing the code is a little icky - # due to the way it returns from this method (maybe use an exception?) - while 'extends' in adapter: - base_adapter_name = adapter.pop( 'extends' ) - base_adapter = adapters.get( base_adapter_name ) - - if base_adapter is None: - suggested_gadgets = installer.FindGadgetForAdapter( - base_adapter_name ) - if suggested_gadgets: - response = utils.AskForInput( - f"The specified base adapter '{base_adapter_name}' 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 base adapter '{base_adapter_name}' " - "is not available. Did you forget to run " - "'VimspectorInstall'?", - persist = True, - error = True ) - return - - core_utils.override( base_adapter, adapter ) - adapter = base_adapter - - # 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 = { - 'relativeFileDirname': lambda: os.path.dirname( relpath( current_file, - self._workspace_root ) ), - '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, - - # The following, starting with uppercase letters, are 'functions' taking - # arguments. - 'SelectProcess': _SelectProcess, - 'PickProcess': _SelectProcess, - } - - # 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 ) - - try: - variables.update( - utils.ParseVariables( adapter.pop( 'variables', {} ), + utils.UserMessage( f"The specified base adapter '{base_adapter_name}' " + "is not available. Did you forget to run " + "'VimspectorInstall'?", + persist = True, + error = True ) + return + + core_utils.override( base_adapter, adapter ) + adapter = base_adapter + + # 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 = { + 'relativeFileDirname': lambda: os.path.dirname( relpath( current_file, + self._workspace_root ) ), + '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, + + # The following, starting with uppercase letters, are 'functions' taking + # arguments. + 'SelectProcess': _SelectProcess, + 'PickProcess': _SelectProcess, + } + + # 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 ) + + try: + variables.update( + utils.ParseVariables( adapter.pop( 'variables', {} ), + variables, + calculus, + USER_CHOICES ) ) + variables.update( + utils.ParseVariables( configuration.pop( 'variables', {} ), + variables, + calculus, + USER_CHOICES ) ) + + + utils.ExpandReferencesInDict( configuration, variables, calculus, - USER_CHOICES ) ) - variables.update( - utils.ParseVariables( configuration.pop( 'variables', {} ), + USER_CHOICES ) + utils.ExpandReferencesInDict( adapter, variables, calculus, - USER_CHOICES ) ) - - - utils.ExpandReferencesInDict( configuration, - variables, - calculus, - USER_CHOICES ) - utils.ExpandReferencesInDict( adapter, - variables, - calculus, - USER_CHOICES ) - except KeyboardInterrupt: - self._Reset() - return - - self._StartWithConfiguration( configuration, adapter ) - - def _StartWithConfiguration( self, configuration, adapter ): - def start(): - self._configuration = configuration - self._adapter = adapter - self._launch_config = None - - self._logger.info( 'Configuration: %s', - json.dumps( self._configuration ) ) - self._logger.info( 'Adapter: %s', - json.dumps( self._adapter ) ) - - - if self.parent_session: - # use the parent session's stuff - self._uiTab = self.parent_session._uiTab - self._stackTraceView = self.parent_session._stackTraceView - self._variablesView = self.parent_session._variablesView - self._outputView = self.parent_session._outputView - self._disassemblyView = self.parent_session._disassemblyView - self._codeView = self.parent_session._codeView - - elif not self._uiTab: - self._SetUpUI() - else: - with utils.NoAutocommands(): - vim.current.tabpage = self._uiTab - - self._stackTraceView.AddSession( self ) - self._Prepare() - if not self._StartDebugAdapter(): - self._logger.info( - "Failed to launch or attach to the debug adapter" ) - return - - self._Initialise() - - if self._saved_variables_data: - self._variablesView.Load( self._saved_variables_data ) - - if self._connection: - self._logger.debug( "Stop debug adapter with callback: start" ) - self.StopAllSessions( interactive = False, then = start ) - return - - start() - - @ParentOnly() - def Restart( self ): - if self._configuration is None or self._adapter is None: - return self.Start() - - self._StartWithConfiguration( self._configuration, self._adapter ) - - def Connection( self ): - return self._connection - - def HasUI( self ): - return self._uiTab and self._uiTab.valid - - def IsUITab( self, tab_number ): - return self.HasUI() and self._uiTab.number == tab_number - - @ParentOnly() - def SwitchTo( self ): - if self.HasUI(): - vim.current.tabpage = self._uiTab - - self._breakpoints.UpdateUI() - - - @ParentOnly() - def SwitchFrom( self ): - self._breakpoints.ClearUI() - - - def OnChannelData( self, data ): - if self._connection is None: - # Should _not_ happen, but maybe possible due to races or vim bufs? - return - - self._connection.OnData( data ) - - - def OnServerStderr( self, data ): - if self._outputView: - self._outputView.Print( 'server', data ) - + USER_CHOICES ) + except KeyboardInterrupt: + self._Reset() + return - def OnRequestTimeout( self, timer_id ): - self._connection.OnRequestTimeout( timer_id ) + self._StartWithConfiguration( configuration, adapter ) - def OnChannelClosed( self ): - # TODO: Not called - self._connection = None + def _StartWithConfiguration( self, configuration, adapter ): + def start(): + self._configuration = configuration + self._adapter = adapter + self._launch_config = None + self._logger.info( 'Configuration: %s', + json.dumps( self._configuration ) ) + self._logger.info( 'Adapter: %s', + json.dumps( self._adapter ) ) - def _StopNextSession( self, terminateDebuggee, then ): - if self.child_sessions: - c = self.child_sessions.pop() - c._StopNextSession( terminateDebuggee, - then = lambda: self._StopNextSession( - terminateDebuggee, - then ) ) - elif self._connection: - self._StopDebugAdapter( terminateDebuggee, callback = then ) - elif then: - then() - def StopAllSessions( self, interactive = False, then = None ): - if not interactive: - self._StopNextSession( None, then ) - elif not self._server_capabilities.get( 'supportTerminateDebuggee' ): - self._StopNextSession( None, then ) - elif not self._stackTraceView.AnyThreadsRunning(): - self._StopNextSession( None, then ) - else: - self._ConfirmTerminateDebugee( - lambda terminateDebuggee: self._StopNextSession( terminateDebuggee, - then ) ) - - @ParentOnly() - @IfConnected() - def Stop( self, interactive = False ): - self._logger.debug( "Stop debug adapter with no callback" ) - self.StopAllSessions( interactive = False ) - - @ParentOnly() - def Destroy( self ): - """Call when the vimspector session will be removed and never used again""" - if self._connection is not None: - raise RuntimeError( - "Can't destroy a session with a live connection" ) - - if self.HasUI(): - raise RuntimeError( "Can't destroy a session with an active UI" ) - - self.ClearBreakpoints() - self._ResetUI() - - - @ParentOnly() - def Reset( self, interactive = False ): - # We reset all of the child sessions in turn - self._logger.debug( "Stop debug adapter with callback: _Reset" ) - self.StopAllSessions( interactive, self._Reset ) - - - def _IsPCPresentAt( self, file_path, line ): - return self._codeView and self._codeView.IsPCPresentAt( file_path, line ) - - - def _ResetUI( self ): - if not self.parent_session: - if self._stackTraceView: - self._stackTraceView.Reset() - if self._variablesView: - self._variablesView.Reset() - if self._outputView: - self._outputView.Reset() - if self._logView: - self._logView.Reset() - if self._codeView: - self._codeView.Reset() - if self._disassemblyView: - self._disassemblyView.Reset() - - self._breakpoints.RemoveConnection( self._connection ) - self._stackTraceView = None - self._variablesView = None - self._outputView = None - self._codeView = None - self._disassemblyView = None - self._remote_term = None - self._uiTab = None - - if self.parent_session: - self.manager.DestroySession( self ) - - - def _Reset( self ): - if self.parent_session: - self._ResetUI() - return - - vim.vars[ 'vimspector_resetting' ] = 1 - self._logger.info( "Debugging complete." ) - - if self.HasUI(): - self._logger.debug( "Clearing down UI" ) - with utils.NoAutocommands(): - vim.current.tabpage = self._uiTab - self._splash_screen = utils.HideSplash( self._api_prefix, - self._splash_screen ) - self._ResetUI() - vim.command( 'tabclose!' ) - else: - self._ResetUI() - - self._breakpoints.SetDisassemblyManager( None ) - utils.SetSessionWindows( { - 'breakpoints': vim.vars[ 'vimspector_session_windows' ].get( - 'breakpoints' ) - } ) - vim.command( 'doautocmd User VimspectorDebugEnded' ) - - vim.vars[ 'vimspector_resetting' ] = 0 - - # make sure that we're displaying signs in any still-open buffers - self._breakpoints.UpdateUI() + if self.parent_session: + # use the parent session's stuff + self._uiTab = self.parent_session._uiTab + self._stackTraceView = self.parent_session._stackTraceView + self._variablesView = self.parent_session._variablesView + self._outputView = self.parent_session._outputView + self._disassemblyView = self.parent_session._disassemblyView + self._codeView = self.parent_session._codeView - @ParentOnly( False ) - def ReadSessionFile( self, session_file: str = None ): - if session_file is None: - session_file = self._DetectSessionFile( - invent_one_if_not_found = False ) + elif not self._uiTab: + self._SetUpUI() + else: + with utils.NoAutocommands(): + vim.current.tabpage = self._uiTab - if session_file is None: - utils.UserMessage( f"No { settings.Get( 'session_file_name' ) } file " - "found. Specify a file with :VimspectorLoadSession " - "", - persist = True, - error = True ) - return False + self._stackTraceView.AddSession( self ) + self._Prepare() + if not self._StartDebugAdapter(): + self._logger.info( "Failed to launch or attach to the debug adapter" ) + return - try: - with open( session_file, 'r' ) as f: - session_data = json.load( f ) + self._Initialise() - USER_CHOICES.update( - session_data.get( 'session', {} ).get( 'user_choices', {} ) ) + if self._saved_variables_data: + self._variablesView.Load( self._saved_variables_data ) - self._breakpoints.Load( session_data.get( 'breakpoints' ) ) + if self._connection: + self._logger.debug( "Stop debug adapter with callback: start" ) + self.StopAllSessions( interactive = False, then = start ) + return - # We might not _have_ a self._variablesView yet so we need a - # mechanism where we save this for later and reload when it's ready - variables_data = session_data.get( 'variables', {} ) - if self._variablesView: - self._variablesView.Load( variables_data ) - else: - self._saved_variables_data = variables_data - - utils.UserMessage( f"Loaded session file { session_file }", - persist=True ) - return True - except OSError: - self._logger.exception( f"Invalid session file { session_file }" ) - utils.UserMessage( f"Session file { session_file } not found", - persist=True, - error=True ) - return False - except json.JSONDecodeError: - self._logger.exception( f"Invalid session file { session_file }" ) - utils.UserMessage( "The session file could not be read", - persist = True, - error = True ) - return False - - - @ParentOnly( False ) - def WriteSessionFile( self, session_file: str = None ): - if session_file is None: - session_file = self._DetectSessionFile( - invent_one_if_not_found = True ) - elif os.path.isdir( session_file ): - session_file = self._DetectSessionFile( invent_one_if_not_found = True, - in_directory = session_file ) + start() + @ParentOnly() + def Restart( self ): + if self._configuration is None or self._adapter is None: + return self.Start() - try: - with open( session_file, 'w' ) as f: - f.write( json.dumps( { - 'breakpoints': self._breakpoints.Save(), - 'session': { - 'user_choices': USER_CHOICES, - }, - 'variables': self._variablesView.Save() if self._variablesView else {} - } ) ) - - utils.UserMessage( f"Wrote { session_file }" ) - return True - except OSError: - self._logger.exception( - f"Unable to write session file { session_file }" ) - utils.UserMessage( "The session file could not be read", - persist = True, - error = True ) - return False - - - def _DetectSessionFile( self, - invent_one_if_not_found: bool, - in_directory: str = None ): - session_file_name = settings.Get( 'session_file_name' ) - - if in_directory: - # If a dir was supplied, read from there - write_directory = in_directory - file_path = os.path.join( in_directory, session_file_name ) - if not os.path.exists( file_path ): - file_path = None - else: - # Otherwise, search based on the current file, and write based on CWD - current_file = utils.GetBufferFilepath( vim.current.buffer ) - write_directory = os.getcwd() - # Search from the path of the file we're editing. But note that if we - # invent a file, we always use CWD as that's more like what would be - # expected. - file_path = utils.PathToConfigFile( session_file_name, - os.path.dirname( current_file ) ) + self._StartWithConfiguration( self._configuration, self._adapter ) + def Connection( self ): + return self._connection - if file_path: - return file_path + def HasUI( self ): + return self._uiTab and self._uiTab.valid - if invent_one_if_not_found: - return os.path.join( write_directory, session_file_name ) + def IsUITab( self, tab_number ): + return self.HasUI() and self._uiTab.number == tab_number - return None + @ParentOnly() + def SwitchTo( self ): + if self.HasUI(): + vim.current.tabpage = self._uiTab + self._breakpoints.UpdateUI() - @CurrentSession() - @IfConnected() - def StepOver( self, **kwargs ): - if self._stackTraceView.GetCurrentThreadId() is None: - return - - arguments = { - 'threadId': self._stackTraceView.GetCurrentThreadId(), - 'granularity': self._CurrentSteppingGranularity(), - } - arguments.update( kwargs ) - - if not self._server_capabilities.get( 'supportsSteppingGranularity' ): - arguments.pop( 'granularity' ) - - self._connection.DoRequest( None, { - 'command': 'next', - 'arguments': arguments, - } ) - - # TODO: WHy is this different from StepInto and StepOut - self._stackTraceView.OnContinued( self ) - self.ClearCurrentPC() - - @CurrentSession() - @IfConnected() - def StepInto( self, **kwargs ): - threadId = self._stackTraceView.GetCurrentThreadId() - if threadId is None: - return - def handler( *_ ): - self._stackTraceView.OnContinued( self, { 'threadId': threadId } ) - self.ClearCurrentPC() + @ParentOnly() + def SwitchFrom( self ): + self._breakpoints.ClearUI() - arguments = { - 'threadId': threadId, - 'granularity': self._CurrentSteppingGranularity(), - } - arguments.update( kwargs ) - self._connection.DoRequest( handler, { - 'command': 'stepIn', - 'arguments': arguments, - } ) - @CurrentSession() - @IfConnected() - def StepOut( self, **kwargs ): - threadId = self._stackTraceView.GetCurrentThreadId() - if threadId is None: - return + def OnChannelData( self, data ): + if self._connection is None: + # Should _not_ happen, but maybe possible due to races or vim bufs? + return - def handler( *_ ): - self._stackTraceView.OnContinued( self, { 'threadId': threadId } ) - self.ClearCurrentPC() + self._connection.OnData( data ) - arguments = { - 'threadId': threadId, - 'granularity': self._CurrentSteppingGranularity(), - } - arguments.update( kwargs ) - self._connection.DoRequest( handler, { - 'command': 'stepOut', - 'arguments': arguments, - } ) - def _CurrentSteppingGranularity( self ): - if self._disassemblyView and self._disassemblyView.IsCurrent(): - return 'instruction' + def OnServerStderr( self, data ): + if self._outputView: + self._outputView.Print( 'server', data ) - return 'statement' - @CurrentSession() - def Continue( self ): - if not self._connection: - self.Start() - return + def OnRequestTimeout( self, timer_id ): + self._connection.OnRequestTimeout( timer_id ) - threadId = self._stackTraceView.GetCurrentThreadId() - if threadId is None: - utils.UserMessage( 'No current thread', persist = True ) - return + def OnChannelClosed( self ): + # TODO: Not called + self._connection = None - def handler( msg ): - self._stackTraceView.OnContinued( self, { - 'threadId': threadId, - 'allThreadsContinued': ( msg.get( 'body' ) or {} ).get( - 'allThreadsContinued', - True ) - } ) - self.ClearCurrentPC() - - self._connection.DoRequest( handler, { - 'command': 'continue', - 'arguments': { - 'threadId': threadId, - }, - } ) - @CurrentSession() - @IfConnected() - def Pause( self ): - if self._stackTraceView.GetCurrentThreadId() is None: - utils.UserMessage( 'No current thread', persist = True ) - return + def _StopNextSession( self, terminateDebuggee, then ): + if self.child_sessions: + c = self.child_sessions.pop() + c._StopNextSession( terminateDebuggee, + then = lambda: self._StopNextSession( + terminateDebuggee, + then ) ) + elif self._connection: + self._StopDebugAdapter( terminateDebuggee, callback = then ) + elif then: + then() - self._connection.DoRequest( None, { - 'command': 'pause', - 'arguments': { - 'threadId': self._stackTraceView.GetCurrentThreadId(), + def StopAllSessions( self, interactive = False, then = None ): + if not interactive: + self._StopNextSession( None, then ) + elif not self._server_capabilities.get( 'supportTerminateDebuggee' ): + self._StopNextSession( None, then ) + elif not self._stackTraceView.AnyThreadsRunning(): + self._StopNextSession( None, then ) + else: + self._ConfirmTerminateDebugee( + lambda terminateDebuggee: self._StopNextSession( terminateDebuggee, + then ) ) + + @ParentOnly() + @IfConnected() + def Stop( self, interactive = False ): + self._logger.debug( "Stop debug adapter with no callback" ) + self.StopAllSessions( interactive = False ) + + @ParentOnly() + def Destroy( self ): + """Call when the vimspector session will be removed and never used again""" + if self._connection is not None: + raise RuntimeError( "Can't destroy a session with a live connection" ) + + if self.HasUI(): + raise RuntimeError( "Can't destroy a session with an active UI" ) + + self.ClearBreakpoints() + self._ResetUI() + + + @ParentOnly() + def Reset( self, interactive = False ): + # We reset all of the child sessions in turn + self._logger.debug( "Stop debug adapter with callback: _Reset" ) + self.StopAllSessions( interactive, self._Reset ) + + + def _IsPCPresentAt( self, file_path, line ): + return self._codeView and self._codeView.IsPCPresentAt( file_path, line ) + + + def _ResetUI( self ): + if not self.parent_session: + if self._stackTraceView: + self._stackTraceView.Reset() + if self._variablesView: + self._variablesView.Reset() + if self._outputView: + self._outputView.Reset() + if self._logView: + self._logView.Reset() + if self._codeView: + self._codeView.Reset() + if self._disassemblyView: + self._disassemblyView.Reset() + + self._breakpoints.RemoveConnection( self._connection ) + self._stackTraceView = None + self._variablesView = None + self._outputView = None + self._codeView = None + self._disassemblyView = None + self._remote_term = None + self._uiTab = None + + if self.parent_session: + self.manager.DestroySession( self ) + + + def _Reset( self ): + if self.parent_session: + self._ResetUI() + return + + vim.vars[ 'vimspector_resetting' ] = 1 + self._logger.info( "Debugging complete." ) + + if self.HasUI(): + self._logger.debug( "Clearing down UI" ) + with utils.NoAutocommands(): + vim.current.tabpage = self._uiTab + self._splash_screen = utils.HideSplash( self._api_prefix, + self._splash_screen ) + self._ResetUI() + vim.command( 'tabclose!' ) + else: + self._ResetUI() + + self._breakpoints.SetDisassemblyManager( None ) + utils.SetSessionWindows( { + 'breakpoints': vim.vars[ 'vimspector_session_windows' ].get( + 'breakpoints' ) + } ) + vim.command( 'doautocmd User VimspectorDebugEnded' ) + + vim.vars[ 'vimspector_resetting' ] = 0 + + # make sure that we're displaying signs in any still-open buffers + self._breakpoints.UpdateUI() + + @ParentOnly( False ) + def ReadSessionFile( self, session_file: str = None ): + if session_file is None: + session_file = self._DetectSessionFile( invent_one_if_not_found = False ) + + if session_file is None: + utils.UserMessage( f"No { settings.Get( 'session_file_name' ) } file " + "found. Specify a file with :VimspectorLoadSession " + "", + persist = True, + error = True ) + return False + + try: + with open( session_file, 'r' ) as f: + session_data = json.load( f ) + + USER_CHOICES.update( + session_data.get( 'session', {} ).get( 'user_choices', {} ) ) + + self._breakpoints.Load( session_data.get( 'breakpoints' ) ) + + # We might not _have_ a self._variablesView yet so we need a + # mechanism where we save this for later and reload when it's ready + variables_data = session_data.get( 'variables', {} ) + if self._variablesView: + self._variablesView.Load( variables_data ) + else: + self._saved_variables_data = variables_data + + utils.UserMessage( f"Loaded session file { session_file }", + persist=True ) + return True + except OSError: + self._logger.exception( f"Invalid session file { session_file }" ) + utils.UserMessage( f"Session file { session_file } not found", + persist=True, + error=True ) + return False + except json.JSONDecodeError: + self._logger.exception( f"Invalid session file { session_file }" ) + utils.UserMessage( "The session file could not be read", + persist = True, + error = True ) + return False + + + @ParentOnly( False ) + def WriteSessionFile( self, session_file: str = None ): + if session_file is None: + session_file = self._DetectSessionFile( invent_one_if_not_found = True ) + elif os.path.isdir( session_file ): + session_file = self._DetectSessionFile( invent_one_if_not_found = True, + in_directory = session_file ) + + + try: + with open( session_file, 'w' ) as f: + f.write( json.dumps( { + 'breakpoints': self._breakpoints.Save(), + 'session': { + 'user_choices': USER_CHOICES, }, + 'variables': self._variablesView.Save() if self._variablesView else {} + } ) ) + + utils.UserMessage( f"Wrote { session_file }" ) + return True + except OSError: + self._logger.exception( f"Unable to write session file { session_file }" ) + utils.UserMessage( "The session file could not be read", + persist = True, + error = True ) + return False + + + def _DetectSessionFile( self, + invent_one_if_not_found: bool, + in_directory: str = None ): + session_file_name = settings.Get( 'session_file_name' ) + + if in_directory: + # If a dir was supplied, read from there + write_directory = in_directory + file_path = os.path.join( in_directory, session_file_name ) + if not os.path.exists( file_path ): + file_path = None + else: + # Otherwise, search based on the current file, and write based on CWD + current_file = utils.GetBufferFilepath( vim.current.buffer ) + write_directory = os.getcwd() + # Search from the path of the file we're editing. But note that if we + # invent a file, we always use CWD as that's more like what would be + # expected. + file_path = utils.PathToConfigFile( session_file_name, + os.path.dirname( current_file ) ) + + + if file_path: + return file_path + + if invent_one_if_not_found: + return os.path.join( write_directory, session_file_name ) + + return None + + + @CurrentSession() + @IfConnected() + def StepOver( self, **kwargs ): + if self._stackTraceView.GetCurrentThreadId() is None: + return + + arguments = { + 'threadId': self._stackTraceView.GetCurrentThreadId(), + 'granularity': self._CurrentSteppingGranularity(), + } + arguments.update( kwargs ) + + if not self._server_capabilities.get( 'supportsSteppingGranularity' ): + arguments.pop( 'granularity' ) + + self._connection.DoRequest( None, { + 'command': 'next', + 'arguments': arguments, + } ) + + # TODO: WHy is this different from StepInto and StepOut + self._stackTraceView.OnContinued( self ) + self.ClearCurrentPC() + + @CurrentSession() + @IfConnected() + def StepInto( self, **kwargs ): + threadId = self._stackTraceView.GetCurrentThreadId() + if threadId is None: + return + + def handler( *_ ): + self._stackTraceView.OnContinued( self, { 'threadId': threadId } ) + self.ClearCurrentPC() + + arguments = { + 'threadId': threadId, + 'granularity': self._CurrentSteppingGranularity(), + } + arguments.update( kwargs ) + self._connection.DoRequest( handler, { + 'command': 'stepIn', + 'arguments': arguments, + } ) + + @CurrentSession() + @IfConnected() + def StepOut( self, **kwargs ): + threadId = self._stackTraceView.GetCurrentThreadId() + if threadId is None: + return + + def handler( *_ ): + self._stackTraceView.OnContinued( self, { 'threadId': threadId } ) + self.ClearCurrentPC() + + arguments = { + 'threadId': threadId, + 'granularity': self._CurrentSteppingGranularity(), + } + arguments.update( kwargs ) + self._connection.DoRequest( handler, { + 'command': 'stepOut', + 'arguments': arguments, + } ) + + def _CurrentSteppingGranularity( self ): + if self._disassemblyView and self._disassemblyView.IsCurrent(): + return 'instruction' + + return 'statement' + + @CurrentSession() + def Continue( self ): + if not self._connection: + self.Start() + return + + threadId = self._stackTraceView.GetCurrentThreadId() + if threadId is None: + utils.UserMessage( 'No current thread', persist = True ) + return + + def handler( msg ): + self._stackTraceView.OnContinued( self, { + 'threadId': threadId, + 'allThreadsContinued': ( msg.get( 'body' ) or {} ).get( + 'allThreadsContinued', + True ) } ) - - @IfConnected() - def PauseContinueThread( self ): - self._stackTraceView.PauseContinueThread() - - @CurrentSession() - @IfConnected() - def SetCurrentThread( self ): - self._stackTraceView.SetCurrentThread() - - @CurrentSession() - @IfConnected() - def ExpandVariable( self, buf = None, line_num = None ): - self._variablesView.ExpandVariable( buf, line_num ) - - @CurrentSession() - @IfConnected() - def SetVariableValue( self, new_value = None, buf = None, line_num = None ): - if not self._server_capabilities.get( 'supportsSetVariable' ): - return - self._variablesView.SetVariableValue( new_value, buf, line_num ) - - @ParentOnly() - def ReadMemory( self, length = None, offset = None ): - # We use the parent session because the actual connection is returned from - # the variables view (and might not be our self._connection) at least in - # theory. - if not self._server_capabilities.get( 'supportsReadMemoryRequest' ): - utils.UserMessage( "Server does not support memory request", - error = True ) - return - - connection: debug_adapter_connection.DebugAdapterConnection - connection, memoryReference = self._variablesView.GetMemoryReference() - if memoryReference is None or connection is None: - utils.UserMessage( "Cannot find memory reference for that", - error = True ) - return - - if length is None: - length = utils.AskForInput( 'How much data to display? ', - default_value = '1024' ) - - try: - length = int( length ) - except ValueError: - return - - if offset is None: - offset = utils.AskForInput( 'Location offset? ', - default_value = '0' ) - - try: - offset = int( offset ) - except ValueError: - return - - - def handler( msg ): - self._codeView.ShowMemory( connection.GetSessionId(), - memoryReference, - length, - offset, - msg ) - - connection.DoRequest( handler, { - 'command': 'readMemory', - 'arguments': { - 'memoryReference': memoryReference, - 'count': int( length ), - 'offset': int( offset ) - } - } ) - - - @CurrentSession() - @IfConnected() - @RequiresUI() - def ShowDisassembly( self ): - if self._disassemblyView and self._disassemblyView.WindowIsValid(): - return - - if not self._codeView or not self._codeView._window.valid: - return - - if not self._stackTraceView: - return - - if not self._server_capabilities.get( 'supportsDisassembleRequest', False ): - utils.UserMessage( "Sorry, server doesn't support that" ) - return - - with utils.LetCurrentWindow( self._codeView._window ): - vim.command( - f'rightbelow { settings.Int( "disassembly_height" ) }new' ) - self._disassemblyView = disassembly.DisassemblyView( - vim.current.window, - self._api_prefix, - self._render_emitter ) - - self._breakpoints.SetDisassemblyManager( self._disassemblyView ) - - utils.UpdateSessionWindows( { - 'disassembly': utils.WindowID( vim.current.window, self._uiTab ) - } ) - - self._disassemblyView.SetCurrentFrame( - self._connection, - self._stackTraceView.GetCurrentFrame(), - True ) - - - def OnDisassemblyWindowScrolled( self, win_id ): - if self._disassemblyView: - self._disassemblyView.OnWindowScrolled( win_id ) - - - @CurrentSession() - @IfConnected() - def AddWatch( self, expression ): - self._variablesView.AddWatch( self._connection, - self._stackTraceView.GetCurrentFrame(), - expression ) - - @CurrentSession() - @IfConnected() - def EvaluateConsole( self, expression, verbose ): - self._outputView.Evaluate( self._connection, - self._stackTraceView.GetCurrentFrame(), - expression, - verbose ) - - @CurrentSession() - @IfConnected() - def DeleteWatch( self ): - self._variablesView.DeleteWatch() - - - @CurrentSession() - @IfConnected() - def HoverEvalTooltip( self, winnr, bufnr, lnum, expression, is_hover ): - frame = self._stackTraceView.GetCurrentFrame() - # Check if RIP is in a frame - if frame is None: - self._logger.debug( 'Tooltip: Not in a stack frame' ) - return '' - - # Check if cursor in code window - if winnr == int( self._codeView._window.number ): - return self._variablesView.HoverEvalTooltip( self._connection, - frame, - expression, - is_hover ) - - return self._variablesView.HoverVarWinTooltip( bufnr, - lnum, - is_hover ) - # Return variable aware function - - - @CurrentSession() - def CleanUpTooltip( self ): - return self._variablesView.CleanUpTooltip() - - @IfConnected() - def ExpandFrameOrThread( self ): - self._stackTraceView.ExpandFrameOrThread() - - @IfConnected() - def UpFrame( self ): - self._stackTraceView.UpFrame() - - @IfConnected() - def DownFrame( self ): - self._stackTraceView.DownFrame() - - def ToggleLog( self ): - if self.HasUI(): - return self.ShowOutput( 'Vimspector' ) - - if self._logView and self._logView.WindowIsValid(): - self._logView.Reset() - self._logView = None - return - - if self._logView: - self._logView.Reset() - - # TODO: The UI code is too scattered. Re-organise into a UI class that - # just deals with these things like window layout and custmisattion. + self.ClearCurrentPC() + + self._connection.DoRequest( handler, { + 'command': 'continue', + 'arguments': { + 'threadId': threadId, + }, + } ) + + @CurrentSession() + @IfConnected() + def Pause( self ): + if self._stackTraceView.GetCurrentThreadId() is None: + utils.UserMessage( 'No current thread', persist = True ) + return + + self._connection.DoRequest( None, { + 'command': 'pause', + 'arguments': { + 'threadId': self._stackTraceView.GetCurrentThreadId(), + }, + } ) + + @IfConnected() + def PauseContinueThread( self ): + self._stackTraceView.PauseContinueThread() + + @CurrentSession() + @IfConnected() + def SetCurrentThread( self ): + self._stackTraceView.SetCurrentThread() + + @CurrentSession() + @IfConnected() + def ExpandVariable( self, buf = None, line_num = None ): + self._variablesView.ExpandVariable( buf, line_num ) + + @CurrentSession() + @IfConnected() + def SetVariableValue( self, new_value = None, buf = None, line_num = None ): + if not self._server_capabilities.get( 'supportsSetVariable' ): + return + self._variablesView.SetVariableValue( new_value, buf, line_num ) + + @ParentOnly() + def ReadMemory( self, length = None, offset = None ): + # We use the parent session because the actual connection is returned from + # the variables view (and might not be our self._connection) at least in + # theory. + if not self._server_capabilities.get( 'supportsReadMemoryRequest' ): + utils.UserMessage( "Server does not support memory request", + error = True ) + return + + connection: debug_adapter_connection.DebugAdapterConnection + connection, memoryReference = self._variablesView.GetMemoryReference() + if memoryReference is None or connection is None: + utils.UserMessage( "Cannot find memory reference for that", + error = True ) + return + + if length is None: + length = utils.AskForInput( 'How much data to display? ', + default_value = '1024' ) + + try: + length = int( length ) + except ValueError: + return + + if offset is None: + offset = utils.AskForInput( 'Location offset? ', + default_value = '0' ) + + try: + offset = int( offset ) + except ValueError: + return + + + def handler( msg ): + self._codeView.ShowMemory( connection.GetSessionId(), + memoryReference, + length, + offset, + msg ) + + connection.DoRequest( handler, { + 'command': 'readMemory', + 'arguments': { + 'memoryReference': memoryReference, + 'count': int( length ), + 'offset': int( offset ) + } + } ) + + + @CurrentSession() + @IfConnected() + @RequiresUI() + def ShowDisassembly( self ): + if self._disassemblyView and self._disassemblyView.WindowIsValid(): + return + + if not self._codeView or not self._codeView._window.valid: + return + + if not self._stackTraceView: + return + + if not self._server_capabilities.get( 'supportsDisassembleRequest', False ): + utils.UserMessage( "Sorry, server doesn't support that" ) + return + + with utils.LetCurrentWindow( self._codeView._window ): + vim.command( f'rightbelow { settings.Int( "disassembly_height" ) }new' ) + self._disassemblyView = disassembly.DisassemblyView( + vim.current.window, + self._api_prefix, + self._render_emitter ) + + self._breakpoints.SetDisassemblyManager( self._disassemblyView ) + + utils.UpdateSessionWindows( { + 'disassembly': utils.WindowID( vim.current.window, self._uiTab ) + } ) + + self._disassemblyView.SetCurrentFrame( + self._connection, + self._stackTraceView.GetCurrentFrame(), + True ) + + + def OnDisassemblyWindowScrolled( self, win_id ): + if self._disassemblyView: + self._disassemblyView.OnWindowScrolled( win_id ) + + + @CurrentSession() + @IfConnected() + def AddWatch( self, expression ): + self._variablesView.AddWatch( self._connection, + self._stackTraceView.GetCurrentFrame(), + expression ) + + @CurrentSession() + @IfConnected() + def EvaluateConsole( self, expression, verbose ): + self._outputView.Evaluate( self._connection, + self._stackTraceView.GetCurrentFrame(), + expression, + verbose ) + + @CurrentSession() + @IfConnected() + def DeleteWatch( self ): + self._variablesView.DeleteWatch() + + + @CurrentSession() + @IfConnected() + def HoverEvalTooltip( self, winnr, bufnr, lnum, expression, is_hover ): + frame = self._stackTraceView.GetCurrentFrame() + # Check if RIP is in a frame + if frame is None: + self._logger.debug( 'Tooltip: Not in a stack frame' ) + return '' + + # Check if cursor in code window + if winnr == int( self._codeView._window.number ): + return self._variablesView.HoverEvalTooltip( self._connection, + frame, + expression, + is_hover ) + + return self._variablesView.HoverVarWinTooltip( bufnr, + lnum, + is_hover ) + # Return variable aware function + + + @CurrentSession() + def CleanUpTooltip( self ): + return self._variablesView.CleanUpTooltip() + + @IfConnected() + def ExpandFrameOrThread( self ): + self._stackTraceView.ExpandFrameOrThread() + + @IfConnected() + def UpFrame( self ): + self._stackTraceView.UpFrame() + + @IfConnected() + def DownFrame( self ): + self._stackTraceView.DownFrame() + + def ToggleLog( self ): + if self.HasUI(): + return self.ShowOutput( 'Vimspector' ) + + if self._logView and self._logView.WindowIsValid(): + self._logView.Reset() + self._logView = None + return + + if self._logView: + self._logView.Reset() + + # TODO: The UI code is too scattered. Re-organise into a UI class that + # just deals with these things like window layout and custmisattion. + vim.command( f'botright { settings.Int( "bottombar_height" ) }new' ) + win = vim.current.window + self._logView = output.OutputView( win, self._api_prefix ) + self._logView.AddLogFileView() + self._logView.ShowOutput( 'Vimspector' ) + + @RequiresUI() + def ShowOutput( self, category ): + if not self._outputView.WindowIsValid(): + # TODO: The UI code is too scattered. Re-organise into a UI class that + # just deals with these things like window layout and custmisattion. + # currently, this class and the CodeView share some responsibility for + # this and poking into each View class to check its window is valid also + # feels wrong. + with utils.LetCurrentTabpage( self._uiTab ): vim.command( f'botright { settings.Int( "bottombar_height" ) }new' ) - win = vim.current.window - self._logView = output.OutputView( win, self._api_prefix ) - self._logView.AddLogFileView() - self._logView.ShowOutput( 'Vimspector' ) - - @RequiresUI() - def ShowOutput( self, category ): - if not self._outputView.WindowIsValid(): - # TODO: The UI code is too scattered. Re-organise into a UI class that - # just deals with these things like window layout and custmisattion. - # currently, this class and the CodeView share some responsibility for - # this and poking into each View class to check its window is valid also - # feels wrong. - with utils.LetCurrentTabpage( self._uiTab ): - vim.command( - f'botright { settings.Int( "bottombar_height" ) }new' ) - self._outputView.UseWindow( vim.current.window ) - utils.UpdateSessionWindows( { - 'output': utils.WindowID( vim.current.window, self._uiTab ) - } ) - - self._outputView.ShowOutput( category ) - - @RequiresUI( otherwise=[] ) - def GetOutputBuffers( self ): - return self._outputView.GetCategories() - - @CurrentSession() - @IfConnected( otherwise=[] ) - def GetCompletionsSync( self, text_line, column_in_bytes ): - if not self._server_capabilities.get( 'supportsCompletionsRequest' ): - return [] - - response = self._connection.DoRequestSync( { - 'command': 'completions', - 'arguments': { - 'frameId': self._stackTraceView.GetCurrentFrame()[ 'id' ], - # TODO: encoding ? bytes/codepoints - 'text': text_line, - 'column': column_in_bytes - } + self._outputView.UseWindow( vim.current.window ) + utils.UpdateSessionWindows( { + 'output': utils.WindowID( vim.current.window, self._uiTab ) } ) - # TODO: - # - start / length - # - sortText - return response[ 'body' ][ 'targets' ] - - - @CurrentSession() - @IfConnected( otherwise=[] ) - def GetCommandLineCompletions( self, ArgLead, prev_non_keyword_char ): - items = [] - for candidate in self.GetCompletionsSync( ArgLead, prev_non_keyword_char ): - label = candidate.get( 'text', candidate[ 'label' ] ) - start = prev_non_keyword_char - 1 - if 'start' in candidate and 'length' in candidate: - start = candidate[ 'start' ] - items.append( ArgLead[ 0 : start ] + label ) - - return items - - - @ParentOnly() - def RefreshSigns( self ): - if self._connection: - self._codeView.Refresh() - self._breakpoints.Refresh() - - - @ParentOnly() - def _SetUpUI( self ): - vim.command( '$tab split' ) - - # Switch to this session now that we've made it visible. Note that the - # TabEnter autocmd does trigger when the above is run, but that's before the - # following line assigns the tab to this session, so when we try to find - # this session by tab number, it's not found. So we have to manually switch - # to it when creating a new tab. - utils.Call( 'vimspector#internal#state#SwitchToSession', - self.session_id ) - - self._uiTab = vim.current.tabpage - - mode = settings.Get( 'ui_mode' ) - - if mode == 'auto': - # Go vertical if there isn't enough horizontal space for at least: - # the left bar width - # + the code min width - # + the terminal min width - # + enough space for a sign column and number column? - min_width = ( settings.Int( 'sidebar_width' ) - + 1 + 2 + 3 - + settings.Int( 'code_minwidth' ) - + 1 + settings.Int( 'terminal_minwidth' ) ) - - min_height = ( settings.Int( 'code_minheight' ) + 1 + - settings.Int( 'topbar_height' ) + 1 + - settings.Int( 'bottombar_height' ) + 1 + - 2 ) - - mode = ( 'vertical' - if vim.options[ 'columns' ] < min_width - else 'horizontal' ) - - if vim.options[ 'lines' ] < min_height: - mode = 'horizontal' - - self._logger.debug( 'min_width/height: %s/%s, actual: %s/%s - result: %s', - min_width, - min_height, - vim.options[ 'columns' ], - vim.options[ 'lines' ], - mode ) - - if mode == 'vertical': - self._SetUpUIVertical() - else: - self._SetUpUIHorizontal() - - - def _SetUpUIHorizontal( self ): - # Code window - code_window = vim.current.window - self._codeView = code.CodeView( self.session_id, - code_window, - self._api_prefix, - self._render_emitter, - self._breakpoints.IsBreakpointPresentAt ) - - # Call stack - vim.command( - f'topleft vertical { settings.Int( "sidebar_width" ) }new' ) - stack_trace_window = vim.current.window - one_third = int( vim.eval( 'winheight( 0 )' ) ) / 3 - self._stackTraceView = stack_trace.StackTraceView( self.session_id, - stack_trace_window ) - - # Watches - vim.command( 'leftabove new' ) - watch_window = vim.current.window - - # Variables - vim.command( 'leftabove new' ) - vars_window = vim.current.window - - with utils.LetCurrentWindow( vars_window ): - vim.command( f'{ one_third }wincmd _' ) - with utils.LetCurrentWindow( watch_window ): - vim.command( f'{ one_third }wincmd _' ) - with utils.LetCurrentWindow( stack_trace_window ): - vim.command( f'{ one_third }wincmd _' ) - - self._variablesView = variables.VariablesView( self.session_id, - vars_window, - watch_window ) - - # Output/logging - vim.current.window = code_window - vim.command( f'rightbelow { settings.Int( "bottombar_height" ) }new' ) - output_window = vim.current.window - self._outputView = output.DAPOutputView( output_window, - self._api_prefix, - session_id = self.session_id ) - - utils.SetSessionWindows( { - 'mode': 'horizontal', - 'tabpage': self._uiTab.number, - 'code': utils.WindowID( code_window, self._uiTab ), - 'stack_trace': utils.WindowID( stack_trace_window, self._uiTab ), - 'variables': utils.WindowID( vars_window, self._uiTab ), - 'watches': utils.WindowID( watch_window, self._uiTab ), - 'output': utils.WindowID( output_window, self._uiTab ), - 'eval': None, # updated every time eval popup is opened - 'breakpoints': vim.vars[ 'vimspector_session_windows' ].get( - 'breakpoints' ) # same as above, but for breakpoints - } ) - with utils.RestoreCursorPosition(): - with utils.RestoreCurrentWindow(): - with utils.RestoreCurrentBuffer( vim.current.window ): - vim.command( 'doautocmd User VimspectorUICreated' ) - - - def _SetUpUIVertical( self ): - # Code window - code_window = vim.current.window - self._codeView = code.CodeView( self.session_id, - code_window, - self._api_prefix, - self._render_emitter, - self._breakpoints.IsBreakpointPresentAt ) - - # Call stack - vim.command( - f'topleft { settings.Int( "topbar_height" ) }new' ) - stack_trace_window = vim.current.window - one_third = int( vim.eval( 'winwidth( 0 )' ) ) / 3 - self._stackTraceView = stack_trace.StackTraceView( self.session_id, - stack_trace_window ) - - - # Watches - vim.command( 'leftabove vertical new' ) - watch_window = vim.current.window - - # Variables - vim.command( 'leftabove vertical new' ) - vars_window = vim.current.window - - - with utils.LetCurrentWindow( vars_window ): - vim.command( f'{ one_third }wincmd |' ) - with utils.LetCurrentWindow( watch_window ): - vim.command( f'{ one_third }wincmd |' ) - with utils.LetCurrentWindow( stack_trace_window ): - vim.command( f'{ one_third }wincmd |' ) - - self._variablesView = variables.VariablesView( self.session_id, - vars_window, - watch_window ) - - - # Output/logging - vim.current.window = code_window - vim.command( f'rightbelow { settings.Int( "bottombar_height" ) }new' ) - output_window = vim.current.window - self._outputView = output.DAPOutputView( output_window, - self._api_prefix, - session_id = self.session_id ) - - utils.SetSessionWindows( { - 'mode': 'vertical', - 'tabpage': self._uiTab.number, - 'code': utils.WindowID( code_window, self._uiTab ), - 'stack_trace': utils.WindowID( stack_trace_window, self._uiTab ), - 'variables': utils.WindowID( vars_window, self._uiTab ), - 'watches': utils.WindowID( watch_window, self._uiTab ), - 'output': utils.WindowID( output_window, self._uiTab ), - 'eval': None, # updated every time eval popup is opened - 'breakpoints': vim.vars[ 'vimspector_session_windows' ].get( - 'breakpoints' ) # same as above, but for breakpoints - } ) - with utils.RestoreCursorPosition(): - with utils.RestoreCurrentWindow(): - with utils.RestoreCurrentBuffer( vim.current.window ): - vim.command( 'doautocmd User VimspectorUICreated' ) - - - @RequiresUI() - def ClearCurrentFrame( self ): - self.SetCurrentFrame( None ) - - - def ClearCurrentPC( self ): - self._codeView.SetCurrentFrame( None, False ) - if self._disassemblyView: - self._disassemblyView.SetCurrentFrame( None, None, False ) - - - @RequiresUI() - def SetCurrentFrame( self, frame, reason = '' ): - if not frame: - self._variablesView.Clear() - - target = self._codeView - if self._disassemblyView and self._disassemblyView.IsCurrent(): - target = self._disassemblyView - - if not self._codeView.SetCurrentFrame( frame, - target == self._codeView ): - return False - - if self._disassemblyView: - self._disassemblyView.SetCurrentFrame( self._connection, - frame, - target == self._disassemblyView ) - - # the codeView.SetCurrentFrame already checked the frame was valid and - # countained a valid source - assert frame - if self._codeView.current_syntax not in ( 'ON', 'OFF' ): - self._variablesView.SetSyntax( self._codeView.current_syntax ) - self._stackTraceView.SetSyntax( self._codeView.current_syntax ) - else: - self._variablesView.SetSyntax( None ) - self._stackTraceView.SetSyntax( None ) - - self._variablesView.LoadScopes( self._connection, frame ) - self._variablesView.EvaluateWatches( self._connection, frame ) - - if reason == 'stopped': - self._breakpoints.ClearTemporaryBreakpoint( frame[ 'source' ][ 'path' ], - frame[ 'line' ] ) - - return True - def _StartDebugAdapter( self ): - self._splash_screen = utils.DisplaySplash( - self._api_prefix, - self._splash_screen, - f"Starting debug adapter for session {self.DisplayName()}..." ) - - if self._connection: - utils.UserMessage( 'The connection is already created. Please try again', - persist = True ) - return False - - self._logger.info( 'Starting debug adapter with: %s', - json.dumps( self._adapter ) ) - - self._init_complete = False - self._launch_complete = False - self._run_on_server_exit = None - - self._connection_type = 'job' - if 'port' in self._adapter: - self._connection_type = 'channel' - - if self._adapter[ 'port' ] == 'ask': - port = utils.AskForInput( 'Enter port to connect to: ' ) - if port is None: - self._Reset() - return False - self._adapter[ 'port' ] = port - - self._connection_type = self._api_prefix + self._connection_type - self._logger.debug( f"Connection Type: { self._connection_type }" ) - - self._adapter[ 'env' ] = self._adapter.get( 'env', {} ) - - if 'cwd' in self._configuration: - self._adapter[ 'cwd' ] = self._configuration[ 'cwd' ] - elif 'cwd' not in self._adapter: - self._adapter[ 'cwd' ] = os.getcwd() - - vim.vars[ '_vimspector_adapter_spec' ] = self._adapter - - # if the debug adapter is lame and requires a terminal or has any - # input/output on stdio, then launch it that way - if self._adapter.get( 'tty', False ): - if 'port' not in self._adapter: - utils.UserMessage( "Invalid adapter configuration. When using a tty, " - "communication must use socket. Add the 'port' to " - "the adapter config." ) - return False - - if 'command' not in self._adapter: - utils.UserMessage( "Invalid adapter configuration. When using a tty, " - "a command must be supplied. Add the 'command' to " - "the adapter config." ) - return False - - command = self._adapter[ 'command' ] - if isinstance( command, str ): - command = shlex.split( command ) - - self._adapter_term = terminal.LaunchTerminal( - self._api_prefix, - { - 'args': command, - 'cwd': self._adapter[ 'cwd' ], - 'env': self._adapter[ 'env' ], - }, - self._codeView._window, - self._adapter_term ) - - if not vim.eval( "vimspector#internal#{}#StartDebugSession( " - " {}," - " g:_vimspector_adapter_spec " - ")".format( self._connection_type, - self.session_id ) ): - self._logger.error( "Unable to start debug server" ) - self._splash_screen = utils.DisplaySplash( - self._api_prefix, - self._splash_screen, - [ - "Unable to start or connect to debug adapter", - "", - "Check :messages and :VimspectorToggleLog for more information.", - "", - ":VimspectorReset to close down vimspector", - ] ) - return False - else: - handlers = [ self ] - if 'custom_handler' in self._adapter: - spec = self._adapter[ 'custom_handler' ] - if isinstance( spec, dict ): - module = spec[ 'module' ] - cls = spec[ 'class' ] - else: - module, cls = spec.rsplit( '.', 1 ) - - try: - CustomHandler = getattr( - importlib.import_module( module ), cls ) - handlers = [ CustomHandler( self ), self ] - except ImportError: - self._logger.exception( "Unable to load custom adapter %s", - spec ) - - self._connection = debug_adapter_connection.DebugAdapterConnection( - handlers = handlers, - session_id = self.session_id, - send_func = lambda msg: utils.Call( - "vimspector#internal#{}#Send".format( self._connection_type ), - self.session_id, - msg ), - sync_timeout = self._adapter.get( 'sync_timeout' ), - async_timeout = self._adapter.get( 'async_timeout' ) ) - - self._logger.info( 'Debug Adapter Started' ) - return True - - def _StopDebugAdapter( self, terminateDebuggee, callback ): - arguments = {} - - if terminateDebuggee is not None: - arguments[ 'terminateDebuggee' ] = terminateDebuggee - - self._splash_screen = utils.DisplaySplash( + self._outputView.ShowOutput( category ) + + @RequiresUI( otherwise=[] ) + def GetOutputBuffers( self ): + return self._outputView.GetCategories() + + @CurrentSession() + @IfConnected( otherwise=[] ) + def GetCompletionsSync( self, text_line, column_in_bytes ): + if not self._server_capabilities.get( 'supportsCompletionsRequest' ): + return [] + + response = self._connection.DoRequestSync( { + 'command': 'completions', + 'arguments': { + 'frameId': self._stackTraceView.GetCurrentFrame()[ 'id' ], + # TODO: encoding ? bytes/codepoints + 'text': text_line, + 'column': column_in_bytes + } + } ) + # TODO: + # - start / length + # - sortText + return response[ 'body' ][ 'targets' ] + + + @CurrentSession() + @IfConnected( otherwise=[] ) + def GetCommandLineCompletions( self, ArgLead, prev_non_keyword_char ): + items = [] + for candidate in self.GetCompletionsSync( ArgLead, prev_non_keyword_char ): + label = candidate.get( 'text', candidate[ 'label' ] ) + start = prev_non_keyword_char - 1 + if 'start' in candidate and 'length' in candidate: + start = candidate[ 'start' ] + items.append( ArgLead[ 0 : start ] + label ) + + return items + + + @ParentOnly() + def RefreshSigns( self ): + if self._connection: + self._codeView.Refresh() + self._breakpoints.Refresh() + + + @ParentOnly() + def _SetUpUI( self ): + vim.command( '$tab split' ) + + # Switch to this session now that we've made it visible. Note that the + # TabEnter autocmd does trigger when the above is run, but that's before the + # following line assigns the tab to this session, so when we try to find + # this session by tab number, it's not found. So we have to manually switch + # to it when creating a new tab. + utils.Call( 'vimspector#internal#state#SwitchToSession', + self.session_id ) + + self._uiTab = vim.current.tabpage + + mode = settings.Get( 'ui_mode' ) + + if mode == 'auto': + # Go vertical if there isn't enough horizontal space for at least: + # the left bar width + # + the code min width + # + the terminal min width + # + enough space for a sign column and number column? + min_width = ( settings.Int( 'sidebar_width' ) + + 1 + 2 + 3 + + settings.Int( 'code_minwidth' ) + + 1 + settings.Int( 'terminal_minwidth' ) ) + + min_height = ( settings.Int( 'code_minheight' ) + 1 + + settings.Int( 'topbar_height' ) + 1 + + settings.Int( 'bottombar_height' ) + 1 + + 2 ) + + mode = ( 'vertical' + if vim.options[ 'columns' ] < min_width + else 'horizontal' ) + + if vim.options[ 'lines' ] < min_height: + mode = 'horizontal' + + self._logger.debug( 'min_width/height: %s/%s, actual: %s/%s - result: %s', + min_width, + min_height, + vim.options[ 'columns' ], + vim.options[ 'lines' ], + mode ) + + if mode == 'vertical': + self._SetUpUIVertical() + else: + self._SetUpUIHorizontal() + + + def _SetUpUIHorizontal( self ): + # Code window + code_window = vim.current.window + self._codeView = code.CodeView( self.session_id, + code_window, + self._api_prefix, + self._render_emitter, + self._breakpoints.IsBreakpointPresentAt ) + + # Call stack + vim.command( + f'topleft vertical { settings.Int( "sidebar_width" ) }new' ) + stack_trace_window = vim.current.window + one_third = int( vim.eval( 'winheight( 0 )' ) ) / 3 + self._stackTraceView = stack_trace.StackTraceView( self.session_id, + stack_trace_window ) + + # Watches + vim.command( 'leftabove new' ) + watch_window = vim.current.window + + # Variables + vim.command( 'leftabove new' ) + vars_window = vim.current.window + + with utils.LetCurrentWindow( vars_window ): + vim.command( f'{ one_third }wincmd _' ) + with utils.LetCurrentWindow( watch_window ): + vim.command( f'{ one_third }wincmd _' ) + with utils.LetCurrentWindow( stack_trace_window ): + vim.command( f'{ one_third }wincmd _' ) + + self._variablesView = variables.VariablesView( self.session_id, + vars_window, + watch_window ) + + # Output/logging + vim.current.window = code_window + vim.command( f'rightbelow { settings.Int( "bottombar_height" ) }new' ) + output_window = vim.current.window + self._outputView = output.DAPOutputView( output_window, + self._api_prefix, + session_id = self.session_id ) + + utils.SetSessionWindows( { + 'mode': 'horizontal', + 'tabpage': self._uiTab.number, + 'code': utils.WindowID( code_window, self._uiTab ), + 'stack_trace': utils.WindowID( stack_trace_window, self._uiTab ), + 'variables': utils.WindowID( vars_window, self._uiTab ), + 'watches': utils.WindowID( watch_window, self._uiTab ), + 'output': utils.WindowID( output_window, self._uiTab ), + 'eval': None, # updated every time eval popup is opened + 'breakpoints': vim.vars[ 'vimspector_session_windows' ].get( + 'breakpoints' ) # same as above, but for breakpoints + } ) + with utils.RestoreCursorPosition(): + with utils.RestoreCurrentWindow(): + with utils.RestoreCurrentBuffer( vim.current.window ): + vim.command( 'doautocmd User VimspectorUICreated' ) + + + def _SetUpUIVertical( self ): + # Code window + code_window = vim.current.window + self._codeView = code.CodeView( self.session_id, + code_window, + self._api_prefix, + self._render_emitter, + self._breakpoints.IsBreakpointPresentAt ) + + # Call stack + vim.command( + f'topleft { settings.Int( "topbar_height" ) }new' ) + stack_trace_window = vim.current.window + one_third = int( vim.eval( 'winwidth( 0 )' ) ) / 3 + self._stackTraceView = stack_trace.StackTraceView( self.session_id, + stack_trace_window ) + + + # Watches + vim.command( 'leftabove vertical new' ) + watch_window = vim.current.window + + # Variables + vim.command( 'leftabove vertical new' ) + vars_window = vim.current.window + + + with utils.LetCurrentWindow( vars_window ): + vim.command( f'{ one_third }wincmd |' ) + with utils.LetCurrentWindow( watch_window ): + vim.command( f'{ one_third }wincmd |' ) + with utils.LetCurrentWindow( stack_trace_window ): + vim.command( f'{ one_third }wincmd |' ) + + self._variablesView = variables.VariablesView( self.session_id, + vars_window, + watch_window ) + + + # Output/logging + vim.current.window = code_window + vim.command( f'rightbelow { settings.Int( "bottombar_height" ) }new' ) + output_window = vim.current.window + self._outputView = output.DAPOutputView( output_window, + self._api_prefix, + session_id = self.session_id ) + + utils.SetSessionWindows( { + 'mode': 'vertical', + 'tabpage': self._uiTab.number, + 'code': utils.WindowID( code_window, self._uiTab ), + 'stack_trace': utils.WindowID( stack_trace_window, self._uiTab ), + 'variables': utils.WindowID( vars_window, self._uiTab ), + 'watches': utils.WindowID( watch_window, self._uiTab ), + 'output': utils.WindowID( output_window, self._uiTab ), + 'eval': None, # updated every time eval popup is opened + 'breakpoints': vim.vars[ 'vimspector_session_windows' ].get( + 'breakpoints' ) # same as above, but for breakpoints + } ) + with utils.RestoreCursorPosition(): + with utils.RestoreCurrentWindow(): + with utils.RestoreCurrentBuffer( vim.current.window ): + vim.command( 'doautocmd User VimspectorUICreated' ) + + + @RequiresUI() + def ClearCurrentFrame( self ): + self.SetCurrentFrame( None ) + + + def ClearCurrentPC( self ): + self._codeView.SetCurrentFrame( None, False ) + if self._disassemblyView: + self._disassemblyView.SetCurrentFrame( None, None, False ) + + + @RequiresUI() + def SetCurrentFrame( self, frame, reason = '' ): + if not frame: + self._variablesView.Clear() + + target = self._codeView + if self._disassemblyView and self._disassemblyView.IsCurrent(): + target = self._disassemblyView + + if not self._codeView.SetCurrentFrame( frame, + target == self._codeView ): + return False + + if self._disassemblyView: + self._disassemblyView.SetCurrentFrame( self._connection, + frame, + target == self._disassemblyView ) + + # the codeView.SetCurrentFrame already checked the frame was valid and + # countained a valid source + assert frame + if self._codeView.current_syntax not in ( 'ON', 'OFF' ): + self._variablesView.SetSyntax( self._codeView.current_syntax ) + self._stackTraceView.SetSyntax( self._codeView.current_syntax ) + else: + self._variablesView.SetSyntax( None ) + self._stackTraceView.SetSyntax( None ) + + self._variablesView.LoadScopes( self._connection, frame ) + self._variablesView.EvaluateWatches( self._connection, frame ) + + if reason == 'stopped': + self._breakpoints.ClearTemporaryBreakpoint( frame[ 'source' ][ 'path' ], + frame[ 'line' ] ) + + return True + + def _StartDebugAdapter( self ): + self._splash_screen = utils.DisplaySplash( + self._api_prefix, + self._splash_screen, + f"Starting debug adapter for session {self.DisplayName()}..." ) + + if self._connection: + utils.UserMessage( 'The connection is already created. Please try again', + persist = True ) + return False + + self._logger.info( 'Starting debug adapter with: %s', + json.dumps( self._adapter ) ) + + self._init_complete = False + self._launch_complete = False + self._run_on_server_exit = None + + self._connection_type = 'job' + if 'port' in self._adapter: + self._connection_type = 'channel' + + if self._adapter[ 'port' ] == 'ask': + port = utils.AskForInput( 'Enter port to connect to: ' ) + if port is None: + self._Reset() + return False + self._adapter[ 'port' ] = port + + self._connection_type = self._api_prefix + self._connection_type + self._logger.debug( f"Connection Type: { self._connection_type }" ) + + self._adapter[ 'env' ] = self._adapter.get( 'env', {} ) + + if 'cwd' in self._configuration: + self._adapter[ 'cwd' ] = self._configuration[ 'cwd' ] + elif 'cwd' not in self._adapter: + self._adapter[ 'cwd' ] = os.getcwd() + + vim.vars[ '_vimspector_adapter_spec' ] = self._adapter + + # if the debug adapter is lame and requires a terminal or has any + # input/output on stdio, then launch it that way + if self._adapter.get( 'tty', False ): + if 'port' not in self._adapter: + utils.UserMessage( "Invalid adapter configuration. When using a tty, " + "communication must use socket. Add the 'port' to " + "the adapter config." ) + return False + + if 'command' not in self._adapter: + utils.UserMessage( "Invalid adapter configuration. When using a tty, " + "a command must be supplied. Add the 'command' to " + "the adapter config." ) + return False + + command = self._adapter[ 'command' ] + if isinstance( command, str ): + command = shlex.split( command ) + + self._adapter_term = terminal.LaunchTerminal( self._api_prefix, - self._splash_screen, - f"Shutting down debug adapter for session {self.DisplayName()}..." ) - - def handler( *args ): - self._splash_screen = utils.HideSplash( self._api_prefix, - self._splash_screen ) - - if callback: - self._logger.debug( - "Setting server exit handler before disconnect" ) - assert not self._run_on_server_exit - self._run_on_server_exit = callback - - vim.eval( 'vimspector#internal#{}#StopDebugSession( {} )'.format( - self._connection_type, - self.session_id ) ) - - self._connection.DoRequest( - handler, { - 'command': 'disconnect', - 'arguments': arguments, + 'args': command, + 'cwd': self._adapter[ 'cwd' ], + 'env': self._adapter[ 'env' ], }, - failure_handler = handler, - timeout = self._connection.sync_timeout ) - - - def _ConfirmTerminateDebugee( self, then ): - def handle_choice( choice ): - terminateDebuggee = None - if choice == 1: - # yes - terminateDebuggee = True - elif choice == 2: - # no - terminateDebuggee = False - elif choice <= 0: - # Abort - return - # Else, use server default - - then( terminateDebuggee ) - - utils.Confirm( self._api_prefix, - "Terminate debuggee?", - handle_choice, - default_value = 3, - options = [ '(Y)es', '(N)o', '(D)efault' ], - keys = [ 'y', 'n', 'd' ] ) - - def _PrepareAttach( self, adapter_config, launch_config ): - attach_config = adapter_config.get( 'attach' ) - - if not attach_config: - return - - if 'remote' in attach_config: - # FIXME: We almost want this to feed-back variables to be expanded later, - # e.g. expand variables when we use them, not all at once. This would - # remove the whole %PID% hack. - remote = attach_config[ 'remote' ] - remote_exec_cmd = self._GetRemoteExecCommand( remote ) - - # FIXME: Why does this not use self._GetCommands ? - pid_cmd = remote_exec_cmd + remote[ 'pidCommand' ] - - self._logger.debug( 'Getting PID: %s', pid_cmd ) - pid = subprocess.check_output( pid_cmd ).decode( 'utf-8' ).strip() - self._logger.debug( 'Got PID: %s', pid ) - - if not pid: - # FIXME: We should raise an exception here or something - utils.UserMessage( 'Unable to get PID', persist = True ) - return - - if 'initCompleteCommand' in remote: - initcmd = remote_exec_cmd + remote[ 'initCompleteCommand' ][ : ] - for index, item in enumerate( initcmd ): - initcmd[ index ] = item.replace( '%PID%', pid ) - - self._on_init_complete_handlers.append( - lambda: subprocess.check_call( initcmd ) ) - - commands = self._GetCommands( remote, 'attach' ) - - for command in commands: - cmd = remote_exec_cmd + command - - for index, item in enumerate( cmd ): - cmd[ index ] = item.replace( '%PID%', pid ) - - self._logger.debug( 'Running remote app: %s', cmd ) - self._remote_term = terminal.LaunchTerminal( - self._api_prefix, - { - 'args': cmd, - 'cwd': os.getcwd() - }, - self._codeView._window, - self._remote_term ) - else: - if attach_config[ 'pidSelect' ] == 'ask': - prop = attach_config[ 'pidProperty' ] - if prop not in launch_config: - # NOTE: We require that any custom picker process handles no-arguments - # as well as any arguments supplied in the config. - pid = _SelectProcess() - if pid is None: - return - launch_config[ prop ] = pid - return - elif attach_config[ 'pidSelect' ] == 'none': - return - - raise ValueError( 'Unrecognised pidSelect {0}'.format( - attach_config[ 'pidSelect' ] ) ) - - if 'delay' in attach_config: - utils.UserMessage( f"Waiting ( { attach_config[ 'delay' ] } )..." ) - vim.command( f'sleep { attach_config[ "delay" ] }' ) - - - def _PrepareLaunch( self, command_line, adapter_config, launch_config ): - run_config = adapter_config.get( 'launch', {} ) - - if 'remote' in run_config: - remote = run_config[ 'remote' ] - remote_exec_cmd = self._GetRemoteExecCommand( remote ) - commands = self._GetCommands( remote, 'run' ) - - for index, command in enumerate( commands ): - cmd = remote_exec_cmd + command[ : ] - full_cmd = [] - for item in cmd: - if isinstance( command_line, list ): - if item == '%CMD%': - full_cmd.extend( command_line ) - else: - full_cmd.append( item ) - else: - full_cmd.append( item.replace( '%CMD%', command_line ) ) - - self._logger.debug( 'Running remote app: %s', full_cmd ) - self._remote_term = terminal.LaunchTerminal( - self._api_prefix, - { - 'args': full_cmd, - 'cwd': os.getcwd() - }, - self._codeView._window, - self._remote_term ) - - if 'delay' in run_config: - utils.UserMessage( f"Waiting ( {run_config[ 'delay' ]} )..." ) - vim.command( f'sleep { run_config[ "delay" ] }' ) - - - - def _GetSSHCommand( self, remote ): - ssh_config = remote.get( 'ssh', {} ) - ssh = ssh_config.get( 'cmd', [ 'ssh' ] ) + ssh_config.get( 'args', [] ) - - if 'account' in remote: - ssh.append( remote[ 'account' ] + '@' + remote[ 'host' ] ) - else: - ssh.append( remote[ 'host' ] ) - - return ssh - - def _GetShellCommand( self ): - return [] - - def _GetDockerCommand( self, remote ): - docker = [ 'docker', 'exec', '-t' ] - if 'workdir' in remote: - docker.extend(["-w", remote['workdir']]) - docker.append( remote[ 'container' ] ) - return docker - - def _GetRemoteExecCommand( self, remote ): - is_ssh_cmd = any( key in remote for key in [ 'ssh', - 'host', - 'account', ] ) - is_docker_cmd = 'container' in remote - - if is_ssh_cmd: - return self._GetSSHCommand( remote ) - elif is_docker_cmd: - return self._GetDockerCommand( remote ) + self._codeView._window, + self._adapter_term ) + + if not vim.eval( "vimspector#internal#{}#StartDebugSession( " + " {}," + " g:_vimspector_adapter_spec " + ")".format( self._connection_type, + self.session_id ) ): + self._logger.error( "Unable to start debug server" ) + self._splash_screen = utils.DisplaySplash( + self._api_prefix, + self._splash_screen, + [ + "Unable to start or connect to debug adapter", + "", + "Check :messages and :VimspectorToggleLog for more information.", + "", + ":VimspectorReset to close down vimspector", + ] ) + return False + else: + handlers = [ self ] + if 'custom_handler' in self._adapter: + spec = self._adapter[ 'custom_handler' ] + if isinstance( spec, dict ): + module = spec[ 'module' ] + cls = spec[ 'class' ] else: - # if it's neither docker nor ssh, run locally - return self._GetShellCommand() - + module, cls = spec.rsplit( '.', 1 ) - def _GetCommands( self, remote, pfx ): - commands = remote.get( pfx + 'Commands', None ) + try: + CustomHandler = getattr( importlib.import_module( module ), cls ) + handlers = [ CustomHandler( self ), self ] + except ImportError: + self._logger.exception( "Unable to load custom adapter %s", + spec ) + + self._connection = debug_adapter_connection.DebugAdapterConnection( + handlers = handlers, + session_id = self.session_id, + send_func = lambda msg: utils.Call( + "vimspector#internal#{}#Send".format( self._connection_type ), + self.session_id, + msg ), + sync_timeout = self._adapter.get( 'sync_timeout' ), + async_timeout = self._adapter.get( 'async_timeout' ) ) + + self._logger.info( 'Debug Adapter Started' ) + return True + + def _StopDebugAdapter( self, terminateDebuggee, callback ): + arguments = {} + + if terminateDebuggee is not None: + arguments[ 'terminateDebuggee' ] = terminateDebuggee + + self._splash_screen = utils.DisplaySplash( + self._api_prefix, + self._splash_screen, + f"Shutting down debug adapter for session {self.DisplayName()}..." ) + + def handler( *args ): + self._splash_screen = utils.HideSplash( self._api_prefix, + self._splash_screen ) + + if callback: + self._logger.debug( "Setting server exit handler before disconnect" ) + assert not self._run_on_server_exit + self._run_on_server_exit = callback + + vim.eval( 'vimspector#internal#{}#StopDebugSession( {} )'.format( + self._connection_type, + self.session_id ) ) + + self._connection.DoRequest( + handler, + { + 'command': 'disconnect', + 'arguments': arguments, + }, + failure_handler = handler, + timeout = self._connection.sync_timeout ) + + + def _ConfirmTerminateDebugee( self, then ): + def handle_choice( choice ): + terminateDebuggee = None + if choice == 1: + # yes + terminateDebuggee = True + elif choice == 2: + # no + terminateDebuggee = False + elif choice <= 0: + # Abort + return + # Else, use server default + + then( terminateDebuggee ) + + utils.Confirm( self._api_prefix, + "Terminate debuggee?", + handle_choice, + default_value = 3, + options = [ '(Y)es', '(N)o', '(D)efault' ], + keys = [ 'y', 'n', 'd' ] ) + + def _PrepareAttach( self, adapter_config, launch_config ): + attach_config = adapter_config.get( 'attach' ) + + if not attach_config: + return + + if 'remote' in attach_config: + # FIXME: We almost want this to feed-back variables to be expanded later, + # e.g. expand variables when we use them, not all at once. This would + # remove the whole %PID% hack. + remote = attach_config[ 'remote' ] + remote_exec_cmd = self._GetRemoteExecCommand( remote ) + + # FIXME: Why does this not use self._GetCommands ? + pid_cmd = remote_exec_cmd + remote[ 'pidCommand' ] + + self._logger.debug( 'Getting PID: %s', pid_cmd ) + pid = subprocess.check_output( pid_cmd ).decode( 'utf-8' ).strip() + self._logger.debug( 'Got PID: %s', pid ) + + if not pid: + # FIXME: We should raise an exception here or something + utils.UserMessage( 'Unable to get PID', persist = True ) + return + + if 'initCompleteCommand' in remote: + initcmd = remote_exec_cmd + remote[ 'initCompleteCommand' ][ : ] + for index, item in enumerate( initcmd ): + initcmd[ index ] = item.replace( '%PID%', pid ) + + self._on_init_complete_handlers.append( + lambda: subprocess.check_call( initcmd ) ) + + commands = self._GetCommands( remote, 'attach' ) + + for command in commands: + cmd = remote_exec_cmd + command + + for index, item in enumerate( cmd ): + cmd[ index ] = item.replace( '%PID%', pid ) + + self._logger.debug( 'Running remote app: %s', cmd ) + self._remote_term = terminal.LaunchTerminal( + self._api_prefix, + { + 'args': cmd, + 'cwd': os.getcwd() + }, + self._codeView._window, + self._remote_term ) + else: + if attach_config[ 'pidSelect' ] == 'ask': + prop = attach_config[ 'pidProperty' ] + if prop not in launch_config: + # NOTE: We require that any custom picker process handles no-arguments + # as well as any arguments supplied in the config. + pid = _SelectProcess() + if pid is None: + return + launch_config[ prop ] = pid + return + elif attach_config[ 'pidSelect' ] == 'none': + return + + raise ValueError( 'Unrecognised pidSelect {0}'.format( + attach_config[ 'pidSelect' ] ) ) + + if 'delay' in attach_config: + utils.UserMessage( f"Waiting ( { attach_config[ 'delay' ] } )..." ) + vim.command( f'sleep { attach_config[ "delay" ] }' ) + + + def _PrepareLaunch( self, command_line, adapter_config, launch_config ): + run_config = adapter_config.get( 'launch', {} ) + + if 'remote' in run_config: + remote = run_config[ 'remote' ] + remote_exec_cmd = self._GetRemoteExecCommand( remote ) + commands = self._GetCommands( remote, 'run' ) + + for index, command in enumerate( commands ): + cmd = remote_exec_cmd + command[ : ] + full_cmd = [] + for item in cmd: + if isinstance( command_line, list ): + if item == '%CMD%': + full_cmd.extend( command_line ) + else: + full_cmd.append( item ) + else: + full_cmd.append( item.replace( '%CMD%', command_line ) ) + + self._logger.debug( 'Running remote app: %s', full_cmd ) + self._remote_term = terminal.LaunchTerminal( + self._api_prefix, + { + 'args': full_cmd, + 'cwd': os.getcwd() + }, + self._codeView._window, + self._remote_term ) - if isinstance( commands, list ): - return commands - elif commands is not None: - raise ValueError( "Invalid commands; must be list" ) + if 'delay' in run_config: + utils.UserMessage( f"Waiting ( {run_config[ 'delay' ]} )..." ) + vim.command( f'sleep { run_config[ "delay" ] }' ) - command = remote[ pfx + 'Command' ] - if isinstance( command, str ): - command = shlex.split( command ) - if not isinstance( command, list ): - raise ValueError( "Invalid command; must be list/string" ) + def _GetSSHCommand( self, remote ): + ssh_config = remote.get( 'ssh', {} ) + ssh = ssh_config.get( 'cmd', [ 'ssh' ] ) + ssh_config.get( 'args', [] ) - if not command: - raise ValueError( 'Could not determine commands for ' + pfx ) + if 'account' in remote: + ssh.append( remote[ 'account' ] + '@' + remote[ 'host' ] ) + else: + ssh.append( remote[ 'host' ] ) - return [ command ] + return ssh - def _Initialise( self ): - self._splash_screen = utils.DisplaySplash( - self._api_prefix, - self._splash_screen, - f"Initializing debug session {self.DisplayName()}..." ) - - # For a good explanation as to why this sequence is the way it is, see - # https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522 - # - # In short, we do what VSCode does: - # 1. Send the initialize request and wait for the reply - # 2a. When we receive the initialize reply, send the launch/attach request - # 2b. When we receive the initialized notification, send the breakpoints - # - if supportsConfigurationDoneRequest, send it - # - else, send the empty exception breakpoints request - # 3. When we have received both the receive the launch/attach reply *and* - # the connfiguration done reply (or, if we didn't send one, a response to - # the empty exception breakpoints request), we request threads - # 4. The threads response triggers things like scopes and triggers setting - # the current frame. - # - def handle_initialize_response( msg ): - self._server_capabilities = msg.get( 'body' ) or {} - # TODO/FIXME: We assume that the capabilities are the same for all - # connections. We should fix this when we split the server bp - # representation out? - if not self.parent_session: - self._breakpoints.SetServerCapabilities( - self._server_capabilities ) - self._Launch() - - self._connection.DoRequest( handle_initialize_response, { - 'command': 'initialize', - 'arguments': { - 'adapterID': self._adapter.get( 'name', 'adapter' ), - 'clientID': 'vimspector', - 'clientName': 'vimspector', - 'linesStartAt1': True, - 'columnsStartAt1': True, - 'locale': 'en_GB', - 'pathFormat': 'path', - 'supportsVariableType': True, - 'supportsVariablePaging': False, - 'supportsRunInTerminalRequest': True, - 'supportsMemoryReferences': True, - 'supportsStartDebuggingRequest': True - }, - } ) + def _GetShellCommand( self ): + return [] + def _GetDockerCommand( self, remote ): + docker = [ 'docker', 'exec', '-t' ] + docker.append( remote[ 'container' ] ) + return docker - def OnFailure( self, reason, request, message ): - msg = "Request for '{}' failed: {}\nResponse: {}".format( request, - reason, - message ) - self._outputView.Print( 'server', msg ) + def _GetRemoteExecCommand( self, remote ): + is_ssh_cmd = any( key in remote for key in [ 'ssh', + 'host', + 'account', ] ) + is_docker_cmd = 'container' in remote + if is_ssh_cmd: + return self._GetSSHCommand( remote ) + elif is_docker_cmd: + return self._GetDockerCommand( remote ) + else: + # if it's neither docker nor ssh, run locally + return self._GetShellCommand() + + + def _GetCommands( self, remote, pfx ): + commands = remote.get( pfx + 'Commands', None ) + + if isinstance( commands, list ): + return commands + elif commands is not None: + raise ValueError( "Invalid commands; must be list" ) + + command = remote[ pfx + 'Command' ] + + if isinstance( command, str ): + command = shlex.split( command ) + + if not isinstance( command, list ): + raise ValueError( "Invalid command; must be list/string" ) + + if not command: + raise ValueError( 'Could not determine commands for ' + pfx ) + + return [ command ] + + def _Initialise( self ): + self._splash_screen = utils.DisplaySplash( + self._api_prefix, + self._splash_screen, + f"Initializing debug session {self.DisplayName()}..." ) + + # For a good explanation as to why this sequence is the way it is, see + # https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522 + # + # In short, we do what VSCode does: + # 1. Send the initialize request and wait for the reply + # 2a. When we receive the initialize reply, send the launch/attach request + # 2b. When we receive the initialized notification, send the breakpoints + # - if supportsConfigurationDoneRequest, send it + # - else, send the empty exception breakpoints request + # 3. When we have received both the receive the launch/attach reply *and* + # the connfiguration done reply (or, if we didn't send one, a response to + # the empty exception breakpoints request), we request threads + # 4. The threads response triggers things like scopes and triggers setting + # the current frame. + # + def handle_initialize_response( msg ): + self._server_capabilities = msg.get( 'body' ) or {} + # TODO/FIXME: We assume that the capabilities are the same for all + # connections. We should fix this when we split the server bp + # representation out? + if not self.parent_session: + self._breakpoints.SetServerCapabilities( self._server_capabilities ) + self._Launch() + + self._connection.DoRequest( handle_initialize_response, { + 'command': 'initialize', + 'arguments': { + 'adapterID': self._adapter.get( 'name', 'adapter' ), + 'clientID': 'vimspector', + 'clientName': 'vimspector', + 'linesStartAt1': True, + 'columnsStartAt1': True, + 'locale': 'en_GB', + 'pathFormat': 'path', + 'supportsVariableType': True, + 'supportsVariablePaging': False, + 'supportsRunInTerminalRequest': True, + 'supportsMemoryReferences': True, + 'supportsStartDebuggingRequest': True + }, + } ) + + + def OnFailure( self, reason, request, message ): + msg = "Request for '{}' failed: {}\nResponse: {}".format( request, + reason, + message ) + self._outputView.Print( 'server', msg ) + + + def _Prepare( self ): + self._on_init_complete_handlers = [] + + self._logger.debug( "LAUNCH!" ) + if self._launch_config is None: + self._launch_config = {} + # TODO: Should we use core_utils.override for this? That would strictly be + # a change in behaviour as dicts in the specific configuration would merge + # with dicts in the adapter, where before they would overlay + self._launch_config.update( self._adapter.get( 'configuration', {} ) ) + self._launch_config.update( self._configuration[ 'configuration' ] ) + + request = self._configuration.get( + 'remote-request', + self._launch_config.get( 'request', 'launch' ) ) + + if request == "attach": + self._splash_screen = utils.DisplaySplash( + self._api_prefix, + self._splash_screen, + f"Attaching to debuggee {self.DisplayName()}..." ) + + self._PrepareAttach( self._adapter, self._launch_config ) + elif request == "launch": + self._splash_screen = utils.DisplaySplash( + self._api_prefix, + self._splash_screen, + f"Launching debuggee {self.DisplayName()}..." ) + + # FIXME: This cmdLine hack is not fun. + self._PrepareLaunch( self._configuration.get( 'remote-cmdLine', [] ), + self._adapter, + self._launch_config ) + + # FIXME: name is mandatory. Forcefully add it (we should really use the + # _actual_ name, but that isn't actually remembered at this point) + if 'name' not in self._launch_config: + self._launch_config[ 'name' ] = 'test' + + + def _Launch( self ): + def failure_handler( reason, msg ): + text = [ + f'Initialize for session {self.DisplayName()} Failed', + '' ] + reason.splitlines() + [ + '', 'Use :VimspectorReset to close' ] + self._logger.info( "Launch failed: %s", '\n'.join( text ) ) + self._splash_screen = utils.DisplaySplash( self._api_prefix, + self._splash_screen, + text ) + + self._connection.DoRequest( + lambda msg: self._OnLaunchComplete(), + { + 'command': self._launch_config[ 'request' ], + 'arguments': self._launch_config + }, + failure_handler ) + + + def _OnLaunchComplete( self ): + self._launch_complete = True + self._LoadThreadsIfReady() + + def _OnInitializeComplete( self ): + self._init_complete = True + self._LoadThreadsIfReady() + + def _LoadThreadsIfReady( self ): + # NOTE: You might think we should only load threads on a stopped event, + # but the spec is clear: + # + # After a successful launch or attach the development tool requests the + # baseline of currently existing threads with the threads request and + # then starts to listen for thread events to detect new or terminated + # threads. + # + # Of course, specs are basically guidelines. MS's own cpptools simply + # doesn't respond top threads request when attaching via gdbserver. At + # least it would appear that way. + # + # As it turns out this is due to a bug in gdbserver which means that + # attachment doesn't work due to sending the signal to the process group + # leader rather than the process. The workaround is to manually SIGTRAP the + # PID. + # + if self._launch_complete and self._init_complete: + self._splash_screen = utils.HideSplash( self._api_prefix, + self._splash_screen ) + + for h in self._on_init_complete_handlers: + h() + self._on_init_complete_handlers = [] + + self._stackTraceView.LoadThreads( self, True ) + + + @CurrentSession() + @IfConnected() + @RequiresUI() + def PrintDebugInfo( self ): + def Line(): + return ( "--------------------------------------------------------------" + "------------------" ) + + def Pretty( obj ): + if obj is None: + return [ "None" ] + return [ Line() ] + json.dumps( obj, indent=2 ).splitlines() + [ Line() ] + + + debugInfo = [ + "Vimspector Debug Info", + Line(), + f"ConnectionType: { self._connection_type }", + "Adapter: " ] + Pretty( self._adapter ) + [ + "Configuration: " ] + Pretty( self._configuration ) + [ + f"API Prefix: { self._api_prefix }", + f"Launch/Init: { self._launch_complete } / { self._init_complete }", + f"Workspace Root: { self._workspace_root }", + "Launch Config: " ] + Pretty( self._launch_config ) + [ + "Server Capabilities: " ] + Pretty( self._server_capabilities ) + [ + "Line Breakpoints: " ] + Pretty( self._breakpoints._line_breakpoints ) + [ + "Func Breakpoints: " ] + Pretty( self._breakpoints._func_breakpoints ) + [ + "Ex Breakpoints: " ] + Pretty( self._breakpoints._exception_breakpoints ) + + self._outputView.ClearCategory( 'DebugInfo' ) + self._outputView.Print( "DebugInfo", debugInfo ) + self.ShowOutput( "DebugInfo" ) + + + def OnEvent_loadedSource( self, msg ): + pass + + + def OnEvent_capabilities( self, msg ): + self._server_capabilities.update( + ( msg.get( 'body' ) or {} ).get( 'capabilities' ) or {} ) + + + def OnEvent_initialized( self, message ): + def OnBreakpointsDone(): + self._breakpoints.Refresh() + if self._server_capabilities.get( 'supportsConfigurationDoneRequest' ): + self._connection.DoRequest( + lambda msg: self._OnInitializeComplete(), + { + 'command': 'configurationDone', + } + ) + else: + self._OnInitializeComplete() - def _Prepare( self ): - self._on_init_complete_handlers = [] + self._breakpoints.SetConfiguredBreakpoints( + self._configuration.get( 'breakpoints', {} ) ) + self._breakpoints.AddConnection( self._connection ) + self._breakpoints.UpdateUI( OnBreakpointsDone ) - self._logger.debug( "LAUNCH!" ) - if self._launch_config is None: - self._launch_config = {} - # TODO: Should we use core_utils.override for this? That would strictly be - # a change in behaviour as dicts in the specific configuration would merge - # with dicts in the adapter, where before they would overlay - self._launch_config.update( - self._adapter.get( 'configuration', {} ) ) - self._launch_config.update( self._configuration[ 'configuration' ] ) - request = self._configuration.get( - 'remote-request', - self._launch_config.get( 'request', 'launch' ) ) + def OnEvent_thread( self, message ): + self._stackTraceView.OnThreadEvent( self, message[ 'body' ] ) - if request == "attach": - self._splash_screen = utils.DisplaySplash( - self._api_prefix, - self._splash_screen, - f"Attaching to debuggee {self.DisplayName()}..." ) - self._PrepareAttach( self._adapter, self._launch_config ) - elif request == "launch": - self._splash_screen = utils.DisplaySplash( - self._api_prefix, - self._splash_screen, - f"Launching debuggee {self.DisplayName()}..." ) - - # FIXME: This cmdLine hack is not fun. - self._PrepareLaunch( self._configuration.get( 'remote-cmdLine', [] ), - self._adapter, - self._launch_config ) - - # FIXME: name is mandatory. Forcefully add it (we should really use the - # _actual_ name, but that isn't actually remembered at this point) - if 'name' not in self._launch_config: - self._launch_config[ 'name' ] = 'test' - - - def _Launch( self ): - def failure_handler( reason, msg ): - text = [ - f'Initialize for session {self.DisplayName()} Failed', - '' ] + reason.splitlines() + [ - '', 'Use :VimspectorReset to close' ] - self._logger.info( "Launch failed: %s", '\n'.join( text ) ) - self._splash_screen = utils.DisplaySplash( self._api_prefix, - self._splash_screen, - text ) + def OnEvent_breakpoint( self, message ): + reason = message[ 'body' ][ 'reason' ] + bp = message[ 'body' ][ 'breakpoint' ] + if reason == 'changed': + self._breakpoints.UpdatePostedBreakpoint( self._connection, bp ) + elif reason == 'new': + self._breakpoints.AddPostedBreakpoint( self._connection, bp ) + elif reason == 'removed': + self._breakpoints.DeletePostedBreakpoint( self._connection, bp ) + else: + utils.UserMessage( + 'Unrecognised breakpoint event (undocumented): {0}'.format( reason ), + persist = True ) + + def OnRequest_runInTerminal( self, message ): + params = message[ 'arguments' ] + + if not params.get( 'cwd' ) : + params[ 'cwd' ] = self._workspace_root + self._logger.debug( 'Defaulting working directory to %s', + params[ 'cwd' ] ) + + term_id = self._codeView.LaunchTerminal( params ) + + response = { + 'processId': int( utils.Call( + 'vimspector#internal#{}term#GetPID'.format( self._api_prefix ), + term_id ) ) + } + + self._connection.DoResponse( message, None, response ) + + def OnEvent_terminated( self, message ): + # The debugging _session_ has terminated. This does not mean that the + # debuggee has terminated (that's the exited event). + # + # We will handle this when the server actually exists. + # + # FIXME we should always wait for this event before disconnecting closing + # any socket connection + # self._stackTraceView.OnTerminated( self ) + self.SetCurrentFrame( None ) + + + def OnEvent_exited( self, message ): + utils.UserMessage( 'The debuggee exited with status code: {}'.format( + message[ 'body' ][ 'exitCode' ] ) ) + self._stackTraceView.OnExited( self, message ) + self.ClearCurrentPC() + + + def OnRequest_startDebugging( self, message ): + self._DoStartDebuggingRequest( message, + message[ 'arguments' ][ 'request' ], + message[ 'arguments' ][ 'configuration' ], + self._adapter ) + + def _DoStartDebuggingRequest( self, + message, + request_type, + launch_arguments, + adapter, + session_name = None ): + + session = self.manager.NewSession( + session_name = session_name or launch_arguments.get( 'name' ), + parent_session = self ) + + # Inject the launch config (HACK!). This will actually mean that the + # configuration passed below is ignored. + session._launch_config = launch_arguments + session._launch_config[ 'request' ] = request_type + + # FIXME: We probably do need to add a StartWithLauncArguments and somehow + # tell the new session that it shoud not support "Restart" requests ? + # + # In fact, what even would Reset do... ? + session._StartWithConfiguration( { 'configuration': launch_arguments }, + adapter ) + + self._connection.DoResponse( message, None, {} ) + + def OnEvent_process( self, message ): + utils.UserMessage( 'debuggee was started: {}'.format( + message[ 'body' ][ 'name' ] ) ) + + def OnEvent_module( self, message ): + pass + + def OnEvent_continued( self, message ): + self._stackTraceView.OnContinued( self, message[ 'body' ] ) + self.ClearCurrentPC() + + @ParentOnly() + def Clear( self ): + self._codeView.Clear() + if self._disassemblyView: + self._disassemblyView.Clear() + self._stackTraceView.Clear() + self._variablesView.Clear() + + def OnServerExit( self, status ): + self._logger.info( "The server has terminated with status %s", + status ) + + if self._connection is not None: + # Can be None if the server dies _before_ StartDebugSession vim function + # returns + self._connection.Reset() + + self._stackTraceView.ConnectionClosed( self ) + self._breakpoints.ConnectionClosed( self._connection ) + self._variablesView.ConnectionClosed( self._connection ) + if self._disassemblyView: + self._disassemblyView.ConnectionClosed( self._connection ) + + self.Clear() + self._ResetServerState() + + if self._run_on_server_exit: + self._logger.debug( "Running server exit handler" ) + callback = self._run_on_server_exit + self._run_on_server_exit = None + callback() + else: + self._logger.debug( "No server exit handler" ) - self._connection.DoRequest( - lambda msg: self._OnLaunchComplete(), - { - 'command': self._launch_config[ 'request' ], - 'arguments': self._launch_config - }, - failure_handler ) - - - def _OnLaunchComplete( self ): - self._launch_complete = True - self._LoadThreadsIfReady() - - def _OnInitializeComplete( self ): - self._init_complete = True - self._LoadThreadsIfReady() - - def _LoadThreadsIfReady( self ): - # NOTE: You might think we should only load threads on a stopped event, - # but the spec is clear: - # - # After a successful launch or attach the development tool requests the - # baseline of currently existing threads with the threads request and - # then starts to listen for thread events to detect new or terminated - # threads. - # - # Of course, specs are basically guidelines. MS's own cpptools simply - # doesn't respond top threads request when attaching via gdbserver. At - # least it would appear that way. - # - # As it turns out this is due to a bug in gdbserver which means that - # attachment doesn't work due to sending the signal to the process group - # leader rather than the process. The workaround is to manually SIGTRAP the - # PID. - # - if self._launch_complete and self._init_complete: - self._splash_screen = utils.HideSplash( self._api_prefix, - self._splash_screen ) - - for h in self._on_init_complete_handlers: - h() - self._on_init_complete_handlers = [] - - self._stackTraceView.LoadThreads( self, True ) - - - @CurrentSession() - @IfConnected() - @RequiresUI() - def PrintDebugInfo( self ): - def Line(): - return ( "--------------------------------------------------------------" - "------------------" ) - - def Pretty( obj ): - if obj is None: - return [ "None" ] - return [ Line() ] + json.dumps( obj, indent=2 ).splitlines() + [ Line() ] - - - debugInfo = [ - "Vimspector Debug Info", - Line(), - f"ConnectionType: { self._connection_type }", - "Adapter: " ] + Pretty( self._adapter ) + [ - "Configuration: " ] + Pretty( self._configuration ) + [ - f"API Prefix: { self._api_prefix }", - f"Launch/Init: { self._launch_complete } / { self._init_complete }", - f"Workspace Root: { self._workspace_root }", - "Launch Config: " ] + Pretty( self._launch_config ) + [ - "Server Capabilities: " ] + Pretty( self._server_capabilities ) + [ - "Line Breakpoints: " ] + Pretty( self._breakpoints._line_breakpoints ) + [ - "Func Breakpoints: " ] + Pretty( self._breakpoints._func_breakpoints ) + [ - "Ex Breakpoints: " ] + Pretty( self._breakpoints._exception_breakpoints ) - - self._outputView.ClearCategory( 'DebugInfo' ) - self._outputView.Print( "DebugInfo", debugInfo ) - self.ShowOutput( "DebugInfo" ) - - - def OnEvent_loadedSource( self, msg ): - pass - - - def OnEvent_capabilities( self, msg ): - self._server_capabilities.update( - ( msg.get( 'body' ) or {} ).get( 'capabilities' ) or {} ) - - - def OnEvent_initialized( self, message ): - def OnBreakpointsDone(): - self._breakpoints.Refresh() - if self._server_capabilities.get( 'supportsConfigurationDoneRequest' ): - self._connection.DoRequest( - lambda msg: self._OnInitializeComplete(), - { - 'command': 'configurationDone', - } - ) - else: - self._OnInitializeComplete() + def OnEvent_output( self, message ): + if self._outputView: + self._outputView.OnOutput( message[ 'body' ] ) - self._breakpoints.SetConfiguredBreakpoints( - self._configuration.get( 'breakpoints', {} ) ) - self._breakpoints.AddConnection( self._connection ) - self._breakpoints.UpdateUI( OnBreakpointsDone ) + def OnEvent_stopped( self, message ): + event = message[ 'body' ] + reason = event.get( 'reason' ) or '' + description = event.get( 'description' ) + text = event.get( 'text' ) + if description: + explanation = description + '(' + reason + ')' + else: + explanation = reason - def OnEvent_thread( self, message ): - self._stackTraceView.OnThreadEvent( self, message[ 'body' ] ) + if text: + explanation += ': ' + text + msg = 'Paused in thread {0} due to {1}'.format( + event.get( 'threadId', '' ), + explanation ) + utils.UserMessage( msg ) - def OnEvent_breakpoint( self, message ): - reason = message[ 'body' ][ 'reason' ] - bp = message[ 'body' ][ 'breakpoint' ] - if reason == 'changed': - self._breakpoints.UpdatePostedBreakpoint( self._connection, bp ) - elif reason == 'new': - self._breakpoints.AddPostedBreakpoint( self._connection, bp ) - elif reason == 'removed': - self._breakpoints.DeletePostedBreakpoint( self._connection, bp ) - else: - utils.UserMessage( - 'Unrecognised breakpoint event (undocumented): {0}'.format( - reason ), - persist = True ) - - def OnRequest_runInTerminal( self, message ): - params = message[ 'arguments' ] - - if not params.get( 'cwd' ) : - params[ 'cwd' ] = self._workspace_root - self._logger.debug( 'Defaulting working directory to %s', - params[ 'cwd' ] ) - - term_id = self._codeView.LaunchTerminal( params ) - - response = { - 'processId': int( utils.Call( - 'vimspector#internal#{}term#GetPID'.format( self._api_prefix ), - term_id ) ) - } - - self._connection.DoResponse( message, None, response ) - - def OnEvent_terminated( self, message ): - # The debugging _session_ has terminated. This does not mean that the - # debuggee has terminated (that's the exited event). - # - # We will handle this when the server actually exists. - # - # FIXME we should always wait for this event before disconnecting closing - # any socket connection - # self._stackTraceView.OnTerminated( self ) - self.SetCurrentFrame( None ) - - - def OnEvent_exited( self, message ): - utils.UserMessage( 'The debuggee exited with status code: {}'.format( - message[ 'body' ][ 'exitCode' ] ) ) - self._stackTraceView.OnExited( self, message ) - self.ClearCurrentPC() - - - def OnRequest_startDebugging( self, message ): - self._DoStartDebuggingRequest( message, - message[ 'arguments' ][ 'request' ], - message[ 'arguments' ][ 'configuration' ], - self._adapter ) - - def _DoStartDebuggingRequest( self, - message, - request_type, - launch_arguments, - adapter, - session_name = None ): - - session = self.manager.NewSession( - session_name = session_name or launch_arguments.get( 'name' ), - parent_session = self ) - - # Inject the launch config (HACK!). This will actually mean that the - # configuration passed below is ignored. - session._launch_config = launch_arguments - session._launch_config[ 'request' ] = request_type - - # FIXME: We probably do need to add a StartWithLauncArguments and somehow - # tell the new session that it shoud not support "Restart" requests ? - # - # In fact, what even would Reset do... ? - session._StartWithConfiguration( { 'configuration': launch_arguments }, - adapter ) - - self._connection.DoResponse( message, None, {} ) - - def OnEvent_process( self, message ): - utils.UserMessage( 'debuggee was started: {}'.format( - message[ 'body' ][ 'name' ] ) ) - - def OnEvent_module( self, message ): - pass - - def OnEvent_continued( self, message ): - self._stackTraceView.OnContinued( self, message[ 'body' ] ) - self.ClearCurrentPC() - - @ParentOnly() - def Clear( self ): - self._codeView.Clear() - if self._disassemblyView: - self._disassemblyView.Clear() - self._stackTraceView.Clear() - self._variablesView.Clear() - - def OnServerExit( self, status ): - self._logger.info( "The server has terminated with status %s", - status ) - - if self._connection is not None: - # Can be None if the server dies _before_ StartDebugSession vim function - # returns - self._connection.Reset() - - self._stackTraceView.ConnectionClosed( self ) - self._breakpoints.ConnectionClosed( self._connection ) - self._variablesView.ConnectionClosed( self._connection ) - if self._disassemblyView: - self._disassemblyView.ConnectionClosed( self._connection ) - - self.Clear() - self._ResetServerState() - - if self._run_on_server_exit: - self._logger.debug( "Running server exit handler" ) - callback = self._run_on_server_exit - self._run_on_server_exit = None - callback() - else: - self._logger.debug( "No server exit handler" ) + if self._outputView: + self._outputView.Print( 'server', msg ) - def OnEvent_output( self, message ): - if self._outputView: - self._outputView.OnOutput( message[ 'body' ] ) + self._stackTraceView.OnStopped( self, event ) - def OnEvent_stopped( self, message ): - event = message[ 'body' ] - reason = event.get( 'reason' ) or '' - description = event.get( 'description' ) - text = event.get( 'text' ) + def BreakpointsAsQuickFix( self ): + return self._breakpoints.BreakpointsAsQuickFix() - if description: - explanation = description + '(' + reason + ')' - else: - explanation = reason + def ListBreakpoints( self ): + self._breakpoints.ToggleBreakpointsView() - if text: - explanation += ': ' + text + def ToggleBreakpointViewBreakpoint( self ): + self._breakpoints.ToggleBreakpointViewBreakpoint() - msg = 'Paused in thread {0} due to {1}'.format( - event.get( 'threadId', '' ), - explanation ) - utils.UserMessage( msg ) + def ToggleAllBreakpointsViewBreakpoint( self ): + self._breakpoints.ToggleAllBreakpointsViewBreakpoint() - if self._outputView: - self._outputView.Print( 'server', msg ) + def DeleteBreakpointViewBreakpoint( self ): + self._breakpoints.ClearBreakpointViewBreakpoint() - self._stackTraceView.OnStopped( self, event ) + def JumpToBreakpointViewBreakpoint( self ): + self._breakpoints.JumpToBreakpointViewBreakpoint() - def BreakpointsAsQuickFix( self ): - return self._breakpoints.BreakpointsAsQuickFix() + def EditBreakpointOptionsViewBreakpoint( self ): + self._breakpoints.EditBreakpointOptionsViewBreakpoint() - def ListBreakpoints( self ): - self._breakpoints.ToggleBreakpointsView() + def JumpToNextBreakpoint( self ): + self._breakpoints.JumpToNextBreakpoint() - def ToggleBreakpointViewBreakpoint( self ): - self._breakpoints.ToggleBreakpointViewBreakpoint() + def JumpToPreviousBreakpoint( self ): + self._breakpoints.JumpToPreviousBreakpoint() - def ToggleAllBreakpointsViewBreakpoint( self ): - self._breakpoints.ToggleAllBreakpointsViewBreakpoint() + def JumpToProgramCounter( self ): + self._stackTraceView.JumpToProgramCounter() - def DeleteBreakpointViewBreakpoint( self ): - self._breakpoints.ClearBreakpointViewBreakpoint() + def ToggleBreakpoint( self, options ): + return self._breakpoints.ToggleBreakpoint( options ) - def JumpToBreakpointViewBreakpoint( self ): - self._breakpoints.JumpToBreakpointViewBreakpoint() - def EditBreakpointOptionsViewBreakpoint( self ): - self._breakpoints.EditBreakpointOptionsViewBreakpoint() + def RunTo( self, file_name, line ): + self._breakpoints.ClearTemporaryBreakpoints() + self._breakpoints.AddTemporaryLineBreakpoint( file_name, + line, + { 'temporary': True }, + lambda: self.Continue() ) - def JumpToNextBreakpoint( self ): - self._breakpoints.JumpToNextBreakpoint() + @CurrentSession() + @IfConnected() + def GoTo( self, file_name, line ): + def failure_handler( reason, *args ): + utils.UserMessage( f"Can't jump to location: {reason}", error=True ) - def JumpToPreviousBreakpoint( self ): - self._breakpoints.JumpToPreviousBreakpoint() + def handle_targets( msg ): + targets = msg.get( 'body', {} ).get( 'targets', [] ) + if not targets: + failure_handler( "No targets" ) + return - def JumpToProgramCounter( self ): - self._stackTraceView.JumpToProgramCounter() + if len( targets ) == 1: + target_selected = 0 + else: + target_selected = utils.SelectFromList( "Which target?", [ + t[ 'label' ] for t in targets + ], ret = 'index' ) - def ToggleBreakpoint( self, options ): - return self._breakpoints.ToggleBreakpoint( options ) + if target_selected is None: + return + self._connection.DoRequest( None, { + 'command': 'goto', + 'arguments': { + 'threadId': self._stackTraceView.GetCurrentThreadId(), + 'targetId': targets[ target_selected ][ 'id' ] + }, + }, failure_handler ) - def RunTo( self, file_name, line ): - self._breakpoints.ClearTemporaryBreakpoints() - self._breakpoints.AddTemporaryLineBreakpoint( file_name, - line, - { 'temporary': True }, - lambda: self.Continue() ) + if not self._server_capabilities.get( 'supportsGotoTargetsRequest', False ): + failure_handler( "Server doesn't support it" ) + return - @CurrentSession() - @IfConnected() - def GoTo( self, file_name, line ): - def failure_handler( reason, *args ): - utils.UserMessage( f"Can't jump to location: {reason}", error=True ) + self._connection.DoRequest( handle_targets, { + 'command': 'gotoTargets', + 'arguments': { + 'source': { + 'path': utils.NormalizePath( file_name ) + }, + 'line': line + }, + }, failure_handler ) - def handle_targets( msg ): - targets = msg.get( 'body', {} ).get( 'targets', [] ) - if not targets: - failure_handler( "No targets" ) - return - if len( targets ) == 1: - target_selected = 0 - else: - target_selected = utils.SelectFromList( "Which target?", [ - t[ 'label' ] for t in targets - ], ret = 'index' ) - - if target_selected is None: - return - - self._connection.DoRequest( None, { - 'command': 'goto', - 'arguments': { - 'threadId': self._stackTraceView.GetCurrentThreadId(), - 'targetId': targets[ target_selected ][ 'id' ] - }, - }, failure_handler ) - - if not self._server_capabilities.get( 'supportsGotoTargetsRequest', False ): - failure_handler( "Server doesn't support it" ) - return + def SetLineBreakpoint( self, file_name, line_num, options, then = None ): + return self._breakpoints.SetLineBreakpoint( file_name, + line_num, + options, + then ) - self._connection.DoRequest( handle_targets, { - 'command': 'gotoTargets', - 'arguments': { - 'source': { - 'path': utils.NormalizePath( file_name ) - }, - 'line': line - }, - }, failure_handler ) + def ClearLineBreakpoint( self, file_name, line_num ): + return self._breakpoints.ClearLineBreakpoint( file_name, line_num ) + def ClearBreakpoints( self ): + return self._breakpoints.ClearBreakpoints() - def SetLineBreakpoint( self, file_name, line_num, options, then = None ): - return self._breakpoints.SetLineBreakpoint( file_name, - line_num, - options, - then ) + def ResetExceptionBreakpoints( self ): + return self._breakpoints.ResetExceptionBreakpoints() - def ClearLineBreakpoint( self, file_name, line_num ): - return self._breakpoints.ClearLineBreakpoint( file_name, line_num ) + def AddFunctionBreakpoint( self, function, options ): + return self._breakpoints.AddFunctionBreakpoint( function, options ) - def ClearBreakpoints( self ): - return self._breakpoints.ClearBreakpoints() - def ResetExceptionBreakpoints( self ): - return self._breakpoints.ResetExceptionBreakpoints() +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 - def AddFunctionBreakpoint( self, function, options ): - return self._breakpoints.AddFunctionBreakpoint( function, options ) + yield utils.PathToConfigFile( '.gadgets.json', + os.path.dirname( current_file ) ) -def PathsToAllGadgetConfigs( vimspector_base, current_file ): - yield install.GetGadgetConfigFile( vimspector_base ) +def PathsToAllConfigFiles( vimspector_base, current_file, filetypes ): + for ft in filetypes + [ '_all' ]: for p in sorted( glob.glob( - os.path.join( install.GetGadgetConfigDir( vimspector_base ), + os.path.join( install.GetConfigDirForFiletype( vimspector_base, ft ), '*.json' ) ) ): - yield p + yield p - yield utils.PathToConfigFile( '.gadgets.json', + for ft in filetypes: + yield utils.PathToConfigFile( f'.vimspector.{ft}.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 ) ) + yield utils.PathToConfigFile( '.vimspector.json', + os.path.dirname( current_file ) ) def _SelectProcess( *args ): - value = 0 - - custom_picker = settings.Get( 'custom_process_picker_func' ) - if custom_picker: - try: - value = utils.Call( custom_picker, *args ) - except vim.error: - pass - else: - # Use the built-in one - vimspector_process_list: str = None - try: - try: - vimspector_process_list = installer.FindExecutable( - 'vimspector_process_list' ) - except installer.MissingExecutable: - vimspector_process_list = installer.FindExecutable( - 'vimspector_process_list', - [ os.path.join( install.GetSupportDir(), - 'vimspector_process_list' ) ] ) - except installer.MissingExecutable: - pass - - default_pid = None - if vimspector_process_list: - output = subprocess.check_output( - ( vimspector_process_list, ) + args ).decode( 'utf-8' ) - # if there's only one entry, use it as the default value for input. - lines = output.splitlines() - if len( lines ) == 2: - default_pid = lines[ -1 ].split()[ 0 ] - utils.UserMessage( lines ) - - value = utils.AskForInput( 'Enter Process ID: ', - default_value = default_pid ) - - if value: - try: - return int( value ) - except ValueError: - return 0 - - return 0 + value = 0 + + custom_picker = settings.Get( 'custom_process_picker_func' ) + if custom_picker: + try: + value = utils.Call( custom_picker, *args ) + except vim.error: + pass + else: + # Use the built-in one + vimspector_process_list: str = None + try: + try: + vimspector_process_list = installer.FindExecutable( + 'vimspector_process_list' ) + except installer.MissingExecutable: + vimspector_process_list = installer.FindExecutable( + 'vimspector_process_list', + [ os.path.join( install.GetSupportDir(), + 'vimspector_process_list' ) ] ) + except installer.MissingExecutable: + pass + + default_pid = None + if vimspector_process_list: + output = subprocess.check_output( + ( vimspector_process_list, ) + args ).decode( 'utf-8' ) + # if there's only one entry, use it as the default value for input. + lines = output.splitlines() + if len( lines ) == 2: + default_pid = lines[ -1 ].split()[ 0 ] + utils.UserMessage( lines ) + + value = utils.AskForInput( 'Enter Process ID: ', + default_value = default_pid ) + + if value: + try: + return int( value ) + except ValueError: + return 0 + + return 0 From 6ab618df1c7f4c7d0a4f8e12642d5e9981058a74 Mon Sep 17 00:00:00 2001 From: Jordan Walsh Date: Fri, 22 Dec 2023 14:53:45 -0500 Subject: [PATCH 05/43] Added support for working directory parameter of docker exec --- docs/schema/vimspector.schema.json | 4 ++++ python3/vimspector/debug_session.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/docs/schema/vimspector.schema.json b/docs/schema/vimspector.schema.json index 7b319637a..23f006482 100644 --- a/docs/schema/vimspector.schema.json +++ b/docs/schema/vimspector.schema.json @@ -186,6 +186,10 @@ "type": "array", "items": { "type": "string" }, "description": "A single command to execute for remote-launch. Like runCommands but for a single command." + }, + "workdir": { + "type": "string", + "description": "For containers. The value passed to docker exec for the working directory." } } } diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 0ede8984a..a1c367f14 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -1684,6 +1684,8 @@ def _GetShellCommand( self ): def _GetDockerCommand( self, remote ): docker = [ 'docker', 'exec', '-t' ] + if 'workdir' in remote: + docker.extend(["-w", remote['workdir']]) docker.append( remote[ 'container' ] ) return docker From f82577b6655d15b54bc88e7ac220369d90190ada Mon Sep 17 00:00:00 2001 From: Jordan Walsh Date: Fri, 22 Dec 2023 20:06:04 -0500 Subject: [PATCH 06/43] refactored docker args from pr feedback --- docs/schema/vimspector.schema.json | 9 +++++---- python3/vimspector/debug_session.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/schema/vimspector.schema.json b/docs/schema/vimspector.schema.json index 23f006482..6445d577e 100644 --- a/docs/schema/vimspector.schema.json +++ b/docs/schema/vimspector.schema.json @@ -113,6 +113,11 @@ "type": "string", "description": "Name or container id of the docker run container to connect to (via docker exec). Note the container must already be running (Vimspector will not start it) and it must have the port forwarded to the host if subsequently connecting via a port (for example docker run -p 8765:8765 -it simple_python)." }, + "docker_args": { + "type": "array", + "items": {"type": "string"}, + "description": "Extra command line args to pass to docker exec." + }, "ssh": { "type": "object", "description": "Optional to customize the ssh client and its arguments to execute for remote-launch or remote-attach.", @@ -186,10 +191,6 @@ "type": "array", "items": { "type": "string" }, "description": "A single command to execute for remote-launch. Like runCommands but for a single command." - }, - "workdir": { - "type": "string", - "description": "For containers. The value passed to docker exec for the working directory." } } } diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index a1c367f14..4562ee902 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -1684,8 +1684,8 @@ def _GetShellCommand( self ): def _GetDockerCommand( self, remote ): docker = [ 'docker', 'exec', '-t' ] - if 'workdir' in remote: - docker.extend(["-w", remote['workdir']]) + if 'docker_args' in remote: + docker += remote['docker_args'] docker.append( remote[ 'container' ] ) return docker From 0e88fd468311a7335bd1afb66e7e792bcf2263e2 Mon Sep 17 00:00:00 2001 From: Jordan Walsh Date: Tue, 2 Jan 2024 20:08:39 -0500 Subject: [PATCH 07/43] Fixed linter complaints --- python3/vimspector/debug_session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 4562ee902..f60a188e3 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -1685,7 +1685,7 @@ def _GetShellCommand( self ): def _GetDockerCommand( self, remote ): docker = [ 'docker', 'exec', '-t' ] if 'docker_args' in remote: - docker += remote['docker_args'] + docker += remote[ 'docker_args' ] docker.append( remote[ 'container' ] ) return docker From 01300d07cfe560b2004f8f3d1be52007a97d18bb Mon Sep 17 00:00:00 2001 From: Jordan Walsh Date: Sun, 31 Mar 2024 20:30:50 -0400 Subject: [PATCH 08/43] Adjused indent to quiet linter --- python3/vimspector/debug_session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index ad2430708..6663b6412 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -1687,7 +1687,7 @@ def _GetShellCommand( self ): def _GetDockerCommand( self, remote ): docker = [ 'docker', 'exec', '-t' ] if 'docker_args' in remote: - docker += remote[ 'docker_args' ] + docker += remote[ 'docker_args' ] docker.append( remote[ 'container' ] ) return docker From 1a77ca83cc5c8b4d0b16e2873014582e9c5ca2f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:53:28 +0000 Subject: [PATCH 09/43] Bump rexml from 3.3.3 to 3.3.6 in /docs Bumps [rexml](https://github.com/ruby/rexml) from 3.3.3 to 3.3.6. - [Release notes](https://github.com/ruby/rexml/releases) - [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md) - [Commits](https://github.com/ruby/rexml/compare/v3.3.3...v3.3.6) --- updated-dependencies: - dependency-name: rexml dependency-type: indirect ... Signed-off-by: dependabot[bot] --- docs/Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index b5a74e7c8..f008e6aaf 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -224,7 +224,7 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - rexml (3.3.3) + rexml (3.3.6) strscan rouge (3.26.0) ruby2_keywords (0.0.5) From eabcff580d6b4a49e3ba124e557cfa53c84fd89e Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 27 Sep 2024 19:03:18 +0100 Subject: [PATCH 10/43] Update upload-artifacts action --- .github/workflows/build.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 3607bd7f7..916f164e9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -81,7 +81,7 @@ jobs: VIMSPECTOR_MIMODE: gdb - name: "Upload test logs" - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: 'test-logs-${{ runner.os }}-${{ matrix.runtime }}' @@ -163,7 +163,7 @@ jobs: VIMSPECTOR_MIMODE: lldb - name: "Upload test logs" - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: 'test-logs-${{ runner.os }}-${{ matrix.runtime }}' From 948cde20621440d8b51bf6b78426d891525f9f8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Sep 2024 18:47:00 +0000 Subject: [PATCH 11/43] Bump webrick from 1.8.1 to 1.8.2 in /docs Bumps [webrick](https://github.com/ruby/webrick) from 1.8.1 to 1.8.2. - [Release notes](https://github.com/ruby/webrick/releases) - [Commits](https://github.com/ruby/webrick/compare/v1.8.1...v1.8.2) --- updated-dependencies: - dependency-name: webrick dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- docs/Gemfile | 2 +- docs/Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Gemfile b/docs/Gemfile index 18f79a6e6..c83685cd2 100644 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -29,4 +29,4 @@ gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem "wdm", "~> 0.1.0" if Gem.win_platform? -gem "webrick", "~> 1.7" +gem "webrick", "~> 1.8" diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index f008e6aaf..7185dbe68 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -251,7 +251,7 @@ GEM unf_ext unf_ext (0.0.8.2) unicode-display_width (1.8.0) - webrick (1.8.1) + webrick (1.8.2) PLATFORMS ruby @@ -261,7 +261,7 @@ DEPENDENCIES jekyll-feed (~> 0.6) minima (~> 2.0) tzinfo-data - webrick (~> 1.7) + webrick (~> 1.8) BUNDLED WITH 2.3.8 From 727fc4113038069681a6afcd14414d3ef433bd9a Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 8 Oct 2024 21:01:12 +0100 Subject: [PATCH 12/43] Update CodeLLDB to v1.11.0 --- python3/vimspector/gadgets.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/python3/vimspector/gadgets.py b/python3/vimspector/gadgets.py index 44d482183..eca121600 100644 --- a/python3/vimspector/gadgets.py +++ b/python3/vimspector/gadgets.py @@ -546,12 +546,12 @@ '${version}/${file_name}', }, 'all': { - 'version': 'v1.10.0', + 'version': 'v1.11.0', }, 'macos': { - 'file_name': 'codelldb-x86_64-darwin.vsix', + 'file_name': 'codelldb-darwin-x64.vsix', 'checksum': - '91b10d5670a40434c308c09cb511a5b3e096c82b446a0bbbe4224af33204f5cf', + '5be44ccc6d1e44a0cad5c67458a6968c0c6baf091093005221d467f10dd68dc6', 'make_executable': [ 'adapter/codelldb', 'lldb/bin/debugserver', @@ -560,14 +560,14 @@ ], }, 'macos_arm64': { - 'file_name': 'codelldb-aarch64-darwin.vsix', + 'file_name': 'codelldb-darwin-arm64.vsix', 'checksum': - '4ab0795a726bc52d6e2fa8ebc610baa3f262ebea89adac478cf4a34c72167a41', + '6634c094def2463d38b7b220bcebb49bac81391ef2e9988c4d41e88a996d726c', }, 'linux': { - 'file_name': 'codelldb-x86_64-linux.vsix', + 'file_name': 'codelldb-linux-x64.vsix', 'checksum': - 'd12bff19811974e14688e9754d8d7b9a2430868c3bac883d695032a4acd012ca', + 'b857287f70a18a4fc2d7563aa9fdbcfa9cb2b37d5666fc78394fc8131ee335e2', 'make_executable': [ 'adapter/codelldb', 'lldb/bin/lldb', @@ -576,19 +576,19 @@ ], }, 'linux_arm64': { - 'file_name': 'codelldb-aarch64-linux.vsix', + 'file_name': 'codelldb-linux-arm64.vsix', 'checksum': - '0a81f6617834754537520b7bae2ea9ad50d26b372f8c8bd967dae099e4b27d06', + 'ebbd358dddc1538384cdfb94823da85d13a7a3a4c3eac466de8bb5394f81386a', }, 'linux_armv7': { - 'file_name': 'codelldb-arm-linux.vsix', + 'file_name': 'codelldb-linux-armhf.vsix', 'checksum': - '4bfc5ee753d4359c9ba3cf8fc726f4245a62fd283b718b5120ef1b404baf68c9', + 'a22f1b38a94a94cb2cb814399de9da153cd2ddb2539b97353f05b60668fe0e9f', }, 'windows': { - 'file_name': 'codelldb-x86_64-windows.vsix', + 'file_name': 'codelldb-win32-x64.vsix', 'checksum': - '2f251384e4356edcffe168439714d00de5ca434b263719cbdaf63c9d2f0ffe64', + '375807832e2e9e41dd66f000200d4a55978977f3f10ad9799286f1f9fbe017e6', 'make_executable': [] }, 'adapters': { From d7f42bfdf300fde9ebadffc616b32106dfa6adcc Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 8 Oct 2024 21:09:12 +0100 Subject: [PATCH 13/43] Use stdio for codelldb transport This is much faster to start up in practice and simpler, less error prone. THanks to https://github.com/vadimcn/codelldb/pull/1135 --- python3/vimspector/gadgets.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/python3/vimspector/gadgets.py b/python3/vimspector/gadgets.py index eca121600..c96e278c7 100644 --- a/python3/vimspector/gadgets.py +++ b/python3/vimspector/gadgets.py @@ -597,9 +597,7 @@ 'type': 'CodeLLDB', "command": [ "${gadgetDir}/CodeLLDB/adapter/codelldb", - "--port", "${unusedLocalPort}" ], - "port": "${unusedLocalPort}", "configuration": { "type": "lldb", "name": "lldb", From 977a81b255a48b4774ad3603ac645a38a469a5e3 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 8 Oct 2024 22:39:35 +0100 Subject: [PATCH 14/43] Update macOS to 13 in CI; next one is arm64... --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 916f164e9..fed984796 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -97,7 +97,7 @@ jobs: # SSH_PASS: ${{ secrets.SSH_PASS }} MacOS: - runs-on: 'macos-12' + runs-on: 'macos-13' strategy: fail-fast: false matrix: From 2f5420fb63f9c9ba0023c448518751f216cf6baf Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 8 Oct 2024 23:20:13 +0100 Subject: [PATCH 15/43] Update delve to 1.23.1 --- 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 c96e278c7..193a66e1b 100644 --- a/python3/vimspector/gadgets.py +++ b/python3/vimspector/gadgets.py @@ -353,7 +353,7 @@ gadget ), 'all': { 'path': 'github.com/go-delve/delve/cmd/dlv', - 'version': '1.22.1', + 'version': '1.23.1', }, 'adapters': { "delve": { From e3739100ffb5df0a1ddd04a1de599389e2c3628b Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 8 Oct 2024 23:20:35 +0100 Subject: [PATCH 16/43] remove vscode-node-debug2 --- README.md | 36 ----------------------------------- python3/vimspector/gadgets.py | 21 -------------------- 2 files changed, 57 deletions(-) diff --git a/README.md b/README.md index 2df4d3239..f379c36a4 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,6 @@ runtime dependencies). They are categorised by their level of support: | Bourne Shell | Supported | `--all` or `--enable-bash` | vscode-bash-debug | Bash v?? | | Lua | Tested | `--all` or `--enable-lua` | local-lua-debugger-vscode | Node >=12.13.0, Npm, Lua interpreter | | Node.js | Supported | `--force-enable-node` | vscode-js-debug | Node >= 18 | -| Node.js (legacy) | Supported | `--force-enable-node_legacy` | vscode-node-debug2 | 6 < Node < 12, Npm | | Javascript | Supported | `--force-enable-chrome` | debugger-for-chrome | Chrome | | Javascript | Supported | `--force-enable-firefox` | vscode-firefox-debug | Firefox | | Java | Supported | `--force-enable-java ` | vscode-java-debug | Compatible LSP plugin (see [later](#java)) | @@ -2172,41 +2171,6 @@ multiple debug sessions. For a user, that shouldn't change anything (other than perhaps a slightly confusing stack trace). But it does make things more complicated and so there may be subtle bugs. -* Node.js (legacy) - -**NOTE**: This configuration uses the *deprecated* legacy debug adapter and will -be removed in future. Please update your configurations to use the `js-debug` -adapter. You _may_ be able to just change the adapter name. - -Requires: - -* `install_gadget.py --force-enable-node` -* For installation, a Node.js environment that is < node 12. I believe this is an - incompatibility with gulp. Advice, use [nvm](https://github.com/nvm-sh/nvm) with `nvm install --lts 10; nvm - use --lts 10; ./install_gadget.py --force-enable-node ...` -* Options described here: - https://code.visualstudio.com/docs/nodejs/nodejs-debugging -* Example: `support/test/node/simple` - -```json -{ - "configurations": { - "run": { - "adapter": "vscode-node", - "filetypes": [ "javascript", "typescript" ], // optional - "configuration": { - "request": "launch", - "protocol": "auto", - "stopOnEntry": true, - "console": "integratedTerminal", - "program": "${workspaceRoot}/simple.js", - "cwd": "${workspaceRoot}" - } - } - } -} -``` - * Chrome/Firefox This uses the chrome/firefox debugger (they are very similar), see diff --git a/python3/vimspector/gadgets.py b/python3/vimspector/gadgets.py index 193a66e1b..a84f7f7d5 100644 --- a/python3/vimspector/gadgets.py +++ b/python3/vimspector/gadgets.py @@ -463,27 +463,6 @@ }, }, }, - 'vscode-node-debug2': { - 'language': 'node_legacy', - 'enabled': False, - 'repo': { - 'url': 'https://github.com/microsoft/vscode-node-debug2', - 'ref': 'v1.43.0' - }, - 'do': lambda name, root, gadget: installer.InstallNodeDebug( name, - root, - gadget ), - 'adapters': { - 'vscode-node': { - 'name': 'node2', - 'type': 'node2', - 'command': [ - 'node', - '${gadgetDir}/vscode-node-debug2/out/src/nodeDebug.js' - ] - }, - }, - }, 'vscode-firefox-debug': { 'language': 'firefox', 'enabled': False, From 5a91171184028d3c5dc1616d4c6510cb6022c072 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 8 Oct 2024 23:32:45 +0100 Subject: [PATCH 17/43] Update go to 1.23.2 --- tests/ci/image/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ci/image/Dockerfile b/tests/ci/image/Dockerfile index 3b86ef06b..036059aca 100644 --- a/tests/ci/image/Dockerfile +++ b/tests/ci/image/Dockerfile @@ -3,7 +3,7 @@ FROM ubuntu:20.04 ENV DEBIAN_FRONTEND=noninteractive ENV LC_ALL C.UTF-8 ARG GOARCH=amd64 -ARG GOVERSION=1.20.1 +ARG GOVERSION=1.23.2 ARG NODE_MAJOR=18 ARG VIM_VERSION=v8.2.4797 ARG NVIM_VERSION=v0.8.3 From 17962472e467389b2b4f645e568a755e1bb8fa42 Mon Sep 17 00:00:00 2001 From: puremourning Date: Tue, 8 Oct 2024 23:33:51 +0000 Subject: [PATCH 18/43] Update vim docs --- doc/vimspector.txt | 95 ++++++++++++++-------------------------------- 1 file changed, 28 insertions(+), 67 deletions(-) diff --git a/doc/vimspector.txt b/doc/vimspector.txt index eb5b058ef..0ea58d33e 100644 --- a/doc/vimspector.txt +++ b/doc/vimspector.txt @@ -316,8 +316,6 @@ runtime dependencies). They are categorised by their level of support: -------------------------------------------------------------------------------------------------------------------------------------------------------------- | Node.js | Supported | '--force-enable-node' | vscode-js-debug | Node >= 18 | -------------------------------------------------------------------------------------------------------------------------------------------------------------- -| Node.js (legacy) | Supported | '--force-enable-node_legacy' | vscode-node-debug2 | 6 < Node < 12, Npm | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | Javascript | Supported | '--force-enable-chrome' | debugger-for-chrome | Chrome | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | Javascript | Supported | '--force-enable-firefox' | vscode-firefox-debug | Firefox | @@ -2509,42 +2507,6 @@ multiple debug sessions. For a user, that shouldn't change anything (other than perhaps a slightly confusing stack trace). But it does make things more complicated and so there may be subtle bugs. -- Node.js (legacy) - -**NOTE**: This configuration uses the _deprecated_ legacy debug adapter and -will be removed in future. Please update your configurations to use the -'js-debug' adapter. You _may_ be able to just change the adapter name. - -Requires: - -- 'install_gadget.py --force-enable-node' - -- For installation, a Node.js environment that is < node 12. I believe this - is an incompatibility with gulp. Advice, use nvm [57] with 'nvm install - --lts 10; nvm use --lts 10; ./install_gadget.py --force-enable-node ...' - -- Options described here: - https://code.visualstudio.com/docs/nodejs/nodejs-debugging - -- Example: 'support/test/node/simple' -> - { - "configurations": { - "run": { - "adapter": "vscode-node", - "filetypes": [ "javascript", "typescript" ], // optional - "configuration": { - "request": "launch", - "protocol": "auto", - "stopOnEntry": true, - "console": "integratedTerminal", - "program": "${workspaceRoot}/simple.js", - "cwd": "${workspaceRoot}" - } - } - } - } -< - Chrome/Firefox This uses the chrome/firefox debugger (they are very similar), see https://mark @@ -2588,12 +2550,12 @@ It allows you to debug scripts running inside chrome from within Vim. *vimspector-java* Java ~ -Vimspector works well with the java debug server [58], which runs as a jdt.ls +Vimspector works well with the java debug server [57], which runs as a jdt.ls (Java Language Server) plugin, rather than a standalone debug adapter. Vimspector is not in the business of running language servers, only debug adapters, so this means that you need a compatible Language Server Protocol -editor plugin to use Java. I recommend YouCompleteMe [59], which has full +editor plugin to use Java. I recommend YouCompleteMe [58], which has full support for jdt.ls, and most importantly a trivial way to load the debug adapter and to use it with Vimspector. @@ -2601,7 +2563,7 @@ adapter and to use it with Vimspector. *vimspector-hot-code-replace* Hot code replace ~ -When using the java debug server [58], Vimspector supports the hot code replace +When using the java debug server [57], Vimspector supports the hot code replace custom feature. By default, when the underlying class files change, vimspector asks the user if they wish to reload these classes at runtime. @@ -2620,7 +2582,7 @@ This behaviour can be customised: *vimspector-usage-with-youcompleteme* Usage with YouCompleteMe ~ -- Set up YCM for java [59]. +- Set up YCM for java [58]. - Get Vimspector to download the java debug plugin: 'install_gadget.py --force-enable-java ' or ':VimspectorInstall @@ -2685,24 +2647,24 @@ If you see "Unable to get DAP port - is JDT.LS initialized?", try running ':YcmCompleter ExecuteCommand vscode.java.startDebugSession' and note the output. If you see an error like 'ResponseFailedException: Request failed: -32601: No delegateCommandHandler for vscode.java.startDebugSession', make sure -that: _Your YCM jdt.ls is actually working, see the YCM docs [60] for +that: _Your YCM jdt.ls is actually working, see the YCM docs [59] for troubleshooting_ The YCM jdt.ls has had time to initialize before you start the debugger * That 'g:ycm_java_jdtls_extension_path' is set in '.vimrc' or prior to YCM starting -For the launch arguments, see the vscode document [61]. +For the launch arguments, see the vscode document [60]. ------------------------------------------------------------------------------- *vimspector-other-lsp-clients* Other LSP clients ~ -See this issue [62] for more background. +See this issue [61] for more background. ------------------------------------------------------------------------------- *vimspector-lua* Lua ~ -Lua is supported through local-lua-debugger-vscode [63]. This debugger uses +Lua is supported through local-lua-debugger-vscode [62]. This debugger uses stdio to communicate with the running process, so calls to 'io.read' will cause problems. @@ -2760,7 +2722,7 @@ problems. Other servers ~ - Java - vscode-javac. This works, but is not as functional as Java Debug - Server. Take a look at this comment [64] for instructions. + Server. Take a look at this comment [63] for instructions. - See also the wiki [15] which has community-contributed plugin files for some languages. @@ -3017,7 +2979,7 @@ Pre-launch building strategies ~ In many cases you will want to rebuild your project before starting a new debugging session. Vimspector is not a task manager and implementing this functionality is out of the scope of this project. However, there are some -strategies described in the community wiki [65] to achieve similar +strategies described in the community wiki [64] to achieve similar functionality. ------------------------------------------------------------------------------- @@ -3173,7 +3135,7 @@ FAQ ~ additional language support 2. How do I stop it starting a new Terminal.app on macOS? See this comment - [66] + [65] 3. Can I specify answers to the annoying questions about exception breakpoints in my '.vimspector.json' ? Yes, see here [26]. @@ -3210,10 +3172,10 @@ FAQ ~ 4. Do I _have_ to put a '.vimspector.json' in the root of every project? No, you can use 'g:vimspector_adapters' and 'g:vimspector_configurations' or - put all of your adapter and debug configs in a single directory [67] if + put all of your adapter and debug configs in a single directory [66] if you want to, but note the caveat that '${workspaceRoot}' won't be calculated correctly in that case. The vimsepctor author uses this a lot - [68] + [67] 5. I'm confused about remote debugging configuration, can you explain it? eh... kind of. Reference: https://puremourning.github.io/vimspector/confi @@ -3221,7 +3183,7 @@ FAQ ~ ://github.com/puremourning/vimspector/issues/478#issuecomment-943515093 6. I'm trying to debug a Django (django?) project and it's not working. Can - you help? sure, check this link which has a working example [69]. Or + you help? sure, check this link which has a working example [68]. Or google it. 7. Can vimspector build my code before debugging it? Can I deploy it to a @@ -3229,7 +3191,7 @@ FAQ ~ debugger, not a task system or build automation system - there are other tools for that. There is however a hack you can use - you can use a 'shell' variable to execute a command and just discard the output. Other - options are discussed in this issue [70] + options are discussed in this issue [69] 8. It's annoying to manually type in the PID when attaching. Do you have a PID picker? There's no PID picker in vimspector at the moment, but you @@ -3350,19 +3312,18 @@ References ~ [54] https://github.com/microsoft/vscode-js-debug [55] https://github.com/microsoft/vscode-js-debug/blob/main/OPTIONS.md [56] https://github.com/microsoft/vscode-js-debug/blob/main/src/common/contributionUtils.ts#L61 -[57] https://github.com/nvm-sh/nvm -[58] https://github.com/Microsoft/java-debug -[59] https://github.com/ycm-core/YouCompleteMe#java-semantic-completion -[60] https://github.com/ycm-core/YouCompleteMe#troubleshooting -[61] https://code.visualstudio.com/docs/java/java-debugging -[62] https://github.com/puremourning/vimspector/issues/3 -[63] https://github.com/tomblind/local-lua-debugger-vscode -[64] https://github.com/puremourning/vimspector/issues/3#issuecomment-576916076 -[65] https://github.com/puremourning/vimspector/wiki/Pre-launch-building-strategies -[66] https://github.com/puremourning/vimspector/issues/90#issuecomment-577857322 -[67] https://puremourning.github.io/vimspector/configuration.html#debug-configurations -[68] https://github.com/puremourning/.vim-mac/tree/master/vimspector-conf -[69] https://www.reddit.com/r/neovim/comments/mz4ari/how_to_set_up_vimspector_for_django_debugging/ -[70] https://github.com/puremourning/vimspector/issues/227 +[57] https://github.com/Microsoft/java-debug +[58] https://github.com/ycm-core/YouCompleteMe#java-semantic-completion +[59] https://github.com/ycm-core/YouCompleteMe#troubleshooting +[60] https://code.visualstudio.com/docs/java/java-debugging +[61] https://github.com/puremourning/vimspector/issues/3 +[62] https://github.com/tomblind/local-lua-debugger-vscode +[63] https://github.com/puremourning/vimspector/issues/3#issuecomment-576916076 +[64] https://github.com/puremourning/vimspector/wiki/Pre-launch-building-strategies +[65] https://github.com/puremourning/vimspector/issues/90#issuecomment-577857322 +[66] https://puremourning.github.io/vimspector/configuration.html#debug-configurations +[67] https://github.com/puremourning/.vim-mac/tree/master/vimspector-conf +[68] https://www.reddit.com/r/neovim/comments/mz4ari/how_to_set_up_vimspector_for_django_debugging/ +[69] https://github.com/puremourning/vimspector/issues/227 vim: ft=help From 0a698681b7226758d6e963fcd1744c3a41176969 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 5 Mar 2021 21:10:08 +0000 Subject: [PATCH 19/43] Data breakpoints Works with CodeLLDB mostly for named things. Expressions don't seem to be supported by anyone. Fixed some state issues with the connections being retained across restarts in watches. ugh. so messy. Added access type question. Java debug adapter returns broken breakpoints info response --- autoload/vimspector.vim | 8 ++ python3/vimspector/breakpoints.py | 68 +++++++++-- python3/vimspector/debug_session.py | 31 ++++- python3/vimspector/settings.py | 1 + python3/vimspector/variables.py | 110 +++++++++++++++--- .../cpp/simple_c_program/.vimspector.json | 4 +- support/test/cpp/simple_c_program/memory.cpp | 24 ++++ support/test/go/structs/.gitignore | 1 + support/test/go/structs/.vimspector.json | 29 +++++ support/test/go/structs/__debug_bin | Bin 0 -> 1921602 bytes support/test/go/structs/hello-world.go | 25 ++++ .../test/java/test_project/.vimspector.json | 3 + support/test/rust/vimspector_test/src/main.rs | 16 +++ 13 files changed, 294 insertions(+), 26 deletions(-) create mode 100644 support/test/cpp/simple_c_program/memory.cpp create mode 100644 support/test/go/structs/.gitignore create mode 100644 support/test/go/structs/.vimspector.json create mode 100755 support/test/go/structs/__debug_bin create mode 100644 support/test/go/structs/hello-world.go diff --git a/autoload/vimspector.vim b/autoload/vimspector.vim index 4785399e7..1e4ecc22d 100644 --- a/autoload/vimspector.vim +++ b/autoload/vimspector.vim @@ -439,6 +439,14 @@ function! vimspector#ShowDisassembly( ... ) abort py3 _vimspector_session.ShowDisassembly() endfunction +function! vimspector#AddDataBreakpoint( ... ) abort + if !s:Enabled() + return + endif + " TODO: how to set options? + py3 _vimspector_session.AddDataBreakpoint( {} ) +endfunction + function! vimspector#DeleteWatch() abort if !s:Enabled() return diff --git a/python3/vimspector/breakpoints.py b/python3/vimspector/breakpoints.py index 2aab632eb..bbe573537 100644 --- a/python3/vimspector/breakpoints.py +++ b/python3/vimspector/breakpoints.py @@ -219,6 +219,7 @@ def __init__( self, self._func_breakpoints = [] self._exception_breakpoints = None self._configured_breakpoints = {} + self._data_breakponts = [] self._server_capabilities = {} @@ -523,23 +524,21 @@ def _ClearServerBreakpointData( self, conn: DebugAdapterConnection ): if not bp[ 'server_bp' ]: del bp[ 'server_bp' ] - # Clear all instruction breakpoints because they aren't truly portable # across sessions. - # - # TODO: It might be possible to re-resolve the address stored in the - # breakpoint, though this would only work in a limited way (as load - # addresses will frequently not be the same across runs) - - def ShouldKeep( bp ): + def ShouldKeepInsBP( bp ): if not bp[ 'is_instruction_breakpoint' ]: return True if 'address' in bp and bp[ 'session_id' ] != conn.GetSessionId(): return True return False - breakpoints[ : ] = [ bp for bp in breakpoints if ShouldKeep( bp ) ] + breakpoints[ : ] = [ bp for bp in breakpoints if ShouldKeepInsBP( bp ) ] + + # Erase any data breakpoints for this connection too + self._data_breakponts[ : ] = [ bp for bp in self._data_breakponts + if bp[ 'conn' ] != conn.GetSessionId() ] def _CopyServerLineBreakpointProperties( self, @@ -807,7 +806,19 @@ def AddFunctionBreakpoint( self, function, options ): # 'condition': ..., # 'hitCondition': ..., } ) + self.UpdateUI() + + def AddDataBreakpoint( self, + conn: DebugAdapterConnection, + info, + options ): + self._data_breakponts.append( { + 'state': 'ENABLED', + 'conn': conn.GetSessionId(), + 'info': info, + 'options': options + } ) self.UpdateUI() @@ -1014,6 +1025,37 @@ def response_handler( conn, msg, bp_idxs = [] ): failure_handler = response_received ) + if self._data_breakponts and self._server_capabilities[ + 'supportsDataBreakpoints' ]: + connection: DebugAdapterConnection + for connection in self._connections: + breakpoints = [] + for bp in self._data_breakponts: + if bp[ 'state' ] != 'ENABLED': + continue + if bp[ 'conn' ] != connection.GetSessionId(): + continue + if not bp[ 'info' ].get( 'dataId' ): + continue + + data_bp = {} + data_bp.update( bp[ 'options' ] ) + data_bp[ 'dataId' ] = bp[ 'info' ][ 'dataId' ] + breakpoints.append( data_bp ) + + if breakpoints: + self._awaiting_bp_responses += 1 + connection.DoRequest( + lambda msg, conn=connection: response_handler( conn, msg ), + { + 'command': 'setDataBreakpoints', + 'arguments': { + 'breakpoints': breakpoints, + }, + }, + failure_handler = response_received + ) + if self._exception_breakpoints: for connection in self._connections: self._awaiting_bp_responses += 1 @@ -1112,6 +1154,11 @@ def Save( self ): if bps: line[ file_name ] = bps + # TODO: Some way to persis data breakpoints? Currently they require + # variablesReference, which is clearly not something that can be persisted + # + # That said, the spec now seems to support data bps on expressions, though i + # can't see any servers which support that. return { 'line': line, 'function': self._func_breakpoints, @@ -1183,6 +1230,11 @@ def _HideBreakpoints( self ): signs.UnplaceSign( bp[ 'sign_id' ], 'VimspectorBP' ) del bp[ 'sign_id' ] + # TODO could/should we show a sign in the variables view when there's a data + # brakpoint on the variable? Not sure how best to actually do that, but + # maybe the variable view can pass that info when calling AddDataBreakpoint, + # such as the variablesReference/name + def _SignToLine( self, file_name, bp ): if bp[ 'is_instruction_breakpoint' ]: diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 941a5f4cb..5e5bbe6f1 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -1016,7 +1016,36 @@ def OnDisassemblyWindowScrolled( self, win_id ): self._disassemblyView.OnWindowScrolled( win_id ) - @CurrentSession() + @ParentOnly() + def AddDataBreakpoint( self, opts, buf = None, line_num = None ): + # Use the parent session, because the _connection_ comes from the + # variable/watch result that is actually chosen + + def add_bp( conn, msg ): + breakpoint_info = msg.get( 'body' ) + if not breakpoint_info: + utils.UserMessage( "Can't set data breakpoint here" ) + return + + if breakpoint_info[ 'dataId' ] is None: + utils.UserMessage( + f"Can't set data breakpoint here: {breakpoint_info[ 'description' ]}" + ) + return + + access_types = breakpoint_info.get( 'accessTypes' ) + if access_types and 'accessType' not in opts: + access_type = utils.SelectFromList( 'What type of access?', + access_types ) + if access_type is not None: + opts[ 'accessType' ] = access_type + + self._breakpoints.AddDataBreakpoint( conn, + breakpoint_info, + opts ) + + self._variablesView.GetDataBreakpointInfo( add_bp, buf, line_num ) + @IfConnected() def AddWatch( self, expression ): self._variablesView.AddWatch( self._connection, diff --git a/python3/vimspector/settings.py b/python3/vimspector/settings.py index 26d6258b3..a4295048e 100644 --- a/python3/vimspector/settings.py +++ b/python3/vimspector/settings.py @@ -92,6 +92,7 @@ 'delete': [ '' ], 'set_value': [ '', '' ], 'read_memory': [ 'm' ], + 'add_data_breakpoint': [ '' ], }, 'stack_trace': { 'expand_or_jump': [ '', '<2-LeftMouse>' ], diff --git a/python3/vimspector/variables.py b/python3/vimspector/variables.py index 4aa9262d6..2c12d9a34 100644 --- a/python3/vimspector/variables.py +++ b/python3/vimspector/variables.py @@ -19,7 +19,7 @@ from functools import partial import typing -from vimspector import utils, settings +from vimspector import utils, settings, session_manager from vimspector.debug_adapter_connection import DebugAdapterConnection @@ -62,13 +62,25 @@ def IsContained( self ): def VariablesReference( self ): assert False + @abc.abstractmethod + def FrameID( self ): + assert False + + @abc.abstractmethod + def Name( self ): + assert False + + @abc.abstractmethod def MemoryReference( self ): - assert None + assert False @abc.abstractmethod def HoverText( self ): return "" + def Update( self, connection ): + self.connection = connection + class Scope( Expandable ): """Holds an expandable scope (a DAP scope dict), with expand/collapse state""" @@ -82,7 +94,14 @@ def VariablesReference( self ): def MemoryReference( self ): return None - def Update( self, scope ): + def FrameID( self ): + return None + + def Name( self ): + return self.scope[ 'name' ] + + def Update( self, connection, scope ): + super().Update( connection ) self.scope = scope def HoverText( self ): @@ -91,8 +110,12 @@ def HoverText( self ): class WatchResult( Expandable ): """Holds the result of a Watch expression with expand/collapse.""" - def __init__( self, connection: DebugAdapterConnection, result: dict ): + def __init__( self, + connection: DebugAdapterConnection, + watch, + result: dict ): super().__init__( connection ) + self.watch = watch self.result = result # A new watch result is marked as changed self.changed = True @@ -103,9 +126,15 @@ def VariablesReference( self ): def MemoryReference( self ): return self.result.get( 'memoryReference' ) + def FrameID( self ): + return self.watch.expression.get( 'frameId' ) + + def Name( self ): + return self.watch.expression.get( 'expression' ) + def Update( self, connection, result ): + super().Update( connection ) self.changed = False - self.connection = connection if self.result[ 'result' ] != result[ 'result' ]: self.changed = True self.result = result @@ -121,8 +150,8 @@ def HoverText( self ): class WatchFailure( WatchResult ): - def __init__( self, connection: DebugAdapterConnection, reason ): - super().__init__( connection, { 'result': reason } ) + def __init__( self, connection: DebugAdapterConnection, watch, reason ): + super().__init__( connection, watch, { 'result': reason } ) self.changed = True @@ -130,7 +159,8 @@ class Variable( Expandable ): """Holds one level of an expanded value tree. Also itself expandable.""" def __init__( self, connection: DebugAdapterConnection, - container: Expandable, variable: dict ): + container: Expandable, + variable: dict ): super().__init__( connection = connection, container = container ) self.variable = variable # A new variable appearing is marked as changed @@ -142,9 +172,15 @@ def VariablesReference( self ): def MemoryReference( self ): return self.variable.get( 'memoryReference' ) + def FrameID( self ): + return self.container.FrameID() + + def Name( self ): + return self.variable[ 'name' ] + def Update( self, connection, variable ): + super().Update( connection ) self.changed = False - self.connection = connection if self.variable[ 'value' ] != variable[ 'value' ]: self.changed = True self.variable = variable @@ -171,6 +207,11 @@ def __init__( self, connection: DebugAdapterConnection, expression: dict ): self.result = None def SetCurrentFrame( self, connection, frame ): + if connection is None: + self.connection = None + self.result.connection = None + return + if self.connection is None: self.connection = connection elif self.connection != connection: @@ -227,6 +268,9 @@ def AddExpandMappings( mappings = None ): for mapping in utils.GetVimList( mappings, 'read_memory' ): vim.command( f'nnoremap { mapping } ' ':call vimspector#ReadMemory()' ) + for mapping in utils.GetVimList( mappings, 'add_data_breakpoint' ): + vim.command( f'nnoremap { mapping } ' + ':call vimspector#AddDataBreakpoint()' ) @@ -323,7 +367,7 @@ def ConnectionClosed( self, connection ): ] for w in self._watches: if w.connection == connection: - w.connection = None + w.SetCurrentFrame( None, None ) def Reset( self ): @@ -363,7 +407,7 @@ def scopes_consumer( message ): if not found: scope = Scope( connection, scope_body ) else: - scope.Update( scope_body ) + scope.Update( connection, scope_body ) new_scopes.append( scope ) @@ -434,7 +478,7 @@ def handler( message ): watch = self._variable_eval if watch.result is None or watch.result.connection != connection: - watch.result = WatchResult( connection, message[ 'body' ] ) + watch.result = WatchResult( connection, watch, message[ 'body' ] ) else: watch.result.Update( connection, message[ 'body' ] ) @@ -543,7 +587,9 @@ def _UpdateWatchExpression( self, watch: Watch, message: dict ): if watch.result is not None: watch.result.Update( watch.connection, message[ 'body' ] ) else: - watch.result = WatchResult( watch.connection, message[ 'body' ] ) + watch.result = WatchResult( watch.connection, + watch, + message[ 'body' ] ) if ( watch.result.IsExpandable() and watch.result.IsExpanded() ): @@ -563,7 +609,7 @@ def _WatchExpressionFailed( self, reason: str, watch: Watch ): # We already have a result for this watch. Wut ? return - watch.result = WatchFailure( watch.connection, reason ) + watch.result = WatchFailure( watch.connection, watch, reason ) self._DrawWatches() def _GetVariable( self, buf = None, line_num = None ): @@ -677,7 +723,6 @@ def GetMemoryReference( self ): if variable is None: return None, None - # TODO: Return the connection too! return variable.connection, variable.MemoryReference() @@ -853,4 +898,39 @@ def SetSyntax( self, syntax ): syntax, self._vars.buf, self._watch.buf ) + + def GetDataBreakpointInfo( self, + then, + buf = None, + line_num = None ): + variable: Expandable + view: View + + variable, view = self._GetVariable( buf, line_num ) + if variable is None: + return None + + if not session_manager.Get().GetSession( + variable.connection.GetSessionId() )._server_capabilities.get( + 'supportsDataBreakpoints' ): + return None + + arguments = { + 'name': variable.Name() + } + frameId = variable.FrameID() + if frameId: + arguments[ 'frameId' ] = frameId + + if variable.IsContained(): + arguments[ 'variablesReference' ] = ( + variable.container.VariablesReference() ) + + variable.connection.DoRequest( lambda msg: then( variable.connection, + msg ), { + 'command': 'dataBreakpointInfo', + 'arguments': arguments, + } ) + + # vim: sw=2 diff --git a/support/test/cpp/simple_c_program/.vimspector.json b/support/test/cpp/simple_c_program/.vimspector.json index f277fc774..e5804cf17 100644 --- a/support/test/cpp/simple_c_program/.vimspector.json +++ b/support/test/cpp/simple_c_program/.vimspector.json @@ -64,7 +64,7 @@ "adapter": "vscode-cpptools", "variables": { "BUILDME": { - "shell": "g++ -o ${workspaceRoot}/test -g -std=c++17 ${workspaceRoot}/test_c.cpp" + "shell": "g++ -o ${workspaceRoot}/test -g -std=c++17 ${file}" }, "arch": { "shell": "uname -m" @@ -86,7 +86,7 @@ "adapter": "vscode-cpptools", "variables": { "BUILDME": { - "shell": "g++ -o ${workspaceRoot}/test -g -std=c++17 ${workspaceRoot}/test_c.cpp" + "shell": "g++ -o ${workspaceRoot}/test -g -std=c++17 ${file}" } }, "configuration": { diff --git a/support/test/cpp/simple_c_program/memory.cpp b/support/test/cpp/simple_c_program/memory.cpp new file mode 100644 index 000000000..cac551ba1 --- /dev/null +++ b/support/test/cpp/simple_c_program/memory.cpp @@ -0,0 +1,24 @@ +struct Test +{ + int x; + int y; +}; + +int main( int argc , char ** argv ) +{ + Test x[] = { + { 1, 2 }, + { 3, 4 }, + { 5, 6 }, + }; + + Test y = { 7, 8 }; + + x[0].x += argc; + argv[ 0 ][ 0 ] = 'x' ; + + y.x += **argv; + y.y += argc * **argv; + + return argc; +} diff --git a/support/test/go/structs/.gitignore b/support/test/go/structs/.gitignore new file mode 100644 index 000000000..242c034c1 --- /dev/null +++ b/support/test/go/structs/.gitignore @@ -0,0 +1 @@ +hello_world diff --git a/support/test/go/structs/.vimspector.json b/support/test/go/structs/.vimspector.json new file mode 100644 index 000000000..da54999a1 --- /dev/null +++ b/support/test/go/structs/.vimspector.json @@ -0,0 +1,29 @@ +{ + "configurations": { + "run-legacy": { + "adapter": "vscode-go", + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/hello-world.go", + "mode": "debug", + "trace": true, + "env": { "GO111MODULE": "off" } + } + }, + "run-delve": { + "adapter": "delve", + "configuration": { + "request": "launch", + "env": { "GO111MODULE": "off" }, + + "mode": "debug", // debug|test + "program": "${workspaceRoot}/hello-world.go" + + // "args": [], + // "buildFlags": ... + // "stackTraceDepth": ..., + // "showGlobalVariables": true, + } + } + } +} diff --git a/support/test/go/structs/__debug_bin b/support/test/go/structs/__debug_bin new file mode 100755 index 0000000000000000000000000000000000000000..919aef79b6d6d4391d06b95608188f1f16da0d8a GIT binary patch literal 1921602 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