[gnome-builder] ctags: perform more incremental ctags updates



commit 04204e93dd221cab8e88a40399927fc753126383
Author: Christian Hergert <chergert redhat com>
Date:   Sun Mar 19 03:55:22 2017 -0700

    ctags: perform more incremental ctags updates
    
    This changes how we do ctags indexes to be a bit more friendly on larger
    codebases. Now, we do a single index pass at startup of the project to
    update all our ctags indexes. Then, when a file is saved, we only reindex
    that directory, ignoring the other directories.
    
    When reloading the indexes, we only pop the cache if the mtime is newer
    so that old indexes shouldn't get reloaded/replaced.
    
    In practice, if i save a file like src/foo.c, only src/tags will get
    reloaded. Note that src/tags is actually based on the tags directory
    in ~/.cache/gnome-builder/tags/$project_id, not the actual source tree.
    
    I think there is a bunch more we can still improve, but this should make
    things a bit better.

 plugins/ctags/ctags-plugin.c      |    1 -
 plugins/ctags/ide-ctags-builder.c |  456 ++++++++++++++++++++-----------------
 plugins/ctags/ide-ctags-builder.h |    7 +-
 plugins/ctags/ide-ctags-index.c   |   10 +-
 plugins/ctags/ide-ctags-index.h   |    1 +
 plugins/ctags/ide-ctags-service.c |  258 +++++++++++++++-------
 6 files changed, 435 insertions(+), 298 deletions(-)
---
diff --git a/plugins/ctags/ctags-plugin.c b/plugins/ctags/ctags-plugin.c
index 803d88d..7cbde47 100644
--- a/plugins/ctags/ctags-plugin.c
+++ b/plugins/ctags/ctags-plugin.c
@@ -39,7 +39,6 @@ void
 peas_register_types (PeasObjectModule *module)
 {
   _ide_ctags_index_register_type (G_TYPE_MODULE (module));
-  _ide_ctags_builder_register_type (G_TYPE_MODULE (module));
   _ide_ctags_completion_item_register_type (G_TYPE_MODULE (module));
   _ide_ctags_completion_provider_register_type (G_TYPE_MODULE (module));
   _ide_ctags_highlighter_register_type (G_TYPE_MODULE (module));
diff --git a/plugins/ctags/ide-ctags-builder.c b/plugins/ctags/ide-ctags-builder.c
index a88eaa6..219c87d 100644
--- a/plugins/ctags/ide-ctags-builder.c
+++ b/plugins/ctags/ide-ctags-builder.c
@@ -1,6 +1,6 @@
 /* ide-ctags-builder.c
  *
- * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ * Copyright (C) 2017 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
@@ -18,308 +18,338 @@
 
 #define G_LOG_DOMAIN "ide-ctags-builder"
 
-#include <egg-counter.h>
-#include <glib/gi18n.h>
-#include <glib/gstdio.h>
-#include <ide.h>
-
 #include "ide-ctags-builder.h"
 
-#define BUILD_CTAGS_DELAY_SECONDS 10
-
-EGG_DEFINE_COUNTER (instances, "IdeCtagsBuilder", "Instances", "Number of IdeCtagsBuilder instances.")
-EGG_DEFINE_COUNTER (parse_count, "IdeCtagsBuilder", "Build Count", "Number of build attempts.");
-
 struct _IdeCtagsBuilder
 {
-  IdeObject  parent_instance;
-
-  GSettings *settings;
-
-  GQuark     ctags_path;
-
-  guint      build_timeout;
-
-  guint      is_building : 1;
+  IdeObject  parent;
 };
 
-enum {
-  TAGS_BUILT,
-  LAST_SIGNAL
-};
+typedef struct
+{
+  GFile *directory;
+  GFile *destination;
+  gchar *ctags;
+  guint  recursive : 1;
+} BuildTaskData;
 
-G_DEFINE_DYNAMIC_TYPE (IdeCtagsBuilder, ide_ctags_builder, IDE_TYPE_OBJECT)
+static void tags_builder_iface_init (IdeTagsBuilderInterface *iface);
 
-static guint signals [LAST_SIGNAL];
+G_DEFINE_TYPE_WITH_CODE (IdeCtagsBuilder, ide_ctags_builder, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_TAGS_BUILDER, tags_builder_iface_init))
 
-IdeCtagsBuilder *
-ide_ctags_builder_new (void)
-{
-  return g_object_new (IDE_TYPE_CTAGS_BUILDER, NULL);
-}
+static GHashTable *ignored;
 
 static void
-ide_ctags_builder_build_cb (GObject      *object,
-                            GAsyncResult *result,
-                            gpointer      user_data)
+build_task_data_free (gpointer data)
 {
-  IdeCtagsBuilder *self = (IdeCtagsBuilder *)object;
-  GTask *task = (GTask *)result;
-  GFile *file;
-  GError *error = NULL;
+  BuildTaskData *task_data = data;
 
-  IDE_ENTRY;
+  g_clear_object (&task_data->directory);
+  g_clear_object (&task_data->destination);
+  g_clear_pointer (&task_data->ctags, g_free);
 
-  g_assert (IDE_IS_CTAGS_BUILDER (self));
-  g_assert (G_IS_TASK (task));
+  g_slice_free (BuildTaskData, task_data);
+}
 
-  if (g_task_propagate_boolean (task, &error))
-    {
-      file = g_task_get_task_data (task);
-      g_assert (G_IS_FILE (file));
-      g_signal_emit (self, signals [TAGS_BUILT], 0, file);
-    }
-  else
-    {
-      g_warning ("%s", error->message);
-      g_clear_error (&error);
-    }
+static void
+ide_ctags_builder_class_init (IdeCtagsBuilderClass *klass)
+{
+  ignored = g_hash_table_new (g_str_hash, g_str_equal);
 
-  self->is_building = FALSE;
+  /* TODO: We need a really fast, *THREAD-SAFE* access to determine
+   *       if files are ignored via the VCS.
+   */
 
-  IDE_EXIT;
+  g_hash_table_insert (ignored, ".git", NULL);
+  g_hash_table_insert (ignored, ".bzr", NULL);
+  g_hash_table_insert (ignored, ".svn", NULL);
+  g_hash_table_insert (ignored, ".flatpak-builder", NULL);
+  g_hash_table_insert (ignored, ".libs", NULL);
+  g_hash_table_insert (ignored, ".deps", NULL);
+  g_hash_table_insert (ignored, "autom4te.cache", NULL);
+  g_hash_table_insert (ignored, "build-aux", NULL);
 }
 
 static void
