[gtksourceview/wip/chergert/snippets: 121/121] snippet: implement basic snippet engine



commit 1c1bd5efa9b6bd8dee0ddcfc98684100bc3f7258
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).
    
     - GtkSourceSnippetBundle is used for multiple purposes because it is handy
       to do so without increasing the number of GObjects we need and because
       it flows naturally. It is used to parse new snippet files as well as
       merge multiple snippet files together. Furthermore, it serves as a
       GListModel with a filtered set of snippets when queried by the
       snippet manager. This type is private, however.
    
     - 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.
    
     - A number of snippet variables are made available as seen in other snippet
       engines, notable textmate and Visual Studio Code.
    
     - You can toggle snippets on/off in test-widget using a checkbox. The
       snippets from data/snippets/ are available based on the current
       language.

 data/meson.build                                |   11 +-
 data/snippets/licenses.snippets                 |  342 ++++++
 data/styles/classic.xml                         |    2 +
 docs/reference/meson.build                      |    4 +
 gtksourceview/gtksource.h                       |    4 +
 gtksourceview/gtksourcebuffer-private.h         |    2 +
 gtksourceview/gtksourcebuffer.c                 |   46 +
 gtksourceview/gtksourceinit.c                   |    5 +
 gtksourceview/gtksourcemarshalers.list          |    1 +
 gtksourceview/gtksourcesnippet-private.h        |   64 +
 gtksourceview/gtksourcesnippet.c                | 1412 +++++++++++++++++++++++
 gtksourceview/gtksourcesnippet.h                |   70 ++
 gtksourceview/gtksourcesnippetbundle-parser.c   |  359 ++++++
 gtksourceview/gtksourcesnippetbundle-private.h  |   67 ++
 gtksourceview/gtksourcesnippetbundle.c          |  660 +++++++++++
 gtksourceview/gtksourcesnippetchunk-private.h   |   55 +
 gtksourceview/gtksourcesnippetchunk.c           |  477 ++++++++
 gtksourceview/gtksourcesnippetchunk.h           |   67 ++
 gtksourceview/gtksourcesnippetcontext-private.h |   29 +
 gtksourceview/gtksourcesnippetcontext.c         |  919 +++++++++++++++
 gtksourceview/gtksourcesnippetcontext.h         |   65 ++
 gtksourceview/gtksourcesnippetmanager-private.h |   32 +
 gtksourceview/gtksourcesnippetmanager.c         |  420 +++++++
 gtksourceview/gtksourcesnippetmanager.h         |   57 +
 gtksourceview/gtksourcestylescheme-private.h    |    2 +
 gtksourceview/gtksourcestylescheme.c            |    9 +
 gtksourceview/gtksourcetypes-private.h          |    1 +
 gtksourceview/gtksourcetypes.h                  |    4 +
 gtksourceview/gtksourceview-private.h           |   52 +
 gtksourceview/gtksourceview-snippets.c          |  559 +++++++++
 gtksourceview/gtksourceview.c                   |  277 ++++-
 gtksourceview/gtksourceview.h                   |   12 +
 gtksourceview/meson.build                       |   11 +
 tests/meson.build                               |    8 +-
 tests/test-snippets.c                           |   58 +
 tests/test-widget.c                             |   30 +
 tests/test-widget.ui                            |   11 +
 37 files changed, 6184 insertions(+), 20 deletions(-)
