diff --git a/autoload/vimspector.vim b/autoload/vimspector.vim index 4785399e7..1e4ecc22d 100644 --- a/autoload/vimspector.vim +++ b/autoload/vimspector.vim @@ -439,6 +439,14 @@ function! vimspector#ShowDisassembly( ... ) abort py3 _vimspector_session.ShowDisassembly() endfunction +function! vimspector#AddDataBreakpoint( ... ) abort + if !s:Enabled() + return + endif + " TODO: how to set options? + py3 _vimspector_session.AddDataBreakpoint( {} ) +endfunction + function! vimspector#DeleteWatch() abort if !s:Enabled() return diff --git a/python3/vimspector/breakpoints.py b/python3/vimspector/breakpoints.py index 2aab632eb..bbe573537 100644 --- a/python3/vimspector/breakpoints.py +++ b/python3/vimspector/breakpoints.py @@ -219,6 +219,7 @@ def __init__( self, self._func_breakpoints = [] self._exception_breakpoints = None self._configured_breakpoints = {} + self._data_breakponts = [] self._server_capabilities = {} @@ -523,23 +524,21 @@ def _ClearServerBreakpointData( self, conn: DebugAdapterConnection ): if not bp[ 'server_bp' ]: del bp[ 'server_bp' ] - # Clear all instruction breakpoints because they aren't truly portable # across sessions. - # - # TODO: It might be possible to re-resolve the address stored in the - # breakpoint, though this would only work in a limited way (as load - # addresses will frequently not be the same across runs) - - def ShouldKeep( bp ): + def ShouldKeepInsBP( bp ): if not bp[ 'is_instruction_breakpoint' ]: return True if 'address' in bp and bp[ 'session_id' ] != conn.GetSessionId(): return True return False - breakpoints[ : ] = [ bp for bp in breakpoints if ShouldKeep( bp ) ] + breakpoints[ : ] = [ bp for bp in breakpoints if ShouldKeepInsBP( bp ) ] + + # Erase any data breakpoints for this connection too + self._data_breakponts[ : ] = [ bp for bp in self._data_breakponts + if bp[ 'conn' ] != conn.GetSessionId() ] def _CopyServerLineBreakpointProperties( self, @@ -807,7 +806,19 @@ def AddFunctionBreakpoint( self, function, options ): # 'condition': ..., # 'hitCondition': ..., } ) + self.UpdateUI() + + def AddDataBreakpoint( self, + conn: DebugAdapterConnection, + info, + options ): + self._data_breakponts.append( { + 'state': 'ENABLED', + 'conn': conn.GetSessionId(), + 'info': info, + 'options': options + } ) self.UpdateUI() @@ -1014,6 +1025,37 @@ def response_handler( conn, msg, bp_idxs = [] ): failure_handler = response_received ) + if self._data_breakponts and self._server_capabilities[ + 'supportsDataBreakpoints' ]: + connection: DebugAdapterConnection + for connection in self._connections: + breakpoints = [] + for bp in self._data_breakponts: + if bp[ 'state' ] != 'ENABLED': + continue + if bp[ 'conn' ] != connection.GetSessionId(): + continue + if not bp[ 'info' ].get( 'dataId' ): + continue + + data_bp = {} + data_bp.update( bp[ 'options' ] ) + data_bp[ 'dataId' ] = bp[ 'info' ][ 'dataId' ] + breakpoints.append( data_bp ) + + if breakpoints: + self._awaiting_bp_responses += 1 + connection.DoRequest( + lambda msg, conn=connection: response_handler( conn, msg ), + { + 'command': 'setDataBreakpoints', + 'arguments': { + 'breakpoints': breakpoints, + }, + }, + failure_handler = response_received + ) + if self._exception_breakpoints: for connection in self._connections: self._awaiting_bp_responses += 1 @@ -1112,6 +1154,11 @@ def Save( self ): if bps: line[ file_name ] = bps + # TODO: Some way to persis data breakpoints? Currently they require + # variablesReference, which is clearly not something that can be persisted + # + # That said, the spec now seems to support data bps on expressions, though i + # can't see any servers which support that. return { 'line': line, 'function': self._func_breakpoints, @@ -1183,6 +1230,11 @@ def _HideBreakpoints( self ): signs.UnplaceSign( bp[ 'sign_id' ], 'VimspectorBP' ) del bp[ 'sign_id' ] + # TODO could/should we show a sign in the variables view when there's a data + # brakpoint on the variable? Not sure how best to actually do that, but + # maybe the variable view can pass that info when calling AddDataBreakpoint, + # such as the variablesReference/name + def _SignToLine( self, file_name, bp ): if bp[ 'is_instruction_breakpoint' ]: diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 941a5f4cb..5e5bbe6f1 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -1016,7 +1016,36 @@ def OnDisassemblyWindowScrolled( self, win_id ): self._disassemblyView.OnWindowScrolled( win_id ) - @CurrentSession() + @ParentOnly() + def AddDataBreakpoint( self, opts, buf = None, line_num = None ): + # Use the parent session, because the _connection_ comes from the + # variable/watch result that is actually chosen + + def add_bp( conn, msg ): + breakpoint_info = msg.get( 'body' ) + if not breakpoint_info: + utils.UserMessage( "Can't set data breakpoint here" ) + return + + if breakpoint_info[ 'dataId' ] is None: + utils.UserMessage( + f"Can't set data breakpoint here: {breakpoint_info[ 'description' ]}" + ) + return + + access_types = breakpoint_info.get( 'accessTypes' ) + if access_types and 'accessType' not in opts: + access_type = utils.SelectFromList( 'What type of access?', + access_types ) + if access_type is not None: + opts[ 'accessType' ] = access_type + + self._breakpoints.AddDataBreakpoint( conn, + breakpoint_info, + opts ) + + self._variablesView.GetDataBreakpointInfo( add_bp, buf, line_num ) + @IfConnected() def AddWatch( self, expression ): self._variablesView.AddWatch( self._connection, diff --git a/python3/vimspector/settings.py b/python3/vimspector/settings.py index 26d6258b3..a4295048e 100644 --- a/python3/vimspector/settings.py +++ b/python3/vimspector/settings.py @@ -92,6 +92,7 @@ 'delete': [ '' ], 'set_value': [ '', '' ], 'read_memory': [ 'm' ], + 'add_data_breakpoint': [ '' ], }, 'stack_trace': { 'expand_or_jump': [ '', '<2-LeftMouse>' ], diff --git a/python3/vimspector/variables.py b/python3/vimspector/variables.py index 4aa9262d6..2c12d9a34 100644 --- a/python3/vimspector/variables.py +++ b/python3/vimspector/variables.py @@ -19,7 +19,7 @@ from functools import partial import typing -from vimspector import utils, settings +from vimspector import utils, settings, session_manager from vimspector.debug_adapter_connection import DebugAdapterConnection @@ -62,13 +62,25 @@ def IsContained( self ): def VariablesReference( self ): assert False + @abc.abstractmethod + def FrameID( self ): + assert False + + @abc.abstractmethod + def Name( self ): + assert False + + @abc.abstractmethod def MemoryReference( self ): - assert None + assert False @abc.abstractmethod def HoverText( self ): return "" + def Update( self, connection ): + self.connection = connection + class Scope( Expandable ): """Holds an expandable scope (a DAP scope dict), with expand/collapse state""" @@ -82,7 +94,14 @@ def VariablesReference( self ): def MemoryReference( self ): return None - def Update( self, scope ): + def FrameID( self ): + return None + + def Name( self ): + return self.scope[ 'name' ] + + def Update( self, connection, scope ): + super().Update( connection ) self.scope = scope def HoverText( self ): @@ -91,8 +110,12 @@ def HoverText( self ): class WatchResult( Expandable ): """Holds the result of a Watch expression with expand/collapse.""" - def __init__( self, connection: DebugAdapterConnection, result: dict ): + def __init__( self, + connection: DebugAdapterConnection, + watch, + result: dict ): super().__init__( connection ) + self.watch = watch self.result = result # A new watch result is marked as changed self.changed = True @@ -103,9 +126,15 @@ def VariablesReference( self ): def MemoryReference( self ): return self.result.get( 'memoryReference' ) + def FrameID( self ): + return self.watch.expression.get( 'frameId' ) + + def Name( self ): + return self.watch.expression.get( 'expression' ) + def Update( self, connection, result ): + super().Update( connection ) self.changed = False - self.connection = connection if self.result[ 'result' ] != result[ 'result' ]: self.changed = True self.result = result @@ -121,8 +150,8 @@ def HoverText( self ): class WatchFailure( WatchResult ): - def __init__( self, connection: DebugAdapterConnection, reason ): - super().__init__( connection, { 'result': reason } ) + def __init__( self, connection: DebugAdapterConnection, watch, reason ): + super().__init__( connection, watch, { 'result': reason } ) self.changed = True @@ -130,7 +159,8 @@ class Variable( Expandable ): """Holds one level of an expanded value tree. Also itself expandable.""" def __init__( self, connection: DebugAdapterConnection, - container: Expandable, variable: dict ): + container: Expandable, + variable: dict ): super().__init__( connection = connection, container = container ) self.variable = variable # A new variable appearing is marked as changed @@ -142,9 +172,15 @@ def VariablesReference( self ): def MemoryReference( self ): return self.variable.get( 'memoryReference' ) + def FrameID( self ): + return self.container.FrameID() + + def Name( self ): + return self.variable[ 'name' ] + def Update( self, connection, variable ): + super().Update( connection ) self.changed = False - self.connection = connection if self.variable[ 'value' ] != variable[ 'value' ]: self.changed = True self.variable = variable @@ -171,6 +207,11 @@ def __init__( self, connection: DebugAdapterConnection, expression: dict ): self.result = None def SetCurrentFrame( self, connection, frame ): + if connection is None: + self.connection = None + self.result.connection = None + return + if self.connection is None: self.connection = connection elif self.connection != connection: @@ -227,6 +268,9 @@ def AddExpandMappings( mappings = None ): for mapping in utils.GetVimList( mappings, 'read_memory' ): vim.command( f'nnoremap { mapping } ' ':call vimspector#ReadMemory()' ) + for mapping in utils.GetVimList( mappings, 'add_data_breakpoint' ): + vim.command( f'nnoremap { mapping } ' + ':call vimspector#AddDataBreakpoint()' ) @@ -323,7 +367,7 @@ def ConnectionClosed( self, connection ): ] for w in self._watches: if w.connection == connection: - w.connection = None + w.SetCurrentFrame( None, None ) def Reset( self ): @@ -363,7 +407,7 @@ def scopes_consumer( message ): if not found: scope = Scope( connection, scope_body ) else: - scope.Update( scope_body ) + scope.Update( connection, scope_body ) new_scopes.append( scope ) @@ -434,7 +478,7 @@ def handler( message ): watch = self._variable_eval if watch.result is None or watch.result.connection != connection: - watch.result = WatchResult( connection, message[ 'body' ] ) + watch.result = WatchResult( connection, watch, message[ 'body' ] ) else: watch.result.Update( connection, message[ 'body' ] ) @@ -543,7 +587,9 @@ def _UpdateWatchExpression( self, watch: Watch, message: dict ): if watch.result is not None: watch.result.Update( watch.connection, message[ 'body' ] ) else: - watch.result = WatchResult( watch.connection, message[ 'body' ] ) + watch.result = WatchResult( watch.connection, + watch, + message[ 'body' ] ) if ( watch.result.IsExpandable() and watch.result.IsExpanded() ): @@ -563,7 +609,7 @@ def _WatchExpressionFailed( self, reason: str, watch: Watch ): # We already have a result for this watch. Wut ? return - watch.result = WatchFailure( watch.connection, reason ) + watch.result = WatchFailure( watch.connection, watch, reason ) self._DrawWatches() def _GetVariable( self, buf = None, line_num = None ): @@ -677,7 +723,6 @@ def GetMemoryReference( self ): if variable is None: return None, None - # TODO: Return the connection too! return variable.connection, variable.MemoryReference() @@ -853,4 +898,39 @@ def SetSyntax( self, syntax ): syntax, self._vars.buf, self._watch.buf ) + + def GetDataBreakpointInfo( self, + then, + buf = None, + line_num = None ): + variable: Expandable + view: View + + variable, view = self._GetVariable( buf, line_num ) + if variable is None: + return None + + if not session_manager.Get().GetSession( + variable.connection.GetSessionId() )._server_capabilities.get( + 'supportsDataBreakpoints' ): + return None + + arguments = { + 'name': variable.Name() + } + frameId = variable.FrameID() + if frameId: + arguments[ 'frameId' ] = frameId + + if variable.IsContained(): + arguments[ 'variablesReference' ] = ( + variable.container.VariablesReference() ) + + variable.connection.DoRequest( lambda msg: then( variable.connection, + msg ), { + 'command': 'dataBreakpointInfo', + 'arguments': arguments, + } ) + + # vim: sw=2 diff --git a/support/test/cpp/simple_c_program/.vimspector.json b/support/test/cpp/simple_c_program/.vimspector.json index f277fc774..e5804cf17 100644 --- a/support/test/cpp/simple_c_program/.vimspector.json +++ b/support/test/cpp/simple_c_program/.vimspector.json @@ -64,7 +64,7 @@ "adapter": "vscode-cpptools", "variables": { "BUILDME": { - "shell": "g++ -o ${workspaceRoot}/test -g -std=c++17 ${workspaceRoot}/test_c.cpp" + "shell": "g++ -o ${workspaceRoot}/test -g -std=c++17 ${file}" }, "arch": { "shell": "uname -m" @@ -86,7 +86,7 @@ "adapter": "vscode-cpptools", "variables": { "BUILDME": { - "shell": "g++ -o ${workspaceRoot}/test -g -std=c++17 ${workspaceRoot}/test_c.cpp" + "shell": "g++ -o ${workspaceRoot}/test -g -std=c++17 ${file}" } }, "configuration": { diff --git a/support/test/cpp/simple_c_program/memory.cpp b/support/test/cpp/simple_c_program/memory.cpp new file mode 100644 index 000000000..cac551ba1 --- /dev/null +++ b/support/test/cpp/simple_c_program/memory.cpp @@ -0,0 +1,24 @@ +struct Test +{ + int x; + int y; +}; + +int main( int argc , char ** argv ) +{ + Test x[] = { + { 1, 2 }, + { 3, 4 }, + { 5, 6 }, + }; + + Test y = { 7, 8 }; + + x[0].x += argc; + argv[ 0 ][ 0 ] = 'x' ; + + y.x += **argv; + y.y += argc * **argv; + + return argc; +} diff --git a/support/test/go/structs/.gitignore b/support/test/go/structs/.gitignore new file mode 100644 index 000000000..242c034c1 --- /dev/null +++ b/support/test/go/structs/.gitignore @@ -0,0 +1 @@ +hello_world diff --git a/support/test/go/structs/.vimspector.json b/support/test/go/structs/.vimspector.json new file mode 100644 index 000000000..da54999a1 --- /dev/null +++ b/support/test/go/structs/.vimspector.json @@ -0,0 +1,29 @@ +{ + "configurations": { + "run-legacy": { + "adapter": "vscode-go", + "configuration": { + "request": "launch", + "program": "${workspaceRoot}/hello-world.go", + "mode": "debug", + "trace": true, + "env": { "GO111MODULE": "off" } + } + }, + "run-delve": { + "adapter": "delve", + "configuration": { + "request": "launch", + "env": { "GO111MODULE": "off" }, + + "mode": "debug", // debug|test + "program": "${workspaceRoot}/hello-world.go" + + // "args": [], + // "buildFlags": ... + // "stackTraceDepth": ..., + // "showGlobalVariables": true, + } + } + } +} diff --git a/support/test/go/structs/__debug_bin b/support/test/go/structs/__debug_bin new file mode 100755 index 000000000..919aef79b Binary files /dev/null and b/support/test/go/structs/__debug_bin differ diff --git a/support/test/go/structs/hello-world.go b/support/test/go/structs/hello-world.go new file mode 100644 index 000000000..efff2c069 --- /dev/null +++ b/support/test/go/structs/hello-world.go @@ -0,0 +1,25 @@ +package main +import "fmt" + +type Toaster struct { + Power int + Colour string +} + +type Test struct { + Test string + Toast Toaster +} + +func main() { + var v = "test" + test := Test{ + Test: "This is\na\ntest", + Toast: Toaster{ + Power: 10, + Colour: "green", + }, + } + fmt.Println("hello world: " + v) + fmt.Println("Hi " + test.Test) +} diff --git a/support/test/java/test_project/.vimspector.json b/support/test/java/test_project/.vimspector.json index 39f4639ba..58219a917 100644 --- a/support/test/java/test_project/.vimspector.json +++ b/support/test/java/test_project/.vimspector.json @@ -7,6 +7,7 @@ "mainClass": "com.vimspector.test.TestApplication", "sourcePaths": [ "${workspaceRoot}/src/main/java" ], "classPaths": [ "${workspaceRoot}/target/classes" ], + "project": "TestApplication", "args": "hello world!", "stopOnEntry": true, "console": "integratedTerminal", @@ -20,6 +21,7 @@ "configuration": { "request": "attach", "sourcePaths": [ "${workspaceRoot}/src/main/java" ], + "project": "TestApplication", "stopOnEntry": true, "hostName": "localhost", "port": "${JVMDebugPort}", @@ -32,6 +34,7 @@ "adapter": "vscode-javac", "configuration": { "request": "attach", + "project": "TestApplication", "port": "${debugPort}", "sourceRoots": [ "${workspaceRoot}/src/main/java" diff --git a/support/test/rust/vimspector_test/src/main.rs b/support/test/rust/vimspector_test/src/main.rs index 509a6d43c..9cea16605 100644 --- a/support/test/rust/vimspector_test/src/main.rs +++ b/support/test/rust/vimspector_test/src/main.rs @@ -1,10 +1,26 @@ +struct Point { + x: i32, + y: i32, +} + struct Foo { x: i32, } fn main() { let s = "World!"; + println!("Hello, {}!", s); + let f = Foo { x: 42 }; let g = &f; println!("Hello, {} {} {}!", s, g.x, f.x); + + let mut p = Point{ x: 1, y: 11 }; + + p.x = 11; + p.y = 11; + p.x += 11; + p.y += 11; + + println!("Point: {}, {}", p.x, p.y); }