-ide_ctags_builder_process_wait_cb (GObject      *object,
-                                   GAsyncResult *result,
-                                   gpointer      user_data)
+ide_ctags_builder_init (IdeCtagsBuilder *self)
 {
-  IdeSubprocess *process = (IdeSubprocess *)object;
-  g_autoptr(GTask) task = user_data;
-  GError *error = NULL;
-
-  IDE_ENTRY;
-
-  g_assert (IDE_IS_SUBPROCESS (process));
-  g_assert (G_IS_TASK (task));
+}
 
-  if (!ide_subprocess_wait_finish (process, result, &error))
-    g_task_return_error (task, error);
-  else
-    g_task_return_boolean (task, TRUE);
+IdeTagsBuilder *
+ide_ctags_builder_new (IdeContext *context)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
 
-  IDE_EXIT;
+  return g_object_new (IDE_TYPE_CTAGS_BUILDER,
+                       "context", context,
+                       NULL);
 }
 
-static void
-ide_ctags_builder_build_worker (GTask        *task,
-                                gpointer      source_object,
-                                gpointer      task_data,
-                                GCancellable *cancellable)
+static gboolean
+ide_ctags_builder_build (IdeCtagsBuilder *self,
+                         const gchar     *ctags,
+                         GFile           *directory,
+                         GFile           *destination,
+                         gboolean         recursive,
+                         GCancellable    *cancellable)
 {
-  IdeCtagsBuilder *self = source_object;
-  g_autoptr(GFile) workdir = NULL;
   g_autoptr(IdeSubprocessLauncher) launcher = NULL;
-  g_autoptr(IdeSubprocess) process = NULL;
-  g_autofree gchar *tags_file = NULL;
-  g_autofree gchar *tags_filename = NULL;
-  g_autofree gchar *workpath = NULL;
+  g_autoptr(IdeSubprocess) subprocess = NULL;
+  g_autoptr(GPtrArray) directories = NULL;
+  g_autoptr(GPtrArray) dest_directories = NULL;
+  g_autoptr(GFile) tags_file = NULL;
+  g_autoptr(GFileEnumerator) enumerator = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *cwd = NULL;
+  g_autofree gchar *dest_dir = NULL;
   g_autofree gchar *options_path = NULL;
-  g_autofree gchar *tagsdir = NULL;
-  IdeContext *context;
-  IdeProject *project;
-  GError *error = NULL;
-  IdeVcs *vcs;
-
-  IDE_ENTRY;
+  g_autofree gchar *tags_path = NULL;
+  g_autoptr(GString) filenames = NULL;
+  GOutputStream *stdin_stream;
+  gpointer infoptr;
 
-  g_assert (G_IS_TASK (task));
   g_assert (IDE_IS_CTAGS_BUILDER (self));
-  g_assert (task_data == NULL);
-  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (G_IS_FILE (directory));
+  g_assert (G_IS_FILE (destination));
 
-  /*
-   * Get our necessary components, and then release the context hold
-   * which we acquired before passing work to this thread.
-   */
-  context = ide_object_get_context (IDE_OBJECT (self));
-  project = ide_context_get_project (context);
-  vcs = ide_context_get_vcs (context);
-  workdir = g_object_ref (ide_vcs_get_working_directory (vcs));
-  tags_filename = g_strconcat (ide_project_get_id (project), ".tags", NULL);
-  tags_file = g_build_filename (g_get_user_cache_dir (),
-                                ide_get_program_name (),
-                                "tags",
-                                tags_filename,
-                                NULL);
+  dest_dir = g_file_get_path (destination);
+  if (0 != g_mkdir_with_parents (dest_dir, 0750))
+    return FALSE;
+
+  tags_file = g_file_get_child (destination, "tags");
+  tags_path = g_file_get_path (tags_file);
+  cwd = g_file_get_path (directory);
   options_path = g_build_filename (g_get_user_config_dir (),
                                    ide_get_program_name (),
                                    "ctags.conf",
                                    NULL);
-  ide_object_release (IDE_OBJECT (self));
+  directories = g_ptr_array_new_with_free_func (g_object_unref);
+  dest_directories = g_ptr_array_new_with_free_func (g_object_unref);
+  filenames = g_string_new (NULL);
 
-  /*
-   * If the file is not native, ctags can't generate anything for us.
-   */
-  if (!(workpath = g_file_get_path (workdir)))
-    {
-      g_task_return_new_error (task,
-                               G_IO_ERROR,
-                               G_IO_ERROR_INVALID_FILENAME,
-                               "ctags can only operate on local files.");
-      IDE_EXIT;
-    }
-
-  /* create the directory if necessary */
-  tagsdir = g_path_get_dirname (tags_file);
-  if (!g_file_test (tagsdir, G_FILE_TEST_IS_DIR))
-    g_mkdir_with_parents (tagsdir, 0750);
+  launcher = ide_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDIN_PIPE |
+                                          G_SUBPROCESS_FLAGS_STDERR_SILENCE);
 
