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() {