Skip to content

Commit 3abde2b

Browse files
committed
Fixes #94 - adds support for conditional breakpoints
This adds some unit tests for conditional breakpoints and refactors some existing unit tests I wrote to be a bit more DRY. Also, VSCode supports column breakpoints in its protocol. And PowerShell supports breakpoints on columns other than 1. So why don't I see any UI in VSCode to create a breakpoint at a specific column? Is there something we have to do to declare support for column breakpoints?
1 parent f901c6a commit 3abde2b

File tree

8 files changed

+358
-118
lines changed

8 files changed

+358
-118
lines changed

src/PowerShellEditorServices.Protocol/DebugAdapter/Breakpoint.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,24 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter
77
{
88
public class Breakpoint
99
{
10+
/// <summary>
11+
/// Gets an boolean indicator that if true, breakpoint could be set
12+
/// (but not necessarily at the desired location).
13+
/// </summary>
1014
public bool Verified { get; set; }
1115

16+
/// <summary>
17+
/// Gets an optional message about the state of the breakpoint. This is shown to the user
18+
/// and can be used to explain why a breakpoint could not be verified.
19+
/// </summary>
20+
public string Message { get; set; }
21+
22+
public string Source { get; set; }
23+
1224
public int Line { get; set; }
1325

26+
public int? Column { get; set; }
27+
1428
private Breakpoint()
1529
{
1630
}
@@ -20,8 +34,11 @@ public static Breakpoint Create(
2034
{
2135
return new Breakpoint
2236
{
37+
Verified = breakpointDetails.Verified,
38+
Message = breakpointDetails.Message,
39+
Source = breakpointDetails.Source,
2340
Line = breakpointDetails.LineNumber,
24-
Verified = true
41+
Column = breakpointDetails.ColumnNumber
2542
};
2643
}
2744
}

src/PowerShellEditorServices.Protocol/DebugAdapter/SetBreakpointsRequest.cs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77

88
namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter
99
{
10-
// /** SetBreakpoints request; value of command field is "setBreakpoints".
11-
// Sets multiple breakpoints for a single source and clears all previous breakpoints in that source.
12-
// To clear all breakpoint for a source, specify an empty array.
13-
// When a breakpoint is hit, a StoppedEvent (event type 'breakpoint') is generated.
14-
// */
10+
/// <summary>
11+
/// SetBreakpoints request; value of command field is "setBreakpoints".
12+
/// Sets multiple breakpoints for a single source and clears all previous breakpoints in that source.
13+
/// To clear all breakpoint for a source, specify an empty array.
14+
/// When a breakpoint is hit, a StoppedEvent (event type 'breakpoint') is generated.
15+
/// </summary>
1516
public class SetBreakpointsRequest
1617
{
1718
public static readonly
@@ -23,12 +24,20 @@ public class SetBreakpointsRequestArguments
2324
{
2425
public Source Source { get; set; }
2526

26-
public int[] Lines { get; set; }
27+
public SourceBreakpoint[] Breakpoints { get; set; }
28+
}
29+
30+
public class SourceBreakpoint
31+
{
32+
public int Line { get; set; }
33+
34+
public int? Column { get; set; }
35+
36+
public string Condition { get; set; }
2737
}
2838

2939
public class SetBreakpointsResponseBody
3040
{
3141
public Breakpoint[] Breakpoints { get; set; }
3242
}
3343
}
34-

src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,21 @@ protected async Task HandleSetBreakpointsRequest(
172172
editorSession.Workspace.GetFile(
173173
setBreakpointsParams.Source.Path);
174174

175+
var breakpointDetails = new BreakpointDetails[setBreakpointsParams.Breakpoints.Length];
176+
for (int i = 0; i < breakpointDetails.Length; i++)
177+
{
178+
SourceBreakpoint srcBreakpoint = setBreakpointsParams.Breakpoints[i];
179+
breakpointDetails[i] = BreakpointDetails.Create(
180+
scriptFile.FilePath,
181+
srcBreakpoint.Line,
182+
srcBreakpoint.Column,
183+
srcBreakpoint.Condition);
184+
}
185+
175186
BreakpointDetails[] breakpoints =
176-
await editorSession.DebugService.SetBreakpoints(
187+
await editorSession.DebugService.SetLineBreakpoints(
177188
scriptFile,
178-
setBreakpointsParams.Lines);
189+
breakpointDetails);
179190

180191
await requestContext.SendResult(
181192
new SetBreakpointsResponseBody

src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ await requestContext.SendEvent(
6060
null);
6161

6262
// Now send the Initialize response to continue setup
63-
await requestContext.SendResult(new InitializeResponseBody());
63+
await requestContext.SendResult(
64+
new InitializeResponseBody
65+
{
66+
SupportsConditionalBreakpoints = true,
67+
});
6468
}
6569
}
6670
}

src/PowerShellEditorServices/Debugging/BreakpointDetails.cs

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
44
//
55

6-
using Microsoft.PowerShell.EditorServices.Utility;
76
using System;
87
using System.Management.Automation;
8+
using Microsoft.PowerShell.EditorServices.Utility;
99

1010
namespace Microsoft.PowerShell.EditorServices
1111
{
@@ -15,11 +15,68 @@ namespace Microsoft.PowerShell.EditorServices
1515
/// </summary>
1616
public class BreakpointDetails
1717
{
18+
/// <summary>
19+
/// Gets or sets a boolean indicator that if true, breakpoint could be set
20+
/// (but not necessarily at the desired location).
21+
/// </summary>
22+
public bool Verified { get; set; }
23+
24+
/// <summary>
25+
/// Gets or set an optional message about the state of the breakpoint. This is shown to the user
26+
/// and can be used to explain why a breakpoint could not be verified.
27+
/// </summary>
28+
public string Message { get; set; }
29+
30+
/// <summary>
31+
/// Gets the source where the breakpoint is located. Used only for debug purposes.
32+
/// </summary>
33+
public string Source { get; private set; }
34+
1835
/// <summary>
1936
/// Gets the line number at which the breakpoint is set.
2037
/// </summary>
2138
public int LineNumber { get; private set; }
2239

40+
/// <summary>
41+
/// Gets the column number at which the breakpoint is set. If null, the default of 1 is used.
42+
/// </summary>
43+
public int? ColumnNumber { get; private set; }
44+
45+
/// <summary>
46+
/// Gets the breakpoint condition string.
47+
/// </summary>
48+
public string Condition { get; private set; }
49+
50+
private BreakpointDetails()
51+
{
52+
}
53+
54+
/// <summary>
55+
/// Creates an instance of the BreakpointDetails class from the individual
56+
/// pieces of breakpoint information provided by the client.
57+
/// </summary>
58+
/// <param name="source"></param>
59+
/// <param name="line"></param>
60+
/// <param name="column"></param>
61+
/// <param name="condition"></param>
62+
/// <returns></returns>
63+
public static BreakpointDetails Create(
64+
string source,
65+
int line,
66+
int? column = null,
67+
string condition = null)
68+
{
69+
Validate.IsNotNull("source", source);
70+
71+
return new BreakpointDetails
72+
{
73+
Source = source,
74+
LineNumber = line,
75+
ColumnNumber = column,
76+
Condition = condition
77+
};
78+
}
79+
2380
/// <summary>
2481
/// Creates an instance of the BreakpointDetails class from a
2582
/// PowerShell Breakpoint object.
@@ -31,18 +88,26 @@ public static BreakpointDetails Create(Breakpoint breakpoint)
3188
Validate.IsNotNull("breakpoint", breakpoint);
3289

3390
LineBreakpoint lineBreakpoint = breakpoint as LineBreakpoint;
34-
if (lineBreakpoint != null)
35-
{
36-
return new BreakpointDetails
37-
{
38-
LineNumber = lineBreakpoint.Line
39-
};
40-
}
41-
else
91+
if (lineBreakpoint == null)
4292
{
4393
throw new ArgumentException(
4494
"Expected breakpoint type:" + breakpoint.GetType().Name);
4595
}
96+
97+
var breakpointDetails = new BreakpointDetails
98+
{
99+
Verified = true,
100+
Source = lineBreakpoint.Script,
101+
LineNumber = lineBreakpoint.Line,
102+
Condition = lineBreakpoint.Action?.ToString()
103+
};
104+
105+
if (lineBreakpoint.Column > 0)
106+
{
107+
breakpointDetails.ColumnNumber = lineBreakpoint.Column;
108+
}
109+
110+
return breakpointDetails;
46111
}
47112
}
48113
}

src/PowerShellEditorServices/Debugging/DebugService.cs

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Collections.Generic;
88
using System.Linq;
99
using System.Management.Automation;
10+
using System.Management.Automation.Language;
1011
using System.Threading.Tasks;
1112
using Microsoft.PowerShell.EditorServices.Utility;
1213

@@ -57,47 +58,119 @@ public DebugService(PowerShellContext powerShellContext)
5758
#region Public Methods
5859

5960
/// <summary>
60-
/// Sets the list of breakpoints for the current debugging session.
61+
/// Sets the list of line breakpoints for the current debugging session.
6162
/// </summary>
6263
/// <param name="scriptFile">The ScriptFile in which breakpoints will be set.</param>
63-
/// <param name="lineNumbers">The line numbers at which breakpoints will be set.</param>
64+
/// <param name="breakpoints">BreakpointDetails for each breakpoint that will be set.</param>
6465
/// <param name="clearExisting">If true, causes all existing breakpoints to be cleared before setting new ones.</param>
6566
/// <returns>An awaitable Task that will provide details about the breakpoints that were set.</returns>
66-
public async Task<BreakpointDetails[]> SetBreakpoints(
67+
public async Task<BreakpointDetails[]> SetLineBreakpoints(
6768
ScriptFile scriptFile,
68-
int[] lineNumbers,
69+
BreakpointDetails[] breakpoints,
6970
bool clearExisting = true)
7071
{
71-
IEnumerable<Breakpoint> resultBreakpoints = null;
72+
var resultBreakpointDetails = new List<BreakpointDetails>();
7273

7374
if (clearExisting)
7475
{
7576
await this.ClearBreakpointsInFile(scriptFile);
7677
}
7778

78-
if (lineNumbers.Length > 0)
79+
if (breakpoints.Length > 0)
7980
{
8081
// Fix for issue #123 - file paths that contain wildcard chars [ and ] need to
8182
// quoted and have those wildcard chars escaped.
8283
string escapedScriptPath =
8384
PowerShellContext.EscapePath(scriptFile.FilePath, escapeSpaces: false);
8485

85-
PSCommand psCommand = new PSCommand();
86-
psCommand.AddCommand("Set-PSBreakpoint");
87-
psCommand.AddParameter("Script", escapedScriptPath);
88-
psCommand.AddParameter("Line", lineNumbers.Length > 0 ? lineNumbers : null);
86+
// Line breakpoints with no condition and no column number are the most common,
87+
// so let's optimize for that case by making a single call to Set-PSBreakpoint
88+
// with all the lines to set a breakpoint on.
89+
int[] lineOnlyBreakpoints =
90+
breakpoints.Where(b => (b.ColumnNumber == null) && (b.Condition == null))
91+
.Select(b => b.LineNumber)
92+
.ToArray();
8993

90-
resultBreakpoints =
91-
await this.powerShellContext.ExecuteCommand<Breakpoint>(
92-
psCommand);
94+
if (lineOnlyBreakpoints.Length > 0)
95+
{
96+
PSCommand psCommand = new PSCommand();
97+
psCommand.AddCommand("Set-PSBreakpoint");
98+
psCommand.AddParameter("Script", escapedScriptPath);
99+
psCommand.AddParameter("Line", lineOnlyBreakpoints);
93100

94-
return
95-
resultBreakpoints
96-
.Select(BreakpointDetails.Create)
101+
var configuredBreakpoints =
102+
await this.powerShellContext.ExecuteCommand<Breakpoint>(psCommand);
103+
104+
resultBreakpointDetails.AddRange(
105+
configuredBreakpoints.Select(BreakpointDetails.Create));
106+
}
107+
108+
// Process the rest of the breakpoints
109+
var advancedLineBreakpoints =
110+
breakpoints.Where(b => (b.ColumnNumber != null) || (b.Condition != null))
97111
.ToArray();
112+
113+
foreach (BreakpointDetails breakpoint in advancedLineBreakpoints)
114+
{
115+
PSCommand psCommand = new PSCommand();
116+
psCommand.AddCommand("Set-PSBreakpoint");
117+
psCommand.AddParameter("Script", escapedScriptPath);
118+
psCommand.AddParameter("Line", breakpoint.LineNumber);
119+
120+
// Check if the user has specified the column number for the breakpoint.
121+
if (breakpoint.ColumnNumber.HasValue)
122+
{
123+
// It bums me out that PowerShell will silently ignore a breakpoint
124+
// where either the line or the column is invalid. I'd rather have an
125+
// error message I could rely back to the client.
126+
psCommand.AddParameter("Column", breakpoint.ColumnNumber.Value);
127+
}
128+
129+
// Check if this is a "conditional" line breakpoint.
130+
if (breakpoint.Condition != null)
131+
{
132+
try
133+
{
134+
ScriptBlock actionScriptBlock = ScriptBlock.Create(breakpoint.Condition);
135+
136+
// Check for "advanced" condition syntax i.e. if the user has specified
137+
// a "break" or "continue" statement anywhere in their scriptblock,
138+
// pass their scriptblock through to the Action parameter as-is.
139+
Ast breakOrContinueStatementAst =
140+
actionScriptBlock.Ast.Find(
141+
ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true);
142+
143+
// If this isn't advanced syntax then the conditions string should be a simple
144+
// expression that needs to be wrapped in a "if" test that conditionally executes
145+
// a break statement.
146+
if (breakOrContinueStatementAst == null)
147+
{
148+
string wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}";
149+
actionScriptBlock = ScriptBlock.Create(wrappedCondition);
150+
}
151+
152+
psCommand.AddParameter("Action", actionScriptBlock);
153+
}
154+
catch (ParseException ex)
155+
{
156+
// Failed to create conditional breakpoint likely because the user provided an
157+
// invalid PowerShell expression. Let the user know why.
158+
breakpoint.Verified = false;
159+
breakpoint.Message = ex.Message;
160+
resultBreakpointDetails.Add(breakpoint);
161+
continue;
162+
}
163+
}
164+
165+
IEnumerable<Breakpoint> configuredBreakpoints =
166+
await this.powerShellContext.ExecuteCommand<Breakpoint>(psCommand);
167+
168+
resultBreakpointDetails.AddRange(
169+
configuredBreakpoints.Select(BreakpointDetails.Create));
170+
}
98171
}
99172

100-
return new BreakpointDetails[0];
173+
return resultBreakpointDetails.ToArray();
101174
}
102175

103176
/// <summary>

test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public Task DisposeAsync()
4848
}
4949

5050
[Fact]
51-
public async Task DebugAdapterStopsOnBreakpoints()
51+
public async Task DebugAdapterStopsOnLineBreakpoints()
5252
{
5353
await this.SendRequest(
5454
SetBreakpointsRequest.Type,
@@ -58,7 +58,11 @@ await this.SendRequest(
5858
{
5959
Path = DebugScriptPath
6060
},
61-
Lines = new int[] { 5, 7 }
61+
Breakpoints = new []
62+
{
63+
new SourceBreakpoint { Line = 5 },
64+
new SourceBreakpoint { Line = 7 }
65+
}
6266
});
6367

6468
Task<StoppedEventBody> breakEventTask = this.WaitForEvent(StoppedEvent.Type);

0 commit comments

Comments
 (0)