-  /* remove the existing tags file (we already have it in memory anyway) */
-  if (g_file_test (tags_file, G_FILE_TEST_EXISTS))
-    g_unlink (tags_file);
+  ide_subprocess_launcher_set_cwd (launcher, cwd);
+  ide_subprocess_launcher_setenv (launcher, "TMPDIR", cwd, TRUE);
+  ide_subprocess_launcher_set_stdout_file_path (launcher, tags_path);
 
-  launcher = ide_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE);
-
-  ide_subprocess_launcher_push_argv (launcher, g_quark_to_string (self->ctags_path));
+  ide_subprocess_launcher_push_argv (launcher, ctags);
   ide_subprocess_launcher_push_argv (launcher, "-f");
   ide_subprocess_launcher_push_argv (launcher, "-");
-  ide_subprocess_launcher_push_argv (launcher, "--recurse=yes");
   ide_subprocess_launcher_push_argv (launcher, "--tag-relative=no");
   ide_subprocess_launcher_push_argv (launcher, "--exclude=.git");
   ide_subprocess_launcher_push_argv (launcher, "--exclude=.bzr");
   ide_subprocess_launcher_push_argv (launcher, "--exclude=.svn");
+  ide_subprocess_launcher_push_argv (launcher, "--exclude=.flatpak-builder");
   ide_subprocess_launcher_push_argv (launcher, "--sort=yes");
   ide_subprocess_launcher_push_argv (launcher, "--languages=all");
   ide_subprocess_launcher_push_argv (launcher, "--file-scope=yes");
   ide_subprocess_launcher_push_argv (launcher, "--c-kinds=+defgpstx");
+
   if (g_file_test (options_path, G_FILE_TEST_IS_REGULAR))
     {
       ide_subprocess_launcher_push_argv (launcher, "--options");
       ide_subprocess_launcher_push_argv (launcher, options_path);
     }
-  ide_subprocess_launcher_push_argv (launcher, ".");
+
+  /* Read filenames from stdin, which we will provided below */
+  ide_subprocess_launcher_push_argv (launcher, "-L");
+  ide_subprocess_launcher_push_argv (launcher, "-");
+
+  subprocess = ide_subprocess_launcher_spawn (launcher, cancellable, &error);
+
+  if (subprocess == NULL)
+    {
+      g_warning ("%s", error->message);
+      return FALSE;
+    }
+
+  stdin_stream = ide_subprocess_get_stdin_pipe (subprocess);
 
   /*
-   * Create our arguments to launch the ctags generation process.
-   */
-  ide_subprocess_launcher_set_cwd (launcher, workpath);
-  ide_subprocess_launcher_set_stdout_file_path (launcher, tags_file);
-  /*
-   * ctags can sometimes write to TMPDIR for incremental writes so that it
-   * can sort internally. On large files this can cause us to run out of
-   * tmpfs. Instead, just use the home dir which should map to something
-   * that is persistent.
+   * We do our own recursive building of ctags instead of --recursive=yes
+   * so that we can have smaller files to update. This helps on larger
+   * projects where we would have to rescan the whole project after a
+   * file is saved.
+   *
+   * Additionally, while walking the file-system tree, we append files
+   * to stdin of our ctags process to tell it to process them.
    */
-  ide_subprocess_launcher_setenv (launcher, "TMPDIR", tagsdir, TRUE);
-  process = ide_subprocess_launcher_spawn (launcher, cancellable, &error);
 
-  EGG_COUNTER_INC (parse_count);
+  enumerator = g_file_enumerate_children (directory,
+                                          G_FILE_ATTRIBUTE_STANDARD_NAME","
+                                          G_FILE_ATTRIBUTE_STANDARD_TYPE,
+                                          G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+                                          cancellable,
+                                          &error);
 
-  if (process == NULL)
+  if (enumerator == NULL)
+    IDE_GOTO (finish_subprocess);
+
+  while (NULL != (infoptr = g_file_enumerator_next_file (enumerator, cancellable, &error)))
     {
-      g_task_return_error (task, error);
-      IDE_EXIT;
+      g_autoptr(GFileInfo) info = infoptr;
+      const gchar *name;
+      GFileType type;
+
+      name = g_file_info_get_name (info);
+      type = g_file_info_get_file_type (info);
+
+      if (g_hash_table_contains (ignored, name))
+        continue;
+
+      if (type == G_FILE_TYPE_DIRECTORY)
+        {
+          if (recursive)
+            {
+              g_ptr_array_add (directories, g_file_get_child (directory, name));
+              g_ptr_array_add (dest_directories, g_file_get_child (destination, name));
+            }
+        }
+      else if (type == G_FILE_TYPE_REGULAR)
+        {
+          g_string_append_printf (filenames, "%s\n", name);
+        }
     }
 
-  g_task_set_task_data (task, g_file_new_for_path (tags_file), g_object_unref);
+  g_output_stream_write_all (stdin_stream, filenames->str, filenames->len, NULL, NULL, NULL);
 
