[gtksourceview/wip/chergert/gsv-gtk4: 229/259] This is a straightforward port of Builder's snippet system to GtkSourceView. A number of new objects




commit 76d5a1ff77ffc16f6eee0da57344b0266743957e
Author: Christian Hergert <chergert redhat com>
Date:   Tue Sep 8 18:21:13 2020 -0700

    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.
    
     - A completion provider is provided so that applications can use snippets
       provided by the snippet manager, however it still needs porting to the
       new completion engine.
    
     - 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.
    
     - Documentation for the file format and snippet text format is provided
       as part of the gtk-doc installation.
    
     - A snippets.rng file is provided to validate snippet files
    
     - Various style schemes have gotten "snippet-focus" styles that apply
       to the focus positions of the snippet.

 data/meson.build                                   |    5 +
 data/snippets/licenses.snippets                    |  342 +++++
 data/snippets/snippets.rng                         |   58 +
 data/styles/classic.xml                            |    1 +
 data/styles/cobalt.xml                             |    1 +
 data/styles/kate.xml                               |    1 +
 data/styles/oblivion.xml                           |    1 +
 data/styles/solarized-dark.xml                     |    1 +
 data/styles/solarized-light.xml                    |    1 +
 data/styles/tango.xml                              |    1 +
 docs/reference/gtksourceview-5.0-sections.txt      |  113 ++
 docs/reference/gtksourceview-docs.xml.in           |   10 +
 docs/reference/meson.build                         |   11 +
 docs/reference/snippet-reference.xml.in            |  207 +++
 gtksourceview/completion-providers/meson.build     |    1 +
 .../snippets/gtksourcecompletionsnippets.c         |  425 ++++++
 .../snippets/gtksourcecompletionsnippets.h         |   49 +
 .../gtksourcecompletionsnippetsproposal-private.h  |   37 +
 .../snippets/gtksourcecompletionsnippetsproposal.c |  184 +++
 .../completion-providers/snippets/meson.build      |   49 +
 gtksourceview/gtksource.h                          |    8 +-
 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                   | 1503 ++++++++++++++++++++
 gtksourceview/gtksourcesnippet.h                   |   75 +
 gtksourceview/gtksourcesnippetbundle-parser.c      |  359 +++++
 gtksourceview/gtksourcesnippetbundle-private.h     |   67 +
 gtksourceview/gtksourcesnippetbundle.c             |  666 +++++++++
 gtksourceview/gtksourcesnippetchunk-private.h      |   55 +
 gtksourceview/gtksourcesnippetchunk.c              |  592 ++++++++
 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              |   29 +-
 gtksourceview/gtksourceview-snippets.c             |  560 ++++++++
 gtksourceview/gtksourceview.c                      |  249 ++++
 gtksourceview/gtksourceview.h                      |   12 +
 gtksourceview/meson.build                          |   11 +
 tests/meson.build                                  |    8 +-
 tests/test-snippets.c                              |   58 +
 tests/test-widget.c                                |   57 +-
 tests/test-widget.ui                               |   11 +
 53 files changed, 7525 insertions(+), 16 deletions(-)
---
diff --git a/data/meson.build b/data/meson.build
index 995df87d..e92067b9 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -25,4 +25,9 @@ install_subdir('fonts',
   exclude_files: [ 'BuilderBlocks.ttx' ]
 )
 
+install_subdir('snippets',
+    install_dir: pkgdatadir,
+  exclude_files: [],
+)
+
 subdir('icons')
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/snippets/snippets.rng b/data/snippets/snippets.rng
new file mode 100644
index 00000000..769ea9e5
--- /dev/null
+++ b/data/snippets/snippets.rng
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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/>.
+
+-->
+<grammar xmlns="http://relaxng.org/ns/structure/1.0";
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes";>
+<start>
+  <element name="snippets">
+    <choice>
+      <attribute name="group"/>
+      <attribute name="_group"/>
+    </choice>
+    <attribute name="version">
+      <value>1.0</value>
+    </attribute>
+    <optional>
+      <oneOrMore>
+        <element name="snippet">
+          <choice>
+            <attribute name="name"/>
+            <attribute name="_name"/>
+          </choice>
+          <choice>
+            <attribute name="description"/>
+            <attribute name="_description"/>
+          </choice>
+          <attribute name="trigger"/>
+          <oneOrMore>
+            <element name="text">
+              <optional>
+                <attribute name="languages"/>
+              </optional>
+              <text/>
+            </element>
+          </oneOrMore>
+        </element>
+      </oneOrMore>
+    </optional>
+  </element>
+</start>
+</grammar>
diff --git a/data/styles/classic.xml b/data/styles/classic.xml
index 183f5881..ad75bbcc 100644
--- a/data/styles/classic.xml
+++ b/data/styles/classic.xml
@@ -47,6 +47,7 @@
   <style name="current-line-number"         background="#eeeeec"/>
   <style name="draw-spaces"                 foreground="#babdb6"/>
   <style name="background-pattern"          background="#rgba(73,74,71,0.1)"/>
+  <style name="snippet-focus"               background="gray"/>
 
   <!-- Bracket Matching -->
   <style name="bracket-match"               foreground="white" background="gray"/>
diff --git a/data/styles/cobalt.xml b/data/styles/cobalt.xml
index 44051567..6d6159ee 100644
--- a/data/styles/cobalt.xml
+++ b/data/styles/cobalt.xml
@@ -60,6 +60,7 @@
   <style name="draw-spaces"                 foreground="bluish_grey"/>
   <style name="right-margin"                foreground="light_grey" background="light_grey"/>
   <style name="background-pattern"          background="dark_medium_blue_blend"/>
+  <style name="snippet-focus"               foreground="light_blue" background="steelblue3"/>
 
   <!-- Bracket Matching -->
   <style name="bracket-match"               background="steelblue3"/>
diff --git a/data/styles/kate.xml b/data/styles/kate.xml
index 617ba583..5c786d2d 100644
--- a/data/styles/kate.xml
+++ b/data/styles/kate.xml
@@ -45,6 +45,7 @@
   <style name="bracket-mismatch"            background="red"/>
   <style name="draw-spaces"                 foreground="#d3d7cf"/>
   <style name="background-pattern"          background="#f3f3f3"/>
+  <style name="snippet-focus"               foreground="white" background="grey"/>
 
   <!-- Search Matching -->
   <style name="search-match"                background="yellow"/>
diff --git a/data/styles/oblivion.xml b/data/styles/oblivion.xml
index 4b0a8cec..f69f9196 100644
--- a/data/styles/oblivion.xml
+++ b/data/styles/oblivion.xml
@@ -69,6 +69,7 @@
   <style name="line-numbers"                foreground="aluminium5" background="black"/>
   <style name="draw-spaces"                 foreground="aluminium4"/>
   <style name="background-pattern"          background="aluminium6-5-blend"/>
+  <style name="snippet-focus"               foreground="aluminium6" background="aluminium4-3-blend"/>
 
   <!-- Bracket Matching -->
   <style name="bracket-match"               foreground="chocolate2"/>
diff --git a/data/styles/solarized-dark.xml b/data/styles/solarized-dark.xml
index 16cf7c7f..0a67d9e8 100644
--- a/data/styles/solarized-dark.xml
+++ b/data/styles/solarized-dark.xml
@@ -51,6 +51,7 @@
   <style name="secondary-cursor"            foreground="base00"/>
   <style name="current-line"                background="base02"/>
   <style name="line-numbers"                foreground="base01" background="base02"/>
+  <style name="snippet-focus"               foreground="base01" background="base02"/>
   <style name="background-pattern"          background="base03-02-blend"/>
 
   <!-- Bracket Matching -->
diff --git a/data/styles/solarized-light.xml b/data/styles/solarized-light.xml
index 33d2bad6..ae2c8480 100644
--- a/data/styles/solarized-light.xml
+++ b/data/styles/solarized-light.xml
@@ -51,6 +51,7 @@
   <style name="cursor"                      foreground="base01"/>
   <style name="current-line"                background="base2"/>
   <style name="line-numbers"                foreground="base1" background="base2"/>
+  <style name="snippet-focus"               foreground="base1" background="base2"/>
   <style name="background-pattern"          background="base3-2-blend"/>
   <style name="draw-spaces"                 foreground="base4"/>
 
diff --git a/data/styles/tango.xml b/data/styles/tango.xml
index 6b272176..cd6f97da 100644
--- a/data/styles/tango.xml
+++ b/data/styles/tango.xml
@@ -59,6 +59,7 @@
   <style name="current-line-number"         background="aluminium1"/>
   <style name="draw-spaces"                 foreground="aluminium3"/>
   <style name="background-pattern"          background="#f3f3f3"/>
+  <style name="snippet-focus"               foreground="aluminium6" background="aluminium2"/>
 
   <!-- Bracket Matching -->
   <style name="bracket-match"               foreground="aluminium1" background="aluminium3"/>
diff --git a/docs/reference/gtksourceview-5.0-sections.txt b/docs/reference/gtksourceview-5.0-sections.txt
index 504cd009..bacb98da 100644
--- a/docs/reference/gtksourceview-5.0-sections.txt
+++ b/docs/reference/gtksourceview-5.0-sections.txt
@@ -206,6 +206,21 @@ GtkSourceCompletionWordsClass
 gtk_source_completion_words_get_type
 </SECTION>
 
