[gnome-builder] grep: Rework whole plugin



commit 494bc9463891c359486ceec9179f4017dd5201a7
Author: vanadiae <vanadiae35 gmail com>
Date:   Sun Aug 1 18:30:01 2021 +0200

    grep: Rework whole plugin
    
    This modifies the plugin in the following ways:
    - There is now a persistent project-wide utilities panel that can't be
    closed. This helps avoiding that the search panels keep piling up in the
    panels list, by encouraging to use the project-wide persistent panel.
    - Going with the previous change, the search settings can be changed
    from within the panel, not only from the project tree's Find in Files
    popover. Search queries can be changed and re-done from the same panel.
    Replacements can be done once a search has been done.
    - Searches done from the Find in Files popover are created in a new
    grep panel, and can be closed either using its close button, or using
    the Escape key with the focus inside the panel.
    - Ctrl+Shift+F can be used to reveal the project-wide grep panel
    
    That doesn't solve all the issues the grep panel has, but it improves
    the situation greatly. In particular, I'd like at one point to make
    the search results appear in a tree -like way, with tree levels being
    each directory or file, like so:
    
    data
    => meson.build
    =>=> meson.build line 44
    =>=> meson.build line 55
    => foobar.svg
    =>=> foobar.svg line 66
    src
    => foobar.c
    =>=> foobar.c line 6
    
    That way we would be able to collapse entirely folders or files results
    that we don't care about, like the po/ directory or simply unselecting
    all the results on a file so that it is faster to only do replacing in
    files we want. All this will likely be easier to do in GTK 4.

 src/libide/gui/ide-shortcuts-window.ui   |   7 +
 src/plugins/grep/gbp-grep-editor-addin.c | 133 ++++++++++++
 src/plugins/grep/gbp-grep-editor-addin.h |  31 +++
 src/plugins/grep/gbp-grep-model.c        |  10 +-
 src/plugins/grep/gbp-grep-panel.c        | 356 ++++++++++++++++++++++++++++---
 src/plugins/grep/gbp-grep-panel.h        |   9 +-
 src/plugins/grep/gbp-grep-panel.ui       | 168 +++++++++++----
 src/plugins/grep/gbp-grep-popover.c      |  31 +--
 src/plugins/grep/gbp-grep-tree-addin.c   |  23 --
 src/plugins/grep/grep-plugin.c           |   5 +
 src/plugins/grep/meson.build             |   1 +
 11 files changed, 648 insertions(+), 126 deletions(-)
---
diff --git a/src/libide/gui/ide-shortcuts-window.ui b/src/libide/gui/ide-shortcuts-window.ui
index e4615da45..36a987b06 100644
--- a/src/libide/gui/ide-shortcuts-window.ui
+++ b/src/libide/gui/ide-shortcuts-window.ui
@@ -95,6 +95,13 @@
                 <property name="accelerator">&lt;ctrl&gt;F9</property>
               </object>
             </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="title" translatable="yes" context="shortcut window">Show Find in Project 
panel</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;f</property>
+              </object>
+            </child>
           </object>
         </child>
         <child>
