[gnome-builder/wip/chergert/editorsearch: 1/7] editor: add new IdeEditorSearch abstraction



commit 77ce4721321c1bc2a2112e52aedf5fcdcee48c8c
Author: Christian Hergert <chergert redhat com>
Date:   Tue Oct 3 19:08:21 2017 -0700

    editor: add new IdeEditorSearch abstraction
    
    This attempts to unify a bunch of our editor search. We still need to
    adjust keybindings and other bits from the sourceview.
    
    In particular, we want to reduce how many GtkSourceSearchContext we have
    active at any given time. Also, we want better control over visibility of
    search results based on interactive search.

 src/libide/editor/ide-editor-private.h             |   32 +-
 src/libide/editor/ide-editor-search-bar-actions.c  |  154 ---
 .../editor/ide-editor-search-bar-shortcuts.c       |   31 +-
 src/libide/editor/ide-editor-search-bar.c          |  713 ++++-------
 src/libide/editor/ide-editor-search-bar.h          |   23 +-
 src/libide/editor/ide-editor-search-bar.ui         |   97 +-
 src/libide/editor/ide-editor-search.c              | 1417 ++++++++++++++++++++
 src/libide/editor/ide-editor-search.h              |   76 ++
 src/libide/editor/ide-editor-view-actions.c        |    4 +-
 src/libide/editor/ide-editor-view.c                |  163 +--
 src/libide/editor/ide-editor-view.h                |    2 +
 src/libide/editor/meson.build                      |    3 +-
 src/libide/ide.h                                   |    1 +
 src/libide/util/ide-action-group.h                 |  197 +++
 14 files changed, 2077 insertions(+), 836 deletions(-)
---
diff --git a/src/libide/editor/ide-editor-private.h b/src/libide/editor/ide-editor-private.h
index 844376a..a235f12 100644
--- a/src/libide/editor/ide-editor-private.h
+++ b/src/libide/editor/ide-editor-private.h
@@ -24,6 +24,7 @@
 
 #include "editor/ide-editor-perspective.h"
 #include "editor/ide-editor-properties.h"
+#include "editor/ide-editor-search.h"
 #include "editor/ide-editor-search-bar.h"
 #include "editor/ide-editor-sidebar.h"
 #include "editor/ide-editor-view-addin.h"
@@ -63,8 +64,7 @@ struct _IdeEditorView
   DzlBindingGroup         *buffer_bindings;
   DzlSignalGroup          *buffer_signals;
 
-  GtkSourceSearchSettings *search_settings;
-  GtkSourceSearchContext  *search_context;
+  IdeEditorSearch         *search;
 
   GCancellable            *destroy_cancellable;
 
@@ -89,38 +89,10 @@ struct _IdeEditorView
   guint                    show_map : 1;
 };
 
-struct _IdeEditorSearchBar
-{
-  DzlBin                   parent_instance;
-
-  /* Owned references */
-  DzlSignalGroup          *buffer_signals;
-  GtkSourceSearchContext  *context;
-  DzlSignalGroup          *context_signals;
-  GtkSourceSearchSettings *settings;
-  DzlSignalGroup          *settings_signals;
-  GdTaggedEntryTag        *search_entry_tag;
-
-  /* Template widgets */
-  GtkCheckButton          *case_sensitive;
-  GtkButton               *replace_all_button;
-  GtkButton               *replace_button;
-  GtkSearchEntry          *replace_entry;
-  GdTaggedEntry           *search_entry;
-  GtkGrid                 *search_options;
-  GtkCheckButton          *use_regex;
-  GtkCheckButton          *whole_word;
-
-  GSettings               *quick_highlight_settings;
-
-  guint                    quick_highlight_enabled : 1;
-};
-
 void _ide_editor_view_init_actions           (IdeEditorView        *self);
 void _ide_editor_view_init_settings          (IdeEditorView        *self);
 void _ide_editor_view_init_shortcuts         (IdeEditorView        *self);
 void _ide_editor_view_update_actions         (IdeEditorView        *self);
-void _ide_editor_search_bar_init_actions     (IdeEditorSearchBar   *self);
 void _ide_editor_search_bar_init_shortcuts   (IdeEditorSearchBar   *self);
 void _ide_editor_sidebar_set_open_pages      (IdeEditorSidebar     *self,
                                               GListModel           *open_pages);
diff --git a/src/libide/editor/ide-editor-search-bar-shortcuts.c 
b/src/libide/editor/ide-editor-search-bar-shortcuts.c
index 6dcfd4a..c2b265e 100644
--- a/src/libide/editor/ide-editor-search-bar-shortcuts.c
+++ b/src/libide/editor/ide-editor-search-bar-shortcuts.c
@@ -18,9 +18,27 @@
 
 #define G_LOG_DOMAIN "ide-editor-search-bar-shortcuts"
 
-#include "editor/ide-editor-private.h"
 #include "editor/ide-editor-search-bar.h"
 
+static void
+ide_editor_search_bar_shortcuts_activate_previous (GtkWidget *widget,
+                                                   gpointer   user_data)
+{
+  IdeEditorSearchBar *self = user_data;
+  IdeEditorSearch *search;
+
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
+
+  search = ide_editor_search_bar_get_search (self);
+
+  if (search != NULL)
+    {
+      ide_editor_search_move (search, IDE_EDITOR_SEARCH_PREVIOUS);
+      g_signal_emit_by_name (self, "stop-search");
+    }
+}
+
 void
 _ide_editor_search_bar_init_shortcuts (IdeEditorSearchBar *self)
 {
@@ -28,15 +46,22 @@ _ide_editor_search_bar_init_shortcuts (IdeEditorSearchBar *self)
 
   controller = dzl_shortcut_controller_find (GTK_WIDGET (self));
 
+  dzl_shortcut_controller_add_command_callback (controller,
+                                                "org.gnome.builder.editor.search-bar.activate-previous",
+                                                "<Shift>Return",
+                                                DZL_SHORTCUT_PHASE_BUBBLE,
+                                                ide_editor_search_bar_shortcuts_activate_previous,
+                                                self, NULL);
+
   dzl_shortcut_controller_add_command_action (controller,
                                               "org.gnome.builder.editor.search-bar.move-next",
                                               "Down",
                                               DZL_SHORTCUT_PHASE_BUBBLE,
-                                              "editor-view.move-next-search-result");
+                                              "editor-search.move-next");
 
   dzl_shortcut_controller_add_command_action (controller,
                                               "org.gnome.builder.editor.search-bar.move-previous",
                                               "Up",
                                               DZL_SHORTCUT_PHASE_BUBBLE,
-                                              "editor-view.move-previous-search-result");
+                                              "editor-search.move-previous");
 }
diff --git a/src/libide/editor/ide-editor-search-bar.c b/src/libide/editor/ide-editor-search-bar.c
index 3302f74..ebb6abc 100644
--- a/src/libide/editor/ide-editor-search-bar.c
+++ b/src/libide/editor/ide-editor-search-bar.c
@@ -18,18 +18,44 @@
 
 #define G_LOG_DOMAIN "ide-editor-search-bar"
 
+#include <dazzle.h>
 #include <glib/gi18n.h>
+#include <libgd/gd-tagged-entry.h>
 
-#include "ide-macros.h"
-
-#include "application/ide-application.h"
 #include "editor/ide-editor-private.h"
+#include "editor/ide-editor-search.h"
 #include "editor/ide-editor-search-bar.h"
 
+struct _IdeEditorSearchBar
+{
+  DzlBin                   parent_instance;
+
+  DzlSignalGroup          *search_signals;
+  DzlBindingGroup         *search_bindings;
+  IdeEditorSearch         *search;
+
+  GdTaggedEntryTag        *search_entry_tag;
+
+  GtkCheckButton          *case_sensitive;
+  GtkButton               *replace_all_button;
+  GtkButton               *replace_button;
+  GtkSearchEntry          *replace_entry;
+  GdTaggedEntry           *search_entry;
+  GtkGrid                 *search_options;
+  GtkCheckButton          *use_regex;
+  GtkCheckButton          *whole_word;
+  GtkLabel                *search_text_error;
+
+  guint                    match_source;
+
+  guint                    show_options : 1;
+  guint                    replace_mode : 1;
+};
+
 enum {
   PROP_0,
-  PROP_CONTEXT,
-  PROP_SETTINGS,
+  PROP_REPLACE_MODE,
+  PROP_SHOW_OPTIONS,
   N_PROPS
 };
 
@@ -48,7 +74,7 @@ ide_editor_search_bar_get_replace_mode (IdeEditorSearchBar *self)
 {
   g_return_val_if_fail (IDE_IS_EDITOR_SEARCH_BAR (self), FALSE);
 
-  return gtk_widget_get_visible (GTK_WIDGET (self->replace_entry));
+  return self->replace_mode;
 }
 
 void
@@ -57,9 +83,15 @@ ide_editor_search_bar_set_replace_mode (IdeEditorSearchBar *self,
 {
   g_return_if_fail (IDE_IS_EDITOR_SEARCH_BAR (self));
 
-  gtk_widget_set_visible (GTK_WIDGET (self->replace_entry), replace_mode);
-  gtk_widget_set_visible (GTK_WIDGET (self->replace_button), replace_mode);
-  gtk_widget_set_visible (GTK_WIDGET (self->replace_all_button), replace_mode);
+  replace_mode = !!replace_mode;
+
+  if (replace_mode != self->replace_mode)
+    {
+      self->replace_mode = replace_mode;
+      gtk_widget_set_visible (GTK_WIDGET (self->replace_entry), replace_mode);
+      gtk_widget_set_visible (GTK_WIDGET (self->replace_button), replace_mode);
+      gtk_widget_set_visible (GTK_WIDGET (self->replace_all_button), replace_mode);
+    }
 }
 
 static gboolean
@@ -69,19 +101,23 @@ maybe_escape_regex (GBinding     *binding,
                     gpointer      user_data)
 {
   IdeEditorSearchBar *self = user_data;
+  const gchar *entry_text;
 
   g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
   g_assert (from_value != NULL);
   g_assert (to_value != NULL);
 
-  if (g_value_get_string (from_value) == NULL)
-    g_value_set_static_string (to_value, "");
+  entry_text = g_value_get_string (from_value);
+
+  if (entry_text == NULL)
+    {
+      g_value_set_static_string (to_value, "");
+    }
   else
     {
-      const gchar *entry_text = g_value_get_string (from_value);
       g_autofree gchar *unescaped = NULL;
 
-      if (!gtk_source_search_settings_get_regex_enabled (self->settings))
+      if (self->search != NULL && !ide_editor_search_get_regex_enabled (self->search))
         entry_text = unescaped = gtk_source_utils_unescape_search_text (entry_text);
 
       g_value_set_string (to_value, entry_text);
@@ -98,6 +134,8 @@ pacify_null_text (GBinding     *binding,
 {
   g_assert (from_value != NULL);
   g_assert (to_value != NULL);
+  g_assert (G_VALUE_HOLDS_STRING (from_value));
+  g_assert (G_VALUE_HOLDS_STRING (to_value));
 
   if (g_value_get_string (from_value) == NULL)
     g_value_set_static_string (to_value, "");
@@ -108,424 +146,195 @@ pacify_null_text (GBinding     *binding,
 }
 
 static void
-update_replace_actions_sensitivity (IdeEditorSearchBar *self)
+ide_editor_search_bar_grab_focus (GtkWidget *widget)
 {
-  g_autoptr(GError) regex_error = NULL;
-  g_autoptr(GError) replace_regex_error = NULL;
-  GtkSourceBuffer *buffer;
-  GtkTextIter begin;
-  GtkTextIter end;
-  const gchar *search_text;
-  const gchar *replace_text;
-  gint pos;
-  gint count;
-  gboolean enable_replace;
-  gboolean enable_replace_all;
-  gboolean replace_regex_valid;
+  IdeEditorSearchBar *self = (IdeEditorSearchBar *)widget;
 
   g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
 
-  if (self->context == NULL || self->settings == NULL)
-    return;
-
-  buffer = gtk_source_search_context_get_buffer (self->context);
-
-  gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (buffer), &begin, &end);
-  replace_text = gtk_entry_get_text (GTK_ENTRY (self->replace_entry));
-
-  /* Gather enough info to determine if Replace or Replace All would make sense */
-  search_text = gtk_entry_get_text (GTK_ENTRY (self->search_entry));
-  pos = gtk_source_search_context_get_occurrence_position (self->context, &begin, &end);
-  count = gtk_source_search_context_get_occurrences_count (self->context);
-  regex_error = gtk_source_search_context_get_regex_error (self->context);
-  replace_regex_valid = gtk_source_search_settings_get_regex_enabled (self->settings) ?
-                        g_regex_check_replacement (replace_text, NULL, &replace_regex_error) :
-                        TRUE;
-
-  enable_replace = (!ide_str_empty0 (search_text) &&
-                    regex_error == NULL &&
-                    replace_regex_valid &&
-                    pos > 0);
-
-  enable_replace_all = (!ide_str_empty0 (search_text) &&
-                        regex_error == NULL &&
-                        replace_regex_valid &&
-                        count > 0);
-
-  dzl_gtk_widget_action_set (GTK_WIDGET (self), "search-bar", "replace",
-                             "enabled", enable_replace,
-                             NULL);
-  dzl_gtk_widget_action_set (GTK_WIDGET (self), "search-bar", "replace-all",
-                             "enabled", enable_replace_all,
-                             NULL);
+  /* Be careful to not reselect or it can reselect the whole
+   * entry text (causing next character to overwrite).
+   */
+  if (!gtk_widget_has_focus (GTK_WIDGET (self->search_entry)))
+    gtk_widget_grab_focus (GTK_WIDGET (self->search_entry));
 }
 
 static void
-on_notify_search_text (IdeEditorSearchBar      *self,
-                       GParamSpec              *pspec,
-                       GtkSourceSearchSettings *search_settings)
+search_entry_populate_popup (IdeEditorSearchBar *self,
+                             GtkWidget          *widget,
+                             GdTaggedEntry      *entry)
 {
-  GtkWidget *widget;
-  IdeEditorView *editor_view;
-  IdeSourceView *view;
-  GtkSourceSearchContext *view_search_context;
-  GtkSourceSearchSettings *view_search_settings;
-  const gchar *search_text;
-
   g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
-  g_assert (GTK_SOURCE_IS_SEARCH_SETTINGS (search_settings));
+  g_assert (GTK_IS_MENU (widget));
+  g_assert (GTK_IS_ENTRY (entry));
 
-  /* We set the view context search text for keymodes searching */
-  if (self->context == NULL)
+  if (GTK_IS_MENU (widget))
     {
-      if (NULL != (widget = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_EDITOR_VIEW)))
-        {
-          editor_view = IDE_EDITOR_VIEW (widget);
-          view = ide_editor_view_get_view (editor_view);
-
-          search_text = gtk_source_search_settings_get_search_text (search_settings);
+      DzlApplication *app = DZL_APPLICATION (g_application_get_default ());
+      GMenu *menu = dzl_application_get_menu_by_id (app, "ide-editor-search-bar-entry-menu");
 
-          if (NULL != (view_search_context = ide_source_view_get_search_context (view)))
-            {
-              view_search_settings = gtk_source_search_context_get_settings (view_search_context);
-              gtk_source_search_settings_set_search_text (view_search_settings, search_text);
-            }
-        }
+      gtk_menu_shell_bind_model (GTK_MENU_SHELL (widget), G_MENU_MODEL (menu), NULL, TRUE);
     }
-
-  update_replace_actions_sensitivity (self);
 }
 
 static void
-set_position_label (IdeEditorSearchBar *self,
-                    const gchar        *text)
+ide_editor_search_bar_real_stop_search (IdeEditorSearchBar *self)
 {
   g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
-
-  if (ide_str_empty0 (text))
-    {
-      if (self->search_entry_tag != NULL)
-        {
-          gd_tagged_entry_remove_tag (self->search_entry, self->search_entry_tag);
-          g_clear_object (&self->search_entry_tag);
-        }
-
-      return;
-    }
-
-  if (self->search_entry_tag == NULL)
-    {
-      self->search_entry_tag = gd_tagged_entry_tag_new ("");
-      gd_tagged_entry_add_tag (self->search_entry, self->search_entry_tag);
-      gd_tagged_entry_tag_set_style (self->search_entry_tag,
-                                     "search-occurrences-tag");
-    }
-
-  gd_tagged_entry_tag_set_label (self->search_entry_tag, text);
 }
 
 static void
-update_search_position_label (IdeEditorSearchBar *self)
+search_entry_stop_search (IdeEditorSearchBar *self,
+                          GtkSearchEntry     *entry)
 {
-  g_autofree gchar *text = NULL;
-  GtkStyleContext *context;
-  GtkSourceBuffer *buffer;
-  GtkTextIter begin;
-  GtkTextIter end;
-  const gchar *search_text;
-  gint count;
-  gint pos;
-
-  g_return_if_fail (IDE_IS_EDITOR_SEARCH_BAR (self));
-
-  if (self->settings == NULL || self->context == NULL)
-    return;
-
-  buffer = gtk_source_search_context_get_buffer (self->context);
-
-  gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (buffer), &begin, &end);
-  pos = gtk_source_search_context_get_occurrence_position (self->context, &begin, &end);
-  count = gtk_source_search_context_get_occurrences_count (self->context);
-
-  if ((pos == -1) || (count == -1))
-    {
-      /*
-       * We are not yet done scanning the buffer.
-       * We will be updated when we know more, so just hide it for now.
-       */
-      set_position_label (self, NULL);
-      return;
-    }
-
-  context = gtk_widget_get_style_context (GTK_WIDGET (self->search_entry));
-  search_text = gtk_entry_get_text (GTK_ENTRY (self->search_entry));
-
-  /* We use our own error class because we don't want to colide with styling
-   * from GTK+ themes.
-   */
-  if ((count == 0) && !ide_str_empty0 (search_text))
-    gtk_style_context_add_class (context, "search-missing");
-  else
-    gtk_style_context_remove_class (context, "search-missing");
+  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
+  g_assert (GTK_IS_SEARCH_ENTRY (entry));
 
-  /* translators: first %u is the Nth position of second %u N occurrences */
-  text = g_strdup_printf (_("%u of %u"), pos, count);
-  set_position_label (self, text);
+  g_signal_emit (self, signals [STOP_SEARCH], 0);
 }
 
 static void
-on_notify_occurrences_count (IdeEditorSearchBar     *self,
-                             GParamSpec             *pspec,
-                             GtkSourceSearchContext *search_context)
+search_entry_previous_match (IdeEditorSearchBar *self,
+                             GtkSearchEntry     *entry)
 {
   g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
-  g_assert (GTK_SOURCE_IS_SEARCH_CONTEXT (search_context));
+  g_assert (GTK_IS_SEARCH_ENTRY (entry));
 
-  update_search_position_label (self);
-  update_replace_actions_sensitivity (self);
+  if (self->search != NULL)
+    ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_BACKWARD);
 }
 
 static void
-on_cursor_moved (IdeEditorSearchBar *self,
-                 const GtkTextIter  *iter,
-                 IdeBuffer          *buffer)
+search_entry_next_match (IdeEditorSearchBar *self,
+                         GtkSearchEntry     *entry)
 {
-  gint count;
-
   g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
-  g_assert (iter != NULL);
-  g_assert (IDE_IS_BUFFER (buffer));
-
-  count = gtk_source_search_context_get_occurrences_count (self->context);
+  g_assert (GTK_IS_SEARCH_ENTRY (entry));
 
-  if (count != -1)
-    {
-      update_search_position_label (self);
-      update_replace_actions_sensitivity (self);
-    }
+  if (self->search != NULL)
+    ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_FORWARD);
 }
 
 static void
