Skip to content

Commit

Permalink
Tyrrrz#13 - Implement command suggestions
Browse files Browse the repository at this point in the history
  • Loading branch information
mauricel committed Apr 12, 2021
1 parent 0d244f2 commit bcfe99c
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 5 deletions.
69 changes: 69 additions & 0 deletions CliFx.Tests/SuggestDirectiveSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ public class Command : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
";

private string _cmd2CommandCs = @"
[Command(""cmd02"")]
public class Command02 : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
";

public CliApplicationBuilder TestApplicationFactory(params string[] commandClasses)
Expand Down Expand Up @@ -73,5 +81,66 @@ public async Task Suggest_directive_is_enabled_by_default()
// Assert
exitCode.Should().Be(0);
}

private string FormatExpectedOutput(string [] s)
{
if( s.Length == 0)
{
return "";
}
return string.Join("\r\n", s) + "\r\n";
}

[Theory]
[InlineData("supply all commands if nothing supplied",
"clifx.exe", new[] { "cmd", "cmd02" })]
[InlineData("supply all commands that match partially",
"clifx.exe c", new[] { "cmd", "cmd02" })]
[InlineData("supply command options if match found, regardles of other partial matches (no options defined)",
"clifx.exe cmd", new string[] { })]
[InlineData("supply nothing if no partial match applies",
"clifx.exe cmd2", new string[] { })]
public async Task Suggest_directive_accepts_command_line_by_environment_variable(string usecase, string variableContents, string[] expected)
{
// Arrange
var application = TestApplicationFactory(_cmdCommandCs, _cmd2CommandCs)
.Build();

// Act
var exitCode = await application.RunAsync(
new[] { "[suggest]", "--envvar", "CLIFX-{GUID}", "--cursor", variableContents.Length.ToString() },
new Dictionary<string, string>()
{
["CLIFX-{GUID}"] = variableContents
}
);

var stdOut = FakeConsole.ReadOutputString();

// Assert
exitCode.Should().Be(0);
stdOut.Should().Be(FormatExpectedOutput(expected), usecase);
}

//[Theory]
//[InlineData("happy case", "clifx.exe c", "")]
//public async Task Suggest_directive_generates_suggestions(string because, string commandline, string expectedResult)
//{
// // Arrange
// var application = TestApplicationFactory(_cmdCommandCs)
// .Build();

// // Act
// var exitCode = await application.RunAsync(
// new[] { "[suggest]", commandline }
// );

// var stdOut = FakeConsole.ReadOutputString();

// // Assert
// exitCode.Should().Be(0);

// stdOut.Should().Be(expectedResult + "\r\n", because);
//}
}
}
4 changes: 3 additions & 1 deletion CliFx/CliApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ private async ValueTask<int> RunAsync(ApplicationSchema applicationSchema, Comma
// Handle suggest directive
if (IsSuggestModeEnabled(commandInput))
{
_console.Output.WriteLine("cmd");
new SuggestionService(applicationSchema)
.GetSuggestions(commandInput).ToList()
.ForEach(p => _console.Output.WriteLine(p));
return 0;
}

Expand Down
18 changes: 14 additions & 4 deletions CliFx/Input/CommandInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ internal partial class CommandInput

public IReadOnlyList<EnvironmentVariableInput> EnvironmentVariables { get; }

public IReadOnlyList<string> OriginalCommandLine { get; }

public bool IsDebugDirectiveSpecified => Directives.Any(d => d.IsDebugDirective);

public bool IsPreviewDirectiveSpecified => Directives.Any(d => d.IsPreviewDirective);
Expand All @@ -28,18 +30,21 @@ internal partial class CommandInput

public bool IsVersionOptionSpecified => Options.Any(o => o.IsVersionOption);


public CommandInput(
string? commandName,
IReadOnlyList<DirectiveInput> directives,
IReadOnlyList<ParameterInput> parameters,
IReadOnlyList<OptionInput> options,
IReadOnlyList<EnvironmentVariableInput> environmentVariables)
IReadOnlyList<EnvironmentVariableInput> environmentVariables,
IReadOnlyList<string> originalCommandLine)
{
CommandName = commandName;
Directives = directives;
Parameters = parameters;
Options = options;
EnvironmentVariables = environmentVariables;
OriginalCommandLine = originalCommandLine;
}
}

Expand Down Expand Up @@ -95,11 +100,15 @@ private static IReadOnlyList<DirectiveInput> ParseDirectives(
}
}

// Move the index to the position where the command name ended
// Move the index to the position where the command name ended, and return the matching commandName
if (!string.IsNullOrWhiteSpace(commandName))
{
index = lastIndex + 1;
return commandName;
}

return commandName;
// Otherwise leave index where it is, and return the potentialCommandName for auto-suggestion purposes
return potentialCommandNameComponents.JoinToString(" ");
}

private static IReadOnlyList<ParameterInput> ParseParameters(
Expand Down Expand Up @@ -225,7 +234,8 @@ ref index
parsedDirectives,
parsedParameters,
parsedOptions,
parsedEnvironmentVariables
parsedEnvironmentVariables,
commandLineArguments
);
}
}
Expand Down
76 changes: 76 additions & 0 deletions CliFx/SuggestionService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using CliFx.Input;
using CliFx.Schema;
using CliFx.Utils;
using System;
using System.Collections.Generic;
using System.Linq;

namespace CliFx
{
internal class SuggestionService
{
private ApplicationSchema _applicationSchema;

public SuggestionService(ApplicationSchema applicationSchema)
{
_applicationSchema = applicationSchema;
}

public IEnumerable<string> GetSuggestions(CommandInput commandInput)
{
var text = ExtractCommandText(commandInput);
var suggestArgs = CommandLineSplitter.Split(text).Skip(1); // ignore the application name

var suggestInput = CommandInput.Parse(
suggestArgs.ToArray(),
commandInput.EnvironmentVariables.ToDictionary(p => p.Name, p => p.Value),
_applicationSchema.GetCommandNames());

var commandMatch = _applicationSchema.Commands
.FirstOrDefault(p => string.Equals(p.Name, suggestInput.CommandName, StringComparison.OrdinalIgnoreCase));

// suggest a command name if we don't have an exact match
if (commandMatch == null)
{
return _applicationSchema.GetCommandNames()
.Where(p => p.Contains(suggestInput.CommandName, StringComparison.OrdinalIgnoreCase))
.OrderBy(p => p)
.ToList();
}

return NoSuggestions();
}

private string ExtractCommandText(CommandInput input)
{
// Accept command line arguments via environment variable as a workaround to powershell escape sequence shennidgans
var commandCacheVariable = input.Options.FirstOrDefault(p => p.Identifier == "envvar")?.Values[0];

if (commandCacheVariable == null)
{
// ignore cursor position as we don't know what the original user input string is
return string.Join(" ", input.OriginalCommandLine.Where(arg => !IsDirective(arg)));
}

var command = input.EnvironmentVariables.FirstOrDefault(p => string.Equals(p.Name, commandCacheVariable))?.Value ?? "";
var cursorPositionText = input.Options.FirstOrDefault(p => p.Identifier == "cursor")?.Values[0];
var cursorPosition = command.Length;

if (int.TryParse(cursorPositionText, out cursorPosition) && cursorPosition < command.Length)
{
return command.Remove(cursorPosition);
}
return command;
}

private static bool IsDirective(string arg)
{
return arg.StartsWith('[') && arg.EndsWith(']');
}

private static List<string> NoSuggestions()
{
return new List<string>();
}
}
}

0 comments on commit bcfe99c

Please sign in to comment.