[gnome-builder: 36/139] keybindings: break up keybindings into plugins



commit 98f1f925ed20dd5df90d7f2d0cff986acdd59029
Author: Christian Hergert <chergert redhat com>
Date:   Wed Jan 9 16:18:17 2019 -0800

    keybindings: break up keybindings into plugins
    
    This splits the keybindings out into plugins to avoid having them directly
    in the libide core. It also ensures that external plugins can add new
    keybindings.

 src/libide/{keybindings => gui}/ide-keybindings.c  |   17 +-
 src/libide/{keybindings => gui}/ide-keybindings.h  |    0
 src/libide/keybindings/default.css                 |   60 -
 src/libide/keybindings/ide-shortcuts-window.ui     |  547 -------
 src/libide/keybindings/meson.build                 |    8 -
 src/libide/keybindings/shared.css                  |   24 -
 .../emacs/emacs-plugin.c}                          |   32 +-
 src/plugins/emacs/emacs.gresource.xml              |    7 +
 src/plugins/emacs/emacs.plugin                     |    9 +
 src/plugins/emacs/gbp-emacs-preferences-addin.c    |   89 ++
 src/plugins/emacs/gbp-emacs-preferences-addin.h    |   31 +
 .../emacs}/keybindings/emacs.css                   |   32 +-
 src/plugins/emacs/meson.build                      |   12 +
 .../sublime/gbp-sublime-preferences-addin.c        |   89 ++
 .../sublime/gbp-sublime-preferences-addin.h        |   31 +
 .../sublime}/keybindings/sublime.css               |   48 +-
 src/plugins/sublime/meson.build                    |   12 +
 src/plugins/sublime/sublime-plugin.c               |   36 +
 src/plugins/sublime/sublime.gresource.xml          |    7 +
 src/plugins/sublime/sublime.plugin                 |    9 +
 src/plugins/vim/gb-vim.c                           | 1661 ++++++++++++++++++++
 src/plugins/vim/gb-vim.h                           |   48 +
 src/plugins/vim/gbp-vim-command-provider.c         |  122 ++
 .../vim/gbp-vim-command-provider.h}                |   10 +-
 src/plugins/vim/gbp-vim-command.c                  |  141 ++
 src/plugins/vim/gbp-vim-command.h                  |   36 +
 src/plugins/vim/gbp-vim-preferences-addin.c        |   89 ++
 src/plugins/vim/gbp-vim-preferences-addin.h        |   31 +
 src/{libide => plugins/vim}/keybindings/vim.css    |   59 +-
 src/plugins/vim/meson.build                        |   15 +
 src/plugins/vim/vim-plugin.c                       |   41 +
 src/plugins/vim/vim.gresource.xml                  |    7 +
 src/plugins/vim/vim.plugin                         |    9 +
 33 files changed, 2631 insertions(+), 738 deletions(-)
---
diff --git a/src/libide/keybindings/ide-keybindings.c b/src/libide/gui/ide-keybindings.c
similarity index 95%
rename from src/libide/keybindings/ide-keybindings.c
rename to src/libide/gui/ide-keybindings.c
index 1ddd1c5d5..f97638150 100644
--- a/src/libide/keybindings/ide-keybindings.c
+++ b/src/libide/gui/ide-keybindings.c
@@ -25,10 +25,9 @@
 #include <dazzle.h>
 #include <glib/gi18n.h>
 #include <libpeas/peas.h>
+#include <libide-core.h>
 
