From 269d09b73e7a3ab4b4bd07addef69523d61b29d4 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Mon, 21 May 2018 23:44:06 +0100 Subject: [PATCH] Very basic support for launch configuration This change refactors the way we launch the job and puts it all in an internal namespace. Having done that, we are able to launch the job from the python side. This allows us to neatly load a json file, simlar in format to .vscode's launch.json, but sufficiently different that users won't just expect the launch.json to work. This change allows selecting between 2 different adapters to debug the same c program. --- .vimspector.json | 35 ++++ autoload/vimspector.vim | 109 +----------- autoload/vimspector/internal/job.vim | 110 ++++++++++++ python3/vimspector/code.py | 4 +- .../vimspector/debug_adapter_connection.py | 17 +- python3/vimspector/debug_session.py | 162 ++++++++++-------- python3/vimspector/stack_trace.py | 6 +- python3/vimspector/utils.py | 48 +++++- 8 files changed, 302 insertions(+), 189 deletions(-) create mode 100644 .vimspector.json create mode 100644 autoload/vimspector/internal/job.vim diff --git a/.vimspector.json b/.vimspector.json new file mode 100644 index 0000000..4628986 --- /dev/null +++ b/.vimspector.json @@ -0,0 +1,35 @@ +{ + "lldb-mi Launch": { + "adapter": { + "name": "lldb-mi", + "command": [ + "node", + "/Users/ben/.vscode/extensions/webfreak.debug-0.22.0/out/src/lldb.js" + ] + }, + "configuration": { + "request": "launch", + "target": "support/test/cpp/simple_c_program/test", + "args": [], + "cwd": ".", + "lldbmipath": "/Users/ben/.vscode/extensions/ms-vscode.cpptools-0.17.1/debugAdapters/lldb/bin/lldb-mi" + } + }, + "ms Launch": { + "adapter": { + "name": "cppdbg", + "command": [ "/Users/ben/.vscode/extensions/ms-vscode.cpptools-0.17.1/debugAdapters/OpenDebugAD7" ] + }, + "configuration": { + "name": "ms Launch", + "type": "cppdbg", + "request": "launch", + "program": "/Users/ben/.vim/bundle/vimspector/support/test/cpp/simple_c_program/test", + "args": [], + "cwd": "/Users/ben", + "environment": [], + "externalConsole": true, + "MIMode": "lldb" + } + } +} diff --git a/autoload/vimspector.vim b/autoload/vimspector.vim index 3b1a960..6faddd8 100644 --- a/autoload/vimspector.vim +++ b/autoload/vimspector.vim @@ -19,109 +19,11 @@ let s:save_cpo = &cpo set cpo&vim " }}} -let s:plugin_base = expand( ':p:h' ) . '/../' -let s:command = [ - \ 'node', - \ '/Users/ben/.vscode/extensions/webfreak.debug-0.22.0/out/src/lldb.js' - \ ] - -" let s:command = [ -" \ '/Users/ben/.vscode/extensions/ms-vscode.cpptools-0.17.1/' -" \ 'debugAdapters/OpenDebugAD7' -" \ ] -" -" \ 'node', -" \ '/Users/ben/Development/debugger/vscode-mock-debug/out/debugAdapter.js' -" \ ] - -function! s:_OnServerData( channel, data ) abort - py3 << EOF -_vimspector_session.OnChannelData( vim.eval( 'a:data' ) ) -EOF -endfunction - -function! s:_OnServerError( channel, data ) abort - echom "Channel received error: " . a:data -endfunction - -function! s:_OnExit( channel, status ) abort - echom "Channel exit with status " . a:status -endfunction - -function! s:_OnClose( channel ) abort - echom "Channel closed" -endfunction - -function! vimspector#StartDebugSession() abort - " TODO: - " - Work out the debug configuration (e.g. using RemoteDebug) - " - Start a job running the server in raw mode - " - Start up the python thread to communicate - " - Set up the UI: - " - Signs? - " - Console? - " - " For now, lets: - " - start up an echo process - " - get python talking to it - if exists( 's:job' ) - echo "Job is already running" - return - endif - - let s:job = job_start( s:command, - \ { - \ 'in_mode': 'raw', - \ 'out_mode': 'raw', - \ 'err_mode': 'raw', - \ 'exit_cb': function( 's:_OnExit' ), - \ 'close_cb': function( 's:_OnClose' ), - \ 'out_cb': function( 's:_OnServerData' ), - \ 'err_cb': function( 's:_OnServerError' ) - \ } - \ ) - - if job_status( s:job ) != 'run' - echom 'Fail whale. Job is ' . job_status( s:job ) - return - endif -endfunction - -function! s:_Send( msg ) abort - if job_status( s:job ) != 'run' - echom "Server isnt running" - return - endif - - let ch = job_getchannel( s:job ) - if ch == 'channel fail' - echom "Channel was closed unexpectedly!" - return - endif - - call ch_sendraw( ch, a:msg ) - -endfunction - -function! vimspector#StopDebugSession() abort - py3 _vimspector_session.Stop() - - if job_status( s:job ) == 'run' - job_stop( s:job, 'term' ) - endif - - unlet s:job -endfunction - +" TODO: Test function function! vimspector#Launch() abort - call vimspector#StartDebugSession() - " call vimspector#WriteMessageToServer( 'test' ) - - let ch = job_getchannel( s:job ) - py3 << EOF from vimspector import debug_session -_vimspector_session = debug_session.DebugSession( vim.Function( 's:_Send' ) ) +_vimspector_session = debug_session.DebugSession() _vimspector_session.Start() EOF endfunction @@ -146,6 +48,10 @@ function! vimspector#Pause() abort py3 _vimspector_session.Pause() endfunction +function! vimspector#Stop() abort + py3 _vimspector_session.Stop() +endfunction + function! vimspector#ExpandVariable() abort py3 _vimspector_session.ExpandVariable() endfunction @@ -154,6 +60,7 @@ function! vimspector#GoToFrame() abort py3 _vimspector_session.GoToFrame() endfunction -" Boilerplate {{{ +" Boilerplate {{{ let &cpo=s:save_cpo unlet s:save_cpo +" }}} diff --git a/autoload/vimspector/internal/job.vim b/autoload/vimspector/internal/job.vim new file mode 100644 index 0000000..1852222 --- /dev/null +++ b/autoload/vimspector/internal/job.vim @@ -0,0 +1,110 @@ +" vimspector - A multi-language debugging system for Vim +" Copyright 2018 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. + + +" Boilerplate {{{ +let s:save_cpo = &cpo +set cpo&vim +" }}} + +let s:plugin_base = expand( ':p:h' ) . '/../' +let s:command = [ + \ 'node', + \ '/Users/ben/.vscode/extensions/webfreak.debug-0.22.0/out/src/lldb.js' + \ ] + +" let s:command = [ +" \ '/Users/ben/.vscode/extensions/ms-vscode.cpptools-0.17.1/' +" \ 'debugAdapters/OpenDebugAD7' +" \ ] +" +" \ 'node', +" \ '/Users/ben/Development/debugger/vscode-mock-debug/out/debugAdapter.js' +" \ ] + + +function! s:_OnServerData( channel, data ) abort + py3 << EOF +_vimspector_session.OnChannelData( vim.eval( 'a:data' ) ) +EOF +endfunction + +function! s:_OnServerError( channel, data ) abort + echom "Channel received error: " . a:data +endfunction + +function! s:_OnExit( channel, status ) abort + echom "Channel exit with status " . a:status +endfunction + +function! s:_OnClose( channel ) abort + echom "Channel closed" +endfunction + +function! s:_Send( msg ) abort + if job_status( s:job ) != 'run' + echom "Server isnt running" + return + endif + + let ch = job_getchannel( s:job ) + if ch == 'channel fail' + echom "Channel was closed unexpectedly!" + return + endif + + call ch_sendraw( ch, a:msg ) +endfunction + +function! vimspector#internal#job#StartDebugSession( config ) abort + if exists( 's:job' ) + echo "Job is already running" + return v:none + endif + + let s:job = job_start( a:config[ 'command' ], + \ { + \ 'in_mode': 'raw', + \ 'out_mode': 'raw', + \ 'err_mode': 'raw', + \ 'exit_cb': function( 's:_OnExit' ), + \ 'close_cb': function( 's:_OnClose' ), + \ 'out_cb': function( 's:_OnServerData' ), + \ 'err_cb': function( 's:_OnServerError' ) + \ } + \ ) + + if job_status( s:job ) != 'run' + echom 'Fail whale. Job is ' . job_status( s:job ) + return v:none + endif + + return funcref( 's:_Send' ) +endfunction + +function! vimspector#internal#job#StopDebugSession() abort + py3 _vimspector_session.Stop() + + if job_status( s:job ) == 'run' + job_stop( s:job, 'term' ) + endif + + unlet s:job +endfunction + +" Boilerplate {{{ +let &cpo=s:save_cpo +unlet s:save_cpo +" }}} diff --git a/python3/vimspector/code.py b/python3/vimspector/code.py index cf8e054..d2f5938 100644 --- a/python3/vimspector/code.py +++ b/python3/vimspector/code.py @@ -13,11 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging import vim -_logger = logging.getLogger( __name__ ) - SIGN_ID_OFFSET = 10000000 @@ -38,6 +35,7 @@ class CodeView( object ): vim.command( 'nnoremenu WinBar.Step :call vimspector#StepInto()' ) vim.command( 'nnoremenu WinBar.Finish :call vimspector#StepOut()' ) vim.command( 'nnoremenu WinBar.Pause :call vimspector#Pause()' ) + vim.command( 'nnoremenu WinBar.Stop :call vimspector#Stop()' ) vim.command( 'sign define vimspectorPC text=>> texthl=Search' ) diff --git a/python3/vimspector/debug_adapter_connection.py b/python3/vimspector/debug_adapter_connection.py index 54b5926..8ca5480 100644 --- a/python3/vimspector/debug_adapter_connection.py +++ b/python3/vimspector/debug_adapter_connection.py @@ -15,13 +15,15 @@ import logging import json -import vim -_logger = logging.getLogger( __name__ ) +from vimspector import utils class DebugAdapterConnection( object ): def __init__( self, handler, send_func ): + self._logger = logging.getLogger( __name__ ) + utils.SetUpLogging( self._logger ) + self._Write = send_func self._SetState( 'READ_HEADER' ) self._buffer = bytes() @@ -41,7 +43,7 @@ class DebugAdapterConnection( object ): def OnData( self, data ): data = bytes( data, 'utf-8' ) - _logger.debug( 'Received ({0}/{1}): {2},'.format( type( data ), + self._logger.debug( 'Received ({0}/{1}): {2},'.format( type( data ), len( data ), data ) ) @@ -69,7 +71,7 @@ class DebugAdapterConnection( object ): msg = json.dumps( msg ) data = 'Content-Length: {0}\r\n\r\n{1}'.format( len( msg ), msg ) - _logger.debug( 'Sending: {0}'.format( data ) ) + self._logger.debug( 'Sending: {0}'.format( data ) ) self._Write( data ) def _ReadHeaders( self ): @@ -102,7 +104,7 @@ class DebugAdapterConnection( object ): message = json.loads( payload ) - _logger.debug( 'Message received: {0}'.format( message ) ) + self._logger.debug( 'Message received: {0}'.format( message ) ) self._OnMessageReceived( message ) @@ -116,8 +118,9 @@ class DebugAdapterConnection( object ): if handler: handler( message ) else: - _logger.error( 'Request failed: {0}'.format( message[ 'message' ] ) ) - vim.command( "echom 'Request failed: {0}'".format( + self._logger.error( + 'Request failed: {0}'.format( message[ 'message' ] ) ) + utils.UserMessage( 'Request failed: {0}'.format( message[ 'message' ] ) ) elif message[ 'type' ] == 'event': method = 'OnEvent_' + message[ 'event' ] diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 326b67e..1dc9418 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -15,6 +15,7 @@ import logging import vim +import json from vimspector import ( code, debug_adapter_connection, @@ -22,16 +23,13 @@ from vimspector import ( code, utils, variables ) -_logger = logging.getLogger( __name__ ) - class DebugSession( object ): - def __init__( self, channel_send_func ): - utils.SetUpLogging() + def __init__( self ): + self._logger = logging.getLogger( __name__ ) + utils.SetUpLogging( self._logger ) - self._connection = debug_adapter_connection.DebugAdapterConnection( - self, - channel_send_func ) + self._connection = None self._uiTab = None self._threadsBuffer = None @@ -40,59 +38,38 @@ class DebugSession( object ): self._currentThread = None self._currentFrame = None + def Start( self, configuration = None ): + launch_config_file = utils.PathToConfigFile( '.vimspector.json' ) + + if not launch_config_file: + utils.UserMessage( 'Unable to find .vimspector.json. You need to tell ' + 'vimspector how to launch your application' ) + return + + with open( launch_config_file, 'r' ) as f: + launch_config = json.load( f ) + + if not configuration: + configuration = utils.SelectFromList( 'Which launch configuration?', + list( launch_config.keys() ) ) + + if not configuration: + return + + configuration = launch_config[ configuration ] + + self._StartDebugAdapter( configuration[ 'adapter' ] ) + self._Initialise( configuration[ 'adapter' ], + configuration[ 'configuration' ] ) self._SetUpUI() - def _SetUpUI( self ): - vim.command( 'tabnew' ) - self._uiTab = vim.current.tabpage - - # Code window - self._codeView = code.CodeView( vim.current.window ) - - # Threads - vim.command( '50vspl' ) - vim.command( 'enew' ) - self._threadsBuffer = vim.current.buffer - utils.SetUpScratchBuffer( self._threadsBuffer ) - - with utils.TemporaryVimOption( 'eadirection', 'ver' ): - with utils.TemporaryVimOption( 'equalalways', 1 ): - # Call stack - vim.command( 'spl' ) - vim.command( 'enew' ) - self._stackTraceView = stack_trace.StackTraceView( self, - self._connection, - vim.current.buffer ) - - # Output/logging - vim.command( 'spl' ) - vim.command( 'enew' ) - self._outputBuffer = vim.current.buffer - utils.SetUpScratchBuffer( self._outputBuffer ) - - # Variables - vim.command( 'spl' ) - vim.command( 'enew' ) - self._variablesView = variables.VariablesView( self._connection, - vim.current.buffer ) - - - def SetCurrentFrame( self, frame ): - self._currentFrame = frame - self._codeView.SetCurrentFrame( frame ) - self._variablesView.LoadScopes( frame ) - - def OnChannelData( self, data ): self._connection.OnData( data ) - def Start( self ): - self._Initialise() - def Stop( self ): self._codeView.Clear() - self._connection.DoRequest( None, { + self._connection.DoRequest( lambda msg: self._CleanUpUi(), { 'command': 'disconnect', 'arguments': { 'terminateDebugee': True @@ -145,34 +122,81 @@ class DebugSession( object ): def GoToFrame( self ): self._stackTraceView.GoToFrame() - def _Initialise( self ): - def handler( message ) : - self._connection.DoRequest( None, { - 'command': 'launch', + def _SetUpUI( self ): + vim.command( 'tabnew' ) + self._uiTab = vim.current.tabpage - 'arguments': { - "target": "/Users/Ben/.vim/bundle/vimspector/support/test/cpp/" - "simple_c_program/test", - "args": [], - "cwd": "/Users/ben", - "stopOnEntry": True, - 'lldbmipath': - '/Users/ben/.vscode/extensions/ms-vscode.cpptools-0.17.1/' - 'debugAdapters/lldb/bin/lldb-mi', - } - } ) + # Code window + self._codeView = code.CodeView( vim.current.window ) + + # Threads + vim.command( '50vspl' ) + vim.command( 'enew' ) + self._threadsBuffer = vim.current.buffer + utils.SetUpScratchBuffer( self._threadsBuffer ) + + with utils.TemporaryVimOption( 'eadirection', 'ver' ): + with utils.TemporaryVimOption( 'equalalways', 1 ): + # Call stack + vim.command( 'spl' ) + vim.command( 'enew' ) + self._stackTraceView = stack_trace.StackTraceView( self, + self._connection, + vim.current.buffer ) + + # Output/logging + vim.command( 'spl' ) + vim.command( 'enew' ) + self._outputBuffer = vim.current.buffer + utils.SetUpScratchBuffer( self._outputBuffer ) + + # Variables + vim.command( 'spl' ) + vim.command( 'enew' ) + self._variablesView = variables.VariablesView( self._connection, + vim.current.buffer ) + + def SetCurrentFrame( self, frame ): + self._currentFrame = frame + self._codeView.SetCurrentFrame( frame ) + self._variablesView.LoadScopes( frame ) + + def _StartDebugAdapter( self, adapter_config ): + self._logger.info( 'Starting debug adapter with: {0}'.format( json.dumps( + adapter_config ) ) ) + + channel_send_func = vim.bindeval( + "vimspector#internal#job#StartDebugSession( {0} )".format( + json.dumps( adapter_config ) ) ) + + self._connection = debug_adapter_connection.DebugAdapterConnection( + self, + channel_send_func ) + + self._logger.info( 'Debug Adapter Started' ) - self._connection.DoRequest( handler, { + def _Initialise( self, adapter_config, launch_config ): + self._logger.info( 'Initialising adapter with config {0}'.format( + json.dumps( launch_config ) ) ) + + self._connection.DoRequest( lambda msg: self._Launch( launch_config ), { 'command': 'initialize', 'arguments': { - 'adapterID': 'cppdbg', # Apparently only MS debugger cares + 'adapterID': adapter_config.get( 'name', 'adapter' ), 'linesStartAt1': True, 'columnsStartAt1': True, 'pathFormat': 'path', }, } ) + + def _Launch( self, launch_config ): + self._connection.DoRequest( None, { + 'command': launch_config[ 'request' ], + 'arguments': launch_config + } ) + def OnEvent_initialized( self, message ): self._connection.DoRequest( None, { 'command': 'setFunctionBreakpoints', diff --git a/python3/vimspector/stack_trace.py b/python3/vimspector/stack_trace.py index 37c31a6..4b010f4 100644 --- a/python3/vimspector/stack_trace.py +++ b/python3/vimspector/stack_trace.py @@ -13,13 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging import vim from vimspector import utils -_logger = logging.getLogger( __name__ ) - class StackTraceView( object ): def __init__( self, session, connection, buf ): @@ -59,10 +56,11 @@ class StackTraceView( object ): stackFrames = message[ 'body' ][ 'stackFrames' ] for frame in stackFrames: + source = frame[ 'source' ] or { 'name': '' } self._buf.append( '{0}: {1}@{2}:{3}'.format( frame[ 'id' ], frame[ 'name' ], - frame[ 'source' ][ 'name' ], + source[ 'name' ], frame[ 'line' ] ) ) self._line_to_frame[ len( self._buf ) ] = frame diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index 5fa52c4..5f2ec3b 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -18,16 +18,15 @@ import logging import os import contextlib import vim - -_logger = logging.getLogger( __name__ ) +import json -def SetUpLogging(): +def SetUpLogging( logger ): handler = logging.FileHandler( os.path.expanduser( '~/.vimspector.log' ) ) - _logger.setLevel( logging.DEBUG ) + logger.setLevel( logging.DEBUG ) handler.setFormatter( logging.Formatter( '%(asctime)s - %(levelname)s - %(message)s' ) ) - _logger.addHandler( handler ) + logger.addHandler( handler ) def SetUpScratchBuffer( buf ): @@ -68,3 +67,42 @@ def TemporaryVimOption( opt, value ): yield finally: vim.options[ opt ] = old_value + + +def PathToConfigFile( file_name ): + p = os.getcwd() + while True: + candidate = os.path.join( p, file_name ) + if os.path.exists( candidate ): + return candidate + + parent = os.path.dirname( p ) + if parent == p: + return None + p = parent + + +def Escape( msg ): + return msg.replace( "'", "''" ) + + +def UserMessage( msg, persist=False ): + vim.command( 'redraw' ) + cmd = 'echom' if persist else 'echo' + for line in msg.split( '\n' ): + vim.command( '{0} \'{1}\''.format( cmd, Escape( line ) ) ) + + +def SelectFromList( prompt, options ): + vim.eval( 'inputsave()' ) + display_options = [ prompt ] + display_options.extend( [ '{0}: {1}'.format( i + 1, v ) + for i, v in enumerate( options ) ] ) + try: + selection = int( vim.eval( + 'inputlist( ' + json.dumps( display_options ) + ' )' ) ) - 1 + if selection < 0 or selection >= len( options ): + return None + return options[ selection ] + finally: + vim.eval( 'inputrestore()' )