[gtksourceview/wip/chergert/snippets: 3/4] snippet: implement basic snippet engine
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gtksourceview/wip/chergert/snippets: 3/4] snippet: implement basic snippet engine
- Date: Mon, 27 Jan 2020 22:38:34 +0000 (UTC)
commit ae13af26451689ecba9d027ea0618c6bad53d225
Author: Christian Hergert <chergert redhat com>
Date: Fri Jan 17 16:12:00 2020 -0800
snippet: implement basic snippet engine
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.
- gtksourceview-snippets.c contains integration bits to be hooked into
GtkSourceView. Keeping much of this outside of gtksourceview.c helps
to keep things mostly self-contained but also ensures that we don't keep
growing gtksourceview.c with complexity and size. Future additions to
gtksourceview.c should be done this way when it makes sense (such as
adding indenters).
- To come is support for snippet files such as we have for language specs
and style schemes. Some work needs to be done to either port the syntax
from Builder, or to use XML like we do elsewhere in GtkSourceView. This
is future work that will build on this snippet engine.
- A completion provider should also be added that can use snippets once a
snippet manager has been implemented. Some applications may want to use
this to show additional information about a snippet.
- Tabbing will expand the snippet based on the current word.
- The classic 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.
- Applications can insert snippets using gtk_source_view_push_snippet().
- Using the mouse or touch input to move to another chunk will cause it to
be focused (and selected). Moving between chunks manually will cause the
snippet to be released.
data/styles/classic.xml | 2 +
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 | 64 ++
gtksourceview/gtksourcesnippet.c | 1324 +++++++++++++++++++++++
gtksourceview/gtksourcesnippet.h | 70 ++
gtksourceview/gtksourcesnippetchunk-private.h | 52 +
gtksourceview/gtksourcesnippetchunk.c | 477 ++++++++
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-private.h | 52 +
gtksourceview/gtksourceview-snippets.c | 513 +++++++++
gtksourceview/gtksourceview.c | 189 +++-
gtksourceview/gtksourceview.h | 7 +
gtksourceview/meson.build | 7 +
23 files changed, 3882 insertions(+), 15 deletions(-)
---
diff --git a/data/styles/classic.xml b/data/styles/classic.xml
index 379ede77..e8fcf983 100644
--- a/data/styles/classic.xml
+++ b/data/styles/classic.xml
@@ -127,6 +127,8 @@
<style name="sh:variable" foreground="#6A5ACD"/>
+ <style name="snippet-focus" background="gray"/>
+
<!-- legacy styles for old lang files -->
<style name="Others" foreground="#2E8B57" bold="true"/>
<style name="Others 2" foreground="#008B8B"/>
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..b790b659
--- /dev/null
+++ b/gtksourceview/gtksourcesnippet-private.h
@@ -0,0 +1,64 @@
+/*
+ * 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
+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_after_insert_text (GtkSourceSnippet *self,
+ GtkTextBuffer *buffer,
+ GtkTextIter *iter,
+ const gchar *text,
+ gint len);
+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
+guint _gtk_source_snippet_count_affected_chunks (GtkSourceSnippet *snippet,
+ const GtkTextIter *begin,
+ const GtkTextIter *end);
+G_GNUC_INTERNAL
+gboolean _gtk_source_snippet_contains_range (GtkSourceSnippet *snippet,
+ const GtkTextIter *begin,
+ const GtkTextIter *end);
+
+G_END_DECLS
diff --git a/gtksourceview/gtksourcesnippet.c b/gtksourceview/gtksourcesnippet.c
new file mode 100644
index 00000000..35eec713
--- /dev/null
+++ b/gtksourceview/gtksourcesnippet.c
@@ -0,0 +1,1324 @@
+/*
+ * 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-private.h"
+#include "gtksourcesnippetcontext-private.h"
+
+struct _GtkSourceSnippet
+{
+ GObject parent_instance;
+
+ GtkSourceSnippetContext *context;
+ GtkTextBuffer *buffer;
+
+ GQueue chunks;
+ GtkSourceSnippetChunk *current_chunk;
+
+ GtkTextMark *begin_mark;
+ GtkTextMark *end_mark;
+ gchar *trigger;
+ const gchar *language_id;
+ gchar *description;
+
+ /* This is used to track the insert position within a snippet
+ * while we make transforms. We don't use marks here because
+ * the gravity of the mark is not enought o assure we end up
+ * at the correct position. So instead we are relative to the
+ * beginning of the snippet.
+ */
+ gint saved_insert_pos;
+
+ gint focus_position;
+ gint max_focus_position;
+
+ guint inserted : 1;
+};
+
+G_DEFINE_TYPE (GtkSourceSnippet, gtk_source_snippet, G_TYPE_OBJECT)
+
+enum {
+ PROP_0,
+ PROP_BUFFER,
+ PROP_DESCRIPTION,
+ PROP_LANGUAGE_ID,
+ PROP_POSITION,
+ PROP_TRIGGER,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void gtk_source_snippet_update_marks (GtkSourceSnippet *snippet);
+static void gtk_source_snippet_clear_tags (GtkSourceSnippet *snippet);
+
+static inline void
+print_chunk_positions (GtkSourceSnippet *snippet)
+{
+ guint i = 0;
+
+ for (const GList *l = snippet->chunks.head; l; l = l->next)
+ {
+ GtkSourceSnippetChunk *chunk = l->data;
+ GtkTextIter begin, end;
+
+ if (_gtk_source_snippet_chunk_get_bounds (chunk, &begin, &end))
+ {
+ gchar *real_text = gtk_text_iter_get_slice (&begin, &end);
+ g_printerr (" Chunk %-2u: %u:%u to %u:%u - %s (text-set=%d)\n",
+ i,
+ gtk_text_iter_get_line (&begin),
+ gtk_text_iter_get_line_offset (&begin),
+ gtk_text_iter_get_line (&end),
+ gtk_text_iter_get_line_offset (&end),
+ real_text,
+ gtk_source_snippet_chunk_get_text_set (chunk));
+ g_free (real_text);
+ }
+
+ i++;
+ }
+}
+
+static void
+gtk_source_snippet_save_insert (GtkSourceSnippet *snippet)
+{
+ GtkTextMark *insert;
+ GtkTextIter iter;
+ GtkTextIter begin;
+ GtkTextIter end;
+
+ g_assert (GTK_SOURCE_IS_SNIPPET (snippet));
+
+ if (snippet->current_chunk == NULL ||
+ !_gtk_source_snippet_chunk_get_bounds (snippet->current_chunk, &begin, &end))
+ {
+ snippet->saved_insert_pos = 0;
+ return;
+ }
+
+ insert = gtk_text_buffer_get_insert (snippet->buffer);
+ gtk_text_buffer_get_iter_at_mark (snippet->buffer, &iter, insert);
+
+ if (_gtk_source_snippet_chunk_contains (snippet->current_chunk, &iter))
+ {
+ snippet->saved_insert_pos =
+ gtk_text_iter_get_offset (&iter) -
+ gtk_text_iter_get_offset (&begin);
+ }
+}
+
+static void
+gtk_source_snippet_restore_insert (GtkSourceSnippet *snippet)
+{
+ GtkTextIter begin;
+ GtkTextIter end;
+
+ g_assert (GTK_SOURCE_IS_SNIPPET (snippet));
+
+ if (snippet->current_chunk == NULL ||
+ !_gtk_source_snippet_chunk_get_bounds (snippet->current_chunk, &begin, &end))
+ {
+ snippet->saved_insert_pos = 0;
+ return;
+ }
+
+ gtk_text_iter_forward_chars (&begin, snippet->saved_insert_pos);
+ gtk_text_buffer_select_range (snippet->buffer, &begin, &begin);
+ snippet->saved_insert_pos = 0;
+}
+
+/**
+ * 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)
+{
+ return g_object_new (GTK_SOURCE_TYPE_SNIPPET,
+ "trigger", trigger,
+ "language-id", language_id,
+ NULL);
+}
+
+/**
+ * gtk_source_snippet_copy:
+ * @snippet: a #GtkSourceSnippet
+ *
+ * Does a deep copy of the snippet.
+ *
+ * Returns: (transfer full): A new #GtkSourceSnippet
+ *
+ * Since: 5.0
+ */
+GtkSourceSnippet *
+gtk_source_snippet_copy (GtkSourceSnippet *snippet)
+{
+ GtkSourceSnippet *ret;
+
+ g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), NULL);
+
+ ret = g_object_new (GTK_SOURCE_TYPE_SNIPPET,
+ "trigger", snippet->trigger,
+ "language-id", snippet->language_id,
+ "description", snippet->description,
+ NULL);
+
+ for (const GList *l = snippet->chunks.head; l; l = l->next)
+ {
+ GtkSourceSnippetChunk *old_chunk = l->data;
+ GtkSourceSnippetChunk *new_chunk = gtk_source_snippet_chunk_copy (old_chunk);
+
+ gtk_source_snippet_add_chunk (ret, g_steal_pointer (&new_chunk));
+ }
+
+ return g_steal_pointer (&ret);
+}
+
+/**
+ * gtk_source_snippet_get_focus_position:
+ * @snippet: 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 *snippet)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), -1);
+
+ return snippet->focus_position;
+}
+
+/**
+ * gtk_source_snippet_get_n_chunks:
+ * @snippet: 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 *snippet)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), 0);
+
+ return snippet->chunks.length;
+}
+
+/**
+ * gtk_source_snippet_get_nth_chunk:
+ * @snippet: 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 *snippet,
+ guint nth)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), 0);
+
+ if (nth < snippet->chunks.length)
+ return g_queue_peek_nth (&snippet->chunks, nth);
+
+ return NULL;
+}
+
+/**
+ * gtk_source_snippet_get_trigger:
+ * @snippet: 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 *snippet)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), NULL);
+
+ return snippet->trigger;
+}
+
+/**
+ * gtk_source_snippet_set_trigger:
+ * @snippet: a #GtkSourceSnippet
+ * @trigger: the trigger word
+ *
+ * Sets the trigger for the snippet.
+ *
+ * Since: 5.0
+ */
+void
+gtk_source_snippet_set_trigger (GtkSourceSnippet *snippet,
+ const gchar *trigger)
+{
+ g_return_if_fail (GTK_SOURCE_IS_SNIPPET (snippet));
+
+ if (g_strcmp0 (trigger, snippet->trigger) != 0)
+ {
+ g_free (snippet->trigger);
+ snippet->trigger = g_strdup (trigger);
+ g_object_notify_by_pspec (G_OBJECT (snippet),
+ properties [PROP_TRIGGER]);
+ }
+}
+
+/**
+ * gtk_source_snippet_get_language_id:
+ * @snippet: 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 *snippet)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), NULL);
+
+ return snippet->language_id;
+}
+
+/**
+ * gtk_source_snippet_set_language_id:
+ * @snippet: 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 *snippet,
+ const gchar *language_id)
+{
+ g_return_if_fail (GTK_SOURCE_IS_SNIPPET (snippet));
+
+ language_id = g_intern_string (language_id);
+
+ if (language_id != snippet->language_id)
+ {
+ snippet->language_id = language_id;
+ g_object_notify_by_pspec (G_OBJECT (snippet),
+ properties [PROP_LANGUAGE_ID]);
+ }
+}
+
+/**
+ * gtk_source_snippet_get_description:
+ * @snippet: a #GtkSourceSnippet
+ *
+ * Gets the description for the snippet.
+ *
+ * Since: 5.0
+ */
+const gchar *
+gtk_source_snippet_get_description (GtkSourceSnippet *snippet)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), NULL);
+
+ return snippet->description;
+}
+
+/**
+ * gtk_source_snippet_set_description:
+ * @snippet: a #GtkSourceSnippet
+ * @description: the snippet description
+ *
+ * Sets the description for the snippet.
+ *
+ * Since: 5.0
+ */
+void
+gtk_source_snippet_set_description (GtkSourceSnippet *snippet,
+ const gchar *description)
+{
+ g_return_if_fail (GTK_SOURCE_IS_SNIPPET (snippet));
+
+ if (g_strcmp0 (description, snippet->description) != 0)
+ {
+ g_free (snippet->description);
+ snippet->description = g_strdup (description);
+ g_object_notify_by_pspec (G_OBJECT (snippet),
+ properties [PROP_DESCRIPTION]);
+ }
+}
+
+static void
+gtk_source_snippet_select_chunk (GtkSourceSnippet *snippet,
+ GtkSourceSnippetChunk *chunk)
+{
+ GtkTextIter begin;
+ GtkTextIter end;
+
+ g_return_if_fail (GTK_SOURCE_IS_SNIPPET (snippet));
+ g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk));
+ g_return_if_fail (chunk->focus_position >= 0);
+
+ if (!_gtk_source_snippet_chunk_get_bounds (chunk, &begin, &end))
+ {
+ return;
+ }
+
+ g_debug ("Selecting chunk with range %d:%d to %d:%d (offset %d+%d)",
+ 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));
+
+ snippet->current_chunk = chunk;
+ snippet->focus_position = chunk->focus_position;
+
+ gtk_text_buffer_select_range (snippet->buffer, &begin, &end);
+
+#ifndef G_DISABLE_ASSERT
+ {
+ GtkTextIter set_begin;
+ GtkTextIter set_end;
+
+ gtk_text_buffer_get_selection_bounds (snippet->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_insert_set (GtkSourceSnippet *snippet,
+ GtkTextMark *mark)
+{
+ GtkTextIter begin;
+ GtkTextIter end;
+ GtkTextIter iter;
+
+ g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), FALSE);
+ g_return_val_if_fail (GTK_IS_TEXT_MARK (mark), FALSE);
+ g_return_val_if_fail (snippet->current_chunk != NULL, FALSE);
+ g_return_val_if_fail (snippet->buffer != NULL, FALSE);
+
+ gtk_text_buffer_get_iter_at_mark (snippet->buffer, &iter, mark);
+
+ if (_gtk_source_snippet_chunk_get_bounds (snippet->current_chunk, &begin, &end))
+ {
+ if (gtk_text_iter_compare (&begin, &iter) <= 0 &&
+ gtk_text_iter_compare (&end, &iter) >= 0)
+ {
+ /* No change, we're still in the current chunk */
+ return TRUE;
+ }
+ }
+
+ /* See if the insertion position would place us in any of the other
+ * snippet chunks that are a focus position.
+ */
+ for (const GList *l = snippet->chunks.head; l; l = l->next)
+ {
+ GtkSourceSnippetChunk *chunk = l->data;
+
+ if (chunk->focus_position <= 0 || chunk == snippet->current_chunk)
+ {
+ continue;
+ }
+
+ if (_gtk_source_snippet_chunk_get_bounds (chunk, &begin, &end))
+ {
+ /* Ignore this chunk if it is empty. There is no way
+ * to disambiguate between side-by-side empty chunks
+ * to make this a meaningful movement.
+ */
+ if (gtk_text_iter_equal (&begin, &end))
+ {
+ continue;
+ }
+
+ /* This chunk contains the focus position, so make it
+ * our new chunk to edit.
+ */
+ if (gtk_text_iter_compare (&begin, &iter) <= 0 &&
+ gtk_text_iter_compare (&end, &iter) >= 0)
+ {
+ gtk_source_snippet_select_chunk (snippet, chunk);
+ return TRUE;
+ }
+ }
+ }
+
+ return FALSE;
+}
+
+gboolean
+_gtk_source_snippet_move_next (GtkSourceSnippet *snippet)
+{
+ GtkTextIter iter;
+
+ g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), FALSE);
+
+ snippet->focus_position++;
+
+ for (const GList *l = snippet->chunks.head; l; l = l->next)
+ {
+ GtkSourceSnippetChunk *chunk = l->data;
+
+ if (gtk_source_snippet_chunk_get_focus_position (chunk) == snippet->focus_position)
+ {
+ gtk_source_snippet_select_chunk (snippet, chunk);
+ return TRUE;
+ }
+ }
+
+ for (const GList *l = snippet->chunks.tail; l; l = l->prev)
+ {
+ GtkSourceSnippetChunk *chunk = l->data;
+
+ if (gtk_source_snippet_chunk_get_focus_position (chunk) == 0)
+ {
+ gtk_source_snippet_select_chunk (snippet, chunk);
+ return FALSE;
+ }
+ }
+
+ g_debug ("No more tab stops, moving to end of snippet");
+
+ snippet->current_chunk = NULL;
+ gtk_text_buffer_get_iter_at_mark (snippet->buffer, &iter, snippet->end_mark);
+ gtk_text_buffer_select_range (snippet->buffer, &iter, &iter);
+
+ return FALSE;
+}
+
+gboolean
+_gtk_source_snippet_move_previous (GtkSourceSnippet *snippet)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), FALSE);
+
+ if (snippet->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 (snippet->buffer,
+ &iter,
+ snippet->begin_mark);
+ gtk_text_buffer_select_range (snippet->buffer, &iter, &iter);
+
+ return FALSE;
+ }
+
+ snippet->focus_position--;
+
+ for (const GList *l = snippet->chunks.head; l; l = l->next)
+ {
+ GtkSourceSnippetChunk *chunk = l->data;
+
+ if (gtk_source_snippet_chunk_get_focus_position (chunk) == snippet->focus_position)
+ {
+ gtk_source_snippet_select_chunk (snippet, chunk);
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static void
+gtk_source_snippet_update_context (GtkSourceSnippet *snippet)
+{
+ GtkSourceSnippetContext *context;
+
+ g_return_if_fail (GTK_SOURCE_IS_SNIPPET (snippet));
+
+ if (snippet->chunks.length == 0)
+ {
+ return;
+ }
+
+ context = gtk_source_snippet_get_context (snippet);
+
+ _gtk_source_snippet_context_emit_changed (context);
+
+ for (const GList *l = snippet->chunks.head; l; l = l->next)
+ {
+ GtkSourceSnippetChunk *chunk = l->data;
+ 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 *snippet)
+{
+ g_assert (GTK_SOURCE_IS_SNIPPET (snippet));
+
+ if (snippet->begin_mark != NULL && snippet->end_mark != NULL)
+ {
+ GtkTextBuffer *buffer;
+ GtkTextIter begin;
+ GtkTextIter end;
+ GtkTextTag *tag;
+
+ buffer = gtk_text_mark_get_buffer (snippet->begin_mark);
+
+ gtk_text_buffer_get_iter_at_mark (buffer, &begin, snippet->begin_mark);
+ gtk_text_buffer_get_iter_at_mark (buffer, &end, snippet->end_mark);
+
+ 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 *snippet)
+{
+ GtkTextBuffer *buffer;
+ GtkTextTag *tag;
+
+ g_assert (GTK_SOURCE_IS_SNIPPET (snippet));
+
+ gtk_source_snippet_clear_tags (snippet);
+
+ buffer = gtk_text_mark_get_buffer (snippet->begin_mark);
+ tag = _gtk_source_buffer_get_snippet_focus_tag (GTK_SOURCE_BUFFER (buffer));
+
+ for (const GList *l = snippet->chunks.head; l; l = l->next)
+ {
+ GtkSourceSnippetChunk *chunk = l->data;
+ gint focus_position = gtk_source_snippet_chunk_get_focus_position (chunk);
+
+ if (focus_position >= 0)
+ {
+ GtkTextIter begin;
+ GtkTextIter end;
+
+ _gtk_source_snippet_chunk_get_bounds (chunk, &begin, &end);
+ gtk_text_buffer_apply_tag (buffer, tag, &begin, &end);
+ }
+ }
+}
+
+gboolean
+_gtk_source_snippet_begin (GtkSourceSnippet *snippet,
+ GtkTextBuffer *buffer,
+ GtkTextIter *iter)
+{
+ GtkSourceSnippetContext *context;
+ GtkTextMark *mark;
+
+ g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), FALSE);
+ g_return_val_if_fail (!snippet->buffer, FALSE);
+ g_return_val_if_fail (!snippet->begin_mark, FALSE);
+ g_return_val_if_fail (!snippet->end_mark, FALSE);
+ g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), FALSE);
+ g_return_val_if_fail (iter, FALSE);
+
+ snippet->inserted = TRUE;
+
+ context = gtk_source_snippet_get_context (snippet);
+
+ gtk_source_snippet_update_context (snippet);
+ _gtk_source_snippet_context_emit_changed (context);
+ gtk_source_snippet_update_context (snippet);
+
+ snippet->buffer = g_object_ref (buffer);
+
+ mark = gtk_text_buffer_create_mark (buffer, NULL, iter, TRUE);
+ snippet->begin_mark = g_object_ref (mark);
+
+ mark = gtk_text_buffer_create_mark (buffer, NULL, iter, FALSE);
+ snippet->end_mark = g_object_ref (mark);
+
+ gtk_text_buffer_begin_user_action (buffer);
+
+ for (const GList *l = snippet->chunks.head; l; l = l->next)
+ {
+ GtkSourceSnippetChunk *chunk = l->data;
+ GtkTextMark *begin;
+ GtkTextMark *end;
+ const gchar *text;
+
+ text = gtk_source_snippet_chunk_get_text (chunk);
+
+ begin = gtk_text_buffer_create_mark (buffer, NULL, iter, TRUE);
+ end = gtk_text_buffer_create_mark (buffer, NULL, iter, FALSE);
+
+ g_set_object (&chunk->begin_mark, begin);
+ g_set_object (&chunk->end_mark, end);
+
+ if (text != NULL && text[0] != 0)
+ {
+ snippet->current_chunk = chunk;
+ gtk_text_buffer_insert (buffer, iter, text, -1);
+ gtk_source_snippet_update_marks (snippet);
+ }
+ }
+
+ snippet->current_chunk = NULL;
+
+ gtk_text_buffer_end_user_action (buffer);
+
+ gtk_source_snippet_update_tags (snippet);
+
+ return _gtk_source_snippet_move_next (snippet);
+}
+
+void
+_gtk_source_snippet_finish (GtkSourceSnippet *snippet)
+{
+ g_return_if_fail (GTK_SOURCE_IS_SNIPPET (snippet));
+ g_return_if_fail (snippet->buffer != NULL);
+
+ gtk_source_snippet_clear_tags (snippet);
+
+ if (snippet->begin_mark != NULL)
+ {
+ gtk_text_buffer_delete_mark (snippet->buffer, snippet->begin_mark);
+ g_clear_object (&snippet->begin_mark);
+ }
+
+ if (snippet->end_mark != NULL)
+ {
+ gtk_text_buffer_delete_mark (snippet->buffer, snippet->end_mark);
+ g_clear_object (&snippet->end_mark);
+ }
+
+ g_clear_object (&snippet->buffer);
+}
+
+void
+gtk_source_snippet_add_chunk (GtkSourceSnippet *snippet,
+ GtkSourceSnippetChunk *chunk)
+{
+ gint focus_position;
+
+ g_return_if_fail (GTK_SOURCE_IS_SNIPPET (snippet));
+ g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk));
+ g_return_if_fail (!snippet->inserted);
+ g_return_if_fail (chunk->link.data != NULL);
+ g_return_if_fail (chunk->link.prev == NULL);
+ g_return_if_fail (chunk->link.next == NULL);
+
+ g_object_ref_sink (chunk);
+
+ g_queue_push_tail_link (&snippet->chunks, &chunk->link);
+
+ gtk_source_snippet_chunk_set_context (chunk, snippet->context);
+
+ focus_position = gtk_source_snippet_chunk_get_focus_position (chunk);
+ snippet->max_focus_position = MAX (snippet->max_focus_position, focus_position);
+}
+
+static void
+gtk_source_snippet_update_marks (GtkSourceSnippet *snippet)
+{
+ GtkSourceSnippetChunk *current;
+ GtkTextBuffer *buffer;
+ GtkTextIter current_begin;
+ GtkTextIter current_end;
+
+ g_assert (GTK_SOURCE_IS_SNIPPET (snippet));
+
+ buffer = GTK_TEXT_BUFFER (snippet->buffer);
+ current = snippet->current_chunk;
+
+ if (current == NULL ||
+ !_gtk_source_snippet_chunk_get_bounds (current, ¤t_begin, ¤t_end))
+ {
+ return;
+ }
+
+ /* If the begin of this chunk has come before the end
+ * of the last chunk, then that mights we are empty and
+ * the right gravity of the begin mark was greedily taken
+ * when inserting into a previous mark. This can happen
+ * when you (often intermittently) have empty chunks.
+ *
+ * For example, imagine 4 empty chunks:
+ *
+ * [][][][]
+ *
+ * Except in reality to GtkTextBuffer, that's more like:
+ *
+ * [[[[]]]]
+ *
+ * When the user types 't' into the first chunk we'll end up
+ * with something like this:
+ *
+ * [[[[t]]]]
+ *
+ * and we need to modify things to look like this:
+ *
+ * [t][[[]]]
+ *
+ * We also must worry about the situation where text
+ * is inserted into the second position like:
+ *
+ * [t[t]][[]]
+ *
+ * and detect the situation to move the end mark for the
+ * first item backwards into:
+ *
+ * [t][t][[]]
+ */
+
+ for (const GList *l = current->link.prev; l; l = l->prev)
+ {
+ GtkSourceSnippetChunk *chunk = l->data;
+ GtkTextIter begin;
+ GtkTextIter end;
+
+ if (_gtk_source_snippet_chunk_get_bounds (chunk, &begin, &end))
+ {
+ if (gtk_text_iter_compare (&end, ¤t_begin) > 0)
+ {
+ gtk_text_buffer_move_mark (buffer, chunk->end_mark, ¤t_begin);
+ end = current_begin;
+ }
+
+ if (gtk_text_iter_compare (&begin, &end) > 0)
+ {
+ gtk_text_buffer_move_mark (buffer, chunk->begin_mark, &end);
+ begin = end;
+ }
+ }
+ }
+
+ for (const GList *l = current->link.next; l; l = l->next)
+ {
+ GtkSourceSnippetChunk *chunk = l->data;
+ GtkTextIter begin;
+ GtkTextIter end;
+
+ if (_gtk_source_snippet_chunk_get_bounds (chunk, &begin, &end))
+ {
+ if (gtk_text_iter_compare (&begin, ¤t_end) < 0)
+ {
+ gtk_text_buffer_move_mark (buffer, chunk->begin_mark, ¤t_end);
+ begin = current_end;
+ }
+
+ if (gtk_text_iter_compare (&end, &begin) < 0)
+ {
+ gtk_text_buffer_move_mark (buffer, chunk->end_mark, &begin);
+ end = begin;
+ }
+ }
+ }
+}
+
+static void
+gtk_source_snippet_rewrite_updated_chunks (GtkSourceSnippet *snippet)
+{
+ GtkSourceSnippetChunk *saved;
+
+ g_return_if_fail (GTK_SOURCE_IS_SNIPPET (snippet));
+
+ saved = snippet->current_chunk;
+
+ for (const GList *l = snippet->chunks.head; l; l = l->next)
+ {
+ GtkSourceSnippetChunk *chunk = l->data;
+ GtkTextIter begin;
+ GtkTextIter end;
+ const gchar *text;
+ gchar *real_text;
+
+ /* Temporarily set current chunk to help other utilities
+ * to adjust marks appropriately.
+ */
+ snippet->current_chunk = chunk;
+
+ _gtk_source_snippet_chunk_get_bounds (chunk, &begin, &end);
+ real_text = gtk_text_iter_get_slice (&begin, &end);
+
+ text = gtk_source_snippet_chunk_get_text (chunk);
+
+ if (g_strcmp0 (text, real_text) != 0)
+ {
+ gtk_text_buffer_delete (snippet->buffer, &begin, &end);
+ gtk_text_buffer_insert (snippet->buffer, &begin, text, -1);
+ gtk_source_snippet_update_marks (snippet);
+ }
+
+ g_free (real_text);
+ }
+
+ snippet->current_chunk = saved;
+}
+
+void
+_gtk_source_snippet_after_insert_text (GtkSourceSnippet *snippet,
+ GtkTextBuffer *buffer,
+ GtkTextIter *iter,
+ const gchar *text,
+ gint len)
+{
+ g_return_if_fail (GTK_SOURCE_IS_SNIPPET (snippet));
+ g_return_if_fail (snippet->current_chunk != NULL);
+ g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+ g_return_if_fail (iter != NULL);
+ g_return_if_fail (snippet->current_chunk != NULL);
+
+ /* This function is guaranteed to only be called once for the
+ * actual insert by gtksourceview-snippets.c. That allows us
+ * to update marks, update the context for shared variables, and
+ * delete/insert text in linked chunks.
+ */
+
+ /* Save our insert position so we can restore it after updating
+ * linked chunks (which could be rewritten).
+ */
+ gtk_source_snippet_save_insert (snippet);
+
+ /* Now save the modified text for the iter in question */
+ _gtk_source_snippet_chunk_save_text (snippet->current_chunk);
+
+ /* First we want to update marks from the inserted text */
+ gtk_source_snippet_update_marks (snippet);
+
+ /* Update the context (two passes to ensure that we handle chunks
+ * referencing chunks which come after themselves in the array).
+ */
+ gtk_source_snippet_update_context (snippet);
+ gtk_source_snippet_update_context (snippet);
+
+ /* Now go and rewrite each chunk that has changed. This may also
+ * update marks after each pass so that text marks don't overlap.
+ */
+ gtk_source_snippet_rewrite_updated_chunks (snippet);
+
+ /* Now we can apply tags for the given chunks */
+ gtk_source_snippet_update_tags (snippet);
+
+ /* Place the insertion cursor back where the user expects it */
+ gtk_source_snippet_restore_insert (snippet);
+}
+
+void
+_gtk_source_snippet_after_delete_range (GtkSourceSnippet *snippet,
+ GtkTextBuffer *buffer,
+ GtkTextIter *begin,
+ GtkTextIter *end)
+{
+ g_return_if_fail (GTK_SOURCE_IS_SNIPPET (snippet));
+ g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+ g_return_if_fail (begin != NULL);
+ g_return_if_fail (end != NULL);
+ g_return_if_fail (snippet->current_chunk != NULL);
+
+ /* Now save the modified text for the iter in question */
+ _gtk_source_snippet_chunk_save_text (snippet->current_chunk);
+
+ /* Stash our cursor position so we can restore it after changes */
+ gtk_source_snippet_save_insert (snippet);
+
+ /* First update mark positions based on the deletions */
+ gtk_source_snippet_update_marks (snippet);
+
+ /* Update the context (two passes to ensure that we handle chunks
+ * referencing chunks which come after themselves in the array).
+ */
+ gtk_source_snippet_update_context (snippet);
+ gtk_source_snippet_update_context (snippet);
+
+ /* Now go and rewrite each chunk that has changed. This may also
+ * update marks after each pass so that text marks don't overlap.
+ */
+ gtk_source_snippet_rewrite_updated_chunks (snippet);
+
+ /* Now update any scheme styling for focus positions */
+ gtk_source_snippet_update_tags (snippet);
+
+ /* Place the insertion cursor back where the user expects it */
+ gtk_source_snippet_restore_insert (snippet);
+}
+
+gboolean
+_gtk_source_snippet_contains_range (GtkSourceSnippet *snippet,
+ const GtkTextIter *begin,
+ const GtkTextIter *end)
+{
+ GtkTextIter snippet_begin;
+ GtkTextIter snippet_end;
+
+ g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), FALSE);
+ g_return_val_if_fail (begin != NULL, FALSE);
+ g_return_val_if_fail (end != NULL, FALSE);
+ g_return_val_if_fail (snippet->buffer != NULL, FALSE);
+ g_return_val_if_fail (snippet->begin_mark != NULL, FALSE);
+ g_return_val_if_fail (snippet->end_mark != NULL, FALSE);
+
+ gtk_text_buffer_get_iter_at_mark (snippet->buffer,
+ &snippet_begin,
+ snippet->begin_mark);
+ gtk_text_buffer_get_iter_at_mark (snippet->buffer,
+ &snippet_end,
+ snippet->end_mark);
+
+ return gtk_text_iter_compare (begin, &snippet_begin) >= 0 &&
+ gtk_text_iter_compare (end, &snippet_end) <= 0;
+}
+
+guint
+_gtk_source_snippet_count_affected_chunks (GtkSourceSnippet *snippet,
+ const GtkTextIter *begin,
+ const GtkTextIter *end)
+{
+ guint count = 0;
+
+ g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), FALSE);
+ g_return_val_if_fail (begin != NULL, FALSE);
+ g_return_val_if_fail (end != NULL, FALSE);
+
+ if (gtk_text_iter_equal (begin, end))
+ {
+ return 0;
+ }
+
+ for (const GList *l = snippet->chunks.head; l; l = l->next)
+ {
+ GtkSourceSnippetChunk *chunk = l->data;
+ GtkTextIter chunk_begin;
+ GtkTextIter chunk_end;
+
+ if (_gtk_source_snippet_chunk_get_bounds (chunk, &chunk_begin, &chunk_end))
+ {
+ /* We only care about this chunk if it's non-empty. As
+ * we may have multiple "empty" chunks if they are right
+ * next to each other. Those can be safely ignored
+ * unless we have a chunk after them which is also
+ * overlapped.
+ */
+ if (gtk_text_iter_equal (&chunk_begin, &chunk_end))
+ {
+ continue;
+ }
+
+ /* Special case when we are deleting a whole chunk
+ * content that is non-empty.
+ */
+ if (gtk_text_iter_equal (begin, &chunk_begin) &&
+ gtk_text_iter_equal (end, &chunk_end))
+ {
+ return 1;
+ }
+
+ if (gtk_text_iter_compare (end, &chunk_begin) >= 0 &&
+ gtk_text_iter_compare (begin, &chunk_end) <= 0)
+ {
+ count++;
+ }
+ }
+ }
+
+ return count;
+}
+
+/**
+ * gtk_source_snippet_get_context:
+ * @snippet: 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 *snippet)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), NULL);
+
+ if (snippet->context == NULL)
+ {
+ snippet->context = gtk_source_snippet_context_new ();
+
+ for (const GList *l = snippet->chunks.head; l; l = l->next)
+ {
+ GtkSourceSnippetChunk *chunk = l->data;
+
+ gtk_source_snippet_chunk_set_context (chunk, snippet->context);
+ }
+ }
+
+ return snippet->context;
+}
+
+static void
+gtk_source_snippet_dispose (GObject *object)
+{
+ GtkSourceSnippet *snippet = (GtkSourceSnippet *)object;
+
+ if (snippet->begin_mark != NULL)
+ {
+ gtk_text_buffer_delete_mark (snippet->buffer, snippet->begin_mark);
+ g_clear_object (&snippet->begin_mark);
+ }
+
+ if (snippet->end_mark != NULL)
+ {
+ gtk_text_buffer_delete_mark (snippet->buffer, snippet->end_mark);
+ g_clear_object (&snippet->end_mark);
+ }
+
+ while (snippet->chunks.length > 0)
+ {
+ GtkSourceSnippetChunk *chunk = snippet->chunks.head->data;
+
+ g_queue_unlink (&snippet->chunks, &chunk->link);
+ g_object_unref (chunk);
+ }
+
+ g_clear_object (&snippet->buffer);
+ g_clear_object (&snippet->context);
+
+ G_OBJECT_CLASS (gtk_source_snippet_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_snippet_finalize (GObject *object)
+{
+ GtkSourceSnippet *snippet = (GtkSourceSnippet *)object;
+
+ g_clear_pointer (&snippet->description, g_free);
+ g_clear_pointer (&snippet->trigger, g_free);
+ g_clear_object (&snippet->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 *snippet = GTK_SOURCE_SNIPPET (object);
+
+ switch (prop_id)
+ {
+ case PROP_BUFFER:
+ g_value_set_object (value, snippet->buffer);
+ break;
+
+ case PROP_TRIGGER:
+ g_value_set_string (value, snippet->trigger);
+ break;
+
+ case PROP_LANGUAGE_ID:
+ g_value_set_string (value, snippet->language_id);
+ break;
+
+ case PROP_DESCRIPTION:
+ g_value_set_string (value, snippet->description);
+ break;
+
+ case PROP_POSITION:
+ g_value_set_uint (value, snippet->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 *snippet = GTK_SOURCE_SNIPPET (object);
+
+ switch (prop_id)
+ {
+ case PROP_TRIGGER:
+ gtk_source_snippet_set_trigger (snippet, g_value_get_string (value));
+ break;
+
+ case PROP_LANGUAGE_ID:
+ gtk_source_snippet_set_language_id (snippet, g_value_get_string (value));
+ break;
+
+ case PROP_DESCRIPTION:
+ gtk_source_snippet_set_description (snippet, 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_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 *snippet)
+{
+ snippet->max_focus_position = -1;
+}
+
+gchar *
+_gtk_source_snippet_get_edited_text (GtkSourceSnippet *snippet)
+{
+ GtkTextIter begin;
+ GtkTextIter end;
+
+ g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), NULL);
+
+ if (snippet->begin_mark == NULL || snippet->end_mark == NULL)
+ {
+ return NULL;
+ }
+
+ gtk_text_buffer_get_iter_at_mark (snippet->buffer, &begin, snippet->begin_mark);
+ gtk_text_buffer_get_iter_at_mark (snippet->buffer, &end, snippet->end_mark);
+
+ return gtk_text_iter_get_slice (&begin, &end);
+}
+
+void
+_gtk_source_snippet_replace_current_chunk_text (GtkSourceSnippet *snippet,
+ const gchar *new_text)
+{
+ g_return_if_fail (GTK_SOURCE_IS_SNIPPET (snippet));
+
+ if (snippet->current_chunk != NULL)
+ {
+ gtk_source_snippet_chunk_set_text (snippet->current_chunk, new_text);
+ gtk_source_snippet_chunk_set_text_set (snippet->current_chunk, TRUE);
+ }
+}
diff --git a/gtksourceview/gtksourcesnippet.h b/gtksourceview/gtksourcesnippet.h
new file mode 100644
index 00000000..b3618009
--- /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 *snippet);
+GTK_SOURCE_AVAILABLE_IN_5_0
+const gchar *gtk_source_snippet_get_trigger (GtkSourceSnippet *snippet);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void gtk_source_snippet_set_trigger (GtkSourceSnippet *snippet,
+ const gchar *trigger);
+GTK_SOURCE_AVAILABLE_IN_5_0
+const gchar *gtk_source_snippet_get_language_id (GtkSourceSnippet *snippet);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void gtk_source_snippet_set_language_id (GtkSourceSnippet *snippet,
+ const gchar *language_id);
+GTK_SOURCE_AVAILABLE_IN_5_0
+const gchar *gtk_source_snippet_get_description (GtkSourceSnippet *snippet);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void gtk_source_snippet_set_description (GtkSourceSnippet *snippet,
+ const gchar *description);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void gtk_source_snippet_add_chunk (GtkSourceSnippet *snippet,
+ GtkSourceSnippetChunk *chunk);
+GTK_SOURCE_AVAILABLE_IN_5_0
+guint gtk_source_snippet_get_n_chunks (GtkSourceSnippet *snippet);
+GTK_SOURCE_AVAILABLE_IN_5_0
+gint gtk_source_snippet_get_focus_position (GtkSourceSnippet *snippet);
+GTK_SOURCE_AVAILABLE_IN_5_0
+GtkSourceSnippetChunk *gtk_source_snippet_get_nth_chunk (GtkSourceSnippet *snippet,
+ guint nth);
+GTK_SOURCE_AVAILABLE_IN_5_0
+GtkSourceSnippetContext *gtk_source_snippet_get_context (GtkSourceSnippet *snippet);
+
+G_END_DECLS
diff --git a/gtksourceview/gtksourcesnippetchunk-private.h b/gtksourceview/gtksourcesnippetchunk-private.h
new file mode 100644
index 00000000..e19d621f
--- /dev/null
+++ b/gtksourceview/gtksourcesnippetchunk-private.h
@@ -0,0 +1,52 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2014-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 "gtksourcesnippetchunk.h"
+
+G_BEGIN_DECLS
+
+struct _GtkSourceSnippetChunk
+{
+ GInitiallyUnowned parent_instance;
+
+ GList link;
+
+ GtkSourceSnippetContext *context;
+ gchar *spec;
+ gchar *text;
+ GtkTextMark *begin_mark;
+ GtkTextMark *end_mark;
+
+ gulong context_changed_handler;
+
+ gint focus_position;
+
+ guint text_set : 1;
+};
+
+void _gtk_source_snippet_chunk_save_text (GtkSourceSnippetChunk *chunk);
+gboolean _gtk_source_snippet_chunk_contains (GtkSourceSnippetChunk *chunk,
+ const GtkTextIter *iter);
+gboolean _gtk_source_snippet_chunk_get_bounds (GtkSourceSnippetChunk *chunk,
+ GtkTextIter *begin,
+ GtkTextIter *end);
+
+G_END_DECLS
diff --git a/gtksourceview/gtksourcesnippetchunk.c b/gtksourceview/gtksourcesnippetchunk.c
new file mode 100644
index 00000000..49de6a64
--- /dev/null
+++ b/gtksourceview/gtksourcesnippetchunk.c
@@ -0,0 +1,477 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2014-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-private.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
+ */
+
+G_DEFINE_TYPE (GtkSourceSnippetChunk, gtk_source_snippet_chunk, G_TYPE_INITIALLY_UNOWNED)
+
+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)
+{
+ g_assert (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk));
+ g_assert (GTK_SOURCE_IS_SNIPPET_CONTEXT (context));
+
+ if (!chunk->text_set)
+ {
+ gchar *text;
+
+ 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
+delete_and_unref_mark (GtkTextMark *mark)
+{
+ g_assert (!mark || GTK_IS_TEXT_MARK (mark));
+
+ if (mark != NULL)
+ {
+ gtk_text_buffer_delete_mark (gtk_text_mark_get_buffer (mark), mark);
+ g_object_unref (mark);
+ }
+}
+
+static void
+gtk_source_snippet_chunk_finalize (GObject *object)
+{
+ GtkSourceSnippetChunk *chunk = (GtkSourceSnippetChunk *)object;
+
+ g_assert (chunk->link.prev == NULL);
+ g_assert (chunk->link.next == NULL);
+
+ g_clear_pointer (&chunk->begin_mark, delete_and_unref_mark);
+ g_clear_pointer (&chunk->end_mark, delete_and_unref_mark);
+ 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->link.data = chunk;
+ chunk->focus_position = -1;
+ chunk->spec = g_strdup ("");
+}
+
+gboolean
+_gtk_source_snippet_chunk_get_bounds (GtkSourceSnippetChunk *chunk,
+ GtkTextIter *begin,
+ GtkTextIter *end)
+{
+ GtkTextBuffer *buffer;
+
+ g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk), FALSE);
+ g_return_val_if_fail (begin != NULL, FALSE);
+ g_return_val_if_fail (end != NULL, FALSE);
+
+ if (chunk->begin_mark == NULL || chunk->end_mark == NULL)
+ {
+ return FALSE;
+ }
+
+ buffer = gtk_text_mark_get_buffer (chunk->begin_mark);
+
+ gtk_text_buffer_get_iter_at_mark (buffer, begin, chunk->begin_mark);
+ gtk_text_buffer_get_iter_at_mark (buffer, end, chunk->end_mark);
+
+ return TRUE;
+}
+
+void
+_gtk_source_snippet_chunk_save_text (GtkSourceSnippetChunk *chunk)
+{
+ GtkTextBuffer *buffer;
+ GtkTextIter begin;
+ GtkTextIter end;
+
+ g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk));
+
+ buffer = gtk_text_mark_get_buffer (chunk->begin_mark);
+
+ gtk_text_buffer_get_iter_at_mark (buffer, &begin, chunk->begin_mark);
+ gtk_text_buffer_get_iter_at_mark (buffer, &end, chunk->end_mark);
+
+ g_free (chunk->text);
+ chunk->text = gtk_text_iter_get_slice (&begin, &end);
+ g_object_notify_by_pspec (G_OBJECT (chunk),
+ properties [PROP_TEXT]);
+
+ if (chunk->text_set != TRUE)
+ {
+ chunk->text_set = TRUE;
+ g_object_notify_by_pspec (G_OBJECT (chunk),
+ properties [PROP_TEXT_SET]);
+ }
+}
+
+gboolean
+_gtk_source_snippet_chunk_contains (GtkSourceSnippetChunk *chunk,
+ const GtkTextIter *iter)
+{
+ GtkTextIter begin;
+ GtkTextIter end;
+
+ g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_CHUNK (chunk), FALSE);
+ g_return_val_if_fail (iter != NULL, FALSE);
+
+ if (_gtk_source_snippet_chunk_get_bounds (chunk, &begin, &end))
+ {
+ return gtk_text_iter_compare (&begin, iter) <= 0 &&
+ gtk_text_iter_compare (iter, &end) <= 0;
+ }
+
+ return FALSE;
+}
diff --git a/gtksourceview/gtksourcesnippetchunk.h b/gtksourceview/gtksourcesnippetchunk.h
new file mode 100644
index 00000000..fb150d69
--- /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,
GInitiallyUnowned)
+
+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, "<", 4);
+ break;
+
+ case '>':
+ g_string_append_len (str, ">", 4);
+ break;
+
+ case '&':
+ g_string_append_len (str, "&", 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-private.h b/gtksourceview/gtksourceview-private.h
new file mode 100644
index 00000000..c4521f7f
--- /dev/null
+++ b/gtksourceview/gtksourceview-private.h
@@ -0,0 +1,52 @@
+/*
+ * 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 "gtksourceview.h"
+
+G_BEGIN_DECLS
+
+typedef struct _GtkSourceViewSnippets
+{
+ GtkSourceView *view;
+ GtkTextBuffer *buffer;
+ GQueue queue;
+ gulong view_notify_buffer_handler;
+ gulong buffer_insert_text_handler;
+ gulong buffer_insert_text_after_handler;
+ gulong buffer_delete_range_handler;
+ gulong buffer_delete_range_after_handler;
+ gulong buffer_cursor_moved_handler;
+} GtkSourceViewSnippets;
+
+void _gtk_source_view_snippets_init (GtkSourceViewSnippets *snippets,
+ GtkSourceView *view);
+void _gtk_source_view_snippets_shutdown (GtkSourceViewSnippets *snippets);
+void _gtk_source_view_snippets_push (GtkSourceViewSnippets *snippets,
+ GtkSourceSnippet *snippet,
+ GtkTextIter *iter);
+void _gtk_source_view_snippets_pop (GtkSourceViewSnippets *snippets);
+void _gtk_source_view_snippets_pop_all (GtkSourceViewSnippets *snippets);
+gboolean _gtk_source_view_snippets_key_pressed (GtkSourceViewSnippets *snippets,
+ guint key,
+ guint keycode,
+ GdkModifierType state);
+
+G_END_DECLS
diff --git a/gtksourceview/gtksourceview-snippets.c b/gtksourceview/gtksourceview-snippets.c
new file mode 100644
index 00000000..07cd5b97
--- /dev/null
+++ b/gtksourceview/gtksourceview-snippets.c
@@ -0,0 +1,513 @@
+/*
+ * 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 <string.h>
+
+#include "gtksourcebuffer.h"
+#include "gtksourceiter-private.h"
+#include "gtksourcesnippet-private.h"
+#include "gtksourcesnippetchunk.h"
+#include "gtksourceview-private.h"
+
+static void
+gtk_source_view_snippets_block (GtkSourceViewSnippets *snippets)
+{
+ g_assert (snippets != NULL);
+
+ g_signal_handler_block (snippets->buffer,
+ snippets->buffer_insert_text_handler);
+ g_signal_handler_block (snippets->buffer,
+ snippets->buffer_insert_text_after_handler);
+ g_signal_handler_block (snippets->buffer,
+ snippets->buffer_delete_range_handler);
+ g_signal_handler_block (snippets->buffer,
+ snippets->buffer_delete_range_after_handler);
+ g_signal_handler_block (snippets->buffer,
+ snippets->buffer_cursor_moved_handler);
+}
+
+static void
+gtk_source_view_snippets_unblock (GtkSourceViewSnippets *snippets)
+{
+ g_assert (snippets != NULL);
+
+ g_signal_handler_unblock (snippets->buffer,
+ snippets->buffer_insert_text_handler);
+ g_signal_handler_unblock (snippets->buffer,
+ snippets->buffer_insert_text_after_handler);
+ g_signal_handler_unblock (snippets->buffer,
+ snippets->buffer_delete_range_handler);
+ g_signal_handler_unblock (snippets->buffer,
+ snippets->buffer_delete_range_after_handler);
+ g_signal_handler_unblock (snippets->buffer,
+ snippets->buffer_cursor_moved_handler);
+}
+
+static void
+buffer_insert_text_cb (GtkTextBuffer *buffer,
+ GtkTextIter *location,
+ const gchar *text,
+ gint len,
+ GtkSourceViewSnippets *snippets)
+{
+ GtkSourceSnippet *snippet;
+
+ g_assert (GTK_IS_TEXT_BUFFER (buffer));
+ g_assert (location != NULL);
+ g_assert (text != NULL);
+ g_assert (snippets != NULL);
+
+ snippet = g_queue_peek_head (&snippets->queue);
+
+ if (snippet != NULL)
+ {
+ /* We'll complete the user action in the after phase */
+ gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (buffer));
+ }
+}
+
+static void
+buffer_insert_text_after_cb (GtkTextBuffer *buffer,
+ GtkTextIter *location,
+ const gchar *text,
+ gint len,
+ GtkSourceViewSnippets *snippets)
+{
+ GtkSourceSnippet *snippet;
+
+ g_assert (GTK_IS_TEXT_BUFFER (buffer));
+ g_assert (location != NULL);
+ g_assert (text != NULL);
+ g_assert (snippets != NULL);
+
+ snippet = g_queue_peek_head (&snippets->queue);
+
+ if (snippet != NULL)
+ {
+ gtk_source_view_snippets_block (snippets);
+ _gtk_source_snippet_after_insert_text (snippet,
+ GTK_TEXT_BUFFER (buffer),
+ location,
+ text,
+ len);
+ gtk_source_view_snippets_unblock (snippets);
+
+ /* Copmlete our action from the before phase */
+ gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (buffer));
+ }
+}
+
+static void
+buffer_delete_range_cb (GtkTextBuffer *buffer,
+ GtkTextIter *begin,
+ GtkTextIter *end,
+ GtkSourceViewSnippets *snippets)
+{
+ GtkSourceSnippet *snippet;
+
+ g_assert (GTK_IS_TEXT_BUFFER (buffer));
+ g_assert (begin != NULL);
+ g_assert (end != NULL);
+
+ snippet = g_queue_peek_head (&snippets->queue);
+
+ if (snippet != NULL)
+ {
+ /* If the deletion will affect multiple chunks in the snippet,
+ * then we want to cancel all active snippets and go back to
+ * regular editing.
+ */
+ if (_gtk_source_snippet_count_affected_chunks (snippet, begin, end) > 1)
+ {
+ _gtk_source_view_snippets_pop_all (snippets);
+ return;
+ }
+
+ /* We'll complete the user action in the after phase */
+ gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (buffer));
+ }
+}
+
+static void
+buffer_delete_range_after_cb (GtkTextBuffer *buffer,
+ GtkTextIter *begin,
+ GtkTextIter *end,
+ GtkSourceViewSnippets *snippets)
+{
+ GtkSourceSnippet *snippet;
+
+ g_assert (GTK_IS_TEXT_BUFFER (buffer));
+ g_assert (begin != NULL);
+ g_assert (end != NULL);
+
+ snippet = g_queue_peek_head (&snippets->queue);
+
+ if (snippet != NULL)
+ {
+ gtk_source_view_snippets_block (snippets);
+ _gtk_source_snippet_after_delete_range (snippet,
+ GTK_TEXT_BUFFER (buffer),
+ begin,
+ end);
+ gtk_source_view_snippets_unblock (snippets);
+
+ /* Copmlete our action from the before phase */
+ gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (buffer));
+ }
+}
+
+static void
+buffer_cursor_moved_cb (GtkSourceBuffer *buffer,
+ GtkSourceViewSnippets *snippets)
+{
+ GtkSourceSnippet *snippet;
+
+ g_assert (GTK_SOURCE_IS_BUFFER (buffer));
+ g_assert (snippets != NULL);
+
+ snippet = g_queue_peek_head (&snippets->queue);
+
+ if (snippet != NULL)
+ {
+ GtkTextMark *insert;
+
+ insert = gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (buffer));
+
+ while (snippet != NULL &&
+ !_gtk_source_snippet_insert_set (snippet, insert))
+ {
+ g_object_unref (g_queue_pop_head (&snippets->queue));
+ snippet = g_queue_peek_head (&snippets->queue);
+ }
+ }
+
+}
+
+static void
+view_notify_buffer_cb (GtkSourceView *view,
+ GParamSpec *pspec,
+ GtkSourceViewSnippets *snippets)
+{
+ GtkTextBuffer *buffer;
+
+ g_assert (view != NULL);
+ g_assert (snippets != NULL);
+ g_assert (snippets->view == view);
+
+ g_queue_clear_full (&snippets->queue, g_object_unref);
+
+ g_clear_signal_handler (&snippets->buffer_insert_text_handler,
+ snippets->buffer);
+ g_clear_signal_handler (&snippets->buffer_insert_text_after_handler,
+ snippets->buffer);
+ g_clear_signal_handler (&snippets->buffer_delete_range_handler,
+ snippets->buffer);
+ g_clear_signal_handler (&snippets->buffer_delete_range_after_handler,
+ snippets->buffer);
+ g_clear_signal_handler (&snippets->buffer_cursor_moved_handler,
+ snippets->buffer);
+
+ snippets->buffer = NULL;
+
+ buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
+
+ if (GTK_SOURCE_IS_BUFFER (buffer))
+ {
+ snippets->buffer = buffer;
+ snippets->buffer_insert_text_handler =
+ g_signal_connect (snippets->buffer,
+ "insert-text",
+ G_CALLBACK (buffer_insert_text_cb),
+ snippets);
+ snippets->buffer_insert_text_after_handler =
+ g_signal_connect_after (snippets->buffer,
+ "insert-text",
+ G_CALLBACK (buffer_insert_text_after_cb),
+ snippets);
+ snippets->buffer_delete_range_handler =
+ g_signal_connect (snippets->buffer,
+ "delete-range",
+ G_CALLBACK (buffer_delete_range_cb),
+ snippets);
+ snippets->buffer_delete_range_after_handler =
+ g_signal_connect_after (snippets->buffer,
+ "delete-range",
+ G_CALLBACK (buffer_delete_range_after_cb),
+ snippets);
+ snippets->buffer_cursor_moved_handler =
+ g_signal_connect_after (snippets->buffer,
+ "cursor-moved",
+ G_CALLBACK (buffer_cursor_moved_cb),
+ snippets);
+ }
+}
+
+void
+_gtk_source_view_snippets_init (GtkSourceViewSnippets *snippets,
+ GtkSourceView *view)
+{
+ g_return_if_fail (snippets != NULL);
+ g_return_if_fail (GTK_SOURCE_IS_VIEW (view));
+
+ memset (snippets, 0, sizeof *snippets);
+
+ snippets->view = view;
+ snippets->view_notify_buffer_handler =
+ g_signal_connect (view,
+ "notify::buffer",
+ G_CALLBACK (view_notify_buffer_cb),
+ snippets);
+
+ view_notify_buffer_cb (view, NULL, snippets);
+}
+
+void
+_gtk_source_view_snippets_shutdown (GtkSourceViewSnippets *snippets)
+{
+ g_return_if_fail (snippets != NULL);
+
+ g_queue_clear_full (&snippets->queue, g_object_unref);
+
+ g_clear_signal_handler (&snippets->view_notify_buffer_handler,
+ snippets->view);
+ g_clear_signal_handler (&snippets->buffer_insert_text_handler,
+ snippets->buffer);
+ g_clear_signal_handler (&snippets->buffer_insert_text_after_handler,
+ snippets->buffer);
+ g_clear_signal_handler (&snippets->buffer_delete_range_handler,
+ snippets->buffer);
+ g_clear_signal_handler (&snippets->buffer_delete_range_after_handler,
+ snippets->buffer);
+ g_clear_signal_handler (&snippets->buffer_cursor_moved_handler,
+ snippets->buffer);
+
+ snippets->buffer = NULL;
+ snippets->view = NULL;
+}
+
+static GtkSourceSnippet *
+lookup_snippet_by_trigger (GtkSourceViewSnippets *snippets,
+ const gchar *word)
+{
+ /* TODO: integrate with snippet manager */
+ return NULL;
+}
+
+static gboolean
+gtk_source_view_snippets_try_expand (GtkSourceViewSnippets *snippets,
+ GtkTextIter *iter)
+{
+ GtkSourceSnippet *snippet;
+ GtkTextIter begin;
+ gchar *word;
+
+ g_assert (snippets != NULL);
+ 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 (snippets, word);
+
+ g_free (word);
+
+ if (snippet != NULL)
+ {
+ _gtk_source_view_snippets_push (snippets, snippet, iter);
+ g_object_unref (snippet);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+gboolean
+_gtk_source_view_snippets_key_pressed (GtkSourceViewSnippets *snippets,
+ guint key,
+ guint keycode,
+ GdkModifierType state)
+{
+ GdkModifierType modifiers;
+ gboolean editable;
+
+ g_return_val_if_fail (snippets != NULL, FALSE);
+ g_return_val_if_fail (snippets->view != NULL, FALSE);
+
+ if (snippets->buffer == NULL)
+ {
+ return FALSE;
+ }
+
+ /* Be careful when testing for modifier state equality:
+ * caps lock, num lock,etc need to be taken into account */
+ modifiers = gtk_accelerator_get_default_mod_mask ();
+ editable = gtk_text_view_get_editable (GTK_TEXT_VIEW (snippets->view));
+
+ if ((key == GDK_KEY_Tab || key == GDK_KEY_KP_Tab || key == GDK_KEY_ISO_Left_Tab) &&
+ ((state & modifiers) == 0 ||
+ (state & modifiers) == GDK_SHIFT_MASK) &&
+ editable &&
+ gtk_text_view_get_accepts_tab (GTK_TEXT_VIEW (snippets->view)))
+ {
+ GtkSourceSnippet *snippet = g_queue_peek_head (&snippets->queue);
+ GtkTextIter begin, end;
+ gboolean has_selection;
+
+ /* If we already have a snippet expanded, then we might need
+ * to move forward or backward between snippet positions.
+ */
+ if (snippet != NULL)
+ {
+ if ((state & modifiers) == 0)
+ {
+ if (!_gtk_source_snippet_move_next (snippet))
+ {
+ _gtk_source_view_snippets_pop (snippets);
+ }
+
+ return GDK_EVENT_STOP;
+ }
+ else if (state & GDK_SHIFT_MASK)
+ {
+ if (!_gtk_source_snippet_move_previous (snippet))
+ {
+ _gtk_source_view_snippets_pop (snippets);
+ }
+
+ return GDK_EVENT_STOP;
+ }
+ }
+
+ has_selection = gtk_text_buffer_get_selection_bounds (snippets->buffer,
+ &begin, &end);
+
+ /* tab: if there is no selection and the current word is a
+ * snippet trigger, then we should expand that snippet.
+ */
+ if ((state & modifiers) == 0 &&
+ !has_selection &&
+ gtk_source_view_snippets_try_expand (snippets, &end))
+ {
+ return GDK_EVENT_STOP;
+ }
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+void
+_gtk_source_view_snippets_push (GtkSourceViewSnippets *snippets,
+ GtkSourceSnippet *snippet,
+ GtkTextIter *iter)
+{
+ gboolean more_to_focus;
+
+ g_return_if_fail (snippets != NULL);
+ g_return_if_fail (GTK_SOURCE_IS_SNIPPET (snippet));
+ g_return_if_fail (iter != NULL);
+
+ if (snippets->buffer == NULL)
+ {
+ return;
+ }
+
+ g_queue_push_head (&snippets->queue, g_object_ref (snippet));
+
+ gtk_text_buffer_begin_user_action (snippets->buffer);
+ gtk_source_view_snippets_block (snippets);
+ more_to_focus = _gtk_source_snippet_begin (snippet, snippets->buffer, iter);
+ gtk_source_view_snippets_unblock (snippets);
+ gtk_text_buffer_end_user_action (snippets->buffer);
+
+ gtk_text_view_scroll_mark_onscreen (GTK_TEXT_VIEW (snippets->view),
+ gtk_text_buffer_get_insert (snippets->buffer));
+
+ if (!more_to_focus)
+ {
+ _gtk_source_view_snippets_pop (snippets);
+ }
+}
+
+void
+_gtk_source_view_snippets_pop (GtkSourceViewSnippets *snippets)
+{
+ GtkSourceSnippet *next_snippet;
+ GtkSourceSnippet *snippet;
+
+ g_return_if_fail (snippets != NULL);
+
+ if (snippets->buffer == NULL)
+ {
+ return;
+ }
+
+ snippet = g_queue_pop_head (&snippets->queue);
+
+ if (snippet != NULL)
+ {
+ _gtk_source_snippet_finish (snippet);
+
+ next_snippet = g_queue_peek_head (&snippets->queue);
+
+ 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_move_next (next_snippet);
+
+ g_free (new_text);
+ }
+
+ g_object_unref (snippet);
+ }
+}
+
+void
+_gtk_source_view_snippets_pop_all (GtkSourceViewSnippets *self)
+{
+ g_return_if_fail (self != NULL);
+
+ while (self->queue.length > 0)
+ {
+ _gtk_source_view_snippets_pop (self);
+ }
+}
diff --git a/gtksourceview/gtksourceview.c b/gtksourceview/gtksourceview.c
index 9b21fa3e..4fc67886 100644
--- a/gtksourceview/gtksourceview.c
+++ b/gtksourceview/gtksourceview.c
@@ -22,8 +22,6 @@
#include "config.h"
-#include "gtksourceview.h"
-
#include <string.h>
#include <fribidi.h>
#include <gtk/gtk.h>
@@ -31,25 +29,30 @@
#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"
+#include "gtksourceview-private.h"
/**
* SECTION:view
@@ -156,6 +159,7 @@ enum
MOVE_LINES,
MOVE_TO_MATCHING_BRACKET,
MOVE_WORDS,
+ PUSH_SNIPPET,
SHOW_COMPLETION,
SMART_HOME_END,
N_SIGNALS
@@ -207,6 +211,8 @@ typedef struct
GtkSourceCompletion *completion;
+ GtkSourceViewSnippets snippets;
+
guint right_margin_pos;
gint cached_right_margin_pos;
guint tab_width;
@@ -316,6 +322,9 @@ 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_constructed (GObject *object)
@@ -511,6 +520,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:
@@ -824,6 +834,35 @@ 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::smart-home-end:
* @view: the #GtkSourceView
@@ -1346,6 +1385,8 @@ gtk_source_view_init (GtkSourceView *view)
gtk_style_context_add_class (context, "sourceview");
gtk_source_view_populate_extra_menu (view);
+
+ _gtk_source_view_snippets_init (&priv->snippets, view);
}
static void
@@ -1360,6 +1401,9 @@ gtk_source_view_dispose (GObject *object)
remove_source_buffer (view);
+ /* Release our snippet state. This is safe to call multiple times. */
+ _gtk_source_view_snippets_shutdown (&priv->snippets);
+
/* Disconnect notify buffer because the destroy of the textview will set
* the buffer to NULL, and we call get_buffer in the notify which would
* reinstate a buffer which we don't want.
@@ -3838,10 +3882,10 @@ do_ctrl_backspace (GtkSourceView *view)
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;
@@ -3898,6 +3942,11 @@ gtk_source_view_key_pressed (GtkSourceView *view,
}
}
+ if (_gtk_source_view_snippets_key_pressed (&priv->snippets, key, keycode, state))
+ {
+ return GDK_EVENT_STOP;
+ }
+
/* if tab or shift+tab:
* with shift+tab key is GDK_ISO_Left_Tab (yay! on win32 and mac too!)
*/
@@ -4930,3 +4979,113 @@ 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);
+
+ g_assert (GTK_SOURCE_IS_VIEW (view));
+ g_assert (GTK_SOURCE_IS_SNIPPET (snippet));
+ g_assert (location != NULL);
+
+ _gtk_source_view_snippets_push (&priv->snippets, snippet, location);
+}
+
+/**
+ * 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)
+{
+ GtkSourceSnippetContext *context;
+ 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));
+
+ 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);
+}
diff --git a/gtksourceview/gtksourceview.h b/gtksourceview/gtksourceview.h
index af1a7463..367e8b2c 100644
--- a/gtksourceview/gtksourceview.h
+++ b/gtksourceview/gtksourceview.h
@@ -96,6 +96,9 @@ struct _GtkSourceViewClass
gboolean down);
void (*move_words) (GtkSourceView *view,
gint step);
+ void (*push_snippet) (GtkSourceView *view,
+ GtkSourceSnippet *snippet,
+ GtkTextIter *location);
/*< private >*/
gpointer _reserved[20];
@@ -197,5 +200,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..5073577a 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',
@@ -100,6 +106,7 @@ core_private_c = files([
'gtksourcemarkssequence.c',
'gtksourcepixbufhelper.c',
'gtksourceregex.c',
+ 'gtksourceview-snippets.c',
])
core_c_args = [
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]