[gtksourceview/wip/chergert/snippets] snippet: implement basic snippet system



commit 0976d130daa7811418b352f5e5b3f00e4032cd4d
Author: Christian Hergert <chergert redhat com>
Date:   Fri Jan 17 16:12:00 2020 -0800

    snippet: implement basic snippet system
    
    This is a straightforward port of Builder's snippet system to
    GtkSourceView. A number of new objects are added as part of this
    commit to the GtkSourceView ABI.
    
     - GtkSourceSnippet provides an object representing a snippet to be
       inserted into a textview. Snippets are associated with a textview
       rather than a buffer because of incremental state that is necessary
       to interact with widgetry and event controllers. Additionally, it
       doesn't make sense to have this attached to the buffer when the
       view area could be different.
    
     - GtkSourceSnippetChunk is a single chunk of a snippet. A snippet contains
       zero or more chunks. A chunk can have a spec (which can be evaluated
       using variables) or text set (such as after typing in the editor). Also,
       a chunk can have a "focus-position" which allows the user to tab
       through the chunks of the snippet.
    
     - GtkSourceSnippetContext provides state which can be expanded as
       part of the snippet. This is useful to expand variables set by the
       application or filters to transform input text or other variables.
    
    The style scheme has been adjusted to give us access to a focus position
    tag so that they are highlighted to the user. Style schemes bundled with
    GtkSourceView will want to implement this in a future commit.
    
    Tabbing will expand the snippet based on the current word. However, some
    applications may want to use a completion provider to show the user
    more information about the snippet. A completion provider will be provided
    in a forthcoming commit.
    
    Applications can insert snippets using gtk_source_view_push_snippet().

 docs/reference/meson.build                      |    2 +
 gtksourceview/gtksource.h                       |    3 +
 gtksourceview/gtksourcebuffer-private.h         |    2 +
 gtksourceview/gtksourcebuffer.c                 |   46 +
 gtksourceview/gtksourcemarshalers.list          |    1 +
 gtksourceview/gtksourcesnippet-private.h        |   75 ++
 gtksourceview/gtksourcesnippet.c                | 1326 +++++++++++++++++++++++
 gtksourceview/gtksourcesnippet.h                |   70 ++
 gtksourceview/gtksourcesnippetchunk.c           |  402 +++++++
 gtksourceview/gtksourcesnippetchunk.h           |   67 ++
 gtksourceview/gtksourcesnippetcontext-private.h |   29 +
 gtksourceview/gtksourcesnippetcontext.c         |  911 ++++++++++++++++
 gtksourceview/gtksourcesnippetcontext.h         |   65 ++
 gtksourceview/gtksourcestylescheme-private.h    |    2 +
 gtksourceview/gtksourcestylescheme.c            |    9 +
 gtksourceview/gtksourcetypes.h                  |    3 +
 gtksourceview/gtksourceview.c                   |  371 ++++++-
 gtksourceview/gtksourceview.h                   |    9 +
 gtksourceview/meson.build                       |    6 +
 19 files changed, 3386 insertions(+), 13 deletions(-)
---
diff --git a/docs/reference/meson.build b/docs/reference/meson.build
index 622a4de8..7fd35b46 100644
--- a/docs/reference/meson.build
+++ b/docs/reference/meson.build
@@ -35,6 +35,8 @@ reference_private_h = [
   'gtksourcepixbufhelper-private.h',
   'gtksourceregex-private.h',
   'gtksourcesearchcontext-private.h',
+  'gtksourcesnippet-private.h',
+  'gtksourcesnippetcontext-private.h',
   'gtksourcestyle-private.h',
   'gtksourcestylescheme-private.h',
   'gtksourcestyleschememanager-private.h',
diff --git a/gtksourceview/gtksource.h b/gtksourceview/gtksource.h
index 517c06ae..bea7c201 100644
--- a/gtksourceview/gtksource.h
+++ b/gtksourceview/gtksource.h
@@ -47,6 +47,9 @@
 #include "gtksourceregion.h"
 #include "gtksourcesearchcontext.h"
 #include "gtksourcesearchsettings.h"
+#include "gtksourcesnippet.h"
+#include "gtksourcesnippetchunk.h"
+#include "gtksourcesnippetcontext.h"
 #include "gtksourcespacedrawer.h"
 #include "gtksourcestyle.h"
 #include "gtksourcestylescheme.h"
diff --git a/gtksourceview/gtksourcebuffer-private.h b/gtksourceview/gtksourcebuffer-private.h
index 9d047ab8..4a0765f0 100644
--- a/gtksourceview/gtksourcebuffer-private.h
+++ b/gtksourceview/gtksourcebuffer-private.h
@@ -69,5 +69,7 @@ GTK_SOURCE_INTERNAL
 gboolean                  _gtk_source_buffer_has_source_marks            (GtkSourceBuffer        *buffer);
 GTK_SOURCE_INTERNAL
 gboolean                  _gtk_source_buffer_has_spaces_tag              (GtkSourceBuffer        *buffer);
+GTK_SOURCE_INTERNAL
+GtkTextTag               *_gtk_source_buffer_get_snippet_focus_tag       (GtkSourceBuffer        *buffer);
 
 G_END_DECLS
diff --git a/gtksourceview/gtksourcebuffer.c b/gtksourceview/gtksourcebuffer.c
index 6bcd66ee..c025738e 100644
--- a/gtksourceview/gtksourcebuffer.c
+++ b/gtksourceview/gtksourcebuffer.c
@@ -172,6 +172,8 @@ typedef struct
        GtkTextMark *tmp_insert_mark;
        GtkTextMark *tmp_selection_bound_mark;
 
+       GtkTextTag *snippet_focus_tag;
+
        GList *search_contexts;
 
        GtkTextTag *invalid_char_tag;
@@ -256,10 +258,18 @@ gtk_source_buffer_tag_added_cb (GtkTextTagTable *table,
                                 GtkTextTag      *tag,
                                 GtkSourceBuffer *buffer)
 {
+       GtkSourceBufferPrivate *priv = gtk_source_buffer_get_instance_private (buffer);
+
        if (GTK_SOURCE_IS_TAG (tag))
        {
                gtk_source_buffer_check_tag_for_spaces (buffer, GTK_SOURCE_TAG (tag));
        }
+
+       if (priv->snippet_focus_tag != NULL)
+       {
+               gtk_text_tag_set_priority (priv->snippet_focus_tag,
+                                          gtk_text_tag_table_get_size (table) - 1);
+       }
 }
 
 static void
@@ -654,6 +664,42 @@ gtk_source_buffer_new_with_language (GtkSourceLanguage *language)
                             NULL);
 }
 
