From 7a915185422cb4bd02f87ec6df7fa01d77d2544f Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sat, 2 Feb 2019 14:54:49 +0000 Subject: [PATCH 01/50] Improve error reporting --- python3/vimspector/debug_session.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 63d4c92..b7eae06 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -477,7 +477,9 @@ class DebugSession( object ): def OnFailure( self, reason, message ): - self._outputView.ServerEcho( reason ) + msg = "Request for '{}' failed: {}".format( message[ 'command' ], + reason ) + self._outputView.ServerEcho( msg ) def _Launch( self ): self._logger.debug( "LAUNCH!" ) From 32e16d9dafcd3637f777c82ac2ae50f32a9e87e1 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sat, 2 Feb 2019 15:49:22 +0000 Subject: [PATCH 02/50] WIP: Try and remember that the server quit --- autoload/vimspector/internal/channel.vim | 1 + autoload/vimspector/internal/job.vim | 7 ++++--- python3/vimspector/debug_adapter_connection.py | 15 +++++++++------ python3/vimspector/debug_session.py | 1 + 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/autoload/vimspector/internal/channel.vim b/autoload/vimspector/internal/channel.vim index c58ff64..d9f5871 100644 --- a/autoload/vimspector/internal/channel.vim +++ b/autoload/vimspector/internal/channel.vim @@ -40,6 +40,7 @@ endfunction function! s:_Send( msg ) abort call ch_sendraw( s:ch, a:msg ) + return 1 endfunction function! vimspector#internal#channel#Timeout( id ) abort diff --git a/autoload/vimspector/internal/job.vim b/autoload/vimspector/internal/job.vim index 4f30ebf..8377225 100644 --- a/autoload/vimspector/internal/job.vim +++ b/autoload/vimspector/internal/job.vim @@ -43,21 +43,22 @@ endfunction function! s:_Send( msg ) abort if ! exists( 's:job' ) echom "Can't send message: Job was not initialised correctly" - return + return 0 endif if job_status( s:job ) != 'run' echom "Can't send message: Job is not running" - return + return 0 endif let ch = job_getchannel( s:job ) if ch == 'channel fail' echom "Channel was closed unexpectedly!" - return + return 0 endif call ch_sendraw( ch, a:msg ) + return 1 endfunction function! vimspector#internal#job#StartDebugSession( config ) abort diff --git a/python3/vimspector/debug_adapter_connection.py b/python3/vimspector/debug_adapter_connection.py index ae9cf37..c739df9 100644 --- a/python3/vimspector/debug_adapter_connection.py +++ b/python3/vimspector/debug_adapter_connection.py @@ -56,11 +56,14 @@ class DebugAdapterConnection( object ): 'timer_start( {}, "vimspector#internal#channel#Timeout" )'.format( timeout ) ) - self._outstanding_requests[ this_id ] = PendingRequest( msg, - handler, - failure_handler, - expiry_id ) - self._SendMessage( msg ) + request = PendingRequest( msg, + handler, + failure_handler, + expiry_id ) + self._outstanding_requests[ this_id ] = request + + if not self._SendMessage( msg ): + self._AbortRequest( request, 'Unable to send message' ) def OnRequestTimeout( self, timer_id ): request_id = None @@ -144,7 +147,7 @@ class DebugAdapterConnection( object ): data = 'Content-Length: {0}\r\n\r\n{1}'.format( len( msg ), msg ) # self._logger.debug( 'Sending: {0}'.format( data ) ) - self._Write( data ) + return self._Write( data ) def _ReadHeaders( self ): parts = self._buffer.split( bytes( '\r\n\r\n', 'utf-8' ), 1 ) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index b7eae06..0750a50 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -578,6 +578,7 @@ class DebugSession( object ): def OnEvent_terminated( self, message ): self.Clear() + self._connection = None utils.UserMessage( "Debugging was terminated." ) def _RemoveBreakpoints( self ): From aed3f459a6e94a781ff4d368d6e273fd0e7d6267 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sat, 2 Feb 2019 10:46:02 +0000 Subject: [PATCH 03/50] Support variables set in the vimspector.json --- python3/vimspector/debug_session.py | 46 ++++++++++++++++++----------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 0750a50..10c4030 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -120,33 +120,45 @@ class DebugSession( object ): with open( launch_config_file, 'r' ) as f: database = json.load( f ) - launch_config = database.get( 'configurations' ) + configurations = database.get( 'configurations' ) adapters = database.get( 'adapters' ) - if len( launch_config ) == 1: - configuration = next( iter( launch_config.keys() ) ) + if len( configurations ) == 1: + configuration_name = next( iter( configurations.keys() ) ) else: - configuration = utils.SelectFromList( 'Which launch configuration?', - list( launch_config.keys() ) ) - if not configuration: + configuration_name = utils.SelectFromList( + 'Which launch configuration?', + list( configurations.keys() ) ) + + if not configuration_name or configuration_name not in configurations: return self._workspace_root = os.path.dirname( launch_config_file ) - variables = { - 'dollar': '$', # HAAACK: work around not having a way to include a literal - 'workspaceRoot': self._workspace_root - } - variables.update( launch_variables ) - utils.ExpandReferencesInDict( launch_config[ configuration ], variables ) - - adapter = launch_config[ configuration ].get( 'adapter' ) + configuration = configurations[ configuration_name ] + adapter = configuration.get( 'adapter' ) if isinstance( adapter, str ): adapter = adapters.get( adapter ) - utils.ExpandReferencesInDict( adapter, variables ) - self._StartWithConfiguration( launch_config[ configuration ], - adapter ) + # TODO: Do we want some form of persistence ? e.g. self._staticVariables, + # set from an api call like SetLaunchParam( 'var', 'value' ), perhaps also a + # way to load .vimspector.local.json which just sets variables + self._variables = { + 'dollar': '$', # HACK + 'workspaceRoot': self._workspace_root + } + self._variables.update( adapter.get( 'variables', {} ) ) + self._variables.update( configuration.get( 'variables', {} ) ) + + utils.ExpandReferencesInDict( configuration, self._variables ) + utils.ExpandReferencesInDict( adapter, self._variables ) + + if not adapter: + utils.UserMessage( 'No adapter configured for {}'.format( + configuration_name ), persist=True ) + return + + self._StartWithConfiguration( configuration, adapter ) def _StartWithConfiguration( self, configuration, adapter ): self._configuration = configuration From 1ca88ad33255fe7c0ee53c113639d04b91497a21 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sat, 2 Feb 2019 10:46:53 +0000 Subject: [PATCH 04/50] WIP: Support remote startup commands, e.g. gdbserver --- autoload/vimspector/internal/job.vim | 19 +++++++ python3/vimspector/debug_session.py | 79 ++++++++++++++++++++++++---- 2 files changed, 87 insertions(+), 11 deletions(-) diff --git a/autoload/vimspector/internal/job.vim b/autoload/vimspector/internal/job.vim index 8377225..f696848 100644 --- a/autoload/vimspector/internal/job.vim +++ b/autoload/vimspector/internal/job.vim @@ -61,6 +61,25 @@ function! s:_Send( msg ) abort return 1 endfunction +function! vimspector#internal#job#RunCommand( cmd ) abort + return job_start( a:cmd, + \ { + \ 'in_mode': 'raw', + \ 'out_mode': 'raw', + \ 'err_mode': 'raw', + \ 'out_cb': funcref( 's:_OnServerError' ), + \ 'err_cb': funcref( 's:_OnServerError' ), + \ 'stoponexit': 'term', + \ } + \ ) +endfunction + +function! vimspector#internal#job#KillCommand( job ) abort + if job_status( a:job ) == 'run' + call job_stop( a:job, 'term' ) + endif +endfunction + function! vimspector#internal#job#StartDebugSession( config ) abort if exists( 's:job' ) echom "Not starging: Job is already running" diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 10c4030..e8dbef3 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -18,6 +18,7 @@ import vim import json import os import functools +import subprocess from collections import defaultdict @@ -55,6 +56,8 @@ class DebugSession( object ): self._func_breakpoints = [] self._configuration = None + self._attach_process = None + vim.command( 'sign define vimspectorBP text==> texthl=Error' ) vim.command( 'sign define vimspectorBPDisabled text=!> texthl=Warning' ) @@ -221,6 +224,11 @@ class DebugSession( object ): self._Reset() def _Reset( self ): + if self._attach_process: + vim.eval( 'vimspector#internal#job#KillCommand( {} )'.format( + json.dumps( self._attach_process ) ) ) + self._attach_process = None + if self._uiTab: self._stackTraceView.Reset() self._variablesView.Reset() @@ -456,17 +464,64 @@ class DebugSession( object ): }, }, failure_handler = handler, timeout = 5000 ) - def _SelectProcess( self, adapter_config, launch_config ): - atttach_config = adapter_config[ 'attach' ] - if atttach_config[ 'pidSelect' ] == 'ask': - pid = utils.AskForInput( 'Enter PID to attach to: ' ) - launch_config[ atttach_config[ 'pidProperty' ] ] = pid - return - elif atttach_config[ 'pidSelect' ] == 'none': - return + def _PrepareAttach( self, adapter_config, launch_config ): - raise ValueError( 'Unrecognised pidSelect {0}'.format( - atttach_config[ 'pidSelect' ] ) ) + atttach_config = adapter_config[ 'attach' ] + + if 'remote' in atttach_config: + remote = atttach_config[ 'remote' ] + ssh = [ 'ssh' ] + + if 'account' in remote: + ssh.append( remote[ 'account' ] + '@' + remote[ 'host' ] ) + else: + ssh.append( remote[ 'host' ] ) + + cmd = ssh + remote[ 'pidCommand' ] + + self._logger.debug( 'Getting PID: %s', cmd ) + pid = subprocess.check_output( ssh + remote[ 'pidCommand' ] ).decode( + 'utf-8' ).strip() + self._logger.debug( 'Got PID: %s', pid ) + + cmd = ssh + remote[ 'attachCommand' ][:] + + for index, item in enumerate( cmd ): + cmd[ index ] = item.replace( '%PID%', pid ) + + # TODO: Log files, etc. ? + self._logger.debug( 'Running remote app: %s', cmd ) + self._attach_process = vim.eval( + 'vimspector#internal#job#RunCommand( {} )'.format( + json.dumps( cmd ) ) ) + else: + if atttach_config[ 'pidSelect' ] == 'ask': + pid = utils.AskForInput( 'Enter PID to attach to: ' ) + launch_config[ atttach_config[ 'pidProperty' ] ] = pid + return + elif atttach_config[ 'pidSelect' ] == 'none': + return + + raise ValueError( 'Unrecognised pidSelect {0}'.format( + atttach_config[ 'pidSelect' ] ) ) + + + def _PrepareRun( self, adapter_config, launch_config ): + run_config = adapter_config.get( 'launch', {} ) + + if 'remote' in run_config: + remote = run_config[ 'remote' ] + ssh = [ 'ssh' ] + if 'account' in remote: + ssh.append( remote[ 'account' ] + '@' + remote[ 'host' ] ) + else: + ssh.append( remote[ 'host' ] ) + + cmd = ssh + remote[ 'runCommand' ][:] + self._logger.debug( 'Running remote app: %s', cmd ) + self._attach_process = vim.eval( + 'vimspector#internal#job#RunCommand( {} )'.format( + json.dumps( cmd ) ) ) def _Initialise( self ): @@ -499,7 +554,9 @@ class DebugSession( object ): launch_config = self._configuration[ 'configuration' ] if launch_config.get( 'request' ) == "attach": - self._SelectProcess( adapter_config, launch_config ) + self._PrepareAttach( adapter_config, launch_config ) + elif launch_config.get( 'request' ) == "run": + self._PrepareRun( adapter_config, 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) From 463f7a8220b13d09a5cb451c952fecaaa5d6fa6e Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 4 Feb 2019 15:04:21 +0000 Subject: [PATCH 05/50] Mark unread output with *; Show vimspector log in output view --- python3/vimspector/code.py | 18 ++---- python3/vimspector/debug_session.py | 9 ++- python3/vimspector/output.py | 99 ++++++++++++++++++++++------- python3/vimspector/utils.py | 40 +++++++++++- 4 files changed, 129 insertions(+), 37 deletions(-) diff --git a/python3/vimspector/code.py b/python3/vimspector/code.py index 09fcc95..10299ec 100644 --- a/python3/vimspector/code.py +++ b/python3/vimspector/code.py @@ -64,18 +64,14 @@ class CodeView( object ): vim.current.window = self._window - buffer_number = int( vim.eval( 'bufnr( "{0}", 1 )'.format( - frame[ 'source' ][ 'path' ] ) ) ) - try: - vim.command( 'bu {0}'.format( buffer_number ) ) - self._window.cursor = ( frame[ 'line' ], frame[ 'column' ] ) - except vim.error as e: - if 'E325' not in str( e ): - self._logger.exception( - 'Unexpected error from vim: loading buffer {}'.format( - buffer_number ) ) - return False + utils.OpenFileInCurrentWindow( frame[ 'source' ][ 'path' ] ) + except vim.error: + self._logger.exception( 'Unexpected vim error opening file {}'.format( + frame[ 'source' ][ 'path' ] ) ) + return False + + self._window.cursor = ( frame[ 'line' ], frame[ 'column' ] ) self._signs[ 'vimspectorPC' ] = self._next_sign_id self._next_sign_id += 1 diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index e8dbef3..588b039 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -725,9 +725,12 @@ class DebugSession( object ): def OnEvent_stopped( self, message ): event = message[ 'body' ] - - utils.UserMessage( 'Paused in thread {0} due to {1}'.format( + msg = 'Paused in thread {0} due to {1}'.format( event.get( 'threadId', '' ), - event.get( 'description', event.get( 'reason', '' ) ) ) ) + event.get( 'description', event.get( 'reason', '' ) ) ) + utils.UserMessage( msg, persist = True ) + + if self._outputView: + self._outputView.ServerEcho( msg ) self._stackTraceView.OnStopped( event ) diff --git a/python3/vimspector/output.py b/python3/vimspector/output.py index 601183e..c3c0e0a 100644 --- a/python3/vimspector/output.py +++ b/python3/vimspector/output.py @@ -19,6 +19,14 @@ import vim import json +class TabBuffer( object ): + def __init__( self, buf, index ): + self.buf = buf + self.index = index + self.flag = False + self.job = None + + BUFFER_MAP = { 'console': 'Console', 'stdout': 'Console', @@ -40,7 +48,11 @@ class OutputView( object ): for b in set( BUFFER_MAP.values() ): self._CreateBuffer( b ) - self.ShowOutput( 'Console' ) + self._CreateBuffer( + 'Vimspector', + file_name = vim.eval( 'expand( "~/.vimspector.log" )' ) ) + + self._ShowOutput( 'Console' ) def ServerEcho( self, text ): self._Print( 'server', text.splitlines() ) @@ -58,15 +70,17 @@ class OutputView( object ): if category not in self._buffers: self._CreateBuffer( category ) - buf = self._buffers[ category ] + buf = self._buffers[ category ].buf + with utils.ModifiableScratchBuffer( buf ): utils.AppendToBuffer( buf, text_lines ) + self._ToggleFlag( category, True ) + # Scroll the buffer with utils.RestoreCurrentWindow(): with utils.RestoreCurrentBuffer( self._window ): - self.ShowOutput( category ) - vim.command( 'normal G' ) + self._ShowOutput( category ) def ConnectionClosed( self ): self._connection = None @@ -76,19 +90,28 @@ class OutputView( object ): def Clear( self ): for buf in self._buffers: - vim.command( 'bwipeout! {0}'.format( self._buffers[ buf ].name ) ) + vim.command( 'bwipeout! {0}'.format( self._buffers[ buf ].buf.name ) ) + + if 'Vimspector' in self._buffers: + if self._buffers[ 'Vimspector' ].job is not None: + utils.TerminateJob( self._buffers[ 'Vimspector' ].job ) self._buffers.clear() - def ShowOutput( self, category ): + def _ShowOutput( self, category ): vim.current.window = self._window - vim.command( 'bu {0}'.format( self._buffers[ category ].name ) ) + vim.command( 'bu {0}'.format( self._buffers[ category ].buf.name ) ) + vim.command( 'normal G' ) + + def ShowOutput( self, category ): + self._ToggleFlag( category, False ) + self._ShowOutput( category ) def Evaluate( self, frame, expression ): if not frame: return - console = self._buffers[ 'Console' ] + console = self._buffers[ 'Console' ].buf utils.AppendToBuffer( console, 'Evaluating: ' + expression ) def print_result( message ): @@ -110,24 +133,56 @@ class OutputView( object ): } } ) - def _CreateBuffer( self, category ): + def _ToggleFlag( self, category, flag ): + if self._buffers[ category ].flag != flag: + self._buffers[ category ].flag = flag + with utils.RestoreCurrentWindow(): + vim.current.window = self._window + self._RenderWinBar( category ) + + def _CreateBuffer( self, category, file_name = None ): with utils.RestoreCurrentWindow(): vim.current.window = self._window with utils.RestoreCurrentBuffer( self._window ): - vim.command( 'enew' ) - self._buffers[ category ] = vim.current.buffer - if category == 'Console': - utils.SetUpPromptBuffer( self._buffers[ category ], - 'vimspector.Console', - '> ', - 'vimspector#EvaluateConsole', - hidden=True ) + if file_name is not None: + tab_buffer = TabBuffer( utils.BufferForFile( file_name ), + len( self._buffers ) ) + tab_buffer.job = utils.SetUpTailBuffer( tab_buffer.buf, file_name ) + self._buffers[ category ] = tab_buffer else: - utils.SetUpHiddenBuffer( self._buffers[ category ], - 'vimspector.Output:{0}'.format( category ) ) + vim.command( 'enew' ) + tab_buffer = TabBuffer( vim.current.buffer, len( self._buffers ) ) + self._buffers[ category ] = tab_buffer + if category == 'Console': + utils.SetUpPromptBuffer( tab_buffer.buf, + 'vimspector.Console', + '> ', + 'vimspector#EvaluateConsole', + hidden=True ) + else: + utils.SetUpHiddenBuffer( + tab_buffer.buf, + 'vimspector.Output:{0}'.format( category ) ) - vim.command( "nnoremenu WinBar.{0} " - ":call vimspector#ShowOutput( '{0}' )".format( - utils.Escape( category ) ) ) + self._RenderWinBar( category ) + + def _RenderWinBar( self, category ): + tab_buffer = self._buffers[ category ] + + try: + if tab_buffer.flag: + vim.command( 'nunmenu WinBar.{}'.format( utils.Escape( category ) ) ) + else: + vim.command( 'nunmenu WinBar.{}*'.format( utils.Escape( category ) ) ) + except vim.error as e: + # E329 means the menu doesn't exist; ignore that. + if 'E329' not in str( e ): + raise + + vim.command( "nnoremenu 1.{0} WinBar.{1}{2} " + ":call vimspector#ShowOutput( '{1}' )".format( + tab_buffer.index, + utils.Escape( category ), + '*' if tab_buffer.flag else '' ) ) diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index 53654fd..6d2213e 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -32,6 +32,41 @@ def SetUpLogging( logger ): logger.addHandler( _log_handler ) +def BufferNumberForFile( file_name ): + return int( vim.eval( 'bufnr( "{0}", 1 )'.format( file_name ) ) ) + + +def BufferForFile( file_name ): + return vim.buffers[ BufferNumberForFile( file_name ) ] + + +def OpenFileInCurrentWindow( file_name ): + buffer_number = BufferNumberForFile( file_name ) + try: + vim.command( 'bu {0}'.format( buffer_number ) ) + except vim.error as e: + if 'E325' not in str( e ): + raise + + return vim.buffers[ buffer_number ] + + +def SetUpTailBuffer( buf, path ): + cmd = [ 'tail', '-F', '-n', '0', '--', path ] + return vim.eval( 'job_start( {}, {{ "out_io": "buffer",' + ' "out_buf": {},' + ' "in_io": "null",' + ' "err_io": "null",' + ' "stoponexit": "term",' + ' "out_modifiable": 0 }} )'.format( + json.dumps( cmd ), buf.number ) ) + + +def TerminateJob( job ): + if vim.eval( 'job_status( {} )'.format( job ) ) == 'run': + vim.eval( 'job_stop( {} )'.format( job ) ) + + def SetUpScratchBuffer( buf, name ): buf.options[ 'buftype' ] = 'nofile' buf.options[ 'swapfile' ] = False @@ -199,7 +234,10 @@ def SelectFromList( prompt, options ): def AskForInput( prompt ): # TODO: Handle the ctrl-c and such responses returning empty or something with InputSave(): - return vim.eval( "input( '{0}' )".format( Escape( prompt ) ) ) + try: + return vim.eval( "input( '{0}' )".format( Escape( prompt ) ) ) + except KeyboardInterrupt: + return '' def AppendToBuffer( buf, line_or_lines, modified=False ): From 85d2f10f32b0bab310406f9f70c5b7e740f0393f Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 4 Feb 2019 15:05:00 +0000 Subject: [PATCH 06/50] Only load threads after _both_ init and launch. Maybe this is the magic that works for all servers --- python3/vimspector/debug_session.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 588b039..9cca9b7 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -57,6 +57,8 @@ class DebugSession( object ): self._configuration = None self._attach_process = None + self._init_complete = False + self._launch_complete = False vim.command( 'sign define vimspectorBP text==> texthl=Error' ) vim.command( 'sign define vimspectorBPDisabled text=!> texthl=Warning' ) @@ -229,6 +231,9 @@ class DebugSession( object ): json.dumps( self._attach_process ) ) ) self._attach_process = None + self._init_complete = False + self._launch_complete = False + if self._uiTab: self._stackTraceView.Reset() self._variablesView.Reset() @@ -381,6 +386,9 @@ class DebugSession( object ): self._logger.info( 'Starting debug adapter with: {0}'.format( json.dumps( self._adapter ) ) ) + self._init_complete = False + self._launch_complete = False + self._connection_type = 'job' if 'port' in self._adapter: self._connection_type = 'channel' @@ -572,7 +580,7 @@ class DebugSession( object ): # then starts to listen for thread events to detect new or terminated # threads. # - lambda msg: self._stackTraceView.LoadThreads( True ), + lambda msg: self._OnLaunchComplete(), { 'command': launch_config[ 'request' ], 'arguments': launch_config @@ -586,11 +594,22 @@ class DebugSession( object ): self._codeView.AddBreakpoints( source, message[ 'body' ][ 'breakpoints' ] ) self._codeView.ShowBreakpoints() + def _OnLaunchComplete( self ): + self._launch_complete = True + self._LoadThreadsIfReady() + + def _OnInitializeComplete( self ): + self._init_complete = True + self._LoadThreadsIfReady() + + def _LoadThreadsIfReady( self ): + if self._launch_complete and self._init_complete: + self._stackTraceView.LoadThreads( True ) def OnEvent_initialized( self, message ): self._SendBreakpoints() self._connection.DoRequest( - None, + self._OnInitializeComplete(), { 'command': 'configurationDone', } From df04256cee3e55630ab7a35026a8776b966554ad Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 4 Feb 2019 15:05:16 +0000 Subject: [PATCH 07/50] FixUp: Merge error with launch variables --- python3/vimspector/debug_session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 9cca9b7..aa77993 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -154,6 +154,7 @@ class DebugSession( object ): } self._variables.update( adapter.get( 'variables', {} ) ) self._variables.update( configuration.get( 'variables', {} ) ) + self._variables.update( launch_variables ) utils.ExpandReferencesInDict( configuration, self._variables ) utils.ExpandReferencesInDict( adapter, self._variables ) @@ -515,7 +516,7 @@ class DebugSession( object ): def _PrepareRun( self, adapter_config, launch_config ): - run_config = adapter_config.get( 'launch', {} ) + run_config = adapter_config.get( 'launch', {} ) if 'remote' in run_config: remote = run_config[ 'remote' ] From 765a9e4a98af3c6965cf4c846b4bf5e68a0394a9 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 4 Feb 2019 15:06:05 +0000 Subject: [PATCH 08/50] Don't bother with the disconnect on VimLeave. It causes more problems than it solves. Just let vim kill the job --- python3/vimspector/debug_session.py | 38 +---------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index aa77993..ad6bc9d 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -232,6 +232,7 @@ class DebugSession( object ): json.dumps( self._attach_process ) ) ) self._attach_process = None + self._connection = None self._init_complete = False self._launch_complete = False @@ -243,10 +244,6 @@ class DebugSession( object ): vim.current.tabpage = self._uiTab vim.command( 'tabclose!' ) - vim.eval( 'vimspector#internal#{}#Reset()'.format( - self._connection_type ) ) - vim.eval( 'vimspector#internal#state#Reset()' ) - # make sure that we're displaying signs in any still-open buffers self._UpdateUIBreakpoints() @@ -421,45 +418,12 @@ class DebugSession( object ): self._logger.info( 'Debug Adapter Started' ) - vim.command( 'augroup vimspector_cleanup' ) - vim.command( 'autocmd!' ) - vim.command( 'autocmd VimLeavePre * py3 ' - '_vimspector_session.CloseDown()' ) - vim.command( 'augroup END' ) - - def CloseDown( self ): - # We have to use a dict because of python's scoping/assignment rules (state - # = False would touch a state variable in handler, not in the enclosing - # scope) - state = { 'done': False } - - def handler( *args ): - state[ 'done' ] = True - - self._connection.DoRequest( handler, { - 'command': 'disconnect', - 'arguments': { - 'terminateDebugee': True - }, - }, failure_handler = handler, timeout = 5000 ) - - # This request times out after 5 seconds - while not state[ 'done' ]: - vim.eval( 'vimspector#internal#{}#ForceRead()'.format( - self._connection_type ) ) - - vim.eval( 'vimspector#internal#{}#StopDebugSession()'.format( - self._connection_type ) ) - def _StopDebugAdapter( self, callback = None ): def handler( *args ): vim.eval( 'vimspector#internal#{}#StopDebugSession()'.format( self._connection_type ) ) - vim.command( 'au! vimspector_cleanup' ) - self._connection.Reset() - self._connection = None self._stackTraceView.ConnectionClosed() self._variablesView.ConnectionClosed() self._outputView.ConnectionClosed() From e006b1510003f1f3f382ebcd5f2c0e658610cd8d Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 12 Feb 2019 09:01:09 +0000 Subject: [PATCH 09/50] More improvements to startup and closedown stability. Remove some hack code --- autoload/vimspector/internal/job.vim | 13 +--- python3/vimspector/debug_session.py | 105 +++++++++++++++------------ python3/vimspector/output.py | 4 + python3/vimspector/stack_trace.py | 9 +++ python3/vimspector/variables.py | 3 + 5 files changed, 80 insertions(+), 54 deletions(-) diff --git a/autoload/vimspector/internal/job.vim b/autoload/vimspector/internal/job.vim index f696848..c0785de 100644 --- a/autoload/vimspector/internal/job.vim +++ b/autoload/vimspector/internal/job.vim @@ -20,24 +20,21 @@ set cpo&vim " }}} function! s:_OnServerData( channel, data ) abort - py3 << EOF -_vimspector_session.OnChannelData( vim.eval( 'a:data' ) ) -EOF + py3 _vimspector_session.OnChannelData( vim.eval( 'a:data' ) ) endfunction function! s:_OnServerError( channel, data ) abort - py3 << EOF -_vimspector_session.OnServerStderr( vim.eval( 'a:data' ) ) -EOF + py3 _vimspector_session.OnServerStderr( vim.eval( 'a:data' ) ) endfunction function! s:_OnExit( channel, status ) abort echom "Channel exit with status " . a:status + unlet s:job + py3 _vimspector_session.OnServerExit( vim.eval( 'a:status' ) ) endfunction function! s:_OnClose( channel ) abort echom "Channel closed" - " py3 _vimspector_session.OnChannelClosed() endfunction function! s:_Send( msg ) abort @@ -120,8 +117,6 @@ function! vimspector#internal#job#StopDebugSession() abort if job_status( s:job ) == 'run' call job_stop( s:job, 'term' ) endif - - unlet s:job endfunction function! vimspector#internal#job#Reset() abort diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index ad6bc9d..b3bc2b1 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -37,13 +37,13 @@ class DebugSession( object ): self._logger = logging.getLogger( __name__ ) utils.SetUpLogging( self._logger ) - self._connection = None - self._uiTab = None self._stackTraceView = None self._variablesView = None self._outputView = None + self._run_on_server_exit = None + self._next_sign_id = SIGN_ID_OFFSET # FIXME: This needs redesigning. There are a number of problems: @@ -54,15 +54,18 @@ class DebugSession( object ): # messy and ill-defined. self._line_breakpoints = defaultdict( list ) self._func_breakpoints = [] - self._configuration = None - self._attach_process = None - self._init_complete = False - self._launch_complete = False + self._ResetServerState() vim.command( 'sign define vimspectorBP text==> texthl=Error' ) vim.command( 'sign define vimspectorBPDisabled text=!> texthl=Warning' ) + def _ResetServerState( self ): + self._connection = None + self._configuration = None + self._init_complete = False + self._launch_complete = False + def ToggleBreakpoint( self ): line, column = vim.current.window.cursor file_name = vim.current.buffer.name @@ -167,26 +170,26 @@ class DebugSession( object ): self._StartWithConfiguration( configuration, adapter ) def _StartWithConfiguration( self, configuration, adapter ): - self._configuration = configuration - self._adapter = adapter - - self._logger.info( 'Configuration: {0}'.format( json.dumps( - self._configuration ) ) ) - self._logger.info( 'Adapter: {0}'.format( json.dumps( - self._adapter ) ) ) - def start(): - self._StartDebugAdapter() - self._Initialise() + self._configuration = configuration + self._adapter = adapter + + self._logger.info( 'Configuration: {0}'.format( json.dumps( + self._configuration ) ) ) + self._logger.info( 'Adapter: {0}'.format( json.dumps( + self._adapter ) ) ) if not self._uiTab: self._SetUpUI() else: vim.current.tabpage = self._uiTab - # FIXME: Encapsulation - self._stackTraceView._connection = self._connection - self._variablesView._connection = self._connection - self._outputView._connection = self._connection + + self._StartDebugAdapter() + self._Initialise() + + self._stackTraceView.ConnectionUp( self._connection ) + self._variablesView.ConnectionUp( self._connection ) + self._outputView.ConnectionUp( self._connection ) if self._connection: self._StopDebugAdapter( start ) @@ -215,6 +218,7 @@ class DebugSession( object ): self._connection.OnRequestTimeout( timer_id ) def OnChannelClosed( self ): + # TODO: Not calld self._connection = None def Stop( self ): @@ -227,15 +231,6 @@ class DebugSession( object ): self._Reset() def _Reset( self ): - if self._attach_process: - vim.eval( 'vimspector#internal#job#KillCommand( {} )'.format( - json.dumps( self._attach_process ) ) ) - self._attach_process = None - - self._connection = None - self._init_complete = False - self._launch_complete = False - if self._uiTab: self._stackTraceView.Reset() self._variablesView.Reset() @@ -243,6 +238,7 @@ class DebugSession( object ): self._codeView.Reset() vim.current.tabpage = self._uiTab vim.command( 'tabclose!' ) + self._uiTab = None # make sure that we're displaying signs in any still-open buffers self._UpdateUIBreakpoints() @@ -381,11 +377,17 @@ class DebugSession( object ): return True def _StartDebugAdapter( self ): + if self._connection: + utils.UserMessage( 'The connection is already created. Please try again', + persist = True ) + return + self._logger.info( 'Starting debug adapter with: {0}'.format( 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: @@ -423,12 +425,9 @@ class DebugSession( object ): vim.eval( 'vimspector#internal#{}#StopDebugSession()'.format( self._connection_type ) ) - self._connection.Reset() - self._stackTraceView.ConnectionClosed() - self._variablesView.ConnectionClosed() - self._outputView.ConnectionClosed() if callback: - callback() + assert not self._run_on_server_exit + self._run_on_server_exit = callback self._connection.DoRequest( handler, { 'command': 'disconnect', @@ -537,14 +536,6 @@ class DebugSession( object ): launch_config[ 'name' ] = 'test' self._connection.DoRequest( - # 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. - # lambda msg: self._OnLaunchComplete(), { 'command': launch_config[ 'request' ], @@ -568,13 +559,25 @@ class DebugSession( object ): 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 apear that way. + # if self._launch_complete and self._init_complete: self._stackTraceView.LoadThreads( True ) def OnEvent_initialized( self, message ): self._SendBreakpoints() self._connection.DoRequest( - self._OnInitializeComplete(), + lambda msg: self._OnInitializeComplete(), { 'command': 'configurationDone', } @@ -629,9 +632,21 @@ class DebugSession( object ): self._stackTraceView.Clear() self._variablesView.Clear() - def OnEvent_terminated( self, message ): + def OnServerExit( self, status ): self.Clear() - self._connection = None + + self._connection.Reset() + self._stackTraceView.ConnectionClosed() + self._variablesView.ConnectionClosed() + self._outputView.ConnectionClosed() + + self._ResetServerState() + + if self._run_on_server_exit: + self._run_on_server_exit() + + def OnEvent_terminated( self, message ): + # We will handle this when the server actually exists utils.UserMessage( "Debugging was terminated." ) def _RemoveBreakpoints( self ): diff --git a/python3/vimspector/output.py b/python3/vimspector/output.py index c3c0e0a..b2502a3 100644 --- a/python3/vimspector/output.py +++ b/python3/vimspector/output.py @@ -82,7 +82,11 @@ class OutputView( object ): with utils.RestoreCurrentBuffer( self._window ): self._ShowOutput( category ) + def ConnectionUp( self, connection ): + self._connection = connection + def ConnectionClosed( self ): + # Don't clear because output is probably still useful self._connection = None def Reset( self ): diff --git a/python3/vimspector/stack_trace.py b/python3/vimspector/stack_trace.py index 920f0da..fc0925c 100644 --- a/python3/vimspector/stack_trace.py +++ b/python3/vimspector/stack_trace.py @@ -37,6 +37,12 @@ class StackTraceView( object ): self._line_to_frame = {} self._line_to_thread = {} + # TODO: We really need a proper state model + # + # AWAIT_CONNECTION -- OnServerReady / RequestThreads --> REQUESTING_THREADS + # REQUESTING -- OnGotThreads / RequestScopes --> REQUESTING_SCOPES + # + def GetCurrentThreadId( self ): return self._currentThread @@ -51,6 +57,9 @@ class StackTraceView( object ): with utils.ModifiableScratchBuffer( self._buf ): utils.ClearBuffer( self._buf ) + def ConnectionUp( self, connection ): + self._connection = connection + def ConnectionClosed( self ): self.Clear() self._connection = None diff --git a/python3/vimspector/variables.py b/python3/vimspector/variables.py index 423cdf7..7fcb20e 100644 --- a/python3/vimspector/variables.py +++ b/python3/vimspector/variables.py @@ -92,6 +92,9 @@ class VariablesView( object ): with utils.ModifiableScratchBuffer( self._watch.win.buffer ): utils.ClearBuffer( self._watch.win.buffer ) + def ConnectionUp( self, connection ): + self._connection = connection + def ConnectionClosed( self ): self.Clear() self._connection = None From bb3909c16f24ff85ac76943b90a9aa2fbd83bf82 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 12 Feb 2019 09:04:52 +0000 Subject: [PATCH 10/50] Improve output view; use jobs to display logs and remote commands --- autoload/vimspector/internal/job.vim | 19 ----------- python3/vimspector/debug_session.py | 43 +++++++++++++++-------- python3/vimspector/output.py | 36 ++++++++++++-------- python3/vimspector/utils.py | 51 ++++++++++++++++++++-------- 4 files changed, 86 insertions(+), 63 deletions(-) diff --git a/autoload/vimspector/internal/job.vim b/autoload/vimspector/internal/job.vim index c0785de..374c9a6 100644 --- a/autoload/vimspector/internal/job.vim +++ b/autoload/vimspector/internal/job.vim @@ -58,25 +58,6 @@ function! s:_Send( msg ) abort return 1 endfunction -function! vimspector#internal#job#RunCommand( cmd ) abort - return job_start( a:cmd, - \ { - \ 'in_mode': 'raw', - \ 'out_mode': 'raw', - \ 'err_mode': 'raw', - \ 'out_cb': funcref( 's:_OnServerError' ), - \ 'err_cb': funcref( 's:_OnServerError' ), - \ 'stoponexit': 'term', - \ } - \ ) -endfunction - -function! vimspector#internal#job#KillCommand( job ) abort - if job_status( a:job ) == 'run' - call job_stop( a:job, 'term' ) - endif -endfunction - function! vimspector#internal#job#StartDebugSession( config ) abort if exists( 's:job' ) echom "Not starging: Job is already running" diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index b3bc2b1..d1d0cd9 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -211,7 +211,8 @@ class DebugSession( object ): def OnServerStderr( self, data ): self._logger.info( "Server stderr: %s", data ) if self._outputView: - self._outputView.ServerEcho( data ) + self._outputView.Print( 'server', data ) + def OnRequestTimeout( self, timer_id ): if self._connection: @@ -461,11 +462,8 @@ class DebugSession( object ): for index, item in enumerate( cmd ): cmd[ index ] = item.replace( '%PID%', pid ) - # TODO: Log files, etc. ? self._logger.debug( 'Running remote app: %s', cmd ) - self._attach_process = vim.eval( - 'vimspector#internal#job#RunCommand( {} )'.format( - json.dumps( cmd ) ) ) + self._outputView.RunJobWithOutput( 'Remote', cmd ) else: if atttach_config[ 'pidSelect' ] == 'ask': pid = utils.AskForInput( 'Enter PID to attach to: ' ) @@ -478,7 +476,7 @@ class DebugSession( object ): atttach_config[ 'pidSelect' ] ) ) - def _PrepareRun( self, adapter_config, launch_config ): + def _PrepareLaunch( self, command_line, adapter_config, launch_config ): run_config = adapter_config.get( 'launch', {} ) if 'remote' in run_config: @@ -490,10 +488,18 @@ class DebugSession( object ): ssh.append( remote[ 'host' ] ) cmd = ssh + remote[ 'runCommand' ][:] - self._logger.debug( 'Running remote app: %s', cmd ) - self._attach_process = vim.eval( - 'vimspector#internal#job#RunCommand( {} )'.format( - json.dumps( cmd ) ) ) + 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._outputView.RunJobWithOutput( 'Remote', full_cmd ) def _Initialise( self ): @@ -518,17 +524,24 @@ class DebugSession( object ): def OnFailure( self, reason, message ): msg = "Request for '{}' failed: {}".format( message[ 'command' ], reason ) - self._outputView.ServerEcho( msg ) + self._outputView.Print( 'server', msg ) def _Launch( self ): self._logger.debug( "LAUNCH!" ) adapter_config = self._adapter launch_config = self._configuration[ 'configuration' ] - if launch_config.get( 'request' ) == "attach": + request = self._configuration.get( + 'remote-request', + launch_config.get( 'request', 'launch' ) ) + + if request == "attach": self._PrepareAttach( adapter_config, launch_config ) - elif launch_config.get( 'request' ) == "run": - self._PrepareRun( adapter_config, launch_config ) + elif request == "launch": + # FIXME: This cmdLine hack is not fun. + self._PrepareLaunch( self._configuration.get( 'remote-cmdLine', [] ), + adapter_config, + 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) @@ -730,6 +743,6 @@ class DebugSession( object ): utils.UserMessage( msg, persist = True ) if self._outputView: - self._outputView.ServerEcho( msg ) + self._outputView.Print( 'server', msg ) self._stackTraceView.OnStopped( event ) diff --git a/python3/vimspector/output.py b/python3/vimspector/output.py index b2502a3..d522c7b 100644 --- a/python3/vimspector/output.py +++ b/python3/vimspector/output.py @@ -24,7 +24,7 @@ class TabBuffer( object ): self.buf = buf self.index = index self.flag = False - self.job = None + self.is_job = False BUFFER_MAP = { @@ -54,7 +54,7 @@ class OutputView( object ): self._ShowOutput( 'Console' ) - def ServerEcho( self, text ): + def Print( self, categroy, text ): self._Print( 'server', text.splitlines() ) def OnOutput( self, event ): @@ -93,12 +93,10 @@ class OutputView( object ): self.Clear() def Clear( self ): - for buf in self._buffers: - vim.command( 'bwipeout! {0}'.format( self._buffers[ buf ].buf.name ) ) - - if 'Vimspector' in self._buffers: - if self._buffers[ 'Vimspector' ].job is not None: - utils.TerminateJob( self._buffers[ 'Vimspector' ].job ) + for category, tab_buffer in self._buffers.items(): + if tab_buffer.is_job: + utils.CleanUpCommand( category ) + vim.command( 'bdelete! {0}'.format( tab_buffer.buf.number ) ) self._buffers.clear() @@ -113,6 +111,7 @@ class OutputView( object ): def Evaluate( self, frame, expression ): if not frame: + self.Print( 'Console', 'There is no current stack frame' ) return console = self._buffers[ 'Console' ].buf @@ -144,17 +143,26 @@ class OutputView( object ): vim.current.window = self._window self._RenderWinBar( category ) - def _CreateBuffer( self, category, file_name = None ): + + def RunJobWithOutput( self, category, cmd ): + self._CreateBuffer( category, cmd = cmd ) + + + def _CreateBuffer( self, category, file_name = None, cmd = None ): with utils.RestoreCurrentWindow(): vim.current.window = self._window with utils.RestoreCurrentBuffer( self._window ): if file_name is not None: - tab_buffer = TabBuffer( utils.BufferForFile( file_name ), - len( self._buffers ) ) - tab_buffer.job = utils.SetUpTailBuffer( tab_buffer.buf, file_name ) - self._buffers[ category ] = tab_buffer + assert cmd is None + cmd = [ 'tail', '-F', '-n', '+1', '--', file_name ] + + if cmd is not None: + buf = utils.SetUpCommandBuffer( cmd, category ) + self._buffers[ category ] = TabBuffer( buf, len( self._buffers ) ) + self._buffers[ category ].is_job = True + self._RenderWinBar( category ) else: vim.command( 'enew' ) tab_buffer = TabBuffer( vim.current.buffer, len( self._buffers ) ) @@ -170,7 +178,7 @@ class OutputView( object ): tab_buffer.buf, 'vimspector.Output:{0}'.format( category ) ) - self._RenderWinBar( category ) + self._RenderWinBar( category ) def _RenderWinBar( self, category ): tab_buffer = self._buffers[ category ] diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index 6d2213e..5313cff 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -51,15 +51,39 @@ def OpenFileInCurrentWindow( file_name ): return vim.buffers[ buffer_number ] -def SetUpTailBuffer( buf, path ): - cmd = [ 'tail', '-F', '-n', '0', '--', path ] - return vim.eval( 'job_start( {}, {{ "out_io": "buffer",' - ' "out_buf": {},' - ' "in_io": "null",' - ' "err_io": "null",' - ' "stoponexit": "term",' - ' "out_modifiable": 0 }} )'.format( - json.dumps( cmd ), buf.number ) ) +def SetUpCommandBuffer( cmd, name ): + vim.command( + 'let g:vimspector_command_job_{name} = job_start(' + ' {cmd},' + ' {{' + ' "out_io": "buffer",' + ' "in_io": "null",' + ' "err_io": "buffer",' + ' "out_name": "_vimspector_log_{name}",' + ' "err_name": "_vimspector_log_{name}",' + ' "out_modifiable": 0,' + ' "err_modifiable": 0,' + ' "stoponexit": "term",' + ' }} )'.format( name = name, + cmd = json.dumps( cmd ) ) ) + + stdout = vim.eval( 'ch_getbufnr( ' + ' job_getchannel( g:vimspector_command_job_{name} ), ' + ' "out"' + ')'.format( name = name ) ) + stderr = vim.eval( 'ch_getbufnr( ' + ' job_getchannel( g:vimspector_command_job_{name} ), ' + ' "err"' + ')'.format( name = name ) ) + + assert stdout == stderr + return vim.buffers[ int( stdout ) ] + + +def CleanUpCommand( name ): + cmd = 'job_stop( g:vimspector_command_job_{name}, "kill" )'.format( + name = name ) + vim.eval( cmd ) def TerminateJob( job ): @@ -253,14 +277,11 @@ def AppendToBuffer( buf, line_or_lines, modified=False ): else: line = 1 buf[:] = line_or_lines - except vim.error as e: + except: # There seem to be a lot of Vim bugs that lead to E351, whose help says that # this is an internal error. Ignore the error, but write a trace to the log. - if 'E315' in str( e ): - logging.getLogger( __name__ ).exception( - 'Internal error while updating buffer' ) - else: - raise e + logging.getLogger( __name__ ).exception( + 'Internal error while updating buffer %s (%s)', buf.name, buf.number ) finally: if not modified: buf.options[ 'modified' ] = False From 0b40c3e04ae46f829a83dc38d0fb2ed79fbf652e Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 12 Feb 2019 09:05:19 +0000 Subject: [PATCH 11/50] Sort the list of configs so they appear consistently to the user --- 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 d1d0cd9..cc238bd 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -136,7 +136,7 @@ class DebugSession( object ): else: configuration_name = utils.SelectFromList( 'Which launch configuration?', - list( configurations.keys() ) ) + sorted( list( configurations.keys() ) ) ) if not configuration_name or configuration_name not in configurations: return From 215af1316a8b6227fa0f01fc4a1f6c763945c22d Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 12 Feb 2019 09:05:47 +0000 Subject: [PATCH 12/50] Fix runInTerminal when the cwd supplied is (heinously) empty --- python3/vimspector/debug_session.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index cc238bd..e4b2b59 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -614,8 +614,10 @@ class DebugSession( object ): def OnRequest_runInTerminal( self, message ): params = message[ 'arguments' ] - if 'cwd' not in params: + if not params.get( 'cwd' ) : params[ 'cwd' ] = self._workspace_root + self._logger.debug( 'Defaulting working directory to %s', + params[ 'cwd' ] ) buffer_number = self._codeView.LaunchTerminal( params ) From ba456e863d8e2ec974fab6a5b6d0621ada1d1fce Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 12 Feb 2019 09:06:13 +0000 Subject: [PATCH 13/50] Replace the log file each time, as this makes debugging easiser --- python3/vimspector/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index 5313cff..647988d 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -21,7 +21,8 @@ import vim import json import string -_log_handler = logging.FileHandler( os.path.expanduser( '~/.vimspector.log' ) ) +_log_handler = logging.FileHandler( os.path.expanduser( '~/.vimspector.log' ), + mode = 'w' ) _log_handler.setFormatter( logging.Formatter( '%(asctime)s - %(levelname)s - %(message)s' ) ) From 305309dcb12b03285e14ef651179d83e33970c83 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 12 Feb 2019 12:33:40 +0000 Subject: [PATCH 14/50] WIP: Don't send many outstanding threads requests --- python3/vimspector/stack_trace.py | 37 ++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/python3/vimspector/stack_trace.py b/python3/vimspector/stack_trace.py index fc0925c..61f1d64 100644 --- a/python3/vimspector/stack_trace.py +++ b/python3/vimspector/stack_trace.py @@ -42,6 +42,9 @@ class StackTraceView( object ): # AWAIT_CONNECTION -- OnServerReady / RequestThreads --> REQUESTING_THREADS # REQUESTING -- OnGotThreads / RequestScopes --> REQUESTING_SCOPES # + # When we attach using gdbserver, this whole thing breaks because we request + # the threads over and over and get duff data back on later threads. + self._requesting_threads = False def GetCurrentThreadId( self ): @@ -58,7 +61,8 @@ class StackTraceView( object ): utils.ClearBuffer( self._buf ) def ConnectionUp( self, connection ): - self._connection = connection + self._connection = connection + self._requesting_threads = False def ConnectionClosed( self ): self.Clear() @@ -69,26 +73,38 @@ class StackTraceView( object ): # TODO: delete the buffer ? def LoadThreads( self, infer_current_frame ): + pending_request = False + if self._requesting_threads: + pending_request = True + return + def consume_threads( message ): - self._threads.clear() + self._requesting_threads = False if not message[ 'body' ][ 'threads' ]: - # This is a protocol error. It is required to return at least one! - utils.UserMessage( 'Server returned no threads. Is it running?', - persist = True ) - return + if pending_request: + # We may have hit a thread event, so try again. + self.LoadThreads( infer_current_frame ) + return + else: + # This is a protocol error. It is required to return at least one! + utils.UserMessage( 'Server returned no threads. Is it running?', + persist = True ) + + self._threads.clear() for thread in message[ 'body' ][ 'threads' ]: self._threads.append( thread ) if infer_current_frame and thread[ 'id' ] == self._currentThread: self._LoadStackTrace( thread, True ) - elif infer_current_frame and not self._currentThread: + elif infer_current_frame and self._currentThread is None: self._currentThread = thread[ 'id' ] self._LoadStackTrace( thread, True ) self._DrawThreads() + self._requesting_threads = True self._connection.DoRequest( consume_threads, { 'command': 'threads', } ) @@ -155,7 +171,7 @@ class StackTraceView( object ): elif event.get( 'allThreadsStopped', False ) and self._threads: self._currentThread = self._threads[ 0 ][ 'id' ] - if self._currentThread: + if self._currentThread is not None: for thread in self._threads: if thread[ 'id' ] == self._currentThread: self._LoadStackTrace( thread, True ) @@ -165,10 +181,11 @@ class StackTraceView( object ): def OnThreadEvent( self, event ): if event[ 'reason' ] == 'started' and self._currentThread is None: + self._currentThread = event[ 'threadId' ] self.LoadThreads( True ) def Continue( self ): - if not self._currentThread: + if self._currentThread is None: utils.UserMessage( 'No current thread', persist = True ) return @@ -183,7 +200,7 @@ class StackTraceView( object ): self.LoadThreads( True ) def Pause( self ): - if not self._currentThread: + if self._currentThread is None: utils.UserMessage( 'No current thread', persist = True ) return From 09850702cf5f3fbfc35eaa5eabbb4d6529d6aefe Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 12 Feb 2019 12:34:17 +0000 Subject: [PATCH 15/50] Print a message if we fail to get a PID --- python3/vimspector/debug_session.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index e4b2b59..1e239e1 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -457,6 +457,11 @@ class DebugSession( object ): '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 + cmd = ssh + remote[ 'attachCommand' ][:] for index, item in enumerate( cmd ): From 3092c06cd74d59d3222da5a01f008d998b801a72 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 12 Feb 2019 22:41:10 +0000 Subject: [PATCH 16/50] Rudimentary support for listing breakpoints using the quickfix window --- autoload/vimspector.vim | 4 ++++ python3/vimspector/code.py | 16 ++++++++++++++ python3/vimspector/debug_session.py | 33 +++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/autoload/vimspector.vim b/autoload/vimspector.vim index 44b8ea0..c3138c7 100644 --- a/autoload/vimspector.vim +++ b/autoload/vimspector.vim @@ -102,6 +102,10 @@ function! vimspector#ShowOutput( category ) abort py3 _vimspector_session.ShowOutput( vim.eval( 'a:category' ) ) endfunction +function! vimspector#ListBreakpoints() abort + py3 _vimspector_session.ListBreakpoints() +endfunction + " Boilerplate {{{ let &cpo=s:save_cpo unlet s:save_cpo diff --git a/python3/vimspector/code.py b/python3/vimspector/code.py index 10299ec..ec64b6a 100644 --- a/python3/vimspector/code.py +++ b/python3/vimspector/code.py @@ -168,6 +168,22 @@ class CodeView( object ): file_name ) ) + def BreakpointsAsQuickFix( self ): + qf = [] + for file_name, breakpoints in self._breakpoints.items(): + for breakpoint in breakpoints: + qf.append( { + 'filename': file_name, + 'lnum': breakpoint.get( 'line', 1 ), + 'col': 1, + 'type': 'L', + 'valid': 1 if breakpoint.get( 'verified' ) else 0, + 'text': "Line breakpoint - {}".format( + 'VERIFIED' if breakpoint.get( 'verified' ) else 'INVALID' ) + } ) + return qf + + def LaunchTerminal( self, params ): # kind = params.get( 'kind', 'integrated' ) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index e4b2b59..81d8fb6 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -66,6 +66,39 @@ class DebugSession( object ): self._init_complete = False self._launch_complete = False + def ListBreakpoints( self ): + # FIXME: Handling of breakpoints is a mess, split between _codeView and this + # object. This makes no sense and should be centralised so that we don't + # have this duplication and bug factory. + qf = [] + if self._connection and self._codeView: + qf = self._codeView.BreakpointsAsQuickFix() + else: + for file_name, breakpoints in self._line_breakpoints.items(): + for bp in breakpoints: + qf.append( { + 'filename': file_name, + 'lnum': bp[ 'line' ], + 'col': 1, + 'type': 'L', + 'valid': 1 if bp[ 'state' ] == 'ENABLED' else 0, + 'text': "Line breakpoint - {}".format( + bp[ 'state' ] ) + } ) + # I think this shows that the qf list is not right for this. + for bp in self._func_breakpoints: + qf.append( { + 'filename': '', + 'lnum': 1, + 'col': 1, + 'type': 'F', + 'valid': 1, + 'text': "Function breakpoint: {}".format( bp[ 'function' ] ), + } ) + + vim.eval( 'setqflist( {} )'.format( json.dumps( qf ) ) ) + + def ToggleBreakpoint( self ): line, column = vim.current.window.cursor file_name = vim.current.buffer.name From 079b392157437de33bc0acf3ca173191c69b6919 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 15 Feb 2019 13:18:58 +0000 Subject: [PATCH 17/50] Fix adding function breakpoints --- plugin/vimspector.vim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/vimspector.vim b/plugin/vimspector.vim index f6d68c9..3655bb0 100644 --- a/plugin/vimspector.vim +++ b/plugin/vimspector.vim @@ -42,7 +42,7 @@ if s:mappings == 'VISUAL_STUDIO' nnoremap :call vimspector#Restart() nnoremap :call vimspector#Pause() nnoremap :call vimspector#ToggleBreakpoint() - nnoremap :call vimspector#AddFunctionBreakpoint( '' ) + nnoremap :call vimspector#AddFunctionBreakpoint( expand( '' ) ) nnoremap :call vimspector#StepOver() nnoremap :call vimspector#StepInto() nnoremap :call vimspector#StepOut() @@ -52,7 +52,7 @@ elseif s:mappings == 'HUMAN' nnoremap :call vimspector#Restart() nnoremap :call vimspector#Pause() nnoremap :call vimspector#ToggleBreakpoint() - nnoremap :call vimspector#AddFunctionBreakpoint( '' ) + nnoremap :call vimspector#AddFunctionBreakpoint( expand( '' ) ) nnoremap :call vimspector#StepOver() nnoremap :call vimspector#StepInto() nnoremap :call vimspector#StepOut() From c684fa0cbbb58fcb71f53d48f93e77ad2d73798e Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 15 Feb 2019 13:19:13 +0000 Subject: [PATCH 18/50] Fix balloons in the GUI --- python3/vimspector/utils.py | 8 ++++++++ python3/vimspector/variables.py | 8 +++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index 647988d..8c63d55 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -343,3 +343,11 @@ def ExpandReferencesInDict( obj, mapping, **kwargs ): for k in obj.keys(): obj[ k ] = expand_refs_in_object( obj[ k ] ) + + +def DisplayBaloon( is_term, display ): + if not is_term: + display = '\n'.join( display ) + + vim.eval( "balloon_show( {0} )".format( + json.dumps( display ) ) ) diff --git a/python3/vimspector/variables.py b/python3/vimspector/variables.py index 7fcb20e..a66bf57 100644 --- a/python3/vimspector/variables.py +++ b/python3/vimspector/variables.py @@ -14,7 +14,6 @@ # limitations under the License. import vim -import json import logging from collections import namedtuple from functools import partial @@ -85,6 +84,7 @@ class VariablesView( object ): self._oldoptions[ 'balloonevalterm' ] = vim.options[ 'balloonevalterm' ] vim.options[ 'balloonevalterm' ] = True + self._is_term = not bool( int( vim.eval( "has( 'gui_running' )" ) ) ) def Clear( self ): with utils.ModifiableScratchBuffer( self._vars.win.buffer ): @@ -356,13 +356,11 @@ class VariablesView( object ): 'Type: ' + body.get( 'type', '' ), 'Value: ' + result ] - vim.eval( "balloon_show( {0} )".format( - json.dumps( display ) ) ) + utils.DisplayBaloon( self._is_term, display ) def failure_handler( reason, message ): display = [ reason ] - vim.eval( "balloon_show( {0} )".format( - json.dumps( display ) ) ) + utils.DisplayBaloon( self._is_term, display ) self._connection.DoRequest( handler, { From 2cfd5afacb00202fe78dee65d8777b5ca69fff0d Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 17 Feb 2019 19:36:21 +0000 Subject: [PATCH 19/50] Add first-pass testing framework based on vim's runtest.vim --- .circleci/config.yml | 45 ++++ .circleci/install_vim.macos.sh | 6 + .gitignore | 6 + .gitmodules | 6 + install_gadget.py | 263 +++++++++++++++++++++ python3/vimspector/breakpoints.py | 190 +++++++++++++++ python3/vimspector/code.py | 21 +- python3/vimspector/debug_session.py | 199 ++-------------- run_test_vim | 8 + run_tests | 31 +++ tests/breakpoints.test.vim | 92 ++++++++ tests/run_test.vim | 347 ++++++++++++++++++++++++++++ tests/testdata/cpp/simple.cpp | 7 + tests/vimrc | 6 + 14 files changed, 1043 insertions(+), 184 deletions(-) create mode 100644 .circleci/config.yml create mode 100644 .circleci/install_vim.macos.sh create mode 100755 install_gadget.py create mode 100644 python3/vimspector/breakpoints.py create mode 100755 run_test_vim create mode 100755 run_tests create mode 100644 tests/breakpoints.test.vim create mode 100644 tests/run_test.vim create mode 100644 tests/testdata/cpp/simple.cpp create mode 100644 tests/vimrc diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..6659ada --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,45 @@ +# Python CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-python/ for more details +# +version: 2 +aliases: + cache: &cache + key: v1-vimspector-{{ .Environment.CIRCLE_JOB }} + update-submodules: &update-submodules + run: + name: Update submodules + command: git submodule update --init --recursive + install-vim: &install-vim + install-gadgets: &install-gadgets + run: + name: Install gadgets + command: 'python3 ./install_gadget.py' + run-tests: &run-tests + run: + name: Run tests + command: './run_tests' + # Increase the version key to clear the cache. + save-cache: &save-cache + save_cache: + <<: *cache + paths: + - 'gadgets/download/*/*/*/*.vsix' + - 'gadgets/download/*/*/*/*.gz' + restore-cache: &restore-cache + restore_cache: + <<: *cache +jobs: + build-macos: + macos: + xcode: "9.0" + steps: + - checkout + - *update-submodules + - *restore-cache + - run: + name: Install vim + command: .circleci/install_vim.macos.sh + - *install-gadgets + - *run-tests + - *save-cache diff --git a/.circleci/install_vim.macos.sh b/.circleci/install_vim.macos.sh new file mode 100644 index 0000000..84d6779 --- /dev/null +++ b/.circleci/install_vim.macos.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -e + +brea update || brew update +brew install vim --with-override-system-vim diff --git a/.gitignore b/.gitignore index 2fef3ae..56a56f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ *.un~ *.sw[op] __pycache__ +.vscode +tests/*.res +tests/messages +tests/debuglog +test.log +gadgets/ diff --git a/.gitmodules b/.gitmodules index e69de29..6c0bbe7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "vendor/vader.vim"] + path = vendor/vader.vim + url = https://github.com/junegunn/vader.vim +[submodule "vendor/vim-themis"] + path = vendor/vim-themis + url = https://github.com/thinca/vim-themis diff --git a/install_gadget.py b/install_gadget.py new file mode 100755 index 0000000..a634b3e --- /dev/null +++ b/install_gadget.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python + +try: + import urllib.request as urllib2 +except ImportError: + import urllib2 + +import contextlib +import os +import collections +import platform +import string +import zipfile +import shutil +import subprocess +import traceback +import tarfile +import hashlib + +GADGETS = { + 'vscode-cpptools': { + 'download': { + 'url': ( 'https://github.com/Microsoft/vscode-cpptools/releases/download/' + '${version}/${file_name}' ), + }, + 'all': { + 'version': '0.21.0', + }, + 'linux': { + 'file_name': 'cpptools-linux.vsix', + 'checksum': None, + }, + 'macos': { + 'file_name': 'cpptools-osx.vsix', + 'checksum': '4c149df241f8a548f928d824565aa9bb3ccaaa426c07aac3b47db3a51ebbb1f4', + }, + 'windows': { + 'file_name': 'cpptools-win32.vsix', + 'checksum': None, + }, + }, + 'vscode-python': { + 'download': { + 'url': ( 'https://github.com/Microsoft/vscode-python/releases/download/' + '${version}/${file_name}' ), + }, + 'all': { + 'version': '2018.12.1', + 'file_name': 'ms-python-release.vsix', + 'checksum': '0406028b7d2fbb86ffd6cda18a36638a95111fd35b19cc198d343a2828f5c3b1', + }, + }, + 'tclpro': { + 'repo': { + 'url': 'https://github.com/puremourning/TclProDebug', + 'ref': 'master', + }, + 'do': lambda root: InstallTclProDebug( root ) + }, + 'vscode-mono-debug': { + 'download': { + 'url': 'https://marketplace.visualstudio.com/_apis/public/gallery/' + 'publishers/ms-vscode/vsextensions/mono-debug/${version}/' + 'vspackage', + 'target': 'vscode-mono-debug.tar.gz', + 'format': 'tar', + }, + 'all': { + 'file_name': 'vscode-mono-debug.vsix', + 'version': '0.15.8', + 'checksum': '723eb2b621b99d65a24f215cb64b45f5fe694105613a900a03c859a62a810470', + } + }, +} + +@contextlib.contextmanager +def CurrentWorkingDir( d ): + cur_d = os.getcwd() + try: + os.chdir( d ) + yield + finally: + os.chdir( cur_d ) + +def InstallTclProDebug( root ): + configure = [ './configure' ] + + if OS == 'macos': + # Apple removed the headers from system frameworks because they are + # determined to make life difficult. And the TCL configure scripts are super + # old so don't know about this. So we do their job for them and try and find + # a tclConfig.sh. + # + # NOTE however that in Apple's infinite wisdom, installing the "headers" in + # the other location is actually broken because the paths in the + # tclConfig.sh are pointing at the _old_ location. You actually do have to + # run the package installation which puts the headers back in order to work. + # This is why the below list is does not contain stuff from + # /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform + # '/Applications/Xcode.app/Contents/Developer/Platforms' + # '/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System' + # '/Library/Frameworks/Tcl.framework', + # '/Applications/Xcode.app/Contents/Developer/Platforms' + # '/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System' + # '/Library/Frameworks/Tcl.framework/Versions' + # '/Current', + for p in [ '/System/Library/Frameworks/Tcl.framework/', + '/usr/local/opt/tcl-tk/lib' ]: + if os.path.exists( os.path.join( p, 'tclConfig.sh' ) ): + configure.append( '--with-tcl=' + p ) + break + + + with CurrentWorkingDir( os.path.join( root, 'lib', 'tclparser' ) ): + subprocess.check_call( configure ) + subprocess.check_call( [ 'make' ] ) + + MakeSymlink( gadget_dir, 'tclpro', root ) + + +def DownloadFileTo( url, destination, file_name = None, checksum = None ): + if not file_name: + file_name = url.split( '/' )[ -1 ] + + file_path = os.path.abspath( os.path.join( destination, file_name ) ) + + if not os.path.isdir( destination ): + os.makedirs( destination ) + + if os.path.exists( file_path ): + if checksum: + if ValidateCheckSumSHA256( file_path, checksum ): + print( "Checksum matches for {}, using it".format( file_path ) ) + return file_path + else: + print( "Checksum doesn't match for {}, removing it".format( + file_path ) ) + + print( "Removing existing {}".format( file_path ) ) + os.remove( file_path ) + + + r = urllib2.Request( url, headers = { 'User-Agent': 'Vimspector' } ) + + print( "Downloading {} to {}/{}".format( url, destination, file_name ) ) + + with contextlib.closing( urllib2.urlopen( r ) ) as u: + with open( file_path, 'wb' ) as f: + f.write( u.read() ) + + if checksum: + if not ValidateCheckSumSHA256( file_path, checksum ): + raise RuntimeError( + 'Checksum for {} ({}) does not match expected {}'.format( + file_path, + GetChecksumSHA254( file_path ), + checksum ) ) + else: + print( "Checksum for {}: {}".format( file_path, + ) ) + + return file_path + + +def GetChecksumSHA254( file_path ): + with open( file_path, 'rb' ) as existing_file: + return hashlib.sha256( existing_file.read() ).hexdigest() + + +def ValidateCheckSumSHA256( file_path, checksum ): + existing_sha256 = GetChecksumSHA254( file_path ) + return existing_sha256 == checksum + + +def RemoveIfExists( destination ): + if os.path.exists( destination ) or os.path.islink( destination ): + print( "Removing existing {}".format( destination ) ) + if os.path.isdir( destination ): + shutil.rmtree( destination ) + else: + os.remove( destination ) + + +def ExtractZipTo( file_path, destination, format ): + print( "Extracting {} to {}".format( file_path, destination ) ) + RemoveIfExists( destination ) + + if format == 'zip': + with zipfile.ZipFile( file_path ) as f: + f.extractall( path = destination ) + return + elif format == 'tar': + try: + with tarfile.open( file_path ) as f: + f.extractall( path = destination ) + except Exception: + # There seems to a bug in python's tarfile that means it can't read some + # windows-generated tar files + os.makedirs( destination ) + with CurrentWorkingDir( destination ): + subprocess.check_call( [ 'tar', 'zxvf', file_path ] ) + + +def MakeSymlink( in_folder, link, pointing_to ): + RemoveIfExists( os.path.join( in_folder, link ) ) + os.symlink( pointing_to, os.path.join( in_folder, link ) ) + + +def CloneRepoTo( url, ref, destination ): + RemoveIfExists( destination ) + subprocess.check_call( [ 'git', 'clone', url, destination ] ) + subprocess.check_call( [ 'git', '-C', destination, 'checkout', ref ] ) + +if platform.system() == 'Darwin': + OS = 'macos' +elif platform.system() == 'Winwdows': + OS = 'windows' +else: + OS = 'linux' + +gadget_dir = os.path.join( os.path.dirname( __file__ ), 'gadgets', OS ) + +for name, gadget in GADGETS.items(): + try: + v = {} + v.update( gadget.get( 'all', {} ) ) + v.update( gadget.get( OS, {} ) ) + + if 'download' in gadget: + if 'file_name' not in v: + raise RuntimeError( "Unsupported OS {} for gadget {}".format( OS, + name ) ) + + destination = os.path.join( gadget_dir, 'download', name, v[ 'version' ] ) + + url = string.Template( gadget[ 'download' ][ 'url' ] ).substitute( v ) + + file_path = DownloadFileTo( + url, + destination, + file_name = gadget[ 'download' ].get( 'target' ), + checksum = v.get( 'checksum' ) ) + root = os.path.join( destination, 'root' ) + ExtractZipTo( file_path, + root, + format = gadget[ 'download' ].get( 'format', 'zip' ) ) + elif 'repo' in gadget: + url = string.Template( gadget[ 'repo' ][ 'url' ] ).substitute( v ) + ref = string.Template( gadget[ 'repo' ][ 'ref' ] ).substitute( v ) + + destination = os.path.join( gadget_dir, 'download', name ) + CloneRepoTo( url, ref, destination ) + root = destination + + if 'do' in gadget: + gadget[ 'do' ]( root ) + else: + MakeSymlink( gadget_dir, name, os.path.join( root, 'extenstion') ), + + print( "Done installing {}".format( name ) ) + except Exception as e: + traceback.print_exc() + print( "FAILED installing {}: {}".format( name, e ) ) diff --git a/python3/vimspector/breakpoints.py b/python3/vimspector/breakpoints.py new file mode 100644 index 0000000..3c8e0b2 --- /dev/null +++ b/python3/vimspector/breakpoints.py @@ -0,0 +1,190 @@ +# vimspector - A multi-language debugging system for Vim +# Copyright 2019 Ben Jackson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import defaultdict + +import vim +import functools + + +class ProjectBreakpoints( object ): + def __init__( self ): + self._connection = None + + # These are the user-entered breakpoints. + self._line_breakpoints = defaultdict( list ) + self._func_breakpoints = [] + + self._next_sign_id = 1 + + # TODO: Change to sign_define ? + vim.command( 'sign define vimspectorBP text==> texthl=Error' ) + vim.command( 'sign define vimspectorBPDisabled text=!> texthl=Warning' ) + + + def ConnectionUp( self, connection ): + self._connection = connection + + + def ConnectionClosed( self ): + self._connection = None + + # for each breakpoint: + # clear its resolved status + + + def ListBreakpoints( self ): + # FIXME: Handling of breakpoints is a mess, split between _codeView and this + # object. This makes no sense and should be centralised so that we don't + # have this duplication and bug factory. + qf = [] + if self._connection and self._codeView: + qf = self._codeView.BreakpointsAsQuickFix() + else: + for file_name, breakpoints in self._line_breakpoints.items(): + for bp in breakpoints: + qf.append( { + 'filename': file_name, + 'lnum': bp[ 'line' ], + 'col': 1, + 'type': 'L', + 'valid': 1 if bp[ 'state' ] == 'ENABLED' else 0, + 'text': "Line breakpoint - {}".format( + bp[ 'state' ] ) + } ) + # I think this shows that the qf list is not right for this. + for bp in self._func_breakpoints: + qf.append( { + 'filename': '', + 'lnum': 1, + 'col': 1, + 'type': 'F', + 'valid': 1, + 'text': "Function breakpoint: {}".format( bp[ 'function' ] ), + } ) + + vim.eval( 'setqflist( {} )'.format( json.dumps( qf ) ) ) + + def ToggleBreakpoint( self ): + line, column = vim.current.window.cursor + file_name = vim.current.buffer.name + + if not file_name: + return + + found_bp = False + for index, bp in enumerate( self._line_breakpoints[ file_name] ): + if bp[ 'line' ] == line: + found_bp = True + if bp[ 'state' ] == 'ENABLED': + bp[ 'state' ] = 'DISABLED' + else: + if 'sign_id' in bp: + vim.command( 'sign unplace {0} group=VimspectorBP'.format( + bp[ 'sign_id' ] ) ) + del self._line_breakpoints[ file_name ][ index ] + + if not found_bp: + self._line_breakpoints[ file_name ].append( { + 'state': 'ENABLED', + 'line': line, + # 'sign_id': , + # + # Used by other breakpoint types: + # 'condition': ..., + # 'hitCondition': ..., + # 'logMessage': ... + } ) + + self.UpdateUI() + + def AddFunctionBreakpoint( self, function ): + self._func_breakpoints.append( { + 'state': 'ENABLED', + 'function': function, + } ) + + # TODO: We don't really have aanything to update here, but if we're going to + # have a UI list of them we should update that at this point + self.UpdateUI() + + + def UpdateUI( self ): + if self._connection: + self.SendBreakpoints() + else: + self._ShowBreakpoints() + + def SendBreakpoints( self, handler ): + for file_name, line_breakpoints in self._line_breakpoints.items(): + breakpoints = [] + for bp in line_breakpoints: + if bp[ 'state' ] != 'ENABLED': + continue + + if 'sign_id' in bp: + vim.command( 'sign unplace {0} group=VimspectorBP'.format( + bp[ 'sign_id' ] ) ) + del bp[ 'sign_id' ] + + breakpoints.append( { 'line': bp[ 'line' ] } ) + + source = { + 'name': os.path.basename( file_name ), + 'path': file_name, + } + + self._connection.DoRequest( + functools.partial( self._UpdateBreakpoints, source ), + { + 'command': 'setBreakpoints', + 'arguments': { + 'source': source, + 'breakpoints': breakpoints, + }, + 'sourceModified': False, # TODO: We can actually check this + } + ) + + self._connection.DoRequest( + functools.partial( self._UpdateBreakpoints, None ), + { + 'command': 'setFunctionBreakpoints', + 'arguments': { + 'breakpoints': [ + { 'name': bp[ 'function' ] } + for bp in self._func_breakpoints if bp[ 'state' ] == 'ENABLED' + ], + } + } + ) + + def _ShowBreakpoints( self ): + for file_name, line_breakpoints in self._line_breakpoints.items(): + for bp in line_breakpoints: + if 'sign_id' in bp: + vim.command( 'sign unplace {0} group=VimspectorBP '.format( + bp[ 'sign_id' ] ) ) + else: + bp[ 'sign_id' ] = self._next_sign_id + self._next_sign_id += 1 + + vim.command( + 'sign place {0} group=VimspectorBP line={1} name={2} file={3}'.format( + bp[ 'sign_id' ] , + bp[ 'line' ], + 'vimspectorBP' if bp[ 'state' ] == 'ENABLED' + else 'vimspectorBPDisabled', + file_name ) ) diff --git a/python3/vimspector/code.py b/python3/vimspector/code.py index ec64b6a..b1394ee 100644 --- a/python3/vimspector/code.py +++ b/python3/vimspector/code.py @@ -20,8 +20,6 @@ from collections import defaultdict from vimspector import utils -SIGN_ID_OFFSET = 10000000 - class CodeView( object ): def __init__( self, window ): @@ -30,7 +28,7 @@ class CodeView( object ): self._logger = logging.getLogger( __name__ ) utils.SetUpLogging( self._logger ) - self._next_sign_id = SIGN_ID_OFFSET + self._next_sign_id = 1 self._breakpoints = defaultdict( list ) self._signs = { 'vimspectorPC': None, @@ -53,7 +51,8 @@ class CodeView( object ): def SetCurrentFrame( self, frame ): if self._signs[ 'vimspectorPC' ]: - vim.command( 'sign unplace {0}'.format( self._signs[ 'vimspectorPC' ] ) ) + vim.command( 'sign unplace {} group=VimspectorCode'.format( + self._signs[ 'vimspectorPC' ] ) ) self._signs[ 'vimspectorPC' ] = None if not frame or not frame.get( 'source' ): @@ -76,7 +75,9 @@ class CodeView( object ): self._signs[ 'vimspectorPC' ] = self._next_sign_id self._next_sign_id += 1 - vim.command( 'sign place {0} line={1} name=vimspectorPC file={2}'.format( + vim.command( 'sign place {0} group=VimspectorCode priority=20' + 'line={1} name=vimspectorPC ' + 'file={2}'.format( self._signs[ 'vimspectorPC' ], frame[ 'line' ], frame[ 'source' ][ 'path' ] ) ) @@ -85,7 +86,8 @@ class CodeView( object ): def Clear( self ): if self._signs[ 'vimspectorPC' ]: - vim.command( 'sign unplace {0}'.format( self._signs[ 'vimspectorPC' ] ) ) + vim.command( 'sign unplace {} group=VimspectorCode'.format( + self._signs[ 'vimspectorPC' ] ) ) self._signs[ 'vimspectorPC' ] = None self._UndisplaySigns() @@ -140,7 +142,7 @@ class CodeView( object ): def _UndisplaySigns( self ): for sign_id in self._signs[ 'breakpoints' ]: - vim.command( 'sign unplace {0}'.format( sign_id ) ) + vim.command( 'sign unplace {} group=VimspectorCode'.format( sign_id ) ) self._signs[ 'breakpoints' ].clear() @@ -160,7 +162,10 @@ class CodeView( object ): self._next_sign_id += 1 self._signs[ 'breakpoints' ].append( sign_id ) vim.command( - 'sign place {0} line={1} name={2} file={3}'.format( + 'sign place {0} group=VimspectorCode priority=9' + 'line={1} ' + 'name={2} ' + 'file={3}'.format( sign_id, breakpoint[ 'line' ], 'vimspectorBP' if breakpoint[ 'verified' ] diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 81d8fb6..e94a09b 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -22,16 +22,14 @@ import subprocess from collections import defaultdict -from vimspector import ( code, +from vimspector import ( breakpoints, + code, debug_adapter_connection, output, stack_trace, utils, variables ) -SIGN_ID_OFFSET = 10005000 - - class DebugSession( object ): def __init__( self ): self._logger = logging.getLogger( __name__ ) @@ -41,112 +39,18 @@ class DebugSession( object ): self._stackTraceView = None self._variablesView = None self._outputView = None + self._breakpoints = breakpoints.ProjectBreakpoints() self._run_on_server_exit = None - self._next_sign_id = SIGN_ID_OFFSET - - # FIXME: This needs redesigning. There are a number of problems: - # - breakpoints don't have to be line-wise (e.g. method/exception) - # - when the server moves/changes a breakpoint, this is not updated, - # leading to them getting out of sync - # - the split of responsibility between this object and the CodeView is - # messy and ill-defined. - self._line_breakpoints = defaultdict( list ) - self._func_breakpoints = [] - self._ResetServerState() - vim.command( 'sign define vimspectorBP text==> texthl=Error' ) - vim.command( 'sign define vimspectorBPDisabled text=!> texthl=Warning' ) - def _ResetServerState( self ): self._connection = None self._configuration = None self._init_complete = False self._launch_complete = False - def ListBreakpoints( self ): - # FIXME: Handling of breakpoints is a mess, split between _codeView and this - # object. This makes no sense and should be centralised so that we don't - # have this duplication and bug factory. - qf = [] - if self._connection and self._codeView: - qf = self._codeView.BreakpointsAsQuickFix() - else: - for file_name, breakpoints in self._line_breakpoints.items(): - for bp in breakpoints: - qf.append( { - 'filename': file_name, - 'lnum': bp[ 'line' ], - 'col': 1, - 'type': 'L', - 'valid': 1 if bp[ 'state' ] == 'ENABLED' else 0, - 'text': "Line breakpoint - {}".format( - bp[ 'state' ] ) - } ) - # I think this shows that the qf list is not right for this. - for bp in self._func_breakpoints: - qf.append( { - 'filename': '', - 'lnum': 1, - 'col': 1, - 'type': 'F', - 'valid': 1, - 'text': "Function breakpoint: {}".format( bp[ 'function' ] ), - } ) - - vim.eval( 'setqflist( {} )'.format( json.dumps( qf ) ) ) - - - def ToggleBreakpoint( self ): - line, column = vim.current.window.cursor - file_name = vim.current.buffer.name - - if not file_name: - return - - found_bp = False - for index, bp in enumerate( self._line_breakpoints[ file_name] ): - if bp[ 'line' ] == line: - found_bp = True - if bp[ 'state' ] == 'ENABLED': - bp[ 'state' ] = 'DISABLED' - else: - if 'sign_id' in bp: - vim.command( 'sign unplace {0}'.format( bp[ 'sign_id' ] ) ) - del self._line_breakpoints[ file_name ][ index ] - - if not found_bp: - self._line_breakpoints[ file_name ].append( { - 'state': 'ENABLED', - 'line': line, - # 'sign_id': , - # - # Used by other breakpoint types: - # 'condition': ..., - # 'hitCondition': ..., - # 'logMessage': ... - } ) - - self._UpdateUIBreakpoints() - - def _UpdateUIBreakpoints( self ): - if self._connection: - self._SendBreakpoints() - else: - self._ShowBreakpoints() - - def AddFunctionBreakpoint( self, function ): - self._func_breakpoints.append( { - 'state': 'ENABLED', - 'function': function, - } ) - - # TODO: We don't really have aanything to update here, but if we're going to - # have a UI list of them we should update that at this point - self._UpdateUIBreakpoints() - def Start( self, launch_variables = {} ): self._configuration = None self._adapter = None @@ -275,7 +179,7 @@ class DebugSession( object ): self._uiTab = None # make sure that we're displaying signs in any still-open buffers - self._UpdateUIBreakpoints() + self._breakpoints.UpdateUI() def StepOver( self ): if self._stackTraceView.GetCurrentThreadId() is None: @@ -590,12 +494,6 @@ class DebugSession( object ): ) - def _UpdateBreakpoints( self, source, message ): - if 'body' not in message: - return - self._codeView.AddBreakpoints( source, message[ 'body' ][ 'breakpoints' ] ) - self._codeView.ShowBreakpoints() - def _OnLaunchComplete( self ): self._launch_complete = True self._LoadThreadsIfReady() @@ -621,7 +519,16 @@ class DebugSession( object ): self._stackTraceView.LoadThreads( True ) def OnEvent_initialized( self, message ): - self._SendBreakpoints() + def update_breakpoints( source, message ): + if 'body' not in message: + return + self._codeView.AddBreakpoints( source, + message[ 'body' ][ 'breakpoints' ] ) + self._codeView.ShowBreakpoints() + + self._codeView.ClearBreakpoints() + self._breakpoints.SendBreakpoints( update_breakpoints ) + self._connection.DoRequest( lambda msg: self._OnInitializeComplete(), { @@ -697,75 +604,6 @@ class DebugSession( object ): # We will handle this when the server actually exists utils.UserMessage( "Debugging was terminated." ) - def _RemoveBreakpoints( self ): - for breakpoints in self._line_breakpoints.values(): - for bp in breakpoints: - if 'sign_id' in bp: - vim.command( 'sign unplace {0}'.format( bp[ 'sign_id' ] ) ) - del bp[ 'sign_id' ] - - def _SendBreakpoints( self ): - self._codeView.ClearBreakpoints() - - for file_name, line_breakpoints in self._line_breakpoints.items(): - breakpoints = [] - for bp in line_breakpoints: - if bp[ 'state' ] != 'ENABLED': - continue - - if 'sign_id' in bp: - vim.command( 'sign unplace {0}'.format( bp[ 'sign_id' ] ) ) - del bp[ 'sign_id' ] - - breakpoints.append( { 'line': bp[ 'line' ] } ) - - source = { - 'name': os.path.basename( file_name ), - 'path': file_name, - } - - self._connection.DoRequest( - functools.partial( self._UpdateBreakpoints, source ), - { - 'command': 'setBreakpoints', - 'arguments': { - 'source': source, - 'breakpoints': breakpoints, - }, - 'sourceModified': False, # TODO: We can actually check this - } - ) - - self._connection.DoRequest( - functools.partial( self._UpdateBreakpoints, None ), - { - 'command': 'setFunctionBreakpoints', - 'arguments': { - 'breakpoints': [ - { 'name': bp[ 'function' ] } - for bp in self._func_breakpoints if bp[ 'state' ] == 'ENABLED' - ], - } - } - ) - - def _ShowBreakpoints( self ): - for file_name, line_breakpoints in self._line_breakpoints.items(): - for bp in line_breakpoints: - if 'sign_id' in bp: - vim.command( 'sign unplace {0}'.format( bp[ 'sign_id' ] ) ) - else: - bp[ 'sign_id' ] = self._next_sign_id - self._next_sign_id += 1 - - vim.command( - 'sign place {0} line={1} name={2} file={3}'.format( - bp[ 'sign_id' ] , - bp[ 'line' ], - 'vimspectorBP' if bp[ 'state' ] == 'ENABLED' - else 'vimspectorBPDisabled', - file_name ) ) - def OnEvent_output( self, message ): if self._outputView: self._outputView.OnOutput( message[ 'body' ] ) @@ -781,3 +619,12 @@ class DebugSession( object ): self._outputView.Print( 'server', msg ) self._stackTraceView.OnStopped( event ) + + def ListBreakpoints( self ): + return self._breakpoints.ListBreakpoints() + + def ToggleBreakpoint( self ): + return self._breakpoints.ToggleBreakpoint() + + def AddFunctionBreakpoint( self, function ): + return self._breakpoints.AddFunctionBreakpoint( function ) diff --git a/run_test_vim b/run_test_vim new file mode 100755 index 0000000..6d79b4b --- /dev/null +++ b/run_test_vim @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +RUN_VIM="vim --noplugin --clean --not-a-term -Nu vimrc" +RUN_TEST="${RUN_VIM} -S run_test.vim" + +pushd tests > /dev/null + +exec $RUN_VIM "$@" diff --git a/run_tests b/run_tests new file mode 100755 index 0000000..a24442f --- /dev/null +++ b/run_tests @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +RUN_VIM="vim --noplugin --clean --not-a-term" +RUN_TEST="${RUN_VIM} -S run_test.vim" + +pushd tests > /dev/null + +echo "Running Vimspector Vim tests" + +RESULT=0 + +for t in *.test.vim; do + echo "" + echo "%RUN: $t" + rm -f messages debuglog + + if ${RUN_TEST} $t --cmd 'au SwapExists * let v:swapchoice = "e"'; then + echo "%PASS: $t PASSED" + else + cat messages + echo "%FAIL: $t FAILED" + RESULT=1 + fi +done + +popd > /dev/null + +echo "" +echo "All done." + +exit $RESULT diff --git a/tests/breakpoints.test.vim b/tests/breakpoints.test.vim new file mode 100644 index 0000000..5a725c9 --- /dev/null +++ b/tests/breakpoints.test.vim @@ -0,0 +1,92 @@ +function! SetUp() + if exists ( 'g:loaded_vimpector' ) + unlet g:loaded_vimpector + endif + + source vimrc + + " This is a bit of a hack + runtime! plugin/**/*.vim +endfunction + +function! ClearDown() + if exists( '*vimspector#internal#state#Reset' ) + call vimspector#internal#state#Reset() + endif +endfunction + +function! SetUp_Test_Mappings_Are_Added_HUMAN() + let g:vimspector_enable_mappings = 'HUMAN' +endfunction + +function! Test_Mappings_Are_Added_HUMAN() + + call assert_true( hasmapto( 'vimspector#Continue()' ) ) + call assert_false( hasmapto( 'vimspector#Launch()' ) ) + call assert_true( hasmapto( 'vimspector#Stop()' ) ) + call assert_true( hasmapto( 'vimspector#Restart()' ) ) + call assert_true( hasmapto( 'vimspector#ToggleBreakpoint()' ) ) + call assert_true( hasmapto( 'vimspector#AddFunctionBreakpoint' ) ) + call assert_true( hasmapto( 'vimspector#StepOver()' ) ) + call assert_true( hasmapto( 'vimspector#StepInto()' ) ) + call assert_true( hasmapto( 'vimspector#StepOut()' ) ) +endfunction + +function! Test_Mappings_Are_Added_VISUAL_STUDIO() + call assert_true( hasmapto( 'vimspector#Continue()' ) ) + call assert_false( hasmapto( 'vimspector#Launch()' ) ) + call assert_true( hasmapto( 'vimspector#Stop()' ) ) + call assert_true( hasmapto( 'vimspector#Restart()' ) ) + call assert_true( hasmapto( 'vimspector#ToggleBreakpoint()' ) ) + call assert_true( hasmapto( 'vimspector#AddFunctionBreakpoint' ) ) + call assert_true( hasmapto( 'vimspector#StepOver()' ) ) + call assert_true( hasmapto( 'vimspector#StepInto()' ) ) + call assert_true( hasmapto( 'vimspector#StepOut()' ) ) +endfunction + +function! SetUp_Test_Signs_Placed_Using_API_Are_Shown() + let g:vimspector_enable_mappings = 'VISUAL_STUDIO' +endfunction + +function! Test_Signs_Placed_Using_API_Are_Shown() + " We need a real file + edit testdata/cpp/simple.cpp + call feedkeys( '/printf' ) + + " Set breakpoint + call vimspector#ToggleBreakpoint() + + call assert_true( exists( '*vimspector#ToggleBreakpoint' ) ) + + let signs = sign_getplaced( '.', { + \ 'group': 'VimspectorBP', + \ 'line': line( '.' ) + \ } ) + + call assert_true( len( signs ) == 1 ) + call assert_true( len( signs[ 0 ].signs ) == 1 ) + call assert_true( signs[ 0 ].signs[ 0 ].name == 'vimspectorBP' ) + + " Disable breakpoint + call vimspector#ToggleBreakpoint() + + let signs = sign_getplaced( '.', { + \ 'group': 'VimspectorBP', + \ 'line': line( '.' ) + \ } ) + + call assert_true( len( signs ) == 1 ) + call assert_true( len( signs[ 0 ].signs ) == 1 ) + call assert_true( signs[ 0 ].signs[ 0 ].name == 'vimspectorBPDisabled' ) + + " Remove breakpoint + call vimspector#ToggleBreakpoint() + + let signs = sign_getplaced( '.', { + \ 'group': 'VimspectorBP', + \ 'line': line( '.' ) + \ } ) + + call assert_true( len( signs ) == 1 ) + call assert_true( len( signs[ 0 ].signs ) == 0 ) +endfunction diff --git a/tests/run_test.vim b/tests/run_test.vim new file mode 100644 index 0000000..de454cb --- /dev/null +++ b/tests/run_test.vim @@ -0,0 +1,347 @@ +" This script is sourced while editing the .vim file with the tests. +" When the script is successful the .res file will be created. +" Errors are appended to the test.log file. +" +" To execute only specific test functions, add a second argument. It will be +" matched against the names of the Test_ funtion. E.g.: +" ../vim -u NONE -S runtest.vim test_channel.vim open_delay +" The output can be found in the "messages" file. +" +" The test script may contain anything, only functions that start with +" "Test_" are special. These will be invoked and should contain assert +" functions. See test_assert.vim for an example. +" +" It is possible to source other files that contain "Test_" functions. This +" can speed up testing, since Vim does not need to restart. But be careful +" that the tests do not interfere with each other. +" +" If an error cannot be detected properly with an assert function add the +" error to the v:errors list: +" call add(v:errors, 'test foo failed: Cannot find xyz') +" +" If preparation for each Test_ function is needed, define a SetUp function. +" It will be called before each Test_ function. +" +" If cleanup after each Test_ function is needed, define a TearDown function. +" It will be called after each Test_ function. +" +" When debugging a test it can be useful to add messages to v:errors: +" call add(v:errors, "this happened") + +set rtp=$VIM/vimfiles,$VIMRUNTIME,$VIM/vimfiles/after +if has('packages') + let &packpath = &rtp +endif + +call ch_logfile( 'debuglog', 'w' ) + +" For consistency run all tests with 'nocompatible' set. +" This also enables use of line continuation. +set nocp viminfo+=nviminfo + +" Use utf-8 by default, instead of whatever the system default happens to be. +" Individual tests can overrule this at the top of the file. +set encoding=utf-8 + +" Avoid stopping at the "hit enter" prompt +set nomore + +" Output all messages in English. +lang mess C + +" Always use forward slashes. +set shellslash + +func RunTheTest(test) + echo 'Executing ' . a:test + + " Avoid stopping at the "hit enter" prompt + set nomore + + " Avoid a three second wait when a message is about to be overwritten by the + " mode message. + set noshowmode + + " Clear any overrides. + call test_override('ALL', 0) + + " Some tests wipe out buffers. To be consistent, always wipe out all + " buffers. + %bwipe! + + " The test may change the current directory. Save and restore the + " directory after executing the test. + let save_cwd = getcwd() + + if exists("*SetUp_" . a:test) + try + exe 'call SetUp_' . a:test + catch + call add(v:errors, + \ 'Caught exception in SetUp_' . a:test . ' before ' + \ . a:test + \ . ': ' + \ . v:exception + \ . ' @ ' + \ . v:throwpoint) + endtry + endif + + if exists("*SetUp") + try + call SetUp() + catch + call add(v:errors, + \ 'Caught exception in SetUp() before ' + \ . a:test + \ . ': ' + \ . v:exception + \ . ' @ ' + \ . v:throwpoint) + endtry + endif + + call add(s:messages, 'Executing ' . a:test) + let s:done += 1 + + if a:test =~ 'Test_nocatch_' + " Function handles errors itself. This avoids skipping commands after the + " error. + exe 'call ' . a:test + else + try + let s:test = a:test + au VimLeavePre * call EarlyExit(s:test) + exe 'call ' . a:test + au! VimLeavePre + catch /^\cskipped/ + call add(s:messages, ' Skipped') + call add(s:skipped, + \ 'SKIPPED ' . a:test + \ . ': ' + \ . substitute(v:exception, '^\S*\s\+', '', '')) + catch + call add(v:errors, + \ 'Caught exception in ' . a:test + \ . ': ' + \ . v:exception + \ . ' @ ' + \ . v:throwpoint) + endtry + endif + + " In case 'insertmode' was set and something went wrong, make sure it is + " reset to avoid trouble with anything else. + set noinsertmode + + if exists("*TearDown") + try + call TearDown() + catch + call add(v:errors, + \ 'Caught exception in TearDown() after ' . a:test + \ . ': ' + \ . v:exception + \ . ' @ ' + \ . v:throwpoint) + endtry + endif + + if exists("*TearDown_" . a:test) + try + exe 'call TearDown_' . a:test + catch + call add(v:errors, + \ 'Caught exception in TearDown_' . a:test . ' after ' . a:test + \ . ': ' + \ . v:exception + \ . ' @ ' + \ . v:throwpoint) + endtry + endif + + " Clear any autocommands + au! + + " Close any extra tab pages and windows and make the current one not modified. + while tabpagenr('$') > 1 + quit! + endwhile + + while 1 + let wincount = winnr('$') + if wincount == 1 + break + endif + bwipe! + if wincount == winnr('$') + " Did not manage to close a window. + only! + break + endif + endwhile + + exe 'cd ' . save_cwd +endfunc + +func AfterTheTest() + if len(v:errors) > 0 + let s:fail += 1 + call add(s:errors, 'Found errors in ' . s:test . ':') + call extend(s:errors, v:errors) + let v:errors = [] + endif +endfunc + +func EarlyExit(test) + " It's OK for the test we use to test the quit detection. + if a:test != 'Test_zz_quit_detected()' + call add(v:errors, 'Test caused Vim to exit: ' . a:test) + endif + + call FinishTesting() +endfunc + +" This function can be called by a test if it wants to abort testing. +func FinishTesting() + call AfterTheTest() + + " Don't write viminfo on exit. + set viminfo= + + if s:fail == 0 + " Success, create the .res file so that make knows it's done. + exe 'split ' . fnamemodify(g:testname, ':r') . '.res' + write + endif + + if len(s:errors) > 0 + " Append errors to test.log + split test.log + call append(line('$'), '') + call append(line('$'), 'From ' . g:testname . ':') + call append(line('$'), s:errors) + write + endif + + if s:done == 0 + let message = 'NO tests executed' + else + let message = 'Executed ' . s:done . (s:done > 1 ? ' tests' : ' test') + endif + echo message + call add(s:messages, message) + if s:fail > 0 + let message = s:fail . ' FAILED:' + echo message + call add(s:messages, message) + call extend(s:messages, s:errors) + endif + + " Add SKIPPED messages + call extend(s:messages, s:skipped) + + " Append messages to the file "messages" + split messages + call append(line('$'), '') + call append(line('$'), 'From ' . g:testname . ':') + call append(line('$'), s:messages) + write + + if s:fail > 0 + cquit! + else + qall! + endif +endfunc + +" Source the test script. First grab the file name, in case the script +" navigates away. g:testname can be used by the tests. +let g:testname = expand('%') +let s:done = 0 +let s:fail = 0 +let s:errors = [] +let s:messages = [] +let s:skipped = [] +try + source % +catch + let s:fail += 1 + call add(s:errors, + \ 'Caught exception: ' . + \ v:exception . + \ ' @ ' . v:throwpoint) +endtry + +" Names of flaky tests. +let s:flaky_tests = [] + +" Pattern indicating a common flaky test failure. +let s:flaky_errors_re = '__does_not_match__' + +" Locate Test_ functions and execute them. +redir @q +silent function /^Test_ +redir END +let s:tests = split(substitute(@q, 'function \(\k*()\)', '\1', 'g')) + +" If there is an extra argument filter the function names against it. +if argc() > 1 + let s:tests = filter(s:tests, 'v:val =~ argv(1)') +endif + +" Execute the tests in alphabetical order. +for s:test in sort(s:tests) + " Silence, please! + set belloff=all + let prev_error = '' + let total_errors = [] + let run_nr = 1 + + call RunTheTest(s:test) + + " Repeat a flaky test. Give up when: + " - it fails again with the same message + " - it fails five times (with a different mesage) + if len(v:errors) > 0 + \ && (index(s:flaky_tests, s:test) >= 0 + \ || v:errors[0] =~ s:flaky_errors_re) + while 1 + call add(s:messages, 'Found errors in ' . s:test . ':') + call extend(s:messages, v:errors) + + call add(total_errors, 'Run ' . run_nr . ':') + call extend(total_errors, v:errors) + + if run_nr == 5 || prev_error == v:errors[0] + call add(total_errors, 'Flaky test failed too often, giving up') + let v:errors = total_errors + break + endif + + call add(s:messages, 'Flaky test failed, running it again') + + " Flakiness is often caused by the system being very busy. Sleep a + " couple of seconds to have a higher chance of succeeding the second + " time. + sleep 2 + + let prev_error = v:errors[0] + let v:errors = [] + let run_nr += 1 + + call RunTheTest(s:test) + + if len(v:errors) == 0 + " Test passed on rerun. + break + endif + endwhile + endif + + call AfterTheTest() +endfor + +call FinishTesting() + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/tests/testdata/cpp/simple.cpp b/tests/testdata/cpp/simple.cpp new file mode 100644 index 0000000..e3e623a --- /dev/null +++ b/tests/testdata/cpp/simple.cpp @@ -0,0 +1,7 @@ +#include + +int main( int argc, char ** ) +{ + printf( "this is a test %d", argc ); + return 0; +} diff --git a/tests/vimrc b/tests/vimrc new file mode 100644 index 0000000..9901c3a --- /dev/null +++ b/tests/vimrc @@ -0,0 +1,6 @@ +let g:vimspector_test_plugin_path = expand( ':h:h' ) + +let &rtp = &rtp . ',' . g:vimspector_test_plugin_path + +filetype plugin indent on +syntax enable From 90a076274ddd90d8821d82498e6ee24629ccdf9a Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 17 Feb 2019 19:46:09 +0000 Subject: [PATCH 20/50] Don't use circleci, it's a bit of a pain --- .circleci/config.yml | 45 ---------------------------------- .circleci/install_vim.macos.sh | 6 ----- 2 files changed, 51 deletions(-) delete mode 100644 .circleci/config.yml delete mode 100644 .circleci/install_vim.macos.sh diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 6659ada..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,45 +0,0 @@ -# Python CircleCI 2.0 configuration file -# -# Check https://circleci.com/docs/2.0/language-python/ for more details -# -version: 2 -aliases: - cache: &cache - key: v1-vimspector-{{ .Environment.CIRCLE_JOB }} - update-submodules: &update-submodules - run: - name: Update submodules - command: git submodule update --init --recursive - install-vim: &install-vim - install-gadgets: &install-gadgets - run: - name: Install gadgets - command: 'python3 ./install_gadget.py' - run-tests: &run-tests - run: - name: Run tests - command: './run_tests' - # Increase the version key to clear the cache. - save-cache: &save-cache - save_cache: - <<: *cache - paths: - - 'gadgets/download/*/*/*/*.vsix' - - 'gadgets/download/*/*/*/*.gz' - restore-cache: &restore-cache - restore_cache: - <<: *cache -jobs: - build-macos: - macos: - xcode: "9.0" - steps: - - checkout - - *update-submodules - - *restore-cache - - run: - name: Install vim - command: .circleci/install_vim.macos.sh - - *install-gadgets - - *run-tests - - *save-cache diff --git a/.circleci/install_vim.macos.sh b/.circleci/install_vim.macos.sh deleted file mode 100644 index 84d6779..0000000 --- a/.circleci/install_vim.macos.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -set -e - -brea update || brew update -brew install vim --with-override-system-vim From a504e560563a97d8e2c1846a7def5b8df006fb17 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 17 Feb 2019 19:52:33 +0000 Subject: [PATCH 21/50] Set up CI with Azure Pipelines --- azure-pipelines.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 azure-pipelines.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..71b121a --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,20 @@ +# Starter pipeline +# Start with a minimal pipeline that you can customize to build and deploy your code. +# Add steps that build, run tests, deploy, and more: +# https://aka.ms/yaml + +trigger: +- master + +pool: + vmImage: 'Ubuntu-16.04' + +steps: +- script: sudo apt-get install vim + displayName: 'Install vim' + +- script: python3 install_gadget.py + displayName: 'Install gadgets' + +- script: ./run_tests + displayName: 'Run the tests' \ No newline at end of file From bd48b00acce48b66c9bfe20512a9b96d8afbd260 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 17 Feb 2019 20:25:01 +0000 Subject: [PATCH 22/50] Update azure pipeline - use 18.04 VM --- azure-pipelines.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 71b121a..bd2f156 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -3,18 +3,17 @@ # Add steps that build, run tests, deploy, and more: # https://aka.ms/yaml -trigger: -- master - pool: vmImage: 'Ubuntu-16.04' +container: 'ubuntu:18.04' + steps: -- script: sudo apt-get install vim +- script: sudo apt-get install python3-dev vim displayName: 'Install vim' - script: python3 install_gadget.py displayName: 'Install gadgets' - script: ./run_tests - displayName: 'Run the tests' \ No newline at end of file + displayName: 'Run the tests' From b5cd5ea891dbd457a2ca27901e109ccadd51dab8 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 17 Feb 2019 20:28:11 +0000 Subject: [PATCH 23/50] No sudo ? --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index bd2f156..fcfac9b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -9,7 +9,7 @@ pool: container: 'ubuntu:18.04' steps: -- script: sudo apt-get install python3-dev vim +- script: apt-get install python3-dev vim displayName: 'Install vim' - script: python3 install_gadget.py From c57070ee82f524c9504e3c92e18b951272a9f257 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 17 Feb 2019 20:41:52 +0000 Subject: [PATCH 24/50] Print some info for debugging --- azure-pipelines.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fcfac9b..3a385c9 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -9,6 +9,12 @@ pool: container: 'ubuntu:18.04' steps: +- script: printenv + displayName: 'Print environment' + +- script: whoami + displayName: 'Print user' + - script: apt-get install python3-dev vim displayName: 'Install vim' From 30da2ab862584965706f05ed9f64c6f17b226317 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 17 Feb 2019 21:51:34 +0000 Subject: [PATCH 25/50] Try using my own docker image --- azure-pipelines.yml | 29 ++++++++++------------------- install_gadget.py | 2 +- tests/ci/image/Dockerfile | 9 +++++++++ 3 files changed, 20 insertions(+), 20 deletions(-) create mode 100644 tests/ci/image/Dockerfile diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3a385c9..55bd4c1 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -3,23 +3,14 @@ # Add steps that build, run tests, deploy, and more: # https://aka.ms/yaml -pool: - vmImage: 'Ubuntu-16.04' +jobs: +- job: 'linux' + pool: + vimImage: 'ubuntu-16.04' + container: 'puremourning/vimspector:test' + steps: + - script: python3 install_gadget.py + displayName: 'Install gadgets' -container: 'ubuntu:18.04' - -steps: -- script: printenv - displayName: 'Print environment' - -- script: whoami - displayName: 'Print user' - -- script: apt-get install python3-dev vim - displayName: 'Install vim' - -- script: python3 install_gadget.py - displayName: 'Install gadgets' - -- script: ./run_tests - displayName: 'Run the tests' + - script: ./run_tests + displayName: 'Run the tests' diff --git a/install_gadget.py b/install_gadget.py index a634b3e..200a6cf 100755 --- a/install_gadget.py +++ b/install_gadget.py @@ -157,7 +157,7 @@ def DownloadFileTo( url, destination, file_name = None, checksum = None ): checksum ) ) else: print( "Checksum for {}: {}".format( file_path, - ) ) + GetChecksumSHA254( file_path ) ) ) return file_path diff --git a/tests/ci/image/Dockerfile b/tests/ci/image/Dockerfile new file mode 100644 index 0000000..9d34f6a --- /dev/null +++ b/tests/ci/image/Dockerfile @@ -0,0 +1,9 @@ +FROM ubuntu:18.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get -y dist-upgrade && \ + apt-get -y install python3 vim tcl tcl-dev tcllib && apt-get -y autoremove + +RUN ln -fs /usr/share/zoneinfo/Europe/London /etc/localtime && \ + dpkg-reconfigure --frontend noninteractive tzdata From 3ea8a12986cd7be4aeaf543e7ed3b6cb438a4b30 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 17 Feb 2019 22:35:30 +0000 Subject: [PATCH 26/50] Fail the job if gadget install fails --- install_gadget.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/install_gadget.py b/install_gadget.py index 200a6cf..ab45efd 100755 --- a/install_gadget.py +++ b/install_gadget.py @@ -220,6 +220,7 @@ else: gadget_dir = os.path.join( os.path.dirname( __file__ ), 'gadgets', OS ) +failed = [] for name, gadget in GADGETS.items(): try: v = {} @@ -260,4 +261,10 @@ for name, gadget in GADGETS.items(): print( "Done installing {}".format( name ) ) except Exception as e: traceback.print_exc() + failed.append( name ) print( "FAILED installing {}: {}".format( name, e ) ) + + +if failed: + raise RuntimeError( 'Failed to install gadgets: {}'.format( + ','.join( failed ) ) ) From 7f78429f8d27736dc0931663e79a625d32be573a Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 17 Feb 2019 22:46:52 +0000 Subject: [PATCH 27/50] Install ca-certs --- tests/ci/image/Dockerfile | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/ci/image/Dockerfile b/tests/ci/image/Dockerfile index 9d34f6a..ca7f4cf 100644 --- a/tests/ci/image/Dockerfile +++ b/tests/ci/image/Dockerfile @@ -2,8 +2,15 @@ FROM ubuntu:18.04 ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get -y dist-upgrade && \ - apt-get -y install python3 vim tcl tcl-dev tcllib && apt-get -y autoremove +RUN apt-get update && \ + apt-get -y dist-upgrade && \ + apt-get -y install python3 \ + ca-cacert \ + vim \ + git \ + tcl-dev \ + tcllib && \ + apt-get -y autoremove RUN ln -fs /usr/share/zoneinfo/Europe/London /etc/localtime && \ dpkg-reconfigure --frontend noninteractive tzdata From b03919489d81ac8b1e96507c1efb738039d1e7aa Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 20 Feb 2019 21:15:25 +0000 Subject: [PATCH 28/50] Install bash debug instead of mono debug, as installing mono debug is a pain --- install_gadget.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/install_gadget.py b/install_gadget.py index ab45efd..14ce3c8 100755 --- a/install_gadget.py +++ b/install_gadget.py @@ -58,6 +58,7 @@ GADGETS = { 'do': lambda root: InstallTclProDebug( root ) }, 'vscode-mono-debug': { + 'enabled': False, 'download': { 'url': 'https://marketplace.visualstudio.com/_apis/public/gallery/' 'publishers/ms-vscode/vsextensions/mono-debug/${version}/' @@ -71,6 +72,17 @@ GADGETS = { 'checksum': '723eb2b621b99d65a24f215cb64b45f5fe694105613a900a03c859a62a810470', } }, + 'vscode-bash-debug': { + 'download': { + 'url': ( 'https://github.com/rogalmic/vscode-bash-debug/releases/' + 'download/${version}/${file_name}' ), + }, + 'all': { + 'file_name': 'bash-debug-0.3.3.vsix', + 'version': 'untagged-3c529a47de44a70c9c76', + 'checksum': '', + } + } } @contextlib.contextmanager @@ -222,6 +234,9 @@ gadget_dir = os.path.join( os.path.dirname( __file__ ), 'gadgets', OS ) failed = [] for name, gadget in GADGETS.items(): + if not gadget.get( 'enabled', True ): + continue + try: v = {} v.update( gadget.get( 'all', {} ) ) From 79ff07b95391df06c7d31a60bbe1029e762506f7 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 20 Feb 2019 21:28:39 +0000 Subject: [PATCH 29/50] Use bash, print vim version --- azure-pipelines.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 55bd4c1..12de1ff 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -9,8 +9,11 @@ jobs: vimImage: 'ubuntu-16.04' container: 'puremourning/vimspector:test' steps: - - script: python3 install_gadget.py + - bash: python3 install_gadget.py displayName: 'Install gadgets' - - script: ./run_tests + - bash: vim --version + displayName: 'Print vim version information' + + - bash: ./run_tests displayName: 'Run the tests' From fb5b6f2d740e6239f9708100965ba52fbdb80a75 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 20 Feb 2019 21:52:27 +0000 Subject: [PATCH 30/50] Use a newer vim; vimspector requires latest builds --- tests/ci/image/Dockerfile | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/ci/image/Dockerfile b/tests/ci/image/Dockerfile index ca7f4cf..1de8b9f 100644 --- a/tests/ci/image/Dockerfile +++ b/tests/ci/image/Dockerfile @@ -4,9 +4,9 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get -y dist-upgrade && \ - apt-get -y install python3 \ + apt-get -y install python3-dev \ ca-cacert \ - vim \ + libncurses5-dev libncursesw5-dev \ git \ tcl-dev \ tcllib && \ @@ -14,3 +14,16 @@ RUN apt-get update && \ RUN ln -fs /usr/share/zoneinfo/Europe/London /etc/localtime && \ dpkg-reconfigure --frontend noninteractive tzdata + +RUN mkdir -p $HOME/vim && \ + cd $HOME/vim && \ + git clone https://github.com/vim/vim && \ + cd vim && \ + git checkout v8.1.0958 && \ + ./configure --with-features=huge \ + --enable-python3interp \ + --enable-terminal \ + --enable-multibyte \ + --enable-fail-if-missing && \ + make -j 8 install + From f8ae01f75c67bfddfe46fa3eab5404b62ad17184 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 20 Feb 2019 22:12:55 +0000 Subject: [PATCH 31/50] Add macos --- azure-pipelines.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 12de1ff..37dc695 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -17,3 +17,19 @@ jobs: - bash: ./run_tests displayName: 'Run the tests' + +- job: 'macos' + pool: + vmImage: 'macOS-10.13' + steps: + - bash: brew install vim --with-override-system-vim + displayName: 'Install vim' + + - bash: python3 install_gadget.py + displayName: 'Install gadgets' + + - bash: vim --version + displayName: 'Print vim version information' + + - bash: ./run_tests + displayName: 'Run the tests' From 5a0ba177546e0676f6f043394f9648e648772a56 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 20 Feb 2019 22:32:00 +0000 Subject: [PATCH 32/50] install macvim. presumably it always overrides system vim --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 37dc695..679779b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -22,7 +22,7 @@ jobs: pool: vmImage: 'macOS-10.13' steps: - - bash: brew install vim --with-override-system-vim + - bash: brew install macvim displayName: 'Install vim' - bash: python3 install_gadget.py From 69ea032bc1063ce5d85e40964c6b7911b9499ed9 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 20 Feb 2019 22:35:05 +0000 Subject: [PATCH 33/50] Correct name of Lina-Ux OS --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 679779b..20e7417 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -4,7 +4,7 @@ # https://aka.ms/yaml jobs: -- job: 'linux' +- job: 'lina-ux' pool: vimImage: 'ubuntu-16.04' container: 'puremourning/vimspector:test' From 0b1a06f47e753e1bf8ce616e3ca26fcfc1842091 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 20 Feb 2019 22:35:51 +0000 Subject: [PATCH 34/50] Apparently Linx-Ux is invalid. --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 20e7417..a6d15da 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -4,7 +4,7 @@ # https://aka.ms/yaml jobs: -- job: 'lina-ux' +- job: 'linax' pool: vimImage: 'ubuntu-16.04' container: 'puremourning/vimspector:test' From 75851e06529ce3480879376a8366496ea203951e Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 21 Feb 2019 16:12:58 +0000 Subject: [PATCH 35/50] Move the vimscript into vimscript; it's so much easier. Support multiple commands and jobs --- autoload/vimspector/internal/job.vim | 49 ++++++++++++++++++ python3/vimspector/debug_session.py | 74 +++++++++++++++++++++------- python3/vimspector/output.py | 20 ++++++-- python3/vimspector/utils.py | 40 +++------------ 4 files changed, 128 insertions(+), 55 deletions(-) diff --git a/autoload/vimspector/internal/job.vim b/autoload/vimspector/internal/job.vim index 374c9a6..77f539e 100644 --- a/autoload/vimspector/internal/job.vim +++ b/autoload/vimspector/internal/job.vim @@ -113,6 +113,55 @@ function! vimspector#internal#job#ForceRead() abort endif endfunction +function! vimspector#internal#job#StartCommandWithLog( cmd, category ) + if ! exists( 's:commands' ) + let s:commands = {} + endif + + if ! has_key( s:commands, a:category ) + let s:commands[ a:category ] = [] + endif + + let l:index = len( s:commands[ a:category ] ) + + call add( s:commands[ a:category ], job_start( + \ a:cmd, + \ { + \ 'out_io': 'buffer', + \ 'in_io': 'null', + \ 'err_io': 'buffer', + \ 'out_name': '_vimspector_log_' . a:category . '_out', + \ 'err_name': '_vimspector_log_' . a:category . '_err', + \ 'out_modifiable': 0, + \ 'err_modifiable': 0, + \ 'stoponexit': 'kill' + \ } ) ) + + let l:stdout = ch_getbufnr( + \ job_getchannel( s:commands[ a:category ][ index ] ), 'out' ) + let l:stderr = ch_getbufnr( + \ job_getchannel( s:commands[ a:category ][ index ] ), 'err' ) + + + return [ l:stdout, l:stderr ] +endfunction + + +function! vimspector#internal#job#CleanUpCommand( category ) + if ! exists( 's:commands' ) + let s:commands = {} + endif + + if ! has_key( s:commands, a:category ) + return + endif + for j in s:commands[ a:category ] + call job_stop( j, 'kill' ) + endfor + + unlet s:commands[ a:category ] +endfunction + " Boilerplate {{{ let &cpo=s:save_cpo unlet s:save_cpo diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 1e239e1..b27a060 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -19,6 +19,7 @@ import json import os import functools import subprocess +import shlex from collections import defaultdict @@ -430,6 +431,10 @@ class DebugSession( object ): assert not self._run_on_server_exit self._run_on_server_exit = callback + arguments = {} + if self._server_capabilities.get( 'supportTerminateDebuggee' ): + arguments[ 'terminateDebugee' ] = True + self._connection.DoRequest( handler, { 'command': 'disconnect', 'arguments': { @@ -439,9 +444,15 @@ class DebugSession( object ): def _PrepareAttach( self, adapter_config, launch_config ): - atttach_config = adapter_config[ 'attach' ] + atttach_config = adapter_config.get( 'attach' ) + + if not atttach_config: + return if 'remote' in atttach_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 = atttach_config[ 'remote' ] ssh = [ 'ssh' ] @@ -462,13 +473,16 @@ class DebugSession( object ): utils.UserMessage( 'Unable to get PID', persist = True ) return - cmd = ssh + remote[ 'attachCommand' ][:] + commands = self._GetCommands( remote, 'attach' ) - for index, item in enumerate( cmd ): - cmd[ index ] = item.replace( '%PID%', pid ) + for command in commands: + cmd = ssh + command[:] - self._logger.debug( 'Running remote app: %s', cmd ) - self._outputView.RunJobWithOutput( 'Remote', cmd ) + for index, item in enumerate( cmd ): + cmd[ index ] = item.replace( '%PID%', pid ) + + self._logger.debug( 'Running remote app: %s', cmd ) + self._outputView.RunJobWithOutput( 'Remote', cmd ) else: if atttach_config[ 'pidSelect' ] == 'ask': pid = utils.AskForInput( 'Enter PID to attach to: ' ) @@ -481,6 +495,7 @@ class DebugSession( object ): atttach_config[ 'pidSelect' ] ) ) + def _PrepareLaunch( self, command_line, adapter_config, launch_config ): run_config = adapter_config.get( 'launch', {} ) @@ -492,21 +507,46 @@ class DebugSession( object ): else: ssh.append( remote[ 'host' ] ) - cmd = ssh + remote[ 'runCommand' ][:] - full_cmd = [] - for item in cmd: - if isinstance( command_line, list ): - if item == '%CMD%': - full_cmd.extend( command_line ) + commands = self._GetCommands( remote, 'run' ) + + for index, command in enumerate( commands ): + cmd = ssh + 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 ) - else: - full_cmd.append( item.replace( '%CMD%', command_line ) ) + full_cmd.append( item.replace( '%CMD%', command_line ) ) - self._logger.debug( 'Running remote app: %s', full_cmd ) - self._outputView.RunJobWithOutput( 'Remote', full_cmd ) + self._logger.debug( 'Running remote app: %s', full_cmd ) + self._outputView.RunJobWithOutput( 'Remote{}'.format( index ), + full_cmd ) + 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" ) + + commands = remote[ pfx + 'Command' ] + + if isinstance( commands, str ): + commands = shlex.split( commands ) + + if not isinstance( commands, list ): + raise ValueError( "Invalid command; must be list/string" ) + + if not commands: + raise ValueError( 'Could not determine commands for ' + pfx ) + + return commands + def _Initialise( self ): adapter_config = self._adapter self._connection.DoRequest( lambda msg: self._Launch(), { diff --git a/python3/vimspector/output.py b/python3/vimspector/output.py index d522c7b..fbeedce 100644 --- a/python3/vimspector/output.py +++ b/python3/vimspector/output.py @@ -96,7 +96,12 @@ class OutputView( object ): for category, tab_buffer in self._buffers.items(): if tab_buffer.is_job: utils.CleanUpCommand( category ) - vim.command( 'bdelete! {0}'.format( tab_buffer.buf.number ) ) + try: + vim.command( 'bdelete! {0}'.format( tab_buffer.buf.number ) ) + except vim.error as e: + # FIXME: For now just ignore the "no buffers were deleted" error + if 'E516' not in e: + raise self._buffers.clear() @@ -159,10 +164,15 @@ class OutputView( object ): cmd = [ 'tail', '-F', '-n', '+1', '--', file_name ] if cmd is not None: - buf = utils.SetUpCommandBuffer( cmd, category ) - self._buffers[ category ] = TabBuffer( buf, len( self._buffers ) ) - self._buffers[ category ].is_job = True - self._RenderWinBar( category ) + out, err = utils.SetUpCommandBuffer( cmd, category ) + self._buffers[ category + '-out' ] = TabBuffer( out, + len( self._buffers ) ) + self._buffers[ category + '-out' ].is_job = True + self._buffers[ category + '-err' ] = TabBuffer( err, + len( self._buffers ) ) + self._buffers[ category + '-err' ].is_job = False + self._RenderWinBar( category + '-out' ) + self._RenderWinBar( category + '-err' ) else: vim.command( 'enew' ) tab_buffer = TabBuffer( vim.current.buffer, len( self._buffers ) ) diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index 8c63d55..f27f7cf 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -53,43 +53,17 @@ def OpenFileInCurrentWindow( file_name ): def SetUpCommandBuffer( cmd, name ): - vim.command( - 'let g:vimspector_command_job_{name} = job_start(' - ' {cmd},' - ' {{' - ' "out_io": "buffer",' - ' "in_io": "null",' - ' "err_io": "buffer",' - ' "out_name": "_vimspector_log_{name}",' - ' "err_name": "_vimspector_log_{name}",' - ' "out_modifiable": 0,' - ' "err_modifiable": 0,' - ' "stoponexit": "term",' - ' }} )'.format( name = name, - cmd = json.dumps( cmd ) ) ) + bufs = vim.bindeval( + 'vimspector#internal#job#StartCommandWithLog( {}, "{}" )'.format( + json.dumps( cmd ), + name ) ) - stdout = vim.eval( 'ch_getbufnr( ' - ' job_getchannel( g:vimspector_command_job_{name} ), ' - ' "out"' - ')'.format( name = name ) ) - stderr = vim.eval( 'ch_getbufnr( ' - ' job_getchannel( g:vimspector_command_job_{name} ), ' - ' "err"' - ')'.format( name = name ) ) - - assert stdout == stderr - return vim.buffers[ int( stdout ) ] + return ( vim.buffers[ bufnr ] for bufnr in bufs ) def CleanUpCommand( name ): - cmd = 'job_stop( g:vimspector_command_job_{name}, "kill" )'.format( - name = name ) - vim.eval( cmd ) - - -def TerminateJob( job ): - if vim.eval( 'job_status( {} )'.format( job ) ) == 'run': - vim.eval( 'job_stop( {} )'.format( job ) ) + return vim.eval( 'vimspector#internal#job#CleanUpCommand( "{}" )'.format( + name ) ) def SetUpScratchBuffer( buf, name ): From 2d7c67968a87bc7e06efcc0f0bb42bc94e1a005a Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 21 Feb 2019 16:13:51 +0000 Subject: [PATCH 36/50] Be more forceful when closing th ejob --- autoload/vimspector/internal/job.vim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/autoload/vimspector/internal/job.vim b/autoload/vimspector/internal/job.vim index 77f539e..e24b60a 100644 --- a/autoload/vimspector/internal/job.vim +++ b/autoload/vimspector/internal/job.vim @@ -96,7 +96,8 @@ function! vimspector#internal#job#StopDebugSession() abort endif if job_status( s:job ) == 'run' - call job_stop( s:job, 'term' ) + echom "Terminating job" + call job_stop( s:job, 'kill' ) endif endfunction From e216bc291d4e3363b663d5859878dde4c9c3efdc Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 21 Feb 2019 16:14:05 +0000 Subject: [PATCH 37/50] Respect (some) server capabilities --- python3/vimspector/debug_session.py | 63 ++++++++++++++++++----------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index b27a060..a2a3680 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -66,6 +66,7 @@ class DebugSession( object ): self._configuration = None self._init_complete = False self._launch_complete = False + self._server_capabilities = {} def ToggleBreakpoint( self ): line, column = vim.current.window.cursor @@ -437,11 +438,11 @@ class DebugSession( object ): self._connection.DoRequest( handler, { 'command': 'disconnect', - 'arguments': { - 'terminateDebugee': True - }, + 'arguments': arguments, }, failure_handler = handler, timeout = 5000 ) + # TODO: Use the 'tarminate' request if supportsTerminateRequest set + def _PrepareAttach( self, adapter_config, launch_config ): atttach_config = adapter_config.get( 'attach' ) @@ -548,11 +549,14 @@ class DebugSession( object ): return commands def _Initialise( self ): - adapter_config = self._adapter - self._connection.DoRequest( lambda msg: self._Launch(), { + def handle_initialize_response( msg ): + self._server_capabilities = msg.get( 'body' ) or {} + self._Launch() + + self._connection.DoRequest( handle_initialize_response, { 'command': 'initialize', 'arguments': { - 'adapterID': adapter_config.get( 'name', 'adapter' ), + 'adapterID': self._adapter.get( 'name', 'adapter' ), 'clientID': 'vimspector', 'clientName': 'vimspector', 'linesStartAt1': True, @@ -632,18 +636,30 @@ class DebugSession( object ): if self._launch_complete and self._init_complete: self._stackTraceView.LoadThreads( True ) + + def OnEvent_capabiilities( self, msg ): + self._server_capabilities.update( + ( msg.get( 'body' ) or {} ).get( 'capabilities' ) or {} ) + + def OnEvent_initialized( self, message ): self._SendBreakpoints() - self._connection.DoRequest( - lambda msg: self._OnInitializeComplete(), - { - 'command': 'configurationDone', - } - ) + + if self._server_capabilities.get( 'supportsConfigurationDoneRequest' ): + self._connection.DoRequest( + lambda msg: self._OnInitializeComplete(), + { + 'command': 'configurationDone', + } + ) + else: + self._OnInitializeComplete() + def OnEvent_thread( self, message ): self._stackTraceView.OnThreadEvent( message[ 'body' ] ) + def OnEvent_breakpoint( self, message ): reason = message[ 'body' ][ 'reason' ] bp = message[ 'body' ][ 'breakpoint' ] @@ -748,18 +764,19 @@ class DebugSession( object ): } ) - self._connection.DoRequest( - functools.partial( self._UpdateBreakpoints, None ), - { - 'command': 'setFunctionBreakpoints', - 'arguments': { - 'breakpoints': [ - { 'name': bp[ 'function' ] } - for bp in self._func_breakpoints if bp[ 'state' ] == 'ENABLED' - ], + if self._server_capabilities.get( 'supportsFunctionBreakpoints' ): + self._connection.DoRequest( + functools.partial( self._UpdateBreakpoints, None ), + { + 'command': 'setFunctionBreakpoints', + 'arguments': { + 'breakpoints': [ + { 'name': bp[ 'function' ] } + for bp in self._func_breakpoints if bp[ 'state' ] == 'ENABLED' + ], + } } - } - ) + ) def _ShowBreakpoints( self ): for file_name, line_breakpoints in self._line_breakpoints.items(): From 14f6814ff1c4b778ee3f043cec11d9397e0897e9 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 24 Feb 2019 19:41:20 +0000 Subject: [PATCH 38/50] Improve gadget installation; add system wide adapter config --- .gitignore | 1 + install_gadget.py | 129 ++++++++++++++++++++++++---- python3/vimspector/debug_session.py | 24 +++++- python3/vimspector/install.py | 32 +++++++ python3/vimspector/utils.py | 2 +- tests/breakpoints.test.vim | 3 +- 6 files changed, 169 insertions(+), 22 deletions(-) create mode 100644 python3/vimspector/install.py diff --git a/.gitignore b/.gitignore index 56a56f3..a29bc85 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ tests/messages tests/debuglog test.log gadgets/ +*.pyc diff --git a/install_gadget.py b/install_gadget.py index 14ce3c8..7e79004 100755 --- a/install_gadget.py +++ b/install_gadget.py @@ -1,5 +1,20 @@ #!/usr/bin/env python +# vimspector - A multi-language debugging system for Vim +# Copyright 2019 Ben Jackson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + try: import urllib.request as urllib2 except ImportError: @@ -8,7 +23,6 @@ except ImportError: import contextlib import os import collections -import platform import string import zipfile import shutil @@ -16,6 +30,14 @@ import subprocess import traceback import tarfile import hashlib +import sys +import json + +# Include vimspector source, for utils +sys.path.insert( 1, os.path.join( os.path.dirname( __file__ ), + 'python3' ) ) + +from vimspector import install GADGETS = { 'vscode-cpptools': { @@ -23,6 +45,7 @@ GADGETS = { 'url': ( 'https://github.com/Microsoft/vscode-cpptools/releases/download/' '${version}/${file_name}' ), }, + 'do': lambda name, root: InstallCppTools( name, root ), 'all': { 'version': '0.21.0', }, @@ -38,6 +61,18 @@ GADGETS = { 'file_name': 'cpptools-win32.vsix', 'checksum': None, }, + "adapters": { + "vscode-cpptools": { + "name": "cppdbg", + "command": [ + "${gadgetDir}/vscode-cpptools/debugAdapters/OpenDebugAD7" + ], + "attach": { + "pidProperty": "processId", + "pidSelect": "ask" + }, + }, + }, }, 'vscode-python': { 'download': { @@ -55,7 +90,7 @@ GADGETS = { 'url': 'https://github.com/puremourning/TclProDebug', 'ref': 'master', }, - 'do': lambda root: InstallTclProDebug( root ) + 'do': lambda name, root: InstallTclProDebug( name, root ) }, 'vscode-mono-debug': { 'enabled': False, @@ -94,7 +129,33 @@ def CurrentWorkingDir( d ): finally: os.chdir( cur_d ) -def InstallTclProDebug( root ): + +def MakeExecutable( file_path ): + # TODO: import stat and use them by _just_ adding the X bit. + print( 'Making executable: {}'.format( file_path ) ) + os.chmod( file_path, 0o755 ) + + +def InstallCppTools( name, root ): + extension = os.path.join( root, 'extension' ) + + # It's hilarious, but the execute bits aren't set in the vsix. So they + # actually have javascript code which does this. It's just a horrible horrible + # hoke that really is not funny. + MakeExecutable( os.path.join( extension, 'debugAdapters', 'OpenDebugAD7' ) ) + with open( os.path.join( extension, 'package.json' ) ) as f: + package = json.load( f ) + runtime_dependencies = package[ 'runtimeDependencies' ] + for dependency in runtime_dependencies: + for binary in dependency.get( 'binaries' ): + file_path = os.path.abspath( os.path.join( extension, binary ) ) + if os.path.exists( file_path ): + MakeExecutable( os.path.join( extension, binary ) ) + + MakeExtensionSymlink( name, root ) + + +def InstallTclProDebug( name, root ): configure = [ './configure' ] if OS == 'macos': @@ -127,7 +188,7 @@ def InstallTclProDebug( root ): subprocess.check_call( configure ) subprocess.check_call( [ 'make' ] ) - MakeSymlink( gadget_dir, 'tclpro', root ) + MakeSymlink( gadget_dir, name, root ) def DownloadFileTo( url, destination, file_name = None, checksum = None ): @@ -186,11 +247,28 @@ def ValidateCheckSumSHA256( file_path, checksum ): def RemoveIfExists( destination ): if os.path.exists( destination ) or os.path.islink( destination ): - print( "Removing existing {}".format( destination ) ) - if os.path.isdir( destination ): - shutil.rmtree( destination ) - else: + if os.path.islink( destination ): + print( "Removing file {}".format( destination ) ) os.remove( destination ) + else: + print( "Removing dir {}".format( destination ) ) + shutil.rmtree( destination ) + + +# Python's ZipFile module strips execute bits from files, for no good reason +# other than crappy code. Let's do it's job for it. +class ModePreservingZipFile( zipfile.ZipFile ): + def extract( self, member, path = None, pwd = None ): + if not isinstance(member, zipfile.ZipInfo): + member = self.getinfo(member) + + if path is None: + path = os.getcwd() + + ret_val = self._extract_member(member, path, pwd) + attr = member.external_attr >> 16 + os.chmod(ret_val, attr) + return ret_val def ExtractZipTo( file_path, destination, format ): @@ -198,7 +276,7 @@ def ExtractZipTo( file_path, destination, format ): RemoveIfExists( destination ) if format == 'zip': - with zipfile.ZipFile( file_path ) as f: + with ModePreservingZipFile( file_path ) as f: f.extractall( path = destination ) return elif format == 'tar': @@ -213,8 +291,16 @@ def ExtractZipTo( file_path, destination, format ): subprocess.check_call( [ 'tar', 'zxvf', file_path ] ) +def MakeExtensionSymlink( name, root ): + MakeSymlink( gadget_dir, name, os.path.join( root, 'extension' ) ), + + def MakeSymlink( in_folder, link, pointing_to ): RemoveIfExists( os.path.join( in_folder, link ) ) + + in_folder = os.path.abspath( in_folder ) + pointing_to = os.path.relpath( os.path.abspath( pointing_to ), + in_folder ) os.symlink( pointing_to, os.path.join( in_folder, link ) ) @@ -223,16 +309,14 @@ def CloneRepoTo( url, ref, destination ): subprocess.check_call( [ 'git', 'clone', url, destination ] ) subprocess.check_call( [ 'git', '-C', destination, 'checkout', ref ] ) -if platform.system() == 'Darwin': - OS = 'macos' -elif platform.system() == 'Winwdows': - OS = 'windows' -else: - OS = 'linux' +OS = install.GetOS() +gadget_dir = install.GetGadgetDir( os.path.dirname( __file__ ), OS ) -gadget_dir = os.path.join( os.path.dirname( __file__ ), 'gadgets', OS ) +print( 'OS = ' + OS ) +print( 'gadget_dir = ' + gadget_dir ) failed = [] +all_adapters = {} for name, gadget in GADGETS.items(): if not gadget.get( 'enabled', True ): continue @@ -269,9 +353,12 @@ for name, gadget in GADGETS.items(): root = destination if 'do' in gadget: - gadget[ 'do' ]( root ) + gadget[ 'do' ]( name, root ) else: - MakeSymlink( gadget_dir, name, os.path.join( root, 'extenstion') ), + MakeExtensionSymlink( name, root ) + + all_adapters.update( gadget.get( 'adapters', {} ) ) + print( "Done installing {}".format( name ) ) except Exception as e: @@ -280,6 +367,12 @@ for name, gadget in GADGETS.items(): print( "FAILED installing {}: {}".format( name, e ) ) +with open( install.GetGadgetConfigFile( os.path.dirname( __file__ ) ), + 'w' ) as f: + json.dump( { 'adapters': all_adapters }, f, indent=2, sort_keys=True ) + if failed: raise RuntimeError( 'Failed to install gadgets: {}'.format( ','.join( failed ) ) ) + + diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index e94a09b..384e903 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -25,16 +25,27 @@ from collections import defaultdict from vimspector import ( breakpoints, code, debug_adapter_connection, + install, output, stack_trace, utils, variables ) +VIMSPECTOR_HOME = os.path.abspath( os.path.join( os.path.dirname( __file__ ), + '..', + '..' ) ) + + class DebugSession( object ): def __init__( self ): self._logger = logging.getLogger( __name__ ) utils.SetUpLogging( self._logger ) + self._logger.info( 'VIMSPECTOR_HOME = %s', VIMSPECTOR_HOME ) + self._logger.info( 'gadgetDir = %s', + install.GetGadgetDir( VIMSPECTOR_HOME, + install.GetOS() ) ) + self._uiTab = None self._stackTraceView = None self._variablesView = None @@ -66,7 +77,15 @@ class DebugSession( object ): database = json.load( f ) configurations = database.get( 'configurations' ) - adapters = database.get( 'adapters' ) + adapters = {} + + for gadget_config_file in [ install.GetGadgetConfigFile( VIMSPECTOR_HOME ), + utils.PathToConfigFile( '.gadgets.json' ) ]: + if gadget_config_file and os.path.exists( gadget_config_file ): + with open( gadget_config_file, 'r' ) as f: + adapters.update( json.load( f ).get( 'adapters' ) or {} ) + + adapters.update( database.get( 'adapters' ) or {} ) if len( configurations ) == 1: configuration_name = next( iter( configurations.keys() ) ) @@ -90,7 +109,8 @@ class DebugSession( object ): # way to load .vimspector.local.json which just sets variables self._variables = { 'dollar': '$', # HACK - 'workspaceRoot': self._workspace_root + 'workspaceRoot': self._workspace_root, + 'gadgetDir': install.GetGadgetDir( VIMSPECTOR_HOME, install.GetOS() ) } self._variables.update( adapter.get( 'variables', {} ) ) self._variables.update( configuration.get( 'variables', {} ) ) diff --git a/python3/vimspector/install.py b/python3/vimspector/install.py new file mode 100644 index 0000000..280b07b --- /dev/null +++ b/python3/vimspector/install.py @@ -0,0 +1,32 @@ +# vimspector - A multi-language debugging system for Vim +# Copyright 2019 Ben Jackson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import platform +import os + +def GetOS(): + if platform.system() == 'Darwin': + return 'macos' + elif platform.system() == 'Winwdows': + return 'windows' + else: + return 'linux' + +def GetGadgetDir( vimspector_base, OS ): + return os.path.join( os.path.abspath( vimspector_base ), 'gadgets', OS ) + +def GetGadgetConfigFile( vimspector_base ): + return os.path.join( GetGadgetDir( vimspector_base, GetOS() ), + '.gadgets.json' ) diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index 647988d..b9e9191 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -279,7 +279,7 @@ def AppendToBuffer( buf, line_or_lines, modified=False ): line = 1 buf[:] = line_or_lines except: - # There seem to be a lot of Vim bugs that lead to E351, whose help says that + # There seem to be a lot of Vim bugs that lead to E315, whose help says that # this is an internal error. Ignore the error, but write a trace to the log. logging.getLogger( __name__ ).exception( 'Internal error while updating buffer %s (%s)', buf.name, buf.number ) diff --git a/tests/breakpoints.test.vim b/tests/breakpoints.test.vim index 5a725c9..1b21d60 100644 --- a/tests/breakpoints.test.vim +++ b/tests/breakpoints.test.vim @@ -20,7 +20,6 @@ function! SetUp_Test_Mappings_Are_Added_HUMAN() endfunction function! Test_Mappings_Are_Added_HUMAN() - call assert_true( hasmapto( 'vimspector#Continue()' ) ) call assert_false( hasmapto( 'vimspector#Launch()' ) ) call assert_true( hasmapto( 'vimspector#Stop()' ) ) @@ -89,4 +88,6 @@ function! Test_Signs_Placed_Using_API_Are_Shown() call assert_true( len( signs ) == 1 ) call assert_true( len( signs[ 0 ].signs ) == 0 ) + + " TODO: Use the screen dump test ? endfunction From 8528d80510788589844bfbcb675a7e8a9ed4c2c6 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 24 Feb 2019 19:41:41 +0000 Subject: [PATCH 39/50] Fix breakpoints --- python3/vimspector/breakpoints.py | 21 +++++++++++++++++---- python3/vimspector/debug_session.py | 21 +++++++++++++-------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/python3/vimspector/breakpoints.py b/python3/vimspector/breakpoints.py index 3c8e0b2..94f0edf 100644 --- a/python3/vimspector/breakpoints.py +++ b/python3/vimspector/breakpoints.py @@ -16,7 +16,7 @@ from collections import defaultdict import vim -import functools +import os class ProjectBreakpoints( object ): @@ -27,6 +27,9 @@ class ProjectBreakpoints( object ): self._line_breakpoints = defaultdict( list ) self._func_breakpoints = [] + # FIXME: Remove this. Remove breakpoints nonesense from code.py + self._breakpoints_handler = None + self._next_sign_id = 1 # TODO: Change to sign_define ? @@ -127,7 +130,17 @@ class ProjectBreakpoints( object ): else: self._ShowBreakpoints() - def SendBreakpoints( self, handler ): + + # FIXME: Remove this temporary compat .layer + def SetBreakpointsHandler( self, handler ): + self._breakpoints_handler = handler + + def SendBreakpoints( self ): + if not self._breakpoints_handler: + handler = lambda source, msg: self._ShowBreakpoints() + else: + handler = self._breakpoints_handler + for file_name, line_breakpoints in self._line_breakpoints.items(): breakpoints = [] for bp in line_breakpoints: @@ -147,7 +160,7 @@ class ProjectBreakpoints( object ): } self._connection.DoRequest( - functools.partial( self._UpdateBreakpoints, source ), + lambda msg: handler( source, msg ), { 'command': 'setBreakpoints', 'arguments': { @@ -159,7 +172,7 @@ class ProjectBreakpoints( object ): ) self._connection.DoRequest( - functools.partial( self._UpdateBreakpoints, None ), + lambda msg: handler( None, msg ), { 'command': 'setFunctionBreakpoints', 'arguments': { diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 384e903..2ea9e6e 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -147,6 +147,16 @@ class DebugSession( object ): self._stackTraceView.ConnectionUp( self._connection ) self._variablesView.ConnectionUp( self._connection ) self._outputView.ConnectionUp( self._connection ) + self._breakpoints.ConnectionUp( self._connection ) + + def update_breakpoints( source, message ): + if 'body' not in message: + return + self._codeView.AddBreakpoints( source, + message[ 'body' ][ 'breakpoints' ] ) + self._codeView.ShowBreakpoints() + + self._breakpoints.SetBreakpointsHandler( update_breakpoints ) if self._connection: self._StopDebugAdapter( start ) @@ -539,15 +549,8 @@ class DebugSession( object ): self._stackTraceView.LoadThreads( True ) def OnEvent_initialized( self, message ): - def update_breakpoints( source, message ): - if 'body' not in message: - return - self._codeView.AddBreakpoints( source, - message[ 'body' ][ 'breakpoints' ] ) - self._codeView.ShowBreakpoints() - self._codeView.ClearBreakpoints() - self._breakpoints.SendBreakpoints( update_breakpoints ) + self._breakpoints.SendBreakpoints() self._connection.DoRequest( lambda msg: self._OnInitializeComplete(), @@ -614,6 +617,8 @@ class DebugSession( object ): self._stackTraceView.ConnectionClosed() self._variablesView.ConnectionClosed() self._outputView.ConnectionClosed() + self._breakpoints.ConnectionClosed() + self._breakpoints.SetBreakpointsHandler( None ) self._ResetServerState() From 01c0dca3ad7f4645a01a6033c7d46099317a97df Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 24 Feb 2019 19:41:49 +0000 Subject: [PATCH 40/50] Fix signs --- python3/vimspector/code.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python3/vimspector/code.py b/python3/vimspector/code.py index b1394ee..9aa85da 100644 --- a/python3/vimspector/code.py +++ b/python3/vimspector/code.py @@ -75,7 +75,7 @@ class CodeView( object ): self._signs[ 'vimspectorPC' ] = self._next_sign_id self._next_sign_id += 1 - vim.command( 'sign place {0} group=VimspectorCode priority=20' + vim.command( 'sign place {0} group=VimspectorCode priority=20 ' 'line={1} name=vimspectorPC ' 'file={2}'.format( self._signs[ 'vimspectorPC' ], @@ -162,7 +162,7 @@ class CodeView( object ): self._next_sign_id += 1 self._signs[ 'breakpoints' ].append( sign_id ) vim.command( - 'sign place {0} group=VimspectorCode priority=9' + 'sign place {0} group=VimspectorCode priority=9 ' 'line={1} ' 'name={2} ' 'file={3}'.format( From 541686712c78af60a11cfef3907e31d47241bca9 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 26 Feb 2019 15:24:07 +0000 Subject: [PATCH 41/50] Use a list comprehension and try and catch some nasites --- autoload/vimspector/internal/job.vim | 6 +++++- python3/vimspector/utils.py | 11 ++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/autoload/vimspector/internal/job.vim b/autoload/vimspector/internal/job.vim index e24b60a..2d95fe7 100644 --- a/autoload/vimspector/internal/job.vim +++ b/autoload/vimspector/internal/job.vim @@ -138,12 +138,16 @@ function! vimspector#internal#job#StartCommandWithLog( cmd, category ) \ 'stoponexit': 'kill' \ } ) ) + if job_status( s:commands[ a:category ][ index ] ) !=# 'run' + echom "Unable to start job for " . a:cmd + return v:none + endif + let l:stdout = ch_getbufnr( \ job_getchannel( s:commands[ a:category ][ index ] ), 'out' ) let l:stderr = ch_getbufnr( \ job_getchannel( s:commands[ a:category ][ index ] ), 'err' ) - return [ l:stdout, l:stderr ] endfunction diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index f27f7cf..f13f349 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -58,7 +58,16 @@ def SetUpCommandBuffer( cmd, name ): json.dumps( cmd ), name ) ) - return ( vim.buffers[ bufnr ] for bufnr in bufs ) + if bufs is None: + raise RuntimeError( "Unable to start job {}: {}".format( cmd, name ) ) + elif not all( [ b > 0 for b in bufs ] ): + raise RuntimeError( "Unable to get all streams for job {}: {}".format( + name, + cmd ) ) + + UserMessage( 'Bufs: {}'.format( [ int(b) for b in bufs ] ), persist = True ) + + return [ vim.buffers[ b ] for b in bufs ] def CleanUpCommand( name ): From 174923cbc81267bb7e9bb67d079b6458225c757a Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 1 Mar 2019 13:25:15 +0000 Subject: [PATCH 42/50] Rudimentary support for exception breakpoint options --- python3/vimspector/debug_session.py | 67 ++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index a2a3680..8743341 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -64,6 +64,7 @@ class DebugSession( object ): def _ResetServerState( self ): self._connection = None self._configuration = None + self._exceptionBreakpoints = None self._init_complete = False self._launch_complete = False self._server_capabilities = {} @@ -778,6 +779,58 @@ class DebugSession( object ): } ) + if self._exceptionBreakpoints is None: + self._SetUpExceptionBreakpoints() + + if self._exceptionBreakpoints: + self._connection.DoRequest( + None, # There is nothing on the response to this + { + 'command': 'setExceptionBreakpoints', + 'arguments': self._exceptionBreakpoints + } + ) + + def _SetUpExceptionBreakpoints( self ): + exceptionBreakpointFilters = self._server_capabilities.get( + 'exceptionBreakpointFilters', + [] ) + + if exceptionBreakpointFilters or not self._server_capabilities.get( + 'supportsConfigurationDoneRequest' ): + exceptionFilters = [] + if exceptionBreakpointFilters: + for f in exceptionBreakpointFilters: + response = utils.AskForInput( + "Enable exception filter '{}'? (Y/N)".format( f[ 'label' ] ) ) + + if response == 'Y': + exceptionFilters.append( f[ 'filter' ] ) + elif not response and f.get( 'default' ): + exceptionFilters.append( f[ 'filter' ] ) + + self._exceptionBreakpoints = { + 'filters': exceptionFilters + } + + if self._server_capabilities.get( 'supportsExceptionOptions' ): + # FIXME Sigh. The python debug adapter requires this + # key to exist. Even though it is optional. + break_mode = utils.SelectFromList( 'When to break on exception?', + [ 'never', + 'always', + 'unhandled', + 'userHandled' ] ) + + if not break_mode: + break_mode = 'unhandled' + + path = [ { 'nagate': True, 'names': [ 'DO_NOT_MATCH' ] } ] + self._exceptionBreakpoints[ 'exceptionOptions' ] = [ { + 'path': path, + 'breakMode': break_mode + } ] + def _ShowBreakpoints( self ): for file_name, line_breakpoints in self._line_breakpoints.items(): for bp in line_breakpoints: @@ -801,9 +854,21 @@ class DebugSession( object ): 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 + + if text: + explanation += ': ' + text + msg = 'Paused in thread {0} due to {1}'.format( event.get( 'threadId', '' ), - event.get( 'description', event.get( 'reason', '' ) ) ) + explanation ) utils.UserMessage( msg, persist = True ) if self._outputView: From 629d6bf612a3191f6197faa97c7e0da97a36ba2a Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 1 Mar 2019 13:25:36 +0000 Subject: [PATCH 43/50] Allow variables to come from shell commands --- python3/vimspector/debug_session.py | 6 ++++-- python3/vimspector/utils.py | 31 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 8743341..b6d2068 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -158,8 +158,10 @@ class DebugSession( object ): 'dollar': '$', # HACK 'workspaceRoot': self._workspace_root } - self._variables.update( adapter.get( 'variables', {} ) ) - self._variables.update( configuration.get( 'variables', {} ) ) + self._variables.update( + utils.ParseVariables( adapter.get( 'variables', {} ) ) ) + self._variables.update( + utils.ParseVariables( configuration.get( 'variables', {} ) ) ) self._variables.update( launch_variables ) utils.ExpandReferencesInDict( configuration, self._variables ) diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index f13f349..e8c7a16 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -328,6 +328,37 @@ def ExpandReferencesInDict( obj, mapping, **kwargs ): obj[ k ] = expand_refs_in_object( obj[ k ] ) +def ParseVariables( variables ): + new_variables = {} + for n,v in variables.items(): + if isinstance( v, dict ): + if 'shell' in v: + import subprocess + import shlex + + new_v = v.copy() + # Bit of a hack. Allows environment variables to be used. + ExpandReferencesInDict( new_v, {} ) + + env = os.environ.copy() + env.update( new_v.get( 'env' ) or {} ) + cmd = new_v[ 'shell' ] + if not isinstance( cmd, list ): + cmd = shlex.split( cmd ) + + new_variables[ n ] = subprocess.check_output( + cmd, + cwd = new_v.get( 'cwd' ) or os.getcwd(), + env = env ).decode( 'utf-8' ).strip() + else: + raise ValueError( + "Unsupported variable defn {}: Missing 'shell'".format( n ) ) + else: + new_variables[ n ] = v + + return new_variables + + def DisplayBaloon( is_term, display ): if not is_term: display = '\n'.join( display ) From 9e1cd10c9914bcff968da38bfdae79bf228b9121 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 1 Mar 2019 13:25:52 +0000 Subject: [PATCH 44/50] When restarting, make sure that we have some configuraiton --- python3/vimspector/debug_session.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index b6d2068..686254a 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -207,6 +207,9 @@ class DebugSession( object ): # FIXME: For some reason this doesn't work when run from the WinBar. It just # beeps and doesn't display the config selector. One option is to just not # display the selector and restart with the same opitons. + if not self._configuration or not self._adapter: + return Start() + self._StartWithConfiguration( self._configuration, self._adapter ) def OnChannelData( self, data ): From cc7af4b1a4d5b537b4f78d3d59f78a1e22850aa4 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 1 Mar 2019 13:26:05 +0000 Subject: [PATCH 45/50] Make sure that single commands still work --- python3/vimspector/debug_session.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 686254a..c55cf84 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -541,23 +541,23 @@ class DebugSession( object ): elif commands is not None: raise ValueError( "Invalid commands; must be list" ) - commands = remote[ pfx + 'Command' ] + command = remote[ pfx + 'Command' ] - if isinstance( commands, str ): - commands = shlex.split( commands ) + if isinstance( command, str ): + command = shlex.split( command ) - if not isinstance( commands, list ): + if not isinstance( command, list ): raise ValueError( "Invalid command; must be list/string" ) - if not commands: + if not command: raise ValueError( 'Could not determine commands for ' + pfx ) - return commands + return [ command ] def _Initialise( self ): def handle_initialize_response( msg ): - self._server_capabilities = msg.get( 'body' ) or {} - self._Launch() + self._server_capabilities = msg.get( 'body' ) or {} + self._Launch() self._connection.DoRequest( handle_initialize_response, { 'command': 'initialize', From 67f74025812d6c762e08b1d15f0d4ad8ede0b896 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 2 Apr 2019 16:26:52 +0100 Subject: [PATCH 46/50] Fix flake8 errors --- python3/vimspector/breakpoints.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/python3/vimspector/breakpoints.py b/python3/vimspector/breakpoints.py index 34f266a..e15a649 100644 --- a/python3/vimspector/breakpoints.py +++ b/python3/vimspector/breakpoints.py @@ -18,6 +18,9 @@ from collections import defaultdict import vim import os +import json +from vimspector import utils + class ProjectBreakpoints( object ): def __init__( self ): @@ -54,7 +57,7 @@ class ProjectBreakpoints( object ): self._connection = None # for each breakpoint: - # clear its resolved status + # clear its resolved status def ListBreakpoints( self ): @@ -140,13 +143,15 @@ class ProjectBreakpoints( object ): self._ShowBreakpoints() - # FIXME: Remove this temporary compat .layer def SetBreakpointsHandler( self, handler ): + # FIXME: Remove this temporary compat .layer self._breakpoints_handler = handler + def SendBreakpoints( self ): if not self._breakpoints_handler: - handler = lambda source, msg: self._ShowBreakpoints() + def handler( source, msg ): + return self._ShowBreakpoints() else: handler = self._breakpoints_handler @@ -229,7 +234,7 @@ class ProjectBreakpoints( object ): } if self._server_capabilities.get( 'supportsExceptionOptions' ): - # FIXME Sigh. The python debug adapter requires this + # FIXME Sigh. The python debug adapter requires this # key to exist. Even though it is optional. break_mode = utils.SelectFromList( 'When to break on exception?', [ 'never', From e80c874e3b8df8d000f92dd87e6d390271840954 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 2 Apr 2019 16:27:37 +0100 Subject: [PATCH 47/50] Toggle the state before handling the message. I think this implies there is some re-entrancy here that i am not handling correctly, but this fixes an issue with vscode-python (for now) --- python3/vimspector/debug_adapter_connection.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/python3/vimspector/debug_adapter_connection.py b/python3/vimspector/debug_adapter_connection.py index c739df9..844bb23 100644 --- a/python3/vimspector/debug_adapter_connection.py +++ b/python3/vimspector/debug_adapter_connection.py @@ -199,11 +199,11 @@ class DebugAdapterConnection( object ): self._logger.debug( 'Message received: {0}'.format( message ) ) - try: - self._OnMessageReceived( message ) - finally: - # Don't allow exceptions to break message reading - self._SetState( 'READ_HEADER' ) + # We read the message, so the next time we get data from the socket it must + # be a header. + self._SetState( 'READ_HEADER' ) + self._OnMessageReceived( message ) + def _OnMessageReceived( self, message ): if not self._handler: From c9138c465b06f9d430cd14fe6b74492b466d592a Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sat, 6 Apr 2019 16:37:30 +0100 Subject: [PATCH 48/50] Upgrade python gadget --- install_gadget.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/install_gadget.py b/install_gadget.py index 7e79004..e5fc8d2 100755 --- a/install_gadget.py +++ b/install_gadget.py @@ -80,9 +80,17 @@ GADGETS = { '${version}/${file_name}' ), }, 'all': { - 'version': '2018.12.1', + 'version': '2019.3.6352', 'file_name': 'ms-python-release.vsix', - 'checksum': '0406028b7d2fbb86ffd6cda18a36638a95111fd35b19cc198d343a2828f5c3b1', + 'checksum': + 'f7e5552db3783d6b45ba4b84005d7b42a372033ca84c0fce82eb70e7372336c6', + }, + 'adapters': { + "name": "vscode-python", + "command": [ + "node", + "${gadgetDir}/vscode-python/out/client/debugger/debugAdapter/main.js", + ] }, }, 'tclpro': { From d24515a03546425237211734de36838728244866 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sat, 6 Apr 2019 16:42:56 +0100 Subject: [PATCH 49/50] Fix default adapter config --- install_gadget.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/install_gadget.py b/install_gadget.py index e5fc8d2..73d8212 100755 --- a/install_gadget.py +++ b/install_gadget.py @@ -86,11 +86,13 @@ GADGETS = { 'f7e5552db3783d6b45ba4b84005d7b42a372033ca84c0fce82eb70e7372336c6', }, 'adapters': { - "name": "vscode-python", - "command": [ - "node", - "${gadgetDir}/vscode-python/out/client/debugger/debugAdapter/main.js", - ] + "vscode-python": { + "name": "vscode-python", + "command": [ + "node", + "${gadgetDir}/vscode-python/out/client/debugger/debugAdapter/main.js", + ], + } }, }, 'tclpro': { From 5cb6f9cea2c9d424f763f0cb6e5638c80337e4fb Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sat, 27 Apr 2019 13:24:09 +0100 Subject: [PATCH 50/50] Actually the tclconfig.sh in system headers contains bogus paths. You just need homebrew --- install_gadget.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/install_gadget.py b/install_gadget.py index 73d8212..daf6502 100755 --- a/install_gadget.py +++ b/install_gadget.py @@ -187,8 +187,7 @@ def InstallTclProDebug( name, root ): # '/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System' # '/Library/Frameworks/Tcl.framework/Versions' # '/Current', - for p in [ '/System/Library/Frameworks/Tcl.framework/', - '/usr/local/opt/tcl-tk/lib' ]: + for p in [ '/usr/local/opt/tcl-tk/lib' ]: if os.path.exists( os.path.join( p, 'tclConfig.sh' ) ): configure.append( '--with-tcl=' + p ) break