-  ide_subprocess_wait_async (process,
-                             cancellable,
-                             ide_ctags_builder_process_wait_cb,
-                             g_object_ref (task));
+finish_subprocess:
+  g_output_stream_close (stdin_stream, NULL, NULL);
 
-  IDE_EXIT;
-}
+  if (!ide_subprocess_wait_check (subprocess, NULL, &error))
+    {
+      g_warning ("%s", error->message);
+      return FALSE;
+    }
 
-void
-ide_ctags_builder_rebuild (IdeCtagsBuilder *self)
-{
-  g_autoptr(GTask) task = NULL;
+  for (guint i = 0; i < directories->len; i++)
+    {
+      GFile *child = g_ptr_array_index (directories, i);
+      GFile *dest_child = g_ptr_array_index (dest_directories, i);
 
-  g_return_if_fail (IDE_IS_CTAGS_BUILDER (self));
+      g_assert (G_IS_FILE (child));
+      g_assert (G_IS_FILE (dest_child));
 
-  /* Make sure we aren't already in shutdown. */
-  if (!ide_object_hold (IDE_OBJECT (self)))
-    return;
+      if (!ide_ctags_builder_build (self, ctags, child, dest_child, recursive, cancellable))
+        return FALSE;
+    }
 
-  task = g_task_new (self, NULL, ide_ctags_builder_build_cb, NULL);
-  ide_thread_pool_push_task (IDE_THREAD_POOL_INDEXER, task, ide_ctags_builder_build_worker);
+  return TRUE;
 }
 
 static void
-ide_ctags_builder__ctags_path_changed (IdeCtagsBuilder *self,
-                                       const gchar     *key,
-                                       GSettings       *settings)
+ide_ctags_builder_build_worker (GTask        *task,
+                                gpointer      source_object,
+                                gpointer      task_data_ptr,
+                                GCancellable *cancellable)
 {
-  g_autofree gchar *ctags_path = NULL;
+  BuildTaskData *task_data = task_data_ptr;
+  IdeCtagsBuilder *self = source_object;
+  const gchar *ctags;
 
-  g_assert (IDE_IS_CTAGS_BUILDER (self));
-  g_assert (ide_str_equal0 (key, "ctags-path"));
-  g_assert (G_IS_SETTINGS (settings));
+  IDE_ENTRY;
 
-  ctags_path = g_settings_get_string (settings, "ctags-path");
-  self->ctags_path = g_quark_from_string (ctags_path);
-}
+  g_assert (G_IS_TASK (task));
+  g_assert (IDE_IS_CTAGS_BUILDER (source_object));
+  g_assert (G_IS_FILE (task_data->directory));
 
-static void
-ide_ctags_builder_finalize (GObject *object)
-{
-  IdeCtagsBuilder *self = (IdeCtagsBuilder *)object;
+  ctags = task_data->ctags;
+  if (!g_find_program_in_path (ctags))
+    ctags = "ctags";
 
-  ide_clear_source (&self->build_timeout);
-  g_clear_object (&self->settings);
+  ide_ctags_builder_build (self,
+                           ctags,
+                           task_data->directory,
+                           task_data->destination,
+                           task_data->recursive,
+                           cancellable);
 
-  G_OBJECT_CLASS (ide_ctags_builder_parent_class)->finalize (object);
+  g_task_return_boolean (task, TRUE);
 
-  EGG_COUNTER_DEC (instances);
+  IDE_EXIT;
 }
 
 static void
-ide_ctags_builder_class_init (IdeCtagsBuilderClass *klass)
+ide_ctags_builder_build_async (IdeTagsBuilder      *builder,
+                               GFile               *directory_or_file,
+                               gboolean             recursive,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
 {
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
-
-  object_class->finalize = ide_ctags_builder_finalize;
-
-  signals [TAGS_BUILT] =
-    g_signal_new ("tags-built",
-                  G_TYPE_FROM_CLASS (klass),
-                  G_SIGNAL_RUN_LAST,
-                  0,
-                  NULL, NULL, NULL,
-                  G_TYPE_NONE,
-                  1,
-                  G_TYPE_FILE);
-}
+  IdeCtagsBuilder *self = (IdeCtagsBuilder *)builder;
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GSettings) settings = NULL;
+  g_autofree gchar *destination_path = NULL;
+  g_autofree gchar *relative_path = NULL;
+  BuildTaskData *task_data;
+  IdeContext *context;
+  const gchar *project_id;
+  GFile *workdir;
 
-static void
-ide_ctags_builder_class_finalize (IdeCtagsBuilderClass *klass)
-{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_CTAGS_BUILDER (self));
+  g_assert (G_IS_FILE (directory_or_file));
+
+  settings = g_settings_new ("org.gnome.builder.code-insight");
+
+  task_data = g_slice_new0 (BuildTaskData);
+  task_data->ctags = g_settings_get_string (settings, "ctags-path");
+  task_data->directory = g_object_ref (directory_or_file);
+  task_data->recursive = recursive;
+  task_data->ctags = g_strdup ("ctags");
+
+  /*
+   * The destination directory for the tags should match the hierarchy
+   * of the projects source tree, but be based in something like
+   * ~/.cache/gnome-builder/tags/$project_id/ so that they can be reused
+   * even between configuration changes. Primarily, we want to avoid
+   * putting things in the source tree.
+   */
+  context = ide_object_get_context (IDE_OBJECT (self));
+  project_id = ide_project_get_id (ide_context_get_project (context));
+  workdir = ide_vcs_get_working_directory (ide_context_get_vcs (context));
+  relative_path = g_file_get_relative_path (workdir, directory_or_file);
+  destination_path = g_build_filename (g_get_user_cache_dir (),
+                                       ide_get_program_name (),
+                                       "tags",
+                                       project_id,
+                                       relative_path,
+                                       NULL);
+  task_data->destination = g_file_new_for_path (destination_path);
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_ctags_builder_build_async);
+  g_task_set_task_data (task, task_data, build_task_data_free);
+  ide_thread_pool_push_task (IDE_THREAD_POOL_INDEXER, task, ide_ctags_builder_build_worker);
+
+  IDE_EXIT;
 }
 