+static void
+update_snippet_focus_style (GtkSourceBuffer *buffer)
+{
+       GtkSourceBufferPrivate *priv = gtk_source_buffer_get_instance_private (buffer);
+       GtkSourceStyle *style = NULL;
+
+       if (priv->snippet_focus_tag == NULL)
+       {
+               return;
+       }
+
+       if (priv->style_scheme != NULL)
+       {
+               style = _gtk_source_style_scheme_get_snippet_focus_style (priv->style_scheme);
+       }
+
+       gtk_source_style_apply (style, priv->snippet_focus_tag);
+}
+
+GtkTextTag *
+_gtk_source_buffer_get_snippet_focus_tag (GtkSourceBuffer *buffer)
+{
+       GtkSourceBufferPrivate *priv = gtk_source_buffer_get_instance_private (buffer);
+
+       if (priv->snippet_focus_tag == NULL)
+       {
+               priv->snippet_focus_tag =
+                       gtk_text_buffer_create_tag (GTK_TEXT_BUFFER (buffer),
+                                                   NULL,
+                                                   NULL);
+               update_snippet_focus_style (buffer);
+       }
+
+       return priv->snippet_focus_tag;
+}
+
 static void
 update_bracket_match_style (GtkSourceBuffer *buffer)
 {
diff --git a/gtksourceview/gtksourcemarshalers.list b/gtksourceview/gtksourcemarshalers.list
index 2236f2b8..98df9a87 100644
--- a/gtksourceview/gtksourcemarshalers.list
+++ b/gtksourceview/gtksourcemarshalers.list
@@ -10,4 +10,5 @@ VOID:BOXED,BOXED,UINT,FLAGS,INT
 VOID:BOXED,ENUM
 VOID:BOXED,INT
 VOID:ENUM,INT
+VOID:OBJECT,BOXED
 VOID:OBJECT,UINT
diff --git a/gtksourceview/gtksourcesnippet-private.h b/gtksourceview/gtksourcesnippet-private.h
new file mode 100644
index 00000000..19f3981e
--- /dev/null
+++ b/gtksourceview/gtksourcesnippet-private.h
@@ -0,0 +1,75 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2020 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/>.
+ */
+
+ #pragma once
+
+ #include "gtksourcesnippet.h"
+
+ G_BEGIN_DECLS
+
+G_GNUC_INTERNAL
+void         _gtk_source_snippet_replace_current_chunk_text (GtkSourceSnippet *self,
+                                                             const gchar      *new_text);
+G_GNUC_INTERNAL
+gchar       *_gtk_source_snippet_get_edited_text            (GtkSourceSnippet *self);
+G_GNUC_INTERNAL
+void         _gtk_source_snippet_pause                      (GtkSourceSnippet *self);
+G_GNUC_INTERNAL
+void         _gtk_source_snippet_unpause                    (GtkSourceSnippet *self);
+G_GNUC_INTERNAL
+gboolean     _gtk_source_snippet_begin                      (GtkSourceSnippet *self,
+                                                             GtkTextBuffer    *buffer,
+                                                             GtkTextIter      *iter);
+G_GNUC_INTERNAL
+void         _gtk_source_snippet_finish                     (GtkSourceSnippet *self);
+G_GNUC_INTERNAL
+gboolean     _gtk_source_snippet_move_next                  (GtkSourceSnippet *self);
+G_GNUC_INTERNAL
+gboolean     _gtk_source_snippet_move_previous              (GtkSourceSnippet *self);
+G_GNUC_INTERNAL
+void         _gtk_source_snippet_before_insert_text         (GtkSourceSnippet *self,
+                                                             GtkTextBuffer    *buffer,
+                                                             GtkTextIter      *iter,
+                                                             gchar            *text,
+                                                             gint              len);
+G_GNUC_INTERNAL
+void         _gtk_source_snippet_after_insert_text          (GtkSourceSnippet *self,
+                                                             GtkTextBuffer    *buffer,
+                                                             GtkTextIter      *iter,
+                                                             gchar            *text,
+                                                             gint              len);
+G_GNUC_INTERNAL
+void         _gtk_source_snippet_before_delete_range        (GtkSourceSnippet *self,
+                                                             GtkTextBuffer    *buffer,
+                                                             GtkTextIter      *begin,
+                                                             GtkTextIter      *end);
+G_GNUC_INTERNAL
+void         _gtk_source_snippet_after_delete_range         (GtkSourceSnippet *self,
+                                                             GtkTextBuffer    *buffer,
+                                                             GtkTextIter      *begin,
+                                                             GtkTextIter      *end);
+G_GNUC_INTERNAL
+gboolean     _gtk_source_snippet_insert_set                 (GtkSourceSnippet *self,
+                                                             GtkTextMark      *mark);
+G_GNUC_INTERNAL
+GtkTextMark *_gtk_source_snippet_get_mark_begin             (GtkSourceSnippet *self);
+G_GNUC_INTERNAL
+GtkTextMark *_gtk_source_snippet_get_mark_end               (GtkSourceSnippet *self);
+
+G_END_DECLS
diff --git a/gtksourceview/gtksourcesnippet.c b/gtksourceview/gtksourcesnippet.c
new file mode 100644
index 00000000..14edd854
--- /dev/null
+++ b/gtksourceview/gtksourcesnippet.c
@@ -0,0 +1,1326 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2020 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/>.
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "gtksourcebuffer-private.h"
+#include "gtksourcesnippet-private.h"
+#include "gtksourcesnippetchunk.h"
+#include "gtksourcesnippetcontext.h"
+#include "gtksourcesnippetcontext-private.h"
+
+struct _GtkSourceSnippet
+{
+       GObject                  parent_instance;
+
+       GtkSourceSnippetContext *context;
+       GtkTextBuffer           *buffer;
+       GPtrArray               *chunks;
+       GArray                  *runs;
+
+       GtkTextMark             *mark_begin;
+       GtkTextMark             *mark_end;
+       gchar                   *trigger;
+       const gchar             *language_id;
+       gchar                   *description;
+
+       gint                     focus_position;
+       gint                     max_focus_position;
+       gint                     current_chunk;
+
+       guint                    inserted : 1;
+};
+
+G_DEFINE_TYPE (GtkSourceSnippet, gtk_source_snippet, G_TYPE_OBJECT)
+
+enum {
+       PROP_0,
+       PROP_BUFFER,
+       PROP_DESCRIPTION,
+       PROP_LANGUAGE_ID,
+       PROP_MARK_BEGIN,
+       PROP_MARK_END,
+       PROP_POSITION,
+       PROP_TRIGGER,
+       N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * gtk_source_snippet_new:
+ * @trigger: (nullable): the trigger word
+ * @language_id: (nullable): the source language
+ *
+ * Creates a new #GtkSourceSnippet
+ *
+ * Returns: (transfer full): A new #GtkSourceSnippet
+ *
+ * Since: 5.0
+ */
+GtkSourceSnippet *
+gtk_source_snippet_new (const gchar *trigger,
+                        const gchar *language_id)
+{
+       g_return_val_if_fail (trigger != NULL, NULL);
+
+       return g_object_new (GTK_SOURCE_TYPE_SNIPPET,
+                            "trigger", trigger,
+                            "language-id", language_id,
+                            NULL);
+}
+
+/**
+ * gtk_source_snippet_copy:
+ * @self: a #GtkSourceSnippet
+ *
+ * Does a deep copy of the snippet.
+ *
+ * Returns: (transfer full): A new #GtkSourceSnippet
+ *
+ * Since: 5.0
+ */
+GtkSourceSnippet *
+gtk_source_snippet_copy (GtkSourceSnippet *self)
+{
+       GtkSourceSnippet *ret;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), NULL);
+
+       ret = g_object_new (GTK_SOURCE_TYPE_SNIPPET,
+                           "trigger", self->trigger,
+                           "language-id", self->language_id,
+                           "description", self->description,
+                           NULL);
+
+       for (guint i = 0; i < self->chunks->len; i++)
+       {
+               GtkSourceSnippetChunk *old_chunk;
+               GtkSourceSnippetChunk *new_chunk;
+
+               old_chunk = g_ptr_array_index (self->chunks, i);
+               new_chunk = gtk_source_snippet_chunk_copy (old_chunk);
+               gtk_source_snippet_add_chunk (ret, new_chunk);
+               g_object_unref (new_chunk);
+       }
+
+       return g_steal_pointer (&ret);
+}
+
+/**
+ * gtk_source_snippet_get_focus_position:
+ * @self: a #GtkSourceSnippet
+ *
+ * Gets the current focus for the snippet. This is changed
+ * as the user tabs through focus locations.
+ *
+ * Returns: The focus position, or -1 if unset.
+ *
+ * Since: 5.0
+ */
+gint
+gtk_source_snippet_get_focus_position (GtkSourceSnippet *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), -1);
+
+       return self->focus_position;
+}
+
+/**
+ * gtk_source_snippet_get_n_chunks:
+ * @self: a #GtkSourceSnippet
+ *
+ * Gets the number of chunks in the snippet.
+ *
+ * Note that not all chunks are editable.
+ *
+ * Returns: The number of chunks.
+ *
+ * Since: 5.0
+ */
+guint
+gtk_source_snippet_get_n_chunks (GtkSourceSnippet *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), 0);
+
+       return self->chunks->len;
+}
+
+/**
+ * gtk_source_snippet_get_nth_chunk:
+ * @self: a #GtkSourceSnippet
+ * @nth: the nth chunk to get
+ *
+ * Gets the chunk at @nth.
+ *
+ * Returns: (transfer none): an #GtkSourceSnippetChunk
+ *
+ * Since: 5.0
+ */
+GtkSourceSnippetChunk *
+gtk_source_snippet_get_nth_chunk (GtkSourceSnippet *self,
+                           guint             nth)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), 0);
+
+       if (nth < self->chunks->len)
+               return g_ptr_array_index (self->chunks, nth);
+
+       return NULL;
+}
+
+/**
+ * gtk_source_snippet_get_trigger:
+ * @self: a #GtkSourceSnippet
+ *
+ * Gets the trigger for the source snippet. A trigger is
+ * a word that can be expanded into the full snippet when
+ * the user presses Tab.
+ *
+ * Returns: (nullable): A string or %NULL
+ *
+ * Since: 5.0
+ */
+const gchar *
+gtk_source_snippet_get_trigger (GtkSourceSnippet *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), NULL);
+
+       return self->trigger;
+}
+
+/**
+ * gtk_source_snippet_set_trigger:
+ * @self: a #GtkSourceSnippet
+ * @trigger: the trigger word
+ *
+ * Sets the trigger for the snippet.
+ *
+ * Since: 5.0
+ */
+void
+gtk_source_snippet_set_trigger (GtkSourceSnippet *self,
+                                const gchar      *trigger)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+
+       if (g_strcmp0 (trigger, self->trigger) != 0)
+       {
+               g_free (self->trigger);
+               self->trigger = g_strdup (trigger);
+               g_object_notify_by_pspec (G_OBJECT (self),
+                                         properties [PROP_TRIGGER]);
+       }
+}
+
+/**
+ * gtk_source_snippet_get_language_id:
+ * @self: a #GtkSourceSnippet
+ *
+ * Gets the language-id used for the source snippet.
+ *
+ * The language identifier should be one that matches a
+ * source language #GtkSourceLanguage:id property.
+ *
+ * Returns: the language identifier
+ *
+ * Since: 5.0
+ */
+const gchar *
+gtk_source_snippet_get_language_id (GtkSourceSnippet *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), NULL);
+
+       return self->language_id;
+}
+
+/**
+ * gtk_source_snippet_set_language_id:
+ * @self: a #GtkSourceSnippet
+ * @language_id: the language identifier for the snippet
+ *
+ * Sets the language identifier for the snippet.
+ *
+ * This should match the #GtkSourceLanguage:id identifier.
+ *
+ * Since: 5.0
+ */
+void
+gtk_source_snippet_set_language_id (GtkSourceSnippet *self,
+                                    const gchar      *language_id)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+
+       language_id = g_intern_string (language_id);
+
+       if (language_id != self->language_id)
+       {
+               self->language_id = language_id;
+               g_object_notify_by_pspec (G_OBJECT (self),
+                                         properties [PROP_LANGUAGE_ID]);
+       }
+}
+
+/**
+ * gtk_source_snippet_get_description:
+ * @self: a #GtkSourceSnippet
+ *
+ * Gets the description for the snippet.
+ *
+ * Since: 5.0
+ */
+const gchar *
+gtk_source_snippet_get_description (GtkSourceSnippet *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), NULL);
+
+       return self->description;
+}
+
+/**
+ * gtk_source_snippet_set_description:
+ * @self: a #GtkSourceSnippet
+ * @description: the snippet description
+ *
+ * Sets the description for the snippet.
+ *
+ * Since: 5.0
+ */
+void
+gtk_source_snippet_set_description (GtkSourceSnippet *self,
+                                    const gchar      *description)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+
+       if (g_strcmp0 (description, self->description) != 0)
+       {
+               g_free (self->description);
+               self->description = g_strdup (description);
+               g_object_notify_by_pspec (G_OBJECT (self),
+                                         properties [PROP_DESCRIPTION]);
+       }
+}
+
+static gint
+gtk_source_snippet_get_offset (GtkSourceSnippet *self,
+                               GtkTextIter      *iter)
+{
+       GtkTextIter begin;
+       gint ret;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), 0);
+       g_return_val_if_fail (iter, 0);
+
+       gtk_text_buffer_get_iter_at_mark (self->buffer, &begin, self->mark_begin);
+       ret = gtk_text_iter_get_offset (iter) - gtk_text_iter_get_offset (&begin);
+       ret = MAX (0, ret);
+
+       return ret;
+}
+
+static gint
+gtk_source_snippet_get_index (GtkSourceSnippet *self,
+                              GtkTextIter      *iter)
+{
+       gint offset;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), 0);
+       g_return_val_if_fail (iter, 0);
+
+       offset = gtk_source_snippet_get_offset (self, iter);
+
+       for (guint i = 0; i < self->runs->len; i++)
+       {
+               gint run = g_array_index (self->runs, gint, i);
+
+               offset -= run;
+
+               if (offset <= 0)
+               {
+                       /*
+                        * NOTE:
+                        *
+                        * This is the central part of the hack by using offsets
+                        * instead of textmarks (which gives us lots of gravity grief).
+                        * We guess which snippet it is based on the current chunk.
+                        */
+                       if (self->current_chunk > -1 && (i + 1) == (guint)self->current_chunk)
+                               return i + 1;
+                       else
+                               return i;
+               }
+       }
+
+       return (self->runs->len - 1);
+}
+
+static gboolean
+gtk_source_snippet_within_bounds (GtkSourceSnippet *self,
+                                  GtkTextIter      *iter)
+{
+       GtkTextIter begin;
+       GtkTextIter end;
+       gboolean ret;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), FALSE);
+       g_return_val_if_fail (iter, FALSE);
+
+       gtk_text_buffer_get_iter_at_mark (self->buffer, &begin, self->mark_begin);
+       gtk_text_buffer_get_iter_at_mark (self->buffer, &end, self->mark_end);
+
+       ret = ((gtk_text_iter_compare (&begin, iter) <= 0) &&
+              (gtk_text_iter_compare (&end, iter) >= 0));
+
+       return ret;
+}
+
+gboolean
+_gtk_source_snippet_insert_set (GtkSourceSnippet *self,
+                                GtkTextMark      *mark)
+{
+       GtkTextIter iter;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), FALSE);
+       g_return_val_if_fail (GTK_IS_TEXT_MARK (mark), FALSE);
+
+       gtk_text_buffer_get_iter_at_mark (self->buffer, &iter, mark);
+
+       if (!gtk_source_snippet_within_bounds (self, &iter))
+       {
+               return FALSE;
+       }
+
+       self->current_chunk = gtk_source_snippet_get_index (self, &iter);
+
+       return TRUE;
+}
+
+static void
+gtk_source_snippet_get_nth_chunk_range (GtkSourceSnippet *self,
+                                        gint              nth,
+                                        GtkTextIter      *begin,
+                                        GtkTextIter      *end)
+{
+       gint run;
+
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+       g_return_if_fail (nth >= 0);
+       g_return_if_fail (begin);
+       g_return_if_fail (end);
+
+       gtk_text_buffer_get_iter_at_mark (self->buffer, begin, self->mark_begin);
+
+       for (guint i = 0; i < nth; i++)
+       {
+               run = g_array_index (self->runs, gint, i);
+               gtk_text_iter_forward_chars (begin, run);
+       }
+
+       gtk_text_iter_assign (end, begin);
+       run = g_array_index (self->runs, gint, nth);
+       gtk_text_iter_forward_chars (end, run);
+}
+
+static void
+gtk_source_snippet_get_chunk_range (GtkSourceSnippet      *self,
+                                    GtkSourceSnippetChunk *chunk,
+                                    GtkTextIter           *begin,
+                                    GtkTextIter           *end)
+{
+
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk));
+
+       for (guint i = 0; i < self->chunks->len; i++)
+       {
+               GtkSourceSnippetChunk *item;
+
+               item = g_ptr_array_index (self->chunks, i);
+
+               if (item == chunk)
+               {
+                       gtk_source_snippet_get_nth_chunk_range (self, i, begin, end);
+                       return;
+               }
+       }
+
+       g_warn_if_reached ();
+}
+
+static void
+gtk_source_snippet_select_chunk (GtkSourceSnippet *self,
+                                 gint              nth)
+{
+       GtkTextIter begin;
+       GtkTextIter end;
+
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+       g_return_if_fail (nth >= 0);
+       g_return_if_fail ((guint)nth < self->runs->len);
+
+       gtk_source_snippet_get_nth_chunk_range (self, nth, &begin, &end);
+       gtk_text_iter_order (&begin, &end);
+
+       g_debug ("Selecting chunk %d with range %d:%d to %d:%d (offset %d+%d)",
+                nth,
+                gtk_text_iter_get_line (&begin) + 1,
+                gtk_text_iter_get_line_offset (&begin) + 1,
+                gtk_text_iter_get_line (&end) + 1,
+                gtk_text_iter_get_line_offset (&end) + 1,
+                gtk_text_iter_get_offset (&begin),
+                gtk_text_iter_get_offset (&end) - gtk_text_iter_get_offset (&begin));
+
+       gtk_text_buffer_select_range (self->buffer, &begin, &end);
+       self->current_chunk = nth;
+
+#ifndef G_DISABLE_ASSERT
+       {
+               GtkTextIter set_begin;
+               GtkTextIter set_end;
+
+               gtk_text_buffer_get_selection_bounds (self->buffer, &set_begin, &set_end);
+
+               g_assert (gtk_text_iter_equal (&set_begin, &begin));
+               g_assert (gtk_text_iter_equal (&set_end, &end));
+       }
+#endif
+}
+
+gboolean
+_gtk_source_snippet_move_next (GtkSourceSnippet *self)
+{
+       GtkTextIter iter;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), FALSE);
+
+       if (self->focus_position > self->max_focus_position)
+       {
+               return FALSE;
+       }
+
+       self->focus_position++;
+
+       for (guint i = 0; i < self->chunks->len; i++)
+       {
+               GtkSourceSnippetChunk *chunk = g_ptr_array_index (self->chunks, i);
+
+               if (gtk_source_snippet_chunk_get_focus_position (chunk) == self->focus_position)
+               {
+                       gtk_source_snippet_select_chunk (self, i);
+                       return TRUE;
+               }
+       }
+
+       for (guint i = 0; i < self->chunks->len; i++)
+       {
+               GtkSourceSnippetChunk *chunk = g_ptr_array_index (self->chunks, i);
+
+               if (gtk_source_snippet_chunk_get_focus_position (chunk) == 0)
+               {
+                       gtk_source_snippet_select_chunk (self, i);
+                       return FALSE;
+               }
+       }
+
+       g_debug ("No more tab stops, moving to end of snippet");
+
+       gtk_text_buffer_get_iter_at_mark (self->buffer, &iter, self->mark_end);
+       gtk_text_buffer_select_range (self->buffer, &iter, &iter);
+       self->current_chunk = self->chunks->len - 1;
+
+       return FALSE;
+}
+
+gboolean
+_gtk_source_snippet_move_previous (GtkSourceSnippet *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), FALSE);
+
+       if (self->focus_position == 1)
+       {
+               GtkTextIter iter;
+
+               /* Nothing to select before this position, just move
+                * the insertion mark to the beginning of the snippet.
+                */
+               gtk_text_buffer_get_iter_at_mark (self->buffer,
+                                                 &iter,
+                                                 self->mark_begin);
+               gtk_text_buffer_select_range (self->buffer, &iter, &iter);
+
+               return FALSE;
+       }
+
+       self->focus_position = MAX (1, self->focus_position - 1);
+
+       for (guint i = 0; i < self->chunks->len; i++)
+       {
+               GtkSourceSnippetChunk *chunk = g_ptr_array_index (self->chunks, i);
+
+               if (gtk_source_snippet_chunk_get_focus_position (chunk) == self->focus_position)
+               {
+                       gtk_source_snippet_select_chunk (self, i);
+                       return TRUE;
+               }
+       }
+
+       return FALSE;
+}
+
+static void
+gtk_source_snippet_update_context (GtkSourceSnippet *self)
+{
+       GtkSourceSnippetContext *context;
+
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+
+       if (self->chunks == NULL || self->chunks->len == 0)
+       {
+               return;
+       }
+
+       context = gtk_source_snippet_get_context (self);
+
+       _gtk_source_snippet_context_emit_changed (context);
+
+       for (guint i = 0; i < self->chunks->len; i++)
+       {
+               GtkSourceSnippetChunk *chunk = g_ptr_array_index (self->chunks, i);
+               gint focus_position;
+
+               g_assert (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk));
+
+               focus_position = gtk_source_snippet_chunk_get_focus_position (chunk);
+
+               if (focus_position > 0)
+               {
+                       const gchar *text;
+
+                       if ((text = gtk_source_snippet_chunk_get_text (chunk)))
+                       {
+                               gchar key[12];
+
+                               g_snprintf (key, sizeof key, "%d", focus_position);
+                               key[sizeof key - 1] = '\0';
+
+                               gtk_source_snippet_context_set_variable (context, key, text);
+                       }
+               }
+       }
+
+       _gtk_source_snippet_context_emit_changed (context);
+}
+
+static void
+gtk_source_snippet_clear_tags (GtkSourceSnippet *self)
+{
+       g_assert (GTK_SOURCE_IS_SNIPPET (self));
+
+       if (self->mark_begin != NULL && self->mark_end != NULL)
+       {
+               GtkTextBuffer *buffer;
+               GtkTextIter begin;
+               GtkTextIter end;
+               GtkTextTag *tag;
+
+               buffer = gtk_text_mark_get_buffer (self->mark_begin);
+
+               gtk_text_buffer_get_iter_at_mark (buffer, &begin, self->mark_begin);
+               gtk_text_buffer_get_iter_at_mark (buffer, &end, self->mark_end);
+
+               tag = _gtk_source_buffer_get_snippet_focus_tag (GTK_SOURCE_BUFFER (buffer));
+
+               gtk_text_buffer_remove_tag (buffer, tag, &begin, &end);
+       }
+}
+
+static void
+gtk_source_snippet_update_tags (GtkSourceSnippet *self)
+{
+       GtkTextBuffer *buffer;
+       GtkTextTag *tag;
+
+       g_assert (GTK_SOURCE_IS_SNIPPET (self));
+
+       gtk_source_snippet_clear_tags (self);
+
+       buffer = gtk_text_mark_get_buffer (self->mark_begin);
+       tag = _gtk_source_buffer_get_snippet_focus_tag (GTK_SOURCE_BUFFER (buffer));
+
+       for (guint i = 0; i < self->chunks->len; i++)
+       {
+               GtkSourceSnippetChunk *chunk = g_ptr_array_index (self->chunks, i);
+               gint focus_position = gtk_source_snippet_chunk_get_focus_position (chunk);
+
+               if (focus_position >= 0)
+               {
+                       GtkTextIter begin;
+                       GtkTextIter end;
+
+                       gtk_source_snippet_get_chunk_range (self, chunk, &begin, &end);
+                       gtk_text_buffer_apply_tag (buffer, tag, &begin, &end);
+               }
+       }
+}
+
+gboolean
+_gtk_source_snippet_begin (GtkSourceSnippet *self,
+                           GtkTextBuffer    *buffer,
+                           GtkTextIter      *iter)
+{
+       GtkSourceSnippetContext *context;
+       GtkTextMark *mark;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), FALSE);
+       g_return_val_if_fail (!self->buffer, FALSE);
+       g_return_val_if_fail (!self->mark_begin, FALSE);
+       g_return_val_if_fail (!self->mark_end, FALSE);
+       g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), FALSE);
+       g_return_val_if_fail (iter, FALSE);
+
+       self->inserted = TRUE;
+
+       context = gtk_source_snippet_get_context (self);
+
+       gtk_source_snippet_update_context (self);
+       _gtk_source_snippet_context_emit_changed (context);
+       gtk_source_snippet_update_context (self);
+
+       self->buffer = g_object_ref (buffer);
+
+       mark = gtk_text_buffer_create_mark (buffer, NULL, iter, TRUE);
+       self->mark_begin = g_object_ref (mark);
+
+       gtk_text_buffer_begin_user_action (buffer);
+
+       for (guint i = 0; i < self->chunks->len; i++)
+       {
+               GtkSourceSnippetChunk *chunk;
+               const gchar *text;
+
+               chunk = g_ptr_array_index (self->chunks, i);
+               text = gtk_source_snippet_chunk_get_text (chunk);
+
+               if (text != NULL)
+               {
+                       gint len;
+
+                       len = g_utf8_strlen (text, -1);
+                       g_array_append_val (self->runs, len);
+                       gtk_text_buffer_insert (buffer, iter, text, -1);
+               }
+       }
+
+       mark = gtk_text_buffer_create_mark (buffer, NULL, iter, FALSE);
+       self->mark_end = g_object_ref (mark);
+
+       gtk_text_buffer_end_user_action (buffer);
+
+       gtk_source_snippet_update_tags (self);
+
+       return _gtk_source_snippet_move_next (self);
+}
+
+void
+_gtk_source_snippet_finish (GtkSourceSnippet *self)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+
+       gtk_source_snippet_clear_tags (self);
+
+       g_clear_object (&self->mark_begin);
+       g_clear_object (&self->mark_end);
+       g_clear_object (&self->buffer);
+}
+
+void
+gtk_source_snippet_add_chunk (GtkSourceSnippet      *self,
+                              GtkSourceSnippetChunk *chunk)
+{
+       gint focus_position;
+
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk));
+       g_return_if_fail (!self->inserted);
+
+       g_ptr_array_add (self->chunks, g_object_ref (chunk));
+
+       gtk_source_snippet_chunk_set_context (chunk, self->context);
+
+       focus_position = gtk_source_snippet_chunk_get_focus_position (chunk);
+       self->max_focus_position = MAX (self->max_focus_position, focus_position);
+}
+
+static gchar *
+gtk_source_snippet_get_nth_text (GtkSourceSnippet *self,
+                                 gint              nth)
+{
+       GtkTextIter iter;
+       GtkTextIter end;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), NULL);
+       g_return_val_if_fail (nth >= 0, NULL);
+
+       gtk_text_buffer_get_iter_at_mark (self->buffer, &iter, self->mark_begin);
+
+       for (gint i = 0; i < nth; i++)
+       {
+               gtk_text_iter_forward_chars (&iter, g_array_index (self->runs, gint, i));
+       }
+
+       gtk_text_iter_assign (&end, &iter);
+       gtk_text_iter_forward_chars (&end, g_array_index (self->runs, gint, nth));
+
+       return gtk_text_buffer_get_text (self->buffer, &iter, &end, TRUE);
+}
+
+static void
+gtk_source_snippet_replace_chunk_text (GtkSourceSnippet *self,
+                                       gint              nth,
+                                       const gchar      *text)
+{
+       GtkTextIter begin;
+       GtkTextIter end;
+       gint diff = 0;
+
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+       g_return_if_fail (nth >= 0);
+       g_return_if_fail (text);
+
+       /*
+        * This replaces the text for the snippet. We insert new text before
+        * we delete the old text to ensure things are more stable as we
+        * manipulate the runs. Avoiding zero-length runs, even temporarily
+        * can be helpful.
+        */
+
+       gtk_source_snippet_get_nth_chunk_range (self, nth, &begin, &end);
+
+       if (!gtk_text_iter_equal (&begin, &end))
+       {
+               gtk_text_iter_order (&begin, &end);
+               diff = gtk_text_iter_get_offset (&end) - gtk_text_iter_get_offset (&begin);
+       }
+
+       g_array_index (self->runs, gint, nth) += g_utf8_strlen (text, -1);
+       gtk_text_buffer_insert (self->buffer, &begin, text, -1);
+
+       /* At this point, begin should be updated to the end of where we inserted
+        * our new text. If `diff` is non-zero, then we need to remove those
+        * characters immediately after `begin`.
+        */
+       if (diff != 0)
+       {
+               end = begin;
+               gtk_text_iter_forward_chars (&end, diff);
+               g_array_index (self->runs, gint, nth) -= diff;
+               gtk_text_buffer_delete (self->buffer, &begin, &end);
+       }
+}
+
+static void
+gtk_source_snippet_rewrite_updated_chunks (GtkSourceSnippet *self)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+
+       for (guint i = 0; i < self->chunks->len; i++)
+       {
+               GtkSourceSnippetChunk *chunk = g_ptr_array_index (self->chunks, i);
+               const gchar *text;
+               gchar *real_text;
+
+               text = gtk_source_snippet_chunk_get_text (chunk);
+               real_text = gtk_source_snippet_get_nth_text (self, i);
+
+               if (g_strcmp0 (text, real_text) != 0)
+               {
+                       gtk_source_snippet_replace_chunk_text (self, i, text);
+               }
+
+               g_free (real_text);
+       }
+}
+
+void
+_gtk_source_snippet_before_insert_text (GtkSourceSnippet *self,
+                                        GtkTextBuffer    *buffer,
+                                        GtkTextIter      *iter,
+                                        gchar            *text,
+                                        gint              len)
+{
+       gint utf8_len;
+       gint n;
+
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+       g_return_if_fail (self->current_chunk >= 0);
+       g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+       g_return_if_fail (iter);
+
+       n = gtk_source_snippet_get_index (self, iter);
+       utf8_len = g_utf8_strlen (text, len);
+       g_array_index (self->runs, gint, n) += utf8_len;
+
+#if 0
+       g_print ("I: ");
+       for (n = 0; n < self->runs->len; n++)
+               g_print ("%d ", g_array_index (self->runs, gint, n));
+       g_print ("\n");
+#endif
+}
+
+void
+_gtk_source_snippet_after_insert_text (GtkSourceSnippet *self,
+                                       GtkTextBuffer    *buffer,
+                                       GtkTextIter      *iter,
+                                       gchar            *text,
+                                       gint              len)
+{
+       GtkSourceSnippetChunk *chunk;
+       GtkTextMark *here;
+       gchar *new_text;
+       gint n;
+
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+       g_return_if_fail (self->current_chunk >= 0);
+       g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+       g_return_if_fail (iter);
+
+       n = gtk_source_snippet_get_index (self, iter);
+       chunk = g_ptr_array_index (self->chunks, n);
+       new_text = gtk_source_snippet_get_nth_text (self, n);
+       gtk_source_snippet_chunk_set_text (chunk, new_text);
+       gtk_source_snippet_chunk_set_text_set (chunk, TRUE);
+       g_free (new_text);
+
+       here = gtk_text_buffer_create_mark (buffer, NULL, iter, TRUE);
+
+       gtk_source_snippet_update_context (self);
+       gtk_source_snippet_update_context (self);
+       gtk_source_snippet_rewrite_updated_chunks (self);
+
+       gtk_text_buffer_get_iter_at_mark (buffer, iter, here);
+       gtk_text_buffer_delete_mark (buffer, here);
+
+       gtk_source_snippet_update_tags (self);
+}
+
+void
+_gtk_source_snippet_before_delete_range (GtkSourceSnippet *self,
+                                         GtkTextBuffer    *buffer,
+                                         GtkTextIter      *begin,
+                                         GtkTextIter      *end)
+{
+       gint *run;
+       gint len;
+       gint n;
+       gint lower_bound = -1;
+       gint upper_bound = -1;
+
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+       g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+       g_return_if_fail (begin);
+       g_return_if_fail (end);
+
+       len = gtk_text_iter_get_offset (end) - gtk_text_iter_get_offset (begin);
+       n = gtk_source_snippet_get_index (self, begin);
+
+       if (n < 0)
+       {
+               return;
+       }
+
+       self->current_chunk = n;
+
+       while (len != 0 && (guint)n < self->runs->len)
+       {
+               if (lower_bound == -1 || n < lower_bound)
+               {
+                       lower_bound = n;
+               }
+
+               if (n > upper_bound)
+               {
+                       upper_bound = n;
+               }
+
+               run = &g_array_index (self->runs, gint, n);
+
+               if (len > *run)
+               {
+                       len -= *run;
+                       *run = 0;
+                       n++;
+                       continue;
+               }
+
+               *run -= len;
+
+               break;
+       }
+
+       if (lower_bound == -1 || upper_bound == -1)
+       {
+               return;
+       }
+
+       for (gint i = lower_bound; i <= upper_bound; i++)
+       {
+               GtkSourceSnippetChunk *chunk = g_ptr_array_index (self->chunks, i);
+               gchar *new_text;
+
+               new_text = gtk_source_snippet_get_nth_text (self, i);
+               gtk_source_snippet_chunk_set_text (chunk, new_text);
+               gtk_source_snippet_chunk_set_text_set (chunk, TRUE);
+               g_free (new_text);
+       }
+
+#if 0
+       g_print ("D: ");
+       for (n = 0; n < self->runs->len; n++)
+               g_print ("%d ", g_array_index (self->runs, gint, n));
+       g_print ("\n");
+#endif
+}
+
+void
+_gtk_source_snippet_after_delete_range (GtkSourceSnippet *self,
+                                        GtkTextBuffer    *buffer,
+                                        GtkTextIter      *begin,
+                                        GtkTextIter      *end)
+{
+       GtkTextMark *here;
+
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+       g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+       g_return_if_fail (begin);
+       g_return_if_fail (end);
+
+       here = gtk_text_buffer_create_mark (buffer, NULL, begin, TRUE);
+
+       gtk_source_snippet_update_context (self);
+       gtk_source_snippet_update_context (self);
+       gtk_source_snippet_rewrite_updated_chunks (self);
+
+       gtk_text_buffer_get_iter_at_mark (buffer, begin, here);
+       gtk_text_buffer_get_iter_at_mark (buffer, end, here);
+       gtk_text_buffer_delete_mark (buffer, here);
+
+       gtk_source_snippet_update_tags (self);
+}
+
+GtkTextMark *
+_gtk_source_snippet_get_mark_begin (GtkSourceSnippet *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), NULL);
+
+       return self->mark_begin;
+}
+
+GtkTextMark *
+_gtk_source_snippet_get_mark_end (GtkSourceSnippet *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), NULL);
+
+       return self->mark_end;
+}
+
+/**
+ * gtk_source_snippet_get_context:
+ * @self: an #GtkSourceSnippet
+ *
+ * Get's the context used for expanding the snippet.
+ *
+ * Returns: (nullable) (transfer none): an #GtkSourceSnippetContext
+ *
+ * Since: 5.0
+ */
+GtkSourceSnippetContext *
+gtk_source_snippet_get_context (GtkSourceSnippet *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), NULL);
+
+       if (self->context == NULL)
+       {
+               GtkSourceSnippetChunk *chunk;
+
+               self->context = gtk_source_snippet_context_new ();
+
+               for (guint i = 0; i < self->chunks->len; i++)
+               {
+                       chunk = g_ptr_array_index (self->chunks, i);
+                       gtk_source_snippet_chunk_set_context (chunk, self->context);
+               }
+       }
+
+       return self->context;
+}
+
+static void
+gtk_source_snippet_dispose (GObject *object)
+{
+       GtkSourceSnippet *self = (GtkSourceSnippet *)object;
+
+       if (self->mark_begin != NULL)
+       {
+               gtk_text_buffer_delete_mark (self->buffer, self->mark_begin);
+               g_clear_object (&self->mark_begin);
+       }
+
+       if (self->mark_end != NULL)
+       {
+               gtk_text_buffer_delete_mark (self->buffer, self->mark_end);
+               g_clear_object (&self->mark_end);
+       }
+
+       g_clear_pointer (&self->runs, g_array_unref);
+       g_clear_pointer (&self->chunks, g_ptr_array_unref);
+
+       g_clear_object (&self->buffer);
+       g_clear_object (&self->context);
+
+       G_OBJECT_CLASS (gtk_source_snippet_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_snippet_finalize (GObject *object)
+{
+       GtkSourceSnippet *self = (GtkSourceSnippet *)object;
+
+       g_clear_pointer (&self->description, g_free);
+       g_clear_pointer (&self->trigger, g_free);
+       g_clear_object (&self->buffer);
+
+       G_OBJECT_CLASS (gtk_source_snippet_parent_class)->finalize (object);
+}
+
+static void
+gtk_source_snippet_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+       GtkSourceSnippet *self = GTK_SOURCE_SNIPPET (object);
+
+       switch (prop_id)
+       {
+       case PROP_BUFFER:
+               g_value_set_object (value, self->buffer);
+               break;
+
+       case PROP_MARK_BEGIN:
+               g_value_set_object (value, self->mark_begin);
+               break;
+
+       case PROP_MARK_END:
+               g_value_set_object (value, self->mark_end);
+               break;
+
+       case PROP_TRIGGER:
+               g_value_set_string (value, self->trigger);
+               break;
+
+       case PROP_LANGUAGE_ID:
+               g_value_set_string (value, self->language_id);
+               break;
+
+       case PROP_DESCRIPTION:
+               g_value_set_string (value, self->description);
+               break;
+
+       case PROP_POSITION:
+               g_value_set_uint (value, self->focus_position);
+               break;
+
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+       }
+}
+
+static void
+gtk_source_snippet_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+       GtkSourceSnippet *self = GTK_SOURCE_SNIPPET (object);
+
+       switch (prop_id)
+       {
+       case PROP_TRIGGER:
+               gtk_source_snippet_set_trigger (self, g_value_get_string (value));
+               break;
+
+       case PROP_LANGUAGE_ID:
+               gtk_source_snippet_set_language_id (self, g_value_get_string (value));
+               break;
+
+       case PROP_DESCRIPTION:
+               gtk_source_snippet_set_description (self, g_value_get_string (value));
+               break;
+
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+       }
+}
+
+static void
+gtk_source_snippet_class_init (GtkSourceSnippetClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->dispose = gtk_source_snippet_dispose;
+       object_class->finalize = gtk_source_snippet_finalize;
+       object_class->get_property = gtk_source_snippet_get_property;
+       object_class->set_property = gtk_source_snippet_set_property;
+
+       properties[PROP_BUFFER] =
+               g_param_spec_object ("buffer",
+                                    "Buffer",
+                                    "The GtkTextBuffer for the snippet",
+                                    GTK_TYPE_TEXT_BUFFER,
+                                    (G_PARAM_READABLE |
+                                     G_PARAM_EXPLICIT_NOTIFY |
+                                     G_PARAM_STATIC_STRINGS));
+
+       properties[PROP_MARK_BEGIN] =
+               g_param_spec_object ("mark-begin",
+                                    "Mark Begin",
+                                    "The beginning text mark",
+                                    GTK_TYPE_TEXT_MARK,
+                                    (G_PARAM_READABLE |
+                                     G_PARAM_EXPLICIT_NOTIFY |
+                                     G_PARAM_STATIC_STRINGS));
+
+       properties[PROP_MARK_END] =
+               g_param_spec_object ("mark-end",
+                                    "Mark End",
+                                    "The ending text mark",
+                                    GTK_TYPE_TEXT_MARK,
+                                    (G_PARAM_READABLE |
+                                     G_PARAM_EXPLICIT_NOTIFY |
+                                     G_PARAM_STATIC_STRINGS));
+
+       properties[PROP_TRIGGER] =
+               g_param_spec_string ("trigger",
+                                    "Trigger",
+                                    "The trigger for the snippet",
+                                    NULL,
+                                    (G_PARAM_READWRITE |
+                                     G_PARAM_EXPLICIT_NOTIFY |
+                                     G_PARAM_STATIC_STRINGS));
+
+       properties[PROP_LANGUAGE_ID] =
+               g_param_spec_string ("language-id",
+                                    "Language Id",
+                                    "The language-id for the snippet",
+                                    NULL,
+                                    (G_PARAM_READWRITE |
+                                     G_PARAM_EXPLICIT_NOTIFY |
+                                     G_PARAM_STATIC_STRINGS));
+
+       properties[PROP_DESCRIPTION] =
+               g_param_spec_string ("description",
+                                    "Description",
+                                    "The description for the snippet",
+                                    NULL,
+                                    (G_PARAM_READWRITE |
+                                     G_PARAM_EXPLICIT_NOTIFY |
+                                     G_PARAM_STATIC_STRINGS));
+
+       properties[PROP_POSITION] =
+               g_param_spec_int ("position",
+                                 "Position",
+                                 "The current position",
+                                 -1,
+                                 G_MAXINT,
+                                 -1,
+                                 (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gtk_source_snippet_init (GtkSourceSnippet *self)
+{
+       self->max_focus_position = -1;
+       self->chunks = g_ptr_array_new_with_free_func (g_object_unref);
+       self->runs = g_array_new (FALSE, FALSE, sizeof (gint));
+}
+
+gchar *
+_gtk_source_snippet_get_edited_text (GtkSourceSnippet *self)
+{
+       GtkTextIter begin;
+       GtkTextIter end;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (self), NULL);
+
+       if (self->mark_begin == NULL || self->mark_end == NULL)
+       {
+               return NULL;
+       }
+
+       gtk_text_buffer_get_iter_at_mark (self->buffer, &begin, self->mark_begin);
+       gtk_text_buffer_get_iter_at_mark (self->buffer, &end, self->mark_end);
+
+       return gtk_text_iter_get_slice (&begin, &end);
+}
+
+void
+_gtk_source_snippet_replace_current_chunk_text (GtkSourceSnippet *self,
+                                                const gchar      *new_text)
+{
+       GtkSourceSnippetChunk *chunk;
+       gint utf8_len;
+
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+       g_return_if_fail (self->chunks != NULL);
+
+       if (self->current_chunk < 0 || self->current_chunk >= self->chunks->len)
+       {
+               return;
+       }
+
+       chunk = g_ptr_array_index (self->chunks, self->current_chunk);
+
+       gtk_source_snippet_chunk_set_text (chunk, new_text);
+       gtk_source_snippet_chunk_set_text_set (chunk, TRUE);
+
+       g_assert (self->current_chunk >= 0);
+       g_assert (self->current_chunk < self->runs->len);
+
+       utf8_len = g_utf8_strlen (new_text, -1);
+       g_array_index (self->runs, gint, self->current_chunk) = utf8_len;
+}
+
+void
+_gtk_source_snippet_pause (GtkSourceSnippet *self)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+
+}
+
+void
+_gtk_source_snippet_unpause (GtkSourceSnippet *self)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (self));
+
+}
diff --git a/gtksourceview/gtksourcesnippet.h b/gtksourceview/gtksourcesnippet.h
new file mode 100644
index 00000000..85588023
--- /dev/null
+++ b/gtksourceview/gtksourcesnippet.h
@@ -0,0 +1,70 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2020 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/>.
+ */
+
+#pragma once
+
+#if !defined (GTK_SOURCE_H_INSIDE) && !defined (GTK_SOURCE_COMPILATION)
+#error "Only <gtksourceview/gtksource.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+#include "gtksourcetypes.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_SNIPPET (gtk_source_snippet_get_type())
+
+GTK_SOURCE_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (GtkSourceSnippet, gtk_source_snippet, GTK_SOURCE, SNIPPET, GObject)
+
+GTK_SOURCE_AVAILABLE_IN_5_0
+GtkSourceSnippet        *gtk_source_snippet_new                        (const gchar           *trigger,
+                                                                        const gchar           *language_id);
+GTK_SOURCE_AVAILABLE_IN_5_0
+GtkSourceSnippet        *gtk_source_snippet_copy                       (GtkSourceSnippet      *self);
+GTK_SOURCE_AVAILABLE_IN_5_0
+const gchar             *gtk_source_snippet_get_trigger                (GtkSourceSnippet      *self);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                     gtk_source_snippet_set_trigger                (GtkSourceSnippet      *self,
+                                                                        const gchar           *trigger);
+GTK_SOURCE_AVAILABLE_IN_5_0
+const gchar             *gtk_source_snippet_get_language_id            (GtkSourceSnippet      *self);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                     gtk_source_snippet_set_language_id            (GtkSourceSnippet      *self,
+                                                                        const gchar           *language_id);
+GTK_SOURCE_AVAILABLE_IN_5_0
+const gchar             *gtk_source_snippet_get_description            (GtkSourceSnippet      *self);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                     gtk_source_snippet_set_description            (GtkSourceSnippet      *self,
+                                                                        const gchar           *description);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                     gtk_source_snippet_add_chunk                  (GtkSourceSnippet      *self,
+                                                                        GtkSourceSnippetChunk *chunk);
+GTK_SOURCE_AVAILABLE_IN_5_0
+guint                    gtk_source_snippet_get_n_chunks               (GtkSourceSnippet      *self);
+GTK_SOURCE_AVAILABLE_IN_5_0
+gint                     gtk_source_snippet_get_focus_position         (GtkSourceSnippet      *self);
+GTK_SOURCE_AVAILABLE_IN_5_0
+GtkSourceSnippetChunk   *gtk_source_snippet_get_nth_chunk              (GtkSourceSnippet      *self,
+                                                                        guint                  nth);
+GTK_SOURCE_AVAILABLE_IN_5_0
+GtkSourceSnippetContext *gtk_source_snippet_get_context                (GtkSourceSnippet      *self);
+
+G_END_DECLS
diff --git a/gtksourceview/gtksourcesnippetchunk.c b/gtksourceview/gtksourcesnippetchunk.c
new file mode 100644
index 00000000..4b58854a
--- /dev/null
+++ b/gtksourceview/gtksourcesnippetchunk.c
@@ -0,0 +1,402 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2020 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/>.
+ */
+
+#include "config.h"
+
+#include "gtksourcesnippetchunk.h"
+#include "gtksourcesnippetcontext.h"
+
+/**
+ * SECTION:snippetchunk
+ * @title: GtkSourceSnippetChunk
+ * @short_description: An chunk of text within the source snippet
+ *
+ * The #GtkSourceSnippetChunk represents a single chunk of text that
+ * may or may not be an edit point within the snippet. Chunks that are
+ * an edit point (also called a tab stop) have the
+ * #GtkSourceSnippetChunk:focus-position property set.
+ *
+ * Since: 5.0
+ */
+
+struct _GtkSourceSnippetChunk
+{
+       GObject                  parent_instance;
+
+       GtkSourceSnippetContext *context;
+       gulong                   context_changed_handler;
+       gint                     focus_position;
+       gchar                   *spec;
+       gchar                   *text;
+
+       guint                    text_set : 1;
+};
+
+G_DEFINE_TYPE (GtkSourceSnippetChunk, gtk_source_snippet_chunk, G_TYPE_OBJECT)
+
+enum {
+       PROP_0,
+       PROP_CONTEXT,
+       PROP_SPEC,
+       PROP_FOCUS_POSITION,
+       PROP_TEXT,
+       PROP_TEXT_SET,
+       N_PROPS
+};
+
+static GParamSpec *properties[N_PROPS];
+
+/**
+ * gtk_source_snippet_chunk_new:
+ *
+ * Create a new #GtkSourceSnippetChunk that can be added to
+ * a #GtkSourceSnippet.
+ *
+ * Since: 5.0
+ */
+GtkSourceSnippetChunk *
+gtk_source_snippet_chunk_new (void)
+{
+       return g_object_new (GTK_SOURCE_TYPE_SNIPPET_CHUNK, NULL);
+}
+
+/**
+ * gtk_source_snippet_chunk_copy:
+ *
+ * Copies the source snippet.
+ *
+ * Returns: (transfer full): An #GtkSourceSnippetChunk.
+ *
+ * Since: 5.0
+ */
+GtkSourceSnippetChunk *
+gtk_source_snippet_chunk_copy (GtkSourceSnippetChunk *chunk)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk), NULL);
+
+       return g_object_new (GTK_SOURCE_TYPE_SNIPPET_CHUNK,
+                            "spec", chunk->spec,
+                            "focus-position", chunk->focus_position,
+                            NULL);
+}
+
+static void
+on_context_changed (GtkSourceSnippetContext *context,
+                    GtkSourceSnippetChunk   *chunk)
+{
+       gchar *text;
+
+       g_assert (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk));
+       g_assert (GTK_SOURCE_IS_SNIPPET_CONTEXT (context));
+
+       if (!chunk->text_set)
+       {
+               text = gtk_source_snippet_context_expand (context, chunk->spec);
+               gtk_source_snippet_chunk_set_text (chunk, text);
+               g_free (text);
+       }
+}
+
+/**
+ * gtk_source_snippet_chunk_get_context:
+ *
+ * Gets the context for the snippet insertion.
+ *
+ * Returns: (transfer none): An #GtkSourceSnippetContext.
+ *
+ * Since: 5.0
+ */
+GtkSourceSnippetContext *
+gtk_source_snippet_chunk_get_context (GtkSourceSnippetChunk *chunk)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk), NULL);
+
+       return chunk->context;
+}
+
+void
+gtk_source_snippet_chunk_set_context (GtkSourceSnippetChunk   *chunk,
+                                      GtkSourceSnippetContext *context)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk));
+       g_return_if_fail (!context || GTK_SOURCE_IS_SNIPPET_CONTEXT (context));
+
+       if (context != chunk->context)
+       {
+               g_clear_signal_handler (&chunk->context_changed_handler,
+                                       chunk->context);
+               g_clear_object (&chunk->context);
+
+               if (context != NULL)
+               {
+                       chunk->context = g_object_ref (context);
+                       chunk->context_changed_handler =
+                               g_signal_connect_object (chunk->context,
+                                                        "changed",
+                                                        G_CALLBACK (on_context_changed),
+                                                        chunk,
+                                                        0);
+               }
+
+               g_object_notify_by_pspec (G_OBJECT (chunk),
+                                         properties [PROP_CONTEXT]);
+       }
+}
+
+const gchar *
+gtk_source_snippet_chunk_get_spec (GtkSourceSnippetChunk *chunk)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk), NULL);
+
+       return chunk->spec;
+}
+
+void
+gtk_source_snippet_chunk_set_spec (GtkSourceSnippetChunk *chunk,
+                                   const gchar           *spec)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk));
+
+       if (g_strcmp0 (spec, chunk->spec) != 0)
+       {
+               g_free (chunk->spec);
+               chunk->spec = g_strdup (spec);
+               g_object_notify_by_pspec (G_OBJECT (chunk),
+                                         properties [PROP_SPEC]);
+       }
+}
+
+gint
+gtk_source_snippet_chunk_get_focus_position (GtkSourceSnippetChunk *chunk)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk), 0);
+
+       return chunk->focus_position;
+}
+
+void
+gtk_source_snippet_chunk_set_focus_position (GtkSourceSnippetChunk *chunk,
+                                             gint                   focus_position)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk));
+
+       focus_position = MAX (focus_position, -1);
+
+       if (chunk->focus_position != focus_position)
+       {
+               chunk->focus_position = focus_position;
+               g_object_notify_by_pspec (G_OBJECT (chunk),
+                                         properties [PROP_FOCUS_POSITION]);
+       }
+}
+
+const gchar *
+gtk_source_snippet_chunk_get_text (GtkSourceSnippetChunk *chunk)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk), NULL);
+
+       return chunk->text ? chunk->text : "";
+}
+
+void
+gtk_source_snippet_chunk_set_text (GtkSourceSnippetChunk *chunk,
+                                   const gchar           *text)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk));
+
+       if (g_strcmp0 (chunk->text, text) != 0)
+       {
+               g_free (chunk->text);
+               chunk->text = g_strdup (text);
+               g_object_notify_by_pspec (G_OBJECT (chunk),
+                                         properties [PROP_TEXT]);
+       }
+}
+
+gboolean
+gtk_source_snippet_chunk_get_text_set (GtkSourceSnippetChunk *chunk)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk), FALSE);
+
+       return chunk->text_set;
+}
+
+void
+gtk_source_snippet_chunk_set_text_set (GtkSourceSnippetChunk *chunk,
+                                       gboolean               text_set)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk));
+
+       text_set = !!text_set;
+
+       if (chunk->text_set != text_set)
+       {
+               chunk->text_set = text_set;
+               g_object_notify_by_pspec (G_OBJECT (chunk),
+                                         properties [PROP_TEXT_SET]);
+       }
+}
+
+static void
+gtk_source_snippet_chunk_finalize (GObject *object)
+{
+       GtkSourceSnippetChunk *chunk = (GtkSourceSnippetChunk *)object;
+
+       g_clear_pointer (&chunk->spec, g_free);
+       g_clear_pointer (&chunk->text, g_free);
+       g_clear_object (&chunk->context);
+
+       G_OBJECT_CLASS (gtk_source_snippet_chunk_parent_class)->finalize (object);
+}
+
+static void
+gtk_source_snippet_chunk_get_property (GObject    *object,
+                                       guint       prop_id,
+                                       GValue     *value,
+                                       GParamSpec *pspec)
+{
+       GtkSourceSnippetChunk *chunk = GTK_SOURCE_SNIPPET_CHUNK (object);
+
+       switch (prop_id)
+       {
+       case PROP_CONTEXT:
+               g_value_set_object (value, gtk_source_snippet_chunk_get_context (chunk));
+               break;
+
+       case PROP_SPEC:
+               g_value_set_string (value, gtk_source_snippet_chunk_get_spec (chunk));
+               break;
+
+       case PROP_FOCUS_POSITION:
+               g_value_set_int (value, gtk_source_snippet_chunk_get_focus_position (chunk));
+               break;
+
+       case PROP_TEXT:
+               g_value_set_string (value, gtk_source_snippet_chunk_get_text (chunk));
+               break;
+
+       case PROP_TEXT_SET:
+               g_value_set_boolean (value, gtk_source_snippet_chunk_get_text_set (chunk));
+               break;
+
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+       }
+}
+
+static void
+gtk_source_snippet_chunk_set_property (GObject      *object,
+                                       guint         prop_id,
+                                       const GValue *value,
+                                       GParamSpec   *pspec)
+{
+       GtkSourceSnippetChunk *chunk = GTK_SOURCE_SNIPPET_CHUNK (object);
+
+       switch (prop_id)
+       {
+       case PROP_CONTEXT:
+               gtk_source_snippet_chunk_set_context (chunk, g_value_get_object (value));
+               break;
+
+       case PROP_FOCUS_POSITION:
+               gtk_source_snippet_chunk_set_focus_position (chunk, g_value_get_int (value));
+               break;
+
+       case PROP_SPEC:
+               gtk_source_snippet_chunk_set_spec (chunk, g_value_get_string (value));
+               break;
+
+       case PROP_TEXT:
+               gtk_source_snippet_chunk_set_text (chunk, g_value_get_string (value));
+               break;
+
+       case PROP_TEXT_SET:
+               gtk_source_snippet_chunk_set_text_set (chunk, g_value_get_boolean (value));
+               break;
+
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+       }
+}
+
+static void
+gtk_source_snippet_chunk_class_init (GtkSourceSnippetChunkClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->finalize = gtk_source_snippet_chunk_finalize;
+       object_class->get_property = gtk_source_snippet_chunk_get_property;
+       object_class->set_property = gtk_source_snippet_chunk_set_property;
+
+       properties[PROP_CONTEXT] =
+               g_param_spec_object ("context",
+                                    "Context",
+                                    "The snippet context.",
+                                    GTK_SOURCE_TYPE_SNIPPET_CONTEXT,
+                                    (G_PARAM_READWRITE |
+                                     G_PARAM_EXPLICIT_NOTIFY |
+                                     G_PARAM_STATIC_STRINGS));
+
+       properties[PROP_SPEC] =
+               g_param_spec_string ("spec",
+                                    "Spec",
+                                    "The specification to expand using the context.",
+                                    NULL,
+                                    (G_PARAM_READWRITE |
+                                     G_PARAM_EXPLICIT_NOTIFY |
+                                     G_PARAM_STATIC_STRINGS));
+
+       properties[PROP_FOCUS_POSITION] =
+               g_param_spec_int ("focus-position",
+                                 "Focus Position",
+                                 "The focus position for the chunk.",
+                                 -1,
+                                 G_MAXINT,
+                                 -1,
+                                 (G_PARAM_READWRITE |
+                                  G_PARAM_EXPLICIT_NOTIFY |
+                                  G_PARAM_STATIC_STRINGS));
+
+       properties[PROP_TEXT] =
+               g_param_spec_string ("text",
+                                    "Text",
+                                    "The text for the chunk.",
+                                    NULL,
+                                    (G_PARAM_READWRITE |
+                                     G_PARAM_EXPLICIT_NOTIFY |
+                                     G_PARAM_STATIC_STRINGS));
+
+       properties[PROP_TEXT_SET] =
+               g_param_spec_boolean ("text-set",
+                                     "If text property is set",
+                                     "If the text property has been manually set.",
+                                     FALSE,
+                                     (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_snippet_chunk_init (GtkSourceSnippetChunk *chunk)
+{
+       chunk->focus_position = -1;
+       chunk->spec = g_strdup ("");
+}
diff --git a/gtksourceview/gtksourcesnippetchunk.h b/gtksourceview/gtksourcesnippetchunk.h
new file mode 100644
index 00000000..460478e8
--- /dev/null
+++ b/gtksourceview/gtksourcesnippetchunk.h
@@ -0,0 +1,67 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2020 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/>.
+ */
+
+#pragma once
+
+#if !defined (GTK_SOURCE_H_INSIDE) && !defined (GTK_SOURCE_COMPILATION)
+#error "Only <gtksourceview/gtksource.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+#include "gtksourcetypes.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_SNIPPET_CHUNK (gtk_source_snippet_chunk_get_type())
+
+GTK_SOURCE_AVAILABLE_IN_5_0
+G_DECLARE_FINAL_TYPE (GtkSourceSnippetChunk, gtk_source_snippet_chunk, GTK_SOURCE, SNIPPET_CHUNK, GObject)
+
+GTK_SOURCE_AVAILABLE_IN_5_0
+GtkSourceSnippetChunk   *gtk_source_snippet_chunk_new                (void);
+GTK_SOURCE_AVAILABLE_IN_5_0
+GtkSourceSnippetChunk   *gtk_source_snippet_chunk_copy               (GtkSourceSnippetChunk   *chunk);
+GTK_SOURCE_AVAILABLE_IN_5_0
+GtkSourceSnippetContext *gtk_source_snippet_chunk_get_context        (GtkSourceSnippetChunk   *chunk);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                     gtk_source_snippet_chunk_set_context        (GtkSourceSnippetChunk   *chunk,
+                                                                      GtkSourceSnippetContext *context);
+GTK_SOURCE_AVAILABLE_IN_5_0
+const gchar             *gtk_source_snippet_chunk_get_spec           (GtkSourceSnippetChunk   *chunk);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                     gtk_source_snippet_chunk_set_spec           (GtkSourceSnippetChunk   *chunk,
+                                                                      const gchar             *spec);
+GTK_SOURCE_AVAILABLE_IN_5_0
+gint                     gtk_source_snippet_chunk_get_focus_position (GtkSourceSnippetChunk   *chunk);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                     gtk_source_snippet_chunk_set_focus_position (GtkSourceSnippetChunk   *chunk,
+                                                                      gint                     position);
+GTK_SOURCE_AVAILABLE_IN_5_0
+const gchar             *gtk_source_snippet_chunk_get_text           (GtkSourceSnippetChunk   *chunk);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                     gtk_source_snippet_chunk_set_text           (GtkSourceSnippetChunk   *chunk,
+                                                                      const gchar             *text);
+GTK_SOURCE_AVAILABLE_IN_5_0
+gboolean                 gtk_source_snippet_chunk_get_text_set       (GtkSourceSnippetChunk   *chunk);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                     gtk_source_snippet_chunk_set_text_set       (GtkSourceSnippetChunk   *chunk,
+                                                                      gboolean                 text_set);
+
+G_END_DECLS
diff --git a/gtksourceview/gtksourcesnippetcontext-private.h b/gtksourceview/gtksourcesnippetcontext-private.h
new file mode 100644
index 00000000..8fb734ca
--- /dev/null
+++ b/gtksourceview/gtksourcesnippetcontext-private.h
@@ -0,0 +1,29 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2020 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/>.
+ */
+
+#pragma once
+
+#include "gtksourcesnippetcontext.h"
+
+G_BEGIN_DECLS
+
+G_GNUC_INTERNAL
+void _gtk_source_snippet_context_emit_changed (GtkSourceSnippetContext *context);
+
+G_END_DECLS
diff --git a/gtksourceview/gtksourcesnippetcontext.c b/gtksourceview/gtksourcesnippetcontext.c
new file mode 100644
index 00000000..dacd517a
--- /dev/null
+++ b/gtksourceview/gtksourcesnippetcontext.c
@@ -0,0 +1,911 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2020 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/>.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <glib/gi18n.h>
+#include <stdlib.h>
+
+#include "gtksourcesnippetcontext-private.h"
+
+/**
+ * SECTION:snippetcontext
+ * @title: GtkSourceSnippetContext
+ * @short_description: Context for expanding #GtkSourceSnippetChunk
+ *
+ * This class is currently used primary as a hashtable. However, the longer
+ * term goal is to have it hold onto a GjsContext as well as other languages
+ * so that #GtkSourceSnippetChunk can expand themselves by executing
+ * script within the context.
+ *
+ * The #GtkSourceSnippet will build the context and then expand each of the
+ * chunks during the insertion/edit phase.
+ *
+ * Since: 5.0
+ */
+
+struct _GtkSourceSnippetContext
+{
+  GObject     parent_instance;
+
+  GHashTable *constants;
+  GHashTable *variables;
+  gchar      *line_prefix;
+  gint        tab_width;
+  guint       use_spaces : 1;
+};
+
+struct _GtkSourceSnippetContextClass
+{
+  GObjectClass parent;
+};
+
+G_DEFINE_TYPE (GtkSourceSnippetContext, gtk_source_snippet_context, G_TYPE_OBJECT)
+
+enum {
+  CHANGED,
+  N_SIGNALS
+};
+
+typedef gchar *(*InputFilter) (const gchar *input);
+
+static GHashTable *filters;
+static guint signals[N_SIGNALS];
+
+/**
+ * gtk_source_snippet_context_new:
+ *
+ * Creates a new #GtkSourceSnippetContext.
+ *
+ * Generally, this isn't needed unless you are controlling the
+ * expansion of snippets manually.
+ *
+ * Returns: (transfer full): a #GtkSourceSnippetContext
+ *
+ * Since: 5.0
+ */
+GtkSourceSnippetContext *
+gtk_source_snippet_context_new (void)
+{
+       return g_object_new (GTK_SOURCE_TYPE_SNIPPET_CONTEXT, NULL);
+}
+
+/**
+ * gtk_source_snippet_context_clear_variables:
+ * @self: a #GtkSourceSnippetContext
+ *
+ * Removes all variables from the context.
+ *
+ * Since: 5.0
+ */
+void
+gtk_source_snippet_context_clear_variables (GtkSourceSnippetContext *self)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self));
+
+       g_hash_table_remove_all (self->variables);
+}
+
+/**
+ * gtk_source_snippet_context_set_variable:
+ * @self: a #GtkSourceSnippetContext
+ * @key: the variable name
+ * @value: the value for the variable
+ *
+ * Sets a variable within the context.
+ *
+ * This variable may be overridden by future updates to the
+ * context.
+ *
+ * Since: 5.0
+ */
+void
+gtk_source_snippet_context_set_variable (GtkSourceSnippetContext *self,
+                                         const gchar             *key,
+                                         const gchar             *value)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self));
+       g_return_if_fail (key);
+
+       g_hash_table_replace (self->variables, g_strdup (key), g_strdup (value));
+}
+
+/**
+ * gtk_source_snippet_context_set_constant:
+ * @self: a #GtkSourceSnippetContext
+ * @key: the constant name
+ * @value: the value of the constant
+ *
+ * Sets a constatnt within the context. This is similar to
+ * a variable set with gtk_source_snippet_context_set_variable()
+ * but is expected to not change during use of the snippet.
+ *
+ * Examples would be the date or users name.
+ *
+ * Since: 5.0
+ */
+void
+gtk_source_snippet_context_set_constant (GtkSourceSnippetContext *self,
+                                         const gchar             *key,
+                                         const gchar             *value)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self));
+       g_return_if_fail (key);
+
+       g_hash_table_replace (self->constants, g_strdup (key), g_strdup (value));
+}
+
+/**
+ * gtk_source_snippet_context_get_variable:
+ * @self: a #GtkSourceSnippetContext
+ * @key: the name of the variable
+ *
+ * Gets the current value for a variable named @key.
+ *
+ * Returns: (transfer none) (nullable): the value for the variable, or %NULL
+ *
+ * Since: 5.0
+ */
+const gchar *
+gtk_source_snippet_context_get_variable (GtkSourceSnippetContext *self,
+                                         const gchar             *key)
+{
+       const gchar *ret;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self), NULL);
+
+       if (!(ret = g_hash_table_lookup (self->variables, key)))
+               ret = g_hash_table_lookup (self->constants, key);
+
+       return ret;
+}
+
+static gchar *
+filter_lower (const gchar *input)
+{
+       return input != NULL ? g_utf8_strdown (input, -1) : NULL;
+}
+
+static gchar *
+filter_upper (const gchar *input)
+{
+       return input != NULL ? g_utf8_strup (input, -1) : NULL;
+}
+
+static gchar *
+filter_capitalize (const gchar *input)
+{
+       gunichar c;
+       GString *str;
+
+       if (input == NULL)
+               return NULL;
+
+       if (*input == 0)
+               return g_strdup ("");
+
+       c = g_utf8_get_char (input);
+       if (g_unichar_isupper (c))
+               return g_strdup (input);
+
+       str = g_string_new (NULL);
+       input = g_utf8_next_char (input);
+       g_string_append_unichar (str, g_unichar_toupper (c));
+       if (*input)
+               g_string_append (str, input);
+
+       return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_uncapitalize (const gchar *input)
+{
+       gunichar c;
+       GString *str;
+
+       if (input == NULL)
+               return NULL;
+
+       c = g_utf8_get_char (input);
+       if (g_unichar_islower (c))
+               return g_strdup (input);
+
+       str = g_string_new (NULL);
+       input = g_utf8_next_char (input);
+       g_string_append_unichar (str, g_unichar_tolower (c));
+       g_string_append (str, input);
+
+       return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_html (const gchar *input)
+{
+       GString *str;
+
+       if (input == NULL)
+               return NULL;
+
+       str = g_string_new (NULL);
+
+       for (; *input; input = g_utf8_next_char (input))
+       {
+               gunichar c = g_utf8_get_char (input);
+
+               switch (c)
+               {
+               case '<':
+                       g_string_append_len (str, "&lt;", 4);
+                       break;
+
+               case '>':
+                       g_string_append_len (str, "&gt;", 4);
+                       break;
+
+               case '&':
+                       g_string_append_len (str, "&amp;", 5);
+                       break;
+
+               default:
+                       g_string_append_unichar (str, c);
+                       break;
+               }
+       }
+
+       return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_camelize (const gchar *input)
+{
+       gboolean next_is_upper = TRUE;
+       gboolean skip = FALSE;
+       GString *str;
+
+       if (input == NULL)
+               return NULL;
+
+       if (!strchr (input, '_') && !strchr (input, ' ') && !strchr (input, '-'))
+               return filter_capitalize (input);
+
+       str = g_string_new (NULL);
+
+       for (; *input; input = g_utf8_next_char (input))
+       {
+               gunichar c = g_utf8_get_char (input);
+
+               switch (c)
+               {
+               case '_':
+               case '-':
+               case ' ':
+                       next_is_upper = TRUE;
+                       skip = TRUE;
+                       break;
+
+               default:
+                       break;
+               }
+
+               if (skip)
+               {
+                       skip = FALSE;
+                       continue;
+               }
+
+               if (next_is_upper)
+               {
+                       c = g_unichar_toupper (c);
+                       next_is_upper = FALSE;
+               }
+               else
+               {
+                       c = g_unichar_tolower (c);
+               }
+
+               g_string_append_unichar (str, c);
+       }
+
+       if (g_str_has_suffix (str->str, "Private"))
+               g_string_truncate (str, str->len - strlen ("Private"));
+
+       return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_functify (const gchar *input)
+{
+       gunichar last = 0;
+       GString *str;
+
+       if (input == NULL)
+               return NULL;
+
+       str = g_string_new (NULL);
+
+       for (; *input; input = g_utf8_next_char (input))
+       {
+               gunichar c = g_utf8_get_char (input);
+               gunichar n = g_utf8_get_char (g_utf8_next_char (input));
+
+               if (last)
+               {
+                       if ((g_unichar_islower (last) && g_unichar_isupper (c)) ||
+                           (g_unichar_isupper (c) && g_unichar_islower (n)))
+                       {
+                               g_string_append_c (str, '_');
+                       }
+               }
+
+               if ((c == ' ') || (c == '-'))
+               {
+                       c = '_';
+               }
+
+               g_string_append_unichar (str, g_unichar_tolower (c));
+
+               last = c;
+       }
+
+       if (g_str_has_suffix (str->str, "_private") ||
+           g_str_has_suffix (str->str, "_PRIVATE"))
+       {
+               g_string_truncate (str, str->len - strlen ("_private"));
+       }
+
+       return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_namespace (const gchar *input)
+{
+       gunichar last = 0;
+       GString *str;
+       gboolean first_is_lower = FALSE;
+
+       if (input == NULL)
+               return NULL;
+
+       str = g_string_new (NULL);
+
+       for (; *input; input = g_utf8_next_char (input))
+       {
+               gunichar c = g_utf8_get_char (input);
+               gunichar n = g_utf8_get_char (g_utf8_next_char (input));
+
+               if (c == '_')
+                       break;
+
+               if (last)
+               {
+                       if ((g_unichar_islower (last) && g_unichar_isupper (c)) ||
+                           (g_unichar_isupper (c) && g_unichar_islower (n)))
+                               break;
+               }
+               else
+               {
+                       first_is_lower = g_unichar_islower (c);
+               }
+
+               if ((c == ' ') || (c == '-'))
+                       break;
+
+               g_string_append_unichar (str, c);
+
+               last = c;
+       }
+
+       if (first_is_lower)
+       {
+               gchar *ret;
+
+               ret = filter_capitalize (str->str);
+               g_string_free (str, TRUE);
+
+               return g_steal_pointer (&ret);
+       }
+
+       return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_class (const gchar *input)
+{
+       gchar *camel;
+       gchar *ns;
+       gchar *ret = NULL;
+
+       if (input == NULL)
+               return NULL;
+
+       camel = filter_camelize (input);
+       ns = filter_namespace (input);
+
+       if (g_str_has_prefix (camel, ns))
+       {
+               ret = g_strdup (camel + strlen (ns));
+       }
+       else
+       {
+               ret = camel;
+               camel = NULL;
+       }
+
+       g_free (camel);
+       g_free (ns);
+
+       return ret;
+}
+
+static gchar *
+filter_instance (const gchar *input)
+{
+       const gchar *tmp;
+       gchar *funct = NULL;
+       gchar *ret;
+
+       if (input == NULL)
+               return NULL;
+
+       if (!strchr (input, '_'))
+       {
+               funct = filter_functify (input);
+               input = funct;
+       }
+
+       if ((tmp = strrchr (input, '_')))
+       {
+               ret = g_strdup (tmp+1);
+       }
+       else
+       {
+               ret = g_strdup (input);
+       }
+
+       g_free (funct);
+
+       return g_steal_pointer (&ret);
+}
+
+static gchar *
+filter_space (const gchar *input)
+{
+       GString *str;
+
+       if (input == NULL)
+               return NULL;
+
+       str = g_string_new (NULL);
+       for (; *input; input = g_utf8_next_char (input))
+               g_string_append_c (str, ' ');
+
+       return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_descend_path (const gchar *input)
+{
+       const gchar *pos;
+
+       if (input == NULL)
+               return NULL;
+
+       while (*input == G_DIR_SEPARATOR)
+       {
+               input++;
+       }
+
+       if ((pos = strchr (input, G_DIR_SEPARATOR)))
+       {
+               return g_strdup (pos + 1);
+       }
+
+       return NULL;
+}
+
+static gchar *
+filter_stripsuffix (const gchar *input)
+{
+       const gchar *endpos;
+
+       if (input == NULL)
+               return NULL;
+
+       endpos = strrchr (input, '.');
+
+       if (endpos != NULL)
+       {
+               return g_strndup (input, (endpos - input));
+       }
+
+       return g_strdup (input);
+}
+
+static gchar *
+filter_slash_to_dots (const gchar *input)
+{
+       GString *str;
+
+       if (input == NULL)
+               return NULL;
+
+       str = g_string_new (NULL);
+
+       for (; *input; input = g_utf8_next_char (input))
+       {
+               gunichar ch = g_utf8_get_char (input);
+
+               if (ch == G_DIR_SEPARATOR)
+               {
+                       g_string_append_c (str, '.');
+               }
+               else
+               {
+                       g_string_append_unichar (str, ch);
+               }
+       }
+
+       return g_string_free (str, FALSE);
+}
+
+static gchar *
+apply_filter (gchar       *input,
+              const gchar *filter)
+{
+       InputFilter filter_func;
+
+       if ((filter_func = g_hash_table_lookup (filters, filter)))
+       {
+               gchar *tmp = input;
+               input = filter_func (input);
+               g_free (tmp);
+       }
+
+       return input;
+}
+
+static gchar *
+apply_filters (GString     *str,
+               const gchar *filters_list)
+{
+       gchar **filter_names;
+       gchar *input = g_string_free (str, FALSE);
+
+       filter_names = g_strsplit (filters_list, "|", 0);
+       for (guint i = 0; filter_names[i]; i++)
+               input = apply_filter (input, filter_names[i]);
+
+       g_strfreev (filter_names);
+
+       return input;
+}
+
+static gchar *
+scan_forward (const gchar  *input,
+              const gchar **endpos,
+              gunichar      needle)
+{
+       const gchar *begin = input;
+
+       for (; *input; input = g_utf8_next_char (input))
+       {
+               gunichar c = g_utf8_get_char (input);
+
+               if (c == needle)
+               {
+                       *endpos = input;
+                       return g_strndup (begin, (input - begin));
+               }
+       }
+
+       *endpos = NULL;
+
+       return NULL;
+}
+
+gchar *
+gtk_source_snippet_context_expand (GtkSourceSnippetContext *self,
+                                   const gchar             *input)
+{
+       const gchar *expand;
+       gboolean is_dynamic;
+       GString *str;
+       gchar key[12];
+       glong n;
+       gint i;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self), NULL);
+       g_return_val_if_fail (input, NULL);
+
+       is_dynamic = (*input == '$');
+
+       str = g_string_new (NULL);
+
+       for (; *input; input = g_utf8_next_char (input))
+       {
+               gunichar c = g_utf8_get_char (input);
+
+               if (c == '\\')
+               {
+                       input = g_utf8_next_char (input);
+                       if (!*input)
+                               break;
+                       c = g_utf8_get_char (input);
+               }
+               else if (is_dynamic && c == '$')
+               {
+                       input = g_utf8_next_char (input);
+
+                       if (!*input)
+                               break;
+
+                       c = g_utf8_get_char (input);
+
+                       if (g_unichar_isdigit (c))
+                       {
+                               errno = 0;
+                               n = strtol (input, (gchar * *) &input, 10);
+                               if (((n == LONG_MIN) || (n == LONG_MAX)) && errno == ERANGE)
+                                       break;
+                               input--;
+                               g_snprintf (key, sizeof key, "%ld", n);
+                               key[sizeof key - 1] = '\0';
+                               expand = gtk_source_snippet_context_get_variable (self, key);
+                               if (expand)
+                                       g_string_append (str, expand);
+                               continue;
+                       }
+                       else
+                       {
+                               if (strchr (input, '|'))
+                               {
+                                       gchar *lkey;
+
+                                       lkey = g_strndup (input, strchr (input, '|') - input);
+                                       expand = gtk_source_snippet_context_get_variable (self, lkey);
+                                       g_free (lkey);
+
+                                       if (expand)
+                                       {
+                                               g_string_append (str, expand);
+                                               input = strchr (input, '|') - 1;
+                                       }
+                                       else
+                                       {
+                                               input += strlen (input) - 1;
+                                       }
+                               }
+                               else
+                               {
+                                       expand = gtk_source_snippet_context_get_variable (self, input);
+
+                                       if (expand)
+                                       {
+                                               g_string_append (str, expand);
+                                       }
+                                       else
+                                       {
+                                               g_string_append_c (str, '$');
+                                               g_string_append (str, input);
+                                       }
+
+                                       input += strlen (input) - 1;
+                               }
+
+                               continue;
+                       }
+               }
+               else if (is_dynamic && c == '|')
+               {
+                       return apply_filters (str, input + 1);
+               }
+               else if (c == '`')
+               {
+                       const gchar *endpos = NULL;
+                       gchar *slice;
+
+                       slice = scan_forward (input + 1, &endpos, '`');
+
+                       if (slice)
+                       {
+                               gchar *expanded;
+
+                               input = endpos;
+
+                               expanded = gtk_source_snippet_context_expand (self, slice);
+
+                               g_string_append (str, expanded);
+
+                               g_free (expanded);
+                               g_free (slice);
+
+                               continue;
+                       }
+               }
+               else if (c == '\t')
+               {
+                       if (self->use_spaces)
+                       {
+                               for (i = 0; i < self->tab_width; i++)
+                                       g_string_append_c (str, ' ');
+                       }
+                       else
+                       {
+                               g_string_append_c (str, '\t');
+                       }
+
+                       continue;
+               }
+               else if (c == '\n')
+               {
+                       g_string_append_c (str, '\n');
+
+                       if (self->line_prefix)
+                       {
+                               g_string_append (str, self->line_prefix);
+                       }
+
+                       continue;
+               }
+
+               g_string_append_unichar (str, c);
+       }
+
+       return g_string_free (str, FALSE);
+}
+
+void
+gtk_source_snippet_context_set_tab_width (GtkSourceSnippetContext *self,
+                                          gint                     tab_width)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self));
+
+       if (tab_width != self->tab_width)
+       {
+               self->tab_width = tab_width;
+       }
+}
+
+void
+gtk_source_snippet_context_set_use_spaces (GtkSourceSnippetContext *self,
+                                           gboolean                 use_spaces)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self));
+
+       use_spaces = !!use_spaces;
+
+       if (self->use_spaces != use_spaces)
+       {
+               self->use_spaces = use_spaces;
+       }
+}
+
+void
+gtk_source_snippet_context_set_line_prefix (GtkSourceSnippetContext *self,
+                                            const gchar             *line_prefix)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self));
+
+       if (g_strcmp0 (line_prefix, self->line_prefix) != 0)
+       {
+               g_free (self->line_prefix);
+               self->line_prefix = g_strdup (line_prefix);
+       }
+}
+
+void
+_gtk_source_snippet_context_emit_changed (GtkSourceSnippetContext *self)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self));
+
+       g_signal_emit (self, signals [CHANGED], 0);
+}
+
+static void
+gtk_source_snippet_context_finalize (GObject *object)
+{
+       GtkSourceSnippetContext *self = (GtkSourceSnippetContext *)object;
+
+       g_clear_pointer (&self->constants, g_hash_table_unref);
+       g_clear_pointer (&self->variables, g_hash_table_unref);
+       g_clear_pointer (&self->line_prefix, g_free);
+
+       G_OBJECT_CLASS (gtk_source_snippet_context_parent_class)->finalize (object);
+}
+
+static void
+gtk_source_snippet_context_class_init (GtkSourceSnippetContextClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->finalize = gtk_source_snippet_context_finalize;
+
+       /**
+        * GtkSourceSnippetContext::changed:
+        *
+        * The "changed" signal is emitted when a change has been
+        * discovered in one of the chunks of the snippet which has
+        * caused a variable or other dynamic data within the context
+        * to have changed.
+        *
+        * Since: 5.0
+        */
+       signals[CHANGED] =
+               g_signal_new ("changed",
+                             G_TYPE_FROM_CLASS (klass),
+                             G_SIGNAL_RUN_FIRST,
+                             0,
+                             NULL, NULL, NULL,
+                             G_TYPE_NONE,
+                             0);
+
+       filters = g_hash_table_new (g_str_hash, g_str_equal);
+       g_hash_table_insert (filters, (gpointer) "lower", filter_lower);
+       g_hash_table_insert (filters, (gpointer) "upper", filter_upper);
+       g_hash_table_insert (filters, (gpointer) "capitalize", filter_capitalize);
+       g_hash_table_insert (filters, (gpointer) "decapitalize", filter_uncapitalize);
+       g_hash_table_insert (filters, (gpointer) "uncapitalize", filter_uncapitalize);
+       g_hash_table_insert (filters, (gpointer) "html", filter_html);
+       g_hash_table_insert (filters, (gpointer) "camelize", filter_camelize);
+       g_hash_table_insert (filters, (gpointer) "functify", filter_functify);
+       g_hash_table_insert (filters, (gpointer) "namespace", filter_namespace);
+       g_hash_table_insert (filters, (gpointer) "class", filter_class);
+       g_hash_table_insert (filters, (gpointer) "space", filter_space);
+       g_hash_table_insert (filters, (gpointer) "stripsuffix", filter_stripsuffix);
+       g_hash_table_insert (filters, (gpointer) "instance", filter_instance);
+       g_hash_table_insert (filters, (gpointer) "slash_to_dots", filter_slash_to_dots);
+       g_hash_table_insert (filters, (gpointer) "descend_path", filter_descend_path);
+}
+
+static void
+gtk_source_snippet_context_init (GtkSourceSnippetContext *self)
+{
+       GDateTime *dt;
+       gchar *str;
+
+       self->variables = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+       self->constants = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+#define ADD_CONSTANT(k, v) \
+       g_hash_table_insert (self->constants, g_strdup (k), g_strdup (v))
+
+       ADD_CONSTANT ("username", g_get_user_name ());
+       ADD_CONSTANT ("fullname", g_get_real_name ());
+       ADD_CONSTANT ("author", g_get_real_name ());
+
+       dt = g_date_time_new_now_local ();
+       str = g_date_time_format (dt, "%Y");
+       ADD_CONSTANT ("year", str);
+       g_free (str);
+       str = g_date_time_format (dt, "%b");
+       ADD_CONSTANT ("shortmonth", str);
+       g_free (str);
+       str = g_date_time_format (dt, "%d");
+       ADD_CONSTANT ("day", str);
+       g_free (str);
+       str = g_date_time_format (dt, "%a");
+       ADD_CONSTANT ("shortweekday", str);
+       g_free (str);
+       g_date_time_unref (dt);
+
+       ADD_CONSTANT ("email", "unknown@localhost");
+
+#undef ADD_CONSTANT
+}
diff --git a/gtksourceview/gtksourcesnippetcontext.h b/gtksourceview/gtksourcesnippetcontext.h
new file mode 100644
index 00000000..14e569d6
--- /dev/null
+++ b/gtksourceview/gtksourcesnippetcontext.h
@@ -0,0 +1,65 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2020 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/>.
+ */
+
+#pragma once
+
+#if !defined (GTK_SOURCE_H_INSIDE) && !defined (GTK_SOURCE_COMPILATION)
+#error "Only <gtksourceview/gtksource.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+#include "gtksourcetypes.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_SNIPPET_CONTEXT (gtk_source_snippet_context_get_type())
+
+GTK_SOURCE_AVAILABLE_IN_5_0
+G_DECLARE_FINAL_TYPE (GtkSourceSnippetContext, gtk_source_snippet_context, GTK_SOURCE, SNIPPET_CONTEXT, 
GObject)
+
+GTK_SOURCE_AVAILABLE_IN_5_0
+GtkSourceSnippetContext *gtk_source_snippet_context_new             (void);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                     gtk_source_snippet_context_clear_variables (GtkSourceSnippetContext *self);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                     gtk_source_snippet_context_set_variable    (GtkSourceSnippetContext *self,
+                                                                     const gchar             *key,
+                                                                     const gchar             *value);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                     gtk_source_snippet_context_set_constant    (GtkSourceSnippetContext *self,
+                                                                     const gchar             *key,
+                                                                     const gchar             *value);
+GTK_SOURCE_AVAILABLE_IN_5_0
+const gchar             *gtk_source_snippet_context_get_variable    (GtkSourceSnippetContext *self,
+                                                                     const gchar             *key);
+GTK_SOURCE_AVAILABLE_IN_5_0
+gchar                   *gtk_source_snippet_context_expand          (GtkSourceSnippetContext *self,
+                                                                     const gchar             *input);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                     gtk_source_snippet_context_set_tab_width   (GtkSourceSnippetContext *self,
+                                                                     gint                     tab_width);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                     gtk_source_snippet_context_set_use_spaces  (GtkSourceSnippetContext *self,
+                                                                     gboolean                 use_spaces);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                     gtk_source_snippet_context_set_line_prefix (GtkSourceSnippetContext *self,
+                                                                     const gchar             *line_prefix);
+
+G_END_DECLS
diff --git a/gtksourceview/gtksourcestylescheme-private.h b/gtksourceview/gtksourcestylescheme-private.h
index a7d6c641..5fd56f24 100644
--- a/gtksourceview/gtksourcestylescheme-private.h
+++ b/gtksourceview/gtksourcestylescheme-private.h
@@ -41,6 +41,8 @@ void                  _gtk_source_style_scheme_unapply                      (Gtk
 G_GNUC_INTERNAL
 GtkSourceStyle       *_gtk_source_style_scheme_get_matching_brackets_style  (GtkSourceStyleScheme *scheme);
 G_GNUC_INTERNAL
+GtkSourceStyle       *_gtk_source_style_scheme_get_snippet_focus_style      (GtkSourceStyleScheme *scheme);
+G_GNUC_INTERNAL
 GtkSourceStyle       *_gtk_source_style_scheme_get_right_margin_style       (GtkSourceStyleScheme *scheme);
 G_GNUC_INTERNAL
 GtkSourceStyle       *_gtk_source_style_scheme_get_draw_spaces_style        (GtkSourceStyleScheme *scheme);
diff --git a/gtksourceview/gtksourcestylescheme.c b/gtksourceview/gtksourcestylescheme.c
index 94a14c9e..0ee340da 100644
--- a/gtksourceview/gtksourcestylescheme.c
+++ b/gtksourceview/gtksourcestylescheme.c
@@ -62,6 +62,7 @@
 #define STYLE_CURRENT_LINE_NUMBER      "current-line-number"
 #define STYLE_RIGHT_MARGIN             "right-margin"
 #define STYLE_DRAW_SPACES              "draw-spaces"
+#define STYLE_SNIPPET_FOCUS            "snippet-focus"
 #define STYLE_BACKGROUND_PATTERN       "background-pattern"
 
 #define STYLE_SCHEME_VERSION           "1.0"
@@ -618,6 +619,14 @@ _gtk_source_style_scheme_get_draw_spaces_style (GtkSourceStyleScheme *scheme)
        return gtk_source_style_scheme_get_style (scheme, STYLE_DRAW_SPACES);
 }
 
