From b512bd16bdbe02d241ce0efb9ad9ca5c9ab29c4a Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Fri, 12 Dec 2025 22:31:41 +1000 Subject: [PATCH] debugging - remove executing line on stop --- .../Features/Debugging/Debugger.cs | 4 +- .../Features/Debugging/DebuggingService.cs | 7 +-- .../Features/Debugging/ExecutionStopInfo.cs | 6 ++- .../Features/Run/RunService.cs | 2 +- .../Features/CodeEditor/CodeEditorPanel.cs | 45 ++++++++++++++----- 5 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/SharpIDE.Application/Features/Debugging/Debugger.cs b/src/SharpIDE.Application/Features/Debugging/Debugger.cs index c33a8d7..abe6b99 100644 --- a/src/SharpIDE.Application/Features/Debugging/Debugger.cs +++ b/src/SharpIDE.Application/Features/Debugging/Debugger.cs @@ -10,9 +10,9 @@ public class Debugger public required SharpIdeProjectModel Project { get; init; } public required int ProcessId { get; init; } private DebuggingService _debuggingService = new DebuggingService(); - public async Task Attach(string? debuggerExecutablePath, Dictionary> breakpointsByFile, CancellationToken cancellationToken) + public async Task Attach(string? debuggerExecutablePath, Dictionary> breakpointsByFile, SharpIdeProjectModel project, CancellationToken cancellationToken) { - await _debuggingService.Attach(ProcessId, debuggerExecutablePath, breakpointsByFile, cancellationToken); + await _debuggingService.Attach(ProcessId, debuggerExecutablePath, breakpointsByFile, project, cancellationToken); } public async Task StepOver(int threadId, CancellationToken cancellationToken = default) => await _debuggingService.StepOver(threadId, cancellationToken); diff --git a/src/SharpIDE.Application/Features/Debugging/DebuggingService.cs b/src/SharpIDE.Application/Features/Debugging/DebuggingService.cs index 05a413c..9039ae5 100644 --- a/src/SharpIDE.Application/Features/Debugging/DebuggingService.cs +++ b/src/SharpIDE.Application/Features/Debugging/DebuggingService.cs @@ -8,6 +8,7 @@ using Newtonsoft.Json.Linq; using SharpIDE.Application.Features.Debugging.Signing; using SharpIDE.Application.Features.Events; using SharpIDE.Application.Features.SolutionDiscovery; +using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; namespace SharpIDE.Application.Features.Debugging; @@ -15,7 +16,7 @@ namespace SharpIDE.Application.Features.Debugging; public class DebuggingService { private DebugProtocolHost _debugProtocolHost = null!; - public async Task Attach(int debuggeeProcessId, string? debuggerExecutablePath, Dictionary> breakpointsByFile, CancellationToken cancellationToken = default) + public async Task Attach(int debuggeeProcessId, string? debuggerExecutablePath, Dictionary> breakpointsByFile, SharpIdeProjectModel project, CancellationToken cancellationToken = default) { Guard.Against.NegativeOrZero(debuggeeProcessId, nameof(debuggeeProcessId), "Process ID must be a positive integer."); await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); @@ -86,7 +87,7 @@ public class DebuggingService { var filePath = additionalProperties?["source"]?["path"]!.Value()!; var line = (additionalProperties?["line"]?.Value()!).Value; - var executionStopInfo = new ExecutionStopInfo { FilePath = filePath, Line = line, ThreadId = @event.ThreadId!.Value }; + var executionStopInfo = new ExecutionStopInfo { FilePath = filePath, Line = line, ThreadId = @event.ThreadId!.Value, Project = project }; GlobalEvents.Instance.DebuggerExecutionStopped.InvokeParallelFireAndForget(executionStopInfo); } else @@ -97,7 +98,7 @@ public class DebuggingService var topFrame = stackTraceResponse.StackFrames.Single(); var filePath = topFrame.Source.Path; var line = topFrame.Line; - var executionStopInfo = new ExecutionStopInfo { FilePath = filePath, Line = line, ThreadId = @event.ThreadId!.Value }; + var executionStopInfo = new ExecutionStopInfo { FilePath = filePath, Line = line, ThreadId = @event.ThreadId!.Value, Project = project }; GlobalEvents.Instance.DebuggerExecutionStopped.InvokeParallelFireAndForget(executionStopInfo); } diff --git a/src/SharpIDE.Application/Features/Debugging/ExecutionStopInfo.cs b/src/SharpIDE.Application/Features/Debugging/ExecutionStopInfo.cs index 30da7a6..ee6edf1 100644 --- a/src/SharpIDE.Application/Features/Debugging/ExecutionStopInfo.cs +++ b/src/SharpIDE.Application/Features/Debugging/ExecutionStopInfo.cs @@ -1,8 +1,12 @@ -namespace SharpIDE.Application.Features.Debugging; +using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; + +namespace SharpIDE.Application.Features.Debugging; public class ExecutionStopInfo { public required string FilePath { get; init; } public required int Line { get; init; } public required int ThreadId { get; init; } + // Currently assuming only one instance of a project can be debugged at a time + public required SharpIdeProjectModel Project { get; init; } } diff --git a/src/SharpIDE.Application/Features/Run/RunService.cs b/src/SharpIDE.Application/Features/Run/RunService.cs index bd504e5..03b51e7 100644 --- a/src/SharpIDE.Application/Features/Run/RunService.cs +++ b/src/SharpIDE.Application/Features/Run/RunService.cs @@ -94,7 +94,7 @@ public class RunService(ILogger logger, RoslynAnalysis roslynAnalysi // Attach debugger (which internally uses a DiagnosticClient to resume startup) var debugger = new Debugger { Project = project, ProcessId = process.ProcessId }; _debugger = debugger; - await debugger.Attach(debuggerExecutablePath, Breakpoints.ToDictionary(), project.RunningCancellationTokenSource.Token).ConfigureAwait(false); + await debugger.Attach(debuggerExecutablePath, Breakpoints.ToDictionary(), project, project.RunningCancellationTokenSource.Token).ConfigureAwait(false); } project.Running = true; diff --git a/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs b/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs index 59bbd6a..c11e707 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using Ardalis.GuardClauses; using Godot; using R3; @@ -19,7 +20,7 @@ public partial class CodeEditorPanel : MarginContainer public SharpIdeSolutionModel Solution { get; set; } = null!; private PackedScene _sharpIdeCodeEditScene = GD.Load("res://Features/CodeEditor/SharpIdeCodeEdit.tscn"); private TabContainer _tabContainer = null!; - private ExecutionStopInfo? _debuggerExecutionStopInfo; + private ConcurrentDictionary _debuggerExecutionStopInfoByProject = []; [Inject] private readonly RunService _runService = null!; public override void _Ready() @@ -31,6 +32,7 @@ public partial class CodeEditorPanel : MarginContainer tabBar.TabCloseDisplayPolicy = TabBar.CloseButtonDisplayPolicy.ShowAlways; tabBar.TabClosePressed += OnTabClosePressed; GlobalEvents.Instance.DebuggerExecutionStopped.Subscribe(OnDebuggerExecutionStopped); + GlobalEvents.Instance.ProjectStoppedDebugging.Subscribe(OnProjectStoppedDebugging); } public override void _ExitTree() @@ -131,6 +133,7 @@ public partial class CodeEditorPanel : MarginContainer await newTab.SetSharpIdeFile(file, fileLinePosition); } + private static readonly Color ExecutingLineColor = new Color("665001"); private async Task OnDebuggerExecutionStopped(ExecutionStopInfo executionStopInfo) { Guard.Against.Null(Solution, nameof(Solution)); @@ -144,26 +147,32 @@ public partial class CodeEditorPanel : MarginContainer } var lineInt = executionStopInfo.Line - 1; // Debugging is 1-indexed, Godot is 0-indexed Guard.Against.Negative(lineInt, nameof(lineInt)); - _debuggerExecutionStopInfo = executionStopInfo; + if (_debuggerExecutionStopInfoByProject.TryGetValue(executionStopInfo.Project, out _)) throw new InvalidOperationException("Debugger is already stopped for this project."); + _debuggerExecutionStopInfoByProject[executionStopInfo.Project] = executionStopInfo; await this.InvokeAsync(() => { - var focusedTab = _tabContainer.GetChild(_tabContainer.CurrentTab); - focusedTab.SetLineBackgroundColor(lineInt, new Color("665001")); - focusedTab.SetLineAsExecuting(lineInt, true); + var tabForStopInfo = _tabContainer.GetChildren().OfType().Single(t => t.SharpIdeFile.Path == executionStopInfo.FilePath); + tabForStopInfo.SetLineBackgroundColor(lineInt, ExecutingLineColor); + tabForStopInfo.SetLineAsExecuting(lineInt, true); }); } private enum DebuggerStepAction { StepOver, StepIn, StepOut, Continue } + [RequiresGodotUiThread] private void SendDebuggerStepCommand(DebuggerStepAction debuggerStepAction) { - if (_debuggerExecutionStopInfo is null) return; // ie not currently stopped - var godotLine = _debuggerExecutionStopInfo.Line - 1; - var focusedTab = _tabContainer.GetChild(_tabContainer.CurrentTab); - focusedTab.SetLineAsExecuting(godotLine, false); - focusedTab.SetLineColour(godotLine); - var threadId = _debuggerExecutionStopInfo.ThreadId; - _debuggerExecutionStopInfo = null; + // TODO: Debugging needs a rework - debugging commands should be scoped to a debug session, ie the debug panel sub-tabs + // For now, just use the first project that is currently stopped + var stoppedProjects = _debuggerExecutionStopInfoByProject.Keys.ToList(); + if (stoppedProjects.Count == 0) return; // ie not currently stopped anywhere + var project = stoppedProjects[0]; + if (!_debuggerExecutionStopInfoByProject.TryRemove(project, out var executionStopInfo)) return; + var godotLine = executionStopInfo.Line - 1; + var tabForStopInfo = _tabContainer.GetChildren().OfType().Single(t => t.SharpIdeFile.Path == executionStopInfo.FilePath); + tabForStopInfo.SetLineAsExecuting(godotLine, false); + tabForStopInfo.SetLineColour(godotLine); + var threadId = executionStopInfo.ThreadId; _ = Task.GodotRun(async () => { var task = debuggerStepAction switch @@ -177,6 +186,18 @@ public partial class CodeEditorPanel : MarginContainer await task; }); } + + private async Task OnProjectStoppedDebugging(SharpIdeProjectModel project) + { + if (!_debuggerExecutionStopInfoByProject.TryRemove(project, out var executionStopInfo)) return; + await this.InvokeAsync(() => + { + var godotLine = executionStopInfo.Line - 1; + var tabForStopInfo = _tabContainer.GetChildren().OfType().Single(t => t.SharpIdeFile.Path == executionStopInfo.FilePath); + tabForStopInfo.SetLineAsExecuting(godotLine, false); + tabForStopInfo.SetLineColour(godotLine); + }); + } } file static class TabContainerExtensions