diff --git a/src/PowerShellEditorServices.Host/DebugAdapter.cs b/src/PowerShellEditorServices.Host/DebugAdapter.cs index d18395a38..87a252f97 100644 --- a/src/PowerShellEditorServices.Host/DebugAdapter.cs +++ b/src/PowerShellEditorServices.Host/DebugAdapter.cs @@ -276,7 +276,7 @@ protected async Task HandleStackTraceRequest( newStackFrames.Add( StackFrame.Create( stackFrames[i], - i + 1)); + i)); } await requestContext.SendResult( @@ -310,7 +310,7 @@ protected async Task HandleVariablesRequest( EditorSession editorSession, RequestContext requestContext) { - VariableDetails[] variables = + VariableDetailsBase[] variables = editorSession.DebugService.GetVariables( variablesParams.VariablesReference); diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/ScopesRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/ScopesRequest.cs index 452cf0bad..b9cdc85ed 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/ScopesRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/ScopesRequest.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System.Diagnostics; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter @@ -14,6 +15,7 @@ public static readonly RequestType.Create("scopes"); } + [DebuggerDisplay("FrameId = {FrameId}")] public class ScopesRequestArguments { public int FrameId { get; set; } diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/StackTraceRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/StackTraceRequest.cs index 444c5cdb7..b16fb6a70 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/StackTraceRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/StackTraceRequest.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System.Diagnostics; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter @@ -14,6 +15,7 @@ public static readonly RequestType.Create("stackTrace"); } + [DebuggerDisplay("ThreadId = {ThreadId}, Levels = {Levels}")] public class StackTraceRequestArguments { public int ThreadId { get; private set; } @@ -27,4 +29,3 @@ public class StackTraceResponseBody public StackFrame[] StackFrames { get; set; } } } - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/Variable.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/Variable.cs index 6a69be269..c029e1ce8 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/Variable.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/Variable.cs @@ -15,7 +15,7 @@ public class Variable // /** If variablesReference is > 0, the variable is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. */ public int VariablesReference { get; set; } - public static Variable Create(VariableDetails variable) + public static Variable Create(VariableDetailsBase variable) { return new Variable { diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/VariablesRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/VariablesRequest.cs index 94bb0d01b..945cd3400 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/VariablesRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/VariablesRequest.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System.Diagnostics; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter @@ -14,6 +15,7 @@ public static readonly RequestType.Create("variables"); } + [DebuggerDisplay("VariablesReference = {VariablesReference}")] public class VariablesRequestArguments { public int VariablesReference { get; set; } diff --git a/src/PowerShellEditorServices/Debugging/DebugService.cs b/src/PowerShellEditorServices/Debugging/DebugService.cs index 171e2836e..01301eb19 100644 --- a/src/PowerShellEditorServices/Debugging/DebugService.cs +++ b/src/PowerShellEditorServices/Debugging/DebugService.cs @@ -27,8 +27,10 @@ public class DebugService new Dictionary>(); private int nextVariableId; - private List currentVariables; - private StackFrameDetails[] callStackFrames; + private List variables; + private VariableContainerDetails globalScopeVariables; + private VariableContainerDetails scriptScopeVariables; + private StackFrameDetails[] stackFrameDetails; #endregion @@ -155,38 +157,28 @@ public void Abort() /// /// /// An array of VariableDetails instances which describe the requested variables. - public VariableDetails[] GetVariables(int variableReferenceId) + public VariableDetailsBase[] GetVariables(int variableReferenceId) { - VariableDetails[] childVariables = null; + VariableDetailsBase[] childVariables; - if (variableReferenceId >= VariableDetails.FirstVariableId) + VariableDetailsBase parentVariable = this.variables[variableReferenceId]; + if (parentVariable.IsExpandable) { - int correctedId = - (variableReferenceId - VariableDetails.FirstVariableId); + childVariables = parentVariable.GetChildren(); - VariableDetails parentVariable = - this.currentVariables[correctedId]; - - if (parentVariable.IsExpandable) + foreach (var child in childVariables) { - childVariables = parentVariable.GetChildren(); - - foreach (var child in childVariables) + // Only add child if it hasn't already been added. + if (child.Id < 0) { - this.currentVariables.Add(child); - child.Id = this.nextVariableId; - this.nextVariableId++; + child.Id = this.nextVariableId++; + this.variables.Add(child); } } - else - { - childVariables = new VariableDetails[0]; - } } else { - // TODO: Get variables for the desired scope ID - childVariables = this.currentVariables.ToArray(); + childVariables = new VariableDetailsBase[0]; } return childVariables; @@ -200,13 +192,13 @@ public VariableDetails[] GetVariables(int variableReferenceId) /// The variable expression string to evaluate. /// The ID of the stack frame in which the expression should be evaluated. /// A VariableDetails object containing the result. - public VariableDetails GetVariableFromExpression(string variableExpression, int stackFrameId) + public VariableDetailsBase GetVariableFromExpression(string variableExpression, int stackFrameId) { // Break up the variable path string[] variablePathParts = variableExpression.Split('.'); - VariableDetails resolvedVariable = null; - IEnumerable variableList = this.currentVariables; + VariableDetailsBase resolvedVariable = null; + IEnumerable variableList = this.variables; foreach (var variableName in variablePathParts) { @@ -267,7 +259,7 @@ await this.powerShellContext.ExecuteScriptString( /// public StackFrameDetails[] GetStackFrames() { - return this.callStackFrames; + return this.stackFrameDetails; } /// @@ -278,10 +270,11 @@ public StackFrameDetails[] GetStackFrames() /// The list of VariableScope instances which describe the available variable scopes. public VariableScope[] GetVariableScopes(int stackFrameId) { - // TODO: Return different scopes based on PowerShell scoping mechanics return new VariableScope[] { - new VariableScope(1, "Locals") + new VariableScope(this.stackFrameDetails[stackFrameId].LocalVariables.Id, "Local"), + new VariableScope(this.scriptScopeVariables.Id, "Script"), + new VariableScope(this.globalScopeVariables.Id, "Global"), }; } @@ -310,25 +303,43 @@ private async Task ClearBreakpointsInFile(ScriptFile scriptFile) } } - private async Task FetchVariables() + private async Task FetchStackFramesAndVariables() { - this.nextVariableId = VariableDetails.FirstVariableId; - this.currentVariables = new List(); + this.nextVariableId = VariableDetailsBase.FirstVariableId; + this.variables = new List(); + + // Create a dummy variable for index 0, should never see this. + this.variables.Add(new VariableDetails("Dummy", null)); + await FetchGlobalAndScriptVariables(); + await FetchStackFrames(); + + } + + private async Task FetchGlobalAndScriptVariables() + { + this.scriptScopeVariables = await FetchVariableContainer("Script"); + this.globalScopeVariables = await FetchVariableContainer("Global"); + } + + private async Task FetchVariableContainer(string scope) + { PSCommand psCommand = new PSCommand(); psCommand.AddCommand("Get-Variable"); - psCommand.AddParameter("Scope", "Local"); + psCommand.AddParameter("Scope", scope); - var results = await this.powerShellContext.ExecuteCommand(psCommand); + var variableContainerDetails = new VariableContainerDetails(this.nextVariableId++, "Scope: " + scope); + this.variables.Add(variableContainerDetails); - foreach (var variable in results) + var results = await this.powerShellContext.ExecuteCommand(psCommand); + foreach (PSVariable variable in results) { - var details = new VariableDetails(variable); - details.Id = this.nextVariableId; - this.currentVariables.Add(details); - - this.nextVariableId++; + var variableDetails = new VariableDetails(variable) { Id = this.nextVariableId++ }; + this.variables.Add(variableDetails); + variableContainerDetails.Children.Add(variableDetails); } + + return variableContainerDetails; } private async Task FetchStackFrames() @@ -338,10 +349,14 @@ private async Task FetchStackFrames() var results = await this.powerShellContext.ExecuteCommand(psCommand); - this.callStackFrames = - results - .Select(StackFrameDetails.Create) - .ToArray(); + var callStackFrames = results.ToArray(); + this.stackFrameDetails = new StackFrameDetails[callStackFrames.Length]; + + for (int i = 0; i < callStackFrames.Length; i++) + { + VariableContainerDetails localVariables = await FetchVariableContainer(i.ToString()); + this.stackFrameDetails[i] = StackFrameDetails.Create(callStackFrames[i], localVariables); + } } #endregion @@ -355,9 +370,8 @@ private async Task FetchStackFrames() private async void OnDebuggerStop(object sender, DebuggerStopEventArgs e) { - // Get the call stack and local variables - await this.FetchStackFrames(); - await this.FetchVariables(); + // Get call stack and variables. + await this.FetchStackFramesAndVariables(); // Notify the host that the debugger is stopped if (this.DebuggerStopped != null) diff --git a/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs b/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs index b799b722d..27bd4caa0 100644 --- a/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs +++ b/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs @@ -3,7 +3,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using System.Collections.Generic; using System.Management.Automation; namespace Microsoft.PowerShell.EditorServices @@ -34,6 +33,11 @@ public class StackFrameDetails /// public int ColumnNumber { get; private set; } + /// + /// Gets or sets the VariableContainerDetails that contains the local variables. + /// + public VariableContainerDetails LocalVariables { get; private set; } + /// /// Creates an instance of the StackFrameDetails class from a /// CallStackFrame instance provided by the PowerShell engine. @@ -41,19 +45,20 @@ public class StackFrameDetails /// /// The original CallStackFrame instance from which details will be obtained. /// + /// + /// A variable container with all the local variables for this stack frame. /// A new instance of the StackFrameDetails class. static internal StackFrameDetails Create( - CallStackFrame callStackFrame) + CallStackFrame callStackFrame, + VariableContainerDetails localVariables) { - Dictionary localVariables = - callStackFrame.GetFrameVariables(); - return new StackFrameDetails { ScriptPath = callStackFrame.ScriptName, FunctionName = callStackFrame.FunctionName, LineNumber = callStackFrame.Position.StartLineNumber, - ColumnNumber = callStackFrame.Position.StartColumnNumber + ColumnNumber = callStackFrame.Position.StartColumnNumber, + LocalVariables = localVariables }; } } diff --git a/src/PowerShellEditorServices/Debugging/VariableContainerDetails.cs b/src/PowerShellEditorServices/Debugging/VariableContainerDetails.cs new file mode 100644 index 000000000..2e1b38350 --- /dev/null +++ b/src/PowerShellEditorServices/Debugging/VariableContainerDetails.cs @@ -0,0 +1,59 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Container for variables that is not itself a variable per se. However given how + /// VSCode uses an integer variable reference id for every node under the "Variables" tool + /// window, it is useful to treat containers, typically scope containers, as a variable. + /// Note that these containers are not necessarily always a scope container. Consider a + /// container such as "Auto" or "My". These aren't scope related but serve as just another + /// way to organize variables into a useful UI structure. + /// + [DebuggerDisplay("Name = {Name}, Id = {Id}, Count = {Children.Count}")] + public class VariableContainerDetails : VariableDetailsBase + { + private readonly List children; + + /// + /// Instantiates an instance of VariableScopeDetails. + /// + /// The variable reference id for this scope. + /// The name of the variable scope. + public VariableContainerDetails(int id, string name) + { + Validate.IsNotNull(name, "name"); + + this.Id = id; + this.Name = name; + this.IsExpandable = true; + this.ValueString = " "; // An empty string isn't enough due to a temporary bug in VS Code. + + this.children = new List(); + } + + /// + /// Gets the collection of child variables. + /// + public List Children + { + get { return this.children; } + } + + /// + /// Returns the details of the variable container's children. If empty, returns an empty array. + /// + /// + public override VariableDetailsBase[] GetChildren() + { + return this.children.ToArray(); + } + } +} diff --git a/src/PowerShellEditorServices/Debugging/VariableDetails.cs b/src/PowerShellEditorServices/Debugging/VariableDetails.cs index 6254f1ba7..91981aef0 100644 --- a/src/PowerShellEditorServices/Debugging/VariableDetails.cs +++ b/src/PowerShellEditorServices/Debugging/VariableDetails.cs @@ -6,6 +6,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Management.Automation; using System.Reflection; @@ -16,7 +17,8 @@ namespace Microsoft.PowerShell.EditorServices /// Contains details pertaining to a variable in the current /// debugging session. /// - public class VariableDetails + [DebuggerDisplay("Name = {Name}, Id = {Id}, Value = {ValueString}")] + public class VariableDetails : VariableDetailsBase { #region Fields @@ -25,55 +27,11 @@ public class VariableDetails /// public const string DollarPrefix = "$"; - /// - /// Provides a constant for the variable ID of the local variable scope. - /// - public const int LocalScopeVariableId = 1; - - /// - /// Provides a constant for the variable ID of the global variable scope. - /// - public const int GlobalScopeVariableId = 2; - - /// - /// Provides a constant that is used as the starting variable ID for all - /// variables in a given scope. - /// - public const int FirstVariableId = 10; - private object valueObject; private VariableDetails[] cachedChildren; #endregion - #region Properties - - /// - /// Gets the numeric ID of the variable which can be used to refer - /// to it in future requests. - /// - public int Id { get; set; } - - /// - /// Gets the variable's name. - /// - public string Name { get; private set; } - - /// - /// Gets the string representation of the variable's value. - /// If the variable is an expandable object, this string - /// will be empty. - /// - public string ValueString { get; private set; } - - /// - /// Returns true if the variable's value is expandable, meaning - /// that it has child properties or its contents can be enumerated. - /// - public bool IsExpandable { get; private set; } - - #endregion - #region Constructors /// @@ -110,6 +68,7 @@ public VariableDetails(string name, object value) { this.valueObject = value; + this.Id = -1; // Not been assigned a variable reference id yet this.Name = name; this.IsExpandable = GetIsExpandable(value); this.ValueString = @@ -127,7 +86,7 @@ public VariableDetails(string name, object value) /// details of its children. Otherwise it returns an empty array. /// /// - public VariableDetails[] GetChildren() + public override VariableDetailsBase[] GetChildren() { VariableDetails[] childVariables = null; diff --git a/src/PowerShellEditorServices/Debugging/VariableDetailsBase.cs b/src/PowerShellEditorServices/Debugging/VariableDetailsBase.cs new file mode 100644 index 000000000..11d466750 --- /dev/null +++ b/src/PowerShellEditorServices/Debugging/VariableDetailsBase.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Defines the common details between a variable and a variable container such as a scope + /// in the current debugging session. + /// + public abstract class VariableDetailsBase + { + /// + /// Provides a constant that is used as the starting variable ID for all. + /// Avoid 0 as it indicates a variable node with no children. + /// variables. + /// + public const int FirstVariableId = 1; + + /// + /// Gets the numeric ID of the variable which can be used to refer + /// to it in future requests. + /// + public int Id { get; set; } + + /// + /// Gets the variable's name. + /// + public string Name { get; protected set; } + + /// + /// Gets the string representation of the variable's value. + /// If the variable is an expandable object, this string + /// will be empty. + /// + public string ValueString { get; protected set; } + + /// + /// Returns true if the variable's value is expandable, meaning + /// that it has child properties or its contents can be enumerated. + /// + public bool IsExpandable { get; protected set; } + + /// + /// If this variable instance is expandable, this method returns the + /// details of its children. Otherwise it returns an empty array. + /// + /// + public abstract VariableDetailsBase[] GetChildren(); + } +} \ No newline at end of file diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index 71968fdca..a7707d85d 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -63,7 +63,9 @@ + + diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index 576600684..73a1bfb76 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -171,9 +171,10 @@ await this.debugService.SetBreakpoints( this.powerShellContext.ExecuteScriptString(variablesFile.FilePath); await this.AssertDebuggerStopped(variablesFile.FilePath); - VariableDetails[] variables = - debugService.GetVariables( - VariableDetails.LocalScopeVariableId); + StackFrameDetails[] stackFrames = debugService.GetStackFrames(); + + VariableDetailsBase[] variables = + debugService.GetVariables(stackFrames[0].LocalVariables.Id); // TODO: Add checks for correct value strings as well