---
diff --git a/data/meson.build b/data/meson.build
index 45387d48..98d5e739 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -1,7 +1,6 @@
 install_subdir('language-specs',
     install_dir: pkgdatadir,
-  exclude_files: [ 'Makefile.am',
-                   'check-language.sh',
+  exclude_files: [ 'check-language.sh',
                    'lang_v1_to_v2.xslt',
                    'language-specs.its',
                    'language-specs.pot',
@@ -10,10 +9,14 @@ install_subdir('language-specs',
                    'update-pot.sh' ]
 )
 
+install_subdir('snippets',
+    install_dir: pkgdatadir,
+  exclude_files: [ ],
+)
+
 install_subdir('styles',
     install_dir: pkgdatadir,
-  exclude_files: [ 'Makefile.am',
-                   'check-style.sh',
+  exclude_files: [ 'check-style.sh',
                    'styles.its',
                    'styles.pot',
                    'testdark.xml',
diff --git a/data/snippets/licenses.snippets b/data/snippets/licenses.snippets
new file mode 100644
index 00000000..83456c6c
--- /dev/null
+++ b/data/snippets/licenses.snippets
@@ -0,0 +1,342 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ This file is part of GtkSourceView
+
+ Copyright (C) 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/>.
+
+ SPDX-License-Identifier: LGPL-2.1-or-later
+
+-->
+<snippets _group="Licenses">
+  <snippet _name="GPLv3 or later" trigger="gpl3" _description="File header with GPLv3+ license">
+    <text languages="python;python3;"><![CDATA[# ${1:$TM_FILENAME}
+#
+# Copyright $CURRENT_YEAR ${2:$NAME} <${3:$EMAIL}>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+$0]]></text>
+    <text languages="c;chdr;cpp;cpphdr;css;js;java;"><![CDATA[/*
+ * ${1:$TM_FILENAME}
+ *
+ * Copyright $CURRENT_YEAR ${2:$NAME} <${3:$EMAIL}>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+$0]]></text>
+    <text languages="c-sharp;rust;"><![CDATA[// ${1:$TM_FILENAME}
+//
+// Copyright $CURRENT_YEAR ${2:$NAME} <${3:$EMAIL}>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+$0]]></text>
+  </snippet>
+  <snippet _name="LGPLv3 or later" trigger="lgpl3" _description="File header with LGPLv3 or later license">
+    <text languages="python;python3;"><![CDATA[# ${1:$TM_FILENAME}
+#
+# Copyright $CURRENT_YEAR ${2:$NAME} <${3:$EMAIL}>
+#
+# This file 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 3 of the License, or (at
+# your option) any later version.
+#
+# This file 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 program. If not, see <http://www.gnu.org/licenses/>.
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+$0]]></text>
+    <text languages="c;chdr;cpp;cpphdr;css;js;java;"><![CDATA[/*
+ * ${1:$TM_FILENAME}
+ *
+ * Copyright $CURRENT_YEAR ${2:$NAME} <${3:$EMAIL}>
+ *
+ * This file 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 3 of the License, or (at
+ * your option) any later version.
+ *
+ * This file 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 program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+$0]]></text>
+    <text languages="c-sharp;rust;"><![CDATA[// ${1:$TM_FILENAME}
+//
+// Copyright $CURRENT_YEAR ${2:$NAME} <${3:$EMAIL}>
+//
+// This file 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 3 of the License, or (at
+// your option) any later version.
+//
+// This file 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 program. If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: LGPL-3.0-or-later
+$0]]></text>
+  </snippet>
+  <snippet _name="LGPLv2.1 or later" trigger="lgpl2" _description="File header with LGPL 2.1 or later 
license">
+    <text languages="python;python3;"><![CDATA[# ${1:$TM_FILENAME}
+#
+# Copyright $CURRENT_YEAR ${2:$NAME} <${3:$EMAIL}>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library; if not, see <http://www.gnu.org/licenses/>.
+#
+# SPDX-License-Identifier: LGPL-2.1-or-later
+$0]]></text>
+    <text languages="c;chdr;cpp;cpphdr;css;js;java;"><![CDATA[/*
+ * ${1:$TM_FILENAME}
+ *
+ * Copyright $CURRENT_YEAR ${2:$NAME} <${3:$EMAIL}>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+$0]]></text>
+    <text languages="c-sharp;rust;"><![CDATA[// ${1:$TM_FILENAME}
+//
+// Copyright $CURRENT_YEAR ${2:$NAME} <${3:$EMAIL}>
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with this library; if not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+$0]]></text>
+  </snippet>
+  <snippet _name="Apache 2.0" trigger="apache2" _description="File header with Apache 2.0 license">
+    <text languages="python;python3;"><![CDATA[# ${1:$TM_FILENAME}
+#
+# Copyright $CURRENT_YEAR ${2:$NAME} <${3:$EMAIL}>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+$0]]></text>
+    <text languages="c;chdr;cpp;cpphdr;css;js;java;"><![CDATA[/*
+ * ${1:$TM_FILENAME}
+ *
+ * Copyright $CURRENT_YEAR ${2:$NAME} <${3:$EMAIL}>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+$0]]></text>
+    <text languages="c-sharp;rust;"><![CDATA[// ${1:$TM_FILENAME}
+//
+// Copyright $CURRENT_YEAR ${2:$NAME} <${3:$EMAIL}>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+$0]]></text>
+  </snippet>
+  <snippet _name="MIT" trigger="mit" _description="File header with MIT license">
+    <text languages="python;python3;"><![CDATA[# ${1:$TM_FILENAME}
+#
+# Copyright $CURRENT_YEAR ${2:$NAME} <${3:$EMAIL}>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+$0]]></text>
+    <text languages="c;chdr;cpp;cpphdr;css;js;java;"><![CDATA[/*
+ * ${1:$TM_FILENAME}
+ *
+ * Copyright $CURRENT_YEAR ${2:$NAME} <${3:$EMAIL}>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+$0]]></text>
+    <text languages="c-sharp;rust;"><![CDATA[// ${1:$TM_FILENAME}
+//
+// Copyright $CURRENT_YEAR ${2:$NAME} <${3:$EMAIL}>
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//
+// SPDX-License-Identifier: MIT
+$0]]></text>
+  </snippet>
+</snippets>
diff --git a/data/styles/classic.xml b/data/styles/classic.xml
index 183f5881..d83fa955 100644
--- a/data/styles/classic.xml
+++ b/data/styles/classic.xml
@@ -128,6 +128,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..707e2b3d 100644
--- a/docs/reference/meson.build
+++ b/docs/reference/meson.build
@@ -35,6 +35,10 @@ reference_private_h = [
   'gtksourcepixbufhelper-private.h',
   'gtksourceregex-private.h',
   'gtksourcesearchcontext-private.h',
+  'gtksourcesnippet-private.h',
+  'gtksourcesnippetbundle-private.h',
+  'gtksourcesnippetcontext-private.h',
+  'gtksourcesnippetmanager-private.h',
   'gtksourcestyle-private.h',
   'gtksourcestylescheme-private.h',
   'gtksourcestyleschememanager-private.h',
diff --git a/gtksourceview/gtksource.h b/gtksourceview/gtksource.h
index 517c06ae..2656d70b 100644
--- a/gtksourceview/gtksource.h
+++ b/gtksourceview/gtksource.h
@@ -47,6 +47,10 @@
 #include "gtksourceregion.h"
 #include "gtksourcesearchcontext.h"
 #include "gtksourcesearchsettings.h"
+#include "gtksourcesnippet.h"
+#include "gtksourcesnippetchunk.h"
+#include "gtksourcesnippetcontext.h"
+#include "gtksourcesnippetmanager.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/gtksourceinit.c b/gtksourceview/gtksourceinit.c
index 0e10b047..9de2a811 100644
--- a/gtksourceview/gtksourceinit.c
+++ b/gtksourceview/gtksourceinit.c
@@ -32,6 +32,7 @@
 #include "gtksourceinit.h"
 #include "gtksourcelanguagemanager-private.h"
 #include "gtksourcemap.h"
+#include "gtksourcesnippetmanager-private.h"
 #include "gtksourcestyleschemechooser.h"
 #include "gtksourcestyleschemechooserbutton.h"
 #include "gtksourcestyleschemechooserwidget.h"
@@ -233,6 +234,7 @@ gtk_source_finalize (void)
        {
                GtkSourceLanguageManager *language_manager;
                GtkSourceStyleSchemeManager *style_scheme_manager;
+               GtkSourceSnippetManager *snippet_manager;
 
                language_manager = _gtk_source_language_manager_peek_default ();
                g_clear_object (&language_manager);
@@ -240,6 +242,9 @@ gtk_source_finalize (void)
                style_scheme_manager = _gtk_source_style_scheme_manager_peek_default ();
                g_clear_object (&style_scheme_manager);
 
+               snippet_manager = _gtk_source_snippet_manager_peek_default ();
+               g_clear_object (&snippet_manager);
+
                done = TRUE;
        }
 }
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..5170d0fc
--- /dev/null
+++ b/gtksourceview/gtksourcesnippet.c
@@ -0,0 +1,1412 @@
+/*
+ * 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 "gtksourcelanguage.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_pass (GtkSourceSnippet *snippet)
+{
+       GtkSourceSnippetContext *context;
+
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (snippet));
+
+       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_update_context (GtkSourceSnippet *snippet,
+                                   gboolean          emit_changed)
+{
+       g_assert (GTK_SOURCE_IS_SNIPPET (snippet));
+
+       /* First pass */
+       gtk_source_snippet_update_context_pass (snippet);
+
+       if (emit_changed)
+       {
+               GtkSourceSnippetContext *context;
+
+               context = gtk_source_snippet_get_context (snippet);
+               _gtk_source_snippet_context_emit_changed (context);
+       }
+
+       /* Second pass, to handle possible wrap around cases */
+       gtk_source_snippet_update_context_pass (snippet);
+}
+
+static void
+gtk_source_snippet_setup_context (GtkSourceSnippet        *snippet,
+                                 GtkSourceSnippetContext *context,
+                                  GtkSourceBuffer         *buffer,
+                                  const GtkTextIter       *iter)
+{
+       static const struct {
+               const gchar *name;
+               const gchar *key;
+       } metadata[] = {
+               { "BLOCK_COMMENT_START", "block-comment-start" },
+               { "BLOCK_COMMENT_END", "block-comment-end" },
+               { "LINE_COMMENT", "line-comment-start" },
+       };
+       GtkSourceLanguage *l;
+       GtkTextIter begin;
+       GtkTextIter end;
+       gchar *text;
+
+       g_assert (GTK_SOURCE_IS_SNIPPET (snippet));
+       g_assert (GTK_SOURCE_IS_BUFFER (buffer));
+       g_assert (iter != NULL);
+
+       /* This updates a number of snippet variables that are familiar to
+        * users of existing snippet engines. Notably, the TM_ prefixed
+        * variables have been (re)used across numerous text editors.
+        */
+
+       /* TM_CURRENT_LINE */
+       begin = end = *iter;
+       if (!gtk_text_iter_starts_line (&begin))
+               gtk_text_iter_set_offset (&begin, 0);
+       if (!gtk_text_iter_ends_line (&end))
+               gtk_text_iter_forward_to_line_end (&end);
+       text = gtk_text_iter_get_slice (&begin, &end);
+       gtk_source_snippet_context_set_constant (context, "TM_CURRENT_LINE", text);
+       g_free (text);
+
+       /* TM_SELECTED_TEXT */
+       if (gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (buffer), &begin, &end))
+       {
+               text = gtk_text_iter_get_slice (&begin, &end);
+               gtk_source_snippet_context_set_constant (context, "TM_SELECTED_TEXT", text);
+               g_free (text);
+       }
+
+       /* TM_LINE_INDEX */
+       text = g_strdup_printf ("%u", gtk_text_iter_get_line (iter));
+       gtk_source_snippet_context_set_constant (context, "TM_LINE_INDEX", text);
+       g_free (text);
+
+       /* TM_LINE_NUMBER */
+       text = g_strdup_printf ("%u", gtk_text_iter_get_line (iter) + 1);
+       gtk_source_snippet_context_set_constant (context, "TM_LINE_NUMBER", text);
+       g_free (text);
+
+       /* Various metadata fields */
+       l = gtk_source_buffer_get_language (buffer);
+       if (l != NULL)
+       {
+               for (guint i = 0; i < G_N_ELEMENTS (metadata); i++)
+               {
+                       const gchar *name = metadata[i].name;
+                       const gchar *val = gtk_source_language_get_metadata (l, metadata[i].key);
+
+                       if (val != NULL)
+                       {
+                               gtk_source_snippet_context_set_constant (context, name, val);
+                       }
+               }
+       }
+
+       gtk_source_snippet_update_context (snippet, TRUE);
+}
+
+
+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_SOURCE_IS_BUFFER (buffer), FALSE);
+       g_return_val_if_fail (iter != NULL, FALSE);
+
+       snippet->inserted = TRUE;
+
+       context = gtk_source_snippet_get_context (snippet);
+       gtk_source_snippet_setup_context (snippet, context, GTK_SOURCE_BUFFER (buffer), iter);
+
+       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, &current_begin, &current_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, &current_begin) > 0)
+                       {
+                               gtk_text_buffer_move_mark (buffer, chunk->end_mark, &current_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, &current_end) < 0)
+                       {
+                               gtk_text_buffer_move_mark (buffer, chunk->begin_mark, &current_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, FALSE);
+
+       /* 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, FALSE);
+
+       /* 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/gtksourcesnippetbundle-parser.c b/gtksourceview/gtksourcesnippetbundle-parser.c
new file mode 100644
index 00000000..62c02f7f
--- /dev/null
+++ b/gtksourceview/gtksourcesnippetbundle-parser.c
@@ -0,0 +1,359 @@
+/*
+ * 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 "gtksourcesnippetbundle-private.h"
+#include "gtksourcesnippetchunk.h"
+
+typedef struct
+{
+       GString   *cur_text;
+       GPtrArray *chunks;
+       guint      lineno;
+} TextParser;
+
+static void
+flush_chunk (TextParser *parser)
+{
+       g_assert (parser != NULL);
+       g_assert (parser->chunks != NULL);
+
+       if (parser->cur_text->len > 0)
+       {
+               GtkSourceSnippetChunk *chunk = gtk_source_snippet_chunk_new ();
+
+               gtk_source_snippet_chunk_set_spec (chunk, parser->cur_text->str);
+               g_ptr_array_add (parser->chunks, g_object_ref_sink (chunk));
+               g_string_truncate (parser->cur_text, 0);
+       }
+}
+
+static void
+do_part_named (TextParser  *parser,
+               const gchar *name)
+{
+       GtkSourceSnippetChunk *chunk = gtk_source_snippet_chunk_new ();
+       gchar *spec = g_strdup_printf ("$%s", name);
+
+       gtk_source_snippet_chunk_set_spec (chunk, spec);
+       gtk_source_snippet_chunk_set_focus_position (chunk, -1);
+
+       g_ptr_array_add (parser->chunks, g_object_ref_sink (chunk));
+
+       g_free (spec);
+}
+
+static void
+do_part_linked (TextParser *parser,
+                gint        n)
+{
+       GtkSourceSnippetChunk *chunk = gtk_source_snippet_chunk_new ();
+
+       if (n > 0)
+       {
+               gchar text[12];
+
+               g_snprintf (text, sizeof text, "$%u", n);
+               text[sizeof text - 1] = '\0';
+
+               gtk_source_snippet_chunk_set_spec (chunk, text);
+       }
+       else
+       {
+               gtk_source_snippet_chunk_set_spec (chunk, "");
+               gtk_source_snippet_chunk_set_focus_position (chunk, 0);
+       }
+
+       g_ptr_array_add (parser->chunks, g_object_ref_sink (chunk));
+}
+
+static void
+do_part_simple (TextParser  *parser,
+                const gchar *line)
+{
+       g_string_append (parser->cur_text, line);
+}
+
+static void
+do_part_n (TextParser  *parser,
+           gint         n,
+           const gchar *inner)
+{
+       GtkSourceSnippetChunk *chunk;
+
+       g_assert (parser != NULL);
+       g_assert (n >= -1);
+       g_assert (inner != NULL);
+
+       chunk = gtk_source_snippet_chunk_new ();
+       gtk_source_snippet_chunk_set_spec (chunk, n ? inner : "");
+       gtk_source_snippet_chunk_set_focus_position (chunk, n);
+
+       g_ptr_array_add (parser->chunks, g_object_ref_sink (chunk));
+}
+
+static gboolean
+parse_variable (const gchar  *line,
+                glong        *n,
+                gchar       **inner,
+                const gchar **endptr,
+                gchar       **name)
+{
+       gboolean has_inner = FALSE;
+       char *end = NULL;
+       gint brackets;
+       gint i;
+
+       *n = -1;
+       *inner = NULL;
+       *endptr = NULL;
+       *name = NULL;
+
+       g_assert (*line == '$');
+
+       line++;
+
+       *endptr = line;
+
+       if (!*line)
+       {
+               *endptr = NULL;
+               return FALSE;
+       }
+
+       if (*line == '{')
+       {
+               has_inner = TRUE;
+               line++;
+       }
+
+       if (g_ascii_isdigit (*line))
+       {
+               errno = 0;
+               *n = strtol (line, &end, 10);
+
+               if (((*n == LONG_MIN) || (*n == LONG_MAX)) && errno == ERANGE)
+                       return FALSE;
+               else if (*n < 0)
+                       return FALSE;
+
+               line = end;
+       }
+       else if (g_ascii_isalpha (*line) || *line == '_')
+       {
+               const gchar *cur;
+
+               for (cur = line; *cur; cur++)
+               {
+                       if (g_ascii_isalnum (*cur) || *cur == '_')
+                               continue;
+                       break;
+               }
+
+               *endptr = cur;
+               *name = g_strndup (line, cur - line);
+               *n = -2;
+               return TRUE;
+       }
+
+       if (has_inner)
+       {
+               if (*line == ':')
+                       line++;
+
+               brackets = 1;
+
+               for (i = 0; line[i]; i++)
+               {
+                       switch (line[i])
+                       {
+                       case '{':
+                               brackets++;
+                               break;
+
+                       case '}':
+                               brackets--;
+                               break;
+
+                       default:
+                               break;
+                       }
+
+                       if (!brackets)
+                       {
+                               *inner = g_strndup (line, i);
+                               *endptr = &line[i + 1];
+                               return TRUE;
+                       }
+               }
+
+               return FALSE;
+       }
+
+       *endptr = line;
+
+       return TRUE;
+}
+
+static void
+do_part (TextParser  *parser,
+         const gchar *line)
+{
+       const gchar *dollar;
+       gchar *str;
+       gchar *inner;
+       gchar *name;
+       glong n;
+
+       g_assert (line != NULL);
+
+again:
+       if (!*line)
+               return;
+
+       if (!(dollar = strchr (line, '$')))
+       {
+               do_part_simple (parser, line);
+               return;
+       }
+
+       /*
+        * Parse up to the next $ as a simple.
+        * If it is $N or ${N} then it is a linked chunk w/o tabstop.
+        * If it is ${N:""} then it is a chunk w/ tabstop.
+        * If it is ${blah|upper} then it is a non-tab stop chunk performing
+        * some sort of of expansion.
+        */
+
+       g_assert (dollar >= line);
+
+       if (dollar != line)
+       {
+               str = g_strndup (line, (dollar - line));
+               do_part_simple (parser, str);
+               g_free (str);
+               line = dollar;
+       }
+
+parse_dollar:
+       inner = NULL;
+
+       if (!parse_variable (line, &n, &inner, &line, &name))
+       {
+               do_part_simple (parser, line);
+               return;
+       }
+
+#if 0
+       g_printerr ("Parse Variable: N=%d  inner=\"%s\"\n", n, inner);
+       g_printerr ("  Left over: \"%s\"\n", line);
+#endif
+
+       flush_chunk (parser);
+
+       if (inner != NULL)
+       {
+               do_part_n (parser, n, inner);
+               g_clear_pointer (&inner, g_free);
+       }
+       else if (n == -2 && name)
+       {
+               do_part_named (parser, name);
+       }
+       else
+       {
+               do_part_linked (parser, n);
+       }
+
+       g_free (name);
+
+       if (line != NULL)
+       {
+               if (*line == '$')
+               {
+                       goto parse_dollar;
+               }
+               else
+               {
+                       goto again;
+               }
+       }
+}
+
+static gboolean
+feed_line (TextParser  *parser,
+           const gchar *line)
+{
+       g_assert (parser != NULL);
+       g_assert (line != NULL);
+
+       if (parser->cur_text->len || parser->chunks->len > 0)
+       {
+               g_string_append_c (parser->cur_text, '\n');
+       }
+
+       do_part (parser, line);
+
+       return TRUE;
+}
+
+GPtrArray *
+_gtk_source_snippet_bundle_parse_text (const gchar  *text,
+                                       GError      **error)
+{
+       TextParser parser;
+       gchar **lines = NULL;
+
+       g_return_val_if_fail (text != NULL, NULL);
+
+       /* Setup the parser */
+       parser.cur_text = g_string_new (NULL);
+       parser.lineno = 0;
+       parser.chunks = g_ptr_array_new_with_free_func (g_object_unref);
+
+       lines = g_strsplit (text, "\n", 0);
+       for (guint i = 0; lines[i] != NULL; i++)
+       {
+               parser.lineno++;
+
+               if (!feed_line (&parser, lines[i]))
+               {
+                       goto handle_error;
+               }
+       }
+
+       goto finish;
+
+handle_error:
+       g_set_error (error,
+                    G_IO_ERROR,
+                    G_IO_ERROR_INVALID_DATA,
+                    "Failed to parse snippet text at line %u",
+                    parser.lineno);
+       g_clear_pointer (&parser.chunks, g_ptr_array_unref);
+
+finish:
+       g_string_free (parser.cur_text, TRUE);
+       g_strfreev (lines);
+
+       return g_steal_pointer (&parser.chunks);
+}
diff --git a/gtksourceview/gtksourcesnippetbundle-private.h b/gtksourceview/gtksourcesnippetbundle-private.h
new file mode 100644
index 00000000..775d2ac9
--- /dev/null
+++ b/gtksourceview/gtksourcesnippetbundle-private.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
+
+#include <gio/gio.h>
+
+#include "gtksourcetypes.h"
+#include "gtksourcetypes-private.h"
+
+G_BEGIN_DECLS
+
+typedef struct
+{
+       const gchar *group;
+       const gchar *name;
+       const gchar *trigger;
+       const gchar *language;
+       const gchar *description;
+       const gchar *text;
+} GtkSourceSnippetInfo;
+
+#define GTK_SOURCE_TYPE_SNIPPET_BUNDLE (_gtk_source_snippet_bundle_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceSnippetBundle, _gtk_source_snippet_bundle, GTK_SOURCE, SNIPPET_BUNDLE, 
GObject)
+
+G_GNUC_INTERNAL
+GtkSourceSnippetBundle  *_gtk_source_snippet_bundle_new           (void);
+G_GNUC_INTERNAL
+GtkSourceSnippetBundle  *_gtk_source_snippet_bundle_new_from_file (const gchar              *path,
+                                                                   GtkSourceSnippetManager  *manager);
+G_GNUC_INTERNAL
+void                     _gtk_source_snippet_bundle_merge         (GtkSourceSnippetBundle   *self,
+                                                                   GtkSourceSnippetBundle   *other);
+G_GNUC_INTERNAL
+const gchar            **_gtk_source_snippet_bundle_list_groups   (GtkSourceSnippetBundle   *self);
+G_GNUC_INTERNAL
+GtkSourceSnippet        *_gtk_source_snippet_bundle_get_snippet   (GtkSourceSnippetBundle   *self,
+                                                                   const gchar              *group,
+                                                                   const gchar              *language_id,
+                                                                   const gchar              *trigger);
+G_GNUC_INTERNAL
+GListModel              *_gtk_source_snippet_bundle_list_matching (GtkSourceSnippetBundle   *self,
+                                                                   const gchar              *group,
+                                                                   const gchar              *language_id,
+                                                                   const gchar              *trigger_prefix);
+G_GNUC_INTERNAL
+GPtrArray               *_gtk_source_snippet_bundle_parse_text    (const gchar              *text,
+                                                                   GError                  **error);
+
+G_END_DECLS
diff --git a/gtksourceview/gtksourcesnippetbundle.c b/gtksourceview/gtksourcesnippetbundle.c
new file mode 100644
index 00000000..6bc72c81
--- /dev/null
+++ b/gtksourceview/gtksourcesnippetbundle.c
@@ -0,0 +1,660 @@
+/*
+ * 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 "gtksourcesnippet.h"
+#include "gtksourcesnippetchunk.h"
+#include "gtksourcesnippetbundle-private.h"
+#include "gtksourcesnippetmanager-private.h"
+
+struct _GtkSourceSnippetBundle
+{
+       GObject  parent_instance;
+       GArray  *infos;
+};
+
+typedef struct
+{
+       GtkSourceSnippetManager *manager;
+       GtkSourceSnippetBundle  *self;
+       gchar                   *group;
+       gchar                   *name;
+       gchar                   *description;
+       gchar                   *trigger;
+       gchar                  **languages;
+       GString                 *text;
+} ParseState;
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GtkSourceSnippetBundle, _gtk_source_snippet_bundle, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static gint
+compare_infos (const GtkSourceSnippetInfo *info_a,
+              const GtkSourceSnippetInfo *info_b)
+{
+       gint ret = g_strcmp0 (info_a->language, info_b->language);
+
+       if (ret == 0)
+       {
+               ret = g_strcmp0 (info_a->trigger, info_b->trigger);
+       }
+
+       return ret;
+}
+
+static void
+gtk_source_snippet_bundle_dispose (GObject *object)
+{
+       GtkSourceSnippetBundle *self = (GtkSourceSnippetBundle *)object;
+
+       if (self->infos->len > 0)
+       {
+               g_array_remove_range (self->infos, 0, self->infos->len);
+       }
+
+       G_OBJECT_CLASS (_gtk_source_snippet_bundle_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_snippet_bundle_finalize (GObject *object)
+{
+       GtkSourceSnippetBundle *self = (GtkSourceSnippetBundle *)object;
+
+       g_clear_pointer (&self->infos, g_array_unref);
+
+       G_OBJECT_CLASS (_gtk_source_snippet_bundle_parent_class)->finalize (object);
+}
+
+static void
+_gtk_source_snippet_bundle_class_init (GtkSourceSnippetBundleClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->dispose = gtk_source_snippet_bundle_dispose;
+       object_class->finalize = gtk_source_snippet_bundle_finalize;
+}
+
+static void
+_gtk_source_snippet_bundle_init (GtkSourceSnippetBundle *self)
+{
+       self->infos = g_array_new (FALSE, FALSE, sizeof (GtkSourceSnippetInfo));
+}
+
+static void
+gtk_source_snippet_bundle_add (GtkSourceSnippetBundle     *self,
+                               const GtkSourceSnippetInfo *info)
+{
+       g_assert (GTK_SOURCE_IS_SNIPPET_BUNDLE (self));
+       g_assert (info != NULL);
+
+       /* If there is no name, and no trigger, then there is no way to
+        * instantiate the snippet. Just ignore it.
+        */
+       if (info->name != NULL || info->trigger != NULL)
+       {
+               g_array_append_vals (self->infos, info, 1);
+       }
+}
+
+static void
+text_and_cdata (GMarkupParseContext  *context,
+                const gchar          *text,
+                gsize                 text_len,
+                gpointer              user_data,
+                GError              **error)
+{
+       ParseState *state = user_data;
+
+       g_assert (state != NULL);
+       g_assert (GTK_SOURCE_IS_SNIPPET_BUNDLE (state->self));
+
+       g_string_append_len (state->text, text, text_len);
+}
+
+static const GMarkupParser text_parser = {
+       .text = text_and_cdata,
+};
+
+static void
+elements_start_element (GMarkupParseContext  *context,
+                        const gchar          *element_name,
+                        const gchar         **attribute_names,
+                        const gchar         **attribute_values,
+                        gpointer              user_data,
+                        GError              **error)
+{
+       ParseState *state = user_data;
+
+       g_assert (state != NULL);
+       g_assert (GTK_SOURCE_IS_SNIPPET_BUNDLE (state->self));
+       g_assert (element_name != NULL);
+
+       if (g_strcmp0 (element_name, "text") == 0)
+       {
+               const gchar *languages = NULL;
+
+               if (!g_markup_collect_attributes (element_name, attribute_names, attribute_values, error,
+                                                 G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, 
"languages", &languages,
+                                                 G_MARKUP_COLLECT_INVALID))
+                       return;
+
+               if (languages != NULL && languages[0] != 0)
+               {
+                       gchar **strv = g_strsplit (languages, ";", 0);
+
+                       g_strfreev (state->languages);
+                       state->languages = g_steal_pointer (&strv);
+               }
+
+               g_markup_parse_context_push (context, &text_parser, state);
+       }
+       else
+       {
+               g_set_error (error,
+                            G_MARKUP_ERROR,
+                            G_MARKUP_ERROR_UNKNOWN_ELEMENT,
+                            "Element %s not supported",
+                            element_name);
+       }
+}
+
+static void
+elements_end_element (GMarkupParseContext  *context,
+                      const gchar          *element_name,
+                      gpointer              user_data,
+                      GError              **error)
+{
+       ParseState *state = user_data;
+
+       g_assert (state != NULL);
+       g_assert (GTK_SOURCE_IS_SNIPPET_MANAGER (state->manager));
+       g_assert (GTK_SOURCE_IS_SNIPPET_BUNDLE (state->self));
+       g_assert (element_name != NULL);
+
+       if (g_strcmp0 (element_name, "text") == 0 &&
+           state->languages != NULL &&
+           state->languages[0] != NULL)
+       {
+               GtkSourceSnippetInfo info;
+
+               info.group = _gtk_source_snippet_manager_intern (state->manager, state->group);
+               info.name = _gtk_source_snippet_manager_intern (state->manager, state->name);
+               info.description = _gtk_source_snippet_manager_intern (state->manager, state->description);
+               info.trigger = _gtk_source_snippet_manager_intern (state->manager, state->trigger);
+               info.text = _gtk_source_snippet_manager_intern (state->manager, state->text->str);
+
+               for (guint i = 0; state->languages[i]; i++)
+               {
+                       info.language = _gtk_source_snippet_manager_intern (state->manager, 
state->languages[i]);
+
+                       gtk_source_snippet_bundle_add (state->self, &info);
+               }
+
+       }
+
+       g_clear_pointer (&state->languages, g_strfreev);
+       g_string_truncate (state->text, 0);
+
+       g_markup_parse_context_pop (context);
+}
+
+static const GMarkupParser elements_parser = {
+       .start_element = elements_start_element,
+       .end_element = elements_end_element,
+};
+
+static void
+snippet_start_element (GMarkupParseContext  *context,
+                       const gchar          *element_name,
+                       const gchar         **attribute_names,
+                       const gchar         **attribute_values,
+                       gpointer              user_data,
+                       GError              **error)
+{
+       ParseState *state = user_data;
+       const gchar *_name = NULL;
+       const gchar *_description = NULL;
+       const gchar *trigger = NULL;
+
+       g_assert (state != NULL);
+       g_assert (GTK_SOURCE_IS_SNIPPET_BUNDLE (state->self));
+       g_assert (element_name != NULL);
+
+       if (g_strcmp0 (element_name, "snippet") != 0)
+       {
+               g_set_error (error,
+                            G_MARKUP_ERROR,
+                            G_MARKUP_ERROR_UNKNOWN_ELEMENT,
+                            "Element %s not supported",
+                            element_name);
+               return;
+       }
+
+       if (!g_markup_collect_attributes (element_name, attribute_names, attribute_values, error,
+                                         G_MARKUP_COLLECT_STRING, "trigger", &trigger,
+                                         G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "_name", 
&_name,
+                                         G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, 
"_description", &_description,
+                                         G_MARKUP_COLLECT_INVALID))
+               return;
+
+       if (_name != NULL)
+       {
+               const gchar *name = g_dgettext (GETTEXT_PACKAGE, _name);
+
+               if (g_strcmp0 (state->name, name) != 0)
+               {
+                       g_free (state->name);
+                       state->name = g_strdup (name);
+               }
+       }
+
+       if (_description != NULL)
+       {
+               const gchar *description = g_dgettext (GETTEXT_PACKAGE, _description);
+
+               if (g_strcmp0 (state->description, description) != 0)
+               {
+                       g_free (state->description);
+                       state->description = g_strdup (description);
+               }
+       }
+
+       if (g_strcmp0 (state->trigger, trigger) != 0)
+       {
+               g_free (state->trigger);
+               state->trigger = g_strdup (trigger);
+       }
+
+       g_markup_parse_context_push (context, &elements_parser, state);
+}
+
+static void
+snippet_end_element (GMarkupParseContext  *context,
+                     const gchar          *element_name,
+                     gpointer              user_data,
+                     GError              **error)
+{
+       ParseState *state = user_data;
+
+       g_assert (state != NULL);
+       g_assert (GTK_SOURCE_IS_SNIPPET_BUNDLE (state->self));
+       g_assert (element_name != NULL);
+
+       g_clear_pointer (&state->trigger, g_free);
+       g_clear_pointer (&state->name, g_free);
+
+       g_markup_parse_context_pop (context);
+}
+
+static const GMarkupParser snippet_parser = {
+       .start_element = snippet_start_element,
+       .end_element = snippet_end_element,
+};
+
+static void
+snippets_start_element (GMarkupParseContext  *context,
+                        const gchar          *element_name,
+                        const gchar         **attribute_names,
+                        const gchar         **attribute_values,
+                        gpointer              user_data,
+                        GError              **error)
+{
+       ParseState *state = user_data;
+       const gchar *_group = NULL;
+
+       g_assert (state != NULL);
+       g_assert (GTK_SOURCE_IS_SNIPPET_BUNDLE (state->self));
+       g_assert (element_name != NULL);
+
+       if (g_strcmp0 (element_name, "snippets") != 0)
+       {
+               g_set_error (error,
+                            G_MARKUP_ERROR,
+                            G_MARKUP_ERROR_UNKNOWN_ELEMENT,
+                            "Element %s not supported",
+                            element_name);
+               return;
+       }
+
+       if (!g_markup_collect_attributes (element_name, attribute_names, attribute_values, error,
+                                         G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "_group", 
&_group,
+                                         G_MARKUP_COLLECT_INVALID))
+               return;
+
+       if (_group != NULL)
+       {
+               g_free (state->group);
+               state->group = g_strdup (g_dgettext (GETTEXT_PACKAGE, _group));
+       }
+
+       g_markup_parse_context_push (context, &snippet_parser, state);
+}
+
+static void
+snippets_end_element (GMarkupParseContext  *context,
+                      const gchar          *element_name,
+                      gpointer              user_data,
+                      GError              **error)
+{
+       ParseState *state = user_data;
+
+       g_assert (state != NULL);
+       g_assert (GTK_SOURCE_IS_SNIPPET_BUNDLE (state->self));
+       g_assert (element_name != NULL);
+
+       g_clear_pointer (&state->group, g_free);
+
+       g_markup_parse_context_pop (context);
+}
+
+static const GMarkupParser snippets_parser = {
+       .start_element = snippets_start_element,
+       .end_element = snippets_end_element,
+};
+
+static gboolean
+gtk_source_snippet_bundle_parse (GtkSourceSnippetBundle  *self,
+                                 GtkSourceSnippetManager *manager,
+                                 const gchar             *path)
+{
+       gchar *contents = NULL;
+       gsize length = 0;
+       gboolean ret = FALSE;
+
+       g_assert (GTK_SOURCE_IS_SNIPPET_BUNDLE (self));
+       g_assert (path != NULL);
+
+       if (g_file_get_contents (path, &contents, &length, NULL))
+       {
+               GMarkupParseContext *context;
+               ParseState state = {0};
+
+               state.self = self;
+               state.manager = manager;
+               state.text = g_string_new (NULL);
+
+               context = g_markup_parse_context_new (&snippets_parser,
+                                                     (G_MARKUP_TREAT_CDATA_AS_TEXT |
+                                                      G_MARKUP_PREFIX_ERROR_POSITION),
+                                                      &state, NULL);
+
+               ret = g_markup_parse_context_parse (context, contents, length, NULL);
+
+               g_clear_pointer (&state.description, g_free);
+               g_clear_pointer (&state.languages, g_strfreev);
+               g_clear_pointer (&state.name, g_free);
+               g_clear_pointer (&state.trigger, g_free);
+               g_clear_pointer (&state.group, g_free);
+               g_string_free (state.text, TRUE);
+
+               g_markup_parse_context_free (context);
+               g_free (contents);
+
+               g_array_sort (self->infos, (GCompareFunc) compare_infos);
+
+#if 0
+               for (guint i = 0; i < self->infos->len; i++)
+               {
+                       GtkSourceSnippetInfo *info = &g_array_index (self->infos, GtkSourceSnippetInfo, i);
+                       g_print ("group=%s name=%s language=%s trigger=%s\n",
+                                info->group, info->name, info->language, info->trigger);
+               }
+#endif
+       }
+
+       return ret;
+}
+
+
+GtkSourceSnippetBundle *
+_gtk_source_snippet_bundle_new (void)
+{
+       return g_object_new (GTK_SOURCE_TYPE_SNIPPET_BUNDLE, NULL);
+}
+
+GtkSourceSnippetBundle *
+_gtk_source_snippet_bundle_new_from_file (const gchar             *path,
+                                          GtkSourceSnippetManager *manager)
+{
+       GtkSourceSnippetBundle *self;
+
+       g_return_val_if_fail (path != NULL, NULL);
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_MANAGER (manager), NULL);
+
+       self = _gtk_source_snippet_bundle_new ();
+
+       if (!gtk_source_snippet_bundle_parse (self, manager, path))
+       {
+               g_clear_object (&self);
+       }
+
+       return g_steal_pointer (&self);
+}
+
+void
+_gtk_source_snippet_bundle_merge (GtkSourceSnippetBundle *self,
+                                  GtkSourceSnippetBundle *other)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_BUNDLE (self));
+       g_return_if_fail (!other || GTK_SOURCE_IS_SNIPPET_BUNDLE (other));
+
+       if (other == NULL || other->infos->len == 0)
+       {
+               return;
+       }
+
+       g_array_append_vals (self->infos, other->infos->data, other->infos->len);
+       g_array_sort (self->infos, (GCompareFunc) compare_infos);
+}
+
+const gchar **
+_gtk_source_snippet_bundle_list_groups (GtkSourceSnippetBundle *self)
+{
+       GHashTable *ht;
+       guint len;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_BUNDLE (self), NULL);
+
+       ht = g_hash_table_new (NULL, NULL);
+
+       for (guint i = 0; i < self->infos->len; i++)
+       {
+               const GtkSourceSnippetInfo *info = &g_array_index (self->infos, GtkSourceSnippetInfo, i);
+
+               /* We can use pointer comparison because all of these strings
+                * are interned using the same #GStringChunk with
+                * g_string_chunk_insert_const().
+                */
+               if (!g_hash_table_contains (ht, info->group))
+               {
+                       g_hash_table_add (ht, (gchar *)info->group);
+               }
+       }
+
+       return (const gchar **)g_hash_table_get_keys_as_array (ht, &len);
+}
+
+static GtkSourceSnippet *
+create_snippet_from_info (const GtkSourceSnippetInfo *info)
+{
+       GtkSourceSnippet *snippet;
+       GPtrArray *chunks = NULL;
+
+       g_assert (info != NULL);
+
+       if (info->text != NULL)
+       {
+               chunks = _gtk_source_snippet_bundle_parse_text (info->text, NULL);
+
+               if (chunks == NULL)
+               {
+                       GtkSourceSnippetChunk *chunk;
+
+                       /* If we failed to parse, then show the text unprocessed
+                        * to the user so they at least get something in the
+                        * editor to help them debug the issue.
+                        */
+                       chunks = g_ptr_array_new_with_free_func (g_object_unref);
+                       chunk = gtk_source_snippet_chunk_new ();
+                       gtk_source_snippet_chunk_set_text (chunk, info->text);
+                       gtk_source_snippet_chunk_set_text_set (chunk, TRUE);
+                       g_ptr_array_add (chunks, g_object_ref_sink (chunk));
+               }
+       }
+
+       snippet = gtk_source_snippet_new (info->trigger, info->language);
+       gtk_source_snippet_set_description (snippet, info->description);
+
+       if (chunks != NULL)
+       {
+               for (guint i = 0; i < chunks->len; i++)
+               {
+                       GtkSourceSnippetChunk *chunk = g_ptr_array_index (chunks, i);
+                       gtk_source_snippet_add_chunk (snippet, chunk);
+               }
+       }
+
+       g_clear_pointer (&chunks, g_ptr_array_unref);
+
+       return g_steal_pointer (&snippet);
+}
+
+static gboolean
+info_matches (const GtkSourceSnippetInfo *info,
+              const gchar                *group,
+              const gchar                *language_id,
+              const gchar                *trigger,
+              gboolean                    trigger_prefix_only)
+{
+       g_assert (info != NULL);
+
+       if (group != NULL && g_strcmp0 (group, info->group) != 0)
+               return FALSE;
+
+       if (language_id != NULL && g_strcmp0 (language_id, info->language) != 0)
+               return FALSE;
+
+       if (trigger != NULL)
+       {
+               if (info->trigger == NULL)
+                       return FALSE;
+
+               if (trigger_prefix_only)
+               {
+                       if (!g_str_has_prefix (info->trigger, trigger))
+                               return FALSE;
+               }
+               else
+               {
+                       if (!g_str_equal (trigger, info->trigger))
+                               return FALSE;
+               }
+       }
+
+       return TRUE;
+}
+
+GtkSourceSnippet *
+_gtk_source_snippet_bundle_get_snippet (GtkSourceSnippetBundle *self,
+                                        const gchar            *group,
+                                        const gchar            *language_id,
+                                        const gchar            *trigger)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_BUNDLE (self), NULL);
+
+       /* TODO: This could use bsearch(), but the complication here is that
+        *       we want to ignore fields when the key field is NULL and the
+        *       sort order for infos doesn't match what we are querying, so
+        *       we would need an alternate index.
+        */
+
+       for (guint i = 0; i < self->infos->len; i++)
+       {
+               const GtkSourceSnippetInfo *info = &g_array_index (self->infos, GtkSourceSnippetInfo, i);
+
+               if (info_matches (info, group, language_id, trigger, FALSE))
+               {
+                       return create_snippet_from_info (info);
+               }
+       }
+
+       return NULL;
+}
+
+GListModel *
+_gtk_source_snippet_bundle_list_matching (GtkSourceSnippetBundle *self,
+                                          const gchar            *group,
+                                          const gchar            *language_id,
+                                          const gchar            *trigger_prefix)
+{
+       GtkSourceSnippetBundle *ret;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_BUNDLE (self), NULL);
+
+       ret = _gtk_source_snippet_bundle_new ();
+
+       for (guint i = 0; i < self->infos->len; i++)
+       {
+               const GtkSourceSnippetInfo *info = &g_array_index (self->infos, GtkSourceSnippetInfo, i);
+
+               if (info_matches (info, group, language_id, trigger_prefix, TRUE))
+               {
+                       g_array_append_vals (ret->infos, info, 1);
+               }
+       }
+
+       return G_LIST_MODEL (g_steal_pointer (&ret));
+}
+
+static GType
+gtk_source_snippet_bundle_get_item_type (GListModel *model)
+{
+       return GTK_SOURCE_TYPE_SNIPPET;
+}
+
+static guint
+gtk_source_snippet_bundle_get_n_items (GListModel *model)
+{
+       return GTK_SOURCE_SNIPPET_BUNDLE (model)->infos->len;
+}
+
+static gpointer
+gtk_source_snippet_bundle_get_item (GListModel *model,
+                                    guint       position)
+{
+       GtkSourceSnippetBundle *self = GTK_SOURCE_SNIPPET_BUNDLE (model);
+
+       if (position >= self->infos->len)
+       {
+               return NULL;
+       }
+
+       return create_snippet_from_info (&g_array_index (self->infos, GtkSourceSnippetInfo, position));
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+       iface->get_item_type = gtk_source_snippet_bundle_get_item_type;
+       iface->get_n_items = gtk_source_snippet_bundle_get_n_items;
+       iface->get_item = gtk_source_snippet_bundle_get_item;
+}
diff --git a/gtksourceview/gtksourcesnippetchunk-private.h b/gtksourceview/gtksourcesnippetchunk-private.h
new file mode 100644
index 00000000..052aa864
--- /dev/null
+++ b/gtksourceview/gtksourcesnippetchunk-private.h
@@ -0,0 +1,55 @@
+/*
+ * 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;
+};
+
+G_GNUC_INTERNAL
+void     _gtk_source_snippet_chunk_save_text  (GtkSourceSnippetChunk *chunk);
+G_GNUC_INTERNAL
+gboolean _gtk_source_snippet_chunk_contains   (GtkSourceSnippetChunk *chunk,
+                                               const GtkTextIter     *iter);
+G_GNUC_INTERNAL
+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..aa9e532e
--- /dev/null
+++ b/gtksourceview/gtksourcesnippetcontext.c
@@ -0,0 +1,919 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2020 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <glib/gi18n.h>
+#include <stdlib.h>
+
+#include "gtksourcesnippetcontext-private.h"
+
+/**
+ * SECTION:snippetcontext
+ * @title: GtkSourceSnippetContext
+ * @short_description: Context for expanding #GtkSourceSnippetChunk
+ *
+ * This class is currently used primary as a hashtable. However, the longer
+ * term goal is to have it hold onto a GjsContext as well as other languages
+ * so that #GtkSourceSnippetChunk can expand themselves by executing
+ * script within the context.
+ *
+ * The #GtkSourceSnippet will build the context and then expand each of the
+ * chunks during the insertion/edit phase.
+ *
+ * Since: 5.0
+ */
+
+struct _GtkSourceSnippetContext
+{
+  GObject     parent_instance;
+
+  GHashTable *constants;
+  GHashTable *variables;
+  gchar      *line_prefix;
+  gint        tab_width;
+  guint       use_spaces : 1;
+};
+
+struct _GtkSourceSnippetContextClass
+{
+  GObjectClass parent;
+};
+
+G_DEFINE_TYPE (GtkSourceSnippetContext, gtk_source_snippet_context, G_TYPE_OBJECT)
+
+enum {
+  CHANGED,
+  N_SIGNALS
+};
+
+typedef gchar *(*InputFilter) (const gchar *input);
+
+static GHashTable *filters;
+static guint signals[N_SIGNALS];
+
+/**
+ * gtk_source_snippet_context_new:
+ *
+ * Creates a new #GtkSourceSnippetContext.
+ *
+ * Generally, this isn't needed unless you are controlling the
+ * expansion of snippets manually.
+ *
+ * Returns: (transfer full): a #GtkSourceSnippetContext
+ *
+ * Since: 5.0
+ */
+GtkSourceSnippetContext *
+gtk_source_snippet_context_new (void)
+{
+       return g_object_new (GTK_SOURCE_TYPE_SNIPPET_CONTEXT, NULL);
+}
+
+/**
+ * gtk_source_snippet_context_clear_variables:
+ * @self: a #GtkSourceSnippetContext
+ *
+ * Removes all variables from the context.
+ *
+ * Since: 5.0
+ */
+void
+gtk_source_snippet_context_clear_variables (GtkSourceSnippetContext *self)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self));
+
+       g_hash_table_remove_all (self->variables);
+}
+
+/**
+ * gtk_source_snippet_context_set_variable:
+ * @self: a #GtkSourceSnippetContext
+ * @key: the variable name
+ * @value: the value for the variable
+ *
+ * Sets a variable within the context.
+ *
+ * This variable may be overridden by future updates to the
+ * context.
+ *
+ * Since: 5.0
+ */
+void
+gtk_source_snippet_context_set_variable (GtkSourceSnippetContext *self,
+                                         const gchar             *key,
+                                         const gchar             *value)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self));
+       g_return_if_fail (key);
+
+       g_hash_table_replace (self->variables, g_strdup (key), g_strdup (value));
+}
+
+/**
+ * gtk_source_snippet_context_set_constant:
+ * @self: a #GtkSourceSnippetContext
+ * @key: the constant name
+ * @value: the value of the constant
+ *
+ * Sets a constatnt within the context. This is similar to
+ * a variable set with gtk_source_snippet_context_set_variable()
+ * but is expected to not change during use of the snippet.
+ *
+ * Examples would be the date or users name.
+ *
+ * Since: 5.0
+ */
+void
+gtk_source_snippet_context_set_constant (GtkSourceSnippetContext *self,
+                                         const gchar             *key,
+                                         const gchar             *value)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self));
+       g_return_if_fail (key);
+
+       g_hash_table_replace (self->constants, g_strdup (key), g_strdup (value));
+}
+
+/**
+ * gtk_source_snippet_context_get_variable:
+ * @self: a #GtkSourceSnippetContext
+ * @key: the name of the variable
+ *
+ * Gets the current value for a variable named @key.
+ *
+ * Returns: (transfer none) (nullable): the value for the variable, or %NULL
+ *
+ * Since: 5.0
+ */
+const gchar *
+gtk_source_snippet_context_get_variable (GtkSourceSnippetContext *self,
+                                         const gchar             *key)
+{
+       const gchar *ret;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self), NULL);
+
+       if (!(ret = g_hash_table_lookup (self->variables, key)))
+               ret = g_hash_table_lookup (self->constants, key);
+
+       return ret;
+}
+
+static gchar *
+filter_lower (const gchar *input)
+{
+       return input != NULL ? g_utf8_strdown (input, -1) : NULL;
+}
+
+static gchar *
+filter_upper (const gchar *input)
+{
+       return input != NULL ? g_utf8_strup (input, -1) : NULL;
+}
+
+static gchar *
+filter_capitalize (const gchar *input)
+{
+       gunichar c;
+       GString *str;
+
+       if (input == NULL)
+               return NULL;
+
+       if (*input == 0)
+               return g_strdup ("");
+
+       c = g_utf8_get_char (input);
+       if (g_unichar_isupper (c))
+               return g_strdup (input);
+
+       str = g_string_new (NULL);
+       input = g_utf8_next_char (input);
+       g_string_append_unichar (str, g_unichar_toupper (c));
+       if (*input)
+               g_string_append (str, input);
+
+       return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_uncapitalize (const gchar *input)
+{
+       gunichar c;
+       GString *str;
+
+       if (input == NULL)
+               return NULL;
+
+       c = g_utf8_get_char (input);
+       if (g_unichar_islower (c))
+               return g_strdup (input);
+
+       str = g_string_new (NULL);
+       input = g_utf8_next_char (input);
+       g_string_append_unichar (str, g_unichar_tolower (c));
+       g_string_append (str, input);
+
+       return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_html (const gchar *input)
+{
+       GString *str;
+
+       if (input == NULL)
+               return NULL;
+
+       str = g_string_new (NULL);
+
+       for (; *input; input = g_utf8_next_char (input))
+       {
+               gunichar c = g_utf8_get_char (input);
+
+               switch (c)
+               {
+               case '<':
+                       g_string_append_len (str, "&lt;", 4);
+                       break;
+
+               case '>':
+                       g_string_append_len (str, "&gt;", 4);
+                       break;
+
+               case '&':
+                       g_string_append_len (str, "&amp;", 5);
+                       break;
+
+               default:
+                       g_string_append_unichar (str, c);
+                       break;
+               }
+       }
+
+       return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_camelize (const gchar *input)
+{
+       gboolean next_is_upper = TRUE;
+       gboolean skip = FALSE;
+       GString *str;
+
+       if (input == NULL)
+               return NULL;
+
+       if (!strchr (input, '_') && !strchr (input, ' ') && !strchr (input, '-'))
+               return filter_capitalize (input);
+
+       str = g_string_new (NULL);
+
+       for (; *input; input = g_utf8_next_char (input))
+       {
+               gunichar c = g_utf8_get_char (input);
+
+               switch (c)
+               {
+               case '_':
+               case '-':
+               case ' ':
+                       next_is_upper = TRUE;
+                       skip = TRUE;
+                       break;
+
+               default:
+                       break;
+               }
+
+               if (skip)
+               {
+                       skip = FALSE;
+                       continue;
+               }
+
+               if (next_is_upper)
+               {
+                       c = g_unichar_toupper (c);
+                       next_is_upper = FALSE;
+               }
+               else
+               {
+                       c = g_unichar_tolower (c);
+               }
+
+               g_string_append_unichar (str, c);
+       }
+
+       if (g_str_has_suffix (str->str, "Private"))
+               g_string_truncate (str, str->len - strlen ("Private"));
+
+       return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_functify (const gchar *input)
+{
+       gunichar last = 0;
+       GString *str;
+
+       if (input == NULL)
+               return NULL;
+
+       str = g_string_new (NULL);
+
+       for (; *input; input = g_utf8_next_char (input))
+       {
+               gunichar c = g_utf8_get_char (input);
+               gunichar n = g_utf8_get_char (g_utf8_next_char (input));
+
+               if (last)
+               {
+                       if ((g_unichar_islower (last) && g_unichar_isupper (c)) ||
+                           (g_unichar_isupper (c) && g_unichar_islower (n)))
+                       {
+                               g_string_append_c (str, '_');
+                       }
+               }
+
+               if ((c == ' ') || (c == '-'))
+               {
+                       c = '_';
+               }
+
+               g_string_append_unichar (str, g_unichar_tolower (c));
+
+               last = c;
+       }
+
+       if (g_str_has_suffix (str->str, "_private") ||
+           g_str_has_suffix (str->str, "_PRIVATE"))
+       {
+               g_string_truncate (str, str->len - strlen ("_private"));
+       }
+
+       return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_namespace (const gchar *input)
+{
+       gunichar last = 0;
+       GString *str;
+       gboolean first_is_lower = FALSE;
+
+       if (input == NULL)
+               return NULL;
+
+       str = g_string_new (NULL);
+
+       for (; *input; input = g_utf8_next_char (input))
+       {
+               gunichar c = g_utf8_get_char (input);
+               gunichar n = g_utf8_get_char (g_utf8_next_char (input));
+
+               if (c == '_')
+                       break;
+
+               if (last)
+               {
+                       if ((g_unichar_islower (last) && g_unichar_isupper (c)) ||
+                           (g_unichar_isupper (c) && g_unichar_islower (n)))
+                               break;
+               }
+               else
+               {
+                       first_is_lower = g_unichar_islower (c);
+               }
+
+               if ((c == ' ') || (c == '-'))
+                       break;
+
+               g_string_append_unichar (str, c);
+
+               last = c;
+       }
+
+       if (first_is_lower)
+       {
+               gchar *ret;
+
+               ret = filter_capitalize (str->str);
+               g_string_free (str, TRUE);
+
+               return g_steal_pointer (&ret);
+       }
+
+       return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_class (const gchar *input)
+{
+       gchar *camel;
+       gchar *ns;
+       gchar *ret = NULL;
+
+       if (input == NULL)
+               return NULL;
+
+       camel = filter_camelize (input);
+       ns = filter_namespace (input);
+
+       if (g_str_has_prefix (camel, ns))
+       {
+               ret = g_strdup (camel + strlen (ns));
+       }
+       else
+       {
+               ret = camel;
+               camel = NULL;
+       }
+
+       g_free (camel);
+       g_free (ns);
+
+       return ret;
+}
+
+static gchar *
+filter_instance (const gchar *input)
+{
+       const gchar *tmp;
+       gchar *funct = NULL;
+       gchar *ret;
+
+       if (input == NULL)
+               return NULL;
+
+       if (!strchr (input, '_'))
+       {
+               funct = filter_functify (input);
+               input = funct;
+       }
+
+       if ((tmp = strrchr (input, '_')))
+       {
+               ret = g_strdup (tmp+1);
+       }
+       else
+       {
+               ret = g_strdup (input);
+       }
+
+       g_free (funct);
+
+       return g_steal_pointer (&ret);
+}
+
+static gchar *
+filter_space (const gchar *input)
+{
+       GString *str;
+
+       if (input == NULL)
+               return NULL;
+
+       str = g_string_new (NULL);
+       for (; *input; input = g_utf8_next_char (input))
+               g_string_append_c (str, ' ');
+
+       return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_descend_path (const gchar *input)
+{
+       const gchar *pos;
+
+       if (input == NULL)
+               return NULL;
+
+       while (*input == G_DIR_SEPARATOR)
+       {
+               input++;
+       }
+
+       if ((pos = strchr (input, G_DIR_SEPARATOR)))
+       {
+               return g_strdup (pos + 1);
+       }
+
+       return NULL;
+}
+
+static gchar *
+filter_stripsuffix (const gchar *input)
+{
+       const gchar *endpos;
+
+       if (input == NULL)
+               return NULL;
+
+       endpos = strrchr (input, '.');
+
+       if (endpos != NULL)
+       {
+               return g_strndup (input, (endpos - input));
+       }
+
+       return g_strdup (input);
+}
+
+static gchar *
+filter_slash_to_dots (const gchar *input)
+{
+       GString *str;
+
+       if (input == NULL)
+               return NULL;
+
+       str = g_string_new (NULL);
+
+       for (; *input; input = g_utf8_next_char (input))
+       {
+               gunichar ch = g_utf8_get_char (input);
+
+               if (ch == G_DIR_SEPARATOR)
+               {
+                       g_string_append_c (str, '.');
+               }
+               else
+               {
+                       g_string_append_unichar (str, ch);
+               }
+       }
+
+       return g_string_free (str, FALSE);
+}
+
+static gchar *
+apply_filter (gchar       *input,
+              const gchar *filter)
+{
+       InputFilter filter_func;
+
+       if ((filter_func = g_hash_table_lookup (filters, filter)))
+       {
+               gchar *tmp = input;
+               input = filter_func (input);
+               g_free (tmp);
+       }
+
+       return input;
+}
+
+static gchar *
+apply_filters (GString     *str,
+               const gchar *filters_list)
+{
+       gchar **filter_names;
+       gchar *input = g_string_free (str, FALSE);
+
+       filter_names = g_strsplit (filters_list, "|", 0);
+       for (guint i = 0; filter_names[i]; i++)
+               input = apply_filter (input, filter_names[i]);
+
+       g_strfreev (filter_names);
+
+       return input;
+}
+
+static gchar *
+scan_forward (const gchar  *input,
+              const gchar **endpos,
+              gunichar      needle)
+{
+       const gchar *begin = input;
+
+       for (; *input; input = g_utf8_next_char (input))
+       {
+               gunichar c = g_utf8_get_char (input);
+
+               if (c == needle)
+               {
+                       *endpos = input;
+                       return g_strndup (begin, (input - begin));
+               }
+       }
+
+       *endpos = NULL;
+
+       return NULL;
+}
+
+gchar *
+gtk_source_snippet_context_expand (GtkSourceSnippetContext *self,
+                                   const gchar             *input)
+{
+       const gchar *expand;
+       gboolean is_dynamic;
+       GString *str;
+       gchar key[12];
+       glong n;
+       gint i;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self), NULL);
+       g_return_val_if_fail (input, NULL);
+
+       is_dynamic = (*input == '$');
+
+       str = g_string_new (NULL);
+
+       for (; *input; input = g_utf8_next_char (input))
+       {
+               gunichar c = g_utf8_get_char (input);
+
+               if (c == '\\')
+               {
+                       input = g_utf8_next_char (input);
+                       if (!*input)
+                               break;
+                       c = g_utf8_get_char (input);
+               }
+               else if (is_dynamic && c == '$')
+               {
+                       input = g_utf8_next_char (input);
+
+                       if (!*input)
+                               break;
+
+                       c = g_utf8_get_char (input);
+
+                       if (g_unichar_isdigit (c))
+                       {
+                               errno = 0;
+                               n = strtol (input, (gchar * *) &input, 10);
+                               if (((n == LONG_MIN) || (n == LONG_MAX)) && errno == ERANGE)
+                                       break;
+                               input--;
+                               g_snprintf (key, sizeof key, "%ld", n);
+                               key[sizeof key - 1] = '\0';
+                               expand = gtk_source_snippet_context_get_variable (self, key);
+                               if (expand)
+                                       g_string_append (str, expand);
+                               continue;
+                       }
+                       else
+                       {
+                               if (strchr (input, '|'))
+                               {
+                                       gchar *lkey;
+
+                                       lkey = g_strndup (input, strchr (input, '|') - input);
+                                       expand = gtk_source_snippet_context_get_variable (self, lkey);
+                                       g_free (lkey);
+
+                                       if (expand)
+                                       {
+                                               g_string_append (str, expand);
+                                               input = strchr (input, '|') - 1;
+                                       }
+                                       else
+                                       {
+                                               input += strlen (input) - 1;
+                                       }
+                               }
+                               else
+                               {
+                                       expand = gtk_source_snippet_context_get_variable (self, input);
+
+                                       if (expand)
+                                       {
+                                               g_string_append (str, expand);
+                                       }
+                                       else
+                                       {
+                                               g_string_append_c (str, '$');
+                                               g_string_append (str, input);
+                                       }
+
+                                       input += strlen (input) - 1;
+                               }
+
+                               continue;
+                       }
+               }
+               else if (is_dynamic && c == '|')
+               {
+                       return apply_filters (str, input + 1);
+               }
+               else if (c == '`')
+               {
+                       const gchar *endpos = NULL;
+                       gchar *slice;
+
+                       slice = scan_forward (input + 1, &endpos, '`');
+
+                       if (slice)
+                       {
+                               gchar *expanded;
+
+                               input = endpos;
+
+                               expanded = gtk_source_snippet_context_expand (self, slice);
+
+                               g_string_append (str, expanded);
+
+                               g_free (expanded);
+                               g_free (slice);
+
+                               continue;
+                       }
+               }
+               else if (c == '\t')
+               {
+                       if (self->use_spaces)
+                       {
+                               for (i = 0; i < self->tab_width; i++)
+                                       g_string_append_c (str, ' ');
+                       }
+                       else
+                       {
+                               g_string_append_c (str, '\t');
+                       }
+
+                       continue;
+               }
+               else if (c == '\n')
+               {
+                       g_string_append_c (str, '\n');
+
+                       if (self->line_prefix)
+                       {
+                               g_string_append (str, self->line_prefix);
+                       }
+
+                       continue;
+               }
+
+               g_string_append_unichar (str, c);
+       }
+
+       return g_string_free (str, FALSE);
+}
+
+void
+gtk_source_snippet_context_set_tab_width (GtkSourceSnippetContext *self,
+                                          gint                     tab_width)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self));
+
+       if (tab_width != self->tab_width)
+       {
+               self->tab_width = tab_width;
+       }
+}
+
+void
+gtk_source_snippet_context_set_use_spaces (GtkSourceSnippetContext *self,
+                                           gboolean                 use_spaces)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self));
+
+       use_spaces = !!use_spaces;
+
+       if (self->use_spaces != use_spaces)
+       {
+               self->use_spaces = use_spaces;
+       }
+}
+
+void
+gtk_source_snippet_context_set_line_prefix (GtkSourceSnippetContext *self,
+                                            const gchar             *line_prefix)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self));
+
+       if (g_strcmp0 (line_prefix, self->line_prefix) != 0)
+       {
+               g_free (self->line_prefix);
+               self->line_prefix = g_strdup (line_prefix);
+       }
+}
+
+void
+_gtk_source_snippet_context_emit_changed (GtkSourceSnippetContext *self)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_CONTEXT (self));
+
+       g_signal_emit (self, signals [CHANGED], 0);
+}
+
+static void
+gtk_source_snippet_context_finalize (GObject *object)
+{
+       GtkSourceSnippetContext *self = (GtkSourceSnippetContext *)object;
+
+       g_clear_pointer (&self->constants, g_hash_table_unref);
+       g_clear_pointer (&self->variables, g_hash_table_unref);
+       g_clear_pointer (&self->line_prefix, g_free);
+
+       G_OBJECT_CLASS (gtk_source_snippet_context_parent_class)->finalize (object);
+}
+
+static void
+gtk_source_snippet_context_class_init (GtkSourceSnippetContextClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->finalize = gtk_source_snippet_context_finalize;
+
+       /**
+        * GtkSourceSnippetContext::changed:
+        *
+        * The "changed" signal is emitted when a change has been
+        * discovered in one of the chunks of the snippet which has
+        * caused a variable or other dynamic data within the context
+        * to have changed.
+        *
+        * Since: 5.0
+        */
+       signals[CHANGED] =
+               g_signal_new ("changed",
+                             G_TYPE_FROM_CLASS (klass),
+                             G_SIGNAL_RUN_FIRST,
+                             0,
+                             NULL, NULL, NULL,
+                             G_TYPE_NONE,
+                             0);
+
+       filters = g_hash_table_new (g_str_hash, g_str_equal);
+       g_hash_table_insert (filters, (gpointer) "lower", filter_lower);
+       g_hash_table_insert (filters, (gpointer) "upper", filter_upper);
+       g_hash_table_insert (filters, (gpointer) "capitalize", filter_capitalize);
+       g_hash_table_insert (filters, (gpointer) "decapitalize", filter_uncapitalize);
+       g_hash_table_insert (filters, (gpointer) "uncapitalize", filter_uncapitalize);
+       g_hash_table_insert (filters, (gpointer) "html", filter_html);
+       g_hash_table_insert (filters, (gpointer) "camelize", filter_camelize);
+       g_hash_table_insert (filters, (gpointer) "functify", filter_functify);
+       g_hash_table_insert (filters, (gpointer) "namespace", filter_namespace);
+       g_hash_table_insert (filters, (gpointer) "class", filter_class);
+       g_hash_table_insert (filters, (gpointer) "space", filter_space);
+       g_hash_table_insert (filters, (gpointer) "stripsuffix", filter_stripsuffix);
+       g_hash_table_insert (filters, (gpointer) "instance", filter_instance);
+       g_hash_table_insert (filters, (gpointer) "slash_to_dots", filter_slash_to_dots);
+       g_hash_table_insert (filters, (gpointer) "descend_path", filter_descend_path);
+}
+
+static void
+gtk_source_snippet_context_init (GtkSourceSnippetContext *self)
+{
+       static const struct {
+               const gchar *name;
+               const gchar *format;
+       } date_time_formats[] = {
+               { "CURRENT_YEAR", "%Y" },
+               { "CURRENT_YEAR_SHORT", "%y" },
+               { "CURRENT_MONTH", "%m" },
+               { "CURRENT_MONTH", "%m" },
+               { "CURRENT_MONTH_NAME", "%B" },
+               { "CURRENT_MONTH_NAME_SHORT", "%b" },
+               { "CURRENT_DATE", "%e" },
+               { "CURRENT_DAY_NAME", "%A" },
+               { "CURRENT_DAY_NAME_SHORT", "%a" },
+               { "CURRENT_HOUR", "%H" },
+               { "CURRENT_MINUTE", "%M" },
+               { "CURRENT_SECOND", "%S" },
+               { "CURRENT_SECONDS_UNIX", "%s" },
+       };
+       GDateTime *dt;
+
+       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 SET_CONSTANT(k, v) g_hash_table_insert (self->constants, g_strdup (k), g_strdup (v))
+       SET_CONSTANT ("NAME_SHORT", g_get_user_name ());
+       SET_CONSTANT ("NAME", g_get_real_name ());
+       SET_CONSTANT ("EMAIL", "");
+#undef SET_CONSTANT
+
+       dt = g_date_time_new_now_local ();
+
+       for (guint i = 0; i < G_N_ELEMENTS (date_time_formats); i++)
+       {
+               g_hash_table_insert (self->constants,
+                                    g_strdup (date_time_formats[i].name),
+                                    g_date_time_format (dt, date_time_formats[i].format));
+       }
+
+       g_date_time_unref (dt);
+}
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/gtksourcesnippetmanager-private.h b/gtksourceview/gtksourcesnippetmanager-private.h
new file mode 100644
index 00000000..fd569639
--- /dev/null
+++ b/gtksourceview/gtksourcesnippetmanager-private.h
@@ -0,0 +1,32 @@
+/*
+ * 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 "gtksourcesnippetmanager.h"
+
+G_BEGIN_DECLS
+
+G_GNUC_INTERNAL
+GtkSourceSnippetManager *_gtk_source_snippet_manager_peek_default (void);
+G_GNUC_INTERNAL
+const gchar             *_gtk_source_snippet_manager_intern       (GtkSourceSnippetManager *manager,
+                                                                   const gchar             *str);
+
+G_END_DECLS
diff --git a/gtksourceview/gtksourcesnippetmanager.c b/gtksourceview/gtksourcesnippetmanager.c
new file mode 100644
index 00000000..d3a5a21b
--- /dev/null
+++ b/gtksourceview/gtksourcesnippetmanager.c
@@ -0,0 +1,420 @@
+/*
+ * 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 "gtksourcesnippet-private.h"
+#include "gtksourcesnippetbundle-private.h"
+#include "gtksourcesnippetmanager-private.h"
+#include "gtksourceutils-private.h"
+
+/**
+ * SECTION:snippetmanager
+ * @title: GtkSourceSnippetManager
+ * @short_description: Provides access to #GtkSourceSnippet
+ * @see_also: #GtkSourceSnippet
+ *
+ * #GtkSourceSnippetManager is an object which processes snippet description
+ * files and creates #GtkSourceSnippet objects.
+ *
+ * Use gtk_source_snippet_manager_get_default() to retrieve the default
+ * instance of #GtkSourceSnippetManager.
+ *
+ * Use gtk_source_snippet_manager_get_snippets() to retrieve snippets for
+ * a given snippets.
+ *
+ * Since: 5.0
+ */
+
+#define SNIPPET_DIR         "snippets"
+#define SNIPPET_FILE_SUFFIX ".snippets"
+
+struct _GtkSourceSnippetManager
+{
+       GObject parent_instance;
+
+       /* To reduce the number of duplicated strings, we use a GStringChunk
+        * so that all of the GtkSourceSnippetInfo structs can point to const
+        * data. The bundles use _gtk_source_snippet_manager_intern() to get
+        * an "interned" string inside this string chunk.
+        */
+       GStringChunk *strings;
+
+       /* The snippet search path to look up files containing snippets like
+        * "license.snippets".
+        */
+       gchar **search_path;
+
+       /* The GtkSourceSnippetBundle handles both parsing a single snippet
+        * file on disk as well as collecting all the parsed files together.
+        * The strings contained in it are "const", and reference @strings
+        * to reduce duplicated memory as well as fragmentation.
+        *
+        * When searching for matching snippets, by language, name etc, we
+        * query the @bundle.
+        */
+       GtkSourceSnippetBundle *bundle;
+};
+
+enum {
+       PROP_0,
+       PROP_SEARCH_PATH,
+       N_PROPS
+};
+
+static GtkSourceSnippetManager *default_instance;
+static GParamSpec *properties[N_PROPS];
+
+G_DEFINE_TYPE (GtkSourceSnippetManager, gtk_source_snippet_manager, G_TYPE_OBJECT)
+
+static void
+gtk_source_snippet_manager_dispose (GObject *object)
+{
+       GtkSourceSnippetManager *self = GTK_SOURCE_SNIPPET_MANAGER (object);
+
+       if (self->bundle != NULL)
+       {
+               g_object_run_dispose (G_OBJECT (self->bundle));
+       }
+
+       G_OBJECT_CLASS (gtk_source_snippet_manager_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_snippet_manager_finalize (GObject *object)
+{
+       GtkSourceSnippetManager *self = GTK_SOURCE_SNIPPET_MANAGER (object);
+
+       g_clear_object (&self->bundle);
+       g_clear_pointer (&self->search_path, g_strfreev);
+       g_clear_pointer (&self->strings, g_string_chunk_free);
+
+       G_OBJECT_CLASS (gtk_source_snippet_manager_parent_class)->finalize (object);
+}
+
+static void
+gtk_source_snippet_manager_set_property (GObject      *object,
+                                         guint         prop_id,
+                                         const GValue *value,
+                                         GParamSpec   *pspec)
+{
+       GtkSourceSnippetManager *self = GTK_SOURCE_SNIPPET_MANAGER (object);
+
+       switch (prop_id)
+       {
+       case PROP_SEARCH_PATH:
+               gtk_source_snippet_manager_set_search_path (self, g_value_get_boxed (value));
+               break;
+
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+gtk_source_snippet_manager_get_property (GObject    *object,
+                                         guint       prop_id,
+                                         GValue     *value,
+                                         GParamSpec *pspec)
+{
+       GtkSourceSnippetManager *self = GTK_SOURCE_SNIPPET_MANAGER (object);
+
+       switch (prop_id)
+       {
+       case PROP_SEARCH_PATH:
+               g_value_set_boxed (value, gtk_source_snippet_manager_get_search_path (self));
+               break;
+
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+gtk_source_snippet_manager_class_init (GtkSourceSnippetManagerClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->dispose = gtk_source_snippet_manager_dispose;
+       object_class->finalize = gtk_source_snippet_manager_finalize;
+       object_class->set_property = gtk_source_snippet_manager_set_property;
+       object_class->get_property = gtk_source_snippet_manager_get_property;
+
+       /**
+        * GtkSourceSnippetManager:search-path:
+        *
+        * The "search-path" property contains a list of directories to search
+        * for files containing snippets (*.snippets).
+        *
+        * Since: 5.0
+        */
+       properties[PROP_SEARCH_PATH] =
+               g_param_spec_boxed ("search-path",
+                                   "Snippet directories",
+                                   "List of directories with snippet definitions (*.snippets)",
+                                   G_TYPE_STRV,
+                                   (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_manager_init (GtkSourceSnippetManager *self)
+{
+}
+
+/**
+ * gtk_source_snippet_manager_get_default:
+ *
+ * Returns the default #GtkSourceSnippetManager instance.
+ *
+ * Returns: (transfer none) (not nullable): a #GtkSourceSnippetManager which
+ *   is owned by GtkSourceView library and must not be unref'd.
+ *
+ * Since: 5.0
+ */
+GtkSourceSnippetManager *
+gtk_source_snippet_manager_get_default (void)
+{
+       if (default_instance == NULL)
+       {
+               GtkSourceSnippetManager *self;
+
+               self = g_object_new (GTK_SOURCE_TYPE_SNIPPET_MANAGER, NULL);
+               g_set_weak_pointer (&default_instance, self);
+       }
+
+       return default_instance;
+}
+
+GtkSourceSnippetManager *
+_gtk_source_snippet_manager_peek_default (void)
+{
+       return default_instance;
+}
+
+const gchar *
+_gtk_source_snippet_manager_intern (GtkSourceSnippetManager *self,
+                                    const gchar             *str)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_MANAGER (self), NULL);
+
+       if (str == NULL)
+       {
+               return NULL;
+       }
+
+       if (self->strings == NULL)
+       {
+               self->strings = g_string_chunk_new (4096*2);
+       }
+
+       return g_string_chunk_insert_const (self->strings, str);
+}
+
+/**
+ * gtk_source_snippet_manager_set_search_path:
+ * @self: a #GtkSourceSnippetManager
+ * @dirs: (nullable) (array zero-terminated=1): a %NULL-terminated array of
+ *   strings or %NULL.
+ *
+ * Sets the list of directories in which the #GtkSourceSnippetManagerlooks for
+ * snippet files.  If @dirs is %NULL, the search path is reset to default.
+ *
+ * <note>
+ *   <para>
+ *     At the moment this function can be called only before the
+ *     snippet files are loaded for the first time. In practice
+ *     to set a custom search path for a #GtkSourceSnippetManager,
+ *     you have to call this function right after creating it.
+ *   </para>
+ * </note>
+ *
+ * Since: 5.0
+ */
+void
+gtk_source_snippet_manager_set_search_path (GtkSourceSnippetManager *self,
+                                            const gchar * const     *dirs)
+{
+       gchar **tmp;
+
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_MANAGER (self));
+
+       tmp = self->search_path;
+
+       if (dirs == NULL)
+               self->search_path = _gtk_source_utils_get_default_dirs (SNIPPET_DIR);
+       else
+               self->search_path = g_strdupv ((gchar **)dirs);
+
+       g_strfreev (tmp);
+
+       g_object_notify_by_pspec (G_OBJECT (self),
+                                 properties [PROP_SEARCH_PATH]);
+}
+
+/**
+ * gtk_source_snippet_manager_get_search_path:
+ * @self: a #GtkSourceSnippetManager.
+ *
+ * Gets the list directories where @self looks for snippet files.
+ *
+ * Returns: (array zero-terminated=1) (transfer none): %NULL-terminated array
+ *   containg a list of snippet files directories.
+ *   The array is owned by @lm and must not be modified.
+ *
+ * Since: 5.0
+ */
+const gchar * const *
+gtk_source_snippet_manager_get_search_path (GtkSourceSnippetManager *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_MANAGER (self), NULL);
+
+       if (self->search_path == NULL)
+               self->search_path = _gtk_source_utils_get_default_dirs (SNIPPET_DIR);
+
+       return (const gchar * const *)self->search_path;
+}
+
+static void
+ensure_snippets (GtkSourceSnippetManager *self)
+{
+       GtkSourceSnippetBundle *bundle;
+       GSList *filenames;
+
+       g_assert (GTK_SOURCE_IS_SNIPPET_MANAGER (self));
+
+       filenames = _gtk_source_utils_get_file_list (
+               (gchar **)gtk_source_snippet_manager_get_search_path (self),
+               SNIPPET_FILE_SUFFIX,
+               TRUE);
+
+       bundle = _gtk_source_snippet_bundle_new ();
+
+       for (const GSList *f = filenames; f; f = f->next)
+       {
+               const gchar *filename = f->data;
+               GtkSourceSnippetBundle *parsed;
+
+               parsed = _gtk_source_snippet_bundle_new_from_file (filename, self);
+
+               if (parsed != NULL)
+                       _gtk_source_snippet_bundle_merge (bundle, parsed);
+               else
+                       g_warning ("Error reading snippet file '%s'", filename);
+
+               g_clear_object (&parsed);
+       }
+
+       g_clear_object (&self->bundle);
+       self->bundle = g_steal_pointer (&bundle);
+
+       g_slist_free_full (filenames, g_free);
+
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET_BUNDLE (self->bundle));
+}
+
+/**
+ * gtk_source_snippet_manager_list_groups:
+ * @self: a #GtkSourceSnippetManager
+ *
+ * List all the known groups within the snippet manager.
+ *
+ * The result should be freed with g_free(), and the invidual strings are
+ * owned by @self and should never be freed by the caller.
+ *
+ * Returns: (transfer container) (array zero-terminated=1) (element-type utf8):
+ *   An array of strings which should be freed with g_free().
+ *
+ * Since: 5.0
+ */
+const gchar **
+gtk_source_snippet_manager_list_groups (GtkSourceSnippetManager *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_MANAGER (self), NULL);
+
+       ensure_snippets (self);
+
+       return _gtk_source_snippet_bundle_list_groups (self->bundle);
+}
+
+/**
+ * gtk_source_snippet_manager_list_matching:
+ * @self: a #GtkSourceSnippetManager
+ * @group: (nullable): a group name or %NULL
+ * @language_id: (nullable): a #GtkSourceLanguage:id or %NULL
+ * @trigger_prefix: (nullable): a prefix for a trigger to activate
+ *
+ * Queries the known snippets for those matching @group, @language_id, and/or
+ * @trigger_prefix. If any of these are %NULL, they will be ignored when
+ * filtering the available snippets.
+ *
+ * The #GListModel only contains information about the available snippets until
+ * g_list_model_get_item() is called for a specific snippet. This helps reduce
+ * the number of #GObject's that are created at runtime to those needed by
+ * the calling application.
+ *
+ * Returns: (transfer full): a #GListModel of #GtkSourceSnippet.
+ *
+ * Since: 5.0
+ */
+GListModel *
+gtk_source_snippet_manager_list_matching (GtkSourceSnippetManager *self,
+                                          const gchar             *group,
+                                          const gchar             *language_id,
+                                          const gchar             *trigger_prefix)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_MANAGER (self), NULL);
+
+       ensure_snippets (self);
+
+       return _gtk_source_snippet_bundle_list_matching (self->bundle, group, language_id, trigger_prefix);
+}
+
+/**
+ * gtk_source_snippet_manager_get_snippet:
+ * @self: a #GtkSourceSnippetManager
+ * @group: (nullable): a group name or %NULL
+ * @language_id: (nullable): a #GtkSourceLanguage:id or %NULL
+ * @trigger: the trigger for the snippet
+ *
+ * Queries the known snippets for the first matching @group, @language_id,
+ * and/or @trigger. If @group or @language_id are %NULL, they will be ignored.
+ *
+ * Returns: (transfer full) (nullable): a #GtkSourceSnippet or %NULL if no
+ *   matching snippet was found.
+ *
+ * Since: 5.0
+ */
+GtkSourceSnippet *
+gtk_source_snippet_manager_get_snippet (GtkSourceSnippetManager *self,
+                                        const gchar             *group,
+                                        const gchar             *language_id,
+                                        const gchar             *trigger)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET_MANAGER (self), NULL);
+
+       ensure_snippets (self);
+
+       return _gtk_source_snippet_bundle_get_snippet (self->bundle, group, language_id, trigger);
+}
diff --git a/gtksourceview/gtksourcesnippetmanager.h b/gtksourceview/gtksourcesnippetmanager.h
new file mode 100644
index 00000000..b6f51b95
--- /dev/null
+++ b/gtksourceview/gtksourcesnippetmanager.h
@@ -0,0 +1,57 @@
+/*
+ * 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 <gio/gio.h>
+
+#include "gtksourcetypes.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_SNIPPET_MANAGER (gtk_source_snippet_manager_get_type())
+
+GTK_SOURCE_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (GtkSourceSnippetManager, gtk_source_snippet_manager, GTK_SOURCE, SNIPPET_MANAGER, 
GObject)
+
+GTK_SOURCE_AVAILABLE_IN_5_0
+GtkSourceSnippetManager  *gtk_source_snippet_manager_get_default     (void);
+GTK_SOURCE_AVAILABLE_IN_5_0
+const gchar * const      *gtk_source_snippet_manager_get_search_path (GtkSourceSnippetManager *self);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                      gtk_source_snippet_manager_set_search_path (GtkSourceSnippetManager *self,
+                                                                      const gchar * const     *dirs);
+GTK_SOURCE_AVAILABLE_IN_5_0
+GtkSourceSnippet         *gtk_source_snippet_manager_get_snippet     (GtkSourceSnippetManager *self,
+                                                                      const gchar             *group,
+                                                                      const gchar             *language_id,
+                                                                      const gchar             *trigger);
+GTK_SOURCE_AVAILABLE_IN_5_0
+const gchar             **gtk_source_snippet_manager_list_groups     (GtkSourceSnippetManager *self);
+GTK_SOURCE_AVAILABLE_IN_5_0
+GListModel               *gtk_source_snippet_manager_list_matching   (GtkSourceSnippetManager *self,
+                                                                      const gchar             *group,
+                                                                      const gchar             *language_id,
+                                                                      const gchar             
*trigger_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-private.h b/gtksourceview/gtksourcetypes-private.h
index 5d0570db..c49946d3 100644
--- a/gtksourceview/gtksourcetypes-private.h
+++ b/gtksourceview/gtksourcetypes-private.h
@@ -33,6 +33,7 @@ typedef struct _GtkSourceGutterRendererMarks    GtkSourceGutterRendererMarks;
 typedef struct _GtkSourceMarksSequence          GtkSourceMarksSequence;
 typedef struct _GtkSourcePixbufHelper           GtkSourcePixbufHelper;
 typedef struct _GtkSourceRegex                  GtkSourceRegex;
+typedef struct _GtkSourceSnippetBundle          GtkSourceSnippetBundle;
 
 #ifdef _MSC_VER
 /* For Visual Studio, we need to export the symbols used by the unit tests */