-on_notify_regex_error (IdeEditorSearchBar     *self,
-                       GParamSpec             *pspec,
-                       GtkSourceSearchContext *search_context)
+search_entry_activate (IdeEditorSearchBar *self,
+                       GdTaggedEntry      *entry)
 {
-  g_autoptr(GError) error = NULL;
-  PangoAttrList *attrs = NULL;
-  const gchar *tooltip_text = NULL;
-
   g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
-  g_assert (GTK_SOURCE_IS_SEARCH_CONTEXT (search_context));
-
-  /*
-   * If the regular expression is invalid, add a white squiggly underline;
-   * otherwise remove it. We will also set the tooltip-text to the error
-   * that occurred while parsing the regex.
-   */
+  g_assert (GD_IS_TAGGED_ENTRY (entry));
 
-  error = gtk_source_search_context_get_regex_error (search_context);
+  if (self->search != NULL)
+    ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_FORWARD);
 
-  if (error != NULL)
-    {
-      attrs = pango_attr_list_new ();
-      pango_attr_list_insert (attrs, pango_attr_underline_new (PANGO_UNDERLINE_ERROR));
-      pango_attr_list_insert (attrs, pango_attr_underline_color_new (65535, 65535, 65535));
-      tooltip_text = error->message;
-    }
-
-  gtk_entry_set_attributes (GTK_ENTRY (self->search_entry), attrs);
-  gtk_widget_set_tooltip_text (GTK_WIDGET (self->search_entry), tooltip_text);
-
-  update_replace_actions_sensitivity (self);
-
-  pango_attr_list_unref (attrs);
+  g_signal_emit (self, signals [STOP_SEARCH], 0);
 }
 
 static void
-check_replace_text (IdeEditorSearchBar *self)
+search_entry_changed (IdeEditorSearchBar *self,
+                      GdTaggedEntry      *entry)
 {
-  g_autoptr(GError) error = NULL;
-  PangoAttrList *attrs = NULL;
-  const gchar *tooltip_text = NULL;
-
   g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
-  g_assert (self->settings != NULL);
-
-  if (self->context == NULL)
-    return;
+  g_assert (GD_IS_TAGGED_ENTRY (entry));
 
   /*
-   * If the replace expression is invalid, add a white squiggly underline;
-   * otherwise remove it. Also set the error message to the tooltip text
-   * so that the user can get some info on the error.
+   * After the text has been changed, ask the IdeEditorSearch to see if
+   * the search request is valid. Highlight the invalid range of text with
+   * squigglies to denote what is broken.
+   *
+   * Also, add a tooltip to ensure that the user can figure out what they
+   * broke in the process.
    */
-  if (gtk_source_search_settings_get_regex_enabled (self->settings))
-    {
-      const gchar *replace_text;
 
-      replace_text = gtk_entry_get_text (GTK_ENTRY (self->replace_entry));
+  if (self->search != NULL)
+    {
+      g_autoptr(GError) error = NULL;
+      PangoAttrList *attrs = NULL;
+      guint begin = 0;
+      guint end = 0;
 
-      if (!g_regex_check_replacement (replace_text, NULL, &error))
+      if (ide_editor_search_get_search_text_invalid (self->search, &begin, &end, &error))
         {
-          attrs = pango_attr_list_new ();
-          pango_attr_list_insert (attrs, pango_attr_underline_new (PANGO_UNDERLINE_ERROR));
-          pango_attr_list_insert (attrs, pango_attr_underline_color_new (65535, 65535, 65535));
-          tooltip_text = error->message;
-        }
-    }
-
-  gtk_entry_set_attributes (GTK_ENTRY (self->replace_entry), attrs);
-  gtk_widget_set_tooltip_text (GTK_WIDGET (self->replace_entry), tooltip_text);
+          PangoAttribute *attr;
 
-  pango_attr_list_unref (attrs);
-}
-
-static void
-on_notify_regex_enabled (IdeEditorSearchBar      *self,
-                         GParamSpec              *pspec,
-                         GtkSourceSearchSettings *search_settings)
-{
-  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
-  g_assert (GTK_SOURCE_IS_SEARCH_SETTINGS (search_settings));
+          attrs = pango_attr_list_new ();
 
-  check_replace_text (self);
-}
+          attr = pango_attr_underline_new (PANGO_UNDERLINE_ERROR);
+          attr->start_index = begin;
+          attr->end_index = end;
+          pango_attr_list_insert (attrs, attr);
 
-static void
-ide_editor_search_bar_grab_focus (GtkWidget *widget)
-{
-  IdeEditorSearchBar *self = (IdeEditorSearchBar *)widget;
+          attr = pango_attr_underline_color_new (65535, 0, 0);
+          pango_attr_list_insert (attrs, attr);
+        }
 
-  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
+      gtk_entry_set_attributes (GTK_ENTRY (entry), attrs);
+      gtk_label_set_label (self->search_text_error,
+                           error ? error->message : NULL);
+      gtk_widget_set_visible (GTK_WIDGET (self->search_text_error), error != NULL);
 
-  /* Be careful to not reselect or it can reselect the whole
-   * entry text (causing next character to overwrite).
-   */
-  if (!gtk_widget_has_focus (GTK_WIDGET (self->search_entry)))
-    gtk_widget_grab_focus (GTK_WIDGET (self->search_entry));
+      g_clear_pointer (&attrs, pango_attr_list_unref);
+    }
 }
 
-static void
-ide_editor_search_bar_bind_context (IdeEditorSearchBar     *self,
-                                    GtkSourceSearchContext *context,
-                                    DzlSignalGroup         *context_signals)
+static gboolean
+update_match_positions (gpointer user_data)
 {
-  GtkSourceBuffer *buffer;
+  IdeEditorSearchBar *self = user_data;
+  g_autofree gchar *str = NULL;
+  guint count;
+  guint pos;
 
   g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
-  g_assert (GTK_SOURCE_IS_SEARCH_CONTEXT (context));
-  g_assert (DZL_IS_SIGNAL_GROUP (context_signals));
 
-  self->quick_highlight_enabled = g_settings_get_boolean (self->quick_highlight_settings, "enabled");
-  if (self->quick_highlight_enabled)
-    g_settings_set_boolean (self->quick_highlight_settings, "enabled", FALSE);
-
-  buffer = gtk_source_search_context_get_buffer (context);
-  dzl_signal_group_set_target (self->buffer_signals, buffer);
-}
+  self->match_source = 0;
 
-static void
-ide_editor_search_bar_unbind_context (IdeEditorSearchBar *self,
-                                      DzlSignalGroup     *context_signals)
-{
-  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
-  g_assert (DZL_IS_SIGNAL_GROUP (context_signals));
+  count = ide_editor_search_get_match_count (self->search);
+  pos = ide_editor_search_get_match_position (self->search);
 
-  if (self->quick_highlight_enabled)
-    g_settings_set_boolean (self->quick_highlight_settings, "enabled", TRUE);
-
-  if (self->buffer_signals != NULL)
-    dzl_signal_group_set_target (self->buffer_signals, NULL);
-}
-
-static void
-ide_editor_search_bar_bind_settings (IdeEditorSearchBar      *self,
-                                     GtkSourceSearchSettings *settings,
-                                     DzlSignalGroup          *settings_signals)
-{
-  g_autoptr(DzlPropertiesGroup) group = NULL;
-
-  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
-  g_assert (GTK_SOURCE_IS_SEARCH_SETTINGS (settings));
-  g_assert (DZL_IS_SIGNAL_GROUP (settings_signals));
-
-  g_object_bind_property_full (self->search_entry, "text",
-                               settings, "search-text",
-                               G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL,
-                               maybe_escape_regex, pacify_null_text,
-                               self, NULL);
-
-  group = dzl_properties_group_new (G_OBJECT (settings));
-  dzl_properties_group_add_all_properties (group);
-  gtk_widget_insert_action_group (GTK_WIDGET (self),
-                                  "search-settings",
-                                  G_ACTION_GROUP (group));
-}
-
-static void
-search_entry_populate_popup (IdeEditorSearchBar *self,
-                             GtkWidget          *widget,
-                             GdTaggedEntry      *entry)
-{
-  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
-  g_assert (GTK_IS_MENU (widget));
-  g_assert (GTK_IS_ENTRY (entry));
+  if (count > 0)
+    str = g_strdup_printf (_("%u of %u"), pos, count);
 
-  if (GTK_IS_MENU (widget))
+  if (str == NULL)
     {
-      DzlApplication *app = DZL_APPLICATION (IDE_APPLICATION_DEFAULT);
-      GMenu *menu = dzl_application_get_menu_by_id (app, "ide-editor-search-bar-entry-menu");
-      gtk_menu_shell_bind_model (GTK_MENU_SHELL (widget), G_MENU_MODEL (menu), NULL, TRUE);
+      if (self->search_entry_tag != NULL)
+        {
+          gd_tagged_entry_remove_tag (self->search_entry,
+                                      self->search_entry_tag);
+          g_clear_object (&self->search_entry_tag);
+        }
     }
-}
-
-static void
-search_entry_stop_search (IdeEditorSearchBar *self,
-                          GtkSearchEntry     *entry)
-{
-  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
-  g_assert (GTK_IS_SEARCH_ENTRY (entry));
-
-  g_signal_emit (self, signals [STOP_SEARCH], 0);
-}
-
-static void
-search_entry_previous_match (IdeEditorSearchBar *self,
-                             GtkSearchEntry     *entry)
-{
-  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
-  g_assert (GTK_IS_SEARCH_ENTRY (entry));
-
-  dzl_gtk_widget_action (GTK_WIDGET (self),
-                         "editor-view",
-                         "move-previous-search-result",
-                         NULL);
-}
+  else
+    {
+      if (self->search_entry_tag == NULL)
+        {
+          self->search_entry_tag = gd_tagged_entry_tag_new ("");
+          gd_tagged_entry_add_tag (self->search_entry, self->search_entry_tag);
+          gd_tagged_entry_tag_set_style (self->search_entry_tag,
+                                         "search-occurrences-tag");
+        }
 
-static void
-search_entry_next_match (IdeEditorSearchBar *self,
-                         GtkSearchEntry     *entry)
-{
-  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
-  g_assert (GTK_IS_SEARCH_ENTRY (entry));
+      gd_tagged_entry_tag_set_label (self->search_entry_tag, str);
+    }
 
-  dzl_gtk_widget_action (GTK_WIDGET (self),
-                         "editor-view",
-                         "move-next-search-result",
-                         NULL);
+  return G_SOURCE_REMOVE;
 }
 
 static void
-search_entry_activate (IdeEditorSearchBar *self,
-                       GtkSearchEntry     *entry)
+ide_editor_search_bar_notify_match (IdeEditorSearchBar *self,
+                                    GParamSpec         *pspec,
+                                    IdeEditorSearch    *search)
 {
   g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
-  g_assert (GTK_IS_SEARCH_ENTRY (entry));
+  g_assert (IDE_IS_EDITOR_SEARCH (search));
 
-  dzl_gtk_widget_action (GTK_WIDGET (self),
-                         "editor-view",
-                         "activate-next-search-result",
-                         NULL);
+  /* Queue an update to our match positions, but only
+   * do so after returning to the main loop to avoid
+   * doing lots of extra work during heavy scanning.
+   */
+  if (self->match_source == 0)
+    self->match_source = gdk_threads_add_idle_full (G_PRIORITY_LOW,
+                                                    update_match_positions,
+                                                    g_object_ref (self),
+                                                    g_object_unref);
 }
 
 static void
