summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Hergert <chergert@redhat.com>2022-11-16 15:10:50 -0800
committerChristian Hergert <chergert@redhat.com>2022-11-16 15:10:50 -0800
commitca6c7587c2f67c9cc9adff4126d5aaf87ea10f4f (patch)
tree754aee0bd8327a550c176410c08aec2acb11e770
parentc082de7bfd0afa904e11d48c17586b21aa556469 (diff)
parentdc73ece2b96886a778dfe34e808106e89469f1f0 (diff)
downloadgtksourceview-ca6c7587c2f67c9cc9adff4126d5aaf87ea10f4f.tar.gz
Merge branch 'wip/chergert/fix-completion-snapshots'
-rw-r--r--gtksourceview/gtksourcecompletionlistbox.c132
-rw-r--r--gtksourceview/gtksourcelistsnapshot-private.h40
-rw-r--r--gtksourceview/gtksourcelistsnapshot.c406
-rw-r--r--gtksourceview/meson.build1
-rw-r--r--testsuite/meson.build1
-rw-r--r--testsuite/test-listsnapshot.c160
6 files changed, 711 insertions, 29 deletions
diff --git a/gtksourceview/gtksourcecompletionlistbox.c b/gtksourceview/gtksourcecompletionlistbox.c
index a8f7d209..a7f6b810 100644
--- a/gtksourceview/gtksourcecompletionlistbox.c
+++ b/gtksourceview/gtksourcecompletionlistbox.c
@@ -27,12 +27,21 @@
#include "gtksourcecompletionlistboxrow-private.h"
#include "gtksourcecompletionproposal.h"
#include "gtksourcecompletionprovider.h"
+#include "gtksourcelistsnapshot-private.h"
#include "gtksourceview-private.h"
struct _GtkSourceCompletionListBox
{
GtkWidget parent_instance;
+ /* Used to snapshot a GListModel of the visible range during a
+ * frame cycle, so that it cannot change underneath us. We hold()
+ * and release() the snapshot from frame clock callbacks.
+ */
+ GtkSourceListSnapshot *list_snapshot;
+ gulong update_handler;
+ gulong after_paint_handler;
+
/* The box containing the rows. */
GtkBox *box;
@@ -42,11 +51,6 @@ struct _GtkSourceCompletionListBox
/* The completion context that is being displayed. */
GtkSourceCompletionContext *context;
- /* The handler for GtkSourceCompletionContext::items-chaged which should
- * be disconnected when no longer needed.
- */
- gulong items_changed_handler;
-
/* The number of rows we expect to have visible to the user. */
guint n_rows;
@@ -116,12 +120,21 @@ enum {
N_SIGNALS
};
-static void gtk_source_completion_list_box_queue_update (GtkSourceCompletionListBox *self);
-static gboolean gtk_source_completion_list_box_update_cb (GtkWidget *widget,
- GdkFrameClock *frame_clock,
- gpointer user_data);
-static void gtk_source_completion_list_box_do_update (GtkSourceCompletionListBox *self,
- gboolean update_selection);
+static void gtk_source_completion_list_box_queue_update (GtkSourceCompletionListBox *self);
+static gboolean gtk_source_completion_list_box_update_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data);
+static void gtk_source_completion_list_box_after_update_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock);
+static void gtk_source_completion_list_box_after_paint_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock);
+static void gtk_source_completion_list_box_do_update (GtkSourceCompletionListBox *self,
+ gboolean update_selection);
+static void gtk_source_completion_list_box_items_changed_cb (GtkSourceCompletionListBox *self,
+ guint position,
+ guint removed,
+ guint added,
+ GListModel *model);
G_DEFINE_TYPE_WITH_CODE (GtkSourceCompletionListBox, gtk_source_completion_list_box, GTK_TYPE_WIDGET,
G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL))
@@ -548,6 +561,42 @@ _gtk_source_completion_list_box_key_pressed_cb (GtkSourceCompletionListBox *self
}
static void
+gtk_source_completion_list_box_realize (GtkWidget *widget)
+{
+ GtkSourceCompletionListBox *self = GTK_SOURCE_COMPLETION_LIST_BOX (widget);
+ GdkFrameClock *frame_clock;
+
+ GTK_WIDGET_CLASS (gtk_source_completion_list_box_parent_class)->realize (widget);
+
+ frame_clock = gtk_widget_get_frame_clock (widget);
+
+ self->update_handler =
+ g_signal_connect_data (frame_clock,
+ "update",
+ G_CALLBACK (gtk_source_completion_list_box_after_update_cb),
+ self, NULL,
+ G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+ self->after_paint_handler =
+ g_signal_connect_swapped (frame_clock,
+ "after-paint",
+ G_CALLBACK (gtk_source_completion_list_box_after_paint_cb),
+ self);
+}
+
+static void
+gtk_source_completion_list_box_unrealize (GtkWidget *widget)
+{
+ GtkSourceCompletionListBox *self = GTK_SOURCE_COMPLETION_LIST_BOX (widget);
+ GdkFrameClock *frame_clock;
+
+ frame_clock = gtk_widget_get_frame_clock (widget);
+ g_clear_signal_handler (&self->update_handler, frame_clock);
+ g_clear_signal_handler (&self->after_paint_handler, frame_clock);
+
+ GTK_WIDGET_CLASS (gtk_source_completion_list_box_parent_class)->unrealize (widget);
+}
+
+static void
gtk_source_completion_list_box_constructed (GObject *object)
{
GtkSourceCompletionListBox *self = (GtkSourceCompletionListBox *)object;
@@ -582,6 +631,7 @@ gtk_source_completion_list_box_dispose (GObject *object)
self->box = NULL;
}
+ g_clear_object (&self->list_snapshot);
g_clear_object (&self->before_size_group);
g_clear_object (&self->typed_text_size_group);
g_clear_object (&self->after_size_group);
@@ -693,6 +743,9 @@ gtk_source_completion_list_box_class_init (GtkSourceCompletionListBoxClass *klas
object_class->get_property = gtk_source_completion_list_box_get_property;
object_class->set_property = gtk_source_completion_list_box_set_property;
+ widget_class->realize = gtk_source_completion_list_box_realize;
+ widget_class->unrealize = gtk_source_completion_list_box_unrealize;
+
properties [PROP_ALTERNATE] =
g_param_spec_int ("alternate",
"Alternate",
@@ -804,6 +857,13 @@ gtk_source_completion_list_box_init (GtkSourceCompletionListBox *self)
self);
gtk_widget_add_controller (GTK_WIDGET (self), key);
+ self->list_snapshot = gtk_source_list_snapshot_new ();
+ g_signal_connect_object (self->list_snapshot,
+ "items-changed",
+ G_CALLBACK (gtk_source_completion_list_box_items_changed_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
self->box = g_object_new (GTK_TYPE_BOX,
"orientation", GTK_ORIENTATION_VERTICAL,
"visible", TRUE,
@@ -1158,25 +1218,10 @@ _gtk_source_completion_list_box_set_context (GtkSourceCompletionListBox *self,
g_return_if_fail (GTK_SOURCE_IS_COMPLETION_LIST_BOX (self));
g_return_if_fail (!context || GTK_SOURCE_IS_COMPLETION_CONTEXT (context));
- if (self->context == context)
- return;
-
- if (self->context != NULL && self->items_changed_handler != 0)
- {
- g_signal_handler_disconnect (self->context, self->items_changed_handler);
- self->items_changed_handler = 0;
- }
-
- g_set_object (&self->context, context);
-
- if (self->context != NULL)
+ if (g_set_object (&self->context, context))
{
- self->items_changed_handler =
- g_signal_connect_object (self->context,
- "items-changed",
- G_CALLBACK (gtk_source_completion_list_box_items_changed_cb),
- self,
- G_CONNECT_SWAPPED);
+ gtk_source_list_snapshot_set_model (self->list_snapshot,
+ G_LIST_MODEL (context));
}
gtk_source_completion_list_box_set_selected (self, -1);
@@ -1340,3 +1385,32 @@ _gtk_source_completion_list_box_set_show_icons (GtkSourceCompletionListBox *self
gtk_source_completion_list_box_queue_update (self);
}
+
+static void
+gtk_source_completion_list_box_after_update_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock)
+{
+ GtkSourceCompletionListBox *self = (GtkSourceCompletionListBox *)widget;
+ double lower;
+ double page_size;
+
+ g_assert (GTK_SOURCE_IS_COMPLETION_LIST_BOX (self));
+ g_assert (GDK_IS_FRAME_CLOCK (frame_clock));
+
+ lower = gtk_adjustment_get_lower (self->vadjustment);
+ page_size = gtk_adjustment_get_page_size (self->vadjustment);
+
+ gtk_source_list_snapshot_hold (self->list_snapshot, lower, lower + page_size);
+}
+
+static void
+gtk_source_completion_list_box_after_paint_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock)
+{
+ GtkSourceCompletionListBox *self = (GtkSourceCompletionListBox *)widget;
+
+ g_assert (GTK_SOURCE_IS_COMPLETION_LIST_BOX (self));
+ g_assert (GDK_IS_FRAME_CLOCK (frame_clock));
+
+ gtk_source_list_snapshot_release (self->list_snapshot);
+}
diff --git a/gtksourceview/gtksourcelistsnapshot-private.h b/gtksourceview/gtksourcelistsnapshot-private.h
new file mode 100644
index 00000000..456f2592
--- /dev/null
+++ b/gtksourceview/gtksourcelistsnapshot-private.h
@@ -0,0 +1,40 @@
+/* gtksourcelistsnapshot-private.h
+ *
+ * Copyright 2022 Christian Hergert <chergert@redhat.com>
+ *
+ * This library 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.
+ *
+ * This library 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 General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_LIST_SNAPSHOT (gtk_source_list_snapshot_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceListSnapshot, gtk_source_list_snapshot, GTK_SOURCE, LIST_SNAPSHOT, GObject)
+
+GtkSourceListSnapshot *gtk_source_list_snapshot_new (void);
+GListModel *gtk_source_list_snapshot_get_model (GtkSourceListSnapshot *self);
+void gtk_source_list_snapshot_set_model (GtkSourceListSnapshot *self,
+ GListModel *model);
+void gtk_source_list_snapshot_hold (GtkSourceListSnapshot *self,
+ guint position,
+ guint length);
+void gtk_source_list_snapshot_release (GtkSourceListSnapshot *self);
+
+G_END_DECLS
diff --git a/gtksourceview/gtksourcelistsnapshot.c b/gtksourceview/gtksourcelistsnapshot.c
new file mode 100644
index 00000000..733d720a
--- /dev/null
+++ b/gtksourceview/gtksourcelistsnapshot.c
@@ -0,0 +1,406 @@
+/* gtksourcelistsnapshot.c
+ *
+ * Copyright 2022 Christian Hergert <chergert@redhat.com>
+ *
+ * This library 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.
+ *
+ * This library 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 General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include <gtk/gtk.h>
+
+#include "gtksourcelistsnapshot-private.h"
+
+/*
+ * GtkSourceListSnapshot:
+ *
+ * This classes purpose is to allow snapshoting a range of a GListModel
+ * and ensuring that no changes to the underlying model will cause that
+ * range to invalidate.
+ *
+ * The "hold" be done at the point where you want to avoid any model
+ * changes causing the widgetry to invalidate, and released after you've
+ * completed your snapshot work.
+ *
+ * If :model changes, or ::items-changed on the current model is emitted,
+ * that will be supressed until the hold is released. Objects for the
+ * hold range are retained so they may be returned from
+ * g_list_model_get_item().
+ */
+
+struct _GtkSourceListSnapshot
+{
+ GObject parent_instance;
+ GListModel *model;
+ GSignalGroup *signal_group;
+ GPtrArray *held_items;
+ guint held_position;
+ guint held_n_items;
+ guint real_n_items;
+ guint invalid : 1;
+};
+
+enum {
+ PROP_0,
+ PROP_MODEL,
+ N_PROPS
+};
+
+static GType
+gtk_source_list_snapshot_get_item_type (GListModel *model)
+{
+ GtkSourceListSnapshot *self = GTK_SOURCE_LIST_SNAPSHOT (model);
+
+ if (self->model != NULL)
+ {
+ return g_list_model_get_item_type (self->model);
+ }
+
+ return G_TYPE_OBJECT;
+}
+
+static guint
+gtk_source_list_snapshot_get_n_items (GListModel *model)
+{
+ GtkSourceListSnapshot *self = GTK_SOURCE_LIST_SNAPSHOT (model);
+
+ if (self->held_position == GTK_INVALID_LIST_POSITION)
+ {
+ return self->real_n_items;
+ }
+
+ return self->held_n_items;
+}
+
+static gpointer
+gtk_source_list_snapshot_get_item (GListModel *model,
+ guint position)
+{
+ GtkSourceListSnapshot *self = GTK_SOURCE_LIST_SNAPSHOT (model);
+
+ if (self->held_position == GTK_INVALID_LIST_POSITION)
+ {
+ if (self->model == NULL)
+ {
+ return NULL;
+ }
+
+ return g_list_model_get_item (self->model, position);
+ }
+
+ if (position >= self->held_position &&
+ position < self->held_position + self->held_items->len)
+ {
+ return g_object_ref (g_ptr_array_index (self->held_items, position - self->held_position));
+ }
+
+ return NULL;
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+ iface->get_item_type = gtk_source_list_snapshot_get_item_type;
+ iface->get_n_items = gtk_source_list_snapshot_get_n_items;
+ iface->get_item = gtk_source_list_snapshot_get_item;
+}
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (GtkSourceListSnapshot, gtk_source_list_snapshot, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gtk_source_list_snapshot_bind_cb (GtkSourceListSnapshot *self,
+ GListModel *model,
+ GSignalGroup *signal_group)
+{
+ guint old_n_items;
+ guint new_n_items;
+
+ g_assert (GTK_SOURCE_IS_LIST_SNAPSHOT (self));
+ g_assert (G_IS_LIST_MODEL (model));
+ g_assert (G_IS_SIGNAL_GROUP (signal_group));
+
+ old_n_items = self->real_n_items;
+ new_n_items = g_list_model_get_n_items (model);
+
+ if (self->held_position == GTK_INVALID_LIST_POSITION)
+ {
+ if (old_n_items || new_n_items)
+ {
+ g_list_model_items_changed (G_LIST_MODEL (self), 0, old_n_items, new_n_items);
+ }
+ }
+ else
+ {
+ self->invalid = TRUE;
+ }
+
+ self->real_n_items = new_n_items;
+}
+
+static void
+gtk_source_list_snapshot_unbind_cb (GtkSourceListSnapshot *self,
+ GSignalGroup *signal_group)
+{
+ guint old_n_items;
+ guint new_n_items;
+
+ g_assert (GTK_SOURCE_IS_LIST_SNAPSHOT (self));
+ g_assert (G_IS_SIGNAL_GROUP (signal_group));
+
+ old_n_items = self->real_n_items;
+ new_n_items = 0;
+
+ self->real_n_items = new_n_items;
+
+ if (self->held_position == GTK_INVALID_LIST_POSITION)
+ {
+ if (old_n_items || new_n_items)
+ {
+ g_list_model_items_changed (G_LIST_MODEL (self), 0, old_n_items, new_n_items);
+ }
+ }
+ else
+ {
+ self->invalid = TRUE;
+ }
+}
+
+static void
+gtk_source_list_snapshot_items_changed_cb (GtkSourceListSnapshot *self,
+ guint position,
+ guint removed,
+ guint added,
+ GListModel *model)
+{
+ g_assert (GTK_SOURCE_IS_LIST_SNAPSHOT (self));
+ g_assert (G_IS_LIST_MODEL (model));
+
+ self->real_n_items -= removed;
+ self->real_n_items += added;
+
+ if (self->held_position == GTK_INVALID_LIST_POSITION)
+ {
+ if (removed || added)
+ {
+ g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added);
+ }
+ }
+ else
+ {
+ self->invalid = TRUE;
+ }
+}
+
+static void
+gtk_source_list_snapshot_dispose (GObject *object)
+{
+ GtkSourceListSnapshot *self = (GtkSourceListSnapshot *)object;
+
+ g_clear_pointer (&self->held_items, g_ptr_array_unref);
+
+ if (self->signal_group != NULL)
+ {
+ g_signal_group_set_target (self->signal_group, NULL);
+ g_clear_object (&self->signal_group);
+ }
+
+ g_clear_object (&self->model);
+
+ G_OBJECT_CLASS (gtk_source_list_snapshot_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_list_snapshot_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtkSourceListSnapshot *self = GTK_SOURCE_LIST_SNAPSHOT (object);
+
+ switch (prop_id)
+ {
+ case PROP_MODEL:
+ g_value_set_object (value, self->model);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtk_source_list_snapshot_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtkSourceListSnapshot *self = GTK_SOURCE_LIST_SNAPSHOT (object);
+
+ switch (prop_id)
+ {
+ case PROP_MODEL:
+ gtk_source_list_snapshot_set_model (self, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtk_source_list_snapshot_class_init (GtkSourceListSnapshotClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = gtk_source_list_snapshot_dispose;
+ object_class->get_property = gtk_source_list_snapshot_get_property;
+ object_class->set_property = gtk_source_list_snapshot_set_property;
+
+ properties [PROP_MODEL] =
+ g_param_spec_object ("model", NULL, NULL,
+ G_TYPE_LIST_MODEL,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gtk_source_list_snapshot_init (GtkSourceListSnapshot *self)
+{
+ self->signal_group = g_signal_group_new (G_TYPE_LIST_MODEL);
+ self->held_items = g_ptr_array_new_with_free_func (g_object_unref);
+ self->held_position = GTK_INVALID_LIST_POSITION;
+
+ g_signal_connect_object (self->signal_group,
+ "bind",
+ G_CALLBACK (gtk_source_list_snapshot_bind_cb),
+ self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->signal_group,
+ "unbind",
+ G_CALLBACK (gtk_source_list_snapshot_unbind_cb),
+ self,
+ G_CONNECT_SWAPPED);
+ g_signal_group_connect_object (self->signal_group,
+ "items-changed",
+ G_CALLBACK (gtk_source_list_snapshot_items_changed_cb),
+ self,
+ G_CONNECT_SWAPPED);
+}
+
+GtkSourceListSnapshot *
+gtk_source_list_snapshot_new (void)
+{
+ return g_object_new (GTK_SOURCE_TYPE_LIST_SNAPSHOT, NULL);
+}
+
+/**
+ * gtk_source_list_snapshot_get_model:
+ * @self: a #GtkSourceListSnapshot
+ *
+ * Gets the underlying model, if any.
+ *
+ * Returns: (transfer none) (nullable): a #GListModel or %NULL
+ */
+GListModel *
+gtk_source_list_snapshot_get_model (GtkSourceListSnapshot *self)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_LIST_SNAPSHOT (self), NULL);
+
+ return self->model;
+}
+
+void
+gtk_source_list_snapshot_set_model (GtkSourceListSnapshot *self,
+ GListModel *model)
+{
+ g_return_if_fail (GTK_SOURCE_IS_LIST_SNAPSHOT (self));
+ g_return_if_fail (!model || G_IS_LIST_MODEL (model));
+
+ if (g_set_object (&self->model, model))
+ {
+ g_signal_group_set_target (self->signal_group, model);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]);
+ }
+}
+
+void
+gtk_source_list_snapshot_hold (GtkSourceListSnapshot *self,
+ guint position,
+ guint length)
+{
+ g_return_if_fail (GTK_SOURCE_IS_LIST_SNAPSHOT (self));
+ g_return_if_fail (self->held_position == GTK_INVALID_LIST_POSITION);
+ g_return_if_fail (self->held_items != NULL);
+ g_return_if_fail (self->held_items->len == 0);
+ g_return_if_fail (self->held_n_items == 0);
+
+ self->held_position = position;
+
+ if (self->model != NULL)
+ {
+ self->held_n_items = g_list_model_get_n_items (self->model);
+ }
+
+ if (position > self->held_n_items)
+ {
+ position = self->held_n_items;
+ }
+
+ if (position + length > self->held_n_items)
+ {
+ length = self->held_n_items - position;
+ }
+
+ for (guint i = 0; i < length; i++)
+ {
+ g_ptr_array_add (self->held_items,
+ g_list_model_get_item (self->model, position + i));
+ }
+}
+
+void
+gtk_source_list_snapshot_release (GtkSourceListSnapshot *self)
+{
+ gboolean was_invalid;
+ guint old_n_items;
+ guint new_n_items;
+
+ g_return_if_fail (GTK_SOURCE_IS_LIST_SNAPSHOT (self));
+ g_return_if_fail (self->held_position != GTK_INVALID_LIST_POSITION);
+ g_return_if_fail (self->held_items != NULL);
+
+ was_invalid = self->invalid;
+ old_n_items = self->held_n_items;
+ new_n_items = self->model ? g_list_model_get_n_items (self->model) : 0;
+
+ self->invalid = FALSE;
+ self->held_n_items = 0;
+ self->held_position = GTK_INVALID_LIST_POSITION;
+
+ if (self->held_items->len > 0)
+ {
+ g_ptr_array_remove_range (self->held_items, 0, self->held_items->len);
+ }
+
+ if (was_invalid)
+ {
+ g_list_model_items_changed (G_LIST_MODEL (self), 0, old_n_items, new_n_items);
+ }
+}
diff --git a/gtksourceview/meson.build b/gtksourceview/meson.build
index 5ba8f20e..37a660f8 100644
--- a/gtksourceview/meson.build
+++ b/gtksourceview/meson.build
@@ -128,6 +128,7 @@ core_private_c = files([
'gtksourceinformative.c',
'gtksourceiter.c',
'gtksourcelanguage-parser-2.c',
+ 'gtksourcelistsnapshot.c',
'gtksourcemarkssequence.c',
'gtksourcepixbufhelper.c',
'gtksourceregex.c',
diff --git a/testsuite/meson.build b/testsuite/meson.build
index 8195b14d..adbe140b 100644
--- a/testsuite/meson.build
+++ b/testsuite/meson.build
@@ -31,6 +31,7 @@ testsuite_sources = [
['test-language'],
['test-languagemanager'],
['test-language-specs', false],
+ ['test-listsnapshot'],
['test-mark'],
['test-printcompositor'],
['test-regex'],
diff --git a/testsuite/test-listsnapshot.c b/testsuite/test-listsnapshot.c
new file mode 100644
index 00000000..3edeaf38
--- /dev/null
+++ b/testsuite/test-listsnapshot.c
@@ -0,0 +1,160 @@
+/* test-listsnapshot.c
+ *
+ * Copyright 2022 Christian Hergert <chergert@redhat.com>
+ *
+ * This library 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.
+ *
+ * This library 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 General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include <gtksourceview/gtksourcelistsnapshot-private.h>
+
+typedef struct
+{
+ guint call_count;
+ guint position;
+ guint removed;
+ guint added;
+} ItemsChanged;
+
+static void
+items_changed_cb (GListModel *model,
+ guint position,
+ guint removed,
+ guint added,
+ ItemsChanged *state)
+{
+#if 0
+ g_printerr ("%d %d %d\n", position, removed, added);
+#endif
+
+ state->call_count++;
+ state->position = position;
+ state->removed = removed;
+ state->added = added;
+}
+
+static void
+test_list_snapshot (void)
+{
+ GtkSourceListSnapshot *list_snapshot;
+ GListStore *store;
+ GMenu *menu;
+ ItemsChanged state = {0};
+
+ list_snapshot = gtk_source_list_snapshot_new ();
+ g_signal_connect (list_snapshot, "items-changed", G_CALLBACK (items_changed_cb), &state);
+ g_assert_cmpint (0, ==, g_list_model_get_n_items (G_LIST_MODEL (list_snapshot)));
+ g_assert_cmpint (G_TYPE_OBJECT, ==, g_list_model_get_item_type (G_LIST_MODEL (list_snapshot)));
+
+ store = g_list_store_new (G_TYPE_MENU_MODEL);
+
+ menu = g_menu_new ();
+ g_list_store_append (store, menu);
+
+ /* initial model set (with items) */
+ gtk_source_list_snapshot_set_model (list_snapshot, G_LIST_MODEL (store));
+ g_assert_cmpint (1, ==, g_list_model_get_n_items (G_LIST_MODEL (list_snapshot)));
+ g_assert_cmpint (G_TYPE_MENU_MODEL, ==, g_list_model_get_item_type (G_LIST_MODEL (list_snapshot)));
+ g_assert_cmpint (state.call_count, ==, 1);
+ g_assert_cmpint (state.position, ==, 0);
+ g_assert_cmpint (state.removed, ==, 0);
+ g_assert_cmpint (state.added, ==, 1);
+
+ /* try to reset, no changes/emission */
+ gtk_source_list_snapshot_set_model (list_snapshot, G_LIST_MODEL (store));
+ g_assert_cmpint (1, ==, g_list_model_get_n_items (G_LIST_MODEL (list_snapshot)));
+ g_assert_cmpint (G_TYPE_MENU_MODEL, ==, g_list_model_get_item_type (G_LIST_MODEL (list_snapshot)));
+ g_assert_cmpint (state.call_count, ==, 1);
+
+ /* clear model */
+ gtk_source_list_snapshot_set_model (list_snapshot, NULL);
+ g_assert_cmpint (0, ==, g_list_model_get_n_items (G_LIST_MODEL (list_snapshot)));
+ g_assert_cmpint (G_TYPE_OBJECT, ==, g_list_model_get_item_type (G_LIST_MODEL (list_snapshot)));
+ g_assert_cmpint (state.call_count, ==, 2);
+ g_assert_cmpint (state.position, ==, 0);
+ g_assert_cmpint (state.removed, ==, 1);
+ g_assert_cmpint (state.added, ==, 0);
+
+ /* set model again */
+ gtk_source_list_snapshot_set_model (list_snapshot, G_LIST_MODEL (store));
+ g_assert_cmpint (1, ==, g_list_model_get_n_items (G_LIST_MODEL (list_snapshot)));
+ g_assert_cmpint (G_TYPE_MENU_MODEL, ==, g_list_model_get_item_type (G_LIST_MODEL (list_snapshot)));
+ g_assert_cmpint (state.call_count, ==, 3);
+ g_assert_cmpint (state.position, ==, 0);
+ g_assert_cmpint (state.removed, ==, 0);
+ g_assert_cmpint (state.added, ==, 1);
+
+ /* Add some more items so we can hold a range */
+ for (guint i = 0; i < 100; i++)
+ {
+ GMenu *m = g_menu_new ();
+ state.call_count = 0;
+ g_list_store_append (store, m);
+ g_assert_cmpint (state.call_count, ==, 1);
+ g_assert_cmpint (state.position, ==, 1+i);
+ g_assert_cmpint (state.removed, ==, 0);
+ g_assert_cmpint (state.added, ==, 1);
+ g_object_unref (m);
+ }
+ g_assert_cmpint (101, ==, g_list_model_get_n_items (G_LIST_MODEL (store)));
+ g_assert_cmpint (101, ==, g_list_model_get_n_items (G_LIST_MODEL (list_snapshot)));
+
+ /* Hold a range so we can test changing things arround */
+ gtk_source_list_snapshot_hold (list_snapshot, 10, 20);
+ g_assert_cmpint (101, ==, g_list_model_get_n_items (G_LIST_MODEL (list_snapshot)));
+ for (guint i = 0; i <= 100; i++)
+ {
+ GMenu *item = g_list_model_get_item (G_LIST_MODEL (list_snapshot), i);
+
+ if (i < 10 || i >= 30)
+ {
+ g_assert_null (item);
+ }
+ else
+ {
+ g_assert_nonnull (item);
+ g_assert_true (G_IS_MENU (item));
+ g_object_unref (item);
+ }
+ }
+
+ /* Now remove everything and ensure we're still good */
+ state.call_count = 0;
+ while (g_list_model_get_n_items (G_LIST_MODEL (store)))
+ {
+ g_list_store_remove (store, 0);
+ g_assert_cmpint (state.call_count, ==, 0);
+ }
+
+ /* Now release our hold */
+ gtk_source_list_snapshot_release (list_snapshot);
+ g_assert_cmpint (state.call_count, ==, 1);
+ g_assert_cmpint (state.position, ==, 0);
+ g_assert_cmpint (state.removed, ==, 101);
+ g_assert_cmpint (state.added, ==, 0);
+
+ g_assert_finalize_object (list_snapshot);
+ g_assert_finalize_object (store);
+ g_assert_finalize_object (menu);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ g_test_init (&argc, &argv, NULL);
+ g_test_add_func ("/GtkSource/ListSnapshot/basic", test_list_snapshot);
+ return g_test_run ();
+}