Implement a completion for View parameter of format cmdlets (#14513)

This commit is contained in:
Ilya
2021-07-13 17:32:48 +00:00
committed by GitHub
parent 00bc0f6d29
commit 0f3bc73d9a
2 changed files with 212 additions and 7 deletions
@@ -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<CompletionResult> result, CommandAst commandAst)
private static IEnumerable<PSTypeName> 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<PSTypeName> 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<CompletionResult> result, CommandAst commandAst)
{
IEnumerable<PSTypeName> 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<string, AstParameterArgumentPair> boundArguments,
List<CompletionResult> result,
CommandAst commandAst,
string commandName)
{
IEnumerable<PSTypeName> 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<ConfigurationDefinitionAst>(expression) != null;
}
private static void CompleteFormatViewByInferredType(CompletionContext context, string[] inferredTypeNames, List<CompletionResult> 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<string>();
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<PSTypeName> inferredTypes, List<CompletionResult> results, string memberName, Func<object, bool> filter, bool isStatic)
{
bool extensionMethodsAdded = false;
@@ -301,6 +301,111 @@ Describe "TabCompletion" -Tags CI {
$actual | Should -BeExactly $expected
}
Context "Format cmdlet's View paramter completion" {
BeforeAll {
$viewDefinition = @'
<?xml version="1.0" encoding="utf-8"?>
<Configuration>
<ViewDefinitions>
<View>
<Name>R A M</Name>
<ViewSelectedBy>
<TypeName>System.Diagnostics.Process</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>ProcName</Label>
<Width>40</Width>
<Alignment>Center</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>PagedMem</Label>
<Width>40</Width>
<Alignment>Center</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>PeakWS</Label>
<Width>40</Width>
<Alignment>Center</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<Alignment>Center</Alignment>
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<Alignment>Center</Alignment>
<PropertyName>PagedMemorySize</PropertyName>
</TableColumnItem>
<TableColumnItem>
<Alignment>Center</Alignment>
<PropertyName>PeakWorkingSet</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
'@
$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 | <cmd> -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 | <cmd>' -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'