-static void
-ide_ctags_builder_init (IdeCtagsBuilder *self)
+static gboolean
+ide_ctags_builder_build_finish (IdeTagsBuilder  *builder,
+                                GAsyncResult    *result,
+                                GError         **error)
 {
-  g_autofree gchar *ctags_path = NULL;
+  gboolean ret;
 
-  EGG_COUNTER_INC (instances);
+  IDE_ENTRY;
 
-  self->settings = g_settings_new ("org.gnome.builder.code-insight");
+  g_return_val_if_fail (IDE_IS_CTAGS_BUILDER (builder), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
 
-  g_signal_connect_object (self->settings,
-                           "changed::ctags-path",
-                           G_CALLBACK (ide_ctags_builder__ctags_path_changed),
-                           self,
-                           G_CONNECT_SWAPPED);
+  ret = g_task_propagate_boolean (G_TASK (result), error);
 
-  ctags_path = g_settings_get_string (self->settings, "ctags-path");
-  self->ctags_path = g_quark_from_string (ctags_path);
+  IDE_RETURN (ret);
 }
 
-void
-_ide_ctags_builder_register_type (GTypeModule *module)
+static void
+tags_builder_iface_init (IdeTagsBuilderInterface *iface)
 {
-  ide_ctags_builder_register_type (module);
+  iface->build_async = ide_ctags_builder_build_async;
+  iface->build_finish = ide_ctags_builder_build_finish;
 }
diff --git a/plugins/ctags/ide-ctags-builder.h b/plugins/ctags/ide-ctags-builder.h
index c07ba89..1440542 100644
--- a/plugins/ctags/ide-ctags-builder.h
+++ b/plugins/ctags/ide-ctags-builder.h
@@ -1,6 +1,6 @@
 /* ide-ctags-builder.h
  *
- * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ * Copyright (C) 2017 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
@@ -19,7 +19,7 @@
 #ifndef IDE_CTAGS_BUILDER_H
 #define IDE_CTAGS_BUILDER_H
 
-#include "ide-object.h"
+#include <ide.h>
 
 G_BEGIN_DECLS
 
@@ -27,8 +27,7 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (IdeCtagsBuilder, ide_ctags_builder, IDE, CTAGS_BUILDER, IdeObject)
 
-IdeCtagsBuilder *ide_ctags_builder_new     (void);
-void             ide_ctags_builder_rebuild (IdeCtagsBuilder *self);
+IdeTagsBuilder *ide_ctags_builder_new (IdeContext *context);
 
 G_END_DECLS
 
diff --git a/plugins/ctags/ide-ctags-index.c b/plugins/ctags/ide-ctags-index.c
index 1e97ac3..b337459 100644
--- a/plugins/ctags/ide-ctags-index.c
+++ b/plugins/ctags/ide-ctags-index.c
@@ -445,7 +445,7 @@ ide_ctags_index_init_async (GAsyncInitable      *initable,
       return;
     }
 
-  g_task_run_in_thread (task, ide_ctags_index_build_index);
+  ide_thread_pool_push_task (IDE_THREAD_POOL_INDEXER, task, ide_ctags_index_build_index);
 }
 
 static gboolean
@@ -677,3 +677,11 @@ ide_ctags_index_find_with_path (IdeCtagsIndex *self,
 
   return ar;
 }
+
+gboolean
+ide_ctags_index_get_is_empty (IdeCtagsIndex *self)
+{
+  g_return_val_if_fail (IDE_IS_CTAGS_INDEX (self), FALSE);
+
+  return self->index == NULL || self->index->len == 0;
+}
diff --git a/plugins/ctags/ide-ctags-index.h b/plugins/ctags/ide-ctags-index.h
index c21a70d..6d26aba 100644
--- a/plugins/ctags/ide-ctags-index.h
+++ b/plugins/ctags/ide-ctags-index.h
@@ -71,6 +71,7 @@ GPtrArray                *ide_ctags_index_find_with_path(IdeCtagsIndex
 gchar                    *ide_ctags_index_resolve_path  (IdeCtagsIndex            *self,
                                                          const gchar              *path);
 GFile                    *ide_ctags_index_get_file      (IdeCtagsIndex            *self);
+gboolean                  ide_ctags_index_get_is_empty  (IdeCtagsIndex            *self);
 gsize                     ide_ctags_index_get_size      (IdeCtagsIndex            *self);
 const gchar              *ide_ctags_index_get_path_root (IdeCtagsIndex            *self);
 const IdeCtagsIndexEntry *ide_ctags_index_lookup        (IdeCtagsIndex            *self,
diff --git a/plugins/ctags/ide-ctags-service.c b/plugins/ctags/ide-ctags-service.c
index 8264883..fc7d4d9 100644
--- a/plugins/ctags/ide-ctags-service.c
+++ b/plugins/ctags/ide-ctags-service.c
@@ -37,10 +37,19 @@ struct _IdeCtagsService
   IdeCtagsBuilder  *builder;
   GPtrArray        *highlighters;
   GPtrArray        *completions;
+  GHashTable       *build_timeout_by_dir;
 
-  guint             build_tags_timeout;
+  guint             queued_miner_handler;
+  guint             miner_active : 1;
+  guint             needs_recursive_mine : 1;
 };
 
+typedef struct
+{
+  gchar *path;
+  guint  recursive;
+} MineInfo;
+
 static void service_iface_init (IdeServiceInterface *iface);
 
 G_DEFINE_DYNAMIC_TYPE_EXTENDED (IdeCtagsService, ide_ctags_service, IDE_TYPE_OBJECT, 0,
@@ -60,6 +69,11 @@ ide_ctags_service_build_index_init_cb (GObject      *object,
 
   if (!g_async_initable_init_finish (G_ASYNC_INITABLE (index), result, &error))
     g_task_return_error (task, error);
+  else if (ide_ctags_index_get_is_empty (index))
+    g_task_return_new_error (task,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NONE,
+                             "tags file is empty");
   else
     g_task_return_pointer (task, g_object_ref (index), g_object_unref);
 }
@@ -68,11 +82,15 @@ static guint64
 get_file_mtime (GFile *file)
 {
   g_autoptr(GFileInfo) info = NULL;
+  g_autofree gchar *path = NULL;
 
   if ((info = g_file_query_info (file, G_FILE_ATTRIBUTE_TIME_MODIFIED,
                                  G_FILE_QUERY_INFO_NONE, NULL, NULL)))
     return g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
 
+  path = g_file_get_uri (file);
+  g_warning ("Failed to get mtime for %s", path);
+
   return 0;
 }
 
@@ -123,7 +141,6 @@ ide_ctags_service_build_index_cb (EggTaskCache  *cache,
   IdeCtagsService *self = user_data;
   g_autoptr(IdeCtagsIndex) index = NULL;
   GFile *file = (GFile *)key;
-  g_autofree gchar *uri = NULL;
   g_autofree gchar *path_root = NULL;
 
   IDE_ENTRY;
@@ -136,8 +153,12 @@ ide_ctags_service_build_index_cb (EggTaskCache  *cache,
   path_root = resolve_path_root (self, file);
   index = ide_ctags_index_new (file, path_root, get_file_mtime (file));
 
-  uri = g_file_get_uri (file);
-  g_debug ("Building ctags in memory index for %s", uri);
+#ifdef IDE_ENABLE_TRACE
+  {
+    g_autofree gchar *uri = g_file_get_uri (file);
+    IDE_TRACE_MSG ("Building ctags in memory index for %s", uri);
+  }
+#endif
 
   g_async_initable_init_async (G_ASYNC_INITABLE (index),
                                G_PRIORITY_DEFAULT,
@@ -166,8 +187,12 @@ ide_ctags_service_tags_loaded_cb (GObject      *object,
 
   if (!(index = egg_task_cache_get_finish (cache, result, &error)))
     {
-      g_debug ("%s", error->message);
+      /* don't log if it was an empty file */
+      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NONE))
+        g_debug ("%s", error->message);
+
       g_clear_error (&error);
