[gnome-builder] libide/editor: port editor search from Text Editor



commit 49fca918d327e7adfae690991bbd326abb8d47a2
Author: Christian Hergert <chergert redhat com>
Date:   Fri Jul 15 13:35:11 2022 -0700

    libide/editor: port editor search from Text Editor
    
    This also revamps it quite a bit to be a different search style that we're
    looking to do in Text Editor at some point too. It's not exact to the
    designs, but mostly because we'll want a flowbox like layout manager that
    can reflow buttons based on allocated width.

 src/libide/editor/ide-editor-page-private.h       |   3 +
 src/libide/editor/ide-editor-page.c               |  68 ++
 src/libide/editor/ide-editor-page.h               |   2 +
 src/libide/editor/ide-editor-page.ui              |  25 +
 src/libide/editor/ide-editor-search-bar-private.h |  78 +++
 src/libide/editor/ide-editor-search-bar.c         | 798 ++++++++++++++++++++++
 src/libide/editor/ide-editor-search-bar.ui        | 158 +++++
 src/libide/editor/libide-editor.gresource.xml     |   1 +
 src/libide/editor/meson.build                     |  28 +-
 src/libide/gtk/ide-search-entry.ui                |   1 +
 src/libide/sourceview/ide-source-view.c           |  10 +-
 11 files changed, 1157 insertions(+), 15 deletions(-)
---
diff --git a/src/libide/editor/ide-editor-page-private.h b/src/libide/editor/ide-editor-page-private.h
index b9aca73b8..b9233a989 100644
--- a/src/libide/editor/ide-editor-page-private.h
+++ b/src/libide/editor/ide-editor-page-private.h
@@ -23,6 +23,7 @@
 #include <libide-plugins.h>
 
 #include "ide-editor-page.h"
+#include "ide-editor-search-bar-private.h"
 
 G_BEGIN_DECLS
 
@@ -44,6 +45,8 @@ struct _IdeEditorPage
   GtkScrolledWindow       *scroller;
   GtkSourceMap            *map;
   GtkRevealer             *map_revealer;
+  IdeEditorSearchBar      *search_bar;
+  GtkRevealer             *search_revealer;
 
   guint                    completion_blocked : 1;
 };
diff --git a/src/libide/editor/ide-editor-page.c b/src/libide/editor/ide-editor-page.c
index 1483956e8..7ee6ae3d8 100644
--- a/src/libide/editor/ide-editor-page.c
+++ b/src/libide/editor/ide-editor-page.c
@@ -321,6 +321,56 @@ ide_editor_page_get_file_or_directory (IdePage *page)
   IDE_RETURN (ret);
 }
 
+static void
+set_search_visible (IdeEditorPage          *self,
+                    gboolean                search_visible,
+                    IdeEditorSearchBarMode  mode)
+{
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
+
+  if (search_visible)
+    {
+      _ide_editor_search_bar_set_mode (self->search_bar, mode);
+      _ide_editor_search_bar_attach (self->search_bar, self->buffer);
+    }
+  else
+    {
+      _ide_editor_search_bar_detach (self->search_bar);
+    }
+
+  gtk_revealer_set_reveal_child (self->search_revealer, search_visible);
+
+  if (search_visible)
+    _ide_editor_search_bar_grab_focus (self->search_bar);
+}
+
+static void
+search_hide_action (GtkWidget  *widget,
+                    const char *action_name,
+                    GVariant   *param)
+{
+  IdeEditorPage *self = IDE_EDITOR_PAGE (widget);
+
+  set_search_visible (self, FALSE, 0);
+  gtk_widget_grab_focus (GTK_WIDGET (self->view));
+}
+
+static void
+search_begin_find_action (GtkWidget  *widget,
+                          const char *action_name,
+                          GVariant   *param)
+{
+  set_search_visible (IDE_EDITOR_PAGE (widget), TRUE, IDE_EDITOR_SEARCH_BAR_MODE_SEARCH);
+}
+
+static void
+search_begin_replace_action (GtkWidget  *widget,
+                             const char *action_name,
+                             GVariant   *param)
+{
+  set_search_visible (IDE_EDITOR_PAGE (widget), TRUE, IDE_EDITOR_SEARCH_BAR_MODE_REPLACE);
+}
+
 static void
 ide_editor_page_dispose (GObject *object)
 {
@@ -452,13 +502,23 @@ ide_editor_page_class_init (IdeEditorPageClass *klass)
   gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-editor/ide-editor-page.ui");
   gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, map);
   gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, map_revealer);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, search_bar);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, search_revealer);
   gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, scroller);
   gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, view);
   gtk_widget_class_bind_template_callback (widget_class, ide_editor_page_focus_enter_cb);
 
+  gtk_widget_class_install_action (widget_class, "search.hide", NULL, search_hide_action);
+  gtk_widget_class_install_action (widget_class, "search.begin-find", NULL, search_begin_find_action);
+  gtk_widget_class_install_action (widget_class, "search.begin-replace", NULL, search_begin_replace_action);
+
   gtk_widget_class_add_binding_action (widget_class, GDK_KEY_s, GDK_CONTROL_MASK, "page.save", NULL);
+  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_f, GDK_CONTROL_MASK, "search.begin-find", NULL);
+  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_h, GDK_CONTROL_MASK, "search.begin-replace", 
NULL);
 
   _ide_editor_page_class_actions_init (klass);
+
+  g_type_ensure (IDE_TYPE_EDITOR_SEARCH_BAR);
 }
 
 static void
@@ -859,3 +919,11 @@ ide_editor_page_scroll_to_visual_position (IdeEditorPage *self,
   gtk_text_view_scroll_mark_onscreen (GTK_TEXT_VIEW (self->view),
                                       gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (self->buffer)));
 }
