Skip to content

Display IEnumerables and IDictionaries in debugger prettily (with "Raw View" available) #1634

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 14 commits into from
Jan 24, 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
Expand Up @@ -26,8 +26,7 @@ internal class VariableDetails : VariableDetailsBase
/// Provides a constant for the dollar sign variable prefix string.
/// </summary>
public const string DollarPrefix = "$";

private object valueObject;
protected object ValueObject { get; }
private VariableDetails[] cachedChildren;

#endregion
Expand Down Expand Up @@ -81,7 +80,7 @@ public VariableDetails(PSPropertyInfo psProperty)
/// <param name="value">The variable's value.</param>
public VariableDetails(string name, object value)
{
this.valueObject = value;
this.ValueObject = value;

this.Id = -1; // Not been assigned a variable reference id yet
this.Name = name;
Expand Down Expand Up @@ -109,7 +108,7 @@ public override VariableDetailsBase[] GetChildren(ILogger logger)
{
if (this.cachedChildren == null)
{
this.cachedChildren = GetChildren(this.valueObject, logger);
this.cachedChildren = GetChildren(this.ValueObject, logger);
}

return this.cachedChildren;
Expand Down Expand Up @@ -175,19 +174,18 @@ private static string GetValueStringAndType(object value, bool isExpandable, out
if (value is bool)
{
// Set to identifier recognized by PowerShell to make setVariable from the debug UI more natural.
valueString = (bool) value ? "$true" : "$false";
valueString = (bool)value ? "$true" : "$false";

// We need to use this "magic value" to highlight in vscode properly
// These "magic values" are analagous to TypeScript and are visible in VSCode here:
// https://github.com/microsoft/vscode/blob/57ca9b99d5b6a59f2d2e0f082ae186559f45f1d8/src/vs/workbench/contrib/debug/browser/baseDebugView.ts#L68-L78
// NOTE: we don't do numbers and strings since they (so far) seem to get detected properly by
//serialization, and the original .NET type can be preserved so it shows up in the variable name
//type hover as the original .NET type.
// NOTE: we don't do numbers and strings since they (so far) seem to get detected properly by
// serialization, and the original .NET type can be preserved so it shows up in the variable name
// type hover as the original .NET type.
typeName = "boolean";
}
else if (isExpandable)
{

// Get the "value" for an expandable object.
if (value is DictionaryEntry)
{
Expand Down Expand Up @@ -367,12 +365,19 @@ private VariableDetails[] GetChildren(object obj, ILogger logger)
return childVariables.ToArray();
}

private static void AddDotNetProperties(object obj, List<VariableDetails> childVariables)
protected static void AddDotNetProperties(object obj, List<VariableDetails> childVariables, bool noRawView = false)
{
Type objectType = obj.GetType();
var properties =
objectType.GetProperties(
BindingFlags.Public | BindingFlags.Instance);

// For certain array or dictionary types, we want to hide additional properties under a "raw view" header
// to reduce noise. This is inspired by the C# vscode extension.
if (!noRawView && obj is IEnumerable)
{
childVariables.Add(new VariableDetailsRawView(obj));
return;
}

var properties = objectType.GetProperties(BindingFlags.Public | BindingFlags.Instance);

foreach (var property in properties)
{
Expand Down Expand Up @@ -424,4 +429,25 @@ public override string ToString()
}
}
}

/// <summary>
/// A VariableDetails that only returns the raw view properties of the object, rather than its values.
/// </summary>
internal sealed class VariableDetailsRawView : VariableDetails
{
private const string RawViewName = "Raw View";

public VariableDetailsRawView(object value) : base(RawViewName, value)
{
this.ValueString = "";
this.Type = "";
}

public override VariableDetailsBase[] GetChildren(ILogger logger)
{
List<VariableDetails> childVariables = new();
AddDotNetProperties(ValueObject, childVariables, noRawView: true);
return childVariables.ToArray();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,30 @@ function Test-Variables {
Test-Variables
# NOTE: If a line is added to the function above, the line numbers in the
# associated unit tests MUST be adjusted accordingly.

$SCRIPT:simpleArray = @(
1
2
'red'
'blue'
)

# This is a dummy function that the test will use to stop and evaluate the debug environment
function __BreakDebuggerEnumerableShowsRawView{}; __BreakDebuggerEnumerableShowsRawView

$SCRIPT:simpleDictionary = @{
item1 = 1
item2 = 2
item3 = 'red'
item4 = 'blue'
}
function __BreakDebuggerDictionaryShowsRawView{}; __BreakDebuggerDictionaryShowsRawView

$SCRIPT:sortedDictionary = [Collections.Generic.SortedDictionary[string, object]]::new()
$sortedDictionary[1] = 1
$sortedDictionary[2] = 2
$sortedDictionary['red'] = 'red'
$sortedDictionary['blue'] = 'red'

# This is a dummy function that the test will use to stop and evaluate the debug environment
function __BreakDebuggerDerivedDictionaryPropertyInRawView{}; __BreakDebuggerDerivedDictionaryPropertyInRawView
122 changes: 116 additions & 6 deletions test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ private void AssertDebuggerPaused()

private void AssertDebuggerStopped(
string scriptPath = "",
int lineNumber = -1)
int lineNumber = -1,
CommandBreakpointDetails commandBreakpointDetails = default)
{
var eventArgs = debuggerStoppedQueue.Take(new CancellationTokenSource(5000).Token);

Expand All @@ -132,6 +133,11 @@ private void AssertDebuggerStopped(
{
Assert.Equal(lineNumber, eventArgs.LineNumber);
}

if (commandBreakpointDetails is not null)
{
Assert.Equal(commandBreakpointDetails.Name, eventArgs.OriginalEvent.InvocationInfo.MyCommand.Name);
}
}

private Task<IReadOnlyList<LineBreakpoint>> GetConfirmedBreakpoints(ScriptFile scriptFile)
Expand Down Expand Up @@ -210,7 +216,8 @@ public async Task DebuggerAcceptsScriptArgs()
Assert.True(var.IsExpandable);

var childVars = debugService.GetVariables(var.Id);
Assert.Equal(9, childVars.Length);
// 2 variables plus "Raw View"
Assert.Equal(3, childVars.Length);
Assert.Equal("\"Bar\"", childVars[0].ValueString);
Assert.Equal("\"Baz\"", childVars[1].ValueString);

Expand All @@ -227,7 +234,7 @@ public async Task DebuggerAcceptsScriptArgs()
Assert.True(var.IsExpandable);

childVars = debugService.GetVariables(var.Id);
Assert.Equal(8, childVars.Length);
Assert.Equal(2, childVars.Length);
Assert.Equal("\"Extra1\"", childVars[0].ValueString);
}

Expand Down Expand Up @@ -532,14 +539,15 @@ await debugService.SetLineBreakpointsAsync(
Assert.True(objVar.IsExpandable);

var objChildren = debugService.GetVariables(objVar.Id);
Assert.Equal(9, objChildren.Length);
// Two variables plus "Raw View"
Assert.Equal(3, objChildren.Length);

var arrVar = Array.Find(variables, v => v.Name == "$arrVar");
Assert.NotNull(arrVar);
Assert.True(arrVar.IsExpandable);

var arrChildren = debugService.GetVariables(arrVar.Id);
Assert.Equal(11, arrChildren.Length);
Assert.Equal(5, arrChildren.Length);

var classVar = Array.Find(variables, v => v.Name == "$classVar");
Assert.NotNull(classVar);
Expand Down Expand Up @@ -709,7 +717,8 @@ await debugService.SetLineBreakpointsAsync(
Assert.True(var.IsExpandable);

VariableDetailsBase[] childVars = debugService.GetVariables(var.Id);
Assert.Equal(9, childVars.Length);
// 2 variables plus "Raw View"
Assert.Equal(3, childVars.Length);
Assert.Equal("[0]", childVars[0].Name);
Assert.Equal("[1]", childVars[1].Name);

Expand Down Expand Up @@ -772,6 +781,107 @@ await debugService.SetLineBreakpointsAsync(
Assert.Equal("\"John\"", childVars["Name"]);
}

[Fact]
public async Task DebuggerEnumerableShowsRawView()
{
CommandBreakpointDetails breakpoint = CommandBreakpointDetails.Create("__BreakDebuggerEnumerableShowsRawView");
await debugService.SetCommandBreakpointsAsync(new[] { breakpoint }).ConfigureAwait(true);

// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFile();
AssertDebuggerStopped(commandBreakpointDetails: breakpoint);

VariableDetailsBase simpleArrayVar = Array.Find(
GetVariables(VariableContainerDetails.ScriptScopeName),
v => v.Name == "$simpleArray");
Assert.NotNull(simpleArrayVar);
VariableDetailsBase rawDetailsView = Array.Find(
simpleArrayVar.GetChildren(NullLogger.Instance),
v => v.Name == "Raw View");
Assert.NotNull(rawDetailsView);
Assert.Empty(rawDetailsView.Type);
Assert.Empty(rawDetailsView.ValueString);
VariableDetailsBase[] rawViewChildren = rawDetailsView.GetChildren(NullLogger.Instance);
Assert.Equal(7, rawViewChildren.Length);
Assert.Equal("Length", rawViewChildren[0].Name);
Assert.Equal("4", rawViewChildren[0].ValueString);
Assert.Equal("LongLength", rawViewChildren[1].Name);
Assert.Equal("4", rawViewChildren[1].ValueString);
Assert.Equal("Rank", rawViewChildren[2].Name);
Assert.Equal("1", rawViewChildren[2].ValueString);
Assert.Equal("SyncRoot", rawViewChildren[3].Name);
Assert.Equal("IsReadOnly", rawViewChildren[4].Name);
Assert.Equal("$false", rawViewChildren[4].ValueString);
Assert.Equal("IsFixedSize", rawViewChildren[5].Name);
Assert.Equal("$true", rawViewChildren[5].ValueString);
Assert.Equal("IsSynchronized", rawViewChildren[6].Name);
Assert.Equal("$false", rawViewChildren[6].ValueString);
}

[Fact]
public async Task DebuggerDictionaryShowsRawView()
{
CommandBreakpointDetails breakpoint = CommandBreakpointDetails.Create("__BreakDebuggerDictionaryShowsRawView");
await debugService.SetCommandBreakpointsAsync(new[] { breakpoint }).ConfigureAwait(true);

// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFile();
AssertDebuggerStopped(commandBreakpointDetails: breakpoint);

VariableDetailsBase simpleDictionaryVar = Array.Find(
GetVariables(VariableContainerDetails.ScriptScopeName),
v => v.Name == "$simpleDictionary");
Assert.NotNull(simpleDictionaryVar);
VariableDetailsBase rawDetailsView = Array.Find(
simpleDictionaryVar.GetChildren(NullLogger.Instance),
v => v.Name == "Raw View");
Assert.NotNull(rawDetailsView);
Assert.Empty(rawDetailsView.Type);
Assert.Empty(rawDetailsView.ValueString);
VariableDetailsBase[] rawViewChildren = rawDetailsView.GetChildren(NullLogger.Instance);
Assert.Equal(7, rawViewChildren.Length);
Assert.Equal("IsReadOnly", rawViewChildren[0].Name);
Assert.Equal("$false", rawViewChildren[0].ValueString);
Assert.Equal("IsFixedSize", rawViewChildren[1].Name);
Assert.Equal("$false", rawViewChildren[1].ValueString);
Assert.Equal("IsSynchronized", rawViewChildren[2].Name);
Assert.Equal("$false", rawViewChildren[2].ValueString);
Assert.Equal("Keys", rawViewChildren[3].Name);
Assert.Equal("Values", rawViewChildren[4].Name);
Assert.Equal("[ValueCollection: 4]", rawViewChildren[4].ValueString);
Assert.Equal("SyncRoot", rawViewChildren[5].Name);
Assert.Equal("Count", rawViewChildren[6].Name);
Assert.Equal("4", rawViewChildren[6].ValueString);
}

[Fact]
public async Task DebuggerDerivedDictionaryPropertyInRawView()
{
CommandBreakpointDetails breakpoint = CommandBreakpointDetails.Create("__BreakDebuggerDerivedDictionaryPropertyInRawView");
await debugService.SetCommandBreakpointsAsync(new[] { breakpoint }).ConfigureAwait(true);

// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFile();
AssertDebuggerStopped(commandBreakpointDetails: breakpoint);

VariableDetailsBase sortedDictionaryVar = Array.Find(
GetVariables(VariableContainerDetails.ScriptScopeName),
v => v.Name == "$sortedDictionary");
Assert.NotNull(sortedDictionaryVar);
VariableDetailsBase[] simpleDictionaryChildren = sortedDictionaryVar.GetChildren(NullLogger.Instance);
// 4 items + Raw View
Assert.Equal(5, simpleDictionaryChildren.Length);
VariableDetailsBase rawDetailsView = Array.Find(
simpleDictionaryChildren,
v => v.Name == "Raw View");
Assert.NotNull(rawDetailsView);
Assert.Empty(rawDetailsView.Type);
Assert.Empty(rawDetailsView.ValueString);
VariableDetailsBase[] rawViewChildren = rawDetailsView.GetChildren(NullLogger.Instance);
Assert.Equal(4, rawViewChildren.Length);
Assert.NotNull(Array.Find(rawViewChildren, v => v .Name == "Comparer"));
}

[Fact]
public async Task DebuggerVariablePSCustomObjectDisplaysCorrectly()
{
Expand Down