[gtksourceview] words: perform word completion from model



commit d53389d19252de7706c17452b43c3dd917e1a543
Author: Christian Hergert <chergert redhat com>
Date:   Sat Nov 27 16:57:38 2021 -0600

    words: perform word completion from model
    
    This moves the word completion work to a model rather than in the provider
    itself as it results in much easier to understand code.
    
    Additionally, it fixes an issue where we weren't seeing updates to the
    results.
    
    I'd like to note though, that there is still an initial scan timeout in
    the buffer scanner to add words, which is 5 seconds. So if you're testing
    this you'll need to wait for long enough for the word you just typed to
    show up in the library or it will not be available for completion.
    
    Fixes #181

 .../words/gtksourcecompletionwords.c               | 168 ++++---------
 .../words/gtksourcecompletionwordsmodel-private.h  |  42 ++++
 .../words/gtksourcecompletionwordsmodel.c          | 277 +++++++++++++++++++++
 .../completion-providers/words/meson.build         |   1 +
 4 files changed, 362 insertions(+), 126 deletions(-)
---
diff --git a/gtksourceview/completion-providers/words/gtksourcecompletionwords.c 
b/gtksourceview/completion-providers/words/gtksourcecompletionwords.c
index 898a4b72..0b2a6177 100644
--- a/gtksourceview/completion-providers/words/gtksourcecompletionwords.c
+++ b/gtksourceview/completion-providers/words/gtksourcecompletionwords.c
@@ -40,6 +40,7 @@
 #include "gtksourcecompletionwords.h"
 #include "gtksourcecompletionwordslibrary-private.h"
 #include "gtksourcecompletionwordsbuffer-private.h"
+#include "gtksourcecompletionwordsmodel-private.h"
 #include "gtksourcecompletionwordsutils-private.h"
 
 #define BUFFER_KEY "GtkSourceCompletionWordsBufferKey"
@@ -103,15 +104,6 @@ G_DEFINE_TYPE_WITH_CODE (GtkSourceCompletionWords,
                         G_IMPLEMENT_INTERFACE (GTK_SOURCE_TYPE_COMPLETION_PROVIDER,
                                                gtk_source_completion_words_iface_init))
 
-static void
-populate_free (Populate *p)
-{
-       g_clear_object (&p->context);
-       g_clear_object (&p->ret);
-       g_clear_pointer (&p->word, g_free);
-       g_slice_free (Populate, p);
-}
-
 static gchar *
 gtk_source_completion_words_get_title (GtkSourceCompletionProvider *self)
 {
@@ -121,57 +113,6 @@ gtk_source_completion_words_get_title (GtkSourceCompletionProvider *self)
        return g_strdup (priv->title);
 }
 