+GtkSourceStyle *
+_gtk_source_style_scheme_get_snippet_focus_style (GtkSourceStyleScheme *scheme)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_STYLE_SCHEME (scheme), NULL);
+
+       return gtk_source_style_scheme_get_style (scheme, STYLE_SNIPPET_FOCUS);
+}
+
 static gboolean
 get_color (GtkSourceStyle *style,
            gboolean        foreground,
diff --git a/gtksourceview/gtksourcetypes.h b/gtksourceview/gtksourcetypes.h
index 3ac09423..8cf43b47 100644
--- a/gtksourceview/gtksourcetypes.h
+++ b/gtksourceview/gtksourcetypes.h
@@ -59,6 +59,9 @@ typedef struct _GtkSourceMark                      GtkSourceMark;
 typedef struct _GtkSourcePrintCompositor           GtkSourcePrintCompositor;
 typedef struct _GtkSourceSearchContext             GtkSourceSearchContext;
 typedef struct _GtkSourceSearchSettings            GtkSourceSearchSettings;
+typedef struct _GtkSourceSnippet                   GtkSourceSnippet;
+typedef struct _GtkSourceSnippetChunk              GtkSourceSnippetChunk;
+typedef struct _GtkSourceSnippetContext            GtkSourceSnippetContext;
 typedef struct _GtkSourceSpaceDrawer               GtkSourceSpaceDrawer;
 typedef struct _GtkSourceStyle                     GtkSourceStyle;
 typedef struct _GtkSourceStyleSchemeChooserButton  GtkSourceStyleSchemeChooserButton;
diff --git a/gtksourceview/gtksourceview.c b/gtksourceview/gtksourceview.c
index ddc82eb1..3c316fd7 100644
--- a/gtksourceview/gtksourceview.c
+++ b/gtksourceview/gtksourceview.c
@@ -31,25 +31,29 @@
 #include <pango/pango-tabs.h>
 #include <glib/gi18n-lib.h>
 
-#include "gtksourcebuffer.h"
+#include "gtksource-enumtypes.h"
+#include "gtksource-marshal.h"
+
 #include "gtksourcebuffer-private.h"
+#include "gtksourcebuffer.h"
 #include "gtksourcebufferinternal-private.h"
+#include "gtksourcecompletion-private.h"
 #include "gtksourcecompletion.h"
-#include "gtksourcegutter.h"
+#include "gtksourcecompletionprovider.h"
 #include "gtksourcegutter-private.h"
+#include "gtksourcegutter.h"
 #include "gtksourcegutterrendererlines-private.h"
 #include "gtksourcegutterrenderermarks-private.h"
-#include "gtksource-enumtypes.h"
+#include "gtksourceiter-private.h"
 #include "gtksourcemark.h"
 #include "gtksourcemarkattributes.h"
-#include "gtksource-marshal.h"
-#include "gtksourcestylescheme-private.h"
-#include "gtksourcecompletion-private.h"
-#include "gtksourcecompletionprovider.h"
-#include "gtksourceiter-private.h"
 #include "gtksourcesearchcontext-private.h"
-#include "gtksourcespacedrawer.h"
+#include "gtksourcesnippet-private.h"
+#include "gtksourcesnippetchunk.h"
+#include "gtksourcesnippetcontext.h"
 #include "gtksourcespacedrawer-private.h"
+#include "gtksourcespacedrawer.h"
+#include "gtksourcestylescheme-private.h"
 
 /**
  * SECTION:view
@@ -156,6 +160,8 @@ enum
        MOVE_LINES,
        MOVE_TO_MATCHING_BRACKET,
        MOVE_WORDS,
+       PUSH_SNIPPET,
+       POP_SNIPPET,
        SHOW_COMPLETION,
        SMART_HOME_END,
        N_SIGNALS
@@ -203,6 +209,8 @@ typedef struct
 
        GtkSourceCompletion *completion;
 
+       GQueue snippets;
+
        guint right_margin_pos;
        gint cached_right_margin_pos;
        guint tab_width;
@@ -311,6 +319,10 @@ static gboolean       gtk_source_view_drag_drop            (GtkDropTarget
                                                             int                      y,
                                                             GtkSourceView           *view);
 static void           gtk_source_view_populate_extra_menu  (GtkSourceView           *view);
+static void           gtk_source_view_real_push_snippet    (GtkSourceView           *view,
+                                                            GtkSourceSnippet        *snippet,
+                                                            GtkTextIter             *location);
+static void           gtk_source_view_pop_snippet          (GtkSourceView           *view);
 
 static void
 gtk_source_view_constructed (GObject *object)
@@ -506,6 +518,7 @@ gtk_source_view_class_init (GtkSourceViewClass *klass)
        klass->show_completion = gtk_source_view_show_completion_real;
        klass->move_lines = gtk_source_view_move_lines;
        klass->move_words = gtk_source_view_move_words;
+       klass->push_snippet = gtk_source_view_real_push_snippet;
 
        /**
         * GtkSourceView:completion:
@@ -819,6 +832,56 @@ gtk_source_view_class_init (GtkSourceViewClass *klass)
                                    G_TYPE_FROM_CLASS (klass),
                                    g_cclosure_marshal_VOID__INTv);
 
+       /**
+        * GtkSourceView::push-snippet:
+        * @view: a #GtkSourceView
+        * @snippet: a #GtkSourceSnippet
+        * @location: (inout): a #GtkTextIter
+        *
+        * The ::push-snippet signal is emitted to insert a new snippet into
+        * the view. If another snippet was active, it will be paused until all
+        * focus positions of @snippet have been exhausted.
+        *
+        * @location will be updated to point at the end of the snippet.
+        *
+        * Since: 5.0
+        */
+       signals[PUSH_SNIPPET] =
+               g_signal_new ("push-snippet",
+                             G_TYPE_FROM_CLASS (klass),
+                             G_SIGNAL_RUN_LAST,
+                             G_STRUCT_OFFSET (GtkSourceViewClass, push_snippet),
+                             NULL, NULL,
+                             _gtk_source_marshal_VOID__OBJECT_BOXED,
+                             G_TYPE_NONE,
+                             2,
+                             GTK_SOURCE_TYPE_SNIPPET,
+                             GTK_TYPE_TEXT_ITER);
+       g_signal_set_va_marshaller (signals[PUSH_SNIPPET],
+                                   G_TYPE_FROM_CLASS (klass),
+                                   _gtk_source_marshal_VOID__OBJECT_BOXEDv);
+
+       /**
+        * GtkSourceView::pop-snippet:
+        * @view: a #GtkSourceView
+        * @snippet: a #GtkSourceSnippet
+        *
+        * The ::pop-snippet signal is emitted when a snippet's focus positions
+        * have been exhausted.
+        *
+        * Since: 5.0
+        */
+       signals[POP_SNIPPET] =
+               g_signal_new ("pop-snippet",
+                             G_TYPE_FROM_CLASS (klass),
+                             G_SIGNAL_RUN_LAST,
+                             G_STRUCT_OFFSET (GtkSourceViewClass, pop_snippet),
+                             NULL, NULL,
+                             NULL,
+                             G_TYPE_NONE,
+                             1,
+                             GTK_SOURCE_TYPE_SNIPPET);
+
        /**
         * GtkSourceView::smart-home-end:
         * @view: the #GtkSourceView
@@ -3837,12 +3900,88 @@ do_ctrl_backspace (GtkSourceView *view)
        return FALSE;
 }
 
+static GtkSourceSnippet *
+lookup_snippet_by_trigger (GtkSourceView *view,
+                           const gchar   *word)
+{
+       GtkSourceSnippet *snippet;
+       GtkSourceSnippetChunk *chunk;
+
+       g_assert (GTK_SOURCE_IS_VIEW (view));
+       g_assert (word != NULL);
+
+       snippet = gtk_source_snippet_new (word, NULL);
+
+       for (guint i = 0; i < 3; i++)
+       {
+               chunk = gtk_source_snippet_chunk_new ();
+               gtk_source_snippet_chunk_set_spec (chunk, word);
+               gtk_source_snippet_chunk_set_focus_position (chunk, i+1);
+               gtk_source_snippet_add_chunk (snippet, chunk);
+               g_object_unref (chunk);
+       }
+
+       chunk = gtk_source_snippet_chunk_new ();
+       gtk_source_snippet_chunk_set_focus_position (chunk, 0);
+       gtk_source_snippet_add_chunk (snippet, chunk);
+       g_object_unref (chunk);
+
+       return snippet;
+}
+
+static gboolean
+gtk_source_view_try_expand_snippet (GtkSourceView *view,
+                                    GtkTextIter   *iter)
+{
+       GtkSourceSnippet *snippet;
+       GtkTextIter begin;
+       gchar *word;
+
+       g_assert (GTK_SOURCE_IS_VIEW (view));
+       g_assert (iter != NULL);
+
+       if (gtk_text_iter_starts_line (iter) ||
+           !_gtk_source_iter_ends_full_word (iter))
+       {
+               return FALSE;
+       }
+
+       begin = *iter;
+
+       _gtk_source_iter_backward_full_word_start (&begin);
+
+       if (gtk_text_iter_compare (&begin, iter) >= 0)
+       {
+               return FALSE;
+       }
+
+       word = gtk_text_iter_get_slice (&begin, iter);
+
+       if (word == NULL || *word == 0)
+       {
+               return FALSE;
+       }
+
+       snippet = lookup_snippet_by_trigger (view, word);
+
+       g_free (word);
+
+       if (snippet != NULL)
+       {
+               gtk_source_view_push_snippet (view, snippet, iter);
+               g_object_unref (snippet);
+               return TRUE;
+       }
+
+       return FALSE;
+}
+
 static gboolean
 gtk_source_view_key_pressed (GtkSourceView         *view,
-                            guint                  key,
-                            guint                  keycode,
-                            guint                  state,
-                            GtkEventControllerKey *controller)
+                             guint                  key,
+                             guint                  keycode,
+                             guint                  state,
+                             GtkEventControllerKey *controller)
 {
        GtkSourceViewPrivate *priv = gtk_source_view_get_instance_private (view);
        GtkTextBuffer *buf;
@@ -3908,11 +4047,47 @@ gtk_source_view_key_pressed (GtkSourceView         *view,
            editable &&
            gtk_text_view_get_accepts_tab (GTK_TEXT_VIEW (view)))
        {
+               GtkSourceSnippet *snippet = g_queue_peek_head (&priv->snippets);
                GtkTextIter s, e;
                gboolean has_selection;
 
                has_selection = gtk_text_buffer_get_selection_bounds (buf, &s, &e);
 
+               /* If we already have a snippet expanded, then we might need
+                * to move forward or backward between snippet positions.
+                */
+               if (snippet != NULL)
+               {
+                       if (state == 0)
+                       {
+                               if (!_gtk_source_snippet_move_next (snippet))
+                               {
+                                       gtk_source_view_pop_snippet (view);
+                               }
+
+                               return GDK_EVENT_STOP;
+                       }
+                       else if (state & GDK_SHIFT_MASK)
+                       {
+                               if (!_gtk_source_snippet_move_previous (snippet))
+                               {
+                                       gtk_source_view_pop_snippet (view);
+                               }
+
+                               return GDK_EVENT_STOP;
+                       }
+               }
+
+               /* tab: if there is no selection and the current word is a
+                * snippet trigger, then we should expand that snippet.
+                */
+               if (state == 0 &&
+                   !has_selection &&
+                   gtk_source_view_try_expand_snippet (view, &e))
+               {
+                       return GDK_EVENT_STOP;
+               }
+
                if (priv->indent_on_tab)
                {
                        /* shift+tab: always unindent */
@@ -4931,3 +5106,173 @@ gtk_source_view_queue_draw (GtkSourceView *view)
                _gtk_source_gutter_queue_draw (priv->right_gutter);
        }
 }