diff --git a/gtksourceview/gtksourcetypes.h b/gtksourceview/gtksourcetypes.h
index 3ac09423..2348964b 100644
--- a/gtksourceview/gtksourcetypes.h
+++ b/gtksourceview/gtksourcetypes.h
@@ -59,6 +59,10 @@ 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 _GtkSourceSnippetManager            GtkSourceSnippetManager;
 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..f09a2fcd
--- /dev/null
+++ b/gtksourceview/gtksourceview-snippets.c
@@ -0,0 +1,559 @@
+/*
+ * 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 "gtksourcelanguage.h"
+#include "gtksourcesnippet-private.h"
+#include "gtksourcesnippetchunk.h"
+#include "gtksourcesnippetmanager.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
+gtk_source_view_snippets_scroll_to_insert (GtkSourceViewSnippets *snippets)
+{
+       g_assert (snippets != NULL);
+
+       gtk_text_view_scroll_mark_onscreen (GTK_TEXT_VIEW (snippets->view),
+                                           gtk_text_buffer_get_insert (snippets->buffer));
+}
+
+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))
+               {
+                       snippet = g_queue_pop_head (&snippets->queue);
+                       _gtk_source_snippet_finish (snippet);
+                       g_object_unref (snippet);
+
+                       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)
+{
+       GtkSourceSnippetManager *manager;
+       GtkSourceLanguage *language;
+       const gchar *language_id = NULL;
+
+       g_assert (snippets != NULL);
+       g_assert (word != NULL);
+
+       if (word[0] == 0)
+       {
+               return NULL;
+       }
+
+       manager = gtk_source_snippet_manager_get_default ();
+       language = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (snippets->buffer));
+
+       if (language != NULL)
+       {
+               language_id = gtk_source_language_get_id (language);
+       }
+
+       return gtk_source_snippet_manager_get_snippet (manager, NULL, language_id, word);
+}
+
+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_text_buffer_delete (snippets->buffer, &begin, iter);
+               gtk_source_view_push_snippet (snippets->view, 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;
+       }
+
+       if (!gtk_source_view_get_enable_snippets (snippets->view))
+       {
+               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);
+                               }
+
+                               gtk_source_view_snippets_scroll_to_insert (snippets);
+
+                               return GDK_EVENT_STOP;
+                       }
+                       else if (state & GDK_SHIFT_MASK)
+                       {
+                               if (!_gtk_source_snippet_move_previous (snippet))
+                               {
+                                       _gtk_source_view_snippets_pop (snippets);
+                               }
+
+                               gtk_source_view_snippets_scroll_to_insert (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))
+               {
+                       gtk_source_view_snippets_scroll_to_insert (snippets);
+                       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);
+               }
+
+               gtk_source_view_snippets_scroll_to_insert (snippets);
+
+               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 d9b06cce..5930ab96 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
@@ -179,6 +183,7 @@ enum
        PROP_BACKGROUND_PATTERN,
        PROP_SMART_BACKSPACE,
        PROP_SPACE_DRAWER,
+       PROP_ENABLE_SNIPPETS,
        N_PROPS
 };
 
@@ -207,6 +212,8 @@ typedef struct
 
        GtkSourceCompletion *completion;
 
+       GtkSourceViewSnippets snippets;
+
        guint right_margin_pos;
        gint cached_right_margin_pos;
        guint tab_width;
@@ -227,6 +234,7 @@ typedef struct
        guint indent_on_tab : 1;
        guint show_right_margin  : 1;
        guint smart_backspace : 1;
+       guint enable_snippets : 1;
 } GtkSourceViewPrivate;
 
 typedef struct
@@ -316,6 +324,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 +522,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:
@@ -718,6 +730,28 @@ gtk_source_view_class_init (GtkSourceViewClass *klass)
                                     (G_PARAM_READABLE |
                                      G_PARAM_STATIC_STRINGS));
 
+       /**
+        * GtkSourceView:enable-snippets:
+        *
+        * The "enable-snippets" property denotes if snippets should be
+        * expanded when the user presses Tab after having typed a word
+        * matching the snippets found in #GtkSourceSnippetManager.
+        *
+        * The user may tab through focus-positions of the snippet if any
+        * are available by pressing Tab repeatedly until the desired focus
+        * position is selected.
+        *
+        * Since: 5.0
+        */
+       properties [PROP_ENABLE_SNIPPETS] =
+               g_param_spec_boolean ("enable-snippets",
+                                     "Enable Snippets",
+                                     "Whether to enable snippet expansion",
+                                     FALSE,
+                                     (G_PARAM_READWRITE |
+                                      G_PARAM_EXPLICIT_NOTIFY |
+                                      G_PARAM_STATIC_STRINGS));
+
        g_object_class_install_properties (object_class, N_PROPS, properties);
 
        /**
@@ -824,6 +858,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
@@ -1186,6 +1249,10 @@ gtk_source_view_set_property (GObject      *object,
                        gtk_source_view_set_smart_backspace (view, g_value_get_boolean (value));
                        break;
 
+               case PROP_ENABLE_SNIPPETS:
+                       gtk_source_view_set_enable_snippets (view, g_value_get_boolean (value));
+                       break;
+
                default:
                        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
                        break;
@@ -1266,6 +1333,10 @@ gtk_source_view_get_property (GObject    *object,
                        g_value_set_object (value, gtk_source_view_get_space_drawer (view));
                        break;
 
+               case PROP_ENABLE_SNIPPETS:
+                       g_value_set_boolean (value, gtk_source_view_get_enable_snippets (view));
+                       break;
+
                default:
                        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
                        break;
@@ -1346,6 +1417,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 +1433,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.
@@ -3841,10 +3917,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;
@@ -3901,6 +3977,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!)
         */
