[gtksourceview/wip/chergert/vim: 2/2] vim: add GtkSourceVimIMContext




commit 15f4c2704131624d571f91f697811efb0567da89
Author: Christian Hergert <chergert redhat com>
Date:   Mon Oct 18 15:42:54 2021 -0700

    vim: add GtkSourceVimIMContext
    
    This adds a new GtkIMContext implementation that attempts to emulate
    a modern Vim experience. It can be used with a GtkSourceView by
    connecting it to a GtkEventControllerKey and adding it to a view.
    
    This is an initial implementation and could use further work on
    matching semantics with Vim. However it has a number of features
    beyond what was implemented in GNOME Builder.
    
    Various features supported by GtkSourceVimIMContext include:
    
     - Normal, Insert, Replace, Visual, and Visual Line modes
     - Support for an integrated command bar and current command preview
     - Search and replace
     - Motions and Text Objects
     - History replay
     - Jumplists within the current file
     - Registers including the system and primary clipboards
     - Creation and motion to marks
     - Some commonly used Vim commands
    
    Fixes #233

 NEWS                                          |    7 +
 docs/reference/gtksourceview-5.0-sections.txt |   19 +
 docs/reference/gtksourceview-docs.xml.in      |    1 +
 gtksourceview/gtksource.h                     |    1 +
 gtksourceview/gtksourceinit.c                 |    2 +
 gtksourceview/gtksourcetypes.h                |    1 +
 gtksourceview/gtksourcevimimcontext-private.h |   38 +
 gtksourceview/gtksourcevimimcontext.c         |  605 ++++++
 gtksourceview/gtksourcevimimcontext.h         |   49 +
 gtksourceview/meson.build                     |    5 +
 gtksourceview/vim/gtksourcevim.c              |  505 +++++
 gtksourceview/vim/gtksourcevim.h              |   48 +
 gtksourceview/vim/gtksourcevimcharpending.c   |  101 +
 gtksourceview/vim/gtksourcevimcharpending.h   |   36 +
 gtksourceview/vim/gtksourcevimcommand.c       | 1737 +++++++++++++++
 gtksourceview/vim/gtksourcevimcommand.h       |   49 +
 gtksourceview/vim/gtksourcevimcommandbar.c    |  315 +++
 gtksourceview/vim/gtksourcevimcommandbar.h    |   38 +
 gtksourceview/vim/gtksourceviminsert.c        |  598 ++++++
 gtksourceview/vim/gtksourceviminsert.h        |   60 +
 gtksourceview/vim/gtksourceviminsertliteral.c |  101 +
 gtksourceview/vim/gtksourceviminsertliteral.h |   34 +
 gtksourceview/vim/gtksourcevimjumplist.c      |  260 +++
 gtksourceview/vim/gtksourcevimjumplist.h      |   41 +
 gtksourceview/vim/gtksourcevimmarks.c         |  160 ++
 gtksourceview/vim/gtksourcevimmarks.h         |   42 +
 gtksourceview/vim/gtksourcevimmotion.c        | 2840 +++++++++++++++++++++++++
 gtksourceview/vim/gtksourcevimmotion.h        |   90 +
 gtksourceview/vim/gtksourcevimnormal.c        | 1478 +++++++++++++
 gtksourceview/vim/gtksourcevimnormal.h        |   35 +
 gtksourceview/vim/gtksourcevimregisters.c     |  325 +++
 gtksourceview/vim/gtksourcevimregisters.h     |   44 +
 gtksourceview/vim/gtksourcevimreplace.c       |  139 ++
 gtksourceview/vim/gtksourcevimreplace.h       |   34 +
 gtksourceview/vim/gtksourcevimstate.c         | 1464 +++++++++++++
 gtksourceview/vim/gtksourcevimstate.h         |  199 ++
 gtksourceview/vim/gtksourcevimtexthistory.c   |  344 +++
 gtksourceview/vim/gtksourcevimtexthistory.h   |   38 +
 gtksourceview/vim/gtksourcevimtextobject.c    |  557 +++++
 gtksourceview/vim/gtksourcevimtextobject.h    |   59 +
 gtksourceview/vim/gtksourcevimvisual.c        |  919 ++++++++
 gtksourceview/vim/gtksourcevimvisual.h        |   45 +
 gtksourceview/vim/meson.build                 |   18 +
 po/POTFILES.in                                |    3 +
 tests/meson.build                             |    1 +
 tests/test-vim.c                              |  206 ++
 tests/test-widget.c                           |   45 +-
 tests/test-widget.ui                          |   36 +-
 testsuite/meson.build                         |    3 +
 testsuite/test-vim-input.c                    |  227 ++
 testsuite/test-vim-state.c                    |   74 +
 testsuite/test-vim-text-object.c              |  248 +++
 52 files changed, 14315 insertions(+), 9 deletions(-)
---
diff --git a/NEWS b/NEWS
index cf9e0f8f..447273d5 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,11 @@
 
+ * A new Vim emulator in the form of GtkSourceVimIMContext. This can emulate
+   basic features such as Normal, Insert, Replace, Visual, Visual Line, and
+   Command Bar modes. Additionally, many Vim-style motions and text-objects
+   are supported. Registers, Marks, and Jumplists are also partially suppored.
+   The + and * registers can be used to integrate with both the system and
+   primary GDK clipboards. Various commands are available in teh command bar
+   such as nohl, set, :s, :join, and more.
  * Updated language specs: rst.lang, c.lang, vala.lang, spice.lang,
    lean.lang, and yara.lang.
  * Updated style schemes which improve the quality of output when used with
diff --git a/docs/reference/gtksourceview-5.0-sections.txt b/docs/reference/gtksourceview-5.0-sections.txt
index 4bbe4160..b552943b 100644
--- a/docs/reference/gtksourceview-5.0-sections.txt
+++ b/docs/reference/gtksourceview-5.0-sections.txt
@@ -1143,3 +1143,22 @@ gtk_source_smart_home_end_type_get_type
 GTK_SOURCE_TYPE_VIEW_GUTTER_POSITION
 gtk_source_view_gutter_position_get_type
 </SECTION>
+
+<SECTION>
+<FILE>vimimcontext</FILE>
+GtkSourceVimIMContext
+gtk_source_vim_im_context_new
+gtk_source_vim_im_context_get_command_text
+gtk_source_vim_im_context_get_command_bar_text
+gtk_source_vim_im_context_execute_command
+<SUBSECTION Standard>
+GtkSourceVimIMContextClass
+GTK_SOURCE_TYPE_VIM_IM_CONTEXT
+GTK_SOURCE_VIM_IM_CONTEXT
+GTK_SOURCE_VIM_IM_CONTEXT_CONST
+GTK_SOURCE_VIM_IM_CONTEXT_CLASS
+GTK_SOURCE_IS_VIM_IM_CONTEXT
+GTK_SOURCE_IS_VIM_IM_CONTEXT_CLASS
+GTK_SOURCE_VIM_IM_CONTEXT_GET_CLASS
+gtk_source_vim_im_context_get_type
+</SECTION>
diff --git a/docs/reference/gtksourceview-docs.xml.in b/docs/reference/gtksourceview-docs.xml.in
index 711da860..0b0cfd3c 100644
--- a/docs/reference/gtksourceview-docs.xml.in
+++ b/docs/reference/gtksourceview-docs.xml.in
@@ -113,6 +113,7 @@
       <xi:include href="xml/spacedrawer.xml"/>
       <xi:include href="xml/tag.xml"/>
       <xi:include href="xml/utils.xml"/>
+      <xi:include href="xml/vimimcontext.xml"/>
       <xi:include href="xml/version.xml"/>
     </chapter>
   </part>
diff --git a/gtksourceview/gtksource.h b/gtksourceview/gtksource.h
index 8d7c97b0..be1e07c3 100644
--- a/gtksourceview/gtksource.h
+++ b/gtksourceview/gtksource.h
@@ -67,6 +67,7 @@
 #include "gtksourceutils.h"
 #include "gtksourceversion.h"
 #include "gtksourceview.h"
+#include "gtksourcevimimcontext.h"
 #include "gtksource-enumtypes.h"
 
 #include "completion-providers/words/gtksourcecompletionwords.h"
diff --git a/gtksourceview/gtksourceinit.c b/gtksourceview/gtksourceinit.c
index 34c8d86a..28c305c8 100644
--- a/gtksourceview/gtksourceinit.c
+++ b/gtksourceview/gtksourceinit.c
@@ -46,6 +46,7 @@
 #include "gtksourcestyleschememanager-private.h"
 #include "gtksourcestyleschemepreview.h"
 #include "gtksourceview.h"
+#include "gtksourcevimimcontext.h"
 
 #ifdef G_OS_WIN32
 #define WIN32_LEAN_AND_MEAN
@@ -203,6 +204,7 @@ gtk_source_init (void)
                g_type_ensure (GTK_SOURCE_TYPE_STYLE_SCHEME_CHOOSER_WIDGET);
                g_type_ensure (GTK_SOURCE_TYPE_STYLE_SCHEME_PREVIEW);
                g_type_ensure (GTK_SOURCE_TYPE_VIEW);
+               g_type_ensure (GTK_SOURCE_TYPE_VIM_IM_CONTEXT);
 
                display = gdk_display_get_default ();
 
diff --git a/gtksourceview/gtksourcetypes.h b/gtksourceview/gtksourcetypes.h
index a6a8da02..61859e31 100644
--- a/gtksourceview/gtksourcetypes.h
+++ b/gtksourceview/gtksourcetypes.h
@@ -54,6 +54,7 @@ typedef struct _GtkSourceHover                     GtkSourceHover;
 typedef struct _GtkSourceHoverContext              GtkSourceHoverContext;
 typedef struct _GtkSourceHoverDisplay              GtkSourceHoverDisplay;
 typedef struct _GtkSourceHoverProvider             GtkSourceHoverProvider;
+typedef struct _GtkSourceVimIMContext              GtkSourceVimIMContext;
 typedef struct _GtkSourceIndenter                  GtkSourceIndenter;
 typedef struct _GtkSourceLanguage                  GtkSourceLanguage;
 typedef struct _GtkSourceLanguageManager           GtkSourceLanguageManager;
diff --git a/gtksourceview/gtksourcevimimcontext-private.h b/gtksourceview/gtksourcevimimcontext-private.h
new file mode 100644
index 00000000..c6f9f61f
--- /dev/null
+++ b/gtksourceview/gtksourcevimimcontext-private.h
@@ -0,0 +1,38 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include "gtksourcevimimcontext.h"
+
+G_BEGIN_DECLS
+
+typedef void (*GtkSourceVimIMContextObserver) (GtkSourceVimIMContext *im_context,
+                                               const char            *string,
+                                               gboolean               reset,
+                                               gpointer               user_data);
+
+void _gtk_source_vim_im_context_add_observer (GtkSourceVimIMContext         *self,
+                                              GtkSourceVimIMContextObserver  observer,
+                                              gpointer                       user_data,
+                                              GDestroyNotify                 notify);
+
+G_END_DECLS
diff --git a/gtksourceview/gtksourcevimimcontext.c b/gtksourceview/gtksourcevimimcontext.c
new file mode 100644
index 00000000..d0409870
--- /dev/null
+++ b/gtksourceview/gtksourcevimimcontext.c
@@ -0,0 +1,605 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "gtksourceview.h"
+#include "gtksourcevimimcontext-private.h"
+#include "gtksource-enumtypes.h"
+
+#include "vim/gtksourcevim.h"
+#include "vim/gtksourcevimcommand.h"
+
+/**
+ * SECTION:vimimcontext
+ * @title: GtkSourceVimIMContext
+ * @short_description: Vim emulation
+ *
+ * The #GtkSourceVimIMContext is a #GtkIMContext implementation that can
+ * be used to provide Vim-like editing controls within a #GtkSourceView.
+ *
+ * The #GtkSourceViMIMContext will process incoming #GdkKeyEvent as the
+ * user types. It should be used in conjunction with a #GtkEventControllerKey.
+ *
+ * Various features supported by #GtkSourceVimIMContext include:
+ *
+ *  - Normal, Insert, Replace, Visual, and Visual Line modes
+ *  - Support for an integrated command bar and current command preview
+ *  - Search and replace
+ *  - Motions and Text Objects
+ *  - History replay
+ *  - Jumplists within the current file
+ *  - Registers including the system and primary clipboards
+ *  - Creation and motion to marks
+ *  - Some commonly used Vim commands
+ *
+ * It is recommended that applications display the contents of
+ * #GtkSourceVimIMContext:command-bar-text and
+ * #GtkSourceVimIMContext:command-text to the user as they represent the
+ * command-bar and current command preview found in Vim.
+ *
+ * #GtkSourceVimIMContext attempts to work with additional #GtkIMContext
+ * implementations such as IBus by querying the #GtkTextView before processing
+ * the command in states which support it (notably Insert and Replace modes).
+ *
+ * <informalexample><programlisting>
+ *  GtkEventController *key;
+ *  GtkSourceView *view;
+ *  GtkIMContext *im_context;
+ *
+ *  view = gtk_source_view_new ();
+ *  im_context = gtk_source_vim_im_context_new ();
+ *  key = gtk_event_controller_key_new ();
+ *
+ *  gtk_event_controller_key_set_im_context (GTK_EVENT_CONTROLLER_KEY (key), im_context);
+ *  gtk_event_controller_set_propagation_phase (key, GTK_PHASE_CAPTURE);
+ *  gtk_widget_add_controller (GTK_WIDGET (view), key);
+ *
+ *  g_object_bind_property (im_context, "command-bar-text", command_bar_label, "label", 0);
+ *  g_object_bind_property (im_context, "command-text", command_label, "label", 0);
+ * </programlisting></informalexample>
+ *
+ * Since: 5.4
+ */
+
+struct _GtkSourceVimIMContext
+{
+       GtkIMContext  parent_instance;
+       GtkSourceVim *vim;
+       GArray       *observers;
+       guint         reset_observer : 1;
+};
+
+typedef struct
+{
+       GtkSourceVimIMContextObserver observer;
+       gpointer data;
+       GDestroyNotify notify;
+} Observer;
+
+G_DEFINE_TYPE (GtkSourceVimIMContext, gtk_source_vim_im_context, GTK_TYPE_IM_CONTEXT)
+
+enum {
+       PROP_0,
+       PROP_COMMAND_BAR_TEXT,
+       PROP_COMMAND_TEXT,
+       N_PROPS
+};
+
+enum {
+       EXECUTE_COMMAND,
+       FORMAT_TEXT,
+       EDIT,
+       WRITE,
+       N_SIGNALS
+};
+
+static GParamSpec *properties[N_PROPS];
+static guint signals[N_SIGNALS];
+
+static void
+clear_observer (Observer *o)
+{
+       if (o->notify)
+       {
+               o->notify (o->data);
+       }
+}
+
+GtkIMContext *
+gtk_source_vim_im_context_new (void)
+{
+       return g_object_new (GTK_SOURCE_TYPE_VIM_IM_CONTEXT, NULL);
+}
+
+static gboolean
+gtk_source_vim_im_context_real_execute_command (GtkSourceVimIMContext *self,
+                                                const char            *command)
+{
+       g_auto(GStrv) parts = NULL;
+
+       g_assert (GTK_SOURCE_IS_VIM_IM_CONTEXT (self));
+       g_assert (command != NULL);
+
+       parts = g_strsplit (command, " ", 2);
+
+       if (parts[1] != NULL)
+       {
+               g_strstrip (parts[1]);
+       }
+
+       if (g_str_equal (command, ":w") ||
+           g_str_equal (command, ":write"))
+       {
+               g_signal_emit (self, signals[WRITE], 0, NULL);
+               return TRUE;
+       }
+       else if (g_str_equal (command, ":e") ||
+                g_str_equal (command, ":edit"))
+       {
+               g_signal_emit (self, signals[EDIT], 0, NULL);
+               return TRUE;
+       }
+       else if (g_str_has_prefix (command, ":w ") ||
+                g_str_has_prefix (command, ":write "))
+       {
+               g_signal_emit (self, signals[WRITE], 0, parts[1]);
+               return TRUE;
+       }
+       else if (g_str_has_prefix (command, ":e ") ||
+                g_str_has_prefix (command, ":edit "))
+       {
+               g_signal_emit (self, signals[EDIT], 0, parts[1]);
+               return TRUE;
+       }
+
+       return FALSE;
+}
+
+static void
+on_vim_notify_cb (GtkSourceVimIMContext *self,
+                  GParamSpec            *pspec,
+                  GtkSourceVim          *vim)
+{
+       g_assert (GTK_SOURCE_IS_VIM_IM_CONTEXT (self));
+       g_assert (GTK_SOURCE_IS_VIM (vim));
+
+       if (g_str_equal (pspec->name, "command-bar-text"))
+               pspec = properties[PROP_COMMAND_BAR_TEXT];
+       else if (g_str_equal (pspec->name, "command-text"))
+               pspec = properties[PROP_COMMAND_TEXT];
+       else
+               pspec = NULL;
+
+       if (pspec)
+               g_object_notify_by_pspec (G_OBJECT (self), pspec);
+}
+
+static gboolean
+on_vim_execute_command_cb (GtkSourceVimIMContext *self,
+                           const char            *command,
+                           GtkSourceVim          *vim)
+{
+       gboolean ret = FALSE;
+
+       g_assert (GTK_SOURCE_IS_VIM_IM_CONTEXT (self));
+       g_assert (GTK_SOURCE_IS_VIM (vim));
+
+       g_signal_emit (self, signals[EXECUTE_COMMAND], 0, command, &ret);
+       return ret;
+}
+
+static void
+on_vim_ready_cb (GtkSourceVimIMContext *self,
+                 GtkSourceVim          *vim)
+{
+       g_assert (GTK_SOURCE_IS_VIM_IM_CONTEXT (self));
+       g_assert (GTK_SOURCE_IS_VIM (vim));
+
+       self->reset_observer = TRUE;
+}
+
+static void
+on_vim_format_cb (GtkSourceVimIMContext *self,
+                  GtkTextIter           *begin,
+                  GtkTextIter           *end,
+                  GtkSourceVim          *vim)
+{
+       g_assert (GTK_SOURCE_IS_VIM_IM_CONTEXT (self));
+       g_assert (begin != NULL);
+       g_assert (end != NULL);
+       g_assert (GTK_SOURCE_IS_VIM (vim));
+
+       g_signal_emit (self, signals [FORMAT_TEXT], 0, begin, end);
+}
+
+static void
+gtk_source_vim_im_context_set_client_widget (GtkIMContext *context,
+                                             GtkWidget    *widget)
+{
+       GtkSourceVimIMContext *self = (GtkSourceVimIMContext *)context;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_IM_CONTEXT (self));
+
+       if (self->vim != NULL)
+       {
+               g_object_run_dispose (G_OBJECT (self->vim));
+               g_clear_object (&self->vim);
+       }
+
+       self->vim = gtk_source_vim_new (GTK_SOURCE_VIEW (widget));
+
+       g_signal_connect_object (self->vim,
+                                "notify",
+                                G_CALLBACK (on_vim_notify_cb),
+                                self,
+                                G_CONNECT_SWAPPED);
+
+       g_signal_connect_object (self->vim,
+                                "execute-command",
+                                G_CALLBACK (on_vim_execute_command_cb),
+                                self,
+                                G_CONNECT_SWAPPED);
+
+       g_signal_connect_object (self->vim,
+                                "format",
+                                G_CALLBACK (on_vim_format_cb),
+                                self,
+                                G_CONNECT_SWAPPED);
+
+       g_signal_connect_object (self->vim,
+                                "ready",
+                                G_CALLBACK (on_vim_ready_cb),
+                                self,
+                                G_CONNECT_SWAPPED);
+
+       g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_COMMAND_TEXT]);
+       g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_COMMAND_BAR_TEXT]);
+}
+
+static void
+gtk_source_vim_im_context_reset (GtkIMContext *context)
+{
+       GtkSourceVimIMContext *self = (GtkSourceVimIMContext *)context;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_IM_CONTEXT (self));
+
+       gtk_source_vim_reset (self->vim);
+}
+
+static void
+gtk_source_vim_im_context_focus_in (GtkIMContext *context)
+{
+       g_assert (GTK_SOURCE_IS_VIM_IM_CONTEXT (context));
+
+}
+
+static void
+gtk_source_vim_im_context_focus_out (GtkIMContext *context)
+{
+       g_assert (GTK_SOURCE_IS_VIM_IM_CONTEXT (context));
+}
+
+static gboolean
+gtk_source_vim_im_context_filter_keypress (GtkIMContext *context,
+                                           GdkEvent     *event)
+{
+       GtkSourceVimIMContext *self = (GtkSourceVimIMContext *)context;
+
+       g_assert (GTK_SOURCE_IS_VIM_IM_CONTEXT (self));
+       g_assert (gdk_event_get_event_type (event) == GDK_KEY_PRESS ||
+                 gdk_event_get_event_type (event) == GDK_KEY_RELEASE);
+
+       if (self->vim == NULL)
+       {
+               return FALSE;
+       }
+
+       if (gdk_event_get_event_type (event) == GDK_KEY_PRESS)
+       {
+               GdkModifierType mods;
+               guint keyval;
+               char str[16];
+
+               mods = gdk_event_get_modifier_state (event);
+               keyval = gdk_key_event_get_keyval (event);
+               gtk_source_vim_state_keyval_to_string (keyval, mods, str);
+
+               for (guint i = 0; i < self->observers->len; i++)
+               {
+                       const Observer *o = &g_array_index (self->observers, Observer, i);
+
+                       o->observer (self, str, self->reset_observer, o->data);
+               }
+
+               self->reset_observer = FALSE;
+       }
+
+       return gtk_source_vim_state_handle_event (GTK_SOURCE_VIM_STATE (self->vim), event);
+}
+
+static void
+gtk_source_vim_im_context_dispose (GObject *object)
+{
+       GtkSourceVimIMContext *self = (GtkSourceVimIMContext *)object;
+
+       g_clear_object (&self->vim);
+       g_clear_pointer (&self->observers, g_array_unref);
+
+       G_OBJECT_CLASS (gtk_source_vim_im_context_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_vim_im_context_get_property (GObject    *object,
+                                        guint       prop_id,
+                                        GValue     *value,
+                                        GParamSpec *pspec)
+{
+       GtkSourceVimIMContext *self = GTK_SOURCE_VIM_IM_CONTEXT (object);
+
+       switch (prop_id)
+       {
+       case PROP_COMMAND_TEXT:
+               g_value_set_string (value, gtk_source_vim_im_context_get_command_text (self));
+               break;
+
+       case PROP_COMMAND_BAR_TEXT:
+               g_value_set_string (value, gtk_source_vim_im_context_get_command_bar_text (self));
+               break;
+
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+       }
+}
+
+static void
+gtk_source_vim_im_context_class_init (GtkSourceVimIMContextClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       GtkIMContextClass *im_context_class = GTK_IM_CONTEXT_CLASS (klass);
+
+       object_class->dispose = gtk_source_vim_im_context_dispose;
+       object_class->get_property = gtk_source_vim_im_context_get_property;
+
+       im_context_class->set_client_widget = gtk_source_vim_im_context_set_client_widget;
+       im_context_class->reset = gtk_source_vim_im_context_reset;
+       im_context_class->focus_in = gtk_source_vim_im_context_focus_in;
+       im_context_class->focus_out = gtk_source_vim_im_context_focus_out;
+       im_context_class->filter_keypress = gtk_source_vim_im_context_filter_keypress;
+
+       properties [PROP_COMMAND_TEXT] =
+               g_param_spec_string ("command-text",
+                                    "Command Text",
+                                    "The text for the current command",
+                                    NULL,
+                                    (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+       properties [PROP_COMMAND_BAR_TEXT] =
+               g_param_spec_string ("command-bar-text",
+                                    "Command Bar Text",
+                                    "The text for the command bar",
+                                    NULL,
+                                    (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_properties (object_class, N_PROPS, properties);
+
+       /**
+        * GtkSourceVimIMContext::execute-command:
+        * @self: a #GtkSourceVimIMContext
+        * @command: the command to execute
+        *
+        * The "execute-command" signal is emitted when a command should be
+        * executed. This might be something like ":wq" or ":e &lt;path&gt;".
+        *
+        * If the application chooses to implement this, it should return
+        * %TRUE from this signal to indicate the command has been handled.
+        *
+        * Returns: %TRUE if handled; otherwise %FALSE.
+        *
+        * Since: 5.4
+        */
+       signals[EXECUTE_COMMAND] =
+               g_signal_new_class_handler ("execute-command",
+                                           G_TYPE_FROM_CLASS (klass),
+                                           G_SIGNAL_RUN_LAST,
+                                           G_CALLBACK (gtk_source_vim_im_context_real_execute_command),
+                                           g_signal_accumulator_true_handled, NULL,
+                                           NULL,
+                                           G_TYPE_BOOLEAN,
+                                           1,
+                                           G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+
+       /**
+        * GtkSourceVimIMContext::format-text:
+        * @self: a #GtkSourceVimIMContext
+        * @begin: the start location
+        * @end: the end location
+        *
+        * Requests that the application format the text between
+        * @begin and @end.
+        *
+        * Since: 5.4
+        */
+       signals[FORMAT_TEXT] =
+               g_signal_new ("format-text",
+                             G_TYPE_FROM_CLASS (klass),
+                             G_SIGNAL_RUN_LAST,
+                             0,
+                             NULL, NULL,
+                             NULL,
+                             G_TYPE_NONE,
+                             2,
+                             GTK_TYPE_TEXT_ITER,
+                             GTK_TYPE_TEXT_ITER);
+
+       /**
+        * GtkSourceVimIMContext::write:
+        * @self: a #GtkSourceVimIMContext
+        * @view: the #GtkSourceView
+        * @path: (nullable): the path if provided, otherwise %NULL
+        *
+        * Requests the application save the file. If a filename was provided,
+        * it will be available to the signal handler as @path.
+        *
+        * This may be executed in relation to the user running the
+        * `:write` or `:w` commands.
+        *
+        * Since: 5.4
+        */
+       signals[WRITE] =
+               g_signal_new ("write",
+                             G_TYPE_FROM_CLASS (klass),
+                             G_SIGNAL_RUN_LAST,
+                             0,
+                             NULL, NULL,
+                             NULL,
+                             G_TYPE_NONE,
+                             2,
+                             GTK_SOURCE_TYPE_VIEW,
+                             G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+
+       /**
+        * GtkSourceVimIMContext::edit:
+        * @self: a #GtkSourceVimIMContext
+        * @view: the #GtkSourceView
+        * @path: (nullable): the path if provided, otherwise %NULL
+        *
+        * Requests the application open the file found at @path. If @path is
+        * %NULL, then the current file should be reloaded from storage.
+        *
+        * This may be executed in relation to the user running the
+        * `:edit` or `:e` commands.
+        *
+        * Since: 5.4
+        */
+       signals[EDIT] =
+               g_signal_new ("edit",
+                             G_TYPE_FROM_CLASS (klass),
+                             G_SIGNAL_RUN_LAST,
+                             0,
+                             NULL, NULL,
+                             NULL,
+                             G_TYPE_NONE,
+                             2,
+                             GTK_SOURCE_TYPE_VIEW,
+                             G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+}
+
+static void
+gtk_source_vim_im_context_init (GtkSourceVimIMContext *self)
+{
+       self->observers = g_array_new (FALSE, FALSE, sizeof (Observer));
+       g_array_set_clear_func (self->observers, (GDestroyNotify)clear_observer);
+}
+
+void
+_gtk_source_vim_im_context_add_observer (GtkSourceVimIMContext         *self,
+                                         GtkSourceVimIMContextObserver  observer,
+                                         gpointer                       data,
+                                         GDestroyNotify                 notify)
+{
+       Observer o;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_IM_CONTEXT (self));
+       g_return_if_fail (observer != NULL);
+
+       o.observer = observer;
+       o.data = data;
+       o.notify = notify;
+
+       g_array_append_val (self->observers, o);
+}
+
+/**
+ * gtk_source_vim_im_context_get_command_text:
+ * @self: a #GtkSourceVimIMContext
+ *
+ * Gets the current command text as it is entered by the user.
+ *
+ * Returns: (not nullable): A string containing the command text
+ *
+ * Since: 5.4
+ */
+const char *
+gtk_source_vim_im_context_get_command_text (GtkSourceVimIMContext *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_IM_CONTEXT (self), NULL);
+
+       if (self->vim == NULL)
+               return NULL;
+
+       return gtk_source_vim_get_command_text (self->vim);
+}
+
+/**
+ * gtk_source_vim_im_context_get_command_bar_text:
+ * @self: a #GtkSourceVimIMContext
+ *
+ * Gets the current command-bar text as it is entered by the user.
+ *
+ * Returns: (not nullable): A string containing the command-bar text
+ *
+ * Since: 5.4
+ */
+const char *
+gtk_source_vim_im_context_get_command_bar_text (GtkSourceVimIMContext *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_IM_CONTEXT (self), NULL);
+
+       if (self->vim == NULL)
+               return NULL;
+
+       return gtk_source_vim_get_command_bar_text (self->vim);
+}
+
+/**
+ * gtk_source_vim_im_context_execute_command:
+ * @self: a #GtkSourceVimIMContext
+ * @command: the command text
+ *
+ * Executes @command as if it was typed into the command bar by the
+ * user except that this does not emit the
+ * #GtkSourceVimIMContext::execute-command signal.
+ *
+ * Since: 5.4
+ */
+void
+gtk_source_vim_im_context_execute_command (GtkSourceVimIMContext *self,
+                                           const char            *command)
+{
+       GtkSourceVimState *normal;
+       GtkSourceVimState *parsed;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_IM_CONTEXT (self));
+       g_return_if_fail (command != NULL);
+
+       if (self->vim == NULL)
+               return;
+
+       normal = gtk_source_vim_state_get_child (GTK_SOURCE_VIM_STATE (self->vim));
+       if (!(parsed = gtk_source_vim_command_new_parsed (normal, command)))
+               return;
+
+       gtk_source_vim_state_set_parent (parsed, normal);
+       gtk_source_vim_state_repeat (parsed);
+       gtk_source_vim_state_unparent (parsed);
+       g_object_unref (parsed);
+}
diff --git a/gtksourceview/gtksourcevimimcontext.h b/gtksourceview/gtksourcevimimcontext.h
new file mode 100644
index 00000000..a9dae017
--- /dev/null
+++ b/gtksourceview/gtksourcevimimcontext.h
@@ -0,0 +1,49 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined (GTK_SOURCE_H_INSIDE) && !defined (GTK_SOURCE_COMPILATION)
+#error "Only <gtksourceview/gtksource.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+#include "gtksourcetypes.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_VIM_IM_CONTEXT (gtk_source_vim_im_context_get_type())
+
+GTK_SOURCE_AVAILABLE_IN_5_4
+G_DECLARE_FINAL_TYPE (GtkSourceVimIMContext, gtk_source_vim_im_context, GTK_SOURCE, VIM_IM_CONTEXT, 
GtkIMContext)
+
+GTK_SOURCE_AVAILABLE_IN_5_4
+GtkIMContext     *gtk_source_vim_im_context_new                  (void);
+GTK_SOURCE_AVAILABLE_IN_5_4
+const char       *gtk_source_vim_im_context_get_command_text     (GtkSourceVimIMContext *self);
+GTK_SOURCE_AVAILABLE_IN_5_4
+const char       *gtk_source_vim_im_context_get_command_bar_text (GtkSourceVimIMContext *self);
+GTK_SOURCE_AVAILABLE_IN_5_4
+void              gtk_source_vim_im_context_execute_command      (GtkSourceVimIMContext *self,
+                                                                  const char            *command);
+
+G_END_DECLS
diff --git a/gtksourceview/meson.build b/gtksourceview/meson.build
index 691c0cd8..ee9efebe 100644
--- a/gtksourceview/meson.build
+++ b/gtksourceview/meson.build
@@ -5,6 +5,8 @@ core_marshallers = gnome.genmarshal('gtksource-marshal',
   valist_marshallers: true,
 )
 
+subdir('vim')
+
 core_public_h = files([
   'gtksource.h',
   'gtksourcebuffer.h',
@@ -54,6 +56,7 @@ core_public_h = files([
   'gtksourcetypes.h',
   'gtksourceutils.h',
   'gtksourceview.h',
+  'gtksourcevimimcontext.h',
 ])
 
 core_public_c = files([
@@ -104,6 +107,7 @@ core_public_c = files([
   'gtksourceutils.c',
   'gtksourceversion.c',
   'gtksourceview.c',
+  'gtksourcevimimcontext.c',
 ])
 
 core_private_c = files([
@@ -221,6 +225,7 @@ core_sources = [
   gtksourceversion_h,
   core_marshallers,
   gtksource_res,
+  vim_sources,
 ]
 
 install_headers(
diff --git a/gtksourceview/vim/gtksourcevim.c b/gtksourceview/vim/gtksourcevim.c
new file mode 100644
index 00000000..b8a0be33
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevim.c
@@ -0,0 +1,505 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "gtksourcebuffer.h"
+#include "gtksourceview.h"
+
+#include "gtksourcevim.h"
+#include "gtksourcevimcommand.h"
+#include "gtksourcevimcommandbar.h"
+#include "gtksourceviminsert.h"
+#include "gtksourcevimnormal.h"
+#include "gtksourcevimreplace.h"
+#include "gtksourcevimvisual.h"
+
+struct _GtkSourceVim
+{
+       GtkSourceVimState  parent_instance;
+       GString           *command_text;
+       GtkSourceBuffer   *buffer;
+       guint              constrain_insert_source;
+       guint              in_handle_event : 1;
+};
+
+G_DEFINE_TYPE (GtkSourceVim, gtk_source_vim, GTK_SOURCE_TYPE_VIM_STATE)
+
+enum {
+       PROP_0,
+       PROP_COMMAND_TEXT,
+       PROP_COMMAND_BAR_TEXT,
+       N_PROPS
+};
+
+enum {
+       EXECUTE_COMMAND,
+       FORMAT,
+       READY,
+       SPLIT,
+       N_SIGNALS
+};
+
+static GParamSpec *properties[N_PROPS];
+static guint signals[N_SIGNALS];
+
+GtkSourceVim *
+gtk_source_vim_new (GtkSourceView *view)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIEW (view), NULL);
+
+       return g_object_new (GTK_SOURCE_TYPE_VIM,
+                            "view", view,
+                            NULL);
+}
+
+static gboolean
+gtk_source_vim_handle_event (GtkSourceVimState *state,
+                             GdkEvent          *event)
+{
+       GtkSourceVim *self = (GtkSourceVim *)state;
+       GtkSourceVimState *current;
+       gboolean ret = FALSE;
+
+       g_assert (GTK_SOURCE_IS_VIM (self));
+       g_assert (event != NULL);
+
+       self->in_handle_event = TRUE;
+
+       g_clear_handle_id (&self->constrain_insert_source, g_source_remove);
+
+       current = gtk_source_vim_state_get_current (state);
+       if (current == state)
+               goto finish;
+
+       ret = gtk_source_vim_state_handle_event (current, event);
+
+       g_string_truncate (self->command_text, 0);
+       gtk_source_vim_state_append_command (state, self->command_text);
+       g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_COMMAND_TEXT]);
+       g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_COMMAND_BAR_TEXT]);
+
+finish:
+       self->in_handle_event = FALSE;
+
+       return ret;
+}
+
+static gboolean
+constrain_insert_source (gpointer data)
+{
+       GtkSourceVim *self = data;
+       GtkSourceVimState *current;
+       GtkSourceBuffer *buffer;
+       GtkTextIter iter, selection;
+
+       self->constrain_insert_source = 0;
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, &selection);
+       current = gtk_source_vim_state_get_current (GTK_SOURCE_VIM_STATE (self));
+
+       if (!GTK_SOURCE_IS_VIM_INSERT (current) &&
+           !GTK_SOURCE_IS_VIM_REPLACE (current) &&
+           !gtk_text_buffer_get_has_selection (GTK_TEXT_BUFFER (buffer)))
+       {
+               if (gtk_text_iter_ends_line (&iter) &&
+                   !gtk_text_iter_starts_line (&iter))
+               {
+                       gtk_text_iter_backward_char (&iter);
+                       gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer), &iter, &iter);
+               }
+       }
+       else if (GTK_SOURCE_IS_VIM_NORMAL (current) &&
+                gtk_text_buffer_get_has_selection (GTK_TEXT_BUFFER (buffer)))
+       {
+               GtkSourceVimState *visual;
+
+               /* Enter visual mode */
+               visual = gtk_source_vim_visual_new (GTK_SOURCE_VIM_VISUAL_CHAR);
+               gtk_source_vim_state_push (current, visual);
+               g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_COMMAND_TEXT]);
+               g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_COMMAND_BAR_TEXT]);
+       }
+
+       return G_SOURCE_REMOVE;
+}
+
+static void
+on_cursor_moved_cb (GtkSourceVim    *self,
+                    GtkSourceBuffer *buffer)
+{
+       g_assert (GTK_SOURCE_IS_VIM (self));
+       g_assert (GTK_SOURCE_IS_BUFFER (buffer));
+
+       if (self->in_handle_event)
+               return;
+
+       /* Make sure we are placed on a character instead of on a \n
+        * which is possible when the user clicks with a button or other
+        * external tools. Don't do it until an idle callback though so
+        * that we don't affect anything currently processing.
+        */
+       if (self->constrain_insert_source == 0)
+       {
+               self->constrain_insert_source = g_idle_add (constrain_insert_source, self);
+       }
+}
+
+static void
+on_notify_buffer_cb (GtkSourceVim  *self,
+                     GParamSpec    *pspec,
+                     GtkSourceView *view)
+{
+       GtkSourceBuffer *buffer;
+
+       g_assert (GTK_SOURCE_IS_VIM (self));
+       g_assert (GTK_SOURCE_IS_VIEW (view));
+
+       buffer = GTK_SOURCE_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)));
+
+       if (self->buffer == buffer)
+               return;
+
+       if (self->buffer != NULL)
+       {
+               g_signal_handlers_disconnect_by_func (self->buffer,
+                                                     G_CALLBACK (on_cursor_moved_cb),
+                                                     self);
+               g_clear_object (&self->buffer);
+       }
+
+       g_set_object (&self->buffer, buffer);
+
+       if (buffer != NULL)
+       {
+               g_signal_connect_object (buffer,
+                                        "cursor-moved",
+                                        G_CALLBACK (on_cursor_moved_cb),
+                                        self,
+                                        G_CONNECT_SWAPPED);
+               on_cursor_moved_cb (self, buffer);
+       }
+}
+
+static void
+gtk_source_vim_view_set (GtkSourceVimState *state)
+{
+       GtkSourceVim *self = (GtkSourceVim *)state;
+       GtkSourceView *view;
+       GtkTextIter iter;
+
+       g_assert (GTK_SOURCE_IS_VIM (self));
+       g_assert (gtk_source_vim_state_get_child (state) == NULL);
+
+       view = gtk_source_vim_state_get_view (state);
+       gtk_source_vim_state_get_buffer (state, &iter, NULL);
+
+       g_signal_connect_object (view,
+                                "notify::buffer",
+                                G_CALLBACK (on_notify_buffer_cb),
+                                self,
+                                G_CONNECT_SWAPPED);
+       on_notify_buffer_cb (self, NULL, view);
+
+       gtk_source_vim_state_push_jump (state, &iter);
+
+       gtk_source_vim_state_push (state, gtk_source_vim_normal_new ());
+}
+
+static void
+gtk_source_vim_resume (GtkSourceVimState *state,
+                       GtkSourceVimState *from)
+{
+       GtkSourceView *view;
+
+       g_assert (GTK_SOURCE_IS_VIM (state));
+
+       if (!(view = gtk_source_vim_state_get_view (state)))
+               return;
+
+       gtk_text_view_set_overwrite (GTK_TEXT_VIEW (view), FALSE);
+}
+
+static void
+gtk_source_vim_dispose (GObject *object)
+{
+       GtkSourceVim *self = (GtkSourceVim *)object;
+
+       g_clear_handle_id (&self->constrain_insert_source, g_source_remove);
+       g_clear_object (&self->buffer);
+
+       if (self->command_text != NULL)
+       {
+               g_string_free (self->command_text, TRUE);
+               self->command_text = NULL;
+       }
+
+       G_OBJECT_CLASS (gtk_source_vim_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_vim_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+       GtkSourceVim *self = GTK_SOURCE_VIM (object);
+
+       switch (prop_id)
+       {
+               case PROP_COMMAND_TEXT:
+                       g_value_set_string (value, gtk_source_vim_get_command_text (self));
+                       break;
+
+               case PROP_COMMAND_BAR_TEXT:
+                       g_value_set_string (value, gtk_source_vim_get_command_bar_text (self));
+                       break;
+
+               default:
+                       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+       }
+}
+
+static void
+gtk_source_vim_class_init (GtkSourceVimClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       GtkSourceVimStateClass *state_class = GTK_SOURCE_VIM_STATE_CLASS (klass);
+
+       object_class->dispose = gtk_source_vim_dispose;
+       object_class->get_property = gtk_source_vim_get_property;
+
+       state_class->handle_event = gtk_source_vim_handle_event;
+       state_class->view_set = gtk_source_vim_view_set;
+       state_class->resume = gtk_source_vim_resume;
+
+       properties [PROP_COMMAND_TEXT] =
+               g_param_spec_string ("command-text",
+                                    "Command Text",
+                                    "Command Text",
+                                    NULL,
+                                    (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+       properties [PROP_COMMAND_BAR_TEXT] =
+               g_param_spec_string ("command-bar-text",
+                                    "Command Bar Text",
+                                    "Command Bar Text",
+                                    NULL,
+                                    (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_properties (object_class, N_PROPS, properties);
+
+       /**
+        * GtkSourceVim::execute-command:
+        * @self: a #GtkSourceVim
+        * @command: the command to execute
+        *
+        * The "execute-command" signal is emitted when the user has requested
+        * a command to be executed from the command bar (or possibly other
+        * VIM commands internally).
+        *
+        * If the command is something GtkSourceVim can handle internally,
+        * it will do so. Otherwise the application is responsible for
+        * handling it.
+        *
+        * Returns: %TRUE if handled, otherwise %FALSE.
+        */
+       signals[EXECUTE_COMMAND] =
+               g_signal_new_class_handler ("execute-command",
+                                           G_TYPE_FROM_CLASS (klass),
+                                           G_SIGNAL_RUN_LAST,
+                                           NULL,
+                                           g_signal_accumulator_true_handled, NULL,
+                                           NULL,
+                                           G_TYPE_BOOLEAN,
+                                           1,
+                                           G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+
+       /**
+        * GtkSourceVim::format:
+        * @self: a #GtkSourceVim
+        * @begin: the beginning of the text range
+        * @end: the end of the text range
+        *
+        * Requests that the text range @begin to @end be reformatted.
+        * Applications should conntect to this signal to implement
+        * reformatting as they would like.
+        */
+       signals[FORMAT] =
+               g_signal_new ("format",
+                             G_TYPE_FROM_CLASS (klass),
+                             G_SIGNAL_RUN_LAST,
+                             0, NULL, NULL, NULL,
+                             G_TYPE_NONE, 2, GTK_TYPE_TEXT_ITER, GTK_TYPE_TEXT_ITER);
+
+       signals[READY] =
+               g_signal_new ("ready",
+                             G_TYPE_FROM_CLASS (klass),
+                             G_SIGNAL_RUN_LAST,
+                             0,
+                             NULL, NULL,
+                             NULL,
+                             G_TYPE_NONE, 0);
+
+       /**
+        * GtkSourceVim::split:
+        * @self: a #GtkSourceVim
+        * @orientation: a #GtkOrientation for vertical or horizontal
+        * @new_document: %TRUE if a new document should be created
+        * @focus_split: %TRUE if the new document should be focused
+        * @numeric: a numeric value provided with the command such as
+        *   the number of columns for the split
+        */
+       signals[SPLIT] =
+               g_signal_new ("split",
+                             G_TYPE_FROM_CLASS (klass),
+                             G_SIGNAL_RUN_LAST,
+                             0,
+                             NULL, NULL,
+                             NULL,
+                             G_TYPE_NONE,
+                             3,
+                             GTK_TYPE_ORIENTATION,
+                             G_TYPE_BOOLEAN,
+                             G_TYPE_BOOLEAN,
+                             G_TYPE_INT);
+}
+
+static void
+gtk_source_vim_init (GtkSourceVim *self)
+{
+       self->command_text = g_string_new (NULL);
+}
+
+const char *
+gtk_source_vim_get_command_text (GtkSourceVim *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM (self), NULL);
+
+       return self->command_text->str;
+}
+
+const char *
+gtk_source_vim_get_command_bar_text (GtkSourceVim *self)
+{
+       GtkSourceVimState *current;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM (self), NULL);
+
+       current = gtk_source_vim_state_get_current (GTK_SOURCE_VIM_STATE (self));
+
+       while (current != NULL)
+       {
+               if (GTK_SOURCE_IS_VIM_COMMAND_BAR (current))
+               {
+                       return gtk_source_vim_command_bar_get_text (GTK_SOURCE_VIM_COMMAND_BAR (current));
+               }
+
+               if (GTK_SOURCE_VIM_STATE_GET_CLASS (current)->get_command_bar_text)
+               {
+                       return GTK_SOURCE_VIM_STATE_GET_CLASS (current)->get_command_bar_text (current);
+               }
+
+               if (GTK_SOURCE_VIM_STATE_GET_CLASS (current)->command_bar_text)
+               {
+                       return GTK_SOURCE_VIM_STATE_GET_CLASS (current)->command_bar_text;
+               }
+
+               current = gtk_source_vim_state_get_parent (current);
+       }
+
+       return "";
+}
+
+void
+gtk_source_vim_emit_split (GtkSourceVim   *self,
+                           GtkOrientation  orientation,
+                           gboolean        new_document,
+                           gboolean        focus_split,
+                           int             numeric)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM (self));
+
+       g_signal_emit (self, signals[SPLIT], 0,
+                      orientation, new_document, focus_split, numeric);
+}
+
+void
+gtk_source_vim_reset (GtkSourceVim *self)
+{
+       GtkSourceVimState *current;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM (self));
+
+       /* Clear everything up to the top-most Normal mode */
+       while ((current = gtk_source_vim_state_get_current (GTK_SOURCE_VIM_STATE (self))))
+       {
+               GtkSourceVimState *parent = gtk_source_vim_state_get_parent (current);
+
+               if (parent == NULL || parent == GTK_SOURCE_VIM_STATE (self))
+                       break;
+
+               gtk_source_vim_state_pop (current);
+       }
+
+       current = gtk_source_vim_state_get_current (GTK_SOURCE_VIM_STATE (self));
+
+       /* If we found the normal mode (should always happen), then
+        * also tell it to clear anything in progress.
+        */
+       if (GTK_SOURCE_IS_VIM_NORMAL (current))
+       {
+               gtk_source_vim_normal_clear (GTK_SOURCE_VIM_NORMAL (current));
+       }
+}
+
+gboolean
+gtk_source_vim_emit_execute_command (GtkSourceVim *self,
+                                     const char   *command)
+{
+       gboolean ret = FALSE;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM (self), FALSE);
+
+       g_signal_emit (self, signals[EXECUTE_COMMAND], 0, command, &ret);
+
+       return ret;
+}
+
+void
+gtk_source_vim_emit_ready (GtkSourceVim *self)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM (self));
+
+       g_signal_emit (self, signals[READY], 0);
+}
+
+void
+gtk_source_vim_emit_format (GtkSourceVim *self,
+                            GtkTextIter  *begin,
+                            GtkTextIter  *end)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM (self));
+       g_return_if_fail (begin != NULL);
+       g_return_if_fail (end != NULL);
+
+       g_signal_emit (self, signals[FORMAT], 0, begin, end);
+}
diff --git a/gtksourceview/vim/gtksourcevim.h b/gtksourceview/vim/gtksourcevim.h
new file mode 100644
index 00000000..2f48c1fa
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevim.h
@@ -0,0 +1,48 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include "gtksourcevimstate.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_VIM (gtk_source_vim_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceVim, gtk_source_vim, GTK_SOURCE, VIM, GtkSourceVimState)
+
+GtkSourceVim *gtk_source_vim_new                  (GtkSourceView  *view);
+void          gtk_source_vim_reset                (GtkSourceVim   *self);
+const char   *gtk_source_vim_get_command_text     (GtkSourceVim   *self);
+const char   *gtk_source_vim_get_command_bar_text (GtkSourceVim   *self);
+gboolean      gtk_source_vim_emit_execute_command (GtkSourceVim   *self,
+                                                   const char     *command);
+void          gtk_source_vim_emit_format          (GtkSourceVim   *self,
+                                                   GtkTextIter    *begin,
+                                                   GtkTextIter    *end);
+void          gtk_source_vim_emit_ready           (GtkSourceVim   *self);
+void          gtk_source_vim_emit_split           (GtkSourceVim   *self,
+                                                   GtkOrientation  orientation,
+                                                   gboolean        new_document,
+                                                   gboolean        focus_split,
+                                                   int             numeric);
+
+G_END_DECLS
diff --git a/gtksourceview/vim/gtksourcevimcharpending.c b/gtksourceview/vim/gtksourcevimcharpending.c
new file mode 100644
index 00000000..230c4b05
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimcharpending.c
@@ -0,0 +1,101 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "gtksourcevimcharpending.h"
+#include "gtksourceviminsertliteral.h"
+
+struct _GtkSourceVimCharPending
+{
+       GtkSourceVimState parent_class;
+       gunichar character;
+       char string[16];
+};
+
+G_DEFINE_TYPE (GtkSourceVimCharPending, gtk_source_vim_char_pending, GTK_SOURCE_TYPE_VIM_STATE)
+
+static gboolean
+gtk_source_vim_char_pending_handle_keypress (GtkSourceVimState *state,
+                                             guint              keyval,
+                                             guint              keycode,
+                                             GdkModifierType    mods,
+                                             const char        *string)
+{
+       GtkSourceVimCharPending *self = (GtkSourceVimCharPending *)state;
+
+       g_assert (GTK_SOURCE_IS_VIM_CHAR_PENDING (self));
+
+       if (gtk_source_vim_state_is_escape (keyval, mods))
+       {
+               goto completed;
+       }
+
+       gtk_source_vim_state_keyval_unescaped (keyval, mods, self->string);
+
+       if (self->string[0] != 0)
+       {
+               if ((self->string[0] & 0x80) == 0x80)
+                       self->character = g_utf8_get_char (self->string);
+               else
+                       self->character = self->string[0];
+       }
+
+completed:
+       gtk_source_vim_state_pop (state);
+
+       return TRUE;
+}
+
+static void
+gtk_source_vim_char_pending_class_init (GtkSourceVimCharPendingClass *klass)
+{
+       GtkSourceVimStateClass *state_class = GTK_SOURCE_VIM_STATE_CLASS (klass);
+
+       state_class->handle_keypress = gtk_source_vim_char_pending_handle_keypress;
+}
+
+static void
+gtk_source_vim_char_pending_init (GtkSourceVimCharPending *self)
+{
+}
+
+GtkSourceVimState *
+gtk_source_vim_char_pending_new (void)
+{
+       return g_object_new (GTK_SOURCE_TYPE_VIM_CHAR_PENDING, NULL);
+}
+
+gunichar
+gtk_source_vim_char_pending_get_character (GtkSourceVimCharPending *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_CHAR_PENDING (self), 0);
+
+       return self->character;
+}
+
+const char *
+gtk_source_vim_char_pending_get_string (GtkSourceVimCharPending *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_CHAR_PENDING (self), NULL);
+
+       return self->string;
+}
diff --git a/gtksourceview/vim/gtksourcevimcharpending.h b/gtksourceview/vim/gtksourcevimcharpending.h
new file mode 100644
index 00000000..0e8c8ea7
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimcharpending.h
@@ -0,0 +1,36 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include "gtksourcevimstate.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_VIM_CHAR_PENDING (gtk_source_vim_char_pending_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceVimCharPending, gtk_source_vim_char_pending, GTK_SOURCE, VIM_CHAR_PENDING, 
GtkSourceVimState)
+
+GtkSourceVimState *gtk_source_vim_char_pending_new           (void);
+gunichar           gtk_source_vim_char_pending_get_character (GtkSourceVimCharPending *self);
+const char        *gtk_source_vim_char_pending_get_string    (GtkSourceVimCharPending *self);
+
+G_END_DECLS
diff --git a/gtksourceview/vim/gtksourcevimcommand.c b/gtksourceview/vim/gtksourcevimcommand.c
new file mode 100644
index 00000000..b0cce9f0
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimcommand.c
@@ -0,0 +1,1737 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include <gtksourceview/gtksourcebuffer.h>
+#include <gtksourceview/gtksourcelanguagemanager.h>
+#include <gtksourceview/gtksourcelanguage.h>
+#include <gtksourceview/gtksourcesearchcontext.h>
+#include <gtksourceview/gtksourcesearchsettings.h>
+#include <gtksourceview/gtksourcestyleschememanager.h>
+#include <gtksourceview/gtksourcestylescheme.h>
+#include <gtksourceview/gtksourceview.h>
+
+#include "gtksourcevim.h"
+#include "gtksourcevimcharpending.h"
+#include "gtksourcevimcommand.h"
+#include "gtksourcevimjumplist.h"
+#include "gtksourcevimregisters.h"
+
+typedef void (*Command) (GtkSourceVimCommand *self);
+
+struct _GtkSourceVimCommand
+{
+       GtkSourceVimState       parent_instance;
+
+       GtkSourceVimMotion     *motion;
+       GtkSourceVimMotion     *selection_motion;
+       GtkSourceVimTextObject *text_object;
+       GtkTextMark            *mark_begin;
+       GtkTextMark            *mark_end;
+       char                   *command;
+       char                   *options;
+       char                    char_pending[16];
+
+       guint                   ignore_mark : 1;
+};
+
+G_DEFINE_TYPE (GtkSourceVimCommand, gtk_source_vim_command, GTK_SOURCE_TYPE_VIM_STATE)
+
+enum {
+       PROP_0,
+       PROP_COMMAND,
+       PROP_MOTION,
+       PROP_SELECTION_MOTION,
+       N_PROPS
+};
+
+static GParamSpec *properties[N_PROPS];
+static GHashTable *commands;
+static GPtrArray *commands_sorted;
+
+static const struct {
+       const char *ft;
+       const char *id;
+} ft_mappings[] = {
+       { "cs", "c-sharp" },
+       { "docbk", "docbook" },
+       { "javascript", "js" },
+       { "lhaskell", "haskell-literate" },
+       { "spec", "rpmspec" },
+       { "tex", "latex" },
+       { "xhtml", "html" },
+};
+
+static inline gboolean
+parse_number (const char *str,
+              int        *number)
+{
+       gint64 out_num;
+
+       if (str == NULL)
+               return FALSE;
+       else if (!g_ascii_string_to_signed (str, 10, 0, G_MAXINT, &out_num, NULL))
+               return FALSE;
+       *number = out_num;
+       return TRUE;
+}
+
+static void
+gtk_source_vim_command_format (GtkSourceVimCommand *self)
+{
+       GtkSourceVimState *root;
+       GtkSourceBuffer *buffer;
+       GtkTextIter iter;
+       GtkTextIter selection;
+
+       if (!gtk_source_vim_state_get_editable (GTK_SOURCE_VIM_STATE (self)))
+               return;
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, &selection);
+       root = gtk_source_vim_state_get_root (GTK_SOURCE_VIM_STATE (self));
+
+       if (GTK_SOURCE_IS_VIM (root))
+       {
+               gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (buffer));
+               gtk_source_vim_emit_format (GTK_SOURCE_VIM (root), &iter, &selection);
+               gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (buffer));
+
+               gtk_text_iter_order (&iter, &selection);
+
+               gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer), &iter, &iter);
+       }
+
+       self->ignore_mark = TRUE;
+}
+
+static void
+gtk_source_vim_command_shift (GtkSourceVimCommand *self,
+                              int                  direction)
+{
+       GtkSourceBuffer *buffer;
+       GtkSourceView *view;
+       GtkTextIter iter, selection;
+       int count;
+
+       if (!gtk_source_vim_state_get_editable (GTK_SOURCE_VIM_STATE (self)))
+               return;
+
+       gtk_source_vim_state_set_can_repeat (GTK_SOURCE_VIM_STATE (self), TRUE);
+
+       count = gtk_source_vim_state_get_count (GTK_SOURCE_VIM_STATE (self));
+
+       if (count == 0)
+       {
+               return;
+       }
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, &selection);
+       view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (self));
+
+       gtk_text_iter_order (&iter, &selection);
+
+       gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (buffer));
+
+       for (int i = 0; i < count; i++)
+       {
+
+               if (direction > 0)
+                       gtk_source_view_indent_lines (view, &iter, &selection);
+               else
+                       gtk_source_view_unindent_lines (view, &iter, &selection);
+       }
+
+       gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (buffer));
+
+       gtk_text_iter_set_line_offset (&iter, 0);
+       while (!gtk_text_iter_ends_line (&iter) &&
+              g_unichar_isspace (gtk_text_iter_get_char (&iter)))
+               gtk_text_iter_forward_char (&iter);
+
+       gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer), &iter, &iter);
+
+       self->ignore_mark = TRUE;
+}
+
+static void
+gtk_source_vim_command_indent (GtkSourceVimCommand *self)
+{
+       return gtk_source_vim_command_shift (self, 1);
+}
+
+static void
+gtk_source_vim_command_unindent (GtkSourceVimCommand *self)
+{
+       return gtk_source_vim_command_shift (self, -1);
+}
+
+static void
+gtk_source_vim_command_delete (GtkSourceVimCommand *self)
+{
+       GtkSourceBuffer *buffer;
+       GtkTextIter iter, selection;
+       char *text;
+
+       if (!gtk_source_vim_state_get_editable (GTK_SOURCE_VIM_STATE (self)))
+               return;
+
+       gtk_source_vim_state_set_can_repeat (GTK_SOURCE_VIM_STATE (self), TRUE);
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, &selection);
+       text = gtk_text_iter_get_slice (&iter, &selection);
+
+       if (gtk_text_iter_is_end (&selection) || gtk_text_iter_is_end (&iter))
+       {
+               char *tmp = text;
+               text = g_strdup_printf ("%s\n", tmp);
+               g_free (tmp);
+       }
+
+       gtk_source_vim_state_set_current_register_value (GTK_SOURCE_VIM_STATE (self), text);
+
+       gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (buffer));
+       gtk_text_buffer_delete (GTK_TEXT_BUFFER (buffer), &iter, &selection);
+       gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (buffer));
+
+       g_free (text);
+}
+
+static void
+gtk_source_vim_command_join (GtkSourceVimCommand *self)
+{
+       GtkSourceBuffer *buffer;
+       GtkTextIter iter;
+       GtkTextIter selection;
+       GtkTextIter end;
+       guint offset;
+
+       if (!gtk_source_vim_state_get_editable (GTK_SOURCE_VIM_STATE (self)))
+               return;
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, &selection);
+
+       gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (buffer));
+
+       gtk_text_iter_order (&iter, &selection);
+       offset = gtk_text_iter_get_offset (&iter);
+
+       end = iter;
+       if (!gtk_text_iter_ends_line (&end))
+               gtk_text_iter_forward_to_line_end (&end);
+       offset = gtk_text_iter_get_offset (&end);
+
+       gtk_source_buffer_join_lines (buffer, &iter, &selection);
+       gtk_text_buffer_get_iter_at_offset (GTK_TEXT_BUFFER (buffer), &iter, offset);
+       gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer), &iter, &iter);
+
+       gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (buffer));
+
+       gtk_source_vim_state_set_can_repeat (GTK_SOURCE_VIM_STATE (self), TRUE);
+
+       self->ignore_mark = TRUE;
+}
+
+static void
+gtk_source_vim_command_yank (GtkSourceVimCommand *self)
+{
+       GtkTextIter iter;
+       GtkTextIter selection;
+       char *text;
+
+       gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, &selection);
+       text = gtk_text_iter_get_slice (&iter, &selection);
+
+       if (gtk_text_iter_is_end (&iter) || gtk_text_iter_is_end (&selection))
+       {
+               char *tmp = text;
+               text = g_strdup_printf ("%s\n", tmp);
+               g_free (tmp);
+       }
+
+       gtk_source_vim_state_set_current_register_value (GTK_SOURCE_VIM_STATE (self), text);
+
+       g_free (text);
+}
+
+static void
+gtk_source_vim_command_paste_after (GtkSourceVimCommand *self)
+{
+       GtkSourceBuffer *buffer;
+       GtkTextIter iter;
+       GtkTextIter selection;
+       const char *text;
+       int count;
+
+       if (!gtk_source_vim_state_get_editable (GTK_SOURCE_VIM_STATE (self)))
+               return;
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, &selection);
+       text = gtk_source_vim_state_get_current_register_value (GTK_SOURCE_VIM_STATE (self));
+       count = gtk_source_vim_state_get_count (GTK_SOURCE_VIM_STATE (self));
+
+       if (text == NULL)
+       {
+               return;
+       }
+
+       gtk_text_iter_order (&selection, &iter);
+
+       gtk_source_vim_state_begin_user_action (GTK_SOURCE_VIM_STATE (self));
+
+       /* If there is a \n, this is a linewise paste */
+       if (g_str_has_suffix (text, "\n"))
+       {
+               int offset = -1;
+
+               do
+               {
+                       if (!gtk_text_iter_ends_line (&iter))
+                               gtk_text_iter_forward_to_line_end (&iter);
+
+                       gtk_text_buffer_insert (GTK_TEXT_BUFFER (buffer), &iter, "\n", -1);
+
+                       /* Save to place cursor later */
+                       if (offset == -1)
+                               offset = gtk_text_iter_get_offset (&iter);
+
+                       gtk_text_buffer_insert (GTK_TEXT_BUFFER (buffer), &iter, text, strlen (text) - 1);
+               } while (--count > 0);
+
+               /* try to place cursor in same position as vim */
+               gtk_text_buffer_get_iter_at_offset (GTK_TEXT_BUFFER (buffer), &iter, offset);
+               gtk_source_vim_state_select (GTK_SOURCE_VIM_STATE (self), &iter, &iter);
+               self->ignore_mark = TRUE;
+       }
+       else
+       {
+               if (!gtk_text_iter_ends_line (&iter))
+               {
+                       gtk_text_iter_forward_char (&iter);
+               }
+
+               do
+               {
+                       gtk_text_buffer_insert (GTK_TEXT_BUFFER (buffer), &iter, text, -1);
+               } while (--count > 0);
+       }
+
+       gtk_source_vim_state_end_user_action (GTK_SOURCE_VIM_STATE (self));
+}
+
+static void
+gtk_source_vim_command_paste_before (GtkSourceVimCommand *self)
+{
+       GtkSourceBuffer *buffer;
+       GtkTextIter iter;
+       GtkTextIter selection;
+       const char *text;
+       int count;
+
+       if (!gtk_source_vim_state_get_editable (GTK_SOURCE_VIM_STATE (self)))
+               return;
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, &selection);
+       text = gtk_source_vim_state_get_current_register_value (GTK_SOURCE_VIM_STATE (self));
+       count = gtk_source_vim_state_get_count (GTK_SOURCE_VIM_STATE (self));
+
+       if (text == NULL)
+       {
+               return;
+       }
+
+       gtk_text_iter_order (&selection, &iter);
+
+       gtk_source_vim_state_begin_user_action (GTK_SOURCE_VIM_STATE (self));
+
+       /* If there is a \n, this is a linewise paste */
+       if (g_str_has_suffix (text, "\n"))
+       {
+               int offset;
+
+               gtk_text_iter_set_line_offset (&iter, 0);
+               offset = gtk_text_iter_get_offset (&iter);
+
+               do
+               {
+                       gtk_text_buffer_insert (GTK_TEXT_BUFFER (buffer), &iter, text, -1);
+               } while (--count > 0);
+
+               /* try to place cursor in same position as vim */
+               gtk_text_buffer_get_iter_at_offset (GTK_TEXT_BUFFER (buffer), &iter, offset);
+               gtk_source_vim_state_select (GTK_SOURCE_VIM_STATE (self), &iter, &iter);
+               self->ignore_mark = TRUE;
+       }
+       else
+       {
+               do
+               {
+                       gtk_text_buffer_insert (GTK_TEXT_BUFFER (buffer), &iter, text, -1);
+               } while (--count > 0);
+       }
+
+       gtk_source_vim_state_end_user_action (GTK_SOURCE_VIM_STATE (self));
+}
+
+static void
+gtk_source_vim_command_toggle_case (GtkSourceVimCommand *self)
+{
+       GtkSourceBuffer *buffer;
+       GtkTextIter iter;
+       GtkTextIter selection;
+
+       if (!gtk_source_vim_state_get_editable (GTK_SOURCE_VIM_STATE (self)))
+               return;
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, &selection);
+
+       gtk_source_vim_state_begin_user_action (GTK_SOURCE_VIM_STATE (self));
+       gtk_source_buffer_change_case (buffer, GTK_SOURCE_CHANGE_CASE_TOGGLE, &iter, &selection);
+       gtk_source_vim_state_end_user_action (GTK_SOURCE_VIM_STATE (self));
+
+       if (gtk_text_iter_ends_line (&iter) &&
+           !gtk_text_iter_starts_line (&iter))
+       {
+               gtk_text_iter_backward_char (&iter);
+               gtk_source_vim_state_select (GTK_SOURCE_VIM_STATE (self), &iter, &iter);
+       }
+
+       gtk_source_vim_state_set_can_repeat (GTK_SOURCE_VIM_STATE (self), TRUE);
+
+       self->ignore_mark = TRUE;
+}
+
+static void
+gtk_source_vim_command_change_case (GtkSourceVimCommand     *self,
+                                    GtkSourceChangeCaseType  case_type)
+{
+       GtkSourceBuffer *buffer;
+       GtkTextIter iter;
+       GtkTextIter selection;
+
+       if (!gtk_source_vim_state_get_editable (GTK_SOURCE_VIM_STATE (self)))
+               return;
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, &selection);
+
+       gtk_text_iter_order (&iter, &selection);
+
+       gtk_source_vim_state_begin_user_action (GTK_SOURCE_VIM_STATE (self));
+       gtk_source_buffer_change_case (buffer, case_type, &iter, &selection);
+       gtk_source_vim_state_end_user_action (GTK_SOURCE_VIM_STATE (self));
+
+       gtk_source_vim_state_select (GTK_SOURCE_VIM_STATE (self), &iter, &iter);
+
+       gtk_source_vim_state_set_can_repeat (GTK_SOURCE_VIM_STATE (self), TRUE);
+
+       self->ignore_mark = TRUE;
+}
+
+static void
+gtk_source_vim_command_upcase (GtkSourceVimCommand *self)
+{
+       return gtk_source_vim_command_change_case (self, GTK_SOURCE_CHANGE_CASE_UPPER);
+}
+
+static void
+gtk_source_vim_command_downcase (GtkSourceVimCommand *self)
+{
+       return gtk_source_vim_command_change_case (self, GTK_SOURCE_CHANGE_CASE_LOWER);
+}
+
+static char *
+rot13 (const char *str)
+{
+       GString *ret = g_string_new (NULL);
+
+       for (const char *c = str; *c; c = g_utf8_next_char (c))
+       {
+               gunichar ch = g_utf8_get_char (c);
+
+               if ((ch >= 65 && ch <= 90) || (ch >= 97 && ch <= 122))
+               {
+                       if (g_ascii_tolower (ch) < 'n')
+                               g_string_append_c (ret, ch + 13);
+                       else
+                               g_string_append_c (ret, ch - 13);
+               }
+               else
+               {
+                       g_string_append_unichar (ret, ch);
+               }
+       }
+
+       return g_string_free (ret, FALSE);
+}
+
+static void
+gtk_source_vim_command_rot13 (GtkSourceVimCommand *self)
+{
+       GtkSourceBuffer *buffer;
+       GtkTextIter iter;
+       GtkTextIter selection;
+       char *text;
+       char *new_text;
+
+       if (!gtk_source_vim_state_get_editable (GTK_SOURCE_VIM_STATE (self)))
+               return;
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, &selection);
+       text = gtk_text_iter_get_slice (&iter, &selection);
+       new_text = rot13 (text);
+
+       gtk_source_vim_state_begin_user_action (GTK_SOURCE_VIM_STATE (self));
+       gtk_text_buffer_delete (GTK_TEXT_BUFFER (buffer), &iter, &selection);
+       gtk_text_buffer_insert (GTK_TEXT_BUFFER (buffer), &iter, new_text, -1);
+       gtk_source_vim_state_end_user_action (GTK_SOURCE_VIM_STATE (self));
+
+       gtk_source_vim_state_set_can_repeat (GTK_SOURCE_VIM_STATE (self), TRUE);
+
+       g_free (text);
+       g_free (new_text);
+}
+
+static char *
+replace_chars_with (const char *text,
+                    const char *replacement)
+{
+       GString *str;
+       gsize len;
+
+       g_assert (text != NULL);
+       g_assert (replacement != NULL);
+
+       str = g_string_new (NULL);
+       len = strlen (replacement);
+
+       for (const char *c = text; *c; c = g_utf8_next_char (c))
+       {
+               if (*c == '\n')
+                       g_string_append_c (str, '\n');
+               else
+                       g_string_append_len (str, replacement, len);
+       }
+
+       return g_string_free (str, FALSE);
+}
+
+static void
+gtk_source_vim_command_replace_one (GtkSourceVimCommand *self)
+{
+       GtkTextIter iter, selection;
+       GtkSourceBuffer *buffer;
+       char *text;
+       char *new_text;
+
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND (self));
+
+       if (!gtk_source_vim_state_get_editable (GTK_SOURCE_VIM_STATE (self)))
+               return;
+
+       if (self->char_pending[0] == 0)
+       {
+               return;
+       }
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, &selection);
+       text = gtk_text_iter_get_slice (&iter, &selection);
+       new_text = replace_chars_with (text, self->char_pending);
+
+       gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (buffer));
+       gtk_text_buffer_delete (GTK_TEXT_BUFFER (buffer), &iter, &selection);
+       gtk_text_buffer_insert (GTK_TEXT_BUFFER (buffer), &iter, new_text, -1);
+       gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (buffer));
+
+       if (self->motion != NULL &&
+           !gtk_source_vim_motion_is_linewise (self->motion))
+       {
+               gtk_text_iter_backward_char (&iter);
+               gtk_source_vim_state_select (GTK_SOURCE_VIM_STATE (self), &iter, &iter);
+               self->ignore_mark = TRUE;
+       }
+
+       g_free (text);
+       g_free (new_text);
+}
+
+static void
+gtk_source_vim_command_undo (GtkSourceVimCommand *self)
+{
+       GtkSourceBuffer *buffer;
+       int count;
+
+       if (!gtk_source_vim_state_get_editable (GTK_SOURCE_VIM_STATE (self)))
+               return;
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), NULL, NULL);
+       count = gtk_source_vim_state_get_count (GTK_SOURCE_VIM_STATE (self));
+
+       do
+       {
+               if (!gtk_text_buffer_get_can_undo (GTK_TEXT_BUFFER (buffer)))
+                       break;
+
+               gtk_text_buffer_undo (GTK_TEXT_BUFFER (buffer));
+       } while (--count > 0);
+}
+
+static void
+gtk_source_vim_command_redo (GtkSourceVimCommand *self)
+{
+       GtkSourceBuffer *buffer;
+       int count;
+
+       if (!gtk_source_vim_state_get_editable (GTK_SOURCE_VIM_STATE (self)))
+               return;
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), NULL, NULL);
+       count = gtk_source_vim_state_get_count (GTK_SOURCE_VIM_STATE (self));
+
+       do
+       {
+               if (!gtk_text_buffer_get_can_redo (GTK_TEXT_BUFFER (buffer)))
+                       break;
+
+               gtk_text_buffer_redo (GTK_TEXT_BUFFER (buffer));
+       } while (--count > 0);
+}
+
+static void
+gtk_source_vim_command_colorscheme (GtkSourceVimCommand *self)
+{
+       GtkSourceStyleSchemeManager *manager;
+       GtkSourceStyleScheme *scheme;
+       GtkSourceBuffer *buffer;
+       char *stripped;
+
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND (self));
+
+       if (self->options == NULL)
+               return;
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), NULL, NULL);
+       manager = gtk_source_style_scheme_manager_get_default ();
+       stripped = g_strstrip (g_strdup (self->options));
+       scheme = gtk_source_style_scheme_manager_get_scheme (manager, stripped);
+
+       if (scheme != NULL)
+       {
+               gtk_source_buffer_set_style_scheme (buffer, scheme);
+       }
+
+       g_free (stripped);
+}
+
+static void
+gtk_source_vim_command_nohl (GtkSourceVimCommand *self)
+{
+       GtkSourceSearchContext *context;
+
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND (self));
+
+       gtk_source_vim_state_get_search (GTK_SOURCE_VIM_STATE (self), NULL, &context);
+       gtk_source_search_context_set_highlight (context, FALSE);
+}
+
+static void
+gtk_source_vim_command_search (GtkSourceVimCommand *self)
+{
+       GtkSourceSearchContext *context;
+       GtkSourceSearchSettings *settings;
+       GtkSourceBuffer *buffer;
+       GtkSourceView *view;
+       GtkTextIter iter, selection;
+       GtkTextIter match;
+       GRegex *regex;
+
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND (self));
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, &selection);
+       view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (self));
+
+       gtk_source_vim_state_set_reverse_search (GTK_SOURCE_VIM_STATE (self), FALSE);
+       gtk_source_vim_state_get_search (GTK_SOURCE_VIM_STATE (self), &settings, &context);
+
+       if ((regex = g_regex_new (self->options, 0, 0, NULL)))
+       {
+               gtk_source_search_settings_set_search_text (settings, self->options);
+               gtk_source_search_settings_set_regex_enabled (settings, TRUE);
+               g_regex_unref (regex);
+       }
+       else
+       {
+               gtk_source_search_settings_set_regex_enabled (settings, FALSE);
+               gtk_source_search_settings_set_search_text (settings, self->options);
+       }
+
+       gtk_source_search_settings_set_case_sensitive (settings, TRUE);
+       gtk_source_search_settings_set_at_word_boundaries (settings, FALSE);
+       gtk_source_search_context_set_highlight (context, TRUE);
+
+       if (gtk_source_search_context_forward (context, &iter, &match, NULL, NULL))
+       {
+               gtk_source_vim_state_push_jump (GTK_SOURCE_VIM_STATE (self), &iter);
+               gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer), &match, &match);
+               gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (view), &match, 0.25, TRUE, 1.0, 0.0);
+
+               self->ignore_mark = TRUE;
+       }
+       else
+       {
+               gtk_source_search_context_set_highlight (context, FALSE);
+       }
+}
+
+static void
+gtk_source_vim_command_search_reverse (GtkSourceVimCommand *self)
+{
+       GtkSourceSearchContext *context;
+       GtkSourceSearchSettings *settings;
+       GtkSourceBuffer *buffer;
+       GtkSourceView *view;
+       GtkTextIter iter, selection;
+       GtkTextIter match;
+       GRegex *regex;
+
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND (self));
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, &selection);
+       view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (self));
+
+       gtk_source_vim_state_set_reverse_search (GTK_SOURCE_VIM_STATE (self), TRUE);
+       gtk_source_vim_state_get_search (GTK_SOURCE_VIM_STATE (self), &settings, &context);
+
+       if ((regex = g_regex_new (self->options, 0, 0, NULL)))
+       {
+               gtk_source_search_settings_set_search_text (settings, self->options);
+               gtk_source_search_settings_set_regex_enabled (settings, TRUE);
+               g_regex_unref (regex);
+       }
+       else
+       {
+               gtk_source_search_settings_set_regex_enabled (settings, FALSE);
+               gtk_source_search_settings_set_search_text (settings, self->options);
+       }
+
+       gtk_source_search_settings_set_case_sensitive (settings, TRUE);
+       gtk_source_search_settings_set_at_word_boundaries (settings, FALSE);
+       gtk_source_search_context_set_highlight (context, TRUE);
+
+       gtk_text_iter_backward_char (&iter);
+
+       if (gtk_source_search_context_backward (context, &iter, &match, NULL, NULL))
+       {
+               gtk_source_vim_state_push_jump (GTK_SOURCE_VIM_STATE (self), &iter);
+               gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer), &match, &match);
+               gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (view), &match, 0.25, TRUE, 1.0, 0.0);
+
+               self->ignore_mark = TRUE;
+       }
+       else
+       {
+               gtk_source_search_context_set_highlight (context, FALSE);
+       }
+}
+
+static void
+gtk_source_vim_command_line_number (GtkSourceVimCommand *self)
+{
+       int line;
+
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND (self));
+
+       if (parse_number (self->options, &line))
+       {
+               GtkSourceBuffer *buffer;
+               GtkSourceView *view;
+               GtkTextIter iter;
+
+               if (line > 0)
+                       line--;
+
+               view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (self));
+               buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, NULL);
+
+               gtk_source_vim_state_push_jump (GTK_SOURCE_VIM_STATE (self), &iter);
+
+               gtk_text_buffer_get_iter_at_line (GTK_TEXT_BUFFER (buffer), &iter, line);
+               while (!gtk_text_iter_ends_line (&iter) &&
+                      g_unichar_isspace (gtk_text_iter_get_char (&iter)))
+                       gtk_text_iter_forward_char (&iter);
+
+               gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer), &iter, &iter);
+               gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (view), &iter, 0.25, TRUE, 1.0, 0.0);
+
+               self->ignore_mark = TRUE;
+       }
+}
+
+gboolean
+gtk_source_vim_command_parse_search_and_replace (const char  *str,
+                                                 char       **search,
+                                                 char       **replace,
+                                                 char       **options)
+{
+       const char *c;
+       GString *build = NULL;
+       gunichar sep;
+       gboolean escaped;
+
+       g_assert (search != NULL);
+       g_assert (replace != NULL);
+       g_assert (options != NULL);
+
+       *search = NULL;
+       *replace = NULL;
+       *options = NULL;
+
+       if (str == NULL || *str == 0)
+               return FALSE;
+
+       sep = g_utf8_get_char (str);
+       str = g_utf8_next_char (str);
+
+       /* Check for something like "s/" */
+       if (*str == 0)
+               return TRUE;
+
+       build = g_string_new (NULL);
+       escaped = FALSE;
+       for (c = str; *c; c = g_utf8_next_char (c))
+       {
+               gunichar ch = g_utf8_get_char (c);
+
+               if (escaped)
+               {
+                       escaped = FALSE;
+
+                       if (ch == sep)
+                       {
+                               /* don't escape separator in output string */
+                               g_string_truncate (build, build->len - 1);
+                       }
+               }
+               else if (ch == '\\')
+               {
+                       escaped = TRUE;
+               }
+               else if (ch == sep)
+               {
+                       *search = g_string_free (g_steal_pointer (&build), FALSE);
+                       str = g_utf8_next_char (c);
+                       break;
+               }
+
+               g_string_append_unichar (build, ch);
+       }
+
+       if (escaped)
+               return FALSE;
+
+       /* Handle s/foobar (imply //) */
+       if (build != NULL)
+       {
+               *search = g_string_free (g_steal_pointer (&build), FALSE);
+               return TRUE;
+       }
+
+       if (*str == 0)
+               return TRUE;
+
+       build = g_string_new (NULL);
+       escaped = FALSE;
+       for (c = str; *c; c = g_utf8_next_char (c))
+       {
+               gunichar ch = g_utf8_get_char (c);
+
+               if (escaped)
+               {
+                       escaped = FALSE;
+
+                       if (ch == sep)
+                       {
+                               /* don't escape separator in output string */
+                               g_string_truncate (build, build->len - 1);
+                       }
+               }
+               else if (ch == '\\')
+               {
+                       escaped = TRUE;
+               }
+               else if (ch == sep)
+               {
+                       *replace = g_string_free (g_steal_pointer (&build), FALSE);
+                       str = g_utf8_next_char (c);
+                       break;
+               }
+
+               g_string_append_unichar (build, ch);
+       }
+
+       if (escaped)
+               return FALSE;
+
+       /* Handle s/foo/bar (imply trailing /) */
+       if (build != NULL)
+       {
+               *replace = g_string_free (g_steal_pointer (&build), FALSE);
+               return TRUE;
+       }
+
+       if (*str != 0)
+               *options = g_strdup (str);
+
+       return TRUE;
+}
+
+static void
+gtk_source_vim_command_search_replace (GtkSourceVimCommand *self)
+{
+       GtkSourceSearchSettings *settings = NULL;
+       GtkSourceSearchContext *context = NULL;
+       GtkSourceBuffer *buffer;
+       GtkTextIter iter;
+       GtkTextIter match_start;
+       GtkTextIter match_end;
+       const char *replace_str;
+       char *search = NULL;
+       char *replace = NULL;
+       char *options = NULL;
+       gboolean wrapped = FALSE;
+       gboolean flag_g = FALSE;
+       gboolean flag_i = FALSE;
+       gboolean found_match = FALSE;
+       guint line = 0;
+       int last_line;
+
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND (self));
+
+       if (!gtk_source_vim_command_parse_search_and_replace (self->options, &search, &replace, &options))
+               goto cleanup;
+
+       if (search == NULL || search[0] == 0)
+               goto cleanup;
+
+       replace_str = replace ? replace : "";
+
+       for (const char *c = options ? options : ""; *c; c = g_utf8_next_char (c))
+       {
+               flag_g |= *c == 'g';
+               flag_i |= *c == 'i';
+       }
+
+       gtk_source_vim_state_get_search (GTK_SOURCE_VIM_STATE (self), &settings, &context);
+       gtk_source_vim_state_set_reverse_search (GTK_SOURCE_VIM_STATE (self), FALSE);
+
+       gtk_source_search_settings_set_at_word_boundaries (settings, FALSE);
+       gtk_source_search_settings_set_regex_enabled (settings, TRUE);
+       gtk_source_search_settings_set_search_text (settings, search);
+       gtk_source_search_context_set_highlight (context, FALSE);
+       gtk_source_search_settings_set_case_sensitive (settings, !flag_i);
+
+       buffer = gtk_source_search_context_get_buffer (context);
+
+       if (self->mark_begin)
+               gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), &iter, self->mark_begin);
+       else
+               gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (buffer), &iter, NULL);
+
+       line = gtk_text_iter_get_line (&iter);
+       last_line = -1;
+
+       gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (buffer));
+
+       while (gtk_source_search_context_forward (context, &iter, &match_start, &match_end, &wrapped) && 
!wrapped)
+       {
+               guint cur_line = gtk_text_iter_get_line (&match_start);
+
+               if (!found_match)
+               {
+                       GtkTextIter cursor;
+
+                       gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &cursor, NULL);
+                       gtk_source_vim_state_push_jump (GTK_SOURCE_VIM_STATE (self), &cursor);
+
+                       found_match = TRUE;
+               }
+
+               if (self->mark_end)
+               {
+                       GtkTextIter end;
+                       gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), &end, self->mark_end);
+                       if (gtk_text_iter_compare (&match_start, &end) >= 0)
+                               break;
+               }
+               else if (gtk_text_iter_get_line (&match_start) != line)
+               {
+                       /* If we have no bounds, it's only the current line */
+                       break;
+               }
+
+               if (cur_line == last_line && !flag_g)
+               {
+                       goto next_result;
+               }
+
+               last_line = cur_line;
+               if (!gtk_source_search_context_replace (context, &match_start, &match_end, replace_str, -1, 
NULL))
+               {
+                       break;
+               }
+
+       next_result:
+               iter = match_end;
+               gtk_text_iter_forward_char (&iter);
+       }
+
+       gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (buffer));
+
+       if (last_line >= 0)
+       {
+               gtk_text_buffer_get_iter_at_line (GTK_TEXT_BUFFER (buffer), &iter, last_line);
+               while (!gtk_text_iter_ends_line (&iter) &&
+                      g_unichar_isspace (gtk_text_iter_get_char (&iter)))
+                       gtk_text_iter_forward_char (&iter);
+               gtk_source_vim_state_select (GTK_SOURCE_VIM_STATE (self), &iter, &iter);
+               self->ignore_mark = TRUE;
+       }
+
+cleanup:
+
+       g_free (search);
+       g_free (replace);
+       g_free (options);
+}
+
+static void
+gtk_source_vim_command_set (GtkSourceVimCommand *self)
+{
+       GtkSourceVimState *state = (GtkSourceVimState *)self;
+       GtkSourceSearchContext *context;
+       GtkSourceSearchSettings *search;
+       GtkSourceBuffer *buffer;
+       GtkSourceView *view;
+       g_auto(GStrv) parts = NULL;
+
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND (self));
+
+       if (self->options == NULL || g_strstrip (self->options)[0] == 0)
+       {
+               /* TODO: display current settings */
+               return;
+       }
+
+       view = gtk_source_vim_state_get_view (state);
+       buffer = GTK_SOURCE_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)));
+       parts = g_strsplit (self->options, " ", 0);
+
+       for (guint i = 0; parts[i]; i++)
+       {
+               const char *part = parts[i];
+
+               if (g_str_equal (part, "hls"))
+               {
+                       gtk_source_vim_state_get_search (state, &search, &context);
+                       gtk_source_search_context_set_highlight (context, TRUE);
+               }
+               else if (g_str_equal (part, "incsearch"))
+               {
+                       /* TODO */
+               }
+               else if (g_str_has_prefix (part, "ft=") ||
+                        g_str_has_prefix (part, "filetype="))
+               {
+                       const char *ft = strchr (part, '=') + 1;
+                       GtkSourceLanguageManager *manager;
+                       GtkSourceLanguage *language;
+
+                       for (guint j = 0; j < G_N_ELEMENTS (ft_mappings); j++)
+                       {
+                               if (g_str_equal (ft_mappings[j].ft, ft))
+                               {
+                                       ft = ft_mappings[j].id;
+                                       break;
+                               }
+                       }
+
+                       manager = gtk_source_language_manager_get_default ();
+                       language = gtk_source_language_manager_get_language (manager, ft);
+
+                       gtk_source_buffer_set_language (buffer, language);
+               }
+               else if (g_str_has_prefix (part, "ts=") ||
+                        g_str_has_prefix (part, "tabstop="))
+               {
+                       const char *ts = strchr (part, '=') + 1;
+                       int n;
+
+                       if (parse_number (ts, &n))
+                       {
+                               gtk_source_view_set_tab_width (view, CLAMP (n, -1, 32));
+                       }
+               }
+               else if (g_str_has_prefix (part, "sw=") ||
+                        g_str_has_prefix (part, "shiftwidth="))
+               {
+                       const char *sw = strchr (part, '=') + 1;
+                       int n;
+
+                       if (parse_number (sw, &n))
+                       {
+                               gtk_source_view_set_indent_width (view, CLAMP (n, -1, 32));
+                       }
+               }
+               else if (g_str_equal (part, "et") ||
+                        g_str_equal (part, "expandtab"))
+               {
+                       gtk_source_view_set_insert_spaces_instead_of_tabs (view, TRUE);
+               }
+               else if (g_str_equal (part, "noet") ||
+                        g_str_equal (part, "noexpandtab"))
+               {
+                       gtk_source_view_set_insert_spaces_instead_of_tabs (view, FALSE);
+               }
+       }
+}
+
+static void
+gtk_source_vim_command_append_command (GtkSourceVimState *state,
+                                       GString           *string)
+{
+       /* command should be empty during command */
+       g_string_truncate (string, 0);
+}
+
+static void
+gtk_source_vim_command_repeat (GtkSourceVimState *state)
+{
+       GtkSourceVimCommand *self = (GtkSourceVimCommand *)state;
+       GtkSourceBuffer *buffer;
+       Command command;
+       GtkTextIter iter;
+       GtkTextIter selection;
+       GtkTextMark *mark;
+       gboolean linewise = FALSE;
+
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND (self));
+
+       if (self->command == NULL ||
+           !(command = g_hash_table_lookup (commands, self->command)))
+       {
+               return;
+       }
+
+       buffer = gtk_source_vim_state_get_buffer (state, &iter, &selection);
+       mark = gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (buffer), NULL, &iter, TRUE);
+
+       if (self->text_object)
+       {
+               selection = iter;
+               gtk_source_vim_text_object_select (self->text_object, &iter, &selection);
+       }
+       else
+       {
+               if (self->motion)
+               {
+                       gtk_source_vim_motion_apply (self->motion, &iter, TRUE);
+                       linewise |= gtk_source_vim_motion_is_linewise (self->motion);
+               }
+
+               if (self->selection_motion)
+               {
+                       gtk_source_vim_motion_apply (self->selection_motion, &selection, TRUE);
+                       linewise |= gtk_source_vim_motion_is_linewise (self->selection_motion);
+               }
+       }
+
+       if (linewise)
+       {
+               gtk_source_vim_state_select_linewise (state, &iter, &selection);
+       }
+       else
+       {
+               gtk_source_vim_state_select (state, &iter, &selection);
+       }
+
+       command (self);
+
+       if (!self->ignore_mark)
+       {
+               gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), &iter, mark);
+               gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer), &iter, &iter);
+       }
+
+       gtk_text_buffer_delete_mark (GTK_TEXT_BUFFER (buffer), mark);
+}
+
+static void
+gtk_source_vim_command_jump_backward (GtkSourceVimCommand *self)
+{
+       GtkTextIter iter;
+
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND (self));
+
+       if (gtk_source_vim_state_jump_backward (GTK_SOURCE_VIM_STATE (self), &iter))
+       {
+               gtk_source_vim_state_select (GTK_SOURCE_VIM_STATE (self), &iter, &iter);
+               self->ignore_mark = TRUE;
+       }
+}
+
+static void
+gtk_source_vim_command_jump_forward (GtkSourceVimCommand *self)
+{
+       GtkTextIter iter;
+
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND (self));
+
+       if (gtk_source_vim_state_jump_forward (GTK_SOURCE_VIM_STATE (self), &iter))
+       {
+               gtk_source_vim_state_select (GTK_SOURCE_VIM_STATE (self), &iter, &iter);
+               self->ignore_mark = TRUE;
+       }
+}
+
+static void
+gtk_source_vim_command_enter (GtkSourceVimState *state)
+{
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND (state));
+
+}
+
+static void
+gtk_source_vim_command_leave (GtkSourceVimState *state)
+{
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND (state));
+
+       gtk_source_vim_command_repeat (state);
+}
+
+static void
+gtk_source_vim_command_resume (GtkSourceVimState *state,
+                               GtkSourceVimState *from)
+{
+       GtkSourceVimCommand *self = (GtkSourceVimCommand *)state;
+
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND (self));
+       g_assert (GTK_SOURCE_IS_VIM_STATE (from));
+
+       /* Complete if waiting for a motion */
+       if (GTK_SOURCE_IS_VIM_MOTION (from) && self->motion == NULL)
+       {
+               gtk_source_vim_state_reparent (from, state, &self->motion);
+               gtk_source_vim_state_pop (state);
+               return;
+       }
+
+       /* If we're waiting for a character, that could complete too */
+       if (GTK_SOURCE_IS_VIM_CHAR_PENDING (from))
+       {
+               gunichar ch = gtk_source_vim_char_pending_get_character (GTK_SOURCE_VIM_CHAR_PENDING (from));
+               const char *string = gtk_source_vim_char_pending_get_string (GTK_SOURCE_VIM_CHAR_PENDING 
(from));
+
+               if (ch && string && string[0])
+                       g_strlcpy (self->char_pending, string, sizeof self->char_pending);
+
+               gtk_source_vim_state_unparent (from);
+               gtk_source_vim_state_pop (state);
+               return;
+       }
+
+       gtk_source_vim_state_unparent (from);
+}
+
+static int
+sort_longest_first (gconstpointer a,
+                    gconstpointer b)
+{
+       const char * const *astr = a;
+       const char * const *bstr = b;
+       int lena = strlen (*astr);
+       int lenb = strlen (*bstr);
+
+       if (lena > lenb)
+               return -1;
+       else if (lena < lenb)
+               return 1;
+       return 0;
+}
+
+static void
+gtk_source_vim_command_dispose (GObject *object)
+{
+       GtkSourceVimCommand *self = (GtkSourceVimCommand *)object;
+       GtkTextBuffer *buffer;
+
+       if (self->mark_begin)
+       {
+               if ((buffer = gtk_text_mark_get_buffer (self->mark_begin)))
+                       gtk_text_buffer_delete_mark (buffer, self->mark_begin);
+               g_clear_weak_pointer (&self->mark_begin);
+       }
+
+       if (self->mark_end)
+       {
+               if ((buffer = gtk_text_mark_get_buffer (self->mark_end)))
+                       gtk_text_buffer_delete_mark (buffer, self->mark_end);
+               g_clear_weak_pointer (&self->mark_end);
+       }
+
+       gtk_source_vim_state_release (&self->motion);
+       gtk_source_vim_state_release (&self->selection_motion);
+       gtk_source_vim_state_release (&self->text_object);
+
+       g_clear_pointer (&self->command, g_free);
+       g_clear_pointer (&self->options, g_free);
+
+       G_OBJECT_CLASS (gtk_source_vim_command_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_vim_command_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+       GtkSourceVimCommand *self = GTK_SOURCE_VIM_COMMAND (object);
+
+       switch (prop_id)
+       {
+               case PROP_COMMAND:
+                       g_value_set_string (value, self->command);
+                       break;
+
+               case PROP_MOTION:
+                       g_value_set_object (value, self->motion);
+                       break;
+
+               case PROP_SELECTION_MOTION:
+                       g_value_set_object (value, self->selection_motion);
+                       break;
+
+               default:
+                       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+       }
+}
+
+static void
+gtk_source_vim_command_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+       GtkSourceVimCommand *self = GTK_SOURCE_VIM_COMMAND (object);
+
+       switch (prop_id)
+       {
+               case PROP_COMMAND:
+                       self->command = g_value_dup_string (value);
+                       break;
+
+               case PROP_MOTION:
+                       gtk_source_vim_command_set_motion (self, g_value_get_object (value));
+                       break;
+
+               case PROP_SELECTION_MOTION:
+                       gtk_source_vim_command_set_selection_motion (self, g_value_get_object (value));
+                       break;
+
+               default:
+                       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+       }
+}
+
+static void
+gtk_source_vim_command_class_init (GtkSourceVimCommandClass *klass)
+{
+       GtkSourceVimStateClass *state_class = GTK_SOURCE_VIM_STATE_CLASS (klass);
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->dispose = gtk_source_vim_command_dispose;
+       object_class->get_property = gtk_source_vim_command_get_property;
+       object_class->set_property = gtk_source_vim_command_set_property;
+
+       state_class->append_command = gtk_source_vim_command_append_command;
+       state_class->enter = gtk_source_vim_command_enter;
+       state_class->leave = gtk_source_vim_command_leave;
+       state_class->repeat = gtk_source_vim_command_repeat;
+       state_class->resume = gtk_source_vim_command_resume;
+
+       properties [PROP_COMMAND] =
+               g_param_spec_string ("command",
+                                    "Command",
+                                    "The command to run",
+                                    NULL,
+                                    (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+       properties [PROP_MOTION] =
+               g_param_spec_object ("motion",
+                                    "Motion",
+                                    "The motion for the insertion cursor",
+                                    GTK_SOURCE_TYPE_VIM_MOTION,
+                                    (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+       properties [PROP_SELECTION_MOTION] =
+               g_param_spec_object ("selection-motion",
+                                    "Seleciton Motion",
+                                    "The motion for the selection bound",
+                                    GTK_SOURCE_TYPE_VIM_MOTION,
+                                    (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_properties (object_class, N_PROPS, properties);
+
+       commands = g_hash_table_new (g_str_hash, g_str_equal);
+       commands_sorted = g_ptr_array_new ();
+
+#define ADD_COMMAND(name, func) \
+       G_STMT_START { \
+               g_hash_table_insert(commands, (char*)name, (gpointer)func); \
+               g_ptr_array_add (commands_sorted, (char *)name); \
+       } G_STMT_END
+       ADD_COMMAND (":colorscheme",   gtk_source_vim_command_colorscheme);
+       ADD_COMMAND (":delete",        gtk_source_vim_command_delete);
+       ADD_COMMAND (":j",             gtk_source_vim_command_join);
+       ADD_COMMAND (":join",          gtk_source_vim_command_join);
+       ADD_COMMAND (":nohl",          gtk_source_vim_command_nohl);
+       ADD_COMMAND (":redo",          gtk_source_vim_command_redo);
+       ADD_COMMAND (":set",           gtk_source_vim_command_set);
+       ADD_COMMAND (":u",             gtk_source_vim_command_undo);
+       ADD_COMMAND (":undo",          gtk_source_vim_command_undo);
+       ADD_COMMAND (":y",             gtk_source_vim_command_yank);
+       ADD_COMMAND (":yank",          gtk_source_vim_command_yank);
+       ADD_COMMAND ("paste-after",    gtk_source_vim_command_paste_after);
+       ADD_COMMAND ("paste-before",   gtk_source_vim_command_paste_before);
+       ADD_COMMAND ("toggle-case",    gtk_source_vim_command_toggle_case);
+       ADD_COMMAND ("upcase",         gtk_source_vim_command_upcase);
+       ADD_COMMAND ("downcase",       gtk_source_vim_command_downcase);
+       ADD_COMMAND ("rot13",          gtk_source_vim_command_rot13);
+       ADD_COMMAND ("replace-one",    gtk_source_vim_command_replace_one);
+       ADD_COMMAND ("indent",         gtk_source_vim_command_indent);
+       ADD_COMMAND ("unindent",       gtk_source_vim_command_unindent);
+       ADD_COMMAND ("line-number",    gtk_source_vim_command_line_number);
+       ADD_COMMAND ("format",         gtk_source_vim_command_format);
+       ADD_COMMAND ("search",         gtk_source_vim_command_search);
+       ADD_COMMAND ("search-replace", gtk_source_vim_command_search_replace);
+       ADD_COMMAND ("search-reverse", gtk_source_vim_command_search_reverse);
+       ADD_COMMAND ("jump-backward",  gtk_source_vim_command_jump_backward);
+       ADD_COMMAND ("jump-forward",   gtk_source_vim_command_jump_forward);
+#undef ADD_COMMAND
+
+       g_ptr_array_sort (commands_sorted, sort_longest_first);
+}
+
+static void
+gtk_source_vim_command_init (GtkSourceVimCommand *self)
+{
+}
+
+void
+gtk_source_vim_command_set_motion (GtkSourceVimCommand *self,
+                                   GtkSourceVimMotion  *motion)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM_COMMAND (self));
+       g_return_if_fail (!motion || GTK_SOURCE_IS_VIM_MOTION (motion));
+
+       gtk_source_vim_state_reparent (motion, self, &self->motion);
+       g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MOTION]);
+}
+
+void
+gtk_source_vim_command_set_selection_motion (GtkSourceVimCommand *self,
+                                             GtkSourceVimMotion  *selection_motion)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM_COMMAND (self));
+       g_return_if_fail (!selection_motion || GTK_SOURCE_IS_VIM_MOTION (selection_motion));
+
+       gtk_source_vim_state_reparent (selection_motion, self, &self->selection_motion);
+       g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SELECTION_MOTION]);
+}
+
+const char *
+gtk_source_vim_command_get_command (GtkSourceVimCommand *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_COMMAND (self), NULL);
+
+       return self->command;
+}
+
+GtkSourceVimState *
+gtk_source_vim_command_new (const char *command)
+{
+       g_return_val_if_fail (command != NULL, NULL);
+
+       return g_object_new (GTK_SOURCE_TYPE_VIM_COMMAND,
+                            "command", command,
+                            NULL);
+}
+
+void
+gtk_source_vim_command_set_text_object (GtkSourceVimCommand    *self,
+                                        GtkSourceVimTextObject *text_object)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM_COMMAND (self));
+
+       gtk_source_vim_state_reparent (text_object, self, &self->text_object);
+}
+
+static gboolean
+parse_position (GtkSourceVimState  *current,
+                const char        **str,
+                GtkTextIter        *iter)
+{
+       GtkTextBuffer *buffer;
+       const char *c;
+
+       g_assert (GTK_SOURCE_IS_VIM_STATE (current));
+       g_assert (str != NULL);
+       g_assert (*str != NULL);
+       g_assert (iter != NULL);
+
+       buffer = GTK_TEXT_BUFFER (gtk_source_vim_state_get_buffer (current, NULL, NULL));
+       c = *str;
+
+       if (*c == '\'')
+       {
+               GtkTextMark *mark;
+               char name[2];
+
+               c++;
+               name[0] = *c;
+               name[1] = 0;
+
+               if (!(mark = gtk_source_vim_state_get_mark (current, name)))
+                       return FALSE;
+
+               gtk_text_buffer_get_iter_at_mark (buffer, iter, mark);
+
+               /* As of Vim 7.3, substitutions applied to a range defined by
+                * marks or a visual selection (which uses a special type of
+                * marks '< and '>) are not bounded by the column position of
+                * the marks by default.
+                */
+               if (*c == '<' && !gtk_text_iter_starts_line (iter))
+               {
+                       gtk_text_iter_set_line_offset (iter, 0);
+               }
+               else if (*c == '>' && !gtk_text_iter_ends_line (iter))
+               {
+                       if (gtk_text_iter_starts_line (iter))
+                               gtk_text_iter_backward_char (iter);
+               }
+
+               *str = ++c;
+               return TRUE;
+       }
+       else if (*c == '.')
+       {
+               gtk_text_buffer_get_iter_at_mark (buffer, iter, gtk_text_buffer_get_insert (buffer));
+               gtk_text_iter_set_line_offset (iter, 0);
+               *str = ++c;
+               return TRUE;
+       }
+       else if (*c == '$')
+       {
+               gtk_text_buffer_get_end_iter (buffer, iter);
+               *str = ++c;
+               return TRUE;
+       }
+       else if (*c == '+' && g_ascii_isdigit (c[1]))
+       {
+               int number = 0;
+
+               for (++c; *c; c = g_utf8_next_char (c))
+               {
+                       if (!g_ascii_isdigit (*c))
+                               break;
+                       number = number * 10 + *c - '0';
+               }
+
+               gtk_text_buffer_get_iter_at_mark (buffer, iter, gtk_text_buffer_get_insert (buffer));
+               gtk_text_iter_forward_lines (iter, number);
+               if (!gtk_text_iter_ends_line (iter))
+                       gtk_text_iter_forward_to_line_end (iter);
+
+               *str = c;
+
+               return TRUE;
+       }
+       else if (g_ascii_isdigit (*c))
+       {
+               int number = 0;
+
+               for (; *c; c = g_utf8_next_char (c))
+               {
+                       if (!g_ascii_isdigit (*c))
+                               break;
+                       number = number * 10 + *c - '0';
+               }
+
+               if (number > 0)
+                       number--;
+
+               gtk_text_buffer_get_iter_at_line (buffer, iter, number);
+               *str = c;
+               return TRUE;
+       }
+
+       return FALSE;
+}
+
+static gboolean
+parse_range (GtkSourceVimState  *current,
+             const char        **cmdline_inout,
+             GtkTextIter        *begin,
+             GtkTextIter        *end)
+{
+       GtkSourceBuffer *buffer = gtk_source_vim_state_get_buffer (current, NULL, NULL);
+       const char *cmdline = *cmdline_inout;
+
+       if (cmdline[0] == '%')
+       {
+               gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (buffer), begin, end);
+               *cmdline_inout = ++cmdline;
+               return TRUE;
+       }
+
+       if (!parse_position (current, &cmdline, begin))
+               return FALSE;
+
+       if (*cmdline != ',')
+               return FALSE;
+
+       cmdline++;
+       if (!parse_position (current, &cmdline, end))
+               return FALSE;
+
+       *cmdline_inout = cmdline;
+
+       return TRUE;
+}
+
+GtkSourceVimState *
+gtk_source_vim_command_new_parsed (GtkSourceVimState *current,
+                                   const char        *command_line)
+{
+       GtkSourceVimCommand *ret = NULL;
+       GtkSourceVimCommandClass *klass;
+       GtkTextMark *mark_begin = NULL;
+       GtkTextMark *mark_end = NULL;
+       GtkTextIter begin;
+       GtkTextIter end;
+       char *key = NULL;
+       int number;
+
+       g_return_val_if_fail (command_line != NULL, NULL);
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (current), NULL);
+
+       klass = g_type_class_ref (GTK_SOURCE_TYPE_VIM_COMMAND);
+
+       if (*command_line == ':')
+       {
+               command_line++;
+       }
+
+       if (parse_range (current, &command_line, &begin, &end))
+       {
+               GtkSourceBuffer *buffer;
+
+               buffer = gtk_source_vim_state_get_buffer (current, NULL, NULL);
+               mark_begin = gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (buffer), NULL, &begin, TRUE);
+               mark_end = gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (buffer), NULL, &end, FALSE);
+       }
+
+       /* Try to locate command after parsing ranges */
+       key = g_strdup_printf (":%s", command_line);
+       if (g_hash_table_contains (commands, key))
+       {
+               ret = GTK_SOURCE_VIM_COMMAND (gtk_source_vim_command_new (key));
+               goto finish;
+       }
+
+       if (*command_line == '/')
+       {
+               ret = GTK_SOURCE_VIM_COMMAND (gtk_source_vim_command_new ("search"));
+               ret->options = g_strdup (command_line+1);
+
+               goto finish;
+       }
+       else if (*command_line == '?')
+       {
+               ret = GTK_SOURCE_VIM_COMMAND (gtk_source_vim_command_new ("search-reverse"));
+               ret->options = g_strdup (command_line+1);
+
+               goto finish;
+       }
+
+       if (strchr (command_line, ' '))
+       {
+               char **split = g_strsplit (command_line, " ", 2);
+               char *name = g_strdup_printf (":%s", split[0]);
+
+               if (g_hash_table_contains (commands, name))
+               {
+                       ret = GTK_SOURCE_VIM_COMMAND (gtk_source_vim_command_new (name));
+                       ret->options = g_strdup (split[1]);
+               }
+
+               g_strfreev (split);
+               g_free (name);
+
+               if (ret != NULL)
+                       goto finish;
+       }
+
+       if (parse_number (command_line, &number))
+       {
+               ret = GTK_SOURCE_VIM_COMMAND (gtk_source_vim_command_new ("line-number"));
+               ret->options = g_strdup (command_line);
+
+               goto finish;
+       }
+
+       if (*command_line == 's')
+       {
+               ret = GTK_SOURCE_VIM_COMMAND (gtk_source_vim_command_new ("search-replace"));
+               ret->options = g_strdup (command_line+1);
+
+               goto finish;
+       }
+
+finish:
+
+       if (ret != NULL)
+       {
+               g_set_weak_pointer (&ret->mark_begin, mark_begin);
+               g_set_weak_pointer (&ret->mark_end, mark_end);
+       }
+       else if (mark_begin || mark_end)
+       {
+               gtk_text_buffer_delete_mark (gtk_text_mark_get_buffer (mark_begin), mark_begin);
+               gtk_text_buffer_delete_mark (gtk_text_mark_get_buffer (mark_end), mark_end);
+       }
+
+       g_type_class_unref (klass);
+       g_free (key);
+
+       return GTK_SOURCE_VIM_STATE (ret);
+}
diff --git a/gtksourceview/vim/gtksourcevimcommand.h b/gtksourceview/vim/gtksourcevimcommand.h
new file mode 100644
index 00000000..634cac55
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimcommand.h
@@ -0,0 +1,49 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include "gtksourcevimmotion.h"
+#include "gtksourcevimstate.h"
+#include "gtksourcevimtextobject.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_VIM_COMMAND (gtk_source_vim_command_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceVimCommand, gtk_source_vim_command, GTK_SOURCE, VIM_COMMAND, 
GtkSourceVimState)
+
+GtkSourceVimState *gtk_source_vim_command_new                      (const char              *command);
+GtkSourceVimState *gtk_source_vim_command_new_parsed               (GtkSourceVimState       *current,
+                                                                    const char              *command_line);
+const char        *gtk_source_vim_command_get_command              (GtkSourceVimCommand     *self);
+void               gtk_source_vim_command_set_motion               (GtkSourceVimCommand     *self,
+                                                                    GtkSourceVimMotion      *motion);
+void               gtk_source_vim_command_set_selection_motion     (GtkSourceVimCommand     *self,
+                                                                    GtkSourceVimMotion      
*selection_motion);
+void               gtk_source_vim_command_set_text_object          (GtkSourceVimCommand     *self,
+                                                                    GtkSourceVimTextObject  *text_objet);
+gboolean           gtk_source_vim_command_parse_search_and_replace (const char              *str,
+                                                                    char                   **search,
+                                                                    char                   **replace,
+                                                                    char                   **options);
+
+G_END_DECLS
diff --git a/gtksourceview/vim/gtksourcevimcommandbar.c b/gtksourceview/vim/gtksourcevimcommandbar.c
new file mode 100644
index 00000000..b8c2cefc
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimcommandbar.c
@@ -0,0 +1,315 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "gtksourcevim.h"
+#include "gtksourcevimcommand.h"
+#include "gtksourcevimcommandbar.h"
+
+#define MAX_HISTORY 25
+
+struct _GtkSourceVimCommandBar
+{
+       GtkSourceVimState parent_instance;
+       GtkSourceVimCommand *command;
+       GString *buffer;
+       int history_pos;
+};
+
+G_DEFINE_TYPE (GtkSourceVimCommandBar, gtk_source_vim_command_bar, GTK_SOURCE_TYPE_VIM_STATE)
+
+static GPtrArray *history;
+
+GtkSourceVimState *
+gtk_source_vim_command_bar_new (void)
+{
+       return g_object_new (GTK_SOURCE_TYPE_VIM_COMMAND_BAR, NULL);
+}
+
+GtkSourceVimState *
+gtk_source_vim_command_bar_take_command (GtkSourceVimCommandBar *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_COMMAND_BAR (self), NULL);
+
+       return GTK_SOURCE_VIM_STATE (g_steal_pointer (&self->command));
+}
+
+static void
+gtk_source_vim_command_bar_dispose (GObject *object)
+{
+       GtkSourceVimCommandBar *self = (GtkSourceVimCommandBar *)object;
+
+       if (self->buffer == NULL)
+       {
+               g_string_free (self->buffer, TRUE);
+               self->buffer = NULL;
+       }
+
+       G_OBJECT_CLASS (gtk_source_vim_command_bar_parent_class)->dispose (object);
+}
+
+static void
+do_notify (GtkSourceVimCommandBar *self)
+{
+       GtkSourceVimState *root;
+
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND_BAR (self));
+
+       root = gtk_source_vim_state_get_root (GTK_SOURCE_VIM_STATE (self));
+
+       if (GTK_SOURCE_IS_VIM (root))
+       {
+               g_object_notify (G_OBJECT (root), "command-bar-text");
+       }
+}
+
+static void
+move_history (GtkSourceVimCommandBar *self,
+              int                     direction)
+{
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND_BAR (self));
+
+       if (history->len == 0)
+               return;
+
+       if (direction < 0)
+               self->history_pos--;
+       else
+               self->history_pos++;
+
+       if (self->history_pos < 0)
+               self->history_pos = history->len - 1;
+       else if (self->history_pos >= history->len)
+               self->history_pos = 0;
+
+       g_string_truncate (self->buffer, 0);
+       g_string_append (self->buffer, g_ptr_array_index (history, self->history_pos));
+}
+
+static void
+complete_command (GtkSourceVimCommandBar *self,
+                  const char             *prefix)
+{
+       static const char *commands[] = {
+               ":colorscheme",
+               ":write",
+               ":quit",
+               ":edit",
+               ":open",
+               ":file",
+               ":set",
+       };
+
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND_BAR (self));
+
+       for (guint i = 0; i < G_N_ELEMENTS (commands); i++)
+       {
+               if (g_str_has_prefix (commands[i], prefix))
+               {
+                       g_string_truncate (self->buffer, 0);
+                       g_string_append (self->buffer, commands[i]);
+                       g_string_append_c (self->buffer, ' ');
+                       break;
+               }
+       }
+}
+
+static void
+do_execute (GtkSourceVimCommandBar *self,
+            const char             *command)
+{
+       GtkSourceVimState *root;
+       GtkSourceVimState *new_state;
+
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND_BAR (self));
+       g_assert (command != NULL);
+
+       if (history->len > MAX_HISTORY)
+       {
+               g_ptr_array_set_size (history, MAX_HISTORY);
+       }
+
+       g_ptr_array_add (history, g_strdup (command));
+
+       root = gtk_source_vim_state_get_root (GTK_SOURCE_VIM_STATE (self));
+
+       if (GTK_SOURCE_IS_VIM (root))
+       {
+               if (gtk_source_vim_emit_execute_command (GTK_SOURCE_VIM (root), command))
+                       return;
+       }
+
+       if (!(new_state = gtk_source_vim_command_new_parsed (GTK_SOURCE_VIM_STATE (self), command)))
+               return;
+
+       gtk_source_vim_state_reparent (new_state, self, &self->command);
+       gtk_source_vim_state_repeat (new_state);
+       g_object_unref (new_state);
+}
+
+static gboolean
+gtk_source_vim_command_bar_handle_keypress (GtkSourceVimState *state,
+                                            guint              keyval,
+                                            guint              keycode,
+                                            GdkModifierType    mods,
+                                            const char        *str)
+{
+       GtkSourceVimCommandBar *self = (GtkSourceVimCommandBar *)state;
+
+       g_assert (GTK_SOURCE_IS_VIM_COMMAND_BAR (self));
+
+       if (gtk_source_vim_state_is_escape (keyval, mods))
+       {
+               g_string_truncate (self->buffer, 0);
+               do_notify (self);
+               gtk_source_vim_state_pop (state);
+               return TRUE;
+       }
+
+       switch (keyval)
+       {
+               case GDK_KEY_BackSpace:
+               {
+                       gsize len = g_utf8_strlen (self->buffer->str, -1);
+
+                       if (len > 1)
+                       {
+                               char *s = g_utf8_offset_to_pointer (self->buffer->str, len-1);
+                               g_string_truncate (self->buffer, s - self->buffer->str);
+                               do_notify (self);
+                       }
+
+                       return TRUE;
+               }
+
+               case GDK_KEY_Tab:
+               case GDK_KEY_KP_Tab:
+                       complete_command (self, self->buffer->str);
+                       return TRUE;
+
+               case GDK_KEY_Up:
+               case GDK_KEY_KP_Up:
+                       move_history (self, -1);
+                       return TRUE;
+
+               case GDK_KEY_Down:
+               case GDK_KEY_KP_Down:
+                       move_history (self, 1);
+                       return TRUE;
+
+               case GDK_KEY_Return:
+               case GDK_KEY_KP_Enter:
+               case GDK_KEY_ISO_Enter:
+                       do_execute (self, self->buffer->str);
+                       g_string_truncate (self->buffer, 0);
+                       do_notify (self);
+                       gtk_source_vim_state_pop (state);
+                       return TRUE;
+
+               default:
+                       break;
+       }
+
+       if (str[0])
+       {
+               g_string_append (self->buffer, str);
+               do_notify (self);
+       }
+
+       return TRUE;
+}
+
+static void
+gtk_source_vim_command_bar_enter (GtkSourceVimState *state)
+{
+       GtkSourceVimCommandBar *self = (GtkSourceVimCommandBar *)state;
+       GtkSourceView *view;
+
+       g_assert (GTK_SOURCE_VIM_STATE (self));
+
+       self->history_pos = 0;
+
+       if (self->buffer->len == 0)
+       {
+               g_string_append_c (self->buffer, ':');
+               do_notify (self);
+       }
+
+       view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (self));
+       gtk_text_view_set_cursor_visible (GTK_TEXT_VIEW (view), FALSE);
+}
+
+static void
+gtk_source_vim_command_bar_leave (GtkSourceVimState *state)
+{
+       GtkSourceVimCommandBar *self = (GtkSourceVimCommandBar *)state;
+       GtkSourceView *view;
+
+       g_assert (GTK_SOURCE_VIM_STATE (self));
+
+       g_string_truncate (self->buffer, 0);
+       do_notify (self);
+
+       view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (self));
+       gtk_text_view_set_cursor_visible (GTK_TEXT_VIEW (view), TRUE);
+}
+
+static void
+gtk_source_vim_command_bar_class_init (GtkSourceVimCommandBarClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       GtkSourceVimStateClass *state_class = GTK_SOURCE_VIM_STATE_CLASS (klass);
+
+       object_class->dispose = gtk_source_vim_command_bar_dispose;
+
+       state_class->enter = gtk_source_vim_command_bar_enter;
+       state_class->leave = gtk_source_vim_command_bar_leave;
+       state_class->handle_keypress = gtk_source_vim_command_bar_handle_keypress;
+
+       history = g_ptr_array_new_with_free_func (g_free);
+}
+
+static void
+gtk_source_vim_command_bar_init (GtkSourceVimCommandBar *self)
+{
+       self->buffer = g_string_new (NULL);
+}
+
+const char *
+gtk_source_vim_command_bar_get_text (GtkSourceVimCommandBar *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_COMMAND_BAR (self), NULL);
+
+       return self->buffer->str;
+}
+
+void
+gtk_source_vim_command_bar_set_text (GtkSourceVimCommandBar *self,
+                                     const char             *text)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM_COMMAND_BAR (self));
+
+       g_string_truncate (self->buffer, 0);
+       g_string_append (self->buffer, text);
+
+       do_notify (self);
+}
diff --git a/gtksourceview/vim/gtksourcevimcommandbar.h b/gtksourceview/vim/gtksourcevimcommandbar.h
new file mode 100644
index 00000000..dde076ec
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimcommandbar.h
@@ -0,0 +1,38 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include "gtksourcevimstate.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_VIM_COMMAND_BAR (gtk_source_vim_command_bar_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceVimCommandBar, gtk_source_vim_command_bar, GTK_SOURCE, VIM_COMMAND_BAR, 
GtkSourceVimState)
+
+GtkSourceVimState *gtk_source_vim_command_bar_new          (void);
+GtkSourceVimState *gtk_source_vim_command_bar_take_command (GtkSourceVimCommandBar *self);
+const char        *gtk_source_vim_command_bar_get_text     (GtkSourceVimCommandBar *self);
+void               gtk_source_vim_command_bar_set_text     (GtkSourceVimCommandBar *self,
+                                                            const char             *text);
+
+G_END_DECLS
diff --git a/gtksourceview/vim/gtksourceviminsert.c b/gtksourceview/vim/gtksourceviminsert.c
new file mode 100644
index 00000000..e73fc8b0
--- /dev/null
+++ b/gtksourceview/vim/gtksourceviminsert.c
@@ -0,0 +1,598 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "gtksourceindenter.h"
+#include "gtksourceview.h"
+
+#include "gtksourceviminsert.h"
+#include "gtksourceviminsertliteral.h"
+#include "gtksourcevimreplace.h"
+#include "gtksourcevimtexthistory.h"
+
+struct _GtkSourceVimInsert
+{
+       GtkSourceVimState        parent_instance;
+       GtkSourceVimTextHistory *history;
+       GtkSourceVimMotion      *motion;
+       GtkSourceVimMotion      *selection_motion;
+       GtkSourceVimTextObject  *text_object;
+       char                    *prefix;
+       char                    *suffix;
+       GtkSourceVimInsertAt     at;
+       guint                    indent : 1;
+       guint                    finished : 1;
+};
+
+G_DEFINE_TYPE (GtkSourceVimInsert, gtk_source_vim_insert, GTK_SOURCE_TYPE_VIM_STATE)
+
+enum {
+       PROP_0,
+       PROP_INDENT,
+       PROP_PREFIX,
+       PROP_SUFFIX,
+       N_PROPS
+};
+
+static GParamSpec *properties[N_PROPS];
+
+GtkSourceVimState *
+gtk_source_vim_insert_new (void)
+{
+       return g_object_new (GTK_SOURCE_TYPE_VIM_INSERT, NULL);
+}
+
+static gboolean
+clear_to_first_char (GtkSourceVimInsert *self)
+{
+       GtkSourceBuffer *buffer;
+       GtkTextIter insert;
+       GtkTextIter begin;
+
+       g_assert (GTK_SOURCE_IS_VIM_INSERT (self));
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &insert, NULL);
+       begin = insert;
+       gtk_text_iter_set_line_offset (&begin, 0);
+
+       while (gtk_text_iter_compare (&begin, &insert) < 0 &&
+              g_unichar_isspace (gtk_text_iter_get_char (&begin)))
+       {
+               gtk_text_iter_forward_char (&begin);
+       }
+
+       if (gtk_text_iter_equal (&begin, &insert))
+       {
+               gtk_text_iter_set_line_offset (&begin, 0);
+       }
+
+       gtk_text_buffer_delete (GTK_TEXT_BUFFER (buffer), &begin, &insert);
+
+       return TRUE;
+}
+
+static gboolean
+gtk_source_vim_insert_handle_keypress (GtkSourceVimState *state,
+                                       guint              keyval,
+                                       guint              keycode,
+                                       GdkModifierType    mods,
+                                       const char        *string)
+{
+       GtkSourceVimInsert *self = (GtkSourceVimInsert *)state;
+
+       g_assert (GTK_SOURCE_IS_VIM_STATE (state));
+       g_assert (string != NULL);
+
+       /* Leave insert mode if Escape,ctrl+[,ctrl+c was pressed */
+       if (gtk_source_vim_state_is_escape (keyval, mods) ||
+           gtk_source_vim_state_is_ctrl_c (keyval, mods))
+       {
+               gtk_source_vim_state_pop (GTK_SOURCE_VIM_STATE (self));
+               return TRUE;
+       }
+
+       /* Now handle our commands */
+       if ((mods & GDK_CONTROL_MASK) != 0)
+       {
+               switch (keyval)
+               {
+                       case GDK_KEY_u:
+                               return clear_to_first_char (self);
+
+                       case GDK_KEY_v:
+                               gtk_source_vim_state_push (state, gtk_source_vim_insert_literal_new ());
+                               return TRUE;
+
+                       default:
+                               break;
+               }
+       }
+
+       /* XXX: Currently we do not use overwrite mode while in insert even
+        * though that is the only way to get a block cursor. To do that we'd
+        * have to be able to commit text to the textview through the input
+        * method and we don't have a way to do that yet.
+        */
+
+       switch (keyval)
+       {
+               case GDK_KEY_Insert:
+                       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self),
+                                                  gtk_source_vim_replace_new ());
+                       return TRUE;
+
+               default:
+                       return FALSE;
+       }
+}
+
+static gboolean
+gtk_source_vim_insert_handle_event (GtkSourceVimState *state,
+                                    GdkEvent          *event)
+{
+       GtkSourceVimInsert *self = (GtkSourceVimInsert *)state;
+       GtkSourceView *view;
+       char string[16];
+       GdkModifierType mods;
+       guint keyval;
+       guint keycode;
+
+       g_assert (GTK_SOURCE_IS_VIM_INSERT (self));
+       g_assert (event != NULL);
+
+       if (!(view = gtk_source_vim_state_get_view (state)))
+               return FALSE;
+
+       keyval = gdk_key_event_get_keyval (event);
+       keycode = gdk_key_event_get_keycode (event);
+       mods = gdk_event_get_modifier_state (event)
+            & gtk_accelerator_get_default_mod_mask ();
+
+       /* Allow input methods to complete */
+       if (gtk_text_view_im_context_filter_keypress (GTK_TEXT_VIEW (view), event))
+               return TRUE;
+
+       /* Only deal with presses after this */
+       if (gdk_event_get_event_type (event) != GDK_KEY_PRESS)
+               return TRUE;
+
+
+       gtk_source_vim_state_keyval_to_string (keyval, mods, string);
+
+       return GTK_SOURCE_VIM_STATE_GET_CLASS (self)->handle_keypress (state, keyval, keycode, mods, string);
+}
+
+static void
+gtk_source_vim_insert_prepare (GtkSourceVimInsert *self)
+{
+       GtkSourceBuffer *buffer;
+       GtkSourceView *view;
+       GtkTextIter iter;
+       GtkTextIter selection;
+
+       g_assert (GTK_SOURCE_IS_VIM_INSERT (self));
+
+       view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (self));
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, &selection);
+
+       if (self->text_object)
+       {
+               selection = iter;
+               gtk_source_vim_text_object_select (self->text_object, &iter, &selection);
+       }
+       else
+       {
+               if (self->motion)
+               {
+                       gtk_source_vim_motion_apply (self->motion, &iter, TRUE);
+
+                       if (self->at == GTK_SOURCE_VIM_INSERT_AFTER_CHAR ||
+                           self->at == GTK_SOURCE_VIM_INSERT_AFTER_CHAR_UNLESS_BOF)
+                       {
+                               if (self->at == GTK_SOURCE_VIM_INSERT_AFTER_CHAR ||
+                                   (self->at == GTK_SOURCE_VIM_INSERT_AFTER_CHAR_UNLESS_BOF && 
!gtk_text_iter_is_start (&iter)) ||
+                                   (self->at == GTK_SOURCE_VIM_INSERT_AFTER_CHAR_UNLESS_SOL && 
!gtk_text_iter_starts_line (&iter)))
+                               {
+                                       if (!gtk_text_iter_ends_line (&iter))
+                                               gtk_text_iter_forward_char (&iter);
+                               }
+                       }
+
+                       if (self->selection_motion == NULL)
+                       {
+                               selection = iter;
+                       }
+               }
+
+               if (self->selection_motion)
+               {
+                       gtk_source_vim_motion_apply (self->selection_motion, &selection, TRUE);
+
+                       if (self->at == GTK_SOURCE_VIM_INSERT_AFTER_CHAR ||
+                           self->at == GTK_SOURCE_VIM_INSERT_AFTER_CHAR_UNLESS_BOF)
+                       {
+                               if (self->at == GTK_SOURCE_VIM_INSERT_AFTER_CHAR ||
+                                   (self->at == GTK_SOURCE_VIM_INSERT_AFTER_CHAR_UNLESS_BOF && 
!gtk_text_iter_is_start (&iter)) ||
+                                   (self->at == GTK_SOURCE_VIM_INSERT_AFTER_CHAR_UNLESS_SOL && 
!gtk_text_iter_starts_line (&iter)))
+                               {
+                                       if (!gtk_text_iter_ends_line (&selection))
+                                               gtk_text_iter_forward_char (&selection);
+                               }
+                       }
+               }
+       }
+
+       gtk_source_vim_state_select (GTK_SOURCE_VIM_STATE (self), &iter, &selection);
+
+       if (!gtk_text_iter_equal (&iter, &selection))
+       {
+               char *removed = gtk_text_iter_get_slice (&iter, &selection);
+
+               if (((self->text_object && gtk_source_vim_text_object_is_linewise (self->text_object)) ||
+                    (self->motion && gtk_source_vim_motion_is_linewise (self->motion))))
+               {
+                       char *tmp = removed;
+                       removed = g_strdup_printf ("%s\n", tmp);
+                       g_free (tmp);
+               }
+
+               gtk_source_vim_state_set_current_register_value (GTK_SOURCE_VIM_STATE (self), removed);
+               gtk_text_buffer_delete (GTK_TEXT_BUFFER (buffer), &iter, &selection);
+
+               g_free (removed);
+       }
+
+       if (self->suffix)
+       {
+               gsize len = g_utf8_strlen (self->suffix, -1);
+
+               if (len > 0)
+               {
+                       gtk_text_buffer_insert (GTK_TEXT_BUFFER (buffer), &iter, self->suffix, -1);
+                       gtk_text_iter_backward_chars (&iter, len);
+                       gtk_source_vim_state_select (GTK_SOURCE_VIM_STATE (self), &iter, &iter);
+                       selection = iter;
+               }
+       }
+
+       if (self->prefix)
+       {
+               gtk_text_buffer_insert (GTK_TEXT_BUFFER (buffer), &iter, self->prefix, -1);
+               gtk_source_vim_state_select (GTK_SOURCE_VIM_STATE (self), &iter, &iter);
+       }
+
+       if (self->indent && gtk_source_view_get_auto_indent (view))
+       {
+               GtkSourceIndenter *indenter = gtk_source_view_get_indenter (view);
+
+               if (indenter != NULL)
+               {
+                       gtk_source_indenter_indent (indenter, view, &iter);
+               }
+       }
+}
+
+static void
+gtk_source_vim_insert_resume (GtkSourceVimState *state,
+                              GtkSourceVimState *from)
+{
+       GtkSourceVimInsert *self = (GtkSourceVimInsert *)state;
+
+       g_assert (GTK_SOURCE_IS_VIM_INSERT (state));
+       g_assert (GTK_SOURCE_IS_VIM_STATE (from));
+
+       gtk_source_vim_state_set_overwrite (state, FALSE);
+
+       if (GTK_SOURCE_IS_VIM_MOTION (from) && self->motion == NULL)
+       {
+               gtk_source_vim_state_reparent (from, self, &self->motion);
+               gtk_source_vim_text_history_end (self->history);
+               gtk_source_vim_insert_prepare (self);
+               gtk_source_vim_text_history_begin (self->history);
+               return;
+       }
+       else if (GTK_SOURCE_IS_VIM_REPLACE (from))
+       {
+               /* If we are leaving replace mode back to insert then
+                * we need also exit insert mode so we end up back on
+                * Normal mode.
+                */
+               gtk_source_vim_state_unparent (from);
+               gtk_source_vim_state_pop (state);
+               return;
+       }
+
+       gtk_source_vim_state_unparent (from);
+}
+
+static void
+gtk_source_vim_insert_enter (GtkSourceVimState *state)
+{
+       GtkSourceVimInsert *self = (GtkSourceVimInsert *)state;
+       GtkSourceVimState *history;
+
+       g_assert (GTK_SOURCE_IS_VIM_INSERT (self));
+
+       gtk_source_vim_state_begin_user_action (state);
+       gtk_source_vim_state_set_overwrite (state, FALSE);
+
+       history = gtk_source_vim_text_history_new ();
+       gtk_source_vim_state_reparent (history, self, &self->history);
+       gtk_source_vim_insert_prepare (self);
+       gtk_source_vim_text_history_begin (self->history);
+
+       g_object_unref (history);
+}
+
+static void
+gtk_source_vim_insert_leave (GtkSourceVimState *state)
+{
+       GtkSourceVimInsert *self = (GtkSourceVimInsert *)state;
+       int count;
+
+       g_assert (GTK_SOURCE_IS_VIM_INSERT (self));
+
+       self->finished = TRUE;
+
+       gtk_source_vim_text_history_end (self->history);
+
+       count = gtk_source_vim_state_get_count (state);
+
+       while (--count > 0)
+       {
+               gtk_source_vim_insert_prepare (self);
+               gtk_source_vim_text_history_replay (self->history);
+       }
+
+       gtk_source_vim_state_end_user_action (state);
+}
+
+static void
+gtk_source_vim_insert_repeat (GtkSourceVimState *state)
+{
+       GtkSourceVimInsert *self = (GtkSourceVimInsert *)state;
+       int count;
+
+       g_assert (GTK_SOURCE_IS_VIM_INSERT (self));
+
+       count = gtk_source_vim_state_get_count (state);
+
+       gtk_source_vim_state_begin_user_action (state);
+
+       for (int i = 0; i < count; i++)
+       {
+               gtk_source_vim_insert_prepare (self);
+               gtk_source_vim_text_history_replay (self->history);
+       }
+
+       gtk_source_vim_state_end_user_action (state);
+}
+
+static void
+gtk_source_vim_insert_append_command (GtkSourceVimState *state,
+                                      GString           *string)
+{
+       /* command should be empty during insert */
+       g_string_truncate (string, 0);
+}
+
+static void
+gtk_source_vim_insert_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+       GtkSourceVimInsert *self = GTK_SOURCE_VIM_INSERT (object);
+
+       switch (prop_id)
+       {
+               case PROP_INDENT:
+                       g_value_set_boolean (value, self->indent);
+                       break;
+
+               case PROP_PREFIX:
+                       g_value_set_string (value, self->prefix);
+                       break;
+
+               case PROP_SUFFIX:
+                       g_value_set_string (value, self->suffix);
+                       break;
+
+               default:
+                       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+       }
+}
+
+static void
+gtk_source_vim_insert_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+       GtkSourceVimInsert *self = GTK_SOURCE_VIM_INSERT (object);
+
+       switch (prop_id)
+       {
+               case PROP_INDENT:
+                       gtk_source_vim_insert_set_indent (self, g_value_get_boolean (value));
+                       break;
+
+               case PROP_PREFIX:
+                       gtk_source_vim_insert_set_prefix (self, g_value_get_string (value));
+                       break;
+
+               case PROP_SUFFIX:
+                       gtk_source_vim_insert_set_suffix (self, g_value_get_string (value));
+                       break;
+
+               default:
+                       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+       }
+}
+
+static void
+gtk_source_vim_insert_dispose (GObject *object)
+{
+       GtkSourceVimInsert *self = (GtkSourceVimInsert *)object;
+
+       g_clear_pointer (&self->prefix, g_free);
+
+       gtk_source_vim_state_release (&self->history);
+       gtk_source_vim_state_release (&self->motion);
+       gtk_source_vim_state_release (&self->selection_motion);
+       gtk_source_vim_state_release (&self->text_object);
+
+       G_OBJECT_CLASS (gtk_source_vim_insert_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_vim_insert_class_init (GtkSourceVimInsertClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       GtkSourceVimStateClass *state_class = GTK_SOURCE_VIM_STATE_CLASS (klass);
+
+       object_class->dispose = gtk_source_vim_insert_dispose;
+       object_class->get_property = gtk_source_vim_insert_get_property;
+       object_class->set_property = gtk_source_vim_insert_set_property;
+
+       state_class->command_bar_text = _("-- INSERT --");
+       state_class->append_command = gtk_source_vim_insert_append_command;
+       state_class->handle_event = gtk_source_vim_insert_handle_event;
+       state_class->handle_keypress = gtk_source_vim_insert_handle_keypress;
+       state_class->resume = gtk_source_vim_insert_resume;
+       state_class->enter = gtk_source_vim_insert_enter;
+       state_class->leave = gtk_source_vim_insert_leave;
+       state_class->repeat = gtk_source_vim_insert_repeat;
+
+       properties [PROP_INDENT] =
+               g_param_spec_boolean ("indent",
+                                     "Indent",
+                                     "Indent after the prefix text",
+                                     FALSE,
+                                     (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+       properties [PROP_PREFIX] =
+               g_param_spec_string ("prefix",
+                                    "Prefix",
+                                    "Text to insert at the insertion cursor before entering insert mode",
+                                    NULL,
+                                    (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+       properties [PROP_SUFFIX] =
+               g_param_spec_string ("suffix",
+                                    "suffix",
+                                    "Text to insert after the insertion cursor before entering insert mode",
+                                    NULL,
+                                    (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gtk_source_vim_insert_init (GtkSourceVimInsert *self)
+{
+       self->at = GTK_SOURCE_VIM_INSERT_HERE;
+       gtk_source_vim_state_set_can_repeat (GTK_SOURCE_VIM_STATE (self), TRUE);
+}
+
+void
+gtk_source_vim_insert_set_prefix (GtkSourceVimInsert *self,
+                                  const char         *prefix)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM_INSERT (self));
+
+       if (g_strcmp0 (self->prefix, prefix) != 0)
+       {
+               g_free (self->prefix);
+               self->prefix = g_strdup (prefix);
+               g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PREFIX]);
+       }
+}
+
+void
+gtk_source_vim_insert_set_suffix (GtkSourceVimInsert *self,
+                                  const char         *suffix)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM_INSERT (self));
+
+       if (g_strcmp0 (self->suffix, suffix) != 0)
+       {
+               g_free (self->suffix);
+               self->suffix = g_strdup (suffix);
+               g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SUFFIX]);
+       }
+}
+
+void
+gtk_source_vim_insert_set_indent (GtkSourceVimInsert *self,
+                                  gboolean            indent)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM_INSERT (self));
+
+       indent = !!indent;
+
+       if (self->indent != indent)
+       {
+               self->indent = indent;
+               g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_INDENT]);
+       }
+}
+
+void
+gtk_source_vim_insert_set_motion (GtkSourceVimInsert *self,
+                                  GtkSourceVimMotion *motion)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM_INSERT (self));
+       g_return_if_fail (GTK_SOURCE_IS_VIM_MOTION (motion));
+
+       gtk_source_vim_state_reparent (motion, self, &self->motion);
+}
+
+void
+gtk_source_vim_insert_set_selection_motion (GtkSourceVimInsert *self,
+                                            GtkSourceVimMotion *selection_motion)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM_INSERT (self));
+       g_return_if_fail (GTK_SOURCE_IS_VIM_MOTION (selection_motion));
+
+       gtk_source_vim_state_reparent (selection_motion, self, &self->selection_motion);
+}
+
+void
+gtk_source_vim_insert_set_at (GtkSourceVimInsert   *self,
+                              GtkSourceVimInsertAt  at)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM_INSERT (self));
+
+       self->at = at;
+}
+
+void
+gtk_source_vim_insert_set_text_object (GtkSourceVimInsert     *self,
+                                       GtkSourceVimTextObject *text_object)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM_INSERT (self));
+
+       gtk_source_vim_state_reparent (text_object, self, &self->text_object);
+}
diff --git a/gtksourceview/vim/gtksourceviminsert.h b/gtksourceview/vim/gtksourceviminsert.h
new file mode 100644
index 00000000..1f9a114a
--- /dev/null
+++ b/gtksourceview/vim/gtksourceviminsert.h
@@ -0,0 +1,60 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include "gtksourcevimmotion.h"
+#include "gtksourcevimstate.h"
+#include "gtksourcevimtextobject.h"
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+
+       GTK_SOURCE_VIM_INSERT_HERE,
+       GTK_SOURCE_VIM_INSERT_AFTER_CHAR,
+       GTK_SOURCE_VIM_INSERT_AFTER_CHAR_UNLESS_BOF,
+       GTK_SOURCE_VIM_INSERT_AFTER_CHAR_UNLESS_SOL,
+} GtkSourceVimInsertAt;
+
+#define GTK_SOURCE_TYPE_VIM_INSERT (gtk_source_vim_insert_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceVimInsert, gtk_source_vim_insert, GTK_SOURCE, VIM_INSERT, GtkSourceVimState)
+
+GtkSourceVimState *gtk_source_vim_insert_new                  (void);
+void               gtk_source_vim_insert_set_at               (GtkSourceVimInsert     *self,
+                                                               GtkSourceVimInsertAt    at);
+void               gtk_source_vim_insert_set_motion           (GtkSourceVimInsert     *self,
+                                                               GtkSourceVimMotion     *motion);
+void               gtk_source_vim_insert_set_selection_motion (GtkSourceVimInsert     *self,
+                                                               GtkSourceVimMotion     *selection_motion);
+void               gtk_source_vim_insert_set_text_object      (GtkSourceVimInsert     *self,
+                                                               GtkSourceVimTextObject *text_object);
+void               gtk_source_vim_insert_set_indent           (GtkSourceVimInsert     *self,
+                                                               gboolean                indent);
+void               gtk_source_vim_insert_set_prefix           (GtkSourceVimInsert     *self,
+                                                               const char             *prefix);
+void               gtk_source_vim_insert_set_suffix           (GtkSourceVimInsert     *self,
+                                                               const char             *suffix);
+
+
+G_END_DECLS
diff --git a/gtksourceview/vim/gtksourceviminsertliteral.c b/gtksourceview/vim/gtksourceviminsertliteral.c
new file mode 100644
index 00000000..ab95c7f3
--- /dev/null
+++ b/gtksourceview/vim/gtksourceviminsertliteral.c
@@ -0,0 +1,101 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "gtksourceviminsertliteral.h"
+
+struct _GtkSourceVimInsertLiteral
+{
+       GtkSourceVimState parent_instance;
+};
+
+G_DEFINE_TYPE (GtkSourceVimInsertLiteral, gtk_source_vim_insert_literal, GTK_SOURCE_TYPE_VIM_STATE)
+
+GtkSourceVimState *
+gtk_source_vim_insert_literal_new (void)
+{
+       return g_object_new (GTK_SOURCE_TYPE_VIM_INSERT_LITERAL, NULL);
+}
+
+static gboolean
+do_literal (GtkSourceVimInsertLiteral *self,
+            const char                *string)
+{
+       g_assert (GTK_SOURCE_IS_VIM_INSERT_LITERAL (self));
+       g_assert (string != NULL);
+
+       if (string[0] != 0)
+       {
+               GtkSourceBuffer *buffer;
+               GtkSourceView *view;
+               GtkTextIter insert;
+
+               view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (self));
+               buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &insert, NULL);
+
+               if (gtk_text_view_get_overwrite (GTK_TEXT_VIEW (view)))
+               {
+                       GtkTextIter end = insert;
+
+                       if (gtk_text_iter_forward_char (&end))
+                       {
+                               gtk_text_buffer_delete (GTK_TEXT_BUFFER (buffer), &insert, &end);
+                       }
+               }
+
+               gtk_text_buffer_insert (GTK_TEXT_BUFFER (buffer), &insert, string, -1);
+       }
+
+       gtk_source_vim_state_pop (GTK_SOURCE_VIM_STATE (self));
+
+       return TRUE;
+}
+
+static gboolean
+gtk_source_vim_insert_literal_handle_keypress (GtkSourceVimState *state,
+                                               guint              keyval,
+                                               guint              keycode,
+                                               GdkModifierType    mods,
+                                               const char        *string)
+{
+       GtkSourceVimInsertLiteral *self = (GtkSourceVimInsertLiteral *)state;
+       char outbuf[16] = {0};
+
+       g_assert (GTK_SOURCE_IS_VIM_INSERT_LITERAL (self));
+
+       gtk_source_vim_state_keyval_unescaped (keyval, mods, outbuf);
+
+       return do_literal (self, outbuf);
+}
+
+static void
+gtk_source_vim_insert_literal_class_init (GtkSourceVimInsertLiteralClass *klass)
+{
+       GtkSourceVimStateClass *state_class = GTK_SOURCE_VIM_STATE_CLASS (klass);
+
+       state_class->handle_keypress = gtk_source_vim_insert_literal_handle_keypress;
+}
+
+static void
+gtk_source_vim_insert_literal_init (GtkSourceVimInsertLiteral *self)
+{
+}
diff --git a/gtksourceview/vim/gtksourceviminsertliteral.h b/gtksourceview/vim/gtksourceviminsertliteral.h
new file mode 100644
index 00000000..8b2308dc
--- /dev/null
+++ b/gtksourceview/vim/gtksourceviminsertliteral.h
@@ -0,0 +1,34 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include "gtksourcevimstate.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_VIM_INSERT_LITERAL (gtk_source_vim_insert_literal_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceVimInsertLiteral, gtk_source_vim_insert_literal, GTK_SOURCE, 
VIM_INSERT_LITERAL, GtkSourceVimState)
+
+GtkSourceVimState *gtk_source_vim_insert_literal_new (void);
+
+G_END_DECLS
diff --git a/gtksourceview/vim/gtksourcevimjumplist.c b/gtksourceview/vim/gtksourcevimjumplist.c
new file mode 100644
index 00000000..7b27ec95
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimjumplist.c
@@ -0,0 +1,260 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "gtksourcevimjumplist.h"
+
+#define MAX_JUMPS 100
+
+typedef struct
+{
+       GList        link;
+       GtkTextMark *mark;
+} Jump;
+
+struct _GtkSourceVimJumplist
+{
+       GtkSourceVimState parent_instance;
+       GQueue back;
+       GQueue forward;
+};
+
+G_DEFINE_TYPE (GtkSourceVimJumplist, gtk_source_vim_jumplist, GTK_SOURCE_TYPE_VIM_STATE)
+
+GtkSourceVimState *
+gtk_source_vim_jumplist_new (void)
+{
+       return g_object_new (GTK_SOURCE_TYPE_VIM_JUMPLIST, NULL);
+}
+
+static void
+jump_free (Jump *j)
+{
+       g_assert (j->link.data == j);
+       g_assert (j->link.prev == NULL);
+       g_assert (j->link.next == NULL);
+
+       j->link.data = NULL;
+
+       if (j->mark != NULL)
+       {
+               GtkTextBuffer *buffer = gtk_text_mark_get_buffer (j->mark);
+               gtk_text_buffer_delete_mark (buffer, j->mark);
+               g_object_unref (j->mark);
+               j->mark = NULL;
+       }
+
+       g_slice_free (Jump, j);
+}
+
+static gboolean
+jump_equal (const Jump *a,
+            const Jump *b)
+{
+       GtkTextIter ai, bi;
+
+       g_assert (GTK_IS_TEXT_MARK (a->mark));
+       g_assert (GTK_IS_TEXT_MARK (b->mark));
+
+       if (a == b)
+               return TRUE;
+
+       if (a->mark == b->mark)
+               return TRUE;
+
+       gtk_text_buffer_get_iter_at_mark (gtk_text_mark_get_buffer (a->mark), &ai, a->mark);
+       gtk_text_buffer_get_iter_at_mark (gtk_text_mark_get_buffer (b->mark), &bi, b->mark);
+
+       if (gtk_text_iter_get_line (&ai) == gtk_text_iter_get_line (&bi))
+               return TRUE;
+
+       return FALSE;
+}
+
+static void
+clear_queue (GQueue *q)
+{
+       while (q->length > 0)
+       {
+               Jump *head = q->head->data;
+               g_queue_unlink (q, &head->link);
+               jump_free (head);
+       }
+}
+
+static void
+gtk_source_vim_jumplist_dispose (GObject *object)
+{
+       GtkSourceVimJumplist *self = (GtkSourceVimJumplist *)object;
+
+       clear_queue (&self->back);
+       clear_queue (&self->forward);
+
+       G_OBJECT_CLASS (gtk_source_vim_jumplist_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_vim_jumplist_class_init (GtkSourceVimJumplistClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->dispose = gtk_source_vim_jumplist_dispose;
+}
+
+static void
+gtk_source_vim_jumplist_init (GtkSourceVimJumplist *self)
+{
+}
+
+void
+gtk_source_vim_jumplist_push (GtkSourceVimJumplist *self,
+                              const GtkTextIter    *iter)
+{
+       GtkTextBuffer *buffer;
+       Jump *j;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_JUMPLIST (self));
+       g_return_if_fail (iter != NULL);
+
+       buffer = gtk_text_iter_get_buffer (iter);
+
+       j = g_slice_new0 (Jump);
+       j->link.data = j;
+       j->mark = g_object_ref (gtk_text_buffer_create_mark (buffer, NULL, iter, TRUE));
+
+       g_assert (GTK_IS_TEXT_MARK (j->mark));
+
+       for (const GList *item = self->back.tail; item; item = item->prev)
+       {
+               Jump *j2 = item->data;
+
+               if (jump_equal (j, j2))
+               {
+                       g_queue_unlink (&self->back, &j2->link);
+                       jump_free (j2);
+                       goto push;
+               }
+       }
+
+       for (const GList *item = self->forward.head; item; item = item->next)
+       {
+               Jump *j2 = item->data;
+
+               if (jump_equal (j, j2))
+               {
+                       g_queue_unlink (&self->forward, &j2->link);
+                       jump_free (j2);
+                       goto push;
+               }
+       }
+
+push:
+       if (self->back.length + self->forward.length >= MAX_JUMPS)
+       {
+               if (self->back.length > 0)
+               {
+                       Jump *head = self->back.head->data;
+                       g_queue_unlink (&self->back, &head->link);
+                       jump_free (head);
+               }
+               else
+               {
+                       Jump *tail = self->forward.tail->data;
+                       g_queue_unlink (&self->forward, &tail->link);
+                       jump_free (tail);
+               }
+       }
+
+       g_queue_push_tail_link (&self->back, &j->link);
+}
+
+gboolean
+gtk_source_vim_jumplist_previous (GtkSourceVimJumplist *self,
+                                  GtkTextIter          *iter)
+{
+       GtkSourceBuffer *buffer;
+       GtkTextIter before;
+       Jump current = {0};
+       gboolean ret = FALSE;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_JUMPLIST (self), FALSE);
+       g_return_val_if_fail (iter != NULL, FALSE);
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &before, NULL);
+
+       current.mark = gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (buffer));
+       current.link.data = &current;
+
+       gtk_source_vim_jumplist_push (self, &before);
+
+       while (!ret && self->back.length > 0)
+       {
+               Jump *j = g_queue_peek_tail (&self->back);
+
+               if (!jump_equal (&current, j))
+               {
+                       gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), iter, j->mark);
+                       ret = TRUE;
+               }
+
+               g_queue_unlink (&self->back, &j->link);
+               g_queue_push_head_link (&self->forward, &j->link);
+       }
+
+       return ret;
+}
+
+gboolean
+gtk_source_vim_jumplist_next (GtkSourceVimJumplist *self,
+                              GtkTextIter          *iter)
+{
+       GtkSourceBuffer *buffer;
+       GtkTextIter before;
+       Jump current = {0};
+       gboolean ret = FALSE;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_JUMPLIST (self), FALSE);
+       g_return_val_if_fail (iter != NULL, FALSE);
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &before, NULL);
+
+       current.mark = gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (buffer));
+       current.link.data = &current;
+
+       gtk_source_vim_jumplist_push (self, &before);
+
+       while (!ret && self->forward.length > 0)
+       {
+               Jump *j = g_queue_peek_head (&self->forward);
+
+               if (!jump_equal (&current, j))
+               {
+                       gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), iter, j->mark);
+                       ret = TRUE;
+               }
+
+               g_queue_unlink (&self->forward, &j->link);
+               g_queue_push_tail_link (&self->back, &j->link);
+       }
+
+       return ret;
+}
diff --git a/gtksourceview/vim/gtksourcevimjumplist.h b/gtksourceview/vim/gtksourcevimjumplist.h
new file mode 100644
index 00000000..7a6f0396
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimjumplist.h
@@ -0,0 +1,41 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include "gtksourcevimstate.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_VIM_JUMPLIST (gtk_source_vim_jumplist_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceVimJumplist, gtk_source_vim_jumplist, GTK_SOURCE, VIM_JUMPLIST, 
GtkSourceVimState)
+
+GtkSourceVimState *gtk_source_vim_jumplist_new      (void);
+void               gtk_source_vim_jumplist_push     (GtkSourceVimJumplist *self,
+                                                     const GtkTextIter    *iter);
+gboolean           gtk_source_vim_jumplist_previous (GtkSourceVimJumplist *self,
+                                                     GtkTextIter          *iter);
+gboolean           gtk_source_vim_jumplist_next     (GtkSourceVimJumplist *self,
+                                                     GtkTextIter          *iter);
+
+
+G_END_DECLS
diff --git a/gtksourceview/vim/gtksourcevimmarks.c b/gtksourceview/vim/gtksourcevimmarks.c
new file mode 100644
index 00000000..1177ef18
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimmarks.c
@@ -0,0 +1,160 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "gtksourcevimmarks.h"
+
+struct _GtkSourceVimMarks
+{
+       GtkSourceVimState parent_instance;
+       GHashTable *marks;
+};
+
+G_DEFINE_TYPE (GtkSourceVimMarks, gtk_source_vim_marks, GTK_SOURCE_TYPE_VIM_STATE)
+
+GtkSourceVimState *
+gtk_source_vim_marks_new (void)
+{
+       return g_object_new (GTK_SOURCE_TYPE_VIM_MARKS, NULL);
+}
+
+static void
+remove_mark (gpointer data)
+{
+       GtkTextMark *mark = data;
+       GtkTextBuffer *buffer = gtk_text_mark_get_buffer (mark);
+       gtk_text_buffer_delete_mark (buffer, mark);
+       g_object_unref (mark);
+}
+
+static void
+gtk_source_vim_marks_dispose (GObject *object)
+{
+       GtkSourceVimMarks *self = (GtkSourceVimMarks *)object;
+
+       g_clear_pointer (&self->marks, g_hash_table_unref);
+
+       G_OBJECT_CLASS (gtk_source_vim_marks_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_vim_marks_class_init (GtkSourceVimMarksClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->dispose = gtk_source_vim_marks_dispose;
+}
+
+static void
+gtk_source_vim_marks_init (GtkSourceVimMarks *self)
+{
+       self->marks = g_hash_table_new_full (g_str_hash,
+                                            g_str_equal,
+                                            NULL,
+                                            remove_mark);
+}
+
+GtkTextMark *
+gtk_source_vim_marks_get_mark (GtkSourceVimMarks *self,
+                               const char        *name)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_MARKS (self), NULL);
+       g_return_val_if_fail (name != NULL, NULL);
+
+       if (name[0] == '<' || name[0] == '>')
+       {
+               GtkTextIter iter, selection;
+               GtkSourceBuffer *buffer;
+
+               buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, &selection);
+
+               if (gtk_text_iter_compare (&iter, &selection) <= 0)
+               {
+                       if (name[0] == '<')
+                               return gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (buffer));
+                       else
+                               return gtk_text_buffer_get_selection_bound (GTK_TEXT_BUFFER (buffer));
+               }
+               else
+               {
+                       if (name[0] == '<')
+                               return gtk_text_buffer_get_selection_bound (GTK_TEXT_BUFFER (buffer));
+                       else
+                               return gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (buffer));
+               }
+       }
+
+       return g_hash_table_lookup (self->marks, name);
+}
+
+gboolean
+gtk_source_vim_marks_get_iter (GtkSourceVimMarks *self,
+                               const char        *name,
+                               GtkTextIter       *iter)
+{
+       GtkTextMark *mark;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_MARKS (self), FALSE);
+       g_return_val_if_fail (name != NULL, FALSE);
+
+       if (!(mark = gtk_source_vim_marks_get_mark (self, name)))
+               return FALSE;
+
+       if (iter == NULL)
+               return TRUE;
+
+       gtk_text_buffer_get_iter_at_mark (gtk_text_mark_get_buffer (mark), iter, mark);
+
+       return TRUE;
+}
+
+void
+gtk_source_vim_marks_set_mark (GtkSourceVimMarks *self,
+                               const char        *name,
+                               const GtkTextIter *iter)
+{
+       GtkTextBuffer *buffer;
+       GtkTextMark *mark;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_MARKS (self));
+       g_return_if_fail (name != NULL);
+
+       if (iter == NULL)
+       {
+               g_hash_table_remove (self->marks, name);
+               return;
+       }
+
+       if (!(mark = gtk_source_vim_marks_get_mark (self, name)))
+       {
+               buffer = GTK_TEXT_BUFFER (gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), NULL, 
NULL));
+               mark = gtk_text_buffer_create_mark (buffer, NULL, iter, TRUE);
+               g_hash_table_insert (self->marks,
+                                    (char *)g_intern_string (name),
+                                    g_object_ref (mark));
+       }
+       else
+       {
+               buffer = gtk_text_mark_get_buffer (mark);
+               gtk_text_buffer_move_mark (buffer, mark, iter);
+       }
+}
diff --git a/gtksourceview/vim/gtksourcevimmarks.h b/gtksourceview/vim/gtksourcevimmarks.h
new file mode 100644
index 00000000..43678841
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimmarks.h
@@ -0,0 +1,42 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include "gtksourcevimstate.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_VIM_MARKS (gtk_source_vim_marks_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceVimMarks, gtk_source_vim_marks, GTK_SOURCE, VIM_MARKS, GtkSourceVimState)
+
+GtkSourceVimState *gtk_source_vim_marks_new      (void);
+GtkTextMark       *gtk_source_vim_marks_get_mark (GtkSourceVimMarks *self,
+                                                  const char        *name);
+gboolean           gtk_source_vim_marks_get_iter (GtkSourceVimMarks *self,
+                                                  const char        *name,
+                                                  GtkTextIter       *iter);
+void               gtk_source_vim_marks_set_mark (GtkSourceVimMarks *self,
+                                                  const char        *name,
+                                                  const GtkTextIter *iter);
+
+G_END_DECLS
diff --git a/gtksourceview/vim/gtksourcevimmotion.c b/gtksourceview/vim/gtksourcevimmotion.c
new file mode 100644
index 00000000..6f0371f7
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimmotion.c
@@ -0,0 +1,2840 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include <gtksourceview/gtksourceview.h>
+#include <gtksourceview/gtksourcesearchcontext.h>
+#include <gtksourceview/gtksourcesearchsettings.h>
+
+#include "gtksourcevimcharpending.h"
+#include "gtksourcevimmotion.h"
+
+typedef gboolean (*Motion) (GtkTextIter        *iter,
+                            GtkSourceVimMotion *state);
+
+typedef enum {
+       INCLUSIVE = 0,
+       EXCLUSIVE = 1,
+} Inclusivity;
+
+typedef enum {
+       CHARWISE = 0,
+       LINEWISE = 1,
+} MotionWise;
+
+struct _GtkSourceVimMotion
+{
+       GtkSourceVimState parent_instance;
+
+       /* Text as it's typed for append_command() */
+       GString *command_text;
+
+       /* The mark to apply the motion to or NULL */
+       GtkTextMark *mark;
+
+       /* A function to apply the motion */
+       Motion motion;
+
+       /* An array of motions if this is a motion chain (such as those
+        * used by delete to replay Visual state motions.
+        */
+       GPtrArray *chained;
+
+       /* character for f or F */
+       gunichar f_char;
+
+       /* Where are we applying the :count, useful when you need
+        * to deal with empty lines and forward_to_line_end().
+        */
+       int apply_count;
+
+       /* If we need to alter the count of the motion by a value
+        * (typically used for things like yy dd and other things that
+        * are "this line" but can be repeated to extend). Therefore
+        * the value is generally either 0 or -1.
+        */
+       int alter_count;
+
+       /* If this is specified, we want to treat it like a `j` but
+        * with the count subtracted by one. Useful for yy, dd, etc.
+        */
+       guint linewise_keyval;
+
+       /* Apply the motion when leaving the state. This is useful
+        * so that you can either capture a motion for future use
+        * or simply apply it immediately.
+        */
+       guint apply_on_leave : 1;
+
+       /* If the command starts with `g` such as `ge` or `gE`. */
+       guint g_command : 1;
+
+       /* If we're in a [( or ]} type motion */
+       guint bracket_right : 1;
+       guint bracket_left : 1;
+
+       /* If we called gtk_source_vim_motion_bail(). */
+       guint failed : 1;
+
+       /* If we just did f/F and need another char */
+       guint waiting_for_f_char : 1;
+
+       /* If the motion is exclusive (does not include char) */
+       guint inclusivity : 1;
+
+       /* If we're to apply inclusivity (used by chained motions) */
+       guint applying_inclusive : 1;
+
+       guint invalidates_visual_column : 1;
+
+       /* Some motions are considered linewise when applying commands,
+        * generally when they land on a new line. Not all are, however, such
+        * as paragraph or sentence movements.
+        */
+       MotionWise wise : 1;
+
+       /* Moving to marks */
+       guint mark_charwise : 1;
+       guint mark_linewise : 1;
+
+       /* If this motion is a "jump" (:help jumplist) */
+       guint is_jump : 1;
+};
+
+G_DEFINE_TYPE (GtkSourceVimMotion, gtk_source_vim_motion, GTK_SOURCE_TYPE_VIM_STATE)
+
+static inline int
+get_adjusted_count (GtkSourceVimMotion *self)
+{
+       return gtk_source_vim_state_get_count (GTK_SOURCE_VIM_STATE (self)) + self->alter_count;
+}
+
+static inline gboolean
+iter_isspace (const GtkTextIter *iter)
+{
+       return g_unichar_isspace (gtk_text_iter_get_char (iter));
+}
+
+static inline gboolean
+get_number (guint  keyval,
+            int   *n)
+{
+       if (keyval >= GDK_KEY_0 && keyval <= GDK_KEY_9)
+               *n = keyval - GDK_KEY_0;
+       else if (keyval >= GDK_KEY_KP_0 && keyval <= GDK_KEY_KP_9)
+               *n = keyval - GDK_KEY_KP_0;
+       else
+               return FALSE;
+       return TRUE;
+}
+
+static gboolean
+line_is_empty (GtkTextIter *iter)
+{
+       return gtk_text_iter_starts_line (iter) && gtk_text_iter_ends_line (iter);
+}
+
+static gboolean
+motion_none (GtkTextIter        *iter,
+             GtkSourceVimMotion *self)
+{
+       return TRUE;
+}
+
+enum
+{
+       CLASS_0,
+       CLASS_NEWLINE,
+       CLASS_SPACE,
+       CLASS_SPECIAL,
+       CLASS_WORD,
+};
+
+static inline int
+simple_word_classify (gunichar ch)
+{
+       switch (ch)
+       {
+               case ' ':
+               case '\t':
+               case '\n':
+                       return CLASS_SPACE;
+
+               case '"': case '\'':
+               case '(': case ')':
+               case '{': case '}':
+               case '[': case ']':
+               case '<': case '>':
+               case '-': case '+': case '*': case '/':
+               case '!': case '@': case '#': case '$': case '%':
+               case '^': case '&': case ':': case ';': case '?':
+               case '|': case '=': case '\\': case '.': case ',':
+                       return CLASS_SPECIAL;
+
+               case '_':
+               default:
+                       return CLASS_WORD;
+       }
+}
+
+static int
+classify_word (gunichar           ch,
+               const GtkTextIter *iter)
+{
+       return simple_word_classify (ch);
+}
+
+static int
+classify_word_newline_stop (gunichar           ch,
+                            const GtkTextIter *iter)
+{
+       if (gtk_text_iter_starts_line (iter) &&
+           gtk_text_iter_ends_line (iter))
+               return CLASS_NEWLINE;
+
+       return classify_word (ch, iter);
+}
+
+static int
+classify_WORD (gunichar           ch,
+               const GtkTextIter *iter)
+{
+       if (g_unichar_isspace (ch))
+               return CLASS_SPACE;
+
+       return CLASS_WORD;
+}
+
+static int
+classify_WORD_newline_stop (gunichar           ch,
+                            const GtkTextIter *iter)
+{
+       if (gtk_text_iter_starts_line (iter) &&
+           gtk_text_iter_ends_line (iter))
+               return CLASS_NEWLINE;
+
+       return classify_WORD (ch, iter);
+}
+
+static gboolean
+forward_classified_start (GtkTextIter  *iter,
+                          int         (*classify) (gunichar, const GtkTextIter *))
+{
+       gint begin_class;
+       gint cur_class;
+       gunichar ch;
+
+       g_assert (iter);
+
+       ch = gtk_text_iter_get_char (iter);
+       begin_class = classify (ch, iter);
+
+       /* Move to the first non-whitespace character if necessary. */
+       if (begin_class == CLASS_SPACE)
+       {
+               for (;;)
+               {
+                       if (!gtk_text_iter_forward_char (iter))
+                               return FALSE;
+
+                       ch = gtk_text_iter_get_char (iter);
+                       cur_class = classify (ch, iter);
+                       if (cur_class != CLASS_SPACE)
+                               return TRUE;
+               }
+       }
+
+       /* move to first character not at same class level. */
+       while (gtk_text_iter_forward_char (iter))
+       {
+               ch = gtk_text_iter_get_char (iter);
+               cur_class = classify (ch, iter);
+
+               if (cur_class == CLASS_SPACE)
+               {
+                       begin_class = CLASS_0;
+                       continue;
+               }
+
+               if (cur_class != begin_class || cur_class == CLASS_NEWLINE)
+                       return TRUE;
+       }
+
+       return FALSE;
+}
+
+static gboolean
+forward_classified_end (GtkTextIter  *iter,
+                        int         (*classify) (gunichar, const GtkTextIter *))
+{
+       gunichar ch;
+       gint begin_class;
+       gint cur_class;
+
+       g_assert (iter);
+
+       if (!gtk_text_iter_forward_char (iter))
+               return FALSE;
+
+       /* If we are on space, walk to the start of the next word. */
+       ch = gtk_text_iter_get_char (iter);
+       if (classify (ch, iter) == CLASS_SPACE)
+               if (!forward_classified_start (iter, classify))
+                       return FALSE;
+
+       ch = gtk_text_iter_get_char (iter);
+       begin_class = classify (ch, iter);
+
+       if (begin_class == CLASS_NEWLINE)
+       {
+               gtk_text_iter_backward_char (iter);
+               return TRUE;
+       }
+
+       for (;;)
+       {
+               if (!gtk_text_iter_forward_char (iter))
+                       return FALSE;
+
+               ch = gtk_text_iter_get_char (iter);
+               cur_class = classify (ch, iter);
+
+               if (cur_class != begin_class || cur_class == CLASS_NEWLINE)
+               {
+                       gtk_text_iter_backward_char (iter);
+                       return TRUE;
+               }
+       }
+
+       return FALSE;
+}
+
+static gboolean
+backward_classified_end (GtkTextIter  *iter,
+                         int         (*classify) (gunichar, const GtkTextIter *))
+{
+       gunichar ch;
+       gint begin_class;
+       gint cur_class;
+
+       g_assert (iter);
+
+       ch = gtk_text_iter_get_char (iter);
+       begin_class = classify (ch, iter);
+
+       if (begin_class == CLASS_NEWLINE)
+       {
+               gtk_text_iter_forward_char (iter);
+               return TRUE;
+       }
+
+       for (;;)
+       {
+               if (!gtk_text_iter_backward_char (iter))
+                       return FALSE;
+
+               ch = gtk_text_iter_get_char (iter);
+               cur_class = classify (ch, iter);
+
+               if (cur_class == CLASS_NEWLINE)
+               {
+                       gtk_text_iter_forward_char (iter);
+                       return TRUE;
+               }
+
+               /* reset begin_class if we hit space, we can take anything after that */
+               if (cur_class == CLASS_SPACE)
+                       begin_class = CLASS_SPACE;
+
+               if (cur_class != begin_class && cur_class != CLASS_SPACE)
+                       return TRUE;
+       }
+
+       return FALSE;
+}
+
+static gboolean
+backward_classified_start (GtkTextIter  *iter,
+                           int         (*classify) (gunichar, const GtkTextIter *))
+{
+       gunichar ch;
+       gint begin_class;
+       gint cur_class;
+
+       g_assert (iter);
+
+       if (!gtk_text_iter_backward_char (iter))
+               return FALSE;
+
+       /* If we are on space, walk to the end of the previous word. */
+       ch = gtk_text_iter_get_char (iter);
+       if (classify (ch, iter) == CLASS_SPACE)
+               if (!backward_classified_end (iter, classify))
+                       return FALSE;
+
+       ch = gtk_text_iter_get_char (iter);
+       begin_class = classify (ch, iter);
+
+       for (;;)
+       {
+               if (!gtk_text_iter_backward_char (iter))
+                       return FALSE;
+
+               ch = gtk_text_iter_get_char (iter);
+               cur_class = classify (ch, iter);
+
+               if (cur_class != begin_class || cur_class == CLASS_NEWLINE)
+               {
+                       gtk_text_iter_forward_char (iter);
+                       return TRUE;
+               }
+       }
+
+       return FALSE;
+}
+
+static void
+get_iter_at_visual_column (GtkSourceView *view,
+                           GtkTextIter   *iter,
+                           guint          column)
+{
+       gunichar tab_char;
+       guint visual_col = 0;
+       guint tab_width;
+
+       g_assert (GTK_SOURCE_IS_VIEW (view));
+       g_assert (iter != NULL);
+
+       tab_char = g_utf8_get_char ("\t");
+       tab_width = gtk_source_view_get_tab_width (view);
+       gtk_text_iter_set_line_offset (iter, 0);
+
+       while (!gtk_text_iter_ends_line (iter))
+       {
+               if (gtk_text_iter_get_char (iter) == tab_char)
+                       visual_col += (tab_width - (visual_col % tab_width));
+               else
+                       ++visual_col;
+
+               if (visual_col > column)
+                       break;
+
+               /* This does not handle invisible text correctly, but
+                * gtk_text_iter_forward_visible_cursor_position is too slow.
+                */
+               if (!gtk_text_iter_forward_char (iter))
+                       break;
+       }
+}
+
+static gboolean
+motion_line_start (GtkTextIter        *iter,
+                   GtkSourceVimMotion *state)
+{
+       if (!gtk_text_iter_starts_line (iter))
+       {
+               gtk_text_iter_set_line_offset (iter, 0);
+               return TRUE;
+       }
+
+       return FALSE;
+}
+
+static gboolean
+motion_line_first_char (GtkTextIter        *iter,
+                        GtkSourceVimMotion *state)
+{
+       if (!gtk_text_iter_starts_line (iter))
+       {
+               gtk_text_iter_set_line_offset (iter, 0);
+       }
+
+       while (!gtk_text_iter_ends_line (iter) &&
+              g_unichar_isspace (gtk_text_iter_get_char (iter)))
+       {
+               if (!gtk_text_iter_forward_char (iter))
+               {
+                       return FALSE;
+               }
+       }
+
+       return TRUE;
+}
+
+static gboolean
+motion_forward_char_same_line_eol_okay (GtkTextIter        *iter,
+                                        GtkSourceVimMotion *state)
+{
+       if (gtk_text_iter_ends_line (iter))
+               return FALSE;
+       return gtk_text_iter_forward_char (iter);
+}
+
+static gboolean
+motion_forward_char (GtkTextIter        *iter,
+                     GtkSourceVimMotion *state)
+{
+       GtkTextIter begin = *iter;
+
+       gtk_text_iter_forward_char (iter);
+
+       if (gtk_text_iter_ends_line (iter) && !gtk_text_iter_starts_line (iter))
+       {
+               if (gtk_text_iter_is_end (iter))
+                       gtk_text_iter_backward_char (iter);
+               else
+                       gtk_text_iter_forward_char (iter);
+       }
+
+       return !gtk_text_iter_equal (&begin, iter);
+}
+
+static gboolean
+motion_forward_char_same_line (GtkTextIter        *iter,
+                               GtkSourceVimMotion *self)
+{
+       int count = get_adjusted_count (self);
+
+       if (self->apply_count != 1)
+               return FALSE;
+
+       count = MAX (1, count);
+
+       for (guint i = 0; i < count; i++)
+       {
+               if (gtk_text_iter_ends_line (iter))
+                       break;
+
+               if (!gtk_text_iter_forward_char (iter))
+                       break;
+       }
+
+       if (gtk_text_iter_ends_line (iter) && !gtk_text_iter_starts_line (iter))
+               gtk_text_iter_backward_char (iter);
+
+       return TRUE;
+}
+
+static gboolean
+motion_backward_char (GtkTextIter        *iter,
+                      GtkSourceVimMotion *state)
+{
+       GtkTextIter begin = *iter;
+
+       if (gtk_text_iter_backward_char (iter))
+       {
+               if (gtk_text_iter_ends_line (iter) && !gtk_text_iter_starts_line (iter))
+               {
+                       gtk_text_iter_backward_char (iter);
+               }
+       }
+
+       return !gtk_text_iter_equal (&begin, iter);
+}
+
+static gboolean
+motion_backward_char_same_line (GtkTextIter        *iter,
+                                GtkSourceVimMotion *state)
+{
+       if (!gtk_text_iter_starts_line (iter))
+       {
+               return gtk_text_iter_backward_char (iter);
+       }
+
+       return FALSE;
+}
+
+static gboolean
+motion_prev_line_end (GtkTextIter        *iter,
+                      GtkSourceVimMotion *state)
+{
+       guint line = gtk_text_iter_get_line (iter);
+
+       if (line == 0)
+       {
+               gtk_text_iter_set_offset (iter, 0);
+               return TRUE;
+       }
+
+       gtk_text_buffer_get_iter_at_line (gtk_text_iter_get_buffer (iter), iter, line - 1);
+
+       if (!gtk_text_iter_ends_line (iter))
+               gtk_text_iter_forward_to_line_end (iter);
+
+       /* Place on last character, not \n */
+       if (!gtk_text_iter_starts_line (iter))
+               gtk_text_iter_backward_char (iter);
+
+       return TRUE;
+}
+
+static gboolean
+motion_next_line_first_char (GtkTextIter        *iter,
+                             GtkSourceVimMotion *state)
+{
+       GtkTextIter before = *iter;
+
+       if (!gtk_text_iter_ends_line (iter))
+               gtk_text_iter_forward_to_line_end (iter);
+
+       gtk_text_iter_forward_char (iter);
+
+       /* If we are on the same line, then we must be at the end of
+        * the buffer. Just move to one character before EOB.
+        */
+       if (gtk_text_iter_get_line (&before) == gtk_text_iter_get_line (iter))
+       {
+               gtk_text_iter_forward_to_line_end (iter);
+               if (!gtk_text_iter_starts_line (iter))
+                       gtk_text_iter_backward_char (iter);
+               return !gtk_text_iter_equal (&before, iter);
+       }
+
+       while (!gtk_text_iter_ends_line (iter) &&
+              g_unichar_isspace (gtk_text_iter_get_char (iter)))
+       {
+               if (!gtk_text_iter_forward_char (iter))
+                       break;
+       }
+
+       return !gtk_text_iter_equal (&before, iter);
+}
+
+static gboolean
+motion_next_line_visual_column (GtkTextIter        *iter,
+                                GtkSourceVimMotion *self)
+{
+       GtkTextBuffer *buffer = gtk_text_iter_get_buffer (iter);
+       GtkSourceView *view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (self));
+       int column = gtk_source_vim_state_get_visual_column (GTK_SOURCE_VIM_STATE (self));
+       int count = get_adjusted_count (self);
+       int line = gtk_text_iter_get_line (iter);
+
+       self->invalidates_visual_column = FALSE;
+
+       if (self->apply_count != 1 || count == 0)
+               return FALSE;
+
+       gtk_text_buffer_get_iter_at_line (buffer, iter, line + count);
+       get_iter_at_visual_column (view, iter, column);
+
+       if (!gtk_text_iter_starts_line (iter) && gtk_text_iter_ends_line (iter))
+       {
+               gtk_text_iter_backward_char (iter);
+       }
+
+       return TRUE;
+}
+
+static gboolean
+motion_prev_line_visual_column (GtkTextIter        *iter,
+                                GtkSourceVimMotion *self)
+{
+       GtkTextBuffer *buffer = gtk_text_iter_get_buffer (iter);
+       GtkSourceView *view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (self));
+       int column = gtk_source_vim_state_get_visual_column (GTK_SOURCE_VIM_STATE (self));
+       int count = get_adjusted_count (self);
+       int line = gtk_text_iter_get_line (iter);
+
+       self->invalidates_visual_column = FALSE;
+
+       if (self->apply_count != 1 || count == 0)
+               return FALSE;
+
+       line = count > line ? 0 : line - count;
+       gtk_text_buffer_get_iter_at_line (buffer, iter, line);
+       get_iter_at_visual_column (view, iter, column);
+
+       if (!gtk_text_iter_starts_line (iter) && gtk_text_iter_ends_line (iter))
+       {
+               gtk_text_iter_backward_char (iter);
+       }
+
+       return TRUE;
+}
+
+static gboolean
+motion_line_end (GtkTextIter        *iter,
+                 GtkSourceVimMotion *state)
+{
+       GtkTextIter begin = *iter;
+
+       if (!gtk_text_iter_ends_line (iter))
+               gtk_text_iter_forward_to_line_end (iter);
+
+       if (!gtk_text_iter_starts_line (iter))
+               gtk_text_iter_backward_char (iter);
+
+       return !gtk_text_iter_equal (&begin, iter);
+}
+
+static gboolean
+motion_last_line_first_char (GtkTextIter        *iter,
+                             GtkSourceVimMotion *state)
+{
+       gtk_text_buffer_get_end_iter (gtk_text_iter_get_buffer (iter), iter);
+       gtk_text_iter_set_line_offset (iter, 0);
+       while (!gtk_text_iter_is_end (iter) &&
+              g_unichar_isspace (gtk_text_iter_get_char (iter)))
+               gtk_text_iter_forward_char (iter);
+       return TRUE;
+}
+
+static gboolean
+motion_screen_top (GtkTextIter        *iter,
+                   GtkSourceVimMotion *state)
+{
+       GtkSourceView *view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (state));
+       GdkRectangle rect;
+
+       gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (view), &rect);
+       gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), iter, rect.x, rect.y);
+
+       return TRUE;
+}
+
+static gboolean
+motion_screen_bottom (GtkTextIter        *iter,
+                      GtkSourceVimMotion *state)
+{
+       GtkSourceView *view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (state));
+       GdkRectangle rect;
+
+       gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (view), &rect);
+       gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), iter, rect.x, rect.y + rect.height);
+
+       return TRUE;
+}
+
+static gboolean
+motion_screen_middle (GtkTextIter        *iter,
+                      GtkSourceVimMotion *state)
+{
+       GtkSourceView *view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (state));
+       GdkRectangle rect;
+
+       gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (view), &rect);
+       gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), iter, rect.x, rect.y + rect.height / 2);
+
+       return TRUE;
+}
+
+static gboolean
+motion_forward_word_start (GtkTextIter        *iter,
+                           GtkSourceVimMotion *state)
+{
+       return forward_classified_start (iter, classify_word_newline_stop);
+}
+
+static gboolean
+motion_forward_WORD_start (GtkTextIter        *iter,
+                           GtkSourceVimMotion *state)
+{
+       return forward_classified_start (iter, classify_WORD_newline_stop);
+}
+
+static gboolean
+motion_forward_word_end (GtkTextIter        *iter,
+                         GtkSourceVimMotion *state)
+{
+       return forward_classified_end (iter, classify_word_newline_stop);
+}
+
+static gboolean
+motion_forward_WORD_end (GtkTextIter        *iter,
+                         GtkSourceVimMotion *state)
+{
+       return forward_classified_end (iter, classify_WORD_newline_stop);
+}
+
+static gboolean
+motion_backward_word_start (GtkTextIter        *iter,
+                            GtkSourceVimMotion *state)
+{
+       return backward_classified_start (iter, classify_word_newline_stop);
+}
+
+static gboolean
+motion_backward_WORD_start (GtkTextIter        *iter,
+                            GtkSourceVimMotion *state)
+{
+       return backward_classified_start (iter, classify_WORD_newline_stop);
+}
+
+static gboolean
+motion_backward_word_end (GtkTextIter        *iter,
+                          GtkSourceVimMotion *state)
+{
+       return backward_classified_end (iter, classify_word_newline_stop);
+}
+
+static gboolean
+motion_backward_WORD_end (GtkTextIter        *iter,
+                          GtkSourceVimMotion *state)
+{
+       return backward_classified_end (iter, classify_WORD_newline_stop);
+}
+
+static gboolean
+motion_buffer_start (GtkTextIter        *iter,
+                     GtkSourceVimMotion *state)
+{
+       if (!gtk_text_iter_is_start (iter))
+       {
+               gtk_text_iter_set_offset (iter, 0);
+               return TRUE;
+       }
+
+       return FALSE;
+}
+
+static gboolean
+motion_buffer_start_first_char (GtkTextIter        *iter,
+                                GtkSourceVimMotion *state)
+{
+       GtkTextIter before = *iter;
+
+       motion_buffer_start (iter, state);
+
+       while (!gtk_text_iter_ends_line (iter) &&
+              g_unichar_isspace (gtk_text_iter_get_char (iter)))
+       {
+               if (!gtk_text_iter_forward_char (iter))
+                       break;
+       }
+
+       return !gtk_text_iter_equal (&before, iter);
+}
+
+static gboolean
+motion_f_char (GtkTextIter        *iter,
+               GtkSourceVimMotion *state)
+{
+       GtkTextIter before = *iter;
+
+       while (!gtk_text_iter_ends_line (iter))
+       {
+               if (!gtk_text_iter_forward_char (iter))
+                       break;
+
+               if (gtk_text_iter_get_char (iter) == state->f_char)
+                       return TRUE;
+       }
+
+       *iter = before;
+
+       return FALSE;
+}
+
+static gboolean
+motion_F_char (GtkTextIter        *iter,
+               GtkSourceVimMotion *state)
+{
+       GtkTextIter before = *iter;
+
+       while (!gtk_text_iter_starts_line (iter))
+       {
+               if (!gtk_text_iter_backward_char (iter))
+                       break;
+
+               if (gtk_text_iter_get_char (iter) == state->f_char)
+                       return TRUE;
+       }
+
+       *iter = before;
+
+       return FALSE;
+}
+
+static gboolean
+motion_forward_paragraph_end (GtkTextIter        *iter,
+                              GtkSourceVimMotion *state)
+{
+       GtkTextIter before = *iter;
+
+       /* Work our way past the current empty lines */
+       if (line_is_empty (iter))
+       {
+               while (line_is_empty (iter))
+               {
+                       if (!gtk_text_iter_forward_line (iter))
+                               return FALSE;
+               }
+       }
+
+       /* Now find first line that is empty */
+       while (!line_is_empty (iter))
+       {
+               if (!gtk_text_iter_forward_line (iter))
+                       return FALSE;
+       }
+
+       if (gtk_text_iter_is_end (iter) &&
+           !gtk_text_iter_starts_line (iter))
+       {
+               gtk_text_iter_backward_char (iter);
+       }
+
+       return !gtk_text_iter_equal (&before, iter);
+}
+
+static gboolean
+motion_backward_paragraph_start (GtkTextIter        *iter,
+                                 GtkSourceVimMotion *state)
+{
+       GtkTextIter before = *iter;
+
+       /* Work our way past the current empty lines */
+       while (line_is_empty (iter))
+       {
+               if (!gtk_text_iter_backward_line (iter))
+                       goto finish;
+       }
+
+       /* Now find first line that is empty */
+       while (!line_is_empty (iter))
+       {
+               if (!gtk_text_iter_backward_line (iter))
+                       goto finish;
+       }
+
+finish:
+       return !gtk_text_iter_equal (&before, iter);
+}
+
+static gboolean
+motion_forward_sentence_start (GtkTextIter        *iter,
+                               GtkSourceVimMotion *state)
+{
+       GtkTextIter before = *iter;
+       guint newline_count = 0;
+
+       /* If we're at the end of a sentence, then walk past any trailing
+        * characters after the punctuation, and then skip space up until
+        * another non-space character.
+        */
+       switch (gtk_text_iter_get_char (iter))
+       {
+               case '.':
+               case '!':
+               case '?':
+               case '\n':
+                       while (!g_unichar_isspace (gtk_text_iter_get_char (iter)))
+                       {
+                               if (!gtk_text_iter_forward_char (iter))
+                                       goto finish;
+                       }
+                       while (g_unichar_isspace (gtk_text_iter_get_char (iter)))
+                       {
+                               if (!gtk_text_iter_forward_char (iter))
+                                       goto finish;
+                       }
+                       return TRUE;
+
+               default:
+                       break;
+       }
+
+       while (gtk_text_iter_forward_char (iter))
+       {
+               switch (gtk_text_iter_get_char (iter))
+               {
+                       case '\n':
+                               newline_count++;
+                               if (newline_count == 1)
+                                       break;
+                               G_GNUC_FALLTHROUGH;
+                       case '.':
+                       case '!':
+                       case '?':
+                               while (!g_unichar_isspace (gtk_text_iter_get_char (iter)))
+                               {
+                                       if (!gtk_text_iter_forward_char (iter))
+                                               goto finish;
+                               }
+                               while (g_unichar_isspace (gtk_text_iter_get_char (iter)))
+                               {
+                                       if (!gtk_text_iter_forward_char (iter))
+                                               goto finish;
+                               }
+                               return TRUE;
+
+                       default:
+                               break;
+               }
+       }
+
+finish:
+       if (gtk_text_iter_is_end (iter) && !gtk_text_iter_starts_line (iter))
+               gtk_text_iter_backward_char (iter);
+
+       return !gtk_text_iter_equal (&before, iter);
+}
+
+static gboolean
+backward_sentence_end (GtkTextIter *iter)
+{
+       GtkTextIter before = *iter;
+
+       if (line_is_empty (iter))
+       {
+               while (gtk_text_iter_backward_char (iter))
+               {
+                       if (!g_unichar_isspace (gtk_text_iter_get_char (iter)))
+                               break;
+               }
+
+               goto finish;
+       }
+
+       while (gtk_text_iter_backward_char (iter))
+       {
+               switch (gtk_text_iter_get_char (iter))
+               {
+                       case '.':
+                       case '!':
+                       case '?':
+                               goto finish;
+
+                       case '\n':
+                               if (gtk_text_iter_starts_line (iter))
+                               {
+                                       while (gtk_text_iter_backward_char (iter))
+                                       {
+                                               if (!g_unichar_isspace (gtk_text_iter_get_char (iter)))
+                                                       break;
+                                       }
+
+                                       goto finish;
+                               }
+                               break;
+
+                       default:
+                               break;
+               }
+       }
+
+finish:
+       if (gtk_text_iter_is_end (iter) && !gtk_text_iter_starts_line (iter))
+               gtk_text_iter_backward_char (iter);
+
+       return !gtk_text_iter_equal (&before, iter);
+}
+
+static gboolean
+motion_backward_sentence_start (GtkTextIter        *iter,
+                                GtkSourceVimMotion *state)
+{
+       GtkTextIter *winner = NULL;
+       GtkTextIter before = *iter;
+       GtkTextIter para;
+       GtkTextIter sentence;
+       GtkTextIter two_sentence;
+       int distance = G_MAXINT;
+
+       para = *iter;
+       motion_backward_paragraph_start (&para, state);
+
+       sentence = *iter;
+       backward_sentence_end (&sentence);
+       motion_forward_sentence_start (&sentence, state);
+
+       two_sentence = *iter;
+       backward_sentence_end (&two_sentence);
+       backward_sentence_end (&two_sentence);
+       motion_forward_sentence_start (&two_sentence, state);
+
+       if (gtk_text_iter_compare (&para, iter) < 0)
+       {
+               int diff = (int)gtk_text_iter_get_offset (iter) - (int)gtk_text_iter_get_offset (&para);
+
+               if (diff < distance)
+               {
+                       distance = diff;
+                       winner = &para;
+               }
+       }
+
+       if (gtk_text_iter_compare (&sentence, iter) < 0)
+       {
+               int diff = (int)gtk_text_iter_get_offset (iter) - (int)gtk_text_iter_get_offset (&sentence);
+
+               if (diff < distance)
+               {
+                       distance = diff;
+                       winner = &sentence;
+               }
+       }
+
+       if (gtk_text_iter_compare (&two_sentence, iter) < 0)
+       {
+               int diff = (int)gtk_text_iter_get_offset (iter) - (int)gtk_text_iter_get_offset 
(&two_sentence);
+
+               if (diff < distance)
+               {
+                       distance = diff;
+                       winner = &two_sentence;
+               }
+       }
+
+       if (winner != NULL)
+               *iter = *winner;
+       else
+               gtk_text_iter_set_offset (iter, 0);
+
+       return !gtk_text_iter_equal (&before, iter);
+}
+
+static gboolean
+motion_next_scroll_page (GtkTextIter        *iter,
+                         GtkSourceVimMotion *self)
+{
+       int count = get_adjusted_count (self);
+       GtkTextBuffer *buffer = gtk_text_iter_get_buffer (iter);
+       GtkTextMark *insert = gtk_text_buffer_get_insert (buffer);
+
+       if (self->apply_count != 1)
+               return FALSE;
+
+       gtk_source_vim_state_scroll_page (GTK_SOURCE_VIM_STATE (self), count);
+       gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), iter, insert);
+
+       return TRUE;
+}
+
+static gboolean
+motion_prev_scroll_page (GtkTextIter        *iter,
+                         GtkSourceVimMotion *self)
+{
+       int count = get_adjusted_count (self);
+       GtkTextBuffer *buffer = gtk_text_iter_get_buffer (iter);
+       GtkTextMark *insert = gtk_text_buffer_get_insert (buffer);
+
+       if (self->apply_count != 1)
+               return FALSE;
+
+       gtk_source_vim_state_scroll_page (GTK_SOURCE_VIM_STATE (self), -count);
+       gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), iter, insert);
+       return TRUE;
+}
+
+static gboolean
+motion_next_scroll_half_page (GtkTextIter        *iter,
+                              GtkSourceVimMotion *self)
+{
+       int count = get_adjusted_count (self);
+       GtkTextBuffer *buffer = gtk_text_iter_get_buffer (iter);
+       GtkTextMark *insert = gtk_text_buffer_get_insert (buffer);
+
+       if (self->apply_count != 1)
+               return FALSE;
+
+       gtk_source_vim_state_scroll_half_page (GTK_SOURCE_VIM_STATE (self), count);
+       gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), iter, insert);
+
+       return TRUE;
+}
+
+static gboolean
+motion_prev_scroll_half_page (GtkTextIter        *iter,
+                              GtkSourceVimMotion *self)
+{
+       int count = get_adjusted_count (self);
+       GtkTextBuffer *buffer = gtk_text_iter_get_buffer (iter);
+       GtkTextMark *insert = gtk_text_buffer_get_insert (buffer);
+
+       if (self->apply_count != 1)
+               return FALSE;
+
+       gtk_source_vim_state_scroll_half_page (GTK_SOURCE_VIM_STATE (self), -count);
+       gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), iter, insert);
+       return TRUE;
+}
+
+static gboolean
+motion_prev_scroll_line (GtkTextIter        *iter,
+                         GtkSourceVimMotion *self)
+{
+       int count = get_adjusted_count (self);
+       GtkTextBuffer *buffer = gtk_text_iter_get_buffer (iter);
+       GtkTextMark *insert = gtk_text_buffer_get_insert (buffer);
+       GtkSourceView *view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (self));
+       GtkTextIter loc;
+       GdkRectangle rect;
+
+       if (self->apply_count != 1)
+               return FALSE;
+
+       gtk_source_vim_state_scroll_line (GTK_SOURCE_VIM_STATE (self), -count);
+       gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (view), &rect);
+       gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), iter, insert);
+       gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), &loc, rect.x + rect.width, rect.y + 
rect.height);
+
+       if (gtk_text_iter_compare (&loc, iter) < 0)
+       {
+               gtk_text_iter_set_line (iter, gtk_text_iter_get_line (&loc));
+       }
+
+       return TRUE;
+}
+
+static gboolean
+motion_next_scroll_line (GtkTextIter        *iter,
+                         GtkSourceVimMotion *self)
+{
+       int count = get_adjusted_count (self);
+       GtkTextBuffer *buffer = gtk_text_iter_get_buffer (iter);
+       GtkTextMark *insert = gtk_text_buffer_get_insert (buffer);
+       GtkSourceView *view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (self));
+       GtkTextIter loc;
+       GdkRectangle rect;
+
+       if (self->apply_count != 1)
+               return FALSE;
+
+       gtk_source_vim_state_scroll_line (GTK_SOURCE_VIM_STATE (self), count);
+       gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (view), &rect);
+       gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), iter, insert);
+       gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), &loc, rect.x, rect.y);
+
+       if (gtk_text_iter_compare (&loc, iter) > 0)
+       {
+               gtk_text_iter_set_line (iter, gtk_text_iter_get_line (&loc));
+
+               if (!gtk_text_iter_ends_line (iter))
+               {
+                       gtk_text_iter_forward_to_line_end (iter);
+               }
+
+               if (gtk_text_iter_ends_line (iter) &&
+                   !gtk_text_iter_starts_line (iter))
+                       gtk_text_iter_backward_char (iter);
+       }
+
+       return TRUE;
+}
+
+static gboolean
+motion_line_number (GtkTextIter        *iter,
+                    GtkSourceVimMotion *self)
+{
+       int count = get_adjusted_count (self);
+
+       if (self->apply_count != 1)
+               return FALSE;
+
+       if (count > 0)
+               count--;
+
+       gtk_text_iter_set_line (iter, count);
+
+       while (!gtk_text_iter_ends_line (iter) &&
+              g_unichar_isspace (gtk_text_iter_get_char (iter)) &&
+              gtk_text_iter_forward_char (iter))
+       {
+               /* Do Nothing */
+       }
+
+       return TRUE;
+}
+
+static char *
+word_under_cursor (const GtkTextIter *iter)
+{
+       GtkTextIter begin, end;
+
+       end = *iter;
+       if (!gtk_source_vim_iter_ends_word (&end))
+       {
+               if (!gtk_source_vim_iter_forward_word_end (&end))
+                       return FALSE;
+       }
+
+       begin = end;
+       if (!gtk_source_vim_iter_starts_word (&begin))
+       {
+               gtk_source_vim_iter_backward_word_start (&begin);
+       }
+
+       gtk_text_iter_forward_char (&end);
+
+       return gtk_text_iter_get_slice (&begin, &end);
+}
+
+static char *
+WORD_under_cursor (const GtkTextIter *iter)
+{
+       GtkTextIter begin, end;
+
+       end = *iter;
+       if (!gtk_source_vim_iter_ends_WORD (&end))
+       {
+               if (!gtk_source_vim_iter_forward_WORD_end (&end))
+                       return FALSE;
+       }
+
+       begin = end;
+       if (!gtk_source_vim_iter_starts_WORD (&begin))
+       {
+               gtk_source_vim_iter_backward_WORD_start (&begin);
+       }
+
+       gtk_text_iter_forward_char (&end);
+
+       return gtk_text_iter_get_slice (&begin, &end);
+}
+
+static gboolean
+motion_search (GtkTextIter        *iter,
+               GtkSourceVimMotion *self,
+               gboolean            WORD,
+               gboolean            reverse)
+{
+       GtkSourceSearchContext *context;
+       GtkSourceSearchSettings *settings;
+       const char *search_text;
+       char *word;
+       gboolean has_wrapped_around;
+       gboolean ret = FALSE;
+       int count;
+
+       g_assert (iter != NULL);
+       g_assert (GTK_SOURCE_IS_VIM_MOTION (self));
+
+       if (self->apply_count != 1)
+       {
+               return FALSE;
+       }
+
+       gtk_source_vim_state_get_search (GTK_SOURCE_VIM_STATE (self), &settings, &context);
+       gtk_source_vim_state_set_reverse_search (GTK_SOURCE_VIM_STATE (self), reverse);
+
+       if (!gtk_source_search_settings_get_at_word_boundaries (settings))
+       {
+               gtk_source_search_settings_set_at_word_boundaries (settings, TRUE);
+       }
+
+       word = WORD ? WORD_under_cursor (iter) : word_under_cursor (iter);
+       search_text = gtk_source_search_settings_get_search_text (settings);
+
+       if (g_strcmp0 (word, search_text) != 0)
+       {
+               gtk_source_search_settings_set_search_text (settings, word);
+       }
+
+       if (!reverse)
+       {
+               gtk_text_iter_forward_char (iter);
+       }
+
+       g_free (word);
+
+       count = gtk_source_vim_state_get_count (GTK_SOURCE_VIM_STATE (self));
+
+       for (guint i = 0; i < count; i++)
+       {
+               gboolean matched;
+
+               if (reverse)
+                        matched = gtk_source_search_context_backward (context, iter, iter, NULL, 
&has_wrapped_around);
+               else
+                        matched = gtk_source_search_context_forward (context, iter, iter, NULL, 
&has_wrapped_around);
+
+               if (!matched)
+                       break;
+
+               ret = TRUE;
+       }
+
+       gtk_source_search_context_set_highlight (context, ret);
+
+       return ret;
+}
+
+static gboolean
+motion_forward_search_word (GtkTextIter        *iter,
+                            GtkSourceVimMotion *self)
+{
+       return motion_search (iter, self, FALSE, FALSE);
+}
+
+static gboolean
+motion_backward_search_word (GtkTextIter        *iter,
+                             GtkSourceVimMotion *self)
+{
+       return motion_search (iter, self, FALSE, TRUE);
+}
+
+static gboolean
+motion_next_search (GtkTextIter        *iter,
+                    GtkSourceVimMotion *self)
+{
+       GtkSourceSearchContext *context;
+       gboolean has_wrapped_around;
+       gboolean matched;
+
+       gtk_source_vim_state_get_search (GTK_SOURCE_VIM_STATE (self), NULL, &context);
+
+       gtk_text_iter_forward_char (iter);
+
+       matched = gtk_source_search_context_forward (context, iter, iter, NULL, &has_wrapped_around);
+
+       gtk_source_search_context_set_highlight (context, matched);
+
+       return matched;
+}
+
+static gboolean
+motion_prev_search (GtkTextIter        *iter,
+                    GtkSourceVimMotion *self)
+{
+       GtkSourceSearchContext *context;
+       gboolean has_wrapped_around;
+       gboolean matched;
+
+       gtk_source_vim_state_get_search (GTK_SOURCE_VIM_STATE (self), NULL, &context);
+
+       matched = gtk_source_search_context_backward (context, iter, iter, NULL, &has_wrapped_around);
+
+       gtk_source_search_context_set_highlight (context, matched);
+
+       return matched;
+}
+
+GtkSourceVimState *
+gtk_source_vim_motion_new (void)
+{
+       return g_object_new (GTK_SOURCE_TYPE_VIM_MOTION, NULL);
+}
+
+static gboolean
+gtk_source_vim_motion_bail (GtkSourceVimMotion *self)
+{
+       g_assert (GTK_SOURCE_IS_VIM_MOTION (self));
+
+       g_string_truncate (self->command_text, 0);
+
+       self->failed = TRUE;
+       gtk_source_vim_state_pop (GTK_SOURCE_VIM_STATE (self));
+
+       return TRUE;
+}
+
+static gboolean
+gtk_source_vim_motion_complete (GtkSourceVimMotion *self,
+                                Motion              motion,
+                                Inclusivity         inclusivity,
+                                MotionWise          wise)
+{
+       g_assert (GTK_SOURCE_IS_VIM_MOTION (self));
+
+       self->motion = motion;
+       self->inclusivity = inclusivity;
+       self->wise = wise;
+
+       g_string_truncate (self->command_text, 0);
+
+       gtk_source_vim_state_pop (GTK_SOURCE_VIM_STATE (self));
+
+       return TRUE;
+}
+
+static gboolean
+gtk_source_vim_motion_begin_char_pending (GtkSourceVimMotion *self,
+                                          Motion              motion,
+                                          Inclusivity         inclusivity,
+                                          MotionWise          wise)
+{
+       GtkSourceVimState *char_pending;
+
+       g_assert (GTK_SOURCE_IS_VIM_MOTION (self));
+       g_assert (motion != NULL);
+
+       self->motion = motion;
+       self->inclusivity = inclusivity;
+       self->wise = wise;
+
+       char_pending = gtk_source_vim_char_pending_new ();
+       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), char_pending);
+
+       return TRUE;
+}
+
+static gboolean
+motion_bracket (GtkTextIter        *iter,
+                GtkSourceVimMotion *self)
+{
+       GtkTextIter orig = *iter;
+
+       if (self->bracket_left)
+       {
+               gtk_text_iter_backward_char (iter);
+
+               if (self->f_char == '(')
+               {
+                       if (gtk_source_vim_iter_backward_block_paren_start (iter))
+                               return TRUE;
+               }
+
+               if (self->f_char == '{')
+               {
+                       if (gtk_source_vim_iter_backward_block_brace_start (iter))
+                               return TRUE;
+               }
+       }
+       else
+       {
+               if (self->f_char == ')')
+               {
+                       if (gtk_source_vim_iter_forward_block_paren_end (iter))
+                               return TRUE;
+               }
+
+               if (self->f_char == '}')
+               {
+                       if (gtk_source_vim_iter_forward_block_brace_end (iter))
+                               return TRUE;
+               }
+       }
+
+       *iter = orig;
+
+       return FALSE;
+}
+
+static gboolean
+motion_matching_char (GtkTextIter        *iter,
+                      GtkSourceVimMotion *self)
+{
+       GtkTextIter orig = *iter;
+       gunichar ch = gtk_text_iter_get_char (iter);
+       gboolean ret;
+
+       switch (ch)
+       {
+               case '(':
+                       ret = gtk_source_vim_iter_forward_block_paren_end (iter);
+                       break;
+
+               case ')':
+                       ret = gtk_source_vim_iter_backward_block_paren_start (iter);
+                       break;
+
+               case '[':
+                       ret = gtk_source_vim_iter_forward_block_bracket_end (iter);
+                       break;
+
+               case ']':
+                       ret = gtk_source_vim_iter_backward_block_bracket_start (iter);
+                       break;
+
+               case '{':
+                       ret = gtk_source_vim_iter_forward_block_brace_end (iter);
+                       break;
+
+               case '}':
+                       ret = gtk_source_vim_iter_backward_block_brace_start (iter);
+                       break;
+
+               default:
+                       /* TODO: check for #if/#ifdef/#elif/#else/#endif */
+                       ret = FALSE;
+                       break;
+       }
+
+       if (!ret)
+               *iter = orig;
+
+       return ret;
+}
+
+static gboolean
+motion_mark (GtkTextIter        *iter,
+             GtkSourceVimMotion *self)
+{
+       char str[8];
+
+       str[g_unichar_to_utf8 (self->f_char, str)] = 0;
+
+       if (gtk_source_vim_state_get_iter_at_mark (GTK_SOURCE_VIM_STATE (self), str, iter))
+       {
+               if (self->mark_linewise)
+               {
+                       gtk_text_iter_set_line_offset (iter, 0);
+                       while (!gtk_text_iter_ends_line (iter) && iter_isspace (iter))
+                               gtk_text_iter_forward_char (iter);
+               }
+
+               return TRUE;
+       }
+
+       return FALSE;
+}
+
+static gboolean
+gtk_source_vim_motion_handle_keypress (GtkSourceVimState *state,
+                                       guint              keyval,
+                                       guint              keycode,
+                                       GdkModifierType    mods,
+                                       const char        *string)
+{
+       GtkSourceVimMotion *self = (GtkSourceVimMotion *)state;
+       int count;
+       int n;
+
+       g_assert (GTK_SOURCE_IS_VIM_MOTION (self));
+
+       g_string_append (self->command_text, string);
+
+       count = gtk_source_vim_state_get_count (state);
+
+       if (self->waiting_for_f_char)
+       {
+               if (string == NULL || string[0] == 0)
+                       return gtk_source_vim_motion_bail (self);
+
+               self->f_char = g_utf8_get_char (string);
+               gtk_source_vim_state_pop (state);
+               return TRUE;
+       }
+
+       if (self->g_command)
+       {
+               switch (keyval)
+               {
+                       case GDK_KEY_g:
+                               self->is_jump = TRUE;
+                               return gtk_source_vim_motion_complete (self, motion_buffer_start_first_char, 
INCLUSIVE, LINEWISE);
+
+                       case GDK_KEY_e:
+                               return gtk_source_vim_motion_complete (self, motion_backward_word_end, 
INCLUSIVE, CHARWISE);
+
+                       case GDK_KEY_E:
+                               return gtk_source_vim_motion_complete (self, motion_backward_WORD_end, 
INCLUSIVE, CHARWISE);
+
+                       default:
+                               return gtk_source_vim_motion_bail (self);
+               }
+
+               g_assert_not_reached ();
+       }
+
+       if (self->bracket_left || self->bracket_right)
+       {
+               switch (keyval)
+               {
+                       case GDK_KEY_parenleft:
+                               self->f_char = '(';
+                               self->is_jump = TRUE;
+                               return gtk_source_vim_motion_complete (self, motion_bracket, INCLUSIVE, 
CHARWISE);
+
+                       case GDK_KEY_parenright:
+                               self->f_char = ')';
+                               self->is_jump = TRUE;
+                               return gtk_source_vim_motion_complete (self, motion_bracket, INCLUSIVE, 
CHARWISE);
+
+                       case GDK_KEY_braceleft:
+                               self->f_char = '{';
+                               self->is_jump = TRUE;
+                               return gtk_source_vim_motion_complete (self, motion_bracket, INCLUSIVE, 
CHARWISE);
+
+                       case GDK_KEY_braceright:
+                               self->f_char = '}';
+                               self->is_jump = TRUE;
+                               return gtk_source_vim_motion_complete (self, motion_bracket, INCLUSIVE, 
CHARWISE);
+
+                       case GDK_KEY_M:
+                       case GDK_KEY_m:
+                               /* TODO: support next method */
+                       default:
+                               break;
+               }
+       }
+
+       if (self->mark_linewise || self->mark_charwise)
+       {
+               GtkTextIter iter;
+
+               /* Make sure we found the mark */
+               if (!gtk_source_vim_state_get_iter_at_mark (state, string, &iter))
+                       return gtk_source_vim_motion_bail (self);
+
+               self->f_char = string[0];
+
+               if (self->mark_linewise)
+                       return gtk_source_vim_motion_complete (self, motion_mark, INCLUSIVE, LINEWISE);
+               else
+                       return gtk_source_vim_motion_complete (self, motion_mark, EXCLUSIVE, CHARWISE);
+       }
+
+       if (gtk_source_vim_state_get_count_set (state) && get_number (keyval, &n))
+       {
+               count = count * 10 + n;
+               gtk_source_vim_state_set_count (state, count);
+               return TRUE;
+       }
+
+       if ((mods & GDK_CONTROL_MASK) != 0)
+       {
+               switch (keyval)
+               {
+                       /* Technically, none of these are usable with commands
+                        * like d{motion} and therefore may require some extra
+                        * tweaking to see how we use them.
+                        */
+                       case GDK_KEY_f:
+                               return gtk_source_vim_motion_complete (self, motion_next_scroll_page, 
INCLUSIVE, LINEWISE);
+
+                       case GDK_KEY_b:
+                               return gtk_source_vim_motion_complete (self, motion_prev_scroll_page, 
INCLUSIVE, LINEWISE);
+
+                       case GDK_KEY_e:
+                               return gtk_source_vim_motion_complete (self, motion_next_scroll_line, 
INCLUSIVE, LINEWISE);
+
+                       case GDK_KEY_y:
+                               return gtk_source_vim_motion_complete (self, motion_prev_scroll_line, 
INCLUSIVE, LINEWISE);
+
+                       case GDK_KEY_u:
+                               return gtk_source_vim_motion_complete (self, motion_prev_scroll_half_page, 
INCLUSIVE, LINEWISE);
+
+                       case GDK_KEY_d:
+                               return gtk_source_vim_motion_complete (self, motion_next_scroll_half_page, 
INCLUSIVE, LINEWISE);
+
+                       default:
+                               break;
+               }
+       }
+
+       if (keyval != 0 && keyval == self->linewise_keyval)
+       {
+               self->motion = motion_next_line_visual_column;
+               self->inclusivity = EXCLUSIVE;
+               self->wise = LINEWISE;
+               self->alter_count = -1;
+               g_string_truncate (self->command_text, 0);
+               gtk_source_vim_state_pop (GTK_SOURCE_VIM_STATE (self));
+               return TRUE;
+       }
+
+       switch (keyval)
+       {
+               case GDK_KEY_0:
+               case GDK_KEY_KP_0:
+               case GDK_KEY_Home:
+               case GDK_KEY_bar:
+                       return gtk_source_vim_motion_complete (self, motion_line_start, INCLUSIVE, CHARWISE);
+
+               case GDK_KEY_1: case GDK_KEY_KP_1:
+               case GDK_KEY_2: case GDK_KEY_KP_2:
+               case GDK_KEY_3: case GDK_KEY_KP_3:
+               case GDK_KEY_4: case GDK_KEY_KP_4:
+               case GDK_KEY_5: case GDK_KEY_KP_5:
+               case GDK_KEY_6: case GDK_KEY_KP_6:
+               case GDK_KEY_7: case GDK_KEY_KP_7:
+               case GDK_KEY_8: case GDK_KEY_KP_8:
+               case GDK_KEY_9: case GDK_KEY_KP_9:
+                       get_number (keyval, &n);
+                       gtk_source_vim_state_set_count (state, n);
+                       return TRUE;
+
+               case GDK_KEY_asciicircum:
+               case GDK_KEY_underscore:
+                       return gtk_source_vim_motion_complete (self, motion_line_first_char, INCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_space:
+                       return gtk_source_vim_motion_complete (self, motion_forward_char, EXCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_BackSpace:
+                       return gtk_source_vim_motion_complete (self, motion_backward_char, INCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_Left:
+               case GDK_KEY_h:
+                       return gtk_source_vim_motion_complete (self, motion_backward_char_same_line, 
INCLUSIVE, CHARWISE);
+
+               case GDK_KEY_Right:
+               case GDK_KEY_l:
+                       return gtk_source_vim_motion_complete (self, motion_forward_char_same_line, 
EXCLUSIVE, CHARWISE);
+
+               case GDK_KEY_ISO_Enter:
+               case GDK_KEY_KP_Enter:
+               case GDK_KEY_Return:
+                       return gtk_source_vim_motion_complete (self, motion_next_line_first_char, EXCLUSIVE, 
LINEWISE);
+
+               case GDK_KEY_End:
+               case GDK_KEY_dollar:
+                       return gtk_source_vim_motion_complete (self, motion_line_end, INCLUSIVE, CHARWISE);
+
+               case GDK_KEY_Down:
+               case GDK_KEY_j:
+                       return gtk_source_vim_motion_complete (self, motion_next_line_visual_column, 
EXCLUSIVE, LINEWISE);
+
+               case GDK_KEY_Up:
+               case GDK_KEY_k:
+                       return gtk_source_vim_motion_complete (self, motion_prev_line_visual_column, 
INCLUSIVE, LINEWISE);
+
+               case GDK_KEY_G:
+                       self->is_jump = TRUE;
+                       if (gtk_source_vim_state_get_count_set (state))
+                               return gtk_source_vim_motion_complete (self, motion_line_number, INCLUSIVE, 
LINEWISE);
+                       return gtk_source_vim_motion_complete (self, motion_last_line_first_char, INCLUSIVE, 
LINEWISE);
+
+               case GDK_KEY_g:
+                       self->g_command = TRUE;
+                       return TRUE;
+
+               case GDK_KEY_H:
+                       self->is_jump = TRUE;
+                       return gtk_source_vim_motion_complete (self, motion_screen_top, INCLUSIVE, LINEWISE);
+
+               case GDK_KEY_M:
+                       self->is_jump = TRUE;
+                       return gtk_source_vim_motion_complete (self, motion_screen_middle, INCLUSIVE, 
LINEWISE);
+
+               case GDK_KEY_L:
+                       self->is_jump = TRUE;
+                       return gtk_source_vim_motion_complete (self, motion_screen_bottom, INCLUSIVE, 
LINEWISE);
+
+               case GDK_KEY_w:
+                       return gtk_source_vim_motion_complete (self, motion_forward_word_start, EXCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_W:
+                       return gtk_source_vim_motion_complete (self, motion_forward_WORD_start, EXCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_b:
+                       return gtk_source_vim_motion_complete (self, motion_backward_word_start, INCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_B:
+                       return gtk_source_vim_motion_complete (self, motion_backward_WORD_start, INCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_e:
+                       return gtk_source_vim_motion_complete (self, motion_forward_word_end, INCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_E:
+                       return gtk_source_vim_motion_complete (self, motion_forward_WORD_end, INCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_f:
+                       return gtk_source_vim_motion_begin_char_pending (self, motion_f_char, INCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_F:
+                       return gtk_source_vim_motion_begin_char_pending (self, motion_F_char, INCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_t:
+                       return gtk_source_vim_motion_begin_char_pending (self, motion_f_char, EXCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_T:
+                       return gtk_source_vim_motion_begin_char_pending (self, motion_F_char, EXCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_parenleft:
+                       return gtk_source_vim_motion_complete (self, motion_backward_sentence_start, 
INCLUSIVE, CHARWISE);
+
+               case GDK_KEY_parenright:
+                       return gtk_source_vim_motion_complete (self, motion_forward_sentence_start, 
EXCLUSIVE, CHARWISE);
+
+               case GDK_KEY_braceleft:
+                       return gtk_source_vim_motion_complete (self, motion_backward_paragraph_start, 
INCLUSIVE, CHARWISE);
+
+               case GDK_KEY_braceright:
+                       return gtk_source_vim_motion_complete (self, motion_forward_paragraph_end, EXCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_asterisk:
+                       return gtk_source_vim_motion_complete (self, motion_forward_search_word, EXCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_numbersign:
+                       return gtk_source_vim_motion_complete (self, motion_backward_search_word, INCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_n:
+                       self->is_jump = TRUE;
+                       if (gtk_source_vim_state_get_reverse_search (GTK_SOURCE_VIM_STATE (self)))
+                               return gtk_source_vim_motion_complete (self, motion_prev_search, INCLUSIVE, 
CHARWISE);
+                       else
+                               return gtk_source_vim_motion_complete (self, motion_next_search, INCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_N:
+                       self->is_jump = TRUE;
+                       if (gtk_source_vim_state_get_reverse_search (GTK_SOURCE_VIM_STATE (self)))
+                               return gtk_source_vim_motion_complete (self, motion_next_search, INCLUSIVE, 
CHARWISE);
+                       else
+                               return gtk_source_vim_motion_complete (self, motion_prev_search, INCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_bracketleft:
+                       self->bracket_left = TRUE;
+                       return TRUE;
+
+               case GDK_KEY_bracketright:
+                       self->bracket_right = TRUE;
+                       return TRUE;
+
+               case GDK_KEY_percent:
+                       self->is_jump = TRUE;
+                       return gtk_source_vim_motion_complete (self, motion_matching_char, EXCLUSIVE, 
CHARWISE);
+
+               case GDK_KEY_grave:
+                       self->is_jump = TRUE;
+                       self->mark_charwise = TRUE;
+                       return TRUE;
+
+               case GDK_KEY_apostrophe:
+                       self->is_jump = TRUE;
+                       self->mark_linewise = TRUE;
+                       return TRUE;
+
+               default:
+                       return gtk_source_vim_motion_bail (self);
+       }
+
+       return FALSE;
+}
+
+static void
+gtk_source_vim_motion_repeat (GtkSourceVimState *state)
+{
+       GtkSourceVimMotion *self = (GtkSourceVimMotion *)state;
+       GtkSourceBuffer *buffer;
+       GtkTextIter iter;
+       int count;
+
+       g_assert (GTK_SOURCE_IS_VIM_MOTION (self));
+
+       if (self->failed)
+       {
+               return;
+       }
+
+       buffer = gtk_source_vim_state_get_buffer (state, &iter, NULL);
+       count = get_adjusted_count (self);
+
+       if (self->mark != NULL)
+       {
+               gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer),
+                                                 &iter,
+                                                 self->mark);
+       }
+
+       do
+       {
+               if (!gtk_source_vim_motion_apply (self, &iter, FALSE))
+                       break;
+       } while (--count > 0);
+
+       if (self->mark != NULL)
+       {
+               gtk_text_buffer_move_mark (GTK_TEXT_BUFFER (buffer),
+                                          self->mark,
+                                          &iter);
+       }
+       else
+       {
+               gtk_source_vim_state_select (state, &iter, &iter);
+       }
+}
+
+static void
+gtk_source_vim_motion_leave (GtkSourceVimState *state)
+{
+       GtkSourceVimMotion *self = (GtkSourceVimMotion *)state;
+
+       g_assert (GTK_SOURCE_IS_VIM_MOTION (self));
+
+       if (self->apply_on_leave)
+       {
+               /* If this motion is a jump, then add it to the jumplist */
+               if (self->is_jump)
+               {
+                       GtkTextIter origin;
+
+                       gtk_source_vim_state_get_buffer (state, &origin, NULL);
+                       gtk_source_vim_state_push_jump (state, &origin);
+               }
+
+               gtk_source_vim_motion_repeat (state);
+       }
+}
+
+static void
+gtk_source_vim_motion_resume (GtkSourceVimState *state,
+                              GtkSourceVimState *from)
+{
+       GtkSourceVimMotion *self = (GtkSourceVimMotion *)state;
+
+       g_assert (GTK_SOURCE_IS_VIM_STATE (self));
+       g_assert (GTK_SOURCE_IS_VIM_STATE (from));
+
+       if (GTK_SOURCE_IS_VIM_CHAR_PENDING (from))
+       {
+               GtkSourceVimCharPending *pending = GTK_SOURCE_VIM_CHAR_PENDING (from);
+               gunichar ch = gtk_source_vim_char_pending_get_character (pending);
+               const char *str = gtk_source_vim_char_pending_get_string (pending);
+
+               self->f_char = ch;
+               g_string_append (self->command_text, str);
+               gtk_source_vim_state_unparent (from);
+               gtk_source_vim_state_pop (state);
+               return;
+       }
+
+       gtk_source_vim_state_unparent (from);
+}
+
+static void
+gtk_source_vim_motion_append_command (GtkSourceVimState *state,
+                                      GString           *string)
+{
+       GtkSourceVimMotion *self = (GtkSourceVimMotion *)state;
+
+       g_assert (GTK_SOURCE_IS_VIM_MOTION (self));
+       g_assert (state != NULL);
+
+       if (self->command_text->len > 0)
+       {
+               g_string_append_len (string,
+                                    self->command_text->str,
+                                    self->command_text->len);
+       }
+}
+
+static void
+gtk_source_vim_motion_dispose (GObject *object)
+{
+       GtkSourceVimMotion *self = (GtkSourceVimMotion *)object;
+
+       g_clear_pointer (&self->chained, g_ptr_array_unref);
+
+       g_clear_object (&self->mark);
+
+       if (self->command_text != NULL)
+       {
+               g_string_free (self->command_text, TRUE);
+               self->command_text = NULL;
+       }
+
+       G_OBJECT_CLASS (gtk_source_vim_motion_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_vim_motion_class_init (GtkSourceVimMotionClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       GtkSourceVimStateClass *state_class = GTK_SOURCE_VIM_STATE_CLASS (klass);
+
+       object_class->dispose = gtk_source_vim_motion_dispose;
+
+       state_class->append_command = gtk_source_vim_motion_append_command;
+       state_class->handle_keypress = gtk_source_vim_motion_handle_keypress;
+       state_class->leave = gtk_source_vim_motion_leave;
+       state_class->repeat = gtk_source_vim_motion_repeat;
+       state_class->resume = gtk_source_vim_motion_resume;
+}
+
+static void
+gtk_source_vim_motion_init (GtkSourceVimMotion *self)
+{
+       self->apply_on_leave = TRUE;
+       self->command_text = g_string_new (NULL);
+       self->invalidates_visual_column = TRUE;
+       self->wise = CHARWISE;
+       self->inclusivity = INCLUSIVE;
+}
+
+gboolean
+gtk_source_vim_motion_apply (GtkSourceVimMotion *self,
+                             GtkTextIter        *iter,
+                             gboolean            apply_inclusive)
+{
+       gboolean ret = FALSE;
+       guint begin_offset;
+       int count;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_MOTION (self), FALSE);
+
+       if (self->motion == NULL || self->failed)
+       {
+               return FALSE;
+       }
+
+       self->applying_inclusive = !!apply_inclusive;
+
+       begin_offset = gtk_text_iter_get_offset (iter);
+       count = get_adjusted_count (self);
+
+       do
+       {
+               self->apply_count++;
+               if (!self->motion (iter, self))
+                       goto do_inclusive;
+       } while (--count > 0);
+
+       ret = TRUE;
+
+do_inclusive:
+       self->apply_count = 0;
+
+       if (apply_inclusive)
+       {
+               guint end_offset = gtk_text_iter_get_offset (iter);
+
+               if (FALSE) {}
+               else if (self->inclusivity == INCLUSIVE &&
+                        end_offset > begin_offset &&
+                        !gtk_text_iter_ends_line (iter))
+                       gtk_text_iter_forward_char (iter);
+               else if (self->inclusivity == EXCLUSIVE
+                        && end_offset < begin_offset &&
+                        !gtk_text_iter_ends_line (iter))
+                       gtk_text_iter_forward_char (iter);
+       }
+
+       self->applying_inclusive = FALSE;
+
+       return ret;
+}
+
+gboolean
+gtk_source_vim_motion_get_apply_on_leave (GtkSourceVimMotion *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_MOTION (self), FALSE);
+
+       return self->apply_on_leave;
+}
+
+void
+gtk_source_vim_motion_set_apply_on_leave (GtkSourceVimMotion *self,
+                                          gboolean            apply_on_leave)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM_MOTION (self));
+
+       self->apply_on_leave = !!apply_on_leave;
+}
+
+void
+gtk_source_vim_motion_set_mark (GtkSourceVimMotion *self,
+                                GtkTextMark        *mark)
+{
+       g_assert (GTK_SOURCE_IS_VIM_MOTION (self));
+       g_assert (!mark || GTK_IS_TEXT_MARK (mark));
+
+       g_set_object (&self->mark, mark);
+}
+
+GtkSourceVimState *
+gtk_source_vim_motion_new_first_char (void)
+{
+       GtkSourceVimMotion *self;
+
+       self = g_object_new (GTK_SOURCE_TYPE_VIM_MOTION, NULL);
+       self->motion = motion_line_first_char;
+
+       return GTK_SOURCE_VIM_STATE (self);
+}
+
+GtkSourceVimState *
+gtk_source_vim_motion_new_line_end (void)
+{
+       GtkSourceVimMotion *self;
+
+       self = g_object_new (GTK_SOURCE_TYPE_VIM_MOTION, NULL);
+       self->motion = motion_line_end;
+       self->inclusivity = INCLUSIVE;
+       self->wise = CHARWISE;
+
+       return GTK_SOURCE_VIM_STATE (self);
+}
+
+GtkSourceVimState *
+gtk_source_vim_motion_new_line_start (void)
+{
+       GtkSourceVimMotion *self;
+
+       self = g_object_new (GTK_SOURCE_TYPE_VIM_MOTION, NULL);
+       self->motion = motion_line_start;
+       self->inclusivity = INCLUSIVE;
+
+       return GTK_SOURCE_VIM_STATE (self);
+}
+
+GtkSourceVimState *
+gtk_source_vim_motion_new_previous_line_end (void)
+{
+       GtkSourceVimMotion *self;
+
+       self = g_object_new (GTK_SOURCE_TYPE_VIM_MOTION, NULL);
+       self->motion = motion_prev_line_end;
+       self->inclusivity = EXCLUSIVE;
+
+       return GTK_SOURCE_VIM_STATE (self);
+}
+
+GtkSourceVimState *
+gtk_source_vim_motion_new_forward_char (void)
+{
+       GtkSourceVimMotion *self;
+
+       self = g_object_new (GTK_SOURCE_TYPE_VIM_MOTION, NULL);
+       self->motion = motion_forward_char_same_line_eol_okay;
+       self->inclusivity = EXCLUSIVE;
+
+       return GTK_SOURCE_VIM_STATE (self);
+}
+
+static gboolean
+do_motion_line_end_with_nl (GtkTextIter *iter,
+                            int          apply_count,
+                            int          count)
+{
+       /* This function has to take into account newlines so that we
+        * can move and delete whole lines. It is extra complicated
+        * because we can't actually move when we have an empty line.
+        * So we know our :count to apply and can do it in one pass
+        * and rely on subsequent calls to be idempotent. When applying
+        * we get the same result and need not worry about the impedance
+        * mismatch with VIM character movements.
+        */
+
+       if (apply_count != 1)
+               return FALSE;
+
+       if (count == 1)
+       {
+               if (gtk_text_iter_ends_line (iter))
+                       return TRUE;
+
+               return gtk_text_iter_forward_to_line_end (iter);
+       }
+
+       gtk_text_iter_set_line (iter, gtk_text_iter_get_line (iter) + count - 1);
+
+       if (!gtk_text_iter_ends_line (iter))
+               gtk_text_iter_forward_to_line_end (iter);
+
+       return TRUE;
+}
+
+static gboolean
+motion_line_end_with_nl (GtkTextIter        *iter,
+                         GtkSourceVimMotion *self)
+{
+       int count = get_adjusted_count (self);
+       return do_motion_line_end_with_nl (iter, self->apply_count, count);
+}
+
+static gboolean
+motion_next_line_end_with_nl (GtkTextIter        *iter,
+                              GtkSourceVimMotion *self)
+{
+       int count = get_adjusted_count (self);
+       return do_motion_line_end_with_nl (iter, self->apply_count, count + 1);
+}
+
+GtkSourceVimState *
+gtk_source_vim_motion_new_line_end_with_nl (void)
+{
+       GtkSourceVimMotion *self;
+
+       self = g_object_new (GTK_SOURCE_TYPE_VIM_MOTION, NULL);
+       self->motion = motion_line_end_with_nl;
+       self->inclusivity = EXCLUSIVE;
+
+       return GTK_SOURCE_VIM_STATE (self);
+}
+
+GtkSourceVimState *
+gtk_source_vim_motion_new_next_line_end_with_nl (void)
+{
+       GtkSourceVimMotion *self;
+
+       self = g_object_new (GTK_SOURCE_TYPE_VIM_MOTION, NULL);
+       self->motion = motion_next_line_end_with_nl;
+       self->inclusivity = EXCLUSIVE;
+
+       return GTK_SOURCE_VIM_STATE (self);
+}
+
+GtkSourceVimState *
+gtk_source_vim_motion_new_none (void)
+{
+       GtkSourceVimMotion *self;
+
+       self = g_object_new (GTK_SOURCE_TYPE_VIM_MOTION, NULL);
+       self->motion = motion_none;
+       self->inclusivity = INCLUSIVE;
+       self->wise = CHARWISE;
+
+       return GTK_SOURCE_VIM_STATE (self);
+}
+
+static gboolean
+motion_chained (GtkTextIter        *iter,
+                GtkSourceVimMotion *self)
+{
+       GtkTextIter before = *iter;
+
+       for (guint i = 0; i < self->chained->len; i++)
+       {
+               GtkSourceVimMotion *motion = g_ptr_array_index (self->chained, i);
+
+               gtk_source_vim_motion_set_mark (motion, self->mark);
+               gtk_source_vim_motion_apply (motion, iter, self->applying_inclusive);
+               gtk_source_vim_motion_set_mark (motion, NULL);
+       }
+
+       return !gtk_text_iter_equal (&before, iter);
+}
+
+static void
+gtk_source_vim_motion_add (GtkSourceVimMotion *self,
+                           GtkSourceVimMotion *other)
+{
+       g_assert (GTK_SOURCE_IS_VIM_MOTION (self));
+       g_assert (self->motion == motion_chained);
+       g_assert (GTK_SOURCE_IS_VIM_MOTION (other));
+       g_assert (self != other);
+
+       if (self->chained->len > 0)
+       {
+               GtkSourceVimMotion *last;
+
+               last = g_ptr_array_index (self->chained, self->chained->len - 1);
+
+               if (last->motion == other->motion &&
+                   last->inclusivity == other->inclusivity &&
+                   last->f_char == other->f_char)
+               {
+                       int count = gtk_source_vim_state_get_count (GTK_SOURCE_VIM_STATE (last))
+                                 + gtk_source_vim_state_get_count (GTK_SOURCE_VIM_STATE (other));
+
+                       gtk_source_vim_state_set_count (GTK_SOURCE_VIM_STATE (last), count);
+                       return;
+               }
+       }
+
+       gtk_source_vim_motion_set_mark (other, NULL);
+       g_ptr_array_add (self->chained, g_object_ref (other));
+       gtk_source_vim_state_set_parent (GTK_SOURCE_VIM_STATE (other),
+                                        GTK_SOURCE_VIM_STATE (self));
+}
+
+static void
+clear_state (gpointer data)
+{
+       GtkSourceVimState *state = data;
+       gtk_source_vim_state_unparent (state);
+       g_object_unref (state);
+}
+
+GtkSourceVimState *
+gtk_source_vim_motion_chain (GtkSourceVimMotion *self,
+                             GtkSourceVimMotion *other)
+{
+       GtkSourceVimMotion *chained;
+
+       g_return_val_if_fail (!self || GTK_SOURCE_IS_VIM_MOTION (self), NULL);
+       g_return_val_if_fail (!other || GTK_SOURCE_IS_VIM_MOTION (other), NULL);
+
+       if (self == NULL || self->motion != motion_chained)
+       {
+               chained = GTK_SOURCE_VIM_MOTION (gtk_source_vim_motion_new ());
+               chained->motion = motion_chained;
+               chained->inclusivity = INCLUSIVE;
+               chained->chained = g_ptr_array_new_with_free_func (clear_state);
+       }
+       else
+       {
+               chained = g_object_ref (self);
+       }
+
+       if (self != chained && self != NULL)
+               gtk_source_vim_motion_add (chained, self);
+
+       if (other != NULL)
+               gtk_source_vim_motion_add (chained, other);
+
+       return GTK_SOURCE_VIM_STATE (chained);
+}
+
+gboolean
+gtk_source_vim_motion_invalidates_visual_column (GtkSourceVimMotion *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_MOTION (self), FALSE);
+
+       return self->invalidates_visual_column;
+}
+
+gboolean
+gtk_source_vim_motion_is_linewise (GtkSourceVimMotion *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_MOTION (self), FALSE);
+
+       return self->wise == LINEWISE;
+}
+
+gboolean
+gtk_source_vim_motion_is_jump (GtkSourceVimMotion *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_MOTION (self), FALSE);
+
+       return self->is_jump;
+}
+
+GtkSourceVimState *
+gtk_source_vim_motion_new_down (int alter_count)
+{
+       GtkSourceVimMotion *self;
+
+       self = g_object_new (GTK_SOURCE_TYPE_VIM_MOTION, NULL);
+       self->motion = motion_next_line_visual_column;
+       self->inclusivity = EXCLUSIVE;
+       self->wise = LINEWISE;
+       self->alter_count = alter_count;
+
+       return GTK_SOURCE_VIM_STATE (self);
+}
+
+void
+gtk_source_vim_motion_set_linewise_keyval (GtkSourceVimMotion *self,
+                                           guint               keyval)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM_MOTION (self));
+
+       self->linewise_keyval = keyval;
+}
+
+gboolean
+gtk_source_vim_iter_forward_word_end (GtkTextIter *iter)
+{
+       forward_classified_end (iter, classify_word_newline_stop);
+       return TRUE;
+}
+
+gboolean
+gtk_source_vim_iter_forward_WORD_end (GtkTextIter *iter)
+{
+       forward_classified_end (iter, classify_WORD_newline_stop);
+       return TRUE;
+}
+
+gboolean
+gtk_source_vim_iter_backward_word_start (GtkTextIter *iter)
+{
+       backward_classified_start (iter, classify_word_newline_stop);
+       return TRUE;
+}
+
+gboolean
+gtk_source_vim_iter_backward_WORD_start (GtkTextIter *iter)
+{
+       backward_classified_start (iter, classify_WORD_newline_stop);
+       return TRUE;
+}
+
+static inline gboolean
+unichar_ends_sentence (gunichar ch)
+{
+       switch (ch)
+       {
+               case '.':
+               case '!':
+               case '?':
+                       return TRUE;
+
+               default:
+                       return FALSE;
+       }
+}
+
+static inline gboolean
+unichar_can_trail_sentence (gunichar ch)
+{
+       switch (ch)
+       {
+               case '.':
+               case '!':
+               case '?':
+               case '\'':
+               case '"':
+               case ')':
+               case ']':
+                       return TRUE;
+
+               default:
+                       return FALSE;
+       }
+}
+
+gboolean
+gtk_source_vim_iter_forward_sentence_end (GtkTextIter *iter)
+{
+       /* From VIM:
+        *
+        * A sentence is defined as ending at a '.', '!' or '?' followed by either the
+        * end of a line, or by a space or tab.  Any number of closing ')', ']', '"'
+        * and ''' characters may appear after the '.', '!' or '?' before the spaces,
+        * tabs or end of line.  A paragraph and section boundary is also a sentence
+        * boundary.
+        */
+
+       if (gtk_text_iter_is_end (iter))
+               return FALSE;
+
+       /* First find a .!? */
+       while (gtk_text_iter_forward_char (iter))
+       {
+               gunichar ch = gtk_text_iter_get_char (iter);
+
+               if (unichar_ends_sentence (ch))
+                       break;
+
+               /* If we reached a newline, and the next char is also
+                * a newline, then we stop at this newline.
+                */
+               if (gtk_text_iter_ends_line (iter))
+               {
+                       GtkTextIter peek = *iter;
+
+                       if (gtk_text_iter_forward_char (&peek) || gtk_text_iter_is_end (&peek))
+                       {
+                               return TRUE;
+                       }
+               }
+       }
+
+       /* Read past any acceptable trailing chars */
+       while (gtk_text_iter_forward_char (iter))
+       {
+               gunichar ch = gtk_text_iter_get_char (iter);
+
+               if (!unichar_can_trail_sentence (ch))
+                       break;
+       }
+
+       /* If we are on a space or end of a buffer, then we found the end */
+       if (gtk_text_iter_is_end (iter) || iter_isspace (iter))
+       {
+               return TRUE;
+       }
+
+       /* This is not a suitable sentence candidate. We must try again */
+       return gtk_source_vim_iter_forward_sentence_end (iter);
+}
+
+gboolean
+gtk_source_vim_iter_backward_sentence_start (GtkTextIter *iter)
+{
+       return motion_backward_sentence_start (iter, NULL);
+}
+
+gboolean
+gtk_source_vim_iter_forward_paragraph_end (GtkTextIter *iter)
+{
+       return motion_forward_paragraph_end (iter, NULL);
+}
+
+gboolean
+gtk_source_vim_iter_backward_paragraph_start (GtkTextIter *iter)
+{
+       return motion_backward_paragraph_start (iter, NULL);
+}
+
+typedef struct
+{
+       gunichar ch;
+       gunichar opposite;
+       int count;
+} FindPredicate;
+
+static gboolean
+find_predicate (gunichar ch,
+                gpointer data)
+{
+       FindPredicate *find = data;
+
+       if (ch == find->opposite)
+               find->count++;
+       else if (ch == find->ch)
+               find->count--;
+
+       return find->count == 0;
+}
+
+static gboolean
+gtk_source_vim_iter_backward_block_start (GtkTextIter *iter,
+                                          gunichar     ch,
+                                          gunichar     opposite)
+{
+       FindPredicate find = { ch, opposite, 1 };
+
+       if (gtk_text_iter_get_char (iter) == ch)
+               return TRUE;
+
+       return gtk_text_iter_backward_find_char (iter, find_predicate, &find, NULL);
+}
+
+static gboolean
+gtk_source_vim_iter_forward_block_end (GtkTextIter *iter,
+                                       gunichar     ch,
+                                       gunichar     opposite)
+{
+       FindPredicate find = { ch, opposite, 1 };
+
+       if (gtk_text_iter_get_char (iter) == ch)
+               return TRUE;
+
+       return gtk_text_iter_forward_find_char (iter, find_predicate, &find, NULL);
+}
+
+gboolean
+gtk_source_vim_iter_backward_block_paren_start (GtkTextIter *iter)
+{
+       return gtk_source_vim_iter_backward_block_start (iter, '(', ')');
+}
+
+gboolean
+gtk_source_vim_iter_forward_block_paren_end (GtkTextIter *iter)
+{
+       return gtk_source_vim_iter_forward_block_end (iter, ')', '(');
+}
+
+gboolean
+gtk_source_vim_iter_backward_block_brace_start (GtkTextIter *iter)
+{
+       return gtk_source_vim_iter_backward_block_start (iter, '{', '}');
+}
+
+gboolean
+gtk_source_vim_iter_forward_block_brace_end (GtkTextIter *iter)
+{
+       return gtk_source_vim_iter_forward_block_end (iter, '}', '{');
+}
+
+gboolean
+gtk_source_vim_iter_forward_block_bracket_end (GtkTextIter *iter)
+{
+       return gtk_source_vim_iter_forward_block_end (iter, ']', '[');
+}
+
+gboolean
+gtk_source_vim_iter_backward_block_bracket_start (GtkTextIter *iter)
+{
+       return gtk_source_vim_iter_backward_block_start (iter, '[', ']');
+}
+
+gboolean
+gtk_source_vim_iter_forward_block_lt_gt_end (GtkTextIter *iter)
+{
+       return gtk_source_vim_iter_forward_block_end (iter, '>', '<');
+}
+
+gboolean
+gtk_source_vim_iter_backward_block_lt_gt_start (GtkTextIter *iter)
+{
+       return gtk_source_vim_iter_backward_block_start (iter, '<', '>');
+}
+
+static gboolean
+gtk_source_vim_iter_backward_quote_start (GtkTextIter *iter,
+                                          gunichar     ch)
+{
+       FindPredicate find = { ch, 0 , 1 };
+       GtkTextIter limit = *iter;
+       gtk_text_iter_set_line_offset (&limit, 0);
+       return gtk_text_iter_backward_find_char (iter, find_predicate, &find, NULL);
+}
+
+static gboolean
+gtk_source_vim_iter_ends_quote (const GtkTextIter *iter,
+                                gunichar           ch)
+{
+       if (ch == gtk_text_iter_get_char (iter) &&
+           !gtk_text_iter_starts_line (iter))
+       {
+               GtkTextIter alt = *iter;
+
+               if (gtk_source_vim_iter_backward_quote_start (&alt, ch))
+               {
+                       return TRUE;
+               }
+       }
+
+       return FALSE;
+}
+
+static gboolean
+gtk_source_vim_iter_forward_quote_end (GtkTextIter *iter,
+                                       gunichar     ch)
+{
+       FindPredicate find = { ch, 0, 1 };
+       GtkTextIter limit = *iter;
+
+       if (!gtk_text_iter_ends_line (&limit))
+               gtk_text_iter_forward_to_line_end (&limit);
+
+       return gtk_text_iter_forward_find_char (iter, find_predicate, &find, NULL);
+}
+
+gboolean
+gtk_source_vim_iter_forward_quote_double (GtkTextIter *iter)
+{
+       return gtk_source_vim_iter_forward_quote_end (iter, '"');
+}
+
+gboolean
+gtk_source_vim_iter_ends_quote_double (const GtkTextIter *iter)
+{
+       return gtk_source_vim_iter_ends_quote (iter, '"');
+}
+
+gboolean
+gtk_source_vim_iter_ends_quote_single (const GtkTextIter *iter)
+{
+       return gtk_source_vim_iter_ends_quote (iter, '\'');
+}
+
+gboolean
+gtk_source_vim_iter_ends_quote_grave (const GtkTextIter *iter)
+{
+       return gtk_source_vim_iter_ends_quote (iter, '\'');
+}
+
+gboolean
+gtk_source_vim_iter_backward_quote_double (GtkTextIter *iter)
+{
+       return gtk_source_vim_iter_backward_quote_start (iter, '"');
+}
+
+gboolean
+gtk_source_vim_iter_forward_quote_single (GtkTextIter *iter)
+{
+       return gtk_source_vim_iter_forward_quote_end (iter, '\'');
+}
+
+gboolean
+gtk_source_vim_iter_backward_quote_single (GtkTextIter *iter)
+{
+       return gtk_source_vim_iter_backward_quote_start (iter, '\'');
+}
+
+gboolean
+gtk_source_vim_iter_forward_quote_grave (GtkTextIter *iter)
+{
+       return gtk_source_vim_iter_forward_quote_end (iter, '`');
+}
+
+gboolean
+gtk_source_vim_iter_backward_quote_grave (GtkTextIter *iter)
+{
+       return gtk_source_vim_iter_backward_quote_start (iter, '`');
+}
+
+gboolean
+gtk_source_vim_iter_starts_word (const GtkTextIter *iter)
+{
+       GtkTextIter prev;
+
+       if (gtk_text_iter_starts_line (iter))
+       {
+               /* A blank line is a word */
+               return gtk_text_iter_ends_line (iter) || !iter_isspace (iter);
+       }
+       else if (gtk_text_iter_ends_line (iter))
+       {
+               return FALSE;
+       }
+
+       if (iter_isspace (iter))
+               return FALSE;
+
+       prev = *iter;
+       gtk_text_iter_backward_char (&prev);
+
+       return simple_word_classify (gtk_text_iter_get_char (iter)) !=
+              simple_word_classify (gtk_text_iter_get_char (&prev));
+}
+
+gboolean
+gtk_source_vim_iter_ends_word (const GtkTextIter *iter)
+{
+       GtkTextIter next;
+
+       if (gtk_text_iter_ends_line (iter))
+       {
+               /* A blank line is a word */
+               if (gtk_text_iter_starts_line (iter))
+                       return TRUE;
+
+               return FALSE;
+       }
+
+       if (iter_isspace (iter))
+               return FALSE;
+
+       next = *iter;
+       gtk_text_iter_forward_char (&next);
+
+       return simple_word_classify (gtk_text_iter_get_char (iter)) !=
+              simple_word_classify (gtk_text_iter_get_char (&next));
+}
+
+gboolean
+gtk_source_vim_iter_starts_WORD (const GtkTextIter *iter)
+{
+       GtkTextIter prev;
+
+       if (gtk_text_iter_starts_line (iter))
+       {
+               /* A blank line is a word */
+               return gtk_text_iter_ends_line (iter) || !iter_isspace (iter);
+       }
+       else if (gtk_text_iter_ends_line (iter))
+       {
+               return FALSE;
+       }
+
+       if (iter_isspace (iter))
+               return FALSE;
+
+       prev = *iter;
+       gtk_text_iter_backward_char (&prev);
+
+       return iter_isspace (&prev);
+}
+
+gboolean
+gtk_source_vim_iter_ends_WORD (const GtkTextIter *iter)
+{
+       GtkTextIter next;
+
+       if (gtk_text_iter_ends_line (iter))
+       {
+               /* A blank line is a word */
+               if (gtk_text_iter_starts_line (iter))
+                       return TRUE;
+
+               return FALSE;
+       }
+
+       if (iter_isspace (iter))
+               return FALSE;
+
+       next = *iter;
+       if (!gtk_text_iter_forward_char (&next))
+               return TRUE;
+
+       return iter_isspace (&next);
+}
diff --git a/gtksourceview/vim/gtksourcevimmotion.h b/gtksourceview/vim/gtksourcevimmotion.h
new file mode 100644
index 00000000..32531b1b
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimmotion.h
@@ -0,0 +1,90 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "gtksourcevimstate.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_VIM_MOTION (gtk_source_vim_motion_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceVimMotion, gtk_source_vim_motion, GTK_SOURCE, VIM_MOTION, GtkSourceVimState)
+
+GtkSourceVimState *gtk_source_vim_motion_new                       (void);
+GtkSourceVimState *gtk_source_vim_motion_chain                     (GtkSourceVimMotion *self,
+                                                                    GtkSourceVimMotion *other);
+GtkSourceVimState *gtk_source_vim_motion_new_none                  (void);
+GtkSourceVimState *gtk_source_vim_motion_new_first_char            (void);
+GtkSourceVimState *gtk_source_vim_motion_new_line_end              (void);
+GtkSourceVimState *gtk_source_vim_motion_new_line_end_with_nl      (void);
+GtkSourceVimState *gtk_source_vim_motion_new_next_line_end_with_nl (void);
+GtkSourceVimState *gtk_source_vim_motion_new_previous_line_end     (void);
+GtkSourceVimState *gtk_source_vim_motion_new_forward_char          (void);
+GtkSourceVimState *gtk_source_vim_motion_new_down                  (int                 adjust_count);
+GtkSourceVimState *gtk_source_vim_motion_new_line_start            (void);
+void               gtk_source_vim_motion_set_linewise_keyval       (GtkSourceVimMotion *self,
+                                                                    guint               keyval);
+gboolean           gtk_source_vim_motion_get_apply_on_leave        (GtkSourceVimMotion *self);
+void               gtk_source_vim_motion_set_apply_on_leave        (GtkSourceVimMotion *self,
+                                                                    gboolean            apply_on_leave);
+void               gtk_source_vim_motion_set_mark                  (GtkSourceVimMotion *self,
+                                                                    GtkTextMark        *mark);
+gboolean           gtk_source_vim_motion_apply                     (GtkSourceVimMotion *self,
+                                                                    GtkTextIter        *iter,
+                                                                    gboolean            apply_inclusive);
+gboolean           gtk_source_vim_motion_invalidates_visual_column (GtkSourceVimMotion *self);
+gboolean           gtk_source_vim_motion_is_linewise               (GtkSourceVimMotion *self);
+gboolean           gtk_source_vim_motion_is_jump                   (GtkSourceVimMotion *self);
+
+gboolean gtk_source_vim_iter_backward_block_brace_start   (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_backward_block_bracket_start (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_backward_block_lt_gt_start   (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_backward_block_paren_start   (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_backward_paragraph_start     (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_backward_quote_double        (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_backward_quote_grave         (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_backward_quote_single        (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_backward_sentence_start      (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_backward_word_start          (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_backward_WORD_start          (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_forward_block_brace_end      (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_forward_block_bracket_end    (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_forward_block_lt_gt_end      (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_forward_block_paren_end      (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_forward_paragraph_end        (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_forward_quote_double         (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_forward_quote_grave          (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_forward_quote_single         (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_forward_sentence_end         (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_forward_word_end             (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_forward_WORD_end             (GtkTextIter       *iter);
+gboolean gtk_source_vim_iter_ends_word                    (const GtkTextIter *iter);
+gboolean gtk_source_vim_iter_ends_WORD                    (const GtkTextIter *iter);
+gboolean gtk_source_vim_iter_ends_quote_double            (const GtkTextIter *iter);
+gboolean gtk_source_vim_iter_ends_quote_single            (const GtkTextIter *iter);
+gboolean gtk_source_vim_iter_ends_quote_grave             (const GtkTextIter *iter);
+gboolean gtk_source_vim_iter_starts_word                  (const GtkTextIter *iter);
+gboolean gtk_source_vim_iter_starts_WORD                  (const GtkTextIter *iter);
+
+G_END_DECLS
diff --git a/gtksourceview/vim/gtksourcevimnormal.c b/gtksourceview/vim/gtksourcevimnormal.c
new file mode 100644
index 00000000..81c2d8fc
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimnormal.c
@@ -0,0 +1,1478 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "gtksourceview.h"
+
+#include "gtksourcevim.h"
+#include "gtksourcevimcharpending.h"
+#include "gtksourcevimcommand.h"
+#include "gtksourcevimcommandbar.h"
+#include "gtksourceviminsert.h"
+#include "gtksourcevimmotion.h"
+#include "gtksourcevimnormal.h"
+#include "gtksourcevimreplace.h"
+#include "gtksourcevimtextobject.h"
+#include "gtksourcevimvisual.h"
+
+#define REPLAY(_block) do { _block; } while (--self->count > 0);
+
+typedef gboolean (*KeyHandler) (GtkSourceVimNormal *self,
+                                guint               keyval,
+                                guint               keycode,
+                                GdkModifierType     mods,
+                                const char         *string);
+
+typedef enum
+{
+       CHANGE_NONE  = 0,
+       CHANGE_INNER = 1,
+       CHANGE_A     = 2,
+} ChangeModifier;
+
+struct _GtkSourceVimNormal
+{
+       GtkSourceVimState  parent_instance;
+       GString           *command_text;
+       GtkSourceVimState *repeat;
+       GtkSourceVimState *last_visual;
+       KeyHandler         handler;
+       int                count;
+       ChangeModifier     change_modifier;
+       guint              has_count : 1;
+};
+
+static gboolean key_handler_initial (GtkSourceVimNormal *self,
+                                     guint               keyval,
+                                     guint               keycode,
+                                     GdkModifierType     mods,
+                                     const char         *string);
+
+G_DEFINE_TYPE (GtkSourceVimNormal, gtk_source_vim_normal, GTK_SOURCE_TYPE_VIM_STATE)
+
+static void
+gtk_source_vim_normal_emit_ready (GtkSourceVimNormal *self)
+{
+       GtkSourceVimState *parent;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       parent = gtk_source_vim_state_get_parent (GTK_SOURCE_VIM_STATE (self));
+
+       if (GTK_SOURCE_IS_VIM (parent))
+       {
+               gtk_source_vim_emit_ready (GTK_SOURCE_VIM (parent));
+       }
+}
+
+static gboolean
+gtk_source_vim_normal_bail (GtkSourceVimNormal *self)
+{
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       gtk_source_vim_state_beep (GTK_SOURCE_VIM_STATE (self));
+       gtk_source_vim_state_set_current_register (GTK_SOURCE_VIM_STATE (self), NULL);
+       gtk_source_vim_normal_clear (self);
+
+       return TRUE;
+}
+
+static GtkSourceVimState *
+get_text_object (guint keyval,
+                 guint change_modifier)
+{
+       switch (keyval)
+       {
+               case GDK_KEY_w:
+                       if (change_modifier == CHANGE_A)
+                               return gtk_source_vim_text_object_new_a_word ();
+                       else
+                               return gtk_source_vim_text_object_new_inner_word ();
+
+               case GDK_KEY_W:
+                       if (change_modifier == CHANGE_A)
+                               return gtk_source_vim_text_object_new_a_WORD ();
+                       else
+                               return gtk_source_vim_text_object_new_inner_WORD ();
+
+               case GDK_KEY_p:
+                       if (change_modifier == CHANGE_A)
+                               return gtk_source_vim_text_object_new_a_paragraph ();
+                       else
+                               return gtk_source_vim_text_object_new_inner_paragraph ();
+
+               case GDK_KEY_s:
+                       if (change_modifier == CHANGE_A)
+                               return gtk_source_vim_text_object_new_a_sentence ();
+                       else
+                               return gtk_source_vim_text_object_new_inner_sentence ();
+
+               case GDK_KEY_bracketright:
+               case GDK_KEY_bracketleft:
+                       /* TODO: this needs to use separate mechanisms for [ vs ] */
+                       if (change_modifier == CHANGE_A)
+                               return gtk_source_vim_text_object_new_a_block_bracket ();
+                       else
+                               return gtk_source_vim_text_object_new_inner_block_bracket ();
+
+               case GDK_KEY_braceleft:
+               case GDK_KEY_braceright:
+                       /* TODO: this needs to use separate mechanisms for { vs } */
+                       if (change_modifier == CHANGE_A)
+                               return gtk_source_vim_text_object_new_a_block_brace ();
+                       else
+                               return gtk_source_vim_text_object_new_inner_block_brace ();
+
+               case GDK_KEY_less:
+               case GDK_KEY_greater:
+                       /* TODO: this needs to use separate mechanisms for < vs > */
+                       if (change_modifier == CHANGE_A)
+                               return gtk_source_vim_text_object_new_a_block_brace ();
+                       else
+                               return gtk_source_vim_text_object_new_inner_block_brace ();
+
+               case GDK_KEY_apostrophe:
+                       if (change_modifier == CHANGE_A)
+                               return gtk_source_vim_text_object_new_a_quote_single ();
+                       else
+                               return gtk_source_vim_text_object_new_inner_quote_single ();
+
+               case GDK_KEY_quotedbl:
+                       if (change_modifier == CHANGE_A)
+                               return gtk_source_vim_text_object_new_a_quote_double ();
+                       else
+                               return gtk_source_vim_text_object_new_inner_quote_double ();
+
+               case GDK_KEY_grave:
+                       if (change_modifier == CHANGE_A)
+                               return gtk_source_vim_text_object_new_a_quote_grave ();
+                       else
+                               return gtk_source_vim_text_object_new_inner_quote_grave ();
+
+               case GDK_KEY_parenleft:
+               case GDK_KEY_parenright:
+               case GDK_KEY_b:
+                       /* TODO: this needs to use separate mechanisms for ( vs ) */
+                       if (change_modifier == CHANGE_A)
+                               return gtk_source_vim_text_object_new_a_block_paren ();
+                       else
+                               return gtk_source_vim_text_object_new_inner_block_paren ();
+
+               default:
+                       return NULL;
+       }
+}
+
+static gboolean
+gtk_source_vim_normal_replace_one (GtkSourceVimNormal *self)
+{
+       GtkSourceVimState *replace;
+       GtkSourceVimState *char_pending;
+       GtkSourceVimState *motion;
+       GtkSourceVimState *selection_motion;
+       int count;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       count = self->count, self->count = 0;
+
+       char_pending = gtk_source_vim_char_pending_new ();
+       replace = gtk_source_vim_command_new ("replace-one");
+       motion = gtk_source_vim_motion_new_forward_char ();
+       selection_motion = gtk_source_vim_motion_new_none ();
+       gtk_source_vim_state_set_count (motion, count);
+
+       gtk_source_vim_command_set_motion (GTK_SOURCE_VIM_COMMAND (replace),
+                                          GTK_SOURCE_VIM_MOTION (motion));
+       gtk_source_vim_command_set_selection_motion (GTK_SOURCE_VIM_COMMAND (replace),
+                                                    GTK_SOURCE_VIM_MOTION (selection_motion));
+
+       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), replace);
+       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (replace), char_pending);
+
+       g_object_unref (motion);
+       g_object_unref (selection_motion);
+
+       return TRUE;
+}
+
+G_GNUC_NULL_TERMINATED
+static GtkSourceVimState *
+gtk_source_vim_normal_begin_change (GtkSourceVimNormal *self,
+                                    GtkSourceVimState  *insert_motion,
+                                    GtkSourceVimState  *selection_motion,
+                                    ...)
+{
+       GtkSourceVimState *ret;
+       const char *first_property_name;
+       va_list args;
+       int count;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+       g_assert (!insert_motion || GTK_SOURCE_IS_VIM_MOTION (insert_motion));
+       g_assert (!selection_motion || GTK_SOURCE_IS_VIM_MOTION (selection_motion));
+
+       count = self->count, self->count = 0;
+
+       va_start (args, selection_motion);
+       first_property_name = va_arg (args, const char *);
+       ret = GTK_SOURCE_VIM_STATE (g_object_new_valist (GTK_SOURCE_TYPE_VIM_INSERT, first_property_name, 
args));
+       va_end (args);
+
+       if (insert_motion != NULL)
+       {
+               gtk_source_vim_state_set_count (insert_motion, count);
+               gtk_source_vim_motion_set_apply_on_leave (GTK_SOURCE_VIM_MOTION (insert_motion), FALSE);
+               gtk_source_vim_state_set_parent (insert_motion, ret);
+               gtk_source_vim_insert_set_motion (GTK_SOURCE_VIM_INSERT (ret),
+                                                 GTK_SOURCE_VIM_MOTION (insert_motion));
+               g_object_unref (insert_motion);
+       }
+
+       if (selection_motion != NULL)
+       {
+               gtk_source_vim_state_set_count (selection_motion, count);
+               gtk_source_vim_motion_set_apply_on_leave (GTK_SOURCE_VIM_MOTION (selection_motion), FALSE);
+               gtk_source_vim_state_set_parent (selection_motion, ret);
+               gtk_source_vim_insert_set_selection_motion (GTK_SOURCE_VIM_INSERT (ret),
+                                                           GTK_SOURCE_VIM_MOTION (selection_motion));
+               g_object_unref (selection_motion);
+       }
+
+       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), ret);
+
+       return ret;
+}
+
+static GtkSourceVimState *
+gtk_source_vim_normal_begin_insert_text_object (GtkSourceVimNormal *self,
+                                                GtkSourceVimState  *text_object)
+{
+       GtkSourceVimState *ret;
+       int count;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+       g_assert (GTK_SOURCE_IS_VIM_TEXT_OBJECT (text_object));
+
+       count = self->count;
+
+       ret = gtk_source_vim_insert_new ();
+       gtk_source_vim_state_set_parent (text_object, ret);
+       gtk_source_vim_insert_set_text_object (GTK_SOURCE_VIM_INSERT (ret),
+                                              GTK_SOURCE_VIM_TEXT_OBJECT (text_object));
+       gtk_source_vim_state_set_count (ret, count);
+       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), ret);
+
+       return ret;
+}
+
+G_GNUC_NULL_TERMINATED
+static GtkSourceVimState *
+gtk_source_vim_normal_begin_insert (GtkSourceVimNormal   *self,
+                                    GtkSourceVimState    *motion,
+                                    GtkSourceVimInsertAt  at,
+                                    ...)
+{
+       GtkSourceVimState *ret;
+       const char *first_property_name;
+       va_list args;
+       int count;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+       g_assert (!motion || GTK_SOURCE_IS_VIM_MOTION (motion));
+
+       count = self->count;
+
+       va_start (args, at);
+       first_property_name = va_arg (args, const char *);
+       ret = GTK_SOURCE_VIM_STATE (g_object_new_valist (GTK_SOURCE_TYPE_VIM_INSERT, first_property_name, 
args));
+       va_end (args);
+
+       if (motion != NULL)
+       {
+               gtk_source_vim_motion_set_apply_on_leave (GTK_SOURCE_VIM_MOTION (motion), FALSE);
+               gtk_source_vim_insert_set_at (GTK_SOURCE_VIM_INSERT (ret), at);
+               gtk_source_vim_insert_set_motion (GTK_SOURCE_VIM_INSERT (ret),
+                                                 GTK_SOURCE_VIM_MOTION (motion));
+       }
+
+       gtk_source_vim_state_set_count (ret, count);
+       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), ret);
+
+       return ret;
+}
+
+static void
+gtk_source_vim_normal_begin_command (GtkSourceVimNormal *self,
+                                     GtkSourceVimState  *insert_motion,
+                                     GtkSourceVimState  *selection_motion,
+                                     const char         *command_str,
+                                     guint               linewise_keyval)
+{
+       GtkSourceVimCommand *command;
+       gboolean pop_command = TRUE;
+       int count;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+       g_assert (!insert_motion || GTK_SOURCE_IS_VIM_MOTION (insert_motion));
+       g_assert (!selection_motion || GTK_SOURCE_IS_VIM_MOTION (selection_motion));
+
+       count = self->count, self->count = 0;
+
+       if (insert_motion != NULL)
+               gtk_source_vim_state_set_count (GTK_SOURCE_VIM_STATE (insert_motion), count);
+
+       if (selection_motion)
+               gtk_source_vim_state_set_count (GTK_SOURCE_VIM_STATE (selection_motion), count);
+
+       command = g_object_new (GTK_SOURCE_TYPE_VIM_COMMAND,
+                               "motion", insert_motion,
+                               "selection-motion", selection_motion,
+                               "command", command_str,
+                               NULL);
+
+       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self),
+                                  GTK_SOURCE_VIM_STATE (command));
+
+       /* If there is not yet a motion to apply, then that will get
+        * applied to the command as a whole (which will then in turn
+        * repeat motions).
+        */
+       if (insert_motion == NULL)
+       {
+               gtk_source_vim_state_set_count (GTK_SOURCE_VIM_STATE (command), count);
+
+               /* If we got a linewise keyval, then we want to let the motion
+                * know to use the gtk_source_vim_motion_new_down(-1) style
+                * motion. Generally for things like yy, dd, etc.
+                */
+               if (linewise_keyval != 0)
+               {
+                       insert_motion = gtk_source_vim_motion_new ();
+                       gtk_source_vim_motion_set_apply_on_leave (GTK_SOURCE_VIM_MOTION (insert_motion),
+                                                                 FALSE);
+                       gtk_source_vim_motion_set_linewise_keyval (GTK_SOURCE_VIM_MOTION (insert_motion),
+                                                                  linewise_keyval);
+                       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (command),
+                                                  g_object_ref (insert_motion));
+                       pop_command = FALSE;
+               }
+       }
+
+       g_clear_object (&insert_motion);
+       g_clear_object (&selection_motion);
+
+       if (pop_command)
+       {
+               gtk_source_vim_state_pop (GTK_SOURCE_VIM_STATE (command));
+       }
+}
+
+static gboolean
+gtk_source_vim_normal_begin_command_requiring_motion (GtkSourceVimNormal *self,
+                                                      const char         *command_str)
+{
+       GtkSourceVimState *command;
+       GtkSourceVimState *motion;
+       GtkSourceVimState *selection_motion;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+       g_assert (command_str != NULL);
+
+       motion = gtk_source_vim_motion_new ();
+       selection_motion = gtk_source_vim_motion_new_none ();
+
+       gtk_source_vim_motion_set_apply_on_leave (GTK_SOURCE_VIM_MOTION (motion), FALSE);
+
+       command = g_object_new (GTK_SOURCE_TYPE_VIM_COMMAND,
+                               "selection-motion", selection_motion,
+                               "command", command_str,
+                               NULL);
+
+       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), command);
+       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (command), motion);
+
+       g_clear_object (&selection_motion);
+
+       return TRUE;
+}
+
+static void
+gtk_source_vim_normal_begin_visual (GtkSourceVimNormal     *self,
+                                    GtkSourceVimVisualMode  mode)
+{
+       GtkSourceVimState *visual;
+       int count;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       count = self->count, self->count = 0;
+
+       visual = gtk_source_vim_visual_new (mode);
+       gtk_source_vim_state_set_count (visual, count);
+
+       gtk_source_vim_normal_clear (self);
+
+       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), visual);
+}
+
+static void
+go_backward_char (GtkSourceVimNormal *self)
+{
+       GtkTextIter iter;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, NULL);
+       if (!gtk_text_iter_starts_line (&iter) && gtk_text_iter_backward_char (&iter))
+               gtk_source_vim_state_select (GTK_SOURCE_VIM_STATE (self), &iter, &iter);
+}
+
+static void
+keep_on_char (GtkSourceVimNormal *self)
+{
+       GtkTextIter iter;
+
+       gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, NULL);
+
+       if (gtk_text_iter_ends_line (&iter) && !gtk_text_iter_starts_line (&iter))
+       {
+               go_backward_char (self);
+       }
+}
+
+static gboolean
+key_handler_count (GtkSourceVimNormal *self,
+                   guint               keyval,
+                   guint               keycode,
+                   GdkModifierType     mods,
+                   const char         *string)
+{
+       int n;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       self->has_count = TRUE;
+
+       switch (keyval)
+       {
+               case GDK_KEY_0: case GDK_KEY_KP_0: n = 0; break;
+               case GDK_KEY_1: case GDK_KEY_KP_1: n = 1; break;
+               case GDK_KEY_2: case GDK_KEY_KP_2: n = 2; break;
+               case GDK_KEY_3: case GDK_KEY_KP_3: n = 3; break;
+               case GDK_KEY_4: case GDK_KEY_KP_4: n = 4; break;
+               case GDK_KEY_5: case GDK_KEY_KP_5: n = 5; break;
+               case GDK_KEY_6: case GDK_KEY_KP_6: n = 6; break;
+               case GDK_KEY_7: case GDK_KEY_KP_7: n = 7; break;
+               case GDK_KEY_8: case GDK_KEY_KP_8: n = 8; break;
+               case GDK_KEY_9: case GDK_KEY_KP_9: n = 9; break;
+
+               default:
+                       self->handler = key_handler_initial;
+                       return self->handler (self, keyval, keycode, mods, string);
+       }
+
+       self->count = self->count * 10 + n;
+
+       return TRUE;
+}
+
+static gboolean
+key_handler_command (GtkSourceVimNormal *self,
+                     guint               keyval,
+                     guint               keycode,
+                     GdkModifierType     mods,
+                     const char         *string)
+{
+       GtkSourceVimState *new_state;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       switch (keyval)
+       {
+               case GDK_KEY_R:
+                       new_state = gtk_source_vim_replace_new ();
+                       gtk_source_vim_state_set_count (new_state, self->count);
+                       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), new_state);
+                       return TRUE;
+
+               case GDK_KEY_i:
+                       gtk_source_vim_normal_begin_insert (self,
+                                                           gtk_source_vim_motion_new_none (),
+                                                           GTK_SOURCE_VIM_INSERT_HERE,
+                                                           NULL);
+                       return TRUE;
+
+               case GDK_KEY_I:
+                       gtk_source_vim_normal_begin_insert (self,
+                                                           gtk_source_vim_motion_new_first_char (),
+                                                           GTK_SOURCE_VIM_INSERT_HERE,
+                                                           NULL);
+                       return TRUE;
+
+               case GDK_KEY_a:
+                       gtk_source_vim_normal_begin_insert (self,
+                                                           gtk_source_vim_motion_new_none (),
+                                                           GTK_SOURCE_VIM_INSERT_AFTER_CHAR,
+                                                           NULL);
+                       return TRUE;
+
+               case GDK_KEY_A:
+                       gtk_source_vim_normal_begin_insert (self,
+                                                           gtk_source_vim_motion_new_line_end (),
+                                                           GTK_SOURCE_VIM_INSERT_AFTER_CHAR,
+                                                           NULL);
+                       return TRUE;
+
+               case GDK_KEY_o:
+                       gtk_source_vim_normal_begin_insert (self,
+                                                           gtk_source_vim_motion_new_line_end (),
+                                                           GTK_SOURCE_VIM_INSERT_AFTER_CHAR,
+                                                           "prefix", "\n",
+                                                           "indent", TRUE,
+                                                           NULL);
+                       return TRUE;
+
+               case GDK_KEY_O:
+                       gtk_source_vim_normal_begin_insert (self,
+                                                           gtk_source_vim_motion_new_line_start (),
+                                                           GTK_SOURCE_VIM_INSERT_HERE,
+                                                           "suffix", "\n",
+                                                           "indent", TRUE,
+                                                           NULL);
+                       return TRUE;
+
+               case GDK_KEY_C:
+                       if (self->count != 0)
+                               return gtk_source_vim_normal_bail (self);
+                       gtk_source_vim_normal_begin_change (self,
+                                                           gtk_source_vim_motion_new_line_end (),
+                                                           gtk_source_vim_motion_new_none (),
+                                                           GTK_SOURCE_VIM_INSERT_HERE,
+                                                           NULL);
+                       return TRUE;
+
+               case GDK_KEY_D:
+                       if (self->count != 0)
+                               return gtk_source_vim_normal_bail (self);
+                       gtk_source_vim_normal_begin_command (self,
+                                                            gtk_source_vim_motion_new_line_end (),
+                                                            gtk_source_vim_motion_new_none (),
+                                                            ":delete", 0);
+                       return TRUE;
+
+               case GDK_KEY_x:
+                       gtk_source_vim_normal_begin_command (self,
+                                                            gtk_source_vim_motion_new_forward_char (),
+                                                            gtk_source_vim_motion_new_none (),
+                                                            ":delete", 0);
+                       return TRUE;
+
+               case GDK_KEY_S:
+                       gtk_source_vim_normal_begin_change (self,
+                                                           gtk_source_vim_motion_new_line_end (),
+                                                           gtk_source_vim_motion_new_first_char (),
+                                                           GTK_SOURCE_VIM_INSERT_HERE,
+                                                           NULL);
+                       return TRUE;
+
+               case GDK_KEY_s:
+                       gtk_source_vim_normal_begin_change (self,
+                                                           gtk_source_vim_motion_new_forward_char (),
+                                                           gtk_source_vim_motion_new_none (),
+                                                           GTK_SOURCE_VIM_INSERT_HERE,
+                                                           NULL);
+                       return TRUE;
+
+               case GDK_KEY_J:
+                       gtk_source_vim_normal_begin_command (self,
+                                                            gtk_source_vim_motion_new_next_line_end_with_nl 
(),
+                                                            gtk_source_vim_motion_new_line_start (),
+                                                            ":join", 0);
+                       return TRUE;
+
+               case GDK_KEY_u:
+                       gtk_source_vim_normal_begin_command (self, NULL, NULL, ":undo", 0);
+                       return TRUE;
+
+               case GDK_KEY_r:
+                       if ((mods & GDK_CONTROL_MASK) != 0)
+                       {
+                               gtk_source_vim_normal_begin_command (self, NULL, NULL, ":redo", 0);
+                               return TRUE;
+                       }
+                       break;
+
+               case GDK_KEY_period:
+                       if (self->repeat != NULL)
+                       {
+                               gtk_source_vim_state_repeat (self->repeat);
+                               gtk_source_vim_normal_clear (self);
+                               keep_on_char (self);
+                               return TRUE;
+                       }
+                       break;
+
+               case GDK_KEY_Y:
+                       gtk_source_vim_normal_begin_command (self,
+                                                            gtk_source_vim_motion_new_down (-1),
+                                                            gtk_source_vim_motion_new_none (),
+                                                            ":yank", 0);
+                       return TRUE;
+
+               case GDK_KEY_p:
+                       gtk_source_vim_normal_begin_command (self, NULL, NULL, "paste-after", 0);
+                       return TRUE;
+
+               case GDK_KEY_P:
+                       gtk_source_vim_normal_begin_command (self, NULL, NULL, "paste-before", 0);
+                       return TRUE;
+
+               case GDK_KEY_asciitilde:
+                       gtk_source_vim_normal_begin_command (self,
+                                                            gtk_source_vim_motion_new_forward_char (),
+                                                            NULL,
+                                                            "toggle-case", 0);
+                       return TRUE;
+
+               case GDK_KEY_equal:
+               case GDK_KEY_plus:
+               default:
+                       break;
+       }
+
+       return gtk_source_vim_normal_bail (self);
+}
+
+static gboolean
+key_handler_z (GtkSourceVimNormal *self,
+               guint               keyval,
+               guint               keycode,
+               GdkModifierType     mods,
+               const char         *string)
+{
+       GtkSourceVimState *state = (GtkSourceVimState *)self;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       switch (keyval)
+       {
+               case GDK_KEY_z:
+                       gtk_source_vim_state_z_scroll (state, 0.5);
+                       break;
+
+               case GDK_KEY_b:
+                       gtk_source_vim_state_z_scroll (state, 1.0);
+                       break;
+
+               case GDK_KEY_t:
+                       gtk_source_vim_state_z_scroll (state, 0.0);
+                       break;
+
+               default:
+                       return gtk_source_vim_normal_bail (self);
+       }
+
+       gtk_source_vim_normal_clear (self);
+
+       return TRUE;
+}
+
+static gboolean
+key_handler_viewport (GtkSourceVimNormal *self,
+                      guint               keyval,
+                      guint               keycode,
+                      GdkModifierType     mods,
+                      const char         *string)
+{
+       GtkSourceVimState *state = (GtkSourceVimState *)self;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       if ((mods & GDK_CONTROL_MASK) != 0)
+       {
+               switch (keyval)
+               {
+                       case GDK_KEY_d:
+                               gtk_source_vim_state_scroll_half_page (state, MAX (1, self->count));
+                               gtk_source_vim_normal_clear (self);
+                               return TRUE;
+
+                       case GDK_KEY_u:
+                               gtk_source_vim_state_scroll_half_page (state, MIN (-1, -self->count));
+                               gtk_source_vim_normal_clear (self);
+                               return TRUE;
+
+                       case GDK_KEY_e:
+                               gtk_source_vim_state_scroll_line (state, MAX (1, self->count));
+                               gtk_source_vim_normal_clear (self);
+                               return TRUE;
+
+                       case GDK_KEY_y:
+                               gtk_source_vim_state_scroll_line (state, MIN (-1, -self->count));
+                               gtk_source_vim_normal_clear (self);
+                               return TRUE;
+
+                       case GDK_KEY_f:
+                               gtk_source_vim_state_scroll_page (state, MAX (1, self->count));
+                               gtk_source_vim_normal_clear (self);
+                               return TRUE;
+
+                       case GDK_KEY_b:
+                               gtk_source_vim_state_scroll_page (state, MIN (-1, -self->count));
+                               gtk_source_vim_normal_clear (self);
+                               return TRUE;
+
+                       default:
+                               break;
+               }
+       }
+
+       return gtk_source_vim_normal_bail (self);
+}
+
+static gboolean
+key_handler_c_with_modifier (GtkSourceVimNormal *self,
+                             guint               keyval,
+                             guint               keycode,
+                             GdkModifierType     mods,
+                             const char         *string)
+{
+       GtkSourceVimState *text_object;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       text_object = get_text_object (keyval, self->change_modifier);
+
+       if (text_object != NULL)
+       {
+               int count;
+
+               count = self->count, self->count = 0;
+               gtk_source_vim_state_set_count (text_object, count);
+               gtk_source_vim_normal_begin_insert_text_object (self, text_object);
+               gtk_source_vim_normal_clear (self);
+               g_object_unref (text_object);
+
+               return TRUE;
+       }
+
+       return gtk_source_vim_normal_bail (self);
+}
+
+static gboolean
+key_handler_c (GtkSourceVimNormal *self,
+               guint               keyval,
+               guint               keycode,
+               GdkModifierType     mods,
+               const char         *string)
+{
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       switch (keyval)
+       {
+               case GDK_KEY_c:
+                       gtk_source_vim_normal_begin_change (self,
+                                                           gtk_source_vim_motion_new_line_end_with_nl (),
+                                                           gtk_source_vim_motion_new_line_start (),
+                                                           GTK_SOURCE_VIM_INSERT_HERE,
+                                                           NULL);
+                       return TRUE;
+
+               case GDK_KEY_i:
+                       self->change_modifier = CHANGE_INNER;
+                       self->handler = key_handler_c_with_modifier;
+                       return TRUE;
+
+               case GDK_KEY_a:
+                       self->change_modifier = CHANGE_A;
+                       self->handler = key_handler_c_with_modifier;
+                       return TRUE;
+
+               default:
+               {
+                       GtkSourceVimState *motion;
+                       GtkSourceVimState *selection;
+                       GtkSourceVimState *insert;
+                       int count;
+
+                       count = self->count, self->count = 0;
+                       insert = gtk_source_vim_insert_new ();
+                       motion = gtk_source_vim_motion_new ();
+                       selection = gtk_source_vim_motion_new_none ();
+                       gtk_source_vim_motion_set_apply_on_leave (GTK_SOURCE_VIM_MOTION (motion), FALSE);
+                       gtk_source_vim_insert_set_selection_motion (GTK_SOURCE_VIM_INSERT (insert), 
GTK_SOURCE_VIM_MOTION (selection));
+                       gtk_source_vim_state_set_count (motion, count);
+                       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), insert);
+                       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (insert), motion);
+                       gtk_source_vim_state_synthesize (motion, keyval, mods);
+
+                       gtk_source_vim_normal_clear (self);
+
+                       g_object_unref (selection);
+
+                       return TRUE;
+               }
+       }
+}
+
+static gboolean
+key_handler_d_with_modifier (GtkSourceVimNormal *self,
+                             guint               keyval,
+                             guint               keycode,
+                             GdkModifierType     mods,
+                             const char         *string)
+{
+       GtkSourceVimState *text_object;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       text_object = get_text_object (keyval, self->change_modifier);
+
+       if (text_object != NULL)
+       {
+               GtkSourceVimState *command = gtk_source_vim_command_new (":delete");
+               gtk_source_vim_command_set_text_object (GTK_SOURCE_VIM_COMMAND (command),
+                                                       GTK_SOURCE_VIM_TEXT_OBJECT (text_object));
+               gtk_source_vim_normal_clear (self);
+               gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), command);
+               gtk_source_vim_state_pop (command);
+               g_object_unref (text_object);
+               return TRUE;
+       }
+
+       return gtk_source_vim_normal_bail (self);
+}
+
+static gboolean
+key_handler_d (GtkSourceVimNormal *self,
+               guint               keyval,
+               guint               keycode,
+               GdkModifierType     mods,
+               const char         *string)
+{
+       GtkSourceVimState *current;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       switch (keyval)
+       {
+               case GDK_KEY_i:
+                       self->change_modifier = CHANGE_INNER;
+                       self->handler = key_handler_d_with_modifier;
+                       return TRUE;
+
+               case GDK_KEY_a:
+                       self->change_modifier = CHANGE_A;
+                       self->handler = key_handler_d_with_modifier;
+                       return TRUE;
+
+               default:
+               {
+                       gtk_source_vim_normal_begin_command (self,
+                                                            NULL,
+                                                            gtk_source_vim_motion_new_none (),
+                                                            ":delete", GDK_KEY_d);
+                       current = gtk_source_vim_state_get_current (GTK_SOURCE_VIM_STATE (self));
+                       gtk_source_vim_state_synthesize (current, keyval, mods);
+                       return TRUE;
+               }
+       }
+}
+
+static gboolean
+key_handler_shift (GtkSourceVimNormal *self,
+                   guint               keyval,
+                   guint               keycode,
+                   GdkModifierType     mods,
+                   const char         *string)
+{
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       switch (keyval)
+       {
+               case GDK_KEY_greater:
+                       gtk_source_vim_normal_begin_command (self, NULL, NULL, "indent", 0);
+                       return TRUE;
+
+               case GDK_KEY_less:
+                       gtk_source_vim_normal_begin_command (self, NULL, NULL, "unindent", 0);
+                       return TRUE;
+
+               default:
+                       return gtk_source_vim_normal_bail (self);
+       }
+}
+
+static gboolean
+key_handler_search (GtkSourceVimNormal *self,
+                    guint               keyval,
+                    guint               keycode,
+                    GdkModifierType     mods,
+                    const char         *string)
+{
+       GtkSourceVimState *command_bar;
+       const char *text;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       switch (keyval)
+       {
+               case GDK_KEY_slash:
+                       text = "/";
+                       break;
+
+               case GDK_KEY_question:
+                       text = "?";
+                       break;
+
+               default:
+                       return gtk_source_vim_normal_bail (self);
+       }
+
+       command_bar = gtk_source_vim_command_bar_new ();
+       gtk_source_vim_command_bar_set_text (GTK_SOURCE_VIM_COMMAND_BAR (command_bar), text);
+       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), command_bar);
+
+       return TRUE;
+}
+
+static gboolean
+key_handler_register (GtkSourceVimNormal *self,
+                      guint               keyval,
+                      guint               keycode,
+                      GdkModifierType     mods,
+                      const char         *string)
+{
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       if (string == NULL || string[0] == 0)
+       {
+               /* We require a string to access the register */
+               return gtk_source_vim_normal_bail (self);
+       }
+
+       gtk_source_vim_state_set_current_register (GTK_SOURCE_VIM_STATE (self), string);
+
+       self->handler = key_handler_initial;
+
+       return TRUE;
+}
+
+static gboolean
+key_handler_split (GtkSourceVimNormal *self,
+                   guint               keyval,
+                   guint               keycode,
+                   GdkModifierType     mods,
+                   const char         *string)
+{
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       return TRUE;
+}
+
+static gboolean
+key_handler_increment (GtkSourceVimNormal *self,
+                       guint               keyval,
+                       guint               keycode,
+                       GdkModifierType     mods,
+                       const char         *string)
+{
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       return TRUE;
+}
+
+static gboolean
+key_handler_g (GtkSourceVimNormal *self,
+               guint               keyval,
+               guint               keycode,
+               GdkModifierType     mods,
+               const char         *string)
+{
+       GtkSourceVimState *new_state;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       switch (keyval)
+       {
+               case GDK_KEY_question:
+                       return gtk_source_vim_normal_begin_command_requiring_motion (self, "rot13");
+
+               case GDK_KEY_q:
+                       return gtk_source_vim_normal_begin_command_requiring_motion (self, "format");
+
+               case GDK_KEY_g:
+               case GDK_KEY_e:
+               case GDK_KEY_E:
+                       new_state = gtk_source_vim_motion_new ();
+                       gtk_source_vim_state_set_count (new_state, self->count);
+                       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), new_state);
+                       gtk_source_vim_state_synthesize (new_state, GDK_KEY_g, 0);
+                       gtk_source_vim_state_synthesize (new_state, keyval, mods);
+                       return TRUE;
+
+               case GDK_KEY_v:
+                       if (self->last_visual == NULL)
+                               return gtk_source_vim_normal_bail (self);
+
+                       new_state = gtk_source_vim_visual_clone (GTK_SOURCE_VIM_VISUAL (self->last_visual));
+                       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), new_state);
+                       return TRUE;
+
+               default:
+                       return gtk_source_vim_normal_bail (self);
+       }
+}
+
+static gboolean
+key_handler_motion (GtkSourceVimNormal *self,
+                    guint               keyval,
+                    guint               keycode,
+                    GdkModifierType     mods,
+                    const char         *string)
+{
+       GtkSourceVimState *new_state;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       if (self->command_text->len > 0)
+               g_string_truncate (self->command_text, self->command_text->len - 1);
+
+       new_state = gtk_source_vim_motion_new ();
+       gtk_source_vim_state_set_count (new_state, self->count);
+       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), new_state);
+       gtk_source_vim_state_synthesize (new_state, keyval, mods);
+
+       return TRUE;
+}
+
+static gboolean
+key_handler_mark (GtkSourceVimNormal *self,
+                  guint               keyval,
+                  guint               keycode,
+                  GdkModifierType     mods,
+                  const char         *string)
+{
+       GtkTextIter iter;
+
+       if (!g_ascii_isalpha (string[0]))
+       {
+               return gtk_source_vim_normal_bail (self);
+       }
+
+       gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, NULL);
+       gtk_source_vim_state_set_mark (GTK_SOURCE_VIM_STATE (self), string, &iter);
+       gtk_source_vim_normal_clear (self);
+
+       return TRUE;
+}
+
+static gboolean
+key_handler_initial (GtkSourceVimNormal *self,
+                     guint               keyval,
+                     guint               keycode,
+                     GdkModifierType     mods,
+                     const char         *string)
+{
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       if ((mods & GDK_CONTROL_MASK) != 0)
+       {
+               switch (keyval)
+               {
+                       case GDK_KEY_a:
+                       case GDK_KEY_x:
+                               self->handler = key_handler_increment;
+                               break;
+
+                       case GDK_KEY_d:
+                       case GDK_KEY_u:
+                       case GDK_KEY_e:
+                       case GDK_KEY_y:
+                       case GDK_KEY_f:
+                       case GDK_KEY_b:
+                               self->handler = key_handler_viewport;
+                               break;
+
+                       case GDK_KEY_v:
+                               gtk_source_vim_normal_begin_visual (self, GTK_SOURCE_VIM_VISUAL_BLOCK);
+                               return TRUE;
+
+                       case GDK_KEY_w:
+                               self->handler = key_handler_split;
+                               break;
+
+                       case GDK_KEY_r:
+                               self->handler = key_handler_command;
+                               break;
+
+                       case GDK_KEY_o:
+                               gtk_source_vim_normal_begin_command (self, NULL, NULL, "jump-backward", 0);
+                               return TRUE;
+
+                       case GDK_KEY_i:
+                               gtk_source_vim_normal_begin_command (self, NULL, NULL, "jump-forward", 0);
+                               return TRUE;
+
+                       default:
+                               break;
+               }
+       }
+       else
+       {
+               switch (keyval)
+               {
+                       case GDK_KEY_0:
+                       case GDK_KEY_KP_0:
+                       case GDK_KEY_apostrophe:
+                       case GDK_KEY_asciicircum:
+                       case GDK_KEY_asterisk:
+                       case GDK_KEY_b:
+                       case GDK_KEY_bar:
+                       case GDK_KEY_B:
+                       case GDK_KEY_BackSpace:
+                       case GDK_KEY_braceleft:
+                       case GDK_KEY_braceright:
+                       case GDK_KEY_bracketleft:
+                       case GDK_KEY_bracketright:
+                       case GDK_KEY_dollar:
+                       case GDK_KEY_Down:
+                       case GDK_KEY_e:
+                       case GDK_KEY_E:
+                       case GDK_KEY_End:
+                       case GDK_KEY_f:
+                       case GDK_KEY_F:
+                       case GDK_KEY_grave:
+                       case GDK_KEY_G:
+                       case GDK_KEY_h:
+                       case GDK_KEY_H:
+                       case GDK_KEY_ISO_Enter:
+                       case GDK_KEY_j:
+                       case GDK_KEY_k:
+                       case GDK_KEY_KP_Enter:
+                       case GDK_KEY_l:
+                       case GDK_KEY_L:
+                       case GDK_KEY_Left:
+                       case GDK_KEY_M:
+                       case GDK_KEY_n:
+                       case GDK_KEY_numbersign:
+                       case GDK_KEY_N:
+                       case GDK_KEY_parenleft:
+                       case GDK_KEY_parenright:
+                       case GDK_KEY_percent:
+                       case GDK_KEY_Return:
+                       case GDK_KEY_Right:
+                       case GDK_KEY_space:
+                       case GDK_KEY_underscore:
+                       case GDK_KEY_Up:
+                       case GDK_KEY_w:
+                       case GDK_KEY_W:
+                               self->handler = key_handler_motion;
+                               break;
+
+                       case GDK_KEY_m:
+                               self->handler = key_handler_mark;
+                               return TRUE;
+
+                       case GDK_KEY_1: case GDK_KEY_KP_1:
+                       case GDK_KEY_2: case GDK_KEY_KP_2:
+                       case GDK_KEY_3: case GDK_KEY_KP_3:
+                       case GDK_KEY_4: case GDK_KEY_KP_4:
+                       case GDK_KEY_5: case GDK_KEY_KP_5:
+                       case GDK_KEY_6: case GDK_KEY_KP_6:
+                       case GDK_KEY_7: case GDK_KEY_KP_7:
+                       case GDK_KEY_8: case GDK_KEY_KP_8:
+                       case GDK_KEY_9: case GDK_KEY_KP_9:
+                               if (self->has_count == FALSE)
+                                       self->handler = key_handler_count;
+                               break;
+
+                       case GDK_KEY_a:
+                       case GDK_KEY_asciitilde:
+                       case GDK_KEY_A:
+                       case GDK_KEY_C:
+                       case GDK_KEY_D:
+                       case GDK_KEY_i:
+                       case GDK_KEY_I:
+                       case GDK_KEY_J:
+                       case GDK_KEY_o:
+                       case GDK_KEY_O:
+                       case GDK_KEY_p:
+                       case GDK_KEY_P:
+                       case GDK_KEY_period:
+                       case GDK_KEY_R:
+                       case GDK_KEY_s:
+                       case GDK_KEY_S:
+                       case GDK_KEY_u:
+                       case GDK_KEY_x:
+                       case GDK_KEY_equal:
+                       case GDK_KEY_plus:
+                       case GDK_KEY_Y:
+                               self->handler = key_handler_command;
+                               break;
+
+                       case GDK_KEY_quotedbl:
+                               self->handler = key_handler_register;
+                               return TRUE;
+
+                       case GDK_KEY_y:
+                               gtk_source_vim_normal_begin_command (self,
+                                                                    NULL,
+                                                                    gtk_source_vim_motion_new_none (),
+                                                                    ":yank", GDK_KEY_y);
+                               return TRUE;
+
+                       case GDK_KEY_d:
+                               self->handler = key_handler_d;
+                               return TRUE;
+
+                       case GDK_KEY_c:
+                               self->handler = key_handler_c;
+                               return TRUE;
+
+                       case GDK_KEY_g:
+                               self->handler = key_handler_g;
+                               return TRUE;
+
+                       case GDK_KEY_z:
+                               self->handler = key_handler_z;
+                               return TRUE;
+
+                       case GDK_KEY_greater:
+                       case GDK_KEY_less:
+                               self->handler = key_handler_shift;
+                               return TRUE;
+
+                       case GDK_KEY_r:
+                               return gtk_source_vim_normal_replace_one (self);
+
+                       case GDK_KEY_slash:
+                       case GDK_KEY_question:
+                               self->handler = key_handler_search;
+                               break;
+
+                       case GDK_KEY_colon:
+                               gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self),
+                                                          gtk_source_vim_command_bar_new ());
+                               return TRUE;
+
+                       case GDK_KEY_v:
+                               gtk_source_vim_normal_begin_visual (self, GTK_SOURCE_VIM_VISUAL_CHAR);
+                               return TRUE;
+
+                       case GDK_KEY_V:
+                               gtk_source_vim_normal_begin_visual (self, GTK_SOURCE_VIM_VISUAL_LINE);
+                               return TRUE;
+
+                       default:
+                               break;
+               }
+       }
+
+       if (self->handler == key_handler_initial)
+               return gtk_source_vim_normal_bail (self);
+
+       return self->handler (self, keyval, keycode, mods, string);
+}
+
+static gboolean
+gtk_source_vim_normal_handle_keypress (GtkSourceVimState *state,
+                                       guint              keyval,
+                                       guint              keycode,
+                                       GdkModifierType    mods,
+                                       const char        *string)
+{
+       GtkSourceVimNormal *self = (GtkSourceVimNormal *)state;
+
+       g_assert (GTK_SOURCE_IS_VIM_STATE (self));
+
+       g_string_append (self->command_text, string);
+
+       if (gtk_source_vim_state_is_escape (keyval, mods))
+       {
+               gtk_source_vim_normal_clear (self);
+               return TRUE;
+       }
+
+       return self->handler (self, keyval, keycode, mods, string);
+}
+
+static void
+gtk_source_vim_normal_suspend (GtkSourceVimState *state,
+                               GtkSourceVimState *to)
+{
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (state));
+       g_assert (GTK_SOURCE_IS_VIM_STATE (to));
+
+}
+
+static void
+gtk_source_vim_normal_resume (GtkSourceVimState *state,
+                              GtkSourceVimState *from)
+{
+       GtkSourceVimNormal *self = (GtkSourceVimNormal *)state;
+       GtkSourceBuffer *buffer;
+       GtkSourceView *view;
+       GtkTextMark *insert;
+       GtkTextIter origin;
+       gboolean unparent = TRUE;
+
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (self));
+       g_assert (GTK_SOURCE_IS_VIM_STATE (from));
+
+       buffer = gtk_source_vim_state_get_buffer (state, &origin, NULL);
+       insert = gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (buffer));
+       view = gtk_source_vim_state_get_view (state);
+
+       gtk_source_vim_normal_clear (GTK_SOURCE_VIM_NORMAL (state));
+       gtk_source_vim_state_set_overwrite (state, TRUE);
+       gtk_source_vim_state_set_current_register (state, NULL);
+
+       /* Go back one character if we exited replace/insert state */
+       if (GTK_SOURCE_IS_VIM_INSERT (from) || GTK_SOURCE_IS_VIM_REPLACE (from))
+       {
+               go_backward_char (self);
+       }
+       else if (GTK_SOURCE_IS_VIM_VISUAL (from))
+       {
+               /* Store last visual around for reselection in gv */
+               gtk_source_vim_state_reparent (from, self, &self->last_visual);
+               unparent = FALSE;
+       }
+       else if (!GTK_SOURCE_IS_VIM_MOTION (from) ||
+                gtk_source_vim_motion_invalidates_visual_column (GTK_SOURCE_VIM_MOTION (from)))
+       {
+               GtkTextIter iter;
+               guint visual_column;
+
+               gtk_source_vim_state_get_buffer (state, &iter, NULL);
+               visual_column = gtk_source_view_get_visual_column (view, &iter);
+               gtk_source_vim_state_set_visual_column (state, visual_column);
+       }
+
+       /* If we're still on the \n, go back a char */
+       keep_on_char (self);
+
+       /* Always scroll the insert mark onscreen */
+       gtk_text_view_scroll_mark_onscreen (GTK_TEXT_VIEW (view), insert);
+
+       if (gtk_source_vim_state_get_can_repeat (from))
+       {
+               gtk_source_vim_state_reparent (from, self, &self->repeat);
+               unparent = FALSE;
+       }
+
+       if (unparent)
+       {
+               gtk_source_vim_state_unparent (from);
+       }
+}
+
+static void
+gtk_source_vim_normal_enter (GtkSourceVimState *state)
+{
+       g_assert (GTK_SOURCE_IS_VIM_NORMAL (state));
+
+       gtk_source_vim_state_set_overwrite (state, TRUE);
+}
+
+static void
+gtk_source_vim_normal_append_command (GtkSourceVimState *state,
+                                      GString           *string)
+{
+       GtkSourceVimNormal *self = (GtkSourceVimNormal *)state;
+
+       g_assert (GTK_SOURCE_IS_VIM_STATE (state));
+       g_assert (string != NULL);
+
+       if (self->command_text->len > 0)
+       {
+               g_string_append_len (string,
+                                    self->command_text->str,
+                                    self->command_text->len);
+       }
+}
+
+static void
+gtk_source_vim_normal_dispose (GObject *object)
+{
+       GtkSourceVimNormal *self = (GtkSourceVimNormal *)object;
+
+       gtk_source_vim_state_release (&self->last_visual);
+       gtk_source_vim_state_release (&self->repeat);
+
+       if (self->command_text != NULL)
+       {
+               g_string_free (self->command_text, TRUE);
+               self->command_text = NULL;
+       }
+
+       G_OBJECT_CLASS (gtk_source_vim_normal_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_vim_normal_class_init (GtkSourceVimNormalClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       GtkSourceVimStateClass *state_class = GTK_SOURCE_VIM_STATE_CLASS (klass);
+
+       object_class->dispose = gtk_source_vim_normal_dispose;
+
+       state_class->append_command = gtk_source_vim_normal_append_command;
+       state_class->handle_keypress = gtk_source_vim_normal_handle_keypress;
+       state_class->enter = gtk_source_vim_normal_enter;
+       state_class->resume = gtk_source_vim_normal_resume;
+       state_class->suspend = gtk_source_vim_normal_suspend;
+}
+
+static void
+gtk_source_vim_normal_init (GtkSourceVimNormal *self)
+{
+       self->handler = key_handler_initial;
+       self->command_text = g_string_new (NULL);
+}
+
+GtkSourceVimState *
+gtk_source_vim_normal_new (void)
+{
+       return g_object_new (GTK_SOURCE_TYPE_VIM_NORMAL, NULL);
+}
+
+void
+gtk_source_vim_normal_clear (GtkSourceVimNormal *self)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM_NORMAL (self));
+
+       self->handler = key_handler_initial;
+       self->count = 0;
+       self->has_count = FALSE;
+       self->change_modifier = CHANGE_NONE;
+
+       g_string_truncate (self->command_text, 0);
+
+       /* Let the toplevel know we're back at steady state. This is
+        * basically just so observers can watch keys which makes it
+        * much easier to debug issues.
+        */
+       gtk_source_vim_normal_emit_ready (self);
+}
diff --git a/gtksourceview/vim/gtksourcevimnormal.h b/gtksourceview/vim/gtksourcevimnormal.h
new file mode 100644
index 00000000..e0b58b24
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimnormal.h
@@ -0,0 +1,35 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include "gtksourcevimstate.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_VIM_NORMAL (gtk_source_vim_normal_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceVimNormal, gtk_source_vim_normal, GTK_SOURCE, VIM_NORMAL, GtkSourceVimState)
+
+GtkSourceVimState *gtk_source_vim_normal_new   (void);
+void               gtk_source_vim_normal_clear (GtkSourceVimNormal *self);
+
+G_END_DECLS
diff --git a/gtksourceview/vim/gtksourcevimregisters.c b/gtksourceview/vim/gtksourcevimregisters.c
new file mode 100644
index 00000000..31b78a57
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimregisters.c
@@ -0,0 +1,325 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "gtksourcevimregisters.h"
+
+#define DEFAULT_REGISTER "\""
+#define MAX_BYTES (4096L*16L) /* 64kb */
+
+struct _GtkSourceVimRegisters
+{
+       GtkSourceVimState parent_instance;
+
+       GHashTable *values;
+
+       char *clipboard;
+       char *primary_clipboard;
+
+       char *numbered[10];
+       int numbered_pos;
+};
+
+G_DEFINE_TYPE (GtkSourceVimRegisters, gtk_source_vim_registers, GTK_SOURCE_TYPE_VIM_STATE)
+
+static void
+gtk_source_vim_registers_finalize (GObject *object)
+{
+       GtkSourceVimRegisters *self = (GtkSourceVimRegisters *)object;
+
+       g_clear_pointer (&self->values, g_hash_table_unref);
+       g_clear_pointer (&self->clipboard, g_ref_string_release);
+       g_clear_pointer (&self->primary_clipboard, g_ref_string_release);
+
+       for (guint i = 0; i < G_N_ELEMENTS (self->numbered); i++)
+       {
+               g_clear_pointer (&self->numbered[i], g_ref_string_release);
+       }
+
+       G_OBJECT_CLASS (gtk_source_vim_registers_parent_class)->finalize (object);
+}
+
+static void
+gtk_source_vim_registers_class_init (GtkSourceVimRegistersClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->finalize = gtk_source_vim_registers_finalize;
+}
+
+static void
+gtk_source_vim_registers_init (GtkSourceVimRegisters *self)
+{
+       self->values = g_hash_table_new_full (g_str_hash,
+                                             g_str_equal,
+                                             NULL,
+                                             (GDestroyNotify)g_ref_string_release);
+}
+
+static void
+write_clipboard (GtkSourceVimRegisters *self,
+                 GdkClipboard          *clipboard,
+                 char                  *refstr)
+{
+       g_assert (GTK_SOURCE_IS_VIM_REGISTERS (self));
+       g_assert (GDK_IS_CLIPBOARD (clipboard));
+       g_assert (refstr != NULL);
+
+       gdk_clipboard_set_text (clipboard, refstr);
+}
+
+static gboolean
+cancel_cb (gpointer data)
+{
+       g_cancellable_cancel (data);
+       return G_SOURCE_REMOVE;
+}
+
+typedef struct
+{
+       char *text;
+       GMainLoop *main_loop;
+       GCancellable *cancellable;
+} ReadClipboard;
+
+static void
+read_clipboard_cb (GObject      *object,
+                   GAsyncResult *result,
+                   gpointer      user_data)
+{
+       ReadClipboard *clip = user_data;
+
+       g_assert (GDK_IS_CLIPBOARD (object));
+       g_assert (G_IS_ASYNC_RESULT (result));
+       g_assert (clip != NULL);
+       g_assert (clip->main_loop != NULL);
+       g_assert (G_IS_CANCELLABLE (clip->cancellable));
+
+       clip->text = gdk_clipboard_read_text_finish (GDK_CLIPBOARD (object), result, NULL);
+
+       g_main_loop_quit (clip->main_loop);
+}
+
+static void
+read_clipboard (GtkSourceVimRegisters  *self,
+                GdkClipboard           *clipboard,
+                char                  **text)
+{
+       ReadClipboard clip;
+       GSource *source;
+
+       g_assert (GTK_SOURCE_IS_VIM_REGISTERS (self));
+       g_assert (GDK_IS_CLIPBOARD (clipboard));
+
+       clip.text = NULL;
+       clip.main_loop = g_main_loop_new (NULL, FALSE);
+       clip.cancellable = g_cancellable_new ();
+
+       source = g_timeout_source_new (500);
+       g_source_set_name (source, "[gtksourceview cancel clipboard]");
+       g_source_set_callback (source, cancel_cb, clip.cancellable, NULL);
+       g_source_attach (source, NULL);
+
+       gdk_clipboard_read_text_async (clipboard,
+                                      clip.cancellable,
+                                      read_clipboard_cb,
+                                      &clip);
+
+       g_main_loop_run (clip.main_loop);
+
+       g_main_loop_unref (clip.main_loop);
+       g_object_unref (clip.cancellable);
+
+       g_source_destroy (source);
+
+       if (clip.text != NULL)
+       {
+               g_clear_pointer (text, g_ref_string_release);
+               *text = g_ref_string_new (clip.text);
+               g_free (clip.text);
+       }
+}
+
+GtkSourceVimState *
+gtk_source_vim_registers_new (void)
+{
+       return g_object_new (GTK_SOURCE_TYPE_VIM_REGISTERS, NULL);
+}
+
+const char *
+gtk_source_vim_registers_get (GtkSourceVimRegisters *self,
+                              const char            *name)
+{
+       GtkSourceView *view;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_REGISTERS (self), NULL);
+
+       if (name == NULL)
+       {
+               name = DEFAULT_REGISTER;
+       }
+
+       if (g_ascii_isdigit (*name))
+       {
+               return gtk_source_vim_registers_get_numbered (self, *name - '0');
+       }
+
+       view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (self));
+
+       if (g_str_equal (name, "+"))
+       {
+               GdkClipboard *clipboard = gtk_widget_get_clipboard (GTK_WIDGET (view));
+               read_clipboard (self, clipboard, &self->clipboard);
+               return self->clipboard;
+       }
+       else if (g_str_equal (name, "*"))
+       {
+               GdkClipboard *clipboard = gtk_widget_get_primary_clipboard (GTK_WIDGET (view));
+               read_clipboard (self, clipboard, &self->primary_clipboard);
+               return self->primary_clipboard;
+       }
+       else
+       {
+               return g_hash_table_lookup (self->values, name);
+       }
+}
+
+static inline char **
+get_numbered_pos (GtkSourceVimRegisters *self,
+                  guint                  n)
+{
+       return &self->numbered[(self->numbered_pos + n) % 10];
+}
+
+const char *
+gtk_source_vim_registers_get_numbered (GtkSourceVimRegisters *self,
+                                       guint                  n)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_REGISTERS (self), NULL);
+       g_return_val_if_fail (n <= 9, NULL);
+
+       return *get_numbered_pos (self, n);
+}
+
+static void
+gtk_source_vim_registers_push (GtkSourceVimRegisters *self,
+                               char                  *str)
+{
+       char **pos;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_REGISTERS (self));
+
+       if (self->numbered_pos == 0)
+       {
+               self->numbered_pos = G_N_ELEMENTS (self->numbered) - 1;
+       }
+       else
+       {
+               self->numbered_pos--;
+       }
+
+       pos = get_numbered_pos (self, 0);
+
+       if (*pos != NULL)
+       {
+               g_ref_string_release (*pos);
+       }
+
+       *pos = str ? g_ref_string_acquire (str) : NULL;
+}
+
+void
+gtk_source_vim_registers_set (GtkSourceVimRegisters *self,
+                              const char            *name,
+                              const char            *value)
+{
+       GtkSourceView *view;
+       char *str;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_REGISTERS (self));
+
+       if (name == NULL)
+       {
+               name = DEFAULT_REGISTER;
+       }
+
+       /* TODO: Allow :set viminfo to tweak register lines, bytes, etc */
+       if (value != NULL && strlen (value) > MAX_BYTES)
+       {
+               value = NULL;
+       }
+
+       if (value == NULL)
+       {
+               g_hash_table_remove (self->values, name);
+               return;
+       }
+
+       str = g_ref_string_new (value);
+       view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (self));
+
+       if (g_str_equal (name, "+"))
+       {
+               GdkClipboard *clipboard = gtk_widget_get_clipboard (GTK_WIDGET (view));
+               write_clipboard (self, clipboard, str);
+       }
+       else if (g_str_equal (name, "*"))
+       {
+               GdkClipboard *clipboard = gtk_widget_get_primary_clipboard (GTK_WIDGET (view));
+               write_clipboard (self, clipboard, str);
+       }
+       else
+       {
+               g_hash_table_insert (self->values,
+                                    (char *)g_intern_string (name),
+                                    str);
+       }
+
+       /* Push into the 0 numbered register and each 1..8 to
+        * the next numbered register position.
+        */
+       if (g_strcmp0 (name, DEFAULT_REGISTER) == 0)
+       {
+               gtk_source_vim_registers_push (self, str);
+       }
+}
+
+void
+gtk_source_vim_registers_clear (GtkSourceVimRegisters *self,
+                                const char            *name)
+{
+       gtk_source_vim_registers_set (self, name, NULL);
+}
+
+gboolean
+gtk_source_vim_register_is_read_only (const char *name)
+{
+       switch (name ? name[0] : 0)
+       {
+       case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
+       case '%': case '.': case '#': case ':':
+               return TRUE;
+
+       default:
+               return FALSE;
+       }
+}
diff --git a/gtksourceview/vim/gtksourcevimregisters.h b/gtksourceview/vim/gtksourcevimregisters.h
new file mode 100644
index 00000000..94f5bb40
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimregisters.h
@@ -0,0 +1,44 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include "gtksourcevimstate.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_VIM_REGISTERS (gtk_source_vim_registers_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceVimRegisters, gtk_source_vim_registers, GTK_SOURCE, VIM_REGISTERS, 
GtkSourceVimState)
+
+GtkSourceVimState *gtk_source_vim_registers_new          (void);
+const char        *gtk_source_vim_registers_get          (GtkSourceVimRegisters *self,
+                                                          const char            *name);
+const char        *gtk_source_vim_registers_get_numbered (GtkSourceVimRegisters *self,
+                                                          guint                  n);
+void               gtk_source_vim_registers_set          (GtkSourceVimRegisters *self,
+                                                          const char            *name,
+                                                          const char            *string);
+void               gtk_source_vim_registers_clear        (GtkSourceVimRegisters *self,
+                                                          const char            *name);
+gboolean           gtk_source_vim_register_is_read_only  (const char            *name);
+
+G_END_DECLS
diff --git a/gtksourceview/vim/gtksourcevimreplace.c b/gtksourceview/vim/gtksourcevimreplace.c
new file mode 100644
index 00000000..a49b7fe1
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimreplace.c
@@ -0,0 +1,139 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "gtksourcevimreplace.h"
+#include "gtksourceviminsertliteral.h"
+
+struct _GtkSourceVimReplace
+{
+       GtkSourceVimState parent_instance;
+};
+
+G_DEFINE_TYPE (GtkSourceVimReplace, gtk_source_vim_replace, GTK_SOURCE_TYPE_VIM_STATE)
+
+GtkSourceVimState *
+gtk_source_vim_replace_new (void)
+{
+       return g_object_new (GTK_SOURCE_TYPE_VIM_REPLACE, NULL);
+}
+
+static void
+move_to_zero (GtkSourceVimReplace *self)
+{
+       GtkSourceBuffer *buffer;
+       GtkTextIter insert;
+
+       g_assert (GTK_SOURCE_IS_VIM_REPLACE (self));
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &insert, NULL);
+       gtk_text_iter_set_line_offset (&insert, 0);
+       gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer), &insert, &insert);
+}
+
+static gboolean
+gtk_source_vim_replace_handle_keypress (GtkSourceVimState *state,
+                                        guint              keyval,
+                                        guint              keycode,
+                                        GdkModifierType    mods,
+                                        const char        *string)
+{
+       GtkSourceVimReplace *self = (GtkSourceVimReplace *)state;
+
+       g_assert (GTK_SOURCE_IS_VIM_REPLACE (self));
+
+       if (gtk_source_vim_state_is_escape (keyval, mods) ||
+           gtk_source_vim_state_is_ctrl_c (keyval, mods))
+       {
+               gtk_source_vim_state_pop (state);
+               return TRUE;
+       }
+
+       /* Now handle our commands */
+       if ((mods & GDK_CONTROL_MASK) != 0)
+       {
+               switch (keyval)
+               {
+                       case GDK_KEY_u:
+                               move_to_zero (self);
+                               return TRUE;
+
+                       case GDK_KEY_v:
+                               gtk_source_vim_state_push (state, gtk_source_vim_insert_literal_new ());
+                               return TRUE;
+
+                       default:
+                               break;
+               }
+       }
+
+       return FALSE;
+}
+
+static void
+gtk_source_vim_replace_enter (GtkSourceVimState *state)
+{
+       g_assert (GTK_SOURCE_IS_VIM_REPLACE (state));
+
+       gtk_source_vim_state_set_overwrite (state, TRUE);
+       gtk_source_vim_state_begin_user_action (state);
+}
+
+static void
+gtk_source_vim_replace_resume (GtkSourceVimState *state,
+                               GtkSourceVimState *from)
+{
+       g_assert (GTK_SOURCE_IS_VIM_REPLACE (state));
+       g_assert (GTK_SOURCE_IS_VIM_STATE (from));
+
+       gtk_source_vim_state_set_overwrite (state, TRUE);
+       gtk_source_vim_state_end_user_action (state);
+       gtk_source_vim_state_unparent (from);
+}
+
+static void
+gtk_source_vim_replace_append_command (GtkSourceVimState *state,
+                                       GString           *string)
+{
+       /* command should be empty during replace */
+       g_string_truncate (string, 0);
+}
+
+static void
+gtk_source_vim_replace_class_init (GtkSourceVimReplaceClass *klass)
+{
+       GtkSourceVimStateClass *state_class = GTK_SOURCE_VIM_STATE_CLASS (klass);
+
+       state_class->command_bar_text = _("-- REPLACE --");
+       state_class->append_command = gtk_source_vim_replace_append_command;
+       state_class->handle_keypress = gtk_source_vim_replace_handle_keypress;
+       state_class->enter = gtk_source_vim_replace_enter;
+       state_class->resume = gtk_source_vim_replace_resume;
+}
+
+static void
+gtk_source_vim_replace_init (GtkSourceVimReplace *self)
+{
+       gtk_source_vim_state_set_can_repeat (GTK_SOURCE_VIM_STATE (self), TRUE);
+}
diff --git a/gtksourceview/vim/gtksourcevimreplace.h b/gtksourceview/vim/gtksourcevimreplace.h
new file mode 100644
index 00000000..03406329
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimreplace.h
@@ -0,0 +1,34 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include "gtksourcevimstate.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_VIM_REPLACE (gtk_source_vim_replace_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceVimReplace, gtk_source_vim_replace, GTK_SOURCE, VIM_REPLACE, 
GtkSourceVimState)
+
+GtkSourceVimState *gtk_source_vim_replace_new (void);
+
+G_END_DECLS
diff --git a/gtksourceview/vim/gtksourcevimstate.c b/gtksourceview/vim/gtksourcevimstate.c
new file mode 100644
index 00000000..b27a418e
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimstate.c
@@ -0,0 +1,1464 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include "gtksourcebuffer.h"
+#include "gtksourcesearchcontext.h"
+#include "gtksourcesearchsettings.h"
+#include "gtksourceutils-private.h"
+#include "gtksourceview.h"
+
+#include "gtksourcevimjumplist.h"
+#include "gtksourcevimregisters.h"
+#include "gtksourcevimmarks.h"
+#include "gtksourcevimstate.h"
+
+typedef struct
+{
+       /* Owned reference to marks/registers (usually set low in the stack) */
+       GtkSourceVimState *registers;
+       GtkSourceVimState *marks;
+       GtkSourceVimState *jumplist;
+
+       /* Owned reference to the view (usually set low in the stack) */
+       GtkSourceView *view;
+
+       /* The name of the register set with "<name> */
+       const char *current_register;
+
+       /* Unowned parent pointer. The parent owns a reference to this
+        * instance of GtkSourceVimState.
+        */
+       GtkSourceVimState *parent;
+
+       /* Unowned pointer to a child that has been pushed onto our
+        * stack of states. @child must exist within @children.
+        */
+       GtkSourceVimState *child;
+
+       /* We have a custom search context/settings just for our VIM */
+       GtkSourceSearchContext *search_context;
+       GtkSourceSearchSettings *search_settings;
+
+       /* A queue of all our children, using @link of the children nodes
+        * to insert into the queue without extra allocations.
+        */
+       GQueue children;
+
+       /* The GList to be inserted into @children of the parent. */
+       GList link;
+
+       /* A count if one has been associated with the state object */
+       int count;
+
+       /* The column we last were on. Usually this is just set by the
+        * Normal state but could also be set in others (like Visual).
+        */
+       guint column;
+
+       /* Various flags */
+       guint count_set : 1;
+       guint can_repeat : 1;
+       guint column_set : 1;
+       guint reverse_search : 1;
+} GtkSourceVimStatePrivate;
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GtkSourceVimState, gtk_source_vim_state, G_TYPE_OBJECT)
+
+enum {
+       PROP_0,
+       PROP_PARENT,
+       PROP_VIEW,
+       N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+void
+gtk_source_vim_state_keyval_unescaped (guint           keyval,
+                                       GdkModifierType mods,
+                                       char            str[16])
+{
+#define return_str(v) \
+       G_STMT_START { g_strlcpy (str, v, 16); return; } G_STMT_END
+
+       str[0] = 0;
+
+       if (keyval == GDK_KEY_Escape)
+               return_str ("\e");
+
+       if ((mods & GDK_CONTROL_MASK) != 0)
+       {
+               switch (keyval)
+               {
+                       case GDK_KEY_l:
+                               return_str ("\f");
+
+                       case GDK_KEY_a:
+                               return_str ("\a");
+
+                       default:
+                               break;
+               }
+       }
+
+       switch (keyval)
+       {
+               case GDK_KEY_Tab:
+               case GDK_KEY_KP_Tab:
+               case GDK_KEY_ISO_Left_Tab:
+                       return_str ("\t");
+
+               case GDK_KEY_BackSpace:
+                       return_str ("\b");
+
+               case GDK_KEY_Return:
+               case GDK_KEY_KP_Enter:
+               case GDK_KEY_ISO_Enter:
+                       return_str ("\n");
+
+               default:
+                       break;
+       }
+
+       return gtk_source_vim_state_keyval_to_string (keyval, mods, str);
+
+#undef return_str
+}
+
+void
+gtk_source_vim_state_keyval_to_string (guint           keyval,
+                                       GdkModifierType mods,
+                                       char            str[16])
+{
+       int pos = 0;
+
+       if (keyval && (mods & GDK_CONTROL_MASK) != 0)
+       {
+               str[pos++] = '^';
+       }
+
+       switch (keyval)
+       {
+               case GDK_KEY_Escape:
+                       str[pos++] = '^';
+                       str[pos++] = '[';
+                       break;
+
+               case GDK_KEY_BackSpace:
+                       str[pos++] = '^';
+                       str[pos++] = 'H';
+                       break;
+
+               case GDK_KEY_ISO_Left_Tab:
+               case GDK_KEY_Tab:
+                       str[pos++] = '\\';
+                       str[pos++] = 't';
+                       break;
+
+               case GDK_KEY_Return:
+               case GDK_KEY_KP_Enter:
+               case GDK_KEY_ISO_Enter:
+                       str[pos++] = '\\';
+                       str[pos++] = 'n';
+                       break;
+
+               default:
+               {
+                       gunichar ch;
+
+                       /* ctrl things like ^M ^L are all uppercase */
+                       if ((mods & GDK_CONTROL_MASK) != 0)
+                               ch = gdk_keyval_to_unicode (gdk_keyval_to_upper (keyval));
+                       else
+                               ch = gdk_keyval_to_unicode (keyval);
+
+                       pos += g_unichar_to_utf8 (ch, &str[pos]);
+
+                       break;
+               }
+       }
+
+       str[pos] = 0;
+}
+
+static gboolean
+gtk_source_vim_state_real_handle_event (GtkSourceVimState *self,
+                                        GdkEvent          *event)
+{
+       g_assert (GTK_SOURCE_IS_VIM_STATE (self));
+       g_assert (event != NULL);
+
+       if (gdk_event_get_event_type (event) != GDK_KEY_PRESS)
+       {
+               return FALSE;
+       }
+
+       /* Ignore shift/control/etc keyvals */
+       switch (gdk_key_event_get_keyval (event))
+       {
+               case GDK_KEY_Shift_L:
+               case GDK_KEY_Shift_R:
+               case GDK_KEY_Shift_Lock:
+               case GDK_KEY_Caps_Lock:
+               case GDK_KEY_ISO_Lock:
+               case GDK_KEY_Control_L:
+               case GDK_KEY_Control_R:
+               case GDK_KEY_Meta_L:
+               case GDK_KEY_Meta_R:
+               case GDK_KEY_Alt_L:
+               case GDK_KEY_Alt_R:
+               case GDK_KEY_Super_L:
+               case GDK_KEY_Super_R:
+               case GDK_KEY_Hyper_L:
+               case GDK_KEY_Hyper_R:
+               case GDK_KEY_ISO_Level3_Shift:
+               case GDK_KEY_ISO_Next_Group:
+               case GDK_KEY_ISO_Prev_Group:
+               case GDK_KEY_ISO_First_Group:
+               case GDK_KEY_ISO_Last_Group:
+               case GDK_KEY_Mode_switch:
+               case GDK_KEY_Num_Lock:
+               case GDK_KEY_Multi_key:
+               case GDK_KEY_Scroll_Lock:
+                       return FALSE;
+
+               default:
+                       break;
+       }
+
+       if (GTK_SOURCE_VIM_STATE_GET_CLASS (self)->handle_keypress != NULL)
+       {
+               guint keyval;
+               guint keycode;
+               GdkModifierType mods;
+               char string[16];
+
+               keyval = gdk_key_event_get_keyval (event);
+               keycode = gdk_key_event_get_keycode (event);
+               mods = gdk_event_get_modifier_state (event)
+                    & gtk_accelerator_get_default_mod_mask ();
+               gtk_source_vim_state_keyval_to_string (keyval, mods, string);
+
+               return GTK_SOURCE_VIM_STATE_GET_CLASS (self)->handle_keypress (self, keyval, keycode, mods, 
string);
+       }
+
+       return FALSE;
+}
+
+static void
+gtk_source_vim_state_real_resume (GtkSourceVimState *self,
+                                  GtkSourceVimState *from)
+{
+       g_assert (GTK_SOURCE_IS_VIM_STATE (self));
+       g_assert (GTK_SOURCE_IS_VIM_STATE (from));
+
+       gtk_source_vim_state_unparent (from);
+}
+
+static void
+gtk_source_vim_state_dispose (GObject *object)
+{
+       GtkSourceVimState *self = (GtkSourceVimState *)object;
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       if (priv->child)
+       {
+               g_object_run_dispose (G_OBJECT (priv->child));
+       }
+
+       if (priv->parent != NULL &&
+           gtk_source_vim_state_get_child (priv->parent) == self)
+       {
+               gtk_source_vim_state_pop (self);
+       }
+
+       priv->current_register = NULL;
+
+       g_clear_object (&priv->search_context);
+       g_clear_object (&priv->search_settings);
+
+       gtk_source_vim_state_release (&priv->registers);
+       gtk_source_vim_state_release (&priv->marks);
+       gtk_source_vim_state_release (&priv->jumplist);
+
+       /* First remove the children from our list */
+       while (priv->children.length > 0)
+       {
+               GtkSourceVimState *child = g_queue_peek_head (&priv->children);
+               gtk_source_vim_state_unparent (child);
+       }
+
+       /* Now make sure we're unlinked from our parent */
+       if (priv->parent != NULL)
+       {
+               gtk_source_vim_state_unparent (self);
+       }
+
+       g_assert (priv->parent == NULL);
+       g_assert (priv->children.length == 0);
+       g_assert (priv->children.head == NULL);
+       g_assert (priv->children.tail == NULL);
+
+       g_clear_weak_pointer (&priv->view);
+
+       G_OBJECT_CLASS (gtk_source_vim_state_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_vim_state_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+       GtkSourceVimState *self = GTK_SOURCE_VIM_STATE (object);
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       switch (prop_id)
+       {
+               case PROP_PARENT:
+                       g_value_set_object (value, priv->parent);
+                       break;
+
+               case PROP_VIEW:
+                       g_value_set_object (value, priv->view);
+                       break;
+
+               default:
+                       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+       }
+}
+
+static void
+gtk_source_vim_state_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+       GtkSourceVimState *self = GTK_SOURCE_VIM_STATE (object);
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       switch (prop_id)
+       {
+               case PROP_PARENT:
+                       gtk_source_vim_state_set_parent (self, g_value_get_object (value));
+                       break;
+
+               case PROP_VIEW:
+                       g_set_weak_pointer (&priv->view, g_value_get_object (value));
+
+                       if (GTK_SOURCE_VIM_STATE_GET_CLASS (self)->view_set)
+                       {
+                               GTK_SOURCE_VIM_STATE_GET_CLASS (self)->view_set (self);
+                       }
+
+                       break;
+
+               default:
+                       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+       }
+}
+
+static void
+gtk_source_vim_state_class_init (GtkSourceVimStateClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->dispose = gtk_source_vim_state_dispose;
+       object_class->get_property = gtk_source_vim_state_get_property;
+       object_class->set_property = gtk_source_vim_state_set_property;
+
+       klass->handle_event = gtk_source_vim_state_real_handle_event;
+       klass->resume = gtk_source_vim_state_real_resume;
+
+       properties [PROP_PARENT] =
+               g_param_spec_object ("parent",
+                                    "Parent",
+                                    "The parent state",
+                                    GTK_SOURCE_TYPE_VIM_STATE,
+                                    (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+       properties [PROP_VIEW] =
+               g_param_spec_object ("view",
+                                    "View",
+                                    "The source view",
+                                    GTK_SOURCE_TYPE_VIEW,
+                                    (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gtk_source_vim_state_init (GtkSourceVimState *self)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       priv->link.data = self;
+       priv->count = 1;
+}
+
+GtkSourceView *
+gtk_source_vim_state_get_view (GtkSourceVimState *self)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), NULL);
+
+       if (priv->view)
+               return priv->view;
+
+       if (priv->parent == NULL)
+               return NULL;
+
+       return gtk_source_vim_state_get_view (priv->parent);
+}
+
+GtkSourceBuffer *
+gtk_source_vim_state_get_buffer (GtkSourceVimState *self,
+                                 GtkTextIter       *insert,
+                                 GtkTextIter       *selection_bound)
+{
+       GtkSourceView *view;
+       GtkTextBuffer *buffer;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), NULL);
+
+       if (!(view = gtk_source_vim_state_get_view (self)))
+               return NULL;
+
+       buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
+
+       g_assert (GTK_SOURCE_IS_BUFFER (buffer));
+
+       if (insert != NULL)
+       {
+               gtk_text_buffer_get_iter_at_mark (buffer, insert, gtk_text_buffer_get_insert (buffer));
+       }
+
+       if (selection_bound != NULL)
+       {
+               gtk_text_buffer_get_iter_at_mark (buffer, selection_bound, 
gtk_text_buffer_get_selection_bound (buffer));
+       }
+
+       return GTK_SOURCE_BUFFER (buffer);
+}
+
+void
+gtk_source_vim_state_beep (GtkSourceVimState *self)
+{
+       GtkSourceView *view;
+       GdkDisplay *display;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       if ((view = gtk_source_vim_state_get_view (self)) &&
+           (display = gtk_widget_get_display (GTK_WIDGET (view))))
+       {
+               gdk_display_beep (display);
+       }
+}
+
+GtkSourceVimState *
+gtk_source_vim_state_get_child (GtkSourceVimState *self)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), NULL);
+
+       return priv->child;
+}
+
+GtkSourceVimState *
+gtk_source_vim_state_get_current (GtkSourceVimState *self)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), NULL);
+
+       if (priv->child == NULL)
+               return self;
+
+       return gtk_source_vim_state_get_current (priv->child);
+}
+
+GtkSourceVimState *
+gtk_source_vim_state_get_parent (GtkSourceVimState *self)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), NULL);
+
+       return priv->parent;
+}
+
+GtkSourceVimState *
+gtk_source_vim_state_get_root (GtkSourceVimState *self)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), NULL);
+
+       if (priv->parent == NULL)
+               return self;
+
+       return gtk_source_vim_state_get_root (priv->parent);
+}
+
+void
+gtk_source_vim_state_repeat (GtkSourceVimState *self)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       if (GTK_SOURCE_VIM_STATE_GET_CLASS (self)->repeat)
+       {
+               GTK_SOURCE_VIM_STATE_GET_CLASS (self)->repeat (self);
+       }
+}
+
+gboolean
+gtk_source_vim_state_handle_event (GtkSourceVimState *self,
+                                   GdkEvent          *event)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), FALSE);
+       g_return_val_if_fail (event != NULL, FALSE);
+
+       if (GTK_SOURCE_VIM_STATE_GET_CLASS (self)->handle_event)
+       {
+               return GTK_SOURCE_VIM_STATE_GET_CLASS (self)->handle_event (self, event);
+       }
+
+       return FALSE;
+}
+
+/**
+ * gtk_source_vim_state_push:
+ * @self: a #GtkSourceVimState
+ * @new_state: (transfer full): the new child state for @self
+ *
+ * Pushes @new_state as the current child of @self.
+ *
+ * This steals a reference from @new_state to simplify use.
+ * Remember to g_object_ref(new_state) if you need to keep
+ * a reference.
+ */
+void
+gtk_source_vim_state_push (GtkSourceVimState *self,
+                           GtkSourceVimState *new_state)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (new_state));
+       g_return_if_fail (gtk_source_vim_state_get_parent (new_state) == NULL);
+
+       if (priv->child != NULL)
+       {
+               g_warning ("Attempt to push state %s onto %s when it already has a %s",
+                          G_OBJECT_TYPE_NAME (new_state),
+                          G_OBJECT_TYPE_NAME (self),
+                          G_OBJECT_TYPE_NAME (priv->child));
+       }
+
+       gtk_source_vim_state_set_parent (new_state, self);
+
+       priv->child = new_state;
+
+       if (GTK_SOURCE_VIM_STATE_GET_CLASS (self)->suspend)
+       {
+               GTK_SOURCE_VIM_STATE_GET_CLASS (self)->suspend (self, new_state);
+       }
+
+       if (GTK_SOURCE_VIM_STATE_GET_CLASS (new_state)->enter)
+       {
+               GTK_SOURCE_VIM_STATE_GET_CLASS (new_state)->enter (new_state);
+       }
+
+       g_object_unref (new_state);
+}
+
+void
+gtk_source_vim_state_pop (GtkSourceVimState *self)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+       GtkSourceVimStatePrivate *parent_priv;
+       GtkSourceVimState *parent;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+       g_return_if_fail (priv->child == NULL);
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (priv->parent));
+
+       parent = g_object_ref (priv->parent);
+       parent_priv = gtk_source_vim_state_get_instance_private (parent);
+
+       if (parent_priv->child == self)
+       {
+               parent_priv->child = NULL;
+       }
+       else
+       {
+               g_warning ("Attempt to pop state %s from %s but it is not current",
+                          G_OBJECT_TYPE_NAME (self),
+                          G_OBJECT_TYPE_NAME (parent));
+       }
+
+       if (GTK_SOURCE_VIM_STATE_GET_CLASS (self)->leave)
+       {
+               GTK_SOURCE_VIM_STATE_GET_CLASS (self)->leave (self);
+       }
+
+       if (GTK_SOURCE_VIM_STATE_GET_CLASS (parent)->resume)
+       {
+               GTK_SOURCE_VIM_STATE_GET_CLASS (parent)->resume (parent, self);
+       }
+
+       g_object_unref (parent);
+}
+
+void
+gtk_source_vim_state_set_overwrite (GtkSourceVimState *self,
+                                    gboolean           overwrite)
+{
+       GtkSourceView *view;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       view = gtk_source_vim_state_get_view (self);
+
+       if (view != NULL)
+       {
+               gtk_text_view_set_overwrite (GTK_TEXT_VIEW (view), overwrite);
+       }
+}
+
+gboolean
+gtk_source_vim_state_synthesize (GtkSourceVimState *self,
+                                 guint              keyval,
+                                 GdkModifierType    mods)
+{
+       char string[16];
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), FALSE);
+
+       gtk_source_vim_state_keyval_to_string (keyval, mods, string);
+
+       return GTK_SOURCE_VIM_STATE_GET_CLASS (self)->handle_keypress (self, keyval, 0, mods, string);
+}
+
+void
+gtk_source_vim_state_select (GtkSourceVimState *self,
+                             const GtkTextIter *insert,
+                             const GtkTextIter *selection)
+{
+       GtkSourceView *view;
+       GtkTextBuffer *buffer;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+       g_return_if_fail (insert != NULL);
+
+       if (selection == NULL)
+               selection = insert;
+
+       view = gtk_source_vim_state_get_view (self);
+       g_return_if_fail (GTK_SOURCE_IS_VIEW (view));
+
+       buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
+       g_return_if_fail (GTK_SOURCE_IS_BUFFER (buffer));
+
+       gtk_text_buffer_select_range (buffer, insert, selection);
+}
+
+int
+gtk_source_vim_state_get_visible_lines (GtkSourceVimState *self)
+{
+       GtkSourceView *view;
+       GtkTextIter begin, end;
+       GdkRectangle rect;
+       guint bline, eline;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), 2);
+
+       view = gtk_source_vim_state_get_view (self);
+
+       gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (view), &rect);
+       gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), &begin, rect.x, rect.y);
+       gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), &end, rect.x, rect.y + rect.height);
+
+       bline = gtk_text_iter_get_line (&begin);
+       eline = gtk_text_iter_get_line (&end);
+
+       return MAX (2, eline - bline);
+}
+
+void
+gtk_source_vim_state_scroll_line (GtkSourceVimState *self,
+                                  int                count)
+{
+       GtkSourceView *view;
+       GdkRectangle rect;
+       GtkTextIter top;
+       int y, height;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       if (count == 0)
+               count = 1;
+
+       view = gtk_source_vim_state_get_view (self);
+       gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (view), &rect);
+       gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), &top, rect.x, rect.y);
+       gtk_text_view_get_line_yrange (GTK_TEXT_VIEW (view), &top, &y, &height);
+
+       /* Add a line is slightly visible. Works in both directions */
+       if (y < rect.y)
+               count++;
+
+       if (count > 0)
+               gtk_text_iter_forward_lines (&top, count);
+       else
+               gtk_text_iter_backward_lines (&top, -count);
+
+       _gtk_source_view_jump_to_iter (GTK_TEXT_VIEW (view), &top, 0.0, TRUE, 1.0, 0.0);
+
+       gtk_source_vim_state_place_cursor_onscreen (self);
+}
+
+static void
+scroll_half_page_down (GtkSourceVimState *self)
+{
+       GtkSourceView *view;
+       GdkRectangle rect;
+       GtkTextIter iter;
+
+       g_assert (GTK_SOURCE_IS_VIM_STATE (self));
+
+       view = gtk_source_vim_state_get_view (self);
+       gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (view), &rect);
+       gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), &iter, rect.x, rect.y + rect.height / 2);
+       _gtk_source_view_jump_to_iter (GTK_TEXT_VIEW (view), &iter, 0.0, TRUE, 1.0, 0.0);
+}
+
+static void
+scroll_half_page_up (GtkSourceVimState *self)
+{
+       GtkSourceView *view;
+       GdkRectangle rect;
+       GtkTextIter iter;
+
+       g_assert (GTK_SOURCE_IS_VIM_STATE (self));
+
+       view = gtk_source_vim_state_get_view (self);
+       gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (view), &rect);
+       gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), &iter, rect.x, rect.y + rect.height / 2);
+       _gtk_source_view_jump_to_iter (GTK_TEXT_VIEW (view), &iter, 0.0, TRUE, 1.0, 1.0);
+}
+
+void
+gtk_source_vim_state_scroll_half_page (GtkSourceVimState *self,
+                                      int                count)
+{
+       GtkSourceView *view;
+       GdkRectangle rect, loc;
+       GtkTextIter iter;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       if (count == 0)
+               count = 1;
+
+       gtk_source_vim_state_get_buffer (self, &iter, NULL);
+       view = gtk_source_vim_state_get_view (self);
+       gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (view), &rect);
+       gtk_text_view_get_iter_location (GTK_TEXT_VIEW (view), &iter, &loc);
+       gtk_text_view_buffer_to_window_coords (GTK_TEXT_VIEW (view),
+                                              GTK_TEXT_WINDOW_TEXT,
+                                              loc.x, loc.y, &loc.x, &loc.y);
+
+       for (int i = 1; i <= ABS (count); i++)
+       {
+               if (count > 0)
+                       scroll_half_page_down (self);
+               else
+                       scroll_half_page_up (self);
+       }
+
+       gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (view),
+                                              GTK_TEXT_WINDOW_TEXT,
+                                              loc.x, loc.y, &loc.x, &loc.y);
+       gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), &iter, loc.x, loc.y);
+       gtk_source_vim_state_select (self, &iter, &iter);
+
+       gtk_source_vim_state_place_cursor_onscreen (self);
+}
+
+static void
+scroll_page_down (GtkSourceVimState *self)
+{
+       GtkSourceView *view;
+       GdkRectangle rect;
+       GtkTextIter iter;
+
+       g_assert (GTK_SOURCE_IS_VIM_STATE (self));
+
+       view = gtk_source_vim_state_get_view (self);
+       gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (view), &rect);
+       gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), &iter, rect.x, rect.y+rect.height);
+       _gtk_source_view_jump_to_iter (GTK_TEXT_VIEW (view), &iter, 0.0, TRUE, 1.0, 0.0);
+}
+
+static void
+scroll_page_up (GtkSourceVimState *self)
+{
+       GtkSourceView *view;
+       GdkRectangle rect;
+       GtkTextIter iter;
+
+       g_assert (GTK_SOURCE_IS_VIM_STATE (self));
+
+       view = gtk_source_vim_state_get_view (self);
+       gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (view), &rect);
+       gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), &iter, rect.x, rect.y);
+       _gtk_source_view_jump_to_iter (GTK_TEXT_VIEW (view), &iter, 0.0, TRUE, 1.0, 1.0);
+}
+
+void
+gtk_source_vim_state_scroll_page (GtkSourceVimState *self,
+                                  int                count)
+{
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       if (count == 0)
+               count = 1;
+
+       for (int i = 1; i <= ABS (count); i++)
+       {
+               if (count > 0)
+                       scroll_page_down (self);
+               else
+                       scroll_page_up (self);
+       }
+
+       gtk_source_vim_state_place_cursor_onscreen (self);
+}
+
+void
+gtk_source_vim_state_place_cursor_onscreen (GtkSourceVimState *self)
+{
+       GtkSourceView *view;
+       GtkTextIter iter;
+       GdkRectangle rect, loc;
+       gboolean move_insert = FALSE;
+
+       g_assert (GTK_SOURCE_IS_VIM_STATE (self));
+
+       view = gtk_source_vim_state_get_view (self);
+
+       gtk_source_vim_state_get_buffer (self, &iter, NULL);
+       gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (view), &rect);
+       gtk_text_view_get_iter_location (GTK_TEXT_VIEW (view), &iter, &loc);
+
+       if (loc.y < rect.y)
+       {
+               gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view),
+                                                   &iter, rect.x, rect.y);
+               move_insert = TRUE;
+       }
+       else if (loc.y + loc.height > rect.y + rect.height)
+       {
+
+               gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view),
+                                                   &iter, rect.x, rect.y + rect.height);
+               gtk_text_view_get_iter_location (GTK_TEXT_VIEW (view), &iter, &loc);
+               if (loc.y + loc.height > rect.y + rect.height)
+                       gtk_text_iter_backward_line (&iter);
+               move_insert = TRUE;
+       }
+
+       if (move_insert)
+       {
+               while (!gtk_text_iter_ends_line (&iter) &&
+                      g_unichar_isspace (gtk_text_iter_get_char (&iter)))
+               {
+                      gtk_text_iter_forward_char (&iter);
+               }
+               gtk_source_vim_state_select (self, &iter, &iter);
+       }
+}
+
+void
+gtk_source_vim_state_z_scroll (GtkSourceVimState *self,
+                               double             yalign)
+{
+       GtkSourceView *view;
+       GtkTextIter iter;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       gtk_source_vim_state_get_buffer (self, &iter, NULL);
+       view = gtk_source_vim_state_get_view (self);
+
+       gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (view), &iter, 0, TRUE, 1.0, yalign);
+}
+
+void
+gtk_source_vim_state_append_command (GtkSourceVimState *self,
+                                     GString           *string)
+{
+       GtkSourceVimState *child;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       if (GTK_SOURCE_VIM_STATE_GET_CLASS (self)->append_command)
+       {
+               GTK_SOURCE_VIM_STATE_GET_CLASS (self)->append_command (self, string);
+       }
+
+       child = gtk_source_vim_state_get_child (self);
+
+       if (child != NULL)
+       {
+               gtk_source_vim_state_append_command (child, string);
+       }
+}
+
+int
+gtk_source_vim_state_get_count (GtkSourceVimState *self)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), 0);
+
+       return priv->count;
+}
+
+void
+gtk_source_vim_state_set_count (GtkSourceVimState *self,
+                                int                count)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       priv->count = count ? count : 1;
+       priv->count_set = count != 0;
+}
+
+void
+gtk_source_vim_state_unparent (GtkSourceVimState *self)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+       GtkSourceVimStatePrivate *parent_priv;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+       g_return_if_fail (priv->link.data == self);
+
+       if (priv->parent == NULL)
+       {
+               return;
+       }
+
+       parent_priv = gtk_source_vim_state_get_instance_private (priv->parent);
+       priv->parent = NULL;
+
+       if (parent_priv->child == self)
+       {
+               parent_priv->child = NULL;
+       }
+
+       g_queue_unlink (&parent_priv->children, &priv->link);
+
+       g_object_unref (self);
+}
+
+void
+gtk_source_vim_state_set_parent (GtkSourceVimState *self,
+                                 GtkSourceVimState *parent)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+       GtkSourceVimStatePrivate *parent_priv;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+       g_return_if_fail (!parent || GTK_SOURCE_IS_VIM_STATE (parent));
+
+       if (priv->parent == parent)
+               return;
+
+       g_object_ref (self);
+
+       if (priv->parent != NULL)
+       {
+               gtk_source_vim_state_unparent (self);
+       }
+
+       g_assert (priv->parent == NULL);
+       g_assert (priv->link.data == self);
+       g_assert (priv->link.next == NULL);
+       g_assert (priv->link.prev == NULL);
+
+       if (parent != NULL)
+       {
+               priv->parent = parent;
+               parent_priv = gtk_source_vim_state_get_instance_private (parent);
+               g_queue_push_tail_link (&parent_priv->children, &priv->link);
+               g_object_ref (self);
+       }
+
+       g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PARENT]);
+
+       g_object_unref (self);
+}
+
+gboolean
+gtk_source_vim_state_get_count_set (GtkSourceVimState *self)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), FALSE);
+
+       return priv->count_set;
+}
+
+void
+gtk_source_vim_state_begin_user_action (GtkSourceVimState *self)
+{
+       GtkSourceBuffer *buffer;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       buffer = gtk_source_vim_state_get_buffer (self, NULL, NULL);
+       gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (buffer));
+}
+
+void
+gtk_source_vim_state_end_user_action (GtkSourceVimState *self)
+{
+       GtkSourceBuffer *buffer;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       buffer = gtk_source_vim_state_get_buffer (self, NULL, NULL);
+       gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (buffer));
+}
+
+gboolean
+gtk_source_vim_state_get_can_repeat (GtkSourceVimState *self)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), FALSE);
+
+       return priv->can_repeat;
+}
+
+void
+gtk_source_vim_state_set_can_repeat (GtkSourceVimState *self,
+                                     gboolean           can_repeat)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       priv->can_repeat = !!can_repeat;
+}
+
+GtkSourceVimState *
+gtk_source_vim_state_get_registers (GtkSourceVimState *self)
+{
+       GtkSourceVimStatePrivate *priv;
+       GtkSourceVimState *root;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), NULL);
+
+       root = gtk_source_vim_state_get_root (self);
+       priv = gtk_source_vim_state_get_instance_private (root);
+
+       if (priv->registers == NULL)
+       {
+               priv->registers = gtk_source_vim_registers_new ();
+               gtk_source_vim_state_set_parent (priv->registers, GTK_SOURCE_VIM_STATE (root));
+       }
+
+       return priv->registers;
+}
+
+const char *
+gtk_source_vim_state_get_current_register (GtkSourceVimState *self)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), NULL);
+
+       if (priv->current_register != NULL)
+       {
+               return priv->current_register;
+       }
+
+       if (priv->parent != NULL)
+       {
+               return gtk_source_vim_state_get_current_register (priv->parent);
+       }
+
+       return NULL;
+}
+
+void
+gtk_source_vim_state_set_current_register (GtkSourceVimState *self,
+                                           const char        *current_register)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       if (g_strcmp0 (priv->current_register, current_register) != 0)
+       {
+               priv->current_register = g_intern_string (current_register);
+       }
+}
+
+const char *
+gtk_source_vim_state_get_current_register_value (GtkSourceVimState *self)
+{
+       GtkSourceVimState *registers;
+       const char *current_register;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), NULL);
+
+       current_register = gtk_source_vim_state_get_current_register (self);
+       registers = gtk_source_vim_state_get_registers (self);
+
+       return gtk_source_vim_registers_get (GTK_SOURCE_VIM_REGISTERS (registers), current_register);
+}
+
+void
+gtk_source_vim_state_set_current_register_value (GtkSourceVimState *self,
+                                                 const char        *value)
+{
+       GtkSourceVimState *registers;
+       const char *current_register;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       current_register = gtk_source_vim_state_get_current_register (self);
+       registers = gtk_source_vim_state_get_registers (self);
+
+       if (!gtk_source_vim_register_is_read_only (current_register))
+       {
+               gtk_source_vim_registers_set (GTK_SOURCE_VIM_REGISTERS (registers),
+                                             current_register,
+                                             value);
+       }
+}
+
+guint
+gtk_source_vim_state_get_visual_column (GtkSourceVimState *self)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+       GtkSourceView *view;
+       GtkTextIter iter;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), FALSE);
+
+       if (priv->column_set)
+       {
+               return priv->column;
+       }
+
+       if (priv->parent != NULL)
+       {
+               return gtk_source_vim_state_get_visual_column (priv->parent);
+       }
+
+       view = gtk_source_vim_state_get_view (self);
+       gtk_source_vim_state_get_buffer (self, &iter, NULL);
+
+       return gtk_source_view_get_visual_column (view, &iter);
+}
+
+void
+gtk_source_vim_state_set_visual_column (GtkSourceVimState *self,
+                                        int                visual_column)
+{
+       GtkSourceVimStatePrivate *priv = gtk_source_vim_state_get_instance_private (self);
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       if (visual_column < 0)
+       {
+               priv->column_set = FALSE;
+               return;
+       }
+
+       priv->column = visual_column;
+       priv->column_set = TRUE;
+}
+
+static void
+extend_lines (GtkTextIter *a,
+              GtkTextIter *b)
+{
+       if (gtk_text_iter_compare (a, b) <= 0)
+       {
+               gtk_text_iter_set_line_offset (a, 0);
+               if (!gtk_text_iter_ends_line (b))
+                       gtk_text_iter_forward_to_line_end (b);
+               if (gtk_text_iter_ends_line (b) && !gtk_text_iter_is_end (b))
+                       gtk_text_iter_forward_char (b);
+       }
+       else
+       {
+               gtk_text_iter_set_line_offset (b, 0);
+               if (!gtk_text_iter_ends_line (a))
+                       gtk_text_iter_forward_to_line_end (a);
+               if (gtk_text_iter_ends_line (a) && !gtk_text_iter_is_end (a))
+                       gtk_text_iter_forward_char (a);
+       }
+}
+
+void
+gtk_source_vim_state_select_linewise (GtkSourceVimState *self,
+                                      GtkTextIter       *insert,
+                                      GtkTextIter       *selection)
+{
+       GtkSourceBuffer *buffer;
+       GtkTextIter iter1, iter2;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       buffer = gtk_source_vim_state_get_buffer (self, &iter1, &iter2);
+
+       if (insert == NULL)
+               insert = &iter1;
+
+       if (selection == NULL)
+               selection = &iter2;
+
+       extend_lines (insert, selection);
+
+       gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer), insert, selection);
+}
+
+gboolean
+gtk_source_vim_state_get_editable (GtkSourceVimState *self)
+{
+       GtkSourceView *view;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), FALSE);
+
+       view = gtk_source_vim_state_get_view (self);
+
+       return gtk_text_view_get_editable (GTK_TEXT_VIEW (view));
+}
+
+void
+gtk_source_vim_state_get_search (GtkSourceVimState        *self,
+                                 GtkSourceSearchSettings **settings,
+                                 GtkSourceSearchContext  **context)
+{
+       GtkSourceVimStatePrivate *priv;
+       GtkSourceVimState *root;
+       GtkSourceBuffer *buffer;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       root = gtk_source_vim_state_get_root (self);
+       priv = gtk_source_vim_state_get_instance_private (root);
+       buffer = gtk_source_vim_state_get_buffer (self, NULL, NULL);
+
+       if (priv->search_settings == NULL)
+       {
+               priv->search_settings = gtk_source_search_settings_new ();
+               gtk_source_search_settings_set_wrap_around (priv->search_settings, TRUE);
+               gtk_source_search_settings_set_regex_enabled (priv->search_settings, TRUE);
+               gtk_source_search_settings_set_case_sensitive (priv->search_settings, TRUE);
+       }
+
+       if (priv->search_context == NULL)
+       {
+               priv->search_context = gtk_source_search_context_new (buffer, priv->search_settings);
+               gtk_source_search_context_set_highlight (priv->search_context, TRUE);
+       }
+
+       if (settings != NULL)
+       {
+               *settings = priv->search_settings;
+       }
+
+       if (context != NULL)
+       {
+               *context = priv->search_context;
+       }
+}
+
+gboolean
+gtk_source_vim_state_get_reverse_search (GtkSourceVimState *self)
+{
+       GtkSourceVimStatePrivate *priv;
+       GtkSourceVimState *root;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), FALSE);
+
+       root = gtk_source_vim_state_get_root (self);
+       priv = gtk_source_vim_state_get_instance_private (root);
+
+       return priv->reverse_search;
+}
+
+void
+gtk_source_vim_state_set_reverse_search (GtkSourceVimState *self,
+                                         gboolean           reverse_search)
+{
+       GtkSourceVimStatePrivate *priv;
+       GtkSourceVimState *root;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+
+       root = gtk_source_vim_state_get_root (self);
+       priv = gtk_source_vim_state_get_instance_private (root);
+
+       priv->reverse_search = !!reverse_search;
+}
+
+static GtkSourceVimMarks *
+gtk_source_vim_state_get_marks (GtkSourceVimState *self)
+{
+       GtkSourceVimStatePrivate *priv;
+       GtkSourceVimState *root;
+
+       g_assert (GTK_SOURCE_IS_VIM_STATE (self));
+
+       root = gtk_source_vim_state_get_root (self);
+       priv = gtk_source_vim_state_get_instance_private (root);
+
+       if (priv->marks == NULL)
+       {
+               priv->marks = gtk_source_vim_marks_new ();
+               gtk_source_vim_state_set_parent (GTK_SOURCE_VIM_STATE (priv->marks), root);
+       }
+
+       return GTK_SOURCE_VIM_MARKS (priv->marks);
+}
+
+GtkTextMark *
+gtk_source_vim_state_get_mark (GtkSourceVimState *self,
+                               const char        *name)
+{
+       GtkSourceVimMarks *marks;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), NULL);
+       g_return_val_if_fail (name != NULL, NULL);
+
+       marks = gtk_source_vim_state_get_marks (self);
+
+       return gtk_source_vim_marks_get_mark (marks, name);
+}
+
+void
+gtk_source_vim_state_set_mark (GtkSourceVimState *self,
+                               const char        *name,
+                               const GtkTextIter *iter)
+{
+       GtkSourceVimMarks *marks;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+       g_return_if_fail (name != NULL);
+
+       marks = gtk_source_vim_state_get_marks (self);
+
+       return gtk_source_vim_marks_set_mark (marks, name, iter);
+}
+
+gboolean
+gtk_source_vim_state_get_iter_at_mark (GtkSourceVimState *self,
+                                       const char        *name,
+                                       GtkTextIter       *iter)
+{
+       GtkSourceVimMarks *marks;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), FALSE);
+       g_return_val_if_fail (name != NULL, FALSE);
+
+       marks = gtk_source_vim_state_get_marks (self);
+
+       return gtk_source_vim_marks_get_iter (marks, name, iter);
+}
+
+static GtkSourceVimJumplist *
+gtk_source_vim_state_get_jumplist (GtkSourceVimState *self)
+{
+       GtkSourceVimState *root;
+       GtkSourceVimStatePrivate *priv;
+
+       g_assert (GTK_SOURCE_IS_VIM_STATE (self));
+
+       root = gtk_source_vim_state_get_root (self);
+       priv = gtk_source_vim_state_get_instance_private (root);
+
+       if (priv->jumplist == NULL)
+       {
+               priv->jumplist = gtk_source_vim_jumplist_new ();
+               gtk_source_vim_state_set_parent (priv->jumplist, root);
+       }
+
+       return GTK_SOURCE_VIM_JUMPLIST (priv->jumplist);
+}
+
+void
+gtk_source_vim_state_push_jump (GtkSourceVimState *self,
+                                const GtkTextIter *iter)
+{
+       GtkSourceVimJumplist *jumplist;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_STATE (self));
+       g_return_if_fail (iter != NULL);
+
+       jumplist = gtk_source_vim_state_get_jumplist (self);
+       gtk_source_vim_jumplist_push (jumplist, iter);
+}
+
+gboolean
+gtk_source_vim_state_jump_backward (GtkSourceVimState *self,
+                                    GtkTextIter       *iter)
+{
+       GtkSourceVimJumplist *jumplist;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), FALSE);
+       g_return_val_if_fail (iter != NULL, FALSE);
+
+       jumplist = gtk_source_vim_state_get_jumplist (self);
+
+       return gtk_source_vim_jumplist_previous (jumplist, iter);
+}
+
+gboolean
+gtk_source_vim_state_jump_forward (GtkSourceVimState *self,
+                                   GtkTextIter       *iter)
+{
+       GtkSourceVimJumplist *jumplist;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_STATE (self), FALSE);
+       g_return_val_if_fail (iter != NULL, FALSE);
+
+       jumplist = gtk_source_vim_state_get_jumplist (self);
+
+       return gtk_source_vim_jumplist_next (jumplist, iter);
+}
diff --git a/gtksourceview/vim/gtksourcevimstate.h b/gtksourceview/vim/gtksourcevimstate.h
new file mode 100644
index 00000000..d8bf8405
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimstate.h
@@ -0,0 +1,199 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include <gtksourceview/gtksourcetypes.h>
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_VIM_STATE (gtk_source_vim_state_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (GtkSourceVimState, gtk_source_vim_state, GTK_SOURCE, VIM_STATE, GObject)
+
+struct _GtkSourceVimStateClass
+{
+       GObjectClass parent_class;
+
+       const char *command_bar_text;
+
+       const char *(*get_command_bar_text) (GtkSourceVimState *self);
+       void        (*view_set)             (GtkSourceVimState *self);
+       void        (*enter)                (GtkSourceVimState *self);
+       void        (*suspend)              (GtkSourceVimState *self,
+                                            GtkSourceVimState *to);
+       void        (*resume)               (GtkSourceVimState *self,
+                                            GtkSourceVimState *from);
+       void        (*leave)                (GtkSourceVimState *self);
+       gboolean    (*handle_event)         (GtkSourceVimState *self,
+                                            GdkEvent          *event);
+       gboolean    (*handle_keypress)      (GtkSourceVimState *self,
+                                            guint              keyval,
+                                            guint              keycode,
+                                            GdkModifierType    mods,
+                                            const char        *string);
+       void        (*repeat)               (GtkSourceVimState *self);
+       void        (*append_command)       (GtkSourceVimState *self,
+                                            GString           *string);
+};
+
+gboolean           gtk_source_vim_state_get_editable               (GtkSourceVimState        *self);
+void               gtk_source_vim_state_set_parent                 (GtkSourceVimState        *self,
+                                                                    GtkSourceVimState        *parent);
+void               gtk_source_vim_state_unparent                   (GtkSourceVimState        *self);
+void               gtk_source_vim_state_push                       (GtkSourceVimState        *self,
+                                                                    GtkSourceVimState        *new_state);
+void               gtk_source_vim_state_pop                        (GtkSourceVimState        *self);
+void               gtk_source_vim_state_append_command             (GtkSourceVimState        *self,
+                                                                    GString                  *string);
+void               gtk_source_vim_state_beep                       (GtkSourceVimState        *self);
+GtkSourceVimState *gtk_source_vim_state_get_child                  (GtkSourceVimState        *self);
+GtkSourceVimState *gtk_source_vim_state_get_current                (GtkSourceVimState        *self);
+GtkSourceView     *gtk_source_vim_state_get_view                   (GtkSourceVimState        *self);
+GtkSourceBuffer   *gtk_source_vim_state_get_buffer                 (GtkSourceVimState        *self,
+                                                                    GtkTextIter              *insert,
+                                                                    GtkTextIter              
*selection_bound);
+GtkSourceVimState *gtk_source_vim_state_get_root                   (GtkSourceVimState        *self);
+GtkSourceVimState *gtk_source_vim_state_get_parent                 (GtkSourceVimState        *self);
+GtkSourceVimState *gtk_source_vim_state_get_registers              (GtkSourceVimState        *self);
+int                gtk_source_vim_state_get_count                  (GtkSourceVimState        *self);
+gboolean           gtk_source_vim_state_get_count_set              (GtkSourceVimState        *self);
+void               gtk_source_vim_state_set_count                  (GtkSourceVimState        *self,
+                                                                    int                       count);
+gboolean           gtk_source_vim_state_get_can_repeat             (GtkSourceVimState        *self);
+void               gtk_source_vim_state_set_can_repeat             (GtkSourceVimState        *self,
+                                                                    gboolean                  can_repeat);
+void               gtk_source_vim_state_begin_user_action          (GtkSourceVimState        *self);
+void               gtk_source_vim_state_end_user_action            (GtkSourceVimState        *self);
+gboolean           gtk_source_vim_state_handle_event               (GtkSourceVimState        *self,
+                                                                    GdkEvent                 *event);
+void               gtk_source_vim_state_set_overwrite              (GtkSourceVimState        *self,
+                                                                    gboolean                  overwrite);
+gboolean           gtk_source_vim_state_synthesize                 (GtkSourceVimState        *self,
+                                                                    guint                     keyval,
+                                                                    GdkModifierType           mods);
+void               gtk_source_vim_state_repeat                     (GtkSourceVimState        *self);
+int                gtk_source_vim_state_get_visible_lines          (GtkSourceVimState        *self);
+void               gtk_source_vim_state_scroll_page                (GtkSourceVimState        *self,
+                                                                    int                       count);
+void               gtk_source_vim_state_scroll_half_page           (GtkSourceVimState        *self,
+                                                                    int                       count);
+void               gtk_source_vim_state_scroll_line                (GtkSourceVimState        *self,
+                                                                    int                       count);
+void               gtk_source_vim_state_z_scroll                   (GtkSourceVimState        *self,
+                                                                    double                    yalign);
+void               gtk_source_vim_state_select                     (GtkSourceVimState        *self,
+                                                                    const GtkTextIter        *insert,
+                                                                    const GtkTextIter        *selection);
+const char        *gtk_source_vim_state_get_current_register       (GtkSourceVimState        *self);
+void               gtk_source_vim_state_set_current_register       (GtkSourceVimState        *self,
+                                                                    const char               
*current_register);
+const char        *gtk_source_vim_state_get_current_register_value (GtkSourceVimState        *self);
+void               gtk_source_vim_state_set_current_register_value (GtkSourceVimState        *self,
+                                                                    const char               *value);
+void               gtk_source_vim_state_place_cursor_onscreen      (GtkSourceVimState        *self);
+guint              gtk_source_vim_state_get_visual_column          (GtkSourceVimState        *self);
+void               gtk_source_vim_state_set_visual_column          (GtkSourceVimState        *self,
+                                                                    int                       visual_column);
+void               gtk_source_vim_state_select_linewise            (GtkSourceVimState        *self,
+                                                                    GtkTextIter              *insert,
+                                                                    GtkTextIter              *selection);
+void               gtk_source_vim_state_get_search                 (GtkSourceVimState        *self,
+                                                                    GtkSourceSearchSettings **settings,
+                                                                    GtkSourceSearchContext  **context);
+gboolean           gtk_source_vim_state_get_reverse_search         (GtkSourceVimState        *self);
+void               gtk_source_vim_state_set_reverse_search         (GtkSourceVimState        *self,
+                                                                    gboolean                  
reverse_search);
+GtkTextMark       *gtk_source_vim_state_get_mark                   (GtkSourceVimState        *self,
+                                                                    const char               *name);
+void               gtk_source_vim_state_set_mark                   (GtkSourceVimState        *self,
+                                                                   const char               *name,
+                                                                   const GtkTextIter        *iter);
+gboolean           gtk_source_vim_state_get_iter_at_mark           (GtkSourceVimState        *self,
+                                                                   const char               *name,
+                                                                   GtkTextIter              *iter);
+void               gtk_source_vim_state_keyval_to_string           (guint                     keyval,
+                                                                    GdkModifierType           mods,
+                                                                    char                      string[16]);
+void               gtk_source_vim_state_keyval_unescaped           (guint                     keyval,
+                                                                    GdkModifierType           mods,
+                                                                    char                      string[16]);
+void               gtk_source_vim_state_push_jump                  (GtkSourceVimState        *self,
+                                                                    const GtkTextIter        *iter);
+gboolean           gtk_source_vim_state_jump_backward              (GtkSourceVimState        *self,
+                                                                    GtkTextIter              *iter);
+gboolean           gtk_source_vim_state_jump_forward               (GtkSourceVimState        *self,
+                                                                    GtkTextIter              *iter);
+
+static inline void
+gtk_source_vim_state_release (GtkSourceVimState **dest)
+{
+       if (*dest != NULL)
+       {
+               gtk_source_vim_state_unparent (*dest);
+               g_clear_object (dest);
+       }
+}
+
+static inline void
+gtk_source_vim_state_reparent (GtkSourceVimState  *state,
+                               GtkSourceVimState  *parent,
+                               GtkSourceVimState **dest)
+{
+       if (*dest == state)
+               return;
+
+       g_object_ref (parent);
+       g_object_ref (state);
+
+       gtk_source_vim_state_release (dest);
+       gtk_source_vim_state_set_parent (state, parent);
+
+       *dest = state;
+       g_object_unref (parent);
+}
+
+#define gtk_source_vim_state_reparent(a,b,c)                   \
+       (gtk_source_vim_state_reparent)((GtkSourceVimState*)a, \
+                                       (GtkSourceVimState*)b, \
+                                       (GtkSourceVimState**)c)
+
+#define gtk_source_vim_state_release(a) \
+       (gtk_source_vim_state_release)((GtkSourceVimState**)a)
+
+static inline gboolean
+gtk_source_vim_state_is_escape (guint           keyval,
+                                GdkModifierType mods)
+{
+       return keyval == GDK_KEY_Escape ||
+              (keyval == GDK_KEY_bracketleft && (mods & GDK_CONTROL_MASK) != 0);
+}
+
+static inline gboolean
+gtk_source_vim_state_is_ctrl_c (guint           keyval,
+                                GdkModifierType mods)
+{
+       return keyval == GDK_KEY_c && (mods & GDK_CONTROL_MASK) != 0;
+}
+
+G_END_DECLS
diff --git a/gtksourceview/vim/gtksourcevimtexthistory.c b/gtksourceview/vim/gtksourcevimtexthistory.c
new file mode 100644
index 00000000..3a546f6c
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimtexthistory.c
@@ -0,0 +1,344 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "gtksourcebuffer.h"
+
+#include "gtksourcevimregisters.h"
+#include "gtksourcevimtexthistory.h"
+
+typedef enum
+{
+       OP_INSERT,
+       OP_DELETE,
+       OP_BACKSPACE,
+} OpKind;
+
+typedef struct
+{
+       OpKind kind : 2;
+       guint length : 30;
+       guint offset;
+} Op;
+
+struct _GtkSourceVimTextHistory
+{
+       GObject      parent_instance;
+       GArray      *ops;
+       GString     *bytes;
+       int          cursor_position;
+};
+
+G_DEFINE_TYPE (GtkSourceVimTextHistory, gtk_source_vim_text_history, GTK_SOURCE_TYPE_VIM_STATE)
+
+GtkSourceVimState *
+gtk_source_vim_text_history_new (void)
+{
+       return g_object_new (GTK_SOURCE_TYPE_VIM_TEXT_HISTORY, NULL);
+}
+
+static void
+gtk_source_vim_text_history_truncate (GtkSourceVimTextHistory *self)
+{
+       g_assert (GTK_SOURCE_IS_VIM_TEXT_HISTORY (self));
+
+       g_string_truncate (self->bytes, 0);
+
+       if (self->ops->len > 0)
+       {
+               g_array_remove_range (self->ops, 0, self->ops->len);
+       }
+}
+
+static void
+gtk_source_vim_text_history_insert_text_cb (GtkSourceVimTextHistory *self,
+                                            const GtkTextIter       *iter,
+                                            const char              *text,
+                                            int                      len,
+                                            GtkSourceBuffer         *buffer)
+{
+       guint position;
+       Op op;
+
+       g_assert (GTK_SOURCE_IS_VIM_TEXT_HISTORY (self));
+       g_assert (GTK_SOURCE_IS_BUFFER (buffer));
+       g_assert (iter != NULL);
+       g_assert (gtk_text_iter_get_buffer (iter) == GTK_TEXT_BUFFER (buffer));
+       g_assert (text != NULL);
+
+       if (len == 0)
+               return;
+
+       position = gtk_text_iter_get_offset (iter);
+
+       if ((int)position != self->cursor_position)
+       {
+               gtk_source_vim_text_history_truncate (self);
+       }
+
+       op.kind = OP_INSERT;
+       op.length = g_utf8_strlen (text, len);
+       op.offset = self->bytes->len;
+
+       g_string_append_len (self->bytes, text, len);
+       g_array_append_val (self->ops, op);
+
+       self->cursor_position = position + op.length;
+}
+
+static void
+gtk_source_vim_text_history_delete_range_cb (GtkSourceVimTextHistory *self,
+                                             const GtkTextIter       *begin,
+                                             const GtkTextIter       *end,
+                                             GtkSourceBuffer         *buffer)
+{
+       GtkTextIter a, b;
+       Op op;
+
+       g_assert (GTK_SOURCE_IS_VIM_TEXT_HISTORY (self));
+       g_assert (GTK_SOURCE_IS_BUFFER (buffer));
+       g_assert (begin != NULL);
+       g_assert (end != NULL);
+       g_assert (gtk_text_iter_get_buffer (begin) == gtk_text_iter_get_buffer (end));
+
+       if (gtk_text_iter_get_offset (begin) == gtk_text_iter_get_offset (end))
+               return;
+
+       a = *begin;
+       b = *end;
+       gtk_text_iter_order (&a, &b);
+
+       op.length = (int)gtk_text_iter_get_offset (&b) - (int)gtk_text_iter_get_offset (&a);
+       op.offset = 0;
+
+       if (gtk_text_iter_get_offset (&a) == self->cursor_position)
+       {
+               op.kind = OP_DELETE;
+               g_array_append_val (self->ops, op);
+       }
+       else if (gtk_text_iter_get_offset (&b) == self->cursor_position)
+       {
+               op.kind = OP_BACKSPACE;
+               g_array_append_val (self->ops, op);
+       }
+       else
+       {
+               gtk_source_vim_text_history_truncate (self);
+       }
+
+       self->cursor_position = gtk_text_iter_get_offset (&a);
+}
+
+static void
+gtk_source_vim_text_history_dispose (GObject *object)
+{
+       GtkSourceVimTextHistory *self = (GtkSourceVimTextHistory *)object;
+
+       g_clear_pointer (&self->ops, g_array_unref);
+
+       if (self->bytes)
+       {
+               g_string_free (self->bytes, TRUE);
+               self->bytes = NULL;
+       }
+
+       G_OBJECT_CLASS (gtk_source_vim_text_history_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_vim_text_history_class_init (GtkSourceVimTextHistoryClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->dispose = gtk_source_vim_text_history_dispose;
+}
+
+static void
+gtk_source_vim_text_history_init (GtkSourceVimTextHistory *self)
+{
+       self->bytes = g_string_new (NULL);
+       self->ops = g_array_new (FALSE, FALSE, sizeof (Op));
+}
+
+void
+gtk_source_vim_text_history_begin (GtkSourceVimTextHistory *self)
+{
+       GtkSourceBuffer *buffer;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_TEXT_HISTORY (self));
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), NULL, NULL);
+
+       g_signal_connect_object (buffer,
+                                "insert-text",
+                                G_CALLBACK (gtk_source_vim_text_history_insert_text_cb),
+                                self,
+                                G_CONNECT_SWAPPED);
+
+       g_signal_connect_object (buffer,
+                                "delete-range",
+                                G_CALLBACK (gtk_source_vim_text_history_delete_range_cb),
+                                self,
+                                G_CONNECT_SWAPPED);
+}
+
+/*
+ * string_truncate_n_chars:
+ * @str: the GString
+ * @n_chars: the number of chars to remove
+ *
+ * Removes @n_chars from the tail of @str, possibly taking into
+ * account UTF-8 characters that are multi-width.
+ */
+static void
+string_truncate_n_chars (GString *str,
+                         gsize    n_chars)
+{
+       if (str == NULL)
+       {
+               return;
+       }
+
+       if (n_chars >= str->len)
+       {
+               g_string_truncate (str, 0);
+               return;
+       }
+
+       g_assert (str->len > 0);
+
+       while (n_chars > 0 && str->len > 0)
+       {
+               guchar ch = str->str[--str->len];
+
+               /* If high bit is zero we have a one-byte char. If we have
+                * reached the byte with 0xC0 mask set then we are at the
+                * first character of a multi-byte char.
+                */
+               if ((ch & 0x80) == 0 || (ch & 0xC0) == 0xC0)
+               {
+                       n_chars--;
+               }
+       }
+
+       str->str[str->len] = 0;
+}
+
+void
+gtk_source_vim_text_history_end (GtkSourceVimTextHistory *self)
+{
+       GtkSourceVimState *registers;
+       GtkSourceBuffer *buffer;
+       GString *inserted;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_TEXT_HISTORY (self));
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), NULL, NULL);
+
+       g_signal_handlers_disconnect_by_func (buffer,
+                                             G_CALLBACK (gtk_source_vim_text_history_insert_text_cb),
+                                             self);
+       g_signal_handlers_disconnect_by_func (buffer,
+                                             G_CALLBACK (gtk_source_vim_text_history_delete_range_cb),
+                                             self);
+
+       /* Collect the inserted text into a single string and then set that
+        * in the "." register which is a read-only register to the user
+        * containing the last inserted text.
+        */
+       inserted = g_string_new (NULL);
+       for (guint i = 0; i < self->ops->len; i++)
+       {
+               const Op *op = &g_array_index (self->ops, Op, i);
+               const char *str = self->bytes->str + op->offset;
+
+               switch (op->kind)
+               {
+                       case OP_INSERT:
+                               g_string_append_len (inserted, str, g_utf8_offset_to_pointer (str, 
op->length) - str);
+                               break;
+
+                       case OP_BACKSPACE:
+                               string_truncate_n_chars (inserted, op->length);
+                               break;
+
+                       default:
+                       case OP_DELETE:
+                               break;
+               }
+       }
+
+       registers = gtk_source_vim_state_get_registers (GTK_SOURCE_VIM_STATE (self));
+       gtk_source_vim_registers_set (GTK_SOURCE_VIM_REGISTERS (registers), ".", inserted->str);
+       g_string_free (inserted, TRUE);
+}
+
+void
+gtk_source_vim_text_history_replay (GtkSourceVimTextHistory *self)
+{
+       GtkSourceBuffer *buffer;
+       GtkTextIter iter;
+       GtkTextIter end;
+       const char *str;
+       int len;
+
+       g_return_if_fail (GTK_SOURCE_IS_VIM_TEXT_HISTORY (self));
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), &iter, NULL);
+
+       for (guint i = 0; i < self->ops->len; i++)
+       {
+               const Op *op = &g_array_index (self->ops, Op, i);
+
+               switch (op->kind)
+               {
+                       case OP_INSERT:
+                               str = self->bytes->str + op->offset;
+                               len = g_utf8_offset_to_pointer (str, op->length) - str;
+                               gtk_text_buffer_insert (GTK_TEXT_BUFFER (buffer), &iter, str, len);
+                               break;
+
+                       case OP_DELETE:
+                               end = iter;
+                               gtk_text_iter_forward_chars (&end, op->length);
+                               gtk_text_buffer_delete (GTK_TEXT_BUFFER (buffer), &iter, &end);
+                               break;
+
+                       case OP_BACKSPACE:
+                               end = iter;
+                               gtk_text_iter_backward_chars (&end, op->length);
+                               gtk_text_buffer_delete (GTK_TEXT_BUFFER (buffer), &iter, &end);
+                               break;
+
+                       default:
+                               g_assert_not_reached ();
+               }
+       }
+}
+
+gboolean
+gtk_source_vim_text_history_is_empty (GtkSourceVimTextHistory *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_TEXT_HISTORY (self), FALSE);
+
+       return self->ops->len == 0;
+}
diff --git a/gtksourceview/vim/gtksourcevimtexthistory.h b/gtksourceview/vim/gtksourcevimtexthistory.h
new file mode 100644
index 00000000..3b8b05fe
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimtexthistory.h
@@ -0,0 +1,38 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include "gtksourcevimstate.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_VIM_TEXT_HISTORY (gtk_source_vim_text_history_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceVimTextHistory, gtk_source_vim_text_history, GTK_SOURCE, VIM_TEXT_HISTORY, 
GtkSourceVimState)
+
+GtkSourceVimState *gtk_source_vim_text_history_new      (void);
+void               gtk_source_vim_text_history_replay   (GtkSourceVimTextHistory *self);
+void               gtk_source_vim_text_history_begin    (GtkSourceVimTextHistory *self);
+void               gtk_source_vim_text_history_end      (GtkSourceVimTextHistory *self);
+gboolean           gtk_source_vim_text_history_is_empty (GtkSourceVimTextHistory *self);
+
+G_END_DECLS
diff --git a/gtksourceview/vim/gtksourcevimtextobject.c b/gtksourceview/vim/gtksourcevimtextobject.c
new file mode 100644
index 00000000..ced14a25
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimtextobject.c
@@ -0,0 +1,557 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "gtksourcevimmotion.h"
+#include "gtksourcevimtextobject.h"
+
+typedef gboolean (*TextObjectCheck)  (const GtkTextIter *iter);
+typedef gboolean (*TextObjectMotion) (GtkTextIter       *iter);
+typedef gboolean (*TextObjectExtend) (const GtkTextIter *origin,
+                                      GtkTextIter       *inner_begin,
+                                      GtkTextIter       *inner_end,
+                                      GtkTextIter       *a_begin,
+                                      GtkTextIter       *a_end,
+                                      guint              mode);
+
+enum {
+       TEXT_OBJECT_INNER,
+       TEXT_OBJECT_A,
+};
+
+struct _GtkSourceVimTextObject
+{
+       GtkSourceVimState parent_instance;
+
+       TextObjectCheck   ends;
+       TextObjectCheck   starts;
+       TextObjectMotion  forward_end;
+       TextObjectMotion  backward_start;
+       TextObjectExtend  extend;
+
+       guint             inner_or_a : 1;
+       guint             is_linewise : 1;
+};
+
+G_DEFINE_TYPE (GtkSourceVimTextObject, gtk_source_vim_text_object, GTK_SOURCE_TYPE_VIM_STATE)
+
+static gboolean
+gtk_source_vim_iter_always_false (const GtkTextIter *iter)
+{
+       return FALSE;
+}
+
+#define DEFINE_ITER_CHECK(name, char)                 \
+static gboolean                                       \
+gtk_source_vim_iter_##name (const GtkTextIter *iter)  \
+{                                                     \
+       return gtk_text_iter_get_char (iter) == char; \
+}
+DEFINE_ITER_CHECK (starts_paren, '(')
+DEFINE_ITER_CHECK (ends_paren, ')')
+DEFINE_ITER_CHECK (starts_brace, '{')
+DEFINE_ITER_CHECK (ends_brace, '}')
+DEFINE_ITER_CHECK (starts_bracket, '[')
+DEFINE_ITER_CHECK (ends_bracket, ']')
+DEFINE_ITER_CHECK (starts_lt_gt, '<')
+DEFINE_ITER_CHECK (ends_lt_gt, '>')
+#undef DEFINE_ITER_CHECK
+
+static inline gboolean
+iter_isspace (const GtkTextIter *iter)
+{
+       return g_unichar_isspace (gtk_text_iter_get_char (iter));
+}
+
+static inline gboolean
+is_empty_line (const GtkTextIter *iter)
+{
+       return gtk_text_iter_starts_line (iter) && gtk_text_iter_ends_line (iter);
+}
+
+static inline gboolean
+can_trail_sentence (const GtkTextIter *iter)
+{
+       switch (gtk_text_iter_get_char (iter))
+       {
+       case '.': case '!': case '?':
+       case ']': case ')': case '"': case '\'':
+               return TRUE;
+
+       default:
+               return FALSE;
+       }
+}
+
+static inline gboolean
+is_end_sentence_char (const GtkTextIter *iter)
+{
+       switch (gtk_text_iter_get_char (iter))
+       {
+       case '.': case '!': case '?':
+               return TRUE;
+
+       default:
+               return FALSE;
+       }
+}
+
+static gboolean
+gtk_source_vim_iter_ends_sentence (const GtkTextIter *iter)
+{
+       GtkTextIter next, cur;
+
+       if (!can_trail_sentence (iter))
+       {
+               return FALSE;
+       }
+
+       next = *iter;
+       if (gtk_text_iter_forward_char (&next) &&
+           !gtk_text_iter_ends_line (&next) &&
+           !iter_isspace (&next))
+       {
+               return FALSE;
+       }
+
+       cur = *iter;
+       while (!is_end_sentence_char (&cur) && can_trail_sentence (&cur))
+       {
+               gtk_text_iter_backward_char (&cur);
+       }
+
+       return is_end_sentence_char (&cur);
+}
+
+static gboolean
+gtk_source_vim_iter__forward_sentence_end (GtkTextIter *iter)
+{
+       if (gtk_text_iter_is_end (iter) || !gtk_text_iter_forward_char (iter))
+       {
+               return FALSE;
+       }
+
+       do
+       {
+               if (is_empty_line (iter))
+                       return TRUE;
+
+               if (is_end_sentence_char (iter))
+               {
+                       GtkTextIter next = *iter;
+
+                       while (gtk_text_iter_forward_char (&next))
+                       {
+                               if (!can_trail_sentence (&next))
+                                       break;
+                               *iter = next;
+                       }
+
+                       return TRUE;
+               }
+       } while (gtk_text_iter_forward_char (iter));
+
+       return FALSE;
+}
+
+static gboolean
+gtk_source_vim_iter_is_paragraph_break (const GtkTextIter *iter)
+{
+       return gtk_text_iter_is_end (iter) || is_empty_line (iter);
+}
+
+static gboolean
+gtk_source_vim_iter__backward_paragraph_start (GtkTextIter *iter)
+{
+       while (!is_empty_line (iter))
+       {
+               if (gtk_text_iter_is_start (iter))
+                       return TRUE;
+
+               gtk_text_iter_backward_line (iter);
+
+               if (is_empty_line (iter))
+               {
+                       gtk_text_iter_forward_char (iter);
+                       break;
+               }
+       }
+
+       return TRUE;
+}
+
+static gboolean
+gtk_source_vim_iter__forward_paragraph_end (GtkTextIter *iter)
+{
+       gtk_text_iter_forward_char (iter);
+
+       while (!is_empty_line (iter))
+       {
+               if (gtk_text_iter_is_end (iter))
+                       return TRUE;
+
+               gtk_text_iter_forward_line (iter);
+
+               if (is_empty_line (iter))
+               {
+                       /* Place at the end of the previous non-empty line */
+                       gtk_text_iter_backward_char (iter);
+                       return TRUE;
+               }
+       }
+
+       return TRUE;
+}
+
+static void
+backward_to_first_space (GtkTextIter *iter)
+{
+       while (!gtk_text_iter_starts_line (iter))
+       {
+               gtk_text_iter_backward_char (iter);
+
+               if (!iter_isspace (iter))
+               {
+                       gtk_text_iter_forward_char (iter);
+                       return;
+               }
+       }
+}
+
+static void
+forward_to_nonspace (GtkTextIter *iter)
+{
+       while (!gtk_text_iter_ends_line (iter))
+       {
+               if (!iter_isspace (iter))
+               {
+                       break;
+               }
+
+               gtk_text_iter_forward_char (iter);
+       }
+}
+
+static gboolean
+text_object_extend_word (const GtkTextIter *origin,
+                         GtkTextIter       *inner_begin,
+                         GtkTextIter       *inner_end,
+                         GtkTextIter       *a_begin,
+                         GtkTextIter       *a_end,
+                         guint              mode)
+{
+       if (!gtk_text_iter_ends_line (inner_end))
+       {
+               gtk_text_iter_forward_char (inner_end);
+       }
+
+       if (gtk_text_iter_compare (origin, inner_begin) < 0)
+       {
+               *a_begin = *inner_begin;
+               *a_end = *inner_end;
+               backward_to_first_space (a_begin);
+               *inner_end = *inner_begin;
+               *inner_begin = *a_begin;
+       }
+       else
+       {
+               *a_begin = *inner_begin;
+               *a_end = *inner_end;
+               forward_to_nonspace (a_end);
+       }
+
+       return TRUE;
+}
+
+static gboolean
+text_object_extend_one (const GtkTextIter *origin,
+                        GtkTextIter       *inner_begin,
+                        GtkTextIter       *inner_end,
+                        GtkTextIter       *a_begin,
+                        GtkTextIter       *a_end,
+                        guint              mode)
+{
+       *a_begin = *inner_begin;
+       gtk_text_iter_forward_char (inner_begin);
+
+       *a_end = *inner_end;
+       gtk_text_iter_forward_char (a_end);
+
+       return TRUE;
+}
+
+static gboolean
+text_object_extend_paragraph (const GtkTextIter *origin,
+                              GtkTextIter       *inner_begin,
+                              GtkTextIter       *inner_end,
+                              GtkTextIter       *a_begin,
+                              GtkTextIter       *a_end,
+                              guint              mode)
+{
+       GtkTextIter next;
+       gboolean started_on_empty;
+
+       started_on_empty = is_empty_line (inner_begin);
+
+       if (is_empty_line (a_begin))
+       {
+               GtkTextIter prev = *a_begin;
+
+               while (gtk_text_iter_backward_line (&prev) ||
+                      gtk_text_iter_is_start (&prev))
+               {
+                       if (!is_empty_line (&prev))
+                       {
+                               gtk_text_iter_forward_to_line_end (&prev);
+                               gtk_text_iter_forward_char (&prev);
+                               *a_begin = prev;
+                               break;
+                       }
+                       else if (gtk_text_iter_is_start (&prev))
+                       {
+                               *a_begin = prev;
+                               break;
+                       }
+               }
+       }
+
+       next = *a_end;
+
+       while (gtk_text_iter_forward_line (&next) ||
+              gtk_text_iter_is_end (&next))
+       {
+               if (!is_empty_line (&next))
+                       break;
+
+               *a_end = next;
+
+               if (gtk_text_iter_is_end (&next))
+                       break;
+       }
+
+       if (started_on_empty)
+       {
+               *inner_begin = *a_begin;
+               *inner_end = *a_end;
+
+               /* If the original position is empty, then `ap` should
+                * place @a_end at the end of the next found paragraph.
+                */
+               next = *a_end;
+               gtk_text_iter_forward_line (&next);
+               while (!is_empty_line (&next) && !gtk_text_iter_is_end (&next))
+                       gtk_text_iter_forward_line (&next);
+               if (gtk_text_iter_compare (&next, a_end) > 0)
+                       gtk_text_iter_backward_char (&next);
+               *a_end = next;
+       }
+
+       /* If we didn't actually advance, then we failed to find
+        * a paragraph and we should fail the extension to match
+        * what VIM does. (Test with `cap` at position 0 w/ "\n\n").
+        */
+       if (mode == TEXT_OBJECT_A &&
+           started_on_empty &&
+           gtk_text_iter_equal (a_end, inner_end))
+       {
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+static gboolean
+text_object_extend_sentence (const GtkTextIter *origin,
+                             GtkTextIter       *inner_begin,
+                             GtkTextIter       *inner_end,
+                             GtkTextIter       *a_begin,
+                             GtkTextIter       *a_end,
+                             guint              mode)
+{
+       if (gtk_text_iter_starts_line (inner_begin) &&
+           gtk_text_iter_ends_line (inner_begin))
+       {
+               /* swallow up to next none empty line */
+               while (is_empty_line (a_end))
+                       gtk_text_iter_forward_line (a_end);
+       }
+       else if (!gtk_text_iter_ends_line (inner_end))
+       {
+               /* swallow trailing character */
+               gtk_text_iter_forward_char (inner_end);
+
+               *a_end = *inner_end;
+
+               /* swallow up to next sentence for a */
+               while (!gtk_text_iter_ends_line (a_end) && iter_isspace (a_end))
+               {
+                       gtk_text_iter_forward_char (a_end);
+               }
+       }
+
+       return TRUE;
+}
+
+static GtkSourceVimState *
+gtk_source_vim_text_object_new (TextObjectCheck  ends,
+                                TextObjectCheck  starts,
+                                TextObjectMotion forward_end,
+                                TextObjectMotion backward_start,
+                                TextObjectExtend extend,
+                                guint            inner_or_a,
+                                gboolean         is_linewise)
+{
+       GtkSourceVimTextObject *self;
+
+       self = g_object_new (GTK_SOURCE_TYPE_VIM_TEXT_OBJECT, NULL);
+       self->ends = ends;
+       self->starts = starts;
+       self->forward_end = forward_end;
+       self->backward_start = backward_start;
+       self->extend = extend;
+       self->inner_or_a = inner_or_a;
+       self->is_linewise = !!is_linewise;
+
+       return GTK_SOURCE_VIM_STATE (self);
+}
+
+#define TEXT_OBJECT_CTOR(name, ends, starts, forward, backward, extend, inner_or_a, linewise) \
+GtkSourceVimState *                                                                           \
+gtk_source_vim_text_object_new_##name (void)                                                  \
+{                                                                                             \
+       return gtk_source_vim_text_object_new (gtk_source_vim_iter_##ends,                    \
+                                              gtk_source_vim_iter_##starts,                  \
+                                              gtk_source_vim_iter_##forward,                 \
+                                              gtk_source_vim_iter_##backward,                \
+                                              text_object_extend_##extend,                   \
+                                              TEXT_OBJECT_##inner_or_a,                      \
+                                              linewise);                                     \
+}
+
+TEXT_OBJECT_CTOR (inner_word, ends_word, starts_word, forward_word_end, backward_word_start, word, INNER, 
FALSE);
+TEXT_OBJECT_CTOR (inner_WORD, always_false, starts_WORD, forward_WORD_end, backward_WORD_start, word, INNER, 
FALSE);
+TEXT_OBJECT_CTOR (inner_sentence, ends_sentence, always_false, _forward_sentence_end, 
backward_sentence_start, sentence, INNER, FALSE);
+TEXT_OBJECT_CTOR (inner_paragraph, is_paragraph_break, is_paragraph_break, _forward_paragraph_end, 
_backward_paragraph_start, paragraph, INNER, TRUE);
+TEXT_OBJECT_CTOR (inner_block_paren, ends_paren, starts_paren, forward_block_paren_end, 
backward_block_paren_start, one, INNER, FALSE);
+TEXT_OBJECT_CTOR (inner_block_brace, ends_brace, starts_brace, forward_block_brace_end, 
backward_block_brace_start, one, INNER, FALSE);
+TEXT_OBJECT_CTOR (inner_block_bracket, ends_bracket, starts_bracket, forward_block_bracket_end, 
backward_block_bracket_start, one, INNER, FALSE);
+TEXT_OBJECT_CTOR (inner_block_lt_gt, ends_lt_gt, starts_lt_gt, forward_block_lt_gt_end, 
backward_block_lt_gt_start, one, INNER, FALSE);
+TEXT_OBJECT_CTOR (inner_quote_double, ends_quote_double, always_false, forward_quote_double, 
backward_quote_double, one, INNER, FALSE);
+TEXT_OBJECT_CTOR (inner_quote_single, ends_quote_single, always_false, forward_quote_single, 
backward_quote_single, one, INNER, FALSE);
+TEXT_OBJECT_CTOR (inner_quote_grave, ends_quote_grave, always_false, forward_quote_grave, 
backward_quote_grave, one, INNER, FALSE);
+
+TEXT_OBJECT_CTOR (a_word, ends_word, starts_word, forward_word_end, backward_word_start, word, A, FALSE);
+TEXT_OBJECT_CTOR (a_WORD, ends_WORD, starts_WORD, forward_WORD_end, backward_WORD_start, word, A, FALSE);
+TEXT_OBJECT_CTOR (a_sentence, ends_sentence, always_false, _forward_sentence_end, backward_sentence_start, 
sentence, A, FALSE);
+TEXT_OBJECT_CTOR (a_paragraph, is_paragraph_break, is_paragraph_break, _forward_paragraph_end, 
_backward_paragraph_start, paragraph, A, TRUE);
+TEXT_OBJECT_CTOR (a_block_paren, ends_paren, starts_paren, forward_block_paren_end, 
backward_block_paren_start, one, A, FALSE);
+TEXT_OBJECT_CTOR (a_block_brace, ends_brace, starts_brace, forward_block_brace_end, 
backward_block_brace_start, one, A, FALSE);
+TEXT_OBJECT_CTOR (a_block_bracket, ends_bracket, starts_bracket, forward_block_bracket_end, 
backward_block_bracket_start, one, A, FALSE);
+TEXT_OBJECT_CTOR (a_block_lt_gt, ends_lt_gt, starts_lt_gt, forward_block_lt_gt_end, 
backward_block_lt_gt_start, one, A, FALSE);
+TEXT_OBJECT_CTOR (a_quote_double, ends_quote_double, always_false, forward_quote_double, 
backward_quote_double, one, A, FALSE);
+TEXT_OBJECT_CTOR (a_quote_single, ends_quote_single, always_false, forward_quote_single, 
backward_quote_single, one, A, FALSE);
+TEXT_OBJECT_CTOR (a_quote_grave, ends_quote_grave, always_false, forward_quote_grave, backward_quote_grave, 
one, A, FALSE);
+
+#undef TEXT_OBJECT_CTOR
+
+static void
+gtk_source_vim_text_object_dispose (GObject *object)
+{
+       G_OBJECT_CLASS (gtk_source_vim_text_object_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_vim_text_object_class_init (GtkSourceVimTextObjectClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->dispose = gtk_source_vim_text_object_dispose;
+}
+
+static void
+gtk_source_vim_text_object_init (GtkSourceVimTextObject *self)
+{
+}
+
+gboolean
+gtk_source_vim_text_object_select (GtkSourceVimTextObject *self,
+                                   GtkTextIter            *begin,
+                                   GtkTextIter            *end)
+{
+       GtkTextIter inner_begin;
+       GtkTextIter inner_end;
+       GtkTextIter a_begin;
+       GtkTextIter a_end;
+       int count;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_TEXT_OBJECT (self), FALSE);
+       g_return_val_if_fail (begin != NULL, FALSE);
+       g_return_val_if_fail (end != NULL, FALSE);
+       g_return_val_if_fail (GTK_IS_TEXT_BUFFER (gtk_text_iter_get_buffer (begin)), FALSE);
+       g_return_val_if_fail (self->forward_end != NULL, FALSE);
+       g_return_val_if_fail (self->backward_start != NULL, FALSE);
+       g_return_val_if_fail (self->extend != NULL, FALSE);
+
+       inner_end = *begin;
+       if (!self->ends (&inner_end) && !self->forward_end (&inner_end))
+               return FALSE;
+
+       inner_begin = inner_end;
+       if (!self->starts (&inner_begin) && !self->backward_start (&inner_begin))
+               return FALSE;
+
+       count = gtk_source_vim_state_get_count (GTK_SOURCE_VIM_STATE (self));
+       for (int i = 1; i < count; i++)
+       {
+               if (!self->forward_end (&inner_end))
+                       return FALSE;
+       }
+
+       a_begin = inner_begin;
+       a_end = inner_end;
+
+       if (!self->extend (begin, &inner_begin, &inner_end, &a_begin, &a_end, self->inner_or_a))
+       {
+               return FALSE;
+       }
+
+       if (self->inner_or_a == TEXT_OBJECT_INNER)
+       {
+               *begin = inner_begin;
+               *end = inner_end;
+       }
+       else
+       {
+               *begin = a_begin;
+               *end = a_end;
+       }
+
+       return TRUE;
+}
+
+gboolean
+gtk_source_vim_text_object_is_linewise (GtkSourceVimTextObject *self)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_TEXT_OBJECT (self), FALSE);
+
+       return self->is_linewise;
+}
diff --git a/gtksourceview/vim/gtksourcevimtextobject.h b/gtksourceview/vim/gtksourcevimtextobject.h
new file mode 100644
index 00000000..63408784
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimtextobject.h
@@ -0,0 +1,59 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include "gtksourcevimstate.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_VIM_TEXT_OBJECT (gtk_source_vim_text_object_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceVimTextObject, gtk_source_vim_text_object, GTK_SOURCE, VIM_TEXT_OBJECT, 
GtkSourceVimState)
+
+GtkSourceVimState *gtk_source_vim_text_object_new_inner_word          (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_inner_WORD          (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_inner_sentence      (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_inner_paragraph     (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_inner_block_paren   (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_inner_block_brace   (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_inner_block_bracket (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_inner_block_lt_gt   (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_inner_quote_double  (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_inner_quote_single  (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_inner_quote_grave   (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_a_word              (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_a_WORD              (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_a_sentence          (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_a_paragraph         (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_a_block_paren       (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_a_block_brace       (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_a_block_bracket     (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_a_block_lt_gt       (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_a_quote_double      (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_a_quote_single      (void);
+GtkSourceVimState *gtk_source_vim_text_object_new_a_quote_grave       (void);
+gboolean           gtk_source_vim_text_object_is_linewise             (GtkSourceVimTextObject *self);
+gboolean           gtk_source_vim_text_object_select                  (GtkSourceVimTextObject *self,
+                                                                       GtkTextIter            *begin,
+                                                                       GtkTextIter            *end);
+
+G_END_DECLS
diff --git a/gtksourceview/vim/gtksourcevimvisual.c b/gtksourceview/vim/gtksourcevimvisual.c
new file mode 100644
index 00000000..40f95ffb
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimvisual.c
@@ -0,0 +1,919 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "gtksourceview.h"
+
+#include "gtksourcevimcharpending.h"
+#include "gtksourcevimcommand.h"
+#include "gtksourcevimcommandbar.h"
+#include "gtksourceviminsert.h"
+#include "gtksourcevimmotion.h"
+#include "gtksourcevimreplace.h"
+#include "gtksourcevimvisual.h"
+
+typedef gboolean (*KeyHandler) (GtkSourceVimVisual *self,
+                                guint               keyval,
+                                guint               keycode,
+                                GdkModifierType     mods,
+                                const char         *string);
+
+struct _GtkSourceVimVisual
+{
+       GtkSourceVimState parent_class;
+
+       GtkSourceVimVisualMode mode;
+
+       /* A recording of motions so that we can replay commands
+        * such as delete and get a similar result to VIM. Replaying
+        * our motion's visual selection is not enough as after a
+        * delete it would be empty.
+        */
+       GtkSourceVimMotion *motion;
+
+       /* The operation to repeat. This may be a number of things such
+        * as a GtkSourceVimCommand, GtkSourceVimInsert, or GtkSourceVimDelete.
+        */
+       GtkSourceVimState *command;
+
+       KeyHandler handler;
+
+       GtkTextMark *started_at;
+       GtkTextMark *cursor;
+
+       int count;
+};
+
+typedef struct
+{
+       GtkTextBuffer *buffer;
+       GtkTextMark   *cursor;
+       GtkTextMark   *started_at;
+       int            cmp;
+       guint          line;
+       guint          line_offset;
+       guint          start_line;
+       guint          linewise : 1;
+} CursorInfo;
+
+static gboolean gtk_source_vim_visual_bail (GtkSourceVimVisual *self);
+static gboolean key_handler_initial        (GtkSourceVimVisual *self,
+                                            guint               keyval,
+                                            guint               keycode,
+                                            GdkModifierType     mods,
+                                            const char         *string);
+
+G_DEFINE_TYPE (GtkSourceVimVisual, gtk_source_vim_visual, GTK_SOURCE_TYPE_VIM_STATE)
+
+static void
+cursor_info_stash (GtkSourceVimVisual *self,
+                   CursorInfo         *info)
+{
+       GtkTextIter cursor;
+       GtkTextIter started_at;
+
+       g_assert (GTK_SOURCE_IS_VIM_VISUAL (self));
+
+       info->buffer = gtk_text_mark_get_buffer (self->cursor);
+       info->cursor = self->cursor;
+       info->started_at = self->started_at;
+
+       gtk_text_buffer_get_iter_at_mark (info->buffer, &cursor, self->cursor);
+       gtk_text_buffer_get_iter_at_mark (info->buffer, &started_at, self->started_at);
+
+       info->cmp = gtk_text_iter_compare (&cursor, &started_at);
+       info->line = gtk_text_iter_get_line (&cursor);
+       info->line_offset = gtk_text_iter_get_line_offset (&cursor);
+       info->start_line = MIN (gtk_text_iter_get_line (&started_at), info->line);
+       info->linewise = self->mode == GTK_SOURCE_VIM_VISUAL_LINE;
+}
+
+static void
+cursor_info_restore (CursorInfo *info)
+{
+       if (info->linewise)
+       {
+               if (info->cmp > 0)
+               {
+                       GtkTextIter iter;
+
+                       gtk_text_buffer_get_iter_at_line (info->buffer, &iter, info->start_line);
+                       gtk_text_buffer_select_range (info->buffer, &iter, &iter);
+               }
+               else
+               {
+                       GtkTextIter iter;
+
+                       gtk_text_buffer_get_iter_at_line_offset (info->buffer, &iter, info->line, 
info->line_offset);
+                       gtk_text_buffer_select_range (info->buffer, &iter, &iter);
+               }
+       }
+       else
+       {
+               GtkTextIter cursor, started_at;
+
+               gtk_text_buffer_get_iter_at_mark (info->buffer, &cursor, info->cursor);
+               gtk_text_buffer_get_iter_at_mark (info->buffer, &started_at, info->started_at);
+               gtk_text_iter_order (&cursor, &started_at);
+               gtk_text_buffer_select_range (info->buffer, &cursor, &cursor);
+       }
+}
+
+static void
+track_visible_column (GtkSourceVimVisual *self)
+{
+       GtkSourceBuffer *buffer;
+       GtkSourceView *view;
+       GtkTextIter iter;
+       guint visual_column;
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), NULL, NULL);
+       view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (self));
+       gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer),
+                                         &iter,
+                                         self->cursor);
+       visual_column = gtk_source_view_get_visual_column (view, &iter);
+       gtk_source_vim_state_set_visual_column (GTK_SOURCE_VIM_STATE (self), visual_column);
+}
+
+static void
+update_cursor_visible (GtkSourceVimVisual *self)
+{
+       GtkSourceVimState *child = gtk_source_vim_state_get_child (GTK_SOURCE_VIM_STATE (self));
+       gboolean is_line = self->mode == GTK_SOURCE_VIM_VISUAL_LINE;
+
+       gtk_text_mark_set_visible (self->cursor, child == NULL && is_line);
+}
+
+static void
+gtk_source_vim_visual_clear (GtkSourceVimVisual *self)
+{
+       self->handler = key_handler_initial;
+       self->count = 0;
+}
+
+static gboolean
+gtk_source_vim_visual_bail (GtkSourceVimVisual *self)
+{
+       gtk_source_vim_visual_clear (self);
+       return TRUE;
+}
+
+static void
+gtk_source_vim_visual_track_char (GtkSourceVimVisual *self)
+{
+       GtkSourceBuffer *buffer;
+       GtkTextIter cursor;
+       GtkTextIter started_at;
+
+       g_assert (GTK_SOURCE_IS_VIM_VISUAL (self));
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), NULL, NULL);
+       gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), &cursor, self->cursor);
+       gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), &started_at, self->started_at);
+
+       if (gtk_text_iter_equal (&cursor, &started_at))
+       {
+               if (gtk_text_iter_starts_line (&cursor) && gtk_text_iter_ends_line (&cursor))
+               {
+                       /* Leave the selection empty, since we don't really
+                        * have a character to select (other than the newline
+                        * which isn't what VIM does.
+                        */
+               }
+               else if (gtk_text_iter_ends_line (&cursor))
+               {
+                       /* Some how ended up on the \n when we shouldn't. Maybe
+                        * a stray button press or something. Adjust now.
+                        */
+                       gtk_text_iter_backward_char (&started_at);
+               }
+               else
+               {
+                       gtk_text_iter_forward_char (&cursor);
+               }
+
+               gtk_source_vim_state_select (GTK_SOURCE_VIM_STATE (self), &cursor, &started_at);
+       }
+       else if (gtk_text_iter_compare (&started_at, &cursor) < 0)
+       {
+               /* Include the the character under the cursor */
+               if (!gtk_text_iter_ends_line (&cursor))
+               {
+                       gtk_text_iter_forward_char (&cursor);
+               }
+
+               gtk_source_vim_state_select (GTK_SOURCE_VIM_STATE (self), &cursor, &started_at);
+       }
+       else
+       {
+               /* We need to swap the started at so that it is one character
+                * above so that the starting character is still selected.
+                */
+               if (!gtk_text_iter_ends_line (&started_at))
+               {
+                       gtk_text_iter_forward_char (&started_at);
+               }
+
+               gtk_source_vim_state_select (GTK_SOURCE_VIM_STATE (self), &cursor, &started_at);
+       }
+}
+
+static void
+gtk_source_vim_visual_track_line (GtkSourceVimVisual *self)
+{
+       GtkSourceBuffer *buffer;
+       GtkTextIter cursor, started_at;
+
+       g_assert (GTK_SOURCE_IS_VIM_VISUAL (self));
+
+       buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), NULL, NULL);
+       gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), &cursor, self->cursor);
+       gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), &started_at, self->started_at);
+
+       gtk_source_vim_state_select_linewise (GTK_SOURCE_VIM_STATE (self), &cursor, &started_at);
+}
+
+static void
+gtk_source_vim_visual_track_motion (GtkSourceVimVisual *self)
+{
+       GtkSourceView *view;
+
+       g_assert (GTK_SOURCE_IS_VIM_VISUAL (self));
+
+       switch (self->mode)
+       {
+               case GTK_SOURCE_VIM_VISUAL_LINE:
+                       gtk_source_vim_visual_track_line (self);
+                       break;
+
+               case GTK_SOURCE_VIM_VISUAL_CHAR:
+                       gtk_source_vim_visual_track_char (self);
+                       break;
+
+               case GTK_SOURCE_VIM_VISUAL_BLOCK:
+               default:
+                       break;
+       }
+
+       view = gtk_source_vim_state_get_view (GTK_SOURCE_VIM_STATE (self));
+       gtk_text_view_scroll_mark_onscreen (GTK_TEXT_VIEW (view), self->cursor);
+}
+
+static const char *
+gtk_source_vim_visual_get_command_bar_text (GtkSourceVimState *state)
+{
+       GtkSourceVimVisual *self = (GtkSourceVimVisual *)state;
+
+       g_assert (GTK_SOURCE_IS_VIM_VISUAL (self));
+
+       switch (self->mode)
+       {
+               case GTK_SOURCE_VIM_VISUAL_CHAR:
+                       return _("-- VISUAL --");
+
+               case GTK_SOURCE_VIM_VISUAL_LINE:
+                       return _("-- VISUAL LINE --");
+
+               case GTK_SOURCE_VIM_VISUAL_BLOCK:
+                       return _("-- VISUAL BLOCK --");
+
+               default:
+                       g_assert_not_reached ();
+                       return NULL;
+       }
+}
+
+static gboolean
+key_handler_z (GtkSourceVimVisual *self,
+              guint               keyval,
+              guint               keycode,
+              GdkModifierType     mods,
+              const char         *string)
+{
+       GtkSourceVimState *state = GTK_SOURCE_VIM_STATE (self);
+
+       switch (keyval)
+       {
+               case GDK_KEY_z:
+                       gtk_source_vim_state_z_scroll (state, 0.5);
+                       return TRUE;
+
+               case GDK_KEY_b:
+                       gtk_source_vim_state_z_scroll (state, 1.0);
+                       return TRUE;
+
+               case GDK_KEY_t:
+                       gtk_source_vim_state_z_scroll (state, 0.0);
+                       return TRUE;
+
+               default:
+                       return gtk_source_vim_visual_bail (self);
+       }
+}
+
+static gboolean
+key_handler_register (GtkSourceVimVisual *self,
+                     guint               keyval,
+                     guint               keycode,
+                     GdkModifierType     mods,
+                     const char         *string)
+{
+       g_assert (GTK_SOURCE_IS_VIM_VISUAL (self));
+
+       if (string == NULL || string[0] == 0)
+               return gtk_source_vim_visual_bail (self);
+
+       gtk_source_vim_state_set_current_register (GTK_SOURCE_VIM_STATE (self), string);
+
+       self->handler = key_handler_initial;
+
+       return TRUE;
+}
+
+static gboolean
+gtk_source_vim_visual_begin_command (GtkSourceVimVisual *self,
+                                     const char         *command,
+                                     gboolean            restore_cursor)
+{
+       CursorInfo info;
+       int count;
+
+       g_assert (GTK_SOURCE_IS_VIM_VISUAL (self));
+       g_assert (command != NULL);
+
+       count = self->count, self->count = 0;
+
+       gtk_source_vim_visual_clear (self);
+       gtk_source_vim_state_release (&self->command);
+
+       if (restore_cursor)
+               cursor_info_stash (self, &info);
+
+       self->command = gtk_source_vim_command_new (command);
+       gtk_source_vim_state_set_count (self->command, count);
+       gtk_source_vim_state_set_parent (self->command, GTK_SOURCE_VIM_STATE (self));
+       gtk_source_vim_state_repeat (self->command);
+
+       if (gtk_source_vim_state_get_can_repeat (self->command))
+               gtk_source_vim_state_set_can_repeat (GTK_SOURCE_VIM_STATE (self), TRUE);
+
+       if (restore_cursor)
+               cursor_info_restore (&info);
+
+       gtk_source_vim_state_pop (GTK_SOURCE_VIM_STATE (self));
+
+       return TRUE;
+}
+
+static gboolean
+gtk_source_vim_visual_try_motion (GtkSourceVimVisual *self,
+                                  guint               keyval,
+                                  guint               keycode,
+                                  GdkModifierType     mods,
+                                  const char         *str)
+{
+       GtkSourceVimState *motion;
+       int count;
+
+       g_assert (GTK_SOURCE_IS_VIM_VISUAL (self));
+
+       count = self->count, self->count = 0;
+
+       /* Try to apply a motion to our cursor */
+       motion = gtk_source_vim_motion_new ();
+       gtk_source_vim_state_set_count (motion, count);
+       gtk_source_vim_motion_set_mark (GTK_SOURCE_VIM_MOTION (motion), self->cursor);
+       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), motion);
+       gtk_source_vim_state_synthesize (motion, keyval, mods);
+
+       return TRUE;
+}
+
+static gboolean
+gtk_source_vim_visual_begin_insert (GtkSourceVimVisual *self)
+{
+       GtkSourceVimState *insert;
+       GtkSourceVimMotion *motion;
+
+       g_assert (GTK_SOURCE_IS_VIM_VISUAL (self));
+
+       motion = GTK_SOURCE_VIM_MOTION (gtk_source_vim_motion_new_none ());
+       insert = gtk_source_vim_insert_new ();
+
+       if (self->mode == GTK_SOURCE_VIM_VISUAL_LINE)
+       {
+               gtk_source_vim_insert_set_suffix (GTK_SOURCE_VIM_INSERT (insert), "\n");
+       }
+
+       gtk_source_vim_insert_set_at (GTK_SOURCE_VIM_INSERT (insert), GTK_SOURCE_VIM_INSERT_HERE);
+       gtk_source_vim_insert_set_motion (GTK_SOURCE_VIM_INSERT (insert), motion);
+       gtk_source_vim_insert_set_selection_motion (GTK_SOURCE_VIM_INSERT (insert), motion);
+       gtk_source_vim_state_set_can_repeat (GTK_SOURCE_VIM_STATE (self), TRUE);
+       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), insert);
+
+       gtk_source_vim_state_reparent (insert, self, &self->command);
+
+       g_object_unref (motion);
+
+       return TRUE;
+}
+
+static gboolean
+gtk_source_vim_visual_replace (GtkSourceVimVisual *self)
+{
+       g_assert (GTK_SOURCE_IS_VIM_VISUAL (self));
+
+       self->command = gtk_source_vim_command_new ("replace-one");
+
+       gtk_source_vim_state_set_can_repeat (GTK_SOURCE_VIM_STATE (self), TRUE);
+       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self),
+                                  g_object_ref (self->command));
+       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self->command),
+                                  gtk_source_vim_char_pending_new ());
+
+       gtk_source_vim_visual_clear (self);
+
+       return TRUE;
+}
+
+static gboolean
+key_handler_g (GtkSourceVimVisual *self,
+              guint               keyval,
+              guint               keycode,
+              GdkModifierType     mods,
+              const char         *string)
+{
+       GtkSourceVimState *new_state;
+
+       g_assert (GTK_SOURCE_IS_VIM_VISUAL (self));
+
+       switch (keyval)
+       {
+               case GDK_KEY_question:
+                       return gtk_source_vim_visual_begin_command (self, "rot13", TRUE);
+
+               case GDK_KEY_q:
+                       return gtk_source_vim_visual_begin_command (self, "format", FALSE);
+
+               default:
+                       new_state = gtk_source_vim_motion_new ();
+                       gtk_source_vim_motion_set_mark (GTK_SOURCE_VIM_MOTION (new_state), self->cursor);
+                       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), new_state);
+                       gtk_source_vim_state_synthesize (new_state, GDK_KEY_g, 0);
+                       gtk_source_vim_state_synthesize (new_state, keyval, mods);
+                       return TRUE;
+       }
+}
+
+static gboolean
+key_handler_initial (GtkSourceVimVisual *self,
+                     guint               keyval,
+                     guint               keycode,
+                     GdkModifierType     mods,
+                     const char         *string)
+{
+       g_assert (GTK_SOURCE_IS_VIM_VISUAL (self));
+
+       if ((mods & GDK_CONTROL_MASK) != 0)
+       {
+               switch (keyval)
+               {
+                       case GDK_KEY_y:
+                       case GDK_KEY_e:
+                       case GDK_KEY_b:
+                       case GDK_KEY_f:
+                       case GDK_KEY_u:
+                       case GDK_KEY_d:
+                               goto try_visual_motion;
+
+                       default:
+                               break;
+               }
+       }
+
+       if (self->count == 0)
+       {
+               switch (keyval)
+               {
+                       case GDK_KEY_0:
+                       case GDK_KEY_KP_0:
+                               goto try_visual_motion;
+
+                       default:
+                               break;
+               }
+       }
+
+       switch (keyval)
+       {
+               case GDK_KEY_0: case GDK_KEY_KP_0:
+               case GDK_KEY_1: case GDK_KEY_KP_1:
+               case GDK_KEY_2: case GDK_KEY_KP_2:
+               case GDK_KEY_3: case GDK_KEY_KP_3:
+               case GDK_KEY_4: case GDK_KEY_KP_4:
+               case GDK_KEY_5: case GDK_KEY_KP_5:
+               case GDK_KEY_6: case GDK_KEY_KP_6:
+               case GDK_KEY_7: case GDK_KEY_KP_7:
+               case GDK_KEY_8: case GDK_KEY_KP_8:
+               case GDK_KEY_9: case GDK_KEY_KP_9:
+                       self->count *= 10;
+                       if (keyval >= GDK_KEY_0 && keyval <= GDK_KEY_9)
+                               self->count += keyval - GDK_KEY_0;
+                       else if (keyval >= GDK_KEY_KP_0 && keyval <= GDK_KEY_KP_9)
+                               self->count += keyval - GDK_KEY_KP_0;
+                       return TRUE;
+
+               case GDK_KEY_z:
+                       self->handler = key_handler_z;
+                       return TRUE;
+
+               case GDK_KEY_d:
+               case GDK_KEY_x:
+                       return gtk_source_vim_visual_begin_command (self, ":delete", TRUE);
+
+               case GDK_KEY_quotedbl:
+                       self->handler = key_handler_register;
+                       return TRUE;
+
+               case GDK_KEY_y:
+                       return gtk_source_vim_visual_begin_command (self, ":yank", TRUE);
+
+               case GDK_KEY_v:
+                       self->mode = GTK_SOURCE_VIM_VISUAL_CHAR;
+                       gtk_source_vim_visual_track_motion (self);
+                       update_cursor_visible (self);
+                       return TRUE;
+
+               case GDK_KEY_V:
+                       self->mode = GTK_SOURCE_VIM_VISUAL_LINE;
+                       gtk_source_vim_visual_track_motion (self);
+                       update_cursor_visible (self);
+                       return TRUE;
+
+               case GDK_KEY_U:
+                       return gtk_source_vim_visual_begin_command (self, "upcase", TRUE);
+
+               case GDK_KEY_u:
+                       return gtk_source_vim_visual_begin_command (self, "downcase", TRUE);
+
+               case GDK_KEY_g:
+                       self->handler = key_handler_g;
+                       return TRUE;
+
+               case GDK_KEY_c:
+               case GDK_KEY_C:
+                       return gtk_source_vim_visual_begin_insert (self);
+
+               case GDK_KEY_r:
+                       return gtk_source_vim_visual_replace (self);
+
+               case GDK_KEY_greater:
+                       return gtk_source_vim_visual_begin_command (self, "indent", FALSE);
+
+               case GDK_KEY_less:
+                       return gtk_source_vim_visual_begin_command (self, "unindent", FALSE);
+
+               case GDK_KEY_colon:
+               {
+                       GtkSourceVimState *new_state = gtk_source_vim_command_bar_new ();
+                       gtk_source_vim_command_bar_set_text (GTK_SOURCE_VIM_COMMAND_BAR (new_state), 
":'<,'>");
+                       gtk_source_vim_state_push (GTK_SOURCE_VIM_STATE (self), new_state);
+                       return TRUE;
+               }
+
+               default:
+                       break;
+       }
+
+try_visual_motion:
+
+       return gtk_source_vim_visual_try_motion (self, keyval, keycode, mods, string);
+}
+
+static void
+gtk_source_vim_visual_enter (GtkSourceVimState *state)
+{
+       GtkSourceVimVisual *self = (GtkSourceVimVisual *)state;
+       GtkSourceBuffer *buffer;
+       GtkTextIter iter, selection;
+
+       g_assert (GTK_SOURCE_IS_VIM_VISUAL (self));
+
+       buffer = gtk_source_vim_state_get_buffer (state, &iter, &selection);
+
+       if (self->started_at == NULL)
+       {
+               self->started_at = gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (buffer), NULL, &iter, TRUE);
+               g_object_add_weak_pointer (G_OBJECT (self->started_at),
+                                          (gpointer *)&self->started_at);
+       }
+
+       if (self->cursor == NULL)
+       {
+               self->cursor = gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (buffer), NULL, &iter, FALSE);
+               g_object_add_weak_pointer (G_OBJECT (self->cursor),
+                                          (gpointer *)&self->cursor);
+       }
+
+       update_cursor_visible (self);
+
+       track_visible_column (self);
+
+       gtk_source_vim_visual_track_motion (self);
+}
+
+static void
+gtk_source_vim_visual_leave (GtkSourceVimState *state)
+{
+       GtkSourceVimVisual *self = (GtkSourceVimVisual *)state;
+       GtkSourceBuffer *buffer;
+       GtkTextIter iter;
+       GtkTextIter selection;
+
+       g_assert (GTK_SOURCE_IS_VIM_VISUAL (self));
+
+       buffer = gtk_source_vim_state_get_buffer (state, &iter, &selection);
+
+       if (gtk_text_buffer_get_has_selection (GTK_TEXT_BUFFER (buffer)))
+       {
+               gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer),
+                                                 &iter, self->cursor);
+
+               if (gtk_text_iter_ends_line (&iter) &&
+                   !gtk_text_iter_starts_line (&iter))
+               {
+                       gtk_text_iter_backward_char (&iter);
+               }
+
+               gtk_source_vim_state_select (state, &iter, &iter);
+       }
+
+       gtk_text_mark_set_visible (self->cursor, FALSE);
+}
+
+static void
+gtk_source_vim_visual_resume (GtkSourceVimState *state,
+                              GtkSourceVimState *from)
+{
+       GtkSourceVimVisual *self = (GtkSourceVimVisual *)state;
+
+       g_assert (GTK_SOURCE_IS_VIM_VISUAL (self));
+       g_assert (GTK_SOURCE_IS_VIM_STATE (from));
+
+       self->handler = key_handler_initial;
+
+       if (GTK_SOURCE_IS_VIM_MOTION (from))
+       {
+               GtkSourceVimState *chained;
+
+               if (gtk_source_vim_motion_invalidates_visual_column (GTK_SOURCE_VIM_MOTION (from)))
+               {
+                       track_visible_column (self);
+               }
+
+               /* Update our selection to match the motion. If we're in
+                * linewise, that needs to be updated to contain the whole line.
+                */
+               gtk_source_vim_visual_track_motion (self);
+
+               /* Keep the motion around too so we can potentially replay it
+                * for commands like delete, etc.
+                */
+               chained = gtk_source_vim_motion_chain (self->motion, GTK_SOURCE_VIM_MOTION (from));
+               gtk_source_vim_state_set_parent (chained, GTK_SOURCE_VIM_STATE (self));
+               gtk_source_vim_state_reparent (chained, self, &self->motion);
+               g_object_unref (chained);
+       }
+
+       update_cursor_visible (self);
+
+       if (GTK_SOURCE_IS_VIM_COMMAND_BAR (from))
+       {
+               GtkSourceVimState *command = gtk_source_vim_command_bar_take_command 
(GTK_SOURCE_VIM_COMMAND_BAR (from));
+
+               if (command != NULL)
+               {
+                       gtk_source_vim_state_reparent (command, self, &self->command);
+                       g_object_unref (command);
+               }
+
+               gtk_source_vim_state_unparent (from);
+               gtk_source_vim_state_pop (state);
+       }
+       else if (from == self->command)
+       {
+               gtk_source_vim_state_pop (state);
+       }
+       else if (!GTK_SOURCE_IS_VIM_MOTION (from))
+       {
+               gtk_source_vim_state_unparent (from);
+       }
+}
+
+static void
+gtk_source_vim_visual_suspend (GtkSourceVimState *state,
+                               GtkSourceVimState *to)
+{
+       GtkSourceVimVisual *self = (GtkSourceVimVisual *)state;
+
+       g_assert (GTK_SOURCE_IS_VIM_VISUAL (self));
+       g_assert (GTK_SOURCE_IS_VIM_STATE (to));
+
+       update_cursor_visible (self);
+}
+
+static void
+gtk_source_vim_visual_repeat (GtkSourceVimState *state)
+{
+       GtkSourceVimVisual *self = (GtkSourceVimVisual *)state;
+       GtkSourceBuffer *buffer;
+       GtkTextIter iter;
+       GtkTextIter selection;
+       int count;
+
+       g_assert (GTK_SOURCE_IS_VIM_STATE (self));
+
+       count = gtk_source_vim_state_get_count (state);
+       buffer = gtk_source_vim_state_get_buffer (state, &iter, &selection);
+
+       gtk_text_buffer_move_mark (GTK_TEXT_BUFFER (buffer), self->cursor, &iter);
+       gtk_text_buffer_move_mark (GTK_TEXT_BUFFER (buffer), self->started_at, &iter);
+
+       gtk_source_vim_visual_track_motion (self);
+
+       do
+       {
+               if (self->motion != NULL)
+               {
+                       gtk_source_vim_motion_set_mark (self->motion, self->cursor);
+                       gtk_source_vim_state_repeat (GTK_SOURCE_VIM_STATE (self->motion));
+                       gtk_source_vim_visual_track_motion (self);
+                       gtk_source_vim_motion_set_mark (self->motion, NULL);
+               }
+
+               if (self->command != NULL)
+               {
+                       gtk_source_vim_state_repeat (self->command);
+               }
+       } while (--count > 0);
+}
+
+static gboolean
+gtk_source_vim_visual_handle_keypress (GtkSourceVimState *state,
+                                       guint              keyval,
+                                       guint              keycode,
+                                       GdkModifierType    mods,
+                                       const char        *string)
+{
+       GtkSourceVimVisual *self = (GtkSourceVimVisual *)state;
+
+       g_assert (GTK_SOURCE_IS_VIM_VISUAL (state));
+
+       /* Leave insert mode if Escape/ctrl+[ was pressed */
+       if (gtk_source_vim_state_is_escape (keyval, mods))
+       {
+               gtk_source_vim_state_pop (GTK_SOURCE_VIM_STATE (self));
+               return TRUE;
+       }
+
+       return self->handler (self, keyval, keycode, mods, string);
+}
+
+static void
+gtk_source_vim_visual_dispose (GObject *object)
+{
+       GtkSourceVimVisual *self = (GtkSourceVimVisual *)object;
+
+       if (self->cursor)
+       {
+               GtkTextMark *mark = self->cursor;
+               GtkTextBuffer *buffer = gtk_text_mark_get_buffer (mark);
+
+               g_clear_weak_pointer (&self->cursor);
+               gtk_text_buffer_delete_mark (GTK_TEXT_BUFFER (buffer), mark);
+       }
+
+       if (self->started_at)
+       {
+               GtkTextMark *mark = self->started_at;
+               GtkTextBuffer *buffer = gtk_text_mark_get_buffer (mark);
+
+               g_clear_weak_pointer (&self->started_at);
+               gtk_text_buffer_delete_mark (GTK_TEXT_BUFFER (buffer), mark);
+       }
+
+       gtk_source_vim_state_release (&self->motion);
+       gtk_source_vim_state_release (&self->command);
+
+       G_OBJECT_CLASS (gtk_source_vim_visual_parent_class)->dispose (object);
+}
+
+static void
+gtk_source_vim_visual_class_init (GtkSourceVimVisualClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       GtkSourceVimStateClass *state_class = GTK_SOURCE_VIM_STATE_CLASS (klass);
+
+       object_class->dispose = gtk_source_vim_visual_dispose;
+
+       state_class->get_command_bar_text = gtk_source_vim_visual_get_command_bar_text;
+       state_class->handle_keypress = gtk_source_vim_visual_handle_keypress;
+       state_class->enter = gtk_source_vim_visual_enter;
+       state_class->leave = gtk_source_vim_visual_leave;
+       state_class->resume = gtk_source_vim_visual_resume;
+       state_class->suspend = gtk_source_vim_visual_suspend;
+       state_class->repeat = gtk_source_vim_visual_repeat;
+}
+
+static void
+gtk_source_vim_visual_init (GtkSourceVimVisual *self)
+{
+       self->handler = key_handler_initial;
+}
+
+GtkSourceVimState *
+gtk_source_vim_visual_new (GtkSourceVimVisualMode mode)
+{
+       GtkSourceVimVisual *self;
+
+       self = g_object_new (GTK_SOURCE_TYPE_VIM_VISUAL, NULL);
+       self->mode = mode;
+
+       return GTK_SOURCE_VIM_STATE (self);
+}
+
+gboolean
+gtk_source_vim_visual_get_bounds (GtkSourceVimVisual *self,
+                                  GtkTextIter        *cursor,
+                                  GtkTextIter        *started_at)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_VISUAL (self), FALSE);
+
+       if (cursor != NULL)
+       {
+               if (self->cursor == NULL)
+                       return FALSE;
+
+               gtk_text_buffer_get_iter_at_mark (gtk_text_mark_get_buffer (self->cursor),
+                                                 cursor, self->cursor);
+       }
+
+       if (started_at != NULL)
+       {
+               if (self->started_at == NULL)
+                       return FALSE;
+
+               gtk_text_buffer_get_iter_at_mark (gtk_text_mark_get_buffer (self->started_at),
+                                                 started_at, self->started_at);
+       }
+
+       return TRUE;
+}
+
+GtkSourceVimState *
+gtk_source_vim_visual_clone (GtkSourceVimVisual *self)
+{
+       GtkSourceVimState *ret;
+       GtkTextIter cursor;
+       GtkTextIter started_at;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_VIM_VISUAL (self), NULL);
+
+       ret = gtk_source_vim_visual_new (self->mode);
+
+       if (gtk_source_vim_visual_get_bounds (self, &cursor, &started_at))
+       {
+               GtkSourceBuffer *buffer = gtk_source_vim_state_get_buffer (GTK_SOURCE_VIM_STATE (self), NULL, 
NULL);
+               GtkTextMark *mark;
+
+               mark = gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (buffer), NULL, &cursor, FALSE);
+               g_set_weak_pointer (&GTK_SOURCE_VIM_VISUAL (ret)->cursor, mark);
+
+               mark = gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (buffer), NULL, &started_at, TRUE);
+               g_set_weak_pointer (&GTK_SOURCE_VIM_VISUAL (ret)->started_at, mark);
+       }
+
+       return ret;
+}
diff --git a/gtksourceview/vim/gtksourcevimvisual.h b/gtksourceview/vim/gtksourcevimvisual.h
new file mode 100644
index 00000000..986f6211
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimvisual.h
@@ -0,0 +1,45 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include "gtksourcevimstate.h"
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+       GTK_SOURCE_VIM_VISUAL_CHAR,
+       GTK_SOURCE_VIM_VISUAL_LINE,
+       GTK_SOURCE_VIM_VISUAL_BLOCK,
+} GtkSourceVimVisualMode;
+
+#define GTK_SOURCE_TYPE_VIM_VISUAL (gtk_source_vim_visual_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkSourceVimVisual, gtk_source_vim_visual, GTK_SOURCE, VIM_VISUAL, GtkSourceVimState)
+
+GtkSourceVimState *gtk_source_vim_visual_new        (GtkSourceVimVisualMode  mode);
+GtkSourceVimState *gtk_source_vim_visual_clone      (GtkSourceVimVisual     *self);
+gboolean           gtk_source_vim_visual_get_bounds (GtkSourceVimVisual     *self,
+                                                     GtkTextIter            *cursor,
+                                                     GtkTextIter            *started_at);
+
+G_END_DECLS
diff --git a/gtksourceview/vim/meson.build b/gtksourceview/vim/meson.build
new file mode 100644
index 00000000..355880d8
--- /dev/null
+++ b/gtksourceview/vim/meson.build
@@ -0,0 +1,18 @@
+vim_sources = files([
+  'gtksourcevim.c',
+  'gtksourcevimcharpending.c',
+  'gtksourcevimcommandbar.c',
+  'gtksourcevimcommand.c',
+  'gtksourceviminsert.c',
+  'gtksourceviminsertliteral.c',
+  'gtksourcevimjumplist.c',
+  'gtksourcevimmarks.c',
+  'gtksourcevimmotion.c',
+  'gtksourcevimnormal.c',
+  'gtksourcevimregisters.c',
+  'gtksourcevimreplace.c',
+  'gtksourcevimstate.c',
+  'gtksourcevimtexthistory.c',
+  'gtksourcevimtextobject.c',
+  'gtksourcevimvisual.c',
+])
diff --git a/po/POTFILES.in b/po/POTFILES.in
index d14231aa..1227adf1 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -41,3 +41,6 @@ gtksourceview/gtksourcetag.c
 gtksourceview/gtksourceutils.c
 gtksourceview/gtksourceview.c
 gtksourceview/implregex.c
+gtksourceview/vim/gtksourceviminsert.c
+gtksourceview/vim/gtksourcevimreplace.c
+gtksourceview/vim/gtksourcevimvisual.c
diff --git a/tests/meson.build b/tests/meson.build
index 61a85c1a..3473c84d 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -13,6 +13,7 @@ tests_sources = {
                        'load': ['test-load.c'],
                      'widget': ['test-widget.c'],
                     'preview': ['test-preview.c'],
+                        'vim': ['test-vim.c'],
 }
 
 tests_resources = {
diff --git a/tests/test-vim.c b/tests/test-vim.c
new file mode 100644
index 00000000..7d873e3d
--- /dev/null
+++ b/tests/test-vim.c
@@ -0,0 +1,206 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include <gtksourceview/gtksource.h>
+#include <gtksourceview/gtksourcevimimcontext-private.h>
+
+static GMainLoop *main_loop;
+static GString *sequence;
+
+static gboolean
+execute_command (GtkSourceVimIMContext *context,
+                 const char            *command)
+{
+       if (g_str_equal (command, ":q"))
+       {
+               g_main_loop_quit (main_loop);
+               return TRUE;
+       }
+
+       return FALSE;
+}
+
+static void
+load_cb (GObject      *object,
+         GAsyncResult *result,
+         gpointer      user_data)
+{
+       GtkTextBuffer *buffer = user_data;
+       GtkTextIter iter;
+
+       gtk_text_buffer_get_start_iter (buffer, &iter);
+       gtk_text_buffer_select_range (buffer, &iter, &iter);
+       gtk_text_buffer_set_enable_undo (buffer, TRUE);
+}
+
+static void
+open_file (GtkSourceBuffer *buffer,
+           GFile           *file)
+{
+       GtkSourceFileLoader *loader;
+       GtkSourceFile *sfile;
+
+       sfile = gtk_source_file_new ();
+       gtk_source_file_set_location (sfile, file);
+       loader = gtk_source_file_loader_new (buffer, sfile);
+
+       gtk_source_file_loader_load_async (loader,
+                                          G_PRIORITY_DEFAULT,
+                                          NULL, NULL, NULL, NULL, load_cb, buffer);
+
+       g_object_unref (sfile);
+       g_object_unref (loader);
+}
+
+static gboolean
+on_close_request (GtkWindow *window)
+{
+       g_main_loop_quit (main_loop);
+       return FALSE;
+}
+
+static void
+observe_key (GtkSourceVimIMContext *self,
+             const char            *str,
+             gboolean               reset_observer,
+             gpointer               data)
+{
+       GtkLabel *label = data;
+
+       if (reset_observer)
+               g_string_truncate (sequence, 0);
+
+       g_string_append (sequence, str);
+       gtk_label_set_label (label, sequence->str);
+}
+
+int
+main (int argc,
+      char *argv[])
+{
+       GtkWindow *window;
+       GtkSourceStyleSchemeManager *schemes;
+       GtkSourceLanguageManager *languages;
+       GtkScrolledWindow *scroller;
+       GtkSourceView *view;
+       GtkIMContext *im_context;
+       GtkEventController *key;
+       GtkSourceBuffer *buffer;
+       GtkLabel *command_bar;
+       GtkLabel *command;
+       GtkLabel *observe;
+       GtkBox *vbox;
+       GtkBox *box;
+       GFile *file;
+
+       gtk_init ();
+       gtk_source_init ();
+
+       sequence = g_string_new (NULL);
+       schemes = gtk_source_style_scheme_manager_get_default ();
+       languages = gtk_source_language_manager_get_default ();
+
+       main_loop = g_main_loop_new (NULL, FALSE);
+       window = g_object_new (GTK_TYPE_WINDOW,
+                              "default-width", 800,
+                              "default-height", 600,
+                              NULL);
+       scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW,
+                                "vexpand", TRUE,
+                                NULL);
+       buffer = gtk_source_buffer_new (NULL);
+       gtk_source_buffer_set_language (buffer, gtk_source_language_manager_get_language (languages, "c"));
+       gtk_source_buffer_set_style_scheme (buffer, gtk_source_style_scheme_manager_get_scheme (schemes, 
"Adwaita"));
+       view = g_object_new (GTK_SOURCE_TYPE_VIEW,
+                            "auto-indent", TRUE,
+                            "buffer", buffer,
+                            "monospace", TRUE,
+                            "show-line-numbers", TRUE,
+                            "top-margin", 6,
+                            "left-margin", 6,
+                            NULL);
+       vbox = g_object_new (GTK_TYPE_BOX,
+                            "orientation", GTK_ORIENTATION_VERTICAL,
+                            "vexpand", TRUE,
+                            NULL);
+       box = g_object_new (GTK_TYPE_BOX,
+                           "margin-start", 12,
+                           "margin-top", 6,
+                           "margin-bottom", 6,
+                           "margin-end", 12,
+                           "orientation", GTK_ORIENTATION_HORIZONTAL,
+                           "hexpand", TRUE,
+                           NULL);
+       command_bar = g_object_new (GTK_TYPE_LABEL,
+                                   "hexpand", TRUE,
+                                   "xalign", 0.0f,
+                                   "margin-top", 6,
+                                   "margin-bottom", 6,
+                                   "margin-end", 12,
+                                   NULL);
+       command = g_object_new (GTK_TYPE_LABEL,
+                               "xalign", 0.0f,
+                               "margin-top", 6,
+                               "margin-bottom", 6,
+                               "margin-end", 12,
+                               "width-chars", 8,
+                               NULL);
+       observe = g_object_new (GTK_TYPE_LABEL,
+                               "margin-start", 24,
+                               "width-chars", 12,
+                               "wrap", TRUE,
+                               "xalign", 1.0f,
+                               NULL);
+
+       gtk_window_set_child (window, GTK_WIDGET (vbox));
+       gtk_box_append (vbox, GTK_WIDGET (scroller));
+       gtk_box_append (vbox, GTK_WIDGET (box));
+       gtk_scrolled_window_set_child (scroller, GTK_WIDGET (view));
+       gtk_box_append (box, GTK_WIDGET (command_bar));
+       gtk_box_append (box, GTK_WIDGET (command));
+       gtk_box_append (box, GTK_WIDGET (observe));
+
+       im_context = gtk_source_vim_im_context_new ();
+       g_object_bind_property (im_context, "command-bar-text", command_bar, "label", G_BINDING_SYNC_CREATE);
+       g_object_bind_property (im_context, "command-text", command, "label", G_BINDING_SYNC_CREATE);
+       g_signal_connect (im_context, "execute-command", G_CALLBACK (execute_command), NULL);
+       _gtk_source_vim_im_context_add_observer (GTK_SOURCE_VIM_IM_CONTEXT (im_context), observe_key, 
observe, NULL);
+       gtk_im_context_set_client_widget (im_context, GTK_WIDGET (view));
+
+       key = gtk_event_controller_key_new ();
+       gtk_event_controller_key_set_im_context (GTK_EVENT_CONTROLLER_KEY (key), im_context);
+       gtk_event_controller_set_propagation_phase (key, GTK_PHASE_CAPTURE);
+       gtk_widget_add_controller (GTK_WIDGET (view), key);
+
+       g_signal_connect (window, "close-request", G_CALLBACK (on_close_request), NULL);
+       gtk_window_present (window);
+
+       file = g_file_new_for_path (TOP_SRCDIR "/gtksourceview/gtksourcebuffer.c");
+       open_file (buffer, file);
+
+       g_main_loop_run (main_loop);
+
+       gtk_source_finalize ();
+
+       return 0;
+}
diff --git a/tests/test-widget.c b/tests/test-widget.c
index 0f89d685..6ae4775e 100644
--- a/tests/test-widget.c
+++ b/tests/test-widget.c
@@ -41,12 +41,16 @@ struct _TestWidget
        GtkCheckButton *draw_spaces_checkbutton;
        GtkCheckButton *smart_backspace_checkbutton;
        GtkCheckButton *indent_width_checkbutton;
+       GtkCheckButton *vim_checkbutton;
        GtkSpinButton *indent_width_spinbutton;
        GtkLabel *cursor_position_info;
        GtkSourceStyleSchemeChooserButton *chooser_button;
+       GtkIMContext *vim_im_context;
        GtkComboBoxText *background_pattern;
        GtkWidget *top;
        GtkScrolledWindow *scrolledwindow1;
+       GtkEventController *vim_controller;
+       GtkLabel *command_bar;
 };
 
 struct _TestHoverProvider
@@ -986,6 +990,42 @@ enable_hover_toggled_cb (TestWidget     *self,
                gtk_source_hover_remove_provider (hover, test_hover_provider);
 }
 
+static void
+vim_checkbutton_toggled_cb (TestWidget     *self,
+                            GtkCheckButton *button)
+{
+       g_assert (TEST_IS_WIDGET (self));
+       g_assert (GTK_IS_CHECK_BUTTON (button));
+
+       if (gtk_check_button_get_active (button))
+       {
+               if (!self->vim_controller)
+               {
+                       self->vim_im_context = gtk_source_vim_im_context_new ();
+                       gtk_im_context_set_client_widget (self->vim_im_context,
+                                                         GTK_WIDGET (self->view));
+                       g_object_bind_property (self->vim_im_context, "command-bar-text", self->command_bar, 
"label", 0);
+
+                       self->vim_controller = gtk_event_controller_key_new ();
+                       gtk_event_controller_set_propagation_phase (self->vim_controller, GTK_PHASE_CAPTURE);
+                       gtk_event_controller_key_set_im_context (GTK_EVENT_CONTROLLER_KEY 
(self->vim_controller),
+                                                                GTK_IM_CONTEXT (self->vim_im_context));
+                       gtk_widget_add_controller (GTK_WIDGET (self->view),
+                                                  self->vim_controller);
+               }
+       }
+       else
+       {
+               if (self->vim_controller)
+               {
+                       g_clear_object (&self->vim_im_context);
+                       gtk_widget_remove_controller (GTK_WIDGET (self->view),
+                                                     self->vim_controller);
+                       self->vim_controller = NULL;
+               }
+       }
+}
+
 static void
 test_widget_dispose (GObject *object)
 {
@@ -1026,6 +1066,7 @@ test_widget_class_init (TestWidgetClass *klass)
        gtk_widget_class_bind_template_callback (widget_class, smart_home_end_changed_cb);
        gtk_widget_class_bind_template_callback (widget_class, enable_snippets_toggled_cb);
        gtk_widget_class_bind_template_callback (widget_class, enable_hover_toggled_cb);
+       gtk_widget_class_bind_template_callback (widget_class, vim_checkbutton_toggled_cb);
 
        gtk_widget_class_bind_template_child (widget_class, TestWidget, view);
        gtk_widget_class_bind_template_child (widget_class, TestWidget, map);
@@ -1039,8 +1080,10 @@ test_widget_class_init (TestWidgetClass *klass)
        gtk_widget_class_bind_template_child (widget_class, TestWidget, chooser_button);
        gtk_widget_class_bind_template_child (widget_class, TestWidget, background_pattern);
        gtk_widget_class_bind_template_child (widget_class, TestWidget, scrolledwindow1);
+       gtk_widget_class_bind_template_child (widget_class, TestWidget, vim_checkbutton);
+       gtk_widget_class_bind_template_child (widget_class, TestWidget, command_bar);
 
-  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_w, GDK_CONTROL_MASK, "window.close", NULL);
+       gtk_widget_class_add_binding_action (widget_class, GDK_KEY_w, GDK_CONTROL_MASK, "window.close", NULL);
 }
 
 static void
diff --git a/tests/test-widget.ui b/tests/test-widget.ui
index d6c28888..a86898c2 100644
--- a/tests/test-widget.ui
+++ b/tests/test-widget.ui
@@ -217,13 +217,23 @@
                 </layout>
               </object>
             </child>
+            <child>
+              <object class="GtkCheckButton" id="vim_checkbutton">
+                <property name="label">Vim Emulation</property>
+                <signal name="toggled" handler="vim_checkbutton_toggled_cb" object="TestWidget" 
swapped="yes"/>
+                <layout>
+                  <property name="row">13</property>
+                  <property name="column">0</property>
+                </layout>
+              </object>
+            </child>
             <child>
               <object class="GtkCheckButton" id="enable_snippets">
                 <property name="label">Enable snippets</property>
                 <property name="can-focus">1</property>
                 <signal name="toggled" handler="enable_snippets_toggled_cb" object="TestWidget" 
swapped="yes"/>
                 <layout>
-                  <property name="row">13</property>
+                  <property name="row">14</property>
                   <property name="column">0</property>
                 </layout>
               </object>
@@ -234,7 +244,7 @@
                 <property name="can-focus">1</property>
                 <signal name="toggled" handler="enable_hover_toggled_cb" object="TestWidget" swapped="yes"/>
                 <layout>
-                  <property name="row">14</property>
+                  <property name="row">15</property>
                   <property name="column">0</property>
                 </layout>
               </object>
@@ -479,12 +489,22 @@
           </object>
         </child>
         <child>
-          <object class="GtkLabel" id="cursor_position_info">
-            <property name="halign">start</property>
-            <property name="label">Cursor position info</property>
-            <attributes>
-              <attribute name="font-features" value="tnum"/>
-            </attributes>
+          <object class="GtkBox">
+            <child>
+              <object class="GtkLabel" id="command_bar">
+                <property name="halign">start</property>
+                <property name="hexpand">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="cursor_position_info">
+                <property name="halign">end</property>
+                <property name="label">Cursor position info</property>
+                <attributes>
+                  <attribute name="font-features" value="tnum"/>
+                </attributes>
+              </object>
+            </child>
             <layout>
               <property name="column">0</property>
               <property name="row">1</property>
diff --git a/testsuite/meson.build b/testsuite/meson.build
index efa4072a..2940639f 100644
--- a/testsuite/meson.build
+++ b/testsuite/meson.build
@@ -37,6 +37,9 @@ testsuite_sources = [
   ['test-syntax'],
   ['test-utils'],
   ['test-view'],
+  ['test-vim-input'],
+  ['test-vim-state'],
+  ['test-vim-text-object'],
 ]
 
 foreach test: testsuite_sources
diff --git a/testsuite/test-vim-input.c b/testsuite/test-vim-input.c
new file mode 100644
index 00000000..1a6b32a5
--- /dev/null
+++ b/testsuite/test-vim-input.c
@@ -0,0 +1,227 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include <gtksourceview/gtksource.h>
+#include <gtksourceview/vim/gtksourcevim.h>
+#include <gtksourceview/vim/gtksourcevimcommand.h>
+#include <gtksourceview/vim/gtksourceviminsert.h>
+#include <gtksourceview/vim/gtksourcevimnormal.h>
+#include <gtksourceview/vim/gtksourcevimstate.h>
+
+static void
+run_test (const char *text,
+          const char *input,
+          const char *expected)
+{
+       GtkSourceView *view = GTK_SOURCE_VIEW (g_object_ref_sink (gtk_source_view_new ()));
+       GtkSourceBuffer *buffer = GTK_SOURCE_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)));
+       GtkSourceVim *vim = gtk_source_vim_new (view);
+       GtkTextIter begin, end;
+       char *ret;
+
+       gtk_text_buffer_set_text (GTK_TEXT_BUFFER (buffer), text, -1);
+       gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (buffer), &begin, &end);
+       gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer), &begin, &begin);
+
+       for (const char *c = input; *c; c = g_utf8_next_char (c))
+       {
+               GtkSourceVimState *current = gtk_source_vim_state_get_current (GTK_SOURCE_VIM_STATE (vim));
+               gunichar ch = g_utf8_get_char (c);
+               char string[16] = {0};
+               GdkModifierType mods = 0;
+               guint keyval;
+
+               /* It would be nice to send GdkEvent, but we have to rely on
+                * the fact that our engine knows key-presses pretty much
+                * everywhere so that we can send keypresses based on chars.
+                */
+               string[g_unichar_to_utf8 (ch, string)] = 0;
+
+               if (ch == '\e')
+               {
+                       string[0] = '^';
+                       string[1] = '[';
+                       string[2] = 0;
+                       keyval = GDK_KEY_Escape;
+               }
+               else if (ch == '\n')
+               {
+                       string[0] = '\n';
+                       string[1] = 0;
+                       keyval = GDK_KEY_Return;
+               }
+               else
+               {
+                       keyval = gdk_unicode_to_keyval (ch);
+               }
+
+               if (!GTK_SOURCE_VIM_STATE_GET_CLASS (current)->handle_keypress (current, keyval, 0, mods, 
string))
+               {
+                       gtk_text_buffer_insert_at_cursor (GTK_TEXT_BUFFER (buffer), string, -1);
+               }
+       }
+
+       gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (buffer), &begin, &end);
+       ret = gtk_text_iter_get_slice (&begin, &end);
+       g_assert_cmpstr (ret, ==, expected);
+       g_free (ret);
+
+       g_object_unref (vim);
+       g_object_unref (view);
+}
+
+static void
+test_yank (void)
+{
+       run_test ("1\n2\n3", "yGP", "1\n2\n3\n1\n2\n3");
+       run_test ("1\n2\n3", "yGp", "1\n1\n2\n3\n2\n3");
+       run_test ("1\n2\n3", "\"zyGP", "1\n2\n3");
+       run_test ("1\n2\n3", "\"zyG\"zP", "1\n2\n3\n1\n2\n3");
+}
+
+static void
+test_insert (void)
+{
+       run_test ("line1", "o\e", "line1\n");
+       run_test ("line1", "O\e", "\nline1");
+       run_test ("", "itesting\ea this.\e", "testing this.");
+       run_test ("", "3iz\e", "zzz");
+}
+
+static void
+test_change (void)
+{
+       run_test ("word here", "ciwnot\e", "not here");
+}
+
+static void
+test_delete (void)
+{
+       run_test ("a word here.", "v$x", "");
+       run_test ("t\nt\n", "Vx", "t\n");
+       run_test ("a word here.", "vex", " here.");
+       run_test ("line1", "dd", "");
+       run_test ("line1\n", "dj", "");
+       run_test ("line1\n\n", "dj", "");
+       run_test ("1\n2\n", "d2j", "");
+       run_test ("1\n2\n", "d10j", "");
+       run_test ("1\n2\n3\n42", "vjjjx", "2");
+       run_test ("1\n2\n3\n42", "vjjjVx", "");
+       run_test ("1\n2\n3\n4", "dG", "");
+       run_test ("1\n2\n3\n42", "jmzjjd'z", "1\n");
+       run_test ("1\n2\n3\n4\n5", "4Gd1G", "5");
+       run_test ("1\n2\n3\n4\n5", ":4\nd1G", "5");
+
+#if 0
+       /* somehow VIM ignores \n before 4. */
+       run_test ("1\n22\n3\n4", "jlmzjjd`z", "1\n2\n4");
+#endif
+}
+
+static void
+test_search_and_replace (void)
+{
+       static const struct {
+               const char *command;
+               gboolean success;
+               const char *search;
+               const char *replace;
+               const char *options;
+       } parse_s_and_r[] = {
+               { "s/", TRUE, NULL, NULL, NULL },
+               { "s/a", TRUE, "a", NULL, NULL },
+               { "s/a/", TRUE, "a", NULL, NULL },
+               { "s/a/b", TRUE, "a", "b", NULL },
+               { "s/a/b/", TRUE, "a", "b", NULL },
+               { "s/a/b/c", TRUE, "a", "b", "c" },
+               { "s#a#b#c", TRUE, "a", "b", "c" },
+               { "s/^ \\//", TRUE, "^ /", NULL, NULL },
+               { "s/\\/\\/", TRUE, "//", NULL, NULL },
+               { "s/^$//gI", TRUE, "^$", "", "gI" },
+       };
+
+       for (guint i = 0; i < G_N_ELEMENTS (parse_s_and_r); i++)
+       {
+               const char *str = parse_s_and_r[i].command;
+               char *search = NULL;
+               char *replace = NULL;
+               char *options = NULL;
+               gboolean ret;
+
+               g_assert_true (*str == 's');
+
+               str++;
+               ret = gtk_source_vim_command_parse_search_and_replace (str, &search, &replace, &options);
+
+               if (!parse_s_and_r[i].success && ret)
+               {
+                       g_error ("expected %s to fail, but it succeeded",
+                                parse_s_and_r[i].command);
+               }
+               else if (parse_s_and_r[i].success && !ret)
+               {
+                       g_error ("expected %s to pass, but it failed",
+                                parse_s_and_r[i].command);
+               }
+
+               g_assert_cmpstr (search, ==, parse_s_and_r[i].search);
+               g_assert_cmpstr (replace, ==, parse_s_and_r[i].replace);
+               g_assert_cmpstr (options, ==, parse_s_and_r[i].options);
+
+               g_free (search);
+               g_free (replace);
+               g_free (options);
+       }
+
+       run_test ("test test test test", ":s/test\n", " test test test");
+       run_test ("test test test test", ":s/test/bar\n", "bar test test test");
+       run_test ("test test test test", ":s/test/bar/g\n", "bar bar bar bar");
+       run_test ("test test test test", ":s/TEST/bar/gi\n", "bar bar bar bar");
+       run_test ("test test test test", ":s/TEST/bar\n", "test test test test");
+       run_test ("t t t t\nt t t t\n", ":s/t/f\n", "f t t t\nt t t t\n");
+       run_test ("t t t t\nt t t t\n", ":%s/t/f\n", "f t t t\nf t t t\n");
+       run_test ("t t t t\nt t t t\n", ":%s/t/f/g\n", "f f f f\nf f f f\n");
+       run_test ("t t t t\nt t t t\n", ":.,$s/t/f\n", "f t t t\nf t t t\n");
+       run_test ("t t\nt t\nt t\n", ":.,+1s/t/f\n", "f t\nf t\nt t\n");
+       run_test ("t t t t\nt t t t\n", "V:s/t/f\n", "f t t t\nt t t t\n");
+       run_test ("/ / / /", ":s/\\//#/g\n", "# # # #");
+}
+
+int
+main (int argc,
+      char *argv[])
+{
+       int ret;
+
+       gtk_init ();
+       gtk_source_init ();
+       g_test_init (&argc, &argv, NULL);
+       g_test_add_func ("/GtkSourceView/vim-input/yank", test_yank);
+       g_test_add_func ("/GtkSourceView/vim-input/insert", test_insert);
+       g_test_add_func ("/GtkSourceView/vim-input/change", test_change);
+       g_test_add_func ("/GtkSourceView/vim-input/delete", test_delete);
+       g_test_add_func ("/GtkSourceView/vim-input/search-and-replace", test_search_and_replace);
+       ret = g_test_run ();
+       gtk_source_finalize ();
+       return ret;
+}
diff --git a/testsuite/test-vim-state.c b/testsuite/test-vim-state.c
new file mode 100644
index 00000000..026ed09a
--- /dev/null
+++ b/testsuite/test-vim-state.c
@@ -0,0 +1,74 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include <gtksourceview/gtksource.h>
+
+#include <gtksourceview/vim/gtksourcevim.h>
+#include <gtksourceview/vim/gtksourcevimcommand.h>
+#include <gtksourceview/vim/gtksourceviminsert.h>
+#include <gtksourceview/vim/gtksourcevimnormal.h>
+#include <gtksourceview/vim/gtksourcevimstate.h>
+
+static void
+test_parents (void)
+{
+       GtkWidget *view = gtk_source_view_new ();
+       GtkSourceVim *vim = gtk_source_vim_new (GTK_SOURCE_VIEW (view));
+       GtkSourceVimState *normal = gtk_source_vim_state_get_current (GTK_SOURCE_VIM_STATE (vim));
+       GtkSourceVimState *insert = gtk_source_vim_insert_new ();
+       GtkSourceVimState *command = gtk_source_vim_command_new (":join");
+
+       gtk_source_vim_state_push (normal, g_object_ref (insert));
+       g_assert_true (normal == gtk_source_vim_state_get_parent (insert));
+
+       gtk_source_vim_state_pop (insert);
+       g_assert_true (normal == gtk_source_vim_state_get_parent (insert));
+
+       gtk_source_vim_state_push (normal, g_object_ref (command));
+       gtk_source_vim_state_pop (command);
+
+       /* Now insert should be released */
+       g_assert_finalize_object (insert);
+
+       g_assert_true (GTK_SOURCE_IS_VIM_NORMAL (normal));
+       g_object_ref (normal);
+
+       g_assert_finalize_object (vim);
+       g_assert_finalize_object (normal);
+       g_assert_finalize_object (command);
+}
+
+int
+main (int argc,
+      char *argv[])
+{
+       int ret;
+
+       gtk_init ();
+       gtk_source_init ();
+       g_test_init (&argc, &argv, NULL);
+       g_test_add_func ("/GtkSourceView/vim-state/set-parent", test_parents);
+       ret = g_test_run ();
+       gtk_source_finalize ();
+       return ret;
+}
diff --git a/testsuite/test-vim-text-object.c b/testsuite/test-vim-text-object.c
new file mode 100644
index 00000000..6fe1f86f
--- /dev/null
+++ b/testsuite/test-vim-text-object.c
@@ -0,0 +1,248 @@
+/*
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include <gtksourceview/gtksource.h>
+#include <gtksourceview/vim/gtksourcevimtextobject.h>
+
+static void
+run_test (GtkSourceVimState *text_object,
+          const char        *text,
+          guint              position,
+          const char        *expect_selection)
+{
+       GtkSourceBuffer *buffer;
+       GtkTextIter begin, end;
+
+       g_assert (GTK_SOURCE_IS_VIM_TEXT_OBJECT (text_object));
+       g_assert (text != NULL);
+
+       buffer = gtk_source_buffer_new (NULL);
+       gtk_text_buffer_set_text (GTK_TEXT_BUFFER (buffer), text, -1);
+
+       gtk_text_buffer_get_iter_at_offset (GTK_TEXT_BUFFER (buffer), &begin, position);
+       end = begin;
+
+       if (!gtk_source_vim_text_object_select (GTK_SOURCE_VIM_TEXT_OBJECT (text_object), &begin, &end))
+       {
+               char *text_escape;
+               char *exp_escape;
+
+               if (expect_selection == NULL)
+                       goto cleanup;
+
+               text_escape = g_strescape (text, NULL);
+               exp_escape = g_strescape (expect_selection, NULL);
+
+               g_error ("Selection Failed: '%s' at position %u expected '%s'",
+                        text_escape, position, exp_escape);
+
+               g_free (text_escape);
+               g_free (exp_escape);
+       }
+
+       if (expect_selection == NULL)
+       {
+               char *out = gtk_text_iter_get_slice (&begin, &end);
+               char *escaped = g_strescape (out, NULL);
+               g_error ("Expected to fail selection but got '%s'", escaped);
+               g_free (escaped);
+               g_free (out);
+       }
+       else
+       {
+               char *selected_text = gtk_text_iter_get_slice (&begin, &end);
+               g_assert_cmpstr (selected_text, ==, expect_selection);
+               g_free (selected_text);
+       }
+
+cleanup:
+       g_clear_object (&buffer);
+
+       g_assert_finalize_object (text_object);
+}
+
+static void
+test_word (void)
+{
+       run_test (gtk_source_vim_text_object_new_inner_word (), "", 0, "");
+       run_test (gtk_source_vim_text_object_new_inner_word (), "this is some- text to modify\n", 8, "some");
+       run_test (gtk_source_vim_text_object_new_inner_word (), "something  here\n", 10, "  ");
+       run_test (gtk_source_vim_text_object_new_inner_word (), "something  here", 9, "  ");
+       run_test (gtk_source_vim_text_object_new_inner_word (), "a", 0, "a");
+       run_test (gtk_source_vim_text_object_new_inner_word (), "a b", 1, " ");
+       run_test (gtk_source_vim_text_object_new_inner_word (), "+ -", 1, " ");
+       run_test (gtk_source_vim_text_object_new_inner_word (), "z a", 2, "a");
+       run_test (gtk_source_vim_text_object_new_a_word (), "a b", 1, " b");
+       run_test (gtk_source_vim_text_object_new_a_word (), "+ -", 1, " -");
+       run_test (gtk_source_vim_text_object_new_a_word (), "a b", 2, "b");
+       run_test (gtk_source_vim_text_object_new_a_word (), "a b c", 2, "b ");
+       run_test (gtk_source_vim_text_object_new_inner_word (), "\n    \n\n", 2, "    ");
+       run_test (gtk_source_vim_text_object_new_a_word (), "\n    \n\n", 2, "    ");
+}
+
+static void
+test_WORD (void)
+{
+       run_test (gtk_source_vim_text_object_new_inner_WORD (), "this is some- text to modify\n", 8, "some-");
+       run_test (gtk_source_vim_text_object_new_inner_WORD (), "something  here\n", 10, "  ");
+       run_test (gtk_source_vim_text_object_new_inner_WORD (), "something  here", 9, "  ");
+       run_test (gtk_source_vim_text_object_new_inner_WORD (), "\n    \n\n", 2, "    ");
+       run_test (gtk_source_vim_text_object_new_a_WORD (), "\n    \n\n", 2, "    ");
+}
+
+static void
+test_block (void)
+{
+       run_test (gtk_source_vim_text_object_new_a_block_paren (), "this_is_a_function (some stuff\n  and 
some more)\ntrailing", 23, "(some stuff\n  and some more)");
+       run_test (gtk_source_vim_text_object_new_inner_block_paren (), "this_is_a_function (some stuff\n  and 
some more)\ntrailing", 23, "some stuff\n  and some more");
+       run_test (gtk_source_vim_text_object_new_inner_block_paren (), "(should not match\n", 5, NULL);
+       run_test (gtk_source_vim_text_object_new_inner_block_paren (), "(m)", 0, "m");
+       run_test (gtk_source_vim_text_object_new_inner_block_paren (), "(m)", 1, "m");
+       run_test (gtk_source_vim_text_object_new_inner_block_paren (), "(m)", 2, "m");
+       run_test (gtk_source_vim_text_object_new_inner_block_paren (), "(m)", 3, NULL);
+       run_test (gtk_source_vim_text_object_new_a_block_paren (), "(m)", 0, "(m)");
+       run_test (gtk_source_vim_text_object_new_a_block_paren (), "(m)", 1, "(m)");
+       run_test (gtk_source_vim_text_object_new_a_block_paren (), "(m)", 2, "(m)");
+       run_test (gtk_source_vim_text_object_new_inner_block_paren (), "(m)", 3, NULL);
+       run_test (gtk_source_vim_text_object_new_inner_block_paren (), "()", 2, NULL);
+       run_test (gtk_source_vim_text_object_new_inner_block_paren (), "()", 1, "");
+       run_test (gtk_source_vim_text_object_new_inner_block_paren (), "()", 0, "");
+       run_test (gtk_source_vim_text_object_new_a_block_paren (), "() ", 1, "()");
+       run_test (gtk_source_vim_text_object_new_a_block_paren (), "() ", 0, "()");
+       run_test (gtk_source_vim_text_object_new_a_block_lt_gt (), "<a></a>", 0, "<a>");
+       run_test (gtk_source_vim_text_object_new_inner_block_lt_gt (), "<a>", 0, "a");
+       run_test (gtk_source_vim_text_object_new_inner_block_lt_gt (), "<a>", 2, "a");
+       run_test (gtk_source_vim_text_object_new_inner_block_lt_gt (), "<a></a>", 0, "a");
+       run_test (gtk_source_vim_text_object_new_inner_block_lt_gt (), "<a></a>", 1, "a");
+       run_test (gtk_source_vim_text_object_new_inner_block_lt_gt (), "<a></a>", 2, "a");
+       run_test (gtk_source_vim_text_object_new_inner_block_lt_gt (), "<a></a>", 3, "/a");
+
+       run_test (gtk_source_vim_text_object_new_inner_block_bracket (), "[a[b[c]]]", 0, "a[b[c]]");
+       run_test (gtk_source_vim_text_object_new_inner_block_bracket (), "[a[b[c]]]", 1, "a[b[c]]");
+       run_test (gtk_source_vim_text_object_new_inner_block_bracket (), "[a[b[c]]]", 2, "b[c]");
+       run_test (gtk_source_vim_text_object_new_inner_block_bracket (), "[a[b[c]]]", 3, "b[c]");
+       run_test (gtk_source_vim_text_object_new_inner_block_bracket (), "[a[b[c]]]", 4, "c");
+       run_test (gtk_source_vim_text_object_new_inner_block_bracket (), "[a[b[c]]]", 5, "c");
+       run_test (gtk_source_vim_text_object_new_inner_block_bracket (), "[a[b[c]]]", 6, "c");
+       run_test (gtk_source_vim_text_object_new_inner_block_bracket (), "[a[b[c]]]", 7, "b[c]");
+       run_test (gtk_source_vim_text_object_new_inner_block_bracket (), "[a[b[c]]]", 8, "a[b[c]]");
+       run_test (gtk_source_vim_text_object_new_inner_block_bracket (), "[a[b[c]]]", 9, NULL);
+}
+
+static void
+test_quote (void)
+{
+       run_test (gtk_source_vim_text_object_new_inner_quote_double (), "\"this is a string.\"", 0, "this is 
a string.");
+       run_test (gtk_source_vim_text_object_new_a_quote_double (), "\"this is a string.\"", 0, "\"this is a 
string.\"");
+       run_test (gtk_source_vim_text_object_new_inner_quote_double (), "\"this is a string.\n", 0, NULL);
+       run_test (gtk_source_vim_text_object_new_inner_quote_double (), "\"this \"is a string.\"", 6, "this 
");
+       run_test (gtk_source_vim_text_object_new_a_quote_double (), "\"this \"is a string.\"", 6, "\"this 
\"");
+       run_test (gtk_source_vim_text_object_new_inner_quote_double (), "\"this \"is a string.\"", 7, "is a 
string.");
+       run_test (gtk_source_vim_text_object_new_inner_quote_double (), "\"this \"is a string.", 7, NULL);
+       run_test (gtk_source_vim_text_object_new_inner_quote_double (), "\"\"", 0, "");
+       run_test (gtk_source_vim_text_object_new_inner_quote_double (), "\"\"", 1, "");
+       run_test (gtk_source_vim_text_object_new_inner_quote_double (), " \"\"", 2, "");
+       run_test (gtk_source_vim_text_object_new_inner_quote_double (), "\"\" ", 1, "");
+       run_test (gtk_source_vim_text_object_new_inner_quote_double (), "\"\" \"", 1, "");
+       run_test (gtk_source_vim_text_object_new_inner_quote_double (), "\"a\" \"", 1, "a");
+       run_test (gtk_source_vim_text_object_new_a_quote_double (), "\"\"", 0, "\"\"");
+       run_test (gtk_source_vim_text_object_new_a_quote_double (), "\"\"", 1, "\"\"");
+       run_test (gtk_source_vim_text_object_new_a_quote_double (), " \"\"", 2, "\"\"");
+       run_test (gtk_source_vim_text_object_new_a_quote_double (), "\"\" ", 1, "\"\"");
+       run_test (gtk_source_vim_text_object_new_a_quote_double (), "\"\" \"", 1, "\"\"");
+       run_test (gtk_source_vim_text_object_new_a_quote_double (), "\"a\"b\"", 2, "\"a\"");
+       run_test (gtk_source_vim_text_object_new_a_quote_double (), "\"a\"b\"", 3, "\"b\"");
+}
+
+static void
+test_sentence (void)
+{
+       run_test (gtk_source_vim_text_object_new_inner_sentence (), "a. b! c?", 0, "a.");
+       run_test (gtk_source_vim_text_object_new_inner_sentence (), "a. b! c?", 1, "a.");
+       run_test (gtk_source_vim_text_object_new_inner_sentence (), "a. b! c?", 2, "b!");
+       run_test (gtk_source_vim_text_object_new_inner_sentence (), "a. b! c?", 3, "b!");
+       run_test (gtk_source_vim_text_object_new_inner_sentence (), "a. b! c?", 4, "b!");
+       run_test (gtk_source_vim_text_object_new_inner_sentence (), "a. b! c?", 5, "c?");
+       run_test (gtk_source_vim_text_object_new_inner_sentence (), "a. b! c?", 6, "c?");
+       run_test (gtk_source_vim_text_object_new_inner_sentence (), "\n a. b! c?", 1, "a.");
+       run_test (gtk_source_vim_text_object_new_inner_sentence (), "\n a. b! c?", 2, "a.");
+
+       run_test (gtk_source_vim_text_object_new_a_sentence (), "a. b! c?", 0, "a. ");
+       run_test (gtk_source_vim_text_object_new_a_sentence (), " a. b! c?", 0, " a. ");
+       run_test (gtk_source_vim_text_object_new_a_sentence (), "\n a. b! c?", 1, "a. ");
+       run_test (gtk_source_vim_text_object_new_a_sentence (), "\n a. b! c?", 2, "a. ");
+}
+
+static void
+test_paragraph (void)
+{
+       GtkSourceVimState *temp;
+
+       run_test (gtk_source_vim_text_object_new_inner_paragraph (), "testing this.\n\n\n", 0, "testing 
this.");
+       run_test (gtk_source_vim_text_object_new_inner_paragraph (), "testing this.\n", 5, "testing this.");
+       run_test (gtk_source_vim_text_object_new_inner_paragraph (), "\n\n", 0, "\n\n");
+       run_test (gtk_source_vim_text_object_new_inner_paragraph (), "\n\n", 1, "\n\n");
+       run_test (gtk_source_vim_text_object_new_inner_paragraph (), "\n\n\n", 1, "\n\n\n");
+       run_test (gtk_source_vim_text_object_new_inner_paragraph (), "what\nwill\n we\n\nfind\nhere.", 1, 
"what\nwill\n we");
+       run_test (gtk_source_vim_text_object_new_inner_paragraph (), "\tword;\n\n\tanother;\n\n\tthird;\n", 
9, "\tanother;");
+       run_test (gtk_source_vim_text_object_new_inner_paragraph (), "\tword;\n\n\tanother;\n", 7, "");
+       run_test (gtk_source_vim_text_object_new_inner_paragraph (), "\t1\n\n\t2\n\n\t3", 8, "");
+       run_test (gtk_source_vim_text_object_new_inner_paragraph (), "\n", 0, "\n");
+       run_test (gtk_source_vim_text_object_new_inner_paragraph (), "\n\na\nb\nc\n", 0, "\n");
+
+       run_test (gtk_source_vim_text_object_new_a_paragraph (), "testing this.\n\n\n", 0, "testing 
this.\n\n\n");
+       run_test (gtk_source_vim_text_object_new_a_paragraph (), "testing this.\n", 5, "testing this.\n");
+       run_test (gtk_source_vim_text_object_new_a_paragraph (), "\n", 0, NULL);
+       run_test (gtk_source_vim_text_object_new_a_paragraph (), "\n\n", 0, NULL);
+       run_test (gtk_source_vim_text_object_new_a_paragraph (), "\n\n", 1, NULL);
+       run_test (gtk_source_vim_text_object_new_a_paragraph (), "\n\n\n", 1, NULL);
+       run_test (gtk_source_vim_text_object_new_a_paragraph (), "what\nwill\n we\n\nfind\nhere.", 1, 
"what\nwill\n we\n");
+       run_test (gtk_source_vim_text_object_new_a_paragraph (), "\tword;\n\n\tanother;\n\n\tthird;\n", 9, 
"\tanother;\n");
+       run_test (gtk_source_vim_text_object_new_a_paragraph (), "\tword;\n\n\tanother;\n", 7, 
"\n\tanother;");
+       run_test (gtk_source_vim_text_object_new_a_paragraph (), "\t1\n\n\t2\n\n\t3\n", 7, "\n\t3");
+       run_test (gtk_source_vim_text_object_new_a_paragraph (), "\t1\n\n\t2\n\n\t3\n", 8, "\t3\n");
+
+       temp = gtk_source_vim_text_object_new_inner_paragraph ();
+       gtk_source_vim_state_set_count (temp, 2);
+       run_test (temp, "t\n\nt", 0, "t\n");
+}
+
+int
+main (int argc,
+      char *argv[])
+{
+       int ret;
+
+       gtk_init ();
+       gtk_source_init ();
+       g_test_init (&argc, &argv, NULL);
+       g_test_add_func ("/GtkSourceView/vim-text-object/word", test_word);
+       g_test_add_func ("/GtkSourceView/vim-text-object/WORD", test_WORD);
+       g_test_add_func ("/GtkSourceView/vim-text-object/block", test_block);
+       g_test_add_func ("/GtkSourceView/vim-text-object/quote", test_quote);
+       g_test_add_func ("/GtkSourceView/vim-text-object/sentence", test_sentence);
+       g_test_add_func ("/GtkSourceView/vim-text-object/paragraph", test_paragraph);
+       ret = g_test_run ();
+       gtk_source_finalize ();
+       return ret;
+}


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