Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using Microsoft.PowerShell;
using Microsoft.PowerShell.Cim;
using Microsoft.PowerShell.Commands;
using Microsoft.PowerShell.Commands.Internal.Format;

namespace System.Management.Automation
{
Expand Down Expand Up @@ -2282,6 +2283,14 @@ private static void NativeCommandArgumentCompletion(
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":
Expand All @@ -2291,6 +2300,10 @@ private static void NativeCommandArgumentCompletion(
{
NativeCompletionMemberName(context, result, commandAst);
}
else if (parameterName.Equals("View", StringComparison.OrdinalIgnoreCase))
{
NativeCompletionFormatViewName(context, boundArguments, result, commandAst, commandName);
}

break;
}
Expand Down Expand Up @@ -3785,44 +3798,78 @@ private static void NativeCompletionPathArgument(CompletionContext context, stri
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);
}

Expand Down Expand Up @@ -5394,11 +5441,11 @@ private static FunctionDefinitionAst GetCommentHelpFunctionTarget(CompletionCont

return null;
}

private static List<CompletionResult> CompleteCommentParameterValue(CompletionContext context, string wordToComplete)
{
FunctionDefinitionAst foundFunction = GetCommentHelpFunctionTarget(context);

ReadOnlyCollection<ParameterAst> foundParameters = null;
if (foundFunction is not null)
{
Expand All @@ -5409,7 +5456,7 @@ private static List<CompletionResult> CompleteCommentParameterValue(CompletionCo
// The helpblock is for a script file
foundParameters = scriptAst.ParamBlock?.Parameters;
}

if (foundParameters is null || foundParameters.Count == 0)
{
return null;
Expand Down Expand Up @@ -5697,6 +5744,59 @@ private static bool IsInDscContext(ExpressionAst expression)
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;
Expand Down Expand Up @@ -6531,7 +6631,7 @@ internal static List<CompletionResult> CompleteHelpTopics(CompletionContext cont

//search for help files for the current culture + en-US as fallback
var searchPaths = new string[]
{
{
Path.Combine(userHelpDir, currentCulture),
Path.Combine(appHelpDir, currentCulture),
Path.Combine(userHelpDir, "en-US"),
Expand Down
107 changes: 106 additions & 1 deletion test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1377,7 +1482,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'
Expand Down