-#include "ide-debug.h"
-
-#include "keybindings/ide-keybindings.h"
+#include "ide-keybindings.h"
 
 struct _IdeKeybindings
 {
@@ -78,8 +77,7 @@ ide_keybindings_load_plugin (IdeKeybindings *self,
     return;
 
   module_name = peas_plugin_info_get_module_name (plugin_info);
-  path = g_strdup_printf ("/org/gnome/builder/plugins/%s/keybindings/%s.css",
-                          module_name, self->mode);
+  path = g_strdup_printf ("/plugins/%s/keybindings/%s.css", module_name, self->mode);
   bytes = g_resources_lookup_data (path, 0, NULL);
   if (bytes == NULL)
     return;
@@ -143,6 +141,15 @@ ide_keybindings_reload (IdeKeybindings *self)
     path = g_strdup_printf ("/org/gnome/builder/keybindings/%s.css", self->mode);
     bytes = g_resources_lookup_data (path, G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
 
+    if (bytes == NULL)
+      {
+        g_clear_pointer (&path, g_free);
+        path = g_strdup_printf ("/plugins/%s/keybindings/%s.css", self->mode, self->mode);
+        bytes = g_resources_lookup_data (path, G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);
+        if (bytes != NULL)
+          g_clear_error (&error);
+      }
+
     if (error == NULL)
       {
         /*
diff --git a/src/libide/keybindings/ide-keybindings.h b/src/libide/gui/ide-keybindings.h
similarity index 100%
rename from src/libide/keybindings/ide-keybindings.h
rename to src/libide/gui/ide-keybindings.h
diff --git a/src/libide/keybindings/ide-shortcuts-window.c b/src/plugins/emacs/emacs-plugin.c
similarity index 51%
rename from src/libide/keybindings/ide-shortcuts-window.c
rename to src/plugins/emacs/emacs-plugin.c
index e11b96e84..fb60cc225 100644
--- a/src/libide/keybindings/ide-shortcuts-window.c
+++ b/src/plugins/emacs/emacs-plugin.c
@@ -1,6 +1,6 @@
-/* ide-shortcuts-window.c
+/* emacs-plugin.c
  *
- * Copyright 2015-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -18,27 +18,19 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#include <glib/gi18n.h>
+#define G_LOG_DOMAIN "emacs-plugin"
 
-#include "keybindings/ide-shortcuts-window.h"
+#include "config.h"
 
-struct _IdeShortcutsWindow
-{
-  GtkShortcutsWindow parent_instance;
-};
-
-G_DEFINE_TYPE (IdeShortcutsWindow, ide_shortcuts_window, GTK_TYPE_SHORTCUTS_WINDOW)
+#include <libpeas/peas.h>
+#include <libide-gui.h>
 
-static void
-ide_shortcuts_window_class_init (IdeShortcutsWindowClass *klass)
-{
-  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
-
-  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/ide-shortcuts-window.ui");
-}
+#include "gbp-emacs-preferences-addin.h"
 
-static void
-ide_shortcuts_window_init (IdeShortcutsWindow *self)
+_IDE_EXTERN void
+_gbp_emacs_register_types (PeasObjectModule *module)
 {
-  gtk_widget_init_template (GTK_WIDGET (self));
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_PREFERENCES_ADDIN,
+                                              GBP_TYPE_EMACS_PREFERENCES_ADDIN);
 }
diff --git a/src/plugins/emacs/emacs.gresource.xml b/src/plugins/emacs/emacs.gresource.xml
new file mode 100644
index 000000000..d269de173
--- /dev/null
+++ b/src/plugins/emacs/emacs.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/emacs">
+    <file>emacs.plugin</file>
+    <file>keybindings/emacs.css</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/emacs/emacs.plugin b/src/plugins/emacs/emacs.plugin
new file mode 100644
index 000000000..9e9a8147f
--- /dev/null
+++ b/src/plugins/emacs/emacs.plugin
@@ -0,0 +1,9 @@
+[Plugin]
+Authors=Christian Hergert <chergert redhat com>
+Builtin=true
+Copyright=Copyright © 2014-2018 Christian Hergert
+Description=Emulation of various Emacs features
+Embedded=_gbp_emacs_register_types
+Hidden=true
+Module=emacs
+Name=Emacs Emulation
diff --git a/src/plugins/emacs/gbp-emacs-preferences-addin.c b/src/plugins/emacs/gbp-emacs-preferences-addin.c
new file mode 100644
index 000000000..ac2222525
--- /dev/null
+++ b/src/plugins/emacs/gbp-emacs-preferences-addin.c
@@ -0,0 +1,89 @@
+/* gbp-emacs-preferences-addin.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-emacs-preferences-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-gui.h>
+
+#include "gbp-emacs-preferences-addin.h"
+
+struct _GbpEmacsPreferencesAddin
+{
+  GObject parent_instance;
+  guint   keybinding_id;
+};
+
+static void
+gbp_emacs_preferences_addin_load (IdePreferencesAddin *addin,
+                                  DzlPreferences      *preferences)
+{
+  GbpEmacsPreferencesAddin *self = (GbpEmacsPreferencesAddin *)addin;
+
+  g_assert (GBP_IS_EMACS_PREFERENCES_ADDIN (self));
+  g_assert (DZL_IS_PREFERENCES (preferences));
+
+  self->keybinding_id = dzl_preferences_add_radio (preferences,
+                                                   "keyboard",
+                                                   "mode",
+                                                   "org.gnome.builder.editor",
+                                                   "keybindings",
+                                                   NULL,
+                                                   "\"emacs\"",
+                                                   _("Emacs"),
+                                                   _("Emulates the Emacs text editor"),
+                                                   NULL,
+                                                   10);
+}
+
+static void
+gbp_emacs_preferences_addin_unload (IdePreferencesAddin *addin,
+                                    DzlPreferences      *preferences)
+{
+  GbpEmacsPreferencesAddin *self = (GbpEmacsPreferencesAddin *)addin;
+
+  g_assert (GBP_IS_EMACS_PREFERENCES_ADDIN (self));
+  g_assert (DZL_IS_PREFERENCES (preferences));
+
+  dzl_preferences_remove_id (preferences, self->keybinding_id);
+}
+
+static void
+preferences_addin_iface_init (IdePreferencesAddinInterface *iface)
+{
+  iface->load = gbp_emacs_preferences_addin_load;
+  iface->unload = gbp_emacs_preferences_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpEmacsPreferencesAddin, gbp_emacs_preferences_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_PREFERENCES_ADDIN,
+                                                preferences_addin_iface_init))
+
+static void
+gbp_emacs_preferences_addin_class_init (GbpEmacsPreferencesAddinClass *klass)
+{
+}
+
+static void
+gbp_emacs_preferences_addin_init (GbpEmacsPreferencesAddin *self)
+{
+}
diff --git a/src/plugins/emacs/gbp-emacs-preferences-addin.h b/src/plugins/emacs/gbp-emacs-preferences-addin.h
new file mode 100644
index 000000000..5fe2dca51
--- /dev/null
+++ b/src/plugins/emacs/gbp-emacs-preferences-addin.h
@@ -0,0 +1,31 @@
+/* gbp-emacs-preferences-addin.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_EMACS_PREFERENCES_ADDIN (gbp_emacs_preferences_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpEmacsPreferencesAddin, gbp_emacs_preferences_addin, GBP, EMACS_PREFERENCES_ADDIN, 
GObject)
+
+G_END_DECLS
diff --git a/src/libide/keybindings/emacs.css b/src/plugins/emacs/keybindings/emacs.css
similarity index 86%
rename from src/libide/keybindings/emacs.css
rename to src/plugins/emacs/keybindings/emacs.css
index 4696ba512..f5028fccb 100644
--- a/src/libide/keybindings/emacs.css
+++ b/src/plugins/emacs/keybindings/emacs.css
@@ -64,14 +64,14 @@
                             "remove-cursors" ()
                             "undo" () };
   bind "<alt>x" { "action" ("win", "show-command-bar", "") };
-  bind "<ctrl>r" { "action" ("editor-view", "find", "") };
-  bind "<ctrl>s" { "action" ("editor-view", "find", "") };
+  bind "<ctrl>r" { "action" ("editor-page", "find", "") };
+  bind "<ctrl>s" { "action" ("editor-page", "find", "") };
   bind "<alt>dollar" { "action" ("spellcheck", "spellcheck", "") };
   bind "<alt>period" { "goto-definition" () };
   bind "<alt>n" { "move-error" (down) };
   bind "<alt>p" { "move-error" (up) };
-  bind "<ctrl>j" { "action" ("layoutgrid", "focus-neighbor", "3") };
-  bind "<shift><ctrl>j" { "action" ("layoutstack", "split-view", "''") };
+  bind "<ctrl>j" { "action" ("grid", "focus-neighbor", "3") };
+  bind "<shift><ctrl>j" { "action" ("frame", "split-page", "''") };
   bind "F2" { "clear-selection" ()
               "movement" (previous-word-end, 0, 1, 1)
               "movement" (next-word-start, 0, 1, 0)
@@ -99,10 +99,10 @@
   bind "<alt>o" { "action" ("win", "find-other-file", "") };
 
   /* cycle "tabs" */
-  bind "<ctrl><alt>Page_Up" { "action" ("layoutstack", "previous-view", "") };
-  bind "<ctrl><alt>KP_Page_Up" { "action" ("layoutstack", "previous-view", "") };
-  bind "<ctrl><alt>Page_Down" { "action" ("layoutstack", "next-view", "") };
-  bind "<ctrl><alt>KP_Page_Down" { "action" ("layoutstack", "next-view", "") };
+  bind "<ctrl><alt>Page_Up" { "action" ("frame", "previous-page", "") };
+  bind "<ctrl><alt>KP_Page_Up" { "action" ("frame", "previous-page", "") };
+  bind "<ctrl><alt>Page_Down" { "action" ("frame", "next-page", "") };
+  bind "<ctrl><alt>KP_Page_Down" { "action" ("frame", "next-page", "") };
 
   bind "<alt>0" { "append-to-count" (0) };
   bind "<alt>1" { "append-to-count" (1) };
@@ -129,20 +129,20 @@
 @binding-set builder-emacs-source-view-x
 {
   bind "<ctrl>c" { "action" ("app", "quit", "") };
-  bind "0" { "action" ("layoutstack", "close-view", "") };
-  bind "k" { "action" ("layoutstack", "close-view", "") };
+  bind "0" { "action" ("frame", "close-page", "") };
+  bind "k" { "action" ("frame", "close-page", "") };
   bind "<ctrl>f" { "action" ("win", "open-with-dialog", "") };
-  bind "<ctrl>s" { "action" ("editor-view", "save", "") };
+  bind "<ctrl>s" { "action" ("editor-page", "save", "") };
   bind "s" { "action" ("win", "save-all", "") };
-  bind "<ctrl>b" { "action" ("layoutstack", "show-list", "") };
-  bind "<ctrl>w" { "action" ("editor-view", "save-as", "") };
+  bind "<ctrl>b" { "action" ("frame", "show-list", "") };
+  bind "<ctrl>w" { "action" ("editor-page", "save-as", "") };
   bind "u" { "clear-count" ()
              "clear-selection" ()
              "remove-cursors" ()
              "redo" () };
-  bind "2" { "action" ("layoutstack", "split-view", "''") };
-  bind "3" { "action" ("layoutstack", "open-in-new-frame", "''") };
-  bind "o" { "action" ("layoutgrid", "focus-neighbor", "0") };
+  bind "2" { "action" ("frame", "split-page", "''") };
+  bind "3" { "action" ("frame", "open-in-new-frame", "''") };
+  bind "o" { "action" ("grid", "focus-neighbor", "0") };
   bind "grave" { "move-error" (down) };
   bind "h" { "select-all" (1) };
   bind "<ctrl>space" { "action" ("history", "move-previous-edit", "") };
diff --git a/src/plugins/emacs/meson.build b/src/plugins/emacs/meson.build
new file mode 100644
index 000000000..a568a5666
--- /dev/null
+++ b/src/plugins/emacs/meson.build
@@ -0,0 +1,12 @@
+plugins_sources += files([
+  'emacs-plugin.c',
+  'gbp-emacs-preferences-addin.c',
+])
+
+plugin_emacs_resources = gnome.compile_resources(
+  'emacs-resources',
+  'emacs.gresource.xml',
+  c_name: 'gbp_emacs',
+)
+
+plugins_sources += plugin_emacs_resources[0]
diff --git a/src/plugins/sublime/gbp-sublime-preferences-addin.c 
b/src/plugins/sublime/gbp-sublime-preferences-addin.c
new file mode 100644
index 000000000..c99435ae7
--- /dev/null
+++ b/src/plugins/sublime/gbp-sublime-preferences-addin.c
@@ -0,0 +1,89 @@
+/* gbp-sublime-preferences-addin.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-sublime-preferences-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-gui.h>
+
+#include "gbp-sublime-preferences-addin.h"
+
+struct _GbpSublimePreferencesAddin
+{
+  GObject parent_instance;
+  guint   keybinding_id;
+};
+
+static void
+gbp_sublime_preferences_addin_load (IdePreferencesAddin *addin,
+                                    DzlPreferences      *preferences)
+{
+  GbpSublimePreferencesAddin *self = (GbpSublimePreferencesAddin *)addin;
+
+  g_assert (GBP_IS_SUBLIME_PREFERENCES_ADDIN (self));
+  g_assert (DZL_IS_PREFERENCES (preferences));
+
+  self->keybinding_id = dzl_preferences_add_radio (preferences,
+                                                   "keyboard",
+                                                   "mode",
+                                                   "org.gnome.builder.editor",
+                                                   "keybindings",
+                                                   NULL,
+                                                   "\"sublime\"",
+                                                   _("Sublime Text"),
+                                                   _("Emulates the Sublime Text editor"),
+                                                   NULL,
+                                                   20);
+}
+
+static void
+gbp_sublime_preferences_addin_unload (IdePreferencesAddin *addin,
+                                      DzlPreferences      *preferences)
+{
+  GbpSublimePreferencesAddin *self = (GbpSublimePreferencesAddin *)addin;
+
+  g_assert (GBP_IS_SUBLIME_PREFERENCES_ADDIN (self));
+  g_assert (DZL_IS_PREFERENCES (preferences));
+
+  dzl_preferences_remove_id (preferences, self->keybinding_id);
+}
+
+static void
+preferences_addin_iface_init (IdePreferencesAddinInterface *iface)
+{
+  iface->load = gbp_sublime_preferences_addin_load;
+  iface->unload = gbp_sublime_preferences_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpSublimePreferencesAddin, gbp_sublime_preferences_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_PREFERENCES_ADDIN,
+                                                preferences_addin_iface_init))
+
+static void
+gbp_sublime_preferences_addin_class_init (GbpSublimePreferencesAddinClass *klass)
+{
+}
+
+static void
+gbp_sublime_preferences_addin_init (GbpSublimePreferencesAddin *self)
+{
+}
diff --git a/src/plugins/sublime/gbp-sublime-preferences-addin.h 
b/src/plugins/sublime/gbp-sublime-preferences-addin.h
new file mode 100644
index 000000000..68dea4975
--- /dev/null
+++ b/src/plugins/sublime/gbp-sublime-preferences-addin.h
@@ -0,0 +1,31 @@
+/* gbp-sublime-preferences-addin.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_SUBLIME_PREFERENCES_ADDIN (gbp_sublime_preferences_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpSublimePreferencesAddin, gbp_sublime_preferences_addin, GBP, 
SUBLIME_PREFERENCES_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/libide/keybindings/sublime.css b/src/plugins/sublime/keybindings/sublime.css
similarity index 90%
rename from src/libide/keybindings/sublime.css
rename to src/plugins/sublime/keybindings/sublime.css
index 5cce05f55..dc33250ee 100644
--- a/src/libide/keybindings/sublime.css
+++ b/src/plugins/sublime/keybindings/sublime.css
@@ -4,9 +4,9 @@
 {
   bind "<ctrl>n" { "action" ("editor", "new-file", "") };
   bind "<ctrl>o" { "action" ("win", "open-with-dialog", "") };
-  bind "<ctrl>s" { "action" ("editor-view", "save", "") };
-  bind "<ctrl><shift>s" { "action" ("editor-view", "save-as", "") };
-  bind "<ctrl>w" { "action" ("layoutstack", "close-view", "") };
+  bind "<ctrl>s" { "action" ("editor-page", "save", "") };
+  bind "<ctrl><shift>s" { "action" ("editor-page", "save-as", "") };
+  bind "<ctrl>w" { "action" ("frame", "close-page", "") };
   bind "<ctrl>q" { "action" ("app", "quit", "") };
 
   bind "<ctrl>z" { "clear-count" ()
@@ -77,26 +77,26 @@
   bind "<alt><shift>m" { "movement" (match-special, 1, 0, 0) };
   bind "<ctrl><shift>a" { "select-tag" (1) };
 
-  bind "<ctrl>f" { "action" ("editor-view", "find", "") };
+  bind "<ctrl>f" { "action" ("editor-page", "find", "") };
   /* Make "Incremental search" and "Use selection for search" do the same thing
    * as Ctrl+F, in Builder they are all the same anyway */
-  bind "<ctrl>i" { "action" ("editor-view", "find", "") };
-  bind "<ctrl>e" { "action" ("editor-view", "find", "") };
-  bind "F3" { "action" ("editor-view", "move-next-search-result", "") };
-  bind "<shift>F3" { "action" ("editor-view", "move-previous-search-result", "") };
-  bind "<ctrl>h" { "action" ("editor-view", "find-replace", "") };
-  bind "<ctrl><shift>e" { "action" ("editor-view", "find-replace", "") };
+  bind "<ctrl>i" { "action" ("editor-page", "find", "") };
+  bind "<ctrl>e" { "action" ("editor-page", "find", "") };
+  bind "F3" { "action" ("editor-page", "move-next-search-result", "") };
+  bind "<shift>F3" { "action" ("editor-page", "move-previous-search-result", "") };
+  bind "<ctrl>h" { "action" ("editor-page", "find-replace", "") };
+  bind "<ctrl><shift>e" { "action" ("editor-page", "find-replace", "") };
 
   bind "<ctrl>quoteleft" { "action" ("dockbin", "bottom-visible", "") };
   bind "<shift>F11" { "action" ("win", "fullscreen", "") };
   bind "F6" { "action" ("spellcheck", "spellcheck", "") };
 
-  bind "<alt>exclam" { "action" ("layoutstack", "split-view", "''") };
-  bind "<alt><shift>KP_1" { "action" ("layoutstack", "split-view", "''") };
+  bind "<alt>exclam" { "action" ("frame", "split-page", "''") };
+  bind "<alt><shift>KP_1" { "action" ("frame", "split-page", "''") };
 
   bind "<ctrl>r" { "action" ("symbol-tree", "search", "") };
   bind "F12" { "goto-definition" () };
-  bind "<ctrl>g" { "action" ("editor-view", "goto-line", "") };
+  bind "<ctrl>g" { "action" ("editor-page", "goto-line", "") };
   bind "<alt>minus" { "action" ("history", "move-previous-edit", "") };
   bind "<alt>underscore" { "action" ("history", "move-next-edit", "") };
   /* Goto matching bracket; should be Ctrl+M but that is hardcoded to comment
@@ -112,12 +112,12 @@
   bind "<ctrl><super>e" { "move-error" (down) };
   bind "<ctrl><super><shift>e" { "move-error" (up) };
 
-  bind "<ctrl>Page_Up" { "action" ("layoutstack", "previous-view", "") };
-  bind "<ctrl>KP_Page_Up" { "action" ("layoutstack", "previous-view", "") };
-  bind "<ctrl>Page_Down" { "action" ("layoutstack", "next-view", "") };
-  bind "<ctrl>KP_Page_Down" { "action" ("layoutstack", "next-view", "") };
-  bind "<ctrl>Tab" { "action" ("layoutstack", "next-view", "") };
-  bind "<ctrl><shift>Tab" { "action" ("layoutstack", "previous-view", "") };
+  bind "<ctrl>Page_Up" { "action" ("frame", "previous-page", "") };
+  bind "<ctrl>KP_Page_Up" { "action" ("frame", "previous-page", "") };
+  bind "<ctrl>Page_Down" { "action" ("frame", "next-page", "") };
+  bind "<ctrl>KP_Page_Down" { "action" ("frame", "next-page", "") };
+  bind "<ctrl>Tab" { "action" ("frame", "next-page", "") };
+  bind "<ctrl><shift>Tab" { "action" ("frame", "previous-page", "") };
   bind "<alt>o" { "action" ("win", "find-other-file", "") };
 
   bind "<ctrl>Up" { "movement" (screen-up, 0, 0, 0) };
@@ -145,7 +145,7 @@
   bind "<ctrl>KP_Add" { "increase-font-size" () };
 
   /* These don't originally have bindings in Sublime */
-  bind "<ctrl>k" { "action" ("layoutstack", "show-list", "") };
+  bind "<ctrl>k" { "action" ("frame", "show-list", "") };
   bind "<ctrl>0" { "reset-font-size" () };
   bind "<ctrl>KP_0" { "reset-font-size" () };
 
@@ -167,7 +167,7 @@
    *   Ctrl+Alt+Shift+P Show scope name; ditto
    *   F4, Shift+F4 Previous/next search result in results panel; Builder
    *     doesn't have a results panel
-   *   Ctrl+1, Ctrl+Shift+1, etc; Builder doesn't have multiple layout stacks
+   *   Ctrl+1, Ctrl+Shift+1, etc; Builder doesn't have multiple frames
    *   Alt+Shift+2, etc; Builder can open vertical splits and close them, which
    *     is already plenty of flexibility for splitting windows
    *
@@ -177,8 +177,8 @@
    *     paste-clipboard-extended, to select pasted text so we can reindent it
    *   Ctrl+Shift+J Expand selection to indentation; selects every contiguous
    *     line at the current indendation level or deeper. Needs movement type
-   *   Alt+1, Alt+2, etc. Go to position in stack; needs a goto-view action in
-   *     ide-layout-stack-actions.c
+   *   Alt+1, Alt+2, etc. Go to position in stack; needs a goto-page action in
+   *     ide-frame-actions.c
    *   Alt+. Close Tag; nice to have for editing HTML
    *   Alt+Shift+W Wrap Selection with Tag; inserts <p> and </p> around the
    *     selection, and selects both "p"'s
@@ -210,7 +210,7 @@
    *     apparently can't be overridden.
    *   Ctrl+Alt+Q doesn't toggle macro recording. Instead, use Ctrl+Super+Q to
    *     stop recording.
-   *   Ctrl+Alt+P, Switch Project, doesn't work when the editor view is focused.
+   *   Ctrl+Alt+P, Switch Project, doesn't work when the editor page is focused.
    *   Previous Error and Next Error were originally bound to Ctrl+K, P and
    *     Ctrl+K, N on SublimeLinter for Linux. We use the shortcuts from macOS
    *     in order to avoid a separate Ctrl+K state.
diff --git a/src/plugins/sublime/meson.build b/src/plugins/sublime/meson.build
new file mode 100644
index 000000000..3272cfc5f
--- /dev/null
+++ b/src/plugins/sublime/meson.build
@@ -0,0 +1,12 @@
+plugins_sources += files([
+  'sublime-plugin.c',
+  'gbp-sublime-preferences-addin.c',
+])
+
+plugin_sublime_resources = gnome.compile_resources(
+  'sublime-resources',
+  'sublime.gresource.xml',
+  c_name: 'gbp_sublime',
+)
+
+plugins_sources += plugin_sublime_resources[0]
diff --git a/src/plugins/sublime/sublime-plugin.c b/src/plugins/sublime/sublime-plugin.c
new file mode 100644
index 000000000..f77e4a0fe
--- /dev/null
+++ b/src/plugins/sublime/sublime-plugin.c
@@ -0,0 +1,36 @@
+/* sublime-plugin.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "sublime-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-gui.h>
+
+#include "gbp-sublime-preferences-addin.h"
+
+_IDE_EXTERN void
+_gbp_sublime_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_PREFERENCES_ADDIN,
+                                              GBP_TYPE_SUBLIME_PREFERENCES_ADDIN);
+}
diff --git a/src/plugins/sublime/sublime.gresource.xml b/src/plugins/sublime/sublime.gresource.xml
new file mode 100644
index 000000000..699dc4a8f
--- /dev/null
+++ b/src/plugins/sublime/sublime.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/sublime">
+    <file>sublime.plugin</file>
+    <file>keybindings/sublime.css</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/sublime/sublime.plugin b/src/plugins/sublime/sublime.plugin
new file mode 100644
index 000000000..a9fa502ff
--- /dev/null
+++ b/src/plugins/sublime/sublime.plugin
@@ -0,0 +1,9 @@
+[Plugin]
+Authors=Philip Chimento <philip endlessm com>
+Builtin=true
+Copyright=Copyright © 2018 Philip Chimento
+Description=Emulation of various Sublime Text features
+Embedded=_gbp_sublime_register_types
+Hidden=true
+Module=sublime
+Name=Sublime Text Emulation
diff --git a/src/plugins/vim/gb-vim.c b/src/plugins/vim/gb-vim.c
new file mode 100644
index 000000000..5ad7bdf9a
--- /dev/null
+++ b/src/plugins/vim/gb-vim.c
@@ -0,0 +1,1661 @@
+/* gb-vim.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gb-vim"
+
+#include <errno.h>
+#include <glib/gi18n.h>
+#include <gtksourceview/gtksource.h>
+#include <libide-editor.h>
+#include <libide-gui.h>
+
+#include "gb-vim.h"
+
+G_DEFINE_QUARK (gb-vim-error-quark, gb_vim_error)
+
+typedef gboolean (*GbVimSetFunc)     (GtkSourceView  *source_view,
+                                      const gchar    *key,
+                                      const gchar    *value,
+                                      GError        **error);
+typedef gboolean (*GbVimCommandFunc) (GtkWidget      *active_widget,
+                                      const gchar    *command,
+                                      const gchar    *options,
+                                      GError        **error);
+
+typedef struct
+{
+  const gchar  *name;
+  GbVimSetFunc  func;
+} GbVimSet;
+
+typedef struct
+{
+  const gchar *name;
+  const gchar *alias;
+} GbVimSetAlias;
+
+typedef struct
+{
+  const gchar      *name;
+  GbVimCommandFunc  func;
+  const gchar      *description;
+} GbVimCommand;
+
+typedef struct
+{
+  GtkWidget   *active_widget;
+  gchar *file_path;
+} SplitCallbackData;
+
+static GFile *
+find_workdir (GtkWidget *active_widget)
+{
+  g_autoptr(GFile) workdir = NULL;
+  IdeWorkbench *workbench;
+  IdeContext *context;
+
+  if (!(workbench = ide_widget_get_workbench (active_widget)) ||
+      !(context = ide_workbench_get_context (workbench)) ||
+      !(workdir = ide_context_ref_workdir (context)))
+    return NULL;
+
+  return g_steal_pointer (&workdir);
+}
+
+static gboolean
+int32_parse (gint         *value,
+             const gchar  *str,
+             gint          lower,
+             gint          upper,
+             const gchar  *param_name,
+             GError      **error)
+{
+  gint64 v64;
+  gchar *v64_str;
+
+  g_assert (value);
+  g_assert (str);
+  g_assert (lower <= upper);
+  g_assert (param_name);
+
+  v64 = g_ascii_strtoll (str, NULL, 10);
+
+  if (((v64 == G_MININT64) || (v64 == G_MAXINT64)) && (errno == ERANGE))
+    {
+      g_set_error (error,
+                   GB_VIM_ERROR,
+                   GB_VIM_ERROR_NOT_NUMBER,
+                   _("Number required"));
+      return FALSE;
+    }
+
+  if ((v64 < lower) || (v64 > upper))
+    {
+      v64_str = g_strdup_printf ("%"G_GINT64_FORMAT, v64);
+      g_set_error (error,
+                   GB_VIM_ERROR,
+                   GB_VIM_ERROR_NUMBER_OUT_OF_RANGE,
+                   _("%s is invalid for %s"),
+                   v64_str, param_name);
+      g_free (v64_str);
+      return FALSE;
+    }
+
+  *value = v64;
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_autoindent (GtkSourceView  *source_view,
+                       const gchar    *key,
+                       const gchar    *value,
+                       GError        **error)
+{
+  g_object_set (source_view, "auto-indent", TRUE, NULL);
+  return TRUE;
+}
+
+
+static gboolean
+gb_vim_set_expandtab (GtkSourceView  *source_view,
+                      const gchar    *key,
+                      const gchar    *value,
+                      GError        **error)
+{
+  g_object_set (source_view, "insert-spaces-instead-of-tabs", TRUE, NULL);
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_filetype (GtkSourceView  *source_view,
+                     const gchar    *key,
+                     const gchar    *value,
+                     GError        **error)
+{
+  GtkSourceLanguageManager *manager;
+  GtkSourceLanguage *language;
+  GtkTextBuffer *buffer;
+
+  if (0 == g_strcmp0 (value, "cs"))
+    value = "c-sharp";
+  else if (0 == g_strcmp0 (value, "xhmtl"))
+    value = "html";
+  else if (0 == g_strcmp0 (value, "javascript"))
+    value = "js";
+
+  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (source_view));
+  manager = gtk_source_language_manager_get_default ();
+  language = gtk_source_language_manager_get_language (manager, value);
+
+  if (language == NULL)
+    {
+      g_set_error (error,
+                   GB_VIM_ERROR,
+                   GB_VIM_ERROR_UNKNOWN_OPTION,
+                   _("Cannot find language “%s”"),
+                   value);
+      return FALSE;
+    }
+
+  g_object_set (buffer, "language", language, NULL);
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_noautoindent (GtkSourceView  *source_view,
+                         const gchar    *key,
+                         const gchar    *value,
+                         GError        **error)
+{
+  g_object_set (source_view, "auto-indent", FALSE, NULL);
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_noexpandtab (GtkSourceView  *source_view,
+                        const gchar    *key,
+                        const gchar    *value,
+                        GError        **error)
+{
+  g_object_set (source_view, "insert-spaces-instead-of-tabs", FALSE, NULL);
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_nonumber (GtkSourceView  *source_view,
+                     const gchar    *key,
+                     const gchar    *value,
+                     GError        **error)
+{
+  g_object_set (source_view, "show-line-numbers", FALSE, NULL);
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_number (GtkSourceView  *source_view,
+                   const gchar    *key,
+                   const gchar    *value,
+                   GError        **error)
+{
+  g_object_set (source_view, "show-line-numbers", TRUE, NULL);
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_scrolloff (GtkSourceView  *source_view,
+                      const gchar    *key,
+                      const gchar    *value,
+                      GError        **error)
+{
+  gint scroll_offset = 0;
+
+  if (!int32_parse (&scroll_offset, value, 0, G_MAXINT32, "scroll size", error))
+    return FALSE;
+  if (IDE_IS_SOURCE_VIEW (source_view))
+    g_object_set (source_view, "scroll-offset", scroll_offset, NULL);
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_shiftwidth (GtkSourceView  *source_view,
+                       const gchar    *key,
+                       const gchar    *value,
+                       GError        **error)
+{
+  gint shiftwidth = 0;
+
+  if (!int32_parse (&shiftwidth, value, 0, G_MAXINT32, "shift width", error))
+    return FALSE;
+
+  if (shiftwidth == 0)
+    shiftwidth = -1;
+
+  g_object_set (source_view, "indent-width", shiftwidth, NULL);
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_tabstop (GtkSourceView  *source_view,
+                    const gchar    *key,
+                    const gchar    *value,
+                    GError        **error)
+{
+  gint tabstop  = 0;
+
+  if (!int32_parse (&tabstop , value, 1, 32, "tab stop", error))
+    return FALSE;
+
+  g_object_set (source_view, "tab-width", tabstop, NULL);
+  return TRUE;
+}
+
+static const GbVimSet vim_sets [] = {
+  { "autoindent",    gb_vim_set_autoindent },
+  { "expandtab",     gb_vim_set_expandtab },
+  { "filetype",      gb_vim_set_filetype },
+  { "noautoindent",  gb_vim_set_noautoindent },
+  { "noexpandtab",   gb_vim_set_noexpandtab },
+  { "nonumber",      gb_vim_set_nonumber },
+  { "number",        gb_vim_set_number },
+  { "scrolloff",     gb_vim_set_scrolloff },
+  { "shiftwidth",    gb_vim_set_shiftwidth },
+  { "tabstop",       gb_vim_set_tabstop },
+  { NULL }
+};
+
+static const GbVimSetAlias vim_set_aliases[] = {
+  { "ai",   "autoindent" },
+  { "et",   "expandtab" },
+  { "ft",   "filetype" },
+  { "noet", "noexpandtab" },
+  { "nu",   "number" },
+  { "noai", "noautoindent" },
+  { "nonu", "nonumber" },
+  { "so",   "scrolloff" },
+  { "sw",   "shiftwidth" },
+  { "ts",   "tabstop" },
+  { NULL }
+};
+
+static const GbVimSet *
+lookup_set (const gchar *key)
+{
+  gsize i;
+
+  g_assert (key);
+
+  for (i = 0; vim_set_aliases [i].name; i++)
+    {
+      if (g_str_equal (vim_set_aliases [i].name, key))
+        {
+          key = vim_set_aliases [i].alias;
+          break;
+        }
+    }
+
+  for (i = 0; vim_sets [i].name; i++)
+    {
+      if (g_str_equal (vim_sets [i].name, key))
+        return &vim_sets [i];
+    }
+
+  return NULL;
+}
+
+static gboolean
+gb_vim_set_source_view_error (GError **error)
+{
+  g_set_error (error,
+               GB_VIM_ERROR,
+               GB_VIM_ERROR_NOT_SOURCE_VIEW,
+               _("This command requires a GtkSourceView to be focused"));
+
+  return FALSE;
+}
+
+static gboolean
+gb_vim_set_no_view_error (GError **error)
+{
+  g_set_error (error,
+               GB_VIM_ERROR,
+               GB_VIM_ERROR_NO_VIEW,
+               _("This command requires a view to be focused"));
+
+  return FALSE;
+}
+
+static gboolean
+gb_vim_command_set (GtkWidget      *active_widget,
+                    const gchar    *command,
+                    const gchar    *options,
+                    GError        **error)
+{
+  IdeSourceView *source_view;
+  gboolean ret = FALSE;
+  gchar **parts;
+  gsize i;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+  g_assert (command);
+  g_assert (options);
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+  else
+    return gb_vim_set_source_view_error (error);
+
+  parts = g_strsplit (options, " ", 0);
+
+  if (parts [0] == NULL)
+    {
+      ret = TRUE;
+      goto cleanup;
+    }
+
+  for (i = 0; parts [i]; i++)
+    {
+      const GbVimSet *set;
+      const gchar *value = "";
+      gchar *key = parts [i];
+      gchar *tmp;
+
+      for (tmp = key; *tmp; tmp = g_utf8_next_char (tmp))
+        {
+          if (g_utf8_get_char (tmp) == '=')
+            {
+              *tmp = '\0';
+              value = ++tmp;
+              break;
+            }
+        }
+
+      set = lookup_set (key);
+
+      if (set == NULL)
+        {
+          g_set_error (error,
+                       GB_VIM_ERROR,
+                       GB_VIM_ERROR_UNKNOWN_OPTION,
+                       _("Unknown option: %s"),
+                       key);
+          goto cleanup;
+        }
+
+      if (!set->func (GTK_SOURCE_VIEW (source_view), key, value, error))
+        goto cleanup;
+    }
+
+  ret = TRUE;
+
+cleanup:
+  g_strfreev (parts);
+
+  return ret;
+}
+
+static gboolean
+gb_vim_command_colorscheme (GtkWidget      *active_widget,
+                            const gchar    *command,
+                            const gchar    *options,
+                            GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      GtkSourceStyleSchemeManager *manager;
+      GtkSourceStyleScheme *style_scheme;
+      GtkTextBuffer *buffer;
+      g_autofree gchar *trimmed = NULL;
+      IdeSourceView *source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+
+      trimmed = g_strstrip (g_strdup (options));
+      buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (source_view));
+      manager = gtk_source_style_scheme_manager_get_default ();
+      style_scheme = gtk_source_style_scheme_manager_get_scheme (manager, trimmed);
+
+      if (style_scheme == NULL)
+        {
+          g_set_error (error,
+                       GB_VIM_ERROR,
+                       GB_VIM_ERROR_UNKNOWN_OPTION,
+                       _("Cannot find colorscheme “%s”"),
+                       options);
+          return FALSE;
+        }
+
+      g_object_set (buffer, "style-scheme", style_scheme, NULL);
+
+      return TRUE;
+    }
+  else
+    return gb_vim_set_source_view_error (error);
+}
+
+static gboolean
+gb_vim_command_edit (GtkWidget      *active_widget,
+                     const gchar    *command,
+                     const gchar    *options,
+                     GError        **error)
+{
+  g_autoptr(GFile) workdir = NULL;
+  g_autoptr(GFile) file = NULL;
+  IdeWorkbench *workbench;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (ide_str_empty0 (options))
+    {
+      dzl_gtk_widget_action (GTK_WIDGET (active_widget), "win", "open-with-dialog", NULL);
+      return TRUE;
+    }
+
+  if (!(workdir = find_workdir (active_widget)))
+    {
+      g_set_error (error,
+                   GB_VIM_ERROR,
+                   GB_VIM_ERROR_NOT_SOURCE_VIEW,
+                   _("Failed to locate working directory"));
+      return FALSE;
+    }
+
+  if (g_path_is_absolute (options))
+    file = g_file_new_for_path (options);
+  else
+    file = g_file_get_child (workdir, options);
+
+  workbench = ide_widget_get_workbench (active_widget);
+  ide_workbench_open_async (workbench, file, "editor", 0, NULL, NULL, NULL);
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_command_tabe (GtkWidget      *active_widget,
+                     const gchar    *command,
+                     const gchar    *options,
+                     GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (!ide_str_empty0 (options))
+    return gb_vim_command_edit (active_widget, command, options, error);
+
+  dzl_gtk_widget_action (GTK_WIDGET (active_widget), "editor", "new-file", NULL);
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_command_quit (GtkWidget      *active_widget,
+                     const gchar    *command,
+                     const gchar    *options,
+                     GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      IdeSourceView *source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+
+      dzl_gtk_widget_action (GTK_WIDGET (source_view), "editor-page", "save", NULL);
+    }
+
+  dzl_gtk_widget_action (GTK_WIDGET (active_widget), "frame", "close-page", NULL);
+
+  return TRUE;
+}
+
+static void
+gb_vim_command_split_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  IdeWorkbench *workbench = (IdeWorkbench *)object;
+  SplitCallbackData *split_callback_data = user_data;
+  GtkWidget *active_widget;
+  const gchar *file_path;
+  GVariant *variant;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_WORKBENCH (workbench));
+  g_assert (split_callback_data != NULL);
+
+  if (ide_workbench_open_finish (workbench, result, &error))
+    {
+      active_widget = split_callback_data->active_widget;
+      file_path = split_callback_data->file_path;
+      variant = g_variant_new_string (file_path);
+
+      dzl_gtk_widget_action (GTK_WIDGET (active_widget), "frame", "split-page", variant);
+    }
+
+  g_clear_object (&split_callback_data->active_widget);
+  g_clear_pointer (&split_callback_data->file_path, g_free);
+  g_slice_free (SplitCallbackData, split_callback_data);
+}
+
+static void
+gb_vim_command_vsplit_cb (GObject      *object,
+                          GAsyncResult *result,
+                          gpointer      user_data)
+{
+  IdeWorkbench *workbench = (IdeWorkbench *)object;
+  SplitCallbackData *split_callback_data = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_WORKBENCH (workbench));
+  g_assert (split_callback_data != NULL);
+
+  if (ide_workbench_open_finish (workbench,result, &error))
+    dzl_gtk_widget_action (split_callback_data->active_widget,
+                           "frame",
+                           "open-in-new-frame",
+                           g_variant_new_string (split_callback_data->file_path));
+
+  g_clear_object (&split_callback_data->active_widget);
+  g_clear_pointer (&split_callback_data->file_path, g_free);
+  g_slice_free (SplitCallbackData, split_callback_data);
+}
+
+static gboolean
+load_split_async (GtkWidget            *active_widget,
+                  const gchar          *options,
+                  GAsyncReadyCallback   callback,
+                  GError              **error)
+{
+  g_autoptr(GFile) workdir = NULL;
+  g_autoptr(GFile) file = NULL;
+  g_autofree gchar *file_path = NULL;
+  SplitCallbackData *split_callback_data;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+  g_assert (options != NULL);
+  g_assert (callback != NULL);
+
+  if (!(workdir = find_workdir (active_widget)))
+    {
+      g_set_error (error,
+                   GB_VIM_ERROR,
+                   GB_VIM_ERROR_NOT_SOURCE_VIEW,
+                   _("Failed to locate working directory"));
+      return FALSE;
+    }
+
+  if (!g_path_is_absolute (options))
+    {
+      g_autofree gchar *workdir_path = NULL;
+      workdir_path = g_file_get_path (workdir);
+      file_path = g_build_filename (workdir_path, options, NULL);
+    }
+  else
+    file_path = g_strdup (options);
+
+  file = g_file_new_for_path (file_path);
+
+  split_callback_data = g_slice_new0 (SplitCallbackData);
+  split_callback_data->active_widget = g_object_ref (active_widget);
+  split_callback_data->file_path = g_steal_pointer (&file_path);
+
+  ide_workbench_open_async (ide_widget_get_workbench (active_widget),
+                            file,
+                            "editor",
+                            IDE_BUFFER_OPEN_FLAGS_NO_VIEW,
+                            NULL,
+                            callback,
+                            split_callback_data);
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_command_split (GtkWidget    *active_widget,
+                      const gchar  *command,
+                      const gchar  *options,
+                      GError      **error)
+{
+  GVariant *variant;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (!IDE_IS_PAGE (active_widget))
+    return gb_vim_set_no_view_error (error);
+
+  if (ide_str_empty0 (options))
+    {
+      variant = g_variant_new_string ("");
+      dzl_gtk_widget_action (GTK_WIDGET (active_widget), "frame", "split-page", variant);
+
+      return TRUE;
+    }
+  else
+    return load_split_async (active_widget, options, gb_vim_command_split_cb, error);
+}
+
+static gboolean
+gb_vim_command_vsplit (GtkWidget    *active_widget,
+                       const gchar  *command,
+                       const gchar  *options,
+                       GError      **error)
+{
+  GVariant *variant;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (!IDE_IS_PAGE (active_widget))
+    return gb_vim_set_no_view_error (error);
+
+  if (ide_str_empty0 (options))
+    {
+      variant = g_variant_new_string ("");
+      dzl_gtk_widget_action (GTK_WIDGET (active_widget), "frame", "open-in-new-frame", variant);
+
+      return TRUE;
+    }
+  else
+    return load_split_async (active_widget, options, gb_vim_command_vsplit_cb, error);
+}
+
+static gboolean
+gb_vim_command_write (GtkWidget      *active_widget,
+                      const gchar    *command,
+                      const gchar    *options,
+                      GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      IdeSourceView *source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+
+      dzl_gtk_widget_action (GTK_WIDGET (source_view), "editor-page", "save", NULL);
+
+      return TRUE;
+    }
+  else
+    return gb_vim_set_source_view_error (error);
+}
+
+static gboolean
+gb_vim_command_wq (GtkWidget      *active_widget,
+                   const gchar    *command,
+                   const gchar    *options,
+                   GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    return (gb_vim_command_write (active_widget, command, options, error) &&
+            gb_vim_command_quit (active_widget, command, options, error));
+  else
+    return gb_vim_set_source_view_error (error);
+}
+
+static gboolean
+gb_vim_command_nohl (GtkWidget      *active_widget,
+                     const gchar    *command,
+                     const gchar    *options,
+                     GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      IdeEditorSearch *search = ide_editor_page_get_search (IDE_EDITOR_PAGE (active_widget));
+      ide_editor_search_set_visible (search, FALSE);
+      return TRUE;
+    }
+
+  return gb_vim_set_source_view_error (error);
+}
+
+static gboolean
+gb_vim_command_make (GtkWidget      *active_widget,
+                     const gchar    *command,
+                     const gchar    *options,
+                     GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  /* TODO: check for an open project */
+  dzl_gtk_widget_action (GTK_WIDGET (active_widget), "build-manager", "build", NULL);
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_command_syntax (GtkWidget      *active_widget,
+                       const gchar    *command,
+                       const gchar    *options,
+                       GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      IdeBuffer *buffer = ide_editor_page_get_buffer (IDE_EDITOR_PAGE (active_widget));
+
+      if (g_str_equal (options, "enable") || g_str_equal (options, "on"))
+        gtk_source_buffer_set_highlight_syntax (GTK_SOURCE_BUFFER (buffer), TRUE);
+      else if (g_str_equal (options, "off"))
+        gtk_source_buffer_set_highlight_syntax (GTK_SOURCE_BUFFER (buffer), FALSE);
+      else
+        {
+          g_set_error (error,
+                       GB_VIM_ERROR,
+                       GB_VIM_ERROR_UNKNOWN_OPTION,
+                       _("Invalid :syntax subcommand: %s"),
+                       options);
+          return FALSE;
+        }
+
+      return TRUE;
+    }
+  else
+    return gb_vim_set_source_view_error (error);
+}
+
+static gboolean
+gb_vim_command_sort (GtkWidget      *active_widget,
+                     const gchar    *command,
+                     const gchar    *options,
+                     GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      IdeSourceView *source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+
+      g_signal_emit_by_name (source_view, "sort", FALSE, FALSE);
+      g_signal_emit_by_name (source_view, "clear-selection");
+      g_signal_emit_by_name (source_view, "set-mode", NULL,
+                             IDE_SOURCE_VIEW_MODE_TYPE_PERMANENT);
+
+      return TRUE;
+    }
+  else
+    return gb_vim_set_source_view_error (error);
+}
+
+static gboolean
+gb_vim_command_bnext (GtkWidget      *active_widget,
+                      const gchar    *command,
+                      const gchar    *options,
+                      GError        **error)
+{
+  IdeFrame *frame_;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if ((frame_ = (IdeFrame *)gtk_widget_get_ancestor (active_widget, IDE_TYPE_FRAME)) &&
+      g_list_model_get_n_items (G_LIST_MODEL (frame_)) > 0)
+    dzl_gtk_widget_action (GTK_WIDGET (active_widget), "frame", "next-page", NULL);
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_command_bprevious (GtkWidget      *active_widget,
+                          const gchar    *command,
+                          const gchar    *options,
+                          GError        **error)
+{
+  IdeFrame *frame_;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if ((frame_ = (IdeFrame *)gtk_widget_get_ancestor (active_widget, IDE_TYPE_FRAME)) &&
+      g_list_model_get_n_items (G_LIST_MODEL (frame_)) > 0)
+    dzl_gtk_widget_action (GTK_WIDGET (active_widget), "frame", "previous-page", NULL);
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_command_cnext (GtkWidget      *active_widget,
+                      const gchar    *command,
+                      const gchar    *options,
+                      GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      IdeSourceView *source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+
+      g_signal_emit_by_name (source_view, "move-error", GTK_DIR_DOWN);
+
+      return TRUE;
+    }
+  else
+    return gb_vim_set_source_view_error (error);
+}
+
+static gboolean
+gb_vim_command_cprevious (GtkWidget      *active_widget,
+                          const gchar    *command,
+                          const gchar    *options,
+                          GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      IdeSourceView *source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+
+      g_signal_emit_by_name (source_view, "move-error", GTK_DIR_UP);
+
+      return TRUE;
+    }
+  else
+    return gb_vim_set_source_view_error (error);
+}
+
+static gboolean
+gb_vim_command_buffers (GtkWidget      *active_widget,
+                        const gchar    *command,
+                        const gchar    *options,
+                        GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  dzl_gtk_widget_action (GTK_WIDGET (active_widget), "frame", "show-list", NULL);
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_jump_to_line (GtkWidget      *active_widget,
+                     const gchar    *command,
+                     const gchar    *options,
+                     GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      IdeSourceView *source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+      GtkTextBuffer *buffer;
+      gboolean extend_selection;
+      gint line;
+
+      if (!int32_parse (&line, options, 0, G_MAXINT32, "line number", error))
+        return FALSE;
+
+      buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (source_view));
+      extend_selection = gtk_text_buffer_get_has_selection (buffer);
+      ide_source_view_set_count (IDE_SOURCE_VIEW (source_view), line);
+
+      if (line == 0)
+        {
+          GtkTextIter iter;
+
+          /*
+           * Zero is not a valid line number, and IdeSourceView treats
+           * that as a move to the end of the buffer. Instead, we want
+           * line 1 to be like vi/vim.
+           */
+          gtk_text_buffer_get_start_iter (buffer, &iter);
+          gtk_text_buffer_select_range (buffer, &iter, &iter);
+          gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (source_view),
+                                        gtk_text_buffer_get_insert (buffer),
+                                        0.0, FALSE, 0.0, 0.0);
+        }
+      else
+        {
+          g_signal_emit_by_name (source_view,
+                                 "movement",
+                                 IDE_SOURCE_VIEW_MOVEMENT_NTH_LINE,
+                                 extend_selection, TRUE, TRUE);
+        }
+
+      ide_source_view_set_count (IDE_SOURCE_VIEW (source_view), 0);
+
+      g_signal_emit_by_name (source_view, "save-insert-mark");
+
+      return TRUE;
+    }
+  else
+    return gb_vim_set_source_view_error (error);
+}
+
+static void
+gb_vim_do_substitute_line (GtkTextBuffer *buffer,
+                           GtkTextIter   *begin,
+                           const gchar   *search_text,
+                           const gchar   *replace_text,
+                           gboolean       is_global)
+{
+  GtkSourceSearchContext *search_context;
+  GtkSourceSearchSettings *search_settings;
+  GtkTextIter match_begin;
+  GtkTextIter match_end;
+  gboolean has_wrapped = FALSE;
+  GError *error = NULL;
+  gint line_number;
+
+  g_assert(buffer);
+  g_assert(begin);
+  g_assert(search_text);
+  g_assert(replace_text);
+
+  search_settings = gtk_source_search_settings_new ();
+  search_context =  gtk_source_search_context_new (GTK_SOURCE_BUFFER (buffer), search_settings);
+  line_number = gtk_text_iter_get_line(begin);
+  gtk_text_iter_set_line_offset(begin, 0);
+
+  gtk_source_search_settings_set_search_text (search_settings, search_text);
+  gtk_source_search_settings_set_case_sensitive (search_settings, TRUE);
+
+  while (gtk_source_search_context_forward (search_context, begin, &match_begin, &match_end, &has_wrapped) 
&& !has_wrapped)
+    {
+      if (gtk_text_iter_get_line (&match_end) != line_number)
+        break;
+
+      if (!gtk_source_search_context_replace (search_context, &match_begin, &match_end, replace_text, -1, 
&error))
+        {
+          g_warning ("%s", error->message);
+          g_clear_error (&error);
+          break;
+        }
+
+      *begin = match_end;
+
+      if (!is_global)
+        break;
+    }
+
+  g_clear_object (&search_settings);
+  g_clear_object (&search_context);
+}
+
+static void
+gb_vim_do_substitute (GtkTextBuffer *buffer,
+                      GtkTextIter   *begin,
+                      GtkTextIter   *end,
+                      const gchar   *search_text,
+                      const gchar   *replace_text,
+                      gboolean       is_global,
+                      gboolean       should_search_all_lines)
+{
+  GtkTextIter begin_tmp;
+  GtkTextIter end_tmp;
+  GtkTextMark *last_line;
+  GtkTextIter *current_line;
+  GtkTextMark *end_mark;
+  GtkTextMark *insert;
+
+  g_assert (search_text);
+  g_assert (replace_text);
+  g_assert ((!begin && !end) || (begin && end));
+
+  insert = gtk_text_buffer_get_insert (buffer);
+
+  if (!begin)
+    {
+      if (should_search_all_lines)
+        gtk_text_buffer_get_start_iter (buffer, &begin_tmp);
+      else
+        gtk_text_buffer_get_iter_at_mark (buffer, &begin_tmp, insert);
+      begin = &begin_tmp;
+    }
+
+  if (!end)
+    {
+      if (should_search_all_lines)
+        gtk_text_buffer_get_end_iter (buffer, &end_tmp);
+      else
+        gtk_text_buffer_get_iter_at_mark (buffer, &end_tmp, insert);
+      end = &end_tmp;
+    }
+
+  current_line = begin;
+  last_line = gtk_text_buffer_create_mark (buffer, NULL, current_line, FALSE);
+  end_mark = gtk_text_buffer_create_mark (buffer, NULL, end, FALSE);
+
+  for (guint line = gtk_text_iter_get_line (current_line);
+       line <= gtk_text_iter_get_line (end);
+       line++)
+    {
+      gb_vim_do_substitute_line (buffer, current_line, search_text, replace_text, is_global);
+      gtk_text_buffer_get_iter_at_mark (buffer, current_line, last_line);
+      gtk_text_buffer_get_iter_at_mark (buffer, end, end_mark);
+      gtk_text_iter_set_line (current_line, line + 1);
+    }
+
+  gtk_text_buffer_delete_mark (buffer, last_line);
+  gtk_text_buffer_delete_mark (buffer, end_mark);
+}
+
+static gboolean
+gb_vim_command_substitute (GtkWidget    *active_widget,
+                           const gchar  *command,
+                           const gchar  *options,
+                           GError      **error)
+{
+  IdeSourceView  *source_view;
+  GtkTextBuffer *buffer;
+  const gchar *search_begin = NULL;
+  const gchar *search_end = NULL;
+  const gchar *replace_begin = NULL;
+  const gchar *replace_end = NULL;
+  g_autofree gchar *search_text = NULL;
+  g_autofree gchar *replace_text = NULL;
+  GtkTextIter *substitute_begin = NULL;
+  GtkTextIter *substitute_end = NULL;
+  gunichar separator;
+  gboolean replace_in_every_line = FALSE;
+  gboolean replace_every_occurence_in_line = FALSE;
+  gboolean replace_ask_for_confirmation = FALSE;
+  GtkTextIter selection_begin, selection_end;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+  g_assert (g_str_has_prefix (command, "%s") || g_str_has_prefix (command, "s"));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+  else
+    return gb_vim_set_source_view_error (error);
+
+  if (command[0] == '%')
+    {
+      replace_in_every_line = TRUE;
+      command++;
+    }
+
+  command++;
+
+  separator = g_utf8_get_char (command);
+  if (!separator)
+    goto invalid_request;
+
+  search_begin = command = g_utf8_next_char (command);
+
+  for (; *command; command = g_utf8_next_char (command))
+    {
+      if (*command == '\\')
+        {
+          command = g_utf8_next_char (command);
+          if (!*command)
+            goto invalid_request;
+          continue;
+        }
+
+      if (g_utf8_get_char (command) == separator)
+        {
+          search_end = command;
+          break;
+        }
+    }
+
+  if (search_end == NULL)
+    {
+      search_text = g_strdup (search_begin);
+      replace_text = g_strdup ("");
+    }
+  else
+    {
+      search_text = g_strndup (search_begin, search_end - search_begin);
+
+      replace_begin = command = g_utf8_next_char (command);
+
+      for (; *command; command = g_utf8_next_char (command))
+        {
+          if (*command == '\\')
+            {
+              command = g_utf8_next_char (command);
+              if (!*command)
+                goto invalid_request;
+              continue;
+            }
+
+          if (g_utf8_get_char (command) == separator)
+            {
+              replace_end = command;
+              break;
+            }
+        }
+
+      if (replace_end == NULL)
+        replace_text = g_strdup (replace_begin);
+      else
+        {
+          replace_text = g_strndup (replace_begin, replace_end - replace_begin);
+          command = g_utf8_next_char (command);
+        }
+
+      if (*command)
+        {
+          for (; *command; command++)
+            {
+              switch (*command)
+                {
+                case 'c':
+                  replace_ask_for_confirmation = TRUE;
+                  break;
+
+                case 'g':
+                  replace_every_occurence_in_line = TRUE;
+                  break;
+
+                /* what other options are supported? */
+                default:
+                  break;
+                }
+            }
+        }
+    }
+
+  if (replace_ask_for_confirmation)
+    {
+      GVariant *variant;
+      GVariantBuilder builder;
+
+      g_variant_builder_init (&builder, G_VARIANT_TYPE_STRING_ARRAY);
+      g_variant_builder_add (&builder, "s", search_text);
+      g_variant_builder_add (&builder, "s", replace_text);
+      variant = g_variant_builder_end (&builder);
+
+      dzl_gtk_widget_action (active_widget, "editor-page", "replace-confirm", variant);
+
+      return TRUE;
+    }
+
+  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (source_view));
+
+  if (gtk_text_buffer_get_has_selection (buffer))
+    {
+      gtk_text_buffer_get_selection_bounds (buffer, &selection_begin, &selection_end);
+      substitute_begin = &selection_begin;
+      substitute_end = &selection_end;
+    }
+
+  gtk_text_buffer_begin_user_action (buffer);
+  gb_vim_do_substitute (buffer, substitute_begin, substitute_end, search_text, replace_text, 
replace_every_occurence_in_line, replace_in_every_line);
+  gtk_text_buffer_end_user_action (buffer);
+
+  return TRUE;
+
+invalid_request:
+  g_set_error (error,
+               GB_VIM_ERROR,
+               GB_VIM_ERROR_UNKNOWN_OPTION,
+               _("Invalid search and replace request"));
+  return FALSE;
+}
+
+static const GbVimCommand vim_commands[] = {
+  { "bdelete",     gb_vim_command_quit },
+  { "bnext",       gb_vim_command_bnext },
+  { "bprevious",   gb_vim_command_bprevious },
+  { "buffers",     gb_vim_command_buffers },
+  { "cnext",       gb_vim_command_cnext },
+  { "colorscheme", gb_vim_command_colorscheme, N_("Change the pages colorscheme") },
+  { "cprevious",   gb_vim_command_cprevious },
+  { "edit",        gb_vim_command_edit },
+  { "ls",          gb_vim_command_buffers },
+  { "make",        gb_vim_command_make, N_("Build the project") },
+  { "nohl",        gb_vim_command_nohl, N_("Clear search highlighting") },
+  { "open",        gb_vim_command_edit, N_("Open a file by path") },
+  { "quit",        gb_vim_command_quit, N_("Close the page") },
+  { "set",         gb_vim_command_set, N_("Set various buffer options") },
+  { "sort",        gb_vim_command_sort, N_("Sort the selected lines") },
+  { "split",       gb_vim_command_split, N_("Create a split page below the current page") },
+  { "syntax",      gb_vim_command_syntax, N_("Toggle syntax highlighting") },
+  { "tabe",        gb_vim_command_tabe },
+  { "vsplit",      gb_vim_command_vsplit },
+  { "w",           gb_vim_command_write },
+  { "wq",          gb_vim_command_wq, N_("Save and close the current page") },
+  { "write",       gb_vim_command_write, N_("Save the current page") },
+  { NULL }
+};
+
+static gboolean
+looks_like_substitute (const gchar *line)
+{
+  g_assert (line);
+
+  if (g_str_has_prefix (line, "%s"))
+    return TRUE;
+  return *line == 's';
+}
+
+static const GbVimCommand *
+lookup_command (const gchar  *name,
+                gchar       **options_sup)
+{
+  static GbVimCommand line_command = { "__line__", gb_vim_jump_to_line };
+  gint line;
+  gsize i;
+
+  g_assert (name);
+  g_assert (options_sup);
+
+  *options_sup = NULL;
+
+  for (i = 0; vim_commands [i].name; i++)
+    {
+      if (g_str_has_prefix (vim_commands [i].name, name))
+        return &vim_commands [i];
+    }
+
+  if (g_ascii_isdigit (*name) && int32_parse (&line, name, 0, G_MAXINT32, "line", NULL))
+    {
+      *options_sup = g_strdup (name);
+      return &line_command;
+    }
+
+  return NULL;
+}
+
+gboolean
+gb_vim_execute (GtkWidget      *active_widget,
+                const gchar    *line,
+                GError        **error)
+{
+  g_autofree gchar *name_slice = NULL;
+  g_autofree gchar *options_sup = NULL;
+  const GbVimCommand *command;
+  const gchar *command_name = line;
+  const gchar *options;
+  g_autofree gchar *all_options = NULL;
+  gboolean result;
+
+  g_return_val_if_fail (GTK_IS_WIDGET (active_widget), FALSE);
+  g_return_val_if_fail (line, FALSE);
+
+  for (options = line; *options; options = g_utf8_next_char (options))
+    {
+      gunichar ch;
+
+      ch = g_utf8_get_char (options);
+
+      if (g_unichar_isspace (ch))
+        break;
+    }
+
+  if (g_unichar_isspace (g_utf8_get_char (options)))
+    {
+      command_name = name_slice = g_strndup (line, options - line);
+      options = g_utf8_next_char (options);
+    }
+
+  command = lookup_command (command_name, &options_sup);
+
+  if (command == NULL)
+    {
+      if (looks_like_substitute (line))
+        return gb_vim_command_substitute (active_widget, line, "", error);
+
+      g_set_error (error,
+                   GB_VIM_ERROR,
+                   GB_VIM_ERROR_NOT_FOUND,
+                   _("Not a command: %s"),
+                   command_name);
+
+      return FALSE;
+    }
+
+  if (options_sup)
+    all_options = g_strconcat (options, " ", options_sup, NULL);
+  else
+    all_options = g_strdup (options);
+
+  result = command->func (active_widget, command_name, all_options, error);
+
+  return result;
+}
+
+static gchar *
+joinv_and_add (gchar       **parts,
+               gsize         len,
+               const gchar  *delim,
+               const gchar  *str)
+{
+  GString *gstr;
+  gsize i;
+
+  gstr = g_string_new (parts [0]);
+  for (i = 1; i < len; i++)
+    g_string_append_printf (gstr, "%s%s", delim, parts [i]);
+  g_string_append_printf (gstr, "%s%s", delim, str);
+
+  return g_string_free (gstr, FALSE);
+}
+
+static void
+gb_vim_complete_set (const gchar *line,
+                     GPtrArray   *ar)
+{
+  const gchar *key;
+  gchar **parts;
+  guint len;
+  gsize i;
+
+  parts = g_strsplit (line, " ", 0);
+  len = g_strv_length (parts);
+
+  if (len < 2)
+    {
+      g_strfreev (parts);
+
+      return;
+    }
+
+  key = parts [len - 1];
+
+  for (i = 0; vim_sets [i].name; i++)
+    {
+      if (g_str_has_prefix (vim_sets [i].name, key))
+        g_ptr_array_add (ar, joinv_and_add (parts, len - 1, " ", vim_sets [i].name));
+    }
+
+  for (i = 0; vim_set_aliases [i].name; i++)
+    {
+      if (g_str_has_prefix (vim_set_aliases [i].name, key))
+        g_ptr_array_add (ar, joinv_and_add (parts, len - 1, " ", vim_set_aliases [i].name));
+    }
+
+  g_strfreev (parts);
+}
+
+static void
+gb_vim_complete_command (const gchar *line,
+                         GPtrArray   *ar)
+{
+  gsize i;
+
+  for (i = 0; vim_commands [i].name; i++)
+    {
+      if (g_str_has_prefix (vim_commands [i].name, line))
+        g_ptr_array_add (ar, g_strdup (vim_commands [i].name));
+    }
+}
+
+static void
+gb_vim_complete_edit_files (GtkWidget   *active_widget,
+                            const gchar *command,
+                            GPtrArray   *ar,
+                            const gchar *prefix)
+{
+  g_autoptr(GFile) child = NULL;
+  g_autoptr(GFile) parent = NULL;
+  g_autoptr(GFile) workdir = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (command);
+  g_assert (ar);
+  g_assert (prefix);
+
+  if (!(workdir = find_workdir (active_widget)))
+    IDE_EXIT;
+
+  child = g_file_get_child (workdir, prefix);
+
+  if (g_file_query_exists (child, NULL))
+    {
+      if (g_file_query_file_type (child, 0, NULL) == G_FILE_TYPE_DIRECTORY)
+        {
+          g_autoptr(GFileEnumerator) fe = NULL;
+          GFileInfo *descendent;
+
+          if (!g_str_has_suffix (prefix, "/"))
+            {
+              g_ptr_array_add (ar, g_strdup_printf ("%s %s/", command, prefix));
+              IDE_EXIT;
+            }
+
+          fe = g_file_enumerate_children (child,
+                                          G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
+                                          G_FILE_QUERY_INFO_NONE,
+                                          NULL, NULL);
+
+          if (fe == NULL)
+            IDE_EXIT;
+
+          while ((descendent = g_file_enumerator_next_file (fe, NULL, NULL)))
+            {
+              const gchar *name;
+
+              name = g_file_info_get_display_name (descendent);
+              g_ptr_array_add (ar, g_strdup_printf ("%s %s%s", command, prefix, name));
+              g_object_unref (descendent);
+            }
+
+          IDE_EXIT;
+        }
+    }
+
+  parent = g_file_get_parent (child);
+
+  if (parent != NULL)
+    {
+      g_autoptr(GFileEnumerator) fe = NULL;
+      GFileInfo *descendent;
+      const gchar *slash;
+      const gchar *partial_name;
+      g_autofree gchar *prefix_dir = NULL;
+
+#ifdef IDE_ENABLE_TRACE
+      {
+        g_autofree gchar *parent_path = g_file_get_path (parent);
+        IDE_TRACE_MSG ("parent_path: %s", parent_path);
+      }
+#endif
+
+      if ((slash = strrchr (prefix, G_DIR_SEPARATOR)))
+        {
+          partial_name = slash + 1;
+          prefix_dir = g_strndup (prefix, slash - prefix + 1);
+        }
+      else
+        {
+          partial_name = prefix;
+        }
+
+      fe = g_file_enumerate_children (parent,
+                                      G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME","
+                                      G_FILE_ATTRIBUTE_STANDARD_TYPE,
+                                      G_FILE_QUERY_INFO_NONE,
+                                      NULL, NULL);
+
+      if (fe == NULL)
+        IDE_EXIT;
+
+      while ((descendent = g_file_enumerator_next_file (fe, NULL, NULL)))
+        {
+          const gchar *name;
+          GFileType file_type;
+
+          name = g_file_info_get_display_name (descendent);
+          file_type = g_file_info_get_file_type (descendent);
+
+          IDE_TRACE_MSG ("name=%s prefix=%s", name, prefix);
+
+          if (name && g_str_has_prefix (name, partial_name))
+            {
+              gchar *completed_command;
+              const gchar *descendent_name;
+              g_autofree gchar *full_path = NULL;
+              g_autofree gchar *parent_path = NULL;
+
+              parent_path = g_file_get_path (parent);
+              descendent_name = g_file_info_get_name (descendent);
+              full_path = g_build_filename (parent_path, descendent_name, NULL);
+
+              if (prefix[0] == G_DIR_SEPARATOR)
+                completed_command = g_strdup_printf ("%s %s%s", command, full_path,
+                                                     file_type == G_FILE_TYPE_DIRECTORY ? G_DIR_SEPARATOR_S 
: "");
+              else if (strchr (prefix, G_DIR_SEPARATOR) == NULL)
+                completed_command = g_strdup_printf ("%s %s%s", command, descendent_name,
+                                                     file_type == G_FILE_TYPE_DIRECTORY ? G_DIR_SEPARATOR_S 
: "");
+              else
+                completed_command = g_strdup_printf ("%s %s%s%s", command, prefix_dir, descendent_name,
+                                                     file_type == G_FILE_TYPE_DIRECTORY ? G_DIR_SEPARATOR_S 
: "");
+
+              IDE_TRACE_MSG ("edit completion: %s", completed_command);
+
+              g_ptr_array_add (ar, completed_command);
+            }
+          g_object_unref (descendent);
+        }
+
+      IDE_EXIT;
+    }
+
+  IDE_EXIT;
+}
+
+static void
+gb_vim_complete_edit (GtkWidget *active_widget,
+                      const gchar   *line,
+                      GPtrArray     *ar)
+{
+  gchar **parts;
+
+  parts = g_strsplit (line, " ", 2);
+  if (parts [0] == NULL || parts [1] == NULL)
+    {
+      g_strfreev (parts);
+      return;
+    }
+
+  gb_vim_complete_edit_files (active_widget, parts [0], ar, parts [1]);
+
+  g_strfreev (parts);
+}
+
+static void
+gb_vim_complete_colorscheme (const gchar *line,
+                             GPtrArray   *ar)
+{
+  GtkSourceStyleSchemeManager *manager;
+  const gchar * const *scheme_ids;
+  const gchar *tmp;
+  g_autofree gchar *prefix = NULL;
+  gsize i;
+
+  manager = gtk_source_style_scheme_manager_get_default ();
+  scheme_ids = gtk_source_style_scheme_manager_get_scheme_ids (manager);
+
+  for (tmp = strchr (line, ' ');
+       tmp && *tmp && g_unichar_isspace (g_utf8_get_char (tmp));
+       tmp = g_utf8_next_char (tmp))
+    {
+      /* do nothing */
+    }
+
+  if (!tmp)
+    return;
+
+  prefix = g_strndup (line, tmp - line);
+
+  for (i = 0; scheme_ids [i]; i++)
+    {
+      const gchar *scheme_id = scheme_ids [i];
+
+      if (g_str_has_prefix (scheme_id, tmp))
+        {
+          gchar *item;
+
+          item = g_strdup_printf ("%s%s", prefix, scheme_id);
+          IDE_TRACE_MSG ("colorscheme: %s", item);
+          g_ptr_array_add (ar, item);
+        }
+    }
+}
+
+gchar **
+gb_vim_complete (GtkWidget   *active_widget,
+                 const gchar *line)
+{
+  GPtrArray *ar;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  ar = g_ptr_array_new ();
+
+  if (line != NULL)
+    {
+      if (IDE_IS_EDITOR_PAGE (active_widget))
+        {
+          if (g_str_has_prefix (line, "set "))
+            gb_vim_complete_set (line, ar);
+          else if (g_str_has_prefix (line, "colorscheme "))
+            gb_vim_complete_colorscheme (line, ar);
+        }
+
+      if (g_str_has_prefix (line, "e ") ||
+          g_str_has_prefix (line, "edit ") ||
+          g_str_has_prefix (line, "o ") ||
+          g_str_has_prefix (line, "open ") ||
+          g_str_has_prefix (line, "sp ") ||
+          g_str_has_prefix (line, "split ") ||
+          g_str_has_prefix (line, "vsp ") ||
+          g_str_has_prefix (line, "vsplit ") ||
+          g_str_has_prefix (line, "tabe "))
+          gb_vim_complete_edit (active_widget, line, ar);
+      else
+          gb_vim_complete_command (line, ar);
+    }
+
+  g_ptr_array_add (ar, NULL);
+
+  return (gchar **)g_ptr_array_free (ar, FALSE);
+}
+
+const gchar **
+gb_vim_commands (const gchar   *typed_text,
+                 const gchar ***descriptions)
+{
+  GPtrArray *ar = NULL;
+  GPtrArray *desc_ar = NULL;
+  g_auto(GStrv) parts = NULL;
+
+  g_assert (typed_text);
+  g_assert (descriptions);
+
+  parts = g_strsplit (typed_text, " ", 2);
+  ar = g_ptr_array_new ();
+  desc_ar = g_ptr_array_new ();
+
+  g_assert (parts != NULL);
+  g_assert (parts[0] != NULL);
+
+  for (guint i = 0; vim_commands[i].name; i++)
+    {
+      if (g_str_has_prefix (vim_commands[i].name, parts[0]))
+        {
+          g_ptr_array_add (ar, (gchar *)vim_commands[i].name);
+          g_ptr_array_add (desc_ar, (gchar *)vim_commands[i].description);
+        }
+    }
+
+  g_ptr_array_add (ar, NULL);
+  g_ptr_array_add (desc_ar, NULL);
+
+  *descriptions = (const gchar **)g_ptr_array_free (desc_ar, FALSE);
+
+  return (const gchar **)g_ptr_array_free (ar, FALSE);
+}
diff --git a/src/plugins/vim/gb-vim.h b/src/plugins/vim/gb-vim.h
new file mode 100644
index 000000000..49994fdd9
--- /dev/null
+++ b/src/plugins/vim/gb-vim.h
@@ -0,0 +1,48 @@
+/* gb-vim.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+G_BEGIN_DECLS
+
+#define GB_VIM_ERROR (gb_vim_error_quark())
+
+typedef enum
+{
+  GB_VIM_ERROR_NOT_IMPLEMENTED,
+  GB_VIM_ERROR_NOT_FOUND,
+  GB_VIM_ERROR_NOT_NUMBER,
+  GB_VIM_ERROR_NUMBER_OUT_OF_RANGE,
+  GB_VIM_ERROR_CANNOT_FIND_COLORSCHEME,
+  GB_VIM_ERROR_UNKNOWN_OPTION,
+  GB_VIM_ERROR_NOT_SOURCE_VIEW,
+  GB_VIM_ERROR_NO_VIEW
+} IdeVimError;
+
+GQuark        gb_vim_error_quark (void);
+gboolean      gb_vim_execute     (GtkWidget      *active_widget,
+                                  const gchar    *line,
+                                  GError        **error);
+gchar       **gb_vim_complete    (GtkWidget      *active_widget,
+                                  const gchar    *line);
+const gchar **gb_vim_commands    (const gchar    *typed_text,
+                                  const gchar  ***descriptions);
+
+G_END_DECLS
diff --git a/src/plugins/vim/gbp-vim-command-provider.c b/src/plugins/vim/gbp-vim-command-provider.c
new file mode 100644
index 000000000..e55d95aab
--- /dev/null
+++ b/src/plugins/vim/gbp-vim-command-provider.c
@@ -0,0 +1,122 @@
+/* gbp-vim-command-provider.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-vim-command-provider"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-editor.h>
+#include <libide-gui.h>
+#include <libide-threading.h>
+
+#include "gb-vim.h"
+#include "gbp-vim-command.h"
+#include "gbp-vim-command-provider.h"
+
+struct _GbpVimCommandProvider
+{
+  GObject parent_instance;
+};
+
+static void
+gbp_vim_command_provider_query_async (IdeCommandProvider  *provider,
+                                      IdeWorkspace        *workspace,
+                                      const gchar         *typed_text,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  GbpVimCommandProvider *self = (GbpVimCommandProvider *)provider;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) results = NULL;
+  g_autofree const gchar **commands = NULL;
+  g_autofree const gchar **descriptions = NULL;
+  IdePage *page;
+
+  g_assert (GBP_IS_VIM_COMMAND_PROVIDER (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+  g_assert (typed_text != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_vim_command_provider_query_async);
+
+  results = g_ptr_array_new_with_free_func (g_object_unref);
+  page = ide_workspace_get_most_recent_page (workspace);
+
+  if (!IDE_IS_EDITOR_PAGE (page))
+    goto no_active_widget;
+
+  commands = gb_vim_commands (typed_text, &descriptions);
+
+  for (guint i = 0; commands[i]; i++)
+    {
+      g_autoptr(GbpVimCommand) command = NULL;
+
+      command = gbp_vim_command_new (GTK_WIDGET (page),
+                                     typed_text,
+                                     g_dgettext (GETTEXT_PACKAGE, commands[i]),
+                                     g_dgettext (GETTEXT_PACKAGE, descriptions[i]));
+      g_ptr_array_add (results, g_steal_pointer (&command));
+    }
+
+no_active_widget:
+  ide_task_return_pointer (task,
+                           g_steal_pointer (&results),
+                           (GDestroyNotify)g_ptr_array_unref);
+}
+
+static GPtrArray *
+gbp_vim_command_provider_query_finish (IdeCommandProvider  *provider,
+                                       GAsyncResult        *result,
+                                       GError             **error)
+{
+  GbpVimCommandProvider *self = (GbpVimCommandProvider *)provider;
+  GPtrArray *ret;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_VIM_COMMAND_PROVIDER (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  return IDE_PTR_ARRAY_STEAL_FULL (&ret);
+}
+
+static void
+command_provider_iface_init (IdeCommandProviderInterface *iface)
+{
+  iface->query_async = gbp_vim_command_provider_query_async;
+  iface->query_finish = gbp_vim_command_provider_query_finish;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpVimCommandProvider, gbp_vim_command_provider, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_COMMAND_PROVIDER, command_provider_iface_init))
+
+static void
+gbp_vim_command_provider_class_init (GbpVimCommandProviderClass *klass)
+{
+}
+
+static void
+gbp_vim_command_provider_init (GbpVimCommandProvider *self)
+{
+}
diff --git a/src/libide/keybindings/ide-shortcuts-window.h b/src/plugins/vim/gbp-vim-command-provider.h
similarity index 71%
rename from src/libide/keybindings/ide-shortcuts-window.h
rename to src/plugins/vim/gbp-vim-command-provider.h
index 5ce88ce59..25a4bc03e 100644
--- a/src/libide/keybindings/ide-shortcuts-window.h
+++ b/src/plugins/vim/gbp-vim-command-provider.h
@@ -1,6 +1,6 @@
-/* ide-shortcuts-window.h
+/* gbp-vim-command-provider.h
  *
- * Copyright 2015-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,12 +20,12 @@
 
 #pragma once
 
-#include <gtk/gtk.h>
+#include <glib-object.h>
 
 G_BEGIN_DECLS
 
-#define IDE_TYPE_SHORTCUTS_WINDOW (ide_shortcuts_window_get_type())
+#define GBP_TYPE_VIM_COMMAND_PROVIDER (gbp_vim_command_provider_get_type())
 
-G_DECLARE_FINAL_TYPE (IdeShortcutsWindow, ide_shortcuts_window, IDE, SHORTCUTS_WINDOW, GtkShortcutsWindow)
+G_DECLARE_FINAL_TYPE (GbpVimCommandProvider, gbp_vim_command_provider, GBP, VIM_COMMAND_PROVIDER, GObject)
 
 G_END_DECLS
diff --git a/src/plugins/vim/gbp-vim-command.c b/src/plugins/vim/gbp-vim-command.c
new file mode 100644
index 000000000..5fb01cbe0
--- /dev/null
+++ b/src/plugins/vim/gbp-vim-command.c
@@ -0,0 +1,141 @@
+/* gbp-vim-command.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-vim-command"
+
+#include "config.h"
+
+#include <libide-gui.h>
+
+#include "gbp-vim-command.h"
+#include "gb-vim.h"
+
+struct _GbpVimCommand
+{
+  IdeObject  parent_instance;
+  GtkWidget *active_widget;
+  gchar     *typed_text;
+  gchar     *command;
+  gchar     *description;
+};
+
+static gchar *
+gbp_vim_command_get_title (IdeCommand *command)
+{
+  GbpVimCommand *self = (GbpVimCommand *)command;
+
+  return g_strdup (self->command);
+}
+
+static gchar *
+gbp_vim_command_get_subtitle (IdeCommand *command)
+{
+  GbpVimCommand *self = (GbpVimCommand *)command;
+
+  return g_strdup (self->description);
+}
+
+static void
+gbp_vim_command_run_async (IdeCommand          *command,
+                           GCancellable        *cancellable,
+                           GAsyncReadyCallback  callback,
+                           gpointer             user_data)
+{
+  GbpVimCommand *self = (GbpVimCommand *)command;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (GBP_IS_VIM_COMMAND (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_vim_command_run_async);
+
+  if (!gb_vim_execute (self->active_widget, self->typed_text, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gbp_vim_command_run_finish (IdeCommand    *command,
+                            GAsyncResult  *result,
+                            GError       **error)
+{
+  g_assert (GBP_IS_VIM_COMMAND (command));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+command_iface_init (IdeCommandInterface *iface)
+{
+  iface->get_title = gbp_vim_command_get_title;
+  iface->get_subtitle = gbp_vim_command_get_subtitle;
+  iface->run_async = gbp_vim_command_run_async;
+  iface->run_finish = gbp_vim_command_run_finish;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpVimCommand, gbp_vim_command, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_COMMAND, command_iface_init))
+
+static void
+gbp_vim_command_finalize (GObject *object)
+{
+  GbpVimCommand *self = (GbpVimCommand *)object;
+
+  g_clear_pointer (&self->typed_text, g_free);
+  g_clear_pointer (&self->command, g_free);
+  g_clear_pointer (&self->description, g_free);
+  g_clear_object (&self->active_widget);
+
+  G_OBJECT_CLASS (gbp_vim_command_parent_class)->finalize (object);
+}
+
+static void
+gbp_vim_command_class_init (GbpVimCommandClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gbp_vim_command_finalize;
+}
+
+static void
+gbp_vim_command_init (GbpVimCommand *self)
+{
+}
+
+GbpVimCommand *
+gbp_vim_command_new (GtkWidget   *active_widget,
+                     const gchar *typed_text,
+                     const gchar *command,
+                     const gchar *description)
+{
+  g_autoptr(GbpVimCommand) ret = NULL;
+
+  ret = g_object_new (GBP_TYPE_VIM_COMMAND, NULL);
+  ret->active_widget = g_object_ref (active_widget);
+  ret->typed_text = g_strdup (typed_text);
+  ret->command = g_strdup (command);
+  ret->description = g_strdup (description);
+
+  return g_steal_pointer (&ret);
+}
diff --git a/src/plugins/vim/gbp-vim-command.h b/src/plugins/vim/gbp-vim-command.h
new file mode 100644
index 000000000..79e5994d1
--- /dev/null
+++ b/src/plugins/vim/gbp-vim-command.h
@@ -0,0 +1,36 @@
+/* gbp-vim-command.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_VIM_COMMAND (gbp_vim_command_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpVimCommand, gbp_vim_command, GBP, VIM_COMMAND, IdeObject)
+
+GbpVimCommand *gbp_vim_command_new (GtkWidget   *active_widget,
+                                    const gchar *typed_text,
+                                    const gchar *command,
+                                    const gchar *description);
+
+G_END_DECLS
diff --git a/src/plugins/vim/gbp-vim-preferences-addin.c b/src/plugins/vim/gbp-vim-preferences-addin.c
new file mode 100644
index 000000000..fcb4d238e
--- /dev/null
+++ b/src/plugins/vim/gbp-vim-preferences-addin.c
@@ -0,0 +1,89 @@
+/* gbp-vim-preferences-addin.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-vim-preferences-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-gui.h>
+
+#include "gbp-vim-preferences-addin.h"
+
+struct _GbpVimPreferencesAddin
+{
+  GObject parent_instance;
+  guint   keybinding_id;
+};
+
+static void
+gbp_vim_preferences_addin_load (IdePreferencesAddin *addin,
+                                DzlPreferences      *preferences)
+{
+  GbpVimPreferencesAddin *self = (GbpVimPreferencesAddin *)addin;
+
+  g_assert (GBP_IS_VIM_PREFERENCES_ADDIN (self));
+  g_assert (DZL_IS_PREFERENCES (preferences));
+
+  self->keybinding_id = dzl_preferences_add_radio (preferences,
+                                                   "keyboard",
+                                                   "mode",
+                                                   "org.gnome.builder.editor",
+                                                   "keybindings",
+                                                   NULL,
+                                                   "\"vim\"",
+                                                   _("Vim"),
+                                                   _("Emulates the Vim text editor"),
+                                                   NULL,
+                                                   30);
+}
+
+static void
+gbp_vim_preferences_addin_unload (IdePreferencesAddin *addin,
+                                  DzlPreferences      *preferences)
+{
+  GbpVimPreferencesAddin *self = (GbpVimPreferencesAddin *)addin;
+
+  g_assert (GBP_IS_VIM_PREFERENCES_ADDIN (self));
+  g_assert (DZL_IS_PREFERENCES (preferences));
+
+  dzl_preferences_remove_id (preferences, self->keybinding_id);
+}
+
+static void
+preferences_addin_iface_init (IdePreferencesAddinInterface *iface)
+{
+  iface->load = gbp_vim_preferences_addin_load;
+  iface->unload = gbp_vim_preferences_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpVimPreferencesAddin, gbp_vim_preferences_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_PREFERENCES_ADDIN,
+                                                preferences_addin_iface_init))
+
+static void
+gbp_vim_preferences_addin_class_init (GbpVimPreferencesAddinClass *klass)
+{
+}
+
+static void
+gbp_vim_preferences_addin_init (GbpVimPreferencesAddin *self)
+{
+}
diff --git a/src/plugins/vim/gbp-vim-preferences-addin.h b/src/plugins/vim/gbp-vim-preferences-addin.h
new file mode 100644
index 000000000..7a8663cf4
--- /dev/null
+++ b/src/plugins/vim/gbp-vim-preferences-addin.h
@@ -0,0 +1,31 @@
+/* gbp-vim-preferences-addin.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_VIM_PREFERENCES_ADDIN (gbp_vim_preferences_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpVimPreferencesAddin, gbp_vim_preferences_addin, GBP, VIM_PREFERENCES_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/libide/keybindings/vim.css b/src/plugins/vim/keybindings/vim.css
similarity index 98%
rename from src/libide/keybindings/vim.css
rename to src/plugins/vim/keybindings/vim.css
index 05800d5b3..d339ec166 100644
--- a/src/libide/keybindings/vim.css
+++ b/src/plugins/vim/keybindings/vim.css
@@ -101,7 +101,7 @@
                    "hide-completion" ()
                    "set-mode" ("vim-normal", permanent) };
 
-  bind "<ctrl>k" { "action" ("layoutstack", "show-list", "") };
+  bind "<ctrl>k" { "action" ("frame", "show-list", "") };
   bind "<ctrl>minus" { "decrease-font-size" () };
   bind "<ctrl>plus" { "increase-font-size" () };
   bind "<ctrl>equal" { "increase-font-size" () };
@@ -164,24 +164,25 @@
   bind "KP_9" { "append-to-count" (9)
                 "set-mode" ("vim-normal-with-count", transient) };
 
-  bind "colon" { "action" ("win", "show-command-bar", "") };
+  bind "colon" { "action" ("win", "reveal-command-bar", "") };
 
   /* cycle "tabs" */
