|
Packit |
116408 |
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
|
|
Packit |
116408 |
/*
|
|
Packit |
116408 |
* Copyright (C) 2018 Sébastien Wilmet <swilmet@gnome.org>
|
|
Packit |
116408 |
*
|
|
Packit |
116408 |
* This program is free software; you can redistribute it and/or
|
|
Packit |
116408 |
* modify it under the terms of the GNU General Public License as
|
|
Packit |
116408 |
* published by the Free Software Foundation; either version 2 of the
|
|
Packit |
116408 |
* License, or (at your option) any later version.
|
|
Packit |
116408 |
*
|
|
Packit |
116408 |
* This program is distributed in the hope that it will be useful,
|
|
Packit |
116408 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
Packit |
116408 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Packit |
116408 |
* General Public License for more details.
|
|
Packit |
116408 |
*
|
|
Packit |
116408 |
* You should have received a copy of the GNU General Public License
|
|
Packit |
116408 |
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
Packit |
116408 |
*/
|
|
Packit |
116408 |
|
|
Packit |
116408 |
#include "dh-completion.h"
|
|
Packit |
116408 |
#include <string.h>
|
|
Packit |
116408 |
|
|
Packit |
116408 |
/**
|
|
Packit |
116408 |
* SECTION:dh-completion
|
|
Packit |
116408 |
* @Title: DhCompletion
|
|
Packit |
116408 |
* @Short_description: Support for automatic string completion
|
|
Packit |
116408 |
*
|
|
Packit |
116408 |
* #DhCompletion is a basic replacement for #GCompletion. #GCompletion (part of
|
|
Packit |
116408 |
* GLib) is deprecated, and doesn't use a great data structure (a simple
|
|
Packit |
116408 |
* #GList). #DhCompletion uses a #GSequence instead, so once the data is sorted
|
|
Packit |
116408 |
* it should be faster. #DhCompletion works only with UTF-8 strings, and copies
|
|
Packit |
116408 |
* the strings.
|
|
Packit |
116408 |
*
|
|
Packit |
116408 |
* #DhCompletion is add-only, strings cannot be removed. But with
|
|
Packit |
116408 |
* dh_completion_aggregate_complete(), a #DhCompletion object can be removed
|
|
Packit |
116408 |
* from the list.
|
|
Packit |
116408 |
*/
|
|
Packit |
116408 |
|
|
Packit |
116408 |
struct _DhCompletionPrivate {
|
|
Packit |
116408 |
/* Element types: gchar*, owned. */
|
|
Packit |
116408 |
GSequence *sequence;
|
|
Packit |
116408 |
};
|
|
Packit |
116408 |
|
|
Packit |
116408 |
typedef struct {
|
|
Packit |
116408 |
const gchar *prefix;
|
|
Packit |
116408 |
gsize prefix_bytes_length;
|
|
Packit |
116408 |
gchar *longest_prefix;
|
|
Packit |
116408 |
} CompletionData;
|
|
Packit |
116408 |
|
|
Packit |
116408 |
G_DEFINE_TYPE_WITH_PRIVATE (DhCompletion, dh_completion, G_TYPE_OBJECT)
|
|
Packit |
116408 |
|
|
Packit |
116408 |
static gint
|
|
Packit |
116408 |
compare_func (gconstpointer a,
|
|
Packit |
116408 |
gconstpointer b,
|
|
Packit |
116408 |
gpointer user_data)
|
|
Packit |
116408 |
{
|
|
Packit |
116408 |
const gchar *str_a = a;
|
|
Packit |
116408 |
const gchar *str_b = b;
|
|
Packit |
116408 |
|
|
Packit |
116408 |
/* We rely on the fact that if str_a is not equal to str_b but one is
|
|
Packit |
116408 |
* the prefix of the other, the shorter string is sorted before the
|
|
Packit |
116408 |
* longer one (i.e. the shorter string is "less than" the longer
|
|
Packit |
116408 |
* string). See do_complete().
|
|
Packit |
116408 |
*/
|
|
Packit |
116408 |
|
|
Packit |
116408 |
return g_strcmp0 (str_a, str_b);
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
static void
|
|
Packit |
116408 |
completion_data_init (CompletionData *data,
|
|
Packit |
116408 |
const gchar *prefix)
|
|
Packit |
116408 |
{
|
|
Packit |
116408 |
data->prefix = prefix;
|
|
Packit |
116408 |
data->prefix_bytes_length = strlen (prefix);
|
|
Packit |
116408 |
data->longest_prefix = NULL;
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
static void
|
|
Packit |
116408 |
dh_completion_finalize (GObject *object)
|
|
Packit |
116408 |
{
|
|
Packit |
116408 |
DhCompletion *completion = DH_COMPLETION (object);
|
|
Packit |
116408 |
|
|
Packit |
116408 |
g_sequence_free (completion->priv->sequence);
|
|
Packit |
116408 |
|
|
Packit |
116408 |
G_OBJECT_CLASS (dh_completion_parent_class)->finalize (object);
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
static void
|
|
Packit |
116408 |
dh_completion_class_init (DhCompletionClass *klass)
|
|
Packit |
116408 |
{
|
|
Packit |
116408 |
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
Packit |
116408 |
|
|
Packit |
116408 |
object_class->finalize = dh_completion_finalize;
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
static void
|
|
Packit |
116408 |
dh_completion_init (DhCompletion *completion)
|
|
Packit |
116408 |
{
|
|
Packit |
116408 |
completion->priv = dh_completion_get_instance_private (completion);
|
|
Packit |
116408 |
|
|
Packit |
116408 |
completion->priv->sequence = g_sequence_new (g_free);
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
/**
|
|
Packit |
116408 |
* dh_completion_new:
|
|
Packit |
116408 |
*
|
|
Packit |
116408 |
* Returns: a new #DhCompletion object.
|
|
Packit |
116408 |
* Since: 3.28
|
|
Packit |
116408 |
*/
|
|
Packit |
116408 |
DhCompletion *
|
|
Packit |
116408 |
dh_completion_new (void)
|
|
Packit |
116408 |
{
|
|
Packit |
116408 |
return g_object_new (DH_TYPE_COMPLETION, NULL);
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
/**
|
|
Packit |
116408 |
* dh_completion_add_string:
|
|
Packit |
116408 |
* @completion: a #DhCompletion.
|
|
Packit |
116408 |
* @str: a string.
|
|
Packit |
116408 |
*
|
|
Packit |
116408 |
* Adds a string to the @completion object.
|
|
Packit |
116408 |
*
|
|
Packit |
116408 |
* After adding all the strings you need to call dh_completion_sort().
|
|
Packit |
116408 |
*
|
|
Packit |
116408 |
* Since: 3.28
|
|
Packit |
116408 |
*/
|
|
Packit |
116408 |
void
|
|
Packit |
116408 |
dh_completion_add_string (DhCompletion *completion,
|
|
Packit |
116408 |
const gchar *str)
|
|
Packit |
116408 |
{
|
|
Packit |
116408 |
g_return_if_fail (DH_IS_COMPLETION (completion));
|
|
Packit |
116408 |
g_return_if_fail (str != NULL);
|
|
Packit |
116408 |
|
|
Packit |
116408 |
g_sequence_append (completion->priv->sequence, g_strdup (str));
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
/**
|
|
Packit |
116408 |
* dh_completion_sort:
|
|
Packit |
116408 |
* @completion: a #DhCompletion.
|
|
Packit |
116408 |
*
|
|
Packit |
116408 |
* Sorts all the strings. It is required to call this function after adding
|
|
Packit |
116408 |
* strings with dh_completion_add_string().
|
|
Packit |
116408 |
*
|
|
Packit |
116408 |
* Since: 3.28
|
|
Packit |
116408 |
*/
|
|
Packit |
116408 |
void
|
|
Packit |
116408 |
dh_completion_sort (DhCompletion *completion)
|
|
Packit |
116408 |
{
|
|
Packit |
116408 |
g_return_if_fail (DH_IS_COMPLETION (completion));
|
|
Packit |
116408 |
|
|
Packit |
116408 |
g_sequence_sort (completion->priv->sequence,
|
|
Packit |
116408 |
compare_func,
|
|
Packit |
116408 |
NULL);
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
static gboolean
|
|
Packit |
116408 |
bytes_equal (const gchar *str1_start_pos,
|
|
Packit |
116408 |
const gchar *str1_end_pos,
|
|
Packit |
116408 |
const gchar *str2_start_pos,
|
|
Packit |
116408 |
const gchar *str2_end_pos)
|
|
Packit |
116408 |
{
|
|
Packit |
116408 |
const gchar *str1_pos;
|
|
Packit |
116408 |
const gchar *str2_pos;
|
|
Packit |
116408 |
|
|
Packit |
116408 |
for (str1_pos = str1_start_pos, str2_pos = str2_start_pos;
|
|
Packit |
116408 |
str1_pos < str1_end_pos && str2_pos < str2_end_pos;
|
|
Packit |
116408 |
str1_pos++, str2_pos++) {
|
|
Packit |
116408 |
if (*str1_pos != *str2_pos)
|
|
Packit |
116408 |
return FALSE;
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
return str1_pos == str1_end_pos && str2_pos == str2_end_pos;
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
/* cur_str must have data->prefix as prefix. */
|
|
Packit |
116408 |
static void
|
|
Packit |
116408 |
adjust_longest_prefix (CompletionData *data,
|
|
Packit |
116408 |
const gchar *cur_str)
|
|
Packit |
116408 |
{
|
|
Packit |
116408 |
const gchar *cur_str_pos;
|
|
Packit |
116408 |
gchar *longest_prefix_pos;
|
|
Packit |
116408 |
|
|
Packit |
116408 |
/* Skip the first bytes, they are equal. */
|
|
Packit |
116408 |
cur_str_pos = cur_str + data->prefix_bytes_length;
|
|
Packit |
116408 |
longest_prefix_pos = data->longest_prefix + data->prefix_bytes_length;
|
|
Packit |
116408 |
|
|
Packit |
116408 |
while (*cur_str_pos != '\0' && *longest_prefix_pos != '\0') {
|
|
Packit |
116408 |
const gchar *cur_str_next_pos;
|
|
Packit |
116408 |
gchar *longest_prefix_next_pos;
|
|
Packit |
116408 |
|
|
Packit |
116408 |
cur_str_next_pos = g_utf8_find_next_char (cur_str_pos, NULL);
|
|
Packit |
116408 |
longest_prefix_next_pos = g_utf8_find_next_char (longest_prefix_pos, NULL);
|
|
Packit |
116408 |
|
|
Packit |
116408 |
if (!bytes_equal (cur_str_pos, cur_str_next_pos,
|
|
Packit |
116408 |
longest_prefix_pos, longest_prefix_next_pos)) {
|
|
Packit |
116408 |
break;
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
cur_str_pos = cur_str_next_pos;
|
|
Packit |
116408 |
longest_prefix_pos = longest_prefix_next_pos;
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
if (*longest_prefix_pos != '\0') {
|
|
Packit |
116408 |
/* Shrink data->longest_prefix. */
|
|
Packit |
116408 |
*longest_prefix_pos = '\0';
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
/* Returns TRUE to continue the iteration.
|
|
Packit |
116408 |
* cur_str must have data->prefix as prefix.
|
|
Packit |
116408 |
*/
|
|
Packit |
116408 |
static gboolean
|
|
Packit |
116408 |
next_completion_iteration (CompletionData *data,
|
|
Packit |
116408 |
const gchar *cur_str)
|
|
Packit |
116408 |
{
|
|
Packit |
116408 |
if (cur_str == NULL)
|
|
Packit |
116408 |
return TRUE;
|
|
Packit |
116408 |
|
|
Packit |
116408 |
if (data->longest_prefix == NULL) {
|
|
Packit |
116408 |
data->longest_prefix = g_strdup (cur_str);
|
|
Packit |
116408 |
/* After this point, data->longest_prefix can only shrink. */
|
|
Packit |
116408 |
} else {
|
|
Packit |
116408 |
adjust_longest_prefix (data, cur_str);
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
/* Back to data->prefix, stop the iteration, the longest_prefix can no
|
|
Packit |
116408 |
* longer shrink.
|
|
Packit |
116408 |
*/
|
|
Packit |
116408 |
if (g_str_equal (data->longest_prefix, data->prefix)) {
|
|
Packit |
116408 |
g_free (data->longest_prefix);
|
|
Packit |
116408 |
data->longest_prefix = NULL;
|
|
Packit |
116408 |
return FALSE;
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
return TRUE;
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
/* Like dh_completion_complete() but with @found_string_with_prefix in
|
|
Packit |
116408 |
* addition, to differentiate two different cases when %NULL is returned.
|
|
Packit |
116408 |
*
|
|
Packit |
116408 |
* Another implementation solution: instead of returning (NULL +
|
|
Packit |
116408 |
* found_string_with_prefix=TRUE), return a string equal to @prefix. But it
|
|
Packit |
116408 |
* would be harder to document (because it's less explicit) and less convenient
|
|
Packit |
116408 |
* to use as a public API (I think for a public API we don't want as a return
|
|
Packit |
116408 |
* value a string equal to @prefix).
|
|
Packit |
116408 |
*/
|
|
Packit |
116408 |
static gchar *
|
|
Packit |
116408 |
do_complete (DhCompletion *completion,
|
|
Packit |
116408 |
const gchar *prefix,
|
|
Packit |
116408 |
gboolean *found_string_with_prefix)
|
|
Packit |
116408 |
{
|
|
Packit |
116408 |
GSequenceIter *iter;
|
|
Packit |
116408 |
CompletionData data;
|
|
Packit |
116408 |
|
|
Packit |
116408 |
if (found_string_with_prefix != NULL)
|
|
Packit |
116408 |
*found_string_with_prefix = FALSE;
|
|
Packit |
116408 |
|
|
Packit |
116408 |
g_return_val_if_fail (DH_IS_COMPLETION (completion), NULL);
|
|
Packit |
116408 |
g_return_val_if_fail (prefix != NULL, NULL);
|
|
Packit |
116408 |
|
|
Packit |
116408 |
iter = g_sequence_search (completion->priv->sequence,
|
|
Packit |
116408 |
(gpointer) prefix,
|
|
Packit |
116408 |
compare_func,
|
|
Packit |
116408 |
NULL);
|
|
Packit |
116408 |
|
|
Packit |
116408 |
/* There can be an exact match just *before* iter, since compare_func
|
|
Packit |
116408 |
* returns 0 in that case.
|
|
Packit |
116408 |
*/
|
|
Packit |
116408 |
if (!g_sequence_iter_is_begin (iter)) {
|
|
Packit |
116408 |
GSequenceIter *prev_iter;
|
|
Packit |
116408 |
const gchar *prev_str;
|
|
Packit |
116408 |
|
|
Packit |
116408 |
prev_iter = g_sequence_iter_prev (iter);
|
|
Packit |
116408 |
prev_str = g_sequence_get (prev_iter);
|
|
Packit |
116408 |
|
|
Packit |
116408 |
/* If there is an exact match, the prefix can not be completed. */
|
|
Packit |
116408 |
if (g_str_equal (prev_str, prefix)) {
|
|
Packit |
116408 |
if (found_string_with_prefix != NULL)
|
|
Packit |
116408 |
*found_string_with_prefix = TRUE;
|
|
Packit |
116408 |
return NULL;
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
completion_data_init (&data, prefix);
|
|
Packit |
116408 |
|
|
Packit |
116408 |
/* All the other strings in the GSequence that have @prefix as prefix
|
|
Packit |
116408 |
* will be *after* iter, see the comment in compare_func().
|
|
Packit |
116408 |
*/
|
|
Packit |
116408 |
while (!g_sequence_iter_is_end (iter)) {
|
|
Packit |
116408 |
const gchar *cur_str = g_sequence_get (iter);
|
|
Packit |
116408 |
|
|
Packit |
116408 |
if (!g_str_has_prefix (cur_str, prefix))
|
|
Packit |
116408 |
break;
|
|
Packit |
116408 |
|
|
Packit |
116408 |
if (found_string_with_prefix != NULL)
|
|
Packit |
116408 |
*found_string_with_prefix = TRUE;
|
|
Packit |
116408 |
|
|
Packit |
116408 |
if (!next_completion_iteration (&data, cur_str))
|
|
Packit |
116408 |
break;
|
|
Packit |
116408 |
|
|
Packit |
116408 |
iter = g_sequence_iter_next (iter);
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
return data.longest_prefix;
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
/**
|
|
Packit |
116408 |
* dh_completion_complete:
|
|
Packit |
116408 |
* @completion: a #DhCompletion.
|
|
Packit |
116408 |
* @prefix: the string to complete.
|
|
Packit |
116408 |
*
|
|
Packit |
116408 |
* This function does the equivalent of:
|
|
Packit |
116408 |
* 1. Searches the data structure of @completion to find all strings that have
|
|
Packit |
116408 |
* @prefix as prefix.
|
|
Packit |
116408 |
* 2. From the list found at step 1, find the longest prefix that still matches
|
|
Packit |
116408 |
* all the strings in the list.
|
|
Packit |
116408 |
*
|
|
Packit |
116408 |
* This function assumes that @prefix and the strings contained in @completion
|
|
Packit |
116408 |
* are in UTF-8. If all the strings are valid UTF-8, then the return value will
|
|
Packit |
116408 |
* also be valid UTF-8 (it won't return a partial multi-byte character).
|
|
Packit |
116408 |
*
|
|
Packit |
116408 |
* Returns: (transfer full) (nullable): the completed prefix, or %NULL if a
|
|
Packit |
116408 |
* longer prefix has not been found. Free with g_free() when no longer needed.
|
|
Packit |
116408 |
* Since: 3.28
|
|
Packit |
116408 |
*/
|
|
Packit |
116408 |
gchar *
|
|
Packit |
116408 |
dh_completion_complete (DhCompletion *completion,
|
|
Packit |
116408 |
const gchar *prefix)
|
|
Packit |
116408 |
{
|
|
Packit |
116408 |
return do_complete (completion, prefix, NULL);
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
/**
|
|
Packit |
116408 |
* dh_completion_aggregate_complete:
|
|
Packit |
116408 |
* @completion_objects: (element-type DhCompletion) (nullable): a #GList of
|
|
Packit |
116408 |
* #DhCompletion objects.
|
|
Packit |
116408 |
* @prefix: the string to complete.
|
|
Packit |
116408 |
*
|
|
Packit |
116408 |
* The same as dh_completion_complete(), but aggregated for several
|
|
Packit |
116408 |
* #DhCompletion objects.
|
|
Packit |
116408 |
*
|
|
Packit |
116408 |
* Returns: (transfer full) (nullable): the completed prefix, or %NULL if a
|
|
Packit |
116408 |
* longer prefix has not been found. Free with g_free() when no longer needed.
|
|
Packit |
116408 |
* Since: 3.28
|
|
Packit |
116408 |
*/
|
|
Packit |
116408 |
gchar *
|
|
Packit |
116408 |
dh_completion_aggregate_complete (GList *completion_objects,
|
|
Packit |
116408 |
const gchar *prefix)
|
|
Packit |
116408 |
{
|
|
Packit |
116408 |
CompletionData data;
|
|
Packit |
116408 |
GList *l;
|
|
Packit |
116408 |
|
|
Packit |
116408 |
g_return_val_if_fail (prefix != NULL, NULL);
|
|
Packit |
116408 |
|
|
Packit |
116408 |
completion_data_init (&data, prefix);
|
|
Packit |
116408 |
|
|
Packit |
116408 |
for (l = completion_objects; l != NULL; l = l->next) {
|
|
Packit |
116408 |
DhCompletion *cur_completion = DH_COMPLETION (l->data);
|
|
Packit |
116408 |
gchar *cur_longest_prefix;
|
|
Packit |
116408 |
gboolean found_string_with_prefix;
|
|
Packit |
116408 |
|
|
Packit |
116408 |
cur_longest_prefix = do_complete (cur_completion,
|
|
Packit |
116408 |
prefix,
|
|
Packit |
116408 |
&found_string_with_prefix);
|
|
Packit |
116408 |
|
|
Packit |
116408 |
if (cur_longest_prefix == NULL && found_string_with_prefix) {
|
|
Packit |
116408 |
/* Stop the completion, it is not possible to complete
|
|
Packit |
116408 |
* @prefix.
|
|
Packit |
116408 |
*/
|
|
Packit |
116408 |
g_free (data.longest_prefix);
|
|
Packit |
116408 |
return NULL;
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
if (!next_completion_iteration (&data, cur_longest_prefix)) {
|
|
Packit |
116408 |
g_free (cur_longest_prefix);
|
|
Packit |
116408 |
break;
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
g_free (cur_longest_prefix);
|
|
Packit |
116408 |
}
|
|
Packit |
116408 |
|
|
Packit |
116408 |
return data.longest_prefix;
|
|
Packit |
116408 |
}
|