@@ -4933,3 +5014,169 @@ 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);
+}
+
+/**
+ * gtk_source_view_get_enable_snippets:
+ * @view: a #GtkSourceView
+ *
+ * Gets the #GtkSourceView:enable-snippets property.
+ *
+ * If %TRUE, matching snippets found in the #GtkSourceSnippetManager
+ * may be expanded when the user presses Tab after a word in the
+ * #GtkSourceView.
+ *
+ * Returns: %TRUE if enabled
+ *
+ * Since: 5.0
+ */
+gboolean
+gtk_source_view_get_enable_snippets (GtkSourceView *view)
+{
+       GtkSourceViewPrivate *priv = gtk_source_view_get_instance_private (view);
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIEW (view), FALSE);
+
+       return priv->enable_snippets;
+}
+
+/**
+ * gtk_source_view_set_enable_snippets:
+ * @view: a #GtkSourceView
+ * @enable_snippets: if snippets should be enabled
+ *
+ * Sets the #GtkSourceView:enable-snippets property.
+ *
+ * If @enable_snippets is %TRUE, matching snippets found in the
+ * #GtkSourceSnippetManager may be expanded when the user presses
+ * Tab after a word in the #GtkSourceView.
+ *
+ * Since: 5.0
+ */
+void
+gtk_source_view_set_enable_snippets (GtkSourceView *view,
+                                     gboolean       enable_snippets)
+{
+       GtkSourceViewPrivate *priv = gtk_source_view_get_instance_private (view);
+
+       g_return_if_fail (GTK_SOURCE_IS_VIEW (view));
+
+       enable_snippets = !!enable_snippets;
+
+       if (enable_snippets != priv->enable_snippets)
+       {
+               priv->enable_snippets = enable_snippets;
+               _gtk_source_view_snippets_pop_all (&priv->snippets);
+               g_object_notify_by_pspec (G_OBJECT (view),
+                                         properties [PROP_ENABLE_SNIPPETS]);
+       }
+}
diff --git a/gtksourceview/gtksourceview.h b/gtksourceview/gtksourceview.h
index af1a7463..4b448722 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];
@@ -183,6 +186,11 @@ void                            gtk_source_view_set_smart_home_end
 GTK_SOURCE_AVAILABLE_IN_ALL
 GtkSourceSmartHomeEndType       gtk_source_view_get_smart_home_end                (GtkSourceView             
     *view);
 GTK_SOURCE_AVAILABLE_IN_ALL
