[gnome-builder/wip/chergert/grep] grep: start on grep search model for find in project



commit 56e23f12b7f09f797ea1aa76cc7b2118c3cfe96b
Author: Christian Hergert <chergert redhat com>
Date:   Wed Oct 24 15:08:32 2018 -0700

    grep: start on grep search model for find in project
    
    This knows how to use git-grep vs grep like our todo plugin. It still
    needs a number of UI pieces and finishing of the treemodel to handle the
    low-cardinality storage of checkmark fields. We also need to do the replace
    UI to reuse ProjectEdit workflow.

 meson_options.txt                          |   1 +
 src/plugins/grep/gbp-grep-dialog.c         |  48 ++
 src/plugins/grep/gbp-grep-dialog.h         |  31 ++
 src/plugins/grep/gbp-grep-dialog.ui        | 285 ++++++++++
 src/plugins/grep/gbp-grep-editor-addin.c   | 145 +++++
 src/plugins/grep/gbp-grep-editor-addin.h   |  31 ++
 src/plugins/grep/gbp-grep-model.c          | 829 +++++++++++++++++++++++++++++
 src/plugins/grep/gbp-grep-model.h          |  58 ++
 src/plugins/grep/gbp-grep-panel.c          | 302 +++++++++++
 src/plugins/grep/gbp-grep-panel.h          |  38 ++
 src/plugins/grep/gbp-grep-panel.ui         |  91 ++++
 src/plugins/grep/gbp-grep-plugin.c         |  32 ++
 src/plugins/grep/grep.gresource.xml        |  13 +
 src/plugins/grep/grep.plugin               |   9 +
 src/plugins/grep/meson.build               |  20 +
 src/plugins/grep/themes/Adwaita-dark.css   |   2 +
 src/plugins/grep/themes/Adwaita-shared.css |   9 +
 src/plugins/grep/themes/Adwaita.css        |   2 +
 src/plugins/meson.build                    |   2 +
 19 files changed, 1948 insertions(+)
---
diff --git a/meson_options.txt b/meson_options.txt
index 24cc3dd8a..ae5c8d73a 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -52,6 +52,7 @@ option('with_gjs_symbols', type: 'boolean')
 option('with_glade', type: 'boolean')
 option('with_gnome_code_assistance', type: 'boolean')
 option('with_go_langserv', type: 'boolean')
+option('with_grep', type: 'boolean')
 option('with_history', type: 'boolean')
 option('with_html_completion', type: 'boolean')
 option('with_html_preview', type: 'boolean')