+
+static gchar *
+get_line_prefix (const GtkTextIter *iter)
+{
+       GtkTextIter begin;
+       GString *str;
+
+       g_assert (iter != NULL);
+
+       if (gtk_text_iter_starts_line (iter))
+       {
+               return NULL;
+       }
+
+       begin = *iter;
+       gtk_text_iter_set_line_offset (&begin, 0);
+
+       str = g_string_new (NULL);
+
+       do
+       {
+               gunichar c = gtk_text_iter_get_char (&begin);
+
+               switch (c)
+               {
+               case '\t':
+               case ' ':
+                       g_string_append_unichar (str, c);
+                       break;
+
+               default:
+                       g_string_append_c (str, ' ');
+                       break;
+               }
+       }
+       while (gtk_text_iter_forward_char (&begin) &&
+              (gtk_text_iter_compare (&begin, iter) < 0));
+
+       return g_string_free (str, FALSE);
+}
+
+static void
+gtk_source_view_real_push_snippet (GtkSourceView    *view,
+                                   GtkSourceSnippet *snippet,
+                                   GtkTextIter      *location)
+{
+       GtkSourceViewPrivate *priv = gtk_source_view_get_instance_private (view);
+       GtkTextBuffer *buffer;
+       gboolean more_to_focus;
+
+       g_assert (GTK_SOURCE_IS_VIEW (view));
+       g_assert (GTK_SOURCE_IS_SNIPPET (snippet));
+       g_assert (location != NULL);
+
+       buffer = gtk_text_iter_get_buffer (location);
+
+       g_queue_push_head (&priv->snippets, g_object_ref (snippet));
+
+       gtk_text_buffer_begin_user_action (buffer);
+       more_to_focus = _gtk_source_snippet_begin (snippet, buffer, location);
+       gtk_text_buffer_end_user_action (buffer);
+
+       gtk_text_view_scroll_mark_onscreen (GTK_TEXT_VIEW (view),
+                                           gtk_text_buffer_get_insert (buffer));
+
+       if (!more_to_focus)
+       {
+               gtk_source_view_pop_snippet (view);
+       }
+}
+
+/**
+ * gtk_source_view_push_snippet:
+ * @view: a #GtkSourceView
+ * @snippet: a #GtkSourceSnippet
+ * @location: (nullable): a #GtkTextIter or %NULL for the cursor position
+ *
+ * Inserts a new snippet at @location
+ *
+ * If another snippet was already active, it will be paused and the new
+ * snippet will become active. Once the focus positions of @snippet have
+ * been exhausted, editing will return to the previous snippet.
+ *
+ * Since: 5.0
+ */
+void
+gtk_source_view_push_snippet (GtkSourceView    *view,
+                              GtkSourceSnippet *snippet,
+                              GtkTextIter      *location)
+{
+       GtkSourceViewPrivate *priv = gtk_source_view_get_instance_private (view);
+       GtkSourceSnippetContext *context;
+       GtkSourceSnippet *previous_snippet;
+       GtkTextBuffer *buffer;
+       GtkTextIter iter;
+       gboolean use_spaces;
+       gchar *prefix;
+       gint tab_width;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIEW (view));
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (snippet));
+
+       previous_snippet = g_queue_peek_head (&priv->snippets);
+
+       if (previous_snippet != NULL)
+       {
+               _gtk_source_snippet_pause (previous_snippet);
+       }
+
+       buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
+
+       if (location == NULL)
+       {
+               gtk_text_buffer_get_iter_at_mark (buffer,
+                                                 &iter,
+                                                 gtk_text_buffer_get_insert (buffer));
+               location = &iter;
+       }
+
+       g_return_if_fail (gtk_text_iter_get_buffer (location) == buffer);
+
+       context = gtk_source_snippet_get_context (snippet);
+
+       use_spaces = gtk_source_view_get_insert_spaces_instead_of_tabs (view);
+       gtk_source_snippet_context_set_use_spaces (context, use_spaces);
+
+       tab_width = gtk_source_view_get_tab_width (view);
+       gtk_source_snippet_context_set_tab_width (context, tab_width);
+
+       prefix = get_line_prefix (location);
+       gtk_source_snippet_context_set_line_prefix (context, prefix);
+       g_free (prefix);
+
+       g_signal_emit (view, signals [PUSH_SNIPPET], 0, snippet, location);
+}
+
+static void
+gtk_source_view_pop_snippet (GtkSourceView *view)
+{
+       GtkSourceViewPrivate *priv = gtk_source_view_get_instance_private (view);
+       GtkSourceSnippet *next_snippet;
+       GtkSourceSnippet *snippet;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIEW (view));
+
+       snippet = g_queue_pop_head (&priv->snippets);
+
+       if (snippet == NULL)
+       {
+               return;
+       }
+
+       next_snippet = g_queue_peek_head (&priv->snippets);
+
+       if (next_snippet != NULL)
+       {
+               gchar *new_text;
+
+               new_text = _gtk_source_snippet_get_edited_text (snippet);
+               _gtk_source_snippet_replace_current_chunk_text (next_snippet, new_text);
+               _gtk_source_snippet_unpause (next_snippet);
+               _gtk_source_snippet_move_next (next_snippet);
+
+               g_free (new_text);
+       }
+
+       g_signal_emit (view, signals[POP_SNIPPET], 0, snippet);
+
+       g_object_unref (snippet);
+}
diff --git a/gtksourceview/gtksourceview.h b/gtksourceview/gtksourceview.h
index af1a7463..a7a719c2 100644
--- a/gtksourceview/gtksourceview.h
+++ b/gtksourceview/gtksourceview.h
@@ -96,6 +96,11 @@ struct _GtkSourceViewClass
                                     gboolean           down);
        void (*move_words)          (GtkSourceView     *view,
                                     gint               step);