diff --git a/src/plugins/grep/gbp-grep-editor-addin.c b/src/plugins/grep/gbp-grep-editor-addin.c
new file mode 100644
index 000000000..d599a01de
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-editor-addin.c
@@ -0,0 +1,133 @@
+/* gbp-grep-editor-addin.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-grep-editor-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-editor.h>
+#include <libide-foundry.h>
+#include <libide-gui.h>
+
+#include "gbp-grep-editor-addin.h"
+#include "gbp-grep-panel.h"
+
+#define I_(s) g_intern_static_string(s)
+
+struct _GbpGrepEditorAddin
+{
+  GObject    parent_instance;
+
+  GtkWidget *panel;
+};
+
+static void
+gbp_grep_editor_page_addin_show_project_panel_action (GSimpleAction *action,
+                                                      GVariant      *variant,
+                                                      gpointer       user_data)
+{
+  GbpGrepEditorAddin *self = GBP_GREP_EDITOR_ADDIN (user_data);
+
+  ide_widget_reveal_and_grab (self->panel);
+}
+
+static const DzlShortcutEntry grep_shortcut_entries[] = {
+  { "org.gnome.builder.panel",
+    0, NULL,
+    N_("Editor shortcuts"),
+    N_("Panels"),
+    N_("Show Find in Project panel") },
+};
+
+static const GActionEntry actions[] = {
+  { "show-project-panel", gbp_grep_editor_page_addin_show_project_panel_action },
+};
+
+static void
+gbp_grep_editor_addin_load (IdeEditorAddin   *addin,
+                            IdeEditorSurface *editor_surface)
+{
+  GbpGrepEditorAddin *self = (GbpGrepEditorAddin *)addin;
+  GtkWidget *utilities;
+  g_autoptr(GSimpleActionGroup) group = NULL;
+  DzlShortcutController *controller;
+
+  g_assert (GBP_IS_GREP_EDITOR_ADDIN (self));
+
+  utilities = ide_editor_surface_get_utilities (IDE_EDITOR_SURFACE (editor_surface));
+
+  self->panel = gbp_grep_panel_new ();
+  gtk_container_add (GTK_CONTAINER (utilities), self->panel);
+  gtk_widget_show (self->panel);
+
+  group = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (group),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (editor_surface), "grep", G_ACTION_GROUP (group));
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (editor_surface));
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.panel"),
+                                              I_("<Primary><Shift>f"),
+                                              DZL_SHORTCUT_PHASE_GLOBAL,
+                                              I_("grep.show-project-panel"));
+
+  dzl_shortcut_manager_add_shortcut_entries (NULL,
+                                             grep_shortcut_entries,
+                                             G_N_ELEMENTS (grep_shortcut_entries),
+                                             GETTEXT_PACKAGE);
+}
+
+static void
+gbp_grep_editor_addin_unload (IdeEditorAddin   *addin,
+                              IdeEditorSurface *editor_surface)
+{
+  GbpGrepEditorAddin *self = (GbpGrepEditorAddin *)addin;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_GREP_EDITOR_ADDIN (self));
+
+  gtk_widget_insert_action_group (GTK_WIDGET (editor_surface), "grep", NULL);
+
+  g_clear_pointer (&self->panel, gtk_widget_destroy);
+}
+
+static void
+editor_addin_iface_init (IdeEditorAddinInterface *iface)
+{
+  iface->load = gbp_grep_editor_addin_load;
+  iface->unload = gbp_grep_editor_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpGrepEditorAddin, gbp_grep_editor_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_EDITOR_ADDIN, editor_addin_iface_init))
+
+static void
+gbp_grep_editor_addin_class_init (GbpGrepEditorAddinClass *klass)
+{
+}
+
+static void
+gbp_grep_editor_addin_init (GbpGrepEditorAddin *self)
+{
+}
diff --git a/src/plugins/grep/gbp-grep-editor-addin.h b/src/plugins/grep/gbp-grep-editor-addin.h
new file mode 100644
index 000000000..152273203
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-editor-addin.h
@@ -0,0 +1,31 @@
+/* gbp-grep-editor-addin.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GREP_EDITOR_ADDIN (gbp_grep_editor_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGrepEditorAddin, gbp_grep_editor_addin, GBP, GREP_EDITOR_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/grep/gbp-grep-model.c b/src/plugins/grep/gbp-grep-model.c
index ef0ea9e8a..fcdfb66de 100644
--- a/src/plugins/grep/gbp-grep-model.c
+++ b/src/plugins/grep/gbp-grep-model.c
@@ -1241,13 +1241,17 @@ GFile *
 gbp_grep_model_get_file (GbpGrepModel *self,
                          const gchar  *path)
 {
+  g_autoptr(GFile) directory = NULL;
+
   g_return_val_if_fail (GBP_IS_GREP_MODEL (self), NULL);
 
+  directory = self->directory ? g_object_ref (self->directory) : ide_context_ref_workdir (self->context);
+
   if (!path || !*path || g_strcmp0 (path, ".") == 0)
-    return g_file_dup (self->directory);
+    return g_file_dup (directory);
 
   if (self->was_directory)
-    return g_file_get_child (self->directory, path);
+    return g_file_get_child (directory, path);
   else
-    return g_file_dup (self->directory);
+    return g_file_dup (directory);
 }
diff --git a/src/plugins/grep/gbp-grep-panel.c b/src/plugins/grep/gbp-grep-panel.c
index 82f6ca243..b5c4ced58 100644
--- a/src/plugins/grep/gbp-grep-panel.c
+++ b/src/plugins/grep/gbp-grep-panel.c
@@ -29,18 +29,35 @@
 
 #include "gbp-grep-panel.h"
 
+#define I_ g_intern_string
+
 struct _GbpGrepPanel
 {
   DzlDockWidget      parent_instance;
 
+  GCancellable      *cancellable;
+
   /* Unowned references */
   GtkTreeView       *tree_view;
   GtkTreeViewColumn *toggle_column;
   GtkCheckButton    *check;
-  GtkButton         *close_button;
+
+  GtkStack          *stack;
+  GtkScrolledWindow *scrolled_window;
+  GtkSpinner        *spinner;
+
   GtkButton         *replace_button;
   GtkEntry          *replace_entry;
