From be7d36603d1cc60abd4e8e738ead8f437b7869fa Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Fri, 26 Mar 2021 01:06:40 +0100 Subject: [PATCH] Adding `IArgumentCompleterFactory` for parameterized `ArgumentCompleters` (#12605) --- .../CommandCompletion/CompletionCompleters.cs | 20 ++--- .../CommandCompletion/ExtensibleCompletion.cs | 84 ++++++++++++++++++- .../Parser/ExtensibleCompletion.Tests.ps1 | 71 ++++++++++++++++ 3 files changed, 163 insertions(+), 12 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 853b79e784..1d346e0259 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -2030,19 +2030,17 @@ namespace System.Management.Automation { try { - if (argumentCompleterAttribute.Type != null) + var completer = argumentCompleterAttribute.CreateArgumentCompleter(); + + if (completer != null) { - var completer = Activator.CreateInstance(argumentCompleterAttribute.Type) as IArgumentCompleter; - if (completer != null) + var customResults = completer.CompleteArgument(commandName, parameterName, + context.WordToComplete, commandAst, GetBoundArgumentsAsHashtable(context)); + if (customResults != null) { - var customResults = completer.CompleteArgument(commandName, parameterName, - context.WordToComplete, commandAst, GetBoundArgumentsAsHashtable(context)); - if (customResults != null) - { - result.AddRange(customResults); - result.Add(CompletionResult.Null); - return; - } + result.AddRange(customResults); + result.Add(CompletionResult.Null); + return; } } else diff --git a/src/System.Management.Automation/engine/CommandCompletion/ExtensibleCompletion.cs b/src/System.Management.Automation/engine/CommandCompletion/ExtensibleCompletion.cs index 777fc6c16f..0978262c0e 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/ExtensibleCompletion.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/ExtensibleCompletion.cs @@ -40,19 +40,40 @@ namespace System.Management.Automation Type = type; } + /// + /// Initializes a new instance of the class. + /// This constructor is used by derived attributes implementing . + /// + protected ArgumentCompleterAttribute() + { + if (this is not IArgumentCompleterFactory) + { + throw PSTraceSource.NewInvalidOperationException(); + } + } + /// /// This constructor is used primarily via PowerShell scripts. /// /// public ArgumentCompleterAttribute(ScriptBlock scriptBlock) { - if (scriptBlock == null) + if (scriptBlock is null) { throw PSTraceSource.NewArgumentNullException(nameof(scriptBlock)); } ScriptBlock = scriptBlock; } + + internal IArgumentCompleter CreateArgumentCompleter() + { + return Type != null + ? Activator.CreateInstance(Type) as IArgumentCompleter + : this is IArgumentCompleterFactory factory + ? factory.Create() + : null; + } } /// @@ -85,6 +106,67 @@ namespace System.Management.Automation } #nullable restore + /// + /// Creates a new argument completer. + /// + /// + /// If an attribute that derives from implements this interface, + /// it will be used to create the , thus giving a way to parameterize a completer. + /// The derived attribute can have properties or constructor arguments that are used when creating the completer. + /// + /// + /// This example shows the intended usage of to pass arguments to an argument completer. + /// + /// public class NumberCompleterAttribute : ArgumentCompleterAttribute, IArgumentCompleterFactory { + /// private readonly int _from; + /// private readonly int _to; + /// + /// public NumberCompleterAttribute(int from, int to){ + /// _from = from; + /// _to = to; + /// } + /// + /// // use the attribute parameters to create a parameterized completer + /// IArgumentCompleter Create() => new NumberCompleter(_from, _to); + /// } + /// + /// class NumberCompleter : IArgumentCompleter { + /// private readonly int _from; + /// private readonly int _to; + /// + /// public NumberCompleter(int from, int to){ + /// _from = from; + /// _to = to; + /// } + /// + /// IEnumerable{CompletionResult} CompleteArgument(string commandName, string parameterName, string wordToComplete, + /// CommandAst commandAst, IDictionary fakeBoundParameters) { + /// for(int i = _from; i < _to; i++) { + /// yield return new CompletionResult(i.ToString()); + /// } + /// } + /// } + /// + /// + public interface IArgumentCompleterFactory + { + /// + /// Creates an instance of a class implementing the interface. + /// + /// An IArgumentCompleter instance. + IArgumentCompleter Create(); + } + + /// + /// Base class for parameterized argument completer attributes. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public abstract class ArgumentCompleterFactoryAttribute : ArgumentCompleterAttribute, IArgumentCompleterFactory + { + /// + public abstract IArgumentCompleter Create(); + } + /// /// [Cmdlet(VerbsLifecycle.Register, "ArgumentCompleter", HelpUri = "https://go.microsoft.com/fwlink/?LinkId=528576")] diff --git a/test/powershell/Language/Parser/ExtensibleCompletion.Tests.ps1 b/test/powershell/Language/Parser/ExtensibleCompletion.Tests.ps1 index 3c9edd4391..3514f38971 100644 --- a/test/powershell/Language/Parser/ExtensibleCompletion.Tests.ps1 +++ b/test/powershell/Language/Parser/ExtensibleCompletion.Tests.ps1 @@ -175,6 +175,77 @@ function TestFunction ) } + +class NumberCompleter : IArgumentCompleter +{ + + [int] $From + [int] $To + [int] $Step + + NumberCompleter([int] $from, [int] $to, [int] $step) + { + if ($from -gt $to) { + throw [ArgumentOutOfRangeException]::new("from") + } + $this.From = $from + $this.To = $to + $this.Step = if($step -lt 1) { 1 } else { $step } + } + + [IEnumerable[CompletionResult]] CompleteArgument( + [string] $CommandName, + [string] $parameterName, + [string] $wordToComplete, + [CommandAst] $commandAst, + [IDictionary] $fakeBoundParameters) + { + $resultList = [List[CompletionResult]]::new() + $local:to = $this.To + for ($i = $this.From; $i -le $to; $i += $this.Step) { + if ($i.ToString().StartsWith($wordToComplete, [System.StringComparison]::Ordinal)) { + $num = $i.ToString() + $resultList.Add([CompletionResult]::new($num, $num, "ParameterValue", $num)) + } + } + + return $resultList + } +} + +class NumberCompletionAttribute : ArgumentCompleterAttribute, IArgumentCompleterFactory +{ + [int] $From + [int] $To + [int] $Step + + NumberCompletionAttribute([int] $from, [int] $to) + { + $this.From = $from + $this.To = $to + $this.Step = 1 + } + + [IArgumentCompleter] Create() { return [NumberCompleter]::new($this.From, $this.To, $this.Step) } +} + +function FactoryCompletionAdd { + param( + [NumberCompletion(0, 50, Step = 5)] + [int] $Number + ) +} + +Describe "Factory based extensible completion" -Tags "CI" { + @{ + ExpectedResults = @( + @{CompletionText = "5"; ResultType = "ParameterValue" } + @{CompletionText = "50"; ResultType = "ParameterValue" } + ) + TestInput = 'FactoryCompletionAdd -Number 5' + } | Get-CompletionTestCaseData | Test-Completions +} + Describe "Script block based extensible completion" -Tags "CI" { @{ ExpectedResults = @(