-  bind "<ctrl><alt>Page_Up" { "action" ("layoutstack", "previous-view", "") };
-  bind "<ctrl><alt>KP_Page_Up" { "action" ("layoutstack", "previous-view", "") };
-  bind "<ctrl><alt>Page_Down" { "action" ("layoutstack", "next-view", "") };
-  bind "<ctrl><alt>KP_Page_Down" { "action" ("layoutstack", "next-view", "") };
+  bind "<ctrl><alt>Page_Up" { "action" ("frame", "previous-page", "") };
+  bind "<ctrl><alt>KP_Page_Up" { "action" ("frame", "previous-page", "") };
+  bind "<ctrl><alt>Page_Down" { "action" ("frame", "next-page", "") };
+  bind "<ctrl><alt>KP_Page_Down" { "action" ("frame", "next-page", "") };
 
   /* replay the last recording */
   bind "period" { "replay-macro" (1) };
 
   /* start search backward */
   /* TODO: use internal sourceview search */
-  bind "question" { "action" ("editor-view", "find", "") };
+  bind "question" { "action" ("editor-page", "find", "") };
 
   /* start search */
-  bind "slash" { "action" ("editor-view", "find", "") };
-  bind "KP_Divide" { "action" ("editor-view", "find", "") };
+  bind "slash" { "action" ("editor-search", "at-word-boundaries", "false")
+                 "action" ("editor-page", "find", "") };
+  bind "KP_Divide" { "action" ("editor-page", "find", "") };
 
   /* insert at cursor */
   bind "i" { "begin-macro" ()
@@ -1573,8 +1574,8 @@
   bind "<shift>u" { "set-mode" ("vim-normal-g-u", transient) };
 
   /* cycle "tabs" */
-  bind "<shift>t" { "action" ("layoutstack", "previous-view", "") };
-  bind "t" { "action" ("layoutstack", "next-view", "") };
+  bind "<shift>t" { "action" ("frame", "previous-page", "") };
+  bind "t" { "action" ("frame", "next-page", "") };
 }
 
 @binding-set builder-vim-source-view-normal-g-u
