From 6ce3cd897ef54da8606a8cf15625027bcb16cc01 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Mon, 1 May 2023 00:02:53 +0200 Subject: [PATCH 01/10] Improve variable completion performance --- .../CommandCompletion/CompletionCompleters.cs | 470 +++++++++++------- 1 file changed, 300 insertions(+), 170 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 7d7ecf9ab91..0518da2f0c9 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4843,13 +4843,14 @@ public static IEnumerable CompleteVariable(string variableName internal static List CompleteVariable(CompletionContext context) { - HashSet hashedResults = new HashSet(StringComparer.OrdinalIgnoreCase); - List results = new List(); + HashSet hashedResults = new(StringComparer.OrdinalIgnoreCase); + List results = new(); + List tempResults = new(); var wordToComplete = context.WordToComplete; var colon = wordToComplete.IndexOf(':'); - var lastAst = context.RelatedAsts?.Last(); + var lastAst = context.RelatedAsts?[^1]; var variableAst = lastAst as VariableExpressionAst; var prefix = variableAst != null && variableAst.Splatted ? "@" : "$"; bool tokenAtCursorUsedBraces = context.TokenAtCursor is not null && context.TokenAtCursor.Text.StartsWith("${"); @@ -4857,10 +4858,10 @@ internal static List CompleteVariable(CompletionContext contex // Look for variables in the input (e.g. parameters, etc.) before checking session state - these // variables might not exist in session state yet. var wildcardPattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase); - if (lastAst != null) + if (lastAst is not null) { Ast parent = lastAst.Parent; - var findVariablesVisitor = new FindVariablesVisitor { CompletionVariableAst = lastAst }; + var findVariablesVisitor = new FindVariablesVisitor { CompletionVariableAst = lastAst, StopSearchOffset = lastAst.Extent.StartOffset }; while (parent != null) { if (parent is IParameterMetadataProvider) @@ -4872,95 +4873,58 @@ internal static List CompleteVariable(CompletionContext contex parent = parent.Parent; } - foreach (Tuple varAst in findVariablesVisitor.VariableSources) + foreach (string varName in findVariablesVisitor.FoundVariables) { - Ast astTarget = null; - string userPath = null; - - VariableExpressionAst variableDefinitionAst = varAst.Item2 as VariableExpressionAst; - if (variableDefinitionAst != null) - { - userPath = varAst.Item1; - astTarget = varAst.Item2.Parent; - } - else - { - CommandAst commandParameterAst = varAst.Item2 as CommandAst; - if (commandParameterAst != null) - { - userPath = varAst.Item1; - astTarget = varAst.Item2; - } - } - - if (string.IsNullOrEmpty(userPath)) + if (!wildcardPattern.IsMatch(varName)) { - Diagnostics.Assert(false, "Found a variable source but it was an unknown AST type."); + continue; } - if (wildcardPattern.IsMatch(userPath)) - { - var completedName = (userPath.IndexOfAny(s_charactersRequiringQuotes) == -1) - ? prefix + userPath - : prefix + "{" + userPath + "}"; - var tooltip = userPath; - var ast = astTarget; - - while (ast != null) - { - var parameterAst = ast as ParameterAst; - if (parameterAst != null) - { - var typeConstraint = parameterAst.Attributes.OfType().FirstOrDefault(); - if (typeConstraint != null) - { - tooltip = StringUtil.Format("{0}${1}", typeConstraint.Extent.Text, userPath); - } - - break; - } - - var assignmentAst = ast.Parent as AssignmentStatementAst; - if (assignmentAst != null) - { - if (assignmentAst.Left == ast) - { - tooltip = ast.Extent.Text; - } + var varInfo = findVariablesVisitor.VariableInfoTable[varName]; + var varType = varInfo.LastDeclaredConstraint is null + ? varInfo.LastAssignedType + : varInfo.LastDeclaredConstraint; + var toolTip = varType is null + ? varName + : StringUtil.Format("[{0}]${1}", ToStringCodeMethods.Type(varType, dropNamespaces: true), varName); - break; - } - - var commandAst = ast as CommandAst; - if (commandAst != null) - { - PSTypeName discoveredType = AstTypeInference.InferTypeOf(ast, context.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval).FirstOrDefault(); - if (discoveredType != null) - { - tooltip = StringUtil.Format("[{0}]${1}", discoveredType.Name, userPath); - } - - break; - } - - ast = ast.Parent; - } - - AddUniqueVariable(hashedResults, results, completedName, userPath, tooltip); - } + var completionText = (varName.IndexOfAny(s_charactersRequiringQuotes) == -1) + ? prefix + varName + : prefix + "{" + varName + "}"; + AddUniqueVariable(hashedResults, results, completionText, varName, toolTip); } } - string pattern; - string provider; if (colon == -1) { - pattern = "variable:" + wordToComplete + "*"; - provider = string.Empty; + var allVariables = context.ExecutionContext.SessionState.Internal.GetVariableTable(); + foreach (var key in allVariables.Keys) + { + if (wildcardPattern.IsMatch(key)) + { + var variable = allVariables[key]; + var name = variable.Name; + var value = variable.Value; + var toolTip = value is null + ? key + : StringUtil.Format("[{0}]${1}", ToStringCodeMethods.Type(value.GetType(), dropNamespaces: true), key); + var completionText = !tokenAtCursorUsedBraces && name.IndexOfAny(s_charactersRequiringQuotes) == -1 + ? prefix + name + : prefix + "{" + name + "}"; + AddUniqueVariable(hashedResults, tempResults, prefix + name, key, key); + } + } + + if (tempResults.Count > 0) + { + results.AddRange(tempResults.OrderBy(item => item.ListItemText, StringComparer.OrdinalIgnoreCase)); + tempResults.Clear(); + } } else { - provider = wordToComplete.Substring(0, colon + 1); + string provider = wordToComplete.Substring(0, colon + 1); + string pattern; if (s_variableScopes.Contains(provider, StringComparer.OrdinalIgnoreCase)) { pattern = string.Concat("variable:", wordToComplete.AsSpan(colon + 1), "*"); @@ -4969,65 +4933,57 @@ internal static List CompleteVariable(CompletionContext contex { pattern = wordToComplete + "*"; } - } - var powerShellExecutionHelper = context.Helper; - powerShellExecutionHelper - .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-Item").AddParameter("Path", pattern) - .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Sort-Object").AddParameter("Property", "Name"); + var powerShellExecutionHelper = context.Helper; + powerShellExecutionHelper + .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-Item").AddParameter("Path", pattern) + .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Sort-Object").AddParameter("Property", "Name"); - Exception exceptionThrown; - var psobjs = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); - if (psobjs != null) - { - foreach (dynamic psobj in psobjs) + var psobjs = powerShellExecutionHelper.ExecuteCurrentPowerShell(out _); + + if (psobjs is not null) { - var name = psobj.Name as string; - if (!string.IsNullOrEmpty(name)) + foreach (dynamic psobj in psobjs) { - var tooltip = name; - var variable = PSObject.Base(psobj) as PSVariable; - if (variable != null) + var name = psobj.Name as string; + if (!string.IsNullOrEmpty(name)) { - var value = variable.Value; - if (value != null) + var tooltip = name; + var variable = PSObject.Base(psobj) as PSVariable; + if (variable != null) { - tooltip = StringUtil.Format("[{0}]${1}", - ToStringCodeMethods.Type(value.GetType(), - dropNamespaces: true), name); + var value = variable.Value; + if (value != null) + { + tooltip = StringUtil.Format("[{0}]${1}", + ToStringCodeMethods.Type(value.GetType(), + dropNamespaces: true), name); + } } - } - var completedName = (!tokenAtCursorUsedBraces && name.IndexOfAny(s_charactersRequiringQuotes) == -1) - ? prefix + provider + name - : prefix + "{" + provider + name + "}"; - AddUniqueVariable(hashedResults, results, completedName, name, tooltip); + var completedName = (!tokenAtCursorUsedBraces && name.IndexOfAny(s_charactersRequiringQuotes) == -1) + ? prefix + provider + name + : prefix + "{" + provider + name + "}"; + AddUniqueVariable(hashedResults, results, completedName, name, tooltip); + } } } } if (colon == -1 && "env".StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) { - powerShellExecutionHelper - .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-Item").AddParameter("Path", "env:*") - .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Sort-Object").AddParameter("Property", "Key"); - - psobjs = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); - if (psobjs != null) + var envVars = Environment.GetEnvironmentVariables(); + foreach (var key in envVars.Keys) { - foreach (dynamic psobj in psobjs) - { - var name = psobj.Name as string; - if (!string.IsNullOrEmpty(name)) - { - name = "env:" + name; - var completedName = (!tokenAtCursorUsedBraces && name.IndexOfAny(s_charactersRequiringQuotes) == -1) - ? prefix + name - : prefix + "{" + name + "}"; - AddUniqueVariable(hashedResults, results, completedName, name, "[string]" + name); - } - } + var name = "env:" + key; + var completedName = !tokenAtCursorUsedBraces && name.IndexOfAny(s_charactersRequiringQuotes) == -1 + ? prefix + name + : prefix + "{" + name + "}"; + AddUniqueVariable(hashedResults, tempResults, completedName, name, "[string]" + name); } + + results.AddRange(tempResults.OrderBy(item => item.ListItemText, StringComparer.OrdinalIgnoreCase)); + tempResults.Clear(); } // Return variables already in session state first, because we can sometimes give better information, @@ -5046,41 +5002,35 @@ internal static List CompleteVariable(CompletionContext contex if (colon == -1) { - // If no drive was specified, then look for matching drives/scopes - pattern = wordToComplete + "*"; - powerShellExecutionHelper - .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-PSDrive").AddParameter("Name", pattern) - .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Sort-Object").AddParameter("Property", "Name"); - psobjs = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); - if (psobjs != null) + var allDrives = context.ExecutionContext.SessionState.Drive.GetAll(); + foreach (var drive in allDrives) { - foreach (var psobj in psobjs) + if (drive.Name.Length < 2 || !wildcardPattern.IsMatch(drive.Name)) { - var driveInfo = PSObject.Base(psobj) as PSDriveInfo; - if (driveInfo != null) - { - var name = driveInfo.Name; - if (name != null && !string.IsNullOrWhiteSpace(name) && name.Length > 1) - { - var completedName = (!tokenAtCursorUsedBraces && name.IndexOfAny(s_charactersRequiringQuotes) == -1) - ? prefix + name + ":" - : prefix + "{" + name + ":}"; - - var tooltip = string.IsNullOrEmpty(driveInfo.Description) ? name : driveInfo.Description; - AddUniqueVariable(hashedResults, results, completedName, name, tooltip); - } - } + continue; } + + var completedName = !tokenAtCursorUsedBraces && drive.Name.IndexOfAny(s_charactersRequiringQuotes) == -1 + ? prefix + drive.Name + ":" + : prefix + "{" + drive.Name + ":}"; + var tooltip = string.IsNullOrEmpty(drive.Description) + ? drive.Name + : drive.Description; + AddUniqueVariable(hashedResults, tempResults, completedName, drive.Name, tooltip); + } + + if (tempResults.Count > 0) + { + results.AddRange(tempResults.OrderBy(item => item.ListItemText, StringComparer.OrdinalIgnoreCase)); } - var scopePattern = WildcardPattern.Get(pattern, WildcardOptions.IgnoreCase); foreach (var scope in s_variableScopes) { - if (scopePattern.IsMatch(scope)) + if (wildcardPattern.IsMatch(scope)) { var completedName = (!tokenAtCursorUsedBraces && scope.IndexOfAny(s_charactersRequiringQuotes) == -1) - ? prefix + scope - : prefix + "{" + scope + "}"; + ? prefix + scope + : prefix + "{" + scope + "}"; AddUniqueVariable(hashedResults, results, completedName, scope, scope); } } @@ -5091,50 +5041,206 @@ internal static List CompleteVariable(CompletionContext contex private static void AddUniqueVariable(HashSet hashedResults, List results, string completionText, string listItemText, string tooltip) { - if (!hashedResults.Contains(completionText)) + if (hashedResults.Add(completionText)) { - hashedResults.Add(completionText); results.Add(new CompletionResult(completionText, listItemText, CompletionResultType.Variable, tooltip)); } } + private static readonly HashSet s_varModificationCommands = new(StringComparer.OrdinalIgnoreCase) + { + "New-Variable", + "nv", + "Set-Variable", + "set", + "sv" + }; + + private static readonly string[] s_varModificationParameters = new string[] + { + "Name", + "Value" + }; + + private static readonly string[] s_outVarParameters = new string[] + { + "ErrorVariable", + "ev", + "WarningVariable", + "wv", + "InformationVariable", + "iv", + "OutVariable", + "ov", + + }; + + private sealed class VariableInfo + { + internal Type LastDeclaredConstraint; + internal Type LastAssignedType; + } + private sealed class FindVariablesVisitor : AstVisitor { internal Ast Top; internal Ast CompletionVariableAst; - internal readonly List> VariableSources = new List>(); + internal readonly List FoundVariables = new(); + internal readonly Dictionary VariableInfoTable = new(StringComparer.OrdinalIgnoreCase); + internal int StopSearchOffset; - public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) + private static Type GetInferredVarTypeFromAst(Ast ast) { - if (variableExpressionAst != CompletionVariableAst) + Type type; + switch (ast) { - VariableSources.Add(new Tuple(variableExpressionAst.VariablePath.UserPath, variableExpressionAst)); + case ConstantExpressionAst constant: + type = constant.StaticType; + break; + + case ExpandableStringExpressionAst: + type = typeof(string); + break; + + case ConvertExpressionAst convertExpression: + type = convertExpression.StaticType; + break; + + case HashtableAst: + type = typeof(Hashtable); + break; + + case ArrayExpressionAst: + case ArrayLiteralAst: + type = typeof(object[]); + break; + + case ScriptBlockExpressionAst: + type = typeof(ScriptBlock); + break; + + default: + type = null; + break; + } + + return type; + } + + private void SaveVariableInfo(string variableName, Type variableType, bool isConstraint) + { + VariableInfo varInfo; + if (VariableInfoTable.TryGetValue(variableName, out varInfo)) + { + if (isConstraint) + { + varInfo.LastDeclaredConstraint = variableType; + } + else + { + varInfo.LastAssignedType = variableType; + } + } + else + { + varInfo = isConstraint + ? new VariableInfo() { LastDeclaredConstraint = variableType } + : new VariableInfo() { LastAssignedType = variableType }; + VariableInfoTable.Add(variableName, varInfo); + FoundVariables.Add(variableName); + } + } + + public override AstVisitAction DefaultVisit(Ast ast) + { + if (ast.Extent.StartOffset > StopSearchOffset) + { + return AstVisitAction.StopVisit; } return AstVisitAction.Continue; } - public override AstVisitAction VisitCommand(CommandAst commandAst) + public override AstVisitAction VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) { - // MSFT: 784739 Stack overflow during tab completion of pipeline variable - // $null | % -pv p { $p -> In this case $p is pipelinevariable - // and is used in the same command. PipelineVariables are not available - // in the command they are assigned in. Hence the following code ignores - // if the variable being completed is in the command extent. - if ((commandAst != CompletionVariableAst) && (!CompletionVariableAst.Extent.IsWithin(commandAst.Extent))) + if (assignmentStatementAst.Extent.StartOffset > StopSearchOffset) { - string[] desiredParameters = new string[] { "PV", "PipelineVariable", "OV", "OutVariable" }; + return AstVisitAction.StopVisit; + } - StaticBindingResult bindingResult = StaticParameterBinder.BindCommand(commandAst, false, desiredParameters); - if (bindingResult != null) + if (assignmentStatementAst.Left is ConvertExpressionAst convertExpression) + { + if (convertExpression.Child is VariableExpressionAst variableExpression) { - ParameterBindingResult parameterBindingResult; + if (variableExpression == CompletionVariableAst) + { + return AstVisitAction.Continue; + } - foreach (string commandVariableParameter in desiredParameters) + SaveVariableInfo(variableExpression.VariablePath.UserPath, convertExpression.StaticType, isConstraint: true); + } + } + else if (assignmentStatementAst.Left is VariableExpressionAst variableExpression) + { + if (variableExpression == CompletionVariableAst) + { + return AstVisitAction.Continue; + } + + Type lastAssignedType; + if (assignmentStatementAst.Right is CommandExpressionAst commandExpression) + { + lastAssignedType = GetInferredVarTypeFromAst(commandExpression.Expression); + } + else + { + lastAssignedType = null; + } + + SaveVariableInfo(variableExpression.VariablePath.UserPath, lastAssignedType, isConstraint: false); + } + + return AstVisitAction.Continue; + } + + public override AstVisitAction VisitCommand(CommandAst commandAst) + { + var commandName = commandAst.GetCommandName(); + if (commandName is not null && s_varModificationCommands.Contains(commandName)) + { + StaticBindingResult bindingResult = StaticParameterBinder.BindCommand(commandAst, resolve: false, s_varModificationParameters); + if (bindingResult is not null + && bindingResult.BoundParameters.TryGetValue("Name", out ParameterBindingResult variableName)) + { + var nameValue = variableName.ConstantValue as string; + if (nameValue is not null) + { + Type variableType; + if (bindingResult.BoundParameters.TryGetValue("Value", out ParameterBindingResult variableValue)) + { + variableType = GetInferredVarTypeFromAst(variableValue.Value); + } + else + { + variableType = null; + } + + SaveVariableInfo(nameValue, variableType, isConstraint: false); + } + } + } + + var bindResult = StaticParameterBinder.BindCommand(commandAst, resolve: false, s_outVarParameters); + if (bindResult is not null) + { + foreach (var parameterName in s_outVarParameters) + { + if (bindResult.BoundParameters.TryGetValue(parameterName, out ParameterBindingResult outVarBind)) { - if (bindingResult.BoundParameters.TryGetValue(commandVariableParameter, out parameterBindingResult)) + var varName = outVarBind.ConstantValue as string; + if (varName is not null) { - VariableSources.Add(new Tuple((string)parameterBindingResult.ConstantValue, commandAst)); + SaveVariableInfo(varName, typeof(ArrayList), isConstraint: false); } } } @@ -5143,6 +5249,30 @@ public override AstVisitAction VisitCommand(CommandAst commandAst) return AstVisitAction.Continue; } + public override AstVisitAction VisitParameter(ParameterAst parameterAst) + { + if (parameterAst.Extent.StartOffset > StopSearchOffset) + { + return AstVisitAction.StopVisit; + } + + VariableExpressionAst variableExpression = parameterAst.Name; + if (variableExpression == CompletionVariableAst) + { + return AstVisitAction.Continue; + } + + SaveVariableInfo(variableExpression.VariablePath.UserPath, parameterAst.StaticType, isConstraint: true); + + return AstVisitAction.Continue; + } + + public override AstVisitAction VisitAttribute(AttributeAst attributeAst) + { + // Attributes can't assign values to variables so they aren't interesting. + return AstVisitAction.SkipChildren; + } + public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) { return functionDefinitionAst != Top ? AstVisitAction.SkipChildren : AstVisitAction.Continue; From 099e602a804fc3f8afaefd161c7cb835875f2511 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Mon, 1 May 2023 01:23:56 +0200 Subject: [PATCH 02/10] Fix variable with brace completion --- .../engine/CommandCompletion/CompletionCompleters.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 0518da2f0c9..6b6791f41b3 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4888,7 +4888,7 @@ internal static List CompleteVariable(CompletionContext contex ? varName : StringUtil.Format("[{0}]${1}", ToStringCodeMethods.Type(varType, dropNamespaces: true), varName); - var completionText = (varName.IndexOfAny(s_charactersRequiringQuotes) == -1) + var completionText = !tokenAtCursorUsedBraces && varName.IndexOfAny(s_charactersRequiringQuotes) == -1 ? prefix + varName : prefix + "{" + varName + "}"; AddUniqueVariable(hashedResults, results, completionText, varName, toolTip); @@ -4911,7 +4911,7 @@ internal static List CompleteVariable(CompletionContext contex var completionText = !tokenAtCursorUsedBraces && name.IndexOfAny(s_charactersRequiringQuotes) == -1 ? prefix + name : prefix + "{" + name + "}"; - AddUniqueVariable(hashedResults, tempResults, prefix + name, key, key); + AddUniqueVariable(hashedResults, tempResults, completionText, key, key); } } @@ -4961,7 +4961,7 @@ internal static List CompleteVariable(CompletionContext contex } } - var completedName = (!tokenAtCursorUsedBraces && name.IndexOfAny(s_charactersRequiringQuotes) == -1) + var completedName = !tokenAtCursorUsedBraces && name.IndexOfAny(s_charactersRequiringQuotes) == -1 ? prefix + provider + name : prefix + "{" + provider + name + "}"; AddUniqueVariable(hashedResults, results, completedName, name, tooltip); @@ -4992,7 +4992,7 @@ internal static List CompleteVariable(CompletionContext contex { if (wildcardPattern.IsMatch(specialVariable)) { - var completedName = (!tokenAtCursorUsedBraces && specialVariable.IndexOfAny(s_charactersRequiringQuotes) == -1) + var completedName = !tokenAtCursorUsedBraces && specialVariable.IndexOfAny(s_charactersRequiringQuotes) == -1 ? prefix + specialVariable : prefix + "{" + specialVariable + "}"; @@ -5028,7 +5028,7 @@ internal static List CompleteVariable(CompletionContext contex { if (wildcardPattern.IsMatch(scope)) { - var completedName = (!tokenAtCursorUsedBraces && scope.IndexOfAny(s_charactersRequiringQuotes) == -1) + var completedName = !tokenAtCursorUsedBraces && scope.IndexOfAny(s_charactersRequiringQuotes) == -1 ? prefix + scope : prefix + "{" + scope + "}"; AddUniqueVariable(hashedResults, results, completedName, scope, scope); From 6d3a72e320cfa4dd2218d850035900443da6643f Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Mon, 1 May 2023 17:56:53 +0200 Subject: [PATCH 03/10] Add tests and add back PipelineVariable support. --- .../CommandCompletion/CompletionCompleters.cs | 36 +++++++++++++++++-- .../TabCompletion/TabCompletion.Tests.ps1 | 23 ++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 6b6791f41b3..006728fd0fa 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4861,7 +4861,13 @@ internal static List CompleteVariable(CompletionContext contex if (lastAst is not null) { Ast parent = lastAst.Parent; - var findVariablesVisitor = new FindVariablesVisitor { CompletionVariableAst = lastAst, StopSearchOffset = lastAst.Extent.StartOffset }; + var findVariablesVisitor = new FindVariablesVisitor + { + CompletionVariableAst = lastAst, + StopSearchOffset = + lastAst.Extent.StartOffset, + Context = context.TypeInferenceContext + }; while (parent != null) { if (parent is IParameterMetadataProvider) @@ -5075,6 +5081,12 @@ private static void AddUniqueVariable(HashSet hashedResults, List FoundVariables = new(); internal readonly Dictionary VariableInfoTable = new(StringComparer.OrdinalIgnoreCase); internal int StopSearchOffset; + internal TypeInferenceContext Context; private static Type GetInferredVarTypeFromAst(Ast ast) { @@ -5230,7 +5243,7 @@ public override AstVisitAction VisitCommand(CommandAst commandAst) } } - var bindResult = StaticParameterBinder.BindCommand(commandAst, resolve: false, s_outVarParameters); + var bindResult = StaticParameterBinder.BindCommand(commandAst, resolve: false); if (bindResult is not null) { foreach (var parameterName in s_outVarParameters) @@ -5244,6 +5257,25 @@ public override AstVisitAction VisitCommand(CommandAst commandAst) } } } + + if (commandAst.Parent is PipelineAst pipeline && pipeline.Extent.EndOffset > CompletionVariableAst.Extent.StartOffset) + { + foreach (var parameterName in s_pipelineVariableParameters) + { + if (bindResult.BoundParameters.TryGetValue(parameterName, out ParameterBindingResult outVarBind)) + { + var varName = outVarBind.ConstantValue as string; + if (varName is not null) + { + var inferredTypes = AstTypeInference.InferTypeOf(commandAst, Context, TypeInferenceRuntimePermissions.AllowSafeEval); + Type varType = inferredTypes.Count == 0 + ? null + : inferredTypes[0].Type; + SaveVariableInfo(varName, varType, isConstraint: false); + } + } + } + } } return AstVisitAction.Continue; diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index c56cd2ca94a..c36becd6f09 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -640,6 +640,29 @@ ConstructorTestClass(int i, bool b) $res.CompletionMatches[0].CompletionText | Should -BeExactly 'CommandType' } + It 'Should not complete variables that appear after the cursor' { + $Script = '$TestVar1 = 1; $TestVar^ ; $TestVar2 = 2' + $CursorIndex = $TestString.IndexOf('^') + $res = TabExpansion2 -cursorColumn $CursorIndex -inputScript $TestString.Remove($CursorIndex, 1) + $res | Should -HaveCount 1 + $res.CompletionMatches[0].CompletionText | Should -BeExactly '$TestVar1' + } + + It 'Should not complete pipeline variables outside the pipeline' { + $Script = 'Get-ChildItem -PipelineVariable TestVar1;$TestVar^' + $CursorIndex = $TestString.IndexOf('^') + $res = TabExpansion2 -cursorColumn $CursorIndex -inputScript $TestString.Remove($CursorIndex, 1) + $res | Should -HaveCount 0 + } + + It 'Should complete pipeline variables inside the pipeline' { + $Script = 'Get-ChildItem -PipelineVariable TestVar1 | ForEach-Object -Process {$TestVar^}' + $CursorIndex = $TestString.IndexOf('^') + $res = TabExpansion2 -cursorColumn $CursorIndex -inputScript $TestString.Remove($CursorIndex, 1) + $res | Should -HaveCount 1 + $res.CompletionMatches[0].CompletionText | Should -BeExactly '$TestVar1' + } + Context "Format cmdlet's View paramter completion" { BeforeAll { $viewDefinition = @' From 74579b1dbfe9769c1d6f3cf46ae693a17c3ed6e6 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Mon, 1 May 2023 19:45:53 +0200 Subject: [PATCH 04/10] Fix tests --- .../powershell/Host/TabCompletion/TabCompletion.Tests.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index c36becd6f09..f2cb6f9a42f 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -641,7 +641,7 @@ ConstructorTestClass(int i, bool b) } It 'Should not complete variables that appear after the cursor' { - $Script = '$TestVar1 = 1; $TestVar^ ; $TestVar2 = 2' + $TestString = '$TestVar1 = 1; $TestVar^ ; $TestVar2 = 2' $CursorIndex = $TestString.IndexOf('^') $res = TabExpansion2 -cursorColumn $CursorIndex -inputScript $TestString.Remove($CursorIndex, 1) $res | Should -HaveCount 1 @@ -649,14 +649,14 @@ ConstructorTestClass(int i, bool b) } It 'Should not complete pipeline variables outside the pipeline' { - $Script = 'Get-ChildItem -PipelineVariable TestVar1;$TestVar^' + $TestString = 'Get-ChildItem -PipelineVariable TestVar1;$TestVar^' $CursorIndex = $TestString.IndexOf('^') $res = TabExpansion2 -cursorColumn $CursorIndex -inputScript $TestString.Remove($CursorIndex, 1) - $res | Should -HaveCount 0 + $res.CompletionMatches | Should -HaveCount 0 } It 'Should complete pipeline variables inside the pipeline' { - $Script = 'Get-ChildItem -PipelineVariable TestVar1 | ForEach-Object -Process {$TestVar^}' + $TestString = 'Get-ChildItem -PipelineVariable TestVar1 | ForEach-Object -Process {$TestVar^}' $CursorIndex = $TestString.IndexOf('^') $res = TabExpansion2 -cursorColumn $CursorIndex -inputScript $TestString.Remove($CursorIndex, 1) $res | Should -HaveCount 1 From 64140e390004e24dd3bbfec55e51054fc335a7b8 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Sat, 6 May 2023 23:55:02 +0200 Subject: [PATCH 05/10] Exclude special variables from analysis --- .../engine/CommandCompletion/CompletionCompleters.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 006728fd0fa..c717063ee70 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -5185,7 +5185,7 @@ public override AstVisitAction VisitAssignmentStatement(AssignmentStatementAst a { if (convertExpression.Child is VariableExpressionAst variableExpression) { - if (variableExpression == CompletionVariableAst) + if (variableExpression == CompletionVariableAst || s_specialVariablesCache.Value.Contains(variableExpression.VariablePath.UserPath)) { return AstVisitAction.Continue; } @@ -5195,7 +5195,7 @@ public override AstVisitAction VisitAssignmentStatement(AssignmentStatementAst a } else if (assignmentStatementAst.Left is VariableExpressionAst variableExpression) { - if (variableExpression == CompletionVariableAst) + if (variableExpression == CompletionVariableAst || s_specialVariablesCache.Value.Contains(variableExpression.VariablePath.UserPath)) { return AstVisitAction.Continue; } @@ -5325,7 +5325,7 @@ public override AstVisitAction VisitScriptBlock(ScriptBlockAst scriptBlockAst) private static SortedSet BuildSpecialVariablesCache() { - var result = new SortedSet(); + var result = new SortedSet(StringComparer.OrdinalIgnoreCase); foreach (var member in typeof(SpecialVariables).GetFields(BindingFlags.NonPublic | BindingFlags.Static)) { if (member.FieldType.Equals(typeof(string))) From 7ac09792955066e0035e50f3ef5e064f460961cb Mon Sep 17 00:00:00 2001 From: MartinGC94 <42123497+MartinGC94@users.noreply.github.com> Date: Tue, 9 May 2023 11:55:13 +0200 Subject: [PATCH 06/10] Update src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs Co-authored-by: Dongbo Wang --- .../engine/CommandCompletion/CompletionCompleters.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index c717063ee70..61a819a6a71 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4887,9 +4887,7 @@ internal static List CompleteVariable(CompletionContext contex } var varInfo = findVariablesVisitor.VariableInfoTable[varName]; - var varType = varInfo.LastDeclaredConstraint is null - ? varInfo.LastAssignedType - : varInfo.LastDeclaredConstraint; + var varType = varInfo.LastDeclaredConstraint ?? varInfo.LastAssignedType; var toolTip = varType is null ? varName : StringUtil.Format("[{0}]${1}", ToStringCodeMethods.Type(varType, dropNamespaces: true), varName); From 3d9cfce19cc1aa31c7bb9c2ab74a9b2ffc295f35 Mon Sep 17 00:00:00 2001 From: MartinGC94 <42123497+MartinGC94@users.noreply.github.com> Date: Tue, 9 May 2023 11:59:13 +0200 Subject: [PATCH 07/10] Update src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs Co-authored-by: Dongbo Wang --- .../engine/CommandCompletion/CompletionCompleters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 61a819a6a71..3ebe379274a 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -5260,7 +5260,7 @@ public override AstVisitAction VisitCommand(CommandAst commandAst) { foreach (var parameterName in s_pipelineVariableParameters) { - if (bindResult.BoundParameters.TryGetValue(parameterName, out ParameterBindingResult outVarBind)) + if (bindResult.BoundParameters.TryGetValue(parameterName, out ParameterBindingResult pipeVarBind)) { var varName = outVarBind.ConstantValue as string; if (varName is not null) From 96e7ebde056fb95384be888830938b0ab5be1e22 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Tue, 9 May 2023 14:55:22 +0200 Subject: [PATCH 08/10] Fix var name and ignore drives from providers that don't implement IContentCmdletProvider --- .../engine/CommandCompletion/CompletionCompleters.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 3ebe379274a..7e83088fead 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -12,6 +12,7 @@ using System.Linq; using System.Management.Automation.Internal; using System.Management.Automation.Language; +using System.Management.Automation.Provider; using System.Management.Automation.Runspaces; using System.Reflection; using System.Runtime.InteropServices; @@ -5009,7 +5010,9 @@ internal static List CompleteVariable(CompletionContext contex var allDrives = context.ExecutionContext.SessionState.Drive.GetAll(); foreach (var drive in allDrives) { - if (drive.Name.Length < 2 || !wildcardPattern.IsMatch(drive.Name)) + if (drive.Name.Length < 2 + || !wildcardPattern.IsMatch(drive.Name) + || !drive.Provider.ImplementingType.IsAssignableTo(typeof(IContentCmdletProvider))) { continue; } @@ -5262,7 +5265,7 @@ public override AstVisitAction VisitCommand(CommandAst commandAst) { if (bindResult.BoundParameters.TryGetValue(parameterName, out ParameterBindingResult pipeVarBind)) { - var varName = outVarBind.ConstantValue as string; + var varName = pipeVarBind.ConstantValue as string; if (varName is not null) { var inferredTypes = AstTypeInference.InferTypeOf(commandAst, Context, TypeInferenceRuntimePermissions.AllowSafeEval); From d3f7511f1188f25714493e8e48f1d4fb476121fb Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 9 May 2023 11:51:56 -0700 Subject: [PATCH 09/10] A minor syntax update --- .../engine/CommandCompletion/CompletionCompleters.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 7e83088fead..c947c70f1d5 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4865,8 +4865,7 @@ internal static List CompleteVariable(CompletionContext contex var findVariablesVisitor = new FindVariablesVisitor { CompletionVariableAst = lastAst, - StopSearchOffset = - lastAst.Extent.StartOffset, + StopSearchOffset = lastAst.Extent.StartOffset, Context = context.TypeInferenceContext }; while (parent != null) From 9208fe7b86998b1031c2b6b1a79c363f844c0d62 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Tue, 9 May 2023 20:52:27 +0200 Subject: [PATCH 10/10] Add check stop offset check to VisitCommand --- .../engine/CommandCompletion/CompletionCompleters.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 7e83088fead..2c043c42839 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -5219,6 +5219,11 @@ public override AstVisitAction VisitAssignmentStatement(AssignmentStatementAst a public override AstVisitAction VisitCommand(CommandAst commandAst) { + if (commandAst.Extent.StartOffset > StopSearchOffset) + { + return AstVisitAction.StopVisit; + } + var commandName = commandAst.GetCommandName(); if (commandName is not null && s_varModificationCommands.Contains(commandName)) {