-  GtkSpinner        *spinner;
+
+  GtkButton         *find_button;
+  GtkEntry          *find_entry;
+
+  GtkCheckButton    *regex_button;
+  GtkCheckButton    *whole_words_button;
+  GtkCheckButton    *case_button;
+  GtkCheckButton    *recursive_button;
+
+  GtkButton         *close_button;
 };
 
 enum {
@@ -300,17 +317,10 @@ gbp_grep_panel_replace_edited_cb (GObject      *object,
   g_assert (GBP_IS_GREP_PANEL (self));
 
   if (!ide_buffer_manager_apply_edits_finish (bufmgr, result, &error))
-    {
-      ide_object_warning (IDE_OBJECT (bufmgr), "Failed to apply edits: %s", error->message);
-      return;
-    }
+    ide_object_warning (IDE_OBJECT (bufmgr), "Failed to apply edits: %s", error->message);
 
-  /* Make the treeview visible, but show the old content. Allows the user
-   * to jump to the positions that were edited.
-   */
-  gtk_widget_set_sensitive (GTK_WIDGET (self->tree_view), TRUE);
   gtk_spinner_stop (self->spinner);
-  gtk_widget_hide (GTK_WIDGET (self->spinner));
+  gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->scrolled_window));
 }
 
 static void
@@ -339,10 +349,9 @@ gbp_grep_panel_replace_clicked_cb (GbpGrepPanel *self,
 
   g_debug ("Replacing %u edit points with %s", edits->len, text);
 
-  gtk_widget_set_sensitive (GTK_WIDGET (self->tree_view), FALSE);
   gtk_widget_set_sensitive (GTK_WIDGET (self->replace_button), FALSE);
   gtk_widget_set_sensitive (GTK_WIDGET (self->replace_entry), FALSE);
-  gtk_widget_show (GTK_WIDGET (self->spinner));
+  gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->spinner));
   gtk_spinner_start (self->spinner);
 
   context = ide_widget_get_context (GTK_WIDGET (self));
@@ -355,6 +364,186 @@ gbp_grep_panel_replace_clicked_cb (GbpGrepPanel *self,
                                         g_object_ref (self));
 }
 