+
       IDE_EXIT;
     }
 
@@ -192,20 +217,26 @@ static gboolean
 file_is_newer (IdeCtagsIndex *index,
                GFile         *file)
 {
+  guint64 file_mtime;
+  guint64 ctags_mtime;
+
   g_assert (IDE_IS_CTAGS_INDEX (index));
   g_assert (G_IS_FILE (file));
 
-  return get_file_mtime (file) > ide_ctags_index_get_mtime (index);
+  file_mtime = get_file_mtime (file);
+  ctags_mtime = ide_ctags_index_get_mtime (index);
+
+  return file_mtime > ctags_mtime;
 }
 
 static gboolean
 do_load (gpointer data)
 {
+  IdeCtagsIndex *prev;
   struct {
     IdeCtagsService *self;
     GFile *file;
   } *pair = data;
-  IdeCtagsIndex *prev;
 
   if ((prev = egg_task_cache_peek (pair->self->indexes, pair->file)))
     {
@@ -316,64 +347,103 @@ ide_ctags_service_miner (GTask        *task,
                          gpointer      task_data,
                          GCancellable *cancellable)
 {
-  g_autofree gchar *project_tags = NULL;
-  g_autofree gchar *filename = NULL;
   IdeCtagsService *self = source_object;
-  IdeContext *context;
-  IdeProject *project;
-  IdeVcs *vcs;
-  GFile *file;
+  GArray *mine_info = task_data;
+
+  IDE_ENTRY;
 
   g_assert (G_IS_TASK (task));
   g_assert (IDE_IS_CTAGS_SERVICE (self));
+  g_assert (mine_info != NULL);
 
-  context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
-  project = ide_context_get_project (context);
-  filename = g_strconcat (ide_project_get_id (project), ".tags", NULL);
-  project_tags = g_build_filename (g_get_user_cache_dir (),
-                                   ide_get_program_name (),
-                                   "tags",
-                                   filename,
-                                   NULL);
-
-  /* mine ~/.cache/gnome-builder/tags/<name>.tags */
-  file = g_file_new_for_path (project_tags);
-  ide_ctags_service_load_tags (self, file);
-  g_object_unref (file);
-
-  /* mine the project tree */
-  file = g_object_ref (ide_vcs_get_working_directory (vcs));
-  ide_ctags_service_mine_directory (self, file, TRUE, cancellable);
-  g_object_unref (file);
+  for (guint i = 0; i < mine_info->len; i++)
+    {
+      const MineInfo *info = &g_array_index (mine_info, MineInfo, i);
+      g_autoptr(GFile) file = g_file_new_for_path (info->path);
 
-  /* mine ~/.tags */
-  file = g_file_new_for_path (g_get_home_dir ());
-  ide_ctags_service_mine_directory (self, file, FALSE, cancellable);
-  g_object_unref (file);
+      ide_ctags_service_mine_directory (self, file, info->recursive, cancellable);
+    }
 
-  /* mine /usr/include */
-  file = g_file_new_for_path ("/usr/include");
-  ide_ctags_service_mine_directory (self, file, TRUE, cancellable);
-  g_object_unref (file);
+  self->miner_active = FALSE;
 
-  ide_object_release (IDE_OBJECT (self));
+  IDE_EXIT;
 }
 
 static void
-ide_ctags_service_mine (IdeCtagsService *self)
+clear_mine_info (gpointer data)
+{
+  MineInfo *info = data;
+
+  g_free (info->path);
+}
+
+static gboolean
+ide_ctags_service_do_mine (gpointer data)
 {
+  IdeCtagsService *self = data;
   g_autoptr(GTask) task = NULL;
+  g_autoptr(GArray) mine_info = NULL;
+  g_autofree gchar *path = NULL;
+  IdeContext *context;
+  IdeProject *project;
+  MineInfo info;
+  GFile *workdir;
 
-  g_return_if_fail (IDE_IS_CTAGS_SERVICE (self));
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_CTAGS_SERVICE (self));
+
+  self->queued_miner_handler = 0;
+  self->miner_active = TRUE;
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  project = ide_context_get_project (context);
+  workdir = ide_vcs_get_working_directory (ide_context_get_vcs (context));
+
+  mine_info = g_array_new (FALSE, FALSE, sizeof (MineInfo));
+  g_array_set_clear_func (mine_info, clear_mine_info);
 
-  /* prevent unloading until we release from worker thread */
-  ide_object_hold (IDE_OBJECT (self));
+  /* mine: ~/.cache/gnome-builder/tags/$project_id */
+  info.path = g_build_filename (g_get_user_cache_dir (),
+                                ide_get_program_name (),
+                                "tags",
+                                ide_project_get_id (project),
+                                NULL);
+  info.recursive = TRUE;
+  g_array_append_val (mine_info, info);
+
+  /* mine: ~/.tags */
+  info.path = g_strdup (g_get_home_dir ());
+  info.recursive = FALSE;
+  g_array_append_val (mine_info, info);
+
+  /* mine the project tree */
+  info.path = g_file_get_path (workdir);
+  info.recursive = TRUE;
+  g_array_append_val (mine_info, info);
+
+  task = g_task_new (self, NULL, NULL, NULL);
+  g_task_set_source_tag (task, ide_ctags_service_do_mine);
+  g_task_set_task_data (task, g_steal_pointer (&mine_info), (GDestroyNotify)g_array_unref);
+  ide_thread_pool_push_task (IDE_THREAD_POOL_INDEXER, task, ide_ctags_service_miner);
 
-  self->cancellable = g_cancellable_new ();
+  IDE_RETURN (G_SOURCE_REMOVE);
+}
+
+static void
+ide_ctags_service_queue_mine (IdeCtagsService *self)
+{
+  g_assert (IDE_IS_CTAGS_SERVICE (self));
 
-  task = g_task_new (self, self->cancellable, NULL, NULL);
-  g_task_run_in_thread (task, ide_ctags_service_miner);
+  if (self->queued_miner_handler == 0 && self->miner_active == FALSE)
+    {
+      self->queued_miner_handler =
+        g_timeout_add_full (250,
+                            G_PRIORITY_DEFAULT,
+                            ide_ctags_service_do_mine,
+                            g_object_ref (self),
+                            g_object_unref);
+    }
 }
 
 static void
@@ -407,64 +477,84 @@ build_system_tags_cb (GObject      *object,
 
   g_assert (IDE_IS_TAGS_BUILDER (builder));
 
-  ide_ctags_service_mine (self);
+  ide_ctags_service_queue_mine (self);
 }
 
 static gboolean
-restart_miner (gpointer data)
+restart_miner (gpointer user_data)
 {
-  IdeCtagsService *self = data;
+  g_autofree gpointer *data = user_data;
+  g_autoptr(IdeCtagsService) self = data[0];
+  g_autoptr(GFile) directory = data[1];
+  g_autoptr(IdeTagsBuilder) tags_builder = NULL;
+  IdeBuildSystem *build_system;
   IdeContext *context;
 
   IDE_ENTRY;
 
   g_assert (IDE_IS_CTAGS_SERVICE (self));
 
-  self->build_tags_timeout = 0;
+  g_hash_table_remove (self->build_timeout_by_dir, directory);
 
   context = ide_object_get_context (IDE_OBJECT (self));
+  build_system = ide_context_get_build_system (context);
 
-  if (context != NULL)
-    {
-      IdeBuildSystem *build_system;
-
-      build_system = ide_context_get_build_system (context);
+  if (IDE_IS_TAGS_BUILDER (build_system))
+    tags_builder = g_object_ref (IDE_TAGS_BUILDER (build_system));
+  else
+    tags_builder = ide_ctags_builder_new (context);
 
-      if (IDE_IS_TAGS_BUILDER (build_system))
-        {
-          IdeVcs *vcs;
-          GFile *workdir;
-
-          vcs = ide_context_get_vcs (context);
-          workdir = ide_vcs_get_working_directory (vcs);
-          ide_tags_builder_build_async (IDE_TAGS_BUILDER (build_system), workdir, TRUE, NULL,
-                                        build_system_tags_cb, g_object_ref (self));
-          IDE_GOTO (finish);
-        }
-      else
-        {
-          ide_ctags_builder_rebuild (self->builder);
-        }
-    }
+  ide_tags_builder_build_async (tags_builder,
+                                directory,
+                                self->needs_recursive_mine,
+                                NULL,
+                                build_system_tags_cb,
+                                g_object_ref (self));
 
-finish:
+  self->needs_recursive_mine = FALSE;
 
   IDE_RETURN (G_SOURCE_REMOVE);
 }
 
 static void
+ide_ctags_service_queue_build_for_directory (IdeCtagsService *self,
+                                             GFile           *directory)
+{
+  g_assert (IDE_IS_CTAGS_SERVICE (self));
+  g_assert (G_IS_FILE (directory));
+
+  if (!g_hash_table_lookup (self->build_timeout_by_dir, directory))
+    {
+      gpointer *data;
+      guint source_id;
+
+      data = g_new0 (gpointer, 2);
+      data[0] = g_object_ref (self);
+      data[1] = g_object_ref (directory);
+
+      source_id = g_timeout_add_seconds (5, restart_miner, data);
+
+      g_hash_table_insert (self->build_timeout_by_dir,
+                           g_object_ref (directory),
+                           GUINT_TO_POINTER (source_id));
+    }
+}
+
+static void
 ide_ctags_service_buffer_saved (IdeCtagsService  *self,
                                 IdeBuffer        *buffer,
                                 IdeBufferManager *buffer_manager)
 {
+  g_autoptr(GFile) parent = NULL;
+
   IDE_ENTRY;
 
   g_assert (IDE_IS_CTAGS_SERVICE (self));
   g_assert (IDE_IS_BUFFER (buffer));
   g_assert (IDE_IS_BUFFER_MANAGER (buffer_manager));
 
-  if (self->build_tags_timeout == 0)
-    self->build_tags_timeout = g_timeout_add_seconds (5, restart_miner, self);
+  parent = g_file_get_parent (ide_file_get_file (ide_buffer_get_file (buffer)));
+  ide_ctags_service_queue_build_for_directory (self, parent);
 
   IDE_EXIT;
 }
@@ -475,6 +565,7 @@ ide_ctags_service_context_loaded (IdeService *service)
   IdeBufferManager *buffer_manager;
   IdeCtagsService *self = (IdeCtagsService *)service;
   IdeContext *context;
+  GFile *workdir;
 
   IDE_ENTRY;
 
@@ -482,6 +573,7 @@ ide_ctags_service_context_loaded (IdeService *service)
 
   context = ide_object_get_context (IDE_OBJECT (self));
   buffer_manager = ide_context_get_buffer_manager (context);
+  workdir = ide_vcs_get_working_directory (ide_context_get_vcs (context));
 
   g_signal_connect_object (buffer_manager,
                            "buffer-saved",
@@ -489,7 +581,12 @@ ide_ctags_service_context_loaded (IdeService *service)
                            self,
                            G_CONNECT_SWAPPED);
 
-  ide_ctags_service_mine (self);
+  /*
+   * Rebuild all ctags for the project at startup of the service.
+   * Then we do incrementals from there on out.
+   */
+  self->needs_recursive_mine = TRUE;
+  ide_ctags_service_queue_build_for_directory (self, workdir);
 
   IDE_EXIT;
 }
