diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ScopesHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ScopesHandler.cs
index e74bd1eb9..daa3d8e60 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ScopesHandler.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ScopesHandler.cs
@@ -18,12 +18,20 @@ internal class ScopesHandler : IScopesHandler
public ScopesHandler(DebugService debugService) => _debugService = debugService;
+ ///
+ /// Retrieves the variable scopes (containers) for the currently selected stack frame. Variables details are fetched via a separate request.
+ ///
public Task Handle(ScopesArguments request, CancellationToken cancellationToken)
{
- //We have an artificial breakpoint label, so just copy the stacktrace from the first stack entry for this.
- int frameId = request.FrameId == 0 ? 0 : (int)request.FrameId - 1;
+ // HACK: The StackTraceHandler injects an artificial label frame as the first frame as a performance optimization, so when scopes are requested by the client, we need to adjust the frame index accordingly to match the underlying PowerShell frame, so when the client clicks on the label (or hit the default breakpoint), they get variables populated from the top of the PowerShell stackframe. If the client dives deeper, we need to reflect that as well (though 90% of debug users don't actually investigate this)
+ // VSCode Frame 0 (Label) -> PowerShell StackFrame 0 (for convenience)
+ // VSCode Frame 1 (First Real PS Frame) -> Also PowerShell StackFrame 0
+ // VSCode Frame 2 -> PowerShell StackFrame 1
+ // VSCode Frame 3 -> PowerShell StackFrame 2
+ // etc.
+ int powershellFrameId = request.FrameId == 0 ? 0 : (int)request.FrameId - 1;
- VariableScope[] variableScopes = _debugService.GetVariableScopes(frameId);
+ VariableScope[] variableScopes = _debugService.GetVariableScopes(powershellFrameId);
return Task.FromResult(new ScopesResponse
{
diff --git a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs
index a3c68eadb..bb794516f 100644
--- a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs
+++ b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs
@@ -2,22 +2,31 @@
// Licensed under the MIT License.
using System;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Logging.Debug;
using OmniSharp.Extensions.DebugAdapter.Client;
+using DapStackFrame = OmniSharp.Extensions.DebugAdapter.Protocol.Models.StackFrame;
+using OmniSharp.Extensions.DebugAdapter.Protocol.Events;
using OmniSharp.Extensions.DebugAdapter.Protocol.Models;
using OmniSharp.Extensions.DebugAdapter.Protocol.Requests;
+using OmniSharp.Extensions.JsonRpc.Server;
using Xunit;
using Xunit.Abstractions;
+using Microsoft.Extensions.Logging.Abstractions;
namespace PowerShellEditorServices.Test.E2E
{
+ public class XunitOutputTraceListener(ITestOutputHelper output) : TraceListener
+ {
+ public override void Write(string message) => output.WriteLine(message);
+ public override void WriteLine(string message) => output.WriteLine(message);
+ }
+
[Trait("Category", "DAP")]
public class DebugAdapterProtocolMessageTests : IAsyncLifetime, IDisposable
{
@@ -28,16 +37,26 @@ public class DebugAdapterProtocolMessageTests : IAsyncLifetime, IDisposable
private DebugAdapterClient PsesDebugAdapterClient;
private PsesStdioProcess _psesProcess;
+ ///
+ /// Completes when the debug adapter is started.
+ ///
public TaskCompletionSource