Example Usage:
```cs
var result = await service.ExecuteAsync(context, 0,
maxDifferences: 3);
if (result is SearchResult)
{
var sResult = (SearchResult)result;
var commands = sResult.Commands.Select(x => x.Alias);
await message.Channel.SendMessageAsync(
$"Invalid command - Did you mean:\n{string.Join("\n", commands)}");
}
```
pull/440/head
| @@ -222,26 +222,28 @@ namespace Discord.Commands | |||
| } | |||
| //Execution | |||
| public SearchResult Search(ICommandContext context, int argPos) | |||
| => Search(context, context.Message.Content.Substring(argPos)); | |||
| public SearchResult Search(ICommandContext context, string input) | |||
| public SearchResult Search(ICommandContext context, int argPos, int maxDifferences = 5) | |||
| => Search(context, context.Message.Content.Substring(argPos), maxDifferences); | |||
| public SearchResult Search(ICommandContext context, string input, int maxDifferences = 5) | |||
| { | |||
| string searchInput = _caseSensitive ? input : input.ToLowerInvariant(); | |||
| var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray(); | |||
| if (matches.Length > 0) | |||
| return SearchResult.FromSuccess(input, matches); | |||
| else if (maxDifferences > 0) | |||
| return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command.", _map.GetPartialMatches(searchInput, maxDifferences).ToImmutableArray()); | |||
| else | |||
| return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); | |||
| } | |||
| public Task<IResult> ExecuteAsync(ICommandContext context, int argPos, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) | |||
| => ExecuteAsync(context, context.Message.Content.Substring(argPos), dependencyMap, multiMatchHandling); | |||
| public async Task<IResult> ExecuteAsync(ICommandContext context, string input, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) | |||
| public Task<IResult> ExecuteAsync(ICommandContext context, int argPos, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception, int maxDifferences = 5) | |||
| => ExecuteAsync(context, context.Message.Content.Substring(argPos), dependencyMap, multiMatchHandling, maxDifferences); | |||
| public async Task<IResult> ExecuteAsync(ICommandContext context, string input, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception, int maxDifferences = 5) | |||
| { | |||
| dependencyMap = dependencyMap ?? DependencyMap.Empty; | |||
| var searchResult = Search(context, input); | |||
| var searchResult = Search(context, input, maxDifferences); | |||
| if (!searchResult.IsSuccess) | |||
| return searchResult; | |||
| @@ -1,4 +1,4 @@ | |||
| using System.Collections.Generic; | |||
| using System.Collections.Generic; | |||
| namespace Discord.Commands | |||
| { | |||
| @@ -29,5 +29,10 @@ namespace Discord.Commands | |||
| { | |||
| return _root.GetCommands(_service, text, 0, text != ""); | |||
| } | |||
| public IEnumerable<CommandMatch> GetPartialMatches(string text, int maxDifferences) | |||
| { | |||
| return _root.GetPartialMatches(_service, text, maxDifferences, 0, text != ""); | |||
| } | |||
| } | |||
| } | |||
| @@ -49,6 +49,7 @@ namespace Discord.Commands | |||
| } | |||
| } | |||
| } | |||
| public void RemoveCommand(CommandService service, string text, int index, CommandInfo command) | |||
| { | |||
| int nextSegment = NextSegment(text, index, service._separatorChar); | |||
| @@ -113,6 +114,52 @@ namespace Discord.Commands | |||
| } | |||
| } | |||
| internal IEnumerable<CommandMatch> GetPartialMatches(CommandService service, string text, int maxDifference, int index, bool visitChildren = true) | |||
| { | |||
| var commands = _commands; | |||
| for (int i = 0; i < commands.Length; i++) | |||
| yield return new CommandMatch(_commands[i], _name); | |||
| if (visitChildren) | |||
| { | |||
| string name; | |||
| CommandMapNode nextNode; | |||
| //Search for next segment | |||
| int nextSegment = NextSegment(text, index, service._separatorChar); | |||
| if (nextSegment == -1) | |||
| name = text.Substring(index); | |||
| else | |||
| name = text.Substring(index, nextSegment - index); | |||
| foreach (var key in _nodes.Keys) | |||
| { | |||
| if (LevenshteinDistance(name, key) < maxDifference) | |||
| { | |||
| if (_nodes.TryGetValue(key, out nextNode)) | |||
| foreach (var cmd in nextNode.GetPartialMatches(service, nextSegment == -1 ? "" : text, maxDifference, nextSegment + 1, true)) | |||
| yield return cmd; | |||
| } | |||
| } | |||
| //Check if this is the last command segment before args | |||
| nextSegment = NextSegment(text, index, _whitespaceChars, service._separatorChar); | |||
| if (nextSegment != -1) | |||
| { | |||
| name = text.Substring(index, nextSegment - index); | |||
| foreach (var key in _nodes.Keys) | |||
| { | |||
| if (LevenshteinDistance(name, key) < maxDifference) | |||
| { | |||
| if (_nodes.TryGetValue(key, out nextNode)) | |||
| foreach (var cmd in nextNode.GetPartialMatches(service, nextSegment == -1 ? "" : text, maxDifference, nextSegment + 1, false)) | |||
| yield return cmd; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| private static int NextSegment(string text, int startIndex, char separator) | |||
| { | |||
| return text.IndexOf(separator, startIndex); | |||
| @@ -131,5 +178,51 @@ namespace Discord.Commands | |||
| } | |||
| return (lowest != int.MaxValue) ? lowest : -1; | |||
| } | |||
| private static int LevenshteinDistance(string source, string target) | |||
| { | |||
| var sourceLength = source.Length; | |||
| var targetLength = target.Length; | |||
| if (sourceLength == 0) | |||
| return targetLength; | |||
| if (targetLength == 0) | |||
| return sourceLength; | |||
| var matrix = new int[sourceLength + 1, targetLength + 1]; | |||
| for (int row = 0; row <= sourceLength; matrix[row, 0] = row++) | |||
| { } | |||
| for (int col = 0; col <= targetLength; matrix[0, col] = col++) | |||
| { } | |||
| for (int i = 1; i <= sourceLength; i++) | |||
| { | |||
| char sourceChr = source[i - 1]; | |||
| for (int j = 1; j <= targetLength; j++) | |||
| { | |||
| char targetChr = target[j - 1]; | |||
| int cost = sourceChr == targetChr ? 0 : 1; | |||
| int above = matrix[i - 1, j] + 1; | |||
| int left = matrix[i, j - 1] + 1; | |||
| int diagonal = matrix[i - 1, j - 1] + cost; | |||
| int minimum = int.MaxValue; | |||
| if (above < left) | |||
| minimum = above; | |||
| else | |||
| minimum = left; | |||
| if (diagonal < minimum) | |||
| minimum = diagonal; | |||
| matrix[i, j] = minimum; | |||
| } | |||
| } | |||
| return matrix[sourceLength, targetLength]; | |||
| } | |||
| } | |||
| } | |||
| @@ -24,8 +24,8 @@ namespace Discord.Commands | |||
| public static SearchResult FromSuccess(string text, IReadOnlyList<CommandMatch> commands) | |||
| => new SearchResult(text, commands, null, null); | |||
| public static SearchResult FromError(CommandError error, string reason) | |||
| => new SearchResult(null, null, error, reason); | |||
| public static SearchResult FromError(CommandError error, string reason, IReadOnlyList<CommandMatch> suggestions = null) | |||
| => new SearchResult(null, suggestions, error, reason); | |||
| public static SearchResult FromError(IResult result) | |||
| => new SearchResult(null, null, result.Error, result.ErrorReason); | |||