[gnome-builder/wip/gtk4-port: 520/736] plugins/spellcheck: start on new spellcheck plugin




commit b3b39790c70b08109882f82ae51e0785ef7a73c7
Author: Christian Hergert <chergert redhat com>
Date:   Thu Apr 7 16:30:33 2022 -0700

    plugins/spellcheck: start on new spellcheck plugin
    
    This is adapting Text Editors spelling layer into Builder. Currently it
    just includes importing of the spellcheck code and a buffer addin which
    will unconditionally turn on spellcheck.
    
    This needs preferences to be hooked up and menus to be attached to the
    editors context menu.

 src/plugins/meson.build                            |    2 +-
 src/plugins/spellcheck/cjhtextregion.c             | 1311 ++++++++++++++++++++
 src/plugins/spellcheck/cjhtextregionbtree.h        |  564 +++++++++
 src/plugins/spellcheck/cjhtextregionprivate.h      |  121 ++
 ...ver.h => editor-empty-spell-provider-private.h} |   12 +-
 .../spellcheck/editor-empty-spell-provider.c       |   71 ++
 .../spellcheck/editor-enchant-spell-language.c     |  306 +++++
 ...navigator.h => editor-enchant-spell-language.h} |   18 +-
 .../spellcheck/editor-enchant-spell-provider.c     |  171 +++
 ...ll-widget.h => editor-enchant-spell-provider.h} |   15 +-
 src/plugins/spellcheck/editor-spell-checker.c      |  324 +++++
 src/plugins/spellcheck/editor-spell-checker.h      |   48 +
 src/plugins/spellcheck/editor-spell-cursor.c       |  365 ++++++
 src/plugins/spellcheck/editor-spell-cursor.h       |   45 +
 .../spellcheck/editor-spell-language-info.c        |  162 +++
 .../spellcheck/editor-spell-language-info.h        |   36 +
 src/plugins/spellcheck/editor-spell-language.c     |  177 +++
 src/plugins/spellcheck/editor-spell-language.h     |   64 +
 src/plugins/spellcheck/editor-spell-menu.c         |  236 ++++
 src/plugins/spellcheck/editor-spell-menu.h         |   34 +
 src/plugins/spellcheck/editor-spell-provider.c     |  244 ++++
 src/plugins/spellcheck/editor-spell-provider.h     |   55 +
 .../spellcheck/editor-text-buffer-spell-adapter.c  |  833 +++++++++++++
 .../spellcheck/editor-text-buffer-spell-adapter.h  |   62 +
 .../{gbp-spell-editor-addin.h => editor-types.h}   |   12 +-
 src/plugins/spellcheck/gbp-spell-buffer-addin.c    |  295 +----
 src/plugins/spellcheck/gbp-spell-buffer-addin.h    |   10 +-
 src/plugins/spellcheck/gbp-spell-dict.c            |  562 ---------
 src/plugins/spellcheck/gbp-spell-dict.h            |   44 -
 src/plugins/spellcheck/gbp-spell-editor-addin.c    |  174 ---
 .../spellcheck/gbp-spell-editor-page-addin.c       |  394 ------
 .../spellcheck/gbp-spell-editor-page-addin.h       |   40 -
 .../spellcheck/gbp-spell-language-popover.c        |  380 ------
 src/plugins/spellcheck/gbp-spell-navigator.c       |  658 ----------
 src/plugins/spellcheck/gbp-spell-private.h         |  100 --
 src/plugins/spellcheck/gbp-spell-utils.c           |  200 ---
 src/plugins/spellcheck/gbp-spell-utils.h           |   37 -
 src/plugins/spellcheck/gbp-spell-widget-actions.c  |  181 ---
 src/plugins/spellcheck/gbp-spell-widget.c          | 1185 ------------------
 src/plugins/spellcheck/gbp-spell-widget.ui         |  338 -----
 src/plugins/spellcheck/gtk/menus.ui                |   19 -
 src/plugins/spellcheck/meson.build                 |   21 +-
 src/plugins/spellcheck/spellcheck-plugin.c         |   11 +-
 src/plugins/spellcheck/spellcheck.gresource.xml    |    3 -
 src/plugins/spellcheck/spellcheck.plugin           |    9 +-
 src/plugins/spellcheck/themes/shared.css           |   29 -
 46 files changed, 5328 insertions(+), 4650 deletions(-)
---
diff --git a/src/plugins/meson.build b/src/plugins/meson.build
index dbb31583d..6341d8588 100644
--- a/src/plugins/meson.build
+++ b/src/plugins/meson.build
@@ -121,7 +121,7 @@ subdir('rubocop')
 subdir('rust-analyzer')
 #subdir('shellcmd')
 #subdir('snippets')
-#subdir('spellcheck')
+subdir('spellcheck')
 subdir('stylelint')
 #subdir('sublime')
 #subdir('support')