+       void (*push_snippet)        (GtkSourceView     *view,
+                                    GtkSourceSnippet  *snippet,
+                                    GtkTextIter       *location);
+       void (*pop_snippet)         (GtkSourceView     *view,
+                                    GtkSourceSnippet  *snippet);
 
        /*< private >*/
        gpointer _reserved[20];
@@ -197,5 +202,9 @@ GTK_SOURCE_AVAILABLE_IN_3_16
 GtkSourceBackgroundPatternType  gtk_source_view_get_background_pattern            (GtkSourceView             
     *view);
 GTK_SOURCE_AVAILABLE_IN_3_24
 GtkSourceSpaceDrawer           *gtk_source_view_get_space_drawer                  (GtkSourceView             
     *view);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                            gtk_source_view_push_snippet                      (GtkSourceView             
     *view,
+                                                                                   GtkSourceSnippet          
     *snippet,
+                                                                                   GtkTextIter               
     *location);
 
 G_END_DECLS
diff --git a/gtksourceview/meson.build b/gtksourceview/meson.build
index f28546a8..7aca6f1b 100644
--- a/gtksourceview/meson.build
+++ b/gtksourceview/meson.build
@@ -33,6 +33,9 @@ core_public_h = files([
   'gtksourceregion.h',
   'gtksourcesearchcontext.h',
   'gtksourcesearchsettings.h',
+  'gtksourcesnippet.h',
+  'gtksourcesnippetchunk.h',
+  'gtksourcesnippetcontext.h',
   'gtksourcespacedrawer.h',
   'gtksourcestyle.h',
   'gtksourcestylescheme.h',
@@ -73,6 +76,9 @@ core_public_c = files([
   'gtksourceregion.c',
   'gtksourcesearchcontext.c',
   'gtksourcesearchsettings.c',
+  'gtksourcesnippet.c',
+  'gtksourcesnippetchunk.c',
+  'gtksourcesnippetcontext.c',
   'gtksourcespacedrawer.c',
   'gtksourcestyle.c',
   'gtksourcestylescheme.c',


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