[gnome-builder/wip/chergert/grep: 2/2] grep: add plugin to grep through project tree



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

    grep: add plugin to grep through project tree
    
    This plugin provides a grep backend that can optionally use git-grep when
    the current project is using Git.
    
    You can search for files from the project-tree by selecting "Find in Files".
    That will limit the search results to the directory that has been selected.
    
    If the selected node is a File (instead of a directory), then only results
    from that file will be shown.
    
    You can search using regular expressions supported by the particular grep
    implementation. We don't currently guarantee options here (or validate them
    for correctness). Some effort could be done here using GRegex in the future.
    
    As we already have support for performing edits across a number of files in
    the background, the grep plugin allows you to select matches and replace them
    with new text. In the future we could extend this to try to preserve casing
    of the replacement.
    
    To avoid lots of small strings and numerous copies of them, a custom
    GtkTreeModel was used. Instead we have a single large buffer of results that
    were obtained from grep, and keep an index to the start of each line in the
    buffer. A \0 replaces the \n that we received from grep.
    
    I expect additional work will be needed here, but this seems like a reasonable
    first attempt at the feature. Larger projects may need additional performance
    tweaks.

 meson_options.txt                              |    1 +
 po/POTFILES.in                                 |    3 +
 src/plugins/grep/gbp-grep-model.c              | 1247 ++++++++++++++++++++++++
 src/plugins/grep/gbp-grep-model.h              |   86 ++
 src/plugins/grep/gbp-grep-panel.c              |  533 ++++++++++
 src/plugins/grep/gbp-grep-panel.h              |   38 +
 src/plugins/grep/gbp-grep-panel.ui             |  102 ++
 src/plugins/grep/gbp-grep-plugin.c             |   32 +
 src/plugins/grep/gbp-grep-popover.c            |  241 +++++
 src/plugins/grep/gbp-grep-popover.h            |   31 +
 src/plugins/grep/gbp-grep-popover.ui           |   85 ++
 src/plugins/grep/gbp-grep-project-tree-addin.c |  203 ++++
 src/plugins/grep/gbp-grep-project-tree-addin.h |   31 +
 src/plugins/grep/grep.gresource.xml            |   14 +
 src/plugins/grep/grep.plugin                   |    9 +
 src/plugins/grep/gtk/menus.ui                  |   11 +
 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 +
 src/plugins/project-tree/gtk/menus.ui          |    1 +
 22 files changed, 2703 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/po/POTFILES.in b/po/POTFILES.in
index ddd9a9275..a007db4b2 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -195,6 +195,9 @@ src/plugins/gnome-code-assistance/ide-gca-preferences-addin.c
 src/plugins/gnome-code-assistance/ide-gca-service.c
 src/plugins/gnome-code-assistance/org.gnome.builder.gnome-code-assistance.gschema.xml
 src/plugins/gradle/gradle_plugin.py
+src/plugins/grep/gbp-grep-panel.c
+src/plugins/grep/gbp-grep-popover.ui
+src/plugins/grep/gtk/menus.ui
 src/plugins/html-preview/gtk/menus.ui
 src/plugins/html-preview/html_preview.py
 src/plugins/jedi/jedi_plugin.py
