diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 669c4c2a78..f961e291a2 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -24,6 +24,7 @@ using Microsoft.Management.Infrastructure.Options; using Microsoft.PowerShell; using Microsoft.PowerShell.Cim; using Microsoft.PowerShell.Commands; +using Microsoft.PowerShell.Commands.Internal.Format; namespace System.Management.Automation { @@ -2282,6 +2283,14 @@ namespace System.Management.Automation case "Measure-Object": case "Sort-Object": case "Where-Object": + { + if (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase)) + { + NativeCompletionMemberName(context, result, commandAst); + } + + break; + } case "Format-Custom": case "Format-List": case "Format-Table": @@ -2291,6 +2300,10 @@ namespace System.Management.Automation { NativeCompletionMemberName(context, result, commandAst); } + else if (parameterName.Equals("View", StringComparison.OrdinalIgnoreCase)) + { + NativeCompletionFormatViewName(context, boundArguments, result, commandAst, commandName); + } break; } @@ -3785,44 +3798,78 @@ namespace System.Management.Automation result.Add(CompletionResult.Null); } - private static void NativeCompletionMemberName(CompletionContext context, List result, CommandAst commandAst) + private static IEnumerable GetInferenceTypes(CompletionContext context, CommandAst commandAst) { // Command is something like where-object/foreach-object/format-list/etc. where there is a parameter that is a property name // and we want member names based on the input object, which is either the parameter InputObject, or comes from the pipeline. - if (!(commandAst.Parent is PipelineAst pipelineAst)) - return; + if (commandAst.Parent is not PipelineAst pipelineAst) + { + return null; + } int i; for (i = 0; i < pipelineAst.PipelineElements.Count; i++) { if (pipelineAst.PipelineElements[i] == commandAst) + { break; + } } IEnumerable prevType = null; if (i == 0) { + // based on a type of the argument which is binded to 'InputObject' parameter. AstParameterArgumentPair pair; if (!context.PseudoBindingInfo.BoundArguments.TryGetValue("InputObject", out pair) || !pair.ArgumentSpecified) { - return; + return null; } var astPair = pair as AstPair; if (astPair == null || astPair.Argument == null) { - return; + return null; } prevType = AstTypeInference.InferTypeOf(astPair.Argument, context.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval); } else { + // based on OutputTypeAttribute() of the first cmdlet in pipeline. prevType = AstTypeInference.InferTypeOf(pipelineAst.PipelineElements[i - 1], context.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval); } - CompleteMemberByInferredType(context.TypeInferenceContext, prevType, result, context.WordToComplete + "*", filter: IsPropertyMember, isStatic: false); + return prevType; + } + + private static void NativeCompletionMemberName(CompletionContext context, List result, CommandAst commandAst) + { + IEnumerable prevType = GetInferenceTypes(context, commandAst); + if (prevType is not null) + { + CompleteMemberByInferredType(context.TypeInferenceContext, prevType, result, context.WordToComplete + "*", filter: IsPropertyMember, isStatic: false); + } + + result.Add(CompletionResult.Null); + } + + private static void NativeCompletionFormatViewName( + CompletionContext context, + Dictionary boundArguments, + List result, + CommandAst commandAst, + string commandName) + { + IEnumerable prevType = NativeCommandArgumentCompletion_InferTypesOfArgument(boundArguments, commandAst, context, "InputObject"); + + if (prevType is not null) + { + string[] inferTypeNames = prevType.Select(t => t.Name).ToArray(); + CompleteFormatViewByInferredType(context, inferTypeNames, result, commandName); + } + result.Add(CompletionResult.Null); } @@ -5697,6 +5744,59 @@ namespace System.Management.Automation return Ast.GetAncestorAst(expression) != null; } + private static void CompleteFormatViewByInferredType(CompletionContext context, string[] inferredTypeNames, List results, string commandName) + { + var typeInfoDB = context.TypeInferenceContext.ExecutionContext.FormatDBManager.GetTypeInfoDataBase(); + + if (typeInfoDB is null) + { + return; + } + + Type controlBodyType = commandName switch + { + "Format-Table" => typeof(TableControlBody), + "Format-List" => typeof(ListControlBody), + "Format-Wide" => typeof(WideControlBody), + "Format-Custom" => typeof(ComplexControlBody), + _ => null + }; + + Diagnostics.Assert(controlBodyType is not null, "This should never happen unless a new Format-* cmdlet is added"); + + var wordToComplete = context.WordToComplete; + var quote = HandleDoubleAndSingleQuote(ref wordToComplete); + WildcardPattern viewPattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase); + + var uniqueNames = new HashSet(); + foreach (ViewDefinition viewDefinition in typeInfoDB.viewDefinitionsSection.viewDefinitionList) + { + if (viewDefinition?.appliesTo is not null && controlBodyType == viewDefinition.mainControl.GetType()) + { + foreach (TypeOrGroupReference applyTo in viewDefinition.appliesTo.referenceList) + { + foreach (string inferredTypeName in inferredTypeNames) + { + // We use 'StartsWith()' because 'applyTo.Name' can look like "System.Diagnostics.Process#IncludeUserName". + if (applyTo.name.StartsWith(inferredTypeName, StringComparison.OrdinalIgnoreCase) + && uniqueNames.Add(viewDefinition.name) + && viewPattern.IsMatch(viewDefinition.name)) + { + string completionText = viewDefinition.name; + // If the string is quoted or if it contains characters that need quoting, quote it in single quotes + if (quote != string.Empty || viewDefinition.name.IndexOfAny(s_charactersRequiringQuotes) != -1) + { + completionText = "'" + completionText.Replace("'", "''") + "'"; + } + + results.Add(new CompletionResult(completionText, viewDefinition.name, CompletionResultType.Text, viewDefinition.name)); + } + } + } + } + } + } + internal static void CompleteMemberByInferredType(TypeInferenceContext context, IEnumerable inferredTypes, List results, string memberName, Func filter, bool isStatic) { bool extensionMethodsAdded = false; diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 356101256b..39ab4968b5 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -301,6 +301,111 @@ Describe "TabCompletion" -Tags CI { $actual | Should -BeExactly $expected } + Context "Format cmdlet's View paramter completion" { + BeforeAll { + $viewDefinition = @' + + + + + R A M + + System.Diagnostics.Process + + + + + + 40 + Center + + + + 40 + Center + + + + 40 + Center + + + + + + + Center + Name + + + Center + PagedMemorySize + + + Center + PeakWorkingSet + + + + + + + + +'@ + + $tempViewFile = Join-Path -Path $TestDrive -ChildPath 'processViewDefinition.ps1xml' + Set-Content -LiteralPath $tempViewFile -Value $viewDefinition -Force + + $ps = [PowerShell]::Create() + $null = $ps.AddScript("Update-FormatData -AppendPath $tempViewFile") + $ps.Invoke() + $ps.HadErrors | Should -BeFalse + $ps.Commands.Clear() + + Remove-Item -LiteralPath $tempViewFile -Force -ErrorAction SilentlyContinue + } + + It 'Should complete Get-ChildItem | -View' -TestCases ( + @{ cmd = 'Format-Table'; expected = "children childrenWithHardlink$(if ($EnabledExperimentalFeatures.Contains('PSUnixFileStat')) { ' childrenWithUnixStat' })" }, + @{ cmd = 'Format-List'; expected = 'children' }, + @{ cmd = 'Format-Wide'; expected = 'children' }, + @{ cmd = 'Format-Custom'; expected = '' } + ) { + param($cmd, $expected) + + # The completion is based on OutputTypeAttribute() of the cmdlet. + $res = TabExpansion2 -inputScript "Get-ChildItem | $cmd -View " -cursorColumn "Get-ChildItem | $cmd -View ".Length + $completionText = $res.CompletionMatches.CompletionText | Sort-Object + $completionText -join ' ' | Should -BeExactly $expected + } + + It 'Should complete $processList = Get-Process; $processList | ' -TestCases ( + @{ cmd = 'Format-Table -View '; expected = "'R A M'", "Priority", "process", "ProcessModule", "ProcessWithUserName", "StartTime" }, + @{ cmd = 'Format-List -View '; expected = '' }, + @{ cmd = 'Format-Wide -View '; expected = 'process' }, + @{ cmd = 'Format-Custom -View '; expected = '' }, + @{ cmd = 'Format-Table -View S'; expected = "StartTime" }, + @{ cmd = "Format-Table -View 'S"; expected = "'StartTime'" }, + @{ cmd = "Format-Table -View R"; expected = "'R A M'" } + ) { + param($cmd, $expected) + + $null = $ps.AddScript({ + param ($cmd) + $processList = Get-Process + $res = TabExpansion2 -inputScript "`$processList | $cmd" -cursorColumn "`$processList | $cmd".Length + $completionText = $res.CompletionMatches.CompletionText | Sort-Object + $completionText + }).AddArgument($cmd) + + $result = $ps.Invoke() + $ps.Commands.Clear() + $expected = ($expected | Sort-Object) -join ' ' + $result -join ' ' | Should -BeExactly $expected + } + } + Context NativeCommand { BeforeAll { $nativeCommand = (Get-Command -CommandType Application -TotalCount 1).Name @@ -1384,7 +1489,7 @@ dir -Recurse ` ## Save original culture and temporarily set it to da-DK because there's no localized help for da-DK. $OriginalCulture = [cultureinfo]::CurrentCulture [cultureinfo]::CurrentCulture="da-DK" - + $res = TabExpansion2 -inputScript 'get-help about_spla' -cursorColumn 'get-help about_spla'.Length $res.CompletionMatches | Should -HaveCount 1 $res.CompletionMatches[0].CompletionText | Should -BeExactly 'about_Splatting'