+<SECTION>
+<FILE>completionsnippets</FILE>
+GtkSourceCompletionSnippets
+gtk_source_completion_snippets_new
+<SUBSECTION Standard>
+GTK_SOURCE_COMPLETION_SNIPPETS
+GTK_SOURCE_COMPLETION_SNIPPETS_CLASS
+GTK_SOURCE_COMPLETION_SNIPPETS_GET_CLASS
+GTK_SOURCE_IS_COMPLETION_SNIPPETS
+GTK_SOURCE_IS_COMPLETION_SNIPPETS_CLASS
+GTK_SOURCE_TYPE_COMPLETION_SNIPPETS
+GtkSourceCompletionSnippetsClass
+gtk_source_completion_snippets_get_type
+</SECTION>
+
 <SECTION>
 <FILE>encoding</FILE>
 GtkSourceEncoding
@@ -703,6 +718,101 @@ GtkSourceSearchSettingsClass
 gtk_source_search_settings_get_type
 </SECTION>
 
+<SECTION>
+<FILE>snippet</FILE>
+GtkSourceSnippet
+gtk_source_snippet_add_chunk
+gtk_source_snippet_copy
+gtk_source_snippet_get_context
+gtk_source_snippet_get_description
+gtk_source_snippet_get_focus_position
+gtk_source_snippet_get_language_id
+gtk_source_snippet_get_name
+gtk_source_snippet_get_n_chunks
+gtk_source_snippet_get_nth_chunk
+gtk_source_snippet_get_trigger
+gtk_source_snippet_new
+gtk_source_snippet_set_description
+gtk_source_snippet_set_language_id
+gtk_source_snippet_set_name
+gtk_source_snippet_set_trigger
+<SUBSECTION Standard>
+GTK_SOURCE_IS_SNIPPET
+GTK_SOURCE_IS_SNIPPET_CLASS
+GTK_SOURCE_SNIPPET
+GTK_SOURCE_SNIPPET_CLASS
+GTK_SOURCE_SNIPPET_GET_CLASS
+GTK_SOURCE_TYPE_SNIPPET
+GtkSourceSnippetClass
+</SECTION>
+
+<SECTION>
+<FILE>snippetchunk</FILE>
+GtkSourceSnippetChunk
+gtk_source_snippet_chunk_copy
+gtk_source_snippet_chunk_get_context
+gtk_source_snippet_chunk_get_focus_position
+gtk_source_snippet_chunk_get_spec
+gtk_source_snippet_chunk_get_text
+gtk_source_snippet_chunk_get_text_set
+gtk_source_snippet_chunk_new
+gtk_source_snippet_chunk_set_context
+gtk_source_snippet_chunk_set_focus_position
+gtk_source_snippet_chunk_set_spec
+gtk_source_snippet_chunk_set_text
+gtk_source_snippet_chunk_set_text_set
+<SUBSECTION Standard>
+GTK_SOURCE_IS_SNIPPET_CHUNK
+GTK_SOURCE_IS_SNIPPET_CHUNK_CLASS
+GTK_SOURCE_SNIPPET_CHUNK
+GTK_SOURCE_SNIPPET_CHUNK_CLASS
+GTK_SOURCE_SNIPPET_CHUNK_GET_CLASS
+GTK_SOURCE_TYPE_SNIPPET_CHUNK
+GtkSourceSnippetChunkClass
+</SECTION>
+
+<SECTION>
+<FILE>snippetcontext</FILE>
+GtkSourceSnippetContext
+gtk_source_snippet_context_clear_variables
+gtk_source_snippet_context_expand
+gtk_source_snippet_context_get_variable
+gtk_source_snippet_context_new
+gtk_source_snippet_context_set_constant
+gtk_source_snippet_context_set_line_prefix
+gtk_source_snippet_context_set_tab_width
+gtk_source_snippet_context_set_use_spaces
+gtk_source_snippet_context_set_variable
+<SUBSECTION Standard>
+GTK_SOURCE_IS_SNIPPET_CONTEXT
+GTK_SOURCE_IS_SNIPPET_CONTEXT_CLASS
+GTK_SOURCE_SNIPPET_CONTEXT
+GTK_SOURCE_SNIPPET_CONTEXT_CLASS
+GTK_SOURCE_SNIPPET_CONTEXT_GET_CLASS
+GTK_SOURCE_TYPE_SNIPPET_CONTEXT
+GtkSourceSnippetContextClass
+</SECTION>
+
+<SECTION>
+<FILE>snippetmanager</FILE>
+GtkSourceSnippetManager
+gtk_source_snippet_manager_get_default
+gtk_source_snippet_manager_get_search_path
+gtk_source_snippet_manager_get_snippet
+gtk_source_snippet_manager_list_groups
+gtk_source_snippet_manager_list_matching
+gtk_source_snippet_manager_set_search_path
+<SUBSECTION Standard>
+GTK_SOURCE_IS_SNIPPET_MANAGER
+GTK_SOURCE_IS_SNIPPET_MANAGER_CLASS
+GTK_SOURCE_SNIPPET_MANAGER
+GTK_SOURCE_SNIPPET_MANAGER_CLASS
+GTK_SOURCE_SNIPPET_MANAGER_GET_CLASS
+GTK_SOURCE_TYPE_SNIPPET_MANAGER
+GtkSourceSnippetManagerClass
+</SUBSECTION>
+</SECTION>
+
 <SECTION>
 <FILE>spacedrawer</FILE>
 GtkSourceSpaceDrawer
@@ -924,6 +1034,9 @@ gtk_source_view_set_show_line_marks
 gtk_source_view_get_show_line_marks
 gtk_source_view_set_background_pattern
 gtk_source_view_get_background_pattern
+gtk_source_view_get_enable_snippets
+gtk_source_view_set_enable_snippets
+gtk_source_view_push_snippet
 <SUBSECTION Standard>
 GtkSourceViewClass
 GTK_SOURCE_IS_VIEW
diff --git a/docs/reference/gtksourceview-docs.xml.in b/docs/reference/gtksourceview-docs.xml.in
index de572897..89e3bd49 100644
--- a/docs/reference/gtksourceview-docs.xml.in
+++ b/docs/reference/gtksourceview-docs.xml.in
@@ -50,6 +50,7 @@
       <xi:include href="xml/completionitem.xml"/>
       <xi:include href="xml/completionproposal.xml"/>
       <xi:include href="xml/completionprovider.xml"/>
+      <xi:include href="xml/completionsnippets.xml"/>
       <xi:include href="xml/completionwords.xml"/>
     </chapter>
 
@@ -83,6 +84,15 @@
       <xi:include href="xml/searchsettings.xml"/>
     </chapter>
 
+    <chapter id="snippets">
+      <title>Snippets</title>
+      <xi:include href="snippet-reference.xml"/>
+      <xi:include href="xml/snippet.xml"/>
+      <xi:include href="xml/snippetchunk.xml"/>
+      <xi:include href="xml/snippetcontext.xml"/>
+      <xi:include href="xml/snippetmanager.xml"/>
+    </chapter>
+
     <chapter id="misc">
       <title>Misc</title>
       <xi:include href="xml/map.xml"/>