diff --git a/src/plugins/spellcheck/cjhtextregion.c b/src/plugins/spellcheck/cjhtextregion.c
new file mode 100644
index 000000000..ccaa1c9b7
--- /dev/null
+++ b/src/plugins/spellcheck/cjhtextregion.c
@@ -0,0 +1,1311 @@
+/* cjhtextregion.c
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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 2.1 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "cjhtextregionprivate.h"
+#include "cjhtextregionbtree.h"
+
+/**
+ * SECTION:cjhtextregion
+ * @Title: CjhTextRegion
+ * @Short_description: track regions of text with a hybrid B+Tree and piecetable
+ *
+ * This data-structure is a hybrid between a PieceTable and a B+Tree, which I've
+ * decided to call a Piece+Tree. It allows for very fast tracking of regions of
+ * text (in a single dimention, meaning no sub-regions).
+ *
+ * This is very useful for tracking where work still needs to be done in a text
+ * buffer such as for spelling mistakes, syntax highlighting, error checking, or
+ * multi-device synchronization.
+ *
+ * See_also: https://blogs.gnome.org/chergert/2021/03/26/bplustree_augmented_piecetable/
+ */
+
+#ifndef G_DISABLE_ASSERT
+# define DEBUG_VALIDATE(a,b) G_STMT_START { if (a) cjh_text_region_node_validate(a,b); } G_STMT_END
+#else
+# define DEBUG_VALIDATE(a,b) G_STMT_START { } G_STMT_END
+#endif
+
+static inline void
+cjh_text_region_invalid_cache (CjhTextRegion *region)
+{
+  region->cached_result = NULL;
+  region->cached_result_offset = 0;
+}
+
+G_GNUC_UNUSED static void
+cjh_text_region_node_validate (CjhTextRegionNode *node,
+                               CjhTextRegionNode *parent)
+{
+  gsize length = 0;
+  gsize length_in_parent = 0;
+
+  g_assert (node != NULL);
+  g_assert (UNTAG (node->tagged_parent) == parent);
+  g_assert (cjh_text_region_node_is_leaf (node) ||
+            UNTAG (node->tagged_parent) == node->tagged_parent);
+  g_assert (!parent || !cjh_text_region_node_is_leaf (parent));
+  g_assert (!parent || !SORTED_ARRAY_IS_EMPTY (&parent->branch.children));
+
+  if (parent != NULL)
+    {
+      SORTED_ARRAY_FOREACH (&parent->branch.children, CjhTextRegionChild, child, {
+        if (child->node == node)
+          {
+            length_in_parent = child->length;
+            goto found;
+          }
+      });
+      g_assert_not_reached ();
+    }
+found:
+
+  if (parent != NULL)
+    g_assert_cmpint (length_in_parent, ==, cjh_text_region_node_length (node));
+
+  for (CjhTextRegionNode *iter = parent;
+       iter != NULL;
+       iter = cjh_text_region_node_get_parent (iter))
+    g_assert_false (cjh_text_region_node_is_leaf (iter));
+
+  if (cjh_text_region_node_is_leaf (node))
+    {
+      SORTED_ARRAY_FOREACH (&node->leaf.runs, CjhTextRegionRun, run, {
+        g_assert_cmpint (run->length, >, 0);
+        length += run->length;
+      });
+
+      if (node->leaf.prev != NULL)
+        g_assert_true (cjh_text_region_node_is_leaf (node->leaf.prev));
+
+      if (node->leaf.next != NULL)
+        g_assert_true (cjh_text_region_node_is_leaf (node->leaf.next));
+    }
+  else
+    {
+      SORTED_ARRAY_FOREACH (&node->branch.children, CjhTextRegionChild, child, {
+        CjhTextRegionChild *next = SORTED_ARRAY_FOREACH_PEEK (&node->branch.children);
+
+        g_assert_nonnull (child->node);
+        g_assert_cmpint (child->length, >, 0);
+        g_assert_cmpint (child->length, ==, cjh_text_region_node_length (child->node));
+        g_assert_true (cjh_text_region_node_get_parent (child->node) == node);
+
+        length += child->length;
+
+        if (next != NULL && next->node)
+          {
+            g_assert_cmpint (cjh_text_region_node_is_leaf (child->node), ==,
+                             cjh_text_region_node_is_leaf (next->node));
+
+            if (cjh_text_region_node_is_leaf (child->node))
+              {
+                g_assert_true (child->node->leaf.next == next->node);
+                g_assert_true (child->node == next->node->leaf.prev);
+              }
+            else
+              {
+                g_assert_true (child->node->branch.next == next->node);
+                g_assert_true (child->node == next->node->branch.prev);
+              }
+          }
+      });
+    }
+
+  if (parent != NULL)
+    g_assert_cmpint (length_in_parent, ==, length);
+}
+
+static void
+cjh_text_region_split (CjhTextRegion          *region,
+                       gsize                   offset,
+                       const CjhTextRegionRun *run,
+                       CjhTextRegionRun       *left,
+                       CjhTextRegionRun       *right)
+{
+  if (region->split_func != NULL)
+    region->split_func (offset, run, left, right);
+}
+
+static CjhTextRegionNode *
+cjh_text_region_node_new (CjhTextRegionNode *parent,
+                          gboolean           is_leaf)
+{
+  CjhTextRegionNode *node;
+
+  g_assert (UNTAG (parent) == parent);
+
+  node = g_new0 (CjhTextRegionNode, 1);
+  node->tagged_parent = TAG (parent, is_leaf);
+
+  if (is_leaf)
+    {
+      SORTED_ARRAY_INIT (&node->leaf.runs);
+      node->leaf.prev = NULL;
+      node->leaf.next = NULL;
+    }
+  else
+    {
+      SORTED_ARRAY_INIT (&node->branch.children);
+    }
+
+  g_assert (cjh_text_region_node_get_parent (node) == parent);
+
+  return node;
+}
+
+static void
+cjh_text_region_subtract_from_parents (CjhTextRegion     *region,
+                                       CjhTextRegionNode *node,
+                                       gsize              length)
+{
+  CjhTextRegionNode *parent = cjh_text_region_node_get_parent (node);
+
+  if (parent == NULL || length == 0)
+    return;
+
+  cjh_text_region_invalid_cache (region);
+
+  SORTED_ARRAY_FOREACH (&parent->branch.children, CjhTextRegionChild, child, {
+    if (child->node == node)
+      {
+        g_assert (length <= child->length);
+        child->length -= length;
+        cjh_text_region_subtract_from_parents (region, parent, length);
+        return;
+      }
+  });
+
+  g_assert_not_reached ();
+}
+
+static void
+cjh_text_region_add_to_parents (CjhTextRegion     *region,
+                                CjhTextRegionNode *node,
+                                gsize              length)
+{
+  CjhTextRegionNode *parent = cjh_text_region_node_get_parent (node);
+
+  if (parent == NULL || length == 0)
+    return;
+
+  cjh_text_region_invalid_cache (region);
+
+  SORTED_ARRAY_FOREACH (&parent->branch.children, CjhTextRegionChild, child, {
+    if (child->node == node)
+      {
+        child->length += length;
+        cjh_text_region_add_to_parents (region, parent, length);
+        return;
+      }
+  });
+
+  g_assert_not_reached ();
+}
+
+static inline gboolean
+cjh_text_region_node_is_root (CjhTextRegionNode *node)
+{
+  return node != NULL && cjh_text_region_node_get_parent (node) == NULL;
+}
+
+static CjhTextRegionNode *
+cjh_text_region_node_search_recurse (CjhTextRegionNode *node,
+                                     gsize              offset,
+                                     gsize             *offset_within_node)
+{
+  CjhTextRegionChild *last_child = NULL;
+
+  g_assert (node != NULL);
+  g_assert (offset_within_node != NULL);
+
+  /* If we reached a leaf, that is all we need to do */
+  if (cjh_text_region_node_is_leaf (node))
+    {
+      *offset_within_node = offset;
+      return node;
+    }
+
+  g_assert (!cjh_text_region_node_is_leaf (node));
+  g_assert (!SORTED_ARRAY_IS_EMPTY (&node->branch.children));
+  g_assert (offset <= cjh_text_region_node_length (node));
+
+  SORTED_ARRAY_FOREACH (&node->branch.children, CjhTextRegionChild, child, {
+    g_assert (child->length > 0);
+    g_assert (child->node != NULL);
+
+    if (offset < child->length)
+      return cjh_text_region_node_search_recurse (child->node, offset, offset_within_node);
+
+    offset -= child->length;
+    last_child = child;
+  });
+
+  /* We're right-most, so it belongs at the end. Add back the length we removed
+   * while trying to resolve within the parent branch.
+   */
+  g_assert (last_child != NULL);
+  g_assert (node->branch.next == NULL);
+  return cjh_text_region_node_search_recurse (last_child->node,
+                                              offset + last_child->length,
+                                              offset_within_node);
+}
+
+static CjhTextRegionNode *
+cjh_text_region_search (CjhTextRegion *region,
+                        gsize          offset,
+                        gsize         *offset_within_node)
+{
+  CjhTextRegionNode *result;
+
+  *offset_within_node = 0;
+
+  g_assert (region->cached_result == NULL ||
+            cjh_text_region_node_is_leaf (region->cached_result));
+
+  /* Try to reuse cached node to avoid traversal since in most cases
+   * an insert will be followed by another insert.
+   */
+  if (region->cached_result != NULL && offset >= region->cached_result_offset)
+    {
+      gsize calc_offset = region->cached_result_offset + cjh_text_region_node_length (region->cached_result);
+
+      if (offset < calc_offset ||
+          (offset == calc_offset && region->cached_result->leaf.next == NULL))
+        {
+          *offset_within_node = offset - region->cached_result_offset;
+          return region->cached_result;
+        }
+    }
+
+  if (offset == 0)
+    result = _cjh_text_region_get_first_leaf (region);
+  else
+    result = cjh_text_region_node_search_recurse (&region->root, offset, offset_within_node);
+
+  /* Now save it for cached reuse */
+  if (result != NULL)
+    {
+      region->cached_result = result;
+      region->cached_result_offset = offset - *offset_within_node;
+    }
+
+  return result;
+}
+
+static void
+cjh_text_region_root_split (CjhTextRegion     *region,
+                            CjhTextRegionNode *root)
+{
+  CjhTextRegionNode *left;
+  CjhTextRegionNode *right;
+  CjhTextRegionChild new_child;
+
+  g_assert (region != NULL);
+  g_assert (!cjh_text_region_node_is_leaf (root));
+  g_assert (cjh_text_region_node_is_root (root));
+  g_assert (!SORTED_ARRAY_IS_EMPTY (&root->branch.children));
+
+  left = cjh_text_region_node_new (root, FALSE);
+  right = cjh_text_region_node_new (root, FALSE);
+
+  left->branch.next = right;
+  right->branch.prev = left;
+
+  SORTED_ARRAY_SPLIT2 (&root->branch.children, &left->branch.children, &right->branch.children);
+  SORTED_ARRAY_FOREACH (&left->branch.children, CjhTextRegionChild, child, {
+    cjh_text_region_node_set_parent (child->node, left);
+  });
+  SORTED_ARRAY_FOREACH (&right->branch.children, CjhTextRegionChild, child, {
+    cjh_text_region_node_set_parent (child->node, right);
+  });
+
+  g_assert (SORTED_ARRAY_IS_EMPTY (&root->branch.children));
+
+  new_child.node = right;
+  new_child.length = cjh_text_region_node_length (right);
+  SORTED_ARRAY_PUSH_HEAD (&root->branch.children, new_child);
+
+  new_child.node = left;
+  new_child.length = cjh_text_region_node_length (left);
+  SORTED_ARRAY_PUSH_HEAD (&root->branch.children, new_child);
+
+  g_assert (SORTED_ARRAY_LENGTH (&root->branch.children) == 2);
+
+  DEBUG_VALIDATE (root, NULL);
+  DEBUG_VALIDATE (left, root);
+  DEBUG_VALIDATE (right, root);
+}
+
+static CjhTextRegionNode *
+cjh_text_region_branch_split (CjhTextRegion     *region,
+                              CjhTextRegionNode *left)
+{
+  G_GNUC_UNUSED gsize old_length;
+  CjhTextRegionNode *parent;
+  CjhTextRegionNode *right;
+  gsize right_length = 0;
+  gsize left_length = 0;
+  guint i = 0;
+
+  g_assert (region != NULL);
+  g_assert (left != NULL);
+  g_assert (!cjh_text_region_node_is_leaf (left));
+  g_assert (!cjh_text_region_node_is_root (left));
+
+  old_length = cjh_text_region_node_length (left);
+
+  /*
+   * This operation should not change the height of the tree. Only
+   * splitting the root node can change the height of the tree. So
+   * here we add a new right node, and update the parent to point to
+   * it right after our node.
+   *
+   * Since no new items are added, lengths do not change and we do
+   * not need to update lengths up the hierarchy except for our two
+   * effected nodes (and their direct parent).
+   */
+
+  parent = cjh_text_region_node_get_parent (left);
+
+  /* Create a new node to split half the items into */
+  right = cjh_text_region_node_new (parent, FALSE);
+
+  /* Insert node into branches linked list */
+  right->branch.next = left->branch.next;
+  right->branch.prev = left;
+  if (right->branch.next != NULL)
+    right->branch.next->branch.prev = right;
+  left->branch.next = right;
+
+  SORTED_ARRAY_SPLIT (&left->branch.children, &right->branch.children);
+  SORTED_ARRAY_FOREACH (&right->branch.children, CjhTextRegionChild, child, {
+    cjh_text_region_node_set_parent (child->node, right);
+  });
+
+#ifndef G_DISABLE_ASSERT
+  SORTED_ARRAY_FOREACH (&left->branch.children, CjhTextRegionChild, child, {
+    g_assert (cjh_text_region_node_get_parent (child->node) == left);
+  });
+#endif
+
+  right_length = cjh_text_region_node_length (right);
+  left_length = cjh_text_region_node_length (left);
+
+  g_assert (right_length + left_length == old_length);
+  g_assert (SORTED_ARRAY_LENGTH (&parent->branch.children) < SORTED_ARRAY_CAPACITY 
(&parent->branch.children));
+
+  SORTED_ARRAY_FOREACH (&parent->branch.children, CjhTextRegionChild, child, {
+    i++;
+
+    if (child->node == left)
+      {
+        CjhTextRegionChild right_child;
+
+        right_child.node = right;
+        right_child.length = right_length;
+
+        child->length = left_length;
+
+        SORTED_ARRAY_INSERT_VAL (&parent->branch.children, i, right_child);
+
+        DEBUG_VALIDATE (left, parent);
+        DEBUG_VALIDATE (right, parent);
+        DEBUG_VALIDATE (parent, cjh_text_region_node_get_parent (parent));
+
+        return right;
+      }
+  });
+
+  g_assert_not_reached ();
+}
+
+static CjhTextRegionNode *
+cjh_text_region_leaf_split (CjhTextRegion     *region,
+                            CjhTextRegionNode *left)
+{
+  G_GNUC_UNUSED gsize length;
+  CjhTextRegionNode *parent;
+  CjhTextRegionNode *right;
+  gsize right_length;
+  guint i;
+
+  g_assert (region != NULL);
+  g_assert (left != NULL);
+  g_assert (cjh_text_region_node_is_leaf (left));
+
+  parent = cjh_text_region_node_get_parent (left);
+
+  g_assert (parent != left);
+  g_assert (!cjh_text_region_node_is_leaf (parent));
+  g_assert (!SORTED_ARRAY_IS_EMPTY (&parent->branch.children));
+  g_assert (!SORTED_ARRAY_IS_FULL (&parent->branch.children));
+
+  length = cjh_text_region_node_length (left);
+
+  g_assert (length > 0);
+
+  DEBUG_VALIDATE (parent, cjh_text_region_node_get_parent (parent));
+  DEBUG_VALIDATE (left, parent);
+
+  right = cjh_text_region_node_new (parent, TRUE);
+
+  SORTED_ARRAY_SPLIT (&left->leaf.runs, &right->leaf.runs);
+  right_length = cjh_text_region_node_length (right);
+
+  g_assert (length == right_length + cjh_text_region_node_length (left));
+  g_assert (cjh_text_region_node_is_leaf (left));
+  g_assert (cjh_text_region_node_is_leaf (right));
+
+  i = 0;
+  SORTED_ARRAY_FOREACH (&parent->branch.children, CjhTextRegionChild, child, {
+    G_GNUC_UNUSED const CjhTextRegionChild *next = SORTED_ARRAY_FOREACH_PEEK (&parent->branch.children);
+
+    ++i;
+
+    g_assert (cjh_text_region_node_is_leaf (child->node));
+    g_assert (next == NULL || cjh_text_region_node_is_leaf (next->node));
+
+    if (child->node == left)
+      {
+        CjhTextRegionChild right_child;
+
+        g_assert (child->length >= right_length);
+        g_assert (next == NULL || left->leaf.next == next->node);
+
+        if (left->leaf.next != NULL)
+          left->leaf.next->leaf.prev = right;
+
+        right->leaf.prev = left;
+        right->leaf.next = left->leaf.next;
+        left->leaf.next = right;
+
+        right_child.node = right;
+        right_child.length = right_length;
+
+        child->length -= right_length;
+
+        g_assert (child->length > 0);
+        g_assert (right_child.length > 0);
+
+        SORTED_ARRAY_INSERT_VAL (&parent->branch.children, i, right_child);
+
+        g_assert (right != NULL);
+        g_assert (cjh_text_region_node_is_leaf (right));
+        g_assert (right->leaf.prev == left);
+        g_assert (left->leaf.next == right);
+
+        DEBUG_VALIDATE (left, parent);
+        DEBUG_VALIDATE (right, parent);
+        DEBUG_VALIDATE (parent, cjh_text_region_node_get_parent (parent));
+
+        return right;
+      }
+  });
+
+  g_assert_not_reached ();
+}
+
+static inline gboolean
+cjh_text_region_node_needs_split (CjhTextRegionNode *node)
+{
+  /*
+   * We want to split the tree node if there is not enough space to
+   * split a single entry into two AND add a new entry. That means we
+   * need two empty slots before we ever perform an insert.
+   */
+
+  if (!cjh_text_region_node_is_leaf (node))
+    return SORTED_ARRAY_LENGTH (&node->branch.children) >= (SORTED_ARRAY_CAPACITY (&node->branch.children) - 
2);
+  else
+    return SORTED_ARRAY_LENGTH (&node->leaf.runs) >= (SORTED_ARRAY_CAPACITY (&node->leaf.runs) - 2);
+}
+
+static inline CjhTextRegionNode *
+cjh_text_region_node_split (CjhTextRegion     *region,
+                            CjhTextRegionNode *node)
+{
+  CjhTextRegionNode *parent;
+
+  g_assert (node != NULL);
+
+  cjh_text_region_invalid_cache (region);
+
+  parent = cjh_text_region_node_get_parent (node);
+
+  if (parent != NULL &&
+      cjh_text_region_node_needs_split (parent))
+    cjh_text_region_node_split (region, parent);
+
+  if (!cjh_text_region_node_is_leaf (node))
+    {
+      if (cjh_text_region_node_is_root (node))
+        {
+          cjh_text_region_root_split (region, node);
+          return &region->root;
+        }
+
+      return cjh_text_region_branch_split (region, node);
+    }
+  else
+    {
+      return cjh_text_region_leaf_split (region, node);
+    }
+}
+
+CjhTextRegion *
+_cjh_text_region_new (CjhTextRegionJoinFunc  join_func,
+                      CjhTextRegionSplitFunc split_func)
+{
+  CjhTextRegion *self;
+  CjhTextRegionNode *leaf;
+  CjhTextRegionChild child;
+
+  self = g_new0 (CjhTextRegion, 1);
+  self->length = 0;
+  self->join_func = join_func;
+  self->split_func = split_func;
+
+  /* The B+Tree has a root node (a branch) and a single leaf
+   * as a child to simplify how we do splits/rotations/etc.
+   */
+  leaf = cjh_text_region_node_new (&self->root, TRUE);
+
+  child.node = leaf;
+  child.length = 0;
+
+  SORTED_ARRAY_INIT (&self->root.branch.children);
+  SORTED_ARRAY_PUSH_HEAD (&self->root.branch.children, child);
+
+  return self;
+}
+
+static void
+cjh_text_region_node_free (CjhTextRegionNode *node)
+{
+  if (node == NULL)
+    return;
+
+  if (!cjh_text_region_node_is_leaf (node))
+    {
+      SORTED_ARRAY_FOREACH (&node->branch.children, CjhTextRegionChild, child, {
+        cjh_text_region_node_free (child->node);
+      });
+    }
+
+  g_free (node);
+}
+
+void
+_cjh_text_region_free (CjhTextRegion *region)
+{
+  if (region != NULL)
+    {
+      g_assert (cjh_text_region_node_is_root (&region->root));
+      g_assert (!SORTED_ARRAY_IS_EMPTY (&region->root.branch.children));
+
+      SORTED_ARRAY_FOREACH (&region->root.branch.children, CjhTextRegionChild, child, {
+        cjh_text_region_node_free (child->node);
+      });
+
+      g_free (region);
+    }
+}
+
+static inline gboolean
+join_run (CjhTextRegion          *region,
+          gsize                   offset,
+          const CjhTextRegionRun *left,
+          const CjhTextRegionRun *right,
+          CjhTextRegionRun       *joined)
+{
+  gboolean join;
+
+  g_assert (region != NULL);
+  g_assert (left != NULL);
+  g_assert (right != NULL);
+  g_assert (joined != NULL);
+
+  if (region->join_func != NULL)
+    join = region->join_func (offset, left, right);
+  else
+    join = FALSE;
+
+  if (join)
+    {
+      joined->length = left->length + right->length;
+      joined->data = left->data;
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+void
+_cjh_text_region_insert (CjhTextRegion *region,
+                         gsize          offset,
+                         gsize          length,
+                         gpointer       data)
+{
+  CjhTextRegionRun to_insert = { length, data };
+  CjhTextRegionNode *target;
+  CjhTextRegionNode *node;
+  CjhTextRegionNode *parent;
+  gsize offset_within_node = offset;
+  guint i;
+
+  g_assert (region != NULL);
+  g_assert (offset <= region->length);
+
+  if (length == 0)
+    return;
+
+  target = cjh_text_region_search (region, offset, &offset_within_node);
+
+  g_assert (cjh_text_region_node_is_leaf (target));
+  g_assert (offset_within_node <= cjh_text_region_node_length (target));
+
+  /* We should only hit this if we have an empty tree. */
+  if G_UNLIKELY (SORTED_ARRAY_IS_EMPTY (&target->leaf.runs))
+    {
+      g_assert (offset == 0);
+      SORTED_ARRAY_PUSH_HEAD (&target->leaf.runs, to_insert);
+      g_assert (cjh_text_region_node_length (target) == length);
+      goto inserted;
+    }
+
+  /* Split up to region->root if necessary */
+  if (cjh_text_region_node_needs_split (target))
+    {
+      DEBUG_VALIDATE (target, cjh_text_region_node_get_parent (target));
+
+      /* Split the target into two and then re-locate our position as
+       * we might need to be in another node.
+       *
+       * TODO: Potentially optimization here to look at prev/next to
+       *       locate which we need. Complicated though since we don't
+       *       have real offsets.
+       */
+      cjh_text_region_node_split (region, target);
+
+      target = cjh_text_region_search (region, offset, &offset_within_node);
+
+      g_assert (cjh_text_region_node_is_leaf (target));
+      g_assert (offset_within_node <= cjh_text_region_node_length (target));
+      DEBUG_VALIDATE (target, cjh_text_region_node_get_parent (target));
+    }
+
+  i = 0;
+  SORTED_ARRAY_FOREACH (&target->leaf.runs, CjhTextRegionRun, run, {
+    /*
+     * If this insert request would happen immediately after this run,
+     * we want to see if we can chain it to this run or the beginning
+     * of the next run.
+     *
+     * Note: We coudld also follow the the B+tree style linked-leaf to
+     *       the next leaf and compare against it's first item. But that is
+     *       out of scope for this prototype.
+     */
+
+    if (offset_within_node == 0)
+      {
+        if (!join_run (region, offset, &to_insert, run, run))
+          SORTED_ARRAY_INSERT_VAL (&target->leaf.runs, i, to_insert);
+        goto inserted;
+      }
+    else if (offset_within_node == run->length)
+      {
+        CjhTextRegionRun *next = SORTED_ARRAY_FOREACH_PEEK (&target->leaf.runs);
+
+        /* Try to chain to the end of this run or the beginning of the next */
+        if (!join_run (region, offset, run, &to_insert, run) &&
+            (next == NULL || !join_run (region, offset, &to_insert, next, next)))
+          SORTED_ARRAY_INSERT_VAL (&target->leaf.runs, i + 1, to_insert);
+        goto inserted;
+      }
+    else if (offset_within_node < run->length)
+      {
+        CjhTextRegionRun left;
+        CjhTextRegionRun right;
+
+        left.length = offset_within_node;
+        left.data = run->data;
+
+        right.length = run->length - offset_within_node;
+        right.data = run->data;
+
+        cjh_text_region_split (region, offset - offset_within_node, run, &left, &right);
+
+        *run = left;
+
+        if (!join_run (region, offset, &to_insert, &right, &to_insert))
+          SORTED_ARRAY_INSERT_VAL (&target->leaf.runs, i + 1, right);
+
+        if (!join_run (region, offset - offset_within_node, run, &to_insert, run))
+          SORTED_ARRAY_INSERT_VAL (&target->leaf.runs, i + 1, to_insert);
+
+        goto inserted;
+      }
+
+    offset_within_node -= run->length;
+
+    i++;
+  });
+
+  g_assert_not_reached ();
+
+inserted:
+
+  g_assert (target != NULL);
+
+  /*
+   * Now update each of the parent nodes in the tree so that they have
+   * an apprporiate length along with the child pointer. This allows them
+   * to calculate offsets while walking the tree (without derefrencing the
+   * child node) at the cost of us walking back up the tree.
+   */
+  for (parent = cjh_text_region_node_get_parent (target), node = target;
+       parent != NULL;
+       node = parent, parent = cjh_text_region_node_get_parent (node))
+    {
+      SORTED_ARRAY_FOREACH (&parent->branch.children, CjhTextRegionChild, child, {
+        if (child->node == node)
+          {
+            child->length += length;
+            goto found_in_parent;
+          }
+      });
+
+      g_assert_not_reached ();
+
+    found_in_parent:
+      DEBUG_VALIDATE (node, parent);
+      continue;
+    }
+
+  region->length += length;
+
+  g_assert (region->length == cjh_text_region_node_length (&region->root));
+}
+
+void
+_cjh_text_region_replace (CjhTextRegion *region,
+                          gsize          offset,
+                          gsize          length,
+                          gpointer       data)
+{
+  g_assert (region != NULL);
+
+  if (length == 0)
+    return;
+
+  /* TODO: This could be optimized to avoid possible splits
+   *       by merging adjoining runs.
+   */
+
+  _cjh_text_region_remove (region, offset, length);
+  _cjh_text_region_insert (region, offset, length, data);
+
+  g_assert (region->length == cjh_text_region_node_length (&region->root));
+}
+
+guint
+_cjh_text_region_get_length (CjhTextRegion *region)
+{
+  g_assert (region != NULL);
+
+  return region->length;
+}
+
+static void
+cjh_text_region_branch_compact (CjhTextRegion     *region,
+                                CjhTextRegionNode *node)
+{
+  CjhTextRegionNode *parent;
+  CjhTextRegionNode *left;
+  CjhTextRegionNode *right;
+  CjhTextRegionNode *target;
+  gsize added = 0;
+  gsize length;
+
+  g_assert (region != NULL);
+  g_assert (node != NULL);
+  g_assert (!cjh_text_region_node_is_leaf (node));
+
+  SORTED_ARRAY_FOREACH (&node->branch.children, CjhTextRegionChild, child, {
+    if (child->node == NULL)
+      {
+        g_assert (child->length == 0);
+        SORTED_ARRAY_FOREACH_REMOVE (&node->branch.children);
+      }
+  });
+
+  if (cjh_text_region_node_is_root (node))
+    return;
+
+  parent = cjh_text_region_node_get_parent (node);
+
+  g_assert (parent != NULL);
+  g_assert (!cjh_text_region_node_is_leaf (parent));
+
+  /* Reparent child in our stead if we can remove this node */
+  if (SORTED_ARRAY_LENGTH (&node->branch.children) == 1 &&
+      SORTED_ARRAY_LENGTH (&parent->branch.children) == 1)
+    {
+      CjhTextRegionChild *descendant = &SORTED_ARRAY_PEEK_HEAD (&node->branch.children);
+
+      g_assert (parent->branch.prev == NULL);
+      g_assert (parent->branch.next == NULL);
+      g_assert (node->branch.prev == NULL);
+      g_assert (node->branch.next == NULL);
+      g_assert (descendant->node != NULL);
+
+      SORTED_ARRAY_FOREACH (&parent->branch.children, CjhTextRegionChild, child, {
+        if (child->node == node)
+          {
+            child->node = descendant->node;
+            cjh_text_region_node_set_parent (child->node, parent);
+
+            descendant->node = NULL;
+            descendant->length = 0;
+
+            goto compact_parent;
+          }
+      });
+
+      g_assert_not_reached ();
+    }
+
+  if (node->branch.prev == NULL && node->branch.next == NULL)
+    return;
+
+  if (SORTED_ARRAY_LENGTH (&node->branch.children) >= CJH_TEXT_REGION_MIN_BRANCHES)
+    return;
+
+  length = cjh_text_region_node_length (node);
+  cjh_text_region_subtract_from_parents (region, node, length);
+
+  /* Remove this node, we'll reparent the children with edges */
+  SORTED_ARRAY_FOREACH (&parent->branch.children, CjhTextRegionChild, child, {
+    if (child->node == node)
+      {
+        SORTED_ARRAY_FOREACH_REMOVE (&parent->branch.children);
+        goto found;
+      }
+  });
+
+  g_assert_not_reached ();
+
+found:
+  left = node->branch.prev;
+  right = node->branch.next;
+
+  if (left != NULL)
+    left->branch.next = right;
+
+  if (right != NULL)
+    right->branch.prev = left;
+
+  if (left == NULL ||
+      (right != NULL &&
+       SORTED_ARRAY_LENGTH (&left->branch.children) > SORTED_ARRAY_LENGTH (&right->branch.children)))
+    {
+      target = right;
+
+      g_assert (target->branch.prev == left);
+
+      SORTED_ARRAY_FOREACH_REVERSE (&node->branch.children, CjhTextRegionChild, child, {
+        if (SORTED_ARRAY_LENGTH (&target->branch.children) >= CJH_TEXT_REGION_MAX_BRANCHES-1)
+          {
+            cjh_text_region_add_to_parents (region, target, added);
+            added = 0;
+            cjh_text_region_branch_split (region, target);
+            g_assert (target->branch.prev == left);
+          }
+
+        cjh_text_region_node_set_parent (child->node, target);
+        added += child->length;
+        SORTED_ARRAY_PUSH_HEAD (&target->branch.children, *child);
+
+        child->node = NULL;
+        child->length = 0;
+      });
+
+      cjh_text_region_add_to_parents (region, target, added);
+    }
+  else
+    {
+      target = left;
+
+      g_assert (target->branch.next == right);
+
+      SORTED_ARRAY_FOREACH (&node->branch.children, CjhTextRegionChild, child, {
+        if (SORTED_ARRAY_LENGTH (&target->branch.children) >= CJH_TEXT_REGION_MAX_BRANCHES-1)
+          {
+            cjh_text_region_add_to_parents (region, target, added);
+            added = 0;
+            target = cjh_text_region_branch_split (region, target);
+          }
+
+        cjh_text_region_node_set_parent (child->node, target);
+        added += child->length;
+        SORTED_ARRAY_PUSH_TAIL (&target->branch.children, *child);
+
+        child->node = NULL;
+        child->length = 0;
+      });
+
+      cjh_text_region_add_to_parents (region, target, added);
+    }
+
+  DEBUG_VALIDATE (left, cjh_text_region_node_get_parent (left));
+  DEBUG_VALIDATE (right, cjh_text_region_node_get_parent (right));
+  DEBUG_VALIDATE (parent, cjh_text_region_node_get_parent (parent));
+
+compact_parent:
+  if (parent != NULL)
+    cjh_text_region_branch_compact (region, parent);
+
+  cjh_text_region_node_free (node);
+}
+
+static void
+cjh_text_region_leaf_compact (CjhTextRegion     *region,
+                              CjhTextRegionNode *node)
+{
+  CjhTextRegionNode *parent;
+  CjhTextRegionNode *target;
+  CjhTextRegionNode *left;
+  CjhTextRegionNode *right;
+  gsize added = 0;
+
+  g_assert (region != NULL);
+  g_assert (node != NULL);
+  g_assert (cjh_text_region_node_is_leaf (node));
+  g_assert (SORTED_ARRAY_LENGTH (&node->leaf.runs) < CJH_TEXT_REGION_MIN_RUNS);
+
+  /* Short-circuit if we are the only node */
+  if (node->leaf.prev == NULL && node->leaf.next == NULL)
+    return;
+
+  parent = cjh_text_region_node_get_parent (node);
+  left = node->leaf.prev;
+  right = node->leaf.next;
+
+  g_assert (parent != NULL);
+  g_assert (!cjh_text_region_node_is_leaf (parent));
+  g_assert (left == NULL || cjh_text_region_node_is_leaf (left));
+  g_assert (right == NULL || cjh_text_region_node_is_leaf (right));
+
+  SORTED_ARRAY_FOREACH (&parent->branch.children, CjhTextRegionChild, child, {
+    if (child->node == node)
+      {
+        cjh_text_region_subtract_from_parents (region, node, child->length);
+        g_assert (child->length == 0);
+        SORTED_ARRAY_FOREACH_REMOVE (&parent->branch.children);
+        goto found;
+      }
+  });
+
+  g_assert_not_reached ();
+
+found:
+  if (left != NULL)
+    left->leaf.next = right;
+
+  if (right != NULL)
+    right->leaf.prev = left;
+
+  node->leaf.next = NULL;
+  node->leaf.prev = NULL;
+
+  if (left == NULL ||
+      (right != NULL &&
+       SORTED_ARRAY_LENGTH (&left->leaf.runs) > SORTED_ARRAY_LENGTH (&right->leaf.runs)))
+    {
+      target = right;
+
+      g_assert (target->leaf.prev == left);
+
+      SORTED_ARRAY_FOREACH_REVERSE (&node->leaf.runs, CjhTextRegionRun, run, {
+        if (SORTED_ARRAY_LENGTH (&target->leaf.runs) >= CJH_TEXT_REGION_MAX_RUNS-1)
+          {
+            cjh_text_region_add_to_parents (region, target, added);
+            added = 0;
+            cjh_text_region_node_split (region, target);
+            g_assert (target->leaf.prev == left);
+          }
+
+        added += run->length;
+        SORTED_ARRAY_PUSH_HEAD (&target->leaf.runs, *run);
+      });
+
+      cjh_text_region_add_to_parents (region, target, added);
+    }
+  else
+    {
+      target = left;
+
+      g_assert (target->leaf.next == right);
+
+      SORTED_ARRAY_FOREACH (&node->leaf.runs, CjhTextRegionRun, run, {
+        if (SORTED_ARRAY_LENGTH (&target->leaf.runs) >= CJH_TEXT_REGION_MAX_RUNS-1)
+          {
+            cjh_text_region_add_to_parents (region, target, added);
+            added = 0;
+
+            target = cjh_text_region_node_split (region, target);
+
+            left = target;
+          }
+
+        added += run->length;
+        SORTED_ARRAY_PUSH_TAIL (&target->leaf.runs, *run);
+      });
+
+      cjh_text_region_add_to_parents (region, target, added);
+    }
+
+  DEBUG_VALIDATE (left, cjh_text_region_node_get_parent (left));
+  DEBUG_VALIDATE (right, cjh_text_region_node_get_parent (right));
+  DEBUG_VALIDATE (parent, cjh_text_region_node_get_parent (parent));
+
+  cjh_text_region_branch_compact (region, parent);
+
+  cjh_text_region_node_free (node);
+}
+
+void
+_cjh_text_region_remove (CjhTextRegion *region,
+                         gsize          offset,
+                         gsize          length)
+{
+  CjhTextRegionNode *target;
+  gsize offset_within_node;
+  gsize to_remove = length;
+  gsize calc_offset;
+  guint i;
+
+  g_assert (region != NULL);
+  g_assert (length <= region->length);
+  g_assert (offset < region->length);
+  g_assert (length <= region->length - offset);
+
+  if (length == 0)
+    return;
+
+  target = cjh_text_region_search (region, offset, &offset_within_node);
+
+  g_assert (target != NULL);
+  g_assert (cjh_text_region_node_is_leaf (target));
+  g_assert (SORTED_ARRAY_LENGTH (&target->leaf.runs) > 0);
+  g_assert (offset >= offset_within_node);
+
+  calc_offset = offset - offset_within_node;
+
+  i = 0;
+  SORTED_ARRAY_FOREACH (&target->leaf.runs, CjhTextRegionRun, run, {
+    ++i;
+
+    g_assert (to_remove > 0);
+
+    if (offset_within_node >= run->length)
+      {
+        offset_within_node -= run->length;
+        calc_offset += run->length;
+      }
+    else if (offset_within_node > 0 && to_remove >= run->length - offset_within_node)
+      {
+        CjhTextRegionRun left;
+        CjhTextRegionRun right;
+
+        left.length = offset_within_node;
+        left.data = run->data;
+        right.length = run->length - left.length;
+        right.data = run->data;
+        cjh_text_region_split (region, calc_offset, run, &left, &right);
+
+        to_remove -= right.length;
+        calc_offset += left.length;
+        offset_within_node = 0;
+
+        *run = left;
+
+        if (to_remove == 0)
+          break;
+      }
+    else if (offset_within_node > 0 && to_remove < run->length - offset_within_node)
+      {
+        CjhTextRegionRun left;
+        CjhTextRegionRun right;
+        CjhTextRegionRun right2;
+        CjhTextRegionRun center;
+
+        left.length = offset_within_node;
+        left.data = run->data;
+        right.length = run->length - left.length;
+        right.data = run->data;
+        cjh_text_region_split (region, calc_offset, run, &left, &right);
+
+        center.length = to_remove;
+        center.data = run->data;
+        right2.length = run->length - offset_within_node - to_remove;
+        right2.data = run->data;
+        cjh_text_region_split (region, calc_offset + left.length, &right, &center, &right2);
+
+        *run = left;
+
+        if (!join_run (region, calc_offset, run, &right2, run))
+          SORTED_ARRAY_INSERT_VAL (&target->leaf.runs, i, right2);
+
+        offset_within_node = 0;
+        to_remove = 0;
+
+        break;
+      }
+    else if (offset_within_node == 0 && to_remove < run->length)
+      {
+        CjhTextRegionRun left;
+        CjhTextRegionRun right;
+
+        left.length = to_remove;
+        left.data = run->data;
+
+        right.length = run->length - to_remove;
+        right.data = run->data;
+
+        cjh_text_region_split (region, calc_offset, run, &left, &right);
+
+        to_remove = 0;
+        offset_within_node = 0;
+
+        *run = right;
+
+        break;
+      }
+    else if (offset_within_node == 0 && to_remove >= run->length)
+      {
+        to_remove -= run->length;
+
+        SORTED_ARRAY_FOREACH_REMOVE (&target->leaf.runs);
+
+        if (to_remove == 0)
+          break;
+      }
+    else
+      {
+        g_assert_not_reached ();
+      }
+
+    g_assert (to_remove > 0);
+  });
+
+  region->length -= length - to_remove;
+  cjh_text_region_subtract_from_parents (region, target, length - to_remove);
+
+  if (SORTED_ARRAY_LENGTH (&target->leaf.runs) < CJH_TEXT_REGION_MIN_RUNS)
+    cjh_text_region_leaf_compact (region, target);
+
+  g_assert (region->length == cjh_text_region_node_length (&region->root));
+
+  if (to_remove > 0)
+    _cjh_text_region_remove (region, offset, to_remove);
+}
+
+void
+_cjh_text_region_foreach (CjhTextRegion            *region,
+                          CjhTextRegionForeachFunc  func,
+                          gpointer                  user_data)
+{
+  CjhTextRegionNode *leaf;
+  gsize offset = 0;
+
+  g_return_if_fail (region != NULL);
+  g_return_if_fail (func != NULL);
+
+  for (leaf = _cjh_text_region_get_first_leaf (region);
+       leaf != NULL;
+       leaf = leaf->leaf.next)
+    {
+      g_assert (leaf->leaf.next == NULL || leaf->leaf.next->leaf.prev == leaf);
+
+      SORTED_ARRAY_FOREACH (&leaf->leaf.runs, CjhTextRegionRun, run, {
+        if (func (offset, run, user_data))
+          return;
+        offset += run->length;
+      });
+    }
+}
+
+void
+_cjh_text_region_foreach_in_range (CjhTextRegion            *region,
+                                   gsize                     begin,
+                                   gsize                     end,
+                                   CjhTextRegionForeachFunc  func,
+                                   gpointer                  user_data)
+{
+  CjhTextRegionNode *leaf;
+  gsize position;
+  gsize offset_within_node = 0;
+
+  g_return_if_fail (region != NULL);
+  g_return_if_fail (func != NULL);
+  g_return_if_fail (begin <= region->length);
+  g_return_if_fail (end <= region->length);
+  g_return_if_fail (begin <= end);
+
+  if (begin == end || begin == region->length)
+    return;
+
+  if (begin == 0)
+    leaf = _cjh_text_region_get_first_leaf (region);
+  else
+    leaf = cjh_text_region_search (region, begin, &offset_within_node);
+
+  g_assert (offset_within_node < cjh_text_region_node_length (leaf));
+
+  position = begin - offset_within_node;
+
+  while (position < end)
+    {
+      SORTED_ARRAY_FOREACH (&leaf->leaf.runs, CjhTextRegionRun, run, {
+        if (offset_within_node >= run->length)
+          {
+            offset_within_node -= run->length;
+          }
+        else
+          {
+            offset_within_node = 0;
+            if (func (position, run, user_data))
+              return;
+          }
+
+        position += run->length;
+
+        if (position >= end)
+          break;
+      });
+
+      leaf = leaf->leaf.next;
+    }
+}
diff --git a/src/plugins/spellcheck/cjhtextregionbtree.h b/src/plugins/spellcheck/cjhtextregionbtree.h
new file mode 100644
index 000000000..610874ccc
--- /dev/null
+++ b/src/plugins/spellcheck/cjhtextregionbtree.h
@@ -0,0 +1,564 @@
+/* cjhtextregionbtree.h
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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 2.1 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef __CJH_TEXT_REGION_BTREE_H__
+#define __CJH_TEXT_REGION_BTREE_H__
+
+#include "cjhtextregionprivate.h"
+
+G_BEGIN_DECLS
+
+/* The following set of macros are used to create a queue similar to a
+ * double-ended linked list but using integers as indexes for items within the
+ * queue. Doing so allows for inserting or removing items from a b+tree node
+ * without having to memmove() data to maintain sorting orders.
+ */
+#define VAL_QUEUE_INVALID(Node) ((glib_typeof((Node)->head))-1)
+#define VAL_QUEUE_LENGTH(Node) ((Node)->length)
+#define VAL_QUEUE_EMPTY(Node) ((Node)->head == VAL_QUEUE_INVALID(Node))
+#define VAL_QUEUE_PEEK_HEAD(Node) ((Node)->head)
+#define VAL_QUEUE_PEEK_TAIL(Node) ((Node)->tail)
+#define VAL_QUEUE_IS_VALID(Node, ID) ((ID) != VAL_QUEUE_INVALID(Node))
+#define VAL_QUEUE_NODE(Type, N_Items)                                        \
+  struct {                                                                   \
+    Type length;                                                             \
+    Type head;                                                               \
+    Type tail;                                                               \
+    struct {                                                                 \
+      Type prev;                                                             \
+      Type next;                                                             \
+    } items[N_Items];                                                        \
+  }
+#define VAL_QUEUE_INIT(Node)                                                 \
+  G_STMT_START {                                                             \
+    (Node)->length = 0;                                                      \
+    (Node)->head = VAL_QUEUE_INVALID(Node);                                  \
+    (Node)->tail = VAL_QUEUE_INVALID(Node);                                  \
+    for (guint _i = 0; _i < G_N_ELEMENTS ((Node)->items); _i++)              \
+      {                                                                      \
+        (Node)->items[_i].next = VAL_QUEUE_INVALID(Node);                    \
+        (Node)->items[_i].prev = VAL_QUEUE_INVALID(Node);                    \
+      }                                                                      \
+  } G_STMT_END
+#ifndef G_DISABLE_ASSERT
+# define _VAL_QUEUE_VALIDATE(Node)                                           \
+  G_STMT_START {                                                             \
+    glib_typeof((Node)->head) count = 0;                                     \
+                                                                             \
+    if ((Node)->tail != VAL_QUEUE_INVALID(Node))                             \
+      g_assert_cmpint((Node)->items[(Node)->tail].next, ==, VAL_QUEUE_INVALID(Node)); \
+    if ((Node)->head != VAL_QUEUE_INVALID(Node))                             \
+      g_assert_cmpint((Node)->items[(Node)->head].prev , ==, VAL_QUEUE_INVALID(Node)); \
+                                                                             \
+    for (glib_typeof((Node)->head) _viter = (Node)->head;                    \
+         VAL_QUEUE_IS_VALID(Node, _viter);                                   \
+         _viter = (Node)->items[_viter].next)                                \
+    {                                                                        \
+      count++;                                                               \
+    }                                                                        \
+                                                                             \
+    g_assert_cmpint(count, ==, (Node)->length);                              \
+  } G_STMT_END
+#else
+# define _VAL_QUEUE_VALIDATE(Node) G_STMT_START { } G_STMT_END
+#endif
+#define VAL_QUEUE_PUSH_HEAD(Node, ID)                                        \
+  G_STMT_START {                                                             \
+    (Node)->items[ID].prev = VAL_QUEUE_INVALID(Node);                        \
+    (Node)->items[ID].next = (Node)->head;                                   \
+    if (VAL_QUEUE_IS_VALID(Node, (Node)->head))                              \
+      (Node)->items[(Node)->head].prev = ID;                                 \
+    (Node)->head = ID;                                                       \
+    if (!VAL_QUEUE_IS_VALID(Node, (Node)->tail))                             \
+      (Node)->tail = ID;                                                     \
+    (Node)->length++;                                                        \
+    _VAL_QUEUE_VALIDATE(Node);                                               \
+  } G_STMT_END
+#define VAL_QUEUE_PUSH_TAIL(Node, ID)                                        \
+  G_STMT_START {                                                             \
+    (Node)->items[ID].prev = (Node)->tail;                                   \
+    (Node)->items[ID].next = VAL_QUEUE_INVALID(Node);                        \
+    if (VAL_QUEUE_IS_VALID (Node, (Node)->tail))                             \
+      (Node)->items[(Node)->tail].next = ID;                                 \
+    (Node)->tail = ID;                                                       \
+    if (!VAL_QUEUE_IS_VALID(Node, (Node)->head))                             \
+      (Node)->head = ID;                                                     \
+    (Node)->length++;                                                        \
+    _VAL_QUEUE_VALIDATE(Node);                                               \
+  } G_STMT_END
+#define VAL_QUEUE_INSERT(Node, Nth, Val)                                     \
+  G_STMT_START {                                                             \
+    g_assert_cmpint (VAL_QUEUE_LENGTH(Node),<,G_N_ELEMENTS((Node)->items));  \
+                                                                             \
+    if ((Nth) == 0)                                                          \
+      {                                                                      \
+        VAL_QUEUE_PUSH_HEAD(Node, Val);                                      \
+      }                                                                      \
+    else if ((Nth) == (Node)->length)                                        \
+      {                                                                      \
+        VAL_QUEUE_PUSH_TAIL(Node, Val);                                      \
+      }                                                                      \
+    else                                                                     \
+      {                                                                      \
+        glib_typeof((Node)->head) ID;                                        \
+        glib_typeof((Node)->head) _nth;                                      \
+                                                                             \
+        g_assert_cmpint (VAL_QUEUE_LENGTH(Node), >, 0);                      \
+        g_assert (VAL_QUEUE_IS_VALID(Node, (Node)->head));                   \
+        g_assert (VAL_QUEUE_IS_VALID(Node, (Node)->tail));                   \
+                                                                             \
+        for (ID = (Node)->head, _nth = 0;                                    \
+             _nth < (Nth) && VAL_QUEUE_IS_VALID(Node, ID);                   \
+             ID = (Node)->items[ID].next, ++_nth)                            \
+          { /* Do Nothing */ }                                               \
+                                                                             \
+        g_assert (VAL_QUEUE_IS_VALID(Node, ID));                             \
+        g_assert (VAL_QUEUE_IS_VALID(Node, (Node)->items[ID].prev));         \
+                                                                             \
+        (Node)->items[Val].prev = (Node)->items[ID].prev;                    \
+        (Node)->items[Val].next = ID;                                        \
+        (Node)->items[(Node)->items[ID].prev].next = Val;                    \
+        (Node)->items[ID].prev = Val;                                        \
+                                                                             \
+        (Node)->length++;                                                    \
+                                                                             \
+        _VAL_QUEUE_VALIDATE(Node);                                           \
+      }                                                                      \
+  } G_STMT_END
+#define VAL_QUEUE_POP_HEAD(Node,_pos) VAL_QUEUE_POP_NTH((Node), 0, _pos)
+#define VAL_QUEUE_POP_TAIL(Node,_pos) VAL_QUEUE_POP_NTH((Node), (Node)->length - 1, _pos)
+#define VAL_QUEUE_POP_AT(Node, _pos)                                         \
+  G_STMT_START {                                                             \
+    g_assert (_pos != VAL_QUEUE_INVALID(Node));                              \
+    g_assert (_pos < G_N_ELEMENTS ((Node)->items));                          \
+                                                                             \
+    if ((Node)->items[_pos].prev != VAL_QUEUE_INVALID(Node))                 \
+      (Node)->items[(Node)->items[_pos].prev].next = (Node)->items[_pos].next; \
+    if ((Node)->items[_pos].next != VAL_QUEUE_INVALID(Node))                 \
+      (Node)->items[(Node)->items[_pos].next].prev = (Node)->items[_pos].prev; \
+    if ((Node)->head == _pos)                                                \
+      (Node)->head = (Node)->items[_pos].next;                               \
+    if ((Node)->tail == _pos)                                                \
+      (Node)->tail = (Node)->items[_pos].prev;                               \
+                                                                             \
+    (Node)->items[_pos].prev = VAL_QUEUE_INVALID((Node));                    \
+    (Node)->items[_pos].next = VAL_QUEUE_INVALID((Node));                    \
+                                                                             \
+    (Node)->length--;                                                        \
+                                                                             \
+    _VAL_QUEUE_VALIDATE(Node);                                               \
+  } G_STMT_END
+#define VAL_QUEUE_POP_NTH(Node, Nth, _pos)                                   \
+  G_STMT_START {                                                             \
+    _pos = VAL_QUEUE_INVALID(Node);                                          \
+                                                                             \
+    if (Nth == 0)                                                            \
+      _pos = (Node)->head;                                                   \
+    else if (Nth >= (((Node)->length) - 1))                                  \
+      _pos = (Node)->tail;                                                   \
+    else                                                                     \
+      VAL_QUEUE_NTH (Node, Nth, _pos);                                       \
+                                                                             \
+   if (_pos != VAL_QUEUE_INVALID(Node))                                      \
+     VAL_QUEUE_POP_AT (Node, _pos);                                          \
+  } G_STMT_END
+#define VAL_QUEUE_NTH(Node, Nth, _iter)                                      \
+  G_STMT_START {                                                             \
+    glib_typeof((Node)->head) _nth;                                          \
+    if (Nth == 0)                                                            \
+      _iter = (Node)->head;                                                  \
+    else if (Nth >= (((Node)->length) - 1))                                  \
+      _iter = (Node)->tail;                                                  \
+    else                                                                     \
+      {                                                                      \
+        for (_iter = (Node)->head, _nth = 0;                                 \
+             _nth < (Nth);                                                   \
+             _iter = (Node)->items[_iter].next, ++_nth)                      \
+          {                                                                  \
+            /* Do Nothing */                                                 \
+            g_assert (_iter != VAL_QUEUE_INVALID(Node));                     \
+          }                                                                  \
+      }                                                                      \
+  } G_STMT_END
+#define _VAL_QUEUE_MOVE(Node, Old, New)                                      \
+  G_STMT_START {                                                             \
+    (Node)->items[New] = (Node)->items[Old];                                 \
+    if ((Node)->items[New].prev != VAL_QUEUE_INVALID(Node))                  \
+      (Node)->items[(Node)->items[New].prev].next = New;                     \
+    if ((Node)->items[New].next != VAL_QUEUE_INVALID(Node))                  \
+      (Node)->items[(Node)->items[New].next].prev = New;                     \
+    if ((Node)->head == Old)                                                 \
+      (Node)->head = New;                                                    \
+    if ((Node)->tail == Old)                                                 \
+      (Node)->tail = New;                                                    \
+  } G_STMT_END
+/*
+ * SORTED_ARRAY_FIELD:
+ * @TYPE: The type of the structure used by elements in the array
+ * @N_ITEMS: The maximum number of items in the array
+ *
+ * This creates a new inline structure that can be embedded within
+ * other super-structures.
+ *
+ * @N_ITEMS must be <= 254 or this macro will fail.
+ */
+#define SORTED_ARRAY_FIELD(TYPE,N_ITEMS)                                     \
+  struct {                                                                   \
+    TYPE items[N_ITEMS];                                                     \
+    VAL_QUEUE_NODE(guint8, N_ITEMS) q;                                       \
+  }
+/*
+ * SORTED_ARRAY_INIT:
+ * @FIELD: A pointer to a SortedArray
+ *
+ * This will initialize a node that has been previously registered
+ * using %SORTED_ARRAY_FIELD(). You must call this macro before
+ * using the SortedArray structure.
+ */
+#define SORTED_ARRAY_INIT(FIELD)                                             \
+  G_STMT_START {                                                             \
+    G_STATIC_ASSERT (G_N_ELEMENTS((FIELD)->items) < 255);                    \
+    VAL_QUEUE_INIT(&(FIELD)->q);                                             \
+  } G_STMT_END
+/*
+ * SORTED_ARRAY_LENGTH:
+ * @FIELD: A pointer to the SortedArray field.
+ *
+ * This macro will evaluate to the number of items inserted into
+ * the SortedArray.
+ */
+#define SORTED_ARRAY_LENGTH(FIELD) (VAL_QUEUE_LENGTH(&(FIELD)->q))
+/*
+ * SORTED_ARRAY_CAPACITY:
+ * @FIELD: A pointer to the SortedArray field.
+ *
+ * This macro will evaluate to the number of elements in the SortedArray.
+ * This is dependent on how the SortedArray was instantiated using
+ * the %SORTED_ARRAY_FIELD() macro.
+ */
+#define SORTED_ARRAY_CAPACITY(FIELD) (G_N_ELEMENTS((FIELD)->items))
+/*
+ * SORTED_ARRAY_IS_FULL:
+ * @FIELD: A pointer to the SortedArray field.
+ *
+ * This macro will evaluate to 1 if the SortedArray is at capacity.
+ * Otherwise, the macro will evaluate to 0.
+ */
+#define SORTED_ARRAY_IS_FULL(FIELD) (SORTED_ARRAY_LENGTH(FIELD) == SORTED_ARRAY_CAPACITY(FIELD))
+/*
+ * SORTED_ARRAY_IS_EMPTY:
+ * @FIELD: A SortedArray field
+ *
+ * This macro will evaluate to 1 if the SortedArray contains zero children.
+ */
+#define SORTED_ARRAY_IS_EMPTY(FIELD) (SORTED_ARRAY_LENGTH(FIELD) == 0)
+/*
+ * SORTED_ARRAY_INSERT_VAL:
+ * @FIELD: A pointer to a SortedArray field.
+ * @POSITION: the logical position at which to insert
+ * @ELEMENT: The element to insert
+ *
+ * This will insert a new item into the array. It is invalid API use
+ * to call this function while the SortedArray is at capacity. Check
+ * SORTED_ARRAY_IS_FULL() before using this function to be certain.
+ */
+#define SORTED_ARRAY_INSERT_VAL(FIELD,POSITION,ELEMENT)                      \
+  G_STMT_START {                                                             \
+    guint8 _pos;                                                             \
+                                                                             \
+    g_assert (POSITION <= SORTED_ARRAY_LENGTH(FIELD));                       \
+                                                                             \
+    _pos = VAL_QUEUE_LENGTH(&(FIELD)->q);                                    \
+    g_assert (_pos != VAL_QUEUE_INVALID(&(FIELD)->q));                       \
+    (FIELD)->items[_pos] = ELEMENT;                                          \
+    VAL_QUEUE_INSERT(&(FIELD)->q, POSITION, _pos);                           \
+  } G_STMT_END
+#define SORTED_ARRAY_REMOVE_INDEX(FIELD,POSITION,_ele)                       \
+  G_STMT_START {                                                             \
+    guint8 _pos;                                                             \
+    guint8 _len;                                                             \
+                                                                             \
+    VAL_QUEUE_POP_NTH(&(FIELD)->q, POSITION, _pos);                          \
+    if (_pos == VAL_QUEUE_INVALID(&(FIELD)->q))                              \
+      {                                                                      \
+        g_assert_not_reached ();                                             \
+        break;                                                               \
+      }                                                                      \
+                                                                             \
+    _ele = (FIELD)->items[_pos];                                             \
+    _len = VAL_QUEUE_LENGTH(&(FIELD)->q);                                    \
+                                                                             \
+    /* We must preserve our invariant of having no empty gaps                \
+     * in the array so that se can place new items always at the             \
+     * end (to avoid scanning for an empty spot).                            \
+     * Therefore we move our tail item into the removed slot and             \
+     * adjust the iqueue positions (which are all O(1).                      \
+     */                                                                      \
+                                                                             \
+    if (_pos < _len)                                                         \
+      {                                                                      \
+        (FIELD)->items[_pos] = (FIELD)->items[_len];                         \
+        _VAL_QUEUE_MOVE(&(FIELD)->q, _len, _pos);                            \
+      }                                                                      \
+  } G_STMT_END
+/* SORTED_ARRAY_FOREACH_REMOVE:
+ *
+ * This a form of SORTED_ARRAY_REMOVE_INDEX but to be used when you
+ * are within a SORTED_ARRAY_FOREACH() to avoid extra scanning.
+ */
+#define SORTED_ARRAY_FOREACH_REMOVE(FIELD)                                   \
+  G_STMT_START {                                                             \
+    guint8 _pos = _current;                                                  \
+    guint8 _len = VAL_QUEUE_LENGTH(&(FIELD)->q);                             \
+                                                                             \
+    g_assert (_len > 0);                                                     \
+    g_assert (_pos < _len);                                                  \
+    VAL_QUEUE_POP_AT(&(FIELD)->q, _pos);                                     \
+    g_assert (VAL_QUEUE_LENGTH(&(FIELD)->q) == _len-1);                      \
+    _len--;                                                                  \
+                                                                             \
+    /* We must preserve our invariant of having no empty gaps                \
+     * in the array so that se can place new items always at the             \
+     * end (to avoid scanning for an empty spot).                            \
+     * Therefore we move our tail item into the removed slot and             \
+     * adjust the iqueue positions (which are all O(1).                      \
+     */                                                                      \
+                                                                             \
+    if (_pos < _len)                                                         \
+      {                                                                      \
+        (FIELD)->items[_pos] = (FIELD)->items[_len];                         \
+        _VAL_QUEUE_MOVE(&(FIELD)->q, _len, _pos);                            \
+                                                                             \
+        /* We might need to change the iter if next position moved */        \
+        if (_aiter == _len)                                                  \
+          _aiter = _pos;                                                     \
+      }                                                                      \
+                                                                             \
+  } G_STMT_END
+/*
+ * SORTED_ARRAY_FOREACH:
+ * @FIELD: A pointer to a SortedArray
+ * @Element: The type of the elements in @FIELD
+ * @Name: the name for a pointer of type @Element
+ * @LABlock: a {} tyle block to execute for each item. You may use
+ *    "break" to exit the foreach.
+ *
+ * Calls @Block for every element stored in @FIELD. A pointer to
+ * each element will be provided as a variable named @Name.
+ */
+#define SORTED_ARRAY_FOREACH(FIELD, Element, Name, LABlock)                  \
+  G_STMT_START {                                                             \
+    for (glib_typeof((FIELD)->q.head) _aiter = (FIELD)->q.head;              \
+         _aiter != VAL_QUEUE_INVALID(&(FIELD)->q);                           \
+         /* Do Nothing */)                                                   \
+      {                                                                      \
+        G_GNUC_UNUSED glib_typeof((FIELD)->q.head) _current = _aiter;        \
+        Element * Name = &(FIELD)->items[_aiter];                            \
+        _aiter = (FIELD)->q.items[_aiter].next;                              \
+        LABlock                                                              \
+      }                                                                      \
+  } G_STMT_END
+#define SORTED_ARRAY_FOREACH_REVERSE(FIELD, Element, Name, LABlock)          \
+  G_STMT_START {                                                             \
+    for (glib_typeof((FIELD)->q.head) _aiter = (FIELD)->q.tail;              \
+         _aiter != VAL_QUEUE_INVALID(&(FIELD)->q);                           \
+         /* Do Nothing */)                                                   \
+      {                                                                      \
+        G_GNUC_UNUSED glib_typeof((FIELD)->q.head) _current = _aiter;        \
+        Element * Name = &(FIELD)->items[_aiter];                            \
+        _aiter = (FIELD)->q.items[_aiter].prev;                              \
+        LABlock                                                              \
+      }                                                                      \
+  } G_STMT_END
+#define SORTED_ARRAY_FOREACH_PEEK(FIELD)                                     \
+  (((FIELD)->q.items[_current].next != VAL_QUEUE_INVALID(&(FIELD)->q))       \
+    ? &(FIELD)->items[(FIELD)->q.items[_current].next] : NULL)
+#define SORTED_ARRAY_SPLIT(FIELD, SPLIT)                                     \
+  G_STMT_START {                                                             \
+    guint8 _mid;                                                             \
+                                                                             \
+    SORTED_ARRAY_INIT(SPLIT);                                                \
+                                                                             \
+    _mid = SORTED_ARRAY_LENGTH(FIELD) / 2;                                   \
+                                                                             \
+    for (guint8 _z = 0; _z < _mid; _z++)                                     \
+      {                                                                      \
+        glib_typeof((FIELD)->items[0]) ele;                                  \
+        SORTED_ARRAY_POP_TAIL(FIELD, ele);                                   \
+        SORTED_ARRAY_PUSH_HEAD(SPLIT, ele);                                  \
+      }                                                                      \
+  } G_STMT_END
+#define SORTED_ARRAY_SPLIT2(FIELD, LEFT, RIGHT)                              \
+  G_STMT_START {                                                             \
+    guint8 mid;                                                              \
+                                                                             \
+    SORTED_ARRAY_INIT(LEFT);                                                 \
+    SORTED_ARRAY_INIT(RIGHT);                                                \
+                                                                             \
+    mid = SORTED_ARRAY_LENGTH(FIELD) / 2;                                    \
+                                                                             \
+    for (guint8 i = 0; i < mid; i++)                                         \
+      {                                                                      \
+        glib_typeof((FIELD)->items[0]) ele;                                  \
+        SORTED_ARRAY_POP_TAIL(FIELD, ele);                                   \
+        SORTED_ARRAY_PUSH_HEAD(RIGHT, ele);                                  \
+      }                                                                      \
+                                                                             \
+    while (!SORTED_ARRAY_IS_EMPTY(FIELD))                                    \
+      {                                                                      \
+        glib_typeof((FIELD)->items[0]) ele;                                  \
+        SORTED_ARRAY_POP_TAIL(FIELD, ele);                                   \
+        SORTED_ARRAY_PUSH_HEAD(LEFT, ele);                                   \
+      }                                                                      \
+  } G_STMT_END
+#define SORTED_ARRAY_PEEK_HEAD(FIELD) ((FIELD)->items[VAL_QUEUE_PEEK_HEAD(&(FIELD)->q)])
+#define SORTED_ARRAY_POP_HEAD(FIELD,_ele) SORTED_ARRAY_REMOVE_INDEX(FIELD, 0, _ele)
+#define SORTED_ARRAY_POP_TAIL(FIELD,_ele) SORTED_ARRAY_REMOVE_INDEX(FIELD, SORTED_ARRAY_LENGTH(FIELD)-1, 
_ele)
+#define SORTED_ARRAY_PUSH_HEAD(FIELD, ele)                                   \
+  G_STMT_START {                                                             \
+    guint8 _pos = VAL_QUEUE_LENGTH(&(FIELD)->q);                             \
+    g_assert_cmpint (_pos, <, G_N_ELEMENTS ((FIELD)->items));                \
+    (FIELD)->items[_pos] = ele;                                              \
+    VAL_QUEUE_PUSH_HEAD(&(FIELD)->q, _pos);                                  \
+  } G_STMT_END
+#define SORTED_ARRAY_PUSH_TAIL(FIELD, ele)                                   \
+  G_STMT_START {                                                             \
+    guint8 _pos = VAL_QUEUE_LENGTH(&(FIELD)->q);                             \
+    g_assert_cmpint (_pos, <, G_N_ELEMENTS ((FIELD)->items));                \
+    (FIELD)->items[_pos] = ele;                                              \
+    VAL_QUEUE_PUSH_TAIL(&(FIELD)->q, _pos);                                  \
+  } G_STMT_END
+
+#define CJH_TEXT_REGION_MAX_BRANCHES 26
+#define CJH_TEXT_REGION_MIN_BRANCHES (CJH_TEXT_REGION_MAX_BRANCHES/3)
+#define CJH_TEXT_REGION_MAX_RUNS     26
+#define CJH_TEXT_REGION_MIN_RUNS     (CJH_TEXT_REGION_MAX_RUNS/3)
+
+typedef union  _CjhTextRegionNode   CjhTextRegionNode;
+typedef struct _CjhTextRegionBranch CjhTextRegionBranch;
+typedef struct _CjhTextRegionLeaf   CjhTextRegionLeaf;
+typedef struct _CjhTextRegionChild  CjhTextRegionChild;
+
+struct _CjhTextRegionChild
+{
+  CjhTextRegionNode *node;
+  gsize              length;
+};
+
+struct _CjhTextRegionBranch
+{
+  CjhTextRegionNode *tagged_parent;
+  CjhTextRegionNode *prev;
+  CjhTextRegionNode *next;
+  SORTED_ARRAY_FIELD (CjhTextRegionChild, CJH_TEXT_REGION_MAX_BRANCHES) children;
+};
+
+struct _CjhTextRegionLeaf
+{
+  CjhTextRegionNode *tagged_parent;
+  CjhTextRegionNode *prev;
+  CjhTextRegionNode *next;
+  SORTED_ARRAY_FIELD (CjhTextRegionRun, CJH_TEXT_REGION_MAX_RUNS) runs;
+};
+
+union _CjhTextRegionNode
+{
+  /* pointer to the parent, low bit 0x1 means leaf node */
+  CjhTextRegionNode *tagged_parent;
+  struct _CjhTextRegionLeaf leaf;
+  struct _CjhTextRegionBranch branch;
+};
+
+struct _CjhTextRegion
+{
+  CjhTextRegionNode root;
+  CjhTextRegionJoinFunc join_func;
+  CjhTextRegionSplitFunc split_func;
+  gsize length;
+  CjhTextRegionNode *cached_result;
+  gsize cached_result_offset;
+};
+
+#define TAG(ptr,val) GSIZE_TO_POINTER(GPOINTER_TO_SIZE(ptr)|(gsize)val)
+#define UNTAG(ptr)   GSIZE_TO_POINTER(GPOINTER_TO_SIZE(ptr) & ~(gsize)1)
+
+static inline CjhTextRegionNode *
+cjh_text_region_node_get_parent (CjhTextRegionNode *node)
+{
+  if (node == NULL)
+    return NULL;
+  return UNTAG (node->tagged_parent);
+}
+
+static inline gboolean
+cjh_text_region_node_is_leaf (CjhTextRegionNode *node)
+{
+  CjhTextRegionNode *parent = cjh_text_region_node_get_parent (node);
+
+  return parent != NULL && node->tagged_parent != parent;
+}
+
+static inline void
+cjh_text_region_node_set_parent (CjhTextRegionNode *node,
+                                 CjhTextRegionNode *parent)
+{
+  node->tagged_parent = TAG (parent, cjh_text_region_node_is_leaf (node));
+}
+
+static inline gsize
+cjh_text_region_node_length (CjhTextRegionNode *node)
+{
+  gsize length = 0;
+
+  g_assert (node != NULL);
+
+  if (cjh_text_region_node_is_leaf (node))
+    {
+      SORTED_ARRAY_FOREACH (&node->leaf.runs, CjhTextRegionRun, run, {
+        length += run->length;
+      });
+    }
+  else
+    {
+      SORTED_ARRAY_FOREACH (&node->branch.children, CjhTextRegionChild, child, {
+        length += child->length;
+      });
+    }
+
+  return length;
+}
+
+static inline CjhTextRegionNode *
+_cjh_text_region_get_first_leaf (CjhTextRegion *self)
+{
+  for (CjhTextRegionNode *iter = &self->root;
+       iter;
+       iter = SORTED_ARRAY_PEEK_HEAD (&iter->branch.children).node)
+    {
+      if (cjh_text_region_node_is_leaf (iter))
+        return iter;
+    }
+
+  g_assert_not_reached ();
+}
+
+G_END_DECLS
+
+#endif /* __CJH_TEXT_REGION_BTREE_H__ */
diff --git a/src/plugins/spellcheck/cjhtextregionprivate.h b/src/plugins/spellcheck/cjhtextregionprivate.h
new file mode 100644
index 000000000..d1f16bf3f
--- /dev/null
+++ b/src/plugins/spellcheck/cjhtextregionprivate.h
@@ -0,0 +1,121 @@
+/* cjhtextregionprivate.h
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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 2.1 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef __CJH_TEXT_REGION_PRIVATE_H__
+#define __CJH_TEXT_REGION_PRIVATE_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+typedef struct _CjhTextRegion CjhTextRegion;
+
+typedef struct _CjhTextRegionRun
+{
+  gsize length;
+  gpointer data;
+} CjhTextRegionRun;
+
+/*
+ * CjhTextRegionForeachFunc:
+ * @offset: the offset in characters within the text region
+ * @run: the run of text and data pointer
+ * @user_data: user data supplied
+ *
+ * Function callback to iterate through runs within a text region.
+ *
+ * Returns: %FALSE to coninue iteration, otherwise %TRUE to stop.
+ */
+typedef gboolean (*CjhTextRegionForeachFunc) (gsize                   offset,
+                                              const CjhTextRegionRun *run,
+                                              gpointer                user_data);
+
+/*
+ * CjhTextRegionJoinFunc:
+ *
+ * This callback is used to determine if two runs can be joined together.
+ * This is useful when you have similar data pointers between two runs
+ * and seeing them as one run is irrelevant to the code using the
+ * text region.
+ *
+ * The default calllback for joining will return %FALSE so that no joins
+ * may occur.
+ *
+ * Returns: %TRUE if the runs can be joined; otherwise %FALSE
+ */
+typedef gboolean (*CjhTextRegionJoinFunc) (gsize                   offset,
+                                           const CjhTextRegionRun *left,
+                                           const CjhTextRegionRun *right);
+
+/*
+ * CjhTextRegionSplitFunc:
+ *
+ * This function is responsible for splitting a run into two runs.
+ * This can happen a delete happens in the middle of a run.
+ *
+ * By default, @left will contain the run prior to the delete, and
+ * @right will contain the run after the delete.
+ *
+ * You can use the run lengths to determine where the delete was made
+ * using @offset which is an absolute offset from the beginning of the
+ * region.
+ *
+ * If you would like to keep a single run after the deletion, then
+ * set @right to contain a length of zero and add it's previous
+ * length to @left.
+ *
+ * All the length in @left and @right must be accounted for.
+ *
+ * This function is useful when using CjhTextRegion as a piecetable
+ * where you want to adjust the data pointer to point at a new
+ * section of an original or change buffer.
+ */
+typedef void (*CjhTextRegionSplitFunc)     (gsize                     offset,
+                                            const CjhTextRegionRun   *run,
+                                            CjhTextRegionRun         *left,
+                                            CjhTextRegionRun         *right);
+
+CjhTextRegion *_cjh_text_region_new              (CjhTextRegionJoinFunc     join_func,
+                                                  CjhTextRegionSplitFunc    split_func);
+void           _cjh_text_region_insert           (CjhTextRegion            *region,
+                                                  gsize                     offset,
+                                                  gsize                     length,
+                                                  gpointer                  data);
+void           _cjh_text_region_replace          (CjhTextRegion            *region,
+                                                  gsize                     offset,
+                                                  gsize                     length,
+                                                  gpointer                  data);
+void           _cjh_text_region_remove           (CjhTextRegion            *region,
+                                                  gsize                     offset,
+                                                  gsize                     length);
+guint          _cjh_text_region_get_length       (CjhTextRegion            *region);
+void           _cjh_text_region_foreach          (CjhTextRegion            *region,
+                                                  CjhTextRegionForeachFunc  func,
+                                                  gpointer                  user_data);
+void           _cjh_text_region_foreach_in_range (CjhTextRegion            *region,
+                                                  gsize                     begin,
+                                                  gsize                     end,
+                                                  CjhTextRegionForeachFunc  func,
+                                                  gpointer                  user_data);
+void           _cjh_text_region_free             (CjhTextRegion            *region);
+
+G_END_DECLS
+
+#endif /* __CJH_TEXT_REGION_PRIVATE_H__ */
diff --git a/src/plugins/spellcheck/gbp-spell-language-popover.h 
b/src/plugins/spellcheck/editor-empty-spell-provider-private.h
similarity index 64%
rename from src/plugins/spellcheck/gbp-spell-language-popover.h
rename to src/plugins/spellcheck/editor-empty-spell-provider-private.h
index 35458fbe6..3526c4b4b 100644
--- a/src/plugins/spellcheck/gbp-spell-language-popover.h
+++ b/src/plugins/spellcheck/editor-empty-spell-provider-private.h
@@ -1,6 +1,6 @@
-/* gbp-spell-language-popover.h
+/* editor-empty-spell-provider-private.h
  *
- * Copyright 2017 Sébastien Lafargue <slafargue gnome org>
+ * Copyright 2022 Christian Hergert <chergert redhat com>
  *
  * 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
@@ -20,14 +20,14 @@
 
 #pragma once
 
-#include <gspell/gspell.h>
+#include "editor-spell-provider.h"
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_SPELL_LANGUAGE_POPOVER (gbp_spell_language_popover_get_type())
+#define EDITOR_TYPE_EMPTY_SPELL_PROVIDER (editor_empty_spell_provider_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpSpellLanguagePopover, gbp_spell_language_popover, GBP, SPELL_LANGUAGE_POPOVER, 
GtkButton)
+G_DECLARE_FINAL_TYPE (EditorEmptySpellProvider, editor_empty_spell_provider, EDITOR, EMPTY_SPELL_PROVIDER, 
EditorSpellProvider)
 
-GbpSpellLanguagePopover *gbp_spell_language_popover_new (const GspellLanguage *language);
+EditorSpellProvider *editor_empty_spell_provider_new (void);
 
 G_END_DECLS
diff --git a/src/plugins/spellcheck/editor-empty-spell-provider.c 
b/src/plugins/spellcheck/editor-empty-spell-provider.c
new file mode 100644
index 000000000..897941e68
--- /dev/null
+++ b/src/plugins/spellcheck/editor-empty-spell-provider.c
@@ -0,0 +1,71 @@
+/* editor-empty-spell-provider.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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
+ */
+
+#include "config.h"
+
+#include "editor-empty-spell-provider-private.h"
+
+struct _EditorEmptySpellProvider
+{
+  EditorSpellProvider parent_instance;
+};
+
+G_DEFINE_FINAL_TYPE (EditorEmptySpellProvider, editor_empty_spell_provider, EDITOR_TYPE_SPELL_PROVIDER)
+
+EditorSpellProvider *
+editor_empty_spell_provider_new (void)
+{
+  return g_object_new (EDITOR_TYPE_EMPTY_SPELL_PROVIDER, NULL);
+}
+
+static GPtrArray *
+empty_list_languages (EditorSpellProvider *provider)
+{
+  return g_ptr_array_new_with_free_func (g_object_unref);
+}
+
+static EditorSpellLanguage *
+empty_get_language (EditorSpellProvider *provider,
+                    const char          *language)
+{
+  return NULL;
+}
+
+static gboolean
+empty_supports_language (EditorSpellProvider *provider,
+                         const char          *language)
+{
+  return FALSE;
+}
+
+static void
+editor_empty_spell_provider_class_init (EditorEmptySpellProviderClass *klass)
+{
+  EditorSpellProviderClass *provider_class = EDITOR_SPELL_PROVIDER_CLASS (klass);
+
+  provider_class->list_languages = empty_list_languages;
+  provider_class->get_language = empty_get_language;
+  provider_class->supports_language = empty_supports_language;
+}
+
+static void
+editor_empty_spell_provider_init (EditorEmptySpellProvider *self)
+{
+}
diff --git a/src/plugins/spellcheck/editor-enchant-spell-language.c 
b/src/plugins/spellcheck/editor-enchant-spell-language.c
new file mode 100644
index 000000000..1717cbbd3
--- /dev/null
+++ b/src/plugins/spellcheck/editor-enchant-spell-language.c
@@ -0,0 +1,306 @@
+/* editor-enchant-spell-language.c
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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
+ */
+
+#include "config.h"
+
+#include <enchant.h>
+
+#include "editor-enchant-spell-language.h"
+
+struct _EditorEnchantSpellLanguage
+{
+  EditorSpellLanguage parent_instance;
+  PangoLanguage *language;
+  EnchantDict *native;
+  char *extra_word_chars;
+};
+
+G_DEFINE_TYPE (EditorEnchantSpellLanguage, editor_enchant_spell_language, EDITOR_TYPE_SPELL_LANGUAGE)
+
+enum {
+  PROP_0,
+  PROP_NATIVE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * editor_enchant_spell_language_new:
+ *
+ * Create a new #EditorEnchantSpellLanguage.
+ *
+ * Returns: (transfer full): a newly created #EditorEnchantSpellLanguage
+ */
+EditorSpellLanguage *
+editor_enchant_spell_language_new (const char *code,
+                                   gpointer    native)
+{
+  return g_object_new (EDITOR_TYPE_ENCHANT_SPELL_LANGUAGE,
+                       "code", code,
+                       "native", native,
+                       NULL);
+}
+
+static gboolean
+editor_enchant_spell_language_contains_word (EditorSpellLanguage *language,
+                                             const char          *word,
+                                             gssize               word_len)
+{
+  EditorEnchantSpellLanguage *self = (EditorEnchantSpellLanguage *)language;
+
+  g_assert (EDITOR_IS_ENCHANT_SPELL_LANGUAGE (self));
+  g_assert (word != NULL);
+  g_assert (word_len > 0);
+
+  return enchant_dict_check (self->native, word, word_len) == 0;
+}
+
+static char **
+editor_enchant_spell_language_list_corrections (EditorSpellLanguage *language,
+                                                const char          *word,
+                                                gssize               word_len)
+{
+  EditorEnchantSpellLanguage *self = (EditorEnchantSpellLanguage *)language;
+  size_t count = 0;
+  char **tmp;
+  char **ret = NULL;
+
+  g_assert (EDITOR_IS_ENCHANT_SPELL_LANGUAGE (self));
+  g_assert (word != NULL);
+  g_assert (word_len > 0);
+
+  if ((tmp = enchant_dict_suggest (self->native, word, word_len, &count)) && count > 0)
+    {
+      ret = g_strdupv (tmp);
+      enchant_dict_free_string_list (self->native, tmp);
+    }
+
+  return g_steal_pointer (&ret);
+}
+
+static char **
+editor_enchant_spell_language_split (EditorEnchantSpellLanguage *self,
+                                     const char                 *words)
+{
+  PangoLogAttr *attrs;
+  GArray *ar;
+  gsize n_chars;
+
+  g_assert (EDITOR_IS_ENCHANT_SPELL_LANGUAGE (self));
+
+  if (words == NULL || self->language == NULL)
+    return NULL;
+
+  /* We don't care about splitting obnoxious stuff */
+  if ((n_chars = g_utf8_strlen (words, -1)) > 1024)
+    return NULL;
+
+  attrs = g_newa (PangoLogAttr, n_chars + 1);
+  pango_get_log_attrs (words, -1, -1, self->language, attrs, n_chars + 1);
+
+  ar = g_array_new (TRUE, FALSE, sizeof (char*));
+
+  for (gsize i = 0; i < n_chars + 1; i++)
+    {
+      if (attrs[i].is_word_start)
+        {
+          for (gsize j = i + 1; j < n_chars + 1; j++)
+            {
+              if (attrs[j].is_word_end)
+                {
+                  char *substr = g_utf8_substring (words, i, j);
+                  g_array_append_val (ar, substr);
+                  i = j;
+                  break;
+                }
+            }
+        }
+    }
+
+  return (char **)(gpointer)g_array_free (ar, FALSE);
+}
+
+static void
+editor_enchant_spell_language_add_all_to_session (EditorEnchantSpellLanguage *self,
+                                                  const char * const         *words)
+{
+  g_assert (EDITOR_IS_ENCHANT_SPELL_LANGUAGE (self));
+
+  if (words == NULL || words[0] == NULL)
+    return;
+
+  for (guint i = 0; words[i]; i++)
+    enchant_dict_add_to_session (self->native, words[i], -1);
+}
+
+static void
+editor_enchant_spell_language_add_word (EditorSpellLanguage *language,
+                                        const char          *word)
+{
+  EditorEnchantSpellLanguage *self = (EditorEnchantSpellLanguage *)language;
+
+  g_assert (EDITOR_IS_SPELL_LANGUAGE (language));
+  g_assert (word != NULL);
+
+  enchant_dict_add (self->native, word, -1);
+}
+
+static void
+editor_enchant_spell_language_ignore_word (EditorSpellLanguage *language,
+                                           const char          *word)
+{
+  EditorEnchantSpellLanguage *self = (EditorEnchantSpellLanguage *)language;
+
+  g_assert (EDITOR_IS_SPELL_LANGUAGE (language));
+  g_assert (word != NULL);
+
+  enchant_dict_add_to_session (self->native, word, -1);
+}
+
+static const char *
+editor_enchant_spell_language_get_extra_word_chars (EditorSpellLanguage *language)
+{
+  EditorEnchantSpellLanguage *self = (EditorEnchantSpellLanguage *)language;
+
+  g_assert (EDITOR_IS_SPELL_LANGUAGE (language));
+
+  return self->extra_word_chars;
+}
+
+static void
+editor_enchant_spell_language_constructed (GObject *object)
+{
+  EditorEnchantSpellLanguage *self = (EditorEnchantSpellLanguage *)object;
+  g_auto(GStrv) split = NULL;
+  const char *extra_word_chars;
+  const char *code;
+
+  g_assert (EDITOR_IS_ENCHANT_SPELL_LANGUAGE (self));
+
+  G_OBJECT_CLASS (editor_enchant_spell_language_parent_class)->constructed (object);
+
+  code = editor_spell_language_get_code (EDITOR_SPELL_LANGUAGE (self));
+  self->language = pango_language_from_string (code);
+
+  if ((split = editor_enchant_spell_language_split (self, g_get_real_name ())))
+    editor_enchant_spell_language_add_all_to_session (self, (const char * const *)split);
+
+  if ((extra_word_chars = enchant_dict_get_extra_word_characters (self->native)))
+    {
+      const char *end_pos = NULL;
+
+      /* Sometimes we get invalid UTF-8 from enchant, so handle that directly.
+       * In particular, the data seems corrupted from Fedora.
+       */
+      if (g_utf8_validate (extra_word_chars, -1, &end_pos))
+        self->extra_word_chars = g_strdup (extra_word_chars);
+      else
+        self->extra_word_chars = g_strndup (extra_word_chars, end_pos - extra_word_chars);
+    }
+}
+
+static void
+editor_enchant_spell_language_finalize (GObject *object)
+{
+  EditorEnchantSpellLanguage *self = (EditorEnchantSpellLanguage *)object;
+
+  /* Owned by provider */
+  self->native = NULL;
+
+  G_OBJECT_CLASS (editor_enchant_spell_language_parent_class)->finalize (object);
+}
+
+static void
+editor_enchant_spell_language_get_property (GObject    *object,
+                                            guint       prop_id,
+                                            GValue     *value,
+                                            GParamSpec *pspec)
+{
+  EditorEnchantSpellLanguage *self = EDITOR_ENCHANT_SPELL_LANGUAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_NATIVE:
+      g_value_set_pointer (value, self->native);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+editor_enchant_spell_language_set_property (GObject      *object,
+                                            guint         prop_id,
+                                            const GValue *value,
+                                            GParamSpec   *pspec)
+{
+  EditorEnchantSpellLanguage *self = EDITOR_ENCHANT_SPELL_LANGUAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_NATIVE:
+      self->native = g_value_get_pointer (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+editor_enchant_spell_language_class_init (EditorEnchantSpellLanguageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  EditorSpellLanguageClass *spell_language_class = EDITOR_SPELL_LANGUAGE_CLASS (klass);
+
+  object_class->constructed = editor_enchant_spell_language_constructed;
+  object_class->finalize = editor_enchant_spell_language_finalize;
+  object_class->get_property = editor_enchant_spell_language_get_property;
+  object_class->set_property = editor_enchant_spell_language_set_property;
+
+  spell_language_class->contains_word = editor_enchant_spell_language_contains_word;
+  spell_language_class->list_corrections = editor_enchant_spell_language_list_corrections;
+  spell_language_class->add_word = editor_enchant_spell_language_add_word;
+  spell_language_class->ignore_word = editor_enchant_spell_language_ignore_word;
+  spell_language_class->get_extra_word_chars = editor_enchant_spell_language_get_extra_word_chars;
+
+  properties [PROP_NATIVE] =
+    g_param_spec_pointer ("native",
+                          "Native",
+                          "The native enchant dictionary",
+                          (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+editor_enchant_spell_language_init (EditorEnchantSpellLanguage *self)
+{
+}
+
+gpointer
+editor_enchant_spell_language_get_native (EditorEnchantSpellLanguage *self)
+{
+  g_return_val_if_fail (EDITOR_IS_ENCHANT_SPELL_LANGUAGE (self), NULL);
+
+  return self->native;
+}
diff --git a/src/plugins/spellcheck/gbp-spell-navigator.h 
b/src/plugins/spellcheck/editor-enchant-spell-language.h
similarity index 51%
rename from src/plugins/spellcheck/gbp-spell-navigator.h
rename to src/plugins/spellcheck/editor-enchant-spell-language.h
index 1964e579f..4b0d0f62b 100644
--- a/src/plugins/spellcheck/gbp-spell-navigator.h
+++ b/src/plugins/spellcheck/editor-enchant-spell-language.h
@@ -1,6 +1,6 @@
-/* gbp-spell-navigator.h
+/* editor-enchant-spell-language.h
  *
- * Copyright 2016 Sebastien Lafargue <slafargue gnome org>
+ * Copyright 2021 Christian Hergert <chergert redhat com>
  *
  * 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
@@ -20,18 +20,16 @@
 
 #pragma once
 
-#include <gspell/gspell.h>
+#include "editor-spell-language.h"
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_SPELL_NAVIGATOR (gbp_spell_navigator_get_type())
+#define EDITOR_TYPE_ENCHANT_SPELL_LANGUAGE (editor_enchant_spell_language_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpSpellNavigator, gbp_spell_navigator, GBP, SPELL_NAVIGATOR, GInitiallyUnowned)
+G_DECLARE_FINAL_TYPE (EditorEnchantSpellLanguage, editor_enchant_spell_language, EDITOR, 
ENCHANT_SPELL_LANGUAGE, EditorSpellLanguage)
 
-GspellNavigator *gbp_spell_navigator_new                   (GtkTextView       *view);
-guint            gbp_spell_navigator_get_count             (GbpSpellNavigator *self,
-                                                            const gchar       *word);
-gboolean         gbp_spell_navigator_get_is_words_counted  (GbpSpellNavigator *self);
-gboolean         gbp_spell_navigator_goto_word_start       (GbpSpellNavigator *self);
+EditorSpellLanguage *editor_enchant_spell_language_new        (const char                 *code,
+                                                               gpointer                    native);
+gpointer             editor_enchant_spell_language_get_native (EditorEnchantSpellLanguage *self);
 
 G_END_DECLS
diff --git a/src/plugins/spellcheck/editor-enchant-spell-provider.c 
b/src/plugins/spellcheck/editor-enchant-spell-provider.c
new file mode 100644
index 000000000..fb722c57b
--- /dev/null
+++ b/src/plugins/spellcheck/editor-enchant-spell-provider.c
@@ -0,0 +1,171 @@
+/* editor-enchant-spell-provider.c
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <enchant.h>
+#include <locale.h>
+#include <unicode/uloc.h>
+
+#include "editor-spell-language-info.h"
+
+#include "editor-enchant-spell-language.h"
+#include "editor-enchant-spell-provider.h"
+
+struct _EditorEnchantSpellProvider
+{
+  EditorSpellProvider parent_instance;
+};
+
+G_DEFINE_TYPE (EditorEnchantSpellProvider, editor_enchant_spell_provider, EDITOR_TYPE_SPELL_PROVIDER)
+
+static GHashTable *languages;
+
+static EnchantBroker *
+get_broker (void)
+{
+  static EnchantBroker *broker;
+
+  if (broker == NULL)
+    broker = enchant_broker_init ();
+
+  return broker;
+}
+
+static char *
+get_display_name (const char *code)
+{
+  const char * const *names = g_get_language_names ();
+
+  for (guint i = 0; names[i]; i++)
+    {
+      UChar ret[256];
+      UErrorCode status = U_ZERO_ERROR;
+
+      ret[0] = 0;
+      uloc_getDisplayName (code, names[i], ret, G_N_ELEMENTS (ret), &status);
+      ret[G_N_ELEMENTS (ret)-1] = 0;
+
+      if (status == U_ZERO_ERROR && ret[0] != 0)
+        {
+          GString *str = g_string_new (NULL);
+
+          for (guint j = 0; ret[j]; j++)
+            g_string_append_unichar (str, ret[j]);
+
+          return g_string_free (str, FALSE);
+        }
+    }
+
+  return NULL;
+}
+
+/**
+ * editor_enchant_spell_provider_new:
+ *
+ * Create a new #EditorEnchantSpellProvider.
+ *
+ * Returns: (transfer full): a newly created #EditorEnchantSpellProvider
+ */
+EditorSpellProvider *
+editor_enchant_spell_provider_new (void)
+{
+  return g_object_new (EDITOR_TYPE_ENCHANT_SPELL_PROVIDER,
+                       "display-name", _("Enchant 2"),
+                       NULL);
+}
+
+static gboolean
+editor_enchant_spell_provider_supports_language (EditorSpellProvider *provider,
+                                                 const char          *language)
+{
+  g_assert (EDITOR_IS_ENCHANT_SPELL_PROVIDER (provider));
+  g_assert (language != NULL);
+
+  return enchant_broker_dict_exists (get_broker (), language);
+}
+
+static void
+list_languages_cb (const char * const  lang_tag,
+                   const char * const  provider_name,
+                   const char * const  provider_desc,
+                   const char * const  provider_file,
+                   void               *user_data)
+{
+  GPtrArray *ar = user_data;
+  char *name = get_display_name (lang_tag);
+
+  if (name != NULL)
+    {
+      g_ptr_array_add (ar, editor_spell_language_info_new (name, lang_tag));
+      g_free (name);
+    }
+}
+
+static GPtrArray *
+editor_enchant_spell_provider_list_languages (EditorSpellProvider *provider)
+{
+  EnchantBroker *broker = get_broker ();
+  GPtrArray *ar = g_ptr_array_new_with_free_func (g_object_unref);
+  enchant_broker_list_dicts (broker, list_languages_cb, ar);
+  return ar;
+}
+
+static EditorSpellLanguage *
+editor_enchant_spell_provider_get_language (EditorSpellProvider *provider,
+                                            const char          *language)
+{
+  EditorSpellLanguage *ret;
+
+  g_assert (EDITOR_IS_ENCHANT_SPELL_PROVIDER (provider));
+  g_assert (language != NULL);
+
+  if (languages == NULL)
+    languages = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_object_unref);
+
+  if (!(ret = g_hash_table_lookup (languages, language)))
+    {
+      EnchantDict *dict = enchant_broker_request_dict (get_broker (), language);
+
+      if (dict == NULL)
+        return NULL;
+
+      ret = editor_enchant_spell_language_new (language, dict);
+      g_hash_table_insert (languages, (char *)g_intern_string (language), ret);
+    }
+
+  return ret ? g_object_ref (ret) : NULL;
+}
+
+static void
+editor_enchant_spell_provider_class_init (EditorEnchantSpellProviderClass *klass)
+{
+  EditorSpellProviderClass *spell_provider_class = EDITOR_SPELL_PROVIDER_CLASS (klass);
+
+  spell_provider_class->supports_language = editor_enchant_spell_provider_supports_language;
+  spell_provider_class->list_languages = editor_enchant_spell_provider_list_languages;
+  spell_provider_class->get_language = editor_enchant_spell_provider_get_language;
+}
+
+static void
+editor_enchant_spell_provider_init (EditorEnchantSpellProvider *self)
+{
+}
diff --git a/src/plugins/spellcheck/gbp-spell-widget.h 
b/src/plugins/spellcheck/editor-enchant-spell-provider.h
similarity index 58%
rename from src/plugins/spellcheck/gbp-spell-widget.h
rename to src/plugins/spellcheck/editor-enchant-spell-provider.h
index 74372159c..8ae6c63e8 100644
--- a/src/plugins/spellcheck/gbp-spell-widget.h
+++ b/src/plugins/spellcheck/editor-enchant-spell-provider.h
@@ -1,6 +1,6 @@
-/* gbp-spell-widget.h
+/* editor-enchant-spell-provider.h
  *
- * Copyright 2016 Sebastien Lafargue <slafargue gnome org>
+ * Copyright 2021 Christian Hergert <chergert redhat com>
  *
  * 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
@@ -20,17 +20,14 @@
 
 #pragma once
 
-#include <libide-editor.h>
+#include "editor-spell-provider.h"
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_SPELL_WIDGET (gbp_spell_widget_get_type())
+#define EDITOR_TYPE_ENCHANT_SPELL_PROVIDER (editor_enchant_spell_provider_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpSpellWidget, gbp_spell_widget, GBP, SPELL_WIDGET, GtkBin)
+G_DECLARE_FINAL_TYPE (EditorEnchantSpellProvider, editor_enchant_spell_provider, EDITOR, 
ENCHANT_SPELL_PROVIDER, EditorSpellProvider)
 
-GtkWidget     *gbp_spell_widget_new        (IdeEditorPage  *editor);
-IdeEditorPage *gbp_spell_widget_get_editor (GbpSpellWidget *self);
-void           gbp_spell_widget_set_editor (GbpSpellWidget *self,
-                                            IdeEditorPage  *editor);
+EditorSpellProvider *editor_enchant_spell_provider_new (void);
 
 G_END_DECLS
diff --git a/src/plugins/spellcheck/editor-spell-checker.c b/src/plugins/spellcheck/editor-spell-checker.c
new file mode 100644
index 000000000..18f5605b1
--- /dev/null
+++ b/src/plugins/spellcheck/editor-spell-checker.c
@@ -0,0 +1,324 @@
+/* editor-spell-checker.c
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include "editor-spell-checker.h"
+#include "editor-spell-language.h"
+#include "editor-spell-provider.h"
+
+struct _EditorSpellChecker
+{
+  GObject              parent_instance;
+  EditorSpellProvider *provider;
+  EditorSpellLanguage *language;
+};
+
+G_DEFINE_TYPE (EditorSpellChecker, editor_spell_checker, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_LANGUAGE,
+  PROP_PROVIDER,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * editor_spell_checker_new:
+ *
+ * Create a new #EditorSpellChecker.
+ *
+ * Returns: (transfer full): a newly created #EditorSpellChecker
+ */
+EditorSpellChecker *
+editor_spell_checker_new (EditorSpellProvider *provider,
+                          const char          *language)
+{
+  g_return_val_if_fail (EDITOR_IS_SPELL_PROVIDER (provider) || !provider, NULL);
+
+  if (provider == NULL)
+    provider = editor_spell_provider_get_default ();
+
+  if (language == NULL)
+    language = editor_spell_provider_get_default_code (provider);
+
+  return g_object_new (EDITOR_TYPE_SPELL_CHECKER,
+                       "provider", provider,
+                       "language", language,
+                       NULL);
+}
+
+static void
+editor_spell_checker_constructed (GObject *object)
+{
+  EditorSpellChecker *self = (EditorSpellChecker *)object;
+
+  g_assert (EDITOR_IS_SPELL_CHECKER (self));
+
+  G_OBJECT_CLASS (editor_spell_checker_parent_class)->constructed (object);
+
+  if (self->provider == NULL)
+    self->provider = editor_spell_provider_get_default ();
+}
+
+static void
+editor_spell_checker_finalize (GObject *object)
+{
+  EditorSpellChecker *self = (EditorSpellChecker *)object;
+
+  g_clear_object (&self->provider);
+  g_clear_object (&self->language);
+
+  G_OBJECT_CLASS (editor_spell_checker_parent_class)->finalize (object);
+}
+
+static void
+editor_spell_checker_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  EditorSpellChecker *self = EDITOR_SPELL_CHECKER (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROVIDER:
+      g_value_set_object (value, editor_spell_checker_get_provider (self));
+      break;
+
+    case PROP_LANGUAGE:
+      g_value_set_string (value, editor_spell_checker_get_language (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+editor_spell_checker_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  EditorSpellChecker *self = EDITOR_SPELL_CHECKER (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROVIDER:
+      self->provider = g_value_dup_object (value);
+      break;
+
+    case PROP_LANGUAGE:
+      editor_spell_checker_set_language (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+editor_spell_checker_class_init (EditorSpellCheckerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = editor_spell_checker_constructed;
+  object_class->finalize = editor_spell_checker_finalize;
+  object_class->get_property = editor_spell_checker_get_property;
+  object_class->set_property = editor_spell_checker_set_property;
+
+  /**
+   * EditorSpellChecker:language:
+   *
+   * The "language" to use when checking words with the configured
+   * #EditorSpellProvider. For example, `en_US`.
+   */
+  properties [PROP_LANGUAGE] =
+    g_param_spec_string ("language",
+                         "Language",
+                         "The language code",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * EditorSpellChecker:provider:
+   *
+   * The "provider" property contains the provider that is providing
+   * information to the spell checker.
+   *
+   * Currently, only Enchant is supported, and requires using the
+   * #EditorEnchantSpellProvider. Setting this to %NULL will get
+   * the default provider.
+   */
+  properties [PROP_PROVIDER] =
+    g_param_spec_object ("provider",
+                         "Provider",
+                         "The spell check provider",
+                         EDITOR_TYPE_SPELL_PROVIDER,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+editor_spell_checker_init (EditorSpellChecker *self)
+{
+}
+
+/**
+ * editor_spell_checker_get_language:
+ *
+ * Gets the language being used by the spell checker.
+ *
+ * Returns: (nullable): a string describing the current language.
+ */
+const char *
+editor_spell_checker_get_language (EditorSpellChecker *self)
+{
+  g_return_val_if_fail (EDITOR_IS_SPELL_CHECKER (self), NULL);
+
+  return self->language ? editor_spell_language_get_code (self->language) : NULL;
+}
+
+/**
+ * editor_spell_checker_set_language:
+ * @self: an #EditorSpellChecker
+ * @language: the language to use
+ *
+ * Sets the language code to use when communicating with the provider,
+ * such as `en_US`.
+ */
+void
+editor_spell_checker_set_language (EditorSpellChecker *self,
+                                   const char         *language)
+{
+  g_return_if_fail (EDITOR_IS_SPELL_CHECKER (self));
+
+  if (g_strcmp0 (language, editor_spell_checker_get_language (self)) != 0)
+    {
+      self->language = editor_spell_provider_get_language (self->provider, language);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LANGUAGE]);
+    }
+}
+
+/**
+ * editor_spell_checker_get_provider:
+ *
+ * Gets the spell provider used by the spell checker.
+ *
+ * Currently, only Enchant-2 is supported.
+ *
+ * Returns: (transfer none) (not nullable): an #EditorSpellProvider
+ */
+EditorSpellProvider *
+editor_spell_checker_get_provider (EditorSpellChecker *self)
+{
+  g_return_val_if_fail (EDITOR_IS_SPELL_CHECKER (self), NULL);
+
+  return self->provider;
+}
+
+static inline gboolean
+word_is_number (const char *word,
+                gssize      word_len)
+{
+  g_assert (word_len > 0);
+
+  for (gssize i = 0; i < word_len; i++)
+    {
+      if (word[i] < '0' || word[i] > '9')
+        return FALSE;
+    }
+
+  return TRUE;
+}
+
+gboolean
+editor_spell_checker_check_word (EditorSpellChecker *self,
+                                 const char         *word,
+                                 gssize              word_len)
+{
+  g_return_val_if_fail (EDITOR_IS_SPELL_CHECKER (self), FALSE);
+
+  if (word == NULL || word_len == 0)
+    return FALSE;
+
+  if (self->language == NULL)
+    return TRUE;
+
+  if (word_len < 0)
+    word_len = strlen (word);
+
+  if (word_is_number (word, word_len))
+    return TRUE;
+
+  return editor_spell_language_contains_word (self->language, word, word_len);
+}
+
+char **
+editor_spell_checker_list_corrections (EditorSpellChecker *self,
+                                       const char         *word)
+{
+  g_return_val_if_fail (EDITOR_IS_SPELL_CHECKER (self), NULL);
+  g_return_val_if_fail (word != NULL, NULL);
+
+  if (self->language == NULL)
+    return NULL;
+
+  return editor_spell_language_list_corrections (self->language, word, -1);
+}
+
+void
+editor_spell_checker_add_word (EditorSpellChecker *self,
+                               const char         *word)
+{
+  g_return_if_fail (EDITOR_IS_SPELL_CHECKER (self));
+  g_return_if_fail (word != NULL);
+
+  if (self->language != NULL)
+    editor_spell_language_add_word (self->language, word);
+}
+
+void
+editor_spell_checker_ignore_word (EditorSpellChecker *self,
+                                  const char         *word)
+{
+  g_return_if_fail (EDITOR_IS_SPELL_CHECKER (self));
+  g_return_if_fail (word != NULL);
+
+  if (self->language != NULL)
+    editor_spell_language_ignore_word (self->language, word);
+}
+
+const char *
+editor_spell_checker_get_extra_word_chars (EditorSpellChecker *self)
+{
+  g_return_val_if_fail (EDITOR_IS_SPELL_CHECKER (self), NULL);
+
+  if (self->language != NULL)
+    return editor_spell_language_get_extra_word_chars (self->language);
+
+  return "";
+}
diff --git a/src/plugins/spellcheck/editor-spell-checker.h b/src/plugins/spellcheck/editor-spell-checker.h
new file mode 100644
index 000000000..94c94df18
--- /dev/null
+++ b/src/plugins/spellcheck/editor-spell-checker.h
@@ -0,0 +1,48 @@
+/* editor-spell-checker.h
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include "editor-types.h"
+
+G_BEGIN_DECLS
+
+#define EDITOR_TYPE_SPELL_CHECKER (editor_spell_checker_get_type())
+
+G_DECLARE_FINAL_TYPE (EditorSpellChecker, editor_spell_checker, EDITOR, SPELL_CHECKER, GObject)
+
+EditorSpellChecker   *editor_spell_checker_new                  (EditorSpellProvider *provider,
+                                                                 const char          *language);
+EditorSpellProvider  *editor_spell_checker_get_provider         (EditorSpellChecker  *self);
+const char           *editor_spell_checker_get_language         (EditorSpellChecker  *self);
+void                  editor_spell_checker_set_language         (EditorSpellChecker  *self,
+                                                                 const char          *language);
+gboolean              editor_spell_checker_check_word           (EditorSpellChecker  *self,
+                                                                 const char          *word,
+                                                                 gssize               word_len);
+char                **editor_spell_checker_list_corrections     (EditorSpellChecker  *self,
+                                                                 const char          *word);
+void                  editor_spell_checker_add_word             (EditorSpellChecker  *self,
+                                                                 const char          *word);
+void                  editor_spell_checker_ignore_word          (EditorSpellChecker  *self,
+                                                                 const char          *word);
+const char           *editor_spell_checker_get_extra_word_chars (EditorSpellChecker  *self);
+
+G_END_DECLS
diff --git a/src/plugins/spellcheck/editor-spell-cursor.c b/src/plugins/spellcheck/editor-spell-cursor.c
new file mode 100644
index 000000000..26bbd644b
--- /dev/null
+++ b/src/plugins/spellcheck/editor-spell-cursor.c
@@ -0,0 +1,365 @@
+/* editor-spell-cursor.c
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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
+ */
+
+#include "config.h"
+
+#include "cjhtextregionprivate.h"
+#include "editor-spell-cursor.h"
+
+#define RUN_UNCHECKED NULL
+
+typedef struct
+{
+  CjhTextRegion *region;
+  GtkTextBuffer *buffer;
+  gssize pos;
+} RegionIter;
+
+typedef struct
+{
+  GtkTextBuffer *buffer;
+  GtkTextTag *tag;
+  GtkTextIter pos;
+} TagIter;
+
+typedef struct
+{
+  GtkTextBuffer *buffer;
+  GtkTextIter word_begin;
+  GtkTextIter word_end;
+} WordIter;
+
+struct _EditorSpellCursor
+{
+  RegionIter region;
+  TagIter tag;
+  WordIter word;
+  const char *extra_word_chars;
+};
+
+static void
+region_iter_init (RegionIter    *self,
+                  GtkTextBuffer *buffer,
+                  CjhTextRegion *region)
+{
+  self->region = region;
+  self->buffer = buffer;
+  self->pos = -1;
+}
+
+static gboolean
+region_iter_next_cb (gsize                   position,
+                     const CjhTextRegionRun *run,
+                     gpointer                user_data)
+{
+  if (run->data == RUN_UNCHECKED)
+    {
+      gsize *pos = user_data;
+      *pos = position;
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static gboolean
+region_iter_next (RegionIter  *self,
+                  GtkTextIter *iter)
+{
+  gsize pos, new_pos;
+
+  if (self->pos >= (gssize)_cjh_text_region_get_length (self->region))
+    {
+      gtk_text_buffer_get_end_iter (self->buffer, iter);
+      return FALSE;
+    }
+
+  if (self->pos < 0)
+    pos = 0;
+  else
+    pos = self->pos;
+
+  _cjh_text_region_foreach_in_range (self->region,
+                                     pos,
+                                     _cjh_text_region_get_length (self->region),
+                                     region_iter_next_cb,
+                                     &new_pos);
+
+  pos = MAX (pos, new_pos);
+  gtk_text_buffer_get_iter_at_offset (self->buffer, iter, pos);
+  self->pos = pos;
+
+  return TRUE;
+}
+
+static void
+region_iter_seek (RegionIter        *self,
+                  const GtkTextIter *iter)
+{
+  /* Move to position past the word */
+  self->pos = gtk_text_iter_get_offset (iter) + 1;
+}
+
+static void
+tag_iter_init (TagIter       *self,
+               GtkTextBuffer *buffer,
+               GtkTextTag    *tag)
+{
+  self->buffer = buffer;
+  self->tag = tag;
+  gtk_text_buffer_get_start_iter (buffer, &self->pos);
+}
+
+static gboolean
+tag_iter_next (TagIter     *self,
+               GtkTextIter *pos)
+{
+  if (self->tag && gtk_text_iter_has_tag (&self->pos, self->tag))
+    {
+      /* Should always succeed because we are within the tag */
+      gtk_text_iter_forward_to_tag_toggle (&self->pos, self->tag);
+    }
+
+  *pos = self->pos;
+
+  return TRUE;
+}
+
+static void
+tag_iter_seek (TagIter           *self,
+               const GtkTextIter *iter)
+{
+  self->pos = *iter;
+}
+
+static inline gboolean
+is_extra_word_char (const GtkTextIter *iter,
+                    const char        *extra_word_chars)
+{
+  gunichar ch = gtk_text_iter_get_char (iter);
+
+  /* Short-circuit for known space */
+  if (ch == ' ' || ch == '\n' || ch == '\t' || ch == '\r')
+    return FALSE;
+
+  if (ch == '\'')
+    return TRUE;
+
+  for (const char *c = extra_word_chars; *c; c = g_utf8_next_char (c))
+    {
+      if (ch == g_utf8_get_char (c))
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+gboolean
+editor_spell_iter_forward_word_end (GtkTextIter *iter,
+                                    const char  *extra_word_chars)
+{
+  GtkTextIter tmp = *iter;
+
+  if (gtk_text_iter_forward_word_end (iter))
+    {
+      tmp = *iter;
+
+      if (is_extra_word_char (&tmp, extra_word_chars))
+        {
+          if (editor_spell_iter_forward_word_end (&tmp, extra_word_chars))
+            *iter = tmp;
+        }
+
+      return TRUE;
+    }
+
+  if (gtk_text_iter_is_end (iter) &&
+      gtk_text_iter_ends_word (iter) &&
+      !gtk_text_iter_equal (&tmp, iter))
+    return TRUE;
+
+  return FALSE;
+}
+
+gboolean
+editor_spell_iter_backward_word_start (GtkTextIter *iter,
+                                       const char  *extra_word_chars)
+{
+  GtkTextIter tmp = *iter;
+
+  if (gtk_text_iter_backward_word_start (iter))
+    {
+      tmp = *iter;
+
+      if (gtk_text_iter_backward_char (&tmp) &&
+          is_extra_word_char (&tmp, extra_word_chars))
+        {
+          if (editor_spell_iter_backward_word_start (&tmp, extra_word_chars))
+            *iter = tmp;
+        }
+
+      return TRUE;
+    }
+
+  if (gtk_text_iter_is_start (iter) &&
+      gtk_text_iter_starts_word (iter) &&
+      !gtk_text_iter_equal (&tmp, iter))
+    return TRUE;
+
+  return FALSE;
+}
+
+static void
+word_iter_init (WordIter      *self,
+                GtkTextBuffer *buffer)
+{
+  self->buffer = buffer;
+  gtk_text_buffer_get_start_iter (buffer, &self->word_begin);
+  self->word_end = self->word_begin;
+}
+
+static gboolean
+word_iter_next (WordIter    *self,
+                GtkTextIter *word_begin,
+                GtkTextIter *word_end,
+                const char  *extra_word_chars)
+{
+  if (!editor_spell_iter_forward_word_end (&self->word_end, extra_word_chars))
+    {
+      *word_begin = self->word_end;
+      *word_end = self->word_end;
+      return FALSE;
+    }
+
+  self->word_begin = self->word_end;
+
+  if (!editor_spell_iter_backward_word_start (&self->word_begin, extra_word_chars))
+    {
+      *word_begin = self->word_end;
+      *word_end = self->word_end;
+      return FALSE;
+    }
+
+  *word_begin = self->word_begin;
+  *word_end = self->word_end;
+
+  return TRUE;
+}
+
+static void
+word_iter_seek (WordIter          *self,
+                const GtkTextIter *iter)
+{
+  self->word_begin = *iter;
+  self->word_end = *iter;
+}
+
+EditorSpellCursor *
+editor_spell_cursor_new (GtkTextBuffer *buffer,
+                         CjhTextRegion *region,
+                         GtkTextTag    *no_spell_check_tag,
+                         const char    *extra_word_chars)
+{
+  EditorSpellCursor *self;
+
+  g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), NULL);
+  g_return_val_if_fail (region != NULL, NULL);
+  g_return_val_if_fail (!no_spell_check_tag || GTK_IS_TEXT_TAG (no_spell_check_tag), NULL);
+
+  self = g_rc_box_new0 (EditorSpellCursor);
+  region_iter_init (&self->region, buffer, region);
+  tag_iter_init (&self->tag, buffer, no_spell_check_tag);
+  word_iter_init (&self->word, buffer);
+  self->extra_word_chars = extra_word_chars ? g_intern_string (extra_word_chars) : "";
+
+  return self;
+}
+
+void
+editor_spell_cursor_free (EditorSpellCursor *self)
+{
+  g_rc_box_release (self);
+}
+
+static gboolean
+contains_tag (const GtkTextIter *word_begin,
+              const GtkTextIter *word_end,
+              GtkTextTag        *tag)
+{
+  GtkTextIter toggle_iter;
+
+  if (tag == NULL)
+    return FALSE;
+
+  if (gtk_text_iter_has_tag (word_begin, tag))
+    return TRUE;
+
+  toggle_iter = *word_begin;
+  if (!gtk_text_iter_forward_to_tag_toggle (&toggle_iter, tag))
+    return FALSE;
+
+  return gtk_text_iter_compare (word_end, &toggle_iter) > 0;
+}
+
+gboolean
+editor_spell_cursor_next (EditorSpellCursor *self,
+                          GtkTextIter       *word_begin,
+                          GtkTextIter       *word_end)
+{
+  /* Try to advance skipping any checked region in the buffer */
+  if (!region_iter_next (&self->region, word_end))
+    {
+      *word_begin = *word_end;
+      return FALSE;
+    }
+
+  /* Pass that position to the next iter, so it can skip
+   * past anything that is already checked. Then try to move
+   * forward so that we can skip past regions in the text
+   * buffer that are to be ignored by spellcheck.
+   */
+  tag_iter_seek (&self->tag, word_end);
+  if (!tag_iter_next (&self->tag, word_end))
+    {
+      *word_begin = *word_end;
+      return FALSE;
+    }
+
+  /* Now pass that information to the word iter, so that it can
+   * jump forward to the next word starting from our tag/region
+   * positions.
+   */
+  word_iter_seek (&self->word, word_end);
+  if (!word_iter_next (&self->word, word_begin, word_end, self->extra_word_chars))
+    return FALSE;
+
+  /* Now pass our new position to the region so that it will
+   * skip past the word when advancing.
+   */
+  region_iter_seek (&self->region, word_end);
+
+  /* If this word contains the no-spell-check tag, then try
+   * again to skip past even more content.
+   */
+  if (contains_tag (word_begin, word_end, self->tag.tag))
+    return editor_spell_cursor_next (self, word_begin, word_end);
+
+  return TRUE;
+}
diff --git a/src/plugins/spellcheck/editor-spell-cursor.h b/src/plugins/spellcheck/editor-spell-cursor.h
new file mode 100644
index 000000000..3987e6402
--- /dev/null
+++ b/src/plugins/spellcheck/editor-spell-cursor.h
@@ -0,0 +1,45 @@
+/* editor-spell-cursor.c
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+typedef struct _EditorSpellCursor EditorSpellCursor;
+typedef struct _CjhTextRegion     CjhTextRegion;
+
+EditorSpellCursor *editor_spell_cursor_new               (GtkTextBuffer     *buffer,
+                                                          CjhTextRegion     *region,
+                                                          GtkTextTag        *no_spell_check_tag,
+                                                          const char        *extra_word_chars);
+void               editor_spell_cursor_free              (EditorSpellCursor *cursor);
+gboolean           editor_spell_cursor_next              (EditorSpellCursor *cursor,
+                                                          GtkTextIter       *word_begin,
+                                                          GtkTextIter       *word_end);
+gboolean           editor_spell_iter_forward_word_end    (GtkTextIter       *iter,
+                                                          const char        *extra_word_chars);
+gboolean           editor_spell_iter_backward_word_start (GtkTextIter       *iter,
+                                                          const char        *extra_word_chars);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (EditorSpellCursor, editor_spell_cursor_free)
+
+G_END_DECLS
diff --git a/src/plugins/spellcheck/editor-spell-language-info.c 
b/src/plugins/spellcheck/editor-spell-language-info.c
new file mode 100644
index 000000000..8bc4ceb21
--- /dev/null
+++ b/src/plugins/spellcheck/editor-spell-language-info.c
@@ -0,0 +1,162 @@
+/* editor-spell-language-info.c
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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
+ */
+
+#include "config.h"
+
+#include "editor-spell-language-info.h"
+
+struct _EditorSpellLanguageInfo
+{
+  GObject parent_instance;
+  char *name;
+  char *code;
+};
+
+G_DEFINE_TYPE (EditorSpellLanguageInfo, editor_spell_language_info, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_CODE,
+  PROP_NAME,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * editor_spell_language_info_new:
+ *
+ * Create a new #EditorSpellLanguageInfo.
+ *
+ * Returns: (transfer full): a newly created #EditorSpellLanguageInfo
+ */
+EditorSpellLanguageInfo *
+editor_spell_language_info_new (const char *name,
+                                const char *code)
+{
+  return g_object_new (EDITOR_TYPE_SPELL_LANGUAGE_INFO,
+                       "name", name,
+                       "code", code,
+                       NULL);
+}
+
+static void
+editor_spell_language_info_finalize (GObject *object)
+{
+  EditorSpellLanguageInfo *self = (EditorSpellLanguageInfo *)object;
+
+  g_clear_pointer (&self->name, g_free);
+  g_clear_pointer (&self->code, g_free);
+
+  G_OBJECT_CLASS (editor_spell_language_info_parent_class)->finalize (object);
+}
+
+static void
+editor_spell_language_info_get_property (GObject    *object,
+                                         guint       prop_id,
+                                         GValue     *value,
+                                         GParamSpec *pspec)
+{
+  EditorSpellLanguageInfo *self = EDITOR_SPELL_LANGUAGE_INFO (object);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      g_value_set_string (value, editor_spell_language_info_get_name (self));
+      break;
+
+    case PROP_CODE:
+      g_value_set_string (value, editor_spell_language_info_get_code (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+editor_spell_language_info_set_property (GObject      *object,
+                                         guint         prop_id,
+                                         const GValue *value,
+                                         GParamSpec   *pspec)
+{
+  EditorSpellLanguageInfo *self = EDITOR_SPELL_LANGUAGE_INFO (object);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      self->name = g_value_dup_string (value);
+      break;
+
+    case PROP_CODE:
+      self->code = g_value_dup_string (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+editor_spell_language_info_class_init (EditorSpellLanguageInfoClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = editor_spell_language_info_finalize;
+  object_class->get_property = editor_spell_language_info_get_property;
+  object_class->set_property = editor_spell_language_info_set_property;
+
+  properties [PROP_NAME] =
+    g_param_spec_string ("name",
+                         "Name",
+                         "The name of the language",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CODE] =
+    g_param_spec_string ("code",
+                         "Code",
+                         "The language code",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+editor_spell_language_info_init (EditorSpellLanguageInfo *self)
+{
+}
+
+const char *
+editor_spell_language_info_get_name (EditorSpellLanguageInfo *self)
+{
+  g_return_val_if_fail (EDITOR_IS_SPELL_LANGUAGE_INFO (self), NULL);
+
+  return self->name;
+}
+
+const char *
+editor_spell_language_info_get_code (EditorSpellLanguageInfo *self)
+{
+  g_return_val_if_fail (EDITOR_IS_SPELL_LANGUAGE_INFO (self), NULL);
+
+  return self->code;
+}
diff --git a/src/plugins/spellcheck/editor-spell-language-info.h 
b/src/plugins/spellcheck/editor-spell-language-info.h
new file mode 100644
index 000000000..a84426168
--- /dev/null
+++ b/src/plugins/spellcheck/editor-spell-language-info.h
@@ -0,0 +1,36 @@
+/* editor-spell-language-info.h
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include "editor-types.h"
+
+G_BEGIN_DECLS
+
+#define EDITOR_TYPE_SPELL_LANGUAGE_INFO (editor_spell_language_info_get_type())
+
+G_DECLARE_FINAL_TYPE (EditorSpellLanguageInfo, editor_spell_language_info, EDITOR, SPELL_LANGUAGE_INFO, 
GObject)
+
+EditorSpellLanguageInfo *editor_spell_language_info_new      (const char              *name,
+                                                              const char              *code);
+const char              *editor_spell_language_info_get_name (EditorSpellLanguageInfo *self);
+const char              *editor_spell_language_info_get_code (EditorSpellLanguageInfo *self);
+
+G_END_DECLS
diff --git a/src/plugins/spellcheck/editor-spell-language.c b/src/plugins/spellcheck/editor-spell-language.c
new file mode 100644
index 000000000..98902e0cf
--- /dev/null
+++ b/src/plugins/spellcheck/editor-spell-language.c
@@ -0,0 +1,177 @@
+/* editor-spell-language.c
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include "editor-spell-language.h"
+
+typedef struct
+{
+  const char *code;
+} EditorSpellLanguagePrivate;
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (EditorSpellLanguage, editor_spell_language, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_CODE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+editor_spell_language_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  EditorSpellLanguage *self = EDITOR_SPELL_LANGUAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CODE:
+      g_value_set_string (value, editor_spell_language_get_code (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+editor_spell_language_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  EditorSpellLanguage *self = EDITOR_SPELL_LANGUAGE (object);
+  EditorSpellLanguagePrivate *priv = editor_spell_language_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_CODE:
+      priv->code = g_intern_string (g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+editor_spell_language_class_init (EditorSpellLanguageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->get_property = editor_spell_language_get_property;
+  object_class->set_property = editor_spell_language_set_property;
+
+  properties [PROP_CODE] =
+    g_param_spec_string ("code",
+                         "Code",
+                         "The language code",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+editor_spell_language_init (EditorSpellLanguage *self)
+{
+}
+
+const char *
+editor_spell_language_get_code (EditorSpellLanguage *self)
+{
+  EditorSpellLanguagePrivate *priv = editor_spell_language_get_instance_private (self);
+
+  g_return_val_if_fail (EDITOR_IS_SPELL_LANGUAGE (self), NULL);
+
+  return priv->code;
+}
+
+gboolean
+editor_spell_language_contains_word (EditorSpellLanguage *self,
+                                     const char          *word,
+                                     gssize               word_len)
+{
+  g_return_val_if_fail (EDITOR_IS_SPELL_LANGUAGE (self), FALSE);
+  g_return_val_if_fail (word != NULL, FALSE);
+
+  if (word_len < 0)
+    word_len = strlen (word);
+
+  return EDITOR_SPELL_LANGUAGE_GET_CLASS (self)->contains_word (self, word, word_len);
+}
+
+char **
+editor_spell_language_list_corrections (EditorSpellLanguage *self,
+                                        const char          *word,
+                                        gssize               word_len)
+{
+  g_return_val_if_fail (EDITOR_IS_SPELL_LANGUAGE (self), NULL);
+  g_return_val_if_fail (word != NULL, NULL);
+  g_return_val_if_fail (word != NULL || word_len == 0, NULL);
+
+  if (word_len < 0)
+    word_len = strlen (word);
+
+  if (word_len == 0)
+    return NULL;
+
+  return EDITOR_SPELL_LANGUAGE_GET_CLASS (self)->list_corrections (self, word, word_len);
+}
+
+void
+editor_spell_language_add_word (EditorSpellLanguage *self,
+                                const char          *word)
+{
+  g_return_if_fail (EDITOR_IS_SPELL_LANGUAGE (self));
+  g_return_if_fail (word != NULL);
+
+  if (EDITOR_SPELL_LANGUAGE_GET_CLASS (self)->add_word)
+    EDITOR_SPELL_LANGUAGE_GET_CLASS (self)->add_word (self, word);
+}
+
+void
+editor_spell_language_ignore_word (EditorSpellLanguage *self,
+                                   const char          *word)
+{
+  g_return_if_fail (EDITOR_IS_SPELL_LANGUAGE (self));
+  g_return_if_fail (word != NULL);
+
+  if (EDITOR_SPELL_LANGUAGE_GET_CLASS (self)->ignore_word)
+    EDITOR_SPELL_LANGUAGE_GET_CLASS (self)->ignore_word (self, word);
+}
+
+const char *
+editor_spell_language_get_extra_word_chars (EditorSpellLanguage *self)
+{
+  g_return_val_if_fail (EDITOR_IS_SPELL_LANGUAGE (self), NULL);
+
+  if (EDITOR_SPELL_LANGUAGE_GET_CLASS (self)->get_extra_word_chars)
+    return EDITOR_SPELL_LANGUAGE_GET_CLASS (self)->get_extra_word_chars (self);
+
+  return "";
+}
diff --git a/src/plugins/spellcheck/editor-spell-language.h b/src/plugins/spellcheck/editor-spell-language.h
new file mode 100644
index 000000000..da213a170
--- /dev/null
+++ b/src/plugins/spellcheck/editor-spell-language.h
@@ -0,0 +1,64 @@
+/* editor-spell-language.h
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include "editor-types.h"
+
+G_BEGIN_DECLS
+
+#define EDITOR_TYPE_SPELL_LANGUAGE (editor_spell_language_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (EditorSpellLanguage, editor_spell_language, EDITOR, SPELL_LANGUAGE, GObject)
+
+struct _EditorSpellLanguageClass
+{
+  GObjectClass parent_class;
+
+  gboolean     (*contains_word)        (EditorSpellLanguage *self,
+                                        const char          *word,
+                                        gssize               word_len);
+  char       **(*list_corrections)     (EditorSpellLanguage *self,
+                                        const char          *word,
+                                        gssize               word_len);
+  void         (*add_word)             (EditorSpellLanguage *self,
+                                        const char          *word);
+  void         (*ignore_word)          (EditorSpellLanguage *self,
+                                        const char          *word);
+  const char  *(*get_extra_word_chars) (EditorSpellLanguage *self);
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+const char  *editor_spell_language_get_code             (EditorSpellLanguage *self);
+gboolean     editor_spell_language_contains_word        (EditorSpellLanguage *self,
+                                                         const char          *word,
+                                                         gssize               word_len);
+char       **editor_spell_language_list_corrections     (EditorSpellLanguage *self,
+                                                         const char          *word,
+                                                         gssize               word_len);
+void         editor_spell_language_add_word             (EditorSpellLanguage *self,
+                                                         const char          *word);
+void         editor_spell_language_ignore_word          (EditorSpellLanguage *self,
+                                                         const char          *word);
+const char  *editor_spell_language_get_extra_word_chars (EditorSpellLanguage *self);
+
+G_END_DECLS
diff --git a/src/plugins/spellcheck/editor-spell-menu.c b/src/plugins/spellcheck/editor-spell-menu.c
new file mode 100644
index 000000000..5baadc97a
--- /dev/null
+++ b/src/plugins/spellcheck/editor-spell-menu.c
@@ -0,0 +1,236 @@
+/* editor-spell-menu.c
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "editor-spell-language-info.h"
+#include "editor-spell-menu.h"
+#include "editor-spell-provider.h"
+
+#define MAX_CORRECTIONS 5
+
+#define EDITOR_TYPE_SPELL_CORRECTIONS (editor_spell_corrections_get_type())
+G_DECLARE_FINAL_TYPE (EditorSpellCorrections, editor_spell_corrections, EDITOR, SPELL_CORRECTIONS, 
GMenuModel)
+
+struct _EditorSpellCorrections
+{
+  GMenuModel parent_instance;
+  char *word;
+  char **corrections;
+};
+
+G_DEFINE_TYPE (EditorSpellCorrections, editor_spell_corrections, G_TYPE_MENU_MODEL)
+
+static int
+editor_spell_corrections_get_n_items (GMenuModel *model)
+{
+  EditorSpellCorrections *self = EDITOR_SPELL_CORRECTIONS (model);
+  return self->corrections ? g_strv_length (self->corrections) : 0;
+}
+
+static gboolean
+editor_spell_corrections_is_mutable (GMenuModel *model)
+{
+  return TRUE;
+}
+
+static GMenuModel *
+editor_spell_corrections_get_item_link (GMenuModel *model,
+                                        int         position,
+                                        const char *link)
+{
+  return NULL;
+}
+
+static void
+editor_spell_corrections_get_item_links (GMenuModel  *model,
+                                         int          position,
+                                         GHashTable **links)
+{
+  *links = NULL;
+}
+
+static void
+editor_spell_corrections_get_item_attributes (GMenuModel  *model,
+                                              int          position,
+                                              GHashTable **attributes)
+{
+  EditorSpellCorrections *self = EDITOR_SPELL_CORRECTIONS (model);
+  const char *correction;
+  GHashTable *ht;
+
+  *attributes = NULL;
+
+  if (position < 0 ||
+      self->corrections == NULL ||
+      position >= g_strv_length (self->corrections))
+    return;
+
+  correction = self->corrections[position];
+
+  ht = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_variant_unref);
+  g_hash_table_insert (ht, g_strdup (G_MENU_ATTRIBUTE_ACTION), g_variant_ref_sink (g_variant_new_string 
("spelling.correct")));
+  g_hash_table_insert (ht, g_strdup (G_MENU_ATTRIBUTE_TARGET), g_variant_ref_sink (g_variant_new_string 
(correction)));
+  g_hash_table_insert (ht, g_strdup (G_MENU_ATTRIBUTE_LABEL), g_variant_ref_sink (g_variant_new_string 
(correction)));
+
+  *attributes = ht;
+}
+
+static void
+editor_spell_menu_dispose (GObject *object)
+{
+  EditorSpellCorrections *self = (EditorSpellCorrections *)object;
+
+  g_clear_pointer (&self->word, g_free);
+  g_clear_pointer (&self->corrections, g_strfreev);
+
+  G_OBJECT_CLASS (editor_spell_corrections_parent_class)->dispose (object);
+}
+
+static void
+editor_spell_corrections_class_init (EditorSpellCorrectionsClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GMenuModelClass *menu_model_class = G_MENU_MODEL_CLASS (klass);
+
+  object_class->dispose = editor_spell_menu_dispose;
+
+  menu_model_class->get_n_items = editor_spell_corrections_get_n_items;
+  menu_model_class->is_mutable = editor_spell_corrections_is_mutable;
+  menu_model_class->get_item_link = editor_spell_corrections_get_item_link;
+  menu_model_class->get_item_links = editor_spell_corrections_get_item_links;
+  menu_model_class->get_item_attributes = editor_spell_corrections_get_item_attributes;
+}
+
+static void
+editor_spell_corrections_init (EditorSpellCorrections *self)
+{
+}
+
+static void
+editor_spell_corrections_set (EditorSpellCorrections *self,
+                              const char             *word,
+                              const char * const     *corrections)
+{
+  guint removed = 0;
+  guint added = 0;
+
+  g_assert (EDITOR_IS_SPELL_CORRECTIONS (self));
+
+  if (corrections == (const char * const *)self->corrections)
+    return;
+
+  if (g_strcmp0 (word, self->word) == 0)
+    return;
+
+  if (self->corrections != NULL)
+    removed = g_strv_length (self->corrections);
+
+  if (corrections != NULL)
+    added = g_strv_length ((char **)corrections);
+
+  g_free (self->word);
+  self->word = g_strdup (word);
+  g_strfreev (self->corrections);
+  self->corrections = g_strdupv ((char **)corrections);
+  g_menu_model_items_changed (G_MENU_MODEL (self), 0, removed, added);
+}
+
+static GMenuModel *
+editor_spell_corrections_new (void)
+{
+  return g_object_new (EDITOR_TYPE_SPELL_CORRECTIONS, NULL);
+}
+
+static void
+populate_languages (GMenu *menu)
+{
+  EditorSpellProvider *provider = editor_spell_provider_get_default ();
+  g_autoptr(GPtrArray) infos = editor_spell_provider_list_languages (provider);
+
+  if (infos == NULL)
+    return;
+
+  for (guint i = 0; i < infos->len; i++)
+    {
+      EditorSpellLanguageInfo *info = g_ptr_array_index (infos, i);
+      const char *name = editor_spell_language_info_get_name (info);
+      const char *code = editor_spell_language_info_get_code (info);
+      g_autoptr(GMenuItem) item = g_menu_item_new (name, NULL);
+
+      g_menu_item_set_action_and_target (item, "spelling.language", "s", code);
+      g_menu_append_item (menu, item);
+    }
+}
+
+GMenuModel *
+editor_spell_menu_new (void)
+{
+  g_autoptr(GMenu) menu = g_menu_new ();
+  g_autoptr(GMenuModel) corrections_menu = editor_spell_corrections_new ();
+  g_autoptr(GMenu) languages_menu = g_menu_new ();
+  g_autoptr(GMenuItem) languages_item = g_menu_item_new_submenu (_("Languages"), G_MENU_MODEL 
(languages_menu));
+  g_autoptr(GMenuItem) add_item = g_menu_item_new (_("Add to Dictionary"), "spelling.add");
+  g_autoptr(GMenuItem) ignore_item = g_menu_item_new (_("Ignore"), "spelling.ignore");
+  g_autoptr(GMenuItem) check_item = g_menu_item_new (_("Check Spelling"), "spelling.enabled");
+
+  g_menu_item_set_attribute (add_item, "hidden-when", "s", "action-disabled");
+  g_menu_item_set_attribute (ignore_item, "hidden-when", "s", "action-disabled");
+  g_menu_item_set_attribute (check_item, "role", "s", "check");
+  g_menu_item_set_attribute (languages_item, "submenu-action", "s", "spellcheck.enabled");
+
+  g_menu_append_section (menu, NULL, G_MENU_MODEL (corrections_menu));
+  g_menu_append_item (menu, add_item);
+  g_menu_append_item (menu, ignore_item);
+  g_menu_append_item (menu, check_item);
+  g_menu_append_item (menu, languages_item);
+
+  populate_languages (languages_menu);
+
+  g_object_set_data_full (G_OBJECT (menu),
+                          "LANGUAGES_MENU",
+                          g_object_ref (languages_menu),
+                          g_object_unref);
+
+  g_object_set_data_full (G_OBJECT (menu),
+                          "CORRECTIONS_MENU",
+                          g_object_ref (corrections_menu),
+                          g_object_unref);
+
+  return G_MENU_MODEL (g_steal_pointer (&menu));
+}
+
+void
+editor_spell_menu_set_corrections (GMenuModel         *menu,
+                                   const char         *word,
+                                   const char * const *words)
+{
+  EditorSpellCorrections *corrections_menu;
+
+  g_return_if_fail (G_IS_MENU_MODEL (menu));
+
+  if ((corrections_menu = g_object_get_data (G_OBJECT (menu), "CORRECTIONS_MENU")))
+    {
+      g_assert (EDITOR_IS_SPELL_CORRECTIONS (corrections_menu));
+      editor_spell_corrections_set (corrections_menu, word, words);
+    }
+}
diff --git a/src/plugins/spellcheck/editor-spell-menu.h b/src/plugins/spellcheck/editor-spell-menu.h
new file mode 100644
index 000000000..8a8ce17c7
--- /dev/null
+++ b/src/plugins/spellcheck/editor-spell-menu.h
@@ -0,0 +1,34 @@
+/* editor-spell-menu.h
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include "editor-types.h"
+
+G_BEGIN_DECLS
+
+GMenuModel *editor_spell_menu_new             (void);
+void        editor_spell_menu_set_language    (GMenuModel          *menu,
+                                               EditorSpellLanguage *language);
+void        editor_spell_menu_set_corrections (GMenuModel          *menu,
+                                               const char          *word,
+                                               const char * const  *words);
+
+G_END_DECLS
diff --git a/src/plugins/spellcheck/editor-spell-provider.c b/src/plugins/spellcheck/editor-spell-provider.c
new file mode 100644
index 000000000..236b7ac5f
--- /dev/null
+++ b/src/plugins/spellcheck/editor-spell-provider.c
@@ -0,0 +1,244 @@
+/* editor-spell-provider.c
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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
+ */
+
+#include "config.h"
+
+#include "editor-empty-spell-provider-private.h"
+#include "editor-spell-provider.h"
+#include "editor-enchant-spell-provider.h"
+
+typedef struct
+{
+  char *display_name;
+} EditorSpellProviderPrivate;
+
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (EditorSpellProvider, editor_spell_provider, G_TYPE_OBJECT,
+                                  G_ADD_PRIVATE (EditorSpellProvider))
+
+enum {
+  PROP_0,
+  PROP_DISPLAY_NAME,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+editor_spell_provider_finalize (GObject *object)
+{
+  EditorSpellProvider *self = (EditorSpellProvider *)object;
+  EditorSpellProviderPrivate *priv = editor_spell_provider_get_instance_private (self);
+
+  g_clear_pointer (&priv->display_name, g_free);
+
+  G_OBJECT_CLASS (editor_spell_provider_parent_class)->finalize (object);
+}
+
+static void
+editor_spell_provider_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  EditorSpellProvider *self = EDITOR_SPELL_PROVIDER (object);
+
+  switch (prop_id)
+    {
+    case PROP_DISPLAY_NAME:
+      g_value_set_string (value, editor_spell_provider_get_display_name (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+editor_spell_provider_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  EditorSpellProvider *self = EDITOR_SPELL_PROVIDER (object);
+  EditorSpellProviderPrivate *priv = editor_spell_provider_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_DISPLAY_NAME:
+      priv->display_name = g_value_dup_string (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+editor_spell_provider_class_init (EditorSpellProviderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = editor_spell_provider_finalize;
+  object_class->get_property = editor_spell_provider_get_property;
+  object_class->set_property = editor_spell_provider_set_property;
+
+  properties [PROP_DISPLAY_NAME] =
+    g_param_spec_string ("display-name",
+                         "Display Name",
+                         "Display Name",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+editor_spell_provider_init (EditorSpellProvider *self)
+{
+}
+
+const char *
+editor_spell_provider_get_display_name (EditorSpellProvider *self)
+{
+  EditorSpellProviderPrivate *priv = editor_spell_provider_get_instance_private (self);
+
+  g_return_val_if_fail (EDITOR_IS_SPELL_PROVIDER (self), NULL);
+
+  return priv->display_name;
+}
+
+/**
+ * editor_spell_provider_get_default:
+ *
+ * Gets the default spell provider.
+ *
+ * Returns: (transfer none): an #EditorSpellProvider
+ */
+EditorSpellProvider *
+editor_spell_provider_get_default (void)
+{
+  static EditorSpellProvider *instance;
+
+  if (instance == NULL)
+    {
+      instance = editor_enchant_spell_provider_new ();
+      if (instance == NULL)
+        instance = editor_empty_spell_provider_new ();
+
+      g_set_weak_pointer (&instance, instance);
+    }
+
+  return instance;
+}
+
+/**
+ * editor_spell_provider_supports_language:
+ * @self: an #EditorSpellProvider
+ * @language: the language such as `en_US`.
+ *
+ * Checks of @language is supported by the provider.
+ *
+ * Returns: %TRUE if @language is supported, otherwise %FALSE
+ */
+gboolean
+editor_spell_provider_supports_language (EditorSpellProvider *self,
+                                         const char          *language)
+{
+  g_return_val_if_fail (EDITOR_IS_SPELL_PROVIDER (self), FALSE);
+  g_return_val_if_fail (language != NULL, FALSE);
+
+  return EDITOR_SPELL_PROVIDER_GET_CLASS (self)->supports_language (self, language);
+}
+
+/**
+ * editor_spell_provider_list_languages:
+ * @self: an #EditorSpellProvider
+ *
+ * Gets a list of the languages supported by the provider.
+ *
+ * Returns: (transfer container) (element-type EditorSpellLanguageInfo): an array of
+ *   #EditorSpellLanguageInfo.
+ */
+GPtrArray *
+editor_spell_provider_list_languages (EditorSpellProvider *self)
+{
+  g_return_val_if_fail (EDITOR_IS_SPELL_PROVIDER (self), NULL);
+
+  return EDITOR_SPELL_PROVIDER_GET_CLASS (self)->list_languages (self);
+}
+
+/**
+ * editor_spell_provider_get_language:
+ * @self: an #EditorSpellProvider
+ * @language: the language to load such as `en_US`.
+ *
+ * Gets an #EditorSpellLanguage for the requested language, or %NULL
+ * if the language is not supported.
+ *
+ * Returns: (transfer full) (nullable): an #EditorSpellProvider or %NULL
+ */
+EditorSpellLanguage *
+editor_spell_provider_get_language (EditorSpellProvider *self,
+                                    const char          *language)
+{
+  g_return_val_if_fail (EDITOR_IS_SPELL_PROVIDER (self), NULL);
+  g_return_val_if_fail (language != NULL, NULL);
+
+  return EDITOR_SPELL_PROVIDER_GET_CLASS (self)->get_language (self, language);
+}
+
+const char *
+editor_spell_provider_get_default_code (EditorSpellProvider *self)
+{
+  const char * const *langs;
+  const char *ret;
+
+  g_return_val_if_fail (EDITOR_IS_SPELL_PROVIDER (self), NULL);
+
+  if (EDITOR_SPELL_PROVIDER_GET_CLASS (self)->get_default_code &&
+      (ret = EDITOR_SPELL_PROVIDER_GET_CLASS (self)->get_default_code (self)))
+    return ret;
+
+  langs = g_get_language_names ();
+
+  if (langs != NULL)
+    {
+      for (guint i = 0; langs[i]; i++)
+        {
+          /* Skip past things like "thing.utf8" since we'll
+           * prefer to just have "thing" as it ensures we're
+           * more likely to get code matches elsewhere.
+           */
+          if (strchr (langs[i], '.') != NULL)
+            continue;
+
+          if (editor_spell_provider_supports_language (self, langs[i]))
+            return langs[i];
+        }
+    }
+
+  if (editor_spell_provider_supports_language (self, "en_US"))
+    return "en_US";
+
+  if (editor_spell_provider_supports_language (self, "C"))
+    return "C";
+
+  return NULL;
+}
diff --git a/src/plugins/spellcheck/editor-spell-provider.h b/src/plugins/spellcheck/editor-spell-provider.h
new file mode 100644
index 000000000..29c6d783f
--- /dev/null
+++ b/src/plugins/spellcheck/editor-spell-provider.h
@@ -0,0 +1,55 @@
+/* editor-spell-provider.h
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include "editor-types.h"
+
+G_BEGIN_DECLS
+
+#define EDITOR_TYPE_SPELL_PROVIDER (editor_spell_provider_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (EditorSpellProvider, editor_spell_provider, EDITOR, SPELL_PROVIDER, GObject)
+
+struct _EditorSpellProviderClass
+{
+  GObjectClass parent_class;
+
+  GPtrArray           *(*list_languages)    (EditorSpellProvider *self);
+  gboolean             (*supports_language) (EditorSpellProvider *self,
+                                             const char          *language);
+  EditorSpellLanguage *(*get_language)      (EditorSpellProvider *self,
+                                             const char          *language);
+  const char          *(*get_default_code)  (EditorSpellProvider *self);
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+EditorSpellProvider *editor_spell_provider_get_default       (void);
+const char          *editor_spell_provider_get_default_code  (EditorSpellProvider *self);
+const char          *editor_spell_provider_get_display_name  (EditorSpellProvider *self);
+gboolean             editor_spell_provider_supports_language (EditorSpellProvider *self,
+                                                              const char          *language);
+GPtrArray           *editor_spell_provider_list_languages    (EditorSpellProvider *self);
+EditorSpellLanguage *editor_spell_provider_get_language      (EditorSpellProvider *self,
+                                                              const char          *language);
+
+G_END_DECLS
diff --git a/src/plugins/spellcheck/editor-text-buffer-spell-adapter.c 
b/src/plugins/spellcheck/editor-text-buffer-spell-adapter.c
new file mode 100644
index 000000000..e855ed9fa
--- /dev/null
+++ b/src/plugins/spellcheck/editor-text-buffer-spell-adapter.c
@@ -0,0 +1,833 @@
+/* editor-text-buffer-spell-adapter.c
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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
+ */
+
+#include "config.h"
+
+#include "cjhtextregionprivate.h"
+
+#include <libide-code.h>
+
+#include "editor-spell-checker.h"
+#include "editor-spell-cursor.h"
+#include "editor-spell-language.h"
+#include "editor-text-buffer-spell-adapter.h"
+
+#define RUN_UNCHECKED      GSIZE_TO_POINTER(0)
+#define RUN_CHECKED        GSIZE_TO_POINTER(1)
+#define UPDATE_DELAY_MSECS 100
+#define UPDATE_QUANTA_USEC (G_USEC_PER_SEC/1000L*2) /* 2 msec */
+/* Keyboard repeat is 30 msec by default (see
+ * org.gnome.desktop.peripherals.keyboard repeat-interval) so
+ * we want something longer than that so we are likely
+ * to get removed/re-added on each repeat movement.
+ */
+#define INVALIDATE_DELAY_MSECS 100
+
+typedef struct
+{
+  gint64   deadline;
+  guint    has_unchecked : 1;
+} Update;
+
+typedef struct
+{
+  gsize offset;
+  guint found : 1;
+} ScanForUnchecked;
+
+struct _EditorTextBufferSpellAdapter
+{
+  GObject             parent_instance;
+
+  GtkTextBuffer      *buffer;
+  EditorSpellChecker *checker;
+  CjhTextRegion      *region;
+  GtkTextTag         *tag;
+  GtkTextTag         *no_spell_check_tag;
+
+  guint               cursor_position;
+  guint               incoming_cursor_position;
+  guint               queued_cursor_moved;
+
+  gsize               update_source;
+
+  guint               enabled : 1;
+};
+
+G_DEFINE_TYPE (EditorTextBufferSpellAdapter, editor_text_buffer_spell_adapter, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_BUFFER,
+  PROP_CHECKER,
+  PROP_ENABLED,
+  PROP_LANGUAGE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static inline gboolean
+forward_word_end (EditorTextBufferSpellAdapter *self,
+                  GtkTextIter                  *iter)
+{
+  return editor_spell_iter_forward_word_end (iter,
+                                             editor_spell_checker_get_extra_word_chars (self->checker));
+}
+
+static inline gboolean
+backward_word_start (EditorTextBufferSpellAdapter *self,
+                     GtkTextIter                  *iter)
+{
+  return editor_spell_iter_backward_word_start (iter,
+                                                editor_spell_checker_get_extra_word_chars (self->checker));
+}
+
+static gboolean
+get_current_word (EditorTextBufferSpellAdapter *self,
+                  GtkTextIter                  *begin,
+                  GtkTextIter                  *end)
+{
+  if (gtk_text_buffer_get_selection_bounds (self->buffer, begin, end))
+    return FALSE;
+
+  if (gtk_text_iter_ends_word (end))
+    {
+      backward_word_start (self, begin);
+      return TRUE;
+    }
+
+  if (!gtk_text_iter_starts_word (begin))
+    {
+      if (!gtk_text_iter_inside_word (begin))
+        return FALSE;
+
+      backward_word_start (self, begin);
+    }
+
+  if (!gtk_text_iter_ends_word (end))
+    forward_word_end (self, end);
+
+  return TRUE;
+}
+
+static gboolean
+get_word_at_position (EditorTextBufferSpellAdapter *self,
+                      guint                         position,
+                      GtkTextIter                  *begin,
+                      GtkTextIter                  *end)
+{
+  gtk_text_buffer_get_iter_at_offset (self->buffer, begin, position);
+  *end = *begin;
+
+  if (gtk_text_iter_ends_word (end))
+    {
+      backward_word_start (self, begin);
+      return TRUE;
+    }
+
+  if (!gtk_text_iter_starts_word (begin))
+    {
+      if (!gtk_text_iter_inside_word (begin))
+        return FALSE;
+
+      backward_word_start (self, begin);
+    }
+
+  if (!gtk_text_iter_ends_word (end))
+    forward_word_end (self, end);
+
+  return TRUE;
+}
+
+EditorTextBufferSpellAdapter *
+editor_text_buffer_spell_adapter_new (GtkTextBuffer      *buffer,
+                                      EditorSpellChecker *checker)
+{
+  g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), NULL);
+  g_return_val_if_fail (!checker || EDITOR_IS_SPELL_CHECKER (checker), NULL);
+
+  return g_object_new (EDITOR_TYPE_TEXT_BUFFER_SPELL_ADAPTER,
+                       "buffer", buffer,
+                       "checker", checker,
+                       NULL);
+}
+
+static gboolean
+get_unchecked_start_cb (gsize                   offset,
+                        const CjhTextRegionRun *run,
+                        gpointer                user_data)
+{
+  gsize *pos = user_data;
+
+  if (run->data == RUN_UNCHECKED)
+    {
+      *pos = offset;
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static gboolean
+get_unchecked_start (CjhTextRegion *region,
+                     GtkTextBuffer *buffer,
+                     GtkTextIter   *iter)
+{
+  gsize pos = G_MAXSIZE;
+  _cjh_text_region_foreach (region, get_unchecked_start_cb, &pos);
+  if (pos == G_MAXSIZE)
+    return FALSE;
+  gtk_text_buffer_get_iter_at_offset (buffer, iter, pos);
+  return TRUE;
+}
+
+static gboolean
+editor_text_buffer_spell_adapter_update_range (EditorTextBufferSpellAdapter *self,
+                                               gint64                        deadline)
+{
+  g_autoptr(EditorSpellCursor) cursor = NULL;
+  GtkTextIter word_begin, word_end, begin;
+  const char *extra_word_chars;
+  gboolean ret = FALSE;
+  guint checked = 0;
+
+  g_assert (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self));
+
+  /* Ignore while we are loading or saving */
+  if (ide_buffer_get_loading (IDE_BUFFER (self->buffer)))
+    return TRUE;
+
+  extra_word_chars = editor_spell_checker_get_extra_word_chars (self->checker);
+  cursor = editor_spell_cursor_new (self->buffer, self->region, self->no_spell_check_tag, extra_word_chars);
+
+  /* Get the first unchecked position so that we can remove the tag
+   * from it up to the first word match.
+   */
+  if (!get_unchecked_start (self->region, self->buffer, &begin))
+    {
+      _cjh_text_region_replace (self->region,
+                                0,
+                                _cjh_text_region_get_length (self->region),
+                                RUN_CHECKED);
+      return FALSE;
+    }
+
+  while (editor_spell_cursor_next (cursor, &word_begin, &word_end))
+    {
+      g_autofree char *word = gtk_text_iter_get_slice (&word_begin, &word_end);
+
+      checked++;
+
+      if (!editor_spell_checker_check_word (self->checker, word, -1))
+        gtk_text_buffer_apply_tag (self->buffer, self->tag, &word_begin, &word_end);
+
+      /* Check deadline every five words */
+      if (checked % 5 == 0 && deadline < g_get_monotonic_time ())
+        {
+          ret = TRUE;
+          break;
+        }
+    }
+
+  _cjh_text_region_replace (self->region,
+                            gtk_text_iter_get_offset (&begin),
+                            gtk_text_iter_get_offset (&word_end) - gtk_text_iter_get_offset (&begin),
+                            RUN_CHECKED);
+
+  /* Now remove any tag for the current word to be less annoying */
+  if (get_current_word (self, &word_begin, &word_end))
+    gtk_text_buffer_remove_tag (self->buffer, self->tag, &word_begin, &word_end);
+
+  return ret;
+}
+
+static gboolean
+editor_text_buffer_spell_adapter_run (gint64   deadline,
+                                      gpointer user_data)
+{
+  EditorTextBufferSpellAdapter *self = user_data;
+
+  g_assert (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self));
+
+  if (!editor_text_buffer_spell_adapter_update_range (self, deadline))
+    {
+      self->update_source = 0;
+      return G_SOURCE_REMOVE;
+    }
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+editor_text_buffer_spell_adapter_queue_update (EditorTextBufferSpellAdapter *self)
+{
+  g_assert (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self));
+
+  if (self->checker == NULL || self->buffer == NULL || !self->enabled)
+    {
+      gtk_source_scheduler_clear (&self->update_source);
+      return;
+    }
+
+  if (self->update_source == 0)
+    self->update_source = gtk_source_scheduler_add (editor_text_buffer_spell_adapter_run, self);
+}
+
+void
+editor_text_buffer_spell_adapter_invalidate_all (EditorTextBufferSpellAdapter *self)
+{
+  GtkTextIter begin, end;
+  gsize length;
+
+  g_assert (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self));
+
+  if (!self->enabled)
+    return;
+
+  /* We remove using the known length from the region */
+  if ((length = _cjh_text_region_get_length (self->region)) > 0)
+    {
+      _cjh_text_region_remove (self->region, 0, length - 1);
+      editor_text_buffer_spell_adapter_queue_update (self);
+    }
+
+  /* We add using the length from the buffer because if we were not
+   * enabled previously, the textregion would be empty.
+   */
+  gtk_text_buffer_get_bounds (self->buffer, &begin, &end);
+  if (!gtk_text_iter_equal (&begin, &end))
+    {
+      length = gtk_text_iter_get_offset (&end) - gtk_text_iter_get_offset (&begin);
+      _cjh_text_region_insert (self->region, 0, length, RUN_UNCHECKED);
+      gtk_text_buffer_remove_tag (self->buffer, self->tag, &begin, &end);
+    }
+}
+
+static void
+on_tag_added_cb (EditorTextBufferSpellAdapter *self,
+                 GtkTextTag                   *tag,
+                 GtkTextTagTable              *tag_table)
+{
+  char *name;
+
+  g_assert (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self));
+  g_assert (GTK_IS_TEXT_TAG (tag));
+  g_assert (GTK_IS_TEXT_TAG_TABLE (tag_table));
+
+  g_object_get (tag, "name", &name, NULL);
+  if (name && strcmp (name, "gtksourceview:context-classes:no-spell-check") == 0)
+    {
+      g_set_object (&self->no_spell_check_tag, tag);
+      editor_text_buffer_spell_adapter_invalidate_all (self);
+    }
+}
+
+static void
+on_tag_removed_cb (EditorTextBufferSpellAdapter *self,
+                   GtkTextTag                   *tag,
+                   GtkTextTagTable              *tag_table)
+{
+  g_assert (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self));
+  g_assert (GTK_IS_TEXT_TAG (tag));
+  g_assert (GTK_IS_TEXT_TAG_TABLE (tag_table));
+
+  if (tag == self->no_spell_check_tag)
+    {
+      g_clear_object (&self->no_spell_check_tag);
+      editor_text_buffer_spell_adapter_invalidate_all (self);
+    }
+}
+
+static void
+invalidate_tag_region_cb (EditorTextBufferSpellAdapter *self,
+                          GtkTextTag                   *tag,
+                          GtkTextIter                  *begin,
+                          GtkTextIter                  *end,
+                          GtkTextBuffer                *buffer)
+{
+  g_assert (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self));
+  g_assert (GTK_IS_TEXT_TAG (tag));
+  g_assert (GTK_IS_TEXT_BUFFER (buffer));
+
+  if (!self->enabled)
+    return;
+
+  if (tag == self->no_spell_check_tag)
+    {
+      gsize begin_offset = gtk_text_iter_get_offset (begin);
+      gsize end_offset = gtk_text_iter_get_offset (end);
+
+      _cjh_text_region_replace (self->region, begin_offset, end_offset - begin_offset, RUN_UNCHECKED);
+      editor_text_buffer_spell_adapter_queue_update (self);
+    }
+}
+
+static void
+apply_error_style_cb (GtkSourceBuffer *buffer,
+                      GParamSpec      *pspec,
+                      GtkTextTag      *tag)
+{
+  GtkSourceStyleScheme *scheme;
+  GtkSourceStyle *style;
+
+  g_assert (GTK_SOURCE_IS_BUFFER (buffer));
+  g_assert (GTK_IS_TEXT_TAG (tag));
+
+  g_object_set (tag,
+                "underline", PANGO_UNDERLINE_ERROR,
+                "background-set", FALSE,
+                "foreground-set", FALSE,
+                "weight-set", FALSE,
+                "variant-set", FALSE,
+                "style-set", FALSE,
+                "indent-set", FALSE,
+                "size-set", FALSE,
+                NULL);
+
+  if ((scheme = gtk_source_buffer_get_style_scheme (buffer)))
+    {
+      if ((style = gtk_source_style_scheme_get_style (scheme, "def:misspelled-word")))
+        gtk_source_style_apply (style, tag);
+    }
+}
+
+static void
+editor_text_buffer_spell_adapter_set_buffer (EditorTextBufferSpellAdapter *self,
+                                             GtkTextBuffer                *buffer)
+{
+  g_assert (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self));
+  g_assert (GTK_IS_TEXT_BUFFER (buffer));
+
+  if (g_set_weak_pointer (&self->buffer, buffer))
+    {
+      GtkTextIter begin, end;
+      GtkTextTagTable *tag_table;
+      guint offset;
+      guint length;
+
+      gtk_text_buffer_get_bounds (buffer, &begin, &end);
+
+      offset = gtk_text_iter_get_offset (&begin);
+      length = gtk_text_iter_get_offset (&end) - offset;
+
+      _cjh_text_region_insert (self->region, offset, length, RUN_UNCHECKED);
+
+      self->tag = gtk_text_buffer_create_tag (buffer, NULL,
+                                              "underline", PANGO_UNDERLINE_ERROR,
+                                              NULL);
+
+      g_signal_connect_object (buffer,
+                               "notify::style-scheme",
+                               G_CALLBACK (apply_error_style_cb),
+                               self->tag,
+                               0);
+      apply_error_style_cb (GTK_SOURCE_BUFFER (buffer), NULL, self->tag);
+
+      /* Track tag changes from the tag table and extract "no-spell-check"
+       * tag from GtkSourceView so that we can avoid words with that tag.
+       */
+      tag_table = gtk_text_buffer_get_tag_table (buffer);
+      g_signal_connect_object (tag_table,
+                               "tag-added",
+                               G_CALLBACK (on_tag_added_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+      g_signal_connect_object (tag_table,
+                               "tag-removed",
+                               G_CALLBACK (on_tag_removed_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+
+      g_signal_connect_object (buffer,
+                               "apply-tag",
+                               G_CALLBACK (invalidate_tag_region_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+      g_signal_connect_object (buffer,
+                               "remove-tag",
+                               G_CALLBACK (invalidate_tag_region_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+
+      editor_text_buffer_spell_adapter_queue_update (self);
+    }
+}
+
+void
+editor_text_buffer_spell_adapter_set_enabled (EditorTextBufferSpellAdapter *self,
+                                              gboolean                      enabled)
+{
+  g_assert (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self));
+
+  enabled = !!enabled;
+
+  if (self->enabled != enabled)
+    {
+      GtkTextIter begin, end;
+
+      self->enabled = enabled;
+
+      if (self->buffer && self->tag && !self->enabled)
+        {
+          gtk_text_buffer_get_bounds (self->buffer, &begin, &end);
+          gtk_text_buffer_remove_tag (self->buffer, self->tag, &begin, &end);
+        }
+
+      editor_text_buffer_spell_adapter_invalidate_all (self);
+      editor_text_buffer_spell_adapter_queue_update (self);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ENABLED]);
+    }
+}
+
+static void
+editor_text_buffer_spell_adapter_finalize (GObject *object)
+{
+  EditorTextBufferSpellAdapter *self = (EditorTextBufferSpellAdapter *)object;
+
+  g_clear_object (&self->checker);
+  g_clear_object (&self->no_spell_check_tag);
+  g_clear_pointer (&self->region, _cjh_text_region_free);
+
+  G_OBJECT_CLASS (editor_text_buffer_spell_adapter_parent_class)->finalize (object);
+}
+
+static void
+editor_text_buffer_spell_adapter_dispose (GObject *object)
+{
+  EditorTextBufferSpellAdapter *self = (EditorTextBufferSpellAdapter *)object;
+
+  g_clear_weak_pointer (&self->buffer);
+  gtk_source_scheduler_clear (&self->update_source);
+
+  G_OBJECT_CLASS (editor_text_buffer_spell_adapter_parent_class)->dispose (object);
+}
+
+static void
+editor_text_buffer_spell_adapter_get_property (GObject    *object,
+                                               guint       prop_id,
+                                               GValue     *value,
+                                               GParamSpec *pspec)
+{
+  EditorTextBufferSpellAdapter *self = EDITOR_TEXT_BUFFER_SPELL_ADAPTER (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUFFER:
+      g_value_set_object (value, self->buffer);
+      break;
+
+    case PROP_CHECKER:
+      g_value_set_object (value, editor_text_buffer_spell_adapter_get_checker (self));
+      break;
+
+    case PROP_ENABLED:
+      g_value_set_boolean (value, self->enabled);
+      break;
+
+    case PROP_LANGUAGE:
+      g_value_set_string (value, editor_text_buffer_spell_adapter_get_language (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+editor_text_buffer_spell_adapter_set_property (GObject      *object,
+                                               guint         prop_id,
+                                               const GValue *value,
+                                               GParamSpec   *pspec)
+{
+  EditorTextBufferSpellAdapter *self = EDITOR_TEXT_BUFFER_SPELL_ADAPTER (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUFFER:
+      editor_text_buffer_spell_adapter_set_buffer (self, g_value_get_object (value));
+      break;
+
+    case PROP_CHECKER:
+      editor_text_buffer_spell_adapter_set_checker (self, g_value_get_object (value));
+      break;
+
+    case PROP_ENABLED:
+      editor_text_buffer_spell_adapter_set_enabled (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_LANGUAGE:
+      editor_text_buffer_spell_adapter_set_language (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+editor_text_buffer_spell_adapter_class_init (EditorTextBufferSpellAdapterClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = editor_text_buffer_spell_adapter_dispose;
+  object_class->finalize = editor_text_buffer_spell_adapter_finalize;
+  object_class->get_property = editor_text_buffer_spell_adapter_get_property;
+  object_class->set_property = editor_text_buffer_spell_adapter_set_property;
+
+  properties [PROP_BUFFER] =
+    g_param_spec_object ("buffer",
+                         "Buffer",
+                         "Buffer",
+                         GTK_TYPE_TEXT_BUFFER,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CHECKER] =
+    g_param_spec_object ("checker",
+                         "Checker",
+                         "Checker",
+                         EDITOR_TYPE_SPELL_CHECKER,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ENABLED] =
+    g_param_spec_boolean ("enabled",
+                          "Enabled",
+                          "If spellcheck is enabled",
+                          TRUE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_LANGUAGE] =
+    g_param_spec_string ("language",
+                         "Language",
+                         "The language code such as en_US",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+editor_text_buffer_spell_adapter_init (EditorTextBufferSpellAdapter *self)
+{
+  self->region = _cjh_text_region_new (NULL, NULL);
+}
+
+EditorSpellChecker *
+editor_text_buffer_spell_adapter_get_checker (EditorTextBufferSpellAdapter *self)
+{
+  g_return_val_if_fail (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self), NULL);
+
+  return self->checker;
+}
+
+void
+editor_text_buffer_spell_adapter_set_checker (EditorTextBufferSpellAdapter *self,
+                                              EditorSpellChecker           *checker)
+{
+  g_return_if_fail (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self));
+  g_return_if_fail (!checker || EDITOR_IS_SPELL_CHECKER (checker));
+
+  if (g_set_object (&self->checker, checker))
+    {
+      gsize length = _cjh_text_region_get_length (self->region);
+
+      gtk_source_scheduler_clear (&self->update_source);
+
+      if (length > 0)
+        {
+          _cjh_text_region_remove (self->region, 0, length - 1);
+          _cjh_text_region_insert (self->region, 0, length, RUN_UNCHECKED);
+          g_assert_cmpint (length, ==, _cjh_text_region_get_length (self->region));
+        }
+
+      editor_text_buffer_spell_adapter_queue_update (self);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHECKER]);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LANGUAGE]);
+    }
+}
+
+GtkTextBuffer *
+editor_text_buffer_spell_adapter_get_buffer (EditorTextBufferSpellAdapter *self)
+{
+  g_return_val_if_fail (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self), NULL);
+
+  return self->buffer;
+}
+
+static void
+mark_unchecked (EditorTextBufferSpellAdapter *self,
+                guint                         offset,
+                guint                         length)
+{
+  GtkTextIter begin, end;
+
+  g_assert (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self));
+  g_assert (GTK_IS_TEXT_BUFFER (self->buffer));
+  g_assert (self->enabled);
+
+  gtk_text_buffer_get_iter_at_offset (self->buffer, &begin, offset);
+  gtk_text_buffer_get_iter_at_offset (self->buffer, &end, offset + length);
+
+  if (!gtk_text_iter_starts_word (&begin))
+    backward_word_start (self, &begin);
+
+  if (!gtk_text_iter_ends_word (&end))
+    forward_word_end (self, &end);
+
+  _cjh_text_region_replace (self->region,
+                            gtk_text_iter_get_offset (&begin),
+                            gtk_text_iter_get_offset (&end) - gtk_text_iter_get_offset (&begin),
+                            RUN_UNCHECKED);
+
+  gtk_text_buffer_remove_tag (self->buffer, self->tag, &begin, &end);
+
+  editor_text_buffer_spell_adapter_queue_update (self);
+}
+
+void
+editor_text_buffer_spell_adapter_before_insert_text (EditorTextBufferSpellAdapter *self,
+                                                     guint                         offset,
+                                                     guint                         length)
+{
+  if (self->enabled)
+    _cjh_text_region_insert (self->region, offset, length, RUN_UNCHECKED);
+}
+
+
+void
+editor_text_buffer_spell_adapter_after_insert_text (EditorTextBufferSpellAdapter *self,
+                                                    guint                         offset,
+                                                    guint                         length)
+{
+  if (self->enabled)
+    mark_unchecked (self, offset, length);
+}
+
+void
+editor_text_buffer_spell_adapter_before_delete_range (EditorTextBufferSpellAdapter *self,
+                                                      guint                         offset,
+                                                      guint                         length)
+{
+  if (self->enabled)
+    _cjh_text_region_remove (self->region, offset, length);
+}
+
+void
+editor_text_buffer_spell_adapter_after_delete_range (EditorTextBufferSpellAdapter *self,
+                                                     guint                         offset,
+                                                     guint                         length)
+{
+  if (self->enabled)
+    mark_unchecked (self, offset, 0);
+}
+
+static gboolean
+editor_text_buffer_spell_adapter_cursor_moved_cb (gpointer data)
+{
+  EditorTextBufferSpellAdapter *self = data;
+  GtkTextIter begin, end;
+
+  g_assert (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self));
+
+  self->queued_cursor_moved = 0;
+
+  /* Invalidate the old position */
+  if (self->enabled && get_word_at_position (self, self->cursor_position, &begin, &end))
+    mark_unchecked (self,
+                    gtk_text_iter_get_offset (&begin),
+                    gtk_text_iter_get_offset (&end) - gtk_text_iter_get_offset (&begin));
+
+  self->cursor_position = self->incoming_cursor_position;
+
+  /* Invalidate word at new position */
+  if (self->enabled && get_word_at_position (self, self->cursor_position, &begin, &end))
+    mark_unchecked (self,
+                    gtk_text_iter_get_offset (&begin),
+                    gtk_text_iter_get_offset (&end) - gtk_text_iter_get_offset (&begin));
+
+  return G_SOURCE_REMOVE;
+}
+
+void
+editor_text_buffer_spell_adapter_cursor_moved (EditorTextBufferSpellAdapter *self,
+                                               guint                         position)
+{
+  g_return_if_fail (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self));
+  g_return_if_fail (self->buffer != NULL);
+
+  if (!self->enabled)
+    return;
+
+  self->incoming_cursor_position = position;
+  g_clear_handle_id (&self->queued_cursor_moved, g_source_remove);
+  self->queued_cursor_moved = g_timeout_add_full (G_PRIORITY_LOW,
+                                                  INVALIDATE_DELAY_MSECS,
+                                                  editor_text_buffer_spell_adapter_cursor_moved_cb,
+                                                  g_object_ref (self),
+                                                  g_object_unref);
+}
+
+const char *
+editor_text_buffer_spell_adapter_get_language (EditorTextBufferSpellAdapter *self)
+{
+  g_return_val_if_fail (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self), NULL);
+
+  return self->checker ? editor_spell_checker_get_language (self->checker) : NULL;
+}
+
+void
+editor_text_buffer_spell_adapter_set_language (EditorTextBufferSpellAdapter *self,
+                                               const char                   *language)
+{
+  g_return_if_fail (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self));
+
+  if (self->checker == NULL && language == NULL)
+    return;
+
+  if (self->checker == NULL)
+    {
+      self->checker = editor_spell_checker_new (NULL, language);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHECKER]);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LANGUAGE]);
+    }
+  else if (g_strcmp0 (language, editor_text_buffer_spell_adapter_get_language (self)) != 0)
+    {
+      editor_spell_checker_set_language (self->checker, language);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LANGUAGE]);
+    }
+
+  editor_text_buffer_spell_adapter_invalidate_all (self);
+}
+
+GtkTextTag *
+editor_text_buffer_spell_adapter_get_tag (EditorTextBufferSpellAdapter *self)
+{
+  g_return_val_if_fail (EDITOR_IS_TEXT_BUFFER_SPELL_ADAPTER (self), NULL);
+
+  return self->tag;
+}
+
+gboolean
+editor_text_buffer_spell_adapter_get_enabled (EditorTextBufferSpellAdapter *self)
+{
+  return self ? self->enabled : FALSE;
+}
diff --git a/src/plugins/spellcheck/editor-text-buffer-spell-adapter.h 
b/src/plugins/spellcheck/editor-text-buffer-spell-adapter.h
new file mode 100644
index 000000000..a019fa33e
--- /dev/null
+++ b/src/plugins/spellcheck/editor-text-buffer-spell-adapter.h
@@ -0,0 +1,62 @@
+/* editor-text-buffer-spell-adapter.h
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "editor-types.h"
+
+G_BEGIN_DECLS
+
+#define EDITOR_TYPE_TEXT_BUFFER_SPELL_ADAPTER (editor_text_buffer_spell_adapter_get_type())
+
+G_DECLARE_FINAL_TYPE (EditorTextBufferSpellAdapter, editor_text_buffer_spell_adapter, EDITOR, 
TEXT_BUFFER_SPELL_ADAPTER, GObject)
+
+EditorTextBufferSpellAdapter *editor_text_buffer_spell_adapter_new                 (GtkTextBuffer            
    *buffer,
+                                                                                    EditorSpellChecker       
    *checker);
+GtkTextBuffer      *editor_text_buffer_spell_adapter_get_buffer          (EditorTextBufferSpellAdapter 
*self);
+gboolean            editor_text_buffer_spell_adapter_get_enabled         (EditorTextBufferSpellAdapter 
*self);
+void                editor_text_buffer_spell_adapter_set_enabled         (EditorTextBufferSpellAdapter *self,
+                                                                          gboolean                      
enabled);
+EditorSpellChecker *editor_text_buffer_spell_adapter_get_checker         (EditorTextBufferSpellAdapter 
*self);
+void                editor_text_buffer_spell_adapter_set_checker         (EditorTextBufferSpellAdapter *self,
+                                                                          EditorSpellChecker           
*checker);
+void                editor_text_buffer_spell_adapter_before_insert_text  (EditorTextBufferSpellAdapter *self,
+                                                                          guint                         
offset,
+                                                                          guint                         len);
+void                editor_text_buffer_spell_adapter_after_insert_text   (EditorTextBufferSpellAdapter *self,
+                                                                          guint                         
offset,
+                                                                          guint                         len);
+void                editor_text_buffer_spell_adapter_before_delete_range (EditorTextBufferSpellAdapter *self,
+                                                                          guint                         
offset,
+                                                                          guint                         len);
+void                editor_text_buffer_spell_adapter_after_delete_range  (EditorTextBufferSpellAdapter *self,
+                                                                          guint                         
offset,
+                                                                          guint                         len);
+void                editor_text_buffer_spell_adapter_cursor_moved        (EditorTextBufferSpellAdapter *self,
+                                                                          guint                         
position);
+const char         *editor_text_buffer_spell_adapter_get_language        (EditorTextBufferSpellAdapter 
*self);
+void                editor_text_buffer_spell_adapter_set_language        (EditorTextBufferSpellAdapter *self,
+                                                                          const char                   
*language);
+void                editor_text_buffer_spell_adapter_invalidate_all      (EditorTextBufferSpellAdapter 
*self);
+GtkTextTag         *editor_text_buffer_spell_adapter_get_tag             (EditorTextBufferSpellAdapter 
*self);
+
+G_END_DECLS
diff --git a/src/plugins/spellcheck/gbp-spell-editor-addin.h b/src/plugins/spellcheck/editor-types.h
similarity index 71%
rename from src/plugins/spellcheck/gbp-spell-editor-addin.h
rename to src/plugins/spellcheck/editor-types.h
index 7d260ce9e..14eaaf1f9 100644
--- a/src/plugins/spellcheck/gbp-spell-editor-addin.h
+++ b/src/plugins/spellcheck/editor-types.h
@@ -1,6 +1,6 @@
-/* gbp-spell-editor-addin.h
+/* editor-types.h
  *
- * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2022 Christian Hergert <chergert redhat com>
  *
  * 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
@@ -20,12 +20,12 @@
 
 #pragma once
 
-#include <libide-editor.h>
+#include <gtk/gtk.h>
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_SPELL_EDITOR_ADDIN (gbp_spell_editor_addin_get_type())
-
-G_DECLARE_FINAL_TYPE (GbpSpellEditorAddin, gbp_spell_editor_addin, GBP, SPELL_EDITOR_ADDIN, GObject)
+typedef struct _EditorSpellChecker  EditorSpellChecker;
+typedef struct _EditorSpellLanguage EditorSpellLanguage;
+typedef struct _EditorSpellProvider EditorSpellProvider;
 
 G_END_DECLS
diff --git a/src/plugins/spellcheck/gbp-spell-buffer-addin.c b/src/plugins/spellcheck/gbp-spell-buffer-addin.c
index aec9d3d3e..fab471ccb 100644
--- a/src/plugins/spellcheck/gbp-spell-buffer-addin.c
+++ b/src/plugins/spellcheck/gbp-spell-buffer-addin.c
@@ -1,6 +1,6 @@
 /* gbp-spell-buffer-addin.c
  *
- * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2022 Christian Hergert <chergert redhat com>
  *
  * 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
@@ -20,103 +20,62 @@
 
 #define G_LOG_DOMAIN "gbp-spell-buffer-addin"
 
-#include "gbp-spell-buffer-addin.h"
+#include "config.h"
 
-struct _GbpSpellBufferAddin
-{
-  GObject        parent_instance;
+#include <libide-code.h>
 
-  /* Unowned reference to buffer */
-  IdeBuffer     *buffer;
-  GtkTextTag    *misspelled_tag;
-
-  /* Owned spellchecker instance */
-  GspellChecker *spellchecker;
+#include "gbp-spell-buffer-addin.h"
 
-  /* To allow for dynamic enabling of the inline spellcheck, we keep
-   * track of how many views need it. We will enable the feature in
-   * the buffer if it has manually been enabled (see @enabled) or if
-   * this value is >= 1.
-   */
-  gint           count;
+#include "editor-spell-checker.h"
+#include "editor-text-buffer-spell-adapter.h"
 
-  /* Manual enabling of inline checking */
-  guint          enabled : 1;
-};
+#define METADATA_SPELLING "metadata::gte-spelling"
 
-enum {
-  PROP_0,
-  PROP_ENABLED,
-  N_PROPS
+struct _GbpSpellBufferAddin
+{
+  GObject parent_instance;
+  IdeBuffer *buffer;
+  EditorSpellChecker *checker;
+  EditorTextBufferSpellAdapter *adapter;
 };
 
-static gboolean
-gbp_spell_buffer_addin_get_enabled (GbpSpellBufferAddin *self)
+static void
+check_error (GObject      *object,
+             GAsyncResult *result,
+             gpointer      user_data)
 {
-  g_assert (GBP_IS_SPELL_BUFFER_ADDIN (self));
+  GFile *file = (GFile *)object;
+  g_autoptr(GError) error = NULL;
 
-  return self->enabled || self->count > 0;
+  if (!g_file_set_attributes_finish (file, result, NULL, &error))
+    g_warning ("Failed to persist metadata: %s", error->message);
 }
 
 static void
-gbp_spell_buffer_addin_apply (GbpSpellBufferAddin *self)
+checker_notify_language_cb (GbpSpellBufferAddin *self,
+                            GParamSpec          *pspec,
+                            EditorSpellChecker  *spell_checker)
 {
-  GspellTextBuffer *spell_buffer;
-
-  IDE_ENTRY;
+  g_autoptr(GFileInfo) info = NULL;
+  const char *language_id;
+  GFile *file;
 
   g_assert (GBP_IS_SPELL_BUFFER_ADDIN (self));
+  g_assert (EDITOR_IS_SPELL_CHECKER (spell_checker));
+  g_assert (IDE_IS_BUFFER (self->buffer));
 
-  /* We might be disposed */
-  if (self->buffer == NULL)
+  /* Only persist the metadata if we have a backing file */
+  if (!(file = ide_buffer_get_file (self->buffer)) || !g_file_is_native (file))
     return;
 
-  spell_buffer = gspell_text_buffer_get_from_gtk_text_buffer (GTK_TEXT_BUFFER (self->buffer));
-
-  if (!gbp_spell_buffer_addin_get_enabled (self))
-    {
-      GtkTextIter begin;
-      GtkTextIter end;
-
-      gspell_text_buffer_set_spell_checker (spell_buffer, NULL);
-      g_clear_object (&self->spellchecker);
-
-      gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (self->buffer), &begin, &end);
-      gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (self->buffer),
-                                  self->misspelled_tag, &begin, &end);
-
-      return;
-    }
-
-  if (self->spellchecker == NULL)
-    {
-
-      /* Setup the spell checker for the buffer. We retrain the spellchecker
-       * instance so that we can add words/modify the dictionary at runtime.
-       */
-      self->spellchecker = gspell_checker_new (NULL);
-      gspell_text_buffer_set_spell_checker (spell_buffer, self->spellchecker);
-    }
-
-  IDE_EXIT;
-}
-
-static void
-update_style_scheme (GbpSpellBufferAddin *self,
-                     GParamSpec          *pspec,
-                     IdeBuffer           *buffer)
-{
-  GtkSourceStyleScheme *scheme;
-
-  g_assert (GBP_IS_SPELL_BUFFER_ADDIN (self));
-  g_assert (IDE_IS_BUFFER (buffer));
-
-  scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (buffer));
+  /* Ignore if there is nothing to set */
+  if (!(language_id = editor_spell_checker_get_language (spell_checker)))
+    return;
 
-  if (!ide_source_style_scheme_apply_style (scheme, "misspelled-match", self->misspelled_tag))
-    g_object_set (self->misspelled_tag,
-                  "underline", PANGO_UNDERLINE_SINGLE,
-                  NULL);
+  info = g_file_info_new ();
+  g_file_info_set_attribute_string (info, METADATA_SPELLING, language_id);
+  g_file_set_attributes_async (file, info, G_FILE_QUERY_INFO_NONE,
+                               G_PRIORITY_DEFAULT, NULL, check_error, NULL);
 }
 
 static void
@@ -131,15 +90,16 @@ gbp_spell_buffer_addin_load (IdeBufferAddin *addin,
   g_assert (IDE_IS_BUFFER (buffer));
 
   self->buffer = buffer;
+  self->checker = editor_spell_checker_new (NULL, NULL);
+  self->adapter = editor_text_buffer_spell_adapter_new (GTK_TEXT_BUFFER (buffer), self->checker);
 
-  self->misspelled_tag = gtk_text_buffer_create_tag (GTK_TEXT_BUFFER (buffer), NULL, NULL);
-  g_signal_connect_swapped (self->buffer,
-                            "notify::style-scheme",
-                            G_CALLBACK (update_style_scheme),
-                            self);
-  update_style_scheme (self, NULL, self->buffer);
+  g_signal_connect_object (self->checker,
+                           "notify::language",
+                           G_CALLBACK (checker_notify_language_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
 
-  gbp_spell_buffer_addin_apply (self);
+  editor_text_buffer_spell_adapter_set_enabled (self->adapter, TRUE);
 
   IDE_EXIT;
 }
@@ -155,15 +115,14 @@ gbp_spell_buffer_addin_unload (IdeBufferAddin *addin,
   g_assert (GBP_IS_SPELL_BUFFER_ADDIN (self));
   g_assert (IDE_IS_BUFFER (buffer));
 
-  g_signal_handlers_disconnect_by_func (buffer,
-                                        G_CALLBACK (update_style_scheme),
+  g_signal_handlers_disconnect_by_func (self->checker,
+                                        G_CALLBACK (checker_notify_language_cb),
                                         self);
-  gtk_text_tag_table_remove (gtk_text_buffer_get_tag_table (GTK_TEXT_BUFFER (buffer)),
-                             self->misspelled_tag);
-  self->misspelled_tag = NULL;
+
+  g_clear_object (&self->checker);
+  g_clear_object (&self->adapter);
 
   self->buffer = NULL;
-  gbp_spell_buffer_addin_apply (self);
 
   IDE_EXIT;
 }
@@ -176,164 +135,14 @@ buffer_addin_iface_init (IdeBufferAddinInterface *iface)
 }
 
 G_DEFINE_FINAL_TYPE_WITH_CODE (GbpSpellBufferAddin, gbp_spell_buffer_addin, G_TYPE_OBJECT,
-                         G_IMPLEMENT_INTERFACE (IDE_TYPE_BUFFER_ADDIN, buffer_addin_iface_init))
-
-static GParamSpec *properties [N_PROPS];
-
-static void
-gbp_spell_buffer_addin_get_property (GObject    *object,
-                                     guint       prop_id,
-                                     GValue     *value,
-                                     GParamSpec *pspec)
-{
-  GbpSpellBufferAddin *self = GBP_SPELL_BUFFER_ADDIN (object);
-
-  switch (prop_id)
-    {
-    case PROP_ENABLED:
-      g_value_set_boolean (value, gbp_spell_buffer_addin_get_enabled (self));
-      break;
-
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-    }
-}
-
-static void
-gbp_spell_buffer_addin_set_property (GObject      *object,
-                                     guint         prop_id,
-                                     const GValue *value,
-                                     GParamSpec   *pspec)
-{
-  GbpSpellBufferAddin *self = GBP_SPELL_BUFFER_ADDIN (object);
-
-  switch (prop_id)
-    {
-    case PROP_ENABLED:
-      self->enabled = g_value_get_boolean (value);
-      gbp_spell_buffer_addin_apply (self);
-      break;
-
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-    }
-}
+                               G_IMPLEMENT_INTERFACE (IDE_TYPE_BUFFER_ADDIN, buffer_addin_iface_init))
 
 static void
 gbp_spell_buffer_addin_class_init (GbpSpellBufferAddinClass *klass)
 {
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
-
-  object_class->get_property = gbp_spell_buffer_addin_get_property;
-  object_class->set_property = gbp_spell_buffer_addin_set_property;
-
-  properties [PROP_ENABLED] =
-    g_param_spec_boolean ("enabled",
-                          "Enabled",
-                          "If the spellchecker is enabled",
-                          FALSE,
-                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
-
-  g_object_class_install_properties (object_class, N_PROPS, properties);
 }
 
 static void
 gbp_spell_buffer_addin_init (GbpSpellBufferAddin *self)
 {
 }
-
-/**
- * gbp_spell_buffer_addin_get_checker:
- * @self: a #GbpSpellBufferAddin
- *
- * Gets the #GspellChecker used by the underlying buffer, or %NULL if
- * no spellchecker is active.
- *
- * Returns: (transfer none): a #GspellChecker
- *
- * Since: 3.26
- */
-GspellChecker *
-gbp_spell_buffer_addin_get_checker (GbpSpellBufferAddin *self)
-{
-  g_return_val_if_fail (GBP_IS_SPELL_BUFFER_ADDIN (self), NULL);
-
-  return self->spellchecker;
-}
-
-/**
- * gbp_spell_buffer_addin_begin_checking:
- * @self: a #GbpSpellBufferAddin
- *
- * Views should call this function when they begin their spellchecking
- * process. It dynamically enables various features on the buffer that
- * are necessary for spellchecking.
- *
- * When done, the consumer MUST call gbp_spell_buffer_addin_end_checking()
- * to complete the process. If no more views are active, spellchecking
- * may be disabled on the buffer.
- *
- * Since: 3.26
- */
-void
-gbp_spell_buffer_addin_begin_checking (GbpSpellBufferAddin *self)
-{
-  gboolean before_state;
-  gboolean after_state;
-
-  g_return_if_fail (GBP_IS_SPELL_BUFFER_ADDIN (self));
-  g_return_if_fail (self->count >= 0);
-
-  before_state = gbp_spell_buffer_addin_get_enabled (self);
-  self->count++;
-  after_state = gbp_spell_buffer_addin_get_enabled (self);
-
-  if (before_state != after_state)
-    {
-      gbp_spell_buffer_addin_apply (self);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ENABLED]);
-    }
-}
-
-/**
- * gbp_spell_buffer_addin_end_checking:
- * @self: a #GbpSpellBufferAddin
- *
- * Completes a spellcheck operation. The buffer will return to it's original
- * state. Thay may mean inline checking is disabled.
- */
-void
-gbp_spell_buffer_addin_end_checking (GbpSpellBufferAddin *self)
-{
-  gboolean before_state;
-  gboolean after_state;
-
-  g_return_if_fail (GBP_IS_SPELL_BUFFER_ADDIN (self));
-  g_return_if_fail (self->count >= 0);
-
-  before_state = gbp_spell_buffer_addin_get_enabled (self);
-  self->count--;
-  after_state = gbp_spell_buffer_addin_get_enabled (self);
-
-  if (before_state != after_state)
-    {
-      gbp_spell_buffer_addin_apply (self);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ENABLED]);
-    }
-}
-
-/**
- * gbp_spell_buffer_addin_get_misspelled_tag:
- * @self: a #GbpSpellBufferAddin
- *
- * Gets the tag to use for the current misspelled word.
- *
- * Returns: (nullable) (transfer none): a #GtkTextTag or %NULL.
- */
-GtkTextTag *
-gbp_spell_buffer_addin_get_misspelled_tag (GbpSpellBufferAddin *self)
-{
-  g_return_val_if_fail (GBP_IS_SPELL_BUFFER_ADDIN (self), NULL);
-
-  return self->misspelled_tag;
-}
diff --git a/src/plugins/spellcheck/gbp-spell-buffer-addin.h b/src/plugins/spellcheck/gbp-spell-buffer-addin.h
index 37a3fbad7..97d1617f7 100644
--- a/src/plugins/spellcheck/gbp-spell-buffer-addin.h
+++ b/src/plugins/spellcheck/gbp-spell-buffer-addin.h
@@ -1,6 +1,6 @@
 /* gbp-spell-buffer-addin.h
  *
- * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2022 Christian Hergert <chergert redhat com>
  *
  * 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
@@ -20,8 +20,7 @@
 
 #pragma once
 
-#include <libide-editor.h>
-#include <gspell/gspell.h>
+#include <glib-object.h>
 
 G_BEGIN_DECLS
 
@@ -29,9 +28,4 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (GbpSpellBufferAddin, gbp_spell_buffer_addin, GBP, SPELL_BUFFER_ADDIN, GObject)
 
-GspellChecker *gbp_spell_buffer_addin_get_checker        (GbpSpellBufferAddin *self);
-void           gbp_spell_buffer_addin_begin_checking     (GbpSpellBufferAddin *self);
-void           gbp_spell_buffer_addin_end_checking       (GbpSpellBufferAddin *self);
-GtkTextTag    *gbp_spell_buffer_addin_get_misspelled_tag (GbpSpellBufferAddin *self);
-
 G_END_DECLS
diff --git a/src/plugins/spellcheck/meson.build b/src/plugins/spellcheck/meson.build
index c29516d0f..b2dc68744 100644
--- a/src/plugins/spellcheck/meson.build
+++ b/src/plugins/spellcheck/meson.build
@@ -1,15 +1,18 @@
 if get_option('plugin_spellcheck')
 
 plugins_sources += files([
+  'cjhtextregion.c',
+  'editor-empty-spell-provider.c',
+  'editor-enchant-spell-language.c',
+  'editor-enchant-spell-provider.c',
+  'editor-spell-checker.c',
+  'editor-spell-cursor.c',
+  'editor-spell-language.c',
+  'editor-spell-language-info.c',
+  'editor-spell-menu.c',
+  'editor-spell-provider.c',
+  'editor-text-buffer-spell-adapter.c',
   'gbp-spell-buffer-addin.c',
-  'gbp-spell-dict.c',
-  'gbp-spell-editor-addin.c',
-  'gbp-spell-editor-page-addin.c',
-  'gbp-spell-language-popover.c',
-  'gbp-spell-navigator.c',
-  'gbp-spell-utils.c',
-  'gbp-spell-widget-actions.c',
-  'gbp-spell-widget.c',
   'spellcheck-plugin.c',
 ])
 
@@ -20,8 +23,8 @@ plugin_spellcheck_resources = gnome.compile_resources(
 )
 
 plugins_deps += [
-  dependency('gspell-1', version: '>= 1.2.0'),
   dependency('enchant-2'),
+  dependency('icu-uc'),
 ]
 
 plugins_sources += plugin_spellcheck_resources
diff --git a/src/plugins/spellcheck/spellcheck-plugin.c b/src/plugins/spellcheck/spellcheck-plugin.c
index 95247f9bc..894eef24c 100644
--- a/src/plugins/spellcheck/spellcheck-plugin.c
+++ b/src/plugins/spellcheck/spellcheck-plugin.c
@@ -1,6 +1,6 @@
 /* spellcheck-plugin.c
  *
- * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2022 Christian Hergert <chergert redhat com>
  *
  * 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
@@ -21,11 +21,10 @@
 #include "config.h"
 
 #include <libpeas/peas.h>
+
 #include <libide-editor.h>
 
 #include "gbp-spell-buffer-addin.h"
-#include "gbp-spell-editor-addin.h"
-#include "gbp-spell-editor-page-addin.h"
 
 _IDE_EXTERN void
 _gbp_spellcheck_register_types (PeasObjectModule *module)
@@ -33,10 +32,4 @@ _gbp_spellcheck_register_types (PeasObjectModule *module)
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_BUFFER_ADDIN,
                                               GBP_TYPE_SPELL_BUFFER_ADDIN);
-  peas_object_module_register_extension_type (module,
-                                              IDE_TYPE_EDITOR_ADDIN,
-                                              GBP_TYPE_SPELL_EDITOR_ADDIN);
-  peas_object_module_register_extension_type (module,
-                                              IDE_TYPE_EDITOR_PAGE_ADDIN,
-                                              GBP_TYPE_SPELL_EDITOR_PAGE_ADDIN);
 }
diff --git a/src/plugins/spellcheck/spellcheck.gresource.xml b/src/plugins/spellcheck/spellcheck.gresource.xml
index 26aa92b65..14ed8e602 100644
--- a/src/plugins/spellcheck/spellcheck.gresource.xml
+++ b/src/plugins/spellcheck/spellcheck.gresource.xml
@@ -2,8 +2,5 @@
 <gresources>
   <gresource prefix="/plugins/spellcheck">
     <file>spellcheck.plugin</file>
-    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
-    <file preprocess="xml-stripblanks">gbp-spell-widget.ui</file>
-    <file>themes/shared.css</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/spellcheck/spellcheck.plugin b/src/plugins/spellcheck/spellcheck.plugin
index 54a0d7a31..e521cad0f 100644
--- a/src/plugins/spellcheck/spellcheck.plugin
+++ b/src/plugins/spellcheck/spellcheck.plugin
@@ -1,10 +1,9 @@
 [Plugin]
-Authors=Sébastien Lafargue <slafargue gnome org>
+Authors=Christian Hergert
 Builtin=true
-Copyright=Copyright © 2016 Sébastien Lafargue
-Depends=editor;
+Copyright=Copyright © 2022 Christian Hergert
+Depends=editorui;
 Description=Provides spellchecking for documents
 Embedded=_gbp_spellcheck_register_types
-Hidden=true
 Module=spellcheck
-Name=Spellcheck
+Name=Check Spelling


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