+static void
+gbp_grep_panel_find_entry_text_changed_cb (GbpGrepPanel *self,
+                                           GParamSpec   *pspec,
+                                           GtkEntry     *entry)
+{
+  gboolean is_query_empty;
+
+  g_assert (GBP_IS_GREP_PANEL (self));
+  g_assert (GTK_IS_ENTRY (entry));
+
+  is_query_empty = (g_strcmp0 (gtk_entry_get_text (entry), "") == 0);
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->find_button), !is_query_empty);
+}
+
+static void
+gbp_grep_panel_close_panel_action (GSimpleAction *action,
+                                   GVariant      *variant,
+                                   gpointer       user_data)
+{
+  GbpGrepPanel *self = (GbpGrepPanel *)user_data;
+  gboolean is_project_wide;
+  GbpGrepModel *model;
+
+  g_assert (GBP_IS_GREP_PANEL (self));
+
+  model = gbp_grep_panel_get_model (self);
+  is_project_wide = (model == NULL || gbp_grep_model_get_directory (model) == NULL);
+
+  if (!is_project_wide)
+    gtk_widget_destroy (GTK_WIDGET (self));
+}
+
+static void
+gbp_grep_panel_close_clicked_cb (GbpGrepPanel *self,
+                                 GtkButton    *button)
+{
+  g_assert (GBP_IS_GREP_PANEL (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  gtk_widget_destroy (GTK_WIDGET (self));
+}
+
+static void
+gbp_grep_panel_scan_cb (GObject      *object,
+                        GAsyncResult *result,
+                        gpointer      user_data)
+{
+  GbpGrepModel *model = (GbpGrepModel *)object;
+  g_autoptr(GbpGrepPanel) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (GBP_IS_GREP_MODEL (model));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (GBP_IS_GREP_PANEL (self));
+
+  if (!gbp_grep_model_scan_finish (model, result, &error))
+    /* TODO: For now we warn in the not-very-noticeable messages panel, but when we start
+     * depending on libadwaita we'll be able to use a status page here as an error page,
+     * in the stack.
+     */
+    ide_object_warning (ide_widget_get_context (GTK_WIDGET (self)),
+                        "Failed to find files: %s", error->message);
+  else
+    gbp_grep_panel_set_model (self, model);
+
+  g_clear_object (&self->cancellable);
+
+  gtk_spinner_stop (self->spinner);
+  gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->scrolled_window));
+  gtk_widget_set_sensitive (GTK_WIDGET (self->find_button), TRUE);
+
+  /* The model defaults to selecting all items, so if the "Select all" header check box was
+   * unselected, then we'll end up in an inconsistent state where toggling the header check
+   * box will unselect the items when it should have selected all of them. To avoid this,
+   * just set back the "Select all" check box to "selected" when starting a new search.
+   */
+  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->check), TRUE);
+
+  ide_widget_reveal_and_grab (GTK_WIDGET (self->replace_entry));
+}
+
+/**
+ * gbp_grep_panel_launch_search:
+ * @self: a #GbpGrepPanel
+ *
+ * Launches the search operation with the settings coming from the model
+ * previously set with gbp_grep_panel_set_model ().
+ */
+void
+gbp_grep_panel_launch_search (GbpGrepPanel *self)
+{
+  GbpGrepModel *model;
+  g_autoptr(GFile) root_dir = NULL;
+
+  g_assert (GBP_IS_GREP_PANEL (self));
+
+  /* Nothing's really reusable between search operations (and it isn't allowed anyway by
+   * gbp_grep_model_scan_async()), so just start from a new one. The only part we keep
+   * from it is the search directory because we can't modify it in the UI and so the
+   * only place where it's actually stored is the (old) model.
+   */
+  root_dir = gbp_grep_model_get_directory (gbp_grep_panel_get_model (self));
+  if (root_dir)
+    g_object_ref (root_dir);
+  model = gbp_grep_model_new (ide_widget_get_context (GTK_WIDGET (self)));
+  gbp_grep_model_set_directory (model, root_dir);
+
+  gbp_grep_model_set_use_regex (model, gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON 
(self->regex_button)));
+  gbp_grep_model_set_at_word_boundaries (model, gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON 
(self->whole_words_button)));
+  gbp_grep_model_set_case_sensitive (model, gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON 
(self->case_button)));
+  gbp_grep_model_set_query (model, gtk_entry_get_text (self->find_entry));
+
+  gbp_grep_model_set_recursive (model, gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON 
(self->recursive_button)));
+
+  gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->spinner));
+  gtk_spinner_start (self->spinner);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->find_button), FALSE);
+
+  ide_widget_reveal_and_grab (GTK_WIDGET (self));
+
+  self->cancellable = g_cancellable_new ();
+  gbp_grep_model_scan_async (model,
+                             self->cancellable,
+                             gbp_grep_panel_scan_cb,
+                             g_object_ref (self));
+}
+
+static void
+gbp_grep_panel_find_clicked_cb (GbpGrepPanel *self,
+                                GtkButton    *button)
+{
+  g_assert (GBP_IS_GREP_PANEL (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  gbp_grep_panel_launch_search (self);
+}
+
+/* We can't really use the receives-default/grab_default() stuff as that only really
+ * works when there's only one entry+button in a popover. So here just chain up the
+ * Enter key in the entry to activate the button.
+ */
+static void
+on_entry_activate_toggle_action_button_cb (GtkEntry *entry,
+                                           gpointer  user_data)
+{
+  GtkButton *button = (GtkButton *)user_data;
+
+  g_assert (GTK_IS_ENTRY (entry));
+  g_assert (GTK_IS_BUTTON (button));
+
+  g_signal_emit_by_name (button, "activate", NULL);
+}
+
+static void
+gbp_grep_panel_grab_focus (GtkWidget *widget)
+{
+  GbpGrepPanel *self = (GbpGrepPanel *)widget;
+
+  g_assert (GBP_IS_GREP_PANEL (self));
+
+  gtk_widget_grab_focus (GTK_WIDGET (self->find_entry));
+}
+
+static void
+gbp_grep_panel_finalize (GObject *object)
+{
+  GbpGrepPanel *self = (GbpGrepPanel *)object;
+
+  g_assert (GBP_IS_GREP_PANEL (self));
+
+  if (self->cancellable)
+    {
+      g_cancellable_cancel (self->cancellable);
+      g_clear_object (&self->cancellable);
+    }
+
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "grep", NULL);
+}
+
 static void
 gbp_grep_panel_get_property (GObject    *object,
                              guint       prop_id,
@@ -401,6 +590,9 @@ gbp_grep_panel_class_init (GbpGrepPanelClass *klass)
 
   object_class->get_property = gbp_grep_panel_get_property;
   object_class->set_property = gbp_grep_panel_set_property;
+  object_class->finalize = gbp_grep_panel_finalize;
+
+  widget_class->grab_focus = gbp_grep_panel_grab_focus;
 
   properties [PROP_MODEL] =
     g_param_spec_object ("model", NULL, NULL,
@@ -411,30 +603,83 @@ gbp_grep_panel_class_init (GbpGrepPanelClass *klass)
 
   gtk_widget_class_set_css_name (widget_class, "gbpgreppanel");
   gtk_widget_class_set_template_from_resource (widget_class, "/plugins/grep/gbp-grep-panel.ui");
-  gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, close_button);
+
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, stack);
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, scrolled_window);
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, tree_view);
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, spinner);
+
   gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, replace_button);
   gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, replace_entry);
-  gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, spinner);
-  gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, tree_view);
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, find_button);
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, find_entry);
+
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, regex_button);
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, whole_words_button);
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, case_button);
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, recursive_button);
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, close_button);
 }
 
