debugging - remove executing line on stop

This commit is contained in:
Matt Parker
2025-12-12 22:31:41 +10:00
parent 1ab3d62bec
commit b512bd16bd
5 changed files with 45 additions and 19 deletions

View File

@@ -10,9 +10,9 @@ public class Debugger
public required SharpIdeProjectModel Project { get; init; } public required SharpIdeProjectModel Project { get; init; }
public required int ProcessId { get; init; } public required int ProcessId { get; init; }
private DebuggingService _debuggingService = new DebuggingService(); private DebuggingService _debuggingService = new DebuggingService();
public async Task Attach(string? debuggerExecutablePath, Dictionary<SharpIdeFile, List<Breakpoint>> breakpointsByFile, CancellationToken cancellationToken) public async Task Attach(string? debuggerExecutablePath, Dictionary<SharpIdeFile, List<Breakpoint>> 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); public async Task StepOver(int threadId, CancellationToken cancellationToken = default) => await _debuggingService.StepOver(threadId, cancellationToken);

View File

@@ -8,6 +8,7 @@ using Newtonsoft.Json.Linq;
using SharpIDE.Application.Features.Debugging.Signing; using SharpIDE.Application.Features.Debugging.Signing;
using SharpIDE.Application.Features.Events; using SharpIDE.Application.Features.Events;
using SharpIDE.Application.Features.SolutionDiscovery; using SharpIDE.Application.Features.SolutionDiscovery;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
namespace SharpIDE.Application.Features.Debugging; namespace SharpIDE.Application.Features.Debugging;
@@ -15,7 +16,7 @@ namespace SharpIDE.Application.Features.Debugging;
public class DebuggingService public class DebuggingService
{ {
private DebugProtocolHost _debugProtocolHost = null!; private DebugProtocolHost _debugProtocolHost = null!;
public async Task Attach(int debuggeeProcessId, string? debuggerExecutablePath, Dictionary<SharpIdeFile, List<Breakpoint>> breakpointsByFile, CancellationToken cancellationToken = default) public async Task Attach(int debuggeeProcessId, string? debuggerExecutablePath, Dictionary<SharpIdeFile, List<Breakpoint>> breakpointsByFile, SharpIdeProjectModel project, CancellationToken cancellationToken = default)
{ {
Guard.Against.NegativeOrZero(debuggeeProcessId, nameof(debuggeeProcessId), "Process ID must be a positive integer."); Guard.Against.NegativeOrZero(debuggeeProcessId, nameof(debuggeeProcessId), "Process ID must be a positive integer.");
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
@@ -86,7 +87,7 @@ public class DebuggingService
{ {
var filePath = additionalProperties?["source"]?["path"]!.Value<string>()!; var filePath = additionalProperties?["source"]?["path"]!.Value<string>()!;
var line = (additionalProperties?["line"]?.Value<int>()!).Value; var line = (additionalProperties?["line"]?.Value<int>()!).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); GlobalEvents.Instance.DebuggerExecutionStopped.InvokeParallelFireAndForget(executionStopInfo);
} }
else else
@@ -97,7 +98,7 @@ public class DebuggingService
var topFrame = stackTraceResponse.StackFrames.Single(); var topFrame = stackTraceResponse.StackFrames.Single();
var filePath = topFrame.Source.Path; var filePath = topFrame.Source.Path;
var line = topFrame.Line; 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); GlobalEvents.Instance.DebuggerExecutionStopped.InvokeParallelFireAndForget(executionStopInfo);
} }

View File

@@ -1,8 +1,12 @@
namespace SharpIDE.Application.Features.Debugging; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
namespace SharpIDE.Application.Features.Debugging;
public class ExecutionStopInfo public class ExecutionStopInfo
{ {
public required string FilePath { get; init; } public required string FilePath { get; init; }
public required int Line { get; init; } public required int Line { get; init; }
public required int ThreadId { 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; }
} }

View File