diff --git a/docs/reference/meson.build b/docs/reference/meson.build
index 622a4de8..8fe0ecf3 100644
--- a/docs/reference/meson.build
+++ b/docs/reference/meson.build
@@ -16,6 +16,7 @@ reference_private_h = [
   'gtksourcecompletioncontext-private.h',
   'gtksourcecompletioninfo-private.h',
   'gtksourcecompletionmodel-private.h',
+  'gtksourcecompletionsnippetsproposal-private.h',
   'gtksourcecompletionwordsbuffer-private.h',
   'gtksourcecompletionwordslibrary-private.h',
   'gtksourcecompletionwordsproposal-private.h',
@@ -35,6 +36,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',
@@ -65,6 +70,12 @@ lang_reference_xml = configure_file(
   configuration: config_h
 )
 
+snippet_reference_xml = configure_file(
+          input: 'snippet-reference.xml.in',
+         output: 'snippet-reference.xml',
+  configuration: config_h
+)
+
 style_reference_xml = configure_file(
           input: 'style-reference.xml.in',
          output: 'style-reference.xml',
diff --git a/docs/reference/snippet-reference.xml.in b/docs/reference/snippet-reference.xml.in
new file mode 100644
index 00000000..0d10c907
--- /dev/null
+++ b/docs/reference/snippet-reference.xml.in
@@ -0,0 +1,207 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+   "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd";
+[
+ <!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'">
+]>
+<refentry id="snippet-reference">
+<refmeta>
+<refentrytitle>Snippet Definition Reference</refentrytitle>
+</refmeta>
+
+<refnamediv>
+<refname>Snippet Definition Reference</refname>
+<refpurpose>
+Reference to the GtkSourceView snippet definition file format
+</refpurpose>
+</refnamediv>
+
+<refsect1>
+<title>Overview</title>
+<para>
+This is an overview of the Snippet Definition XML format, describing the
+meaning and usage of every element and attribute.  The formal definition is
+stored in the RelaxNG schema file <filename>snippets.rng</filename> which
+should be installed on your system in the directory
+<filename>${PREFIX}/share/gtksourceview-@GSV_API_VERSION@/</filename> (where
+<filename>${PREFIX}</filename> can be <filename>/usr/</filename> or
+<filename>/usr/local/</filename> if you have installed from source).
+</para>
+</refsect1>
+
+<para>
+The toplevel tag in a snippet file is <code>&lt;snippets&gt;</code>.
+It has the following attributes:
+<variablelist>
+<varlistentry>
+<term><code>_group</code></term>
+<listitem><para>
+The group for the snippet. This is purely used to group snippets together
+in user-interfaces.
+</para></listitem>
+</varlistentry>
+</variablelist>
+</para>
+
+<para>
+Within the snippets tag is one or more <code>&lt;snippets&gt;</code> elements.
+It has the following attributes:
+<variablelist>
+<varlistentry>
+<term><code>_name</code></term>
+<listitem><para>
+The name of the snippet. This may be displayed in the user interface such as
+in a snippet editor or completion providers. An attempt will be made to translate
+it by GtkSourceView.
+</para></listitem>
+</varlistentry>
+<varlistentry>
+<term><code>trigger</code></term>
+<listitem><para>
+The word trigger of the snippet. If the user types this word and hits Tab,
+the snippet will be inserted.
+</para></listitem>
+</varlistentry>
+<varlistentry>
+<term><code>_description</code></term>
+<listitem><para>
+The description of the snippet. This may be displayed in the user interface
+such as in a snippet editor or completion providers. An attempt will be made
+to translate it by GtkSourceView.
+</para></listitem>
+</varlistentry>
+</variablelist>
+</para>
+
+<para>
+Within the snippet tag is one or more <code>&lt;text&gt;</code> elements.
+It has the following attributes:
+<variablelist>
+<varlistentry>
+<term><code>languages</code></term>
+<listitem><para>
+A semicolon separated list of GtkSourceView language identifiers for which this
+text should be used when inserting the snippet. Defining this on the
+<code>&lt;text&gt;</code> tag allows a snippet to have multiple variants based
+on the programming language.
+</para></listitem>
+</varlistentry>
+<varlistentry>
+<term><code>CDATA</code></term>
+<listitem><para>
+Within the <code>&lt;text&gt;</code> tag should be a single
+<code>&lt;![CDATA[]]&gt;</code> tag containing the text for the snippet
+between the []. You do not need to use CDATA if the text does not have any
+embedded characters that will conflict with XML.
+</para></listitem>
+</varlistentry>
+</variablelist>
+</para>
+
+<refsect1>
+<title>Snippet Text Format</title>
+<para>
+GtkSourceView's snippet text format is largely based upon other snippet
+implementations that have gained popularity. Since there are so many, it likely
+differs in some small ways from what you may have used previously.
+</para>
+
+<refsect2>
+<title>Focus Positions</title>
+<para>Focus positions allow the user to move through chunks within the snippet
+that are meant to be edited by the user. Each subsequent "Tab" by the user will
+advance them to the next focus position until all have been exhausted.</para>
+<para>To set a focus position in your snippet, use a dollar sign followed by
+curly braces with the focus position number inside like <code>${1}</code> or
+<code>${2}</code>. The user can also use Shift+Tab to move to a previous tab
+stop.</para>
+<para>The special <code>$0</code> tab stop is used to place the cursor after
+all focus positions have been exhausted. If no focus position was provided, the
+cursor will be placed there automatically.</para>
+<para>You can also set a default value for a focus position by appending a colon
+and the initial value such as <code>${2:default_value}</code>. You can even
+reference other chunks such as <code>${3:$2_$1}</code> to join the contents of
+<code>$2</code>, an underbar <code>_</code>, and <code>$1</code>.</para>
+</refsect2>
+
+<refsect2>
+<title>Variable Expansion</title>
+<para>When a snippet is expanded into the #GtkSourceView, chunks may reference
+a number of built-in or application provided variables. Applications may
+add additional variables with either gtk_source_snippet_context_set_constant()
+(for things that do not change) or gtk_source_snippet_context_set_variable()
+for values that may change.</para>
+<para>Snippet chunks can reference a variable anywhere by using a dollar sign
+followed by the variable name such as <code>$variable</code>.</para>
+<para>You can also reference another focus position's value by usin their focus
+position number as the variable such as <code>$1</code>.</para>
+<para>To post-process a variables value, enclose the variable in curly-brackets
+and use the pipe operator to denote the post-processing function such as
+<code>${$1|capitalize}</code>.</para>
+</refsect2>
+
+<refsect2>
+<title>Predefined Variables</title>
+<para>A number of predefined variables are provided by GtkSourceView which can be extended by applications.
+<variablelist>
+<varlistentry><term><code>$CURRENT_YEAR</code></term><listitem><para>The current 4-digit 
year</para></listitem></varlistentry>
+<varlistentry><term><code>$CURRENT_YEAR_SHORT</code></term><listitem><para>The last 2 digits of the current 
year</para></listitem></varlistentry>
+<varlistentry><term><code>$CURRENT_MONTH</code></term><listitem><para>The current month as a number from 
01-12</para></listitem></varlistentry>
+<varlistentry><term><code>$CURRENT_MONTH_NAME</code></term><listitem><para>The full month name such as 
"August" in the current locale</para></listitem></varlistentry>
+<varlistentry><term><code>$CURRENT_MONTH_NAME_SHORT</code></term><listitem><para>The short month name such 
as "Aug" in the current locale</para></listitem></varlistentry>
+<varlistentry><term><code>$CURRENT_DATE</code></term><listitem><para>The current day of the month from 
1-31</para></listitem></varlistentry>
+<varlistentry><term><code>$CURRENT_DAY_NAME</code></term><listitem><para>The current day such as "Friday" in 
the current locale</para></listitem></varlistentry>
+<varlistentry><term><code>$CURRENT_DAY_NAME_SHORT</code></term><listitem><para>The current day using the 
shortened name such as "Fri" in the current locale</para></listitem></varlistentry>
+<varlistentry><term><code>$CURRENT_HOUR</code></term><listitem><para>The current hour in 24-hour 
format</para></listitem></varlistentry>
+<varlistentry><term><code>$CURRENT_MINUTE</code></term><listitem><para>The current minute within the 
hour</para></listitem></varlistentry>
+<varlistentry><term><code>$CURRENT_SECOND</code></term><listitem><para>The current second within the 
minute</para></listitem></varlistentry>
+<varlistentry><term><code>$CURRENT_SECONDS_UNIX</code></term><listitem><para>The number of seconds since the 
UNIX epoch</para></listitem></varlistentry>
+<varlistentry><term><code>$NAME_SHORT</code></term><listitem><para>The users login user name (See 
g_get_user_name())</para></listitem></varlistentry>
+<varlistentry><term><code>$NAME</code></term><listitem><para>The users full name, if known (See 
g_get_full_name())</para></listitem></varlistentry>
+<varlistentry><term><code>$TM_CURRENT_LINE</code></term><listitem><para>The contents of the current 
line</para></listitem></varlistentry>
+<varlistentry><term><code>$TM_LINE_INDEX</code></term><listitem><para>The zero-index based line 
number</para></listitem></varlistentry>
+<varlistentry><term><code>$TM_LINE_NUMBER</code></term><listitem><para>The one-index based line 
number</para></listitem></varlistentry>
+</variablelist>
+</para>
+</refsect2>
+
+<refsect2>
+<title>Post-Processing</title>
+<para>
+By appending a pipe to a variable within curly braces, you can post-process
+the variable. A number of built-in functions are available for processing.
+For example <code>${$1|stripsuffix|functify}</code>.
+
+<variablelist>
+<varlistentry><term><code>lower</code></term><listitem><para></para></listitem></varlistentry>
+<varlistentry><term><code>upper</code></term><listitem><para></para></listitem></varlistentry>
+<varlistentry><term><code>captialize</code></term><listitem><para></para></listitem></varlistentry>
+<varlistentry><term><code>uncapitalize</code></term><listitem><para></para></listitem></varlistentry>
+<varlistentry><term><code>html</code></term><listitem><para></para></listitem></varlistentry>
+<varlistentry><term><code>camelize</code></term><listitem><para></para></listitem></varlistentry>
+<varlistentry><term><code>functify</code></term><listitem><para></para></listitem></varlistentry>
+<varlistentry><term><code>namespace</code></term><listitem><para></para></listitem></varlistentry>
+<varlistentry><term><code>class</code></term><listitem><para></para></listitem></varlistentry>
+<varlistentry><term><code>instance</code></term><listitem><para></para></listitem></varlistentry>
+<varlistentry><term><code>space</code></term><listitem><para></para></listitem></varlistentry>
+<varlistentry><term><code>stripsuffix</code></term><listitem><para></para></listitem></varlistentry>
+<varlistentry><term><code>slash_to_dots</code></term><listitem><para></para></listitem></varlistentry>
+<varlistentry><term><code>descend_path</code></term><listitem><para></para></listitem></varlistentry>
+</variablelist>
+</para>
+</refsect2>
+
+</refsect1>
+
+
+<refsect1>
+<title>Default snippets</title>
+<para>
+The GtkSourceView team prefers to just keep a small number of snippets
+distributed with the library. To add a new snippet in GtkSourceView itself,
+the snippet must be very popular, and ideally a GtkSourceView-based application
+must use it by default.
+</para>
+</refsect1>
+
+</refentry>
diff --git a/gtksourceview/completion-providers/meson.build b/gtksourceview/completion-providers/meson.build
index f2613a31..cf109f23 100644
--- a/gtksourceview/completion-providers/meson.build
+++ b/gtksourceview/completion-providers/meson.build
@@ -1 +1,2 @@
+#subdir('snippets')
 subdir('words')
diff --git a/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippets.c 
b/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippets.c
new file mode 100644
index 00000000..db0b91ba
--- /dev/null
+++ b/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippets.c
@@ -0,0 +1,425 @@
+/*
+ * 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 <gtksourceview/gtksource-enumtypes.h>
+#include <gtksourceview/gtksourcebuffer.h>
+#include <gtksourceview/gtksourcecompletion.h>
+#include <gtksourceview/gtksourceiter-private.h>
+#include <gtksourceview/gtksourcelanguage.h>
+#include <gtksourceview/gtksourcesnippet.h>
+#include <gtksourceview/gtksourcesnippetmanager.h>
+#include <gtksourceview/gtksourceview.h>
+
+#include <gtksourceview/completion-providers/words/gtksourcecompletionwordsutils-private.h>
+
+#include "gtksourcecompletionsnippets.h"
+#include "gtksourcecompletionsnippetsproposal-private.h"
+
+/**
+ * SECTION:completionsnippets
+ * @title: GtkSourceCompletionSnippets
+ * @short_description: A GtkSourceCompletionProvider for the completion of snippets
+ *
+ * The #GtkSourceCompletionSnippets is an example of an implementation of
+ * the #GtkSourceCompletionProvider interface. The proposals are snippets
+ * registered with the #GtkSourceSnippetManager.
+ *
+ * Since: 5.0
+ */
+
+typedef struct
+{
+       GdkTexture                    *icon;
+       gchar                         *name;
+       GtkSourceView                 *view;
+       gint                           interactive_delay;
+       gint                           priority;
+       gint                           minimum_word_size;
+       GtkSourceCompletionActivation  activation;
+} GtkSourceCompletionSnippetsPrivate;
+
+static void completion_provider_iface_init (GtkSourceCompletionProviderInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GtkSourceCompletionSnippets, gtk_source_completion_snippets, G_TYPE_OBJECT,
+                         G_ADD_PRIVATE (GtkSourceCompletionSnippets)
+                         G_IMPLEMENT_INTERFACE (GTK_SOURCE_TYPE_COMPLETION_PROVIDER,
+                                                completion_provider_iface_init))
+
+enum {
+       PROP_0,
+       PROP_ACTIVATION,
+       PROP_ICON,
+       PROP_INTERACTIVE_DELAY,
+       PROP_NAME,
+       PROP_PRIORITY,
+       N_PROPS
+};
+
+static GParamSpec *properties[N_PROPS];
+
+static gchar *
+get_word_at_iter (GtkTextIter *iter)
+{
+       GtkTextBuffer *buffer;
+       GtkTextIter start_line;
+       gchar *line_text;
+       gchar *word;
+
+       buffer = gtk_text_iter_get_buffer (iter);
+       start_line = *iter;
+       gtk_text_iter_set_line_offset (&start_line, 0);
+
+       line_text = gtk_text_buffer_get_text (buffer, &start_line, iter, FALSE);
+       word = _gtk_source_completion_words_utils_get_end_word (line_text);
+       g_free (line_text);
+
+       return word;
+}
+
+static GtkSourceView *
+get_view (GtkSourceCompletionContext *context)
+{
+       GtkSourceCompletion *completion;
+       GtkSourceView *view;
+
+       g_object_get (context, "completion", &completion, NULL);
+       view = gtk_source_completion_get_view (completion);
+       g_object_unref (completion);
+
+       return view;
+}
+
+static void
+gtk_source_completion_snippets_populate (GtkSourceCompletionProvider *provider,
+                                         GtkSourceCompletionContext  *context)
+{
+       GtkSourceCompletionSnippets *snippets = GTK_SOURCE_COMPLETION_SNIPPETS (provider);
+       GtkSourceCompletionSnippetsPrivate *priv = gtk_source_completion_snippets_get_instance_private 
(snippets);
+       GtkSourceCompletionActivation activation;
+       GtkSourceSnippetManager *manager;
+       GtkSourceLanguage *language;
+       GtkTextBuffer *buffer;
+       const gchar *language_id = NULL;
+       GtkTextIter iter;
+       GListModel *matches;
+       GList *list = NULL;
+       gchar *word;
+       guint n_items;
+
+       if (!gtk_source_completion_context_get_iter (context, &iter))
+       {
+               gtk_source_completion_context_add_proposals (context, provider, NULL, TRUE);
+               return;
+       }
+
+       priv->view = get_view (context);
+
+       word = get_word_at_iter (&iter);
+
+       activation = gtk_source_completion_context_get_activation (context);
+
+       if (word == NULL ||
+           (activation == GTK_SOURCE_COMPLETION_ACTIVATION_INTERACTIVE &&
+            g_utf8_strlen (word, -1) < (glong)priv->minimum_word_size))
+       {
+               g_free (word);
+               gtk_source_completion_context_add_proposals (context, provider, NULL, TRUE);
+               return;
+       }
+
+       manager = gtk_source_snippet_manager_get_default ();
+       buffer = gtk_text_iter_get_buffer (&iter);
+       language = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (buffer));
+       if (language != NULL)
+               language_id = gtk_source_language_get_id (language);
+
+       matches = gtk_source_snippet_manager_list_matching (manager, NULL, language_id, word);
+       n_items = g_list_model_get_n_items (matches);
+
+       for (guint i = 0; i < n_items; i++)
+       {
+               GtkSourceSnippet *snippet = g_list_model_get_item (matches, i);
+               list = g_list_prepend (list, gtk_source_completion_snippets_proposal_new (snippet));
+               g_object_unref (snippet);
+       }
+
+       gtk_source_completion_context_add_proposals (context, provider, list, TRUE);
+
+  g_list_free_full (list, g_object_unref);
+       g_object_unref (matches);
+       g_free (word);
+}
+
+static gchar *
+gtk_source_completion_snippets_get_name (GtkSourceCompletionProvider *self)
+{
+       GtkSourceCompletionSnippets *snippets = GTK_SOURCE_COMPLETION_SNIPPETS (self);
+       GtkSourceCompletionSnippetsPrivate *priv = gtk_source_completion_snippets_get_instance_private 
(snippets);
+
+       return g_strdup (priv->name);
+}
+
+static GdkTexture *
+gtk_source_completion_snippets_get_icon (GtkSourceCompletionProvider *self)
+{
+       GtkSourceCompletionSnippets *snippets = GTK_SOURCE_COMPLETION_SNIPPETS (self);
+       GtkSourceCompletionSnippetsPrivate *priv = gtk_source_completion_snippets_get_instance_private 
(snippets);
+
+       return priv->icon;
+}
+
+static gint
+gtk_source_completion_snippets_get_interactive_delay (GtkSourceCompletionProvider *provider)
+{
+       GtkSourceCompletionSnippets *snippets = GTK_SOURCE_COMPLETION_SNIPPETS (provider);
+       GtkSourceCompletionSnippetsPrivate *priv = gtk_source_completion_snippets_get_instance_private 
(snippets);
+
+       return priv->interactive_delay;
+}
+
+static gint
+gtk_source_completion_snippets_get_priority (GtkSourceCompletionProvider *provider)
+{
+       GtkSourceCompletionSnippets *snippets = GTK_SOURCE_COMPLETION_SNIPPETS (provider);
+       GtkSourceCompletionSnippetsPrivate *priv = gtk_source_completion_snippets_get_instance_private 
(snippets);
+
+       return priv->priority;
+}
+
+static GtkSourceCompletionActivation
+gtk_source_completion_snippets_get_activation (GtkSourceCompletionProvider *provider)
+{
+       GtkSourceCompletionSnippets *snippets = GTK_SOURCE_COMPLETION_SNIPPETS (provider);
+       GtkSourceCompletionSnippetsPrivate *priv = gtk_source_completion_snippets_get_instance_private 
(snippets);
+
+       return priv->activation;
+}
+
+static gboolean
+gtk_source_completion_snippets_activate_proposal (GtkSourceCompletionProvider *provider,
+                                                  GtkSourceCompletionProposal *proposal,
+                                                  GtkTextIter                 *iter)
+{
+       GtkSourceCompletionSnippets *snippets = GTK_SOURCE_COMPLETION_SNIPPETS (provider);
+       GtkSourceCompletionSnippetsPrivate *priv = gtk_source_completion_snippets_get_instance_private 
(snippets);
+       GtkSourceSnippet *snippet;
+       GtkSourceSnippet *copy;
+       GtkTextIter begin;
+
+       g_assert (GTK_SOURCE_IS_COMPLETION_SNIPPETS (snippets));
+       g_assert (GTK_SOURCE_IS_COMPLETION_SNIPPETS_PROPOSAL (proposal));
+
+       snippet = gtk_source_completion_snippets_proposal_get_snippet 
(GTK_SOURCE_COMPLETION_SNIPPETS_PROPOSAL (proposal));
+
+       if (snippet == NULL || priv->view == NULL)
+       {
+               return FALSE;
+       }
+
+       if (_gtk_source_iter_ends_full_word (iter))
+       {
+               begin = *iter;
+               _gtk_source_iter_backward_full_word_start (&begin);
+               gtk_text_buffer_delete (gtk_text_iter_get_buffer (&begin), &begin, iter);
+       }
+
+       copy = gtk_source_snippet_copy (snippet);
+       gtk_source_view_push_snippet (priv->view, copy, iter);
+       g_object_unref (copy);
+
+       return TRUE;
+}
+
+static void
+completion_provider_iface_init (GtkSourceCompletionProviderInterface *iface)
+{
+       iface->get_activation = gtk_source_completion_snippets_get_activation;
+       iface->get_icon = gtk_source_completion_snippets_get_icon;
+       iface->get_interactive_delay = gtk_source_completion_snippets_get_interactive_delay;
+       iface->get_name = gtk_source_completion_snippets_get_name;
+       iface->get_name = gtk_source_completion_snippets_get_name;
+       iface->get_priority = gtk_source_completion_snippets_get_priority;
+       iface->populate = gtk_source_completion_snippets_populate;
+       iface->activate_proposal = gtk_source_completion_snippets_activate_proposal;
+}
+
+static void
+gtk_source_completion_snippets_finalize (GObject *object)
+{
+       GtkSourceCompletionSnippets *provider = GTK_SOURCE_COMPLETION_SNIPPETS (object);
+       GtkSourceCompletionSnippetsPrivate *priv = gtk_source_completion_snippets_get_instance_private 
(provider);
+
+       priv->view = NULL;
+       g_clear_pointer (&priv->name, g_free);
+       g_clear_object (&priv->icon);
+
+       G_OBJECT_CLASS (gtk_source_completion_snippets_parent_class)->finalize (object);
+}
+
+static void
+gtk_source_completion_snippets_get_property (GObject    *object,
+                                             guint       prop_id,
+                                             GValue     *value,
+                                             GParamSpec *pspec)
+{
+       GtkSourceCompletionSnippets *self = GTK_SOURCE_COMPLETION_SNIPPETS (object);
+       GtkSourceCompletionSnippetsPrivate *priv = gtk_source_completion_snippets_get_instance_private (self);
+
+       switch (prop_id)
+       {
+       case PROP_NAME:
+               g_value_set_string (value, priv->name);
+               break;
+
+       case PROP_ICON:
+               g_value_set_object (value, priv->icon);
+               break;
+
+       case PROP_INTERACTIVE_DELAY:
+               g_value_set_int (value, priv->interactive_delay);
+               break;
+
+       case PROP_PRIORITY:
+               g_value_set_int (value, priv->priority);
+               break;
+
+       case PROP_ACTIVATION:
+               g_value_set_flags (value, priv->activation);
+               break;
+
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+gtk_source_completion_snippets_set_property (GObject      *object,
+                                             guint         prop_id,
+                                             const GValue *value,
+                                             GParamSpec   *pspec)
+{
+       GtkSourceCompletionSnippets *self = GTK_SOURCE_COMPLETION_SNIPPETS (object);
+       GtkSourceCompletionSnippetsPrivate *priv = gtk_source_completion_snippets_get_instance_private (self);
+
+       switch (prop_id)
+       {
+       case PROP_NAME:
+               g_free (priv->name);
+               priv->name = g_value_dup_string (value);
+
+               if (priv->name == NULL)
+               {
+                       priv->name = g_strdup (_("Snippets"));
+               }
+               break;
+
+       case PROP_ICON:
+               g_clear_object (&priv->icon);
+               priv->icon = g_value_dup_object (value);
+               break;
+
+       case PROP_INTERACTIVE_DELAY:
+               priv->interactive_delay = g_value_get_int (value);
+               break;
+
+       case PROP_PRIORITY:
+               priv->priority = g_value_get_int (value);
+               break;
+
+       case PROP_ACTIVATION:
+               priv->activation = g_value_get_flags (value);
+               break;
+
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+gtk_source_completion_snippets_class_init (GtkSourceCompletionSnippetsClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->finalize = gtk_source_completion_snippets_finalize;
+       object_class->get_property = gtk_source_completion_snippets_get_property;
+       object_class->set_property = gtk_source_completion_snippets_set_property;
+
+       properties[PROP_NAME] =
+               g_param_spec_string ("name",
+                                    "Name",
+                                    "The provider name",
+                                    NULL,
+                                    G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+       properties[PROP_ICON] =
+               g_param_spec_object ("icon",
+                                    "Icon",
+                                    "The provider icon",
+                                    GDK_TYPE_TEXTURE,
+                                    G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+       properties[PROP_INTERACTIVE_DELAY] =
+               g_param_spec_int ("interactive-delay",
+                                 "Interactive Delay",
+                                 "The delay before initiating interactive completion",
+                                 -1,
+                                 G_MAXINT,
+                                 50,
+                                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+       properties[PROP_PRIORITY] =
+               g_param_spec_int ("priority",
+                                 "Priority",
+                                 "Provider priority",
+                                 G_MININT,
+                                 G_MAXINT,
+                                 0,
+                                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+       properties[PROP_ACTIVATION] =
+               g_param_spec_flags ("activation",
+                                   "Activation",
+                                   "The type of activation",
+                                   GTK_SOURCE_TYPE_COMPLETION_ACTIVATION,
+                                   GTK_SOURCE_COMPLETION_ACTIVATION_INTERACTIVE |
+                                   GTK_SOURCE_COMPLETION_ACTIVATION_USER_REQUESTED,
+                                   G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+       g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gtk_source_completion_snippets_init (GtkSourceCompletionSnippets *self)
+{
+       GtkSourceCompletionSnippetsPrivate *priv = gtk_source_completion_snippets_get_instance_private (self);
+
+       priv->minimum_word_size = 2;
+}
+
+GtkSourceCompletionSnippets *
+gtk_source_completion_snippets_new (void)
+{
+       return g_object_new (GTK_SOURCE_TYPE_COMPLETION_SNIPPETS, NULL);
+}
diff --git a/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippets.h 
b/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippets.h
new file mode 100644
index 00000000..592da7a4
--- /dev/null
+++ b/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippets.h
@@ -0,0 +1,49 @@
+/*
+ * 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 <gtksourceview/gtksourcetypes.h>
+#include <gtksourceview/gtksourcecompletionprovider.h>
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_COMPLETION_SNIPPETS (gtk_source_completion_snippets_get_type())
+
+GTK_SOURCE_AVAILABLE_IN_5_0
+G_DECLARE_DERIVABLE_TYPE (GtkSourceCompletionSnippets, gtk_source_completion_snippets, GTK_SOURCE, 
COMPLETION_SNIPPETS, GObject)
+
+struct _GtkSourceCompletionSnippetsClass
+{
+       GObjectClass parent_class;
+
+       /*< private >*/
+       gpointer _reserved[10];
+};
+
+GTK_SOURCE_AVAILABLE_IN_5_0
+GtkSourceCompletionSnippets *gtk_source_completion_snippets_new (void);
+
+G_END_DECLS
diff --git a/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippetsproposal-private.h 
b/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippetsproposal-private.h
new file mode 100644
index 00000000..5258af1a
--- /dev/null
+++ b/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippetsproposal-private.h
@@ -0,0 +1,37 @@
+/*
+ * 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/gtksourcetypes.h>
+#include <gtksourceview/gtksourcecompletionproposal.h>
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_COMPLETION_SNIPPETS_PROPOSAL (gtk_source_completion_snippets_proposal_get_type())
+
+G_GNUC_INTERNAL
+G_DECLARE_FINAL_TYPE (GtkSourceCompletionSnippetsProposal, gtk_source_completion_snippets_proposal, 
GTK_SOURCE, COMPLETION_SNIPPETS_PROPOSAL, GObject)
+
+G_GNUC_INTERNAL
+GtkSourceCompletionProposal *gtk_source_completion_snippets_proposal_new         (GtkSourceSnippet           
         *snippet);
+G_GNUC_INTERNAL
+GtkSourceSnippet            *gtk_source_completion_snippets_proposal_get_snippet 
(GtkSourceCompletionSnippetsProposal *self);
+
+G_END_DECLS
diff --git a/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippetsproposal.c 
b/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippetsproposal.c
new file mode 100644
index 00000000..315fc016
--- /dev/null
+++ b/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippetsproposal.c
@@ -0,0 +1,184 @@
+/*
+ * 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/gtksourcesnippet.h>
+
+#include "gtksourcecompletionsnippetsproposal-private.h"
+
+struct _GtkSourceCompletionSnippetsProposal
+{
+       GObject parent_instance;
+       GtkSourceSnippet *snippet;
+};
+
+static void completion_proposal_iface_init (GtkSourceCompletionProposalInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GtkSourceCompletionSnippetsProposal,
+                         gtk_source_completion_snippets_proposal,
+                         G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (GTK_SOURCE_TYPE_COMPLETION_PROPOSAL,
+                                                completion_proposal_iface_init))
+
+enum {
+       PROP_0,
+       PROP_SNIPPET,
+       N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static gchar *
+get_label (GtkSourceCompletionProposal *proposal)
+{
+       GtkSourceCompletionSnippetsProposal *self = GTK_SOURCE_COMPLETION_SNIPPETS_PROPOSAL (proposal);
+       const gchar *label;
+
+       label = gtk_source_snippet_get_name (self->snippet);
+
+       if (label == NULL)
+               label = gtk_source_snippet_get_trigger (self->snippet);
+
+       if (label == NULL)
+               label = gtk_source_snippet_get_description (self->snippet);
+
+       return g_strdup (label);
+}
+
+static const gchar *
+get_icon_name (GtkSourceCompletionProposal *proposal)
+{
+       /* TODO: Add a symbolic icon for GSV to use */
+       return NULL;
+}
+
+static gchar *
+get_info (GtkSourceCompletionProposal *proposal)
+{
+       GtkSourceCompletionSnippetsProposal *self = GTK_SOURCE_COMPLETION_SNIPPETS_PROPOSAL (proposal);
+       return g_strdup (gtk_source_snippet_get_description (self->snippet));
+}
+
+static void
+completion_proposal_iface_init (GtkSourceCompletionProposalInterface *iface)
+{
+       iface->get_label = get_label;
+       iface->get_icon_name = get_icon_name;
+       iface->get_info = get_info;
+}
+
+static void
+gtk_source_completion_snippets_proposal_finalize (GObject *object)
+{
+       GtkSourceCompletionSnippetsProposal *self = GTK_SOURCE_COMPLETION_SNIPPETS_PROPOSAL (object);
+
+       g_clear_object (&self->snippet);
+
+       G_OBJECT_CLASS (gtk_source_completion_snippets_proposal_parent_class)->finalize (object);
+}
+
+static void
+gtk_source_completion_snippets_proposal_get_property (GObject    *object,
+                                                      guint       prop_id,
+                                                      GValue     *value,
+                                                      GParamSpec *pspec)
+{
+       GtkSourceCompletionSnippetsProposal *self = GTK_SOURCE_COMPLETION_SNIPPETS_PROPOSAL (object);
+
+       switch (prop_id)
+       {
+       case PROP_SNIPPET:
+               g_value_set_object (value, self->snippet);
+               break;
+
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+gtk_source_completion_snippets_proposal_set_property (GObject      *object,
+                                                      guint         prop_id,
+                                                      const GValue *value,
+                                                      GParamSpec   *pspec)
+{
+       GtkSourceCompletionSnippetsProposal *self = GTK_SOURCE_COMPLETION_SNIPPETS_PROPOSAL (object);
+
+       switch (prop_id)
+       {
+       case PROP_SNIPPET:
+               self->snippet = g_value_dup_object (value);
+               break;
+
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+gtk_source_completion_snippets_proposal_class_init (GtkSourceCompletionSnippetsProposalClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->finalize = gtk_source_completion_snippets_proposal_finalize;
+       object_class->get_property = gtk_source_completion_snippets_proposal_get_property;
+       object_class->set_property = gtk_source_completion_snippets_proposal_set_property;
+
+       properties [PROP_SNIPPET] =
+               g_param_spec_object ("snippet",
+                                    "snippet",
+                                    "The snippet to expand",
+                                    GTK_SOURCE_TYPE_SNIPPET,
+                                    (G_PARAM_READWRITE |
+                                     G_PARAM_CONSTRUCT_ONLY |
+                                     G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gtk_source_completion_snippets_proposal_init (GtkSourceCompletionSnippetsProposal *self)
+{
+}
+
+GtkSourceCompletionProposal *
+gtk_source_completion_snippets_proposal_new (GtkSourceSnippet *snippet)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), NULL);
+
+       return g_object_new (GTK_SOURCE_TYPE_COMPLETION_SNIPPETS_PROPOSAL,
+                            "snippet", snippet,
+                            NULL);
+}
+
+/**
+ * gtk_source_completion_snippets_proposal_get_snippet:
+ *
+ * Returns: (transfer none): a #GtkSourceSnippet
+ */
+GtkSourceSnippet *
+gtk_source_completion_snippets_proposal_get_snippet (GtkSourceCompletionSnippetsProposal *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_COMPLETION_SNIPPETS_PROPOSAL (self), NULL);
+
+       return self->snippet;
+}
diff --git a/gtksourceview/completion-providers/snippets/meson.build 
b/gtksourceview/completion-providers/snippets/meson.build
new file mode 100644
index 00000000..f9d3199d
--- /dev/null
+++ b/gtksourceview/completion-providers/snippets/meson.build
@@ -0,0 +1,49 @@
+completionsnippets_c_args = [
+  '-DGTK_SOURCE_COMPILATION',
+  '-DG_LOG_DOMAIN="GtkSourceView"',
+]
+
+completionsnippets_public_h = [
+  'gtksourcecompletionsnippets.h',
+]
+
+completionsnippets_sources = [
+  'gtksourcecompletionsnippets.c',
+  'gtksourcecompletionsnippetsproposal.c',
+]
+
+install_headers(
+  completionsnippets_public_h,
+  install_dir: join_paths(
+    pkgincludedir,
+    'gtksourceview',
+    'completion-providers',
+    'snippets'
+  )
+)
+
+completionsnippets_lib = static_library(
+  package_string + 'completionsnippets',
+  completionsnippets_sources,
+  include_directories: rootdir,
+  dependencies: core_dep,
+  c_args: completionsnippets_c_args,
+  install: false,
+)
+
+gtksource_libs += [
+  completionsnippets_lib
+]
+
+completionsnippets_dep = declare_dependency(
+  link_with: completionsnippets_lib,
+  include_directories: rootdir,
+  dependencies: core_dep,
+)
+
+gtksource_deps += completionsnippets_dep
+
+extra_public_sources += files([
+  'gtksourcecompletionsnippets.c',
+  'gtksourcecompletionsnippets.h',
+])
diff --git a/gtksourceview/gtksource.h b/gtksourceview/gtksource.h
index d54bccd5..31942337 100644
--- a/gtksourceview/gtksource.h
+++ b/gtksourceview/gtksource.h
@@ -19,7 +19,6 @@
 
 #define GTK_SOURCE_H_INSIDE
 
-#include "completion-providers/words/gtksourcecompletionwords.h"
 #include "gtksourcetypes.h"
 #include "gtksourcebuffer.h"
 #include "gtksourcecompletion.h"
@@ -46,6 +45,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"
@@ -59,4 +62,7 @@
 #include "gtksourceview.h"
 #include "gtksource-enumtypes.h"
 
+#include "completion-providers/words/gtksourcecompletionwords.h"
+#include "completion-providers/snippets/gtksourcecompletionsnippets.h"
+
 #undef GTK_SOURCE_H_INSIDE
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 240a84ec..758f37ed 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 81e57db9..2a5918df 100644
--- a/gtksourceview/gtksourceinit.c
+++ b/gtksourceview/gtksourceinit.c
@@ -38,6 +38,7 @@
 #include "gtksourceinit.h"
 #include "gtksourcelanguagemanager-private.h"
 #include "gtksourcemap.h"
+#include "gtksourcesnippetmanager-private.h"
 #include "gtksourcestyleschemechooser.h"
 #include "gtksourcestyleschemechooserbutton.h"
 #include "gtksourcestyleschemechooserwidget.h"
@@ -265,6 +266,7 @@ gtk_source_finalize (void)
        {
                GtkSourceLanguageManager *language_manager;
                GtkSourceStyleSchemeManager *style_scheme_manager;
+               GtkSourceSnippetManager *snippet_manager;
 
                g_resources_register (gtksourceview_get_resource ());
 
@@ -274,6 +276,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..cdcafc86
--- /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,
+                                                             GtkSourceBuffer   *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..b9b8bf1f
--- /dev/null
+++ b/gtksourceview/gtksourcesnippet.c
@@ -0,0 +1,1503 @@
+/*
+ * 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"
+
+/**
+ * SECTION:snippet
+ * @title: GtkSourceSnippet
+ * @short_description: Quick insertion code snippets
+ * @see_also: #GtkSourceSnippetChunk, #GtkSourceSnippetContext, #GtkSourceSnippetManager
+ *
+ * The #GtkSourceSnippet represents a series of chunks that can quickly be
+ * inserted into the #GtkSourceView.
+ *
+ * Snippets are defined in XML files which are loaded by the
+ * #GtkSourceSnippetManager. Alternatively, applications can create snippets
+ * on demand and insert them into the #GtkSourceView using
+ * gtk_source_view_push_snippet().
+ *
+ * Snippet chunks can reference other snippet chunks as well as post-process
+ * the values from other chunks such as capitalization.
+ *
+ * Since: 5.0
+ */
+
+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;
+       gchar                   *name;
+
+       /* 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_NAME,
+       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]);
+       }
+}
+
+/**
+ * gtk_source_snippet_get_name:
+ * @snippet: a #GtkSourceSnippet
+ *
+ * Gets the name for the snippet.
+ *
+ * Since: 5.0
+ */
+const gchar *
+gtk_source_snippet_get_name (GtkSourceSnippet *snippet)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), NULL);
+
+       return snippet->name;
+}
+
+/**
+ * gtk_source_snippet_set_name:
+ * @snippet: a #GtkSourceSnippet
+ * @name: the snippet name
+ *
+ * Sets the name for the snippet.
+ *
+ * Since: 5.0
+ */
+void
+gtk_source_snippet_set_name (GtkSourceSnippet *snippet,
+                             const gchar      *name)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SNIPPET (snippet));
+
+       if (g_strcmp0 (name, snippet->name) != 0)
+       {
+               g_free (snippet->name);
+               snippet->name = g_strdup (name);
+               g_object_notify_by_pspec (G_OBJECT (snippet),
+                                         properties [PROP_NAME]);
+       }
+}
+
+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,
+                           GtkSourceBuffer  *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, buffer, iter);
+
+       snippet->buffer = g_object_ref (GTK_TEXT_BUFFER (buffer));
+
+       mark = gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (buffer), NULL, iter, TRUE);
+       snippet->begin_mark = g_object_ref (mark);
+
+       mark = gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (buffer), NULL, iter, FALSE);
+       snippet->end_mark = g_object_ref (mark);
+
+       gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (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 (GTK_TEXT_BUFFER (buffer), NULL, iter, TRUE);
+               end = gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (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 (GTK_TEXT_BUFFER (buffer), iter, text, -1);
+                       gtk_source_snippet_update_marks (snippet);
+               }
+       }
+
+       snippet->current_chunk = NULL;
+
+       gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (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);
+}
+
+/**
+ * gtk_source_snippet_add_chunk:
+ * @snippet: a #GtkSourceSnippet
+ * @chunk: a #GtkSourceSnippetChunk
+ *
+ * Appends @chunk to the @snippet.
+ *
+ * This may only be called before the snippet has been expanded.
+ *
+ * Since: 5.0
+ */
+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->name, 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_NAME:
+               g_value_set_string (value, snippet->name);
+               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;
+
+       case PROP_NAME:
+               gtk_source_snippet_set_name (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_NAME] =
+               g_param_spec_string ("name",
+                                    "Name",
+                                    "The name 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..e341a76c
--- /dev/null
+++ b/gtksourceview/gtksourcesnippet.h
@@ -0,0 +1,75 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2020 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#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_name                   (GtkSourceSnippet      *snippet);
+GTK_SOURCE_AVAILABLE_IN_5_0
+void                     gtk_source_snippet_set_name                   (GtkSourceSnippet      *snippet,
+                                                                        const gchar           *name);
+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..79062967
--- /dev/null
+++ b/gtksourceview/gtksourcesnippetbundle.c
@@ -0,0 +1,666 @@
+/*
+ * 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);
+       gtk_source_snippet_set_name (snippet, info->name);
+
+       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;
+       const gchar *last = NULL;
+
+       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))
+               {
+                       if (info->trigger != NULL && last != info->trigger)
+                       {
+                               g_array_append_vals (ret->infos, info, 1);
+                               last = info->trigger;
+                       }
+               }
+       }
+
+       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..001f26d1
--- /dev/null
+++ b/gtksourceview/gtksourcesnippetchunk.c
@@ -0,0 +1,592 @@
+/*
+ * 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:
+ * @chunk: a #GtkSourceSnippetChunk
+ *
+ * Copies the source snippet.
+ *
+ * Returns: (transfer full): A #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:
+ * @chunk: a #GtkSourceSnippetChunk
+ *
+ * Gets the context for the snippet insertion.
+ *
+ * Returns: (transfer none): A #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]);
+       }
+}
+
+/**
+ * gtk_source_snippet_chunk_get_spec:
+ * @chunk: a #GtkSourceSnippetChunk
+ *
+ * Gets the specification for the chunk.
+ *
+ * The specification is evaluated for variables when other chunks are edited
+ * within the snippet context. If the user has changed the text, the
+ * #GtkSourceSnippetChunk:text and #GtkSourceSnippetChunk:text-set properties
+ * are updated.
+ *
+ * Returns: (transfer none) (nullable): the specificiation, if any
+ *
+ * Since: 5.0
+ */
+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;
+}
+
+/**
+ * gtk_source_snippet_chunk_set_spec:
+ * @chunk: a #GtkSourceSnippetChunk
+ * @spec: the new specification for the chunk
+ *
+ * Sets the specification for the chunk.
+ *
+ * The specification is evaluated for variables when other chunks are edited
+ * within the snippet context. If the user has changed the text, the
+ * #GtkSourceSnippetChunk:text and #GtkSourceSnippetChunk:text-set properties
+ * are updated.
+ *
+ * Since: 5.0
+ */
+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]);
+       }
+}
+
+/**
+ * gtk_source_snippet_chunk_get_focus_position:
+ * @chunk: a #GtkSourceSnippetChunk
+ *
+ * Gets the #GtkSourceSnippetChunk:focus-position.
+ *
+ * The focus-position is used to determine how many tabs it takes for the
+ * snippet to advanced to this chunk.
+ *
+ * A focus-position of zero will be the last focus position of the snippet
+ * and snippet editing ends when it has been reached.
+ *
+ * A focus-position of -1 means the chunk cannot be focused by the user.
+ *
+ * Returns: the focus-position
+ *
+ * Since: 5.0
+ */
+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;
+}
+
+/**
+ * gtk_source_snippet_chunk_set_focus_position:
+ * @chunk: a #GtkSourceSnippetChunk
+ * @focus_position: the focus-position
+ *
+ * Sets the #GtkSourceSnippetChunk:focus-position property.
+ *
+ * The focus-position is used to determine how many tabs it takes for the
+ * snippet to advanced to this chunk.
+ *
+ * A focus-position of zero will be the last focus position of the snippet
+ * and snippet editing ends when it has been reached.
+ *
+ * A focus-position of -1 means the chunk cannot be focused by the user.
+ *
+ * Since: 5.0
+ */
+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]);
+       }
+}
+
+/**
+ * gtk_source_snippet_chunk_get_text:
+ * @chunk: a #GtkSourceSnippetChunk
+ *
+ * Gets the #GtkSourceSnippetChunk:text property.
+ *
+ * The text property is updated when the user edits the text of the chunk.
+ * If it has not been edited, the #GtkSourceSnippetChunk:spec property is
+ * returned.
+ *
+ * Returns: (not nullable): the text of the chunk
+ *
+ * Since: 5.0
+ */
+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 : "";
+}
+
+/**
+ * gtk_source_snippet_chunk_set_text:
+ * @chunk: a #GtkSourceSnippetChunk
+ * @text: the text of the property
+ *
+ * Sets the text for the snippet chunk.
+ *
+ * This is usually used by the snippet engine to update the text, but may
+ * be useful when creating custom snippets to avoid expansion of any
+ * specification.
+ *
+ * Since: 5.0
+ */
+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]);
+       }
+}
+
+/**
+ * gtk_source_snippet_chunk_get_text_set:
+ * @chunk: a #GtkSourceSnippetChunk
+ *
+ * Gets the #GtkSourceSnippetChunk:text-set property.
+ *
+ * This is typically set when the user has edited a snippet chunk.
+ *
+ * Since: 5.0
+ */
+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;
+}
+
+/**
+ * gtk_source_snippet_chunk_set_text_set:
+ * @chunk: a #GtkSourceSnippetChunk
+ * @text_set: the property value
+ *
+ * Sets the #GtkSourceSnippetChunk:text-set property.
+ *
+ * This is typically set when the user has edited a snippet chunk by the
+ * snippet engine.
+ *
+ * Since: 5.0
+ */
+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..d06f9530
--- /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                     
focus_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..9d36b8cd
--- /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_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", "");
+       SET_CONSTANT ("TM_FILENAME", "");
+#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 2f19e2be..6f53ffd4 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 834a7d21..3e0d73bc 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"
@@ -616,6 +617,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 8cc31af1..cde07715 100644
--- a/gtksourceview/gtksourcetypes-private.h
+++ b/gtksourceview/gtksourcetypes-private.h
@@ -38,6 +38,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 5a313fa9..85156f61 100644
--- a/gtksourceview/gtksourcetypes.h
+++ b/gtksourceview/gtksourcetypes.h
@@ -58,6 +58,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
index 515e1eb3..d2002019 100644
--- a/gtksourceview/gtksourceview-private.h
+++ b/gtksourceview/gtksourceview-private.h
@@ -26,12 +26,24 @@
 
 G_BEGIN_DECLS
 