+static const GActionEntry actions[] = {
+  { "close-panel", gbp_grep_panel_close_panel_action },
+};
+
 static void
 gbp_grep_panel_init (GbpGrepPanel *self)
 {
+  g_autoptr(GSimpleActionGroup) group = NULL;
+  DzlShortcutController *controller;
+
   GtkTreeViewColumn *column;
   GtkCellRenderer *cell;
 
   gtk_widget_init_template (GTK_WIDGET (self));
 
-  g_signal_connect_object (self->close_button,
+  group = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (group),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "grep", G_ACTION_GROUP (group));
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (self));
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.grep"),
+                                              I_("Escape"),
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("grep.close-panel"));
+
+  g_signal_connect (self->find_entry,
+                    "activate",
+                    G_CALLBACK (on_entry_activate_toggle_action_button_cb),
+                    self->find_button);
+  g_signal_connect (self->replace_entry,
+                    "activate",
+                    G_CALLBACK (on_entry_activate_toggle_action_button_cb),
+                    self->replace_button);
+
+  g_signal_connect_object (self->replace_button,
                            "clicked",
-                           G_CALLBACK (gtk_widget_destroy),
+                           G_CALLBACK (gbp_grep_panel_replace_clicked_cb),
                            self,
                            G_CONNECT_SWAPPED);
 
-  g_signal_connect_object (self->replace_button,
+  g_signal_connect_object (self->find_button,
                            "clicked",
-                           G_CALLBACK (gbp_grep_panel_replace_clicked_cb),
+                           G_CALLBACK (gbp_grep_panel_find_clicked_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->find_entry,
+                           "notify::text",
+                           G_CALLBACK (gbp_grep_panel_find_entry_text_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->close_button,
+                           "clicked",
+                           G_CALLBACK (gbp_grep_panel_close_clicked_cb),
                            self,
                            G_CONNECT_SWAPPED);
 
@@ -509,6 +754,29 @@ gbp_grep_panel_init (GbpGrepPanel *self)
   gtk_tree_view_append_column (self->tree_view, column);
 }
 
+static char *
+sanitize_workdir (GFile *workdir,
+                  GFile *search_directory)
+{
+  g_autofree char *relative_dir = g_file_get_relative_path (workdir, search_directory);
+  /* To make it clear that it's just the directory inserted in the "Find in %s" string, ensure
+   * the path ends with a directory separator, as g_file_get_relative_path() doesn't do it.
+   * That way we won't end up with "Find in data" but instead "Find in data/".
+   */
+  gboolean is_dir = (g_file_query_file_type (search_directory, G_FILE_QUERY_INFO_NONE, NULL) == 
G_FILE_TYPE_DIRECTORY);
+  g_autofree char *dir_sep_terminated_path = is_dir ?
+    g_strconcat (relative_dir, G_DIR_SEPARATOR_S, NULL) : g_strdup (relative_dir);
+  /* We want a mnemonic on the buttons but paths can contain underscores, and they would
+   * be taken as if they are mnemonics underlines which is not what we want. Instead,
+   * escape the underscore by doubling it so that GTK only renders one and doesn't use
+   * it as mnemonic.
+   */
+  g_autoptr(GString) underscore_escaper = g_string_new (dir_sep_terminated_path);
+  g_string_replace (underscore_escaper, "_", "__", 0);
+
+  return g_strdup (underscore_escaper->str);
+}
+
 void
 gbp_grep_panel_set_model (GbpGrepPanel *self,
                           GbpGrepModel *model)
@@ -518,13 +786,49 @@ gbp_grep_panel_set_model (GbpGrepPanel *self,
 
   if (model != NULL)
     {
-      /* Disable replace button if we have nothing to replace. We only
-       * support setting the model after it has scanned, so this is fine.
+      GFile *search_directory = gbp_grep_model_get_directory (model);
+      IdeContext *context = ide_widget_get_context (GTK_WIDGET (self));
+      g_autoptr(GFile) workdir = ide_context_ref_workdir (context);
+      /* The project-wide default panel (done in the editor addin) uses NULL to indicate project-wide,
+       * but when searching from the top-level "Files" project tree row we'll have the full path
+       * even if it's effectively project-wide, not NULL. It's nice to keep the
+       * "Find in Project" label while allowing to close the panel, so differentiate both cases.
        */
-      if (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (model), NULL) == 0)
-        gtk_widget_set_sensitive (GTK_WIDGET (self->replace_button), FALSE);
-      else
-        gtk_widget_set_sensitive (GTK_WIDGET (self->replace_button), TRUE);
+      gboolean is_initial_panel = (search_directory == NULL);
+      gboolean is_project_wide = (is_initial_panel || g_file_equal (workdir, search_directory));
+
+      gboolean has_item = (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (model), NULL) != 0);
+
+      gtk_widget_set_sensitive (GTK_WIDGET (self->replace_button), has_item);
+      gtk_widget_set_sensitive (GTK_WIDGET (self->replace_entry), has_item);
+
+      gtk_entry_set_text (self->find_entry, gbp_grep_model_get_query (model));
+
+      gtk_widget_set_visible (GTK_WIDGET (self->close_button), !is_initial_panel);
+
+      /* Project wide is done in the UI file directly. */
+      if (!is_project_wide)
+        {
+          g_autofree char *mnemonic_safe_directory = sanitize_workdir (workdir, search_directory);
+          /* TRANSLATORS: %s is the directory or file from where the search was started from the project 
tree. */
+          g_autofree char *find_label = g_strdup_printf (_("_Find in %s"), mnemonic_safe_directory);
+          /* TRANSLATORS: %s is the directory or file from where the search was started from the project 
tree. */
+          g_autofree char *replace_label = g_strdup_printf (_("_Replace in %s"), mnemonic_safe_directory);
+          gboolean is_dir = (g_file_query_file_type (search_directory, G_FILE_QUERY_INFO_NONE, NULL) == 
G_FILE_TYPE_DIRECTORY);
+
+          gtk_button_set_label (self->find_button, find_label);
+          gtk_button_set_label (self->replace_button, replace_label);
+
+          gtk_widget_set_visible (GTK_WIDGET (self->recursive_button), is_dir);
+        }
+      gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->regex_button),
+                                    gbp_grep_model_get_use_regex (model));
+      gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->whole_words_button),
+                                    gbp_grep_model_get_at_word_boundaries (model));
+      gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->case_button),
+                                    gbp_grep_model_get_case_sensitive (model));
+      gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->recursive_button),
+                                    gbp_grep_model_get_recursive (model));
     }
 
   gtk_tree_view_set_model (self->tree_view, GTK_TREE_MODEL (model));