@@ -94,7 +94,7 @@ public class RunService(ILogger<RunService> logger, RoslynAnalysis roslynAnalysi
// Attach debugger (which internally uses a DiagnosticClient to resume startup) // Attach debugger (which internally uses a DiagnosticClient to resume startup)
var debugger = new Debugger { Project = project, ProcessId = process.ProcessId }; var debugger = new Debugger { Project = project, ProcessId = process.ProcessId };
_debugger = debugger; _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; project.Running = true;

View File

@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using Ardalis.GuardClauses; using Ardalis.GuardClauses;
using Godot; using Godot;
using R3; using R3;
@@ -19,7 +20,7 @@ public partial class CodeEditorPanel : MarginContainer
public SharpIdeSolutionModel Solution { get; set; } = null!; public SharpIdeSolutionModel Solution { get; set; } = null!;
private PackedScene _sharpIdeCodeEditScene = GD.Load<PackedScene>("res://Features/CodeEditor/SharpIdeCodeEdit.tscn"); private PackedScene _sharpIdeCodeEditScene = GD.Load<PackedScene>("res://Features/CodeEditor/SharpIdeCodeEdit.tscn");
private TabContainer _tabContainer = null!; private TabContainer _tabContainer = null!;
private ExecutionStopInfo? _debuggerExecutionStopInfo; private ConcurrentDictionary<SharpIdeProjectModel, ExecutionStopInfo> _debuggerExecutionStopInfoByProject = [];
[Inject] private readonly RunService _runService = null!; [Inject] private readonly RunService _runService = null!;
public override void _Ready() public override void _Ready()
@@ -31,6 +32,7 @@ public partial class CodeEditorPanel : MarginContainer
tabBar.TabCloseDisplayPolicy = TabBar.CloseButtonDisplayPolicy.ShowAlways; tabBar.TabCloseDisplayPolicy = TabBar.CloseButtonDisplayPolicy.ShowAlways;
tabBar.TabClosePressed += OnTabClosePressed; tabBar.TabClosePressed += OnTabClosePressed;
GlobalEvents.Instance.DebuggerExecutionStopped.Subscribe(OnDebuggerExecutionStopped); GlobalEvents.Instance.DebuggerExecutionStopped.Subscribe(OnDebuggerExecutionStopped);
GlobalEvents.Instance.ProjectStoppedDebugging.Subscribe(OnProjectStoppedDebugging);
} }
public override void _ExitTree() public override void _ExitTree()
@@ -131,6 +133,7 @@ public partial class CodeEditorPanel : MarginContainer
await newTab.SetSharpIdeFile(file, fileLinePosition); await newTab.SetSharpIdeFile(file, fileLinePosition);
} }
private static readonly Color ExecutingLineColor = new Color("665001");
private async Task OnDebuggerExecutionStopped(ExecutionStopInfo executionStopInfo) private async Task OnDebuggerExecutionStopped(ExecutionStopInfo executionStopInfo)
{ {
Guard.Against.Null(Solution, nameof(Solution)); 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 var lineInt = executionStopInfo.Line - 1; // Debugging is 1-indexed, Godot is 0-indexed
Guard.Against.Negative(lineInt, nameof(lineInt)); 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(() => await this.InvokeAsync(() =>
{ {
var focusedTab = _tabContainer.GetChild<SharpIdeCodeEdit>(_tabContainer.CurrentTab); var tabForStopInfo = _tabContainer.GetChildren().OfType<SharpIdeCodeEdit>().Single(t => t.SharpIdeFile.Path == executionStopInfo.FilePath);
focusedTab.SetLineBackgroundColor(lineInt, new Color("665001")); tabForStopInfo.SetLineBackgroundColor(lineInt, ExecutingLineColor);
focusedTab.SetLineAsExecuting(lineInt, true); tabForStopInfo.SetLineAsExecuting(lineInt, true);
}); });
} }
private enum DebuggerStepAction { StepOver, StepIn, StepOut, Continue } private enum DebuggerStepAction { StepOver, StepIn, StepOut, Continue }
[RequiresGodotUiThread]
private void SendDebuggerStepCommand(DebuggerStepAction debuggerStepAction) private void SendDebuggerStepCommand(DebuggerStepAction debuggerStepAction)
{ {
if (_debuggerExecutionStopInfo is null) return; // ie not currently stopped // TODO: Debugging needs a rework - debugging commands should be scoped to a debug session, ie the debug panel sub-tabs
var godotLine = _debuggerExecutionStopInfo.Line - 1; // For now, just use the first project that is currently stopped
var focusedTab = _tabContainer.GetChild<SharpIdeCodeEdit>(_tabContainer.CurrentTab); var stoppedProjects = _debuggerExecutionStopInfoByProject.Keys.ToList();
focusedTab.SetLineAsExecuting(godotLine, false); if (stoppedProjects.Count == 0) return; // ie not currently stopped anywhere
focusedTab.SetLineColour(godotLine); var project = stoppedProjects[0];
var threadId = _debuggerExecutionStopInfo.ThreadId; if (!_debuggerExecutionStopInfoByProject.TryRemove(project, out var executionStopInfo)) return;
_debuggerExecutionStopInfo = null; var godotLine = executionStopInfo.Line - 1;
var tabForStopInfo = _tabContainer.GetChildren().OfType<SharpIdeCodeEdit>().Single(t => t.SharpIdeFile.Path == executionStopInfo.FilePath);
tabForStopInfo.SetLineAsExecuting(godotLine, false);
tabForStopInfo.SetLineColour(godotLine);
var threadId = executionStopInfo.ThreadId;
_ = Task.GodotRun(async () => _ = Task.GodotRun(async () =>
{ {
var task = debuggerStepAction switch var task = debuggerStepAction switch
@@ -177,6 +186,18 @@ public partial class CodeEditorPanel : MarginContainer
await task; 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<SharpIdeCodeEdit>().Single(t => t.SharpIdeFile.Path == executionStopInfo.FilePath);
tabForStopInfo.SetLineAsExecuting(godotLine, false);
tabForStopInfo.SetLineColour(godotLine);
});
}
} }
file static class TabContainerExtensions file static class TabContainerExtensions