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 | //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(); | string searchInput = _caseSensitive ? input : input.ToLowerInvariant(); | ||||
| var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray(); | var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray(); | ||||
| if (matches.Length > 0) | if (matches.Length > 0) | ||||
| return SearchResult.FromSuccess(input, matches); | return SearchResult.FromSuccess(input, matches); | ||||
| else if (maxDifferences > 0) | |||||
| return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command.", _map.GetPartialMatches(searchInput, maxDifferences).ToImmutableArray()); | |||||
| else | else | ||||
| return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); | 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; | dependencyMap = dependencyMap ?? DependencyMap.Empty; | ||||
| var searchResult = Search(context, input); | |||||
| var searchResult = Search(context, input, maxDifferences); | |||||
| if (!searchResult.IsSuccess) | if (!searchResult.IsSuccess) | ||||
| return searchResult; | return searchResult; | ||||
| @@ -1,4 +1,4 @@ | |||||
| using System.Collections.Generic; | |||||
| using System.Collections.Generic; | |||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| @@ -29,5 +29,10 @@ namespace Discord.Commands | |||||
| { | { | ||||
| return _root.GetCommands(_service, text, 0, text != ""); | 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) | public void RemoveCommand(CommandService service, string text, int index, CommandInfo command) | ||||
| { | { | ||||
| int nextSegment = NextSegment(text, index, service._separatorChar); | 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) | private static int NextSegment(string text, int startIndex, char separator) | ||||
| { | { | ||||
| return text.IndexOf(separator, startIndex); | return text.IndexOf(separator, startIndex); | ||||
| @@ -131,5 +178,51 @@ namespace Discord.Commands | |||||
| } | } | ||||
| return (lowest != int.MaxValue) ? lowest : -1; | 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) | public static SearchResult FromSuccess(string text, IReadOnlyList<CommandMatch> commands) | ||||
| => new SearchResult(text, commands, null, null); | => 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) | public static SearchResult FromError(IResult result) | ||||
| => new SearchResult(null, null, result.Error, result.ErrorReason); | => new SearchResult(null, null, result.Error, result.ErrorReason); | ||||