[gtksourceview/wip/chergert/vim: 4/4] vim: add GtkSourceVimIMContext
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gtksourceview/wip/chergert/vim: 4/4] vim: add GtkSourceVimIMContext
- Date: Fri, 12 Nov 2021 01:20:36 +0000 (UTC)
commit c485ccd39c6da1d38439d0d12a50e607877f5a1a
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.
docs/reference/gtksourceview-5.0-sections.txt | 19 +
docs/reference/gtksourceview-docs.xml.in | 1 +
gtksourceview/gtksource.h | 1 +
gtksourceview/gtksourcetypes.h | 1 +
gtksourceview/gtksourcevimimcontext-private.h | 38 +
gtksourceview/gtksourcevimimcontext.c | 593 ++++++
gtksourceview/gtksourcevimimcontext.h | 49 +
gtksourceview/meson.build | 5 +
gtksourceview/vim/gtksourcevim.c | 486 +++++
gtksourceview/vim/gtksourcevim.h | 48 +
gtksourceview/vim/gtksourcevimcharpending.c | 103 +
gtksourceview/vim/gtksourcevimcharpending.h | 36 +
gtksourceview/vim/gtksourcevimcommand.c | 1733 +++++++++++++++
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 | 2836 +++++++++++++++++++++++++
gtksourceview/vim/gtksourcevimmotion.h | 90 +
gtksourceview/vim/gtksourcevimnormal.c | 1475 +++++++++++++
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 | 1452 +++++++++++++
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 +
tests/meson.build | 1 +
tests/test-vim.c | 206 ++
testsuite/meson.build | 3 +
testsuite/test-vim-input.c | 227 ++
testsuite/test-vim-state.c | 74 +
testsuite/test-vim-text-object.c | 248 +++
47 files changed, 14179 insertions(+)
---
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/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..e4f3d8d2
--- /dev/null
+++ b/gtksourceview/gtksourcevimimcontext.c
@@ -0,0 +1,593 @@
+/*
+ * 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.
+ *
+ * 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 <path>".
+ *
+ * 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..5c6e44f0
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevim.c
@@ -0,0 +1,486 @@
+/*
+ * 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_finalize (GObject *object)
+{
+ GtkSourceVim *self = (GtkSourceVim *)object;
+
+ g_clear_handle_id (&self->constrain_insert_source, g_source_remove);
+ g_clear_object (&self->buffer);
+ g_string_free (self->command_text, TRUE);
+ self->command_text = 0;
+
+ G_OBJECT_CLASS (gtk_source_vim_parent_class)->finalize (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->finalize = gtk_source_vim_finalize;
+ 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;
+
+ 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..070e34c8
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimcharpending.c
@@ -0,0 +1,103 @@
+/*
+ * 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 "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..aa118327
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimcommand.c
@@ -0,0 +1,1733 @@
+/*
+ * 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;
+ 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 (g_hash_table_contains (commands, command_line))
+ {
+ ret = GTK_SOURCE_VIM_COMMAND (gtk_source_vim_command_new (command_line));
+ goto finish;
+ }
+
+ 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);
+ }
+
+ 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);
+
+ 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 = ¤t;
+
+ gtk_source_vim_jumplist_push (self, &before);
+
+ while (!ret && self->back.length > 0)
+ {
+ Jump *j = g_queue_peek_tail (&self->back);
+
+ if (!jump_equal (¤t, 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 = ¤t;
+
+ gtk_source_vim_jumplist_push (self, &before);
+
+ while (!ret && self->forward.length > 0)
+ {
+ Jump *j = g_queue_peek_head (&self->forward);
+
+ if (!jump_equal (¤t, 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..9f5dc8a8
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimmotion.c
@@ -0,0 +1,2836 @@
+/*
+ * 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 (¶, 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 (¶, iter) < 0)
+ {
+ int diff = (int)gtk_text_iter_get_offset (iter) - (int)gtk_text_iter_get_offset (¶);
+
+ if (diff < distance)
+ {
+ distance = diff;
+ winner = ¶
+ }
+ }
+
+ 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);
+ 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..3f4f3af7
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimnormal.c
@@ -0,0 +1,1475 @@
+/*
+ * 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);
+
+ 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..9d9c54cc
--- /dev/null
+++ b/gtksourceview/vim/gtksourcevimstate.c
@@ -0,0 +1,1452 @@
+/*
+ * 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);
+
+ priv->current_register = NULL;
+
+ g_clear_object (&priv->search_context);
+ g_clear_object (&priv->search_settings);
+
+ g_clear_weak_pointer (&priv->view);
+ 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_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 (>K_SOURCE_VIM_VISUAL (ret)->cursor, mark);
+
+ mark = gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (buffer), NULL, &started_at, TRUE);
+ g_set_weak_pointer (>K_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/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/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]