@@ -533,13 +342,12 @@ ide_editor_search_bar_destroy (GtkWidget *widget)
 {
   IdeEditorSearchBar *self = (IdeEditorSearchBar *)widget;
 
-  g_clear_object (&self->buffer_signals);
-  g_clear_object (&self->context);
-  g_clear_object (&self->context_signals);
+  dzl_clear_source (&self->match_source);
+
+  g_clear_object (&self->search_signals);
+  g_clear_object (&self->search_bindings);
+  g_clear_object (&self->search);
   g_clear_object (&self->search_entry_tag);
-  g_clear_object (&self->settings);
-  g_clear_object (&self->settings_signals);
-  g_clear_object (&self->quick_highlight_settings);
 
   GTK_WIDGET_CLASS (ide_editor_search_bar_parent_class)->destroy (widget);
 }
@@ -554,12 +362,12 @@ ide_editor_search_bar_get_property (GObject    *object,
 
   switch (prop_id)
     {
-    case PROP_CONTEXT:
-      g_value_set_object (value, self->context);
+    case PROP_REPLACE_MODE:
+      g_value_set_boolean (value, ide_editor_search_bar_get_replace_mode (self));
       break;
 
-    case PROP_SETTINGS:
-      g_value_set_object (value, self->settings);
+    case PROP_SHOW_OPTIONS:
+      g_value_set_boolean (value, ide_editor_search_bar_get_show_options (self));
       break;
 
     default:
@@ -577,12 +385,12 @@ ide_editor_search_bar_set_property (GObject      *object,
 
   switch (prop_id)
     {
-    case PROP_CONTEXT:
-      ide_editor_search_bar_set_context (self, g_value_get_object (value));
+    case PROP_REPLACE_MODE:
+      ide_editor_search_bar_set_replace_mode (self, g_value_get_boolean (value));
       break;
 
-    case PROP_SETTINGS:
-      ide_editor_search_bar_set_settings (self, g_value_get_object (value));
+    case PROP_SHOW_OPTIONS:
+      ide_editor_search_bar_set_show_options (self, g_value_get_boolean (value));
       break;
 
     default:
@@ -602,30 +410,24 @@ ide_editor_search_bar_class_init (IdeEditorSearchBarClass *klass)
   widget_class->destroy = ide_editor_search_bar_destroy;
   widget_class->grab_focus = ide_editor_search_bar_grab_focus;
 
-  properties [PROP_CONTEXT] =
-    g_param_spec_object ("context",
-                         "Context",
-                         "The search context for locating matches",
-                         GTK_SOURCE_TYPE_SEARCH_CONTEXT,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+  properties [PROP_REPLACE_MODE] =
+    g_param_spec_boolean ("replace-mode", NULL, NULL, FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
 
-  properties [PROP_SETTINGS] =
-    g_param_spec_object ("settings",
-                         "Settings",
-                         "The search settings for locating matches",
-                         GTK_SOURCE_TYPE_SEARCH_SETTINGS,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+  properties [PROP_SHOW_OPTIONS] =
+    g_param_spec_boolean ("show-options", NULL, NULL, FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
   signals [STOP_SEARCH] =
-    g_signal_new ("stop-search",
-                  G_TYPE_FROM_CLASS (klass),
-                  G_SIGNAL_RUN_LAST,
-                  0,
-                  NULL, NULL,
-                  NULL,
-                  G_TYPE_NONE, 0);
+    g_signal_new_class_handler ("stop-search",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (ide_editor_search_bar_real_stop_search),
+                                NULL, NULL,
+                                g_cclosure_marshal_VOID__VOID,
+                                G_TYPE_NONE, 0);
 
   gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/ide-editor-search-bar.ui");
   gtk_widget_class_bind_template_child (widget_class, IdeEditorSearchBar, case_sensitive);
@@ -634,6 +436,7 @@ ide_editor_search_bar_class_init (IdeEditorSearchBarClass *klass)
   gtk_widget_class_bind_template_child (widget_class, IdeEditorSearchBar, replace_entry);
   gtk_widget_class_bind_template_child (widget_class, IdeEditorSearchBar, search_entry);
   gtk_widget_class_bind_template_child (widget_class, IdeEditorSearchBar, search_options);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorSearchBar, search_text_error);
   gtk_widget_class_bind_template_child (widget_class, IdeEditorSearchBar, use_regex);
   gtk_widget_class_bind_template_child (widget_class, IdeEditorSearchBar, whole_word);
 
@@ -647,58 +450,52 @@ ide_editor_search_bar_init (IdeEditorSearchBar *self)
 {
   gtk_widget_init_template (GTK_WIDGET (self));
 
-  g_signal_connect_swapped (self->search_entry,
-                            "activate",
-                            G_CALLBACK (search_entry_activate),
-                            self);
-
-  self->buffer_signals = dzl_signal_group_new (IDE_TYPE_BUFFER);
+  self->search_signals = dzl_signal_group_new (IDE_TYPE_EDITOR_SEARCH);
 
-  dzl_signal_group_connect_swapped (self->buffer_signals,
-                                    "cursor-moved",
-                                    G_CALLBACK (on_cursor_moved),
+  dzl_signal_group_connect_swapped (self->search_signals,
+                                    "notify::match-count",
+                                    G_CALLBACK (ide_editor_search_bar_notify_match),
                                     self);
 
-  self->context_signals = dzl_signal_group_new (GTK_SOURCE_TYPE_SEARCH_CONTEXT);
-
-  dzl_signal_group_connect_swapped (self->context_signals,
-                                    "notify::occurrences-count",
-                                    G_CALLBACK (on_notify_occurrences_count),
+  dzl_signal_group_connect_swapped (self->search_signals,
+                                    "notify::match-position",
+                                    G_CALLBACK (ide_editor_search_bar_notify_match),
                                     self);
 
-  dzl_signal_group_connect_swapped (self->context_signals,
-                                    "notify::regex-error",
-                                    G_CALLBACK (on_notify_regex_error),
-                                    self);
+  self->search_bindings = dzl_binding_group_new ();
 
-  g_signal_connect_swapped (self->context_signals,
-                            "bind",
-                            G_CALLBACK (ide_editor_search_bar_bind_context),
-                            self);
+  dzl_binding_group_bind_full (self->search_bindings, "search-text",
+                               self->search_entry, "text",
+                               G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL,
+                               maybe_escape_regex, pacify_null_text, self, NULL);
 
-  g_signal_connect_swapped (self->context_signals,
-                            "unbind",
-                            G_CALLBACK (ide_editor_search_bar_unbind_context),
-                            self);
+  dzl_binding_group_bind_full (self->search_bindings, "replacement-text",
+                               self->replace_entry, "text",
+                               G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL,
+                               pacify_null_text, pacify_null_text, NULL, NULL);
 
-  self->settings_signals = dzl_signal_group_new (GTK_SOURCE_TYPE_SEARCH_SETTINGS);
+  dzl_binding_group_bind (self->search_bindings, "regex-enabled",
+                          self->use_regex, "active",
+                          G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
 
-  dzl_signal_group_connect_swapped (self->settings_signals,
-                                    "notify::search-text",
-                                    G_CALLBACK (on_notify_search_text),
-                                    self);
+  dzl_binding_group_bind (self->search_bindings, "at-word-boundaries",
+                          self->whole_word, "active",
+                          G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
 
-  dzl_signal_group_connect_swapped (self->settings_signals,
-                                    "notify::regex-enabled",
-                                    G_CALLBACK (on_notify_regex_enabled),
-                                    self);
+  dzl_binding_group_bind (self->search_bindings, "case-sensitive",
+                          self->case_sensitive, "active",
+                          G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
 
-  g_signal_connect_swapped (self->settings_signals,
-                            "bind",
-                            G_CALLBACK (ide_editor_search_bar_bind_settings),
+  g_signal_connect_swapped (self->search_entry,
+                            "activate",
+                            G_CALLBACK (search_entry_activate),
                             self);
 
-  dzl_widget_action_group_attach (self->search_entry, "entry");
+  g_signal_connect_data (self->search_entry,
+                         "changed",
+                         G_CALLBACK (search_entry_changed),
+                         self, NULL,
+                         G_CONNECT_SWAPPED | G_CONNECT_AFTER);
 
   g_signal_connect_swapped (self->search_entry,
                             "populate-popup",
@@ -720,58 +517,58 @@ ide_editor_search_bar_init (IdeEditorSearchBar *self)
                             G_CALLBACK (search_entry_next_match),
                             self);
 
-  self->quick_highlight_settings =
-    g_settings_new_with_path ("org.gnome.builder.extension-type",
-                              
"/org/gnome/builder/extension-types/quick-highlight-plugin/GbpQuickHighlightViewAddin/");
-
-  _ide_editor_search_bar_init_actions (self);
   _ide_editor_search_bar_init_shortcuts (self);
 }
 
-GtkWidget *
-ide_editor_search_bar_new (void)
+gboolean
+ide_editor_search_bar_get_show_options (IdeEditorSearchBar *self)
 {
-  return g_object_new (IDE_TYPE_EDITOR_SEARCH_BAR, NULL);
+  g_return_val_if_fail (IDE_IS_EDITOR_SEARCH_BAR (self), FALSE);
+
+  return self->show_options;
 }
 
 void
-ide_editor_search_bar_set_settings (IdeEditorSearchBar      *self,
-                                    GtkSourceSearchSettings *settings)
+ide_editor_search_bar_set_show_options (IdeEditorSearchBar *self,
+                                        gboolean            show_options)
 {
   g_return_if_fail (IDE_IS_EDITOR_SEARCH_BAR (self));
-  g_return_if_fail (!settings || GTK_SOURCE_IS_SEARCH_SETTINGS (settings));
 
-  if (g_set_object (&self->settings, settings))
+  show_options = !!show_options;
+
+  if (self->show_options != show_options)
     {
-      dzl_signal_group_set_target (self->settings_signals, settings);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SETTINGS]);
+      self->show_options = show_options;
+      gtk_widget_set_visible (GTK_WIDGET (self->search_options), show_options);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_OPTIONS]);
     }
 }
 
-void
-ide_editor_search_bar_set_context (IdeEditorSearchBar     *self,
-                                   GtkSourceSearchContext *context)
+/**
+ * ide_editor_search_bar_get_search:
+ * @self: a #IdeEditorSearchBar
+ *
+ * Gets the #IdeEditorSearch used by the search bar.
+ *
+ * Returns: (transfer none) (nullable): An #IdeEditorSearch or %NULL.
+ */
+IdeEditorSearch *
+ide_editor_search_bar_get_search (IdeEditorSearchBar *self)
 {
-  g_return_if_fail (IDE_IS_EDITOR_SEARCH_BAR (self));
-  g_return_if_fail (!context || GTK_SOURCE_IS_SEARCH_CONTEXT (context));
+  g_return_val_if_fail (IDE_IS_EDITOR_SEARCH_BAR (self), NULL);
 
-
-  if (g_set_object (&self->context, context))
-    {
-      dzl_signal_group_set_target (self->context_signals, context);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONTEXT]);
-    }
+  return self->search;
 }
 
 void
-ide_editor_search_bar_set_search_text (IdeEditorSearchBar *self,
-                                       const gchar        *search_text)
+ide_editor_search_bar_set_search (IdeEditorSearchBar *self,
+                                  IdeEditorSearch    *search)
 {
   g_return_if_fail (IDE_IS_EDITOR_SEARCH_BAR (self));
 
-  if (search_text == NULL)
-    search_text = "";
-
-  if (self->settings != NULL)
-    gtk_source_search_settings_set_search_text (self->settings, search_text);
+  if (g_set_object (&self->search, search))
+    {
+      dzl_signal_group_set_target (self->search_signals, search);
+      dzl_binding_group_set_source (self->search_bindings, search);
+    }
 }
diff --git a/src/libide/editor/ide-editor-search-bar.h b/src/libide/editor/ide-editor-search-bar.h
index 41ad333..15c30f7 100644
--- a/src/libide/editor/ide-editor-search-bar.h
+++ b/src/libide/editor/ide-editor-search-bar.h
@@ -19,9 +19,9 @@
 #pragma once
 
 #include <dazzle.h>
+#include <gtksourceview/gtksource.h>
 
-#include "buffers/ide-buffer.h"
-#include "sourceview/ide-source-view.h"
+#include "editor/ide-editor-search.h"
 
 G_BEGIN_DECLS
 
@@ -29,15 +29,14 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (IdeEditorSearchBar, ide_editor_search_bar, IDE, EDITOR_SEARCH_BAR, DzlBin)
 
-GtkWidget *ide_editor_search_bar_new              (void);
-void       ide_editor_search_bar_set_search_text  (IdeEditorSearchBar      *self,
-                                                   const gchar             *word);
-gboolean   ide_editor_search_bar_get_replace_mode (IdeEditorSearchBar      *self);
-void       ide_editor_search_bar_set_replace_mode (IdeEditorSearchBar      *self,
-                                                   gboolean                 replace_mode);
-void       ide_editor_search_bar_set_context      (IdeEditorSearchBar      *self,
-                                                   GtkSourceSearchContext  *context);
-void       ide_editor_search_bar_set_settings     (IdeEditorSearchBar      *self,
-                                                   GtkSourceSearchSettings *settings);
+IdeEditorSearch *ide_editor_search_bar_get_search       (IdeEditorSearchBar *self);
+void             ide_editor_search_bar_set_search       (IdeEditorSearchBar *self,
+                                                         IdeEditorSearch    *search);
+gboolean         ide_editor_search_bar_get_show_options (IdeEditorSearchBar *self);
+void             ide_editor_search_bar_set_show_options (IdeEditorSearchBar *self,
+                                                         gboolean            show_options);
+gboolean         ide_editor_search_bar_get_replace_mode (IdeEditorSearchBar *self);
+void             ide_editor_search_bar_set_replace_mode (IdeEditorSearchBar *self,
+                                                         gboolean            replace_mode);
 
 G_END_DECLS
diff --git a/src/libide/editor/ide-editor-search-bar.ui b/src/libide/editor/ide-editor-search-bar.ui
index 0472b1d..08332db 100644
--- a/src/libide/editor/ide-editor-search-bar.ui
+++ b/src/libide/editor/ide-editor-search-bar.ui
@@ -12,14 +12,14 @@
         <child>
           <object class="GtkGrid">
             <property name="visible">true</property>
-            <property name="can_focus">false</property>
+            <property name="can-focus">false</property>
             <property name="row_spacing">8</property>
             <property name="column_spacing">8</property>
             <child>
               <object class="GdTaggedEntry" id="search_entry">
                 <property name="visible">true</property>
                 <property name="tag-close-visible">false</property>
-                <property name="can_focus">true</property>
+                <property name="can-focus">true</property>
                 <property name="hexpand">true</property>
                 <property name="primary_icon_name">edit-find-symbolic</property>
                 <property name="primary_icon_activatable">false</property>
@@ -31,9 +31,26 @@
               </packing>
             </child>
             <child>
+              <object class="GtkLabel" id="search_text_error">
+                <property name="visible">false</property>
+                <property name="xalign">0.0</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+                <attributes>
+                  <attribute name="scale" value="0.8333"/>
+                </attributes>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="width">3</property>
+                <property name="top_attach">1</property>
+              </packing>
+            </child>
+            <child>
               <object class="GtkSearchEntry" id="replace_entry">
                 <property name="visible">false</property>
-                <property name="can_focus">true</property>
+                <property name="can-focus">true</property>
                 <property name="width-chars">20</property>
                 <property name="max-width-chars">30</property>
                 <property name="primary_icon_name">edit-find-replace-symbolic</property>
@@ -42,28 +59,27 @@
               </object>
               <packing>
                 <property name="left_attach">0</property>
-                <property name="top_attach">1</property>
+                <property name="top_attach">2</property>
               </packing>
             </child>
             <child>
               <object class="GtkBox">
                 <property name="homogeneous">true</property>
                 <property name="visible">true</property>
-                <property name="can_focus">false</property>
+                <property name="can-focus">false</property>
                 <property name="valign">center</property>
                 <style>
                   <class name="linked"/>
                 </style>
                 <child>
                   <object class="GtkButton">
-                    <property name="action-name">editor-view.move-previous-search-result</property>
+                    <property name="action-name">editor-search.move-previous</property>
                     <property name="visible">true</property>
-                    <property name="can_focus">false</property>
-                    <property name="receives_default">true</property>
+                    <property name="can-focus">false</property>
                     <child>
                       <object class="GtkImage">
                         <property name="visible">true</property>
-                        <property name="can_focus">false</property>
+                        <property name="can-focus">false</property>
                         <property name="icon_name">go-up-symbolic</property>
                         <property name="icon_size">1</property>
                       </object>
@@ -77,14 +93,13 @@
                 </child>
                 <child>
                   <object class="GtkButton">
-                    <property name="action-name">editor-view.move-next-search-result</property>
+                    <property name="action-name">editor-search.move-next</property>
                     <property name="visible">true</property>
-                    <property name="can_focus">false</property>
-                    <property name="receives_default">true</property>
+                    <property name="can-focus">false</property>
                     <child>
                       <object class="GtkImage">
                         <property name="visible">true</property>
-                        <property name="can_focus">false</property>
+                        <property name="can-focus">false</property>
                         <property name="icon_name">go-down-symbolic</property>
                         <property name="icon_size">1</property>
                       </object>
@@ -105,49 +120,45 @@
             <child>
               <object class="GtkButton" id="replace_button">
                 <property name="label" translatable="yes">Replace</property>
-                <property name="action-name">search-bar.replace</property>
+                <property name="action-name">editor-search.replace</property>
                 <property name="visible">false</property>
-                <property name="can_focus">true</property>
-                <property name="receives_default">true</property>
+                <property name="can-focus">true</property>
               </object>
               <packing>
                 <property name="left_attach">1</property>
-                <property name="top_attach">1</property>
+                <property name="top_attach">2</property>
               </packing>
             </child>
             <child>
               <object class="GtkButton" id="replace_all_button">
                 <property name="label" translatable="yes">Replace All</property>
-                <property name="action-name">search-bar.replace-all</property>
+                <property name="action-name">editor-search.replace-all</property>
                 <property name="visible">false</property>
-                <property name="can_focus">true</property>
-                <property name="receives_default">true</property>
+                <property name="can-focus">true</property>
               </object>
               <packing>
                 <property name="left_attach">2</property>
-                <property name="top_attach">1</property>
+                <property name="top_attach">2</property>
               </packing>
             </child>
             <child>
               <object class="GtkBox">
                 <property name="homogeneous">true</property>
                 <property name="visible">true</property>
-                <property name="can_focus">false</property>
+                <property name="can-focus">false</property>
                 <property name="valign">center</property>
                 <property name="spacing">8</property>
                 <child>
                   <object class="GtkToggleButton">
-                    <property name="action-name">search-bar.toggle-search-replace</property>
-                    <property name="action-target">true</property>
+                    <property name="active" bind-source="IdeEditorSearchBar" bind-property="replace-mode" 
bind-flags="sync-create|bidirectional"/>
                     <property name="tooltip-text" translatable="yes">Switch between Search and 
Search-and-Replace</property>
                     <property name="visible">true</property>
-                    <property name="can_focus">true</property>
-                    <property name="receives_default">true</property>
+                    <property name="can-focus">true</property>
                     <property name="image_position">right</property>
                     <child>
                       <object class="GtkImage">
                         <property name="visible">true</property>
-                        <property name="can_focus">false</property>
+                        <property name="can-focus">false</property>
                         <property name="icon_name">edit-find-replace-symbolic</property>
                       </object>
                     </child>
@@ -159,17 +170,16 @@
                   </packing>
                 </child>
                 <child>
-                  <object class="GtkToggleButton">
-                    <property name="action-name">search-bar.toggle-search-options</property>
-                    <property name="action-target">true</property>
+                  <object class="GtkToggleButton" id="show_options">
                     <property name="tooltip-text" translatable="yes">Show or hide search options such as 
case sensitivity</property>
+                    <property name="focus-on-click">false</property>
                     <property name="visible">true</property>
-                    <property name="can_focus">true</property>
-                    <property name="receives_default">true</property>
+                    <property name="can-focus">true</property>
+                    <property name="active" bind-source="IdeEditorSearchBar" bind-property="show-options" 
bind-flags="sync-create|bidirectional"/>
                     <child>
                       <object class="GtkImage">
                         <property name="visible">true</property>
-                        <property name="can_focus">false</property>
+                        <property name="can-focus">false</property>
                         <property name="icon_name">emblem-system-symbolic</property>
                       </object>
                     </child>
@@ -196,17 +206,16 @@
         <child>
           <object class="GtkGrid" id="search_options">
             <property name="visible">false</property>
-            <property name="can_focus">false</property>
+            <property name="can-focus">false</property>
             <property name="column_spacing">8</property>
             <child>
               <object class="GtkCheckButton" id="use_regex">
-                <property name="action-name">search-settings.regex-enabled</property>
                 <property name="label" translatable="yes">Regular expressions</property>
                 <property name="visible">true</property>
-                <property name="can_focus">false</property>
-                <property name="receives_default">false</property>
+                <property name="can-focus">false</property>
+                <property name="focus-on-click">false</property>
                 <property name="xalign">0</property>
-                <property name="draw_indicator">true</property>
+                <property name="draw-indicator">true</property>
               </object>
               <packing>
                 <property name="left_attach">0</property>
@@ -215,13 +224,11 @@
             </child>
             <child>
               <object class="GtkCheckButton" id="case_sensitive">
-                <property name="action-name">search-settings.case-sensitive</property>
                 <property name="label" translatable="yes">Case sensitive</property>
                 <property name="visible">true</property>
-                <property name="can_focus">false</property>
-                <property name="receives_default">false</property>
+                <property name="can-focus">false</property>
                 <property name="xalign">0</property>
-                <property name="draw_indicator">true</property>
+                <property name="draw-indicator">true</property>
               </object>
               <packing>
                 <property name="left_attach">1</property>
@@ -230,13 +237,11 @@
             </child>
             <child>
               <object class="GtkCheckButton" id="whole_word">
-                <property name="action-name">search-settings.at-word-boundaries</property>
                 <property name="label" translatable="yes">Match whole word only</property>
                 <property name="visible">true</property>
-                <property name="can_focus">false</property>
-                <property name="receives_default">false</property>
+                <property name="can-focus">false</property>
                 <property name="xalign">0</property>
-                <property name="draw_indicator">true</property>
+                <property name="draw-indicator">true</property>
               </object>
               <packing>
                 <property name="left_attach">2</property>
diff --git a/src/libide/editor/ide-editor-search.c b/src/libide/editor/ide-editor-search.c
new file mode 100644
index 0000000..c7d3539
--- /dev/null
+++ b/src/libide/editor/ide-editor-search.c
@@ -0,0 +1,1417 @@
+/* ide-editor-search.c
+ *
+ * Copyright (C) 2017 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-editor-search"
+
+#include <dazzle.h>
+#include <string.h>
+
+#include "util/ide-action-group.h"
+#include "editor/ide-editor-search.h"
+
+/**
+ * SECTION:ide-editor-search
+ * @title: IdeEditorSearch
+ *
+ * The #IdeEditorSearch object manages the search features associated
+ * with a single #IdeEditorView (and it's view of the underlying text
+ * buffer).
+ *
+ * This object is meant to help reduce the number of layers performing
+ * search on the underlying buffer as well as track highlighting based
+ * on focus, performance considerations, and directional movements in
+ * a unified manner.
+ *
+ * Additionally, it provides an addin layer to highlight similar words
+ * when then buffer selection changes.
+ *
+ * Since: 3.28
+ */
+
+struct _IdeEditorSearch
+{
+  GObject                  parent_instance;
+
+  GtkSourceView           *view;
+  GtkSourceSearchContext  *context;
+  GtkSourceSearchSettings *settings;
+  gchar                   *replacement_text;
+  DzlSignalGroup          *buffer_signals;
+  GCancellable            *lookahead_cancellable;
+
+  gint                     interactive;
+  gdouble                  scroll_value;
+
+  guint                    match_count;
+  guint                    match_position;
+
+  guint                    busy : 1;
+  guint                    reverse : 1;
+  guint                    visible : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_AT_WORD_BOUNDARIES,
+  PROP_BUSY,
+  PROP_CASE_SENSITIVE,
+  PROP_MATCH_COUNT,
+  PROP_MATCH_POSITION,
+  PROP_REGEX_ENABLED,
+  PROP_REPLACEMENT_TEXT,
+  PROP_REVERSE,
+  PROP_SEARCH_TEXT,
+  PROP_VIEW,
+  PROP_VISIBLE,
+  N_PROPS
+};
+
+static void ide_editor_search_actions_move_next          (IdeEditorSearch *self,
+                                                          GVariant        *param);
+static void ide_editor_search_actions_move_previous      (IdeEditorSearch *self,
+                                                          GVariant        *param);
+static void ide_editor_search_actions_replace            (IdeEditorSearch *self,
+                                                          GVariant        *param);
+static void ide_editor_search_actions_replace_all        (IdeEditorSearch *self,
+                                                          GVariant        *param);
+
+IDE_DEFINE_ACTION_GROUP (IdeEditorSearch, ide_editor_search, {
+  { "move-next", ide_editor_search_actions_move_next },
+  { "move-previous", ide_editor_search_actions_move_previous },
+  { "replace", ide_editor_search_actions_replace },
+  { "replace-all", ide_editor_search_actions_replace_all },
+})
+
+G_DEFINE_TYPE_WITH_CODE (IdeEditorSearch, ide_editor_search, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, ide_editor_search_init_action_group))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_editor_search_settings_notify (IdeEditorSearch         *self,
+                                   GParamSpec              *pspec,
+                                   GtkSourceSearchSettings *settings)
+{
+  g_assert (IDE_IS_EDITOR_SEARCH (self));
+  g_assert (pspec != NULL);
+  g_assert (GTK_SOURCE_IS_SEARCH_SETTINGS (settings));
+
+  /* Proxy the notify from the settings to our instance */
+  pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (self), pspec->name);
+  if (pspec != NULL)
+    g_object_notify_by_pspec (G_OBJECT (self), pspec);
+}
+
+static void
+ide_editor_search_notify_style_scheme (IdeEditorSearch *self,
+                                       GParamSpec      *pspec,
+                                       GtkSourceBuffer *buffer)
+{
+  g_assert (IDE_IS_EDITOR_SEARCH (self));
+  g_assert (GTK_SOURCE_IS_BUFFER (buffer));
+
+  if (self->context != NULL)
+    {
+      GtkSourceStyleScheme *style_scheme;
+      GtkSourceStyle *style = NULL;
+
+      style_scheme = gtk_source_buffer_get_style_scheme (buffer);
+      if (style_scheme != NULL)
+        style = gtk_source_style_scheme_get_style (style_scheme, "search-match");
+
+      gtk_source_search_context_set_match_style (self->context, style);
+    }
+}
+
+static void
+ide_editor_search_bind_buffer (IdeEditorSearch *self,
+                               GtkSourceBuffer *buffer,
+                               DzlSignalGroup  *buffer_signals)
+{
+  g_assert (IDE_IS_EDITOR_SEARCH (self));
+  g_assert (GTK_SOURCE_IS_BUFFER (buffer));
+  g_assert (DZL_IS_SIGNAL_GROUP (buffer_signals));
+
+  ide_editor_search_notify_style_scheme (self, NULL, buffer);
+}
+
+static void
+ide_editor_search_unbind_buffer (IdeEditorSearch *self,
+                                 DzlSignalGroup  *buffer_signals)
+{
+  g_assert (IDE_IS_EDITOR_SEARCH (self));
+  g_assert (DZL_IS_SIGNAL_GROUP (buffer_signals));
+
+  g_clear_object (&self->context);
+}
+
+static void
+ide_editor_search_finalize (GObject *object)
+{
+  IdeEditorSearch *self = (IdeEditorSearch *)object;
+
+  g_clear_object (&self->view);
+  g_clear_object (&self->context);
+  g_clear_object (&self->settings);
+  g_clear_object (&self->buffer_signals);
+  g_clear_object (&self->lookahead_cancellable);
+  g_clear_pointer (&self->replacement_text, g_free);
+
+  G_OBJECT_CLASS (ide_editor_search_parent_class)->finalize (object);
+}
+
+static void
+ide_editor_search_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeEditorSearch *self = IDE_EDITOR_SEARCH (object);
+
+  switch (prop_id)
+    {
+    case PROP_CASE_SENSITIVE:
+      g_value_set_boolean (value, ide_editor_search_get_case_sensitive (self));
+      break;
+
+    case PROP_VIEW:
+      g_value_set_object (value, self->view);
+      break;
+
+    case PROP_SEARCH_TEXT:
+      g_value_set_string (value, ide_editor_search_get_search_text (self));
+      break;
+
+    case PROP_VISIBLE:
+      g_value_set_boolean (value, ide_editor_search_get_visible (self));
+      break;
+
+    case PROP_REGEX_ENABLED:
+      g_value_set_boolean (value, ide_editor_search_get_regex_enabled (self));
+      break;
+
+    case PROP_REPLACEMENT_TEXT:
+      g_value_set_string (value, ide_editor_search_get_replacement_text (self));
+      break;
+
+    case PROP_AT_WORD_BOUNDARIES:
+      g_value_set_boolean (value, ide_editor_search_get_at_word_boundaries (self));
+      break;
+
+    case PROP_BUSY:
+      g_value_set_boolean (value, ide_editor_search_get_busy (self));
+      break;
+
+    case PROP_MATCH_COUNT:
+      g_value_set_uint (value, ide_editor_search_get_match_count (self));
+      break;
+
+    case PROP_MATCH_POSITION:
+      g_value_set_uint (value, ide_editor_search_get_match_position (self));
+      break;
+
+    case PROP_REVERSE:
+      g_value_set_boolean (value, ide_editor_search_get_reverse (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_editor_search_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  IdeEditorSearch *self = IDE_EDITOR_SEARCH (object);
+
+  switch (prop_id)
+    {
+    case PROP_CASE_SENSITIVE:
+      ide_editor_search_set_case_sensitive (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_SEARCH_TEXT:
+      ide_editor_search_set_search_text (self, g_value_get_string (value));
+      break;
+
+    case PROP_VISIBLE:
+      ide_editor_search_set_visible (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_REGEX_ENABLED:
+      ide_editor_search_set_regex_enabled (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_REPLACEMENT_TEXT:
+      ide_editor_search_set_replacement_text (self, g_value_get_string (value));
+      break;
+
+    case PROP_AT_WORD_BOUNDARIES:
+      ide_editor_search_set_at_word_boundaries (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_REVERSE:
+      ide_editor_search_set_reverse (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_VIEW:
+      self->view = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_editor_search_class_init (IdeEditorSearchClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_editor_search_finalize;
+  object_class->get_property = ide_editor_search_get_property;
+  object_class->set_property = ide_editor_search_set_property;
+
+  /**
+   * IdeEditorSearch:view:
+   *
+   * The "view" property is the underlying #GtkSourceView that
+   * is being searched. This must be set when creating the
+   * #IdeEditorSearch and may not be changed after construction.
+   *
+   * Since: 3.28
+   */
+  properties [PROP_VIEW] =
+    g_param_spec_object ("view", NULL,  NULL,
+                         GTK_SOURCE_TYPE_VIEW,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeEditorSearch:at-word-boundaries:
+   *
+   * The "at-word-boundaries" property specifies if the search-text must
+   * only be matched starting from the beginning of a word.
+   *
+   * Since: 3.28
+   */
+  properties [PROP_AT_WORD_BOUNDARIES] =
+    g_param_spec_boolean ("at-word-boundaries", NULL, NULL,
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeEditorSearch:busy:
+   *
+   * The "busy" property specifies if the #IdeEditorSearch is busy
+   * performing a background operation.
+   *
+   * You should not make modifications to the #IdeEditorSearch while
+   * it is busy, or you risk replacing unexpected text in the source view.
+   *
+   * Since: 3.28
+   */
+  properties [PROP_BUSY] =
+    g_param_spec_boolean ("busy", NULL, NULL,
+                          FALSE,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeEditorSearch:case-sensitive:
+   *
+   * The "case-sensitive" property specifies if the search text should
+   * be case sensitive.
+   *
+   * Since: 3.28
+   */
+  properties [PROP_CASE_SENSITIVE] =
+    g_param_spec_boolean ("case-sensitive", NULL, NULL,
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeEditorSearch:match-count:
+   *
+   * The "match-count" property contains the number of matches that have
+   * been discovered. This is reset to zeor when the #IdeEditorSearch
+   * determines it can destroy it's #GtkSourceSearchContext.
+   *
+   * Generally, you should only rely on it's accuracy after calling
+   * ide_editor_search_begin_interactive() and before calling
+   * ide_editor_search_end_interactive().
+   *
+   * Since: 3.28
+   */
+  properties [PROP_MATCH_COUNT] =
+    g_param_spec_uint ("match-count", NULL, NULL,
+                       0, G_MAXUINT, 0,
+                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeEditorSearch:match-position:
+   *
+   * The "match-position" property contains the position within the
+   * discovered search results for which the insertion cursor is placed.
+   *
+   * This value starts from 1, and 0 indicates that the insertion cursor
+   * is not placed within the a search result.
+   *
+   * Since: 3.28
+   */
+  properties [PROP_MATCH_POSITION] =
+    g_param_spec_uint ("match-position", NULL, NULL,
+                       0, G_MAXUINT, 0,
+                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeEditorSearch:regex-enabled:
+   *
+   * The "regex-enabled" property determines if #GRegex should be used
+   * to scan for the #IdeEditorSearch:search-text. Doing so allows the
+   * user to search using common regex values such as "foo.*bar". It
+   * also allows for capture groups to be used in replacement text.
+   *
+   * Since: 3.28
+   */
+  properties [PROP_REGEX_ENABLED] =
+    g_param_spec_boolean ("regex-enabled", NULL, NULL,
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeEditorSearch:replacement-text:
+   *
+   * The "replacement-text" property determines the text to be used when
+   * performing search and replace with ide_editor_search_replace() or
+   * ide_editor_search_replace_all().
+   *
+   * If #IdeEditorSearch:regex-enabled is %TRUE, then the user may use
+   * references to capture groups specified in #IdeEditorSearch:search-text.
+   *
+   * Since: 3.28
+   */
+  properties [PROP_REPLACEMENT_TEXT] =
+    g_param_spec_string ("replacement-text", NULL, NULL, NULL,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeEditorSearch:reverse:
+   *
+   * The "reverse" property determines if relative directions should be
+   * switched, so next is backward, and previous is forward.
+   *
+   * Since: 3.28
+   */
+  properties [PROP_REVERSE] =
+    g_param_spec_boolean ("reverse", NULL, NULL, FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeEditorSearch:search-text:
+   *
+   * The "search-text" property contains the text to search within the buffer.
+   *
+   * If the #IdeEditorSearch:regex-enabled property is set to %TRUE, then
+   * the user may use regular expressions supported by #GRegex to scan the
+   * buffer. They may also specify capture groups to use in search and
+   * replace.
+   *
+   * Since: 3.28
+   */
+  properties [PROP_SEARCH_TEXT] =
+    g_param_spec_string ("search-text", NULL, NULL, NULL,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeEditorSearch:visible:
+   *
+   * The "visible" property is used to specify if the search results should
+   * be highlighted in the buffer. Generally, you'll want this off while the
+   * interactive search is hidden as it allows the #IdeEditorSearch to perform
+   * various optimizations.
+   *
+   * However, some cases, such as Vim search movements, may want to show
+   * the search highlights, but are not within an interactive search.
+   *
+   * Since: 3.28
+   */
+  properties [PROP_VISIBLE] =
+    g_param_spec_string ("visible", NULL, NULL, FALSE,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_editor_search_init (IdeEditorSearch *self)
+{
+  self->settings = gtk_source_search_settings_new ();
+
+  g_signal_connect_swapped (self->settings,
+                            "notify",
+                            G_CALLBACK (ide_editor_search_settings_notify),
+                            self);
+
+  self->buffer_signals = dzl_signal_group_new (GTK_SOURCE_TYPE_BUFFER);
+
+  g_signal_connect_swapped (self->buffer_signals,
+                            "bind",
+                            G_CALLBACK (ide_editor_search_bind_buffer),
+                            self);
+
+  g_signal_connect_swapped (self->buffer_signals,
+                            "unbind",
+                            G_CALLBACK (ide_editor_search_unbind_buffer),
+                            self);
+
+  dzl_signal_group_connect_swapped (self->buffer_signals,
+                                    "notify::style-scheme",
+                                    G_CALLBACK (ide_editor_search_notify_style_scheme),
+                                    self);
+}
+
+/**
+ * ide_editor_search_new:
+ *
+ * Creates a new #IdeEditorSearch instance for the #GtkSourceView.
+ * You should only create one of these per #IdeEditorView.
+ *
+ * Returns: (transfer full): A new #IdeEditorSearch instance
+ *
+ * Since: 3.28
+ */
+IdeEditorSearch *
+ide_editor_search_new (GtkSourceView *view)
+{
+  return g_object_new (IDE_TYPE_EDITOR_SEARCH,
+                       "view", view,
+                       NULL);
+}
+
+static void
+ide_editor_search_notify_occurrences_count (IdeEditorSearch        *self,
+                                            GParamSpec             *pspec,
+                                            GtkSourceSearchContext *context)
+{
+  GtkTextBuffer *buffer;
+  GtkTextView *view;
+  GtkTextIter begin;
+  GtkTextIter end;
+  gint count;
+  gint pos;
+
+  g_assert (IDE_IS_EDITOR_SEARCH (self));
+  g_assert (GTK_SOURCE_IS_SEARCH_CONTEXT (context));
+
+  if (self->view == NULL)
+    return;
+
+  count = gtk_source_search_context_get_occurrences_count (context);
+  self->match_count = MAX (0, count);
+
+  view = GTK_TEXT_VIEW (self->view);
+  buffer = gtk_text_view_get_buffer (view);
+  gtk_text_buffer_get_selection_bounds (buffer, &begin, &end);
+  gtk_text_iter_order (&begin, &end);
+
+  pos = gtk_source_search_context_get_occurrence_position (context, &begin, &end);
+  self->match_position = MAX (0, pos);
+
+  ide_editor_search_set_action_enabled (self, "replace", pos > 0 && count > 0);
+  ide_editor_search_set_action_enabled (self, "replace-all", count > 0);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MATCH_COUNT]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MATCH_POSITION]);
+}
+
+static GtkSourceSearchContext *
+ide_editor_search_acquire_context (IdeEditorSearch *self)
+{
+  g_assert (IDE_IS_EDITOR_SEARCH (self));
+  g_assert (self->settings != NULL);
+  g_assert (self->view != NULL);
+
+  if (self->context == NULL)
+    {
+      GtkSourceBuffer *buffer;
+      GtkTextView *view;
+      gboolean highlight;
+
+      view = GTK_TEXT_VIEW (self->view);
+      buffer = GTK_SOURCE_BUFFER (gtk_text_view_get_buffer (view));
+
+      /* Create our new context */
+      self->context = gtk_source_search_context_new (buffer, self->settings);
+
+      /* Update match info as the context discovers matches */
+      g_signal_connect_object (self->context,
+                               "notify::occurrences-count",
+                               G_CALLBACK (ide_editor_search_notify_occurrences_count),
+                               self,
+                               G_CONNECT_SWAPPED);
+
+      /* Determine if we should highlight immediately */
+      highlight = self->visible || self->interactive > 0;
+      gtk_source_search_context_set_highlight (self->context, highlight);
+
+      /* Update text tag stylign */
+      ide_editor_search_notify_style_scheme (self, NULL, buffer);
+    }
+
+  return self->context;
+}
+
+static void
+ide_editor_search_release_context (IdeEditorSearch *self)
+{
+  g_assert (IDE_IS_EDITOR_SEARCH (self));
+  g_assert (self->context != NULL);
+
+  if (self->context != NULL && self->interactive == 0)
+    {
+      g_signal_handlers_disconnect_by_func (self->context,
+                                            G_CALLBACK (ide_editor_search_notify_occurrences_count),
+                                            self);
+      g_clear_object (&self->context);
+    }
+}
+
+/**
+ * ide_editor_search_set_case_sensitive:
+ * @self: An #IdeEditorSearch
+ * @case_sensitive: %TRUE if the search should be case-sensitive
+ *
+ * See also: #GtkSourceSearchSettings:case-sensitive
+ * Since: 3.28
+ */
+void
+ide_editor_search_set_case_sensitive (IdeEditorSearch *self,
+                                      gboolean         case_sensitive)
+{
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH (self));
+
+  gtk_source_search_settings_set_case_sensitive (self->settings, case_sensitive);
+}
+
+/**
+ * ide_editor_search_get_case_sensitive:
+ * @self: An #IdeEditorSearch
+ *
+ * Gets if the search should be case sensitive.
+ *
+ * Returns: %TRUE if the search is case-sensitive.
+ *
+ * See also: #GtkSourceSearchSettings:case-sensitive
+ * Since: 3.28
+ */
+gboolean
+ide_editor_search_get_case_sensitive (IdeEditorSearch *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_SEARCH (self), FALSE);
+
+  return gtk_source_search_settings_get_case_sensitive (self->settings);
+}
+
+static void
+ide_editor_search_scan_forward_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  GtkSourceSearchContext *context = (GtkSourceSearchContext *)object;
+  g_autoptr(IdeEditorSearch) self = user_data;
+  GtkTextIter begin;
+  GtkTextIter end;
+  gboolean r;
+
+  g_assert (GTK_SOURCE_IS_SEARCH_CONTEXT (context));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_EDITOR_SEARCH (self));
+
+  if (self->view == NULL)
+    return;
+
+  r = gtk_source_search_context_forward_finish2 (context, result, &begin, &end, NULL, NULL);
+
+  if (r == TRUE)
+    {
+      /* Scan forward to the location of the next match */
+      gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (self->view), &begin, 0.0, TRUE, 1.0, 0.5);
+    }
+  else if (self->interactive > 0)
+    {
+      GtkAdjustment *adj;
+
+      /* No match was found, restore to our position pre-search */
+      adj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (self->view));
+      gtk_adjustment_set_value (adj, self->scroll_value);
+    }
+
+  ide_editor_search_notify_occurrences_count (self, NULL, context);
+}
+
+/**
+ * ide_editor_search_set_search_text:
+ * @self: An #IdeEditorSearch
+ * @search_text: (nullable): The search text or %NULL
+ *
+ * See also: #GtkSourceSearchSettings:search-text
+ *
+ * Since: 3.28
+ */
+void
+ide_editor_search_set_search_text (IdeEditorSearch *self,
+                                   const gchar     *search_text)
+{
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH (self));
+
+  gtk_source_search_settings_set_search_text (self->settings, search_text);
+
+  /*
+   * If we are in an interactive search, start scrolling to the next search
+   * result (without moving the insertion cursor). Upon completion of the
+   * interactive search, we will scroll back to our original position if
+   * no move was made.
+   */
+  if (self->interactive > 0 && self->view != NULL)
+    {
+      GtkSourceSearchContext *context;
+      GtkTextBuffer *buffer;
+      GtkTextIter begin;
+      GtkTextIter end;
+
+      /* Cancel any previous lookahead */
+      g_cancellable_cancel (self->lookahead_cancellable);
+      g_clear_object (&self->lookahead_cancellable);
+
+      /* Setup some necessary objects */
+      context = ide_editor_search_acquire_context (self);
+      buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->view));
+      self->lookahead_cancellable = g_cancellable_new ();
+
+      /* Get our start position for the forward scan */
+      gtk_text_buffer_get_selection_bounds (buffer, &begin, &end);
+      gtk_text_iter_order (&begin, &end);
+      gtk_text_iter_forward_char (&end);
+
+      /* Ensure we wrap around to the beginning of the buffer */
+      gtk_source_search_settings_set_wrap_around (self->settings, TRUE);
+
+      gtk_source_search_context_forward_async (context,
+                                               &end,
+                                               self->lookahead_cancellable,
+                                               ide_editor_search_scan_forward_cb,
+                                               g_object_ref (self));
+    }
+}
+
+/**
+ * ide_editor_search_get_search_text:
+ * @self: An #IdeEditorSearch
+ *
+ * Gets the search-text currently being searched.
+ *
+ * Returns: (nullable): The search text or %NULL
+ *
+ * Since: 3.28
+ */
+const gchar *
+ide_editor_search_get_search_text (IdeEditorSearch *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_SEARCH (self), NULL);
+
+  return gtk_source_search_settings_get_search_text (self->settings);
+}
+
+/**
+ * ide_editor_search_get_search_text_invalid:
+ * @self: An #IdeEditorSearch
+ * @invalid_begin: (nullable) (out): a begin location for the invalid range
+ * @invalid_end: (nullable) (out): an end location for the invalid range
+ *
+ * Checks to see if the search text contains invalid contents, such
+ * as an invalid regex.
+ *
+ * Returns: %TRUE if the search text contains invalid content. If %TRUE,
+ *   then @invalid_begin and @invalid_end is set.
+ *
+ * Since: 3.28
+ */
+gboolean
+ide_editor_search_get_search_text_invalid (IdeEditorSearch  *self,
+                                           guint            *invalid_begin,
+                                           guint            *invalid_end,
+                                           GError          **error)
+{
+  const gchar *text;
+  guint dummy;
+
+  g_return_val_if_fail (IDE_IS_EDITOR_SEARCH (self), FALSE);
+
+  /* Fallback to avoid dereference checks */
+  invalid_begin = invalid_begin ? invalid_begin : &dummy;
+  invalid_end = invalid_end ? invalid_end : &dummy;
+
+  text = gtk_source_search_settings_get_search_text (self->settings);
+  if (text == NULL)
+    text = "";
+
+  if (ide_editor_search_get_regex_enabled (self))
+    {
+      g_autoptr(GRegex) regex = NULL;
+      g_autoptr(GError) local_error = NULL;
+
+      if (NULL == (regex = g_regex_new (text, 0, 0, &local_error)))
+        {
+          const gchar *endptr;
+
+          *invalid_begin = 0;
+          *invalid_end = strlen (text);
+
+          /*
+           * Error from GRegex will look something like:
+           * Error while compiling regular expression foo\\\\ at char 7: (message)
+           */
+
+          if (NULL != (endptr = strrchr (local_error->message, ':')))
+            {
+              while (endptr > local_error->message)
+                {
+                  if (g_ascii_isdigit (*(endptr - 1)))
+                    {
+                      endptr--;
+                    }
+                  else
+                    {
+                      *invalid_begin = (guint)g_ascii_strtoull (endptr, NULL, 10);
+                      /* Translate to zero based index */
+                      if ((*invalid_begin) > 0)
+                        (*invalid_begin)--;
+                      break;
+                    }
+                }
+
+              g_propagate_error (error, g_steal_pointer (&local_error));
+            }
+
+          return TRUE;
+        }
+    }
+
+  *invalid_begin = 0;
+  *invalid_end = 0;
+
+  return FALSE;
+}
+
+/**
+ * ide_editor_search_set_visible:
+ * @self: An #IdeEditorSearch
+ * @visible: if the search results should be visible
+ *
+ * Sets the visibility of the search results. You might want to disable
+ * the visibility of search results when the user has requested them to
+ * be dismissed.
+ *
+ * This will allow the user to still make search movements based on the
+ * previous search request, and re-enable visibility upon doing so.
+ *
+ * Since: 3.28
+ */
+void
+ide_editor_search_set_visible (IdeEditorSearch *self,
+                               gboolean         visible)
+{
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH (self));
+
+  visible = !!visible;
+
+  if (visible != self->visible)
+    {
+      self->visible = visible;
+      /* TODO: Dispose the context, but keep the search settings around.
+       *       Upon next movement, we can restore visibility that way.
+       */
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VISIBLE]);
+    }
+}
+
+/**
+ * ide_editor_search_get_visible:
+ * @self: An #IdeEditorSearch
+ *
+ * Gets the #IdeEditorSearch:visible property. This is true if the current
+ * search text should be highlighted in the editor.
+ *
+ * Returns: %TRUE if the current search should be highlighted.
+ *
+ * Since: 3.28
+ */
+gboolean
+ide_editor_search_get_visible (IdeEditorSearch *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_SEARCH (self), FALSE);
+
+  return self->visible;
+}
+
+/**
+ * ide_editor_search_set_regex_enabled:
+ * @self: An #IdeEditorSearch
+ * @regex_enabled: If regex search should be used
+ *
+ * See also: #GtkSourceSearchSettings:regex-enabled
+ *
+ * Since: 3.28
+ */
+void
+ide_editor_search_set_regex_enabled (IdeEditorSearch *self,
+                                     gboolean         regex_enabled)
+{
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH (self));
+
+  gtk_source_search_settings_set_regex_enabled (self->settings, regex_enabled);
+  ide_editor_search_set_action_state (self,
+                                      "regex-enabled",
+                                      g_variant_new_boolean (regex_enabled));
+}
+
+/**
+ * ide_editor_search_get_regex_enabled:
+ * @self: An #IdeEditorSearch
+ *
+ * Gets the #IdeEditorSearch:regex-enabled property. This is true if the
+ * search text can contain regular expressions supported by #GRegex.
+ *
+ * Returns: %TRUE if search text can use regex
+ *
+ * Since: 3.28
+ */
+gboolean
+ide_editor_search_get_regex_enabled (IdeEditorSearch *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_SEARCH (self), FALSE);
+
+  return gtk_source_search_settings_get_regex_enabled (self->settings);
+}
+
+/**
+ * ide_editor_search_set_replacement_text:
+ * @self: An #IdeEditorSearch
+ * @replacement_text: (nullable): The text to use in replacement operations
+ *
+ * This sets the text to use when performing search and replace. See
+ * ide_editor_search_replace() or ide_editor_search_replace_all() to
+ * perform one or more replacements.
+ *
+ * If #IdeEditorSearch:regex-enabled is set, then you may reference
+ * regex groups from the regex in #IdeEditorSearch:search-text.
+ *
+ * Since: 3.28
+ */
+void
+ide_editor_search_set_replacement_text (IdeEditorSearch *self,
+                                        const gchar     *replacement_text)
+{
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH (self));
+
+  if (g_strcmp0 (self->replacement_text, replacement_text) != 0)
+    {
+      g_free (self->replacement_text);
+      self->replacement_text = g_strdup (replacement_text);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_REPLACEMENT_TEXT]);
+    }
+}
+
+/**
+ * ide_editor_search_get_replacement_text:
+ * @self: An #IdeEditorSearch
+ *
+ * Gets the #IdeEditorSearch:replacement-text property. This is the text
+ * that will be used when calling ide_editor_search_replace() or
+ * ide_editor_search_replace_all().
+ *
+ * Returns: (nullable): the replacement text, or %NULL
+ *
+ * Since: 3.28
+ */
+const gchar *
+ide_editor_search_get_replacement_text (IdeEditorSearch *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_SEARCH (self), NULL);
+
+  return self->replacement_text;
+}
+
+/**
+ * ide_editor_search_set_at_word_boundaries:
+ * @self: An #IdeEditorSearch
+ * @at_word_boundaries: %TRUE if search should match only on word boundaries
+ *
+ * See also: gtk_source_search_settings_set_word_boundaries()
+ *
+ * Since: 3.28
+ */
+void
+ide_editor_search_set_at_word_boundaries (IdeEditorSearch *self,
+                                          gboolean         at_word_boundaries)
+{
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH (self));
+
+  gtk_source_search_settings_set_at_word_boundaries (self->settings, at_word_boundaries);
+  ide_editor_search_set_action_state (self, "at-word-boundaries",
+                                      g_variant_new_boolean (at_word_boundaries));
+}
+
+/**
+ * ide_editor_search_get_at_word_boundaries:
+ * @self: An #IdeEditorSearch
+ *
+ * Gets the #IdeEditorSearch:at-word-boundaries property.
+ *
+ * Returns: %TRUE if the search should only match word boundaries.
+ *
+ * See also: #GtkSourceSearchSettings:at-word-boundaries
+ * Since: 3.28
+ */
+gboolean
+ide_editor_search_get_at_word_boundaries (IdeEditorSearch *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_SEARCH (self), FALSE);
+
+  return gtk_source_search_settings_get_at_word_boundaries (self->settings);
+}
+
+/**
+ * ide_editor_search_get_busy:
+ * @self: An #IdeEditorSearch
+ *
+ * Checks to see if the #IdeEditorSearch is busy performing a task. You
+ * might want to use this property to alter the sensitivity of widgets
+ * while background work is processing.
+ *
+ * Since: 3.28
+ */
+gboolean
+ide_editor_search_get_busy (IdeEditorSearch *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_SEARCH (self), FALSE);
+
+  return self->busy;
+}
+
+/**
+ * ide_editor_search_get_match_count:
+ * @self: An #IdeEditorSearch
+ *
+ * Gets the number of matches currently found in the editor. This
+ * will update as new matches are found while scanning the buffer.
+ *
+ * Since: 3.28
+ */
+guint
+ide_editor_search_get_match_count (IdeEditorSearch *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_SEARCH (self), 0);
+
+  return self->match_count;
+}
+
+/**
+ * ide_editor_search_get_match_position:
+ * @self: An #IdeEditorSearch
+ *
+ * Gets the match position of the cursor within the buffer. If the
+ * cursor is within a match, this will be a 1-based index
+ * will update as new matches are found while scanning the buffer.
+ *
+ * Since: 3.28
+ */
+guint
+ide_editor_search_get_match_position (IdeEditorSearch *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_SEARCH (self), 0);
+
+  return self->match_position;
+}
+
+static void
+ide_editor_search_forward_cb (GObject      *object,
+                              GAsyncResult *result,
+                              gpointer      user_data)
+{
+  GtkSourceSearchContext *context = (GtkSourceSearchContext *)object;
+  g_autoptr(IdeEditorSearch) self = user_data;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (GTK_SOURCE_IS_SEARCH_CONTEXT (context));
+  g_assert (IDE_IS_EDITOR_SEARCH (self));
+
+  if (gtk_source_search_context_forward_finish2 (context, result, &begin, &end, NULL, NULL))
+    {
+      if (self->view != NULL)
+        {
+          GtkTextBuffer *buffer = gtk_text_iter_get_buffer (&begin);
+
+          gtk_text_buffer_select_range (buffer, &begin, &end);
+          gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (self->view), &begin, 0.0, TRUE, 1.0, 0.5);
+        }
+    }
+
+  ide_editor_search_notify_occurrences_count (self, NULL, context);
+}
+
+static void
+ide_editor_search_backward_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  GtkSourceSearchContext *context = (GtkSourceSearchContext *)object;
+  g_autoptr(IdeEditorSearch) self = user_data;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (GTK_SOURCE_IS_SEARCH_CONTEXT (context));
+  g_assert (IDE_IS_EDITOR_SEARCH (self));
+
+  if (gtk_source_search_context_forward_finish2 (context, result, &begin, &end, NULL, NULL))
+    {
+      if (self->view != NULL)
+        {
+          GtkTextBuffer *buffer = gtk_text_iter_get_buffer (&begin);
+
+          gtk_text_buffer_select_range (buffer, &begin, &end);
+          gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (self->view), &begin, 0.0, TRUE, 1.0, 0.5);
+        }
+    }
+
+  ide_editor_search_notify_occurrences_count (self, NULL, context);
+}
+
+/**
+ * ide_editor_search_move:
+ * @self: An #IdeEditorSearch
+ * @direction: An #IdeEditorSearchDirection
+ *
+ * This moves the insertion cursor in the buffer to the next match based
+ * upon @direction.
+ *
+ * If direction is %IDE_EDITOR_SEARCH_BACKWARD, the search will stop
+ * at the beginning of the buffer.
+ *
+ * If direction is %IDE_EDITOR_SEARCH_FORWARD, the search will stop
+ * at the end of the buffer.
+ *
+ * If direction is %IDE_EDITOR_SEARCH_NEXT, it will automatically wrap
+ * around to the beginning of the buffer after reaching the end of the
+ * buffer.
+ *
+ * If direction is %IDE_EDITOR_SEARCH_PREVIOUS, the search will
+ * automatically wrap around to the end of the buffer once the beginning
+ * of the buffer has been reached.
+ *
+ * Since: 3.28
+ */
+void
+ide_editor_search_move (IdeEditorSearch          *self,
+                        IdeEditorSearchDirection  direction)
+{
+  GtkSourceSearchContext *context;
+  GtkTextBuffer *buffer;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH (self));
+  g_return_if_fail (self->view != NULL);
+  g_return_if_fail (direction >= 0);
+  g_return_if_fail (direction <= IDE_EDITOR_SEARCH_BACKWARD);
+
+  context = ide_editor_search_acquire_context (self);
+
+  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->view));
+  gtk_text_buffer_get_selection_bounds (buffer, &begin, &end);
+  gtk_text_iter_order (&begin, &end);
+
+  if (self->reverse)
+    {
+      if (direction == IDE_EDITOR_SEARCH_NEXT)
+        direction = IDE_EDITOR_SEARCH_PREVIOUS;
+      else if (direction == IDE_EDITOR_SEARCH_PREVIOUS)
+        direction = IDE_EDITOR_SEARCH_NEXT;
+    }
+
+  switch (direction)
+    {
+    case IDE_EDITOR_SEARCH_FORWARD:
+      gtk_text_iter_forward_char (&end);
+      gtk_source_search_settings_set_wrap_around (self->settings, FALSE);
+      gtk_source_search_context_forward_async (context,
+                                               &end,
+                                               NULL,
+                                               ide_editor_search_forward_cb,
+                                               g_object_ref (self));
+      break;
+
+    case IDE_EDITOR_SEARCH_NEXT:
+      gtk_text_iter_forward_char (&end);
+      gtk_source_search_settings_set_wrap_around (self->settings, TRUE);
+      gtk_source_search_context_forward_async (context,
+                                               &end,
+                                               NULL,
+                                               ide_editor_search_forward_cb,
+                                               g_object_ref (self));
+      break;
+
+    case IDE_EDITOR_SEARCH_BACKWARD:
+      gtk_text_iter_backward_char (&begin);
+      gtk_source_search_settings_set_wrap_around (self->settings, FALSE);
+      gtk_source_search_context_backward_async (context,
+                                                &begin,
+                                                NULL,
+                                                ide_editor_search_backward_cb,
+                                                g_object_ref (self));
+      break;
+
+    case IDE_EDITOR_SEARCH_PREVIOUS:
+      gtk_text_iter_backward_char (&begin);
+      gtk_source_search_settings_set_wrap_around (self->settings, TRUE);
+      gtk_source_search_context_backward_async (context,
+                                                &begin,
+                                                NULL,
+                                                ide_editor_search_backward_cb,
+                                                g_object_ref (self));
+      break;
+
+    default:
+      g_assert_not_reached ();
+    }
+
+  ide_editor_search_release_context (self);
+}
+
+/**
+ * ide_editor_search_replace:
+ * @self: An #IdeEditorSearch
+ *
+ * Replaces the next occurrance of a search result with the
+ * value of #IdeEditorSearch:replacement-text.
+ *
+ * Since: 3.28
+ */
+void
+ide_editor_search_replace (IdeEditorSearch *self)
+{
+  GtkSourceSearchContext *context;
+  const gchar *replacement;
+  GtkTextBuffer *buffer;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH (self));
+  g_return_if_fail (self->view != NULL);
+  g_return_if_fail (self->match_count > 0);
+  g_return_if_fail (self->match_position > 0);
+
+  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->view));
+  gtk_text_buffer_get_selection_bounds (buffer, &begin, &end);
+  gtk_text_iter_order (&begin, &end);
+
+  replacement = self->replacement_text ? self->replacement_text : "";
+  context = ide_editor_search_acquire_context (self);
+
+  /* Replace the current word */
+  gtk_source_search_context_replace2 (context, &begin, &end, replacement, -1, NULL);
+
+  /* Now scan to the next search result */
+  ide_editor_search_move (self, IDE_EDITOR_SEARCH_NEXT);
+
+  ide_editor_search_release_context (self);
+}
+
+/**
+ * ide_editor_search_replace_all:
+ * @self: An #IdeEditorSearch
+ *
+ * Replaces all the occurrances of #IdeEditorSearch:search-text with the
+ * value of #IdeEditorSearch:replacement-text.
+ *
+ * The #IdeEditorSearch:busy property will be set to %TRUE during the
+ * duration of this operation, as no changes may be made while the
+ * operation is pending.
+ *
+ * Since: 3.28
+ */
+void
+ide_editor_search_replace_all (IdeEditorSearch *self)
+{
+  GtkSourceSearchContext *context;
+  const gchar *replacement;
+
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH (self));
+
+  /* TODO: We should set the busy bit and do this incrementally */
+
+  replacement = self->replacement_text ? self->replacement_text : "";
+  context = ide_editor_search_acquire_context (self);
+  gtk_source_search_context_replace_all (context, replacement, -1, NULL);
+  ide_editor_search_release_context (self);
+}
+
+/**
+ * ide_editor_search_begin_interactive:
+ * @self: An #IdeEditorSearch
+ *
+ * This function is used to track when the user begin doing an
+ * interactive search, which is one where they are typing the search
+ * query.
+ *
+ * Tracking this behavior is useful because it allows the editor to
+ * "rubberband", which is to say it can scan forward to the first search
+ * result automatically, and then snap back to the previous location if
+ * the search is aborted.
+ *
+ * Since: 3.28
+ */
+void
+ide_editor_search_begin_interactive (IdeEditorSearch *self)
+{
+  GtkAdjustment *adj;
+
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH (self));
+  g_return_if_fail (self->view != NULL);
+
+  self->interactive++;
+
+  /* Disable reverse search when interactive */
+  ide_editor_search_set_reverse (self, FALSE);
+
+  /* Always highlight matches while in interactive mode */
+  if (self->context != NULL)
+    gtk_source_search_context_set_highlight (self->context, TRUE);
+
+  adj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (self->view));
+  self->scroll_value = gtk_adjustment_get_value (adj);
+}
+
+/**
+ * ide_editor_search_end_interactive:
+ * @self: An #IdeEditorSearch
+ *
+ * This function completes an interactive search previously performed
+ * with ide_editor_search_begin_interactive().
+ *
+ * This should be called when the user has left the search controls,
+ * as it might allow the editor to restore positioning back to the
+ * previous editor location from before the interactive search began.
+ *
+ * Since: 3.28
+ */
+void
+ide_editor_search_end_interactive (IdeEditorSearch *self)
+{
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH (self));
+
+  self->interactive--;
+
+  /* If we are leaving interactive mode, we want to disable the search
+   * highlight unless they were requested manually by other code.
+   */
+  if (self->context != NULL && self->interactive == 0)
+    gtk_source_search_context_set_highlight (self->context, self->visible);
+}
+
+/**
+ * ide_editor_search_get_reverse:
+ * @self: a #IdeEditorSearch
+ *
+ * Checks if search movements should be reversed for relative movements such
+ * as %IDE_EDITOR_SEARCH_NEXT and %IDE_EDITOR_SEARCH_PREVIOUS.
+ *
+ * This might be used when performing searches such as vim's # or * search
+ * operators. After that movements like n or N need to swap directions.
+ *
+ * Returns: %TRUE if relative movements are reversed directions.
+ *
+ * Since: 3.28
+ */
+gboolean
+ide_editor_search_get_reverse (IdeEditorSearch *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_SEARCH (self), FALSE);
+
+  return self->reverse;
+}
+
+/**
+ * ide_editor_search_set_reverse:
+ * @self: a #IdeEditorSearch
+ * @reverse: if relative search directions should reverse
+ *
+ * Sets the "reverse" property.
+ *
+ * This is used to alter the direction for relative search movements.
+ * %IDE_EDITOR_SEARCH_NEXT and %IDE_EDITOR_SEARCH_PREVIOUS will swap
+ * directions so that %IDE_EDITOR_SEARCH_PREVIOUS will search forwards
+ * in the buffer and %IDE_EDITOR_SEARCH_NEXT wills earch backwards.
+ *
+ * Since: 3.28
+ */
+void
+ide_editor_search_set_reverse (IdeEditorSearch *self,
+                               gboolean         reverse)
+{
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH (self));
+
+  reverse = !!reverse;
+
+  if (reverse != self->reverse)
+    {
+      self->reverse = reverse;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_REVERSE]);
+    }
+}
+
+static void
+ide_editor_search_actions_move_next (IdeEditorSearch *self,
+                                     GVariant        *param)
+{
+  ide_editor_search_move (self, IDE_EDITOR_SEARCH_NEXT);
+}
+
+static void
+ide_editor_search_actions_move_previous (IdeEditorSearch *self,
+                                         GVariant        *param)
+{
+  ide_editor_search_move (self, IDE_EDITOR_SEARCH_PREVIOUS);
+}
+
+static void
+ide_editor_search_actions_replace_all (IdeEditorSearch *self,
+                                       GVariant        *param)
+{
+  ide_editor_search_replace_all (self);
+}
+
+static void
+ide_editor_search_actions_replace (IdeEditorSearch *self,
+                                   GVariant        *param)
+{
+  ide_editor_search_replace (self);
+}
diff --git a/src/libide/editor/ide-editor-search.h b/src/libide/editor/ide-editor-search.h
new file mode 100644
index 0000000..4289307
--- /dev/null
+++ b/src/libide/editor/ide-editor-search.h
@@ -0,0 +1,76 @@
+/* ide-editor-search.h
+ *
+ * Copyright (C) 2017 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/>.
+ */
+
+#pragma once
+
+#include <gtksourceview/gtksource.h>
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+  IDE_EDITOR_SEARCH_NEXT,
+  IDE_EDITOR_SEARCH_PREVIOUS,
+  IDE_EDITOR_SEARCH_FORWARD,
+  IDE_EDITOR_SEARCH_BACKWARD,
+} IdeEditorSearchDirection;
+
+#define IDE_TYPE_EDITOR_SEARCH (ide_editor_search_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeEditorSearch, ide_editor_search, IDE, EDITOR_SEARCH, GObject)
+
+IdeEditorSearch *ide_editor_search_new                          (GtkSourceView             *view);
+void             ide_editor_search_set_case_sensitive           (IdeEditorSearch           *self,
+                                                                 gboolean                   case_sensitive);
+gboolean         ide_editor_search_get_case_sensitive           (IdeEditorSearch           *self);
+gboolean         ide_editor_search_get_reverse                  (IdeEditorSearch           *self);
+void             ide_editor_search_set_reverse                  (IdeEditorSearch           *self,
+                                                                 gboolean                   reverse);
+void             ide_editor_search_set_search_text              (IdeEditorSearch           *self,
+                                                                 const gchar               *search_text);
+const gchar     *ide_editor_search_get_search_text              (IdeEditorSearch           *self);
+gboolean         ide_editor_search_get_search_text_invalid      (IdeEditorSearch           *self,
+                                                                 guint                     *invalid_begin,
+                                                                 guint                     *invalid_end,
+                                                                 GError                   **error);
+void             ide_editor_search_set_visible                  (IdeEditorSearch           *self,
+                                                                 gboolean                   visible);
+gboolean         ide_editor_search_get_visible                  (IdeEditorSearch           *self);
+void             ide_editor_search_set_regex_enabled            (IdeEditorSearch           *self,
+                                                                 gboolean                   regex_enabled);
+gboolean         ide_editor_search_get_regex_enabled            (IdeEditorSearch           *self);
+void             ide_editor_search_set_replacement_text         (IdeEditorSearch           *self,
+                                                                 const gchar               
*replacement_text);
+const gchar     *ide_editor_search_get_replacement_text         (IdeEditorSearch           *self);
+gboolean         ide_editor_search_get_replacement_text_invalid (IdeEditorSearch           *self,
+                                                                 guint                     *invalid_begin,
+                                                                 guint                     *invalid_end);
+void             ide_editor_search_set_at_word_boundaries       (IdeEditorSearch           *self,
+                                                                 gboolean                   
at_word_boundaries);
+gboolean         ide_editor_search_get_at_word_boundaries       (IdeEditorSearch           *self);
+gboolean         ide_editor_search_get_busy                     (IdeEditorSearch           *self);
+guint            ide_editor_search_get_match_count              (IdeEditorSearch           *self);
+guint            ide_editor_search_get_match_position           (IdeEditorSearch           *self);
+void             ide_editor_search_move                         (IdeEditorSearch           *self,
+                                                                 IdeEditorSearchDirection   direction);
+void             ide_editor_search_replace                      (IdeEditorSearch           *self);
+void             ide_editor_search_replace_all                  (IdeEditorSearch           *self);
+void             ide_editor_search_begin_interactive            (IdeEditorSearch           *self);
+void             ide_editor_search_end_interactive              (IdeEditorSearch           *self);
+
+G_END_DECLS
diff --git a/src/libide/editor/ide-editor-view-actions.c b/src/libide/editor/ide-editor-view-actions.c
index cd0c0ec..1b87aaa 100644
--- a/src/libide/editor/ide-editor-view-actions.c
+++ b/src/libide/editor/ide-editor-view-actions.c
@@ -404,7 +404,7 @@ ide_editor_view_actions_find (GSimpleAction *action,
   if (gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (self->buffer), &begin, &end))
     {
       g_autofree gchar *word = gtk_text_iter_get_slice (&begin, &end);
-      ide_editor_search_bar_set_search_text (self->search_bar, word);
+      ide_editor_search_set_search_text (self->search, word);
     }
 
   ide_editor_search_bar_set_replace_mode (self->search_bar, FALSE);