+
+void
+ide_editor_page_scroll_to_insert (IdeEditorPage *self)
+{
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
+
+  ide_source_view_scroll_to_insert (self->view);
+}
diff --git a/src/libide/editor/ide-editor-page.h b/src/libide/editor/ide-editor-page.h
index b4ebf0c3d..c34251659 100644
--- a/src/libide/editor/ide-editor-page.h
+++ b/src/libide/editor/ide-editor-page.h
@@ -68,6 +68,8 @@ gboolean       ide_editor_page_save_finish               (IdeEditorPage        *
                                                           GAsyncResult         *result,
                                                           GError              **error);
 IDE_AVAILABLE_IN_ALL
+void           ide_editor_page_scroll_to_insert          (IdeEditorPage        *self);
+IDE_AVAILABLE_IN_ALL
 void           ide_editor_page_scroll_to_visual_position (IdeEditorPage        *self,
                                                           guint                 line,
                                                           guint                 column);
diff --git a/src/libide/editor/ide-editor-page.ui b/src/libide/editor/ide-editor-page.ui
index 96949e5ae..72b16ccb6 100644
--- a/src/libide/editor/ide-editor-page.ui
+++ b/src/libide/editor/ide-editor-page.ui
@@ -39,5 +39,30 @@
         </child>
       </object>
     </child>
+    <child type="content">
+      <object class="GtkRevealer" id="search_revealer">
+        <property name="reveal-child">false</property>
+        <property name="transition-type">slide-up</property>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">vertical</property>
+            <child>
+              <object class="GtkSeparator">
+                <property name="orientation">horizontal</property>
+              </object>
+            </child>
+            <child>
+              <object class="IdeEditorSearchBar" id="search_bar">
+                <property name="halign">center</property>
+                <property name="margin-top">6</property>
+                <property name="margin-bottom">6</property>
+                <property name="margin-start">6</property>
+                <property name="margin-end">6</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
   </template>
 </interface>
