Merge branch 'remote-bp'
This commit is contained in:
commit
cd5cd59ef1
24 changed files with 1933 additions and 344 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -1,3 +1,10 @@
|
|||
*.un~
|
||||
*.sw[op]
|
||||
__pycache__
|
||||
.vscode
|
||||
tests/*.res
|
||||
tests/messages
|
||||
tests/debuglog
|
||||
test.log
|
||||
gadgets/
|
||||
*.pyc
|
||||
|
|
|
|||
6
.gitmodules
vendored
6
.gitmodules
vendored
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -20,44 +20,42 @@ 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
|
||||
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
|
||||
|
|
@ -98,10 +96,9 @@ 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
|
||||
|
||||
unlet s:job
|
||||
endfunction
|
||||
|
||||
function! vimspector#internal#job#Reset() abort
|
||||
|
|
@ -117,6 +114,59 @@ 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'
|
||||
\ } ) )
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
|
|
|||
35
azure-pipelines.yml
Normal file
35
azure-pipelines.yml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# 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
|
||||
|
||||
jobs:
|
||||
- job: 'linax'
|
||||
pool:
|
||||
vimImage: 'ubuntu-16.04'
|
||||
container: 'puremourning/vimspector:test'
|
||||
steps:
|
||||
- bash: python3 install_gadget.py
|
||||
displayName: 'Install gadgets'
|
||||
|
||||
- bash: vim --version
|
||||
displayName: 'Print vim version information'
|
||||
|
||||
- bash: ./run_tests
|
||||
displayName: 'Run the tests'
|
||||
|
||||
- job: 'macos'
|
||||
pool:
|
||||
vmImage: 'macOS-10.13'
|
||||
steps:
|
||||
- bash: brew install macvim
|
||||
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'
|
||||
387
install_gadget.py
Executable file
387
install_gadget.py
Executable file
|
|
@ -0,0 +1,387 @@
|
|||
#!/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:
|
||||
import urllib2
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import collections
|
||||
import string
|
||||
import zipfile
|
||||
import shutil
|
||||
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': {
|
||||
'download': {
|
||||
'url': ( 'https://github.com/Microsoft/vscode-cpptools/releases/download/'
|
||||
'${version}/${file_name}' ),
|
||||
},
|
||||
'do': lambda name, root: InstallCppTools( name, root ),
|
||||
'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,
|
||||
},
|
||||
"adapters": {
|
||||
"vscode-cpptools": {
|
||||
"name": "cppdbg",
|
||||
"command": [
|
||||
"${gadgetDir}/vscode-cpptools/debugAdapters/OpenDebugAD7"
|
||||
],
|
||||
"attach": {
|
||||
"pidProperty": "processId",
|
||||
"pidSelect": "ask"
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'vscode-python': {
|
||||
'download': {
|
||||
'url': ( 'https://github.com/Microsoft/vscode-python/releases/download/'
|
||||
'${version}/${file_name}' ),
|
||||
},
|
||||
'all': {
|
||||
'version': '2019.3.6352',
|
||||
'file_name': 'ms-python-release.vsix',
|
||||
'checksum':
|
||||
'f7e5552db3783d6b45ba4b84005d7b42a372033ca84c0fce82eb70e7372336c6',
|
||||
},
|
||||
'adapters': {
|
||||
"vscode-python": {
|
||||
"name": "vscode-python",
|
||||
"command": [
|
||||
"node",
|
||||
"${gadgetDir}/vscode-python/out/client/debugger/debugAdapter/main.js",
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
'tclpro': {
|
||||
'repo': {
|
||||
'url': 'https://github.com/puremourning/TclProDebug',
|
||||
'ref': 'master',
|
||||
},
|
||||
'do': lambda name, root: InstallTclProDebug( name, root )
|
||||
},
|
||||
'vscode-mono-debug': {
|
||||
'enabled': False,
|
||||
'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',
|
||||
}
|
||||
},
|
||||
'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
|
||||
def CurrentWorkingDir( d ):
|
||||
cur_d = os.getcwd()
|
||||
try:
|
||||
os.chdir( d )
|
||||
yield
|
||||
finally:
|
||||
os.chdir( cur_d )
|
||||
|
||||
|
||||
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':
|
||||
# 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 [ '/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, name, 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,
|
||||
GetChecksumSHA254( 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 ):
|
||||
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 ):
|
||||
print( "Extracting {} to {}".format( file_path, destination ) )
|
||||
RemoveIfExists( destination )
|
||||
|
||||
if format == 'zip':
|
||||
with ModePreservingZipFile( 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 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 ) )
|
||||
|
||||
|
||||
def CloneRepoTo( url, ref, destination ):
|
||||
RemoveIfExists( destination )
|
||||
subprocess.check_call( [ 'git', 'clone', url, destination ] )
|
||||
subprocess.check_call( [ 'git', '-C', destination, 'checkout', ref ] )
|
||||
|
||||
OS = install.GetOS()
|
||||
gadget_dir = install.GetGadgetDir( os.path.dirname( __file__ ), 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
|
||||
|
||||
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' ]( name, root )
|
||||
else:
|
||||
MakeExtensionSymlink( name, root )
|
||||
|
||||
all_adapters.update( gadget.get( 'adapters', {} ) )
|
||||
|
||||
|
||||
print( "Done installing {}".format( name ) )
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
failed.append( name )
|
||||
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 ) ) )
|
||||
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ if s:mappings == 'VISUAL_STUDIO'
|
|||
nnoremap <C-S-F5> :call vimspector#Restart()<CR>
|
||||
nnoremap <F6> :call vimspector#Pause()<CR>
|
||||
nnoremap <F9> :call vimspector#ToggleBreakpoint()<CR>
|
||||
nnoremap <S-F9> :call vimspector#AddFunctionBreakpoint( '<cexpr>' )<CR>
|
||||
nnoremap <S-F9> :call vimspector#AddFunctionBreakpoint( expand( '<cexpr>' ) )<CR>
|
||||
nnoremap <F10> :call vimspector#StepOver()<CR>
|
||||
nnoremap <F11> :call vimspector#StepInto()<CR>
|
||||
nnoremap <S-F11> :call vimspector#StepOut()<CR>
|
||||
|
|
@ -52,7 +52,7 @@ elseif s:mappings == 'HUMAN'
|
|||
nnoremap <F4> :call vimspector#Restart()<CR>
|
||||
nnoremap <F6> :call vimspector#Pause()<CR>
|
||||
nnoremap <F9> :call vimspector#ToggleBreakpoint()<CR>
|
||||
nnoremap <F8> :call vimspector#AddFunctionBreakpoint( '<cexpr>' )<CR>
|
||||
nnoremap <F8> :call vimspector#AddFunctionBreakpoint( expand( '<cexpr>' ) )<CR>
|
||||
nnoremap <F10> :call vimspector#StepOver()<CR>
|
||||
nnoremap <F11> :call vimspector#StepInto()<CR>
|
||||
nnoremap <F12> :call vimspector#StepOut()<CR>
|
||||
|
|
|
|||
270
python3/vimspector/breakpoints.py
Normal file
270
python3/vimspector/breakpoints.py
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
# 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 os
|
||||
|
||||
import json
|
||||
from vimspector import utils
|
||||
|
||||
|
||||
class ProjectBreakpoints( object ):
|
||||
def __init__( self ):
|
||||
self._connection = None
|
||||
|
||||
# These are the user-entered breakpoints.
|
||||
self._line_breakpoints = defaultdict( list )
|
||||
self._func_breakpoints = []
|
||||
|
||||
# FIXME: Remove this. Remove breakpoints nonesense from code.py
|
||||
self._breakpoints_handler = None
|
||||
self._exceptionBreakpoints = None
|
||||
self._server_capabilities = {}
|
||||
|
||||
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 SetServerCapabilities( self, server_capabilities ):
|
||||
self._server_capabilities = server_capabilities
|
||||
|
||||
|
||||
def ConnectionClosed( self ):
|
||||
self._breakpoints_handler = None
|
||||
self._exceptionBreakpoints = None
|
||||
self._server_capabilities = {}
|
||||
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': <filled in when placed>,
|
||||
#
|
||||
# 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 SetBreakpointsHandler( self, handler ):
|
||||
# FIXME: Remove this temporary compat .layer
|
||||
self._breakpoints_handler = handler
|
||||
|
||||
|
||||
def SendBreakpoints( self ):
|
||||
if not self._breakpoints_handler:
|
||||
def handler( source, msg ):
|
||||
return self._ShowBreakpoints()
|
||||
else:
|
||||
handler = self._breakpoints_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(
|
||||
lambda msg: handler( source, msg ),
|
||||
{
|
||||
'command': 'setBreakpoints',
|
||||
'arguments': {
|
||||
'source': source,
|
||||
'breakpoints': breakpoints,
|
||||
},
|
||||
'sourceModified': False, # TODO: We can actually check this
|
||||
}
|
||||
)
|
||||
|
||||
if self._server_capabilities.get( 'supportsFunctionBreakpoints' ):
|
||||
self._connection.DoRequest(
|
||||
lambda msg: handler( None, msg ),
|
||||
{
|
||||
'command': 'setFunctionBreakpoints',
|
||||
'arguments': {
|
||||
'breakpoints': [
|
||||
{ 'name': bp[ 'function' ] }
|
||||
for bp in self._func_breakpoints if bp[ 'state' ] == 'ENABLED'
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
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:
|
||||
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 ) )
|
||||
|
|
@ -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' ):
|
||||
|
|
@ -64,23 +63,21 @@ 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
|
||||
|
||||
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' ] ) )
|
||||
|
|
@ -89,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()
|
||||
|
|
@ -144,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()
|
||||
|
||||
|
|
@ -164,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' ]
|
||||
|
|
@ -172,6 +173,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' )
|
||||
|
||||
|
|
|
|||
|
|
@ -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 )
|
||||
|
|
@ -196,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:
|
||||
|
|
|
|||
|
|
@ -18,17 +18,23 @@ import vim
|
|||
import json
|
||||
import os
|
||||
import functools
|
||||
import subprocess
|
||||
import shlex
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from vimspector import ( code,
|
||||
from vimspector import ( breakpoints,
|
||||
code,
|
||||
debug_adapter_connection,
|
||||
install,
|
||||
output,
|
||||
stack_trace,
|
||||
utils,
|
||||
variables )
|
||||
|
||||
SIGN_ID_OFFSET = 10005000
|
||||
VIMSPECTOR_HOME = os.path.abspath( os.path.join( os.path.dirname( __file__ ),
|
||||
'..',
|
||||
'..' ) )
|
||||
|
||||
|
||||
class DebugSession( object ):
|
||||
|
|
@ -36,75 +42,27 @@ class DebugSession( object ):
|
|||
self._logger = logging.getLogger( __name__ )
|
||||
utils.SetUpLogging( self._logger )
|
||||
|
||||
self._connection = None
|
||||
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
|
||||
self._outputView = None
|
||||
self._breakpoints = breakpoints.ProjectBreakpoints()
|
||||
|
||||
self._next_sign_id = SIGN_ID_OFFSET
|
||||
self._run_on_server_exit = None
|
||||
|
||||
# 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()
|
||||
|
||||
def _ResetServerState( self ):
|
||||
self._connection = None
|
||||
self._configuration = None
|
||||
|
||||
vim.command( 'sign define vimspectorBP text==> texthl=Error' )
|
||||
vim.command( 'sign define vimspectorBPDisabled text=!> texthl=Warning' )
|
||||
|
||||
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': <filled in when placed>,
|
||||
#
|
||||
# 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()
|
||||
self._init_complete = False
|
||||
self._launch_complete = False
|
||||
self._server_capabilities = {}
|
||||
|
||||
def Start( self, launch_variables = {} ):
|
||||
self._configuration = None
|
||||
|
|
@ -120,55 +78,89 @@ class DebugSession( object ):
|
|||
with open( launch_config_file, 'r' ) as f:
|
||||
database = json.load( f )
|
||||
|
||||
launch_config = database.get( 'configurations' )
|
||||
adapters = database.get( 'adapters' )
|
||||
configurations = database.get( 'configurations' )
|
||||
adapters = {}
|
||||
|
||||
if len( launch_config ) == 1:
|
||||
configuration = next( iter( launch_config.keys() ) )
|
||||
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() ) )
|
||||
else:
|
||||
configuration = utils.SelectFromList( 'Which launch configuration?',
|
||||
list( launch_config.keys() ) )
|
||||
if not configuration:
|
||||
configuration_name = utils.SelectFromList(
|
||||
'Which launch configuration?',
|
||||
sorted( 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,
|
||||
'gadgetDir': install.GetGadgetDir( VIMSPECTOR_HOME, install.GetOS() )
|
||||
}
|
||||
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 )
|
||||
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
|
||||
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 )
|
||||
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 )
|
||||
|
|
@ -181,6 +173,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 ):
|
||||
|
|
@ -190,13 +185,15 @@ 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:
|
||||
self._connection.OnRequestTimeout( timer_id )
|
||||
|
||||
def OnChannelClosed( self ):
|
||||
# TODO: Not calld
|
||||
self._connection = None
|
||||
|
||||
def Stop( self ):
|
||||
|
|
@ -216,13 +213,10 @@ class DebugSession( object ):
|
|||
self._codeView.Reset()
|
||||
vim.current.tabpage = self._uiTab
|
||||
vim.command( 'tabclose!' )
|
||||
|
||||
vim.eval( 'vimspector#internal#{}#Reset()'.format(
|
||||
self._connection_type ) )
|
||||
vim.eval( 'vimspector#internal#state#Reset()' )
|
||||
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:
|
||||
|
|
@ -358,9 +352,18 @@ 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:
|
||||
self._connection_type = 'channel'
|
||||
|
|
@ -392,77 +395,141 @@ 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()
|
||||
if callback:
|
||||
callback()
|
||||
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': {
|
||||
'terminateDebugee': True
|
||||
},
|
||||
'arguments': arguments,
|
||||
}, 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
|
||||
# TODO: Use the 'tarminate' request if supportsTerminateRequest set
|
||||
|
||||
raise ValueError( 'Unrecognised pidSelect {0}'.format(
|
||||
atttach_config[ 'pidSelect' ] ) )
|
||||
def _PrepareAttach( self, adapter_config, launch_config ):
|
||||
|
||||
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' ]
|
||||
|
||||
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 )
|
||||
|
||||
if not pid:
|
||||
# FIXME: We should raise an exception here or something
|
||||
utils.UserMessage( 'Unable to get PID', persist = True )
|
||||
return
|
||||
|
||||
commands = self._GetCommands( remote, 'attach' )
|
||||
|
||||
for command in commands:
|
||||
cmd = ssh + command[:]
|
||||
|
||||
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: ' )
|
||||
launch_config[ atttach_config[ 'pidProperty' ] ] = pid
|
||||
return
|
||||
elif atttach_config[ 'pidSelect' ] == 'none':
|
||||
return
|
||||
|
||||
raise ValueError( 'Unrecognised pidSelect {0}'.format(
|
||||
atttach_config[ 'pidSelect' ] ) )
|
||||
|
||||
|
||||
|
||||
def _PrepareLaunch( self, command_line, 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' ] )
|
||||
|
||||
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.replace( '%CMD%', command_line ) )
|
||||
|
||||
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" )
|
||||
|
||||
command = remote[ pfx + 'Command' ]
|
||||
|
||||
if isinstance( command, str ):
|
||||
command = shlex.split( command )
|
||||
|
||||
if not isinstance( command, list ):
|
||||
raise ValueError( "Invalid command; must be list/string" )
|
||||
|
||||
if not command:
|
||||
raise ValueError( 'Could not determine commands for ' + pfx )
|
||||
|
||||
return [ command ]
|
||||
|
||||
def _Initialise( self ):
|
||||
adapter_config = self._adapter
|
||||
self._connection.DoRequest( lambda msg: self._Launch(), {
|
||||
def handle_initialize_response( msg ):
|
||||
self._server_capabilities = msg.get( 'body' ) or {}
|
||||
self._breakpoints.SetServerCapabilities( self._server_capabilities )
|
||||
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,
|
||||
|
|
@ -477,15 +544,26 @@ class DebugSession( object ):
|
|||
|
||||
|
||||
def OnFailure( self, reason, message ):
|
||||
self._outputView.ServerEcho( reason )
|
||||
msg = "Request for '{}' failed: {}".format( message[ 'command' ],
|
||||
reason )
|
||||
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":
|
||||
self._SelectProcess( adapter_config, launch_config )
|
||||
request = self._configuration.get(
|
||||
'remote-request',
|
||||
launch_config.get( 'request', 'launch' ) )
|
||||
|
||||
if request == "attach":
|
||||
self._PrepareAttach( 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)
|
||||
|
|
@ -493,15 +571,7 @@ 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._stackTraceView.LoadThreads( True ),
|
||||
lambda msg: self._OnLaunchComplete(),
|
||||
{
|
||||
'command': launch_config[ 'request' ],
|
||||
'arguments': launch_config
|
||||
|
|
@ -509,25 +579,54 @@ 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()
|
||||
|
||||
def _OnInitializeComplete( self ):
|
||||
self._init_complete = True
|
||||
self._LoadThreadsIfReady()
|
||||
|
||||
def _LoadThreadsIfReady( self ):
|
||||
# NOTE: You might think we should only load threads on a stopped event,
|
||||
# but the spec is clear:
|
||||
#
|
||||
# After a successful launch or attach the development tool requests the
|
||||
# baseline of currently existing threads with the threads request and
|
||||
# then starts to listen for thread events to detect new or terminated
|
||||
# threads.
|
||||
#
|
||||
# Of course, specs are basically guidelines. MS's own cpptools simply
|
||||
# doesn't respond top threads request when attaching via gdbserver. At
|
||||
# least it would apear that way.
|
||||
#
|
||||
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(
|
||||
None,
|
||||
{
|
||||
'command': 'configurationDone',
|
||||
}
|
||||
)
|
||||
self._codeView.ClearBreakpoints()
|
||||
self._breakpoints.SendBreakpoints()
|
||||
|
||||
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' ]
|
||||
|
|
@ -543,8 +642,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 )
|
||||
|
||||
|
|
@ -574,88 +675,57 @@ class DebugSession( object ):
|
|||
self._stackTraceView.Clear()
|
||||
self._variablesView.Clear()
|
||||
|
||||
def OnEvent_terminated( self, message ):
|
||||
def OnServerExit( self, status ):
|
||||
self.Clear()
|
||||
|
||||
self._connection.Reset()
|
||||
self._stackTraceView.ConnectionClosed()
|
||||
self._variablesView.ConnectionClosed()
|
||||
self._outputView.ConnectionClosed()
|
||||
self._breakpoints.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 ):
|
||||
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' ] )
|
||||
|
||||
def OnEvent_stopped( self, message ):
|
||||
event = message[ 'body' ]
|
||||
reason = event.get( 'reason' ) or '<protocol error>'
|
||||
description = event.get( 'description' )
|
||||
text = event.get( 'text' )
|
||||
|
||||
utils.UserMessage( 'Paused in thread {0} due to {1}'.format(
|
||||
if description:
|
||||
explanation = description + '(' + reason + ')'
|
||||
else:
|
||||
explanation = reason
|
||||
|
||||
if text:
|
||||
explanation += ': ' + text
|
||||
|
||||
msg = 'Paused in thread {0} due to {1}'.format(
|
||||
event.get( 'threadId', '<unknown>' ),
|
||||
event.get( 'description', event.get( 'reason', '' ) ) ) )
|
||||
explanation )
|
||||
utils.UserMessage( msg, persist = True )
|
||||
|
||||
if self._outputView:
|
||||
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 )
|
||||
|
|
|
|||
32
python3/vimspector/install.py
Normal file
32
python3/vimspector/install.py
Normal file
|
|
@ -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' )
|
||||
|
|
@ -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.is_job = False
|
||||
|
||||
|
||||
BUFFER_MAP = {
|
||||
'console': 'Console',
|
||||
'stdout': 'Console',
|
||||
|
|
@ -40,9 +48,13 @@ 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" )' ) )
|
||||
|
||||
def ServerEcho( self, text ):
|
||||
self._ShowOutput( 'Console' )
|
||||
|
||||
def Print( self, categroy, text ):
|
||||
self._Print( 'server', text.splitlines() )
|
||||
|
||||
def OnOutput( self, event ):
|
||||
|
|
@ -58,37 +70,56 @@ 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 ConnectionUp( self, connection ):
|
||||
self._connection = connection
|
||||
|
||||
def ConnectionClosed( self ):
|
||||
# Don't clear because output is probably still useful
|
||||
self._connection = None
|
||||
|
||||
def Reset( self ):
|
||||
self.Clear()
|
||||
|
||||
def Clear( self ):
|
||||
for buf in self._buffers:
|
||||
vim.command( 'bwipeout! {0}'.format( self._buffers[ buf ].name ) )
|
||||
for category, tab_buffer in self._buffers.items():
|
||||
if tab_buffer.is_job:
|
||||
utils.CleanUpCommand( category )
|
||||
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()
|
||||
|
||||
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:
|
||||
self.Print( 'Console', 'There is no current stack frame' )
|
||||
return
|
||||
|
||||
console = self._buffers[ 'Console' ]
|
||||
console = self._buffers[ 'Console' ].buf
|
||||
utils.AppendToBuffer( console, 'Evaluating: ' + expression )
|
||||
|
||||
def print_result( message ):
|
||||
|
|
@ -110,24 +141,70 @@ 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 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 ):
|
||||
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:
|
||||
assert cmd is None
|
||||
cmd = [ 'tail', '-F', '-n', '+1', '--', file_name ]
|
||||
|
||||
if cmd is not None:
|
||||
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:
|
||||
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}' )<CR>".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}' )<CR>".format(
|
||||
tab_buffer.index,
|
||||
utils.Escape( category ),
|
||||
'*' if tab_buffer.flag else '' ) )
|
||||
|
|
|
|||
|
|
@ -37,6 +37,15 @@ 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
|
||||
#
|
||||
# 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 ):
|
||||
return self._currentThread
|
||||
|
|
@ -51,6 +60,10 @@ class StackTraceView( object ):
|
|||
with utils.ModifiableScratchBuffer( self._buf ):
|
||||
utils.ClearBuffer( self._buf )
|
||||
|
||||
def ConnectionUp( self, connection ):
|
||||
self._connection = connection
|
||||
self._requesting_threads = False
|
||||
|
||||
def ConnectionClosed( self ):
|
||||
self.Clear()
|
||||
self._connection = None
|
||||
|
|
@ -60,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',
|
||||
} )
|
||||
|
|
@ -146,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 )
|
||||
|
|
@ -156,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
|
||||
|
||||
|
|
@ -174,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
|
||||
|
||||
|
|
|
|||
|
|
@ -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' ) )
|
||||
|
||||
|
|
@ -32,6 +33,48 @@ 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 SetUpCommandBuffer( cmd, name ):
|
||||
bufs = vim.bindeval(
|
||||
'vimspector#internal#job#StartCommandWithLog( {}, "{}" )'.format(
|
||||
json.dumps( cmd ),
|
||||
name ) )
|
||||
|
||||
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 ):
|
||||
return vim.eval( 'vimspector#internal#job#CleanUpCommand( "{}" )'.format(
|
||||
name ) )
|
||||
|
||||
|
||||
def SetUpScratchBuffer( buf, name ):
|
||||
buf.options[ 'buftype' ] = 'nofile'
|
||||
buf.options[ 'swapfile' ] = False
|
||||
|
|
@ -199,7 +242,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 ):
|
||||
|
|
@ -215,14 +261,11 @@ def AppendToBuffer( buf, line_or_lines, modified=False ):
|
|||
else:
|
||||
line = 1
|
||||
buf[:] = line_or_lines
|
||||
except vim.error as e:
|
||||
# There seem to be a lot of Vim bugs that lead to E351, whose help says that
|
||||
except:
|
||||
# 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.
|
||||
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
|
||||
|
|
@ -283,3 +326,42 @@ def ExpandReferencesInDict( obj, mapping, **kwargs ):
|
|||
|
||||
for k in obj.keys():
|
||||
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 )
|
||||
|
||||
vim.eval( "balloon_show( {0} )".format(
|
||||
json.dumps( display ) ) )
|
||||
|
|
|
|||
|
|
@ -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 ):
|
||||
|
|
@ -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
|
||||
|
|
@ -353,13 +356,11 @@ class VariablesView( object ):
|
|||
'Type: ' + body.get( 'type', '<unknown>' ),
|
||||
'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, {
|
||||
|
|
|
|||
8
run_test_vim
Executable file
8
run_test_vim
Executable file
|
|
@ -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 "$@"
|
||||
31
run_tests
Executable file
31
run_tests
Executable file
|
|
@ -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
|
||||
93
tests/breakpoints.test.vim
Normal file
93
tests/breakpoints.test.vim
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
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<CR>' )
|
||||
|
||||
" 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 )
|
||||
|
||||
" TODO: Use the screen dump test ?
|
||||
endfunction
|
||||
29
tests/ci/image/Dockerfile
Normal file
29
tests/ci/image/Dockerfile
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
FROM ubuntu:18.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y dist-upgrade && \
|
||||
apt-get -y install python3-dev \
|
||||
ca-cacert \
|
||||
libncurses5-dev libncursesw5-dev \
|
||||
git \
|
||||
tcl-dev \
|
||||
tcllib && \
|
||||
apt-get -y autoremove
|
||||
|
||||
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
|
||||
|
||||
347
tests/run_test.vim
Normal file
347
tests/run_test.vim
Normal file
|
|
@ -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
|
||||
7
tests/testdata/cpp/simple.cpp
vendored
Normal file
7
tests/testdata/cpp/simple.cpp
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#include <iostream>
|
||||
|
||||
int main( int argc, char ** )
|
||||
{
|
||||
printf( "this is a test %d", argc );
|
||||
return 0;
|
||||
}
|
||||
6
tests/vimrc
Normal file
6
tests/vimrc
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
let g:vimspector_test_plugin_path = expand( '<sfile>:h:h' )
|
||||
|
||||
let &rtp = &rtp . ',' . g:vimspector_test_plugin_path
|
||||
|
||||
filetype plugin indent on
|
||||
syntax enable
|
||||
Loading…
Add table
Add a link
Reference in a new issue