@@ -1905,27 +1906,27 @@
 
 @binding-set builder-vim-source-view-normal-ctrl-w
 {
-  bind "v" { "action" ("layoutstack", "open-in-new-frame", "''") "grab_focus" () };
-  bind "<ctrl>v" { "action" ("layoutstack", "open-in-new-frame", "''") "grab_focus" () };
+  bind "v" { "action" ("frame", "open-in-new-frame", "''") "grab_focus" () };
+  bind "<ctrl>v" { "action" ("frame", "open-in-new-frame", "''") "grab_focus" () };
 
-  bind "c" { "action" ("layoutstack", "close-view", "") };
+  bind "c" { "action" ("frame", "close-page", "") };
 
-  bind "s" { "action" ("layoutstack", "split-view", "''") };
+  bind "s" { "action" ("frame", "split-page", "''") };
 
-  bind "w" { "action" ("layoutgrid", "focus-neighbor", "0") };
-  bind "<ctrl>w" { "action" ("layoutgrid", "focus-neighbor", "0") };
+  bind "w" { "action" ("grid", "focus-neighbor", "0") };
+  bind "<ctrl>w" { "action" ("grid", "focus-neighbor", "0") };
 
-  bind "l" { "action" ("layoutgrid", "focus-neighbor", "5") };
-  bind "Right" { "action" ("layoutgrid", "focus-neighbor", "5") };
+  bind "l" { "action" ("grid", "focus-neighbor", "5") };
+  bind "Right" { "action" ("grid", "focus-neighbor", "5") };
 
-  bind "h" { "action" ("layoutgrid", "focus-neighbor", "4") };
-  bind "Left" { "action" ("layoutgrid", "focus-neighbor", "4") };
+  bind "h" { "action" ("grid", "focus-neighbor", "4") };
+  bind "Left" { "action" ("grid", "focus-neighbor", "4") };
 
-  bind "j" { "action" ("layoutgrid", "focus-neighbor", "3") };
-  bind "Down" { "action" ("layoutgrid", "focus-neighbor", "3") };
+  bind "j" { "action" ("grid", "focus-neighbor", "3") };
+  bind "Down" { "action" ("grid", "focus-neighbor", "3") };
 
-  bind "k" { "action" ("layoutgrid", "focus-neighbor", "2") };
-  bind "Up" { "action" ("layoutgrid", "focus-neighbor", "2") };
+  bind "k" { "action" ("grid", "focus-neighbor", "2") };
+  bind "Up" { "action" ("grid", "focus-neighbor", "2") };
 }
 
 @binding-set builder-vim-source-view-visual-line-g
