Blame src/dh-completion.c

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
}