Merge pull request #297 from puremourning/threads

[WIP] Improve Threads Handling
This commit is contained in:
mergify[bot] 2020-11-22 15:07:22 +00:00 committed by GitHub
commit 23130d74ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 991 additions and 194 deletions

145
README.md
View file

@ -11,16 +11,17 @@ For a tutorial and usage overview, take a look at the
* [Supported languages](#supported-languages)
* [Other languages](#other-languages)
* [Installation](#installation)
* [Quick Start](#quick-start)
* [Dependencies](#dependencies)
* [Neovim differences](#neovim-differences)
* [Windows differences](#windows-differences)
* [Clone the plugin](#clone-the-plugin)
* [Trying it out](#trying-it-out)
* [Cloning the plugin](#cloning-the-plugin)
* [Install some gadgets](#install-some-gadgets)
* [VimspectorInstall and VimspectorUpdate commands](#vimspectorinstall-and-vimspectorupdate-commands)
* [install_gadget.py](#install_gadgetpy)
* [Manual gadget installation](#manual-gadget-installation)
* [The gadget directory](#the-gadget-directory)
* [Trying it out](#trying-it-out)
* [Upgrade](#upgrade)
* [About](#about)
* [Background](#background)
@ -33,9 +34,11 @@ For a tutorial and usage overview, take a look at the
* [Launch and attach by PID:](#launch-and-attach-by-pid)
* [Launch with options](#launch-with-options)
* [Debug configuration selection](#debug-configuration-selection)
* [Get configurations](#get-configurations)
* [Breakpoints](#breakpoints)
* [Exception breakpoints](#exception-breakpoints)
* [Clear breakpoints](#clear-breakpoints)
* [Run to Cursor](#run-to-cursor)
* [Stepping](#stepping)
* [Variables and scopes](#variables-and-scopes)
* [Watches](#watches)
@ -48,6 +51,7 @@ For a tutorial and usage overview, take a look at the
* [Closing debugger](#closing-debugger)
* [Debug adapter configuration](#debug-adapter-configuration)
* [C, C , Rust, etc.](#c-c-rust-etc)
* [Rust](#rust)
* [Remote debugging](#remote-debugging)
* [Remote launch and attach](#remote-launch-and-attach)
* [Python](#python)
@ -65,7 +69,6 @@ For a tutorial and usage overview, take a look at the
* [Usage with YouCompleteMe](#usage-with-youcompleteme)
* [Other LSP clients](#other-lsp-clients)
* [Lua](#lua)
* [Rust](#rust)
* [Other servers](#other-servers)
* [Customisation](#customisation)
* [Changing the default signs](#changing-the-default-signs)
@ -80,7 +83,7 @@ For a tutorial and usage overview, take a look at the
* [License](#license)
* [Sponsorship](#sponsorship)
<!-- Added by: ben, at: Fri 4 Sep 2020 00:48:17 BST -->
<!-- Added by: ben, at: Sun 22 Nov 2020 14:35:00 GMT -->
<!--te-->
@ -121,8 +124,6 @@ And a couple of brief demos:
- logging/stdout display
- simple stable API for custom tooling (e.g. integrate with language server)
For other languages, you'll need some other way to install the gadget.
## Supported languages
The following table lists the languages that are "built-in" (along with their
@ -158,10 +159,12 @@ To use Vimspector with a language that's not "built-in", see this
# Installation
## Quick Start
There are 2 installation methods:
* Using a release tarball, or
* Manually
* Using a release tarball and vim packages
* Using a clone of the repo (e.g. package manager)
Release tarballs come with debug adapters for the default languages
pre-packaged. To use a release tarball:
@ -174,15 +177,27 @@ $ mkdir -p $HOME/.vim/pack
$ curl -L <url> | tar -C $HOME/.vim/pack zxvf -
```
3. Add `packadd! vimspector` to you `.vimrc`
3. Configure your project's debug profiles (create `.vimspector.json`)
Alternatively, you can clone the repo and select which gadgets are installed:
1. Check the dependencies
1. Install the plugin as a Vim package. See `:help packages`.
2. Install some 'gadgets' (debug adapters)
2. Add `packadd! vimspector` to you `.vimrc`
2. Install some 'gadgets' (debug adapters) - see `:VimspectorInstall ...`
3. Configure your project's debug profiles (create `.vimspector.json`)
If you prefer to use a plugin manager, see the plugin manager's docs. For
Vundle, use:
```vim
Plugin 'puremourning/vimspector'
```
The following sections expand on the above brief overview.
## Dependencies
Vimspector requires:
@ -239,20 +254,54 @@ The following features are not implemented for Windows:
* Tailing the vimspector log in the Output Window.
## Clone the plugin
## Trying it out
If you just want to try out vimspector without changing your vim config, there
are example projects for a number of languages in `support/test`, including:
* Python (`support/test/python/simple_python`)
* Go (`support/test/go/hello_world`)
* Nodejs (`support/test/node/simple`)
* Chrome (`support/test/chrome/`)
* etc.
To test one of these out, cd to the directory and run:
```
vim -Nu /path/to/vimspector/tests/vimrc --cmd "let g:vimspector_enable_mappings='HUMAN'"
```
Then press `<F5>`.
There's also a C++ project in `tests/testdata/cpp/simple/` with a `Makefile`
which can be used to check everything is working. This is used by the regression
tests in CI so should always work, and is a good way to check if the problem is
your configuration rather than a bug.
## Cloning the plugin
If you're not using a release tarball, you'll need to clone this repo to the
appropriate place.
1. Clone the plugin
There are many Vim plugin managers, and I'm not going to state a particular
preference, so if you choose to use one, you're on your own with installation
issues.
preference, so if you choose to use one, follow the plugin manager's
documentation. For example, for Vundle, use:
Install vimspector as a Vim package, either by cloning this repository into your
package path, like this:
```viml
Plugin 'puremourning/vimspector'
```
If you don't use a plugin manager already, install vimspector as a Vim package
by cloning this repository into your package path, like this:
```
$ git clone https://github.com/puremourning/vimspector ~/.vim/pack/vimspector/opt/vimspector
```
2. Configure vimspector in your `.vimrc`:
2. Configure vimspector in your `.vimrc`, for example to enable the standard
mapings:
```viml
let g:vimspector_enable_mappings = 'HUMAN'
@ -265,7 +314,7 @@ let g:vimspector_enable_mappings = 'HUMAN'
packadd! vimspector
```
See support/doc/example_vimrc.vim.
See support/doc/example_vimrc.vim for a minimal example.
## Install some gadgets
@ -281,11 +330,10 @@ There are a few ways to do this:
installed for you.
* Using `:VimspectorInstall <adapter> <args...>` (use TAB `wildmenu` to see the
options, also accepts any `install_gadget.py` option)
* Alternatively, using `python3 install_gadget.py <args>` (use `--help` to see
all options)
* When attempting to launch a debug configuration, if the configured adapter
can't be found, vimspector might suggest installing one.
* Use `:VimspectorUpdate` to install the latest supported versions of the
* Using `python3 install_gadget.py <args>` (use `--help` to see all options)
* Attempting to launch a debug configuration; if the configured adapter
can't be found, vimspector will suggest installing one.
* Using `:VimspectorUpdate` to install the latest supported versions of the
gadgets.
Here's a demo of doing somee installs and an upgrade:
@ -293,7 +341,7 @@ Here's a demo of doing somee installs and an upgrade:
[![asciicast](https://asciinema.org/a/Hfu4ZvuyTZun8THNen9FQbTay.svg)](https://asciinema.org/a/Hfu4ZvuyTZun8THNen9FQbTay)
Both `install_gadget.py` and `:VimspectorInstall` do the same set of things,
though the default behaviours are slightly different. For supported languages,
though the default behaviours are slightly different. For supported languages,
they will:
* Download the relevant debug adapter at a version that's been tested from the
@ -306,7 +354,7 @@ they will:
broken in this regard.
* Set up the `gadgetDir` symlinks for the platform.
To install the tested debug adapter for a language, run:
For example, to install the tested debug adapter for a language, run:
| To install | Script | Command |
| --- | --- | --- |
@ -468,30 +516,6 @@ Vimspector will also load any fies matching:
format as `.gadgets.json` but are not overwritten when running
`install_gadget.py`.
## Trying it out
If you just want to try out vimspector without changing your vim config, there
are example projects for a number of languages in `support/test`, including:
* Python (`support/test/python/simple_python`)
* Go (`support/test/go/hello_world`)
* Nodejs (`support/test/node/simple`)
* Chrome (`support/test/chrome/`)
* etc.
To test one of these out, cd to the directory and run:
```
vim -Nu /path/to/vimspector/tests/vimrc --cmd "let g:vimspector_enable_mappings='HUMAN'"
```
Then press `<F5>`.
There's also a C++ project in `tests/testdata/cpp/simple/` with a `Makefile`
which can be used to check everything is working. This is used by the regression
tests in CI so should always work, and is a good way to check if the problem is
your configuration rather than a bug.
## Upgrade
After updating the Vimspector code (either via `git pull` or whatever package
@ -751,6 +775,9 @@ Scopes and variables are represented by the buffer `vimspector.Variables`.
## Watches
The watch window is used to inspect variables and expressions. Expressions are
evaluated in the selected stack frame which is "focussed"
The watches window is a prompt buffer, where that's available. Enter insert mode
to add a new watch expression.
@ -767,7 +794,7 @@ The watches are represented by the buffer `vimspector.StackTrace`.
### Watch autocompletion
The watch prompt buffer has its `omnifunc` set to a function that will
The watch prompt buffer has its `omnifunc` set to a function that will
calcualte completion for the current expression. This is trivailly used with
`<Ctrl-x><Ctrl-o>` (see `:help ins-completion`), or integrated with your
favourite completion system. The filetype in the buffer is set to
@ -783,8 +810,28 @@ let g:ycm_semantic_triggers = {
## Stack Traces
* In the threads window, use `<CR>`, or double-click with left mouse to expand/collapse.
The stack trace window shows the state of each progream thread. Threads which
are stopped can be expanded to show the strack trace of that thread.
Often, but not always, all threads are stopped when a breakpoint is hit. The
status of a thread is show in parentheses after the thread's name. Where
supported by the underlying debugger, threads can be paused and continued
individually from within the Stack Trace window.
A particular thread, highlighted with the `CursorLine` highlight group is the
"focussed" thread. This is the thread that receives commands like "Stop In",
"Stop Out", "Continue" and "Pause" in the code window. The focussed thread can
be changed manually to "switch to" that thread.
* Use `<CR>`, or double-click with left mouse to expand/collapse a thread stack
trace, or use the WinBar button.
* Use `<CR>`, or double-click with left mouse on a stack frame to jump to it.
* Use the WinBar or `vimspector#PauseContinueThread()` to individually pause or
continue the selected thread.
* Use the "Focus" WinBar button, `<leader><CR>` or `vimspector#SetCurrentThread()`
to set the "focussed" thread to the currently selected one. If the selected
line is a stack frame, set the focussed thread to the thread of that frame and
jump to that frame in the code window.
![stack trace](https://puremourning.github.io/vimspector-web/img/vimspector-callstack-window.png)

View file

@ -171,6 +171,20 @@ function! vimspector#Pause() abort
py3 _vimspector_session.Pause()
endfunction
function! vimspector#PauseContinueThread() abort
if !s:Enabled()
return
endif
py3 _vimspector_session.PauseContinueThread()
endfunction
function! vimspector#SetCurrentThread() abort
if !s:Enabled()
return
endif
py3 _vimspector_session.SetCurrentThread()
endfunction
function! vimspector#Stop() abort
if !s:Enabled()
return

View file

@ -34,6 +34,7 @@ function! vimspector#internal#state#Reset() abort
catch /.*/
echohl WarningMsg
echom 'Exception while loading vimspector:' v:exception
echom 'From:' v:throwpoint
echom 'Vimspector unavailable: Requires Vim compiled with Python 3.6'
echohl None
return v:false

View file

@ -32,6 +32,7 @@ class CodeView( object ):
self._logger = logging.getLogger( __name__ )
utils.SetUpLogging( self._logger )
# FIXME: This ID is by group, so should be module scope
self._next_sign_id = 1
self._breakpoints = defaultdict( list )
self._signs = {

View file

@ -429,39 +429,90 @@ class DebugSession( object ):
},
} )
self._stackTraceView.OnContinued()
self._codeView.SetCurrentFrame( None )
@IfConnected()
def StepInto( self ):
if self._stackTraceView.GetCurrentThreadId() is None:
threadId = self._stackTraceView.GetCurrentThreadId()
if threadId is None:
return
self._connection.DoRequest( None, {
def handler( *_ ):
self._stackTraceView.OnContinued( { 'threadId': threadId } )
self._codeView.SetCurrentFrame( None )
self._connection.DoRequest( handler, {
'command': 'stepIn',
'arguments': {
'threadId': self._stackTraceView.GetCurrentThreadId()
'threadId': threadId
},
} )
@IfConnected()
def StepOut( self ):
if self._stackTraceView.GetCurrentThreadId() is None:
threadId = self._stackTraceView.GetCurrentThreadId()
if threadId is None:
return
self._connection.DoRequest( None, {
def handler( *_ ):
self._stackTraceView.OnContinued( { 'threadId': threadId } )
self._codeView.SetCurrentFrame( None )
self._connection.DoRequest( handler, {
'command': 'stepOut',
'arguments': {
'threadId': self._stackTraceView.GetCurrentThreadId()
'threadId': threadId
},
} )
def Continue( self ):
if self._connection:
self._stackTraceView.Continue()
else:
if not self._connection:
self.Start()
return
threadId = self._stackTraceView.GetCurrentThreadId()
if threadId is None:
utils.UserMessage( 'No current thread', persist = True )
return
def handler( msg ):
self._stackTraceView.OnContinued( {
'threadId': threadId,
'allThreadsContinued': ( msg.get( 'body' ) or {} ).get(
'allThreadsContinued',
True )
} )
self._codeView.SetCurrentFrame( None )
self._connection.DoRequest( handler, {
'command': 'continue',
'arguments': {
'threadId': threadId,
},
} )
@IfConnected()
def Pause( self ):
self._stackTraceView.Pause()
if self._stackTraceView.GetCurrentThreadId() is None:
utils.UserMessage( 'No current thread', persist = True )
return
self._connection.DoRequest( None, {
'command': 'pause',
'arguments': {
'threadId': self._stackTraceView.GetCurrentThreadId(),
},
} )
@IfConnected()
def PauseContinueThread( self ):
self._stackTraceView.PauseContinueThread()
@IfConnected()
def SetCurrentThread( self ):
self._stackTraceView.SetCurrentThread()
@IfConnected()
def ExpandVariable( self ):
@ -1098,6 +1149,7 @@ class DebugSession( object ):
def OnEvent_exited( self, message ):
utils.UserMessage( 'The debugee exited with status code: {}'.format(
message[ 'body' ][ 'exitCode' ] ) )
self.SetCurrentFrame( None )
def OnEvent_process( self, message ):
utils.UserMessage( 'The debugee was started: {}'.format(
@ -1107,7 +1159,8 @@ class DebugSession( object ):
pass
def OnEvent_continued( self, message ):
pass
self._stackTraceView.OnContinued( message[ 'body' ] )
self._codeView.SetCurrentFrame( None )
def Clear( self ):
self._codeView.Clear()
@ -1142,6 +1195,7 @@ class DebugSession( object ):
def OnEvent_terminated( self, message ):
# We will handle this when the server actually exists
utils.UserMessage( "Debugging was terminated by the server." )
self.SetCurrentFrame( None )
def OnEvent_output( self, message ):
if self._outputView:

View file

@ -28,11 +28,12 @@ DEFAULTS = {
# Signs
'sign_priority': {
'vimspectorPC': 200,
'vimspectorPCBP': 200,
'vimspectorBP': 9,
'vimspectorBPCond': 9,
'vimspectorBPDisabled': 9,
'vimspectorPC': 200,
'vimspectorPCBP': 200,
'vimspectorBP': 9,
'vimspectorBPCond': 9,
'vimspectorBPDisabled': 9,
'vimspectorCurrentThread': 200
},
# Installer

View file

@ -16,11 +16,75 @@
import vim
import os
import logging
import typing
from vimspector import utils
from vimspector import utils, signs
class Thread:
"""The state of a single thread."""
PAUSED = 0
RUNNING = 1
TERMINATED = 3
state = RUNNING
stopped_event: typing.Dict
thread: typing.Dict
stacktrace: typing.List[ typing.Dict ]
id: str
def __init__( self, thread ):
self.id = thread[ 'id' ]
self.stopped_event = None
self.Update( thread )
def Update( self, thread ):
self.thread = thread
self.stacktrace = None
def Paused( self, event ):
self.state = Thread.PAUSED
self.stopped_event = event
def Continued( self ):
self.state = Thread.RUNNING
self.stopped_event = None
self.Collapse()
def Exited( self ):
self.state = Thread.TERMINATED
self.stopped_event = None
def State( self ):
if self.state == Thread.PAUSED:
return self.stopped_event.get( 'description' ) or 'paused'
elif self.state == Thread.RUNNING:
return 'running'
return 'terminated'
def Expand( self, stack_trace ):
self.stacktrace = stack_trace
def Collapse( self ):
self.stacktrace = None
def IsExpanded( self ):
return self.stacktrace is not None
def CanExpand( self ):
return self.state == Thread.PAUSED
class StackTraceView( object ):
class ThreadRequestState:
NO = 0
REQUESTING = 1
PENDING = 2
# FIXME: Make into a dict by id ?
_threads: typing.List[ Thread ]
_line_to_thread = typing.Dict[ int, Thread ]
def __init__( self, session, win ):
self._logger = logging.getLogger( __name__ )
utils.SetUpLogging( self._logger )
@ -37,25 +101,43 @@ class StackTraceView( object ):
self._sources = {}
self._scratch_buffers = []
# FIXME: This ID is by group, so should be module scope
self._next_sign_id = 1
utils.SetUpHiddenBuffer( self._buf, 'vimspector.StackTrace' )
utils.SetUpUIWindow( win )
vim.command( 'nnoremap <silent> <buffer> <CR> '
':<C-U>call vimspector#GoToFrame()<CR>' )
vim.command( 'nnoremap <silent> <buffer> <2-LeftMouse> '
':<C-U>call vimspector#GoToFrame()<CR>' )
with utils.LetCurrentWindow( win ):
vim.command( 'nnoremap <silent> <buffer> <CR> '
':<C-U>call vimspector#GoToFrame()<CR>' )
vim.command( 'nnoremap <silent> <buffer> <leader><CR> '
':<C-U>call vimspector#SetCurrentThread()<CR>' )
vim.command( 'nnoremap <silent> <buffer> <2-LeftMouse> '
':<C-U>call vimspector#GoToFrame()<CR>' )
if utils.UseWinBar():
vim.command( 'nnoremenu 1.1 WinBar.Pause/Continue '
':call vimspector#PauseContinueThread()<CR>' )
vim.command( 'nnoremenu 1.2 WinBar.Expand/Collapse '
':call vimspector#GoToFrame()<CR>' )
vim.command( 'nnoremenu 1.3 WinBar.Focus '
':call vimspector#SetCurrentThread()<CR>' )
win.options[ 'cursorline' ] = False
if not signs.SignDefined( 'vimspectorCurrentThread' ):
signs.DefineSign( 'vimspectorCurrentThread',
text = '',
double_text = '',
texthl = 'MatchParen',
linehl = 'CursorLine' )
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
self._requesting_threads = StackTraceView.ThreadRequestState.NO
self._pending_thread_request = None
def GetCurrentThreadId( self ):
@ -68,14 +150,19 @@ class StackTraceView( object ):
self._current_frame = None
self._current_thread = None
self._current_syntax = ""
self._threads = []
self._threads.clear()
self._sources = {}
self._requesting_threads = StackTraceView.ThreadRequestState.NO
self._pending_thread_request = None
if self._next_sign_id:
signs.UnplaceSign( self._next_sign_id, 'VimspectorStackTrace' )
self._next_sign_id = 0
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()
@ -86,47 +173,88 @@ class StackTraceView( object ):
utils.CleanUpHiddenBuffer( self._buf )
for b in self._scratch_buffers:
utils.CleanUpHiddenBuffer( b )
self._scratch_buffers = []
self._buf = None
def LoadThreads( self, infer_current_frame ):
pending_request = False
if self._requesting_threads:
pending_request = True
def LoadThreads( self,
infer_current_frame,
reason = '',
stopEvent = None ):
if self._requesting_threads != StackTraceView.ThreadRequestState.NO:
self._requesting_threads = StackTraceView.ThreadRequestState.PENDING
self._pending_thread_request = ( infer_current_frame,
reason,
stopEvent )
return
def consume_threads( message ):
self._requesting_threads = False
if self._requesting_threads == StackTraceView.ThreadRequestState.PENDING:
# We may have hit a thread event, so try again.
self._requesting_threads = StackTraceView.ThreadRequestState.NO
self.LoadThreads( *self._pending_thread_request )
return
if not message[ 'body' ][ 'threads' ]:
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._requesting_threads = StackTraceView.ThreadRequestState.NO
self._pending_thread_request = None
if not ( message.get( 'body' ) or {} ).get( 'threads' ):
# This is a protocol error. It is required to return at least one!
utils.UserMessage( 'Protocol error: Server returned no threads',
persist = False,
error = True )
return
existing_threads = self._threads[ : ]
self._threads.clear()
for thread in message[ 'body' ][ 'threads' ]:
if stopEvent is not None:
stoppedThreadId = stopEvent.get( 'threadId' )
allThreadsStopped = stopEvent.get( 'allThreadsStopped', False )
requesting = False
# FIXME: This is horribly inefficient
for t in message[ 'body' ][ 'threads' ]:
thread = None
for existing_thread in existing_threads:
if existing_thread.id == t[ 'id' ]:
thread = existing_thread
thread.Update( t )
break
if not thread:
thread = Thread( t )
self._threads.append( thread )
if infer_current_frame and thread[ 'id' ] == self._current_thread:
self._LoadStackTrace( thread, True )
elif infer_current_frame and self._current_thread is None:
self._current_thread = thread[ 'id' ]
self._LoadStackTrace( thread, True )
# If the threads were requested due to a stopped event, update any
# stopped thread state. Note we have to do this here (rather than in the
# stopped event handler) because we must apply this event to any new
# threads that are received here.
if stopEvent:
if allThreadsStopped:
thread.Paused( stopEvent )
elif stoppedThreadId is not None and thread.id == stoppedThreadId:
thread.Paused( stopEvent )
self._DrawThreads()
if infer_current_frame:
if thread.id == self._current_thread:
self._LoadStackTrace( thread, True, reason )
requesting = True
elif self._current_thread is None:
self._current_thread = thread.id
self._LoadStackTrace( thread, True, reason )
requesting = True
if not requesting:
self._DrawThreads()
def failure_handler( reason, msg ):
# Make sure we request them again if the request fails
self._requesting_threads = False
self._requesting_threads = StackTraceView.ThreadRequestState.NO
self._pending_thread_request = None
self._requesting_threads = True
self._requesting_threads = StackTraceView.ThreadRequestState.REQUESTING
self._connection.DoRequest( consume_threads, {
'command': 'threads',
}, failure_handler )
@ -135,28 +263,41 @@ class StackTraceView( object ):
self._line_to_frame.clear()
self._line_to_thread.clear()
if self._next_sign_id:
signs.UnplaceSign( self._next_sign_id, 'VimspectorStackTrace' )
else:
self._next_sign_id = 1
with utils.ModifiableScratchBuffer( self._buf ):
utils.ClearBuffer( self._buf )
with utils.RestoreCursorPosition():
utils.ClearBuffer( self._buf )
for thread in self._threads:
icon = '+' if '_frames' not in thread else '-'
for thread in self._threads:
icon = '+' if not thread.IsExpanded() else '-'
line = utils.AppendToBuffer(
self._buf,
f'{icon} Thread {thread.id}: {thread.thread["name"]} '
f'({thread.State()})' )
line = utils.AppendToBuffer(
self._buf,
'{0} Thread: {1}'.format( icon, thread[ 'name' ] ) )
if self._current_thread == thread.id:
signs.PlaceSign( self._next_sign_id,
'VimspectorStackTrace',
'vimspectorCurrentThread',
self._buf.name,
line )
self._line_to_thread[ line ] = thread
self._DrawStackTrace( thread )
self._line_to_thread[ line ] = thread
self._DrawStackTrace( thread )
def _LoadStackTrace( self,
thread,
thread: Thread,
infer_current_frame,
reason = '' ):
def consume_stacktrace( message ):
thread[ '_frames' ] = message[ 'body' ][ 'stackFrames' ]
thread.Expand( message[ 'body' ][ 'stackFrames' ] )
if infer_current_frame:
for frame in thread[ '_frames' ]:
for frame in thread.stacktrace:
if self._JumpToFrame( frame, reason ):
break
@ -165,30 +306,62 @@ class StackTraceView( object ):
self._connection.DoRequest( consume_stacktrace, {
'command': 'stackTrace',
'arguments': {
'threadId': thread[ 'id' ],
'threadId': thread.id,
}
} )
def ExpandFrameOrThread( self ):
def _GetSelectedThread( self ) -> Thread:
if vim.current.buffer != self._buf:
return None
return self._line_to_thread.get( vim.current.window.cursor[ 0 ] )
def GetSelectedThreadId( self ):
thread = self._GetSelectedThread()
return thread.id if thread else thread
def _SetCurrentThread( self, thread: Thread ):
self._current_thread = thread.id
self._DrawThreads()
def SetCurrentThread( self ):
thread = self._GetSelectedThread()
if thread:
self._SetCurrentThread( thread )
elif vim.current.buffer != self._buf:
return
elif vim.current.window.cursor[ 0 ] in self._line_to_frame:
thread, frame = self._line_to_frame[ vim.current.window.cursor[ 0 ] ]
self._SetCurrentThread( thread )
self._JumpToFrame( frame )
else:
utils.UserMessage( "No thread selected" )
current_line = vim.current.window.cursor[ 0 ]
def ExpandFrameOrThread( self ):
thread = self._GetSelectedThread()
if current_line in self._line_to_frame:
self._JumpToFrame( self._line_to_frame[ current_line ] )
elif current_line in self._line_to_thread:
thread = self._line_to_thread[ current_line ]
if '_frames' in thread:
del thread[ '_frames' ]
with utils.RestoreCursorPosition():
self._DrawThreads()
else:
if thread:
if thread.IsExpanded():
thread.Collapse()
self._DrawThreads()
elif thread.CanExpand():
self._LoadStackTrace( thread, False )
else:
utils.UserMessage( "Thread is not stopped" )
elif vim.current.buffer != self._buf:
return
elif vim.current.window.cursor[ 0 ] in self._line_to_frame:
thread, frame = self._line_to_frame[ vim.current.window.cursor[ 0 ] ]
self._JumpToFrame( frame )
def _JumpToFrame( self, frame, reason = '' ):
def do_jump():
if 'line' in frame and frame[ 'line' ] > 0:
# Should this set the current _Thread_ too ? If i jump to a frame in
# Thread 2, should that become the focussed thread ?
self._current_frame = frame
return self._session.SetCurrentFrame( self._current_frame, reason )
return False
@ -205,59 +378,85 @@ class StackTraceView( object ):
else:
return do_jump()
def PauseContinueThread( self ):
thread = self._GetSelectedThread()
if thread is None:
utils.UserMessage( 'No thread selected' )
elif thread.state == Thread.PAUSED:
self._session._connection.DoRequest(
lambda msg: self.OnContinued( {
'threadId': thread.id,
'allThreadsContinued': ( msg.get( 'body' ) or {} ).get(
'allThreadsContinued',
True )
} ),
{
'command': 'continue',
'arguments': {
'threadId': thread.id,
},
} )
elif thread.state == Thread.RUNNING:
self._session._connection.DoRequest( None, {
'command': 'pause',
'arguments': {
'threadId': thread.id,
},
} )
else:
utils.UserMessage(
f'Thread cannot be modified in state {thread.State()}' )
def OnContinued( self, event = None ):
threadId = None
allThreadsContinued = True
if event is not None:
threadId = event[ 'threadId' ]
allThreadsContinued = event.get( 'allThreadsContinued', False )
for thread in self._threads:
if allThreadsContinued:
thread.Continued()
elif thread.id == threadId:
thread.Continued()
break
self._DrawThreads()
def OnStopped( self, event ):
if 'threadId' in event:
self._current_thread = event[ 'threadId' ]
elif event.get( 'allThreadsStopped', False ) and self._threads:
self._current_thread = self._threads[ 0 ][ 'id' ]
threadId = event.get( 'threadId' )
allThreadsStopped = event.get( 'allThreadsStopped', False )
if self._current_thread is not None:
for thread in self._threads:
if thread[ 'id' ] == self._current_thread:
self._LoadStackTrace( thread, True, 'stopped' )
return
# Work out if we should change the current thread
if threadId is not None:
self._current_thread = threadId
elif self._current_thread is None and allThreadsStopped and self._threads:
self._current_thread = self._threads[ 0 ].id
self.LoadThreads( True )
self.LoadThreads( True, 'stopped', event )
def OnThreadEvent( self, event ):
infer_current_frame = False
if event[ 'reason' ] == 'started' and self._current_thread is None:
self._current_thread = event[ 'threadId' ]
self.LoadThreads( True )
infer_current_frame = True
def Continue( self ):
if self._current_thread is None:
utils.UserMessage( 'No current thread', persist = True )
if event[ 'reason' ] == 'exited':
for thread in self._threads:
if thread.id == event[ 'threadId' ]:
thread.Exited()
break
self.LoadThreads( infer_current_frame )
def _DrawStackTrace( self, thread: Thread ):
if not thread.IsExpanded():
return
self._session._connection.DoRequest( None, {
'command': 'continue',
'arguments': {
'threadId': self._current_thread,
},
} )
self._session.ClearCurrentFrame()
self.LoadThreads( True )
def Pause( self ):
if self._current_thread is None:
utils.UserMessage( 'No current thread', persist = True )
return
self._session._connection.DoRequest( None, {
'command': 'pause',
'arguments': {
'threadId': self._current_thread,
},
} )
def _DrawStackTrace( self, thread ):
if '_frames' not in thread:
return
stackFrames = thread[ '_frames' ]
for frame in stackFrames:
for frame in thread.stacktrace:
if frame.get( 'source' ):
source = frame[ 'source' ]
else:
@ -281,7 +480,7 @@ class StackTraceView( object ):
source[ 'name' ],
frame[ 'line' ] ) )
self._line_to_frame[ line ] = frame
self._line_to_frame[ line ] = ( thread, frame )
def _ResolveSource( self, source, and_then ):
source_reference = int( source[ 'sourceReference' ] )

View file

@ -3,7 +3,7 @@ var msg = 'Hello, world!'
var obj = {
test: 'testing',
toast: function() {
return 'toasty' . this.test;
return 'toasty' + this.test;
}
}

View file

@ -1,5 +1,6 @@
function! SetUp()
call vimspector#test#setup#SetUpWithMappings( v:none )
call ThisTestIsFlaky()
endfunction
function! ClearDown()
@ -684,6 +685,7 @@ function! Test_Custom_Breakpoint_Priority_Partial()
endfunction
function! Test_Add_Line_BP_In_Other_File_While_Debugging()
call ThisTestIsFlaky()
let moo = 'moo.py'
let cow = 'cow.py'
lcd ../support/test/python/multiple_files

View file

@ -1,6 +1,7 @@
function! SetUp()
set ambiwidth=double
call vimspector#test#setup#SetUpWithMappings( v:none )
call ThisTestIsFlaky()
endfunction
function! ClearDown()
@ -695,6 +696,7 @@ endfunction
function! Test_Add_Line_BP_In_Other_File_While_Debugging()
call ThisTestIsFlaky()
let moo = 'moo.py'
let cow = 'cow.py'
lcd ../support/test/python/multiple_files

View file

@ -4,13 +4,19 @@ ENV DEBIAN_FRONTEND=noninteractive
ENV LC_ALL C.UTF-8
RUN apt-get update && \
apt install -y curl dirmngr apt-transport-https lsb-release ca-certificates \
software-properties-common && \
apt-get install -y curl \
dirmngr \
apt-transport-https \
lsb-release \
ca-certificates \
software-properties-common && \
curl -sL https://deb.nodesource.com/setup_12.x | bash - && \
add-apt-repository ppa:bartbes/love-stable -y && \
apt-get update && \
apt-get -y dist-upgrade && \
apt-get -y install python3-dev \
apt-get -y install gcc-8 \
g++-8 \
python3-dev \
python3-pip \
python3-venv \
ca-cacert \
@ -31,6 +37,9 @@ RUN apt-get update && \
RUN ln -fs /usr/share/zoneinfo/Europe/London /etc/localtime && \
dpkg-reconfigure --frontend noninteractive tzdata
RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-8 1 \
--slave /usr/bin/g++ g++ /usr/bin/g++-8
## cleanup of files from setup
RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

View file

@ -51,7 +51,10 @@ func s:WaitForCommon(expr, assert, timeout)
let start = reltime()
endif
let iters = 0
while 1
let iters += 1
let errors_before = len( v:errors )
if type(a:expr) == v:t_func
let success = a:expr()
@ -65,6 +68,10 @@ func s:WaitForCommon(expr, assert, timeout)
return slept
endif
if iters % 20 == 0
redraw!
endif
if slept >= a:timeout
break
endif
@ -91,3 +98,31 @@ endfunc
function! ThisTestIsFlaky()
let g:test_is_flaky = v:true
endfunction
function! AssertMatchist( expected, actual ) abort
let ret = assert_equal( len( a:expected ), len( a:actual ) )
let len = min( [ len( a:expected ), len( a:actual ) ] )
let idx = 0
while idx < len
let ret += assert_match( a:expected[ idx ], a:actual[ idx ] )
let idx += 1
endwhile
return ret
endfunction
function! GetBufLine( buf, start, end = '$' )
if type( a:start ) != v:t_string && a:start < 0
let start = getbufinfo( a:buf )[ 0 ].linecount + a:start
else
let start = a:start
endif
if type( a:end ) != v:t_string && a:end < 0
let end = getbufinfo( a:buf )[ 0 ].linecount + a:end
else
let end = a:end
endif
return getbufline( a:buf, start, end )
endfunction

358
tests/stack_trace.test.vim Normal file
View file

@ -0,0 +1,358 @@
let s:fn='testdata/cpp/simple/threads.cpp'
function! SetUp()
call vimspector#test#setup#SetUpWithMappings( 'HUMAN' )
endfunction
function! ClearDown()
call vimspector#test#setup#ClearDown()
endfunction
function! s:StartDebugging()
exe 'edit ' . s:fn
call vimspector#SetLineBreakpoint( s:fn, 15 )
call vimspector#LaunchWithSettings( #{ configuration: 'run-to-breakpoint' } )
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, 15, 1 )
endfunction
function! Test_Multiple_Threads_Continue()
let thread_l = 67
let notify_l = 74
call vimspector#SetLineBreakpoint( s:fn, thread_l )
call vimspector#SetLineBreakpoint( s:fn, notify_l )
call s:StartDebugging()
call vimspector#Continue()
" As we step through the thread creation we should get Thread events
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 )
call cursor( 1, 1 )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '- Thread [0-9]\+: .* (paused)',
\ ' .*: .*@threads.cpp:' . string( thread_l )
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ 1,
\ 2 )
\ )
\ } )
call vimspector#Continue()
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 )
call cursor( 1, 1 )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '- Thread [0-9]\+: .* (paused)',
\ ' .*: .*@threads.cpp:' . string( thread_l )
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ 1,
\ 2 )
\ )
\ } )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '+ Thread [0-9]\+: .* (paused)',
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ '$',
\ '$' )
\ )
\ } )
call vimspector#Continue()
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 )
call cursor( 1, 1 )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '- Thread [0-9]\+: .* (paused)',
\ ' .*: .*@threads.cpp:' . string( thread_l )
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ 1,
\ 2 )
\ )
\ } )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '+ Thread [0-9]\+: .* (paused)',
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ '$',
\ '$' )
\ )
\ } )
call vimspector#Continue()
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 )
call cursor( 1, 1 )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '- Thread [0-9]\+: .* (paused)',
\ ' .*: .*@threads.cpp:' . string( thread_l )
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ 1,
\ 2 )
\ )
\ } )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '+ Thread [0-9]\+: .* (paused)',
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ '$',
\ '$' )
\ )
\ } )
call vimspector#Continue()
" This is the last one
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 )
call cursor( 1, 1 )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '- Thread [0-9]\+: .* (paused)',
\ ' .*: .*@threads.cpp:' . string( thread_l )
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ 1,
\ 2 )
\ )
\ } )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '+ Thread [0-9]\+: .* (paused)',
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ '$',
\ '$' )
\ )
\ } )
call vimspector#Continue()
" So we break out of the loop
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, notify_l, 1 )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '- Thread [0-9]\+: .* (paused)',
\ ' .*: .*@threads.cpp:' . string( notify_l )
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ 1,
\ 2 )
\ )
\ } )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '+ Thread [0-9]\+: .* (paused)',
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ '$',
\ '$' )
\ )
\ } )
call vimspector#ClearBreakpoints()
call vimspector#test#setup#Reset()
%bwipe!
endfunction
function! Test_Multiple_Threads_Step()
let thread_l = 67
if $VIMSPECTOR_MIMODE ==# 'lldb'
" }
let thread_n = thread_l + 1
else
" for ....
let thread_n = 49
endif
let notify_l = 74
call vimspector#SetLineBreakpoint( s:fn, thread_l )
call vimspector#SetLineBreakpoint( s:fn, notify_l )
call s:StartDebugging()
call vimspector#Continue()
" As we step through the thread creation we should get Thread events
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '- Thread [0-9]\+: .* (paused)',
\ ' .*: .*@threads.cpp:' . string( thread_l )
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ 1,
\ 2 )
\ )
\ } )
call vimspector#StepOver()
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_n, 1 )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '+ Thread [0-9]\+: .* (paused)',
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ '$',
\ '$' )
\ )
\ } )
call vimspector#Continue()
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '+ Thread [0-9]\+: .* (paused)',
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ '$',
\ '$' )
\ )
\ } )
call vimspector#StepOver()
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_n, 1 )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ -1,
\ '$' )
\ )
\ } )
call vimspector#Continue()
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ -1,
\ '$' )
\ )
\ } )
call vimspector#StepOver()
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_n, 1 )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ -2,
\ '$' )
\ )
\ } )
call vimspector#Continue()
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ -2,
\ '$' )
\ )
\ } )
call vimspector#StepOver()
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_n, 1 )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ -3,
\ '$' )
\ )
\ } )
call vimspector#Continue()
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_l, 1 )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ -3,
\ '$' )
\ )
\ } )
call vimspector#StepOver()
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, thread_n, 1 )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ -4,
\ '$' )
\ )
\ } )
call vimspector#Continue()
" So we break out of the loop
call vimspector#test#signs#AssertCursorIsAtLineInBuffer( s:fn, notify_l, 1 )
call WaitForAssert( {->
\ AssertMatchist(
\ [
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ '+ Thread [0-9]\+: .* (paused)',
\ ],
\ GetBufLine( winbufnr( g:vimspector_session_windows.stack_trace ),
\ -4,
\ '$' )
\ )
\ } )
call vimspector#ClearBreakpoints()
call vimspector#test#setup#Reset()
%bwipe!
endfunction

