From 6276e7f105d13babfcfb9f68568148b7198a406d Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Sun, 21 Nov 2021 04:35:09 +0100 Subject: [PATCH 01/13] Fix some hashtable completion issues. Prevent completion of keys already in a hashtable and add argument completers for Get-WinEvent and Invoke-CimMethod --- .../CommandCompletion/CompletionAnalysis.cs | 74 ++++----- .../CommandCompletion/CompletionCompleters.cs | 152 +++++++++++++++--- .../Host/TabCompletion/BugFix.Tests.ps1 | 5 +- .../TabCompletion/TabCompletion.Tests.ps1 | 40 +++++ 4 files changed, 202 insertions(+), 69 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 18bd6cbed3d..0677f3ab92c 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -416,7 +416,7 @@ internal List GetResultHelper(CompletionContext completionCont case TokenKind.Generic: case TokenKind.MinusMinus: // for native commands '--' case TokenKind.Identifier: - result = GetResultForIdentifier(completionContext, ref replacementIndex, ref replacementLength, isQuotedString); + result = GetResultForIdentifier(completionContext, ref replacementIndex, ref replacementLength, isQuotedString); break; case TokenKind.Parameter: @@ -918,6 +918,11 @@ internal List GetResultHelper(CompletionContext completionCont if (result == null || result.Count == 0) { result = GetResultForHashtable(completionContext); + if (result is not null && result.Count > 0) + { + completionContext.ReplacementIndex = replacementIndex = completionContext.CursorPosition.Offset; + completionContext.ReplacementLength = replacementLength = 0; + } } if (result == null || result.Count == 0) @@ -940,61 +945,46 @@ internal List GetResultHelper(CompletionContext completionCont // Helper method to auto complete hashtable key private static List GetResultForHashtable(CompletionContext completionContext) { - var lastAst = completionContext.RelatedAsts.Last(); - HashtableAst tempHashtableAst = null; - IScriptPosition cursor = completionContext.CursorPosition; - var hashTableAst = lastAst as HashtableAst; - if (hashTableAst != null) + Ast lastRelatedAst = null; + var cursorPosition = completionContext.CursorPosition; + + for (int i = completionContext.RelatedAsts.Count - 1; i >= 0; i--) { - // Check if the cursor within the hashtable - if (cursor.Offset < hashTableAst.Extent.EndOffset) + Ast ast = completionContext.RelatedAsts[i]; + if (cursorPosition.Offset >= ast.Extent.StartOffset && cursorPosition.Offset <= ast.Extent.EndOffset) { - tempHashtableAst = hashTableAst; + lastRelatedAst = ast; + break; } - else if (cursor.Offset == hashTableAst.Extent.EndOffset) + } + if (lastRelatedAst is HashtableAst hashtableAst) + { + if (completionContext.TokenAtCursor is not null && completionContext.TokenAtCursor.Kind == TokenKind.RCurly) + { + return null; + } + bool cursorIsWithinOrOnSameLineAsKeypair = false; + foreach (var pair in hashtableAst.KeyValuePairs) { - // Exclude the scenario that cursor at the end of hashtable, i.e. after '}' - if (completionContext.TokenAtCursor == null || - completionContext.TokenAtCursor.Kind != TokenKind.RCurly) + if (cursorPosition.Offset >= pair.Item1.Extent.StartOffset + && (cursorPosition.Offset <= pair.Item2.Extent.EndOffset || cursorPosition.LineNumber == pair.Item2.Extent.EndLineNumber)) { - tempHashtableAst = hashTableAst; + cursorIsWithinOrOnSameLineAsKeypair = true; + break; } } - } - else - { - // Handle property completion on a blank line for DynamicKeyword statement - Ast lastChildofHashtableAst; - hashTableAst = Ast.GetAncestorHashtableAst(lastAst, out lastChildofHashtableAst); - - // Check if the hashtable within a DynamicKeyword statement - if (hashTableAst != null) + if (cursorIsWithinOrOnSameLineAsKeypair) { - var keywordAst = Ast.GetAncestorAst(hashTableAst); - if (keywordAst != null) + var tokenBeforeOrAtCursor = completionContext.TokenBeforeCursor ?? completionContext.TokenAtCursor; + if (tokenBeforeOrAtCursor.Kind != TokenKind.Semi) { - // Handle only empty line - if (string.IsNullOrWhiteSpace(cursor.Line)) - { - // Check if the cursor outside of last child of hashtable and within the hashtable - if (cursor.Offset > lastChildofHashtableAst.Extent.EndOffset && - cursor.Offset <= hashTableAst.Extent.EndOffset) - { - tempHashtableAst = hashTableAst; - } - } + return null; } } - } - - hashTableAst = tempHashtableAst; - if (hashTableAst != null) - { completionContext.ReplacementIndex = completionContext.CursorPosition.Offset; completionContext.ReplacementLength = 0; - return CompletionCompleters.CompleteHashtableKey(completionContext, hashTableAst); + return CompletionCompleters.CompleteHashtableKey(completionContext, hashtableAst); } - return null; } diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index d8360e714d8..84ac4bb1e71 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -2274,7 +2274,7 @@ private static void NativeCommandArgumentCompletion( { if (parameterName.Equals("MemberName", StringComparison.OrdinalIgnoreCase)) { - NativeCompletionMemberName(context, result, commandAst); + NativeCompletionMemberName(context, result, commandAst, boundArguments?[parameterName]); } break; @@ -2286,7 +2286,7 @@ private static void NativeCommandArgumentCompletion( { if (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase)) { - NativeCompletionMemberName(context, result, commandAst); + NativeCompletionMemberName(context, result, commandAst, boundArguments?[parameterName]); } break; @@ -2298,7 +2298,7 @@ private static void NativeCommandArgumentCompletion( { if (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase)) { - NativeCompletionMemberName(context, result, commandAst); + NativeCompletionMemberName(context, result, commandAst, boundArguments?[parameterName]); } else if (parameterName.Equals("View", StringComparison.OrdinalIgnoreCase)) { @@ -2313,7 +2313,7 @@ private static void NativeCommandArgumentCompletion( || parameterName.Equals("ExcludeProperty", StringComparison.OrdinalIgnoreCase) || parameterName.Equals("ExpandProperty", StringComparison.OrdinalIgnoreCase)) { - NativeCompletionMemberName(context, result, commandAst); + NativeCompletionMemberName(context, result, commandAst, boundArguments?[parameterName]); } break; @@ -2336,7 +2336,10 @@ private static void NativeCommandArgumentCompletion( case "New-CimInstance": case "Register-CimIndicationEvent": { - NativeCompletionCimCommands(parameterName, boundArguments, result, commandAst, context); + if (!parameterName.Equals("Arguments", StringComparison.OrdinalIgnoreCase)) + { + NativeCompletionCimCommands(parameterName, boundArguments, result, commandAst, context, excludedValues: null); + } break; } @@ -2507,7 +2510,8 @@ private static void NativeCompletionCimCommands( Dictionary boundArguments, List result, CommandAst commandAst, - CompletionContext context) + CompletionContext context, + HashSet excludedValues) { if (boundArguments != null) { @@ -2573,6 +2577,11 @@ private static void NativeCompletionCimCommands( { NativeCompletionCimMethodName(pseudoboundCimNamespace, pseudoboundClassName, !gotInstance, result, context); } + else if (parameter.Equals("Arguments", StringComparison.OrdinalIgnoreCase)) + { + string pseudoboundMethodName = NativeCommandArgumentCompletion_ExtractSecondaryArgument(boundArguments, "MethodName").FirstOrDefault(); + NativeCompletionCimMethodArgumentName(pseudoboundCimNamespace, pseudoboundClassName, pseudoboundMethodName, excludedValues, result, context); + } } } @@ -2715,6 +2724,41 @@ private static void NativeCompletionCimMethodName( result.AddRange(localResults.OrderBy(static x => x.ListItemText, StringComparer.OrdinalIgnoreCase)); } + private static void NativeCompletionCimMethodArgumentName( + string pseudoboundNamespace, + string pseudoboundClassName, + string pseudoboundMethodName, + HashSet excludedParameters, + List result, + CompletionContext context) + { + if (string.IsNullOrWhiteSpace(pseudoboundClassName) || string.IsNullOrWhiteSpace(pseudoboundMethodName)) + { + return; + } + + CimClass cimClass; + using (var cimSession = CimSession.Create(null)) + { + cimClass = cimSession.GetClass(pseudoboundNamespace ?? "root/cimv2", pseudoboundClassName); + } + var methodParameters = cimClass.CimClassMethods[pseudoboundMethodName]?.Parameters; + if (methodParameters is null) + { + return; + } + foreach (var parameter in methodParameters) + { + if ((string.IsNullOrEmpty(context.WordToComplete) || parameter.Name.StartsWith(context.WordToComplete, StringComparison.OrdinalIgnoreCase)) + && !excludedParameters.Contains(parameter.Name) + && parameter.Qualifiers["In"]?.Value is true) + { + string toolTip = $"[{CimInstanceAdapter.CimTypeToTypeNameDisplayString(parameter.CimType)}]"; + result.Add(new CompletionResult(parameter.Name, parameter.Name, CompletionResultType.Property, toolTip)); + } + } + } + private static readonly ConcurrentDictionary> s_cimNamespaceToClassNames = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); @@ -3844,12 +3888,22 @@ private static IEnumerable GetInferenceTypes(CompletionContext conte return prevType; } - private static void NativeCompletionMemberName(CompletionContext context, List result, CommandAst commandAst) + private static void NativeCompletionMemberName(CompletionContext context, List result, CommandAst commandAst, AstParameterArgumentPair parameterInfo) { IEnumerable prevType = GetInferenceTypes(context, commandAst); if (prevType is not null) { - CompleteMemberByInferredType(context.TypeInferenceContext, prevType, result, context.WordToComplete + "*", filter: IsPropertyMember, isStatic: false); + var excludedMembers = new HashSet(StringComparer.OrdinalIgnoreCase); + int cursorOffset = context.CursorPosition.Offset; + if (parameterInfo is AstPair pair) + { + var parameterValues = pair.Argument.FindAll(ast => !(cursorOffset >= ast.Extent.StartOffset && cursorOffset <= ast.Extent.EndOffset) && ast is StringConstantExpressionAst, false); + foreach (Ast ast in parameterValues) + { + excludedMembers.Add(ast.Extent.Text); + } + } + CompleteMemberByInferredType(context.TypeInferenceContext, prevType, result, context.WordToComplete + "*", filter: IsPropertyMember, isStatic: false, excludedMembers); } result.Add(CompletionResult.Null); @@ -5797,7 +5851,7 @@ private static void CompleteFormatViewByInferredType(CompletionContext context, } } - internal static void CompleteMemberByInferredType(TypeInferenceContext context, IEnumerable inferredTypes, List results, string memberName, Func filter, bool isStatic) + internal static void CompleteMemberByInferredType(TypeInferenceContext context, IEnumerable inferredTypes, List results, string memberName, Func filter, bool isStatic, HashSet excludedMembers = null) { bool extensionMethodsAdded = false; HashSet typeNameUsed = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -5813,7 +5867,7 @@ internal static void CompleteMemberByInferredType(TypeInferenceContext context, var members = context.GetMembersByInferredType(psTypeName, isStatic, filter); foreach (var member in members) { - AddInferredMember(member, memberNamePattern, results); + AddInferredMember(member, memberNamePattern, results, excludedMembers); } // Check if we need to complete against the extension methods 'Where' and 'ForEach' @@ -5840,7 +5894,7 @@ internal static void CompleteMemberByInferredType(TypeInferenceContext context, } } - private static void AddInferredMember(object member, WildcardPattern memberNamePattern, List results) + private static void AddInferredMember(object member, WildcardPattern memberNamePattern, List results, HashSet excludedMembers) { string memberName = null; bool isMethod = false; @@ -5893,7 +5947,7 @@ private static void AddInferredMember(object member, WildcardPattern memberNameP getToolTip = memberAst.GetTooltip; } - if (memberName == null || !memberNamePattern.IsMatch(memberName)) + if (memberName == null || !memberNamePattern.IsMatch(memberName) || (excludedMembers is not null && excludedMembers.Contains(memberName))) { return; } @@ -6814,13 +6868,23 @@ internal static List CompleteHashtableKeyForDynamicKeyword( internal static List CompleteHashtableKey(CompletionContext completionContext, HashtableAst hashtableAst) { + int cursorOffset = completionContext.CursorPosition.Offset; + var excludedKeys = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var keyPair in hashtableAst.KeyValuePairs) + { + // Exclude all existing keys, except the key the cursor is currently at + if (!(cursorOffset >= keyPair.Item1.Extent.StartOffset && cursorOffset <= keyPair.Item1.Extent.EndOffset)) + { + _ = excludedKeys.Add(keyPair.Item1.Extent.Text); + } + } var typeAst = hashtableAst.Parent as ConvertExpressionAst; if (typeAst != null) { var result = new List(); CompleteMemberByInferredType( completionContext.TypeInferenceContext, AstTypeInference.InferTypeOf(typeAst, completionContext.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval), - result, completionContext.WordToComplete + "*", IsWriteablePropertyMember, isStatic: false); + result, completionContext.WordToComplete + "*", IsWriteablePropertyMember, isStatic: false, excludedKeys); return result; } @@ -6910,6 +6974,7 @@ internal static List CompleteHashtableKey(CompletionContext co } } + string wordToComplete = completionContext.WordToComplete; if (parameterName != null) { if (parameterName.Equals("GroupBy", StringComparison.OrdinalIgnoreCase)) @@ -6920,7 +6985,7 @@ internal static List CompleteHashtableKey(CompletionContext co case "Format-List": case "Format-Wide": case "Format-Custom": - return GetSpecialHashTableKeyMembers("Expression", "FormatString", "Label"); + return GetSpecialHashTableKeyMembers(excludedKeys, wordToComplete, "Expression", "FormatString", "Label"); } return null; @@ -6935,22 +7000,49 @@ internal static List CompleteHashtableKey(CompletionContext co var result = new List(); CompleteMemberByInferredType( completionContext.TypeInferenceContext, inferredType, - result, completionContext.WordToComplete + "*", IsWriteablePropertyMember, isStatic: false); + result, completionContext.WordToComplete + "*", IsWriteablePropertyMember, isStatic: false, excludedKeys); return result; case "Select-Object": - return GetSpecialHashTableKeyMembers("Name", "Expression"); + return GetSpecialHashTableKeyMembers(excludedKeys, wordToComplete, "Name", "Expression"); case "Sort-Object": - return GetSpecialHashTableKeyMembers("Expression", "Ascending", "Descending"); + return GetSpecialHashTableKeyMembers(excludedKeys, wordToComplete, "Expression", "Ascending", "Descending"); case "Group-Object": - return GetSpecialHashTableKeyMembers("Expression"); + return GetSpecialHashTableKeyMembers(excludedKeys, wordToComplete, "Expression"); case "Format-Table": - return GetSpecialHashTableKeyMembers("Expression", "FormatString", "Label", "Width", "Alignment"); + return GetSpecialHashTableKeyMembers(excludedKeys, wordToComplete, "Expression", "FormatString", "Label", "Width", "Alignment"); case "Format-List": - return GetSpecialHashTableKeyMembers("Expression", "FormatString", "Label"); + return GetSpecialHashTableKeyMembers(excludedKeys, wordToComplete, "Expression", "FormatString", "Label"); case "Format-Wide": - return GetSpecialHashTableKeyMembers("Expression", "FormatString"); + return GetSpecialHashTableKeyMembers(excludedKeys, wordToComplete, "Expression", "FormatString"); case "Format-Custom": - return GetSpecialHashTableKeyMembers("Expression", "Depth"); + return GetSpecialHashTableKeyMembers(excludedKeys, wordToComplete, "Expression", "Depth"); + } + } + + if (parameterName.Equals("FilterHashtable", StringComparison.OrdinalIgnoreCase)) + { + switch (binding.CommandName) + { + case "Get-WinEvent": + return GetSpecialHashTableKeyMembers(excludedKeys, wordToComplete, "LogName", "ProviderName", "Path", "Keywords", "ID", "Level", + "StartTime", "EndTime", "UserID", "Data", "SuppressHashFilter"); + } + } + + if (parameterName.Equals("Arguments", StringComparison.OrdinalIgnoreCase)) + { + switch (binding.CommandName) + { + case "Invoke-CimMethod": + var result = new List(); + NativeCompletionCimCommands(parameterName, binding.BoundArguments, result, commandAst, completionContext, excludedKeys); + // this method adds a null CompletionResult to the list but we don't want that here. + if (result.Count > 1) + { + result.RemoveAt(result.Count - 1); + return result; + } + return null; } } } @@ -6959,13 +7051,25 @@ internal static List CompleteHashtableKey(CompletionContext co return null; } - private static List GetSpecialHashTableKeyMembers(params string[] keys) + private static List GetSpecialHashTableKeyMembers(HashSet excludedKeys, string wordToComplete, params string[] keys) { // Resources were removed because they missed the deadline for loc. // return keys.Select(key => new CompletionResult(key, key, CompletionResultType.Property, // ResourceManagerCache.GetResourceString(typeof(CompletionCompleters).Assembly, // "TabCompletionStrings", key + "HashKeyDescription"))).ToList(); - return keys.Select(static key => new CompletionResult(key, key, CompletionResultType.Property, key)).ToList(); + var result = new List(); + foreach (string key in keys) + { + if ((string.IsNullOrEmpty(wordToComplete) || key.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) && !excludedKeys.Contains(key)) + { + result.Add(new CompletionResult(key, key, CompletionResultType.Property, key)); + } + } + if (result.Count == 0) + { + return null; + } + return result; } #endregion Hashtable Keys diff --git a/test/powershell/Host/TabCompletion/BugFix.Tests.ps1 b/test/powershell/Host/TabCompletion/BugFix.Tests.ps1 index 78be093a5bb..3cc44f3c0e1 100644 --- a/test/powershell/Host/TabCompletion/BugFix.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/BugFix.Tests.ps1 @@ -85,8 +85,7 @@ Describe "Tab completion bug fix" -Tags "CI" { $result.CurrentMatchIndex | Should -Be -1 $result.ReplacementIndex | Should -Be 40 $result.ReplacementLength | Should -Be 0 - $result.CompletionMatches[0].CompletionText | Should -BeExactly 'Expression' - $result.CompletionMatches[1].CompletionText | Should -BeExactly 'Ascending' - $result.CompletionMatches[2].CompletionText | Should -BeExactly 'Descending' + $result.CompletionMatches[0].CompletionText | Should -BeExactly 'Ascending' + $result.CompletionMatches[1].CompletionText | Should -BeExactly 'Descending' } } diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 6513bd87253..3dd93b1a37c 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -128,6 +128,43 @@ Describe "TabCompletion" -Tags CI { $res.CompletionMatches | Should -HaveCount 3 $res.CompletionMatches.CompletionText -join ' ' | Should -BeExactly 'A B C' } + It 'Complete hashtable key without duplicate keys' { + class X { + $A + $B + $C + } + $TestString = '[x]@{A="";^}' + $CursorIndex = $TestString.IndexOf('^') + $res = TabExpansion2 -inputScript $TestString.Remove($CursorIndex, 1) -cursorColumn $CursorIndex + $res.CompletionMatches | Should -HaveCount 2 + $res.CompletionMatches.CompletionText -join ' ' | Should -BeExactly 'B C' + } + It 'Complete hashtable key on empty line after key/value pair' { + class X { + $A + $B + $C + } + $TestString = @' +[x]@{ + B="" + ^ +} +'@ + $CursorIndex = $TestString.IndexOf('^') + $res = TabExpansion2 -inputScript $TestString.Remove($CursorIndex, 1) -cursorColumn $CursorIndex + $res.CompletionMatches | Should -HaveCount 2 + $res.CompletionMatches.CompletionText -join ' ' | Should -BeExactly 'A C' + } + + It 'Complete hashtable keys for Get-WinEvent FilterHashtable' { + $TestString = 'Get-WinEvent -FilterHashtable @{^' + $CursorIndex = $TestString.IndexOf('^') + $res = TabExpansion2 -inputScript $TestString.Remove($CursorIndex, 1) -cursorColumn $CursorIndex + $res.CompletionMatches | Should -HaveCount 11 + $res.CompletionMatches.CompletionText -join ' ' | Should -BeExactly 'LogName ProviderName Path Keywords ID Level StartTime EndTime UserID Data SuppressHashFilter' + } It 'Should complete "Get-Process -Id " with Id and name in tooltip' { Set-StrictMode -Version 3.0 @@ -1457,6 +1494,9 @@ dir -Recurse ` @{ inputStr = '[Microsoft.Management.Infrastructure.CimClass]$c = $null; $c.CimClassNam'; expected = 'CimClassName' } @{ inputStr = '[Microsoft.Management.Infrastructure.CimClass]$c = $null; $c.CimClassName.Substrin'; expected = 'Substring(' } @{ inputStr = 'Get-CimInstance -ClassName Win32_Process | %{ $_.ExecutableP'; expected = 'ExecutablePath' } + @{ inputStr = 'Get-CimInstance -ClassName Win32_Process | Invoke-CimMethod -MethodName SetPriority -Arguments @{'; expected = 'Priority' } + @{ inputStr = 'Get-CimInstance -ClassName Win32_Service | Invoke-CimMethod -MethodName Change -Arguments @{d'; expected = 'DesktopInteract' } + @{ inputStr = 'Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{'; expected = 'CommandLine' } ) } From 401227148b9e23fbd9e91460053ac393d0140c13 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Sun, 21 Nov 2021 04:37:34 +0100 Subject: [PATCH 02/13] Remove some leading space --- .../engine/CommandCompletion/CompletionAnalysis.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 0677f3ab92c..0f0da52f5cb 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -416,7 +416,7 @@ internal List GetResultHelper(CompletionContext completionCont case TokenKind.Generic: case TokenKind.MinusMinus: // for native commands '--' case TokenKind.Identifier: - result = GetResultForIdentifier(completionContext, ref replacementIndex, ref replacementLength, isQuotedString); + result = GetResultForIdentifier(completionContext, ref replacementIndex, ref replacementLength, isQuotedString); break; case TokenKind.Parameter: From 8a54574d6dc66f76daf0ab1a7e8208b42b83b391 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Sun, 21 Nov 2021 11:48:53 +0100 Subject: [PATCH 03/13] Skip Get-WinEvent test on non-Windows platforms --- test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 3dd93b1a37c..6c587de0447 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -158,7 +158,7 @@ Describe "TabCompletion" -Tags CI { $res.CompletionMatches.CompletionText -join ' ' | Should -BeExactly 'A C' } - It 'Complete hashtable keys for Get-WinEvent FilterHashtable' { + It 'Complete hashtable keys for Get-WinEvent FilterHashtable' -Skip:(!$IsWindows) { $TestString = 'Get-WinEvent -FilterHashtable @{^' $CursorIndex = $TestString.IndexOf('^') $res = TabExpansion2 -inputScript $TestString.Remove($CursorIndex, 1) -cursorColumn $CursorIndex From e6ab3148b907f1922ed2f81498f7eb3992d7df72 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Tue, 23 Nov 2021 23:37:22 +0100 Subject: [PATCH 04/13] Add missing test and additional details to the Cim method parameter tooltip --- .../engine/CommandCompletion/CompletionCompleters.cs | 9 ++++++--- .../Host/TabCompletion/TabCompletion.Tests.ps1 | 5 +++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 84ac4bb1e71..ad9bc5ee7ef 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -2740,7 +2740,9 @@ private static void NativeCompletionCimMethodArgumentName( CimClass cimClass; using (var cimSession = CimSession.Create(null)) { - cimClass = cimSession.GetClass(pseudoboundNamespace ?? "root/cimv2", pseudoboundClassName); + using var options = new CimOperationOptions(); + options.Flags |= CimOperationFlags.LocalizedQualifiers; + cimClass = cimSession.GetClass(pseudoboundNamespace ?? "root/cimv2", pseudoboundClassName, options); } var methodParameters = cimClass.CimClassMethods[pseudoboundMethodName]?.Parameters; if (methodParameters is null) @@ -2750,10 +2752,11 @@ private static void NativeCompletionCimMethodArgumentName( foreach (var parameter in methodParameters) { if ((string.IsNullOrEmpty(context.WordToComplete) || parameter.Name.StartsWith(context.WordToComplete, StringComparison.OrdinalIgnoreCase)) - && !excludedParameters.Contains(parameter.Name) + && (excludedParameters is null || !excludedParameters.Contains(parameter.Name)) && parameter.Qualifiers["In"]?.Value is true) { - string toolTip = $"[{CimInstanceAdapter.CimTypeToTypeNameDisplayString(parameter.CimType)}]"; + string parameterDescription = parameter.Qualifiers["Description"]?.Value as string ?? string.Empty; + string toolTip = $"[{CimInstanceAdapter.CimTypeToTypeNameDisplayString(parameter.CimType)}] {parameterDescription}"; result.Add(new CompletionResult(parameter.Name, parameter.Name, CompletionResultType.Property, toolTip)); } } diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 6c587de0447..86c0620f709 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -1747,6 +1747,11 @@ function MyFunction ($param1, $param2) $res = TabExpansion2 -cursorColumn $CursorIndex -inputScript $TestString.Remove($CursorIndex, 1) $res.CompletionMatches.CompletionText | Should -BeExactly $Expected } + + It 'Should complete Select-Object properties without duplicates' { + $res = TabExpansion2 -inputScript '$PSVersionTable | select Count,' -cursorColumn '$PSVersionTable | select Count,'.Length + $res.CompletionMatches.CompletionText | Should -Not -Contain "Count" + } } } From a4807c471808b3f750650ca5bfd799721b80c80a Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Sun, 5 Dec 2021 16:01:07 +0100 Subject: [PATCH 05/13] Add completion for hashtables used for splatting and the Property parameter for Cim cmdlets. --- .../CommandCompletion/CompletionCompleters.cs | 128 ++++++++++++++++-- .../TabCompletion/TabCompletion.Tests.ps1 | 23 ++++ 2 files changed, 138 insertions(+), 13 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index ad9bc5ee7ef..9900e65c678 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -2335,10 +2335,17 @@ private static void NativeCommandArgumentCompletion( case "Invoke-CimMethod": case "New-CimInstance": case "Register-CimIndicationEvent": + case "Set-CimInstance": { - if (!parameterName.Equals("Arguments", StringComparison.OrdinalIgnoreCase)) + if (!parameterName.Equals("Arguments", StringComparison.OrdinalIgnoreCase) + && !(parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase) && !commandName.Equals("Get-CimInstance"))) { - NativeCompletionCimCommands(parameterName, boundArguments, result, commandAst, context, excludedValues: null); + HashSet excludedValues = null; + if (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase) && boundArguments["Property"] is AstPair pair) + { + excludedValues = GetParameterValues(pair, context.CursorPosition.Offset); + } + NativeCompletionCimCommands(parameterName, boundArguments, result, commandAst, context, excludedValues, commandName); } break; } @@ -2511,7 +2518,8 @@ private static void NativeCompletionCimCommands( List result, CommandAst commandAst, CompletionContext context, - HashSet excludedValues) + HashSet excludedValues, + string commandName) { if (boundArguments != null) { @@ -2532,6 +2540,7 @@ private static void NativeCompletionCimCommands( } } + RemoveLastNullCompletionResult(result); if (parameter.Equals("Namespace", StringComparison.OrdinalIgnoreCase)) { NativeCompletionCimNamespace(result, context); @@ -2582,6 +2591,11 @@ private static void NativeCompletionCimCommands( string pseudoboundMethodName = NativeCommandArgumentCompletion_ExtractSecondaryArgument(boundArguments, "MethodName").FirstOrDefault(); NativeCompletionCimMethodArgumentName(pseudoboundCimNamespace, pseudoboundClassName, pseudoboundMethodName, excludedValues, result, context); } + else if (parameter.Equals("Property", StringComparison.OrdinalIgnoreCase)) + { + bool includeReadOnly = !commandName.Equals("Set-CimInstance", StringComparison.OrdinalIgnoreCase); + NativeCompletionCimPropertyName(pseudoboundCimNamespace, pseudoboundClassName, includeReadOnly, excludedValues, result, context); + } } } @@ -2762,6 +2776,42 @@ private static void NativeCompletionCimMethodArgumentName( } } + private static void NativeCompletionCimPropertyName( + string pseudoboundNamespace, + string pseudoboundClassName, + bool includeReadOnly, + HashSet excludedProperties, + List result, + CompletionContext context) + { + if (string.IsNullOrWhiteSpace(pseudoboundClassName)) + { + return; + } + + CimClass cimClass; + using (var cimSession = CimSession.Create(null)) + { + using var options = new CimOperationOptions(); + options.Flags |= CimOperationFlags.LocalizedQualifiers; + cimClass = cimSession.GetClass(pseudoboundNamespace ?? "root/cimv2", pseudoboundClassName, options); + } + + foreach (var property in cimClass.CimClassProperties) + { + bool isReadOnly = (property.Flags & CimFlags.ReadOnly) != 0; + if ((!isReadOnly || (isReadOnly && includeReadOnly)) + && (string.IsNullOrEmpty(context.WordToComplete) || property.Name.StartsWith(context.WordToComplete, StringComparison.OrdinalIgnoreCase)) + && (excludedProperties is null || !excludedProperties.Contains(property.Name))) + { + string propertyDescription = property.Qualifiers["Description"]?.Value as string ?? string.Empty; + string accessString = isReadOnly ? "{ get; }" : "{ get; set; }"; + string toolTip = $"[{CimInstanceAdapter.CimTypeToTypeNameDisplayString(property.CimType)}] {accessString} {propertyDescription}"; + result.Add(new CompletionResult(property.Name, property.Name, CompletionResultType.Property, toolTip)); + } + } + } + private static readonly ConcurrentDictionary> s_cimNamespaceToClassNames = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); @@ -3896,15 +3946,10 @@ private static void NativeCompletionMemberName(CompletionContext context, List prevType = GetInferenceTypes(context, commandAst); if (prevType is not null) { - var excludedMembers = new HashSet(StringComparer.OrdinalIgnoreCase); - int cursorOffset = context.CursorPosition.Offset; + HashSet excludedMembers = null; if (parameterInfo is AstPair pair) { - var parameterValues = pair.Argument.FindAll(ast => !(cursorOffset >= ast.Extent.StartOffset && cursorOffset <= ast.Extent.EndOffset) && ast is StringConstantExpressionAst, false); - foreach (Ast ast in parameterValues) - { - excludedMembers.Add(ast.Extent.Text); - } + excludedMembers = GetParameterValues(pair, context.CursorPosition.Offset); } CompleteMemberByInferredType(context.TypeInferenceContext, prevType, result, context.WordToComplete + "*", filter: IsPropertyMember, isStatic: false, excludedMembers); } @@ -3912,6 +3957,20 @@ private static void NativeCompletionMemberName(CompletionContext context, List + /// Returns all string values bound to a parameter except the one the cursor is currently at. + /// + private static HashSetGetParameterValues(AstPair parameter, int cursorOffset) + { + var result = new HashSet(StringComparer.OrdinalIgnoreCase); + var parameterValues = parameter.Argument.FindAll(ast => !(cursorOffset >= ast.Extent.StartOffset && cursorOffset <= ast.Extent.EndOffset) && ast is StringConstantExpressionAst, false); + foreach (Ast ast in parameterValues) + { + _ = result.Add(ast.Extent.Text); + } + return result; + } + private static void NativeCompletionFormatViewName( CompletionContext context, Dictionary boundArguments, @@ -6872,6 +6931,7 @@ internal static List CompleteHashtableKeyForDynamicKeyword( internal static List CompleteHashtableKey(CompletionContext completionContext, HashtableAst hashtableAst) { int cursorOffset = completionContext.CursorPosition.Offset; + string wordToComplete = completionContext.WordToComplete; var excludedKeys = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var keyPair in hashtableAst.KeyValuePairs) { @@ -6887,7 +6947,7 @@ internal static List CompleteHashtableKey(CompletionContext co var result = new List(); CompleteMemberByInferredType( completionContext.TypeInferenceContext, AstTypeInference.InferTypeOf(typeAst, completionContext.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval), - result, completionContext.WordToComplete + "*", IsWriteablePropertyMember, isStatic: false, excludedKeys); + result, wordToComplete + "*", IsWriteablePropertyMember, isStatic: false, excludedKeys); return result; } @@ -6977,7 +7037,6 @@ internal static List CompleteHashtableKey(CompletionContext co } } - string wordToComplete = completionContext.WordToComplete; if (parameterName != null) { if (parameterName.Equals("GroupBy", StringComparison.OrdinalIgnoreCase)) @@ -7019,6 +7078,17 @@ internal static List CompleteHashtableKey(CompletionContext co return GetSpecialHashTableKeyMembers(excludedKeys, wordToComplete, "Expression", "FormatString"); case "Format-Custom": return GetSpecialHashTableKeyMembers(excludedKeys, wordToComplete, "Expression", "Depth"); + case "Set-CimInstance": + case "New-CimInstance": + var results = new List(); + NativeCompletionCimCommands(parameterName, binding.BoundArguments, results, commandAst, completionContext, excludedKeys, binding.CommandName); + // this method adds a null CompletionResult to the list but we don't want that here. + if (results.Count > 1) + { + results.RemoveAt(results.Count - 1); + return results; + } + return null; } } @@ -7038,7 +7108,7 @@ internal static List CompleteHashtableKey(CompletionContext co { case "Invoke-CimMethod": var result = new List(); - NativeCompletionCimCommands(parameterName, binding.BoundArguments, result, commandAst, completionContext, excludedKeys); + NativeCompletionCimCommands(parameterName, binding.BoundArguments, result, commandAst, completionContext, excludedKeys, binding.CommandName); // this method adds a null CompletionResult to the list but we don't want that here. if (result.Count > 1) { @@ -7051,6 +7121,38 @@ internal static List CompleteHashtableKey(CompletionContext co } } + if (ast.Parent is AssignmentStatementAst assignment && assignment.Left is VariableExpressionAst assignmentVar) + { + var firstSplatUse = completionContext.RelatedAsts[0].Find(currentAst => currentAst.Extent.StartOffset > hashtableAst.Extent.EndOffset + && currentAst is VariableExpressionAst splatVar + && splatVar.Splatted + && splatVar.VariablePath.UserPath.Equals(assignmentVar.VariablePath.UserPath, StringComparison.OrdinalIgnoreCase), true) as VariableExpressionAst; + + if (firstSplatUse is not null && firstSplatUse.Parent is CommandAst command) + { + var binding = new PseudoParameterBinder().DoPseudoParameterBinding(command, null, null, PseudoParameterBinder.BindingType.ParameterCompletion); + if (binding is null) + { + return null; + } + + var results = new List(); + foreach (var parameter in binding.UnboundParameters) + { + if (!excludedKeys.Contains(parameter.Parameter.Name) + && (wordToComplete is null || parameter.Parameter.Name.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase))) + { + results.Add(new CompletionResult(parameter.Parameter.Name, parameter.Parameter.Name, CompletionResultType.ParameterName, $"[{parameter.Parameter.Type.Name}]")); + } + } + + if (results.Count > 0) + { + return results; + } + } + } + return null; } diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 86c0620f709..e06b2ed860f 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -166,6 +166,13 @@ Describe "TabCompletion" -Tags CI { $res.CompletionMatches.CompletionText -join ' ' | Should -BeExactly 'LogName ProviderName Path Keywords ID Level StartTime EndTime UserID Data SuppressHashFilter' } + It 'Complete hashtable keys for a hashtable used for splatting' { + $TestString = '$GetChildItemParams=@{^};Get-ChildItem @GetChildItemParams -Force -Recurse' + $CursorIndex = $TestString.IndexOf('^') + $res = TabExpansion2 -inputScript $TestString.Remove($CursorIndex, 1) -cursorColumn $CursorIndex + $res.CompletionMatches[0].CompletionText | Should -BeExactly 'Path' + } + It 'Should complete "Get-Process -Id " with Id and name in tooltip' { Set-StrictMode -Version 3.0 $cmd = 'Get-Process -Id ' @@ -1497,6 +1504,15 @@ dir -Recurse ` @{ inputStr = 'Get-CimInstance -ClassName Win32_Process | Invoke-CimMethod -MethodName SetPriority -Arguments @{'; expected = 'Priority' } @{ inputStr = 'Get-CimInstance -ClassName Win32_Service | Invoke-CimMethod -MethodName Change -Arguments @{d'; expected = 'DesktopInteract' } @{ inputStr = 'Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{'; expected = 'CommandLine' } + @{ inputStr = 'New-CimInstance Win32_Environment -Property @{'; expected = 'Caption' } + @{ inputStr = 'Get-CimInstance Win32_Environment | Set-CimInstance -Property @{'; expected = 'Name' } + @{ inputStr = 'Set-CimInstance -Namespace root/CIMV'; expected = 'root/CIMV2' } + @{ inputStr = 'Get-CimInstance Win32_Process -Property '; expected = 'Caption' } + @{ inputStr = 'Get-CimInstance Win32_Process -Property Caption,'; expected = 'Description' } + ) + $FailCases = @( + @{ inputStr = "Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments " } + @{ inputStr = "New-CimInstance Win32_Process -Property " } ) } @@ -1507,6 +1523,13 @@ dir -Recurse ` $res.CompletionMatches.Count | Should -BeGreaterThan 0 $res.CompletionMatches[0].CompletionText | Should -Be $expected } + + It "CIM cmdlet input '' should not successfully complete" -TestCases $FailCases -Skip:(!$IsWindows) { + param($inputStr) + + $res = TabExpansion2 -inputScript $inputStr -cursorColumn $inputStr.Length + $res.CompletionMatches[0].ResultType | should -Not -Be 'Property' + } } Context "Module cmdlet completion tests" { From dd059fec5939f7188bc8d5466971073de2f06127 Mon Sep 17 00:00:00 2001 From: MartinGC94 <42123497+MartinGC94@users.noreply.github.com> Date: Mon, 6 Dec 2021 14:10:56 +0100 Subject: [PATCH 06/13] Apply suggestions from code review Co-authored-by: Ilya --- .../engine/CommandCompletion/CompletionAnalysis.cs | 3 +++ .../engine/CommandCompletion/CompletionCompleters.cs | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 0f0da52f5cb..7ae213bc76b 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -957,12 +957,14 @@ private static List GetResultForHashtable(CompletionContext co break; } } + if (lastRelatedAst is HashtableAst hashtableAst) { if (completionContext.TokenAtCursor is not null && completionContext.TokenAtCursor.Kind == TokenKind.RCurly) { return null; } + bool cursorIsWithinOrOnSameLineAsKeypair = false; foreach (var pair in hashtableAst.KeyValuePairs) { @@ -973,6 +975,7 @@ private static List GetResultForHashtable(CompletionContext co break; } } + if (cursorIsWithinOrOnSameLineAsKeypair) { var tokenBeforeOrAtCursor = completionContext.TokenBeforeCursor ?? completionContext.TokenAtCursor; diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 9900e65c678..42a6e0a6341 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -2347,6 +2347,7 @@ private static void NativeCommandArgumentCompletion( } NativeCompletionCimCommands(parameterName, boundArguments, result, commandAst, context, excludedValues, commandName); } + break; } @@ -2758,11 +2759,13 @@ private static void NativeCompletionCimMethodArgumentName( options.Flags |= CimOperationFlags.LocalizedQualifiers; cimClass = cimSession.GetClass(pseudoboundNamespace ?? "root/cimv2", pseudoboundClassName, options); } + var methodParameters = cimClass.CimClassMethods[pseudoboundMethodName]?.Parameters; if (methodParameters is null) { return; } + foreach (var parameter in methodParameters) { if ((string.IsNullOrEmpty(context.WordToComplete) || parameter.Name.StartsWith(context.WordToComplete, StringComparison.OrdinalIgnoreCase)) @@ -3951,6 +3954,7 @@ private static void NativeCompletionMemberName(CompletionContext context, ListGetParameterValues(AstPair parameter, int cursorOf { _ = result.Add(ast.Extent.Text); } + return result; } @@ -6941,6 +6946,7 @@ internal static List CompleteHashtableKey(CompletionContext co _ = excludedKeys.Add(keyPair.Item1.Extent.Text); } } + var typeAst = hashtableAst.Parent as ConvertExpressionAst; if (typeAst != null) { @@ -7088,6 +7094,7 @@ internal static List CompleteHashtableKey(CompletionContext co results.RemoveAt(results.Count - 1); return results; } + return null; } } @@ -7115,6 +7122,7 @@ internal static List CompleteHashtableKey(CompletionContext co result.RemoveAt(result.Count - 1); return result; } + return null; } } @@ -7170,10 +7178,12 @@ private static List GetSpecialHashTableKeyMembers(HashSet Date: Mon, 6 Dec 2021 14:59:02 +0100 Subject: [PATCH 07/13] Add comments, move test and replace alias with the proper command name in test. --- .../engine/CommandCompletion/CompletionAnalysis.cs | 8 ++++++++ .../Host/TabCompletion/TabCompletion.Tests.ps1 | 9 ++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 7ae213bc76b..f075147fd14 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -918,6 +918,7 @@ internal List GetResultHelper(CompletionContext completionCont if (result == null || result.Count == 0) { result = GetResultForHashtable(completionContext); + // Handles the following scenario: [ipaddress]@{Address=""; } if (result is not null && result.Count > 0) { completionContext.ReplacementIndex = replacementIndex = completionContext.CursorPosition.Offset; @@ -948,6 +949,13 @@ private static List GetResultForHashtable(CompletionContext co Ast lastRelatedAst = null; var cursorPosition = completionContext.CursorPosition; + // Enumeration is used over the LastAst pattern because empty lines following a key-value pair will set LastAst to the value. + // Example: + // @{ + // Key1="Value1" + // + // } + // In this case the last 3 Asts will be StringConstantExpression, CommandExpression, and Pipeline instead of the expected Hashtable for (int i = completionContext.RelatedAsts.Count - 1; i >= 0; i--) { Ast ast = completionContext.RelatedAsts[i]; diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index e06b2ed860f..341c82658dc 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -1207,6 +1207,10 @@ dir -Recurse ` $res.CompletionMatches | Should -HaveCount 2 [string]::Join(',', ($res.CompletionMatches.completiontext | Sort-Object)) | Should -BeExactly "1.0,1.1" } + It 'Should complete Select-Object properties without duplicates' { + $res = TabExpansion2 -inputScript '$PSVersionTable | Select-Object -Property Count,' + $res.CompletionMatches.CompletionText | Should -Not -Contain "Count" + } } Context "Module completion for 'using module'" { @@ -1770,11 +1774,6 @@ function MyFunction ($param1, $param2) $res = TabExpansion2 -cursorColumn $CursorIndex -inputScript $TestString.Remove($CursorIndex, 1) $res.CompletionMatches.CompletionText | Should -BeExactly $Expected } - - It 'Should complete Select-Object properties without duplicates' { - $res = TabExpansion2 -inputScript '$PSVersionTable | select Count,' -cursorColumn '$PSVersionTable | select Count,'.Length - $res.CompletionMatches.CompletionText | Should -Not -Contain "Count" - } } } From f7911d0629a126d62161e6518cdda69c7c5b7501 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Mon, 6 Dec 2021 19:27:44 +0100 Subject: [PATCH 08/13] Add empty line before return statement. --- .../engine/CommandCompletion/CompletionAnalysis.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index f075147fd14..5fae5f2baa0 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -996,6 +996,7 @@ private static List GetResultForHashtable(CompletionContext co completionContext.ReplacementLength = 0; return CompletionCompleters.CompleteHashtableKey(completionContext, hashtableAst); } + return null; } From 4c332e2afd57fdf6955f3dad4265a505d093037f Mon Sep 17 00:00:00 2001 From: MartinGC94 <42123497+MartinGC94@users.noreply.github.com> Date: Mon, 3 Jan 2022 23:20:17 +0100 Subject: [PATCH 09/13] Apply suggestions from code review Co-authored-by: Aditya Patwardhan --- .../engine/CommandCompletion/CompletionAnalysis.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 5fae5f2baa0..c31580b2bb2 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -919,7 +919,7 @@ internal List GetResultHelper(CompletionContext completionCont { result = GetResultForHashtable(completionContext); // Handles the following scenario: [ipaddress]@{Address=""; } - if (result is not null && result.Count > 0) + if (result?.Count > 0) { completionContext.ReplacementIndex = replacementIndex = completionContext.CursorPosition.Offset; completionContext.ReplacementLength = replacementLength = 0; From 848a1d4b9a5d3b0847d3bf7b30172f7c56c356ab Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Wed, 16 Feb 2022 03:49:36 +0100 Subject: [PATCH 10/13] Maybe fix merge conflict? --- .../TabCompletion/TabCompletion.Tests.ps1 | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 341c82658dc..bdebe0bcd87 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -1207,6 +1207,37 @@ dir -Recurse ` $res.CompletionMatches | Should -HaveCount 2 [string]::Join(',', ($res.CompletionMatches.completiontext | Sort-Object)) | Should -BeExactly "1.0,1.1" } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + It 'Should complete Select-Object properties without duplicates' { $res = TabExpansion2 -inputScript '$PSVersionTable | Select-Object -Property Count,' $res.CompletionMatches.CompletionText | Should -Not -Contain "Count" From 4abd93cae5f7ed0cce4c8f0ab3ad64620efc0823 Mon Sep 17 00:00:00 2001 From: MartinGC94 <42123497+MartinGC94@users.noreply.github.com> Date: Tue, 1 Mar 2022 22:56:40 +0100 Subject: [PATCH 11/13] Apply suggestions from code review Co-authored-by: Dongbo Wang --- .../CommandCompletion/CompletionCompleters.cs | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 66c4e69f9df..633ab26d024 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -3968,10 +3968,10 @@ private static void NativeCompletionMemberName(CompletionContext context, ListGetParameterValues(AstPair parameter, int cursorOffset) { var result = new HashSet(StringComparer.OrdinalIgnoreCase); - var parameterValues = parameter.Argument.FindAll(ast => !(cursorOffset >= ast.Extent.StartOffset && cursorOffset <= ast.Extent.EndOffset) && ast is StringConstantExpressionAst, false); + var parameterValues = parameter.Argument.FindAll(ast => !(cursorOffset >= ast.Extent.StartOffset && cursorOffset <= ast.Extent.EndOffset) && ast is StringConstantExpressionAst, searchNestedScriptBlocks: false); foreach (Ast ast in parameterValues) { - _ = result.Add(ast.Extent.Text); + result.Add(ast.Extent.Text); } return result; @@ -6944,7 +6944,7 @@ internal static List CompleteHashtableKey(CompletionContext co // Exclude all existing keys, except the key the cursor is currently at if (!(cursorOffset >= keyPair.Item1.Extent.StartOffset && cursorOffset <= keyPair.Item1.Extent.EndOffset)) { - _ = excludedKeys.Add(keyPair.Item1.Extent.Text); + excludedKeys.Add(keyPair.Item1.Extent.Text); } } @@ -7132,14 +7132,23 @@ internal static List CompleteHashtableKey(CompletionContext co if (ast.Parent is AssignmentStatementAst assignment && assignment.Left is VariableExpressionAst assignmentVar) { - var firstSplatUse = completionContext.RelatedAsts[0].Find(currentAst => currentAst.Extent.StartOffset > hashtableAst.Extent.EndOffset - && currentAst is VariableExpressionAst splatVar - && splatVar.Splatted - && splatVar.VariablePath.UserPath.Equals(assignmentVar.VariablePath.UserPath, StringComparison.OrdinalIgnoreCase), true) as VariableExpressionAst; + var firstSplatUse = completionContext.RelatedAsts[0].Find( + currentAst => + currentAst.Extent.StartOffset > hashtableAst.Extent.EndOffset + && currentAst is VariableExpressionAst splatVar + && splatVar.Splatted + && splatVar.VariablePath.UserPath.Equals(assignmentVar.VariablePath.UserPath, StringComparison.OrdinalIgnoreCase), + searchNestedScriptBlocks: true) as VariableExpressionAst; if (firstSplatUse is not null && firstSplatUse.Parent is CommandAst command) { - var binding = new PseudoParameterBinder().DoPseudoParameterBinding(command, null, null, PseudoParameterBinder.BindingType.ParameterCompletion); + var binding = new PseudoParameterBinder() + .DoPseudoParameterBinding( + command, + pipeArgumentType: null, + paramAstAtCursor: null, + PseudoParameterBinder.BindingType.ParameterCompletion); + if (binding is null) { return null; From 5c7be2da34562347c2f99e13726baedb7eb1dc75 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Tue, 1 Mar 2022 23:43:28 +0100 Subject: [PATCH 12/13] Apply changes suggested by daxian-dbw --- .../CommandCompletion/CompletionAnalysis.cs | 5 +++-- .../CommandCompletion/CompletionCompleters.cs | 22 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index f28e8079ca3..eaac62ba33f 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -931,8 +931,8 @@ internal List GetResultHelper(CompletionContext completionCont // Handles the following scenario: [ipaddress]@{Address=""; } if (result?.Count > 0) { - completionContext.ReplacementIndex = replacementIndex = completionContext.CursorPosition.Offset; - completionContext.ReplacementLength = replacementLength = 0; + replacementIndex = completionContext.CursorPosition.Offset; + replacementLength = 0; } } @@ -978,6 +978,7 @@ private static List GetResultForHashtable(CompletionContext co if (lastRelatedAst is HashtableAst hashtableAst) { + // Cursor is just after the hashtable: @{} if (completionContext.TokenAtCursor is not null && completionContext.TokenAtCursor.Kind == TokenKind.RCurly) { return null; diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 633ab26d024..07cea38190f 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -2338,16 +2338,18 @@ private static void NativeCommandArgumentCompletion( case "Register-CimIndicationEvent": case "Set-CimInstance": { - if (!parameterName.Equals("Arguments", StringComparison.OrdinalIgnoreCase) - && !(parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase) && !commandName.Equals("Get-CimInstance"))) + // Avoids completion for parameters that expect a hashtable. + if (parameterName.Equals("Arguments", StringComparison.OrdinalIgnoreCase) + || (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase) && !commandName.Equals("Get-CimInstance"))) { - HashSet excludedValues = null; - if (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase) && boundArguments["Property"] is AstPair pair) - { - excludedValues = GetParameterValues(pair, context.CursorPosition.Offset); - } - NativeCompletionCimCommands(parameterName, boundArguments, result, commandAst, context, excludedValues, commandName); + break; + } + HashSet excludedValues = null; + if (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase) && boundArguments["Property"] is AstPair pair) + { + excludedValues = GetParameterValues(pair, context.CursorPosition.Offset); } + NativeCompletionCimCommands(parameterName, boundArguments, result, commandAst, context, excludedValues, commandName); break; } @@ -7176,10 +7178,6 @@ internal static List CompleteHashtableKey(CompletionContext co private static List GetSpecialHashTableKeyMembers(HashSet excludedKeys, string wordToComplete, params string[] keys) { - // Resources were removed because they missed the deadline for loc. - // return keys.Select(key => new CompletionResult(key, key, CompletionResultType.Property, - // ResourceManagerCache.GetResourceString(typeof(CompletionCompleters).Assembly, - // "TabCompletionStrings", key + "HashKeyDescription"))).ToList(); var result = new List(); foreach (string key in keys) { From 6ff02f51ad3e3ab8a63592eeace19a3015000ce4 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 1 Mar 2022 15:25:08 -0800 Subject: [PATCH 13/13] Break up the code a little to avoid CodeFactor issues --- .../engine/CommandCompletion/CompletionCompleters.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 07cea38190f..b9f2ef70dba 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -2344,13 +2344,14 @@ private static void NativeCommandArgumentCompletion( { break; } + HashSet excludedValues = null; if (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase) && boundArguments["Property"] is AstPair pair) { excludedValues = GetParameterValues(pair, context.CursorPosition.Offset); } - NativeCompletionCimCommands(parameterName, boundArguments, result, commandAst, context, excludedValues, commandName); + NativeCompletionCimCommands(parameterName, boundArguments, result, commandAst, context, excludedValues, commandName); break; }