@@ -523,7 +620,6 @@ ide_ctags_service_stop (IdeService *service)
   if (self->cancellable && !g_cancellable_is_cancelled (self->cancellable))
     g_cancellable_cancel (self->cancellable);
 
-  ide_clear_source (&self->build_tags_timeout);
   g_clear_object (&self->cancellable);
   g_clear_object (&self->builder);
 }
@@ -535,11 +631,11 @@ ide_ctags_service_finalize (GObject *object)
 
   IDE_ENTRY;
 
-  ide_clear_source (&self->build_tags_timeout);
   g_clear_object (&self->indexes);
   g_clear_object (&self->cancellable);
   g_clear_pointer (&self->highlighters, g_ptr_array_unref);
   g_clear_pointer (&self->completions, g_ptr_array_unref);
+  g_clear_pointer (&self->build_timeout_by_dir, g_hash_table_unref);
 
   G_OBJECT_CLASS (ide_ctags_service_parent_class)->finalize (object);
 
@@ -573,6 +669,10 @@ ide_ctags_service_init (IdeCtagsService *self)
   self->highlighters = g_ptr_array_new ();
   self->completions = g_ptr_array_new ();
 
+  self->build_timeout_by_dir = g_hash_table_new_full ((GHashFunc)g_file_hash,
+                                                      (GEqualFunc)g_file_equal,
+                                                      g_object_unref, NULL);
+
   self->indexes = egg_task_cache_new ((GHashFunc)g_file_hash,
                                       (GEqualFunc)g_file_equal,
                                       g_object_ref,



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