Skip to content

Commit

Permalink
Implemented CompletionSuggestions
Browse files Browse the repository at this point in the history
  • Loading branch information
JKamsker committed Jul 20, 2023
1 parent 3214dee commit a39517f
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 48 deletions.
13 changes: 13 additions & 0 deletions src/Spectre.Console.Cli/CommandOfT.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Spectre.Console.Cli.Completion;

namespace Spectre.Console.Cli;

/// <summary>
Expand Down Expand Up @@ -45,4 +47,15 @@ Task<int> ICommand<TSettings>.Execute(CommandContext context, TSettings settings
{
return Task.FromResult(Execute(context, settings));
}

/// <summary>
/// Gets a new suggestion matcher for this command.
/// </summary>
/// <returns>A suggestion matcher.</returns>
public virtual AsyncCommandParameterMatcher<TSettings> AsyncSuggestionMatcher => new();

/// <summary>
/// Gets a new suggestion matcher for this command.
/// </summary>
public virtual CommandParameterMatcher<TSettings> SuggestionMatcher => new();
}
43 changes: 40 additions & 3 deletions src/Spectre.Console.Cli/Completion/CompletionResultExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
namespace Spectre.Console.Cli.Completion;

/// <summary>
/// Extensions for <see cref="CompletionResult"/>.
/// Extensions for Completions
/// </summary>
public static class CompletionResultExtensions
public static class CompletionExtensions
{
/// <summary>
/// Disables completions, that are automatically generated.
Expand All @@ -16,4 +16,41 @@ public static async Task<CompletionResult> WithPreventDefault(this Task<Completi
var completionResult = await result;
return completionResult.WithPreventDefault(preventDefault);
}
}

#pragma warning disable S125 // Sections of code should not be commented out
/*
//"Cannot be infered :/"
/// <summary>
/// Creates a new suggestion matcher for this command.
/// </summary>
/// <returns>A suggestion matcher.</returns>
/// <param name="command">The command.</param>
/// <typeparam name="TSettings">The settings type.</typeparam>
/// <typeparam name="TCommand">The command type.</typeparam>
[SuppressMessage("Roslynator", "RCS1175:Unused 'this' parameter.", Justification = "Extension method")]
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Extension method")]
public static CommandParameterMatcher<TSettings> CreateMatcher<TSettings, TCommand>(this TCommand command)
where TSettings : CommandSettings
where TCommand : Command<TSettings>, IAsyncCommandCompletable
{
return new();
}
/// <summary>
/// Creates a new suggestion matcher for this command.
/// </summary>
/// <typeparam name="TSettings">The settings type.</typeparam>
/// <typeparam name="TCommand">The command type.</typeparam>
/// <returns>A suggestion matcher.</returns>
/// <param name="command">The command.</param>
[SuppressMessage("Roslynator", "RCS1175:Unused 'this' parameter.", Justification = "Extension method")]
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Extension method")]
public static AsyncCommandParameterMatcher<TSettings> CreateAsyncMatcher<TSettings, TCommand>(this TCommand command)
where TSettings : CommandSettings
where TCommand : Command<TSettings>, IAsyncCommandCompletable
{
return new();
}
*/
}
#pragma warning restore S125 // Sections of code should not be commented out
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Spectre.Console.Cli.Completion;