-static gboolean
-add_in_idle (GTask *task)
-{
-       GtkSourceCompletionWords *words = g_task_get_source_object (task);
-       GtkSourceCompletionWordsPrivate *priv = gtk_source_completion_words_get_instance_private (words);
-       Populate *p = g_task_get_task_data (task);
-       guint idx = 0;
-
-       if (g_task_return_error_if_cancelled (task))
-       {
-               g_clear_object (&task);
-               goto cleanup;
-       }
-
-       if (p->populate_iter == NULL)
-       {
-               p->populate_iter = gtk_source_completion_words_library_find_first (priv->library,
-                                                                                  p->word,
-                                                                                  p->word_len);
-       }
-
-       while (idx < priv->proposals_batch_size && p->populate_iter)
-       {
-               GtkSourceCompletionWordsProposal *proposal =
-                               gtk_source_completion_words_library_get_proposal (p->populate_iter);
-
-               /* Only add non-exact matches */
-               if (strcmp (gtk_source_completion_words_proposal_get_word (proposal), p->word) != 0)
-               {
-                       g_list_store_append (p->ret, proposal);
-               }
-
-               p->populate_iter = gtk_source_completion_words_library_find_next (p->populate_iter,
-                                                                                 p->word,
-                                                                                 p->word_len);
-               ++idx;
-       }
-
-       if (p->populate_iter != NULL)
-       {
-               return G_SOURCE_CONTINUE;
-       }
-
-cleanup:
-       g_task_return_pointer (task, g_steal_pointer (&p->ret), g_object_unref);
-       gtk_source_completion_words_library_unlock (priv->library);
-       g_clear_object (&task);
-
-       return G_SOURCE_REMOVE;
-}
-
 static void
 gtk_source_completion_words_populate_async (GtkSourceCompletionProvider *provider,
                                             GtkSourceCompletionContext  *context,
@@ -181,11 +122,9 @@ gtk_source_completion_words_populate_async (GtkSourceCompletionProvider *provide
 {
        GtkSourceCompletionWords *words = GTK_SOURCE_COMPLETION_WORDS (provider);
        GtkSourceCompletionWordsPrivate *priv = gtk_source_completion_words_get_instance_private (words);
-       GtkSourceCompletionActivation activation;
-       Populate *populate;
-       GtkTextIter begin, end;
-       gchar *word;
-       GTask *task = NULL;
+       GListModel *model;
+       GTask *task;
+       char *word;
 
        g_assert (GTK_SOURCE_IS_COMPLETION_WORDS (words));
        g_assert (GTK_SOURCE_IS_COMPLETION_CONTEXT (context));
@@ -194,51 +133,17 @@ gtk_source_completion_words_populate_async (GtkSourceCompletionProvider *provide
        g_task_set_source_tag (task, gtk_source_completion_words_populate_async);
        g_task_set_priority (task, priv->priority);
 
-       if (!gtk_source_completion_context_get_bounds (context, &begin, &end))
-       {
-               g_task_return_new_error (task,
-                                        G_IO_ERROR,
-                                        G_IO_ERROR_NOT_SUPPORTED,
-                                        "No word to complete");
-               g_clear_object (&task);
-               return;
-       }
-
-       g_clear_pointer (&priv->word, g_free);
-
-       word = gtk_text_iter_get_slice (&begin, &end);
-
-       g_assert (word != NULL);
-
-       activation = gtk_source_completion_context_get_activation (context);
-
-       if (activation == GTK_SOURCE_COMPLETION_ACTIVATION_INTERACTIVE &&
-            g_utf8_strlen (word, -1) < (glong)priv->minimum_word_size)
-       {
-               g_free (word);
-               g_task_return_new_error (task,
-                                        G_IO_ERROR,
-                                        G_IO_ERROR_NOT_SUPPORTED,
-                                        "Word is too short to complete");
-               return;
-       }
-
-       populate = g_slice_new0 (Populate);
-       populate->context = g_object_ref (context);
-       populate->activation = activation;
-       populate->word = g_steal_pointer (&word);
-       populate->word_len = g_utf8_strlen (populate->word, -1);
-       populate->ret = g_list_store_new (GTK_SOURCE_TYPE_COMPLETION_PROPOSAL);
-
-       g_task_set_task_data (task, populate, (GDestroyNotify)populate_free);
+       word = gtk_source_completion_context_get_word (context);
+       model = gtk_source_completion_words_model_new (priv->library,
+                                                      priv->proposals_batch_size,
+                                                      priv->minimum_word_size,
+                                                      word);
 
-       gtk_source_completion_words_library_lock (priv->library);
+       g_task_return_pointer (task, g_steal_pointer (&model), g_object_unref);
 
-       /* Do first right now */
-       if (add_in_idle (task))
-       {
-               priv->idle_id = g_idle_add ((GSourceFunc)add_in_idle, task);
-       }
+       g_clear_object (&model);
+       g_clear_object (&task);
+       g_clear_pointer (&word, g_free);
 }
 
 static GListModel *
@@ -535,12 +440,15 @@ gtk_source_completion_words_refilter (GtkSourceCompletionProvider *provider,
                                       GtkSourceCompletionContext  *context,
                                       GListModel                  *model)
 {
-       GtkFilterListModel *filter_model;
-       GtkExpression *expression;
-       GtkStringFilter *filter;
-       gchar *word;
+       GtkSourceCompletionWords *self = (GtkSourceCompletionWords *)provider;
+       GtkSourceCompletionWordsPrivate *priv = gtk_source_completion_words_get_instance_private (self);
+       GtkFilterListModel *filter_model = NULL;
+       GtkExpression *expression = NULL;
+       GtkStringFilter *filter = NULL;
+       GListModel *replaced_model = NULL;
+       char *word;
 
-       g_assert (GTK_SOURCE_IS_COMPLETION_PROVIDER (provider));
+       g_assert (GTK_SOURCE_IS_COMPLETION_WORDS (self));
        g_assert (GTK_SOURCE_IS_COMPLETION_CONTEXT (context));
        g_assert (G_IS_LIST_MODEL (model));
 
@@ -551,23 +459,31 @@ gtk_source_completion_words_refilter (GtkSourceCompletionProvider *provider,
                model = gtk_filter_list_model_get_model (GTK_FILTER_LIST_MODEL (model));
        }
 
-       if (!word || !word[0])
+       g_assert (GTK_SOURCE_IS_COMPLETION_WORDS_MODEL (model));
+
+       if (!gtk_source_completion_words_model_can_filter (GTK_SOURCE_COMPLETION_WORDS_MODEL (model), word))
        {
-               gtk_source_completion_context_set_proposals_for_provider (context, provider, model);
-               g_free (word);
-               return;
+               gtk_source_completion_words_model_cancel (GTK_SOURCE_COMPLETION_WORDS_MODEL (model));
+               replaced_model = gtk_source_completion_words_model_new (priv->library,
+                                                                       priv->proposals_batch_size,
+                                                                       priv->minimum_word_size,
+                                                                       word);
+               gtk_source_completion_context_set_proposals_for_provider (context, provider, replaced_model);
+       }
+       else
+       {
+               expression = gtk_property_expression_new (GTK_SOURCE_TYPE_COMPLETION_WORDS_PROPOSAL, NULL, 
"word");
+               filter = gtk_string_filter_new (g_steal_pointer (&expression));
+               gtk_string_filter_set_search (GTK_STRING_FILTER (filter), word);
+               filter_model = gtk_filter_list_model_new (g_object_ref (model),
+                                                         GTK_FILTER (g_steal_pointer (&filter)));
+               gtk_filter_list_model_set_incremental (filter_model, TRUE);
+               gtk_source_completion_context_set_proposals_for_provider (context, provider, G_LIST_MODEL 
(filter_model));
        }
 
-       expression = gtk_property_expression_new (GTK_SOURCE_TYPE_COMPLETION_WORDS_PROPOSAL, NULL, "word");
-       filter = gtk_string_filter_new (g_steal_pointer (&expression));
-       gtk_string_filter_set_search (GTK_STRING_FILTER (filter), word);
-       filter_model = gtk_filter_list_model_new (g_object_ref (model),
-                                                 GTK_FILTER (g_steal_pointer (&filter)));
-       gtk_filter_list_model_set_incremental (filter_model, TRUE);
-       gtk_source_completion_context_set_proposals_for_provider (context, provider, G_LIST_MODEL 
(filter_model));
-
+       g_clear_object (&replaced_model);
        g_clear_object (&filter_model);
-       g_free (word);
+       g_clear_pointer (&word, g_free);
 }
 
 static void
diff --git a/gtksourceview/completion-providers/words/gtksourcecompletionwordsmodel-private.h 
b/gtksourceview/completion-providers/words/gtksourcecompletionwordsmodel-private.h
new file mode 100644
index 00000000..7071f9e9
--- /dev/null
+++ b/gtksourceview/completion-providers/words/gtksourcecompletionwordsmodel-private.h
@@ -0,0 +1,42 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+#include "gtksourcecompletionwordslibrary-private.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_COMPLETION_WORDS_MODEL (gtk_source_completion_words_model_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceCompletionWordsModel, gtk_source_completion_words_model, GTK_SOURCE, 
COMPLETION_WORDS_MODEL, GObject)
+
+GListModel *gtk_source_completion_words_model_new        (GtkSourceCompletionWordsLibrary *library,
+                                                          guint                            
proposals_batch_size,
+                                                          guint                            minimum_word_size,
+                                                          const char                      *prefix);
+gboolean    gtk_source_completion_words_model_can_filter (GtkSourceCompletionWordsModel   *self,
+                                                          const char                      *word);
+void        gtk_source_completion_words_model_cancel     (GtkSourceCompletionWordsModel   *self);
+
+G_END_DECLS
diff --git a/gtksourceview/completion-providers/words/gtksourcecompletionwordsmodel.c 
b/gtksourceview/completion-providers/words/gtksourcecompletionwordsmodel.c
new file mode 100644
index 00000000..30b68f06
--- /dev/null
+++ b/gtksourceview/completion-providers/words/gtksourcecompletionwordsmodel.c
@@ -0,0 +1,277 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "gtksourcecompletionwordsmodel-private.h"
+
+struct _GtkSourceCompletionWordsModel
+{
+       GObject                          parent_instance;
+       GPtrArray                       *items;
+       GtkSourceCompletionWordsLibrary *library;
+       GCancellable                    *cancellable;
+       GSequenceIter                   *populate_iter;
+       char                            *prefix;
+       gsize                            prefix_len;
+       guint                            proposals_batch_size;
+       guint                            minimum_word_size;
+       guint                            idle_id;
+};
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GtkSourceCompletionWordsModel, gtk_source_completion_words_model, G_TYPE_OBJECT,
+                        G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static void
+gtk_source_completion_words_model_finalize (GObject *object)
+{
+       GtkSourceCompletionWordsModel *self = (GtkSourceCompletionWordsModel *)object;
+
+       g_clear_pointer (&self->items, g_ptr_array_unref);
+       g_clear_pointer (&self->prefix, g_free);
+       g_clear_object (&self->library);
+       g_clear_object (&self->cancellable);
+
+       g_assert (self->idle_id == 0);
+
+       G_OBJECT_CLASS (gtk_source_completion_words_model_parent_class)->finalize (object);
+}
+
+static void
+gtk_source_completion_words_model_dispose (GObject *object)
+{
+       GtkSourceCompletionWordsModel *self = (GtkSourceCompletionWordsModel *)object;
+
+       if (self->idle_id != 0)
+       {
+               g_clear_handle_id (&self->idle_id, g_source_remove);
+               gtk_source_completion_words_library_unlock (self->library);
+       }
+
+       G_OBJECT_CLASS (gtk_source_completion_words_model_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_completion_words_model_class_init (GtkSourceCompletionWordsModelClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->dispose = gtk_source_completion_words_model_dispose;
+       object_class->finalize = gtk_source_completion_words_model_finalize;
+}
+
+static void
+gtk_source_completion_words_model_init (GtkSourceCompletionWordsModel *self)
+{
+       self->items = g_ptr_array_new_with_free_func (g_object_unref);
+}
+
+static GType
+gtk_source_completion_words_model_get_item_type (GListModel *model)
+{
+       return GTK_SOURCE_TYPE_COMPLETION_PROPOSAL;
+}
+
+static guint
+gtk_source_completion_words_model_get_n_items (GListModel *model)
+{
+       GtkSourceCompletionWordsModel *self = (GtkSourceCompletionWordsModel *)model;
+
+       g_assert (GTK_SOURCE_IS_COMPLETION_WORDS_MODEL (self));
+
+       return self->items->len;
+}
+
+static gpointer
+gtk_source_completion_words_model_get_item (GListModel *model,
+                                           guint       position)
+{
+       GtkSourceCompletionWordsModel *self = (GtkSourceCompletionWordsModel *)model;
+
+       g_assert (GTK_SOURCE_IS_COMPLETION_WORDS_MODEL (self));
+
+       if (position < self->items->len)
+               return g_object_ref (g_ptr_array_index (self->items, position));
+
+       return NULL;
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+       iface->get_n_items = gtk_source_completion_words_model_get_n_items;
+       iface->get_item = gtk_source_completion_words_model_get_item;
+       iface->get_item_type = gtk_source_completion_words_model_get_item_type;
+}
+
+static gboolean
+add_in_idle (GtkSourceCompletionWordsModel *self)
+{
+       guint idx = 0;
+       guint old_len;
+
+       old_len = self->items->len;
+
+       if (g_cancellable_is_cancelled (self->cancellable))
+       {
+               goto cleanup;
+       }
+
+       if (self->populate_iter == NULL)
+       {
+               self->populate_iter = gtk_source_completion_words_library_find_first (self->library,
+                                                                                     self->prefix,
+                                                                                     self->prefix_len);
+       }
+
+       while (idx < self->proposals_batch_size && self->populate_iter)
+       {
+               GtkSourceCompletionWordsProposal *proposal;
+
+               proposal = gtk_source_completion_words_library_get_proposal (self->populate_iter);
+
+               /* Only add non-exact matches */
+               if (strcmp (gtk_source_completion_words_proposal_get_word (proposal), self->prefix) != 0)
+               {
+                       g_ptr_array_add (self->items, g_object_ref (proposal));
+               }
+
+               self->populate_iter = gtk_source_completion_words_library_find_next (self->populate_iter,
+                                                                                    self->prefix,
+                                                                                    self->prefix_len);
+               ++idx;
+       }
+
+       if (old_len < self->items->len)
+       {
+               g_list_model_items_changed (G_LIST_MODEL (self),
+                                           old_len,
+                                           0,
+                                           self->items->len - old_len);
+       }
+
+       if (self->populate_iter != NULL)
+       {
+               return G_SOURCE_CONTINUE;
+       }
+
+cleanup:
+       gtk_source_completion_words_library_unlock (self->library);
+       self->idle_id = 0;
+
+       return G_SOURCE_REMOVE;
+}
+
+static void
+gtk_source_completion_words_model_populate (GtkSourceCompletionWordsModel *self)
+{
+       g_assert (GTK_SOURCE_IS_COMPLETION_WORDS_MODEL (self));
+       g_assert (self->prefix != NULL);
+       g_assert (self->items != NULL);
+       g_assert (self->minimum_word_size >= 2);
+       g_assert (self->proposals_batch_size >= 1 || self->proposals_batch_size <= 300);
+       g_assert (self->cancellable == NULL);
+
+       /* Short-circuit if the word is too short. We won't allow replaying
+        * these until we reach a large enough word prefix.
+        */
+       if (strlen (self->prefix) < self->minimum_word_size)
+       {
+               return;
+       }
+
+       gtk_source_completion_words_library_lock (self->library);
+
+       /* Do first scan immediately */
+       if (add_in_idle (self))
+       {
+               self->idle_id = g_idle_add ((GSourceFunc)add_in_idle, self);
+       }
+}
+
+GListModel *
+gtk_source_completion_words_model_new (GtkSourceCompletionWordsLibrary *library,
+                                       guint                            proposals_batch_size,
+                                       guint                            minimum_word_size,
+                                       const char                      *prefix)
+{
+       GtkSourceCompletionWordsModel *self;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_COMPLETION_WORDS_LIBRARY (library), NULL);
+       g_return_val_if_fail (proposals_batch_size >= 1 || proposals_batch_size <= 300, NULL);
+       g_return_val_if_fail (minimum_word_size >= 2, NULL);
+
+       if (prefix == NULL)
+               prefix = "";
+
+       self = g_object_new (GTK_SOURCE_TYPE_COMPLETION_WORDS_MODEL, NULL);
+       self->library = g_object_ref (library);
+       self->proposals_batch_size = proposals_batch_size;
+       self->minimum_word_size = minimum_word_size;
+       self->prefix = g_strdup (prefix);
+       self->prefix_len = strlen (prefix);
+
+       gtk_source_completion_words_model_populate (self);
+
+       return G_LIST_MODEL (self);
+}
+
+gboolean
+gtk_source_completion_words_model_can_filter (GtkSourceCompletionWordsModel *self,
+                                              const char                    *word)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_COMPLETION_WORDS_MODEL (self), FALSE);
+
+       /* If we have an empty word, try to reuse this until we get a real word */
+       if (word == NULL || word[0] == 0)
+       {
+               return self->prefix[0] == 0;
+       }
+
+       /* If the word was too short, nothing we can do as we ignored the
+        * populate request.
+        */
+       if (strlen (self->prefix) < self->minimum_word_size)
+       {
+               return FALSE;
+       }
+
+       /* If the new word starts with our initial word, then we can simply
+        * refilter outside this model using a GtkFilterListModel.
+        */
+       return g_str_has_prefix (word, self->prefix) || g_str_equal (word, self->prefix);
+}
+
+/**
+ * gtk_source_completion_words_model_cancel:
+ * @self: a #GtkSourceCompletionWordsModel
+ *
+ * Cancels any in-flight population by cancelling the task.
+ */
+void
+gtk_source_completion_words_model_cancel (GtkSourceCompletionWordsModel *self)
+{
+       g_return_if_fail (GTK_SOURCE_IS_COMPLETION_WORDS_MODEL (self));
+
+       g_cancellable_cancel (self->cancellable);
+}
diff --git a/gtksourceview/completion-providers/words/meson.build 
b/gtksourceview/completion-providers/words/meson.build
index 29b4d449..e080e01f 100644
--- a/gtksourceview/completion-providers/words/meson.build
+++ b/gtksourceview/completion-providers/words/meson.build
@@ -11,6 +11,7 @@ completionwords_public_c = [
   'gtksourcecompletionwords.c',
   'gtksourcecompletionwordsbuffer.c',
   'gtksourcecompletionwordslibrary.c',
+  'gtksourcecompletionwordsmodel.c',
   'gtksourcecompletionwordsproposal.c',
   'gtksourcecompletionwordsutils.c',
 ]


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]