Skip to content

Commit 3ea4eb9

Browse files
JustinGroteSeeminglyScienceandyleejordan
authored
Display IEnumerables and IDictionaries in debugger prettily (with "Raw View" available) (#1634)
This takes inspiration from the C# extension to display enumerables and dictionaries in a human-friendly and "pretty" manner in the debugger. For advanced debugging, the "Raw View" is available under a pseudo-variable of the same name. Co-authored-by: Patrick Meinecke <[email protected]> Co-authored-by: Andy Schwartzmeyer <[email protected]>
1 parent 47ce69f commit 3ea4eb9

File tree

3 files changed

+182
-19
lines changed

3 files changed

+182
-19
lines changed

src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs

+39-13
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ internal class VariableDetails : VariableDetailsBase
2626
/// Provides a constant for the dollar sign variable prefix string.
2727
/// </summary>
2828
public const string DollarPrefix = "$";
29-
30-
private object valueObject;
29+
protected object ValueObject { get; }
3130
private VariableDetails[] cachedChildren;
3231

3332
#endregion
@@ -81,7 +80,7 @@ public VariableDetails(PSPropertyInfo psProperty)
8180
/// <param name="value">The variable's value.</param>
8281
public VariableDetails(string name, object value)
8382
{
84-
this.valueObject = value;
83+
this.ValueObject = value;
8584

8685
this.Id = -1; // Not been assigned a variable reference id yet
8786
this.Name = name;
@@ -109,7 +108,7 @@ public override VariableDetailsBase[] GetChildren(ILogger logger)
109108
{
110109
if (this.cachedChildren == null)
111110
{
112-
this.cachedChildren = GetChildren(this.valueObject, logger);
111+
this.cachedChildren = GetChildren(this.ValueObject, logger);
113112
}
114113

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

180179
// We need to use this "magic value" to highlight in vscode properly
181180
// These "magic values" are analagous to TypeScript and are visible in VSCode here:
182181
// https://github.com/microsoft/vscode/blob/57ca9b99d5b6a59f2d2e0f082ae186559f45f1d8/src/vs/workbench/contrib/debug/browser/baseDebugView.ts#L68-L78
183-
// NOTE: we don't do numbers and strings since they (so far) seem to get detected properly by
184-
//serialization, and the original .NET type can be preserved so it shows up in the variable name
185-
//type hover as the original .NET type.
182+
// NOTE: we don't do numbers and strings since they (so far) seem to get detected properly by
183+
// serialization, and the original .NET type can be preserved so it shows up in the variable name
184+
// type hover as the original .NET type.
186185
typeName = "boolean";
187186
}
188187
else if (isExpandable)
189188
{
190-
191189
// Get the "value" for an expandable object.
192190
if (value is DictionaryEntry)
193191
{
@@ -367,12 +365,19 @@ private VariableDetails[] GetChildren(object obj, ILogger logger)
367365
return childVariables.ToArray();
368366
}
369367

370-
private static void AddDotNetProperties(object obj, List<VariableDetails> childVariables)
368+
protected static void AddDotNetProperties(object obj, List<VariableDetails> childVariables, bool noRawView = false)
371369
{
372370
Type objectType = obj.GetType();
373-
var properties =
374-
objectType.GetProperties(
375-
BindingFlags.Public | BindingFlags.Instance);
371+
372+
// For certain array or dictionary types, we want to hide additional properties under a "raw view" header
373+
// to reduce noise. This is inspired by the C# vscode extension.
374+
if (!noRawView && obj is IEnumerable)
375+
{
376+
childVariables.Add(new VariableDetailsRawView(obj));
377+
return;
378+
}
379+
380+
var properties = objectType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
376381

377382
foreach (var property in properties)
378383
{
@@ -424,4 +429,25 @@ public override string ToString()
424429
}
425430
}
426431
}
432+
433+
/// <summary>
434+
/// A VariableDetails that only returns the raw view properties of the object, rather than its values.
435+
/// </summary>
436+
internal sealed class VariableDetailsRawView : VariableDetails
437+
{
438+
private const string RawViewName = "Raw View";
439+
440+
public VariableDetailsRawView(object value) : base(RawViewName, value)
441+
{
442+
this.ValueString = "";
443+
this.Type = "";
444+
}
445+
446+
public override VariableDetailsBase[] GetChildren(ILogger logger)
447+
{
448+
List<VariableDetails> childVariables = new();
449+
AddDotNetProperties(ValueObject, childVariables, noRawView: true);
450+
return childVariables.ToArray();
451+
}
452+
}
427453
}

test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1

+27
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,30 @@ function Test-Variables {
2424
Test-Variables
2525
# NOTE: If a line is added to the function above, the line numbers in the
2626
# associated unit tests MUST be adjusted accordingly.
27+
28+
$SCRIPT:simpleArray = @(
29+
1
30+
2
31+
'red'
32+
'blue'
33+
)
34+
35+
# This is a dummy function that the test will use to stop and evaluate the debug environment
36+
function __BreakDebuggerEnumerableShowsRawView{}; __BreakDebuggerEnumerableShowsRawView
37+
38+
$SCRIPT:simpleDictionary = @{
39+
item1 = 1
40+
item2 = 2
41+
item3 = 'red'
42+
item4 = 'blue'
43+
}
44+
function __BreakDebuggerDictionaryShowsRawView{}; __BreakDebuggerDictionaryShowsRawView
45+
46+
$SCRIPT:sortedDictionary = [Collections.Generic.SortedDictionary[string, object]]::new()
47+
$sortedDictionary[1] = 1
48+
$sortedDictionary[2] = 2
49+
$sortedDictionary['red'] = 'red'
50+
$sortedDictionary['blue'] = 'red'
51+
52+
# This is a dummy function that the test will use to stop and evaluate the debug environment
53+
function __BreakDebuggerDerivedDictionaryPropertyInRawView{}; __BreakDebuggerDerivedDictionaryPropertyInRawView

test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs

+116-6
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ private void AssertDebuggerPaused()
112112

113113
private void AssertDebuggerStopped(
114114
string scriptPath = "",
115-
int lineNumber = -1)
115+
int lineNumber = -1,
116+
CommandBreakpointDetails commandBreakpointDetails = default)
116117
{
117118
var eventArgs = debuggerStoppedQueue.Take(new CancellationTokenSource(5000).Token);
118119

@@ -132,6 +133,11 @@ private void AssertDebuggerStopped(
132133
{
133134
Assert.Equal(lineNumber, eventArgs.LineNumber);
134135
}
136+
137+
if (commandBreakpointDetails is not null)
138+
{
139+
Assert.Equal(commandBreakpointDetails.Name, eventArgs.OriginalEvent.InvocationInfo.MyCommand.Name);
140+
}
135141
}
136142

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

212218
var childVars = debugService.GetVariables(var.Id);
213-
Assert.Equal(9, childVars.Length);
219+
// 2 variables plus "Raw View"
220+
Assert.Equal(3, childVars.Length);
214221
Assert.Equal("\"Bar\"", childVars[0].ValueString);
215222
Assert.Equal("\"Baz\"", childVars[1].ValueString);
216223

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

229236
childVars = debugService.GetVariables(var.Id);
230-
Assert.Equal(8, childVars.Length);
237+
Assert.Equal(2, childVars.Length);
231238
Assert.Equal("\"Extra1\"", childVars[0].ValueString);
232239
}
233240

@@ -532,14 +539,15 @@ await debugService.SetLineBreakpointsAsync(
532539
Assert.True(objVar.IsExpandable);
533540

534541
var objChildren = debugService.GetVariables(objVar.Id);
535-
Assert.Equal(9, objChildren.Length);
542+
// Two variables plus "Raw View"
543+
Assert.Equal(3, objChildren.Length);
536544

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

541549
var arrChildren = debugService.GetVariables(arrVar.Id);
542-
Assert.Equal(11, arrChildren.Length);
550+
Assert.Equal(5, arrChildren.Length);
543551

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

711719
VariableDetailsBase[] childVars = debugService.GetVariables(var.Id);
712-
Assert.Equal(9, childVars.Length);
720+
// 2 variables plus "Raw View"
721+
Assert.Equal(3, childVars.Length);
713722
Assert.Equal("[0]", childVars[0].Name);
714723
Assert.Equal("[1]", childVars[1].Name);
715724

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

784+
[Fact]
785+
public async Task DebuggerEnumerableShowsRawView()
786+
{
787+
CommandBreakpointDetails breakpoint = CommandBreakpointDetails.Create("__BreakDebuggerEnumerableShowsRawView");
788+
await debugService.SetCommandBreakpointsAsync(new[] { breakpoint }).ConfigureAwait(true);
789+
790+
// Execute the script and wait for the breakpoint to be hit
791+
Task _ = ExecuteVariableScriptFile();
792+
AssertDebuggerStopped(commandBreakpointDetails: breakpoint);
793+
794+
VariableDetailsBase simpleArrayVar = Array.Find(
795+
GetVariables(VariableContainerDetails.ScriptScopeName),
796+
v => v.Name == "$simpleArray");
797+
Assert.NotNull(simpleArrayVar);
798+
VariableDetailsBase rawDetailsView = Array.Find(
799+
simpleArrayVar.GetChildren(NullLogger.Instance),
800+
v => v.Name == "Raw View");
801+
Assert.NotNull(rawDetailsView);
802+
Assert.Empty(rawDetailsView.Type);
803+
Assert.Empty(rawDetailsView.ValueString);
804+
VariableDetailsBase[] rawViewChildren = rawDetailsView.GetChildren(NullLogger.Instance);
805+
Assert.Equal(7, rawViewChildren.Length);
806+
Assert.Equal("Length", rawViewChildren[0].Name);
807+
Assert.Equal("4", rawViewChildren[0].ValueString);
808+
Assert.Equal("LongLength", rawViewChildren[1].Name);
809+
Assert.Equal("4", rawViewChildren[1].ValueString);
810+
Assert.Equal("Rank", rawViewChildren[2].Name);
811+
Assert.Equal("1", rawViewChildren[2].ValueString);
812+
Assert.Equal("SyncRoot", rawViewChildren[3].Name);
813+
Assert.Equal("IsReadOnly", rawViewChildren[4].Name);
814+
Assert.Equal("$false", rawViewChildren[4].ValueString);
815+
Assert.Equal("IsFixedSize", rawViewChildren[5].Name);
816+
Assert.Equal("$true", rawViewChildren[5].ValueString);
817+
Assert.Equal("IsSynchronized", rawViewChildren[6].Name);
818+
Assert.Equal("$false", rawViewChildren[6].ValueString);
819+
}
820+
821+
[Fact]
822+
public async Task DebuggerDictionaryShowsRawView()
823+
{
824+
CommandBreakpointDetails breakpoint = CommandBreakpointDetails.Create("__BreakDebuggerDictionaryShowsRawView");
825+
await debugService.SetCommandBreakpointsAsync(new[] { breakpoint }).ConfigureAwait(true);
826+
827+
// Execute the script and wait for the breakpoint to be hit
828+
Task _ = ExecuteVariableScriptFile();
829+
AssertDebuggerStopped(commandBreakpointDetails: breakpoint);
830+
831+
VariableDetailsBase simpleDictionaryVar = Array.Find(
832+
GetVariables(VariableContainerDetails.ScriptScopeName),
833+
v => v.Name == "$simpleDictionary");
834+
Assert.NotNull(simpleDictionaryVar);
835+
VariableDetailsBase rawDetailsView = Array.Find(
836+
simpleDictionaryVar.GetChildren(NullLogger.Instance),
837+
v => v.Name == "Raw View");
838+
Assert.NotNull(rawDetailsView);
839+
Assert.Empty(rawDetailsView.Type);
840+
Assert.Empty(rawDetailsView.ValueString);
841+
VariableDetailsBase[] rawViewChildren = rawDetailsView.GetChildren(NullLogger.Instance);
842+
Assert.Equal(7, rawViewChildren.Length);
843+
Assert.Equal("IsReadOnly", rawViewChildren[0].Name);
844+
Assert.Equal("$false", rawViewChildren[0].ValueString);
845+
Assert.Equal("IsFixedSize", rawViewChildren[1].Name);
846+
Assert.Equal("$false", rawViewChildren[1].ValueString);
847+
Assert.Equal("IsSynchronized", rawViewChildren[2].Name);
848+
Assert.Equal("$false", rawViewChildren[2].ValueString);
849+
Assert.Equal("Keys", rawViewChildren[3].Name);
850+
Assert.Equal("Values", rawViewChildren[4].Name);
851+
Assert.Equal("[ValueCollection: 4]", rawViewChildren[4].ValueString);
852+
Assert.Equal("SyncRoot", rawViewChildren[5].Name);
853+
Assert.Equal("Count", rawViewChildren[6].Name);
854+
Assert.Equal("4", rawViewChildren[6].ValueString);
855+
}
856+
857+
[Fact]
858+
public async Task DebuggerDerivedDictionaryPropertyInRawView()
859+
{
860+
CommandBreakpointDetails breakpoint = CommandBreakpointDetails.Create("__BreakDebuggerDerivedDictionaryPropertyInRawView");
861+
await debugService.SetCommandBreakpointsAsync(new[] { breakpoint }).ConfigureAwait(true);
862+
863+
// Execute the script and wait for the breakpoint to be hit
864+
Task _ = ExecuteVariableScriptFile();
865+
AssertDebuggerStopped(commandBreakpointDetails: breakpoint);
866+
867+
VariableDetailsBase sortedDictionaryVar = Array.Find(
868+
GetVariables(VariableContainerDetails.ScriptScopeName),
869+
v => v.Name == "$sortedDictionary");
870+
Assert.NotNull(sortedDictionaryVar);
871+
VariableDetailsBase[] simpleDictionaryChildren = sortedDictionaryVar.GetChildren(NullLogger.Instance);
872+
// 4 items + Raw View
873+
Assert.Equal(5, simpleDictionaryChildren.Length);
874+
VariableDetailsBase rawDetailsView = Array.Find(
875+
simpleDictionaryChildren,
876+
v => v.Name == "Raw View");
877+
Assert.NotNull(rawDetailsView);
878+
Assert.Empty(rawDetailsView.Type);
879+
Assert.Empty(rawDetailsView.ValueString);
880+
VariableDetailsBase[] rawViewChildren = rawDetailsView.GetChildren(NullLogger.Instance);
881+
Assert.Equal(4, rawViewChildren.Length);
882+
Assert.NotNull(Array.Find(rawViewChildren, v => v .Name == "Comparer"));
883+
}
884+
775885
[Fact]
776886
public async Task DebuggerVariablePSCustomObjectDisplaysCorrectly()
777887
{

0 commit comments

Comments
 (0)