[System.AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = true)]
internal sealed class CompletionSuggestionsAttribute : Attribute
{
public string[] Suggestions { get; }

public CompletionSuggestionsAttribute(params string[] suggestions)
{
Suggestions = suggestions;
}
}
61 changes: 44 additions & 17 deletions src/Spectre.Console.Cli/Internal/Commands/CompleteCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ private async Task<string[]> GetCompletionsAsync(CommandModel model, string comm
{
return model.Commands.Where(cmd => !cmd.IsHidden)
.Select(c => c.Name)
.Where(n => n.StartsWith(partialElement))
.Where(n => n.StartsWith(partialElement, StringComparison.OrdinalIgnoreCase))
.ToArray();
}

Expand Down Expand Up @@ -192,7 +192,7 @@ private static CompletionResult GetSuggestions(string partialElement, IEnumerabl
{
return commands.Where(cmd => !cmd.IsHidden)
.Select(c => c.Name)
.Where(n => string.IsNullOrEmpty(partialElement) || n.StartsWith(partialElement))
.Where(n => string.IsNullOrEmpty(partialElement) || n.StartsWith(partialElement, StringComparison.OrdinalIgnoreCase))
.ToArray();
}

Expand Down Expand Up @@ -240,11 +240,13 @@ private async Task<List<CompletionResult>> GetCommandArgumentsAsync(CommandInfo

if (!hasTrailingSpace)
{
if (lastMap?.Parameter is not CommandArgument lastArgument)
if (lastMap?.Parameter is null)
{
return new List<CompletionResult>();
}

var lastArgument = lastMap?.Parameter;

var completions = await CompleteCommandOption(parent, lastArgument, lastMap.Value);
if (completions == null)
{
Expand All @@ -265,29 +267,54 @@ private async Task<List<CompletionResult>> GetCommandArgumentsAsync(CommandInfo
continue;
}

if (parameter.Parameter is ICommandParameterInfo commandArgumentParameter)
if (parameter.Parameter is null)
{
var completions = await CompleteCommandOption(parent, commandArgumentParameter, parameter.Value);
if (completions == null)
{
continue;
}
continue;
}

if (completions.Suggestions.Any() || completions.PreventDefault)
{
result.Add(new(completions));
break;
}
//var valuesViaAttributes = parameter.Parameter.Property.GetCustomAttributes<CompletionSuggestionsAttribute>();
//if (valuesViaAttributes?.Any() == true)
//{
// var values = valuesViaAttributes
// .Where(x => x.Suggestions != null)
// .SelectMany(x => x.Suggestions);

// result.Add(new(values, true));
// continue;
//}

var completions = await CompleteCommandOption(parent, parameter.Parameter, parameter.Value);
if (completions == null)
{
continue;
}

if (completions.Suggestions.Any() || completions.PreventDefault)
{
result.Add(new(completions));
break;
}
}

return result;
}

private async Task<CompletionResult?> CompleteCommandOption(CommandInfo parent, ICommandParameterInfo commandArgumentParameter, string? partialElement)
private async Task<CompletionResult?> CompleteCommandOption(CommandInfo parent, CommandParameter parameter, string? partialElement)
{
partialElement ??= string.Empty;

var valuesViaAttributes = parameter.Property.GetCustomAttributes<CompletionSuggestionsAttribute>();
if (valuesViaAttributes?.Any() == true)
{
var values = valuesViaAttributes
.Where(x => x.Suggestions != null)
.SelectMany(x => x.Suggestions)
.Where(x => string.IsNullOrEmpty(partialElement) || x.StartsWith(partialElement, StringComparison.OrdinalIgnoreCase))
;

return new(values, true);
}

var commandType = parent.CommandType;
if (commandType == null)
{
Expand All @@ -304,12 +331,12 @@ private async Task<List<CompletionResult>> GetCommandArgumentsAsync(CommandInfo
var completer = _typeResolver.Resolve(commandType);
if (completer is IAsyncCommandCompletable typedAsyncCompleter)
{
return await typedAsyncCompleter.GetSuggestionsAsync(commandArgumentParameter, partialElement);
return await typedAsyncCompleter.GetSuggestionsAsync(parameter, partialElement);
}

if (completer is ICommandCompletable typedCompleter)
{
return typedCompleter.GetSuggestions(commandArgumentParameter, partialElement);
return typedCompleter.GetSuggestions(parameter, partialElement);
}

return CompletionResult.None();
Expand Down
30 changes: 2 additions & 28 deletions test/Spectre.Console.Cli.Tests/Data/Commands/LionCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,16 @@
namespace Spectre.Console.Tests.Data;

[Description("The lion command.")]
public class LionCommand : AnimalCommand<LionSettings>, ICommandCompletable, IAsyncCommandCompletable
public class LionCommand : AnimalCommand<LionSettings>, IAsyncCommandCompletable
{
public override int Execute(CommandContext context, LionSettings settings)
{
return 0;
}

public CompletionResult GetSuggestions(ICommandParameterInfo parameter, string prefix)
{
return new CommandParameterMatcher<LionSettings>()
.Add(x => x.Legs, (prefix) =>
{
if (prefix.Length != 0)
{
return FindNextEvenNumber(prefix);
}
return "16";
})
.Add(x => x.Teeth, (prefix) =>
{
if (prefix.Length != 0)
{
return FindNextEvenNumber(prefix);
}
return "32";
})
.Add(x => x.Name, _ => "Angelika")
.Match(parameter, prefix)
.WithPreventDefault();
}

public async Task<CompletionResult> GetSuggestionsAsync(ICommandParameterInfo parameter, string prefix)
{
return await new AsyncCommandParameterMatcher<LionSettings>()
return await AsyncSuggestionMatcher
.Add(x => x.Legs, (prefix) =>
{
if (prefix.Length != 0)
Expand Down
26 changes: 26 additions & 0 deletions test/Spectre.Console.Cli.Tests/Data/Commands/UserAddCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Spectre.Console.Cli.Completion;

namespace Spectre.Console.Cli.Tests.Data.Commands;

// mycommand user add [name] --age [age]
internal class UserAddSettings : CommandSettings
{
[CommandArgument(0, "<name>")]
[Description("The name of the user.")]
[CompletionSuggestions("Angelika", "Arnold", "Bernd", "Cloud", "Jonas")]
public string Name { get; set; }

[CommandOption("-a|--age <age>")]
[Description("The age of the user.")]
[CompletionSuggestions("10", "15", "20", "30")]
public int Age { get; set; }
}

[Description("The user command.")]
internal class UserAddCommand : Command<UserAddSettings>
{
public override int Execute(CommandContext context, UserAddSettings settings)
{
return 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
10
15
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
10
15
20
30
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Angelika
Arnold
Bernd
Cloud
Jonas
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Angelika
Arnold

0 comments on commit a39517f

Please sign in to comment.