mirror of
https://git.jabber.space/devs/cproof.git
synced 2026-07-20 00:06:22 +00:00
Autocompletion failed for nicknames using non Latin scripts. We used `g_str_to_ascii`, which replaces characters it cannot transliterate with '?', leading search failures and false matches between different scripts. Now we do: Use `g_utf8_casefold` for case-insensitive UTF-8 comparison. This ensures that like 'Σ' correctly match 'σ' across all Unicode scripts, providing correct results for non English nicknames. If we don't fine anything typing a base ASCII character matches an accented one (typing `e` matches `è`). This pass uses `g_str_to_ascii` followed by `g_ascii_strdown` for comparison. It is now restricted to run if the search string itself is valid ASCII, preventing the "everything matches '?'" bug in non Latin scripts. Autocomplete items are sorted using `strcmp`. `g_utf8_collate` provides linguistical ordering for a specific language. But its behavior is locale dependent and undefined when comparing strings from different scripts. `strcmp` does byte-wise ordering that correctly follows Unicode code order for UTF-8 strings.
422 lines
10 KiB
C
422 lines
10 KiB
C
/*
|
|
* autocomplete.c
|
|
* vim: expandtab:ts=4:sts=4:sw=4
|
|
*
|
|
* Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <glib.h>
|
|
|
|
#include "common.h"
|
|
#include "tools/autocomplete.h"
|
|
#include "tools/parser.h"
|
|
#include "ui/ui.h"
|
|
|
|
typedef enum {
|
|
PREVIOUS,
|
|
NEXT
|
|
} search_direction;
|
|
|
|
struct autocomplete_t
|
|
{
|
|
GList* items;
|
|
GList* last_found;
|
|
gchar* search_str;
|
|
};
|
|
|
|
static gchar* _search(Autocomplete ac, GList* curr, gboolean quote, search_direction direction);
|
|
|
|
Autocomplete
|
|
autocomplete_new(void)
|
|
{
|
|
return g_new0(struct autocomplete_t, 1);
|
|
}
|
|
|
|
void
|
|
autocomplete_clear(Autocomplete ac)
|
|
{
|
|
if (ac) {
|
|
if (ac->items) {
|
|
g_list_free_full(ac->items, free);
|
|
ac->items = NULL;
|
|
}
|
|
|
|
autocomplete_reset(ac);
|
|
}
|
|
}
|
|
|
|
void
|
|
autocomplete_reset(Autocomplete ac)
|
|
{
|
|
ac->last_found = NULL;
|
|
FREE_SET_NULL(ac->search_str);
|
|
}
|
|
|
|
void
|
|
autocomplete_free(Autocomplete ac)
|
|
{
|
|
if (ac) {
|
|
autocomplete_clear(ac);
|
|
free(ac);
|
|
}
|
|
}
|
|
|
|
gint
|
|
autocomplete_length(Autocomplete ac)
|
|
{
|
|
if (!ac) {
|
|
return 0;
|
|
} else if (!ac->items) {
|
|
return 0;
|
|
} else {
|
|
return g_list_length(ac->items);
|
|
}
|
|
}
|
|
|
|
void
|
|
autocomplete_update(Autocomplete ac, char** items)
|
|
{
|
|
auto_gchar gchar* last_found = NULL;
|
|
auto_gchar gchar* search_str = NULL;
|
|
|
|
if (ac->last_found) {
|
|
last_found = strdup(ac->last_found->data);
|
|
}
|
|
|
|
if (ac->search_str) {
|
|
search_str = strdup(ac->search_str);
|
|
}
|
|
|
|
autocomplete_clear(ac);
|
|
autocomplete_add_all(ac, items);
|
|
|
|
if (last_found) {
|
|
// NULL if last_found was removed on update.
|
|
ac->last_found = g_list_find_custom(ac->items, last_found, (GCompareFunc)strcmp);
|
|
}
|
|
|
|
if (search_str) {
|
|
ac->search_str = strdup(search_str);
|
|
}
|
|
}
|
|
|
|
void
|
|
autocomplete_add_unsorted(Autocomplete ac, const char* item, const gboolean is_reversed)
|
|
{
|
|
if (ac) {
|
|
char* item_cpy;
|
|
GList* curr = g_list_find_custom(ac->items, item, (GCompareFunc)strcmp);
|
|
|
|
// if item already exists
|
|
if (curr) {
|
|
return;
|
|
}
|
|
|
|
item_cpy = strdup(item);
|
|
|
|
if (is_reversed) {
|
|
ac->items = g_list_prepend(ac->items, item_cpy);
|
|
} else {
|
|
ac->items = g_list_append(ac->items, item_cpy);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
autocomplete_add(Autocomplete ac, const char* item)
|
|
{
|
|
if (ac) {
|
|
char* item_cpy;
|
|
GList* curr = g_list_find_custom(ac->items, item, (GCompareFunc)strcmp);
|
|
|
|
// if item already exists
|
|
if (curr) {
|
|
return;
|
|
}
|
|
|
|
item_cpy = strdup(item);
|
|
ac->items = g_list_insert_sorted(ac->items, item_cpy, (GCompareFunc)strcmp);
|
|
}
|
|
}
|
|
|
|
void
|
|
autocomplete_add_all(Autocomplete ac, char** items)
|
|
{
|
|
for (guint i = 0; i < g_strv_length(items); i++) {
|
|
autocomplete_add(ac, items[i]);
|
|
}
|
|
}
|
|
|
|
void
|
|
autocomplete_remove(Autocomplete ac, const char* const item)
|
|
{
|
|
if (ac) {
|
|
GList* curr = g_list_find_custom(ac->items, item, (GCompareFunc)strcmp);
|
|
|
|
if (!curr) {
|
|
return;
|
|
}
|
|
|
|
// reset last found if it points to the item to be removed
|
|
if (ac->last_found == curr) {
|
|
ac->last_found = NULL;
|
|
}
|
|
|
|
free(curr->data);
|
|
ac->items = g_list_delete_link(ac->items, curr);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
void
|
|
autocomplete_remove_all(Autocomplete ac, char** items)
|
|
{
|
|
for (guint i = 0; i < g_strv_length(items); i++) {
|
|
autocomplete_remove(ac, items[i]);
|
|
}
|
|
}
|
|
|
|
GList*
|
|
autocomplete_create_list(Autocomplete ac)
|
|
{
|
|
GList* copy = NULL;
|
|
GList* curr = ac->items;
|
|
|
|
while (curr) {
|
|
copy = g_list_append(copy, strdup(curr->data));
|
|
curr = g_list_next(curr);
|
|
}
|
|
|
|
return copy;
|
|
}
|
|
|
|
gboolean
|
|
autocomplete_contains(Autocomplete ac, const char* value)
|
|
{
|
|
GList* curr = ac->items;
|
|
|
|
while (curr) {
|
|
if (strcmp(curr->data, value) == 0) {
|
|
return TRUE;
|
|
}
|
|
curr = g_list_next(curr);
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
gchar*
|
|
autocomplete_complete(Autocomplete ac, const gchar* search_str, gboolean quote, gboolean previous)
|
|
{
|
|
gchar* found = NULL;
|
|
|
|
// no autocomplete to search
|
|
if (!ac) {
|
|
return NULL;
|
|
}
|
|
|
|
// no items to search
|
|
if (!ac->items) {
|
|
return NULL;
|
|
}
|
|
|
|
// first search attempt
|
|
if (!ac->last_found) {
|
|
if (ac->search_str) {
|
|
FREE_SET_NULL(ac->search_str);
|
|
}
|
|
|
|
ac->search_str = strdup(search_str);
|
|
found = _search(ac, ac->items, quote, NEXT);
|
|
|
|
return found;
|
|
|
|
// subsequent search attempt
|
|
} else {
|
|
if (previous) {
|
|
// search from here-1 to beginning
|
|
found = _search(ac, g_list_previous(ac->last_found), quote, PREVIOUS);
|
|
if (found) {
|
|
return found;
|
|
}
|
|
} else {
|
|
// search from here+1 to end
|
|
found = _search(ac, g_list_next(ac->last_found), quote, NEXT);
|
|
if (found) {
|
|
return found;
|
|
}
|
|
}
|
|
|
|
if (previous) {
|
|
// search from end
|
|
found = _search(ac, g_list_last(ac->items), quote, PREVIOUS);
|
|
if (found) {
|
|
return found;
|
|
}
|
|
} else {
|
|
// search from beginning
|
|
found = _search(ac, ac->items, quote, NEXT);
|
|
if (found) {
|
|
return found;
|
|
}
|
|
}
|
|
|
|
// we found nothing, reset search
|
|
autocomplete_reset(ac);
|
|
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
// autocomplete_func func is used -> autocomplete_param_with_func
|
|
// Autocomplete ac, gboolean quote are used -> autocomplete_param_with_ac
|
|
static char*
|
|
_autocomplete_param_common(const char* const input, char* command, autocomplete_func func, Autocomplete ac, gboolean quote, gboolean previous, void* context)
|
|
{
|
|
int len;
|
|
auto_gchar gchar* command_cpy = g_strdup_printf("%s ", command);
|
|
if (!command_cpy) {
|
|
return NULL;
|
|
}
|
|
|
|
len = strlen(command_cpy);
|
|
|
|
if (strncmp(input, command_cpy, len) == 0) {
|
|
int inp_len = strlen(input);
|
|
auto_char char* found = NULL;
|
|
auto_char char* prefix = malloc(inp_len + 1);
|
|
if (!prefix) {
|
|
return NULL;
|
|
}
|
|
|
|
for (int i = len; i < inp_len; i++) {
|
|
prefix[i - len] = input[i];
|
|
}
|
|
|
|
prefix[inp_len - len] = '\0';
|
|
|
|
if (func) {
|
|
found = func(prefix, previous, context);
|
|
} else {
|
|
found = autocomplete_complete(ac, prefix, quote, previous);
|
|
}
|
|
|
|
if (found) {
|
|
return g_strdup_printf("%s%s", command_cpy, found);
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
char*
|
|
autocomplete_param_with_func(const char* const input, char* command, autocomplete_func func, gboolean previous, void* context)
|
|
{
|
|
return _autocomplete_param_common(input, command, func, NULL, FALSE, previous, context);
|
|
}
|
|
|
|
char*
|
|
autocomplete_param_with_ac(const char* const input, char* command, Autocomplete ac, gboolean quote, gboolean previous)
|
|
{
|
|
return _autocomplete_param_common(input, command, NULL, ac, quote, previous, NULL);
|
|
}
|
|
|
|
char*
|
|
autocomplete_param_no_with_func(const char* const input, char* command, int arg_number, autocomplete_func func, gboolean previous, void* context)
|
|
{
|
|
if (strncmp(input, command, strlen(command)) == 0) {
|
|
// count tokens properly
|
|
int num_tokens = count_tokens(input);
|
|
|
|
// if correct number of tokens, then candidate for autocompletion of last param
|
|
if (num_tokens == arg_number) {
|
|
auto_gchar gchar* start_str = get_start(input, arg_number);
|
|
auto_gchar gchar* comp_str = g_strdup(&input[strlen(start_str)]);
|
|
|
|
// autocomplete param
|
|
if (comp_str) {
|
|
auto_gchar gchar* found = func(comp_str, previous, context);
|
|
if (found) {
|
|
return g_strdup_printf("%s%s", start_str, found);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* remove the last message if we have more than max */
|
|
void
|
|
autocomplete_remove_older_than_max_reverse(Autocomplete ac, int maxsize)
|
|
{
|
|
if (autocomplete_length(ac) > maxsize) {
|
|
GList* last = g_list_last(ac->items);
|
|
if (last) {
|
|
free(last->data);
|
|
ac->items = g_list_delete_link(ac->items, last);
|
|
}
|
|
}
|
|
}
|
|
|
|
static gchar*
|
|
_search(Autocomplete ac, GList* curr, gboolean quote, search_direction direction)
|
|
{
|
|
auto_gchar gchar* search_str_fold = g_utf8_casefold(ac->search_str, -1);
|
|
auto_gchar gchar* search_str_ascii = g_str_to_ascii(ac->search_str, NULL);
|
|
auto_gchar gchar* search_str_ascii_lower = g_ascii_strdown(search_str_ascii, -1);
|
|
|
|
while (curr) {
|
|
gboolean match = FALSE;
|
|
|
|
// Try exact UTF-8 case insensitive match
|
|
auto_gchar gchar* curr_fold = g_utf8_casefold(curr->data, -1);
|
|
if (g_str_has_prefix(curr_fold, search_str_fold)) {
|
|
match = TRUE;
|
|
}
|
|
|
|
// Try transliterated match (allow typing 'e' for 'è')
|
|
if (!match) {
|
|
auto_gchar gchar* curr_ascii = g_str_to_ascii(curr->data, NULL);
|
|
auto_gchar gchar* curr_ascii_lower = g_ascii_strdown(curr_ascii, -1);
|
|
|
|
// We only use transliterated match if the search string conversion didn't result
|
|
// in unknown characters ('?'), to avoid false matches between different scripts.
|
|
if (strchr(search_str_ascii_lower, '?') == NULL) {
|
|
if (g_str_has_prefix(curr_ascii_lower, search_str_ascii_lower)) {
|
|
match = TRUE;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (match) {
|
|
// set pointer to last found
|
|
ac->last_found = curr;
|
|
|
|
// if contains space, quote before returning
|
|
if (quote && g_strrstr(curr->data, " ")) {
|
|
return g_strdup_printf("\"%s\"", (gchar*)curr->data);
|
|
// otherwise just return the string
|
|
} else {
|
|
return strdup(curr->data);
|
|
}
|
|
}
|
|
|
|
if (direction == PREVIOUS) {
|
|
curr = g_list_previous(curr);
|
|
} else {
|
|
curr = g_list_next(curr);
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|