Skip to content

Commit 7f62aac

Browse files
committed
Support breakpoints in untitled files in WinPS
Adds support for setting breakpoints in untitled/unsaved files for Windows PowerShell 5.1. This aligns the breakpoint validation behaviour with the PowerShell 7.x API so that a breakpoint can be set for any ScriptBlock with a filename if it aligns with the client's filename.
1 parent 6bb322e commit 7f62aac

File tree

4 files changed

+79
-28
lines changed

4 files changed

+79
-28
lines changed

src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,57 @@ namespace Microsoft.PowerShell.EditorServices.Services
1818
{
1919
internal class BreakpointService
2020
{
21+
private const string _setPSBreakpointLegacy = @"
22+
[CmdletBinding(DefaultParameterSetName = 'Line')]
23+
param (
24+
[Parameter()]
25+
[ScriptBlock]
26+
$Action,
27+
28+
[Parameter(ParameterSetName = 'Command')]
29+
[Parameter(ParameterSetName = 'Line', Mandatory = $true)]
30+
[string]
31+
$Script,
32+
33+
[Parameter(ParameterSetName = 'Line')]
34+
[int]
35+
$Line,
36+
37+
[Parameter(ParameterSetName = 'Line')]
38+
[int]
39+
$Column,
40+
41+
[Parameter(ParameterSetName = 'Command', Mandatory = $true)]
42+
[string]
43+
$Command
44+
)
45+
46+
if ($PSCmdlet.ParameterSetName -eq 'Command') {
47+
$cmdCtor = [System.Management.Automation.CommandBreakpoint].GetConstructor(
48+
[System.Reflection.BindingFlags]'NonPublic, Public, Instance',
49+
$null,
50+
[type[]]@([string], [System.Management.Automation.WildcardPattern], [string], [ScriptBlock]),
51+
$null)
52+
$pattern = [System.Management.Automation.WildcardPattern]::Get(
53+
$Command,
54+
[System.Management.Automation.WildcardOptions]'Compiled, IgnoreCase')
55+
$b = $cmdCtor.Invoke(@($Script, $pattern, $Command, $Action))
56+
}
57+
else {
58+
$lineCtor = [System.Management.Automation.LineBreakpoint].GetConstructor(
59+
[System.Reflection.BindingFlags]'NonPublic, Public, Instance',
60+
$null,
61+
[type[]]@([string], [int], [int], [ScriptBlock]),
62+
$null)
63+
$b = $lineCtor.Invoke(@($Script, $Line, $Column, $Action))
64+
}
65+
66+
[Runspace]::DefaultRunspace.Debugger.SetBreakpoints(
67+
[System.Management.Automation.Breakpoint[]]@($b))
68+
69+
$b
70+
";
71+
2172
private readonly ILogger<BreakpointService> _logger;
2273
private readonly IInternalPowerShellExecutionService _executionService;
2374
private readonly PsesInternalHost _editorServicesHost;
@@ -114,8 +165,10 @@ public async Task<IReadOnlyList<BreakpointDetails>> SetBreakpointsAsync(string e
114165
psCommand.AddStatement();
115166
}
116167

168+
// Don't use Set-PSBreakpoint as that will try and validate the Script
169+
// path which may or may not exist.
117170
psCommand
118-
.AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint")
171+
.AddScript(_setPSBreakpointLegacy, useLocalScope: true)
119172
.AddParameter("Script", escapedScriptPath)
120173
.AddParameter("Line", breakpoint.LineNumber);
121174

@@ -184,7 +237,7 @@ public async Task<IReadOnlyList<CommandBreakpointDetails>> SetCommandBreakpoints
184237
}
185238

186239
psCommand
187-
.AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint")
240+
.AddScript(_setPSBreakpointLegacy, useLocalScope: true)
188241
.AddParameter("Command", breakpoint.Name);
189242

190243
// Check if this is a "conditional" line breakpoint.

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
using Microsoft.PowerShell.EditorServices.Logging;
1212
using Microsoft.PowerShell.EditorServices.Services;
1313
using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
14-
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace;
1514
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
1615
using Microsoft.PowerShell.EditorServices.Utility;
1716
using OmniSharp.Extensions.DebugAdapter.Protocol.Models;
@@ -31,20 +30,17 @@ internal class BreakpointHandlers : ISetFunctionBreakpointsHandler, ISetBreakpoi
3130
private readonly DebugService _debugService;
3231
private readonly DebugStateService _debugStateService;
3332
private readonly WorkspaceService _workspaceService;
34-
private readonly IRunspaceContext _runspaceContext;
3533

3634
public BreakpointHandlers(
3735
ILoggerFactory loggerFactory,
3836
DebugService debugService,
3937
DebugStateService debugStateService,
40-
WorkspaceService workspaceService,
41-
IRunspaceContext runspaceContext)
38+
WorkspaceService workspaceService)
4239
{
4340
_logger = loggerFactory.CreateLogger<BreakpointHandlers>();
4441
_debugService = debugService;
4542
_debugStateService = debugStateService;
4643
_workspaceService = workspaceService;
47-
_runspaceContext = runspaceContext;
4844
}
4945

