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.
This commit is contained in:
Ben Jackson 2018-05-21 23:44:06 +01:00
commit 269d09b73e
8 changed files with 302 additions and 189 deletions

35
.vimspector.json Normal file
View file

@ -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"
}
}
}

View file

@ -19,109 +19,11 @@ let s:save_cpo = &cpo
set cpo&vim
" }}}
let s:plugin_base = expand( '<sfile>: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
" }}}

View file

@ -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( '<sfile>: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
" }}}

View file

@ -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()<CR>' )
vim.command( 'nnoremenu WinBar.Finish :call vimspector#StepOut()<CR>' )
vim.command( 'nnoremenu WinBar.Pause :call vimspector#Pause()<CR>' )
vim.command( 'nnoremenu WinBar.Stop :call vimspector#Stop()<CR>' )
vim.command( 'sign define vimspectorPC text=>> texthl=Search' )

View file

@ -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' ]

View file

@ -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',

View file

@ -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': '<unknown>' }
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

View file

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