-typedef struct
+typedef struct _GtkSourceViewAssistants
 {
        GtkSourceView *view;
        GQueue         queue;
 } GtkSourceViewAssistants;
 
+typedef struct _GtkSourceViewSnippets
+{
+       GtkSourceView   *view;
+       GtkSourceBuffer *buffer;
+       GQueue           queue;
+       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_add_assistant            (GtkSourceView           *view,
                                                     GtkSourceAssistant      *assistant);
 void     _gtk_source_view_remove_assistant         (GtkSourceView           *view,
@@ -53,4 +65,19 @@ gboolean _gtk_source_view_assistants_handle_key    (GtkSourceViewAssistants *ass
                                                     guint                    keyval,
                                                     GdkModifierType          state);
 
+void     _gtk_source_view_snippets_init        (GtkSourceViewSnippets *snippets,
+                                                GtkSourceView         *view);
+void     _gtk_source_view_snippets_shutdown    (GtkSourceViewSnippets *snippets);
+void     _gtk_source_view_snippets_set_buffer  (GtkSourceViewSnippets *snippets,
+                                                GtkSourceBuffer       *buffer);
+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..5b85d070
--- /dev/null
+++ b/gtksourceview/gtksourceview-snippets.c
@@ -0,0 +1,560 @@
+/*
+ * 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)
+{
+       GtkTextMark *mark;
+
+       g_assert (snippets != NULL);
+
+       mark = gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (snippets->buffer));
+       gtk_text_view_scroll_mark_onscreen (GTK_TEXT_VIEW (snippets->view), mark);
+}
+
+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);
+               }
+       }
+
+}
+
+void
+_gtk_source_view_snippets_set_buffer (GtkSourceViewSnippets *snippets,
+                                      GtkSourceBuffer       *buffer)
+{
+       g_assert (snippets != NULL);
+
+       if (buffer == snippets->buffer)
+       {
+               return;
+       }
+
+       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;
+
+       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)
+{
+       GtkTextBuffer *buffer;
+
+       g_return_if_fail (snippets != NULL);
+       g_return_if_fail (GTK_SOURCE_IS_VIEW (view));
+
+       buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
+
+       memset (snippets, 0, sizeof *snippets);
+       snippets->view = view;
+
+       if (GTK_SOURCE_IS_BUFFER (buffer))
+       {
+               _gtk_source_view_snippets_set_buffer (snippets,
+                                                     GTK_SOURCE_BUFFER (buffer));
+       }
+}
+
+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->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 (GTK_TEXT_BUFFER (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);
+
+       /* It's possible to get here even when GtkSourceView:enable-snippets
+        * is disabled because applications can also push snippets onto
+        * the view, such as with completion providers.
+        */
+
+       if (snippets->buffer == NULL)
+       {
+               return FALSE;
+       }
+
+       /* Be careful when testing for modifier state equality:
+        * caps lock, num lock,etc need to be taken into account */
+       modifiers = gtk_accelerator_get_default_mod_mask ();
+       editable = gtk_text_view_get_editable (GTK_TEXT_VIEW (snippets->view));
+
+       if ((key == GDK_KEY_Tab || key == GDK_KEY_KP_Tab || key == GDK_KEY_ISO_Left_Tab) &&
+           ((state & modifiers) == 0 ||
+            (state & modifiers) == GDK_SHIFT_MASK) &&
+           editable &&
+           gtk_text_view_get_accepts_tab (GTK_TEXT_VIEW (snippets->view)))
+       {
+               GtkSourceSnippet *snippet = g_queue_peek_head (&snippets->queue);
+               GtkTextIter begin, end;
+               gboolean has_selection;
+
+               /* If we already have a snippet expanded, then we might need
+                * to move forward or backward between snippet positions.
+                */
+               if (snippet != NULL)
+               {
+                       if ((state & modifiers) == 0)
+                       {
+                               if (!_gtk_source_snippet_move_next (snippet))
+                               {
+                                       _gtk_source_view_snippets_pop (snippets);
+                               }
+
+                               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 (GTK_TEXT_BUFFER (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)
+{
+       GtkTextMark *mark;
+       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 (GTK_TEXT_BUFFER (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 (GTK_TEXT_BUFFER (snippets->buffer));
+
+       mark = gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (snippets->buffer));
+       gtk_text_view_scroll_mark_onscreen (GTK_TEXT_VIEW (snippets->view), mark);
+
+       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 310c3f40..3820a535 100644
--- a/gtksourceview/gtksourceview.c
+++ b/gtksourceview/gtksourceview.c
@@ -50,6 +50,8 @@
 #include "gtksourcesearchcontext-private.h"
 #include "gtksourcespacedrawer.h"
 #include "gtksourcespacedrawer-private.h"
+#include "gtksourcesnippet.h"
+#include "gtksourcesnippetcontext.h"
 
 /**
  * SECTION:view
@@ -156,6 +158,7 @@ enum
        MOVE_LINES,
        MOVE_TO_MATCHING_BRACKET,
        MOVE_WORDS,
+       PUSH_SNIPPET,
        SHOW_COMPLETION,
        SMART_HOME_END,
        N_SIGNALS
@@ -179,6 +182,7 @@ enum
        PROP_BACKGROUND_PATTERN,
        PROP_SMART_BACKSPACE,
        PROP_SPACE_DRAWER,
+       PROP_ENABLE_SNIPPETS,
        N_PROPS
 };
 
@@ -216,6 +220,8 @@ typedef struct
 
        GtkSourceViewAssistants assistants;
 
+       GtkSourceViewSnippets snippets;
+
        guint background_pattern_color_set : 1;
        guint current_line_color_set : 1;
        guint right_margin_line_color_set : 1;
@@ -229,6 +235,7 @@ typedef struct
        guint indent_on_tab : 1;
        guint show_right_margin  : 1;
        guint smart_backspace : 1;
+       guint enable_snippets : 1;
 } GtkSourceViewPrivate;
 
 typedef struct
@@ -310,6 +317,9 @@ static gboolean       gtk_source_view_rgba_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 GtkSourceCompletion *
 get_completion (GtkSourceView *self)
@@ -534,6 +544,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:
@@ -548,6 +559,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));
+
        /**
         * GtkSourceView:show-line-numbers:
         *
@@ -847,6 +880,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
@@ -1231,6 +1293,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;
@@ -1311,6 +1377,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;
@@ -1390,6 +1460,7 @@ gtk_source_view_init (GtkSourceView *view)
 
        gtk_source_view_populate_extra_menu (view);
 
+       _gtk_source_view_snippets_init (&priv->snippets, view);
        _gtk_source_view_assistants_init (&priv->assistants, view);
 }
 
@@ -1410,6 +1481,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.
@@ -1633,6 +1707,8 @@ remove_source_buffer (GtkSourceView *view)
                                                      search_start_cb,
                                                      view);
 
+               _gtk_source_view_snippets_set_buffer (&priv->snippets, NULL);
+
                g_object_unref (priv->source_buffer);
                priv->source_buffer = NULL;
        }
@@ -1690,6 +1766,8 @@ set_source_buffer (GtkSourceView *view,
                                  view);
 
                buffer_has_selection_changed_cb (GTK_SOURCE_BUFFER (buffer), NULL, view);
+
+               _gtk_source_view_snippets_set_buffer (&priv->snippets, priv->source_buffer);
        }
 
        gtk_source_view_update_style_scheme (view);
@@ -3951,6 +4029,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!)
         */
@@ -4982,3 +5065,169 @@ _gtk_source_view_remove_assistant (GtkSourceView      *view,
 
        _gtk_source_view_assistants_remove (&priv->assistants, assistant);
 }
+
+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 310ab38c..7e7fef3b 100644
--- a/gtksourceview/meson.build
+++ b/gtksourceview/meson.build
@@ -32,6 +32,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',
@@ -71,6 +75,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',
@@ -106,6 +114,9 @@ core_private_c = files([
   'gtksourceregex.c',
   'gtksourcesignalgroup.c',
   'gtksourceview-assistants.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 1100586f..9c65c43d 100644
--- a/tests/test-widget.c
+++ b/tests/test-widget.c
@@ -286,7 +286,7 @@ static void
 show_line_numbers_toggled_cb (TestWidget     *self,
                              GtkCheckButton *button)
 {
-       gboolean enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button));
+       gboolean enabled = gtk_check_button_get_active (button);
        gtk_source_view_set_show_line_numbers (self->priv->view, enabled);
 }
 
@@ -294,7 +294,7 @@ static void
 show_line_marks_toggled_cb (TestWidget     *self,
                            GtkCheckButton *button)
 {
-       gboolean enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button));
+       gboolean enabled = gtk_check_button_get_active (button);
        gtk_source_view_set_show_line_marks (self->priv->view, enabled);
 }
 
@@ -302,7 +302,7 @@ static void
 show_right_margin_toggled_cb (TestWidget     *self,
                              GtkCheckButton *button)
 {
-       gboolean enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button));
+       gboolean enabled = gtk_check_button_get_active (button);
        gtk_source_view_set_show_right_margin (self->priv->view, enabled);
 }
 
@@ -318,7 +318,7 @@ static void
 highlight_syntax_toggled_cb (TestWidget     *self,
                             GtkCheckButton *button)
 {
-       gboolean enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button));
+       gboolean enabled = gtk_check_button_get_active (button);
        gtk_source_buffer_set_highlight_syntax (self->priv->buffer, enabled);
 }
 
@@ -326,7 +326,7 @@ static void
 highlight_matching_bracket_toggled_cb (TestWidget     *self,
                                       GtkCheckButton *button)
 {
-       gboolean enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button));
+       gboolean enabled = gtk_check_button_get_active (button);
        gtk_source_buffer_set_highlight_matching_brackets (self->priv->buffer, enabled);
 }
 