diff --git a/src/plugins/grep/gbp-grep-panel.h b/src/plugins/grep/gbp-grep-panel.h
index 8a8cd3521..4b916b6f3 100644
--- a/src/plugins/grep/gbp-grep-panel.h
+++ b/src/plugins/grep/gbp-grep-panel.h
@@ -30,9 +30,10 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (GbpGrepPanel, gbp_grep_panel, GBP, GREP_PANEL, DzlDockWidget)
 
-GtkWidget    *gbp_grep_panel_new       (void);
-GbpGrepModel *gbp_grep_panel_get_model (GbpGrepPanel *self);
-void          gbp_grep_panel_set_model (GbpGrepPanel *self,
-                                        GbpGrepModel *model);
+GtkWidget    *gbp_grep_panel_new           (void);
+GbpGrepModel *gbp_grep_panel_get_model     (GbpGrepPanel *self);
+void          gbp_grep_panel_set_model     (GbpGrepPanel *self,
+                                            GbpGrepModel *model);
+void          gbp_grep_panel_launch_search (GbpGrepPanel *self);
 
 G_END_DECLS
diff --git a/src/plugins/grep/gbp-grep-panel.ui b/src/plugins/grep/gbp-grep-panel.ui
index 075256925..90e1a29c7 100644
--- a/src/plugins/grep/gbp-grep-panel.ui
+++ b/src/plugins/grep/gbp-grep-panel.ui
@@ -9,14 +9,24 @@
         <property name="vexpand">true</property>
         <property name="visible">true</property>
         <child>
-          <object class="GtkScrolledWindow">
-            <property name="vexpand">true</property>
+          <object class="GtkStack" id="stack">
             <property name="visible">true</property>
             <child>
-              <object class="GtkTreeView" id="tree_view">
-                <property name="activate-on-single-click">true</property>
-                <property name="headers-visible">true</property>
+              <object class="GtkScrolledWindow" id="scrolled_window">
+                <property name="vexpand">true</property>
                 <property name="visible">true</property>
+                <child>
+                  <object class="GtkTreeView" id="tree_view">
+                    <property name="activate-on-single-click">true</property>
+                    <property name="headers-visible">true</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkSpinner" id="spinner">
+                <property name="visible">True</property>
               </object>
             </child>
           </object>
@@ -27,63 +37,141 @@
             <property name="margin-start">6</property>
             <property name="margin-end">6</property>
             <property name="margin-bottom">6</property>
-            <property name="orientation">horizontal</property>
+            <property name="spacing">6</property>
+            <property name="orientation">vertical</property>
             <property name="visible">true</property>
             <child>
-              <object class="GtkSpinner" id="spinner">
-                <property name="halign">end</property>
-              </object>
-              <packing>
-                <property name="expand">true</property>
-                <property name="fill">true</property>
-                <property name="pack-type">start</property>
-                <property name="padding">12</property>
-              </packing>
-            </child>
-            <child type="center">
               <object class="GtkBox">
-                <property name="spacing">12</property>
-                <property name="hexpand">false</property>
+                <property name="visible">True</property>
+                <property name="spacing">6</property>
                 <property name="orientation">horizontal</property>
-                <property name="visible">true</property>
                 <child>
                   <object class="GtkLabel">
-                    <property name="label" translatable="yes">Replace With</property>
-                    <property name="xalign">1.0</property>
+                    <property name="label" translatable="yes">Find and Replace</property>
+                    <property name="halign">start</property>
                     <property name="visible">true</property>
                     <style>
                       <class name="dim-label"/>
                     </style>
