Skip to content

Re-enable line breakpoints for untitled scripts #1724

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Management.Automation;
using System.Management.Automation.Language;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.PowerShell.EditorServices.Services;
using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
using Microsoft.PowerShell.EditorServices.Services.PowerShell;
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging;
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace;
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
using Microsoft.PowerShell.EditorServices.Utility;
using OmniSharp.Extensions.DebugAdapter.Protocol.Events;
Expand All @@ -18,6 +22,9 @@ namespace Microsoft.PowerShell.EditorServices.Handlers
{
internal class ConfigurationDoneHandler : IConfigurationDoneHandler
{
// TODO: We currently set `WriteInputToHost` as true, which writes our debugged commands'
// `GetInvocationText` and that reveals some obscure implementation details we should
// instead hide from the user with pretty strings (or perhaps not write out at all).
private static readonly PowerShellExecutionOptions s_debuggerExecutionOptions = new()
{
MustRunInForeground = true,
Expand All @@ -35,7 +42,10 @@ internal class ConfigurationDoneHandler : IConfigurationDoneHandler
private readonly IInternalPowerShellExecutionService _executionService;
private readonly WorkspaceService _workspaceService;
private readonly IPowerShellDebugContext _debugContext;
private readonly IRunspaceContext _runspaceContext;

// TODO: Decrease these arguments since they're a bunch of interfaces that can be simplified
// (i.e., `IRunspaceContext` should just be available on `IPowerShellExecutionService`).
public ConfigurationDoneHandler(
ILoggerFactory loggerFactory,
IDebugAdapterServerFacade debugAdapterServer,
Expand All @@ -44,7 +54,8 @@ public ConfigurationDoneHandler(
DebugEventHandlerService debugEventHandlerService,
IInternalPowerShellExecutionService executionService,
WorkspaceService workspaceService,
IPowerShellDebugContext debugContext)
IPowerShellDebugContext debugContext,
IRunspaceContext runspaceContext)
{
_logger = loggerFactory.CreateLogger<ConfigurationDoneHandler>();
_debugAdapterServer = debugAdapterServer;
Expand All @@ -54,6 +65,7 @@ public ConfigurationDoneHandler(
_executionService = executionService;
_workspaceService = workspaceService;
_debugContext = debugContext;
_runspaceContext = runspaceContext;
}

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

private async Task LaunchScriptAsync(string scriptToLaunch)
{
// TODO: Theoretically we can make PowerShell respect line breakpoints in untitled
// files, but the previous method was a hack that conflicted with correct passing of
// arguments to the debugged script. We are prioritizing the latter over the former, as
// command breakpoints and `Wait-Debugger` work fine.
string command = ScriptFile.IsUntitledPath(scriptToLaunch)
? string.Concat("{ ", _workspaceService.GetFile(scriptToLaunch).Contents, " }")
: string.Concat('"', scriptToLaunch, '"');
PSCommand command;
if (ScriptFile.IsUntitledPath(scriptToLaunch))
{
ScriptFile untitledScript = _workspaceService.GetFile(scriptToLaunch);
if (BreakpointApiUtils.SupportsBreakpointApis(_runspaceContext.CurrentRunspace))
{
// Parse untitled files with their `Untitled:` URI as the filename which will
// cache the URI and contents within the PowerShell parser. By doing this, we
// light up the ability to debug untitled files with line breakpoints. This is
// only possible with PowerShell 7's new breakpoint APIs since the old API,
// Set-PSBreakpoint, validates that the given path points to a real file.
ScriptBlockAst ast = Parser.ParseInput(
untitledScript.Contents,
untitledScript.DocumentUri.ToString(),
out Token[] _,
out ParseError[] _);

// In order to use utilize the parser's cache (and therefore hit line
// breakpoints) we need to use the AST's `ScriptBlock` object. Due to
// limitations in PowerShell's public API, this means we must use the
// `PSCommand.AddArgument(object)` method, hence this hack where we dot-source
// `$args[0]. Fortunately the dot-source operator maintains a stack of arguments
// on each invocation, so passing the user's arguments directly in the initial
// `AddScript` surprisingly works.
command = PSCommandHelpers
.BuildDotSourceCommandWithArguments("$args[0]", _debugStateService.Arguments)
.AddArgument(ast.GetScriptBlock());
}
else
{
// Without the new APIs we can only execute the untitled script's contents.
// Command breakpoints and `Wait-Debugger` will work.
command = PSCommandHelpers.BuildDotSourceCommandWithArguments(
string.Concat("{ ", untitledScript.Contents, " }"), _debugStateService.Arguments);
}
}
else
{
// For a saved file we just execute its path (after escaping it).
command = PSCommandHelpers.BuildDotSourceCommandWithArguments(
string.Concat('"', scriptToLaunch, '"'), _debugStateService.Arguments);
}

await _executionService.ExecutePSCommandAsync(
PSCommandHelpers.BuildCommandFromArguments(command, _debugStateService.Arguments),
command,
CancellationToken.None,
s_debuggerExecutionOptions).ConfigureAwait(false);
_debugAdapterServer.SendNotification(EventNames.Terminated);
Expand Down
5 changes: 3 additions & 2 deletions src/PowerShellEditorServices/Utility/PSCommandExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,11 @@ private static StringBuilder AddCommandText(this StringBuilder sb, Command comma
return sb;
}

public static PSCommand BuildCommandFromArguments(string command, IEnumerable<string> arguments)
public static PSCommand BuildDotSourceCommandWithArguments(string command, IEnumerable<string> arguments)
{
string args = string.Join(" ", arguments ?? Array.Empty<string>());
string script = string.Concat(". ", command, string.IsNullOrEmpty(args) ? "" : " ", args);
// HACK: We use AddScript instead of AddArgument/AddParameter to reuse Powershell parameter binding logic.
string script = string.Concat(". ", command, " ", string.Join(" ", arguments ?? Array.Empty<string>()));
return new PSCommand().AddScript(script);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ private VariableDetailsBase[] GetVariables(string scopeName)
private Task ExecutePowerShellCommand(string command, params string[] args)
{
return psesHost.ExecutePSCommandAsync(
PSCommandHelpers.BuildCommandFromArguments(string.Concat('"', command, '"'), args),
PSCommandHelpers.BuildDotSourceCommandWithArguments(string.Concat('"', command, '"'), args),
CancellationToken.None);
}

Expand Down