diff --git a/src/plugins/grep/gbp-grep-dialog.c b/src/plugins/grep/gbp-grep-dialog.c
new file mode 100644
index 000000000..387b44a9f
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-dialog.c
@@ -0,0 +1,48 @@
+/* gbp-grep-dialog.c
+ *
+ * Copyright © 2018 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
+ */
+
+#include "config.h"
+
+#define G_LOG_DOMAIN "gbp-grep-dialog"
+
+#include <ide.h>
+
+#include "gbp-grep-dialog.h"
+
+struct _GbpGrepDialog
+{
+  GtkDialog parent_instance;
+};
+
+G_DEFINE_TYPE (GbpGrepDialog, gbp_grep_dialog, GTK_TYPE_DIALOG)
+
+static void
+gbp_grep_dialog_class_init (GbpGrepDialogClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/plugins/grep/gbp-grep-dialog.ui");
+}
+
+static void
+gbp_grep_dialog_init (GbpGrepDialog *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
diff --git a/src/plugins/grep/gbp-grep-dialog.h b/src/plugins/grep/gbp-grep-dialog.h
new file mode 100644
index 000000000..2ce9dfd40
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-dialog.h
@@ -0,0 +1,31 @@
+/* gbp-grep-dialog.h
+ *
+ * Copyright © 2018 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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GREP_DIALOG (gbp_grep_dialog_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGrepDialog, gbp_grep_dialog, GBP, GREP_DIALOG, GtkDialog)
+
+G_END_DECLS
diff --git a/src/plugins/grep/gbp-grep-dialog.ui b/src/plugins/grep/gbp-grep-dialog.ui
new file mode 100644
index 000000000..d46a0e574
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-dialog.ui
@@ -0,0 +1,285 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.0 -->
+<interface>
+  <requires lib="gtk+" version="3.24"/>
+  <template class="GbpGrepDialog" parent="GtkDialog">
+    <property name="can_focus">False</property>
+    <property name="title" translatable="yes">Find &amp; Replace</property>
+    <property name="type_hint">dialog</property>
+    <child internal-child="vbox">
+      <object class="GtkBox">
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">2</property>
+        <child internal-child="action_area">
+          <object class="GtkButtonBox">
+            <property name="can_focus">False</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="vexpand">True</property>
+            <property name="border_width">12</property>
+            <property name="orientation">vertical</property>
+            <property name="spacing">16</property>
+            <child>
+              <object class="GtkGrid">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="row_spacing">12</property>
+                <property name="column_spacing">12</property>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label" translatable="yes">_Search</property>
+                    <property name="use_underline">True</property>
+                    <property name="mnemonic_widget">search_entry</property>
+                    <property name="xalign">1</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label" translatable="yes">_Replace With</property>
+                    <property name="use_underline">True</property>
+                    <property name="mnemonic_widget">replace_entry</property>
+                    <property name="xalign">1</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkSearchEntry" id="search_entry">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="hexpand">True</property>
+                    <property name="primary_icon_name">edit-find-symbolic</property>
+                    <property name="primary_icon_activatable">False</property>
+                    <property name="primary_icon_sensitive">False</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>
+                    <property name="can_focus">True</property>
+                    <property name="hexpand">True</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="top_attach">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">6</property>
+                    <child>
+                      <object class="GtkCheckButton">
+                        <property name="label" translatable="yes">Regular expressions</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">False</property>
+                        <property name="xalign">0</property>
+                        <property name="draw_indicator">True</property>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkCheckButton">
+                        <property name="label" translatable="yes">Case sensitive</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">False</property>
+                        <property name="xalign">0</property>
+                        <property name="draw_indicator">True</property>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkCheckButton">
+                        <property name="label" translatable="yes">Match whole word only</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">False</property>
+                        <property name="xalign">0</property>
+                        <property name="draw_indicator">True</property>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">2</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="left_attach">2</property>
+                    <property name="top_attach">0</property>
+                    <property name="height">2</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="vexpand">True</property>
+                <property name="shadow_type">in</property>
+                <child>
+                  <object class="GtkTreeView">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <child internal-child="selection">
+                      <object class="GtkTreeSelection"/>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="spacing">12</property>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label" translatable="yes">Select</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkButton" id="button1">
+                    <property name="label" translatable="yes">All</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkButton" id="button2">
+                    <property name="label" translatable="yes">None</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <child type="titlebar">
+      <object class="GtkHeaderBar" id="header_bar">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="title" translatable="yes">Find &amp; Replace</property>
+        <child>
+          <object class="GtkButton" id="button_cancel">
+            <property name="label" translatable="yes">_Cancel</property>
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="receives_default">False</property>
+            <property name="use_underline">True</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkButton" id="button_ok">
+            <property name="label" translatable="yes">Re_place</property>
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="receives_default">False</property>
+            <property name="use_underline">True</property>
+            <style>
+              <class name="suggested-action"/>
+            </style>
+          </object>
+          <packing>
+            <property name="pack_type">end</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkSizeGroup">
+    <widgets>
+      <widget name="button1"/>
+      <widget name="button2"/>
+    </widgets>
+  </object>
+</interface>
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..53fc9a3cc
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-editor-addin.c
@@ -0,0 +1,145 @@
+/* gbp-grep-editor-addin.c
+ *
+ * Copyright © 2018 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
+ */
+
+#include "config.h"
+
+#define G_LOG_DOMAIN "gbp-grep-editor-addin"
+
+#include "gbp-grep-dialog.h"
+#include "gbp-grep-editor-addin.h"
+#include "gbp-grep-model.h"
+#include "gbp-grep-panel.h"
+
+struct _GbpGrepEditorAddin
+{
+  GObject               parent_instance;
+  IdeEditorPerspective *editor;
+};
+
+static void
+model_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))
+    return;
+
+  gbp_grep_panel_set_model (panel, model);
+}
+
+static void
+gbp_grep_editor_addin_find_in_files (GSimpleAction *action,
+                                     GVariant      *param,
+                                     gpointer       user_data)
+{
+  GbpGrepEditorAddin *self = user_data;
+  g_autoptr(GbpGrepModel) model = NULL;
+  g_autoptr(GCancellable) cancellable = NULL;
+  IdeWorkbench *workbench;
+  IdeContext *context;
+  GtkWidget *panel;
+  GtkWidget *utils;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_GREP_EDITOR_ADDIN (self));
+
+  workbench = ide_widget_get_workbench (GTK_WIDGET (self->editor));
+  context = ide_workbench_get_context (workbench);
+  model = gbp_grep_model_new (context);
+  cancellable = g_cancellable_new ();
+
+  /* TODO: Show a popover, etc ... */
+  gbp_grep_model_set_recursive (model, TRUE);
+  gbp_grep_model_set_query (model, "tunes");
+
+  panel = gbp_grep_panel_new ();
+  utils = ide_editor_perspective_get_utilities (self->editor);
+  gtk_container_add (GTK_CONTAINER (utils), panel);
+  gtk_widget_show (panel);
+
+  ide_workbench_focus (workbench, panel);
+
+  gbp_grep_model_scan_async (model,
+                             cancellable,
+                             model_scan_cb,
+                             g_object_ref (panel));
+}
+
+static void
+gbp_grep_editor_addin_load (IdeEditorAddin       *addin,
+                            IdeEditorPerspective *editor)
+{
+  GbpGrepEditorAddin *self = (GbpGrepEditorAddin *)addin;
+  g_autoptr(GSimpleActionGroup) group = NULL;
+  static const GActionEntry actions[] = {
+    { "find-in-files", gbp_grep_editor_addin_find_in_files },
+  };
+
+  g_assert (GBP_IS_GREP_EDITOR_ADDIN (self));
+  g_assert (IDE_IS_EDITOR_PERSPECTIVE (editor));
+
+  self->editor = editor;
+
+  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), "grep", G_ACTION_GROUP (group));
+}
+
+static void
+gbp_grep_editor_addin_unload (IdeEditorAddin       *addin,
+                              IdeEditorPerspective *editor)
+{
+  GbpGrepEditorAddin *self = (GbpGrepEditorAddin *)addin;
+
+  g_assert (GBP_IS_GREP_EDITOR_ADDIN (self));
+  g_assert (IDE_IS_EDITOR_PERSPECTIVE (editor));
+
+  gtk_widget_insert_action_group (GTK_WIDGET (editor), "grep", NULL);
+
+  self->editor = NULL;
+}
+
+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..b410e4f14
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-editor-addin.h
@@ -0,0 +1,31 @@
+/* gbp-grep-editor-addin.h
+ *
+ * Copyright © 2018 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 <ide.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
new file mode 100644
index 000000000..728cc6853
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-model.c
@@ -0,0 +1,829 @@
+/* gbp-grep-model.c
+ *
+ * Copyright 2018 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
+ */
+
+#include "config.h"
+
+#define G_LOG_DOMAIN "gbp-grep-model"
+
+#include "gbp-grep-model.h"
+
+typedef struct
+{
+  GBytes    *bytes;
+  GPtrArray *rows;
+} Index;
+
+struct _GbpGrepModel
+{
+  IdeObject parent_instance;
+
+  GFile *directory;
+  gchar *query;
+  Index *index;
+
+  guint has_scanned : 1;
+  guint use_regex : 1;
+  guint recursive : 1;
+  guint case_sensitive : 1;
+  guint at_word_boundaries : 1;
+};
+
+static void tree_model_iface_init (GtkTreeModelIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GbpGrepModel, gbp_grep_model, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_MODEL, tree_model_iface_init))
+
+enum {
+  PROP_0,
+  PROP_AT_WORD_BOUNDARIES,
+  PROP_CASE_SENSITIVE,
+  PROP_DIRECTORY,
+  PROP_RECURSIVE,
+  PROP_USE_REGEX,
+  PROP_QUERY,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+index_free (gpointer data)
+{
+  Index *idx = data;
+
+  g_clear_pointer (&idx->rows, g_ptr_array_unref);
+  g_clear_pointer (&idx->bytes, g_bytes_unref);
+  g_slice_free (Index, idx);
+}
+
+GbpGrepModel *
+gbp_grep_model_new (IdeContext *context)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  return g_object_new (GBP_TYPE_GREP_MODEL,
+                       "context", context,
+                       NULL);
+}
+
+static void
+gbp_grep_model_finalize (GObject *object)
+{
+  GbpGrepModel *self = (GbpGrepModel *)object;
+
+  g_clear_object (&self->directory);
+  g_clear_pointer (&self->index, index_free);
+  g_clear_pointer (&self->query, g_free);
+
+  G_OBJECT_CLASS (gbp_grep_model_parent_class)->finalize (object);
+}
+
+static void
+gbp_grep_model_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  GbpGrepModel *self = GBP_GREP_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_DIRECTORY:
+      g_value_set_object (value, gbp_grep_model_get_directory (self));
+      break;
+
+    case PROP_USE_REGEX:
+      g_value_set_boolean (value, gbp_grep_model_get_use_regex (self));
+      break;
+
+    case PROP_RECURSIVE:
+      g_value_set_boolean (value, gbp_grep_model_get_recursive (self));
+      break;
+
+    case PROP_CASE_SENSITIVE:
+      g_value_set_boolean (value, gbp_grep_model_get_case_sensitive (self));
+      break;
+
+    case PROP_AT_WORD_BOUNDARIES:
+      g_value_set_boolean (value, gbp_grep_model_get_at_word_boundaries (self));
+      break;
+
+    case PROP_QUERY:
+      g_value_set_string (value, gbp_grep_model_get_query (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_grep_model_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  GbpGrepModel *self = GBP_GREP_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_DIRECTORY:
+      gbp_grep_model_set_directory (self, g_value_get_object (value));
+      break;
+
+    case PROP_USE_REGEX:
+      gbp_grep_model_set_use_regex (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_RECURSIVE:
+      gbp_grep_model_set_recursive (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_CASE_SENSITIVE:
+      gbp_grep_model_set_case_sensitive (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_AT_WORD_BOUNDARIES:
+      gbp_grep_model_set_at_word_boundaries (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_QUERY:
+      gbp_grep_model_set_query (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_grep_model_class_init (GbpGrepModelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gbp_grep_model_finalize;
+  object_class->get_property = gbp_grep_model_get_property;
+  object_class->set_property = gbp_grep_model_set_property;
+
+  properties [PROP_DIRECTORY] =
+    g_param_spec_object ("directory", NULL, NULL,
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_USE_REGEX] =
+    g_param_spec_boolean ("use-regex", NULL, NULL,
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_RECURSIVE] =
+    g_param_spec_boolean ("recursive", NULL, NULL,
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CASE_SENSITIVE] =
+    g_param_spec_boolean ("case-sensitive", NULL, NULL,
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_AT_WORD_BOUNDARIES] =
+    g_param_spec_boolean ("at-word-boundaries", NULL, NULL,
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_QUERY] =
+    g_param_spec_string ("query", NULL, NULL, NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gbp_grep_model_init (GbpGrepModel *self)
+{
+}
+
+const gchar *
+gbp_grep_model_get_query (GbpGrepModel *self)
+{
+  g_return_val_if_fail (GBP_IS_GREP_MODEL (self), NULL);
+
+  return self->query;
+}
+
+void
+gbp_grep_model_set_query (GbpGrepModel *self,
+                          const gchar  *query)
+{
+  g_return_if_fail (GBP_IS_GREP_MODEL (self));
+
+  if (g_strcmp0 (query, self->query) != 0)
+    {
+      g_free (self->query);
+      self->query = g_strdup (query);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_QUERY]);
+    }
+}
+
+/**
+ * gbp_grep_model_get_directory:
+ * @self: a #GbpGrepModel
+ *
+ * Returns: (transfer none) (nullable): A #GFile or %NULL
+ */
+GFile *
+gbp_grep_model_get_directory (GbpGrepModel *self)
+{
+  g_return_val_if_fail (GBP_IS_GREP_MODEL (self), NULL);
+
+  return self->directory;
+}
+
+void
+gbp_grep_model_set_directory (GbpGrepModel *self,
+                              GFile        *directory)
+{
+  g_return_if_fail (GBP_IS_GREP_MODEL (self));
+  g_return_if_fail (!directory || G_IS_FILE (directory));
+  g_return_if_fail (self->has_scanned == FALSE);
+
+  if (g_set_object (&self->directory, directory))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DIRECTORY]);
+}
+
+gboolean
+gbp_grep_model_get_use_regex (GbpGrepModel *self)
+{
+  g_return_val_if_fail (GBP_IS_GREP_MODEL (self), FALSE);
+
+  return self->use_regex;
+}
+
+void
+gbp_grep_model_set_use_regex (GbpGrepModel *self,
+                              gboolean      use_regex)
+{
+  g_return_if_fail (GBP_IS_GREP_MODEL (self));
+  g_return_if_fail (self->has_scanned == FALSE);
+
+  use_regex = !!use_regex;
+
+  if (use_regex != self->use_regex)
+    {
+      self->use_regex = use_regex;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_USE_REGEX]);
+    }
+}
+
+gboolean
+gbp_grep_model_get_recursive (GbpGrepModel *self)
+{
+  g_return_val_if_fail (GBP_IS_GREP_MODEL (self), FALSE);
+
+  return self->recursive;
+}
+
+void
+gbp_grep_model_set_recursive (GbpGrepModel *self,
+                              gboolean      recursive)
+{
+  g_return_if_fail (GBP_IS_GREP_MODEL (self));
+  g_return_if_fail (self->has_scanned == FALSE);
+
+  recursive = !!recursive;
+
+  if (recursive != self->recursive)
+    {
+      self->recursive = recursive;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RECURSIVE]);
+    }
+}
+
+gboolean
+gbp_grep_model_get_case_sensitive (GbpGrepModel *self)
+{
+  g_return_val_if_fail (GBP_IS_GREP_MODEL (self), FALSE);
+
+  return self->case_sensitive;
+}
+
+void
+gbp_grep_model_set_case_sensitive (GbpGrepModel *self,
+                                   gboolean      case_sensitive)
+{
+  g_return_if_fail (GBP_IS_GREP_MODEL (self));
+  g_return_if_fail (self->has_scanned == FALSE);
+
+  case_sensitive = !!case_sensitive;
+
+  if (case_sensitive != self->case_sensitive)
+    {
+      self->case_sensitive = case_sensitive;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CASE_SENSITIVE]);
+    }
+}
+
+gboolean
+gbp_grep_model_get_at_word_boundaries (GbpGrepModel *self)
+{
+  g_return_val_if_fail (GBP_IS_GREP_MODEL (self), FALSE);
+
+  return self->at_word_boundaries;
+}
+
+void
+gbp_grep_model_set_at_word_boundaries (GbpGrepModel *self,
+                                       gboolean      at_word_boundaries)
+{
+  g_return_if_fail (GBP_IS_GREP_MODEL (self));
+  g_return_if_fail (self->has_scanned == FALSE);
+
+  at_word_boundaries = !!at_word_boundaries;
+
+  if (at_word_boundaries != self->at_word_boundaries)
+    {
+      self->at_word_boundaries = at_word_boundaries;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_AT_WORD_BOUNDARIES]);
+    }
+}
+
+static IdeSubprocessLauncher *
+gbp_grep_model_create_launcher (GbpGrepModel *self)
+{
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  const gchar *path;
+  IdeContext *context;
+  IdeVcs *vcs;
+  GFile *workdir;
+  GType git_vcs;
+  gboolean use_git_grep = FALSE;
+
+  g_assert (GBP_IS_GREP_MODEL (self));
+  g_assert (self->query != NULL);
+  g_assert (self->query[0] != '\0');
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  vcs = ide_context_get_vcs (context);
+  workdir = ide_vcs_get_working_directory (vcs);
+  git_vcs = g_type_from_name ("IdeGitVcs");
+
+  if (self->directory != NULL)
+    path = g_file_peek_path (self->directory);
+  else
+    path = g_file_peek_path (workdir);
+
+  launcher = ide_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE);
+  ide_subprocess_launcher_set_cwd (launcher, path);
+
+  /*
+   * Soft runtime check for Git support, so that we can use "git grep"
+   * instead of the system "grep".
+   */
+  if (git_vcs != G_TYPE_INVALID && g_type_is_a (G_OBJECT_TYPE (vcs), git_vcs))
+    use_git_grep = TRUE;
+
+  if (use_git_grep)
+    {
+      ide_subprocess_launcher_push_argv (launcher, "git");
+      ide_subprocess_launcher_push_argv (launcher, "grep");
+    }
+  else
+    {
+#ifdef __FreeBSD__
+      ide_subprocess_launcher_push_argv (launcher, "bsdgrep");
+#else
+      ide_subprocess_launcher_push_argv (launcher, "grep");
+#endif
+    }
+
+  ide_subprocess_launcher_push_argv (launcher, "-I");
+  ide_subprocess_launcher_push_argv (launcher, "-H");
+  ide_subprocess_launcher_push_argv (launcher, "-n");
+
+  if (!self->case_sensitive)
+    ide_subprocess_launcher_push_argv (launcher, "-i");
+
+  if (self->at_word_boundaries)
+    ide_subprocess_launcher_push_argv (launcher, "-w");
+
+  if (!use_git_grep)
+    {
+      if (self->recursive)
+        ide_subprocess_launcher_push_argv (launcher, "-r");
+    }
+  else
+    {
+      if (!self->recursive)
+        ide_subprocess_launcher_push_argv (launcher, "--max-depth=0");
+    }
+
+  ide_subprocess_launcher_push_argv (launcher, "-E");
+
+  if (!self->use_regex)
+    {
+      g_autofree gchar *escaped = NULL;
+
+      escaped = g_regex_escape_string (self->query, -1);
+      ide_subprocess_launcher_push_argv (launcher, "-e");
+      ide_subprocess_launcher_push_argv (launcher, escaped);
+    }
+  else
+    {
+      ide_subprocess_launcher_push_argv (launcher, "-e");
+      ide_subprocess_launcher_push_argv (launcher, self->query);
+    }
+
+  if (use_git_grep)
+    {
+      /* Avoid pathological lines up front before reading them into
+       * the UI process memory space.
+       *
+       * Note that we do this *after* our query match because it causes
+       * grep to have to look at every line up to it. So to do this in
+       * reverse order is incredibly slow.
+       */
+      ide_subprocess_launcher_push_argv (launcher, "--and");
+      ide_subprocess_launcher_push_argv (launcher, "-e");
+      ide_subprocess_launcher_push_argv (launcher, "^.{0,256}$");
+    }
+
+  return g_steal_pointer (&launcher);
+}
+
+static void
+gbp_grep_model_build_index (IdeTask      *task,
+                            gpointer      source_object,
+                            gpointer      task_data,
+                            GCancellable *cancellable)
+{
+  GBytes *bytes = task_data;
+  IdeLineReader reader;
+  Index *idx;
+  gchar *buf;
+  gsize len;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (GBP_IS_GREP_MODEL (source_object));
+  g_assert (bytes != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  buf = (gchar *)g_bytes_get_data (bytes, &len);
+  ide_line_reader_init (&reader, buf, len);
+
+  idx = g_slice_new0 (Index);
+  idx->bytes = g_bytes_ref (bytes);
+  idx->rows = g_ptr_array_new ();
+
+  while ((buf = ide_line_reader_next (&reader, &len)))
+    {
+      g_ptr_array_add (idx->rows, buf);
+      buf[len] = 0;
+    }
+
+  ide_task_return_pointer (task, idx, index_free);
+}
+
+static void
+gbp_grep_model_scan_cb (GObject      *object,
+                        GAsyncResult *result,
+                        gpointer      user_data)
+{
+  IdeSubprocess *subprocess = (IdeSubprocess *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GBytes) bytes = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *stdout_buf = NULL;
+  GbpGrepModel *self;
+  gsize len;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SUBPROCESS (object));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+
+  g_assert (GBP_IS_GREP_MODEL (self));
+
+  if (!ide_subprocess_communicate_utf8_finish (subprocess, result, &stdout_buf, NULL, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  if (stdout_buf == NULL)
+    stdout_buf = g_strdup ("");
+
+  len = strlen (stdout_buf);
+  bytes = g_bytes_new_take (g_steal_pointer (&stdout_buf), len);
+  ide_task_set_task_data (task, g_steal_pointer (&bytes), g_bytes_unref);
+  ide_task_run_in_thread (task, gbp_grep_model_build_index);
+
+  IDE_EXIT;
+}
+
+void
+gbp_grep_model_scan_async (GbpGrepModel        *self,
+                           GCancellable        *cancellable,
+                           GAsyncReadyCallback  callback,
+                           gpointer             user_data)
+{
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autoptr(IdeSubprocess) subprocess = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (GBP_IS_GREP_MODEL (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_grep_model_scan_async);
+
+  if (self->has_scanned)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVAL,
+                                 "gbp_grep_model_scan_async() may only be called once");
+      IDE_EXIT;
+    }
+
+  if (dzl_str_empty0 (self->query))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVAL,
+                                 "No query has been set to scan for");
+      IDE_EXIT;
+    }
+
+  self->has_scanned = TRUE;
+
+  launcher = gbp_grep_model_create_launcher (self);
+  subprocess = ide_subprocess_launcher_spawn (launcher, cancellable, &error);
+
+  if (subprocess == NULL)
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  ide_subprocess_communicate_utf8_async (subprocess,
+                                         NULL,
+                                         cancellable,
+                                         gbp_grep_model_scan_cb,
+                                         g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+gboolean
+gbp_grep_model_scan_finish (GbpGrepModel  *self,
+                            GAsyncResult  *result,
+                            GError       **error)
+{
+  g_return_val_if_fail (GBP_IS_GREP_MODEL (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+  g_return_val_if_fail (self->index == NULL, FALSE);
+
+  self->index = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  /*
+   * Normally, we might emit ::row-inserted() for each row. But that
+   * is expensive and our normal use case is that we don't attach the
+   * model until the search has completed.
+   */
+
+  return self->index != NULL;
+}
+
+static GtkTreeModelFlags
+gbp_grep_model_get_flags (GtkTreeModel *tree_model)
+{
+  return GTK_TREE_MODEL_LIST_ONLY;
+}
+
+static gint
+gbp_grep_model_get_n_columns (GtkTreeModel *tree_model)
+{
+  return 2;
+}
+
+static GType
+gbp_grep_model_get_column_type (GtkTreeModel *tree_model,
+                                gint          index_)
+{
+  switch (index_) {
+  case 0:  return G_TYPE_STRING;
+  case 1:  return G_TYPE_BOOLEAN;
+  default: return G_TYPE_INVALID;
+  }
+}
+
+static gboolean
+gbp_grep_model_get_iter (GtkTreeModel *tree_model,
+                         GtkTreeIter  *iter,
+                         GtkTreePath  *path)
+{
+  GbpGrepModel *self = (GbpGrepModel *)tree_model;
+  gint *indicies;
+  gint depth;
+
+  g_assert (GBP_IS_GREP_MODEL (self));
+  g_assert (iter != NULL);
+  g_assert (path != NULL);
+
+  if (self->index == NULL || self->index->rows == NULL)
+    return FALSE;
+
+  indicies = gtk_tree_path_get_indices_with_depth (path, &depth);
+
+  if (depth != 1)
+    return FALSE;
+
+  iter->user_data = GINT_TO_POINTER (indicies[0]);
+
+  return indicies[0] >= 0 && indicies[0] < self->index->rows->len;
+}
+
+static GtkTreePath *
+gbp_grep_model_get_path (GtkTreeModel *tree_model,
+                         GtkTreeIter  *iter)
+{
+  g_assert (GBP_IS_GREP_MODEL (tree_model));
+  g_assert (iter != NULL);
+
+  return gtk_tree_path_new_from_indices (GPOINTER_TO_INT (iter->user_data), -1);
+}
+
+static void
+gbp_grep_model_get_value (GtkTreeModel *tree_model,
+                          GtkTreeIter  *iter,
+                          gint          column,
+                          GValue       *value)
+{
+  GbpGrepModel *self = (GbpGrepModel *)tree_model;
+  guint index_;
+
+  g_assert (GBP_IS_GREP_MODEL (self));
+  g_assert (iter != NULL);
+  g_assert (column == 0 || column == 1);
+  g_assert (value != NULL);
+
+  index_ = GPOINTER_TO_UINT (iter->user_data);
+
+  if (column == 0)
+    {
+      g_value_init (value, G_TYPE_STRING);
+      /* This is a hack that we can do because we know that our
+       * consumer will only use the string for a short time to
+       * parse it into the real value without holding the GValue
+       * past the lifetime of the model.
+       *
+       * It saves us a serious amount of string copies.
+       */
+      g_value_set_static_string (value,
+                                 g_ptr_array_index (self->index->rows, index_));
+    }
+  else if (column == 1)
+    {
+      g_value_init (value, G_TYPE_BOOLEAN);
+      /* TODO: Make this toggle'able */
+      g_value_set_boolean (value, TRUE);
+    }
+}
+
+static gboolean
+gbp_grep_model_iter_next (GtkTreeModel *tree_model,
+                          GtkTreeIter  *iter)
+{
+  GbpGrepModel *self = (GbpGrepModel *)tree_model;
+  guint index_;
+
+  g_assert (GBP_IS_GREP_MODEL (self));
+
+  if (self->index == NULL)
+    return FALSE;
+
+  index_ = GPOINTER_TO_UINT (iter->user_data);
+  if (index_ == G_MAXUINT)
+    return FALSE;
+
+  index_++;
+
+  iter->user_data = GUINT_TO_POINTER (index_);
+
+  return index_ < self->index->rows->len;
+}
+
+static gboolean
+gbp_grep_model_iter_children (GtkTreeModel *tree_model,
+                              GtkTreeIter  *iter,
+                              GtkTreeIter  *parent)
+{
+  GbpGrepModel *self = (GbpGrepModel *)tree_model;
+
+  g_assert (GBP_IS_GREP_MODEL (self));
+  g_assert (iter != NULL);
+
+  iter->user_data = NULL;
+
+  return parent == NULL &&
+         self->index != NULL &&
+         self->index->rows->len > 0;
+}
+
+static gboolean
+gbp_grep_model_iter_has_child (GtkTreeModel *tree_model,
+                               GtkTreeIter  *iter)
+{
+  GbpGrepModel *self = (GbpGrepModel *)tree_model;
+
+  g_assert (GBP_IS_GREP_MODEL (self));
+
+  if (iter == NULL)
+    return self->index != NULL && self->index->rows->len > 0;
+
+  return FALSE;
+}
+
+static gint
+gbp_grep_model_iter_n_children (GtkTreeModel *tree_model,
+                                GtkTreeIter  *iter)
+{
+  GbpGrepModel *self = (GbpGrepModel *)tree_model;
+
+  g_assert (GBP_IS_GREP_MODEL (self));
+
+  if (iter == NULL)
+    {
+      if (self->index != NULL)
+        return self->index->rows->len;
+    }
+
+  return 0;
+}
+
+static gboolean
+gbp_grep_model_iter_nth_child (GtkTreeModel *tree_model,
+                               GtkTreeIter  *iter,
+                               GtkTreeIter  *parent,
+                               gint          n)
+{
+  GbpGrepModel *self = (GbpGrepModel *)tree_model;
+
+  g_assert (GBP_IS_GREP_MODEL (self));
+  g_assert (iter != NULL);
+
+  if (parent == NULL && self->index != NULL)
+    {
+      iter->user_data = GUINT_TO_POINTER (n);
+      return n < self->index->rows->len;
+    }
+
+  return FALSE;
+}
+
+static gboolean
+gbp_grep_model_iter_parent (GtkTreeModel *tree_model,
+                            GtkTreeIter  *iter,
+                            GtkTreeIter  *parent)
+{
+  return FALSE;
+}
+
+static void
+tree_model_iface_init (GtkTreeModelIface *iface)
+{
+  iface->get_flags = gbp_grep_model_get_flags;
+  iface->get_n_columns = gbp_grep_model_get_n_columns;
+  iface->get_column_type = gbp_grep_model_get_column_type;
+  iface->get_iter = gbp_grep_model_get_iter;
+  iface->get_path = gbp_grep_model_get_path;
+  iface->get_value = gbp_grep_model_get_value;
+  iface->iter_next = gbp_grep_model_iter_next;
+  iface->iter_children = gbp_grep_model_iter_children;
+  iface->iter_has_child = gbp_grep_model_iter_has_child;
+  iface->iter_n_children = gbp_grep_model_iter_n_children;
+  iface->iter_nth_child = gbp_grep_model_iter_nth_child;
+  iface->iter_parent = gbp_grep_model_iter_parent;
+}
diff --git a/src/plugins/grep/gbp-grep-model.h b/src/plugins/grep/gbp-grep-model.h
new file mode 100644
index 000000000..76a99a38b
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-model.h
@@ -0,0 +1,58 @@
+/* gbp-grep-model.h
+ *
+ * Copyright 2018 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 <ide.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GREP_MODEL (gbp_grep_model_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGrepModel, gbp_grep_model, GBP, GREP_MODEL, IdeObject)
+
+GbpGrepModel *gbp_grep_model_new                    (IdeContext           *context);
+GFile        *gbp_grep_model_get_directory          (GbpGrepModel         *self);
+void          gbp_grep_model_set_directory          (GbpGrepModel         *self,
+                                                     GFile                *directory);
+gboolean      gbp_grep_model_get_use_regex          (GbpGrepModel         *self);
+void          gbp_grep_model_set_use_regex          (GbpGrepModel         *self,
+                                                     gboolean              use_regex);
+gboolean      gbp_grep_model_get_recursive          (GbpGrepModel         *self);
+void          gbp_grep_model_set_recursive          (GbpGrepModel         *self,
+                                                     gboolean              recursive);
+gboolean      gbp_grep_model_get_case_sensitive     (GbpGrepModel         *self);
+void          gbp_grep_model_set_case_sensitive     (GbpGrepModel         *self,
+                                                     gboolean              case_sensitive);
+gboolean      gbp_grep_model_get_at_word_boundaries (GbpGrepModel         *self);
+void          gbp_grep_model_set_at_word_boundaries (GbpGrepModel         *self,
+                                                     gboolean              at_word_boundaries);
+const gchar  *gbp_grep_model_get_query              (GbpGrepModel         *self);
+void          gbp_grep_model_set_query              (GbpGrepModel         *self,
+                                                     const gchar          *query);
+void          gbp_grep_model_scan_async             (GbpGrepModel         *self,
+                                                     GCancellable         *cancellable,
+                                                     GAsyncReadyCallback   callback,
+                                                     gpointer              user_data);
+gboolean      gbp_grep_model_scan_finish            (GbpGrepModel         *self,
+                                                     GAsyncResult         *result,
+                                                     GError              **error);
+
+G_END_DECLS
diff --git a/src/plugins/grep/gbp-grep-panel.c b/src/plugins/grep/gbp-grep-panel.c
new file mode 100644
index 000000000..326545346
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-panel.c
@@ -0,0 +1,302 @@
+/* gbp-grep-panel.c
+ *
+ * Copyright © 2018 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
+ */
+
+#include "config.h"
+
+#define G_LOG_DOMAIN "gbp-grep-panel"
+
+#include <glib/gi18n.h>
+
+#include "gbp-grep-panel.h"
+
+struct _GbpGrepPanel
+{
+  DzlDockWidget  parent_instance;
+  GtkTreeView   *tree_view;
+};
+
+enum {
+  PROP_0,
+  PROP_MODEL,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (GbpGrepPanel, gbp_grep_panel, DZL_TYPE_DOCK_WIDGET)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+match_data_func (GtkCellLayout   *layout,
+                 GtkCellRenderer *cell,
+                 GtkTreeModel    *model,
+                 GtkTreeIter     *iter,
+                 gpointer         user_data)
+{
+  g_auto(GValue) src = G_VALUE_INIT;
+  g_auto(GValue) dst = G_VALUE_INIT;
+  const gchar *str;
+  const gchar *tmp;
+  guint count = 0;
+
+  g_assert (GTK_IS_CELL_LAYOUT (layout));
+  g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+
+  gtk_tree_model_get_value (model, iter, 0, &src);
+  str = g_value_get_string (&src);
+  g_value_init (&dst, G_TYPE_STRING);
+
+  for (tmp = str; *tmp; tmp = g_utf8_next_char (tmp))
+    {
+      if (*tmp == ':')
+        {
+          if (count == 1)
+            {
+              tmp++;
+              /* We can use static string because we control
+               * the lifetime of the GValue here. Let's us avoid
+               * an unnecessary copy.
+               */
+              g_value_set_static_string (&dst, tmp);
+              break;
+            }
+          count++;
+        }
+    }
+
+  g_object_set_property (G_OBJECT (cell), "text", &dst);
+}
+
+static void
+path_data_func (GtkCellLayout   *layout,
+                GtkCellRenderer *cell,
+                GtkTreeModel    *model,
+                GtkTreeIter     *iter,
+                gpointer         user_data)
+{
+  g_auto(GValue) src = G_VALUE_INIT;
+  g_auto(GValue) dst = G_VALUE_INIT;
+  const gchar *str;
+  const gchar *tmp;
+  guint count = 0;
+
+  g_assert (GTK_IS_CELL_LAYOUT (layout));
+  g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+
+  gtk_tree_model_get_value (model, iter, 0, &src);
+  str = g_value_get_string (&src);
+  g_value_init (&dst, G_TYPE_STRING);
+
+  for (tmp = str; *tmp; tmp = g_utf8_next_char (tmp))
+    {
+      if (*tmp == ':')
+        {
+          if (count == 1)
+            {
+              g_value_take_string (&dst, g_strndup (str, tmp - str));
+              break;
+            }
+          count++;
+        }
+    }
+
+  g_object_set_property (G_OBJECT (cell), "text", &dst);
+}
+
+static void
+filename_data_func (GtkCellLayout   *layout,
+                    GtkCellRenderer *cell,
+                    GtkTreeModel    *model,
+                    GtkTreeIter     *iter,
+                    gpointer         user_data)
+{
+  g_auto(GValue) src = G_VALUE_INIT;
+  g_auto(GValue) dst = G_VALUE_INIT;
+  const gchar *str;
+  const gchar *tmp;
+  const gchar *slash;
+  guint count = 0;
+
+  g_assert (GTK_IS_CELL_LAYOUT (layout));
+  g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+
+  gtk_tree_model_get_value (model, iter, 0, &src);
+  slash = str = g_value_get_string (&src);
+  g_value_init (&dst, G_TYPE_STRING);
+
+  for (tmp = str; *tmp; tmp = g_utf8_next_char (tmp))
+    {
+      if (*tmp == ':')
+        {
+          if (count == 1)
+            {
+              g_value_take_string (&dst, g_strndup (slash, tmp - slash));
+              break;
+            }
+          count++;
+        }
+      else if (*tmp == G_DIR_SEPARATOR)
+        {
+          slash = tmp + 1;
+        }
+    }
+
+  g_object_set_property (G_OBJECT (cell), "text", &dst);
+}
+
+static void
+gbp_grep_panel_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  GbpGrepPanel *self = GBP_GREP_PANEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      g_value_set_object (value, gbp_grep_panel_get_model (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_grep_panel_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  GbpGrepPanel *self = GBP_GREP_PANEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      gbp_grep_panel_set_model (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_grep_panel_class_init (GbpGrepPanelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = gbp_grep_panel_get_property;
+  object_class->set_property = gbp_grep_panel_set_property;
+
+  properties [PROP_MODEL] =
+    g_param_spec_object ("model", NULL, NULL,
+                         GBP_TYPE_GREP_MODEL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_css_name (widget_class, "gbpgreppanel");
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/plugins/grep/gbp-grep-panel.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, tree_view);
+}
+
+static void
+gbp_grep_panel_init (GbpGrepPanel *self)
+{
+  GtkTreeViewColumn *column;
+  GtkCellRenderer *cell;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  column = gtk_tree_view_column_new ();
+  cell = gtk_cell_renderer_toggle_new ();
+  gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, TRUE);
+  gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (column), cell, "active", 1);
+  gtk_tree_view_column_set_expand (column, FALSE);
+  gtk_tree_view_append_column (self->tree_view, column);
+
+  column = gtk_tree_view_column_new ();
+  cell = gtk_cell_renderer_text_new ();
+  gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, TRUE);
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column), cell, filename_data_func, NULL, NULL);
+  gtk_tree_view_column_set_title (column, _("Filename"));
+  gtk_tree_view_column_set_expand (column, FALSE);
+  gtk_tree_view_column_set_resizable (column, TRUE);
+  gtk_tree_view_append_column (self->tree_view, column);
+
+  column = gtk_tree_view_column_new ();
+  cell = g_object_new (GTK_TYPE_CELL_RENDERER_TEXT,
+                       "ellipsize", PANGO_ELLIPSIZE_END,
+                       NULL);
+  gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, TRUE);
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column), cell, match_data_func, NULL, NULL);
+  gtk_tree_view_column_set_title (column, _("Match"));
+  gtk_tree_view_column_set_expand (column, TRUE);
+  gtk_tree_view_column_set_resizable (column, TRUE);
+  gtk_tree_view_append_column (self->tree_view, column);
+
+  column = gtk_tree_view_column_new ();
+  cell = g_object_new (GTK_TYPE_CELL_RENDERER_TEXT,
+                       "ellipsize", PANGO_ELLIPSIZE_END,
+                       "width-chars", 20,
+                       NULL);
+  gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, TRUE);
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column), cell, path_data_func, NULL, NULL);
+  gtk_tree_view_column_set_title (column, _("Path"));
+  gtk_tree_view_column_set_expand (column, FALSE);
+  gtk_tree_view_column_set_resizable (column, TRUE);
+  gtk_tree_view_append_column (self->tree_view, column);
+}
+
+void
+gbp_grep_panel_set_model (GbpGrepPanel *self,
+                          GbpGrepModel *model)
+{
+  g_return_if_fail (GBP_IS_GREP_PANEL (self));
+  g_return_if_fail (!model || GBP_IS_GREP_MODEL (model));
+
+  gtk_tree_view_set_model (self->tree_view, GTK_TREE_MODEL (model));
+}
+
+/**
+ * gbp_grep_panel_get_model:
+ * @self: a #GbpGrepPanel
+ *
+ * Returns: (transfer none) (nullable): a #GbpGrepModel
+ */
+GbpGrepModel *
+gbp_grep_panel_get_model (GbpGrepPanel *self)
+{
+  return GBP_GREP_MODEL (gtk_tree_view_get_model (self->tree_view));
+}
+
+GtkWidget *
+gbp_grep_panel_new (void)
+{
+  return g_object_new (GBP_TYPE_GREP_PANEL, NULL);
+}
diff --git a/src/plugins/grep/gbp-grep-panel.h b/src/plugins/grep/gbp-grep-panel.h
new file mode 100644
index 000000000..04e49c093
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-panel.h
@@ -0,0 +1,38 @@
+/* gbp-grep-panel.h
+ *
+ * Copyright © 2018 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 <ide.h>
+
+#include "gbp-grep-model.h"
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GREP_PANEL (gbp_grep_panel_get_type())
+
+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);
+
+G_END_DECLS
diff --git a/src/plugins/grep/gbp-grep-panel.ui b/src/plugins/grep/gbp-grep-panel.ui
new file mode 100644
index 000000000..3e579e460
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-panel.ui
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpGrepPanel" parent="DzlDockWidget">
+    <property name="icon-name">edit-find-symbolic</property>
+    <property name="title" translatable="yes">Find in Files</property>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="vexpand">true</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkScrolledWindow">
+            <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="GtkBox">
+            <property name="margin-top">6</property>
+            <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="visible">true</property>
+            <child type="center">
+              <object class="GtkBox">
+                <property name="spacing">12</property>
+                <property name="hexpand">false</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="visible">true</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                </child>
+                <child type="center">
+                  <object class="GtkEntry">
+                    <property name="visible">true</property>
+                    <property name="width-chars">30</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkButton" id="replace_button">
+                    <property name="label" translatable="yes">Replace</property>
+                    <property name="visible">true</property>
+                    <style>
+                      <class name="destructive-action"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="position">1</property>
+                    <property name="pack-type">end</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkButton" id="close_button">
+                    <property name="label" translatable="yes">Close</property>
+                    <property name="visible">true</property>
+                  </object>
+                  <packing>
+                    <property name="position">0</property>
+                    <property name="pack-type">end</property>
+                  </packing>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </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-plugin.c b/src/plugins/grep/gbp-grep-plugin.c
new file mode 100644
index 000000000..f2a79638e
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-plugin.c
@@ -0,0 +1,32 @@
+/* gbp-grep-plugin.c
+ *
+ * Copyright 2018 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
+ */
+
+#include <ide.h>
+#include <libpeas/peas.h>
+
+#include "gbp-grep-editor-addin.h"
+
+void
+gbp_grep_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_EDITOR_ADDIN,
+                                              GBP_TYPE_GREP_EDITOR_ADDIN);
+}
diff --git a/src/plugins/grep/grep.gresource.xml b/src/plugins/grep/grep.gresource.xml
new file mode 100644
index 000000000..d0c6cbbb1
--- /dev/null
+++ b/src/plugins/grep/grep.gresource.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/builder/plugins">
+    <file>grep.plugin</file>
+  </gresource>
+  <gresource prefix="/org/gnome/builder/plugins/grep">
+    <file preprocess="xml-stripblanks">gbp-grep-dialog.ui</file>
+    <file preprocess="xml-stripblanks">gbp-grep-panel.ui</file>
+    <file>themes/Adwaita-shared.css</file>
+    <file>themes/Adwaita-dark.css</file>
+    <file>themes/Adwaita.css</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/grep/grep.plugin b/src/plugins/grep/grep.plugin
new file mode 100644
index 000000000..c68179cca
--- /dev/null
+++ b/src/plugins/grep/grep.plugin
@@ -0,0 +1,9 @@
+[Plugin]
+Module=grep
+Name=Find in Files
+Description=Search across project files
+Authors=Christian Hergert <christian hergert me>
+Copyright=Copyright © 2018 Christian Hergert
+Builtin=true
+Depends=editor
+Embedded=gbp_grep_register_types
diff --git a/src/plugins/grep/meson.build b/src/plugins/grep/meson.build
new file mode 100644
index 000000000..5481a5614
--- /dev/null
+++ b/src/plugins/grep/meson.build
@@ -0,0 +1,20 @@
+if get_option('with_grep')
+
+grep_resources = gnome.compile_resources(
+  'grep-resources',
+  'grep.gresource.xml',
+  c_name: 'gbp_grep',
+)
+
+grep_sources = [
+  'gbp-grep-dialog.c',
+  'gbp-grep-editor-addin.c',
+  'gbp-grep-model.c',
+  'gbp-grep-panel.c',
+  'gbp-grep-plugin.c',
+]
+
+gnome_builder_plugins_sources += files(grep_sources)
+gnome_builder_plugins_sources += grep_resources[0]
+
+endif
diff --git a/src/plugins/grep/themes/Adwaita-dark.css b/src/plugins/grep/themes/Adwaita-dark.css
new file mode 100644
index 000000000..0413f3e93
--- /dev/null
+++ b/src/plugins/grep/themes/Adwaita-dark.css
@@ -0,0 +1,2 @@
+@import url("resource:///org/gnome/builder/plugins/grep/themes/Adwaita-shared.css");
+
diff --git a/src/plugins/grep/themes/Adwaita-shared.css b/src/plugins/grep/themes/Adwaita-shared.css
new file mode 100644
index 000000000..b2e178348
--- /dev/null
+++ b/src/plugins/grep/themes/Adwaita-shared.css
@@ -0,0 +1,9 @@
+gbpgreppanel {
+  background-color: @theme_bg_color;
+}
+gbpgreppanel button {
+  min-height: 0px;
+}
+gbpgreppanel {
+  border-left: 1px solid alpha(@borders,.1);
+}
diff --git a/src/plugins/grep/themes/Adwaita.css b/src/plugins/grep/themes/Adwaita.css
new file mode 100644
index 000000000..0413f3e93
--- /dev/null
+++ b/src/plugins/grep/themes/Adwaita.css
@@ -0,0 +1,2 @@
+@import url("resource:///org/gnome/builder/plugins/grep/themes/Adwaita-shared.css");
+
diff --git a/src/plugins/meson.build b/src/plugins/meson.build
index 9ca823dae..76603122e 100644
--- a/src/plugins/meson.build
+++ b/src/plugins/meson.build
@@ -37,6 +37,7 @@ subdir('gjs-symbols')
 subdir('glade')
 subdir('gnome-code-assistance')
 subdir('go-langserv')
+subdir('grep')
 subdir('history')
 subdir('html-completion')
 subdir('html-preview')
@@ -122,6 +123,7 @@ status += [
   'Glade ................. : @0@'.format(get_option('with_glade')),
   'GNOME Code Assistance . : @0@'.format(get_option('with_gnome_code_assistance')),
   'Go Language Server .... : @0@'.format(get_option('with_go_langserv')),
+  'Grep .................. : @0@'.format(get_option('with_grep')),
   'History ............... : @0@'.format(get_option('with_history')),
   'HTML Completion ....... : @0@'.format(get_option('with_html_completion')),
   'HTML Preview .......... : @0@'.format(get_option('with_html_preview')),


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