From 821dce5d5e271517c15e3bb9957745e18bfd3050 Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Tue, 23 Sep 2025 18:13:23 +1000 Subject: [PATCH] refactor debugging for multiple open files --- .../Features/Run/RunService.cs | 2 +- .../Features/CodeEditor/CodeEditorPanel.cs | 54 ++++++++++++++++++- .../Features/CodeEditor/SharpIdeCodeEdit.cs | 45 ++-------------- .../SolutionExplorer/SolutionExplorerPanel.cs | 4 +- src/SharpIDE.Godot/GodotGlobalEvents.cs | 1 + src/SharpIDE.Godot/NodeExtensions.cs | 25 +++++++-- 6 files changed, 83 insertions(+), 48 deletions(-) diff --git a/src/SharpIDE.Application/Features/Run/RunService.cs b/src/SharpIDE.Application/Features/Run/RunService.cs index 72dfbcc..ec277d8 100644 --- a/src/SharpIDE.Application/Features/Run/RunService.cs +++ b/src/SharpIDE.Application/Features/Run/RunService.cs @@ -68,7 +68,7 @@ public class RunService SingleReader = true, SingleWriter = false, }); - var logsDrained = new TaskCompletionSource(); + var logsDrained = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _ = Task.Run(async () => { await foreach(var log in process.CombinedOutputChannel.Reader.ReadAllAsync().ConfigureAwait(false)) diff --git a/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs b/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs index 92de26d..76e91e3 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs @@ -1,4 +1,7 @@ +using Ardalis.GuardClauses; using Godot; +using SharpIDE.Application.Features.Debugging; +using SharpIDE.Application.Features.Events; using SharpIDE.Application.Features.SolutionDiscovery; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; @@ -11,6 +14,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; public override void _Ready() { @@ -20,6 +24,15 @@ public partial class CodeEditorPanel : MarginContainer var tabBar = _tabContainer.GetTabBar(); tabBar.TabCloseDisplayPolicy = TabBar.CloseButtonDisplayPolicy.ShowAlways; tabBar.TabClosePressed += OnTabClosePressed; + GlobalEvents.DebuggerExecutionStopped += OnDebuggerExecutionStopped; + } + + public override void _UnhandledKeyInput(InputEvent @event) + { + if (@event.IsActionPressed(InputStringNames.StepOver)) + { + SendDebuggerStepOver(); + } } private void OnTabClicked(long tab) @@ -37,7 +50,8 @@ public partial class CodeEditorPanel : MarginContainer public async Task SetSharpIdeFile(SharpIdeFile file) { - var existingTab = _tabContainer.GetChildren().OfType().FirstOrDefault(t => t.SharpIdeFile == file); + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + var existingTab = await this.InvokeAsync(() => _tabContainer.GetChildren().OfType().FirstOrDefault(t => t.SharpIdeFile == file)); if (existingTab is not null) { var existingTabIndex = existingTab.GetIndex(); @@ -58,4 +72,42 @@ public partial class CodeEditorPanel : MarginContainer }); await newTab.SetSharpIdeFile(file); } + + private async Task OnDebuggerExecutionStopped(ExecutionStopInfo executionStopInfo) + { + Guard.Against.Null(Solution, nameof(Solution)); + + var currentSharpIdeFile = await this.InvokeAsync(() => _tabContainer.GetChild(_tabContainer.CurrentTab).SharpIdeFile); + + if (executionStopInfo.FilePath != currentSharpIdeFile?.Path) + { + var file = Solution.AllFiles.Single(s => s.Path == executionStopInfo.FilePath); + await GodotGlobalEvents.InvokeFileExternallySelectedAndWait(file).ConfigureAwait(false); + } + var lineInt = executionStopInfo.Line - 1; // Debugging is 1-indexed, Godot is 0-indexed + Guard.Against.Negative(lineInt, nameof(lineInt)); + _debuggerExecutionStopInfo = executionStopInfo; + + await this.InvokeAsync(() => + { + var focusedTab = _tabContainer.GetChild(_tabContainer.CurrentTab); + focusedTab.SetLineBackgroundColor(lineInt, new Color("665001")); + focusedTab.SetLineAsExecuting(lineInt, true); + }); + } + + private void SendDebuggerStepOver() + { + 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; + _ = Task.GodotRun(async () => + { + await Singletons.RunService.SendDebuggerStepOver(threadId); + }); + } } \ No newline at end of file diff --git a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs index 991b48d..0904d2c 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs @@ -7,13 +7,12 @@ using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.Text; using SharpIDE.Application.Features.Analysis; using SharpIDE.Application.Features.Debugging; -using SharpIDE.Application.Features.Events; using SharpIDE.Application.Features.SolutionDiscovery; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; using SharpIDE.RazorAccess; using Task = System.Threading.Tasks.Task; -namespace SharpIDE.Godot; +namespace SharpIDE.Godot.Features.CodeEditor; public partial class SharpIdeCodeEdit : CodeEdit { @@ -33,7 +32,6 @@ public partial class SharpIdeCodeEdit : CodeEdit private ImmutableArray<(FileLinePositionSpan fileSpan, Diagnostic diagnostic)> _diagnostics = []; private ImmutableArray _currentCodeActionsInPopup = []; - private ExecutionStopInfo? _executionStopInfo; private bool _fileChangingSuppressBreakpointToggleEvent; public override void _Ready() @@ -49,26 +47,6 @@ public partial class SharpIdeCodeEdit : CodeEdit SymbolHovered += OnSymbolHovered; SymbolValidate += OnSymbolValidate; SymbolLookup += OnSymbolLookup; - GlobalEvents.DebuggerExecutionStopped += OnDebuggerExecutionStopped; - } - - private async Task OnDebuggerExecutionStopped(ExecutionStopInfo executionStopInfo) - { - Guard.Against.Null(Solution, nameof(Solution)); - if (executionStopInfo.FilePath != _currentFile.Path) - { - var file = Solution.AllFiles.Single(s => s.Path == executionStopInfo.FilePath); - await GodotGlobalEvents.InvokeFileExternallySelectedAndWait(file); - } - var lineInt = executionStopInfo.Line - 1; // Debugging is 1-indexed, Godot is 0-indexed - Guard.Against.Negative(lineInt, nameof(lineInt)); - _executionStopInfo = executionStopInfo; - - await this.InvokeAsync(() => - { - SetLineBackgroundColor(lineInt, new Color("665001")); - SetLineAsExecuting(lineInt, true); - }); } private void OnBreakpointToggled(long line) @@ -241,33 +219,18 @@ public partial class SharpIdeCodeEdit : CodeEdit public override void _UnhandledKeyInput(InputEvent @event) { + if (HasFocus() is false) return; // every tab is currently listening for this input. Only respond if we have focus. Consider refactoring this _UnhandledKeyInput to CodeEditorPanel if (@event.IsActionPressed(InputStringNames.CodeFixes)) { EmitSignalCodeFixesRequested(); } - else if (@event.IsActionPressed(InputStringNames.StepOver)) - { - SendDebuggerStepOver(); - } } - private void SendDebuggerStepOver() - { - if (_executionStopInfo is null) return; - var godotLine = _executionStopInfo.Line - 1; - SetLineAsExecuting(godotLine, false); - SetLineColour(godotLine); - var threadId = _executionStopInfo.ThreadId; - _executionStopInfo = null; - _ = Task.GodotRun(async () => - { - await Singletons.RunService.SendDebuggerStepOver(threadId); - }); - } + private readonly Color _breakpointLineColor = new Color("3a2323"); private readonly Color _executingLineColor = new Color("665001"); - private void SetLineColour(int line) + public void SetLineColour(int line) { var breakpointed = IsLineBreakpointed(line); var executing = IsLineExecuting(line); diff --git a/src/SharpIDE.Godot/Features/SolutionExplorer/SolutionExplorerPanel.cs b/src/SharpIDE.Godot/Features/SolutionExplorer/SolutionExplorerPanel.cs index 46884ea..de794ce 100644 --- a/src/SharpIDE.Godot/Features/SolutionExplorer/SolutionExplorerPanel.cs +++ b/src/SharpIDE.Godot/Features/SolutionExplorer/SolutionExplorerPanel.cs @@ -40,7 +40,8 @@ public partial class SolutionExplorerPanel : MarginContainer private async Task OnFileExternallySelected(SharpIdeFile file) { - GodotGlobalEvents.InvokeFileSelected(file); + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + var task = GodotGlobalEvents.InvokeFileSelectedAndWait(file); var item = FindItemRecursive(_tree.GetRoot(), file); if (item is not null) { @@ -52,6 +53,7 @@ public partial class SolutionExplorerPanel : MarginContainer _tree.QueueRedraw(); }); } + await task.ConfigureAwait(false); } private static TreeItem? FindItemRecursive(TreeItem item, SharpIdeFile file) diff --git a/src/SharpIDE.Godot/GodotGlobalEvents.cs b/src/SharpIDE.Godot/GodotGlobalEvents.cs index c6ee5cd..0f95c6b 100644 --- a/src/SharpIDE.Godot/GodotGlobalEvents.cs +++ b/src/SharpIDE.Godot/GodotGlobalEvents.cs @@ -16,6 +16,7 @@ public static class GodotGlobalEvents public static event Func FileSelected = _ => Task.CompletedTask; public static void InvokeFileSelected(SharpIdeFile file) => FileSelected.InvokeParallelFireAndForget(file); + public static async Task InvokeFileSelectedAndWait(SharpIdeFile file) => await FileSelected.InvokeParallelAsync(file); public static event Func FileExternallySelected = _ => Task.CompletedTask; public static void InvokeFileExternallySelected(SharpIdeFile file) => FileExternallySelected.InvokeParallelFireAndForget(file); public static async Task InvokeFileExternallySelectedAndWait(SharpIdeFile file) => await FileExternallySelected.InvokeParallelAsync(file); diff --git a/src/SharpIDE.Godot/NodeExtensions.cs b/src/SharpIDE.Godot/NodeExtensions.cs index f2b2da6..5a1d7c0 100644 --- a/src/SharpIDE.Godot/NodeExtensions.cs +++ b/src/SharpIDE.Godot/NodeExtensions.cs @@ -36,9 +36,26 @@ public static class NodeExtensions { extension(Node node) { + public Task InvokeAsync(Func workItem) + { + var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + Dispatcher.SynchronizationContext.Post(_ => + { + try + { + var result = workItem(); + taskCompletionSource.SetResult(result); + } + catch (Exception ex) + { + taskCompletionSource.SetException(ex); + } + }, null); + return taskCompletionSource.Task; + } public Task InvokeAsync(Action workItem) { - var taskCompletionSource = new TaskCompletionSource(); + var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); //WorkerThreadPool.AddTask(); Dispatcher.SynchronizationContext.Post(_ => { @@ -57,7 +74,7 @@ public static class NodeExtensions public Task InvokeAsync(Func workItem) { - var taskCompletionSource = new TaskCompletionSource(); + var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); Dispatcher.SynchronizationContext.Post(async void (_) => { try @@ -75,7 +92,7 @@ public static class NodeExtensions public Task InvokeDeferredAsync(Action workItem) { - var taskCompletionSource = new TaskCompletionSource(); + var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); //WorkerThreadPool.AddTask(); Callable.From(() => { @@ -94,7 +111,7 @@ public static class NodeExtensions public Task InvokeDeferredAsync(Func workItem) { - var taskCompletionSource = new TaskCompletionSource(); + var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); //WorkerThreadPool.AddTask(); Callable.From(async void () => {