@@ -334,7 +334,7 @@ static void
 highlight_current_line_toggled_cb (TestWidget     *self,
                                   GtkCheckButton *button)
 {
-       gboolean enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button));
+       gboolean enabled = gtk_check_button_get_active (button);
        gtk_source_view_set_highlight_current_line (self->priv->view, enabled);
 }
 
@@ -342,7 +342,7 @@ static void
 wrap_lines_toggled_cb (TestWidget     *self,
                       GtkCheckButton *button)
 {
-       gboolean enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button));
+       gboolean enabled = gtk_check_button_get_active (button);
        gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (self->priv->view),
                                     enabled ? GTK_WRAP_WORD : GTK_WRAP_NONE);
 }
@@ -351,7 +351,7 @@ static void
 auto_indent_toggled_cb (TestWidget     *self,
                        GtkCheckButton *button)
 {
-       gboolean enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button));
+       gboolean enabled = gtk_check_button_get_active (button);
        gtk_source_view_set_auto_indent (self->priv->view, enabled);
 }
 
@@ -359,7 +359,7 @@ static void
 indent_spaces_toggled_cb (TestWidget     *self,
                          GtkCheckButton *button)
 {
-       gboolean enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button));
+       gboolean enabled = gtk_check_button_get_active (button);
        gtk_source_view_set_insert_spaces_instead_of_tabs (self->priv->view, enabled);
 }
 