View file

@ -2,3 +2,4 @@ simple
variables
struct
printer
threads

View file

@ -2,6 +2,7 @@ def Settings( **kwargs ):
return {
'flags': [
'-x', 'c++',
'-std=c++17',
'-Wextra', '-Werror', '-Wall'
]
}

View file

@ -2,7 +2,8 @@ CXXFLAGS=-g -O0 -std=c++17
.PHONY: all
TARGETS=simple variables struct printer
TARGETS=simple variables struct printer threads
LDLIBS=-lpthread
all: $(TARGETS)

82
tests/testdata/cpp/simple/threads.cpp vendored Normal file
View file

@ -0,0 +1,82 @@
#include <chrono>
#include <condition_variable>
#include <cstdlib>
#include <iostream>
#include <mutex>
#include <string_view>
#include <system_error>
#include <thread>
#include <vector>
#include <charconv>
#include <random>
int main( int argc, char ** argv )
{
int numThreads = {};
if ( argc < 2 )
{
numThreads = 5;
}
else
{
std::string_view numThreadArg( argv[ 1 ] );
if ( auto [ p, ec ] = std::from_chars( numThreadArg.begin(),
numThreadArg.end(),
numThreads );
ec != std::errc() )
{
std::cerr << "Usage " << argv[ 0 ] << " <number of threads>\n";
return 2;
}
}
std::cout << "Creating " << numThreads << " threads" << '\n';
std::vector<std::thread> threads{};
threads.reserve( numThreads );
auto eng = std::default_random_engine() ;
auto dist = std::uniform_int_distribution<int>( 250, 1000 );
std::mutex m;
std::condition_variable v;
bool ready = false;
{
std::lock_guard l(m);
std::cout << "Preparing..." << '\n';
for ( int i = 0; i < numThreads; ++i )
{
using namespace std::chrono_literals;
auto tp = [&,tnum=i]() {
// Wait for the go-ahead
{
std::unique_lock l(m);
while (!ready) {
v.wait(l);
}
}
std::cout << "Started thread " << tnum << '\n';
std::this_thread::sleep_for(
5s + std::chrono::milliseconds( dist( eng ) ) );
std::cout << "Completed thread " << tnum << '\n';
};
threads.emplace_back( tp );
}
std::cout << "Ready to go!" << '\n';
ready = true;
}
v.notify_all();
for ( int i = 0; i < numThreads; ++i )
{
threads[ i ].join();
}
return 0;
}