+void                            gtk_source_view_set_enable_snippets               (GtkSourceView             
     *view,
+                                                                                   gboolean                  
      enable_snippets);
+GTK_SOURCE_AVAILABLE_IN_ALL
+gboolean                        gtk_source_view_get_enable_snippets               (GtkSourceView             
     *view);
+GTK_SOURCE_AVAILABLE_IN_ALL
 guint                           gtk_source_view_get_visual_column                 (GtkSourceView             
     *view,
                                                                                    const GtkTextIter         
     *iter);
 GTK_SOURCE_AVAILABLE_IN_ALL
@@ -197,5 +205,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 e0f3945d..e8b85bfc 100644
--- a/gtksourceview/meson.build
+++ b/gtksourceview/meson.build
@@ -33,6 +33,10 @@ core_public_h = files([
   'gtksourceregion.h',
   'gtksourcesearchcontext.h',
   'gtksourcesearchsettings.h',
+  'gtksourcesnippet.h',
+  'gtksourcesnippetchunk.h',
+  'gtksourcesnippetcontext.h',
+  'gtksourcesnippetmanager.h',
   'gtksourcespacedrawer.h',
   'gtksourcestyle.h',
   'gtksourcestylescheme.h',
@@ -73,6 +77,10 @@ core_public_c = files([
   'gtksourceregion.c',
   'gtksourcesearchcontext.c',
   'gtksourcesearchsettings.c',
+  'gtksourcesnippet.c',
+  'gtksourcesnippetchunk.c',
+  'gtksourcesnippetcontext.c',
+  'gtksourcesnippetmanager.c',
   'gtksourcespacedrawer.c',
   'gtksourcestyle.c',
   'gtksourcestylescheme.c',
@@ -100,6 +108,9 @@ core_private_c = files([
   'gtksourcemarkssequence.c',
   'gtksourcepixbufhelper.c',
   'gtksourceregex.c',
+  'gtksourcesnippetbundle.c',
+  'gtksourcesnippetbundle-parser.c',
+  'gtksourceview-snippets.c',
 ])
 
 core_c_args = [
diff --git a/tests/meson.build b/tests/meson.build
index a33e8228..4dddf302 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -8,6 +8,7 @@ tests_sources = {
                     'int2str': ['test-int2str.c'],
                      'search': ['test-search.c'],
         'search-performances': ['test-search-performances.c'],
+                   'snippets': ['test-snippets.c'],
               'space-drawing': ['test-space-drawing.c'],
                      'widget': ['test-widget.c'],
 }
@@ -18,6 +19,11 @@ tests_resources = {
       'widget': 'test-widget.gresource.xml',
 }
 
+tests_deps = [gtksource_dep]
+if cc.get_id() == 'msvc'
+  tests_deps += [core_dep]
+endif
+
 foreach test_name, test_sources: tests_sources
   if tests_resources.has_key(test_name)
     test_sources += gnome.compile_resources(
@@ -30,6 +36,6 @@ foreach test_name, test_sources: tests_sources
   # well as the static core lib
   executable('test-@0@'.format(test_name), test_sources,
           c_args: tests_c_args,
-    dependencies: cc.get_id() == 'msvc' ? [gtksource_dep, core_dep] : [gtksource_dep],
+    dependencies: tests_deps,
   )
 endforeach
diff --git a/tests/test-snippets.c b/tests/test-snippets.c
new file mode 100644
index 00000000..14d68201
--- /dev/null
+++ b/tests/test-snippets.c
@@ -0,0 +1,58 @@
+/*
+ * 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 <gtksourceview/gtksource.h>
+#include <gtksourceview/gtksourceinit.h>
+
+static const gchar *search_path[] = {
+       TOP_SRCDIR"/data/snippets",
+       NULL
+};
+
+gint
+main (gint argc,
+      gchar *argv[])
+{
+       GtkSourceSnippetManager *mgr;
+       GtkSourceSnippet *snippet;
+       const gchar **groups;
+
+       gtk_init ();
+       gtk_source_init ();
+
+       mgr = gtk_source_snippet_manager_get_default ();
+       gtk_source_snippet_manager_set_search_path (mgr, search_path);
+
+       /* Update if you add new groups to data/snippets/ */
+       groups = gtk_source_snippet_manager_list_groups (mgr);
+       g_assert_cmpint (1, ==, g_strv_length ((gchar **)groups));
+       g_assert_cmpstr (groups[0], ==, "Licenses");
+       g_free (groups);
+
+       /* Make sure we can get gpl3 snippet for C language */
+       snippet = gtk_source_snippet_manager_get_snippet (mgr, NULL, "c", "gpl3");
+       g_assert_nonnull (snippet);
+       g_assert_finalize_object (snippet);
+
+       gtk_source_finalize ();
+
+       return 0;
+}
diff --git a/tests/test-widget.c b/tests/test-widget.c
index 45e99b7a..d5a75211 100644
--- a/tests/test-widget.c
+++ b/tests/test-widget.c
@@ -307,6 +307,14 @@ show_right_margin_toggled_cb (TestWidget     *self,
        gtk_source_view_set_show_right_margin (self->priv->view, enabled);
 }
 
+static void
+enable_snippets_toggled_cb (TestWidget     *self,
+                            GtkCheckButton *button)
+{
+       gboolean enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button));
+       gtk_source_view_set_enable_snippets (self->priv->view, enabled);
+}
+
 static void
 right_margin_position_value_changed_cb (TestWidget    *self,
                                        GtkSpinButton *button)
@@ -969,6 +977,7 @@ test_widget_class_init (TestWidgetClass *klass)
        gtk_widget_class_bind_template_callback (widget_class, backward_string_clicked_cb);
        gtk_widget_class_bind_template_callback (widget_class, forward_string_clicked_cb);
        gtk_widget_class_bind_template_callback (widget_class, smart_home_end_changed_cb);
+       gtk_widget_class_bind_template_callback (widget_class, enable_snippets_toggled_cb);
 
        gtk_widget_class_bind_template_child_private (widget_class, TestWidget, view);
        gtk_widget_class_bind_template_child_private (widget_class, TestWidget, map);
@@ -1090,6 +1099,26 @@ test_widget_new (void)
        return g_object_new (test_widget_get_type (), NULL);
 }
 
+static void
+setup_search_paths (void)
+{
+       GtkSourceSnippetManager *snippets;
+       GtkSourceStyleSchemeManager *styles;
+       GtkSourceLanguageManager *languages;
+       static const gchar *snippets_path[] = { TOP_SRCDIR"/data/snippets", NULL };
+       static const gchar *langs_path[] = { TOP_SRCDIR"/data/language-specs", NULL };
+
+       snippets = gtk_source_snippet_manager_get_default ();
+       gtk_source_snippet_manager_set_search_path (snippets, snippets_path);
+
+       /* Allow use of system styles, but prefer in-tree */
+       styles = gtk_source_style_scheme_manager_get_default ();
+       gtk_source_style_scheme_manager_prepend_search_path (styles, TOP_SRCDIR"/data/styles");
+
+       languages = gtk_source_language_manager_get_default ();
+       gtk_source_language_manager_set_search_path (languages, langs_path);
+}
+
 int
 main (int argc, char *argv[])
 {
@@ -1098,6 +1127,7 @@ main (int argc, char *argv[])
 
        gtk_init ();
        gtk_source_init ();
+       setup_search_paths ();
 
        window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
        gtk_window_set_default_size (GTK_WINDOW (window), 900, 600);
diff --git a/tests/test-widget.ui b/tests/test-widget.ui
index 834e0c1d..191d3441 100644
--- a/tests/test-widget.ui
+++ b/tests/test-widget.ui
@@ -214,6 +214,17 @@
                 </layout>
               </object>
             </child>
+            <child>
+              <object class="GtkCheckButton" id="enable_snippets">
+                <property name="label">Enable snippets</property>
+                <property name="can-focus">1</property>
+                <signal name="toggled" handler="enable_snippets_toggled_cb" object="TestWidget" 
swapped="yes"/>
+                <layout>
+                  <property name="top-attach">13</property>
+                  <property name="left-attach">0</property>
+                </layout>
+              </object>
+            </child>
             <child>
               <object class="GtkGrid" id="grid10">
                 <layout>


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