diff --git a/src/libide/editor/ide-editor-search-bar-private.h 
b/src/libide/editor/ide-editor-search-bar-private.h
new file mode 100644
index 000000000..3e0a6abff
--- /dev/null
+++ b/src/libide/editor/ide-editor-search-bar-private.h
@@ -0,0 +1,78 @@
+/* ide-search-bar.h
+ *
+ * Copyright 2020 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-gtk.h>
+#include <libide-sourceview.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_EDITOR_SEARCH_BAR (ide_editor_search_bar_get_type())
+
+typedef enum
+{
+  IDE_EDITOR_SEARCH_BAR_MODE_SEARCH,
+  IDE_EDITOR_SEARCH_BAR_MODE_REPLACE,
+} IdeEditorSearchBarMode;
+
+G_DECLARE_FINAL_TYPE (IdeEditorSearchBar, ide_editor_search_bar, IDE, EDITOR_SEARCH_BAR, GtkWidget)
+
+struct _IdeEditorSearchBar
+{
+  GtkWidget                parent_instance;
+
+  GtkSourceSearchContext  *context;
+  GtkSourceSearchSettings *settings;
+
+  GtkGrid                 *grid;
+  IdeSearchEntry          *search_entry;
+  GtkEntry                *replace_entry;
+  GtkButton               *replace_button;
+  GtkButton               *replace_all_button;
+  GtkToggleButton         *replace_mode_button;
+
+  guint                    offset_when_shown;
+
+  guint                    can_move : 1;
+  guint                    can_replace : 1;
+  guint                    can_replace_all : 1;
+  guint                    hide_after_move : 1;
+  guint                    scroll_to_first_match : 1;
+  guint                    jump_back_on_hide : 1;
+};
+
+void     _ide_editor_search_bar_attach              (IdeEditorSearchBar     *self,
+                                                     IdeBuffer              *buffer);
+void     _ide_editor_search_bar_detach              (IdeEditorSearchBar     *self);
+void     _ide_editor_search_bar_set_mode            (IdeEditorSearchBar     *self,
+                                                     IdeEditorSearchBarMode  mode);
+void     _ide_editor_search_bar_move_next           (IdeEditorSearchBar     *self,
+                                                     gboolean                hide_after_move);
+void     _ide_editor_search_bar_move_previous       (IdeEditorSearchBar     *self,
+                                                     gboolean                hide_after_move);
+gboolean _ide_editor_search_bar_get_can_move        (IdeEditorSearchBar     *self);
+gboolean _ide_editor_search_bar_get_can_replace     (IdeEditorSearchBar     *self);
+gboolean _ide_editor_search_bar_get_can_replace_all (IdeEditorSearchBar     *self);
+void     _ide_editor_search_bar_replace             (IdeEditorSearchBar     *self);
+void     _ide_editor_search_bar_replace_all         (IdeEditorSearchBar     *self);
+void     _ide_editor_search_bar_grab_focus          (IdeEditorSearchBar     *self);
+
+G_END_DECLS
diff --git a/src/libide/editor/ide-editor-search-bar.c b/src/libide/editor/ide-editor-search-bar.c
new file mode 100644
index 000000000..e2560ef97
--- /dev/null
+++ b/src/libide/editor/ide-editor-search-bar.c
@@ -0,0 +1,798 @@
+/* ide-editor-search-bar.c
+ *
+ * Copyright 2020-2022 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-editor-search-bar"
+
+#include "config.h"
+
+#include <libide-gui.h>
+
+#include "ide-editor-enums-private.h"
+#include "ide-editor-page-private.h"
+#include "ide-editor-search-bar-private.h"
+
+G_DEFINE_FINAL_TYPE (IdeEditorSearchBar, ide_editor_search_bar, GTK_TYPE_WIDGET)
+
+enum {
+  PROP_0,
+  PROP_CAN_MOVE,
+  PROP_CAN_REPLACE,
+  PROP_CAN_REPLACE_ALL,
+  PROP_CASE_SENSITIVE,
+  PROP_MODE,
+  PROP_USE_REGEX,
+  PROP_WHOLE_WORDS,
+  N_PROPS
+};
+
+enum {
+  MOVE_NEXT_SEARCH,
+  MOVE_PREVIOUS_SEARCH,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+update_properties (IdeEditorSearchBar *self)
+{
+  gboolean can_move = _ide_editor_search_bar_get_can_move (self);
+  gboolean can_replace = _ide_editor_search_bar_get_can_replace (self);
+  gboolean can_replace_all = _ide_editor_search_bar_get_can_replace_all (self);
+  int occurrence_position = -1;
+
+  if (can_move != self->can_move)
+    {
+      self->can_move = can_move;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CAN_MOVE]);
+    }
+
+  if (can_replace != self->can_replace)
+    {
+      self->can_replace = can_replace;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CAN_REPLACE]);
+    }
+
+  if (can_replace_all != self->can_replace_all)
+    {
+      self->can_replace_all = can_replace_all;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CAN_REPLACE_ALL]);
+    }
+
+  if (self->context != NULL)
+    {
+      GtkTextBuffer *buffer = GTK_TEXT_BUFFER (gtk_source_search_context_get_buffer (self->context));
+      GtkTextIter begin, end;
+
+      if (gtk_text_buffer_get_selection_bounds (buffer, &begin, &end))
+        occurrence_position = gtk_source_search_context_get_occurrence_position (self->context, &begin, 
&end);
+    }
+
+  ide_search_entry_set_occurrence_position (self->search_entry, occurrence_position);
+}
+
+static void
+ide_editor_search_bar_scroll_to_insert (IdeEditorSearchBar *self)
+{
+  GtkWidget *page;
+
+  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
+
+  if ((page = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_EDITOR_PAGE)))
+    ide_editor_page_scroll_to_insert (IDE_EDITOR_PAGE (page));
+}
+
+static void
+ide_editor_search_bar_move_next_forward_cb (GObject      *object,
+                                            GAsyncResult *result,
+                                            gpointer      user_data)
+{
+  GtkSourceSearchContext *context = (GtkSourceSearchContext *)object;
+  g_autoptr(IdeEditorSearchBar) self = user_data;
+  g_autoptr(GError) error = NULL;
+  GtkSourceBuffer *buffer;
+  GtkTextIter begin;
+  GtkTextIter end;
+  gboolean has_wrapped = FALSE;
+
+  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (GTK_SOURCE_IS_SEARCH_CONTEXT (context));
+
+  if (!gtk_source_search_context_forward_finish (context, result, &begin, &end, &has_wrapped, &error))
+    {
+      if (error != NULL)
+        g_debug ("Search forward error: %s", error->message);
+      return;
+    }
+
+  buffer = gtk_source_search_context_get_buffer (context);
+  gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer), &begin, &end);
+  ide_editor_search_bar_scroll_to_insert (self);
+
+  if (self->hide_after_move)
+    gtk_widget_activate_action (GTK_WIDGET (self), "search.hide", NULL);
+}
+
+void
+_ide_editor_search_bar_move_next (IdeEditorSearchBar *self,
+                                  gboolean            hide_after_move)
+{
+  GtkSourceBuffer *buffer;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
+
+  if (self->context == NULL)
+    return;
+
+  self->hide_after_move = !!hide_after_move;
+  self->jump_back_on_hide = FALSE;
+
+  buffer = gtk_source_search_context_get_buffer (self->context);
+  gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (buffer), &begin, &end);
+  gtk_text_iter_order (&begin, &end);
+
+  gtk_source_search_context_forward_async (self->context,
+                                           &end,
+                                           NULL,
+                                           ide_editor_search_bar_move_next_forward_cb,
+                                           g_object_ref (self));
+}
+
+void
+_ide_editor_search_bar_move_previous (IdeEditorSearchBar *self,
+                                      gboolean            hide_after_move)
+{
+  GtkSourceBuffer *buffer;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
+
+  if (self->context == NULL)
+    return;
+
+  self->hide_after_move = !!hide_after_move;
+  self->jump_back_on_hide = FALSE;
+
+  buffer = gtk_source_search_context_get_buffer (self->context);
+  gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (buffer), &begin, &end);
+  gtk_text_iter_order (&begin, &end);
+
+  gtk_source_search_context_backward_async (self->context,
+                                            &begin,
+                                            NULL,
+                                            /* XXX: fixme */
+                                            ide_editor_search_bar_move_next_forward_cb,
+                                            g_object_ref (self));
+}
+
+static void
+move_next_action (GtkWidget  *widget,
+                  const char *action_name,
+                  GVariant   *param)
+{
+  _ide_editor_search_bar_move_next (IDE_EDITOR_SEARCH_BAR (widget),
+                                    g_variant_get_boolean (param));
+}
+
+static void
+move_previous_action (GtkWidget  *widget,
+                      const char *action_name,
+                      GVariant   *param)
+{
+  _ide_editor_search_bar_move_previous (IDE_EDITOR_SEARCH_BAR (widget),
+                                        g_variant_get_boolean (param));
+}
+
+static gboolean
+text_to_search_text (GBinding     *binding,
+                     const GValue *from_value,
+                     GValue       *to_value,
+                     gpointer      user_data)
+{
+  IdeEditorSearchBar *self = user_data;
+  const gchar *str = g_value_get_string (from_value);
+
+  if (!str || gtk_source_search_settings_get_regex_enabled (self->settings))
+    g_value_set_string (to_value, str);
+  else
+    g_value_take_string (to_value, gtk_source_utils_unescape_search_text (str));
+
+  return TRUE;
+}
+
+static gboolean
+search_text_to_text (GBinding     *binding,
+                     const GValue *from_value,
+                     GValue       *to_value,
+                     gpointer      user_data)
+{
+  IdeEditorSearchBar *self = user_data;
+  const gchar *str = g_value_get_string (from_value);
+
+  if (str == NULL)
+    str = "";
+
+  if (gtk_source_search_settings_get_regex_enabled (self->settings))
+    g_value_take_string (to_value, gtk_source_utils_escape_search_text (str));
+  else
+    g_value_set_string (to_value, str);
+
+  return TRUE;
+}
+
+static gboolean
+mode_to_boolean (GBinding     *binding,
+                 const GValue *from_value,
+                 GValue       *to_value,
+                 gpointer      user_data)
+{
+  if (g_value_get_enum (from_value) == IDE_EDITOR_SEARCH_BAR_MODE_REPLACE)
+    g_value_set_boolean (to_value, TRUE);
+  else
+    g_value_set_boolean (to_value, FALSE);
+  return TRUE;
+}
+
+static gboolean
+boolean_to_mode (GBinding     *binding,
+                 const GValue *from_value,
+                 GValue       *to_value,
+                 gpointer      user_data)
+{
+  if (g_value_get_boolean (from_value))
+    g_value_set_enum (to_value, IDE_EDITOR_SEARCH_BAR_MODE_REPLACE);
+  else
+    g_value_set_enum (to_value, IDE_EDITOR_SEARCH_BAR_MODE_SEARCH);
+  return TRUE;
+}
+
+void
+_ide_editor_search_bar_grab_focus (IdeEditorSearchBar *self)
+{
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH_BAR (self));
+
+  gtk_widget_grab_focus (GTK_WIDGET (self->search_entry));
+}
+
+static void
+on_notify_replace_text_cb (IdeEditorSearchBar *self,
+                           GParamSpec      *pspec,
+                           GtkEntry        *entry)
+{
+  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
+  g_assert (GTK_IS_ENTRY (entry));
+
+  update_properties (self);
+}
+
+static void
+on_notify_search_text_cb (IdeEditorSearchBar   *self,
+                          GParamSpec        *pspec,
+                          IdeSearchEntry *entry)
+{
+  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
+  g_assert (IDE_IS_SEARCH_ENTRY (entry));
+
+  self->scroll_to_first_match = TRUE;
+}
+
+static gboolean
+on_search_key_pressed_cb (GtkEventControllerKey *key,
+                          guint                  keyval,
+                          guint                  keycode,
+                          GdkModifierType        state,
+                          IdeEditorSearchBar       *self)
+{
+  g_assert (GTK_IS_EVENT_CONTROLLER_KEY (key));
+  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
+
+  if ((state & (GDK_CONTROL_MASK | GDK_ALT_MASK)) == 0)
+    {
+      switch (keyval)
+        {
+        case GDK_KEY_Up:
+        case GDK_KEY_KP_Up:
+          _ide_editor_search_bar_move_previous (self, FALSE);
+          return TRUE;
+
+        case GDK_KEY_Down:
+        case GDK_KEY_KP_Down:
+          _ide_editor_search_bar_move_next (self, FALSE);
+          return TRUE;
+
+        default:
+          break;
+        }
+    }
+
+  return FALSE;
+}
+
+static void
+on_settings_notify_cb (IdeEditorSearchBar      *self,
+                       GParamSpec              *pspec,
+                       GtkSourceSearchSettings *settings)
+{
+  if (g_strcmp0 (pspec->name, "at-word-boundaries") == 0)
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WHOLE_WORDS]);
+  else if (g_strcmp0 (pspec->name, "regex-enabled") == 0)
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_USE_REGEX]);
+  else if (g_strcmp0 (pspec->name, "case-sensitive") == 0)
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CASE_SENSITIVE]);
+}
+
+static void
+ide_editor_search_bar_dispose (GObject *object)
+{
+  IdeEditorSearchBar *self = (IdeEditorSearchBar *)object;
+
+  g_clear_pointer ((GtkWidget **)&self->grid, gtk_widget_unparent);
+
+  G_OBJECT_CLASS (ide_editor_search_bar_parent_class)->dispose (object);
+}
+
+static void
+ide_editor_search_bar_finalize (GObject *object)
+{
+  IdeEditorSearchBar *self = (IdeEditorSearchBar *)object;
+
+  g_clear_object (&self->context);
+  g_clear_object (&self->settings);
+
+  G_OBJECT_CLASS (ide_editor_search_bar_parent_class)->finalize (object);
+}
+
+static void
+ide_editor_search_bar_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  IdeEditorSearchBar *self = IDE_EDITOR_SEARCH_BAR (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODE:
+      g_value_set_enum (value,
+                        gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->replace_mode_button)) ?
+                        IDE_EDITOR_SEARCH_BAR_MODE_REPLACE :
+                        IDE_EDITOR_SEARCH_BAR_MODE_SEARCH);
+      break;
+
+    case PROP_CAN_MOVE:
+      g_value_set_boolean (value, _ide_editor_search_bar_get_can_move (self));
+      break;
+
+    case PROP_CAN_REPLACE:
+      g_value_set_boolean (value, _ide_editor_search_bar_get_can_replace (self));
+      break;
+
+    case PROP_CAN_REPLACE_ALL:
+      g_value_set_boolean (value, _ide_editor_search_bar_get_can_replace_all (self));
+      break;
+
+    case PROP_CASE_SENSITIVE:
+      g_value_set_boolean (value, gtk_source_search_settings_get_case_sensitive (self->settings));
+      break;
+
+    case PROP_WHOLE_WORDS:
+      g_value_set_boolean (value, gtk_source_search_settings_get_at_word_boundaries (self->settings));
+      break;
+
+    case PROP_USE_REGEX:
+      g_value_set_boolean (value, gtk_source_search_settings_get_regex_enabled (self->settings));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_editor_search_bar_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  IdeEditorSearchBar *self = IDE_EDITOR_SEARCH_BAR (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODE:
+      _ide_editor_search_bar_set_mode (self, g_value_get_enum (value));
+      break;
+
+    case PROP_CASE_SENSITIVE:
+      gtk_source_search_settings_set_case_sensitive (self->settings, g_value_get_boolean (value));
+      break;
+
+    case PROP_WHOLE_WORDS:
+      gtk_source_search_settings_set_at_word_boundaries (self->settings, g_value_get_boolean (value));
+      break;
+
+    case PROP_USE_REGEX:
+      gtk_source_search_settings_set_regex_enabled (self->settings, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_editor_search_bar_class_init (IdeEditorSearchBarClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = ide_editor_search_bar_dispose;
+  object_class->finalize = ide_editor_search_bar_finalize;
+  object_class->get_property = ide_editor_search_bar_get_property;
+  object_class->set_property = ide_editor_search_bar_set_property;
+
+  gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+  gtk_widget_class_set_css_name (widget_class, "searchbar");
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-editor/ide-editor-search-bar.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorSearchBar, grid);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorSearchBar, replace_all_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorSearchBar, replace_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorSearchBar, replace_entry);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorSearchBar, replace_mode_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorSearchBar, search_entry);
+  gtk_widget_class_bind_template_callback (widget_class, on_search_key_pressed_cb);
+
+  signals [MOVE_NEXT_SEARCH] =
+    g_signal_new_class_handler ("move-next-search",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                G_CALLBACK (_ide_editor_search_bar_move_next),
+                                NULL, NULL,
+                                NULL,
+                                G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
+
+  signals [MOVE_PREVIOUS_SEARCH] =
+    g_signal_new_class_handler ("move-previous-search",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                G_CALLBACK (_ide_editor_search_bar_move_previous),
+                                NULL, NULL,
+                                NULL,
+                                G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
+
+  properties [PROP_MODE] =
+    g_param_spec_enum ("mode",
+                       "Mode",
+                       "The mode for the search bar",
+                       IDE_TYPE_EDITOR_SEARCH_BAR_MODE,
+                       IDE_EDITOR_SEARCH_BAR_MODE_SEARCH,
+                       (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CAN_MOVE] =
+    g_param_spec_boolean ("can-move",
+                          "Can Move",
+                          "If there are search results",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CAN_REPLACE] =
+    g_param_spec_boolean ("can-replace",
+                          "Can Replace",
+                          "If search is ready to replace a single result",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CAN_REPLACE_ALL] =
+    g_param_spec_boolean ("can-replace-all",
+                          "Can Replace All",
+                          "If search is ready to replace all results",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CASE_SENSITIVE] =
+    g_param_spec_boolean ("case-sensitive", NULL, NULL,
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_USE_REGEX] =
+    g_param_spec_boolean ("use-regex", NULL, NULL,
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_WHOLE_WORDS] =
+    g_param_spec_boolean ("whole-words", NULL, NULL,
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_install_property_action (widget_class, "search.case-sensitive", "case-sensitive");
+  gtk_widget_class_install_property_action (widget_class, "search.whole-words", "whole-words");
+  gtk_widget_class_install_property_action (widget_class, "search.use-regex", "use-regex");
+  gtk_widget_class_install_action (widget_class, "search.move-next", "b", move_next_action);
+  gtk_widget_class_install_action (widget_class, "search.move-previous", "b", move_previous_action);
+
+  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_Escape, 0, "search.hide", NULL);
+
+  g_type_ensure (IDE_TYPE_SEARCH_ENTRY);
+}
+
+static void
+ide_editor_search_bar_init (IdeEditorSearchBar *self)
+{
+  self->settings = gtk_source_search_settings_new ();
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (self->replace_entry,
+                           "notify::text",
+                           G_CALLBACK (on_notify_replace_text_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->search_entry,
+                           "notify::text",
+                           G_CALLBACK (on_notify_search_text_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->settings,
+                           "notify",
+                           G_CALLBACK (on_settings_notify_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gtk_source_search_settings_set_wrap_around (self->settings, TRUE);
+
+  g_object_bind_property_full (self->settings, "search-text",
+                               self->search_entry, "text",
+                               G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL,
+                               search_text_to_text, text_to_search_text, self, NULL);
+  g_object_bind_property_full (self->replace_mode_button, "active",
+                               self, "mode",
+                               G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL,
+                               boolean_to_mode, mode_to_boolean, NULL, NULL);
+}
+
+void
+_ide_editor_search_bar_set_mode (IdeEditorSearchBar     *self,
+                                 IdeEditorSearchBarMode  mode)
+{
+  gboolean is_replace;
+
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH_BAR (self));
+
+  is_replace = mode == IDE_EDITOR_SEARCH_BAR_MODE_REPLACE;
+
+  gtk_widget_set_visible (GTK_WIDGET (self->replace_entry), is_replace);
+  gtk_widget_set_visible (GTK_WIDGET (self->replace_button), is_replace);
+  gtk_widget_set_visible (GTK_WIDGET (self->replace_all_button), is_replace);
+  gtk_toggle_button_set_active (self->replace_mode_button, is_replace);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODE]);
+}
+
+static void
+scroll_to_first_match (IdeEditorSearchBar        *self,
+                       GtkSourceSearchContext *context)
+{
+  GtkTextIter iter, match_begin, match_end;
+  GtkTextBuffer *buffer;
+  GtkWidget *page;
+  gboolean wrapped;
+
+  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
+  g_assert (GTK_SOURCE_IS_SEARCH_CONTEXT (context));
+
+  if (!(page = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_EDITOR_PAGE)))
+    return;
+
+  buffer = GTK_TEXT_BUFFER (gtk_source_search_context_get_buffer (context));
+  gtk_text_buffer_get_iter_at_offset (buffer, &iter, self->offset_when_shown);
+  if (gtk_source_search_context_forward (context, &iter, &match_begin, &match_end, &wrapped))
+    {
+      gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (IDE_EDITOR_PAGE (page)->view),
+                                    &match_begin, 0.25, TRUE, 1.0, 0.5);
+      self->jump_back_on_hide = TRUE;
+    }
+
+  self->scroll_to_first_match = FALSE;
+}
+
+static void
+ide_editor_search_bar_notify_occurrences_count_cb (IdeEditorSearchBar     *self,
+                                                   GParamSpec             *pspec,
+                                                   GtkSourceSearchContext *context)
+{
+  guint occurrence_count;
+
+  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
+  g_assert (GTK_SOURCE_IS_SEARCH_CONTEXT (context));
+
+  occurrence_count = gtk_source_search_context_get_occurrences_count (context);
+  ide_search_entry_set_occurrence_count (self->search_entry, occurrence_count);
+
+  if (self->scroll_to_first_match && occurrence_count > 0)
+    scroll_to_first_match (self, context);
+
+  gtk_widget_action_set_enabled (GTK_WIDGET (self), "search.move-next", occurrence_count > 0);
+  gtk_widget_action_set_enabled (GTK_WIDGET (self), "search.move-previous", occurrence_count > 0);
+
+  update_properties (self);
+}
+
+static void
+ide_editor_search_bar_cursor_moved_cb (IdeEditorSearchBar *self,
+                                       IdeBuffer          *buffer)
+{
+  g_assert (IDE_IS_EDITOR_SEARCH_BAR (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  update_properties (self);
+}
+
+void
+_ide_editor_search_bar_attach (IdeEditorSearchBar *self,
+                               IdeBuffer          *buffer)
+{
+  GtkTextIter begin, end, insert;
+
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH_BAR (self));
+
+  gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer),
+                                    &insert,
+                                    gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (buffer)));
+  self->offset_when_shown = gtk_text_iter_get_offset (&insert);
+
+  if (gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (buffer), &begin, &end))
+    {
+      g_autofree gchar *text = gtk_text_iter_get_slice (&begin, &end);
+      gtk_editable_set_text (GTK_EDITABLE (self->search_entry), text);
+    }
+
+  if (self->context != NULL)
+    return;
+
+  self->context = gtk_source_search_context_new (GTK_SOURCE_BUFFER (buffer), self->settings);
+
+  g_signal_connect_object (self->context,
+                           "notify::occurrences-count",
+                           G_CALLBACK (ide_editor_search_bar_notify_occurrences_count_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (buffer,
+                           "cursor-moved",
+                           G_CALLBACK (ide_editor_search_bar_cursor_moved_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+void
+_ide_editor_search_bar_detach (IdeEditorSearchBar *self)
+{
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH_BAR (self));
+
+  if (self->context != NULL)
+    {
+      IdeBuffer *buffer = IDE_BUFFER (gtk_source_search_context_get_buffer (self->context));
+
+      if (self->jump_back_on_hide)
+        ide_editor_search_bar_scroll_to_insert (self);
+
+      g_signal_handlers_disconnect_by_func (self->context,
+                                            G_CALLBACK (ide_editor_search_bar_notify_occurrences_count_cb),
+                                            self);
+      g_signal_handlers_disconnect_by_func (buffer,
+                                            G_CALLBACK (ide_editor_search_bar_cursor_moved_cb),
+                                            self);
+
+      g_clear_object (&self->context);
+    }
+
+  self->hide_after_move = FALSE;
+  self->jump_back_on_hide = FALSE;
+}
+
+gboolean
+_ide_editor_search_bar_get_can_move (IdeEditorSearchBar *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_SEARCH_BAR (self), FALSE);
+
+  return self->context != NULL &&
+         gtk_source_search_context_get_occurrences_count (self->context) > 0;
+}
+
+gboolean
+_ide_editor_search_bar_get_can_replace (IdeEditorSearchBar *self)
+{
+  GtkTextIter begin, end;
+  GtkTextBuffer *buffer;
+
+  g_return_val_if_fail (IDE_IS_EDITOR_SEARCH_BAR (self), FALSE);
+
+  if (self->context == NULL)
+    return FALSE;
+
+  buffer = GTK_TEXT_BUFFER (gtk_source_search_context_get_buffer (self->context));
+
+  return _ide_editor_search_bar_get_can_move (self) &&
+         gtk_text_buffer_get_selection_bounds (buffer, &begin, &end) &&
+         gtk_source_search_context_get_occurrence_position (self->context, &begin, &end) > 0;
+}
+
+gboolean
+_ide_editor_search_bar_get_can_replace_all (IdeEditorSearchBar *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_SEARCH_BAR (self), FALSE);
+
+  return _ide_editor_search_bar_get_can_move (self);
+}
+
+void
+_ide_editor_search_bar_replace (IdeEditorSearchBar *self)
+{
+  g_autoptr(GError) error = NULL;
+  GtkSourceBuffer *buffer;
+  GtkTextIter begin, end;
+  const char *replace;
+
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH_BAR (self));
+
+  if (!_ide_editor_search_bar_get_can_replace (self))
+    return;
+
+  buffer = gtk_source_search_context_get_buffer (self->context);
+  replace = gtk_editable_get_text (GTK_EDITABLE (self->replace_entry));
+
+  gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (buffer), &begin, &end);
+
+  if (!gtk_source_search_context_replace (self->context, &begin, &end, replace, -1, &error))
+    {
+      g_warning ("Failed to replace match: %s", error->message);
+      return;
+    }
+
+  gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer), &end, &end);
+  _ide_editor_search_bar_move_next (self, FALSE);
+}
+
+void
+_ide_editor_search_bar_replace_all (IdeEditorSearchBar *self)
+{
+  g_autoptr(GError) error = NULL;
+  g_autofree char *unescaped = NULL;
+  const char *replace;
+
+  g_return_if_fail (IDE_IS_EDITOR_SEARCH_BAR (self));
+
+  if (!_ide_editor_search_bar_get_can_replace_all (self))
+    return;
+
+  replace = gtk_editable_get_text (GTK_EDITABLE (self->replace_entry));
+  unescaped = gtk_source_utils_unescape_search_text (replace);
+
+  if (!gtk_source_search_context_replace_all (self->context, unescaped, -1, &error))
+    g_warning ("Failed to replace all matches: %s", error->message);
+}
diff --git a/src/libide/editor/ide-editor-search-bar.ui b/src/libide/editor/ide-editor-search-bar.ui
new file mode 100644
index 000000000..e075f30a6
--- /dev/null
+++ b/src/libide/editor/ide-editor-search-bar.ui
@@ -0,0 +1,158 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <template class="IdeEditorSearchBar" parent="GtkWidget">
+    <child>
+      <object class="GtkGrid" id="grid">
+        <property name="column-spacing">6</property>
+        <property name="row-spacing">6</property>
+        <child>
+          <object class="IdeSearchEntry" id="search_entry">
+            <property name="hexpand">true</property>
+            <layout>
+              <property name="row">0</property>
+              <property name="column">0</property>
+            </layout>
+            <child>
+              <object class="GtkEventControllerKey">
+                <property name="propagation-phase">capture</property>
+                <signal name="key-pressed" handler="on_search_key_pressed_cb"/>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkEntry" id="replace_entry">
+            <property name="primary-icon-name">edit-find-replace-symbolic</property>
+            <property name="max-width-chars">20</property>
+            <property name="hexpand">true</property>
+            <property name="visible">false</property>
+            <property name="placeholder-text" translatable="yes">Replace</property>
+            <layout>
+              <property name="row">1</property>
+              <property name="column">0</property>
+            </layout>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="homogeneous">true</property>
+            <property name="orientation">horizontal</property>
+            <property name="spacing">3</property>
+            <child>
+              <object class="GtkButton">
+                <property name="action-name">search.move-previous</property>
+                <property name="action-target">false</property>
+                <property name="tooltip-text" translatable="yes">Move to previous match 
(Ctrl+Shift+G)</property>
+                <property name="icon-name">go-up-symbolic</property>
+                <style>
+                  <class name="flat"/>
+                </style>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton">
+                <property name="action-name">search.move-next</property>
+                <property name="action-target">false</property>
+                <property name="tooltip-text" translatable="yes">Move to next match (Ctrl+G)</property>
+                <property name="icon-name">go-down-symbolic</property>
+                <style>
+                  <class name="flat"/>
+                </style>
+              </object>
+            </child>
+            <layout>
+              <property name="row">0</property>
+              <property name="column">1</property>
+            </layout>
+          </object>
+        </child>
+        <child>
+          <object class="GtkToggleButton" id="replace_mode_button">
+            <property name="tooltip-text" translatable="yes">Search &amp; Replace (Ctrl+H)</property>
+            <property name="icon-name">edit-find-replace-symbolic</property>
+            <layout>
+              <property name="row">0</property>
+              <property name="column">2</property>
+            </layout>
+            <style>
+              <class name="flat"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkMenuButton">
+            <property name="direction">up</property>
+            <property name="icon-name">view-more-symbolic</property>
+            <property name="menu-model">options_menu</property>
+            <layout>
+              <property name="row">0</property>
+              <property name="column">3</property>
+            </layout>
+            <style>
+              <class name="flat"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkButton" id="close_button">
+            <property name="margin-end">3</property>
+            <property name="action-name">search.hide</property>
+            <property name="halign">center</property>
+            <property name="valign">center</property>
+            <property name="tooltip-text" translatable="yes">Close search</property>
+            <property name="icon-name">window-close-symbolic</property>
+            <style>
+              <class name="flat"/>
+              <class name="small-button"/>
+              <class name="circular"/>
+            </style>
+            <layout>
+              <property name="row">0</property>
+              <property name="column">4</property>
+            </layout>
+          </object>
+        </child>
+        <child>
+          <object class="GtkButton" id="replace_button">
+            <property name="action-name">search.replace-one</property>
+            <property name="use-underline">true</property>
+            <property name="label" translatable="yes">_Replace</property>
+            <property name="visible">false</property>
+            <layout>
+              <property name="row">1</property>
+              <property name="column">1</property>
+            </layout>
+          </object>
+        </child>
+        <child>
+          <object class="GtkButton" id="replace_all_button">
+            <property name="action-name">search.replace-all</property>
+            <property name="use-underline">true</property>
+            <property name="label" translatable="yes">Replace _All</property>
+            <property name="visible">false</property>
+            <layout>
+              <property name="row">1</property>
+              <property name="column">2</property>
+              <property name="column-span">3</property>
+            </layout>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <menu id="options_menu">
+    <item>
+      <attribute name="label" translatable="yes">Use Re_gular Expressions</attribute>
+      <attribute name="action">search.use-regex</attribute>
+    </item>
+    <item>
+      <attribute name="label" translatable="yes">_Case Sensitive</attribute>
+      <attribute name="action">search.case-sensitive</attribute>
+    </item>
+    <item>
+      <attribute name="label" translatable="yes">Match Whole Words</attribute>
+      <attribute name="action">search.whole-words</attribute>
+    </item>
+  </menu>
+</interface>
diff --git a/src/libide/editor/libide-editor.gresource.xml b/src/libide/editor/libide-editor.gresource.xml
index 8ceb752e9..d9d0869bd 100644
--- a/src/libide/editor/libide-editor.gresource.xml
+++ b/src/libide/editor/libide-editor.gresource.xml
@@ -2,6 +2,7 @@
 <gresources>
   <gresource prefix="/org/gnome/libide-editor/">
     <file preprocess="xml-stripblanks">ide-editor-page.ui</file>