@@ -2010,7 +2011,7 @@
   bind "i" { "set-mode" ("vim-visual-i", transient) };
   bind "a" { "set-mode" ("vim-visual-a", transient) };
 
-  bind "colon" { "action" ("win", "show-command-bar", "") };
+  bind "colon" { "action" ("win", "reveal-command-bar", "") };
 
   bind "percent" { "move-to-matching-bracket" (1) };
 
@@ -2079,7 +2080,7 @@
              "set-mode" ("vim-normal", permanent) };
 
   bind "slash" { "set-search-text" ("", 1)
-                 "action" ("editor-view", "find", "") };
+                 "action" ("editor-page", "find", "") };
 
   bind "e" { "movement" (next-word-end, 1, 0, 1) };
   bind "<shift>e" { "movement" (next-full-word-end, 1, 0, 1) };
@@ -2375,7 +2376,7 @@ bind "KP_Multiply" { "save-insert-mark" ()
 
 @binding-set builder-vim-source-view-visual-line
 {
-  bind "colon" { "action" ("win", "show-command-bar", "") };
+  bind "colon" { "action" ("win", "reveal-command-bar", "") };
 
   bind "1" { "append-to-count" (1)
              "set-mode" ("vim-visual-line-with-count", transient) };
@@ -2546,7 +2547,7 @@ bind "KP_Multiply" { "save-insert-mark" ()
 
 @binding-set builder-gb-project-tree-vim
 {
-  bind "colon" { "action" ("win", "show-command-bar", "") };
+  bind "colon" { "action" ("win", "reveal-command-bar", "") };
 }
 
 @binding-set builder-vim-workbench
diff --git a/src/plugins/vim/meson.build b/src/plugins/vim/meson.build
new file mode 100644
index 000000000..8c20ea085
--- /dev/null
+++ b/src/plugins/vim/meson.build
@@ -0,0 +1,15 @@
+plugins_sources += files([
+  'vim-plugin.c',
+  'gb-vim.c',
+  'gbp-vim-command.c',
+  'gbp-vim-command-provider.c',
+  'gbp-vim-preferences-addin.c',
+])
+
+plugin_vim_resources = gnome.compile_resources(
+  'vim-resources',
+  'vim.gresource.xml',
+  c_name: 'gbp_vim',
+)
+
+plugins_sources += plugin_vim_resources[0]
diff --git a/src/plugins/vim/vim-plugin.c b/src/plugins/vim/vim-plugin.c
new file mode 100644
index 000000000..5a4cca438
--- /dev/null
+++ b/src/plugins/vim/vim-plugin.c
@@ -0,0 +1,41 @@
+/* vim-plugin.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "vim-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-core.h>
+#include <libide-gui.h>
+
+#include "gbp-vim-command-provider.h"
+#include "gbp-vim-preferences-addin.h"
+
+_IDE_EXTERN void
+_gbp_vim_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_PREFERENCES_ADDIN,
+                                              GBP_TYPE_VIM_PREFERENCES_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_COMMAND_PROVIDER,
+                                              GBP_TYPE_VIM_COMMAND_PROVIDER);
+}
diff --git a/src/plugins/vim/vim.gresource.xml b/src/plugins/vim/vim.gresource.xml
new file mode 100644
index 000000000..6d08eaaf2
--- /dev/null
+++ b/src/plugins/vim/vim.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/vim">
+    <file>vim.plugin</file>
+    <file>keybindings/vim.css</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/vim/vim.plugin b/src/plugins/vim/vim.plugin
new file mode 100644
index 000000000..5d9d0ef21
--- /dev/null
+++ b/src/plugins/vim/vim.plugin
@@ -0,0 +1,9 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2015-2018 Christian Hergert
+Description=Emulation of various VIM features
+Embedded=_gbp_vim_register_types
+Hidden=true
+Module=vim
+Name=VIM Emulation


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