@@ -376,7 +376,7 @@ update_indent_width (TestWidget *self)
 {
        gint indent_width = -1;
 
-       if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->priv->indent_width_checkbutton)))
+       if (gtk_check_button_get_active (self->priv->indent_width_checkbutton))
        {
                indent_width = gtk_spin_button_get_value_as_int (self->priv->indent_width_spinbutton);
        }
@@ -950,6 +950,14 @@ on_background_pattern_changed (GtkComboBox *combobox,
        g_free (text);
 }
 
+static void
+enable_snippets_toggled_cb (TestWidget     *self,
+                            GtkCheckButton *button)
+{
+       gboolean enabled = gtk_check_button_get_active (button);
+       gtk_source_view_set_enable_snippets (self->priv->view, enabled);
+}
+
 static void
 test_widget_dispose (GObject *object)
 {
@@ -988,6 +996,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);
@@ -1005,12 +1014,12 @@ test_widget_class_init (TestWidgetClass *klass)
 }
 
 static void
-show_top_border_window_toggled_cb (GtkToggleButton *checkbutton,
-                                   TestWidget      *self)
+show_top_border_window_toggled_cb (GtkCheckButton *checkbutton,
+                                   TestWidget     *self)
 {
        gint size;
 
-       size = gtk_toggle_button_get_active (checkbutton) ? 20 : 0;
+       size = gtk_check_button_get_active (checkbutton) ? 20 : 0;
 
        if (self->priv->top == NULL)
        {
@@ -1114,6 +1123,27 @@ 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[])
 {
@@ -1124,6 +1154,7 @@ main (int argc, char *argv[])
 
        gtk_init ();
        gtk_source_init ();
+       setup_search_paths ();
 
        window = gtk_window_new ();
        gtk_window_set_default_size (GTK_WINDOW (window), 900, 600);
diff --git a/tests/test-widget.ui b/tests/test-widget.ui
index 61d0991c..7554d81d 100644
--- a/tests/test-widget.ui
+++ b/tests/test-widget.ui
@@ -217,6 +217,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="row">13</property>
+                  <property name="column">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]