@@ -427,7 +427,7 @@ ide_editor_view_actions_find_replace (GSimpleAction *action,
   if (gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (self->buffer), &begin, &end))
     {
       g_autofree gchar *word = gtk_text_iter_get_slice (&begin, &end);
-      ide_editor_search_bar_set_search_text (self->search_bar, word);
+      ide_editor_search_set_search_text (self->search, word);
     }
 
   ide_editor_search_bar_set_replace_mode (self->search_bar, TRUE);
diff --git a/src/libide/editor/ide-editor-view.c b/src/libide/editor/ide-editor-view.c
index 57ed0a6..3d2f4ea 100644
--- a/src/libide/editor/ide-editor-view.c
+++ b/src/libide/editor/ide-editor-view.c
@@ -37,6 +37,7 @@ enum {
   PROP_0,
   PROP_AUTO_HIDE_MAP,
   PROP_BUFFER,
+  PROP_SEARCH,
   PROP_SHOW_MAP,
   PROP_VIEW,
   N_PROPS
@@ -534,7 +535,6 @@ search_revealer_notify_reveal_child (IdeEditorView *self,
                                      GtkRevealer   *revealer)
 {
   GtkSourceCompletion *completion;
-  GtkSourceSearchContext *view_search_context;
 
   g_return_if_fail (IDE_IS_EDITOR_VIEW (self));
   g_return_if_fail (pspec != NULL);
@@ -544,45 +544,14 @@ search_revealer_notify_reveal_child (IdeEditorView *self,
 
   if (!gtk_revealer_get_reveal_child (revealer))
     {
-      /*
-       * Clear the context from the search bar so it doesn't try to
-       * keep updating various UI bits while the bar is not visible.
-       */
-      ide_editor_search_bar_set_context (self->search_bar, NULL);
-
-      /*
-       * If there are no occurrences currently, just destroy the search context
-       * so that we can avoid tracking buffer changes.
-       */
-      if (self->search_context != NULL &&
-          gtk_source_search_context_get_occurrences_count (self->search_context) <= 0)
-        g_clear_object (&self->search_context);
-
-      /*
-       * We might still need the search context so the user can move to the
-       * prev/next search result. However, we do not any longer need to have
-       * highlight enabled.
-       */
-      if (self->search_context != NULL)
-        gtk_source_search_context_set_highlight (self->search_context, FALSE);
+      ide_editor_search_end_interactive (self->search);
 
       /* Restore completion that we blocked below. */
       gtk_source_completion_unblock_interactive (completion);
     }
   else
     {
-      if (self->search_context == NULL)
-        self->search_context = g_object_new (GTK_SOURCE_TYPE_SEARCH_CONTEXT,
-                                             "buffer", self->buffer,
-                                             "settings", self->search_settings,
-                                             NULL);
-
-      gtk_source_search_context_set_highlight (self->search_context, TRUE);
-      ide_editor_search_bar_set_context (self->search_bar, self->search_context);
-
-      /* We need to hide the search highlight on the view context */
-      if (NULL != (view_search_context = ide_source_view_get_search_context (self->source_view)))
-        gtk_source_search_context_set_highlight (view_search_context, FALSE);
+      ide_editor_search_begin_interactive (self->search);
 
       /*
        * Block the completion while the search bar is set. It only
@@ -676,7 +645,8 @@ ide_editor_view_constructed (GObject *object)
                             G_CALLBACK (search_revealer_notify_reveal_child),
                             self);
 
-  ide_editor_search_bar_set_settings (self->search_bar, self->search_settings);
+  self->search = ide_editor_search_new (GTK_SOURCE_VIEW (self->source_view));
+  ide_editor_search_bar_set_search (self->search_bar, self->search);
 
   ide_editor_view_load_fonts (self);
   ide_editor_view_update_map (self);
@@ -698,8 +668,7 @@ ide_editor_view_destroy (GtkWidget *widget)
   g_cancellable_cancel (self->destroy_cancellable);
   g_clear_object (&self->destroy_cancellable);
 
-  g_clear_object (&self->search_settings);
-  g_clear_object (&self->search_context);
+  g_clear_object (&self->search);
   g_clear_object (&self->editor_settings);
   g_clear_object (&self->insight_settings);
 
@@ -750,6 +719,10 @@ ide_editor_view_get_property (GObject    *object,
       g_value_set_object (value, ide_editor_view_get_view (self));
       break;
 
+    case PROP_SEARCH:
+      g_value_set_object (value, ide_editor_view_get_search (self));
+      break;
+
     case PROP_SHOW_MAP:
       g_value_set_boolean (value, ide_editor_view_get_show_map (self));
       break;
@@ -810,6 +783,13 @@ ide_editor_view_class_init (IdeEditorViewClass *klass)
                          IDE_TYPE_BUFFER,
                          (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
 
+  properties [PROP_SEARCH] =
+    g_param_spec_object ("search",
+                         "Search",
+                         "An search helper for the document",
+                         IDE_TYPE_EDITOR_SEARCH,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
   properties [PROP_SHOW_MAP] =
     g_param_spec_boolean ("show-map",
                           "Show Map",
@@ -904,23 +884,6 @@ ide_editor_view_init (IdeEditorView *self)
                            self,
                            G_CONNECT_SWAPPED);
 
-  /*
-   * Setup our search context. The sourceview has it's own search
-   * infrastructure that we want to reserve for use by vim keybindings
-   * and other transient keybinding features. Instead, we have our own
-   * that can have separate state from those.
-   *
-   * We try to avoid creating/maintaining the search-context except
-   * when necessary because has some expensive operations associated
-   * with it's handling of changes to the underlying buffer.
-   */
-  self->search_settings = g_object_new (GTK_SOURCE_TYPE_SEARCH_SETTINGS,
-                                        "at-word-boundaries", FALSE,
-                                        "case-sensitive", FALSE,
-                                        "wrap-around", TRUE,
-                                        NULL);
-
-
   /* Setup bindings for the buffer. */
   self->buffer_bindings = dzl_binding_group_new ();
   dzl_binding_group_bind (self->buffer_bindings, "title", self, "title", 0);
@@ -1206,31 +1169,6 @@ ide_editor_view_move_previous_error (IdeEditorView *self)
   g_signal_emit_by_name (self->source_view, "move-error", GTK_DIR_UP);
 }
 
-static void
-ide_editor_view_move_next_search_result_cb (GObject      *object,
-                                            GAsyncResult *result,
-                                            gpointer      user_data)
-{
-  GtkSourceSearchContext *context = (GtkSourceSearchContext *)object;
-  g_autoptr(IdeEditorView) self = user_data;
-  g_autoptr(GError) error = NULL;
-  GtkTextIter begin;
-  GtkTextIter end;
-  gboolean has_wrapped = FALSE;
-
-  g_assert (IDE_IS_EDITOR_VIEW (self));
-  g_assert (G_IS_ASYNC_RESULT (result));
-
-  if (self->buffer == NULL)
-    return;
-
-  if (gtk_source_search_context_forward_finish2 (context, result, &begin, &end, &has_wrapped, &error))
-    {
-      gtk_text_buffer_select_range (GTK_TEXT_BUFFER (self->buffer), &begin, &end);
-      ide_source_view_scroll_to_insert (self->source_view);
-    }
-}
-
 /**
  * ide_editor_view_move_next_search_result:
  * @self: a #IdeEditorView
@@ -1244,49 +1182,11 @@ ide_editor_view_move_next_search_result_cb (GObject      *object,
 void
 ide_editor_view_move_next_search_result (IdeEditorView *self)
 {
-  GtkTextIter begin;
-  GtkTextIter end;
-
   g_return_if_fail (IDE_IS_EDITOR_VIEW (self));
   g_return_if_fail (self->destroy_cancellable != NULL);
   g_return_if_fail (self->buffer != NULL);
 
-  if (self->search_context == NULL)
-    return;
-
-  if (gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (self->buffer), &begin, &end))
-    gtk_text_iter_order (&begin, &end);
-
-  gtk_source_search_context_forward_async (self->search_context,
-                                           &end,
-                                           self->destroy_cancellable,
-                                           ide_editor_view_move_next_search_result_cb,
-                                           g_object_ref (self));
-}
-
-static void
-ide_editor_view_move_previous_search_result_cb (GObject      *object,
-                                                GAsyncResult *result,
-                                                gpointer      user_data)
-{
-  GtkSourceSearchContext *context = (GtkSourceSearchContext *)object;
-  g_autoptr(IdeEditorView) self = user_data;
-  g_autoptr(GError) error = NULL;
-  GtkTextIter begin;
-  GtkTextIter end;
-  gboolean has_wrapped = FALSE;
-
-  g_assert (IDE_IS_EDITOR_VIEW (self));
-  g_assert (G_IS_ASYNC_RESULT (result));
-
-  if (self->buffer == NULL)
-    return;
-
-  if (gtk_source_search_context_backward_finish2 (context, result, &begin, &end, &has_wrapped, &error))
-    {
-      gtk_text_buffer_select_range (GTK_TEXT_BUFFER (self->buffer), &begin, &end);
-      ide_source_view_scroll_to_insert (self->source_view);
-    }
+  ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_NEXT);
 }
 
 /**
@@ -1302,22 +1202,25 @@ ide_editor_view_move_previous_search_result_cb (GObject      *object,
 void
 ide_editor_view_move_previous_search_result (IdeEditorView *self)
 {
-  GtkTextIter begin;
-  GtkTextIter end;
-
   g_return_if_fail (IDE_IS_EDITOR_VIEW (self));
   g_return_if_fail (self->destroy_cancellable != NULL);
   g_return_if_fail (self->buffer != NULL);
 
-  if (self->search_context == NULL)
-    return;
+  ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_PREVIOUS);
+}
 
-  if (gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (self->buffer), &begin, &end))
-    gtk_text_iter_order (&begin, &end);
+/**
+ * ide_editor_view_get_search:
+ * @self: a #IdeEditorView
+ *
+ * Gets the #IdeEditorSearch used to search within the document.
+ *
+ * Returns: (transfer none): An #IdeEditorSearch
+ */
+IdeEditorSearch *
+ide_editor_view_get_search (IdeEditorView *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_VIEW (self), NULL);
 
-  gtk_source_search_context_backward_async (self->search_context,
-                                            &begin,
-                                            self->destroy_cancellable,
-                                            ide_editor_view_move_previous_search_result_cb,
-                                            g_object_ref (self));
+  return self->search;
 }
diff --git a/src/libide/editor/ide-editor-view.h b/src/libide/editor/ide-editor-view.h
index bf28cd7..1f18444 100644
--- a/src/libide/editor/ide-editor-view.h
+++ b/src/libide/editor/ide-editor-view.h
@@ -21,6 +21,7 @@
 #include <gtksourceview/gtksource.h>
 
 #include "buffers/ide-buffer.h"
+#include "editor/ide-editor-search.h"
 #include "layout/ide-layout-view.h"
 #include "sourceview/ide-source-view.h"
 
@@ -32,6 +33,7 @@ G_DECLARE_FINAL_TYPE (IdeEditorView, ide_editor_view, IDE, EDITOR_VIEW, IdeLayou
 
 IdeBuffer         *ide_editor_view_get_buffer                  (IdeEditorView     *self);
 IdeSourceView     *ide_editor_view_get_view                    (IdeEditorView     *self);
+IdeEditorSearch   *ide_editor_view_get_search                  (IdeEditorView     *self);
 const gchar       *ide_editor_view_get_language_id             (IdeEditorView     *self);
 void               ide_editor_view_scroll_to_line              (IdeEditorView     *self,
                                                                 guint              line);
diff --git a/src/libide/editor/meson.build b/src/libide/editor/meson.build
index 10df754..60c39b8 100644
--- a/src/libide/editor/meson.build
+++ b/src/libide/editor/meson.build
@@ -1,6 +1,7 @@
 editor_headers = [
   'ide-editor-addin.h',
   'ide-editor-perspective.h',
+  'ide-editor-search.h',
   'ide-editor-sidebar.h',
   'ide-editor-utilities.h',
   'ide-editor-view-addin.h',
@@ -10,6 +11,7 @@ editor_headers = [
 editor_sources = [
   'ide-editor-addin.c',
   'ide-editor-perspective.c',
+  'ide-editor-search.c',
   'ide-editor-sidebar.c',
   'ide-editor-utilities.c',
   'ide-editor-view-addin.c',
@@ -30,7 +32,6 @@ editor_private_sources = [
   'ide-editor-properties.c',
   'ide-editor-properties.h',
   'ide-editor-search-bar.c',
-  'ide-editor-search-bar-actions.c',
   'ide-editor-search-bar-shortcuts.c',
   'ide-editor-search-bar.h',
   'ide-editor-view-actions.c',
diff --git a/src/libide/ide.h b/src/libide/ide.h
index 065ba9f..f963504 100644
--- a/src/libide/ide.h
+++ b/src/libide/ide.h
@@ -91,6 +91,7 @@ G_BEGIN_DECLS
 #include "documentation/ide-documentation-provider.h"
 #include "editor/ide-editor-addin.h"
 #include "editor/ide-editor-perspective.h"
+#include "editor/ide-editor-search.h"
 #include "editor/ide-editor-sidebar.h"
 #include "editor/ide-editor-utilities.h"
 #include "editor/ide-editor-view-addin.h"
diff --git a/src/libide/util/ide-action-group.h b/src/libide/util/ide-action-group.h
new file mode 100644
index 0000000..d44042a
--- /dev/null
+++ b/src/libide/util/ide-action-group.h
@@ -0,0 +1,197 @@
+/* ide-action-group.h
+ *
+ * Copyright (C) 2017 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/>.
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define IDE_DEFINE_ACTION_GROUP(Type, prefix, ...)                                \
+struct _##Type##ActionEntry {                                                     \
+  const gchar *name;                                                              \
+  void (*activate) (Type *self, GVariant *param);                                 \
+  const gchar *parameter_type;                                                    \
+  const gchar *state;                                                             \
+  void (*change_state) (Type *self, GVariant *state);                             \
+} prefix##_actions[] = __VA_ARGS__;                                               \
+                                                                                  \
+typedef struct {                                                                  \
+  GVariant *state;                                                                \
+  GVariant *state_hint;                                                           \
+  guint enabled : 1;                                                              \
+} Type##ActionInfo;                                                               \
+                                                                                  \
+static gboolean                                                                   \
+_##prefix##_has_action (GActionGroup *group,                                      \
+                        const gchar *name)                                        \
+{                                                                                 \
+  for (guint i = 0; i < G_N_ELEMENTS(prefix##_actions); i++)                      \
+    {                                                                             \
+      if (g_strcmp0 (name, prefix##_actions[i].name) == 0)                        \
+        return TRUE;                                                              \
+    }                                                                             \
+  return FALSE;                                                                   \
+}                                                                                 \
+                                                                                  \
+static gchar **                                                                   \
+_##prefix##_list_actions (GActionGroup *group)                                    \
+{                                                                                 \
+  GPtrArray *ar = g_ptr_array_new ();                                             \
+                                                                                  \
+  for (guint i = 0; i < G_N_ELEMENTS(prefix##_actions); i++)                      \
+    g_ptr_array_add (ar, g_strdup (prefix##_actions[i].name));                    \
+  g_ptr_array_add (ar, NULL);                                                     \
+                                                                                  \
+  return (gchar **)g_ptr_array_free (ar, FALSE);                                  \
+}                                                                                 \
+                                                                                  \
+static void                                                                       \
+_##prefix##_action_info_free (gpointer data)                                      \
+{                                                                                 \
+  Type##ActionInfo *info = data;                                                  \
+  g_clear_pointer (&info->state, g_variant_unref);                                \
+  g_clear_pointer (&info->state_hint, g_variant_unref);                           \
+  g_slice_free (Type##ActionInfo, info);                                          \
+}                                                                                 \
+                                                                                  \
+static Type##ActionInfo *                                                         \
+_##prefix##_get_action_info (GActionGroup *group,                                 \
+                             const gchar *name)                                   \
+{                                                                                 \
+  g_autofree gchar *fullname = g_strdup_printf ("ACTION-INFO:%s", name);          \
+  Type##ActionInfo *info = g_object_get_data (G_OBJECT (group), fullname);        \
+  if (info == NULL)                                                               \
+    {                                                                             \
+      info = g_slice_new0 (Type##ActionInfo);                                     \
+      info->enabled = TRUE;                                                       \
+      g_object_set_data_full (G_OBJECT (group), fullname, info,                   \
+                              _##prefix##_action_info_free);                      \
+    }                                                                             \
+  return info;                                                                    \
+}                                                                                 \
+                                                                                  \
+static void                                                                       \
+prefix##_set_action_state (Type *self,                                            \
+                           const gchar *name,                                     \
+                           GVariant *state)                                       \
+{                                                                                 \
+  Type##ActionInfo *info = _##prefix##_get_action_info (G_ACTION_GROUP (self),    \
+                                                        name);                    \
+  if (state != info->state)                                                       \
+    {                                                                             \
+      g_clear_pointer (&info->state, g_variant_unref);                            \
+      info->state = state ? g_variant_ref_sink (state) : NULL;                    \
+      g_action_group_action_state_changed (G_ACTION_GROUP (self), name, state);   \
+    }                                                                             \
+}                                                                                 \
+                                                                                  \
+static void                                                                       \
+prefix##_set_action_enabled (Type *self,                                          \
+                             const gchar *name,                                   \
+                             gboolean enabled)                                    \
+{                                                                                 \
+  Type##ActionInfo *info = _##prefix##_get_action_info (G_ACTION_GROUP (self),    \
+                                                        name);                    \
+  if (enabled != info->enabled)                                                   \
+    {                                                                             \
+      info->enabled = !!enabled;                                                  \
+      g_action_group_action_enabled_changed (G_ACTION_GROUP (self),               \
+                                             name, enabled);                      \
+    }                                                                             \
+}                                                                                 \
+                                                                                  \
+static void                                                                       \
+_##prefix##_change_action_state (GActionGroup *group,                             \
+                                 const gchar *name,                               \
+                                 GVariant *state)                                 \
+{                                                                                 \
+  for (guint i = 0; i < G_N_ELEMENTS(prefix##_actions); i++)                      \
+    {                                                                             \
+      if (g_strcmp0 (name, prefix##_actions[i].name) == 0)                        \
+        {                                                                         \
+          if (prefix##_actions[i].change_state)                                   \
+            prefix##_actions[i].change_state ((Type*)group, state);               \
+          return;                                                                 \
+        }                                                                         \
+    }                                                                             \
+}                                                                                 \
+                                                                                  \
+static void                                                                       \
+_##prefix##_activate_action (GActionGroup *group,                                 \
+                             const gchar *name,                                   \
+                             GVariant *param)                                     \
+{                                                                                 \
+  for (guint i = 0; i < G_N_ELEMENTS(prefix##_actions); i++)                      \
+    {                                                                             \
+      if (g_strcmp0 (name, prefix##_actions[i].name) == 0)                        \
+        {                                                                         \
+          if (prefix##_actions[i].activate)                                       \
+            prefix##_actions[i].activate ((Type*)group, param);                   \
+          return;                                                                 \
+        }                                                                         \
+    }                                                                             \
+}                                                                                 \
+                                                                                  \
+static gboolean                                                                   \
+_##prefix##_query_action (GActionGroup *group,                                    \
+                          const gchar *name,                                      \
+                          gboolean *enabled,                                      \
+                          const GVariantType **parameter_type,                    \
+                          const GVariantType **state_type,                        \
+                          GVariant **state_hint,                                  \
+                          GVariant **state)                                       \
+{                                                                                 \
+  for (guint i = 0; i < G_N_ELEMENTS(prefix##_actions); i++)                      \
+    {                                                                             \
+      if (g_strcmp0 (name, prefix##_actions[i].name) == 0)                        \
+        {                                                                         \
+          Type##ActionInfo *info = _##prefix##_get_action_info(group, name);      \
+          if (prefix##_actions[i].change_state && state_type)                     \
+            *state_type = prefix##_actions[i].parameter_type ?                    \
+                          G_VARIANT_TYPE(prefix##_actions[i].parameter_type) :    \
+                          NULL;                                                   \
+          else if (prefix##_actions[i].activate && parameter_type)                \
+            *parameter_type = prefix##_actions[i].parameter_type ?                \
+                              G_VARIANT_TYPE(prefix##_actions[i].parameter_type) :\
+                              NULL;                                               \
+          if (state_hint)                                                         \
+            *state_hint = info->state_hint != NULL ?                              \
+                          g_variant_ref (info->state_hint) : NULL;                \
+          if (state)                                                              \
+            *state = info->state != NULL ?                                        \
+                     g_variant_ref (info->state) : NULL;                          \
+          if (enabled)                                                            \
+            *enabled = info->enabled;                                             \
+          return TRUE;                                                            \
+        }                                                                         \
+    }                                                                             \
+  return FALSE;                                                                   \
+}                                                                                 \
+                                                                                  \
+static void                                                                       \
+prefix##_init_action_group (GActionGroupInterface *iface)                         \
+{                                                                                 \
+  iface->has_action = _##prefix##_has_action;                                     \
+  iface->list_actions = _##prefix##_list_actions;                                 \
+  iface->change_action_state = _##prefix##_change_action_state;                   \
+  iface->activate_action = _##prefix##_activate_action;                           \
+  iface->query_action = _##prefix##_query_action;                                 \
+}
+
+G_END_DECLS


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