diff --git a/src/plugins/grep/gbp-grep-model.c b/src/plugins/grep/gbp-grep-model.c
new file mode 100644
index 000000000..f4b120f45
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-model.c
@@ -0,0 +1,1247 @@
+/* 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;
+
+  /* The root directory to start searching from. */
+  GFile *directory;
+
+  /* The query text, which we use to send to grep as well as use with
+   * GRegex to extract the match positions from a specific line.
+   */
+  gchar *query;
+
+  /* We need to do client-side processing to extract the exact message
+   * locations after grep gives us the matching lines. This allows us to
+   * create IdeProjectEdit source ranges later as well as creating the
+   * match positions for highlighting in the treeview cell renderers.
+   */
+  GRegex *message_regex;
+
+  /* Our index of matches, which can be compiled off the main thread
+   * and then assigned to the model after it has completed building.
+   */
+  Index *index;
+
+  /* We store the index of the toggled items here, and use that to
+   * reverse their selection from a base "all" or "nothing" mode.
+   */
+  GHashTable *toggled;
+
+  /* We cache the last line we parsed, because the view will parse
+   * the same line repeatedly as it builds the cells for display.
+   * This saves us a bunch of repeated work.
+   */
+  GbpGrepModelLine prev_line;
+
+  guint mode;
+
+  guint has_scanned : 1;
+  guint use_regex : 1;
+  guint recursive : 1;
+  guint case_sensitive : 1;
+  guint at_word_boundaries : 1;
+  guint was_directory : 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
+};
+
+enum {
+  MODE_NONE,
+  MODE_ALL,
+};
+
+static GParamSpec *properties [N_PROPS];
+static GRegex     *line_regex;
+
+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);
+}
+
+static void
+clear_line (GbpGrepModelLine *cl)
+{
+  cl->start_of_line = NULL;
+  cl->line = 0;
+  g_clear_pointer (&cl->path, g_free);
+  g_clear_pointer (&cl->matches, g_array_unref);
+}
+
+static gboolean
+gbp_grep_model_line_parse (GbpGrepModelLine *cl,
+                           const gchar      *line,
+                           GRegex           *message_regex)
+{
+  g_autoptr(GMatchInfo) match = NULL;
+  gsize line_len;
+
+  g_assert (cl != NULL);
+  g_assert (line != NULL);
+  g_assert (line_regex != NULL);
+  g_assert (message_regex != NULL);
+
+  line_len = strlen (line);
+
+  if (g_regex_match_full (line_regex, line, line_len, 0, 0, &match, NULL))
+    {
+      g_autofree gchar *pathstr = NULL;
+      g_autofree gchar *linestr = NULL;
+      gint msg_begin = -1;
+      gint msg_end = -1;
+
+      pathstr = g_match_info_fetch (match, 1);
+      linestr = g_match_info_fetch (match, 2);
+
+      if (g_match_info_fetch_pos (match, 3, &msg_begin, &msg_end))
+        {
+          g_autoptr(GMatchInfo) msg_match = NULL;
+          gsize msg_len;
+
+          /* Make sure we parsed the message offset */
+          if (msg_begin < 0)
+            return FALSE;
+
+          cl->start_of_line = line;
+          cl->start_of_message = line + msg_begin;
+          cl->path = g_steal_pointer (&pathstr);
+          cl->matches = g_array_new (FALSE, FALSE, sizeof (GbpGrepModelMatch));
+          cl->line = g_ascii_strtoll (linestr, NULL, 10);
+
+          /* Now parse the matches for the line so that we can highlight
+           * them in the treeview and also determine the IdeProjectEdit
+           * source range when editing files.
+           */
+
+          msg_len = line_len - msg_begin;
+
+          if (g_regex_match_full (message_regex, cl->start_of_message, msg_len, 0, 0, &msg_match, NULL))
+            {
+              do
+                {
+                  gint match_begin = -1;
+                  gint match_end = -1;
+
+                  if (g_match_info_fetch_pos (msg_match, 0, &match_begin, &match_end))
+                    {
+                      GbpGrepModelMatch cm;
+
+                      /*
+                       * We need to convert match offsets from bytes into the
+                       * number of UTF-8 (unichar) characters) so that we get
+                       * proper columns into the target file. Otherwise we risk
+                       * corrupting non-ASCII files.
+                       */
+                      cm.match_begin = g_utf8_strlen (cl->start_of_message, match_begin);
+                      cm.match_end = g_utf8_strlen (cl->start_of_message, match_end);
+                      cm.match_begin_bytes = match_begin;
+                      cm.match_end_bytes = match_end;
+
+                      g_array_append_val (cl->matches, cm);
+                    }
+                }
+              while (g_match_info_next (msg_match, NULL));
+
+              g_clear_pointer (&msg_match, g_match_info_free);
+            }
+        }
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+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;
+
+  clear_line (&self->prev_line);
+
+  g_clear_object (&self->directory);
+  g_clear_pointer (&self->index, index_free);
+  g_clear_pointer (&self->query, g_free);
+  g_clear_pointer (&self->toggled, g_hash_table_unref);
+  g_clear_pointer (&self->message_regex, g_regex_unref);
+
+  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);
+
+  line_regex = g_regex_new ("([a-zA-Z0-9\\+\\-\\.\\/_]+):(\\d+):(.*)", 0, 0, NULL);
+  g_assert (line_regex != NULL);
+}
+
+static void
+gbp_grep_model_init (GbpGrepModel *self)
+{
+  self->mode = MODE_ALL;
+  self->toggled = g_hash_table_new (NULL, NULL);
+}
+
+static void
+gbp_grep_model_clear_regex (GbpGrepModel *self)
+{
+  g_assert (GBP_IS_GREP_MODEL (self));
+
+  g_clear_pointer (&self->message_regex, g_regex_unref);
+}
+
+static gboolean
+gbp_grep_model_rebuild_regex (GbpGrepModel *self)
+{
+  GRegexCompileFlags compile_flags = G_REGEX_OPTIMIZE;
+  g_autoptr(GRegex) regex = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *escaped = NULL;
+  const gchar *query;
+
+  g_assert (GBP_IS_GREP_MODEL (self));
+  g_assert (self->message_regex == NULL);
+
+  if (self->use_regex)
+    query = self->query;
+  else
+    query = escaped = g_regex_escape_string (self->query, -1);
+
+  if (!self->case_sensitive)
+    compile_flags |= G_REGEX_CASELESS;
+
+  if (!(regex = g_regex_new (query, compile_flags, 0, &error)))
+    {
+      g_warning ("Failed to compile regex for match: %s", error->message);
+      return FALSE;
+    }
+
+  self->message_regex = g_steal_pointer (&regex);
+
+  return TRUE;
+}
+
+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);
+      gbp_grep_model_clear_regex (self);
+      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;
+      gbp_grep_model_clear_regex (self);
+      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;
+      gbp_grep_model_clear_regex (self);
+      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;
+      gbp_grep_model_clear_regex (self);
+      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);
+
+  /*
+   * 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}$");
+    }
+
+  if (g_file_test (path, G_FILE_TEST_IS_DIR))
+    {
+      ide_subprocess_launcher_set_cwd (launcher, path);
+      self->was_directory = TRUE;
+    }
+  else
+    {
+      g_autofree gchar *parent = g_path_get_dirname (path);
+      g_autofree gchar *name = g_path_get_basename (path);
+
+      self->was_directory = FALSE;
+
+      ide_subprocess_launcher_set_cwd (launcher, parent);
+      ide_subprocess_launcher_push_argv (launcher, name);
+    }
+
+  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;
+}
+
+void
+gbp_grep_model_select_all (GbpGrepModel *self)
+{
+  g_return_if_fail (GBP_IS_GREP_MODEL (self));
+
+  self->mode = MODE_ALL;
+  g_hash_table_remove_all (self->toggled);
+}
+
+void
+gbp_grep_model_select_none (GbpGrepModel *self)
+{
+  g_return_if_fail (GBP_IS_GREP_MODEL (self));
+
+  self->mode = MODE_NONE;
+  g_hash_table_remove_all (self->toggled);
+}
+
+void
+gbp_grep_model_toggle_row (GbpGrepModel *self,
+                           GtkTreeIter  *iter)
+{
+  g_return_if_fail (GBP_IS_GREP_MODEL (self));
+  g_return_if_fail (iter != NULL);
+
+  if (g_hash_table_contains (self->toggled, iter->user_data))
+    g_hash_table_remove (self->toggled, iter->user_data);
+  else
+    g_hash_table_add (self->toggled, iter->user_data);
+}
+
+void
+gbp_grep_model_toggle_mode (GbpGrepModel *self)
+{
+  g_return_if_fail (GBP_IS_GREP_MODEL (self));
+
+  if (self->mode == MODE_ALL)
+    gbp_grep_model_select_none (self);
+  else
+    gbp_grep_model_select_all (self);
+}
+
+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)
+    {
+      gboolean b = self->mode;
+      if (g_hash_table_contains (self->toggled, iter->user_data))
+        b = !b;
+      g_value_init (value, G_TYPE_BOOLEAN);
+      g_value_set_boolean (value, b);
+    }
+}
+
+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;
+}
+
+static void
+gbp_grep_model_foreach_selected (GbpGrepModel *self,
+                                 void        (*callback) (GbpGrepModel *self, guint index_, gpointer 
user_data),
+                                 gpointer      user_data)
+{
+  g_assert (GBP_IS_GREP_MODEL (self));
+  g_assert (callback != NULL);
+
+  if (self->index == NULL)
+    return;
+
+  if (self->mode == MODE_NONE)
+    {
+      GHashTableIter iter;
+      gpointer key;
+
+      g_hash_table_iter_init (&iter, self->toggled);
+      while (g_hash_table_iter_next (&iter, &key, NULL))
+        callback (self, GPOINTER_TO_UINT (key), user_data);
+    }
+  else if (self->mode == MODE_ALL)
+    {
+      for (guint i = 0; i < self->index->rows->len; i++)
+        {
+          if (!g_hash_table_contains (self->toggled, GINT_TO_POINTER (i)))
+            callback (self, i, user_data);
+        }
+    }
+  else
+    g_assert_not_reached ();
+}
+
+static void
+create_edits_cb (GbpGrepModel *self,
+                 guint         index_,
+                 gpointer      user_data)
+{
+  GPtrArray *edits = user_data;
+  GbpGrepModelLine line = {0};
+  const gchar *row;
+
+  g_assert (GBP_IS_GREP_MODEL (self));
+  g_assert (self->message_regex != NULL);
+  g_assert (edits != NULL);
+
+  row = g_ptr_array_index (self->index->rows, index_);
+
+  if (gbp_grep_model_line_parse (&line, row, self->message_regex))
+    {
+      g_autoptr(IdeFile) file = NULL;
+      g_autoptr(GFile) gfile = NULL;
+      IdeContext *context;
+      guint lineno;
+
+      context = ide_object_get_context (IDE_OBJECT (self));
+      g_assert (IDE_IS_CONTEXT (context));
+
+      gfile = gbp_grep_model_get_file (self, line.path);
+      g_assert (G_IS_FILE (gfile));
+
+      file = ide_file_new (context, gfile);
+      g_assert (IDE_IS_FILE (file));
+
+      lineno = line.line ? line.line - 1 : 0;
+
+      for (guint i = 0; i < line.matches->len; i++)
+        {
+          const GbpGrepModelMatch *match = &g_array_index (line.matches, GbpGrepModelMatch, i);
+          g_autoptr(IdeProjectEdit) edit = NULL;
+          g_autoptr(IdeSourceRange) range = NULL;
+          g_autoptr(IdeSourceLocation) begin = NULL;
+          g_autoptr(IdeSourceLocation) end = NULL;
+
+          begin = ide_source_location_new (file, lineno, match->match_begin, 0);
+          end = ide_source_location_new (file, lineno, match->match_end, 0);
+          range = ide_source_range_new (begin, end);
+
+          edit = g_object_new (IDE_TYPE_PROJECT_EDIT,
+                               "range", range,
+                               NULL);
+
+          g_ptr_array_add (edits, g_steal_pointer (&edit));
+        }
+    }
+
+  clear_line (&line);
+}
+
+/**
+ * gbp_grep_model_create_edits:
+ * @self: a #GbpGrepModel
+ *
+ * Returns: (transfer container): a #GPtrArray of IdeProjectEdit
+ */
+GPtrArray *
+gbp_grep_model_create_edits (GbpGrepModel *self)
+{
+  g_autoptr(GPtrArray) edits = NULL;
+
+  g_return_val_if_fail (GBP_IS_GREP_MODEL (self), NULL);
+
+  if (self->message_regex == NULL)
+    {
+      if (!gbp_grep_model_rebuild_regex (self))
+        return NULL;
+    }
+
+  edits = g_ptr_array_new_with_free_func (g_object_unref);
+  gbp_grep_model_foreach_selected (self, create_edits_cb, edits);
+
+  return g_steal_pointer (&edits);
+}
+
+/**
+ * gbp_grep_model_get_line:
+ * @self: a #GbpGrepModel
+ * @iter: a #GtkTextIter
+ * @line: (out): a location for the line info
+ *
+ * Gets information about the line that @iter points at.
+ */
+void
+gbp_grep_model_get_line (GbpGrepModel            *self,
+                         GtkTreeIter             *iter,
+                         const GbpGrepModelLine **line)
+{
+  const gchar *str;
+  guint index_;
+
+  g_return_if_fail (GBP_IS_GREP_MODEL (self));
+  g_return_if_fail (iter != NULL);
+  g_return_if_fail (line != NULL);
+  g_return_if_fail (self->index != NULL);
+  g_return_if_fail (self->index->rows != NULL);
+
+  *line = NULL;
+
+  index_ = GPOINTER_TO_UINT (iter->user_data);
+  g_return_if_fail (index_ < self->index->rows->len);
+
+  str = g_ptr_array_index (self->index->rows, index_);
+
+  if (str != self->prev_line.start_of_line)
+    {
+      clear_line (&self->prev_line);
+
+      if (self->message_regex == NULL)
+        {
+          if (!gbp_grep_model_rebuild_regex (self))
+            return;
+        }
+
+      gbp_grep_model_line_parse (&self->prev_line, str, self->message_regex);
+    }
+
+  *line = &self->prev_line;
+}
+
+/**
+ * gbp_grep_model_get_file:
+ *
+ * Returns: (transfer full): a #GFile
+ */
+GFile *
+gbp_grep_model_get_file (GbpGrepModel *self,
+                         const gchar  *path)
+{
+  g_return_val_if_fail (GBP_IS_GREP_MODEL (self), NULL);
+
+  if (!path || !*path || g_strcmp0 (path, ".") == 0)
+    return g_file_dup (self->directory);
+
+  if (self->was_directory)
+    return g_file_get_child (self->directory, path);
+  else
+    return g_file_dup (self->directory);
+}
diff --git a/src/plugins/grep/gbp-grep-model.h b/src/plugins/grep/gbp-grep-model.h
new file mode 100644
index 000000000..de81a8885
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-model.h
@@ -0,0 +1,86 @@
+/* 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())
+
+typedef struct
+{
+  gint match_begin;
+  gint match_end;
+  gint match_begin_bytes;
+  gint match_end_bytes;
+} GbpGrepModelMatch;
+
+typedef struct
+{
+  const gchar *start_of_line;
+  const gchar *start_of_message;
+  gchar       *path;
+  GArray      *matches;
+  guint        line;
+} GbpGrepModelLine;
+
+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);
+GPtrArray    *gbp_grep_model_create_edits           (GbpGrepModel            *self);
+void          gbp_grep_model_select_all             (GbpGrepModel            *self);
+void          gbp_grep_model_select_none            (GbpGrepModel            *self);
+void          gbp_grep_model_toggle_mode            (GbpGrepModel            *self);
+void          gbp_grep_model_toggle_row             (GbpGrepModel            *self,
+                                                     GtkTreeIter             *iter);
+void          gbp_grep_model_get_line               (GbpGrepModel            *self,
+                                                     GtkTreeIter             *iter,
+                                                     const GbpGrepModelLine **line);
+GFile        *gbp_grep_model_get_file               (GbpGrepModel            *self,
+                                                     const gchar             *path);
+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..2f41ed33e
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-panel.c
@@ -0,0 +1,533 @@
+/* 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;
+
+  /* Unowned references */
+  GtkTreeView       *tree_view;
+  GtkTreeViewColumn *toggle_column;
+  GtkCheckButton    *check;
+  GtkButton         *close_button;
+  GtkButton         *replace_button;
+  GtkEntry          *replace_entry;
+  GtkSpinner        *spinner;
+};
+
+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)
+{
+  const GbpGrepModelLine *line = NULL;
+  PangoAttrList *attrs = NULL;
+  const gchar *begin = NULL;
+
+  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);
+
+  gbp_grep_model_get_line (GBP_GREP_MODEL (model), iter, &line);
+
+  if G_LIKELY (line != NULL)
+    {
+      goffset adjust;
+
+      /* Skip to the beginning of the text */
+      begin = line->start_of_message;
+      while (*begin && g_unichar_isspace (g_utf8_get_char (begin)))
+        begin = g_utf8_next_char (begin);
+
+      /*
+       * If any of our matches are for space, we can't skip past the starting
+       * space or we will fail to highlight properly.
+       */
+      adjust = begin - line->start_of_message;
+      for (guint i = 0; i < line->matches->len; i++)
+        {
+          const GbpGrepModelMatch *match = &g_array_index (line->matches, GbpGrepModelMatch, i);
+
+          if (match->match_begin < adjust)
+            {
+              begin = line->start_of_message;
+              adjust = 0;
+              break;
+            }
+        }
+
+      /* Now create pango attributes to draw around the matched text so that
+       * the user knows exactly where the match is. We need to adjust for what
+       * we chomped off the beginning of the visible message.
+       */
+      attrs = pango_attr_list_new ();
+      for (guint i = 0; i < line->matches->len; i++)
+        {
+          const GbpGrepModelMatch *match = &g_array_index (line->matches, GbpGrepModelMatch, i);
+          PangoAttribute *bg_attr = pango_attr_background_new (64764, 59881, 20303);
+          PangoAttribute *alpha_attr = pango_attr_background_alpha_new (32767);
+          gint start_index = match->match_begin_bytes - adjust;
+          gint end_index = match->match_end_bytes - adjust;
+
+          bg_attr->start_index = start_index;
+          bg_attr->end_index = end_index;
+
+          alpha_attr->start_index = start_index;
+          alpha_attr->end_index = end_index;
+
+          pango_attr_list_insert (attrs, g_steal_pointer (&bg_attr));
+          pango_attr_list_insert (attrs, g_steal_pointer (&alpha_attr));
+        }
+    }
+
+  g_object_set (cell,
+                "attributes", attrs,
+                "text", begin,
+                NULL);
+
+  g_clear_pointer (&attrs, pango_attr_list_unref);
+}
+
+static void
+path_data_func (GtkCellLayout   *layout,
+                GtkCellRenderer *cell,
+                GtkTreeModel    *model,
+                GtkTreeIter     *iter,
+                gpointer         user_data)
+{
+  const GbpGrepModelLine *line = NULL;
+
+  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);
+
+  gbp_grep_model_get_line (GBP_GREP_MODEL (model), iter, &line);
+
+  if G_LIKELY (line != NULL)
+    {
+      const gchar *slash = strrchr (line->path, G_DIR_SEPARATOR);
+
+      if (slash != NULL)
+        {
+          g_autofree gchar *path = g_strndup (line->path, slash - line->path);
+          g_object_set (cell, "text", path, NULL);
+          return;
+        }
+    }
+
+  g_object_set (cell, "text", ".", NULL);
+}
+
+static void
+filename_data_func (GtkCellLayout   *layout,
+                    GtkCellRenderer *cell,
+                    GtkTreeModel    *model,
+                    GtkTreeIter     *iter,
+                    gpointer         user_data)
+{
+  const GbpGrepModelLine *line = NULL;
+
+  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);
+
+  gbp_grep_model_get_line (GBP_GREP_MODEL (model), iter, &line);
+
+  if G_LIKELY (line != NULL)
+    {
+      const gchar *slash = strrchr (line->path, G_DIR_SEPARATOR);
+
+      if (slash != NULL)
+        g_object_set (cell, "text", slash + 1, NULL);
+      else
+        g_object_set (cell, "text", line->path, NULL);
+
+      return;
+    }
+
+  g_object_set (cell, "text", NULL, NULL);
+}
+
+static void
+gbp_grep_panel_row_activated_cb (GbpGrepPanel      *self,
+                                 GtkTreePath       *path,
+                                 GtkTreeViewColumn *column,
+                                 GtkTreeView       *tree_view)
+{
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (GBP_IS_GREP_PANEL (self));
+  g_assert (path != NULL);
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (column));
+  g_assert (GTK_IS_TREE_VIEW (tree_view));
+
+  /* Ignore if this is the toggle checkbox column */
+  if (column == self->toggle_column)
+    return;
+
+  if ((model = gtk_tree_view_get_model (tree_view)) &&
+      gtk_tree_model_get_iter (model, &iter, path))
+    {
+      const GbpGrepModelLine *line = NULL;
+
+      gbp_grep_model_get_line (GBP_GREP_MODEL (model), &iter, &line);
+
+      if G_LIKELY (line != NULL)
+        {
+          g_autoptr(IdeSourceLocation) location = NULL;
+          g_autoptr(GFile) child = NULL;
+          g_autoptr(IdeFile) ichild = NULL;
+          IdePerspective *editor;
+          IdeWorkbench *workbench;
+          IdeContext *context;
+          guint lineno = line->line;
+
+          workbench = ide_widget_get_workbench (GTK_WIDGET (self));
+          context = ide_workbench_get_context (workbench);
+          editor = ide_workbench_get_perspective_by_name (workbench, "editor");
+
+          if (lineno > 0)
+            lineno--;
+
+          child = gbp_grep_model_get_file (GBP_GREP_MODEL (model), line->path);
+          ichild = ide_file_new (context, child);
+          location = ide_source_location_new (ichild, lineno, 0, 0);
+
+          ide_editor_perspective_focus_location (IDE_EDITOR_PERSPECTIVE (editor), location);
+        }
+    }
+}
+
+static void
+gbp_grep_panel_row_toggled_cb (GbpGrepPanel          *self,
+                               const gchar           *pathstr,
+                               GtkCellRendererToggle *toggle)
+{
+  GtkTreeModel *model;
+  GtkTreePath *path;
+  GtkTreeIter iter;
+
+  g_assert (GBP_IS_GREP_PANEL (self));
+  g_assert (pathstr != NULL);
+  g_assert (GTK_IS_CELL_RENDERER_TOGGLE (toggle));
+
+  path = gtk_tree_path_new_from_string (pathstr);
+  model = gtk_tree_view_get_model (self->tree_view);
+
+  if (gtk_tree_model_get_iter (model, &iter, path))
+    {
+      gbp_grep_model_toggle_row (GBP_GREP_MODEL (model), &iter);
+      gtk_widget_queue_resize (GTK_WIDGET (self->tree_view));
+    }
+
+  g_clear_pointer (&path, gtk_tree_path_free);
+}
+
+static void
+gbp_grep_panel_toggle_all_cb (GbpGrepPanel      *self,
+                              GtkTreeViewColumn *column)
+{
+  GtkToggleButton *toggle;
+  GtkTreeModel *model;
+
+  g_assert (GBP_IS_GREP_PANEL (self));
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (column));
+
+  toggle = GTK_TOGGLE_BUTTON (self->check);
+  gtk_toggle_button_set_active (toggle, !gtk_toggle_button_get_active (toggle));
+
+  model = gtk_tree_view_get_model (self->tree_view);
+  gbp_grep_model_toggle_mode (GBP_GREP_MODEL (model));
+  gtk_widget_queue_resize (GTK_WIDGET (self->tree_view));
+}
+
+static void
+gbp_grep_panel_replace_edited_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  IdeBufferManager *bufmgr = (IdeBufferManager *)object;
+  g_autoptr(GbpGrepPanel) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_BUFFER_MANAGER (bufmgr));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  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;
+    }
+
+  /* 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));
+}
+
+static void
+gbp_grep_panel_replace_clicked_cb (GbpGrepPanel *self,
+                                   GtkButton    *button)
+{
+  g_autoptr(GPtrArray) edits = NULL;
+  IdeBufferManager *bufmgr;
+  const gchar *text;
+  IdeContext *context;
+
+  g_assert (GBP_IS_GREP_PANEL (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  edits = gbp_grep_model_create_edits (GBP_GREP_MODEL (gtk_tree_view_get_model (self->tree_view)));
+  if (edits == NULL || edits->len == 0)
+    return;
+
+  text = gtk_entry_get_text (self->replace_entry);
+
+  for (guint i = 0; i < edits->len; i++)
+    {
+      IdeProjectEdit *edit = g_ptr_array_index (edits, i);
+      ide_project_edit_set_replacement (edit, text);
+    }
+
+  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_spinner_start (self->spinner);
+
+  context = ide_widget_get_context (GTK_WIDGET (self));
+  bufmgr = ide_context_get_buffer_manager (context);
+
+  ide_buffer_manager_apply_edits_async (bufmgr,
+                                        IDE_PTR_ARRAY_STEAL_FULL (&edits),
+                                        NULL,
+                                        gbp_grep_panel_replace_edited_cb,
+                                        g_object_ref (self));
+}
+
+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, close_button);
+  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);
+}
+
+static void
+gbp_grep_panel_init (GbpGrepPanel *self)
+{
+  GtkTreeViewColumn *column;
+  GtkCellRenderer *cell;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (self->close_button,
+                           "clicked",
+                           G_CALLBACK (gtk_widget_destroy),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->replace_button,
+                           "clicked",
+                           G_CALLBACK (gbp_grep_panel_replace_clicked_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->tree_view,
+                           "row-activated",
+                           G_CALLBACK (gbp_grep_panel_row_activated_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  self->check = g_object_new (GTK_TYPE_CHECK_BUTTON,
+                              "margin-bottom", 3,
+                              "margin-end", 6,
+                              "margin-start", 6,
+                              "margin-top", 3,
+                              "visible", TRUE,
+                              "active", TRUE,
+                              NULL);
+  self->toggle_column = g_object_new (GTK_TYPE_TREE_VIEW_COLUMN,
+                                      "visible", TRUE,
+                                      "clickable", TRUE,
+                                      "widget", self->check,
+                                      NULL);
+  g_signal_connect_object (self->toggle_column,
+                           "clicked",
+                           G_CALLBACK (gbp_grep_panel_toggle_all_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  cell = g_object_new (GTK_TYPE_CELL_RENDERER_TOGGLE,
+                       "activatable", TRUE,
+                       NULL);
+  g_signal_connect_object (cell,
+                           "toggled",
+                           G_CALLBACK (gbp_grep_panel_row_toggled_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (self->toggle_column), cell, TRUE);
+  gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (self->toggle_column), cell, "active", 1);
+  gtk_tree_view_column_set_expand (self->toggle_column, FALSE);
+  gtk_tree_view_append_column (self->tree_view, self->toggle_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..c8f92de59
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-panel.ui
@@ -0,0 +1,102 @@
+<?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>
+              <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="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" id="replace_entry">
+                    <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..5d7bcf13c
--- /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-project-tree-addin.h"
+
+void
+gbp_grep_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_PROJECT_TREE_ADDIN,
+                                              GBP_TYPE_GREP_PROJECT_TREE_ADDIN);
+}
diff --git a/src/plugins/grep/gbp-grep-popover.c b/src/plugins/grep/gbp-grep-popover.c
new file mode 100644
index 000000000..56f61fa65
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-popover.c
@@ -0,0 +1,241 @@
+/* gbp-grep-popover.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-popover"
+
+#include <ide.h>
+
+#include "gbp-grep-model.h"
+#include "gbp-grep-panel.h"
+#include "gbp-grep-popover.h"
+
+struct _GbpGrepPopover
+{
+  GtkPopover parent_instance;
+
+  GFile *file;
+
+  GtkEntry       *entry;
+  GtkButton      *button;
+  GtkCheckButton *regex_button;
+  GtkCheckButton *whole_button;
+  GtkCheckButton *case_button;
+  GtkCheckButton *recursive_button;
+};
+
+enum {
+  PROP_0,
+  PROP_FILE,
+  PROP_IS_DIRECTORY,
+  N_PROPS
+};
+
+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))
+    ide_widget_warning (GTK_WIDGET (panel), "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)
+{
+  g_autoptr(GbpGrepModel) model = NULL;
+  IdePerspective *editor;
+  IdeWorkbench *workbench;
+  IdeContext *context;
+  GtkWidget *panel;
+  GtkWidget *utils;
+  gboolean use_regex;
+  gboolean at_word_boundaries;
+  gboolean case_sensitive;
+  gboolean recursive;
+
+  g_assert (GBP_IS_GREP_POPOVER (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  workbench = ide_widget_get_workbench (GTK_WIDGET (self));
+  editor = ide_workbench_get_perspective_by_name (workbench, "editor");
+  utils = ide_editor_perspective_get_utilities (IDE_EDITOR_PERSPECTIVE (editor));
+  context = ide_workbench_get_context (workbench);
+
+  use_regex = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->regex_button));
+  at_word_boundaries = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->whole_button));
+  case_sensitive = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->case_button));
+  recursive = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->recursive_button));
+
+  model = gbp_grep_model_new (context);
+  gbp_grep_model_set_directory (model, self->file);
+  gbp_grep_model_set_use_regex (model, use_regex);
+  gbp_grep_model_set_at_word_boundaries (model, at_word_boundaries);
+  gbp_grep_model_set_case_sensitive (model, case_sensitive);
+  gbp_grep_model_set_query (model, gtk_entry_get_text (self->entry));
+
+  if (gtk_widget_get_visible (GTK_WIDGET (self->recursive_button)))
+    gbp_grep_model_set_recursive (model, recursive);
+  else
+    gbp_grep_model_set_recursive (model, FALSE);
+
+  panel = gbp_grep_panel_new ();
+  gtk_container_add (GTK_CONTAINER (utils), panel);
+  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));
+}
+
+static void
+gbp_grep_popover_entry_activate_cb (GbpGrepPopover *self,
+                                    GtkEntry       *entry)
+{
+  g_assert (GBP_IS_GREP_POPOVER (self));
+  g_assert (GTK_IS_ENTRY (entry));
+
+  gtk_widget_activate (GTK_WIDGET (self->button));
+}
+
+static void
+gbp_grep_popover_finalize (GObject *object)
+{
+  GbpGrepPopover *self = (GbpGrepPopover *)object;
+
+  g_clear_object (&self->file);
+
+  G_OBJECT_CLASS (gbp_grep_popover_parent_class)->finalize (object);
+}
+
+static void
+gbp_grep_popover_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  GbpGrepPopover *self = GBP_GREP_POPOVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_FILE:
+      g_value_set_object (value, self->file);
+      break;
+
+    case PROP_IS_DIRECTORY:
+      g_value_set_boolean (value, gtk_widget_get_visible (GTK_WIDGET (self->recursive_button)));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_grep_popover_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  GbpGrepPopover *self = GBP_GREP_POPOVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_FILE:
+      g_set_object (&self->file, g_value_get_object (value));
+      break;
+
+    case PROP_IS_DIRECTORY:
+      gtk_widget_set_visible (GTK_WIDGET (self->recursive_button), g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_grep_popover_class_init (GbpGrepPopoverClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = gbp_grep_popover_finalize;
+  object_class->get_property = gbp_grep_popover_get_property;
+  object_class->set_property = gbp_grep_popover_set_property;
+
+  properties [PROP_FILE] =
+    g_param_spec_object ("file", NULL, NULL,
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_IS_DIRECTORY] =
+    g_param_spec_boolean ("is-directory", NULL, NULL, FALSE,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/plugins/grep/gbp-grep-popover.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPopover, button);
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPopover, entry);
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPopover, regex_button);
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPopover, whole_button);
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPopover, case_button);
+  gtk_widget_class_bind_template_child (widget_class, GbpGrepPopover, recursive_button);
+}
+
+static void
+gbp_grep_popover_init (GbpGrepPopover *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (self->entry,
+                           "activate",
+                           G_CALLBACK (gbp_grep_popover_entry_activate_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->button,
+                           "clicked",
+                           G_CALLBACK (gbp_grep_popover_button_clicked_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
diff --git a/src/plugins/grep/gbp-grep-popover.h b/src/plugins/grep/gbp-grep-popover.h
new file mode 100644
index 000000000..4f569dea1
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-popover.h
@@ -0,0 +1,31 @@
+/* gbp-grep-popover.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_POPOVER (gbp_grep_popover_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGrepPopover, gbp_grep_popover, GBP, GREP_POPOVER, GtkPopover)
+
+G_END_DECLS
diff --git a/src/plugins/grep/gbp-grep-popover.ui b/src/plugins/grep/gbp-grep-popover.ui
new file mode 100644
index 000000000..1e1cab7e3
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-popover.ui
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpGrepPopover" parent="GtkPopover">
+    <property name="border-width">12</property>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="spacing">6</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Find in Files</property>
+            <property name="visible">true</property>
+            <property name="xalign">0.0</property>
+            <attributes>
+              <attribute name="weight" value="bold"/>
+            </attributes>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">horizontal</property>
+            <property name="spacing">6</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkEntry" id="entry">
+                <property name="placeholder-text" translatable="yes">Search for…</property>
+                <property name="width-chars">24</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="button">
+                <property name="hexpand">false</property>
+                <property name="halign">end</property>
+                <property name="label" translatable="yes">Find</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="suggested-action"/>
+                </style>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="margin-top">6</property>
+            <property name="orientation">vertical</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkCheckButton" id="recursive_button">
+                <property name="active">true</property>
+                <property name="label" translatable="yes">Search _recursively through folders</property>
+                <property name="use-underline">true</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkCheckButton" id="case_button">
+                <property name="label" translatable="yes">Match _case when searching</property>
+                <property name="active">true</property>
+                <property name="use-underline">true</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkCheckButton" id="whole_button">
+                <property name="label" translatable="yes">Match _whole words</property>
+                <property name="use-underline">true</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkCheckButton" id="regex_button">
+                <property name="label" translatable="yes">Allow regular _expressions</property>
+                <property name="use-underline">true</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/grep/gbp-grep-project-tree-addin.c b/src/plugins/grep/gbp-grep-project-tree-addin.c
new file mode 100644
index 000000000..465b56f49
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-project-tree-addin.c
@@ -0,0 +1,203 @@
+/* gbp-grep-project-tree-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-project-tree-addin"
+
+#include "gbp-grep-popover.h"
+#include "gbp-grep-project-tree-addin.h"
+
+/* This crosses the plugin boundary, but it is easier for now
+ * until we get some of the project tree stuff moved into a
+ * libide-project library (or similar).
+ */
+#include "../project-tree/gb-project-file.h"
+
+struct _GbpGrepProjectTreeAddin
+{
+  GObject         parent_instance;
+  DzlTree        *tree;
+  DzlTreeBuilder *builder;
+};
+
+static GFile *
+get_file_for_node (DzlTreeNode *node,
+                   gboolean    *is_dir)
+{
+  GObject *item;
+
+  g_return_val_if_fail (!node || DZL_IS_TREE_NODE (node), NULL);
+
+  if (is_dir)
+    *is_dir = FALSE;
+
+  if (node == NULL)
+    return NULL;
+
+  if (!(item = dzl_tree_node_get_item (node)))
+    return NULL;
+
+  if (GB_IS_PROJECT_FILE (item))
+    {
+      if (is_dir)
+        *is_dir = gb_project_file_get_is_directory (GB_PROJECT_FILE (item));
+      return gb_project_file_get_file (GB_PROJECT_FILE (item));
+    }
+
+  return NULL;
+}
+
+static void
+popover_closed_cb (GtkPopover *popover)
+{
+  IdeWorkbench *workbench;
+
+  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.
+   */
+
+  workbench = ide_widget_get_workbench (GTK_WIDGET (popover));
+  gtk_window_set_focus (GTK_WINDOW (workbench), NULL);
+  gtk_widget_destroy (GTK_WIDGET (popover));
+}
+
+static void
+find_in_files_action (GSimpleAction *action,
+                      GVariant      *param,
+                      gpointer       user_data)
+{
+  GbpGrepProjectTreeAddin *self = user_data;
+  DzlTreeNode *node;
+  GFile *file;
+  gboolean is_dir = FALSE;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_GREP_PROJECT_TREE_ADDIN (self));
+
+  if ((node = dzl_tree_get_selected (self->tree)) &&
+      (file = get_file_for_node (node, &is_dir)))
+    {
+      GtkPopover *popover;
+
+      popover = g_object_new (GBP_TYPE_GREP_POPOVER,
+                              "file", file,
+                              "is-directory", is_dir,
+                              "position", GTK_POS_RIGHT,
+                              NULL);
+      g_signal_connect_after (popover,
+                              "closed",
+                              G_CALLBACK (popover_closed_cb),
+                              NULL);
+      dzl_tree_node_show_popover (node, popover);
+    }
+}
+
+static void
+on_node_selected_cb (GbpGrepProjectTreeAddin *self,
+                     DzlTreeNode             *node,
+                     DzlTreeBuilder          *builder)
+{
+  GFile *file;
+  gboolean is_dir = FALSE;
+
+  g_assert (GBP_IS_GREP_PROJECT_TREE_ADDIN (self));
+  g_assert (!node || DZL_IS_TREE_NODE (node));
+  g_assert (DZL_IS_TREE_BUILDER (builder));
+
+  file = get_file_for_node (node, &is_dir);
+
+  dzl_gtk_widget_action_set (GTK_WIDGET (self->tree), "grep", "find-in-files",
+                             "enabled", (file != NULL),
+                             NULL);
+}
+
+static void
+gbp_grep_project_tree_addin_load (IdeProjectTreeAddin *addin,
+                                  DzlTree             *tree)
+{
+  GbpGrepProjectTreeAddin *self = (GbpGrepProjectTreeAddin *)addin;
+  g_autoptr(GActionMap) group = NULL;
+  static GActionEntry actions[] = {
+    { "find-in-files", find_in_files_action },
+  };
+
+  g_assert (GBP_IS_GREP_PROJECT_TREE_ADDIN (self));
+  g_assert (DZL_IS_TREE (tree));
+  g_assert (self->builder == NULL);
+  g_assert (self->tree == NULL);
+
+  self->tree = tree;
+
+  group = G_ACTION_MAP (g_simple_action_group_new ());
+  g_action_map_add_action_entries (group, actions, G_N_ELEMENTS (actions), self);
+  gtk_widget_insert_action_group (GTK_WIDGET (tree), "grep", G_ACTION_GROUP (group));
+
+  self->builder = g_object_ref_sink (dzl_tree_builder_new ());
+  g_signal_connect_object (self->builder,
+                           "node-selected",
+                           G_CALLBACK (on_node_selected_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  dzl_tree_add_builder (tree, self->builder);
+}
+
+static void
+gbp_grep_project_tree_addin_unload (IdeProjectTreeAddin *addin,
+                                    DzlTree             *tree)
+{
+  GbpGrepProjectTreeAddin *self = (GbpGrepProjectTreeAddin *)addin;
+
+  g_assert (GBP_IS_GREP_PROJECT_TREE_ADDIN (self));
+  g_assert (DZL_IS_TREE (tree));
+  g_assert (self->builder != NULL);
+  g_assert (self->tree == tree);
+
+  gtk_widget_insert_action_group (GTK_WIDGET (tree), "grep", NULL);
+  dzl_tree_remove_builder (tree, self->builder);
+  g_clear_object (&self->builder);
+
+  self->tree = NULL;
+}
+
+static void
+project_tree_addin_iface_init (IdeProjectTreeAddinInterface *iface)
+{
+  iface->load = gbp_grep_project_tree_addin_load;
+  iface->unload = gbp_grep_project_tree_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpGrepProjectTreeAddin, gbp_grep_project_tree_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_PROJECT_TREE_ADDIN,
+                                                project_tree_addin_iface_init))
+
+static void
+gbp_grep_project_tree_addin_class_init (GbpGrepProjectTreeAddinClass *klass)
+{
+}
+
+static void
+gbp_grep_project_tree_addin_init (GbpGrepProjectTreeAddin *self)
+{
+}
diff --git a/src/plugins/grep/gbp-grep-project-tree-addin.h b/src/plugins/grep/gbp-grep-project-tree-addin.h
new file mode 100644
index 000000000..19aa66a1a
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-project-tree-addin.h
@@ -0,0 +1,31 @@
+/* gbp-grep-project-tree-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_PROJECT_TREE_ADDIN (gbp_grep_project_tree_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGrepProjectTreeAddin, gbp_grep_project_tree_addin, GBP, GREP_PROJECT_TREE_ADDIN, 
GObject)
+
+G_END_DECLS
diff --git a/src/plugins/grep/grep.gresource.xml b/src/plugins/grep/grep.gresource.xml
new file mode 100644
index 000000000..ead09764c
--- /dev/null
+++ b/src/plugins/grep/grep.gresource.xml
@@ -0,0 +1,14 @@
+<?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-panel.ui</file>
+    <file preprocess="xml-stripblanks">gbp-grep-popover.ui</file>
+    <file preprocess="xml-stripblanks">gtk/menus.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..8812d8ab0
--- /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;project-tree-plugin
+Embedded=gbp_grep_register_types
diff --git a/src/plugins/grep/gtk/menus.ui b/src/plugins/grep/gtk/menus.ui
new file mode 100644
index 000000000..aef127545
--- /dev/null
+++ b/src/plugins/grep/gtk/menus.ui
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<interface>
+  <menu id="gb-project-tree-popup-menu">
+    <section id="gb-project-tree-find-section">
+      <item>
+        <attribute name="label" translatable="yes">Find in Files</attribute>
+        <attribute name="action">grep.find-in-files</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/src/plugins/grep/meson.build b/src/plugins/grep/meson.build
new file mode 100644
index 000000000..8a921fe25
--- /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-model.c',
+  'gbp-grep-panel.c',
+  'gbp-grep-plugin.c',
+  'gbp-grep-popover.c',
+  'gbp-grep-project-tree-addin.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')),
diff --git a/src/plugins/project-tree/gtk/menus.ui b/src/plugins/project-tree/gtk/menus.ui
index ae30ee1c0..903289782 100644
--- a/src/plugins/project-tree/gtk/menus.ui
+++ b/src/plugins/project-tree/gtk/menus.ui
@@ -49,6 +49,7 @@
         <attribute name="action">project-tree.open-in-terminal</attribute>
       </item>
     </section>
+    <section id="gb-project-tree-find-section"/>
     <section id="gb-project-tree-rename-section">
       <item>
         <attribute name="label" translatable="yes">_Rename</attribute>



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