Skip to content

Fixes #94 - adds support for conditional breakpoints #172

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
Show file tree
Hide file tree
Changes from 1 commit
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
19 changes: 18 additions & 1 deletion src/PowerShellEditorServices.Protocol/DebugAdapter/Breakpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,24 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter
{
public class Breakpoint
{
/// <summary>
/// Gets an boolean indicator that if true, breakpoint could be set
/// (but not necessarily at the desired location).
/// </summary>
public bool Verified { get; set; }

/// <summary>
/// Gets an optional message about the state of the breakpoint. This is shown to the user
/// and can be used to explain why a breakpoint could not be verified.
/// </summary>
public string Message { get; set; }

public string Source { get; set; }

public int Line { get; set; }

public int? Column { get; set; }

private Breakpoint()
{
}
Expand All @@ -20,8 +34,11 @@ public static Breakpoint Create(
{
return new Breakpoint
{
Verified = breakpointDetails.Verified,
Message = breakpointDetails.Message,
Source = breakpointDetails.Source,
Line = breakpointDetails.LineNumber,
Verified = true
Column = breakpointDetails.ColumnNumber
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@

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

public int[] Lines { get; set; }
public SourceBreakpoint[] Breakpoints { get; set; }
}

public class SourceBreakpoint
{
public int Line { get; set; }

public int? Column { get; set; }

public string Condition { get; set; }
}

public class SetBreakpointsResponseBody
{
public Breakpoint[] Breakpoints { get; set; }
}
}

15 changes: 13 additions & 2 deletions src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,21 @@ protected async Task HandleSetBreakpointsRequest(
editorSession.Workspace.GetFile(
setBreakpointsParams.Source.Path);

var breakpointDetails = new BreakpointDetails[setBreakpointsParams.Breakpoints.Length];
for (int i = 0; i < breakpointDetails.Length; i++)
{
SourceBreakpoint srcBreakpoint = setBreakpointsParams.Breakpoints[i];
breakpointDetails[i] = BreakpointDetails.Create(
scriptFile.FilePath,
srcBreakpoint.Line,
srcBreakpoint.Column,
srcBreakpoint.Condition);
}

BreakpointDetails[] breakpoints =
await editorSession.DebugService.SetBreakpoints(
await editorSession.DebugService.SetLineBreakpoints(
scriptFile,
setBreakpointsParams.Lines);
breakpointDetails);

await requestContext.SendResult(
new SetBreakpointsResponseBody
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ await requestContext.SendEvent(
null);

// Now send the Initialize response to continue setup
await requestContext.SendResult(new InitializeResponseBody());
await requestContext.SendResult(
new InitializeResponseBody
{
SupportsConditionalBreakpoints = true,
});
}
}
}
Expand Down
83 changes: 74 additions & 9 deletions src/PowerShellEditorServices/Debugging/BreakpointDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using Microsoft.PowerShell.EditorServices.Utility;
using System;
using System.Management.Automation;
using Microsoft.PowerShell.EditorServices.Utility;

namespace Microsoft.PowerShell.EditorServices
{
Expand All @@ -15,11 +15,68 @@ namespace Microsoft.PowerShell.EditorServices
/// </summary>
public class BreakpointDetails
{
/// <summary>
/// Gets or sets a boolean indicator that if true, breakpoint could be set
/// (but not necessarily at the desired location).
/// </summary>
public bool Verified { get; set; }

/// <summary>
/// Gets or set an optional message about the state of the breakpoint. This is shown to the user
/// and can be used to explain why a breakpoint could not be verified.
/// </summary>
public string Message { get; set; }

/// <summary>
/// Gets the source where the breakpoint is located. Used only for debug purposes.
/// </summary>
public string Source { get; private set; }

/// <summary>
/// Gets the line number at which the breakpoint is set.
/// </summary>
public int LineNumber { get; private set; }

/// <summary>
/// Gets the column number at which the breakpoint is set. If null, the default of 1 is used.
/// </summary>
public int? ColumnNumber { get; private set; }

/// <summary>
/// Gets the breakpoint condition string.
/// </summary>
public string Condition { get; private set; }

private BreakpointDetails()
{
}

/// <summary>
/// Creates an instance of the BreakpointDetails class from the individual
/// pieces of breakpoint information provided by the client.
/// </summary>
/// <param name="source"></param>
/// <param name="line"></param>
/// <param name="column"></param>
/// <param name="condition"></param>
/// <returns></returns>
public static BreakpointDetails Create(
string source,
int line,
int? column = null,
string condition = null)
{
Validate.IsNotNull("source", source);

return new BreakpointDetails
{
Source = source,
LineNumber = line,
ColumnNumber = column,
Condition = condition
};
}

/// <summary>
/// Creates an instance of the BreakpointDetails class from a
/// PowerShell Breakpoint object.
Expand All @@ -31,18 +88,26 @@ public static BreakpointDetails Create(Breakpoint breakpoint)
Validate.IsNotNull("breakpoint", breakpoint);

LineBreakpoint lineBreakpoint = breakpoint as LineBreakpoint;
if (lineBreakpoint != null)
{
return new BreakpointDetails
{
LineNumber = lineBreakpoint.Line
};
}
else
if (lineBreakpoint == null)
{
throw new ArgumentException(
"Expected breakpoint type:" + breakpoint.GetType().Name);
}

var breakpointDetails = new BreakpointDetails
{
Verified = true,
Source = lineBreakpoint.Script,
LineNumber = lineBreakpoint.Line,
Condition = lineBreakpoint.Action?.ToString()
};

if (lineBreakpoint.Column > 0)
{
breakpointDetails.ColumnNumber = lineBreakpoint.Column;
}

return breakpointDetails;
}
}
}
107 changes: 90 additions & 17 deletions src/PowerShellEditorServices/Debugging/DebugService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Language;
using System.Threading.Tasks;
using Microsoft.PowerShell.EditorServices.Utility;

Expand Down Expand Up @@ -57,47 +58,119 @@ public DebugService(PowerShellContext powerShellContext)
#region Public Methods

/// <summary>
/// Sets the list of breakpoints for the current debugging session.
/// Sets the list of line breakpoints for the current debugging session.
/// </summary>
/// <param name="scriptFile">The ScriptFile in which breakpoints will be set.</param>
/// <param name="lineNumbers">The line numbers at which breakpoints will be set.</param>
/// <param name="breakpoints">BreakpointDetails for each breakpoint that will be set.</param>
/// <param name="clearExisting">If true, causes all existing breakpoints to be cleared before setting new ones.</param>
/// <returns>An awaitable Task that will provide details about the breakpoints that were set.</returns>
public async Task<BreakpointDetails[]> SetBreakpoints(
public async Task<BreakpointDetails[]> SetLineBreakpoints(
ScriptFile scriptFile,
int[] lineNumbers,
BreakpointDetails[] breakpoints,
bool clearExisting = true)
{
IEnumerable<Breakpoint> resultBreakpoints = null;
var resultBreakpointDetails = new List<BreakpointDetails>();

if (clearExisting)
{
await this.ClearBreakpointsInFile(scriptFile);
}

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

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

resultBreakpoints =
await this.powerShellContext.ExecuteCommand<Breakpoint>(
psCommand);
if (lineOnlyBreakpoints.Length > 0)
{
PSCommand psCommand = new PSCommand();
psCommand.AddCommand("Set-PSBreakpoint");
psCommand.AddParameter("Script", escapedScriptPath);
psCommand.AddParameter("Line", lineOnlyBreakpoints);

return
resultBreakpoints
.Select(BreakpointDetails.Create)
var configuredBreakpoints =
await this.powerShellContext.ExecuteCommand<Breakpoint>(psCommand);

resultBreakpointDetails.AddRange(
configuredBreakpoints.Select(BreakpointDetails.Create));
}

// Process the rest of the breakpoints
var advancedLineBreakpoints =
breakpoints.Where(b => (b.ColumnNumber != null) || (b.Condition != null))
.ToArray();

foreach (BreakpointDetails breakpoint in advancedLineBreakpoints)
{
PSCommand psCommand = new PSCommand();
psCommand.AddCommand("Set-PSBreakpoint");
psCommand.AddParameter("Script", escapedScriptPath);
psCommand.AddParameter("Line", breakpoint.LineNumber);

// Check if the user has specified the column number for the breakpoint.
if (breakpoint.ColumnNumber.HasValue)
{
// It bums me out that PowerShell will silently ignore a breakpoint
// where either the line or the column is invalid. I'd rather have an
// error message I could rely back to the client.
psCommand.AddParameter("Column", breakpoint.ColumnNumber.Value);
}

// Check if this is a "conditional" line breakpoint.
if (breakpoint.Condition != null)
{
try
{
ScriptBlock actionScriptBlock = ScriptBlock.Create(breakpoint.Condition);

// Check for "advanced" condition syntax i.e. if the user has specified
// a "break" or "continue" statement anywhere in their scriptblock,
// pass their scriptblock through to the Action parameter as-is.
Ast breakOrContinueStatementAst =
actionScriptBlock.Ast.Find(
ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true);

// If this isn't advanced syntax then the conditions string should be a simple
// expression that needs to be wrapped in a "if" test that conditionally executes
// a break statement.
if (breakOrContinueStatementAst == null)
{
string wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}";
actionScriptBlock = ScriptBlock.Create(wrappedCondition);
}

psCommand.AddParameter("Action", actionScriptBlock);
}
catch (ParseException ex)
{
// Failed to create conditional breakpoint likely because the user provided an
// invalid PowerShell expression. Let the user know why.
breakpoint.Verified = false;
breakpoint.Message = ex.Message;
resultBreakpointDetails.Add(breakpoint);
continue;
}
}

IEnumerable<Breakpoint> configuredBreakpoints =
await this.powerShellContext.ExecuteCommand<Breakpoint>(psCommand);

resultBreakpointDetails.AddRange(
configuredBreakpoints.Select(BreakpointDetails.Create));
}
}

return new BreakpointDetails[0];
return resultBreakpointDetails.ToArray();
}

/// <summary>
Expand Down
8 changes: 6 additions & 2 deletions test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public Task DisposeAsync()
}

[Fact]
public async Task DebugAdapterStopsOnBreakpoints()
public async Task DebugAdapterStopsOnLineBreakpoints()
{
await this.SendRequest(
SetBreakpointsRequest.Type,
Expand All @@ -58,7 +58,11 @@ await this.SendRequest(
{
Path = DebugScriptPath
},
Lines = new int[] { 5, 7 }
Breakpoints = new []
{
new SourceBreakpoint { Line = 5 },
new SourceBreakpoint { Line = 7 }
}
});

Task<StoppedEventBody> breakEventTask = this.WaitForEvent(StoppedEvent.Type);
Expand Down
Loading