Skip to content

Commit 30080f3

Browse files
committed
Re-enable line breakpoints for untitled scripts
We managed to make the previous hack work while continuing to support passing the users' arguments. As there was demand for this feature to continue working, despite being a hack, we're keeping it.
1 parent 640eac8 commit 30080f3

File tree

3 files changed

+60
-12
lines changed

3 files changed

+60
-12
lines changed

src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.Management.Automation;
5+
using System.Management.Automation.Language;
46
using System.Threading;
57
using System.Threading.Tasks;
68
using Microsoft.Extensions.Logging;
79
using Microsoft.PowerShell.EditorServices.Services;
10+
using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
811
using Microsoft.PowerShell.EditorServices.Services.PowerShell;
912
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging;
1013
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
14+
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace;
1115
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
1216
using Microsoft.PowerShell.EditorServices.Utility;
1317
using OmniSharp.Extensions.DebugAdapter.Protocol.Events;
@@ -18,6 +22,9 @@ namespace Microsoft.PowerShell.EditorServices.Handlers
1822
{
1923
internal class ConfigurationDoneHandler : IConfigurationDoneHandler
2024
{
25+
// TODO: We currently set `WriteInputToHost` as true, which writes our debugged commands'
26+
// `GetInvocationText` and that reveals some obscure implementation details we should
27+
// instead hide from the user with pretty strings (or perhaps not write out at all).
2128
private static readonly PowerShellExecutionOptions s_debuggerExecutionOptions = new()
2229
{
2330
MustRunInForeground = true,
@@ -35,7 +42,10 @@ internal class ConfigurationDoneHandler : IConfigurationDoneHandler
3542
private readonly IInternalPowerShellExecutionService _executionService;
3643
private readonly WorkspaceService _workspaceService;
3744
private readonly IPowerShellDebugContext _debugContext;
45+
private readonly IRunspaceContext _runspaceContext;
3846

47+
// TODO: Decrease these arguments since they're a bunch of interfaces that can be simplified
48+
// (i.e., `IRunspaceContext` should just be available on `IPowerShellExecutionService`).
3949
public ConfigurationDoneHandler(
4050
ILoggerFactory loggerFactory,
4151
IDebugAdapterServerFacade debugAdapterServer,
@@ -44,7 +54,8 @@ public ConfigurationDoneHandler(
4454
DebugEventHandlerService debugEventHandlerService,
4555
IInternalPowerShellExecutionService executionService,
4656
WorkspaceService workspaceService,
47-
IPowerShellDebugContext debugContext)
57+
IPowerShellDebugContext debugContext,
58+
IRunspaceContext runspaceContext)
4859
{
4960
_logger = loggerFactory.CreateLogger<ConfigurationDoneHandler>();
5061
_debugAdapterServer = debugAdapterServer;
@@ -54,6 +65,7 @@ public ConfigurationDoneHandler(
5465
_executionService = executionService;
5566
_workspaceService = workspaceService;
5667
_debugContext = debugContext;
68+
_runspaceContext = runspaceContext;
5769
}
5870

5971
public Task<ConfigurationDoneResponse> Handle(ConfigurationDoneArguments request, CancellationToken cancellationToken)
@@ -90,16 +102,51 @@ public Task<ConfigurationDoneResponse> Handle(ConfigurationDoneArguments request
90102

91103
private async Task LaunchScriptAsync(string scriptToLaunch)
92104
{
93-
// TODO: Theoretically we can make PowerShell respect line breakpoints in untitled
94-
// files, but the previous method was a hack that conflicted with correct passing of
95-
// arguments to the debugged script. We are prioritizing the latter over the former, as
96-
// command breakpoints and `Wait-Debugger` work fine.
97-
string command = ScriptFile.IsUntitledPath(scriptToLaunch)
98-
? string.Concat("{ ", _workspaceService.GetFile(scriptToLaunch).Contents, " }")
99-
: string.Concat('"', scriptToLaunch, '"');
105+
PSCommand command;
106+
if (ScriptFile.IsUntitledPath(scriptToLaunch))
107+
{
108+
ScriptFile untitledScript = _workspaceService.GetFile(scriptToLaunch);
109+
if (BreakpointApiUtils.SupportsBreakpointApis(_runspaceContext.CurrentRunspace))
110+
{
111+
// Parse untitled files with their `Untitled:` URI as the filename which will
112+
// cache the URI and contents within the PowerShell parser. By doing this, we
113+
// light up the ability to debug untitled files with line breakpoints. This is
114+
// only possible with PowerShell 7's new breakpoint APIs since the old API,
115+
// Set-PSBreakpoint, validates that the given path points to a real file.
116+
ScriptBlockAst ast = Parser.ParseInput(
117+
untitledScript.Contents,
118+
untitledScript.DocumentUri.ToString(),
119+
out Token[] _,
120+
out ParseError[] _);
121+
122+
// In order to use utilize the parser's cache (and therefore hit line
123+
// breakpoints) we need to use the AST's `ScriptBlock` object. Due to
124+
// limitations in PowerShell's public API, this means we must use the
125+
// `PSCommand.AddArgument(object)` method, hence this hack where we dot-source
126+
// `$args[0]. Fortunately the dot-source operator maintains a stack of arguments
127+
// on each invocation, so passing the user's arguments directly in the initial
128+
// `AddScript` surprisingly works.
129+
command = PSCommandHelpers
130+
.BuildDotSourceCommandWithArguments("$args[0]", _debugStateService.Arguments)
131+
.AddArgument(ast.GetScriptBlock());
132+
}
133+
else
134+
{
135+
// Without the new APIs we can only execute the untitled script's contents.
136+
// Command breakpoints and `Wait-Debugger` will work.
137+
command = PSCommandHelpers.BuildDotSourceCommandWithArguments(
138+
string.Concat("{ ", untitledScript.Contents, " }"), _debugStateService.Arguments);
139+
}
140+
}
141+
else
142+
{
143+
// For a saved file we just execute its path (after escaping it).
144+
command = PSCommandHelpers.BuildDotSourceCommandWithArguments(
145+
string.Concat('"', scriptToLaunch, '"'), _debugStateService.Arguments);
146+
}
100147

101148
await _executionService.ExecutePSCommandAsync(
102-
PSCommandHelpers.BuildCommandFromArguments(command, _debugStateService.Arguments),
149+
command,
103150
CancellationToken.None,
104151
s_debuggerExecutionOptions).ConfigureAwait(false);
105152
_debugAdapterServer.SendNotification(EventNames.Terminated);

src/PowerShellEditorServices/Utility/PSCommandExtensions.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,11 @@ private static StringBuilder AddCommandText(this StringBuilder sb, Command comma
129129
return sb;
130130
}
131131

132-
public static PSCommand BuildCommandFromArguments(string command, IEnumerable<string> arguments)
132+
public static PSCommand BuildDotSourceCommandWithArguments(string command, IEnumerable<string> arguments)
133133
{
134+
string args = string.Join(" ", arguments ?? Array.Empty<string>());
135+
string script = string.Concat(". ", command, string.IsNullOrEmpty(args) ? "" : " ", args);
134136
// HACK: We use AddScript instead of AddArgument/AddParameter to reuse Powershell parameter binding logic.
135-
string script = string.Concat(". ", command, " ", string.Join(" ", arguments ?? Array.Empty<string>()));
136137
return new PSCommand().AddScript(script);
137138
}
138139
}

test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ private VariableDetailsBase[] GetVariables(string scopeName)
9696
private Task ExecutePowerShellCommand(string command, params string[] args)
9797
{
9898
return psesHost.ExecutePSCommandAsync(
99-
PSCommandHelpers.BuildCommandFromArguments(string.Concat('"', command, '"'), args),
99+
PSCommandHelpers.BuildDotSourceCommandWithArguments(string.Concat('"', command, '"'), args),
100100
CancellationToken.None);
101101
}
102102

0 commit comments

Comments
 (0)