View file

@ -8,17 +8,6 @@ function! ClearDown()
call vimspector#test#setup#ClearDown()
endfunction
function! s:assert_match_list( expected, actual ) abort
let ret = assert_equal( len( a:expected ), len( a:actual ) )
let len = min( [ len( a:expected ), len( a:actual ) ] )
let idx = 0
while idx < len
let ret += assert_match( a:expected[ idx ], a:actual[ idx ] )
let idx += 1
endwhile
return ret
endfunction
function! s:StartDebugging( ... )
if a:0 == 0
let config = #{
@ -222,7 +211,7 @@ function! Test_ExpandVariables()
call feedkeys( "\<CR>", 'xt' )
call WaitForAssert( {->
\ s:assert_match_list(
\ AssertMatchist(
\ [
\ '- Scope: Locals',
\ ' \*- t (Test): {...}',
@ -240,7 +229,7 @@ function! Test_ExpandVariables()
" Step - stays expanded
call vimspector#StepOver()
call WaitForAssert( {->
\ s:assert_match_list(
\ AssertMatchist(
\ [
\ '- Scope: Locals',
\ ' - t (Test): {...}',
@ -289,7 +278,7 @@ function! Test_ExpandVariables()
call setpos( '.', [ 0, 2, 1 ] )
call feedkeys( "\<CR>", 'xt' )
call WaitForAssert( {->
\ s:assert_match_list(
\ AssertMatchist(
\ [
\ '- Scope: Locals',
\ ' - t (Test): {...}',
@ -389,7 +378,7 @@ function! Test_ExpandWatch()
call feedkeys( "\<CR>", 'xt' )
call WaitForAssert( {->
\ s:assert_match_list(
\ AssertMatchist(
\ [
\ 'Watches: ----',
\ 'Expression: t',
@ -408,7 +397,7 @@ function! Test_ExpandWatch()
" Step - stays expanded
call vimspector#StepOver()
call WaitForAssert( {->
\ s:assert_match_list(
\ AssertMatchist(
\ [
\ 'Watches: ----',
\ 'Expression: t',
@ -460,7 +449,7 @@ function! Test_ExpandWatch()
call setpos( '.', [ 0, 3, 1 ] )
call feedkeys( "\<CR>", 'xt' )
call WaitForAssert( {->
\ s:assert_match_list(
\ AssertMatchist(
\ [
\ 'Watches: ----',
\ 'Expression: t',