From 9b2239729441b56853da3497b637a6ff3ee3727e Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Wed, 31 Aug 2016 12:17:49 -0600 Subject: [PATCH 01/22] Add support for debug protocol setVariableRequest. (#283) * Add support for debug protocol setVariableRequest. * Cleaned up some comments in the SetVariable method and removed some exceptions that we no longer need to catch now that we aren't using System.Convert.ChangeType(). --- .../DebugAdapter/InitializeRequest.cs | 6 + .../DebugAdapter/SetVariableRequest.cs | 36 +++++ .../PowerShellEditorServices.Protocol.csproj | 1 + .../Server/DebugAdapter.cs | 38 +++++ .../Server/DebugAdapterBase.cs | 3 +- .../Debugging/DebugService.cs | 152 +++++++++++++++++- .../InvalidPowerShellExpressionException.cs | 24 +++ .../Debugging/VariableContainerDetails.cs | 18 +++ .../Debugging/VariableDetails.cs | 8 +- .../PowerShellEditorServices.csproj | 1 + .../Session/PowerShellContext.cs | 36 ++++- .../Debugging/VariableTest.ps1 | 12 +- .../Debugging/DebugServiceTests.cs | 140 ++++++++++++++++ 13 files changed, 465 insertions(+), 10 deletions(-) create mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/SetVariableRequest.cs create mode 100644 src/PowerShellEditorServices/Debugging/InvalidPowerShellExpressionException.cs diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializeRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializeRequest.cs index 16c99544e..a8ed4c734 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializeRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializeRequest.cs @@ -52,5 +52,11 @@ public class InitializeResponseBody /// supports a (side effect free) evaluate request for data hovers. /// public bool SupportsEvaluateForHovers { get; set; } + + /// + /// Gets or sets a boolean value that determines whether the debug adapter + /// supports allowing the user to set a variable from the Variables debug windows. + /// + public bool SupportsSetVariable { get; set; } } } diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetVariableRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetVariableRequest.cs new file mode 100644 index 000000000..333327454 --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetVariableRequest.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// 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 +{ + /// + /// SetVariable request; value of command field is "setVariable". + /// Request is initiated when user uses the debugger Variables UI to change the value of a variable. + /// + public class SetVariableRequest + { + public static readonly + RequestType Type = + RequestType.Create("setVariable"); + } + + [DebuggerDisplay("VariablesReference = {VariablesReference}")] + public class SetVariableRequestArguments + { + public int VariablesReference { get; set; } + + public string Name { get; set; } + + public string Value { get; set; } + } + + public class SetVariableResponseBody + { + public string Value { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj b/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj index ea0bc7f84..b4095da4f 100644 --- a/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj +++ b/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj @@ -54,6 +54,7 @@ + diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs index 8cc3a56b1..1a9031fa1 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Microsoft.PowerShell.EditorServices.Debugging; using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; @@ -67,6 +68,7 @@ protected override void Initialize() this.SetRequestHandler(StackTraceRequest.Type, this.HandleStackTraceRequest); this.SetRequestHandler(ScopesRequest.Type, this.HandleScopesRequest); this.SetRequestHandler(VariablesRequest.Type, this.HandleVariablesRequest); + this.SetRequestHandler(SetVariableRequest.Type, this.HandleSetVariablesRequest); this.SetRequestHandler(SourceRequest.Type, this.HandleSourceRequest); this.SetRequestHandler(EvaluateRequest.Type, this.HandleEvaluateRequest); } @@ -461,6 +463,42 @@ protected async Task HandleVariablesRequest( await requestContext.SendResult(variablesResponse); } + protected async Task HandleSetVariablesRequest( + SetVariableRequestArguments setVariableParams, + RequestContext requestContext) + { + try + { + string updatedValue = + await editorSession.DebugService.SetVariable( + setVariableParams.VariablesReference, + setVariableParams.Name, + setVariableParams.Value); + + var setVariableResponse = new SetVariableResponseBody + { + Value = updatedValue + }; + + await requestContext.SendResult(setVariableResponse); + } + catch (Exception ex) when (ex is ArgumentTransformationMetadataException || + ex is InvalidPowerShellExpressionException || + ex is SessionStateUnauthorizedAccessException) + { + // Catch common, innocuous errors caused by the user supplying a value that can't be converted or the variable is not settable. + Logger.Write(LogLevel.Verbose, $"Failed to set variable: {ex.Message}"); + await requestContext.SendError(ex.Message); + } + catch (Exception ex) + { + Logger.Write(LogLevel.Error, $"Unexpected error setting variable: {ex.Message}"); + string msg = + $"Unexpected error: {ex.GetType().Name} - {ex.Message} Please report this error to the PowerShellEditorServices project on GitHub."; + await requestContext.SendError(msg); + } + } + protected Task HandleSourceRequest( SourceRequestArguments sourceParams, RequestContext requestContext) diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs index f84ebb9d7..890cce26e 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs @@ -59,7 +59,8 @@ await requestContext.SendResult( new InitializeResponseBody { SupportsConfigurationDoneRequest = true, SupportsConditionalBreakpoints = true, - SupportsFunctionBreakpoints = true + SupportsFunctionBreakpoints = true, + SupportsSetVariable = true }); // Send the Initialized event so that we get breakpoints diff --git a/src/PowerShellEditorServices/Debugging/DebugService.cs b/src/PowerShellEditorServices/Debugging/DebugService.cs index 73c8e5a8e..e79351eb5 100644 --- a/src/PowerShellEditorServices/Debugging/DebugService.cs +++ b/src/PowerShellEditorServices/Debugging/DebugService.cs @@ -8,7 +8,9 @@ using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; +using System.Text; using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Debugging; using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices @@ -278,7 +280,7 @@ public VariableDetailsBase[] 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. + /// A VariableDetailsBase object containing the result. public VariableDetailsBase GetVariableFromExpression(string variableExpression, int stackFrameId) { // Break up the variable path @@ -314,6 +316,154 @@ public VariableDetailsBase GetVariableFromExpression(string variableExpression, return resolvedVariable; } + /// + /// Sets the specified variable by container variableReferenceId and variable name to the + /// specified new value. If the variable cannot be set or converted to that value this + /// method will throw InvalidPowerShellExpressionException, ArgumentTransformationMetadataException, or + /// SessionStateUnauthorizedAccessException. + /// + /// The container (Autos, Local, Script, Global) that holds the variable. + /// The name of the variable prefixed with $. + /// The new string value. This value must not be null. If you want to set the variable to $null + /// pass in the string "$null". + /// The string representation of the value the variable was set to. + public async Task SetVariable(int variableContainerReferenceId, string name, string value) + { + Validate.IsNotNull(nameof(name), name); + Validate.IsNotNull(nameof(value), value); + + Logger.Write(LogLevel.Verbose, $"SetVariableRequest for '{name}' to value string (pre-quote processing): '{value}'"); + + // An empty or whitespace only value is not a valid expression for SetVariable. + if (value.Trim().Length == 0) + { + throw new InvalidPowerShellExpressionException("Expected an expression."); + } + + // Evaluate the expression to get back a PowerShell object from the expression string. + PSCommand psCommand = new PSCommand(); + psCommand.AddScript(value); + var errorMessages = new StringBuilder(); + var results = + await this.powerShellContext.ExecuteCommand( + psCommand, + errorMessages, + false, + false); + + // Check if PowerShell's evaluation of the expression resulted in an error. + object psobject = results.FirstOrDefault(); + if ((psobject == null) && (errorMessages.Length > 0)) + { + throw new InvalidPowerShellExpressionException(errorMessages.ToString()); + } + + // If PowerShellContext.ExecuteCommand returns an ErrorRecord as output, the expression failed evaluation. + // Ideally we would have a separate means from communicating error records apart from normal output. + ErrorRecord errorRecord = psobject as ErrorRecord; + if (errorRecord != null) + { + throw new InvalidPowerShellExpressionException(errorRecord.ToString()); + } + + // OK, now we have a PS object from the supplied value string (expression) to assign to a variable. + // Get the variable referenced by variableContainerReferenceId and variable name. + VariableContainerDetails variableContainer = (VariableContainerDetails)this.variables[variableContainerReferenceId]; + VariableDetailsBase variable = variableContainer.Children[name]; + + // Determine scope in which the variable lives. This is required later for the call to Get-Variable -Scope. + string scope = null; + if (variableContainerReferenceId == this.scriptScopeVariables.Id) + { + scope = "Script"; + } + else if (variableContainerReferenceId == this.globalScopeVariables.Id) + { + scope = "Global"; + } + else + { + // Determine which stackframe's local scope the variable is in. + for (int i = 0; i < this.stackFrameDetails.Length; i++) + { + var stackFrame = this.stackFrameDetails[i]; + if (stackFrame.LocalVariables.ContainsVariable(variable.Id)) + { + scope = i.ToString(); + break; + } + } + } + + if (scope == null) + { + // Hmm, this would be unexpected. No scope means do not pass GO, do not collect $200. + throw new Exception("Could not find the scope for this variable."); + } + + // Now that we have the scope, get the associated PSVariable object for the variable to be set. + psCommand.Commands.Clear(); + psCommand = new PSCommand(); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable"); + psCommand.AddParameter("Name", name.TrimStart('$')); + psCommand.AddParameter("Scope", scope); + + IEnumerable result = await this.powerShellContext.ExecuteCommand(psCommand, sendErrorToHost: false); + PSVariable psVariable = result.FirstOrDefault(); + if (psVariable == null) + { + throw new Exception($"Failed to retrieve PSVariable object for '{name}' from scope '{scope}'."); + } + + // We have the PSVariable object for the variable the user wants to set and an object to assign to that variable. + // The last step is to determine whether the PSVariable is "strongly typed" which may require a conversion. + // If it is not strongly typed, we simply assign the object directly to the PSVariable potentially changing its type. + // Turns out ArgumentTypeConverterAttribute is not public. So we call the attribute through it's base class - + // ArgumentTransformationAttribute. + var argTypeConverterAttr = + psVariable.Attributes + .OfType() + .FirstOrDefault(a => a.GetType().Name.Equals("ArgumentTypeConverterAttribute")); + + if (argTypeConverterAttr != null) + { + // PSVariable is strongly typed. Need to apply the conversion/transform to the new value. + psCommand.Commands.Clear(); + psCommand = new PSCommand(); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable"); + psCommand.AddParameter("Name", "ExecutionContext"); + psCommand.AddParameter("ValueOnly"); + + errorMessages.Clear(); + + var getExecContextResults = + await this.powerShellContext.ExecuteCommand( + psCommand, + errorMessages, + sendErrorToHost: false); + + EngineIntrinsics executionContext = getExecContextResults.OfType().FirstOrDefault(); + + var msg = $"Setting variable '{name}' using conversion to value: {psobject ?? ""}"; + Logger.Write(LogLevel.Verbose, msg); + + psVariable.Value = argTypeConverterAttr.Transform(executionContext, psobject); + } + else + { + // PSVariable is *not* strongly typed. In this case, whack the old value with the new value. + var msg = $"Setting variable '{name}' directly to value: {psobject ?? ""} - previous type was {psVariable.Value?.GetType().Name ?? ""}"; + Logger.Write(LogLevel.Verbose, msg); + psVariable.Value = psobject; + } + + // Use the VariableDetails.ValueString functionality to get the string representation for client debugger. + // This makes the returned string consistent with the strings normally displayed for variables in the debugger. + var tempVariable = new VariableDetails(psVariable); + Logger.Write(LogLevel.Verbose, $"Set variable '{name}' to: {tempVariable.ValueString ?? ""}"); + return tempVariable.ValueString; + } + /// /// Evaluates an expression in the context of the stopped /// debugger. This method will execute the specified expression diff --git a/src/PowerShellEditorServices/Debugging/InvalidPowerShellExpressionException.cs b/src/PowerShellEditorServices/Debugging/InvalidPowerShellExpressionException.cs new file mode 100644 index 000000000..fd6b13caf --- /dev/null +++ b/src/PowerShellEditorServices/Debugging/InvalidPowerShellExpressionException.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices.Debugging +{ + /// + /// Represents the exception that is thrown when an invalid expression is provided to the DebugService's SetVariable method. + /// + public class InvalidPowerShellExpressionException : Exception + { + /// + /// Initializes a new instance of the SetVariableExpressionException class. + /// + /// Message indicating why the expression is invalid. + public InvalidPowerShellExpressionException(string message) + : base(message) + { + } + } +} diff --git a/src/PowerShellEditorServices/Debugging/VariableContainerDetails.cs b/src/PowerShellEditorServices/Debugging/VariableContainerDetails.cs index 6b98c53c9..8f60dccfe 100644 --- a/src/PowerShellEditorServices/Debugging/VariableContainerDetails.cs +++ b/src/PowerShellEditorServices/Debugging/VariableContainerDetails.cs @@ -77,5 +77,23 @@ public override VariableDetailsBase[] GetChildren() this.children.Values.CopyTo(variablesArray, 0); return variablesArray; } + + /// + /// Determines whether this variable container contains the specified variable by its referenceId. + /// + /// The variableReferenceId to search for. + /// Returns true if this variable container directly contains the specified variableReferenceId, false otherwise. + public bool ContainsVariable(int variableReferenceId) + { + foreach (VariableDetailsBase value in this.children.Values) + { + if (value.Id == variableReferenceId) + { + return true; + } + } + + return false; + } } } diff --git a/src/PowerShellEditorServices/Debugging/VariableDetails.cs b/src/PowerShellEditorServices/Debugging/VariableDetails.cs index 33ed94ab3..8a76e0899 100644 --- a/src/PowerShellEditorServices/Debugging/VariableDetails.cs +++ b/src/PowerShellEditorServices/Debugging/VariableDetails.cs @@ -145,7 +145,13 @@ private static string GetValueString(object value, bool isExpandable) if (value == null) { - valueString = "null"; + // Set to identifier recognized by PowerShell to make setVariable from the debug UI more natural. + valueString = "$null"; + } + else if (value is bool) + { + // Set to identifier recognized by PowerShell to make setVariable from the debug UI more natural. + valueString = (bool) value ? "$true" : "$false"; } else if (isExpandable) { diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index 16d278f3c..bc4498d78 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -71,6 +71,7 @@ + diff --git a/src/PowerShellEditorServices/Session/PowerShellContext.cs b/src/PowerShellEditorServices/Session/PowerShellContext.cs index 093d8c2c6..ec110b7c9 100644 --- a/src/PowerShellEditorServices/Session/PowerShellContext.cs +++ b/src/PowerShellEditorServices/Session/PowerShellContext.cs @@ -324,6 +324,33 @@ public async Task> ExecuteCommand( PSCommand psCommand, bool sendOutputToHost = false, bool sendErrorToHost = true) + { + return await ExecuteCommand(psCommand, null, sendOutputToHost, sendErrorToHost); + } + + /// + /// Executes a PSCommand against the session's runspace and returns + /// a collection of results of the expected type. + /// + /// The expected result type. + /// The PSCommand to be executed. + /// Error messages from PowerShell will be written to the StringBuilder. + /// You must set sendErrorToHost to false for errors to be written to the StringBuilder. This value can be null. + /// + /// If true, causes any output written during command execution to be written to the host. + /// + /// + /// If true, causes any errors encountered during command execution to be written to the host. + /// + /// + /// An awaitable Task which will provide results once the command + /// execution completes. + /// + public async Task> ExecuteCommand( + PSCommand psCommand, + StringBuilder errorMessages, + bool sendOutputToHost = false, + bool sendErrorToHost = true) { RunspaceHandle runspaceHandle = null; IEnumerable executionResult = Enumerable.Empty(); @@ -337,7 +364,7 @@ public async Task> ExecuteCommand( PipelineExecutionRequest executionRequest = new PipelineExecutionRequest( - this, psCommand, sendOutputToHost); + this, psCommand, errorMessages, sendOutputToHost); // Send the pipeline execution request to the pipeline thread this.pipelineResultTask = new TaskCompletionSource(); @@ -415,6 +442,7 @@ await Task.Factory.StartNew>( errorMessage += error.ToString() + "\r\n"; } + errorMessages?.Append(errorMessage); Logger.Write(LogLevel.Error, errorMessage); } else @@ -433,6 +461,8 @@ await Task.Factory.StartNew>( LogLevel.Error, "Runtime exception occurred while executing command:\r\n\r\n" + e.ToString()); + errorMessages?.Append(e.Message); + if (sendErrorToHost) { // Write the error to the host @@ -1209,6 +1239,7 @@ private class PipelineExecutionRequest : IPipelineExecutionRequest { PowerShellContext powerShellContext; PSCommand psCommand; + StringBuilder errorMessages; bool sendOutputToHost; public IEnumerable Results { get; private set; } @@ -1216,10 +1247,12 @@ private class PipelineExecutionRequest : IPipelineExecutionRequest public PipelineExecutionRequest( PowerShellContext powerShellContext, PSCommand psCommand, + StringBuilder errorMessages, bool sendOutputToHost) { this.powerShellContext = powerShellContext; this.psCommand = psCommand; + this.errorMessages = errorMessages; this.sendOutputToHost = sendOutputToHost; } @@ -1228,6 +1261,7 @@ public async Task Execute() this.Results = await this.powerShellContext.ExecuteCommand( psCommand, + errorMessages, sendOutputToHost); // TODO: Deal with errors? diff --git a/test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1 b/test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1 index 32e238dab..7398d8c01 100644 --- a/test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1 @@ -1,11 +1,11 @@ class MyClass { [String] $Name; - [Int32] $Number; -} - -function Test-Variables -{ - $strVar = "Hello" + [Int32] $Number; } +[bool]$scriptBool = $false +$scriptInt = 42 +function Test-Variables { + $strVar = "Hello" + [string]$strVar2 = "Hello2" $arrVar = @(1, 2, $strVar, $objVar) $assocArrVar = @{ firstChild = "Child"; secondChild = 42 } $classVar = [MyClass]::new(); diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index e9a220a06..7c5371c6f 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -504,6 +504,146 @@ await this.debugService.SetLineBreakpoints( this.powerShellContext.AbortExecution(); } + [Fact] + public async Task DebuggerSetsVariablesNoConversion() + { + await this.debugService.SetLineBreakpoints( + this.variableScriptFile, + new[] { BreakpointDetails.Create("", 14) }); + + // Execute the script and wait for the breakpoint to be hit + Task executeTask = + this.powerShellContext.ExecuteScriptString( + this.variableScriptFile.FilePath); + + await this.AssertDebuggerStopped(this.variableScriptFile.FilePath); + + StackFrameDetails[] stackFrames = debugService.GetStackFrames(); + + VariableDetailsBase[] variables = + debugService.GetVariables(stackFrames[0].LocalVariables.Id); + + // Test set of a local string variable (not strongly typed) + string newStrValue = "\"Goodbye\""; + string setStrValue = await debugService.SetVariable(stackFrames[0].LocalVariables.Id, "$strVar", newStrValue); + Assert.Equal(newStrValue, setStrValue); + + VariableScope[] scopes = this.debugService.GetVariableScopes(0); + + // Test set of script scope int variable (not strongly typed) + VariableScope scriptScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.ScriptScopeName); + string newIntValue = "49"; + string newIntExpr = "7 * 7"; + string setIntValue = await debugService.SetVariable(scriptScope.Id, "$scriptInt", newIntExpr); + Assert.Equal(newIntValue, setIntValue); + + // Test set of global scope int variable (not strongly typed) + VariableScope globalScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.GlobalScopeName); + string newGlobalIntValue = "4242"; + string setGlobalIntValue = await debugService.SetVariable(globalScope.Id, "$MaximumAliasCount", newGlobalIntValue); + Assert.Equal(newGlobalIntValue, setGlobalIntValue); + + // The above just tests that the debug service returns the correct new value string. + // Let's step the debugger and make sure the values got set to the new values. + this.debugService.StepOver(); + await this.AssertDebuggerStopped(this.variableScriptFile.FilePath); + + stackFrames = debugService.GetStackFrames(); + + // Test set of a local string variable (not strongly typed) + variables = debugService.GetVariables(stackFrames[0].LocalVariables.Id); + var strVar = variables.FirstOrDefault(v => v.Name == "$strVar"); + Assert.Equal(newStrValue, strVar.ValueString); + + scopes = this.debugService.GetVariableScopes(0); + + // Test set of script scope int variable (not strongly typed) + scriptScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.ScriptScopeName); + variables = debugService.GetVariables(scriptScope.Id); + var intVar = variables.FirstOrDefault(v => v.Name == "$scriptInt"); + Assert.Equal(newIntValue, intVar.ValueString); + + // Test set of global scope int variable (not strongly typed) + globalScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.GlobalScopeName); + variables = debugService.GetVariables(globalScope.Id); + var intGlobalVar = variables.FirstOrDefault(v => v.Name == "$MaximumAliasCount"); + Assert.Equal(newGlobalIntValue, intGlobalVar.ValueString); + + // Abort execution of the script + this.powerShellContext.AbortExecution(); + } + + [Fact] + public async Task DebuggerSetsVariablesWithConversion() + { + await this.debugService.SetLineBreakpoints( + this.variableScriptFile, + new[] { BreakpointDetails.Create("", 14) }); + + // Execute the script and wait for the breakpoint to be hit + Task executeTask = + this.powerShellContext.ExecuteScriptString( + this.variableScriptFile.FilePath); + + await this.AssertDebuggerStopped(this.variableScriptFile.FilePath); + + StackFrameDetails[] stackFrames = debugService.GetStackFrames(); + + VariableDetailsBase[] variables = + debugService.GetVariables(stackFrames[0].LocalVariables.Id); + + // Test set of a local string variable (not strongly typed but force conversion) + string newStrValue = "\"False\""; + string newStrExpr = "$false"; + string setStrValue = await debugService.SetVariable(stackFrames[0].LocalVariables.Id, "$strVar2", newStrExpr); + Assert.Equal(newStrValue, setStrValue); + + VariableScope[] scopes = this.debugService.GetVariableScopes(0); + + // Test set of script scope bool variable (strongly typed) + VariableScope scriptScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.ScriptScopeName); + string newBoolValue = "$true"; + string newBoolExpr = "1"; + string setBoolValue = await debugService.SetVariable(scriptScope.Id, "$scriptBool", newBoolExpr); + Assert.Equal(newBoolValue, setBoolValue); + + // Test set of global scope ActionPreference variable (strongly typed) + VariableScope globalScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.GlobalScopeName); + string newGlobalValue = "Continue"; + string newGlobalExpr = "'Continue'"; + string setGlobalValue = await debugService.SetVariable(globalScope.Id, "$VerbosePreference", newGlobalExpr); + Assert.Equal(newGlobalValue, setGlobalValue); + + // The above just tests that the debug service returns the correct new value string. + // Let's step the debugger and make sure the values got set to the new values. + this.debugService.StepOver(); + await this.AssertDebuggerStopped(this.variableScriptFile.FilePath); + + stackFrames = debugService.GetStackFrames(); + + // Test set of a local string variable (not strongly typed but force conversion) + variables = debugService.GetVariables(stackFrames[0].LocalVariables.Id); + var strVar = variables.FirstOrDefault(v => v.Name == "$strVar2"); + Assert.Equal(newStrValue, strVar.ValueString); + + scopes = this.debugService.GetVariableScopes(0); + + // Test set of script scope bool variable (strongly typed) + scriptScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.ScriptScopeName); + variables = debugService.GetVariables(scriptScope.Id); + var boolVar = variables.FirstOrDefault(v => v.Name == "$scriptBool"); + Assert.Equal(newBoolValue, boolVar.ValueString); + + // Test set of global scope ActionPreference variable (strongly typed) + globalScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.GlobalScopeName); + variables = debugService.GetVariables(globalScope.Id); + var globalVar = variables.FirstOrDefault(v => v.Name == "$VerbosePreference"); + Assert.Equal(newGlobalValue, globalVar.ValueString); + + // Abort execution of the script + this.powerShellContext.AbortExecution(); + } + [Fact] public async Task DebuggerVariableEnumDisplaysCorrectly() { From aff18ec04d48307a1d5a875dc849f1489a89df99 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Sun, 23 Oct 2016 17:27:04 -0600 Subject: [PATCH 02/22] PSES half of program -> script launch prop change. See PR https://github.com/PowerShell/vscode-powershell/pull/324 for details. --- .../Client/DebugAdapterClientBase.cs | 2 +- .../DebugAdapter/LaunchRequest.cs | 7 +++++++ .../Server/DebugAdapter.cs | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs b/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs index 5e6a1758b..5476edbe4 100644 --- a/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs +++ b/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs @@ -22,7 +22,7 @@ public async Task LaunchScript(string scriptFilePath) await this.SendRequest( LaunchRequest.Type, new LaunchRequestArguments { - Program = scriptFilePath + Script = scriptFilePath }); await this.SendRequest(ConfigurationDoneRequest.Type, null); diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/LaunchRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/LaunchRequest.cs index 5fa1cb10e..b81e56f8d 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/LaunchRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/LaunchRequest.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; using System.Collections.Generic; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; @@ -20,8 +21,14 @@ public class LaunchRequestArguments /// /// Gets or sets the absolute path to the program to debug. /// + [Obsolete("This property has been deprecated in favor of the Script property.")] public string Program { get; set; } + /// + /// Gets or sets the absolute path to the script to debug. + /// + public string Script { get; set; } + /// /// Gets or sets a boolean value that indicates whether the script should be /// run with (false) or without (true) debugging support. diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs index 513fe8d80..6dc5438d6 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs @@ -130,7 +130,7 @@ protected async Task HandleLaunchRequest( // Set the working directory for the PowerShell runspace to the cwd passed in via launch.json. // In case that is null, use the the folder of the script to be executed. If the resulting // working dir path is a file path then extract the directory and use that. - string workingDir = launchParams.Cwd ?? launchParams.Program; + string workingDir = launchParams.Cwd ?? launchParams.Script ?? launchParams.Program; workingDir = PowerShellContext.UnescapePath(workingDir); try { @@ -167,7 +167,7 @@ protected async Task HandleLaunchRequest( // params so that the subsequent configurationDone request handler // can launch the script. this.noDebug = launchParams.NoDebug; - this.scriptPathToLaunch = launchParams.Program; + this.scriptPathToLaunch = launchParams.Script ?? launchParams.Program; this.arguments = arguments; // The order of debug protocol messages apparently isn't as guaranteed as we might like. From f56a68367494646c4806d2c7011bad8aabd33fa6 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Wed, 2 Nov 2016 22:36:52 -0600 Subject: [PATCH 03/22] Initial work to support breakpoint hit count. --- .../DebugAdapter/InitializeRequest.cs | 6 ++++++ .../DebugAdapter/SetBreakpointsRequest.cs | 2 ++ .../DebugAdapter/SetFunctionBreakpointsRequest.cs | 2 ++ .../Server/DebugAdapter.cs | 6 ++++-- .../Server/DebugAdapterBase.cs | 3 ++- .../Debugging/BreakpointDetails.cs | 7 +++++-- .../Debugging/BreakpointDetailsBase.cs | 5 +++++ .../Debugging/CommandBreakpointDetails.cs | 4 +++- 8 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializeRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializeRequest.cs index a8ed4c734..c0c843fe4 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializeRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializeRequest.cs @@ -47,6 +47,12 @@ public class InitializeResponseBody /// public bool SupportsConditionalBreakpoints { get; set; } + /// + /// Gets or sets a boolean value that determines whether the debug adapter + /// supports breakpoints that break execution after a specified number of hits. + /// + public bool SupportsHitConditionalBreakpoints { get; set; } + /// /// Gets or sets a boolean value that determines whether the debug adapter /// supports a (side effect free) evaluate request for data hovers. diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetBreakpointsRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetBreakpointsRequest.cs index a4993291d..5b0f88bf2 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetBreakpointsRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetBreakpointsRequest.cs @@ -34,6 +34,8 @@ public class SourceBreakpoint public int? Column { get; set; } public string Condition { get; set; } + + public string HitCondition { get; set; } } public class SetBreakpointsResponseBody diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetFunctionBreakpointsRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetFunctionBreakpointsRequest.cs index 434c6dd59..73b75f82e 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetFunctionBreakpointsRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetFunctionBreakpointsRequest.cs @@ -27,5 +27,7 @@ public class FunctionBreakpoint public string Name { get; set; } public string Condition { get; set; } + + public string HitCondition { get; set; } } } diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs index 513fe8d80..135a0e157 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs @@ -262,7 +262,8 @@ await requestContext.SendResult( scriptFile.FilePath, srcBreakpoint.Line, srcBreakpoint.Column, - srcBreakpoint.Condition); + srcBreakpoint.Condition, + srcBreakpoint.HitCondition); } // If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints. @@ -294,7 +295,8 @@ protected async Task HandleSetFunctionBreakpointsRequest( FunctionBreakpoint funcBreakpoint = setBreakpointsParams.Breakpoints[i]; breakpointDetails[i] = CommandBreakpointDetails.Create( funcBreakpoint.Name, - funcBreakpoint.Condition); + funcBreakpoint.Condition, + funcBreakpoint.HitCondition); } // If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints. diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs index 890cce26e..4f324d269 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs @@ -58,8 +58,9 @@ private async Task HandleInitializeRequest( await requestContext.SendResult( new InitializeResponseBody { SupportsConfigurationDoneRequest = true, - SupportsConditionalBreakpoints = true, SupportsFunctionBreakpoints = true, + SupportsConditionalBreakpoints = true, + SupportsHitConditionalBreakpoints = true, SupportsSetVariable = true }); diff --git a/src/PowerShellEditorServices/Debugging/BreakpointDetails.cs b/src/PowerShellEditorServices/Debugging/BreakpointDetails.cs index f8b16117e..0adaecaed 100644 --- a/src/PowerShellEditorServices/Debugging/BreakpointDetails.cs +++ b/src/PowerShellEditorServices/Debugging/BreakpointDetails.cs @@ -42,12 +42,14 @@ private BreakpointDetails() /// /// /// + /// /// public static BreakpointDetails Create( string source, int line, int? column = null, - string condition = null) + string condition = null, + string hitCondition = null) { Validate.IsNotNull("source", source); @@ -57,7 +59,8 @@ public static BreakpointDetails Create( Source = source, LineNumber = line, ColumnNumber = column, - Condition = condition + Condition = condition, + HitCondition = hitCondition }; } diff --git a/src/PowerShellEditorServices/Debugging/BreakpointDetailsBase.cs b/src/PowerShellEditorServices/Debugging/BreakpointDetailsBase.cs index 3fa718782..9fa83c23e 100644 --- a/src/PowerShellEditorServices/Debugging/BreakpointDetailsBase.cs +++ b/src/PowerShellEditorServices/Debugging/BreakpointDetailsBase.cs @@ -27,5 +27,10 @@ public abstract class BreakpointDetailsBase /// Gets the breakpoint condition string. /// public string Condition { get; protected set; } + + /// + /// Gets the breakpoint hit condition string. + /// + public string HitCondition { get; protected set; } } } diff --git a/src/PowerShellEditorServices/Debugging/CommandBreakpointDetails.cs b/src/PowerShellEditorServices/Debugging/CommandBreakpointDetails.cs index 1d9468df4..8197480c9 100644 --- a/src/PowerShellEditorServices/Debugging/CommandBreakpointDetails.cs +++ b/src/PowerShellEditorServices/Debugging/CommandBreakpointDetails.cs @@ -29,10 +29,12 @@ private CommandBreakpointDetails() /// /// The name of the command to break on. /// Condition string that would be applied to the breakpoint Action parameter. + /// Hit condition string that would be applied to the breakpoint Action parameter. /// public static CommandBreakpointDetails Create( string name, - string condition = null) + string condition = null, + string hitCondition = null) { Validate.IsNotNull(nameof(name), name); From 4123b7ece27380d8d31ac8be881e4f48e7a0efba Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Fri, 4 Nov 2016 21:23:50 -0600 Subject: [PATCH 04/22] Add support for handling just HitCount and Condition + HitCount. Add unit tests for this functionality. --- .../Debugging/DebugService.cs | 103 ++++++++++++++---- .../Debugging/DebugServiceTests.cs | 73 +++++++++++++ 2 files changed, 153 insertions(+), 23 deletions(-) diff --git a/src/PowerShellEditorServices/Debugging/DebugService.cs b/src/PowerShellEditorServices/Debugging/DebugService.cs index e79351eb5..9b7df6e61 100644 --- a/src/PowerShellEditorServices/Debugging/DebugService.cs +++ b/src/PowerShellEditorServices/Debugging/DebugService.cs @@ -35,6 +35,8 @@ public class DebugService private VariableContainerDetails scriptScopeVariables; private StackFrameDetails[] stackFrameDetails; + private static int breakpointHitCounter = 0; + #endregion #region Constructors @@ -102,7 +104,8 @@ public async Task SetLineBreakpoints( } // Check if this is a "conditional" line breakpoint. - if (breakpoint.Condition != null) + if (!String.IsNullOrWhiteSpace(breakpoint.Condition) || + !String.IsNullOrWhiteSpace(breakpoint.HitCondition)) { ScriptBlock actionScriptBlock = GetBreakpointActionScriptBlock(breakpoint); @@ -157,7 +160,8 @@ public async Task SetCommandBreakpoints( psCommand.AddParameter("Command", breakpoint.Name); // Check if this is a "conditional" command breakpoint. - if (breakpoint.Condition != null) + if (!String.IsNullOrWhiteSpace(breakpoint.Condition) || + !String.IsNullOrWhiteSpace(breakpoint.HitCondition)) { ScriptBlock actionScriptBlock = GetBreakpointActionScriptBlock(breakpoint); @@ -719,32 +723,85 @@ private ScriptBlock GetBreakpointActionScriptBlock( { try { - ScriptBlock actionScriptBlock = ScriptBlock.Create(breakpoint.Condition); + ScriptBlock actionScriptBlock; + int? hitCount = null; - // Check for simple, common errors that ScriptBlock parsing will not catch - // e.g. $i == 3 and $i > 3 - string message; - if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out message)) + // If HitCondition specified, parse and verify it. + if (!(String.IsNullOrWhiteSpace(breakpoint.HitCondition))) { - breakpoint.Verified = false; - breakpoint.Message = message; - return null; + int parsedHitCount; + + if (Int32.TryParse(breakpoint.HitCondition, out parsedHitCount)) + { + hitCount = parsedHitCount; + } + else + { + breakpoint.Verified = false; + breakpoint.Message = $"The specified HitCount '{breakpoint.HitCondition}' is not valid. " + + "The HitCount must be an integer number."; + return null; + } } - // Check for "advanced" condition syntax i.e. if the user has specified - // a "break" or "continue" statement anywhere in their scriptblock, - // pass their scriptblock through to the Action parameter as-is. - Ast breakOrContinueStatementAst = - actionScriptBlock.Ast.Find( - ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true); - - // If this isn't advanced syntax then the conditions string should be a simple - // expression that needs to be wrapped in a "if" test that conditionally executes - // a break statement. - if (breakOrContinueStatementAst == null) + // Create an Action scriptblock based on condition and/or hit count passed in. + if (hitCount.HasValue && String.IsNullOrWhiteSpace(breakpoint.Condition)) + { + // In the HitCount only case, this is simple as we can just use the HitCount + // property on the breakpoint object which is represented by $_. + string action = $"if ($_.HitCount -eq {hitCount}) {{ break }}"; + actionScriptBlock = ScriptBlock.Create(action); + } + else if (!String.IsNullOrWhiteSpace(breakpoint.Condition)) + { + // Must be either condition only OR condition and hit count. + actionScriptBlock = ScriptBlock.Create(breakpoint.Condition); + + // Check for simple, common errors that ScriptBlock parsing will not catch + // e.g. $i == 3 and $i > 3 + string message; + if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out message)) + { + breakpoint.Verified = false; + breakpoint.Message = message; + return null; + } + + // Check for "advanced" condition syntax i.e. if the user has specified + // a "break" or "continue" statement anywhere in their scriptblock, + // pass their scriptblock through to the Action parameter as-is. + Ast breakOrContinueStatementAst = + actionScriptBlock.Ast.Find( + ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true); + + // If this isn't advanced syntax then the conditions string should be a simple + // expression that needs to be wrapped in a "if" test that conditionally executes + // a break statement. + if (breakOrContinueStatementAst == null) + { + string wrappedCondition; + + if (hitCount.HasValue) + { + string globalHitCountVarName = + $"$global:__psEditorServices_BreakHitCounter_{breakpointHitCounter++}"; + + wrappedCondition = + $"if ({breakpoint.Condition}) {{ if (++{globalHitCountVarName} -eq {hitCount}) {{ break }} }}"; + } + else + { + wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}"; + } + + actionScriptBlock = ScriptBlock.Create(wrappedCondition); + } + } + else { - string wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}"; - actionScriptBlock = ScriptBlock.Create(wrappedCondition); + // Shouldn't get here unless someone called this with no condition and no hit count. + actionScriptBlock = ScriptBlock.Create("break"); + Logger.Write(LogLevel.Warning, "No condition and no hit count specified by caller."); } return actionScriptBlock; diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index 7c5371c6f..4e8aa6ec4 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -336,6 +336,79 @@ await this.debugService.SetLineBreakpoints( await executeTask; } + [Fact] + public async Task DebuggerStopsOnHitConditionBreakpoint() + { + const int hitCount = 5; + + BreakpointDetails[] breakpoints = + await this.debugService.SetLineBreakpoints( + this.debugScriptFile, + new[] { + BreakpointDetails.Create("", 6, null, null, $"{hitCount}"), + }); + + await this.AssertStateChange(PowerShellContextState.Ready); + + Task executeTask = + this.powerShellContext.ExecuteScriptAtPath( + this.debugScriptFile.FilePath); + + // Wait for conditional breakpoint to hit + await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 6); + + StackFrameDetails[] stackFrames = debugService.GetStackFrames(); + VariableDetailsBase[] variables = + debugService.GetVariables(stackFrames[0].LocalVariables.Id); + + // Verify the breakpoint only broke at the condition ie. $i -eq breakpointValue1 + var i = variables.FirstOrDefault(v => v.Name == "$i"); + Assert.NotNull(i); + Assert.False(i.IsExpandable); + Assert.Equal($"{hitCount}", i.ValueString); + + // Abort script execution early and wait for completion + this.debugService.Abort(); + await executeTask; + } + + [Fact] + public async Task DebuggerStopsOnConditionalAndHitConditionBreakpoint() + { + const int hitCount = 5; + + BreakpointDetails[] breakpoints = + await this.debugService.SetLineBreakpoints( + this.debugScriptFile, + new[] { + BreakpointDetails.Create("", 6, null, $"$i % 2 -eq 0", $"{hitCount}"), + }); + + await this.AssertStateChange(PowerShellContextState.Ready); + + Task executeTask = + this.powerShellContext.ExecuteScriptAtPath( + this.debugScriptFile.FilePath); + + // Wait for conditional breakpoint to hit + await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 6); + + StackFrameDetails[] stackFrames = debugService.GetStackFrames(); + VariableDetailsBase[] variables = + debugService.GetVariables(stackFrames[0].LocalVariables.Id); + + // Verify the breakpoint only broke at the condition ie. $i -eq breakpointValue1 + var i = variables.FirstOrDefault(v => v.Name == "$i"); + Assert.NotNull(i); + Assert.False(i.IsExpandable); + // Condition is even numbers ($i starting at 1) should end up on 10 with a hit count of 5. + Assert.Equal("10", i.ValueString); + + // Abort script execution early and wait for completion + this.debugService.Abort(); + await executeTask; + } + [Fact] public async Task DebuggerProvidesMessageForInvalidConditionalBreakpoint() { From 39507c9b0a6af3acd031002a93ef99603b838d9f Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Sat, 5 Nov 2016 10:41:32 -0600 Subject: [PATCH 05/22] Add PSMisleadingBacktick rule to the default ruleset. --- src/PowerShellEditorServices/Analysis/AnalysisService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Analysis/AnalysisService.cs b/src/PowerShellEditorServices/Analysis/AnalysisService.cs index 375af91e9..489518ccd 100644 --- a/src/PowerShellEditorServices/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices/Analysis/AnalysisService.cs @@ -38,7 +38,8 @@ public class AnalysisService : IDisposable "PSShouldProcess", "PSMissingModuleManifestField", "PSAvoidDefaultValueSwitchParameter", - "PSUseDeclaredVarsMoreThanAssigments" + "PSUseDeclaredVarsMoreThanAssigments", + "PSMisleadingBacktick", }; #endregion // Private Fields From f4612feed2db6efa79b51d3f638cd850140fe73b Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Thu, 17 Nov 2016 22:37:14 -0800 Subject: [PATCH 06/22] Remove PSScriptAnalyzer submodule --- .gitmodules | 4 ---- submodules/PSScriptAnalyzer | 1 - 2 files changed, 5 deletions(-) delete mode 100644 .gitmodules delete mode 160000 submodules/PSScriptAnalyzer diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 1362f1c8d..000000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "submodules/PSScriptAnalyzer"] - path = submodules/PSScriptAnalyzer - url = https://github.com/PowerShell/PSScriptAnalyzer.git - branch = master diff --git a/submodules/PSScriptAnalyzer b/submodules/PSScriptAnalyzer deleted file mode 160000 index e19af12de..000000000 --- a/submodules/PSScriptAnalyzer +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e19af12de15d9c8c6ec3defe307e37a8a89d0993 From 5c33f73ec02473f638fc296c8f81b965224cd5a8 Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Thu, 17 Nov 2016 22:50:02 -0800 Subject: [PATCH 07/22] Remove PSScriptAnalyzer submodule from solution --- PowerShellEditorServices.NoNano.sln | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/PowerShellEditorServices.NoNano.sln b/PowerShellEditorServices.NoNano.sln index 0c1275321..94d330215 100644 --- a/PowerShellEditorServices.NoNano.sln +++ b/PowerShellEditorServices.NoNano.sln @@ -7,12 +7,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F594E7FD-1E7 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{422E561A-8118-4BE7-A54F-9309E4F03AAE}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "submodules", "submodules", "{AF08DA0C-B0A6-47AD-AC55-E13C687D4A91}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScriptAnalyzerEngine", "submodules\PSScriptAnalyzer\Engine\ScriptAnalyzerEngine.csproj", "{F4BDE3D0-3EEF-4157-8A3E-722DF7ADEF60}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScriptAnalyzerBuiltinRules", "submodules\PSScriptAnalyzer\Rules\ScriptAnalyzerBuiltinRules.csproj", "{C33B6B9D-E61C-45A3-9103-895FD82A5C1E}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices", "src\PowerShellEditorServices\PowerShellEditorServices.csproj", "{81E8CBCD-6319-49E7-9662-0475BD0791F4}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices.Host", "src\PowerShellEditorServices.Host\PowerShellEditorServices.Host.csproj", "{B2F6369A-D737-4AFD-8B81-9B094DB07DA7}" @@ -53,14 +47,6 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F4BDE3D0-3EEF-4157-8A3E-722DF7ADEF60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F4BDE3D0-3EEF-4157-8A3E-722DF7ADEF60}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F4BDE3D0-3EEF-4157-8A3E-722DF7ADEF60}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F4BDE3D0-3EEF-4157-8A3E-722DF7ADEF60}.Release|Any CPU.Build.0 = Release|Any CPU - {C33B6B9D-E61C-45A3-9103-895FD82A5C1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C33B6B9D-E61C-45A3-9103-895FD82A5C1E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C33B6B9D-E61C-45A3-9103-895FD82A5C1E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C33B6B9D-E61C-45A3-9103-895FD82A5C1E}.Release|Any CPU.Build.0 = Release|Any CPU {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Debug|Any CPU.Build.0 = Debug|Any CPU {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -102,8 +88,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {F4BDE3D0-3EEF-4157-8A3E-722DF7ADEF60} = {AF08DA0C-B0A6-47AD-AC55-E13C687D4A91} - {C33B6B9D-E61C-45A3-9103-895FD82A5C1E} = {AF08DA0C-B0A6-47AD-AC55-E13C687D4A91} {81E8CBCD-6319-49E7-9662-0475BD0791F4} = {EE0A010C-E246-49AE-92E7-AD4320C45086} {B2F6369A-D737-4AFD-8B81-9B094DB07DA7} = {EE0A010C-E246-49AE-92E7-AD4320C45086} {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028} = {422E561A-8118-4BE7-A54F-9309E4F03AAE} From ec4d64b9f9b843087853ed39ad4b46a2bdad40b4 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 22 Nov 2016 11:33:29 -0800 Subject: [PATCH 08/22] Add missing file references to CoreCLR project --- .../Nano.PowerShellEditorServices.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/PowerShellEditorServices/Nano.PowerShellEditorServices.csproj b/src/PowerShellEditorServices/Nano.PowerShellEditorServices.csproj index 674cf0070..5c25279f1 100644 --- a/src/PowerShellEditorServices/Nano.PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/Nano.PowerShellEditorServices.csproj @@ -51,6 +51,7 @@ + @@ -158,6 +159,7 @@ + From 6ffdae129a2b6698e4f150eee022ca77f10e3370 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 22 Nov 2016 11:32:38 -0800 Subject: [PATCH 09/22] Add PowerShellVersionDetails class and PowerShellVersionRequest This change introduces a new PowerShellVersionDetails class which contains additional details about the version and runtime of the current PowerShell session: version, GitCommitId, edition, and processor architecture. A new protocol request and response pair was added for gathering this version information from the editor client. --- .../PowerShellVersionRequest.cs | 40 +++++++++++++ .../PowerShellEditorServices.Protocol.csproj | 1 + .../Server/LanguageServer.cs | 11 ++++ .../Nano.PowerShellEditorServices.csproj | 2 + .../PowerShellEditorServices.csproj | 1 + .../Session/PowerShellContext.cs | 34 ++++++++--- .../Session/PowerShellVersionDetails.cs | 59 +++++++++++++++++++ .../LanguageServerTests.cs | 15 +++++ 8 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/PowerShellVersionRequest.cs create mode 100644 src/PowerShellEditorServices/Session/PowerShellVersionDetails.cs diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/PowerShellVersionRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/PowerShellVersionRequest.cs new file mode 100644 index 000000000..260f24830 --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/PowerShellVersionRequest.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Session; + +namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer +{ + public class PowerShellVersionRequest + { + public static readonly + RequestType Type = + RequestType.Create("powerShell/getVersion"); + } + + public class PowerShellVersionResponse + { + public string Version { get; set; } + + public string DisplayVersion { get; set; } + + public string Edition { get; set; } + + public string Architecture { get; set; } + + public PowerShellVersionResponse() + { + } + + public PowerShellVersionResponse(PowerShellVersionDetails versionDetails) + { + this.Version = versionDetails.Version.ToString(); + this.DisplayVersion = versionDetails.VersionString; + this.Edition = versionDetails.Edition; + this.Architecture = versionDetails.Architecture; + } + } +} diff --git a/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj b/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj index b4095da4f..64fb6726c 100644 --- a/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj +++ b/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj @@ -58,6 +58,7 @@ + diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index 38a12a5d7..78cb0c929 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -101,6 +101,8 @@ protected override void Initialize() this.SetRequestHandler(InvokeExtensionCommandRequest.Type, this.HandleInvokeExtensionCommandRequest); + this.SetRequestHandler(PowerShellVersionRequest.Type, this.HandlePowerShellVersionRequest); + this.SetRequestHandler(DebugAdapterMessages.EvaluateRequest.Type, this.HandleEvaluateRequest); // Initialize the extension service @@ -799,6 +801,15 @@ protected async Task HandleWorkspaceSymbolRequest( await requestContext.SendResult(symbols.ToArray()); } + protected async Task HandlePowerShellVersionRequest( + object noParams, + RequestContext requestContext) + { + await requestContext.SendResult( + new PowerShellVersionResponse( + this.editorSession.PowerShellContext.PowerShellVersionDetails)); + } + private bool IsQueryMatch(string query, string symbolName) { return symbolName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; diff --git a/src/PowerShellEditorServices/Nano.PowerShellEditorServices.csproj b/src/PowerShellEditorServices/Nano.PowerShellEditorServices.csproj index 5c25279f1..f7354e5bb 100644 --- a/src/PowerShellEditorServices/Nano.PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/Nano.PowerShellEditorServices.csproj @@ -97,6 +97,7 @@ + @@ -137,6 +138,7 @@ + diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index bc4498d78..77e2bbd95 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -117,6 +117,7 @@ + diff --git a/src/PowerShellEditorServices/Session/PowerShellContext.cs b/src/PowerShellEditorServices/Session/PowerShellContext.cs index f31ef63f9..9e05c7053 100644 --- a/src/PowerShellEditorServices/Session/PowerShellContext.cs +++ b/src/PowerShellEditorServices/Session/PowerShellContext.cs @@ -75,12 +75,20 @@ public PowerShellContextState SessionState private set; } + /// + /// Gets the PowerShell version details for the current runspace. + /// + public PowerShellVersionDetails PowerShellVersionDetails + { + get; private set; + } + /// /// Gets the PowerShell version of the current runspace. /// public Version PowerShellVersion { - get; private set; + get { return this.PowerShellVersionDetails.Version; } } /// @@ -88,7 +96,7 @@ public Version PowerShellVersion /// public string PowerShellEdition { - get; private set; + get { return this.PowerShellVersionDetails.Edition; } } /// @@ -188,9 +196,7 @@ private void Initialize(ProfilePaths profilePaths, Runspace initialRunspace) this.powerShell.Runspace = this.currentRunspace; // Get the PowerShell runtime version - Tuple versionEditionTuple = GetPowerShellVersion(); - this.PowerShellVersion = versionEditionTuple.Item1; - this.PowerShellEdition = versionEditionTuple.Item2; + this.PowerShellVersionDetails = GetPowerShellVersion(); // Write out the PowerShell version for tracking purposes Logger.Write( @@ -246,10 +252,12 @@ private void Initialize(ProfilePaths profilePaths, Runspace initialRunspace) this.runspaceWaitQueue.EnqueueAsync(runspaceHandle).Wait(); } - private Tuple GetPowerShellVersion() + private PowerShellVersionDetails GetPowerShellVersion() { Version powerShellVersion = new Version(5, 0); + string versionString = null; string powerShellEdition = "Desktop"; + string architecture = "Unknown"; try { @@ -267,6 +275,14 @@ private Tuple GetPowerShellVersion() { powerShellEdition = edition; } + + var gitCommitId = psVersionTable["GitCommitId"] as string; + if (gitCommitId != null) + { + versionString = gitCommitId; + } + + architecture = this.currentRunspace.SessionStateProxy.GetVariable("env:PROCESSOR_ARCHITECTURE") as string; } } catch (Exception ex) @@ -274,7 +290,11 @@ private Tuple GetPowerShellVersion() Logger.Write(LogLevel.Warning, "Failed to look up PowerShell version. Defaulting to version 5. " + ex.Message); } - return new Tuple(powerShellVersion, powerShellEdition); + return new PowerShellVersionDetails( + powerShellVersion, + versionString, + powerShellEdition, + architecture); } #endregion diff --git a/src/PowerShellEditorServices/Session/PowerShellVersionDetails.cs b/src/PowerShellEditorServices/Session/PowerShellVersionDetails.cs new file mode 100644 index 000000000..e97e007f8 --- /dev/null +++ b/src/PowerShellEditorServices/Session/PowerShellVersionDetails.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; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Provides details about the version of the PowerShell runtime. + /// + public class PowerShellVersionDetails + { + /// + /// Gets the version of the PowerShell runtime. + /// + public Version Version { get; private set; } + + /// + /// Gets the full version string, either the ToString of the Version + /// property or the GitCommitId for open-source PowerShell releases. + /// + public string VersionString { get; private set; } + + /// + /// Gets the PowerShell edition (generally Desktop or Core). + /// + public string Edition { get; private set; } + + /// + /// Gets the architecture of the PowerShell process, either "x86" or + /// "x64". + /// + public string Architecture { get; private set; } + + /// + /// Creates an instance of the PowerShellVersionDetails class. + /// + /// The version of the PowerShell runtime. + /// A string representation of the PowerShell version. + /// The string representation of the PowerShell edition. + /// The string representation of the processor architecture. + public PowerShellVersionDetails( + Version powerShellVersion, + string versionString, + string editionString, + string architectureString) + { + this.Version = powerShellVersion; + this.VersionString = versionString ?? $"{powerShellVersion.Major}.{powerShellVersion.Minor}"; + this.Edition = editionString; + this.Architecture = + string.Equals(architectureString, "AMD64", StringComparison.CurrentCultureIgnoreCase) + ? "x64" + : architectureString; + } + } +} diff --git a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs index 1381a4fa2..d2e037265 100644 --- a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs +++ b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs @@ -702,6 +702,21 @@ await this.languageServiceClient.SendEvent( Assert.Equal("PROFILE: True", outputString); } + [Fact] + public async Task ServiceReturnsPowerShellVersionDetails() + { + PowerShellVersionResponse versionDetails = + await this.SendRequest( + PowerShellVersionRequest.Type, + new PowerShellVersionRequest()); + + // TODO: This should be more robust and predictable. + Assert.StartsWith("5.", versionDetails.Version); + Assert.StartsWith("5.", versionDetails.DisplayVersion); + Assert.Equal("Desktop", versionDetails.Edition); + Assert.Equal("x86", versionDetails.Architecture); + } + private async Task SendOpenFileEvent(string filePath, bool waitForDiagnostics = true) { string fileContents = string.Join(Environment.NewLine, File.ReadAllLines(filePath)); From 295c90175e5329ed9abcad1d10aaffaeff236285 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 29 Nov 2016 06:26:49 -0800 Subject: [PATCH 10/22] Fix issue with debugger output not being written for short scripts This change fixes an issue where script output for very small scripts is not being written out reliably before the debug adapter terminates. The fix is to add an extra output flush before sending the TerminatedEvent back to the client. This fix is temporary until we get the REPL integration online. Resolves #138. Resolves PowerShell/vscode-powershell#284. --- src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs index dcd346e93..86a87cc9b 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs @@ -79,7 +79,10 @@ protected Task LaunchScript(RequestContext requestContext) .ExecuteScriptAtPath(this.scriptPathToLaunch, this.arguments) .ContinueWith( async (t) => { - Logger.Write(LogLevel.Verbose, "Execution completed, terminating..."); + Logger.Write(LogLevel.Verbose, "Execution completed, flushing output then terminating..."); + + // Make sure remaining output is flushed before exiting + await this.outputDebouncer.Flush(); await requestContext.SendEvent( TerminatedEvent.Type, From 79231fca13349ac6362719a178acc3d6e3ee8887 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 1 Dec 2016 07:35:26 -0800 Subject: [PATCH 11/22] Improve PowerShell version and architecture gathering This change makes some slight tweaks to the PowerShell version and architecture gathering code to improve the user experience in the host editors which display those values. --- .../PowerShellVersionRequest.cs | 18 ++++++-- .../Session/PowerShellContext.cs | 36 ++++++++++++---- .../Session/PowerShellVersionDetails.cs | 43 +++++++++++++------ 3 files changed, 73 insertions(+), 24 deletions(-) diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/PowerShellVersionRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/PowerShellVersionRequest.cs index 260f24830..37f7ce1b0 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/PowerShellVersionRequest.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/PowerShellVersionRequest.cs @@ -31,10 +31,22 @@ public PowerShellVersionResponse() public PowerShellVersionResponse(PowerShellVersionDetails versionDetails) { - this.Version = versionDetails.Version.ToString(); - this.DisplayVersion = versionDetails.VersionString; + this.Version = versionDetails.VersionString; + this.DisplayVersion = $"{versionDetails.Version.Major}.{versionDetails.Version.Minor}"; this.Edition = versionDetails.Edition; - this.Architecture = versionDetails.Architecture; + + switch (versionDetails.Architecture) + { + case PowerShellProcessArchitecture.X64: + this.Architecture = "x64"; + break; + case PowerShellProcessArchitecture.X86: + this.Architecture = "x86"; + break; + default: + this.Architecture = "Architecture Unknown"; + break; + } } } } diff --git a/src/PowerShellEditorServices/Session/PowerShellContext.cs b/src/PowerShellEditorServices/Session/PowerShellContext.cs index 9e05c7053..280220372 100644 --- a/src/PowerShellEditorServices/Session/PowerShellContext.cs +++ b/src/PowerShellEditorServices/Session/PowerShellContext.cs @@ -257,32 +257,52 @@ private PowerShellVersionDetails GetPowerShellVersion() Version powerShellVersion = new Version(5, 0); string versionString = null; string powerShellEdition = "Desktop"; - string architecture = "Unknown"; + var architecture = PowerShellProcessArchitecture.Unknown; try { var psVersionTable = this.currentRunspace.SessionStateProxy.GetVariable("PSVersionTable") as Hashtable; if (psVersionTable != null) { - var version = psVersionTable["PSVersion"] as Version; - if (version != null) - { - powerShellVersion = version; - } - var edition = psVersionTable["PSEdition"] as string; if (edition != null) { powerShellEdition = edition; } + // The PSVersion value will either be of Version or SemanticVersion. + // In the former case, take the value directly. In the latter case, + // generate a Version from its string representation. + var version = psVersionTable["PSVersion"]; + if (version is Version) + { + powerShellVersion = (Version)version; + } + else if (string.Equals(powerShellEdition, "Core", StringComparison.CurrentCultureIgnoreCase)) + { + // Expected version string format is 6.0.0-alpha so build a simpler version from that + powerShellVersion = new Version(version.ToString().Split('-')[0]); + } + var gitCommitId = psVersionTable["GitCommitId"] as string; if (gitCommitId != null) { versionString = gitCommitId; } + else + { + versionString = powerShellVersion.ToString(); + } - architecture = this.currentRunspace.SessionStateProxy.GetVariable("env:PROCESSOR_ARCHITECTURE") as string; + var arch = this.currentRunspace.SessionStateProxy.GetVariable("env:PROCESSOR_ARCHITECTURE") as string; + if (string.Equals(arch, "AMD64", StringComparison.CurrentCultureIgnoreCase)) + { + architecture = PowerShellProcessArchitecture.X64; + } + else if (string.Equals(arch, "x86", StringComparison.CurrentCultureIgnoreCase)) + { + architecture = PowerShellProcessArchitecture.X86; + } } } catch (Exception ex) diff --git a/src/PowerShellEditorServices/Session/PowerShellVersionDetails.cs b/src/PowerShellEditorServices/Session/PowerShellVersionDetails.cs index e97e007f8..9f8a596e0 100644 --- a/src/PowerShellEditorServices/Session/PowerShellVersionDetails.cs +++ b/src/PowerShellEditorServices/Session/PowerShellVersionDetails.cs @@ -7,6 +7,27 @@ namespace Microsoft.PowerShell.EditorServices.Session { + /// + /// Defines the possible enumeration values for the PowerShell process architecture. + /// + public enum PowerShellProcessArchitecture + { + /// + /// The processor architecture is unknown or wasn't accessible. + /// + Unknown, + + /// + /// The processor architecture is 32-bit. + /// + X86, + + /// + /// The processor architecture is 64-bit. + /// + X64 + } + /// /// Provides details about the version of the PowerShell runtime. /// @@ -29,31 +50,27 @@ public class PowerShellVersionDetails public string Edition { get; private set; } /// - /// Gets the architecture of the PowerShell process, either "x86" or - /// "x64". + /// Gets the architecture of the PowerShell process. /// - public string Architecture { get; private set; } + public PowerShellProcessArchitecture Architecture { get; private set; } /// /// Creates an instance of the PowerShellVersionDetails class. /// - /// The version of the PowerShell runtime. + /// The version of the PowerShell runtime. /// A string representation of the PowerShell version. /// The string representation of the PowerShell edition. - /// The string representation of the processor architecture. + /// The processor architecture. public PowerShellVersionDetails( - Version powerShellVersion, + Version version, string versionString, string editionString, - string architectureString) + PowerShellProcessArchitecture architecture) { - this.Version = powerShellVersion; - this.VersionString = versionString ?? $"{powerShellVersion.Major}.{powerShellVersion.Minor}"; + this.Version = version; + this.VersionString = versionString; this.Edition = editionString; - this.Architecture = - string.Equals(architectureString, "AMD64", StringComparison.CurrentCultureIgnoreCase) - ? "x64" - : architectureString; + this.Architecture = architecture; } } } From 5f3e08eb96219e5ba0f6f6540b1732bfa292af90 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 24 Mar 2016 08:06:40 -0700 Subject: [PATCH 12/22] DRAFT: Initial "code actions" support --- .../LanguageServer/CodeAction.cs | 32 +++++++ .../LanguageServer/ServerCapabilities.cs | 2 + .../PowerShellEditorServices.Protocol.csproj | 3 +- .../Server/LanguageServer.cs | 88 ++++++++++++++++--- .../Analysis/AnalysisService.cs | 18 ++-- .../Workspace/ScriptFileMarker.cs | 60 +++++++++++-- 6 files changed, 175 insertions(+), 28 deletions(-) create mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/CodeAction.cs diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/CodeAction.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/CodeAction.cs new file mode 100644 index 000000000..deb0cb17f --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/CodeAction.cs @@ -0,0 +1,32 @@ +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Newtonsoft.Json.Linq; + +namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer +{ + public class CodeActionRequest + { + public static readonly + RequestType Type = + RequestType.Create("textDocument/codeAction"); + + public TextDocumentIdentifier TextDocument { get; set; } + + public Range Range { get; set; } + + public CodeActionContext Context { get; set; } + } + + public class CodeActionContext + { + public Diagnostic[] Diagnostics { get; set; } + } + + public class CodeActionCommand + { + public string Title { get; set; } + + public string Command { get; set; } + + public JArray Arguments { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs index 34ba312f8..aeebb411d 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs @@ -24,6 +24,8 @@ public class ServerCapabilities public bool? DocumentSymbolProvider { get; set; } public bool? WorkspaceSymbolProvider { get; set; } + + public bool? CodeActionProvider { get; set; } } /// diff --git a/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj b/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj index 64fb6726c..c26ab8381 100644 --- a/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj +++ b/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj @@ -56,6 +56,7 @@ + @@ -155,7 +156,7 @@ -