summaryrefslogtreecommitdiff
path: root/src/core
diff options
context:
space:
mode:
authorSébastien Helleu <flashcode@flashtux.org>2023-01-30 21:34:44 +0100
committerSébastien Helleu <flashcode@flashtux.org>2023-01-30 21:44:38 +0100
commitb02a10aa48b3164e510e6128acec226932dfe406 (patch)
tree53df42bd2a551c93c340f24e97ad63e874788688 /src/core
parent74154d972d46bc68557c07a38df30bfc0929de4b (diff)
downloadweechat-b02a10aa48b3164e510e6128acec226932dfe406.zip
core: display similar command names when a command is unknown (closes #1877)
Diffstat (limited to 'src/core')
-rw-r--r--src/core/hook/wee-hook-command.c199
-rw-r--r--src/core/hook/wee-hook-command.h8
-rw-r--r--src/core/wee-input.c11
3 files changed, 212 insertions, 6 deletions
diff --git a/src/core/hook/wee-hook-command.c b/src/core/hook/wee-hook-command.c
index 435b0d702..641b345a1 100644
--- a/src/core/hook/wee-hook-command.c
+++ b/src/core/hook/wee-hook-command.c
@@ -27,6 +27,7 @@
#include <string.h>
#include "../weechat.h"
+#include "../wee-arraylist.h"
#include "../wee-config.h"
#include "../wee-hook.h"
#include "../wee-infolist.h"
@@ -35,6 +36,7 @@
#include "../wee-string.h"
#include "../wee-utf8.h"
#include "../../gui/gui-chat.h"
+#include "../../gui/gui-filter.h"
#include "../../plugins/plugin.h"
@@ -523,6 +525,203 @@ hook_command_exec (struct t_gui_buffer *buffer, int any_plugin,
}
/*
+ * Gets relevance for cmd2 (existing command) compared to cmd1 (non-existing
+ * command).
+ *
+ * Both commands are in lower case.
+ *
+ * Returns a number based on the Levenshtein distance between two commands,
+ * lower is better.
+ */
+
+int
+hook_command_similar_get_relevance (const char *cmd1, int length_cmd1,
+ const char *cmd2, int length_cmd2)
+{
+ const char *pos;
+ int relevance, factor;
+
+ /* perfect match if commands are the same (different case) */
+ if (strcmp (cmd1, cmd2) == 0)
+ return -1;
+
+ /* init relevance with Levenshtein distance (lower is better) */
+ relevance = string_levenshtein (cmd1, cmd2, 1);
+
+ /* bonus if one command includes the other */
+ pos = (length_cmd1 < length_cmd2) ?
+ strstr (cmd2, cmd1) : strstr (cmd1, cmd2);
+ if (pos)
+ {
+ factor = 4;
+ /* extra bonus if match is at beginning */
+ if ((pos == cmd1) || (pos == cmd2))
+ factor = 5;
+ relevance /= factor;
+ }
+ else
+ {
+ /* malus if no chars in common between two words */
+ if (string_get_common_bytes_count (cmd1, cmd2) == 0)
+ relevance *= 2;
+ }
+
+ return relevance;
+}
+
+/*
+ * Compares similar commands to sort them by relevance (lower number first:
+ * best relevance).
+ */
+
+int
+hook_command_similar_cmp_cb (void *data, struct t_arraylist *arraylist,
+ void *pointer1, void *pointer2)
+{
+ struct t_hook_command_similar *ptr_cmd1, *ptr_cmd2;
+
+ /* make C compiler happy */
+ (void) data;
+ (void) arraylist;
+
+ ptr_cmd1 = (struct t_hook_command_similar *)pointer1;
+ ptr_cmd2 = (struct t_hook_command_similar *)pointer2;
+
+ if (ptr_cmd1->relevance < ptr_cmd2->relevance)
+ return -1;
+
+ if (ptr_cmd1->relevance > ptr_cmd2->relevance)
+ return 1;
+
+ return string_strcasecmp (ptr_cmd1->command, ptr_cmd2->command);
+}
+
+/*
+ * Frees a similar command.
+ */
+
+void
+hook_command_similar_free_cb (void *data, struct t_arraylist *arraylist,
+ void *pointer)
+{
+ /* make C compiler happy */
+ (void) data;
+ (void) arraylist;
+
+ free (pointer);
+}
+
+/*
+ * Builds an arraylist with similar commands.
+ *
+ * Note: result must be freed after use.
+ */
+
+struct t_arraylist *
+hook_command_build_list_similar_commands (const char *command)
+{
+ struct t_arraylist *list_commands;
+ struct t_hook *ptr_hook;
+ struct t_hook_command_similar *cmd_similar;
+ char *cmd1, *cmd2;
+ int length_cmd1, length_cmd2, relevance;
+
+ cmd1 = string_tolower (command);
+ if (!cmd1)
+ return NULL;
+
+ length_cmd1 = strlen (cmd1);
+
+ list_commands = arraylist_new (64, 1, 0,
+ &hook_command_similar_cmp_cb, NULL,
+ &hook_command_similar_free_cb, NULL);
+
+ for (ptr_hook = weechat_hooks[HOOK_TYPE_COMMAND]; ptr_hook;
+ ptr_hook = ptr_hook->next_hook)
+ {
+ if (ptr_hook->deleted)
+ continue;
+ cmd2 = string_tolower (HOOK_COMMAND(ptr_hook, command));
+ if (!cmd2)
+ continue;
+ length_cmd2 = strlen (cmd2);
+ relevance = hook_command_similar_get_relevance (cmd1, length_cmd1,
+ cmd2, length_cmd2);
+ cmd_similar = (struct t_hook_command_similar *)malloc (
+ sizeof (*cmd_similar));
+ if (cmd_similar)
+ {
+ cmd_similar->command = HOOK_COMMAND(ptr_hook, command);
+ cmd_similar->relevance = relevance;
+ }
+ arraylist_add (list_commands, cmd_similar);
+ free (cmd2);
+ }
+
+ free (cmd1);
+
+ return list_commands;
+}
+
+/*
+ * Displays similar command when an unknown command has been used, to help
+ * the user.
+ */
+
+void
+hook_command_display_error_unknown (const char *command)
+{
+ struct t_arraylist *list_commands;
+ struct t_hook_command_similar *cmd_similar;
+ char **str_commands;
+ int i, list_size, found;
+
+ if (!command || !command[0])
+ return;
+
+ list_commands = hook_command_build_list_similar_commands (command);
+ if (!list_commands)
+ return;
+
+ str_commands = string_dyn_alloc (256);
+ if (!str_commands)
+ {
+ arraylist_free (list_commands);
+ return;
+ }
+
+ found = 0;
+ list_size = arraylist_size (list_commands);
+ for (i = 0; i < list_size; i++)
+ {
+ cmd_similar = (struct t_hook_command_similar *)arraylist_get (
+ list_commands, i);
+ if (cmd_similar->relevance >= 3)
+ break;
+ if (found > 0)
+ string_dyn_concat (str_commands, ", ", -1);
+ string_dyn_concat (str_commands, cmd_similar->command, -1);
+ found++;
+ if (found >= 5)
+ break;
+ }
+ if (found == 0)
+ string_dyn_concat (str_commands, "-", -1);
+
+ gui_chat_printf_date_tags (
+ NULL,
+ 0, GUI_FILTER_TAG_NO_FILTER,
+ _("%sUnknown command \"%s\" (type /help for help), "
+ "commands with similar name: %s"),
+ gui_chat_prefix[GUI_CHAT_PREFIX_ERROR],
+ command,
+ *str_commands);
+
+ string_dyn_free (str_commands, 1);
+ arraylist_free (list_commands);
+}
+
+/*
* Frees data in a command hook.
*/
diff --git a/src/core/hook/wee-hook-command.h b/src/core/hook/wee-hook-command.h
index 41e28363e..cec93e575 100644
--- a/src/core/hook/wee-hook-command.h
+++ b/src/core/hook/wee-hook-command.h
@@ -65,6 +65,13 @@ struct t_hook_command
char **cplt_template_args_concat; /* concatenated arguments */
};
+struct t_hook_command_similar
+{
+ const char *command; /* pointer to command name */
+ int relevance; /* lower is better: mostly based on */
+ /* Levenshtein distance between cmds */
+};
+
extern char *hook_command_get_description (struct t_hook *hook);
extern struct t_hook *hook_command (struct t_weechat_plugin *plugin,
const char *command,
@@ -78,6 +85,7 @@ extern struct t_hook *hook_command (struct t_weechat_plugin *plugin,
extern int hook_command_exec (struct t_gui_buffer *buffer, int any_plugin,
struct t_weechat_plugin *plugin,
const char *string);
+extern void hook_command_display_error_unknown (const char *command);
extern void hook_command_free_data (struct t_hook *hook);
extern int hook_command_add_to_infolist (struct t_infolist_item *item,
struct t_hook *hook);
diff --git a/src/core/wee-input.c b/src/core/wee-input.c
index d10cdd038..a287d7152 100644
--- a/src/core/wee-input.c
+++ b/src/core/wee-input.c
@@ -82,6 +82,7 @@ input_exec_command (struct t_gui_buffer *buffer,
{
char *command, *command_name, *pos;
char **old_commands_allowed, **new_commands_allowed;
+ const char *ptr_command_name;
int rc;
if ((!string) || (!string[0]))
@@ -135,9 +136,11 @@ input_exec_command (struct t_gui_buffer *buffer,
goto end;
}
+ ptr_command_name = utf8_next_char (command_name);
+
/* check if command is allowed */
if (input_commands_allowed
- && !string_match_list (command_name + 1,
+ && !string_match_list (ptr_command_name,
(const char **)input_commands_allowed, 1))
{
if (weechat_debug_core >= 1)
@@ -176,11 +179,7 @@ input_exec_command (struct t_gui_buffer *buffer,
}
else
{
- gui_chat_printf_date_tags (NULL, 0, GUI_FILTER_TAG_NO_FILTER,
- _("%sUnknown command \"%s\" "
- "(type /help for help)"),
- gui_chat_prefix[GUI_CHAT_PREFIX_ERROR],
- command_name);
+ hook_command_display_error_unknown (ptr_command_name);
rc = WEECHAT_RC_ERROR;
}
break;