5046
public async Task<SetBreakpointsResponse> Handle(SetBreakpointsArguments request, CancellationToken cancellationToken)
@@ -182,12 +178,11 @@ public Task<SetExceptionBreakpointsResponse> Handle(SetExceptionBreakpointsArgum
182178

183179
Task.FromResult(new SetExceptionBreakpointsResponse());
184180

185-
private bool IsFileSupportedForBreakpoints(string requestedPath, ScriptFile resolvedScriptFile)
181+
private static bool IsFileSupportedForBreakpoints(string requestedPath, ScriptFile resolvedScriptFile)
186182
{
187-
// PowerShell 7 and above support breakpoints in untitled files
188183
if (ScriptFile.IsUntitledPath(requestedPath))
189184
{
190-
return BreakpointApiUtils.SupportsBreakpointApis(_runspaceContext.CurrentRunspace);
185+
return true;
191186
}
192187

193188
if (string.IsNullOrEmpty(resolvedScriptFile?.FilePath))

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

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@
77
using System.Threading.Tasks;
88
using Microsoft.Extensions.Logging;
99
using Microsoft.PowerShell.EditorServices.Services;
10-
using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
1110
using Microsoft.PowerShell.EditorServices.Services.PowerShell;
1211
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging;
1312
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
14-
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace;
1513
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
1614
using Microsoft.PowerShell.EditorServices.Utility;
1715
using OmniSharp.Extensions.DebugAdapter.Protocol.Events;
@@ -44,7 +42,6 @@ internal class ConfigurationDoneHandler : IConfigurationDoneHandler
4442
private readonly IInternalPowerShellExecutionService _executionService;
4543
private readonly WorkspaceService _workspaceService;
4644
private readonly IPowerShellDebugContext _debugContext;
47-
private readonly IRunspaceContext _runspaceContext;
4845

4946
// TODO: Decrease these arguments since they're a bunch of interfaces that can be simplified
5047
// (i.e., `IRunspaceContext` should just be available on `IPowerShellExecutionService`).
@@ -56,8 +53,7 @@ public ConfigurationDoneHandler(
5653
DebugEventHandlerService debugEventHandlerService,
5754
IInternalPowerShellExecutionService executionService,
5855
WorkspaceService workspaceService,
59-
IPowerShellDebugContext debugContext,
60-
IRunspaceContext runspaceContext)
56+
IPowerShellDebugContext debugContext)
6157
{
6258
_logger = loggerFactory.CreateLogger<ConfigurationDoneHandler>();
6359
_debugAdapterServer = debugAdapterServer;
@@ -67,7 +63,6 @@ public ConfigurationDoneHandler(
6763
_executionService = executionService;
6864
_workspaceService = workspaceService;
6965
_debugContext = debugContext;
70-
_runspaceContext = runspaceContext;
7166
}
7267

7368
public Task<ConfigurationDoneResponse> Handle(ConfigurationDoneArguments request, CancellationToken cancellationToken)
@@ -119,13 +114,11 @@ internal async Task LaunchScriptAsync(string scriptToLaunch)
119114
else // It's a URI to an untitled script, or a raw script.
120115
{
121116
bool isScriptFile = _workspaceService.TryGetFile(scriptToLaunch, out ScriptFile untitledScript);
122-
if (isScriptFile && BreakpointApiUtils.SupportsBreakpointApis(_runspaceContext.CurrentRunspace))
117+
if (isScriptFile)
123118
{
124119
// Parse untitled files with their `Untitled:` URI as the filename which will
125120
// cache the URI and contents within the PowerShell parser. By doing this, we
126-
// light up the ability to debug untitled files with line breakpoints. This is
127-
// only possible with PowerShell 7's new breakpoint APIs since the old API,
128-
// Set-PSBreakpoint, validates that the given path points to a real file.
121+
// light up the ability to debug untitled files with line breakpoints.
129122
ScriptBlockAst ast = Parser.ParseInput(
130123
untitledScript.Contents,
131124
untitledScript.DocumentUri.ToString(),

test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -527,10 +527,11 @@ await debugService.SetCommandBreakpointsAsync(
527527
Assert.Equal("True > ", prompt.ValueString);
528528
}
529529

530-
[SkippableFact]
531-
public async Task DebuggerBreaksInUntitledScript()
530+
[Theory]
531+
[InlineData("Command")]
532+
[InlineData("Line")]
533+
public async Task DebuggerBreaksInUntitledScript(string breakpointType)
532534
{
533-
Skip.IfNot(VersionUtils.PSEdition == "Core", "Untitled script breakpoints only supported in PowerShell Core");
534535
const string contents = "Write-Output $($MyInvocation.Line)";
535536
const string scriptPath = "untitled:Untitled-1";
536537
Assert.True(ScriptFile.IsUntitledPath(scriptPath));
@@ -539,11 +540,20 @@ public async Task DebuggerBreaksInUntitledScript()
539540
Assert.Equal(contents, scriptFile.Contents);
540541
Assert.True(workspace.TryGetFile(scriptPath, out ScriptFile _));
541542

542-
await debugService.SetCommandBreakpointsAsync(
543-
new[] { CommandBreakpointDetails.Create("Write-Output") });
543+
if (breakpointType == "Command")
544+
{
545+
await debugService.SetCommandBreakpointsAsync(
546+
new[] { CommandBreakpointDetails.Create("Write-Output") });
547+
}
548+
else
549+
{
550+
await debugService.SetLineBreakpointsAsync(
551+
scriptFile,
552+
new[] { BreakpointDetails.Create(scriptPath, 1) });
553+
}
544554

545555
ConfigurationDoneHandler configurationDoneHandler = new(
546-
NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null, psesHost);
556+
NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
547557

548558
Task _ = configurationDoneHandler.LaunchScriptAsync(scriptPath);
549559
await AssertDebuggerStopped(scriptPath, 1);
@@ -565,7 +575,7 @@ await debugService.SetCommandBreakpointsAsync(
565575
public async Task RecordsF5CommandInPowerShellHistory()
566576
{
567577
ConfigurationDoneHandler configurationDoneHandler = new(
568-
NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null, psesHost);
578+
NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
569579
await configurationDoneHandler.LaunchScriptAsync(debugScriptFile.FilePath);
570580

571581
IReadOnlyList<string> historyResult = await psesHost.ExecutePSCommandAsync<string>(
@@ -605,7 +615,7 @@ public async Task RecordsF8CommandInHistory()
605615
public async Task OddFilePathsLaunchCorrectly()
606616
{
607617
ConfigurationDoneHandler configurationDoneHandler = new(
608-
NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null, psesHost);
618+
NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
609619
await configurationDoneHandler.LaunchScriptAsync(oddPathScriptFile.FilePath);
610620

611621
IReadOnlyList<string> historyResult = await psesHost.ExecutePSCommandAsync<string>(

0 commit comments

Comments
 (0)