Browse Source

Add command suggestions for incorrect commands

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
FiniteReality 8 years ago
parent
commit
3343b1b5d0
4 changed files with 111 additions and 11 deletions
  1. +10
    -8
      src/Discord.Net.Commands/CommandService.cs
  2. +6
    -1
      src/Discord.Net.Commands/Map/CommandMap.cs
  3. +93
    -0
      src/Discord.Net.Commands/Map/CommandMapNode.cs
  4. +2
    -2
      src/Discord.Net.Commands/Results/SearchResult.cs

+ 10
- 8
src/Discord.Net.Commands/CommandService.cs View File

@@ -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;



+ 6
- 1
src/Discord.Net.Commands/Map/CommandMap.cs View File

@@ -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 != "");
}
}
}

+ 93
- 0
src/Discord.Net.Commands/Map/CommandMapNode.cs View File

@@ -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];
}
}
}

+ 2
- 2
src/Discord.Net.Commands/Results/SearchResult.cs View File

@@ -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);



Loading…
Cancel
Save