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()' )