diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs
index 0b17ba8f7..739885883 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs
@@ -26,8 +26,7 @@ internal class VariableDetails : VariableDetailsBase
/// Provides a constant for the dollar sign variable prefix string.
///
public const string DollarPrefix = "$";
-
- private object valueObject;
+ protected object ValueObject { get; }
private VariableDetails[] cachedChildren;
#endregion
@@ -81,7 +80,7 @@ public VariableDetails(PSPropertyInfo psProperty)
/// The variable's value.
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;
@@ -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;
@@ -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)
{
@@ -367,12 +365,19 @@ private VariableDetails[] GetChildren(object obj, ILogger logger)
return childVariables.ToArray();
}
- private static void AddDotNetProperties(object obj, List childVariables)
+ protected static void AddDotNetProperties(object obj, List 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)
{
@@ -424,4 +429,25 @@ public override string ToString()
}
}
}
+
+ ///
+ /// A VariableDetails that only returns the raw view properties of the object, rather than its values.
+ ///
+ 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 childVariables = new();
+ AddDotNetProperties(ValueObject, childVariables, noRawView: true);
+ return childVariables.ToArray();
+ }
+ }
}
diff --git a/test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1 b/test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1
index 63bf044dc..e8c23d9a0 100644
--- a/test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1
+++ b/test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1
@@ -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
diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs
index 51e2f49ca..d8507afba 100644
--- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs
+++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs
@@ -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);
@@ -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> GetConfirmedBreakpoints(ScriptFile scriptFile)
@@ -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);
@@ -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);
}
@@ -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);
@@ -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);
@@ -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()
{