From d0f5158b0a6c7df81951717b8005e188b4dba87d Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 18 Apr 2023 13:10:06 +0100 Subject: [PATCH 1/4] Support multiple independent root level sessions (tabs) This is a bit clunky but works, even for 2 root level sessions which themselves have child sessions. You have the following facilities: * VimspectorNewSession This creates a new session and makes it active. Optional name is used in place of the generated one when starting a launch. * Switching to a specific debug tab makes that session active. This is intuitive and probably the most common way to work with this. * Switching manually using VimspectorSwitchToSession . * Name/Rename session with VimspectorRenameSession * Root-level sessions are never 'destroyed' but you can manually destroy them (if you're brave) using VimspectorDestroySession . You can't destroy a running/active session. * vimspector#GetSessionName() useful for putting in a statusline. I used this: ``` function! BenGetCustomInfo() if !empty( &buftype ) return '' endif if !exists( '*vimspector#GetSessionName' ) return '' endif return vimspector#GetSessionName() endfunction " ... statusline stuff " Custom info set statusline+=%* set statusline+=%(\ %.20{BenGetCustomInfo()}\ %) ``` TODO: The tests will fail due to the change in name of the session buffers. --- README.md | 115 ++++++++++++++++++++++-- autoload/vimspector.vim | 73 ++++++++++++++- autoload/vimspector/internal/neojob.vim | 4 +- autoload/vimspector/internal/state.vim | 80 +++++++++++------ plugin/vimspector.vim | 14 +++ python3/vimspector/breakpoints.py | 29 ++++-- python3/vimspector/debug_session.py | 88 +++++++++++------- python3/vimspector/output.py | 2 +- python3/vimspector/session_manager.py | 66 +++++++++++++- python3/vimspector/utils.py | 8 +- tests/breakpoints.test.vim | 7 +- tests/tabpage.test.vim | 8 -- tests/variables.test.vim | 10 +-- tests/variables_compact.test.vim | 10 +-- 14 files changed, 396 insertions(+), 118 deletions(-) diff --git a/README.md b/README.md index 8534fcdaa..528f5d273 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ And a couple of brief demos: - logging/stdout display - simple stable API for custom tooling (e.g. integrate with language server) - view hex dump of process memory +- multiple independent debugging sessions (debug different apps in tabs) - multi-process (multi-session) debugging ## Supported languages @@ -569,9 +570,15 @@ Vimspector is a vim UI on top of the Debug Adapter Protocol. It's intended to be Vimspector is not: * a debugger! It's just the UI and some glue. -* fast. It's abstractions all the way down. If you want a fast, native debugger, there are other options. -* comprehensive. It's limited by DAP, and limited by my time. I implement the features I think most users will need, not every feature possible. -* for everyone. Vimspector intentionally provides a "one size fits all" UI and aproach. This means that it can only provide essential/basic debugging features for a given language. This makes it convenient for everyday usage, but not ideal for power users or those with very precise or specific requirements. See [motivation](#motivation) for more info. +* fast. It's abstractions all the way down. If you want a fast, native debugger, + there are other options. +* comprehensive. It's limited by DAP, and limited by my time. I implement the + features I think most users will need, not every feature possible. +* for everyone. Vimspector intentionally provides a "one size fits all" UI and + aproach. This means that it can only provide essential/basic debugging + features for a given language. This makes it convenient for everyday usage, + but not ideal for power users or those with very precise or specific + requirements. See [motivation](#motivation) for more info. ## Status @@ -781,6 +788,10 @@ users, the [mappings](#mappings) section contains the most common commands and default usage. This section can be used as a reference to create your own mappings or custom behaviours. +All the below instructions assume a single debugging session. For deatils on how +to debug multiple independent apps at the same time, see +[multiple debugging sessions][#multiple-debugging-sessions]. + ## Launch and attach by PID: * Create `.vimspector.json`. See [below](#supported-languages). @@ -788,6 +799,9 @@ mappings or custom behaviours. ![debug session](https://puremourning.github.io/vimspector-web/img/vimspector-overview.png) +Launching a new session makes it the active +[debugging session][#multiple-debugging-sessions]. + ### Launch with options To launch a specific debug configuration, or specify [replacement @@ -873,6 +887,12 @@ For example, to get an array of configurations and fuzzy matching on the result See the [mappings](#mappings) section for the default mappings for working with breakpoints. This section describes the full API in vimscript functions. +Breakpoints are associated with the current +[debugging session][#multiple-debugging-sessions]. When switching between +sessions, the breakpont signs for the previous session are removed and the +breakpoints for the newly activated session are displayed. While it might be +useful to see breakpoints for all sessions, this can be very confusing. + ### Breakpoints Window Use `:VimspectorBreakpoints` or map something to `VimspectorBreakpoints` @@ -1253,10 +1273,10 @@ be changed manually to "switch to" that thread. The stack trace is represented by the buffer `vimspector.StackTrace`. -### Multiple sessions +### Child sessions -If there are multiple concurrent debug sessions, such as where the debugee -launches multiple child processes and the debug adapter supports multi-session +If there are child debug sessions, such as where the debugee +launches child processes and the debug adapter supports multi-session debugging, then each session's threads are shown separately. The currently active session is the one that is highlighted as the currently active thread/stack frame. To switch control to a different session, focus a thread @@ -1264,6 +1284,10 @@ within that session. ![multiple sessions](https://user-images.githubusercontent.com/10584846/232473234-666d1a77-81f2-40d5-bc65-ebab774888ce.png) +Note: This refers to sessions created as children of an existing session, and is +not to be confused with +[multiple (parent) debugging sessions][#multiple-debugging-sessions]. + ## Program Output * In the outputs window, use the WinBar to select the output channel. @@ -1348,6 +1372,78 @@ choice as to whether or not to terminate the debuggee, you will be prompted to choose. The same applies for `vimspector#Stop()` which can take an argument: `vimspector#Stop( { 'interactive': v:true } )`. +# Multiple debugging sessions + +**NOTE**: This feature is _experimental_ and any part of it may change in +response to user feedback. + +Vimspector supports starting an arbitrary number of debug sessions. Each session +is associated with an individual UI tab. Typically, you only debug a single app +and so don't need to think about this, but this advanced feature can be useful +if you need to simultaneously debug multiple, independent applications, or +multiple independent instances of your application. + +At any time there is a single "active" root session. Breakpoints are associated +with the current session, and all UI and API commands are applied to the +currently active session. + +When switching between root sessions, the breakpont signs for the previous +session are removed and the breakpoints for the newly activated session are +displayed. While it might be useful to see breakpoints for all sessions, this +can be very confusing. + +A typical workflow might be: + +1. Start debugging a server app (e.g. `:edit server.cc` then ``). This + starts a debug session named after the configuration selected. You could + rename it `:VimspectorRenameSession server`. +2. Open the client code in a new tab (e.g. `:tabedit client.cc`) +3. Instantiate and make active a new debugging session and name it `client`: + `:VimspectorNewSession client` (`client` is now the active session). +4. Add a breakpoint in the `client` session and start debugging with ``. + +You now have 2 vimspector tabs. Intuitively, wwitching to a particular tab will +make its session active. You can also manually switch the active session with +`:VimspectorSwitchToSession `. + +So, in summary you have the following facilities: + +* `VimspectorNewSession ` + This creates a new session and makes it active. Optional name is used + in place of the generated one when starting a launch. +* Switching to a specific debug tab makes that session active. This is + intuitive and probably the most common way to work with this. +* Switching manually using `VimspectorSwitchToSession `. +* Name/Rename session with `VimspectorRenameSession ` +* Root-level sessions are never 'destroyed' but you can manually destroy + them (if you're brave) using `VimspectorDestroySession `. You + can't destroy a running/active session. +* `vimspector#GetSessionName()` useful for putting in a statusline. + +Here's an example of how you can display the current session name in the +`statusline` (see `:help statusline`, or the documentation for your fancy status +line plugin). + +```viml +function! StlVimspectorSession() + " Only include in buffers containing actual files + if !empty( &buftype ) + return '' + endif + + " Abort if vimspector not loaded + if !exists( '*vimspector#GetSessionName' ) + return '' + endif + + return vimspector#GetSessionName() +endfunction + +" ... existing statusline stuff +" set statusline=... +" Show the vimspector active session name (max 20 chars) if there is onw. +set statusline+=%(\ %.20{StlVimspectorSession()}\ %) +``` # Debug profile configuration @@ -1905,7 +2001,8 @@ curl "http://localhost?XDEBUG_SESSION_START=xdebug" or use the previously mentioned Xdebug Helper extension (which sets a `XDEBUG_SESSION` cookie) ### Debug cli application -``` + +```sh export XDEBUG_CONFIG="idekey=xdebug" php ``` @@ -1919,7 +2016,7 @@ Requires: * `install_gadget.py --force-enable-node` * Options described here: https://github.com/microsoft/vscode-js-debug/blob/main/OPTIONS.md -* Example: `support/test/node/simple`, `support/test/node/multiprocess' +* Example: `support/test/node/simple`, `support/test/node/multiprocess` ```json { @@ -2225,7 +2322,7 @@ define them in your `vimrc`. | `vimspectorBPDisabled` | Disabled breakpoint | 9 | | `vimspectorPC` | Program counter (i.e. current line) | 200 | | `vimspectorPCBP` | Program counter and breakpoint | 200 | -| `vimspectorNonActivePC`` | Program counter for non-focused thread | 9 | +| `vimspectorNonActivePC` | Program counter for non-focused thread | 9 | | `vimspectorCurrentThread` | Focussed thread in stack trace view | 200 | | `vimspectorCurrentFrame` | Current stack frame in stack trace view | 200 | diff --git a/autoload/vimspector.vim b/autoload/vimspector.vim index 7aea24a22..0dd8360b9 100644 --- a/autoload/vimspector.vim +++ b/autoload/vimspector.vim @@ -45,9 +45,78 @@ function! s:Enabled() abort let s:enabled = vimspector#internal#state#Reset() endif + if s:enabled && py3eval( '_vimspector_session is None' ) + " We have no active session, so create one + call vimspector#internal#state#NewSession( {} ) + endif + return s:enabled endfunction +function! vimspector#NewSession( ... ) abort + if !s:Enabled() + return + endif + + let options = {} + if a:0 > 0 + call extend( options, { 'session_name': a:1 } ) + endif + + call vimspector#internal#state#NewSession( options ) +endfunction + +function! vimspector#SwitchToSession( name ) abort + if !s:Enabled() + return + endif + + py3 << EOF +s = _vimspector_session_man.FindSessionByName( vim.eval( 'a:name' ) ) +if s is not None: + _VimspectorSwitchTo( s ) +EOF +endfunction + +function! vimspector#DestroySession( name ) abort + if !s:Enabled() + return + endif + + py3 << EOF + +s = _vimspector_session_man.FindSessionByName( vim.eval( 'a:name' ) ) +if s is not None: + s = _vimspector_session_man.DestroyRootSession( s, _vimspector_session ) + _VimspectorMakeActive( s ) + +EOF +endfunction + +function! vimspector#CompleteSessionName( ArgLead, CmdLine, CursorPos ) abort + " Don't call s:Enabled() because we don't want this function to initialise a + " new session + if !s:Initialised() || !s:enabled || py3eval( '_vimspector_session is None' ) + return '' + endif + return py3eval( '"\n".join( _vimspector_session_man.GetSessionNames() )' ) +endfunction + +function! vimspector#GetSessionName() abort + if !s:Initialised() || !s:enabled || py3eval( '_vimspector_session is None' ) + return '' + endif + + return py3eval( '_vimspector_session.Name()' ) +endfunction + +function! vimspector#RenameSession( name ) abort + if !s:Enabled() + return + endif + py3 _vimspector_session.name = vim.eval( 'a:name' ) +endfunction + function! vimspector#Launch( ... ) abort if !s:Enabled() return @@ -548,7 +617,7 @@ endfunction function! vimspector#CompleteOutput( ArgLead, CmdLine, CursorPos ) abort if !s:Enabled() - return + return '' endif let buffers = py3eval( '_vimspector_session.GetOutputBuffers() ' \ . ' if _vimspector_session else []' ) @@ -557,7 +626,7 @@ endfunction function! vimspector#CompleteExpr( ArgLead, CmdLine, CursorPos ) abort if !s:Enabled() - return + return '' endif let col = len( a:ArgLead ) diff --git a/autoload/vimspector/internal/neojob.vim b/autoload/vimspector/internal/neojob.vim index 44865a89c..bca154a9e 100644 --- a/autoload/vimspector/internal/neojob.vim +++ b/autoload/vimspector/internal/neojob.vim @@ -41,7 +41,6 @@ function! s:_OnEvent( session_id, chan_id, data, event ) abort py3 _VimspectorSession( vim.eval( 'a:session_id' ) ).OnServerStderr( \ '\n'.join( vim.eval( 'a:data' ) ) ) elseif a:event ==# 'exit' - echom 'Channel exit with status ' . a:data redraw unlet s:jobs[ a:session_id ] py3 _VimspectorSession( vim.eval( 'a:session_id' ) ).OnServerExit( @@ -53,7 +52,7 @@ function! vimspector#internal#neojob#StartDebugSession( \ session_id, \ config ) abort if has_key( s:jobs, a:session_id ) - echom 'Not starging: Job is already running' + echom 'Not starting: Job is already running' redraw return v:false endif @@ -112,7 +111,6 @@ function! vimspector#internal#neojob#StopDebugSession( session_id ) abort endif if vimspector#internal#neojob#JobIsRunning( s:jobs[ a:session_id ] ) - echom 'Terminating job' redraw call jobstop( s:jobs[ a:session_id ] ) endif diff --git a/autoload/vimspector/internal/state.vim b/autoload/vimspector/internal/state.vim index f79c82316..b0e8099b9 100644 --- a/autoload/vimspector/internal/state.vim +++ b/autoload/vimspector/internal/state.vim @@ -32,16 +32,42 @@ function! vimspector#internal#state#Reset() abort import vim +def _VimspectorMakeActive( session ): + global _vimspector_session + + if '_vimspector_session' not in globals() or _vimspector_session is None: + _vimspector_session = session + return True + + if _vimspector_session == session: + # nothing to do + return False + + if _vimspector_session is not None: + _vimspector_session.SwitchFrom() + + _vimspector_session = session + return True + +def _VimspectorSwitchTo( session ): + global _vimspector_session + + if _VimspectorMakeActive( session ) and _vimspector_session is not None: + _vimspector_session.SwitchTo() + +def _VimspectorSession( session_id ): + return _vimspector_session_man.GetSession( int( session_id ) ) + _vimspector_session_man = __import__( "vimspector", fromlist=[ "session_manager" ] ).session_manager.Get() -# Deprecated -_vimspector_session = _vimspector_session_man.NewSession( - vim.eval( 's:prefix' ) ) +_vimspector_session_man.api_prefix = vim.eval( 's:prefix' ) +if '_vimspector_session' in globals() and _vimspector_session is not None: + _vimspector_session_man.DestroyRootSession( _vimspector_session, + _vimspector_session ) +_VimspectorMakeActive( _vimspector_session_man.NewSession() ) -def _VimspectorSession( session_id ): - return _vimspector_session_man.GetSession( int( session_id ) ) EOF catch /.*/ @@ -56,6 +82,13 @@ EOF return v:true endfunction +function! vimspector#internal#state#NewSession( options ) abort + py3 << EOF +_VimspectorMakeActive( + _vimspector_session_man.NewSession( **vim.eval( 'a:options' ) ) ) +EOF +endfunction + function! vimspector#internal#state#GetAPIPrefix() abort return s:prefix endfunction @@ -63,31 +96,26 @@ endfunction function! vimspector#internal#state#TabClosed( afile ) abort py3 << EOF -# reset if: -# - a tab closed -# - the vimspector session exists -# - the vimspector session does _not_ have a UI (which suggests that it was -# probably the vimspector UI tab that was closed) -# -# noevim helpfully provides the tab number that was closed in , so we -# use that there (it also doesn't correctly invalidate tab objects: -# https://github.com/neovim/neovim/issues/16327) -# -# TODO: set a tab-local variable, or a global which maps tabs to session IDs -# This probably doesn't work now with multiple-sessions - -if '_vimspector_session' in globals() and _vimspector_session: - if int( vim.eval( 's:is_neovim' ) ) and _vimspector_session.IsUITab( - int( vim.eval( 'a:afile' ) ) ): - _vimspector_session.Reset( interactive = False ) - elif not _vimspector_session.HasUI(): - _vimspector_session.Reset( interactive = False ) +# Try to reset any session which was closed. +if '_vimspector_session_man' in globals(): + if int( vim.eval( 's:is_neovim' ) ): + # noevim helpfully provides the tab number that was closed in , so we + # use that there (it also doesn't correctly invalidate tab objects: + # https://github.com/neovim/neovim/issues/16327), so we can't use the same + # code for both. And as usual, when raising bugs against neovim, they are + # simply closed and never fixed. sigh. + s = _vimspector_session_man.FindSessionByTab( int( vim.eval( 'a:afile' ) ) ) + if s: + s.Reset( interactive = False ) + else: + for session in _vimspector_session_man.SessionsWithInvalidUI(): + session.Reset( interactive = False ) EOF endfunction function! vimspector#internal#state#SwitchToSession( id ) abort - py3 _vimspector_session = _VimspectorSession( vim.eval( 'a:id' ) ) + py3 _VimspectorSwitchTo( _VimspectorSession( vim.eval( 'a:id' ) ) ) endfunction @@ -98,7 +126,7 @@ if '_vimspector_session_man' in globals(): int( vim.eval( 'tabpagenr()' ) ) ) if session is not None: - _vimspector_session = session + _VimspectorMakeActive( session ) EOF endfunction diff --git a/plugin/vimspector.vim b/plugin/vimspector.vim index 384c2445d..900933eb3 100644 --- a/plugin/vimspector.vim +++ b/plugin/vimspector.vim @@ -148,6 +148,20 @@ elseif s:mappings ==# 'HUMAN' nmap VimspectorStepOut endif +" Session commands +command! -bar -nargs=? + \ VimspectorNewSession + \ call vimspector#NewSession( ) +command! -bar -nargs=1 -complete=custom,vimspector#CompleteSessionName + \ VimspectorSwitchToSession + \ call vimspector#SwitchToSession( ) +command! -bar -nargs=1 + \ VimspectorRenameSession + \ call vimspector#RenameSession( ) +command! -bar -nargs=1 -complete=custom,vimspector#CompleteSessionName + \ VimspectorDestroySession + \ call vimspector#DestroySession( ) + command! -bar -nargs=1 -complete=custom,vimspector#CompleteExpr \ VimspectorWatch \ call vimspector#AddWatch( ) diff --git a/python3/vimspector/breakpoints.py b/python3/vimspector/breakpoints.py index 68f268837..1b6dc3c98 100644 --- a/python3/vimspector/breakpoints.py +++ b/python3/vimspector/breakpoints.py @@ -53,9 +53,11 @@ def _JumpToBreakpoint( qfbp ): class BreakpointsView( object ): - def __init__( self ): + def __init__( self, session_id ): self._win = None self._buffer = None + self._buffer_name = utils.BufferNameForSession( 'vimspector.Breakpoints', + session_id ) self._breakpoint_list = [] def _HasWindow( self ): @@ -88,8 +90,7 @@ def _UpdateView( self, breakpoint_list, show=True ): vim.command( f'nnoremap { mapping } ' ':call ' f'vimspector#{ func }()' ) - utils.SetUpHiddenBuffer( self._buffer, - "vimspector.Breakpoints" ) + utils.SetUpHiddenBuffer( self._buffer, self._buffer_name ) self._win = vim.current.window @@ -223,7 +224,7 @@ def __init__( self, self._pending_send_breakpoints = [] - self._breakpoints_view = BreakpointsView() + self._breakpoints_view = BreakpointsView( session_id ) if not signs.SignDefined( 'vimspectorBP' ): signs.DefineSign( 'vimspectorBP', @@ -429,11 +430,7 @@ def BreakpointsAsQuickFix( self ): def ClearBreakpoints( self ): # These are the user-entered breakpoints. - for file_name, breakpoints in self._line_breakpoints.items(): - for bp in breakpoints: - self._SignToLine( file_name, bp ) - if 'sign_id' in bp: - signs.UnplaceSign( bp[ 'sign_id' ], 'VimspectorBP' ) + self._HideBreakpoints() self._line_breakpoints = defaultdict( list ) self._func_breakpoints = [] @@ -764,6 +761,11 @@ def AddFunctionBreakpoint( self, function, options ): self.UpdateUI() + def ClearUI( self ): + self._HideBreakpoints() + self._breakpoints_view.CloseBreakpoints() + + def UpdateUI( self, then = None ): def callback(): self._render_subject.emit() @@ -1115,6 +1117,15 @@ def _ShowBreakpoints( self ): file_name, line ) + def _HideBreakpoints( self ): + for file_name, breakpoints in self._line_breakpoints.items(): + for bp in breakpoints: + self._SignToLine( file_name, bp ) + if 'sign_id' in bp: + signs.UnplaceSign( bp[ 'sign_id' ], 'VimspectorBP' ) + del bp[ 'sign_id' ] + + def _SignToLine( self, file_name, bp ): if bp[ 'is_instruction_breakpoint' ]: if self._disassembly_manager and 'address' in bp: diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 325d0efc9..0fbb29d4d 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -167,10 +167,6 @@ def __init__( self, self._ResetServerState() - def __del__( self ): - self.manager.DestroySession( self ) - - def _ResetServerState( self ): self._connection = None self._init_complete = False @@ -179,6 +175,7 @@ def _ResetServerState( self ): self._server_capabilities = {} self.ClearTemporaryBreakpoints() + def GetConfigurations( self, adapters ): current_file = utils.GetBufferFilepath( vim.current.buffer ) filetypes = utils.GetBufferFiletypes( vim.current.buffer ) @@ -210,7 +207,7 @@ def GetConfigurations( self, adapters ): def Name( self ): - name = self.name if self.name else "" + name = self.name if self.name else "Unnamed" return f'{name} ({self.session_id})' @@ -531,6 +528,19 @@ def HasUI( self ): def IsUITab( self, tab_number ): return self.HasUI() and self._uiTab.number == tab_number + @ParentOnly() + def SwitchTo( self ): + if self.HasUI(): + vim.current.tabpage = self._uiTab + + self._breakpoints.UpdateUI() + + + @ParentOnly() + def SwitchFrom( self ): + self._breakpoints.ClearUI() + + def OnChannelData( self, data ): if self._connection is None: # Should _not_ happen, but maybe possible due to races or vim bufs? @@ -569,6 +579,19 @@ def Stop( self, interactive = False ): self._logger.debug( "Stop debug adapter with no callback" ) self.StopAllSessions( interactive = False ) + @ParentOnly() + def Destroy( self ): + """Call when the vimspector session will be removed and never used again""" + if self._connection is not None: + raise RuntimeError( "Can't destroy a session with a live connection" ) + + if self.HasUI(): + raise RuntimeError( "Can't destroy a session with an active UI" ) + + self.ClearBreakpoints() + self._ResetUI() + + @ParentOnly() def Reset( self, interactive = False ): # We reset all of the child sessions in turn @@ -579,41 +602,42 @@ def Reset( self, interactive = False ): def _IsPCPresentAt( self, file_path, line ): return self._codeView and self._codeView.IsPCPresentAt( file_path, line ) - def _Reset( self ): - if self.parent_session: - self._stackTraceView = None - self._variablesView = None - self._outputView = None - self._codeView = None - self._disassemblyView = None - self._remote_term = None - self._uiTab = None - self._breakpoints.RemoveConnection( self._connection ) - return - vim.vars[ 'vimspector_resetting' ] = 1 - self._logger.info( "Debugging complete." ) - - def ResetUI(): + def _ResetUI( self ): + if not self.parent_session: if self._stackTraceView: self._stackTraceView.Reset() if self._variablesView: self._variablesView.Reset() if self._outputView: self._outputView.Reset() + if self._logView: + self._logView.Reset() if self._codeView: self._codeView.Reset() if self._disassemblyView: self._disassemblyView.Reset() - self._stackTraceView = None - self._variablesView = None - self._outputView = None - self._codeView = None - self._disassemblyView = None - self._remote_term = None - self._uiTab = None - self._breakpoints.RemoveConnection( self._connection ) + self._breakpoints.RemoveConnection( self._connection ) + self._stackTraceView = None + self._variablesView = None + self._outputView = None + self._codeView = None + self._disassemblyView = None + self._remote_term = None + self._uiTab = None + + if self.parent_session: + self.manager.DestroySession( self ) + + + def _Reset( self ): + if self.parent_session: + self._ResetUI() + return + + vim.vars[ 'vimspector_resetting' ] = 1 + self._logger.info( "Debugging complete." ) if self.HasUI(): self._logger.debug( "Clearing down UI" ) @@ -621,12 +645,11 @@ def ResetUI(): vim.current.tabpage = self._uiTab self._splash_screen = utils.HideSplash( self._api_prefix, self._splash_screen ) - ResetUI() + self._ResetUI() vim.command( 'tabclose!' ) else: - ResetUI() + self._ResetUI() - self._breakpoints.RemoveConnection( self._connection ) self._breakpoints.SetDisassemblyManager( None ) utils.SetSessionWindows( { 'breakpoints': vim.vars[ 'vimspector_session_windows' ].get( @@ -1226,7 +1249,7 @@ def _SetUpUIVertical( self ): f'topleft { settings.Int( "topbar_height" ) }new' ) stack_trace_window = vim.current.window one_third = int( vim.eval( 'winwidth( 0 )' ) ) / 3 - self._stackTraceView = stack_trace.StackTraceView( self, + self._stackTraceView = stack_trace.StackTraceView( self.session_id, stack_trace_window ) @@ -1943,7 +1966,6 @@ def _DoStartDebuggingRequest( self, session_name = None ): session = self.manager.NewSession( - self._api_prefix, session_name = session_name or launch_arguments.get( 'name' ), parent_session = self ) diff --git a/python3/vimspector/output.py b/python3/vimspector/output.py index 7a4d74744..210777ffd 100644 --- a/python3/vimspector/output.py +++ b/python3/vimspector/output.py @@ -199,7 +199,7 @@ def _CreateBuffer( self, out = utils.SetUpCommandBuffer( self._session_id, cmd, - utils.BufferNameForSession( category, self._session_id ), + category, self._api_prefix, completion_handler = completion_handler ) diff --git a/python3/vimspector/session_manager.py b/python3/vimspector/session_manager.py index 6a7a832d3..5303c9d8f 100644 --- a/python3/vimspector/session_manager.py +++ b/python3/vimspector/session_manager.py @@ -15,6 +15,7 @@ import typing from vimspector.debug_session import DebugSession +from vimspector import utils # Singleton _session_manager = None @@ -23,6 +24,7 @@ class SessionManager: next_session_id: int sessions: typing.Dict[ int, DebugSession ] + api_prefix: str = '' def __init__( self ): self.Reset() @@ -36,20 +38,80 @@ def Reset( self ): def NewSession( self, *args, **kwargs ) -> DebugSession: session_id = self.next_session_id self.next_session_id += 1 - session = DebugSession( session_id, self, *args, **kwargs ) + session = DebugSession( session_id, self, self.api_prefix, *args, **kwargs ) self.sessions[ session_id ] = session return session def DestroySession( self, session: DebugSession ): - del self.sessions[ session.session_id ] + try: + session = self.sessions.pop( session.session_id ) + except KeyError: + return + + + def DestroyRootSession( self, + session: DebugSession, + active_session: DebugSession ): + if session.HasUI() or session.Connection(): + utils.UserMessage( "Can't destroy active session; use VimspectorReset", + error = True ) + return active_session + + try: + self.sessions.pop( session.session_id ) + session.Destroy() + except KeyError: + utils.UserMessage( "Session doesn't exist", error = True ) + return active_session + + if active_session != session: + # OK, we're done. No need to change the active session + return active_session + + # Return the first root session in the list to be the new active one + for existing_session in self.sessions.values(): + if not existing_session.parent_session: + return existing_session + + # There are somehow no non-root sessions. Clear the current one. We'll + # probably create a new one next time the user does anything. + return None def GetSession( self, session_id ) -> DebugSession: return self.sessions.get( session_id ) + def GetSessionNames( self ) -> typing.List[ str ]: + return [ s.Name() + for s in self.sessions.values() + if not s.parent_session and s.Name() ] + + + def SessionsWithInvalidUI( self ): + for _, session in self.sessions.items(): + if not session.parent_session and not session.HasUI(): + yield session + + + def FindSessionByTab( self, tabnr: int ) -> DebugSession: + for _, session in self.sessions.items(): + if session.HasUI() and session.IsUITab( tabnr ): + return session + + return None + + + def FindSessionByName( self, name ) -> DebugSession: + for _, session in self.sessions.items(): + if session.Name() == name: + return session + + return None + + def SessionForTab( self, tabnr ) -> DebugSession: session: DebugSession for _, session in self.sessions.items(): diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index a85833293..ff7768ba9 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -1087,13 +1087,7 @@ def Hex( val: int ): def BufferNameForSession( name, session_id ): - # if session_id == 0: - # # Hack for backward compat - don't suffix with the ID for the "first" - # # session - # return name - - # return f'{name}[{session_id}]' - return name + return f'{name}[{session_id}]' def ClearTextPropertiesForBuffer( buf ): diff --git a/tests/breakpoints.test.vim b/tests/breakpoints.test.vim index 62e0cda7d..fd6b9c960 100644 --- a/tests/breakpoints.test.vim +++ b/tests/breakpoints.test.vim @@ -917,19 +917,20 @@ function! Test_ListBreakpoints() augroup Test_ListBreakpoints autocmd! - autocmd BufEnter,BufFilePost vimspector.Breakpoints + autocmd BufEnter,BufFilePost vimspector.Breakpoints* \ let g:Test_ListBreakpoints_Enter += 1 - autocmd BufLeave vimspector.Breakpoints + autocmd BufLeave vimspector.Breakpoints* \ let g:Test_ListBreakpoints_Leave += 1 augroup END + " vimspector.Breakpoints[0] " @show call vimspector#ListBreakpoints() " buffer is never actually empty call s:CheckBreakpointView( [ '' ] ) " Cursor jumps to the breakpoint window call assert_equal( win_getid(), g:vimspector_session_windows.breakpoints ) - call assert_equal( bufname(), 'vimspector.Breakpoints' ) + call assert_match( 'vimspector.Breakpoints[\[0-9]\+]', bufname() ) call assert_equal( 1, g:Test_ListBreakpoints_Enter ) call win_gotoid( main_win_id ) diff --git a/tests/tabpage.test.vim b/tests/tabpage.test.vim index ad99b7083..6b40845da 100644 --- a/tests/tabpage.test.vim +++ b/tests/tabpage.test.vim @@ -173,14 +173,6 @@ function! Test_Close_Tab_No_Vimspector() %bwipe! endfunction -function! Test_Close_Tab_With_Vimspector() - call s:StartDebugging() - call vimspector#test#setup#WaitForReset() - call s:StartDebugging() - tabclose! - %bwipe! -endfunction - function! Test_Close_Tab_With_Vimspector() call s:StartDebugging() tabedit newfile diff --git a/tests/variables.test.vim b/tests/variables.test.vim index 9999f2b2b..31c60f81c 100644 --- a/tests/variables.test.vim +++ b/tests/variables.test.vim @@ -12,14 +12,10 @@ function! TearDown() endfunction function! ConsoleBufferName() - return 'vimspector.Console' + " return 'vimspector.Console' - " let session_id = py3eval( '_vimspector_session.session_id' ) - " if session_id == 0 - " return 'vimspector.Console' - " endif - - " return 'vimspector.Console' .. session_id + let session_id = py3eval( '_vimspector_session.session_id' ) + return 'vimspector.Console[' .. session_id .. ']' endfunction function! s:StartDebugging( ... ) diff --git a/tests/variables_compact.test.vim b/tests/variables_compact.test.vim index 3d443368e..525c6c4bf 100644 --- a/tests/variables_compact.test.vim +++ b/tests/variables_compact.test.vim @@ -11,14 +11,8 @@ function! TearDown() endfunction function! ConsoleBufferName() - return 'vimspector.Console' - - " let session_id = py3eval( '_vimspector_session.session_id' ) - " if session_id == 0 - " return 'vimspector.Console' - " endif - - " return 'vimspector.Console' .. session_id + let session_id = py3eval( '_vimspector_session.session_id' ) + return 'vimspector.Console[' .. session_id .. ']' endfunction function! s:StartDebugging( ... ) From e78bb518ce6621db9e75fb762c23679d31e0182a Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 21 Apr 2023 10:11:02 +0100 Subject: [PATCH 2/4] Allow specifying a directory in VimspectorMkSession/VimspectorLoadSession --- README.md | 24 ++++++++++++---------- python3/vimspector/debug_session.py | 31 ++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 528f5d273..2a9288799 100644 --- a/README.md +++ b/README.md @@ -1060,18 +1060,22 @@ pick one. Vimspector can save and restore breakpoints (and some other stuff) to a session file. The following commands exist for that: -* `VimspectorMkSession [file name]` - save the current set of line breakpoints, +* `VimspectorMkSession [file/dir name]` - save the current set of line breakpoints, logpoints, conditional breakpoints, function breakpoints and exception - breakpoint filters to the session file. -* `VimspectorLoadSession [file name]` - read breakpoints from the session file - and replace any currently set breakpoints. Prior to loading, all current - breakpoints are cleared (as if `vimspector#ClearLineBreakpoints()` was - called). - -In both cases, the file name argument is optional. By default, the file is named -`.vimspector.session`, but this can be changed globally by setting + breakpoint filters to the supplied session file or the default file in the + supplied directory. +* `VimspectorLoadSession [file/dir name]` - read breakpoints from the session + file supplied or the default file in the supplied directory and replace any + currently set breakpoints. Prior to loading, all current breakpoints are + cleared (as if `vimspector#ClearLineBreakpoints()` was called). + +In both cases, the file/dir name argument is optional. By default, the file is +named `.vimspector.session`, but this can be changed globally by setting `g:vimspector_session_file_name` to something else, or by manually specifying a -path when calling the command. +path when calling the command. If you supply a directory, the default or +configured session file name is read fron or written to that directory. +Othewise, the file is read based on the currently open buffer or written to the +current working directory. Advanced users may wish to automate the process of loading and saving, for example by adding `VimEnter` and `VimLeave` autocommands. It's recommended in diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 0fbb29d4d..a018fc774 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -712,6 +712,10 @@ def ReadSessionFile( self, session_file: str = None ): def WriteSessionFile( self, session_file: str = None ): if session_file is None: session_file = self._DetectSessionFile( invent_one_if_not_found = True ) + elif os.path.isdir( session_file ): + session_file = self._DetectSessionFile( invent_one_if_not_found = True, + in_directory = session_file ) + try: with open( session_file, 'w' ) as f: @@ -733,20 +737,33 @@ def WriteSessionFile( self, session_file: str = None ): return False - def _DetectSessionFile( self, invent_one_if_not_found: bool ): + def _DetectSessionFile( self, + invent_one_if_not_found: bool, + in_directory: str = None ): session_file_name = settings.Get( 'session_file_name' ) - current_file = utils.GetBufferFilepath( vim.current.buffer ) - # Search from the path of the file we're editing. But note that if we invent - # a file, we always use CWD as that's more like what would be expected. - file_path = utils.PathToConfigFile( session_file_name, - os.path.dirname( current_file ) ) + if in_directory: + # If a dir was supplied, read from there + write_directory = in_directory + file_path = os.path.join( in_directory, session_file_name ) + if not os.path.exists( file_path ): + file_path = None + else: + # Otherwise, search based on the current file, and write based on CWD + current_file = utils.GetBufferFilepath( vim.current.buffer ) + write_directory = os.getcwd() + # Search from the path of the file we're editing. But note that if we + # invent a file, we always use CWD as that's more like what would be + # expected. + file_path = utils.PathToConfigFile( session_file_name, + os.path.dirname( current_file ) ) + if file_path: return file_path if invent_one_if_not_found: - return os.path.join( os.getcwd(), session_file_name ) + return os.path.join( write_directory, session_file_name ) return None From 6d4c9990dbe7c5f0b1636e16d2a52d993b5df4a3 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 21 Apr 2023 10:12:19 +0100 Subject: [PATCH 3/4] Set a var to indicate vimspector is in clean mode --- support/minimal_vimrc | 1 + 1 file changed, 1 insertion(+) diff --git a/support/minimal_vimrc b/support/minimal_vimrc index 3626d3959..ec3d01017 100644 --- a/support/minimal_vimrc +++ b/support/minimal_vimrc @@ -1,3 +1,4 @@ +let g:vimspector_clean = 1 let s:vimspector_path = expand( ':p:h:h' ) let g:vimspector_enable_mappings = 'HUMAN' exe 'source ' . s:vimspector_path . '/tests/vimrc' From 2fde0975623fe43170bc378e4fc74df6f6abc071 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 21 Apr 2023 11:34:29 +0100 Subject: [PATCH 4/4] Only clear temporary breakpoints when hit Previously setting a temp breakpoint on a line with an existing line bp turned the line bp into a temp one, and that got deleted on hit. Now, we always add a new temp bp and only delete that one. Also, when setting e line bp that already exists, make sure to update the server and call the callback. Fixes #719 --- python3/vimspector/breakpoints.py | 44 +++++++++++++----- python3/vimspector/debug_session.py | 15 +++--- tests/temporary_breakpoints.test.vim | 68 ++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 21 deletions(-) diff --git a/python3/vimspector/breakpoints.py b/python3/vimspector/breakpoints.py index 1b6dc3c98..dfbcbfe27 100644 --- a/python3/vimspector/breakpoints.py +++ b/python3/vimspector/breakpoints.py @@ -440,6 +440,13 @@ def ClearBreakpoints( self ): def _FindLineBreakpoint( self, file_name, line ): + for bp, index in self._AllBreakpointsOnLine( file_name, line ): + return bp, index + + return None, None + + + def _AllBreakpointsOnLine( self, file_name, line ): file_name = utils.NormalizePath( file_name ) for index, bp in enumerate( self._line_breakpoints[ file_name ] ): self._SignToLine( file_name, bp ) @@ -449,11 +456,9 @@ def _FindLineBreakpoint( self, file_name, line ): if 'server_bp' in bp: for conn, server_bp in bp[ 'server_bp' ].items(): if server_bp.get( 'line', bp[ 'line' ] ) == line: - return bp, index + yield bp, index elif bp[ 'line' ] == line: - return bp, index - - return None, None + yield bp, index def _FindPostedBreakpoint( self, @@ -670,8 +675,8 @@ def SetLineBreakpoint( self, file_name, line_num, options, then = None ): bp, _ = self._FindLineBreakpoint( file_name, line_num ) if bp is not None: bp[ 'options' ] = options - return - self._PutLineBreakpoint( file_name, line_num, options ) + else: + self._PutLineBreakpoint( file_name, line_num, options ) self.UpdateUI( then ) @@ -683,19 +688,34 @@ def ClearLineBreakpoint( self, file_name, line_num ): self.UpdateUI() + def AddTemporaryLineBreakpoint( self, + file_name, + line_num, + options = None, + then = None ): + the_options = { + 'temporary': True + } + if options: + the_options.update( options ) + self._PutLineBreakpoint( file_name, line_num, the_options ) + self.UpdateUI( then ) + + def ClearTemporaryBreakpoint( self, file_name, line_num ): # FIXME: We should use the _FindPostedBreakpoint here instead, as that's way # more accurate at this point. Some servers can now identifyt he breakpoint # ID that actually triggered too. For now, we still have # _UpdateServerBreakpoints change the _user_ breakpiont line and we check # for that _here_, though we could check ['server_bp']['line'] - bp, index = self._FindLineBreakpoint( file_name, line_num ) - if bp is None: - return - if bp[ 'options' ].get( 'temporary' ): - self._DeleteLineBreakpoint( bp, file_name, index ) - self.UpdateUI() + updates = False + for bp, index in self._AllBreakpointsOnLine( file_name, line_num ): + if bp[ 'options' ].get( 'temporary' ): + updates = True + self._DeleteLineBreakpoint( bp, file_name, index ) + if updates: + self.UpdateUI() def ClearTemporaryBreakpoints( self ): to_delete = [] diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index a018fc774..7deaa6e49 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -173,7 +173,7 @@ def _ResetServerState( self ): self._launch_complete = False self._on_init_complete_handlers = [] self._server_capabilities = {} - self.ClearTemporaryBreakpoints() + self._breakpoints.ClearTemporaryBreakpoints() def GetConfigurations( self, adapters ): @@ -2106,11 +2106,11 @@ def ToggleBreakpoint( self, options ): def RunTo( self, file_name, line ): - self.ClearTemporaryBreakpoints() - self.SetLineBreakpoint( file_name, - line, - { 'temporary': True }, - lambda: self.Continue() ) + self._breakpoints.ClearTemporaryBreakpoints() + self._breakpoints.AddTemporaryLineBreakpoint( file_name, + line, + { 'temporary': True }, + lambda: self.Continue() ) @CurrentSession() @IfConnected() @@ -2157,9 +2157,6 @@ def handle_targets( msg ): }, failure_handler ) - def ClearTemporaryBreakpoints( self ): - return self._breakpoints.ClearTemporaryBreakpoints() - def SetLineBreakpoint( self, file_name, line_num, options, then = None ): return self._breakpoints.SetLineBreakpoint( file_name, line_num, diff --git a/tests/temporary_breakpoints.test.vim b/tests/temporary_breakpoints.test.vim index bc2c1ae84..c787b29ca 100644 --- a/tests/temporary_breakpoints.test.vim +++ b/tests/temporary_breakpoints.test.vim @@ -201,5 +201,73 @@ function! Test_StartDebuggingWithRunToCursor() \ vimspector#test#signs#AssertSignGroupEmptyAtLine( 'VimspectorBP', 9 ) \ } ) + call vimspector#test#setup#Reset() + lcd - + %bwipe! +endfunction + +function! Test_Run_To_Cursor_Existing_Line_BP() + call SkipNeovim() + lcd ../support/test/python/multiple_files + edit moo.py + call s:Start() + call vimspector#SetLineBreakpoint( 'moo.py', 5 ) + call vimspector#SetLineBreakpoint( 'moo.py', 8 ) + + call WaitForAssert( { -> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 5, + \ 'vimspectorBP', + \ 9 ) + \ } ) + call WaitForAssert( { -> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 8, + \ 'vimspectorBP', + \ 9 ) + \ } ) + + call vimspector#Continue() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 5, 1 ) + + call WaitForAssert( { -> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorCode', + \ 5, + \ 'vimspectorPCBP', + \ 200 ) + \ } ) + call WaitForAssert( { -> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorBP', + \ 8, + \ 'vimspectorBP', + \ 9 ) + \ } ) + + " So we don't loop and hit the loop breakpoint first... + call vimspector#ClearLineBreakpoint( 'moo.py', 5 ) + + call cursor( 8, 1 ) + call vimspector#RunToCursor() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 8, 1 ) + + call WaitForAssert( { -> + \ vimspector#test#signs#AssertSignGroupSingletonAtLine( + \ 'VimspectorCode', + \ 8, + \ 'vimspectorPCBP', + \ 200 ) + \ } ) + + call cursor( 1, 1 ) + " the loop breakpoint is still there and hit again + call vimspector#Continue() + call vimspector#test#signs#AssertCursorIsAtLineInBuffer( 'moo.py', 8, 1 ) + + call vimspector#test#setup#Reset() lcd - + %bwipe! endfunction