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