+                    <attributes>
+                      <attribute name="weight" value="bold"/>
+                      <attribute name="scale" value="0.8333"/>
+                    </attributes>
                   </object>
                 </child>
-                <child type="center">
-                  <object class="GtkEntry" id="replace_entry">
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="halign">end</property>
+                    <property name="hexpand">True</property>
+                    <property name="spacing">6</property>
+                    <child>
+                      <object class="GtkCheckButton" id="recursive_button">
+                        <property name="visible">true</property>
+                        <property name="active">true</property>
+                        <property name="label" translatable="yes">R_ecursive</property>
+                        <property name="use-underline">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkCheckButton" id="case_button">
+                        <property name="visible">true</property>
+                        <property name="active">true</property>
+                        <property name="label" translatable="yes">Match _Case</property>
+                        <property name="use-underline">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkCheckButton" id="whole_words_button">
+                        <property name="visible">true</property>
+                        <property name="label" translatable="yes">Match _Words</property>
+                        <property name="use-underline">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkCheckButton" id="regex_button">
+                        <property name="visible">true</property>
+                        <property name="label" translatable="yes">Regular E_xpressions</property>
+                        <property name="use-underline">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="close_button">
+                        <property name="visible">False</property>
+                        <!-- Separates it a bit more from the group of check boxes -->
+                        <property name="margin-start">6</property>
+                        <property name="tooltip-text" translatable="yes">Close the panel (Escape 
key)</property>
+                        <child>
+                          <object class="GtkImage">
+                            <property name="visible">True</property>
+                            <property name="icon-name">window-close-symbolic</property>
+                          </object>
+                        </child>
+                        <style>
+                          <class name="circular" />
+                        </style>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkGrid">
+                <property name="visible">True</property>
+                <property name="column-spacing">6</property>
+                <property name="row-spacing">6</property>
+                <child>
+                  <object class="GtkEntry" id="find_entry">
                     <property name="visible">true</property>
-                    <property name="width-chars">30</property>
+                    <property name="hexpand">true</property>
+                    <accessibility>
+                      <relation target="find_button" type="labelled-by"/>
+                    </accessibility>
                   </object>
+                  <packing>
+                    <property name="left-attach">0</property>
+                    <property name="top-attach">0</property>
+                  </packing>
                 </child>
                 <child>
-                  <object class="GtkButton" id="replace_button">
-                    <property name="label" translatable="yes">Replace</property>
+                  <object class="GtkButton" id="find_button">
+                    <property name="visible">true</property>
                     <property name="sensitive">false</property>
+                    <property name="label" translatable="yes">_Find in Project</property>
+                    <property name="use-underline">True</property>
+                  </object>
+                  <packing>
+                    <property name="left-attach">1</property>
+                    <property name="top-attach">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkEntry" id="replace_entry">
                     <property name="visible">true</property>
-                    <style>
-                      <class name="destructive-action"/>
-                    </style>
+                    <property name="hexpand">true</property>
+                    <accessibility>
+                      <relation target="replace_button" type="labelled-by"/>
+                    </accessibility>
                   </object>
                   <packing>
-                    <property name="position">1</property>
-                    <property name="pack-type">end</property>
+                    <property name="left-attach">0</property>
+                    <property name="top-attach">1</property>
                   </packing>
                 </child>
                 <child>
-                  <object class="GtkButton" id="close_button">
-                    <property name="label" translatable="yes">Close</property>
+                  <object class="GtkButton" id="replace_button">
                     <property name="visible">true</property>
+                    <property name="sensitive">false</property>
+                    <property name="label" translatable="yes">_Replace in Project</property>
+                    <property name="use-underline">True</property>
+                    <style>
+                      <class name="destructive-action"/>
+                    </style>
                   </object>
                   <packing>
-                    <property name="position">0</property>
-                    <property name="pack-type">end</property>
+                    <property name="left-attach">1</property>
+                    <property name="top-attach">1</property>
                   </packing>
                 </child>
               </object>
@@ -93,11 +181,5 @@
       </object>
     </child>
   </template>
-  <object class="GtkSizeGroup">
-    <property name="mode">horizontal</property>
-    <widgets>
-      <widget name="close_button"/>
-      <widget name="replace_button"/>
-    </widgets>
-  </object>
 </interface>
+
diff --git a/src/plugins/grep/gbp-grep-popover.c b/src/plugins/grep/gbp-grep-popover.c
index af2df8e2a..216ac1b4d 100644
--- a/src/plugins/grep/gbp-grep-popover.c
+++ b/src/plugins/grep/gbp-grep-popover.c
@@ -55,27 +55,6 @@ G_DEFINE_TYPE (GbpGrepPopover, gbp_grep_popover, GTK_TYPE_POPOVER)
 
 static GParamSpec *properties [N_PROPS];
 