+    <file preprocess="xml-stripblanks">ide-editor-search-bar.ui</file>
     <file preprocess="xml-stripblanks">ide-editor-workspace.ui</file>
     <file preprocess="xml-stripblanks">gtk/menus.ui</file>
     <file>style.css</file>
diff --git a/src/libide/editor/meson.build b/src/libide/editor/meson.build
index c6eb702b8..c1105c696 100644
--- a/src/libide/editor/meson.build
+++ b/src/libide/editor/meson.build
@@ -42,11 +42,9 @@ libide_editor_private_sources = [
   'ide-editor-init.c',
   'ide-editor-page-actions.c',
   'ide-editor-page-settings.c',
+  'ide-editor-search-bar.c',
 ]
 
-libide_editor_sources += libide_editor_public_sources
-libide_editor_sources += libide_editor_private_sources
-
 #
 # Generated Resource Files
 #
@@ -59,6 +57,17 @@ libide_editor_resources = gnome.compile_resources(
 libide_editor_generated_headers += [libide_editor_resources[1]]
 libide_editor_sources += libide_editor_resources
 
+#
+# Enums Generation
+#
+libide_editor_private_enums_headers = [
+  'ide-editor-search-bar-private.h',
+]
+libide_editor_private_enums = gnome.mkenums_simple('ide-editor-enums-private',
+     body_prefix: '#include "config.h"',
+         sources: libide_editor_private_enums_headers,
+)
+
 #
 # Dependencies
 #
@@ -78,16 +87,17 @@ libide_editor_deps = [
   libide_gui_dep,
 ]
 
-libide_editor_internal_deps = [
-]
-
 #
 # Library Definitions
 #
 
-libide_editor = static_library('ide-editor-' + libide_api_version, libide_editor_sources,
-   dependencies: libide_editor_deps + libide_editor_internal_deps,
-         c_args: libide_args + release_args + ['-DIDE_EDITOR_COMPILATION'],
+libide_editor_sources += libide_editor_public_sources
+libide_editor_sources += libide_editor_private_sources
+
+libide_editor = static_library('ide-editor-' + libide_api_version,
+  libide_editor_sources + libide_editor_private_enums,
+  dependencies: libide_editor_deps,
+        c_args: libide_args + release_args + ['-DIDE_EDITOR_COMPILATION'],
 )
 
 libide_editor_dep = declare_dependency(
diff --git a/src/libide/gtk/ide-search-entry.ui b/src/libide/gtk/ide-search-entry.ui
index 7bcb1c30c..e751faee1 100644
--- a/src/libide/gtk/ide-search-entry.ui
+++ b/src/libide/gtk/ide-search-entry.ui
@@ -14,6 +14,7 @@
         <property name="vexpand">true</property>
         <property name="width-chars">12</property>
         <property name="max-width-chars">12</property>
+        <property name="placeholder-text" translatable="yes">Search</property>
         <signal name="notify" handler="on_text_notify_cb" swapped="true"/>
         <signal name="activate" handler="on_text_activate_cb" swapped="true"/>
       </object>
diff --git a/src/libide/sourceview/ide-source-view.c b/src/libide/sourceview/ide-source-view.c
index 1442fa952..17348ce0e 100644
--- a/src/libide/sourceview/ide-source-view.c
+++ b/src/libide/sourceview/ide-source-view.c
@@ -855,17 +855,15 @@ void
 ide_source_view_scroll_to_insert (IdeSourceView *self)
 {
   GtkTextBuffer *buffer;
-  GtkTextView *view;
   GtkTextMark *mark;
+  GtkTextIter iter;
 
   g_return_if_fail (IDE_IS_SOURCE_VIEW (self));
 
-  view = GTK_TEXT_VIEW (self);
-  buffer = gtk_text_view_get_buffer (view);
+  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self));
   mark = gtk_text_buffer_get_insert (buffer);
-
-  /* TODO: use margin to implement  "scroll offset" */
-  gtk_text_view_scroll_to_mark (view, mark, .25, FALSE, .0, .0);
+  gtk_text_buffer_get_iter_at_mark (buffer, &iter, mark);
+  ide_source_view_jump_to_iter (GTK_TEXT_VIEW (self), &iter, .25, TRUE, 1.0, 0.5);
 }
 
 void


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