-static void
-gbp_grep_popover_scan_cb (GObject      *object,
-                          GAsyncResult *result,
-                          gpointer      user_data)
-{
-  GbpGrepModel *model = (GbpGrepModel *)object;
-  g_autoptr(GbpGrepPanel) panel = user_data;
-  g_autoptr(GError) error = NULL;
-
-  g_assert (GBP_IS_GREP_MODEL (model));
-  g_assert (G_IS_ASYNC_RESULT (result));
-  g_assert (GBP_IS_GREP_PANEL (panel));
-
-  if (!gbp_grep_model_scan_finish (model, result, &error))
-    g_warning ("Failed to find files: %s", error->message);
-  else
-    gbp_grep_panel_set_model (panel, model);
-
-  gtk_widget_grab_focus (GTK_WIDGET (panel));
-}
-
 static void
 gbp_grep_popover_button_clicked_cb (GbpGrepPopover *self,
                                     GtkButton      *button)
@@ -118,14 +97,12 @@ gbp_grep_popover_button_clicked_cb (GbpGrepPopover *self,
 
   panel = gbp_grep_panel_new ();
   gtk_container_add (GTK_CONTAINER (utils), panel);
+  gbp_grep_panel_set_model (GBP_GREP_PANEL (panel), model);
   gtk_widget_show (panel);
 
-  gbp_grep_model_scan_async (model,
-                             NULL,
-                             gbp_grep_popover_scan_cb,
-                             g_object_ref (panel));
-
-  dzl_dock_item_present (DZL_DOCK_ITEM (panel));
+  /* gtk_popover_popdown (GTK_POPOVER (self)); */
+  gtk_widget_destroy (GTK_WIDGET (self));
+  gbp_grep_panel_launch_search (GBP_GREP_PANEL (panel));
 }
 
 static void
diff --git a/src/plugins/grep/gbp-grep-tree-addin.c b/src/plugins/grep/gbp-grep-tree-addin.c
index 45038c52c..1a36b0d5a 100644
--- a/src/plugins/grep/gbp-grep-tree-addin.c
+++ b/src/plugins/grep/gbp-grep-tree-addin.c
@@ -35,25 +35,6 @@ struct _GbpGrepTreeAddin
   IdeTree *tree;
 };
 
-static void
-popover_closed_cb (GtkPopover *popover)
-{
-  GtkWidget *toplevel;
-
-  g_assert (IDE_IS_MAIN_THREAD ());
-  g_assert (GTK_IS_POPOVER (popover));
-
-  /*
-   * Clear focus before destroying popover, or we risk some
-   * re-entrancy issues in libdazzle. Needs safer tracking of
-   * focus widgets as gtk is not clearing pointers in destroy.
-   */
-
-  toplevel = gtk_widget_get_toplevel (GTK_WIDGET (popover));
-  gtk_window_set_focus (GTK_WINDOW (toplevel), NULL);
-  gtk_widget_destroy (GTK_WIDGET (popover));
-}
-
 static void
 find_in_files_action (GSimpleAction *action,
                       GVariant      *param,
@@ -82,10 +63,6 @@ find_in_files_action (GSimpleAction *action,
                               "is-directory", is_dir,
                               "position", GTK_POS_RIGHT,
                               NULL);
-      g_signal_connect_after (popover,
-                              "closed",
-                              G_CALLBACK (popover_closed_cb),
-                              NULL);
       ide_tree_show_popover_at_node (self->tree, node, popover);
     }
 }
diff --git a/src/plugins/grep/grep-plugin.c b/src/plugins/grep/grep-plugin.c
index 561dc0b1f..71dfcb95f 100644
--- a/src/plugins/grep/grep-plugin.c
+++ b/src/plugins/grep/grep-plugin.c
@@ -21,9 +21,11 @@
 #include "config.h"
 
 #include <libide-tree.h>
+#include <libide-editor.h>
 #include <libpeas/peas.h>
 
 #include "gbp-grep-tree-addin.h"
+#include "gbp-grep-editor-addin.h"
 
 _IDE_EXTERN void
 _gbp_grep_register_types (PeasObjectModule *module)
@@ -31,4 +33,7 @@ _gbp_grep_register_types (PeasObjectModule *module)
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_TREE_ADDIN,
                                               GBP_TYPE_GREP_TREE_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_EDITOR_ADDIN,
+                                              GBP_TYPE_GREP_EDITOR_ADDIN);
 }
diff --git a/src/plugins/grep/meson.build b/src/plugins/grep/meson.build
index 0803c6e91..54a1e1f9b 100644
--- a/src/plugins/grep/meson.build
+++ b/src/plugins/grep/meson.build
@@ -5,6 +5,7 @@ plugins_sources += files([
   'gbp-grep-panel.c',
   'gbp-grep-popover.c',
   'gbp-grep-tree-addin.c',
+  'gbp-grep-editor-addin.c',
   'grep-plugin.c',
 ])
 


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