[gnome-builder] diagnostics: refactor IdeDiagnostician into IdeDiagnosticsManager



commit ea863a740cd65071ba1a2111e9a6200237e43f73
Author: Christian Hergert <chergert redhat com>
Date:   Thu Oct 27 18:22:10 2016 -0700

    diagnostics: refactor IdeDiagnostician into IdeDiagnosticsManager
    
    The IdeDiangostician was attached to the IdeBuffer directly. Instead,
    IdeDiagnosticsManager is connected to the IdeContext. This is useful
    because it allows us to receive diagnostics out of band from the normal
    diagnose_async()/diagnose_finish() cycle.
    
    Additionally, this allows diagnostics to be created for objects other
    than the buffer in question. Such is useful with language servers where
    they may process diagnostics for the project as a whole.

 libide/Makefile.am                           |    4 +-
 libide/buffers/ide-buffer.c                  |  250 ++----
 libide/diagnostics/ide-diagnostician.c       |  330 -------
 libide/diagnostics/ide-diagnostician.h       |   46 -
 libide/diagnostics/ide-diagnostics-manager.c | 1216 ++++++++++++++++++++++++++
 libide/diagnostics/ide-diagnostics-manager.h |   40 +
 libide/ide-context.c                         |   43 +
 libide/ide-context.h                         |    1 +
 libide/ide-types.h                           |    7 +-
 libide/ide.h                                 |    2 +-
 libidemm/src/libide_methods.defs             |   41 -
 11 files changed, 1369 insertions(+), 611 deletions(-)
---
diff --git a/libide/Makefile.am b/libide/Makefile.am
index 0c074f2..cff9a9f 100644
--- a/libide/Makefile.am
+++ b/libide/Makefile.am
@@ -47,7 +47,7 @@ libide_1_0_la_public_headers =                            \
        devices/ide-device.h                              \
        diagnostics/ide-diagnostic-provider.h             \
        diagnostics/ide-diagnostic.h                      \
-       diagnostics/ide-diagnostician.h                   \
+       diagnostics/ide-diagnostics-manager.h             \
        diagnostics/ide-diagnostics.h                     \
        diagnostics/ide-fixit.h                           \
        diagnostics/ide-source-location.h                 \
@@ -219,7 +219,7 @@ libide_1_0_la_public_sources =                            \
        devices/ide-device.c                              \
        diagnostics/ide-diagnostic-provider.c             \
        diagnostics/ide-diagnostic.c                      \
-       diagnostics/ide-diagnostician.c                   \
+       diagnostics/ide-diagnostics-manager.c             \
        diagnostics/ide-diagnostics.c                     \
        diagnostics/ide-fixit.c                           \
        diagnostics/ide-source-location.c                 \
diff --git a/libide/buffers/ide-buffer.c b/libide/buffers/ide-buffer.c
index c8835ae..6f25ac2 100644
--- a/libide/buffers/ide-buffer.c
+++ b/libide/buffers/ide-buffer.c
@@ -30,8 +30,8 @@
 #include "buffers/ide-buffer.h"
 #include "buffers/ide-unsaved-files.h"
 #include "diagnostics/ide-diagnostic.h"
-#include "diagnostics/ide-diagnostician.h"
 #include "diagnostics/ide-diagnostics.h"
+#include "diagnostics/ide-diagnostics-manager.h"
 #include "diagnostics/ide-source-location.h"
 #include "diagnostics/ide-source-range.h"
 #include "files/ide-file-settings.h"
@@ -70,10 +70,10 @@ typedef struct
   IdeContext             *context;
   IdeDiagnostics         *diagnostics;
   GHashTable             *diagnostics_line_cache;
+  EggSignalGroup         *diagnostics_manager_signals;
   IdeFile                *file;
   GBytes                 *content;
   IdeBufferChangeMonitor *change_monitor;
-  IdeDiagnostician       *diagnostician;
   IdeHighlightEngine     *highlight_engine;
   IdeExtensionAdapter    *rename_provider_adapter;
   IdeExtensionAdapter    *symbol_resolver_adapter;
@@ -85,9 +85,10 @@ typedef struct
 
   gulong                  change_monitor_changed_handler;
 
-  guint                   diagnose_timeout;
   guint                   check_modified_timeout;
 
+  guint                   diagnostics_sequence;
+
   GTimeVal                mtime;
 
   gint                    hold_count;
@@ -96,13 +97,10 @@ typedef struct
   gsize                   change_count;
 
   guint                   changed_on_volume : 1;
-  guint                   diagnostics_dirty : 1;
   guint                   highlight_diagnostics : 1;
-  guint                   in_diagnose : 1;
   guint                   loading : 1;
   guint                   mtime_set : 1;
   guint                   read_only : 1;
-  guint                   has_done_diagnostics_once : 1;
 } IdeBufferPrivate;
 
 G_DEFINE_TYPE_WITH_PRIVATE (IdeBuffer, ide_buffer, GTK_SOURCE_TYPE_BUFFER)
@@ -132,8 +130,6 @@ enum {
   LAST_SIGNAL
 };
 
-static void ide_buffer_queue_diagnose (IdeBuffer *self);
-
 static GParamSpec *properties [LAST_PROP];
 static guint signals [LAST_SIGNAL];
 
@@ -150,39 +146,11 @@ gboolean
 ide_buffer_get_has_diagnostics (IdeBuffer *self)
 {
   IdeBufferPrivate *priv = ide_buffer_get_instance_private (self);
-  guint size;
-  guint i;
 
   g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
 
-  if (priv->diagnostics == NULL)
-    return FALSE;
-
-  /*
-   * The diagnostics set might include warnings for files other than
-   * our own. So we need to verify they are for this file. As long as
-   * this is usually just used via bindings, its not expensive enough
-   * to matter much.
-   */
-
-  size = ide_diagnostics_get_size (priv->diagnostics);
-
-  for (i = 0; i < size; i++)
-    {
-      IdeDiagnostic *diag = ide_diagnostics_index (priv->diagnostics, i);
-      IdeSourceLocation *loc = ide_diagnostic_get_location (diag);
-      IdeFile *file;
-
-      if (loc == NULL)
-        continue;
-
-      file = ide_source_location_get_file (loc);
-
-      if (priv->file && file && ide_file_equal (priv->file, file))
-        return TRUE;
-    }
-
-  return FALSE;
+  return (priv->diagnostics != NULL) &&
+         (ide_diagnostics_get_size (priv->diagnostics) > 0);
 }
 
 /**
@@ -197,11 +165,11 @@ ide_buffer_get_has_diagnostics (IdeBuffer *self)
 gboolean
 ide_buffer_get_busy (IdeBuffer *self)
 {
-  IdeBufferPrivate *priv = ide_buffer_get_instance_private (self);
-
   g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
 
-  return priv->in_diagnose;
+  /* TODO: This should be deprecated */
+
+  return FALSE;
 }
 
 static void
@@ -267,6 +235,7 @@ ide_buffer_set_context (IdeBuffer  *self,
                         IdeContext *context)
 {
   IdeBufferPrivate *priv = ide_buffer_get_instance_private (self);
+  IdeDiagnosticsManager *diagnostics_manager;
 
   g_return_if_fail (IDE_IS_BUFFER (self));
   g_return_if_fail (IDE_IS_CONTEXT (context));
@@ -277,6 +246,10 @@ ide_buffer_set_context (IdeBuffer  *self,
   g_object_weak_ref (G_OBJECT (context),
                      ide_buffer_release_context,
                      self);
+
+  diagnostics_manager = ide_context_get_diagnostics_manager (context);
+
+  egg_signal_group_set_target (priv->diagnostics_manager_signals, diagnostics_manager);
 }
 
 void
@@ -302,7 +275,7 @@ ide_buffer_clear_diagnostics (IdeBuffer *self)
 
   g_assert (IDE_IS_BUFFER (self));
 
-  if (priv->diagnostics_line_cache)
+  if (priv->diagnostics_line_cache != NULL)
     g_hash_table_remove_all (priv->diagnostics_line_cache);
 
   gtk_text_buffer_get_bounds (buffer, &begin, &end);
@@ -398,7 +371,7 @@ ide_buffer_update_diagnostic (IdeBuffer     *self,
       return;
     }
 
-  if ((location = ide_diagnostic_get_location (diagnostic)))
+  if (NULL != (location = ide_diagnostic_get_location (diagnostic)))
     {
       IdeFile *file;
       GtkTextIter iter1;
@@ -407,9 +380,7 @@ ide_buffer_update_diagnostic (IdeBuffer     *self,
       file = ide_source_location_get_file (location);
 
       if (file && priv->file && !ide_file_equal (file, priv->file))
-        {
-          /* Ignore? */
-        }
+        return;
 
       ide_buffer_cache_diagnostic_line (self, location, location, severity);
 
@@ -490,7 +461,10 @@ ide_buffer_set_diagnostics (IdeBuffer      *self,
 {
   IdeBufferPrivate *priv = ide_buffer_get_instance_private (self);
 
+  IDE_ENTRY;
+
   g_assert (IDE_IS_BUFFER (self));
+  g_assert (diagnostics != NULL);
 
   if (diagnostics != priv->diagnostics)
     {
@@ -507,151 +481,65 @@ ide_buffer_set_diagnostics (IdeBuffer      *self,
       g_signal_emit (self, signals [LINE_FLAGS_CHANGED], 0);
       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_DIAGNOSTICS]);
     }
-}
 
-static void
-ide_buffer__file_load_settings_cb (GObject      *object,
-                                   GAsyncResult *result,
-                                   gpointer      user_data)
-{
-  g_autoptr(IdeBuffer) self = user_data;
-  IdeFile *file = (IdeFile *)object;
-  g_autoptr(IdeFileSettings) file_settings = NULL;
-
-  g_assert (IDE_IS_BUFFER (self));
-  g_assert (IDE_IS_FILE (file));
-
-  file_settings = ide_file_load_settings_finish (file, result, NULL);
-
-  if (file_settings)
-    {
-      gboolean insert_trailing_newline;
-
-      insert_trailing_newline = ide_file_settings_get_insert_trailing_newline (file_settings);
-      gtk_source_buffer_set_implicit_trailing_newline (GTK_SOURCE_BUFFER (self),
-                                                       insert_trailing_newline);
-    }
+  IDE_EXIT;
 }
 
 static void
-ide_buffer__diagnostician_diagnose_cb (GObject      *object,
-                                       GAsyncResult *result,
-                                       gpointer      user_data)
+ide_buffer__diagnostics_manager__changed (IdeBuffer             *self,
+                                          IdeDiagnosticsManager *diagnostics_manager)
 {
-  IdeDiagnostician *diagnostician = (IdeDiagnostician *)object;
-  g_autoptr(IdeBuffer) self = user_data;
   IdeBufferPrivate *priv = ide_buffer_get_instance_private (self);
   g_autoptr(IdeDiagnostics) diagnostics = NULL;
-  g_autoptr(GError) error = NULL;
-
-  g_assert (IDE_IS_DIAGNOSTICIAN (diagnostician));
-  g_assert (IDE_IS_BUFFER (self));
-
-  priv->in_diagnose = FALSE;
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
-
-  diagnostics = ide_diagnostician_diagnose_finish (diagnostician, result, &error);
+  GFile *file;
+  guint sequence;
 
-  if (error)
-    g_message ("%s", error->message);
-
-  ide_buffer_set_diagnostics (self, diagnostics);
-
-  if (priv->diagnostics_dirty)
-    ide_buffer_queue_diagnose (self);
-
-  if (!priv->has_done_diagnostics_once)
-    {
-      priv->has_done_diagnostics_once = TRUE;
-      ide_buffer_rehighlight (self);
-    }
-}
-
-static gboolean
-ide_buffer_is_system_file (IdeBuffer *self,
-                           IdeFile   *file)
-{
-  IdeBufferPrivate *priv = ide_buffer_get_instance_private (self);
-  GFile *gfile;
+  IDE_ENTRY;
 
   g_assert (IDE_IS_BUFFER (self));
-  g_assert (IDE_IS_FILE (file));
-
-  if (NULL != (gfile = ide_file_get_file (file)))
-    {
-      IdeVcs *vcs;
-      GFile *workdir;
-
-      vcs = ide_context_get_vcs (priv->context);
-      workdir = ide_vcs_get_working_directory (vcs);
-
-      if (gfile != NULL && !g_file_has_prefix (gfile, workdir))
-        return TRUE;
-    }
-
-  return FALSE;
-}
-
-static gboolean
-ide_buffer__diagnose_timeout_cb (gpointer user_data)
-{
-  IdeBuffer *self = user_data;
-  IdeBufferPrivate *priv = ide_buffer_get_instance_private (self);
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (diagnostics_manager));
 
-  g_assert (IDE_IS_BUFFER (self));
+  /*
+   * To avoid updating diagnostics on every change event (which could happen a
+   * lot) we check the sequence number with our last one to see if anything has
+   * changed in this specific buffer.
+   */
 
-  priv->diagnose_timeout = 0;
+  file = ide_file_get_file (priv->file);
+  sequence = ide_diagnostics_manager_get_sequence_for_file (diagnostics_manager, file);
 
-  if (priv->file != NULL && !ide_buffer_is_system_file (self, priv->file))
+  if (sequence != priv->diagnostics_sequence)
     {
-      priv->diagnostics_dirty = FALSE;
-      priv->in_diagnose = TRUE;
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
-
-      ide_buffer_sync_to_unsaved_files (self);
-      ide_diagnostician_diagnose_async (priv->diagnostician,
-                                        priv->file,
-                                        NULL,
-                                        ide_buffer__diagnostician_diagnose_cb,
-                                        g_object_ref (self));
+      diagnostics = ide_diagnostics_manager_get_diagnostics_for_file (diagnostics_manager, file);
+      ide_buffer_set_diagnostics (self, diagnostics);
+      priv->diagnostics_sequence = sequence;
     }
 
-  return G_SOURCE_REMOVE;
-}
-
-static guint
-ide_buffer_get_diagnose_timeout_msec (void)
-{
-  guint timeout_msec = DEFAULT_DIAGNOSE_TIMEOUT_MSEC;
-
-  if (ide_battery_monitor_get_should_conserve ())
-    timeout_msec = DEFAULT_DIAGNOSE_CONSERVE_TIMEOUT_MSEC;
-
-  return timeout_msec;
+  IDE_EXIT;
 }
 
 static void
-ide_buffer_queue_diagnose (IdeBuffer *self)
+ide_buffer__file_load_settings_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
 {
-  IdeBufferPrivate *priv = ide_buffer_get_instance_private (self);
-  guint timeout_msec;
+  g_autoptr(IdeBuffer) self = user_data;
+  IdeFile *file = (IdeFile *)object;
+  g_autoptr(IdeFileSettings) file_settings = NULL;
 
   g_assert (IDE_IS_BUFFER (self));
+  g_assert (IDE_IS_FILE (file));
 
-  priv->diagnostics_dirty = TRUE;
+  file_settings = ide_file_load_settings_finish (file, result, NULL);
 
-  if (priv->diagnose_timeout != 0)
+  if (file_settings)
     {
-      g_source_remove (priv->diagnose_timeout);
-      priv->diagnose_timeout = 0;
-    }
-
-  /*
-   * Try to real in how often we parse when on battery.
-   */
-  timeout_msec = ide_buffer_get_diagnose_timeout_msec ();
+      gboolean insert_trailing_newline;
 
-  priv->diagnose_timeout = g_timeout_add (timeout_msec, ide_buffer__diagnose_timeout_cb, self);
+      insert_trailing_newline = ide_file_settings_get_insert_trailing_newline (file_settings);
+      gtk_source_buffer_set_implicit_trailing_newline (GTK_SOURCE_BUFFER (self),
+                                                       insert_trailing_newline);
+    }
 }
 
 static void
@@ -750,12 +638,8 @@ ide_buffer_changed (GtkTextBuffer *buffer)
   GTK_TEXT_BUFFER_CLASS (ide_buffer_parent_class)->changed (buffer);
 
   priv->change_count++;
-  priv->diagnostics_dirty = TRUE;
 
   g_clear_pointer (&priv->content, g_bytes_unref);
-
-  if (priv->highlight_diagnostics && !priv->in_diagnose)
-    ide_buffer_queue_diagnose (self);
 }
 
 static void
@@ -992,8 +876,6 @@ ide_buffer_notify_language (IdeBuffer  *self,
 
   if (priv->symbol_resolver_adapter != NULL)
     ide_extension_adapter_set_value (priv->symbol_resolver_adapter, lang_id);
-
-  ide_diagnostician_set_language (priv->diagnostician, language);
 }
 
 static void
@@ -1244,10 +1126,6 @@ ide_buffer_constructed (GObject *object)
                                                              "Symbol-Resolver-Languages",
                                                              NULL);
 
-  priv->diagnostician = g_object_new (IDE_TYPE_DIAGNOSTICIAN,
-                                      "context", priv->context,
-                                      NULL);
-
   g_signal_connect (self,
                     "notify::language",
                     G_CALLBACK (ide_buffer_notify_language),
@@ -1286,23 +1164,18 @@ ide_buffer_dispose (GObject *object)
   if (priv->highlight_engine != NULL)
     g_object_run_dispose (G_OBJECT (priv->highlight_engine));
 
-  if (priv->diagnose_timeout)
-    {
-      g_source_remove (priv->diagnose_timeout);
-      priv->diagnose_timeout = 0;
-    }
-
   if (priv->change_monitor)
     {
       ide_clear_signal_handler (priv->change_monitor, &priv->change_monitor_changed_handler);
       g_clear_object (&priv->change_monitor);
     }
 
+  egg_signal_group_set_target (priv->diagnostics_manager_signals, NULL);
+
   g_clear_pointer (&priv->diagnostics_line_cache, g_hash_table_unref);
   g_clear_pointer (&priv->diagnostics, ide_diagnostics_unref);
   g_clear_pointer (&priv->content, g_bytes_unref);
   g_clear_pointer (&priv->title, g_free);
-  g_clear_object (&priv->diagnostician);
   g_clear_object (&priv->file);
   g_clear_object (&priv->highlight_engine);
   g_clear_object (&priv->rename_provider_adapter);
@@ -1608,6 +1481,13 @@ ide_buffer_init (IdeBuffer *self)
 
   priv->diagnostics_line_cache = g_hash_table_new (g_direct_hash, g_direct_equal);
 
+  priv->diagnostics_manager_signals = egg_signal_group_new (IDE_TYPE_DIAGNOSTICS_MANAGER);
+  egg_signal_group_connect_object (priv->diagnostics_manager_signals,
+                                   "changed",
+                                   G_CALLBACK (ide_buffer__diagnostics_manager__changed),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
   EGG_COUNTER_INC (instances);
 
   IDE_EXIT;
@@ -1830,10 +1710,6 @@ ide_buffer_set_highlight_diagnostics (IdeBuffer *self,
   if (highlight_diagnostics != priv->highlight_diagnostics)
     {
       priv->highlight_diagnostics = highlight_diagnostics;
-      if (!highlight_diagnostics)
-        ide_buffer_clear_diagnostics (self);
-      else
-        ide_buffer_queue_diagnose (self);
       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HIGHLIGHT_DIAGNOSTICS]);
     }
 }
@@ -1884,6 +1760,8 @@ ide_buffer_get_diagnostic_at_iter (IdeBuffer         *self,
           if (!location)
             continue;
 
+          /* TODO: This should look at the range for the diagnostic */
+
           ide_buffer_get_iter_at_location (self, &pos, location);
 
           if (line == gtk_text_iter_get_line (&pos))
diff --git a/libide/diagnostics/ide-diagnostics-manager.c b/libide/diagnostics/ide-diagnostics-manager.c
new file mode 100644
index 0000000..dfc7b91
--- /dev/null
+++ b/libide/diagnostics/ide-diagnostics-manager.c
@@ -0,0 +1,1216 @@
+/* ide-diagnostics-manager.c
+ *
+ * Copyright (C) 2016 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-diagnostics-manager"
+
+#include <gtksourceview/gtksource.h>
+
+#include "ide-context.h"
+#include "ide-debug.h"
+#include "ide-macros.h"
+
+#include "buffers/ide-buffer.h"
+#include "buffers/ide-buffer-manager.h"
+#include "diagnostics/ide-diagnostic.h"
+#include "diagnostics/ide-diagnostic-provider.h"
+#include "diagnostics/ide-diagnostics.h"
+#include "diagnostics/ide-diagnostics-manager.h"
+#include "plugins/ide-extension-set-adapter.h"
+
+typedef struct
+{
+  /*
+   * Our reference count on the group, which is not atomic because we
+   * require access to this structure to be accessed from the main thread
+   * only. This is used so that providers can have access to the group even
+   * after the group has been removed from the IdeDiagnosticsManager.
+   */
+  gint ref_count;
+
+  /*
+   * This is our identifier for the diagnostics. We use this as the key in
+   * the hash table so that we can quickly find the target buffer. If the
+   * IdeBuffer:file property changes, we will have to fallback to a the
+   * buffer to clear old entries.
+   */
+  GFile *file;
+
+  /*
+   * If there is a buffer open for the file, then the buffer will be found
+   * here. This is useful when we detect that a IdeBuffer:file property has
+   * changed and we need to invalidate things. There is a weak reference
+   * to the buffer here, but that is dropped in our ::buffer-unloaded
+   * callback. It's not really necessary other than for some additional
+   * insurance.
+   */
+  GWeakRef buffer_wr;
+
+  /*
+   * This hash table uses the given provider as the key and the last
+   * reported IdeDiagnostics as the value.
+   */
+  GHashTable *diagnostics_by_provider;
+
+  /*
+   * This extension set adapter is used to update the providers that are
+   * available based on the buffers current language. They may change
+   * at runtime due to the buffers language changing. When that happens
+   * we purge items from @diagnostics_by_provider and queue a diagnose
+   * request of the new provider.
+   */
+  IdeExtensionSetAdapter *adapter;
+
+  /*
+   * This is our sequence number for diagnostics. It is monotonically
+   * increasing with every diagnostic discovered.
+   */
+  guint sequence;
+
+  /*
+   * If we are currently diagnosing, then this will be set to a
+   * number greater than zero.
+   */
+  guint in_diagnose;
+
+  /*
+   * If we need a diagnose this bit will be set. If we complete a
+   * diagnosis and this bit is set, then we will automatically queue
+   * another diagnose upon completion.
+   */
+  guint needs_diagnose : 1;
+
+  /*
+   * This bit is set if we know the file or buffer has diagnostics. This
+   * is useful when we've cleaned up our extensions and no longer have
+   * the diagnostics loaded in memory, but we know that it previously
+   * had diagnostics which have not been rectified.
+   */
+  guint has_diagnostics : 1;
+
+  /*
+   * This bit is set when the group has been removed from the
+   * IdeDiagnosticsManager. That allows the providers to cleanup
+   * as necessary when their async operations complete.
+   */
+  guint was_removed : 1;
+
+} IdeDiagnosticsGroup;
+
+struct _IdeDiagnosticsManager
+{
+  IdeObject parent_instance;
+
+  /*
+   * This hashtable contains a mapping of GFile to the IdeDiagnosticsGroup
+   * for the file. When a buffer is renamed (the IdeBuffer:file property
+   * is changed) we need to update this entry so it reflects the new
+   * location.
+   */
+  GHashTable *groups_by_file;
+
+  /*
+   * If any group has a queued diagnose in process, this will be set so
+   * we can coalesce the dispatch of everything at the same time.
+   */
+  guint queued_diagnose_source;
+};
+
+enum {
+  PROP_0,
+  PROP_BUSY,
+  N_PROPS
+};
+
+enum {
+  CHANGED,
+  N_SIGNALS
+};
+
+
+static void     initable_iface_init                       (GInitableIface        *iface);
+static gboolean ide_diagnostics_manager_clear_by_provider (IdeDiagnosticsManager *self,
+                                                           IdeDiagnosticProvider *provider);
+static void     ide_diagnostics_manager_add_diagnostic    (IdeDiagnosticsManager *self,
+                                                           IdeDiagnosticProvider *provider,
+                                                           IdeDiagnostic         *diagnostic);
+static void     ide_diagnostics_group_queue_diagnose      (IdeDiagnosticsGroup   *group,
+                                                           IdeDiagnosticsManager *self);
+
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+
+G_DEFINE_TYPE_WITH_CODE (IdeDiagnosticsManager, ide_diagnostics_manager, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init))
+
+
+static void
+free_diagnostics (gpointer data)
+{
+  IdeDiagnostics *diagnostics = data;
+
+  if (diagnostics != NULL)
+    ide_diagnostics_unref (diagnostics);
+}
+
+static void
+ide_diagnostics_group_free (gpointer data)
+{
+  IdeDiagnosticsGroup *group = data;
+
+  g_assert (group != NULL);
+  g_assert (group->ref_count == 0);
+
+  g_clear_pointer (&group->diagnostics_by_provider, g_hash_table_unref);
+  g_weak_ref_clear (&group->buffer_wr);
+  g_clear_object (&group->adapter);
+  g_clear_object (&group->file);
+  g_slice_free (IdeDiagnosticsGroup, group);
+}
+
+static IdeDiagnosticsGroup *
+ide_diagnostics_group_new (GFile *file)
+{
+  IdeDiagnosticsGroup *group;
+
+  g_assert (G_IS_FILE (file));
+
+  group = g_slice_new0 (IdeDiagnosticsGroup);
+  group->ref_count = 1;
+  group->file = g_object_ref (file);
+
+  g_weak_ref_init (&group->buffer_wr, NULL);
+
+  return group;
+}
+
+static IdeDiagnosticsGroup *
+ide_diagnostics_group_ref (IdeDiagnosticsGroup *group)
+{
+  g_assert (group != NULL);
+  g_assert (group->ref_count > 0);
+
+  group->ref_count++;
+
+  return group;
+}
+
+static void
+ide_diagnostics_group_unref (IdeDiagnosticsGroup *group)
+{
+  g_assert (group != NULL);
+  g_assert (group->ref_count > 0);
+
+  group->ref_count--;
+
+  if (group->ref_count == 0)
+    ide_diagnostics_group_free (group);
+}
+
+static guint
+ide_diagnostics_group_has_diagnostics (IdeDiagnosticsGroup *group)
+{
+  g_assert (group != NULL);
+
+  if (group->diagnostics_by_provider != NULL)
+    {
+      GHashTableIter iter;
+      gpointer value;
+
+      g_hash_table_iter_init (&iter, group->diagnostics_by_provider);
+
+      while (g_hash_table_iter_next (&iter, NULL, &value))
+        {
+          IdeDiagnostics *diagnostics = value;
+
+          if (diagnostics != NULL && ide_diagnostics_get_size (diagnostics) > 0)
+            return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+static gboolean
+ide_diagnostics_group_can_dispose (IdeDiagnosticsGroup *group)
+{
+  g_assert (group != NULL);
+
+  /*
+   * We can cleanup this group if we don't have a buffer loaded and
+   * the adapters have been unloaded and there are no diagnostics
+   * registered for the group.
+   */
+
+  return (g_weak_ref_get (&group->buffer_wr) == NULL) &&
+         (group->adapter == NULL) &&
+         (group->has_diagnostics == FALSE);
+}
+
+static void
+ide_diagnostics_group_add (IdeDiagnosticsGroup   *group,
+                           IdeDiagnosticProvider *provider,
+                           IdeDiagnostic         *diagnostic)
+{
+  IdeDiagnostics *diagnostics;
+
+  g_assert (group != NULL);
+  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
+  g_assert (diagnostic != NULL);
+
+  if (group->diagnostics_by_provider == NULL)
+    group->diagnostics_by_provider = g_hash_table_new_full (NULL, NULL, NULL, free_diagnostics);
+
+  diagnostics = g_hash_table_lookup (group->diagnostics_by_provider, provider);
+
+  if (diagnostics == NULL)
+    {
+      diagnostics = ide_diagnostics_new (NULL);
+      g_hash_table_insert (group->diagnostics_by_provider, provider, diagnostics);
+    }
+
+  ide_diagnostics_add (diagnostics, diagnostic);
+
+  group->has_diagnostics = TRUE;
+  group->sequence++;
+}
+
+static void
+ide_diagnostics_group_diagnose_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  IdeDiagnosticProvider *provider = (IdeDiagnosticProvider *)object;
+  g_autoptr(IdeDiagnosticsManager) self = user_data;
+  g_autoptr(IdeDiagnostics) diagnostics = NULL;
+  g_autoptr(GError) error = NULL;
+  IdeDiagnosticsGroup *group;
+  gboolean changed;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+
+  IDE_TRACE_MSG ("%s diagnosis completed", G_OBJECT_TYPE_NAME (provider));
+
+  diagnostics = ide_diagnostic_provider_diagnose_finish (provider, result, &error);
+
+  if (error != NULL)
+    g_warning ("%s", error->message);
+
+  /*
+   * This fetches the group our provider belongs to. Since the group is
+   * reference counted (and we only release it when our provider is
+   * finalized), we should be guaranteed we have a valid group.
+   */
+  group = g_object_get_data (G_OBJECT (provider), "IDE_DIAGNOSTICS_GROUP");
+  g_assert (group != NULL);
+
+  /*
+   * Clear all of our old diagnostics no matter where they ended up.
+   */
+  changed = ide_diagnostics_manager_clear_by_provider (self, provider);
+
+  /*
+   * The following adds diagnostics to the appropriate group, but tries the
+   * group we belong to first as our fast path. That will almost always be
+   * the case, except when a diagnostic came up for a header or something
+   * while parsing a given file.
+   */
+  if (diagnostics != NULL)
+    {
+      guint length = ide_diagnostics_get_size (diagnostics);
+
+      for (guint i = 0; i < length; i++)
+        {
+          IdeDiagnostic *diagnostic = ide_diagnostics_index (diagnostics, i);
+          GFile *file = ide_diagnostic_get_file (diagnostic);
+
+          if G_LIKELY (g_file_equal (file, group->file))
+            ide_diagnostics_group_add (group, provider, diagnostic);
+          else
+            ide_diagnostics_manager_add_diagnostic (self, provider, diagnostic);
+        }
+
+      if (length > 0)
+        changed = TRUE;
+    }
+
+  group->in_diagnose--;
+
+  /*
+   * Ensure we increment our sequence number even when no diagnostics were
+   * reported. This ensures that the gutter gets cleared and line-flags
+   * cache updated.
+   */
+  if (group->in_diagnose == 0)
+    group->sequence++;
+
+  /*
+   * Since the individual groups have sequence numbers associated with changes,
+   * it's okay to emit this for every provider completion. That allows the UIs
+   * to update faster as each provider completes at the expensive of a little
+   * more CPU activity.
+   */
+  if (changed)
+    g_signal_emit (self, signals [CHANGED], 0);
+
+  /*
+   * If there are no more diagnostics providers active and the group needs
+   * another diagnosis, then we can start the next one now.
+   *
+   * If we are completing this diagnosis and the buffer was already released
+   * (and other diagnose providers have unloaded), we might be able to clean
+   * up the group and be done with things.
+   */
+  if (group->was_removed == FALSE && group->in_diagnose == 0 && group->needs_diagnose)
+    {
+      ide_diagnostics_group_queue_diagnose (group, self);
+    }
+  else if (ide_diagnostics_group_can_dispose (group))
+    {
+      group->was_removed = TRUE;
+      g_hash_table_remove (self->groups_by_file, group->file);
+      IDE_EXIT;
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_diagnostics_group_diagnose_foreach (IdeExtensionSetAdapter *adapter,
+                                        PeasPluginInfo         *plugin_info,
+                                        PeasExtension          *exten,
+                                        gpointer                user_data)
+{
+  IdeDiagnosticProvider *provider = (IdeDiagnosticProvider *)exten;
+  IdeDiagnosticsManager *self = user_data;
+  IdeDiagnosticsGroup *group;
+  IdeContext *context;
+  g_autoptr(IdeFile) file = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
+
+  group = g_object_get_data (G_OBJECT (provider), "IDE_DIAGNOSTICS_GROUP");
+  group->in_diagnose++;
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+
+  file = g_object_new (IDE_TYPE_FILE,
+                       "context", context,
+                       "file", group->file,
+                       NULL);
+
+#ifdef IDE_ENABLE_TRACE
+  {
+    g_autofree gchar *uri = g_file_get_uri (group->file);
+    IDE_TRACE_MSG ("Beginning diagnose on %s with provider %s",
+                   uri, G_OBJECT_TYPE_NAME (provider));
+  }
+#endif
+
+  ide_diagnostic_provider_diagnose_async (provider,
+                                          file,
+                                          NULL,
+                                          ide_diagnostics_group_diagnose_cb,
+                                          g_object_ref (self));
+}
+
+static void
+ide_diagnostics_group_diagnose (IdeDiagnosticsGroup   *group,
+                                IdeDiagnosticsManager *self)
+{
+  IdeBuffer *buffer;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (group != NULL);
+  g_assert (group->in_diagnose == FALSE);
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (group->adapter));
+
+  group->needs_diagnose = FALSE;
+  group->has_diagnostics = FALSE;
+
+  /*
+   * We need to ensure that all the diagnostic providers have access to the
+   * proper data within the unsaved files. So sync the content once to avoid
+   * all providers from having to do this manually.
+   */
+  if (NULL != (buffer = g_weak_ref_get (&group->buffer_wr)))
+    ide_buffer_sync_to_unsaved_files (buffer);
+
+  ide_extension_set_adapter_foreach (group->adapter,
+                                     ide_diagnostics_group_diagnose_foreach,
+                                     self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_diagnostics_manager_begin_diagnose (gpointer data)
+{
+  IdeDiagnosticsManager *self = data;
+  GHashTableIter iter;
+  gpointer value;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+
+  self->queued_diagnose_source = 0;
+
+  g_hash_table_iter_init (&iter, self->groups_by_file);
+
+  while (g_hash_table_iter_next (&iter, NULL, &value))
+    {
+      IdeDiagnosticsGroup *group = value;
+
+      if (group->needs_diagnose)
+        ide_diagnostics_group_diagnose (group, self);
+    }
+
+  IDE_RETURN (G_SOURCE_REMOVE);
+}
+
+static void
+ide_diagnostics_group_queue_diagnose (IdeDiagnosticsGroup   *group,
+                                      IdeDiagnosticsManager *self)
+{
+  g_assert (group != NULL);
+
+  /*
+   * This checks to see if we are diagnosing and if not queues a diagnose.
+   * If a diagnosis is already running, we don't need to do anything now
+   * because the completion of the diagnose will tick off the next diagnose
+   * upon seening group->needs_diagnose==TRUE.
+   */
+
+  group->needs_diagnose = TRUE;
+
+  if (group->in_diagnose == 0 && self->queued_diagnose_source == 0)
+    self->queued_diagnose_source =
+      gdk_threads_add_idle_full (G_PRIORITY_DEFAULT,
+                                 ide_diagnostics_manager_begin_diagnose,
+                                 g_object_ref (self),
+                                 g_object_unref);
+}
+
+static void
+ide_diagnostics_manager_finalize (GObject *object)
+{
+  IdeDiagnosticsManager *self = (IdeDiagnosticsManager *)object;
+
+  ide_clear_source (&self->queued_diagnose_source);
+  g_clear_pointer (&self->groups_by_file, g_hash_table_unref);
+
+  G_OBJECT_CLASS (ide_diagnostics_manager_parent_class)->finalize (object);
+}
+
+static void
+ide_diagnostics_manager_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  IdeDiagnosticsManager *self = (IdeDiagnosticsManager *)object;
+
+  switch (prop_id)
+    {
+    case PROP_BUSY:
+      g_value_set_boolean (value, ide_diagnostics_manager_get_busy (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_diagnostics_manager_class_init (IdeDiagnosticsManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_diagnostics_manager_finalize;
+  object_class->get_property = ide_diagnostics_manager_get_property;
+
+  properties [PROP_BUSY] =
+    g_param_spec_boolean ("busy",
+                          "Busy",
+                          "If the diagnostics manager is busy",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeDiagnosticsManager::changed:
+   * @self: A #IdeDiagnosticsManager
+   *
+   * This signal is emitted when the diagnostics have changed for any
+   * file managed by the IdeDiagnosticsManager.
+   */
+  signals [CHANGED] =
+    g_signal_new ("changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+}
+
+static void
+ide_diagnostics_manager_init (IdeDiagnosticsManager *self)
+{
+  self->groups_by_file = g_hash_table_new_full (g_file_hash,
+                                                (GEqualFunc)g_file_equal,
+                                                NULL,
+                                                (GDestroyNotify)ide_diagnostics_group_unref);
+}
+
+static void
+ide_diagnostics_manager_add_diagnostic (IdeDiagnosticsManager *self,
+                                        IdeDiagnosticProvider *provider,
+                                        IdeDiagnostic         *diagnostic)
+{
+  IdeDiagnosticsGroup *group;
+  GFile *file;
+
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
+  g_assert (diagnostic != NULL);
+
+  /*
+   * This is our slow path for adding a diagnostic to the system. We have
+   * to locate the proper group for the diagnostic and then insert it
+   * into that group.
+   */
+
+  if (NULL == (file = ide_diagnostic_get_file (diagnostic)))
+    return;
+
+  group = g_hash_table_lookup (self->groups_by_file, file);
+
+  if (group == NULL)
+    {
+      group = ide_diagnostics_group_new (file);
+      g_hash_table_insert (self->groups_by_file, group->file, group);
+    }
+
+  ide_diagnostics_group_add (group, provider, diagnostic);
+}
+
+static IdeDiagnosticsGroup *
+ide_diagnostics_manager_find_group_from_buffer (IdeDiagnosticsManager *self,
+                                                IdeBuffer             *buffer)
+{
+  IdeDiagnosticsGroup *group;
+  IdeFile *ifile;
+  GFile *gfile;
+
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  ifile = ide_buffer_get_file (buffer);
+  gfile = ide_file_get_file (ifile);
+  group = g_hash_table_lookup (self->groups_by_file, gfile);
+
+  g_assert (group != NULL);
+
+  return group;
+}
+
+static IdeDiagnosticsGroup *
+ide_diagnostics_manager_find_group_from_adapter (IdeDiagnosticsManager  *self,
+                                                 IdeExtensionSetAdapter *adapter)
+{
+  GHashTableIter iter;
+  gpointer value;
+
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+
+  g_hash_table_iter_init (&iter, self->groups_by_file);
+
+  while (g_hash_table_iter_next (&iter, NULL, &value))
+    {
+      IdeDiagnosticsGroup *group = value;
+
+      if (group->adapter == adapter)
+        return group;
+    }
+
+  g_assert_not_reached ();
+
+  return NULL;
+}
+
+static void
+ide_diagnostics_manager_provider_invalidated (IdeDiagnosticsManager *self,
+                                              IdeDiagnosticProvider *provider)
+{
+  IdeDiagnosticsGroup *group;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
+
+  group = g_object_get_data (G_OBJECT (provider), "IDE_DIAGNOSTICS_GROUP");
+
+  ide_diagnostics_group_queue_diagnose (group, self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_diagnostics_manager_extension_added (IdeExtensionSetAdapter *adapter,
+                                         PeasPluginInfo         *plugin_info,
+                                         PeasExtension          *exten,
+                                         gpointer                user_data)
+{
+  IdeDiagnosticProvider *provider = (IdeDiagnosticProvider *)exten;
+  IdeDiagnosticsManager *self = user_data;
+  IdeDiagnosticsGroup *group;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+
+  group = ide_diagnostics_manager_find_group_from_adapter (self, adapter);
+
+  /*
+   * We will need access to the group upon completion of the diagnostics,
+   * so we add a reference to the group and allow it to be automatically
+   * cleaned up when the provider finalizes.
+   */
+  g_object_set_data_full (G_OBJECT (provider),
+                          "IDE_DIAGNOSTICS_GROUP",
+                          ide_diagnostics_group_ref (group),
+                          (GDestroyNotify)ide_diagnostics_group_unref);
+
+  /*
+   * We insert a dummy entry into the hashtable upon creation so
+   * that when an async diagnosis completes we can use the presence
+   * of this key to know if we've been unloaded.
+   */
+  g_hash_table_insert (group->diagnostics_by_provider, provider, NULL);
+
+  /*
+   * We need to keep track of when the provider has been invalidated so
+   * that we can queue another request to fetch the diagnostics.
+   */
+  g_signal_connect_object (provider,
+                           "invalidated",
+                           G_CALLBACK (ide_diagnostics_manager_provider_invalidated),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  ide_diagnostics_group_queue_diagnose (group, self);
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_diagnostics_manager_clear_by_provider (IdeDiagnosticsManager *self,
+                                           IdeDiagnosticProvider *provider)
+{
+  GHashTableIter iter;
+  gpointer value;
+  gboolean changed = FALSE;
+
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
+
+  g_hash_table_iter_init (&iter, self->groups_by_file);
+
+  while (g_hash_table_iter_next (&iter, NULL, &value))
+    {
+      IdeDiagnosticsGroup *group = value;
+
+      if (group->diagnostics_by_provider != NULL)
+        {
+          g_hash_table_remove (group->diagnostics_by_provider, provider);
+
+          /*
+           * If we caused this hashtable to become empty, we can release the
+           * hashtable. The hashtable is guaranteed to not be empty if there
+           * are other providers loaded for this group.
+           */
+          if (g_hash_table_size (group->diagnostics_by_provider) == 0)
+            g_clear_pointer (&group->diagnostics_by_provider, g_hash_table_unref);
+
+          /*
+           * TODO: If this provider is not part of this group, we can possibly
+           *       dispose of the group if there are no diagnostics.
+           */
+
+          changed = TRUE;
+        }
+    }
+
+  return changed;
+}
+
+static void
+ide_diagnostics_manager_extension_removed (IdeExtensionSetAdapter *adapter,
+                                           PeasPluginInfo         *plugin_info,
+                                           PeasExtension          *exten,
+                                           gpointer                user_data)
+{
+  IdeDiagnosticProvider *provider = (IdeDiagnosticProvider *)exten;
+  IdeDiagnosticsManager *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+
+  g_signal_handlers_disconnect_by_func (provider,
+                                        G_CALLBACK (ide_diagnostics_manager_provider_invalidated),
+                                        self);
+
+  /*
+   * The goal of the following is to reomve our diagnostics from any file
+   * that has been loaded. It is possible for diagnostic providers to effect
+   * files outside the buffer they are loaded for and this ensures that we
+   * clean those up.
+   */
+  ide_diagnostics_manager_clear_by_provider (self, provider);
+
+  IDE_EXIT;
+}
+
+static void
+ide_diagnostics_manager_buffer_changed (IdeDiagnosticsManager *self,
+                                        IdeBuffer             *buffer)
+{
+  IdeDiagnosticsGroup *group;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  group = ide_diagnostics_manager_find_group_from_buffer (self, buffer);
+  ide_diagnostics_group_queue_diagnose (group, self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_diagnostics_manager_buffer_notify_language (IdeDiagnosticsManager *self,
+                                                GParamSpec            *pspec,
+                                                IdeBuffer             *buffer)
+{
+  IdeDiagnosticsGroup *group;
+  GtkSourceLanguage *language;
+  const gchar *language_id = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (pspec != NULL);
+  g_assert (g_str_equal (pspec->name, "language"));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  /*
+   * The goal here is to get the new language_id for the buffer and
+   * alter the set of loaded diagnostic providers to match those registered
+   * for the particular language_id. IdeExtensionSetAdapter does most of
+   * the hard work, we just need to update the "match value".
+   */
+
+  language = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (buffer));
+  if (language != NULL)
+    language_id = gtk_source_language_get_id (language);
+  group = ide_diagnostics_manager_find_group_from_buffer (self, buffer);
+  ide_extension_set_adapter_set_value (group->adapter, language_id);
+
+  IDE_EXIT;
+}
+
+static void
+ide_diagnostics_manager_buffer_notify_file (IdeDiagnosticsManager *self,
+                                            GParamSpec            *pspec,
+                                            IdeBuffer             *buffer)
+{
+  GHashTableIter iter;
+  IdeFile *ifile;
+  GFile *gfile;
+  gpointer value;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (pspec != NULL);
+  g_assert (g_str_equal (pspec->name, "file"));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  /*
+   * The goal here is to steal the group that is in the hash table using
+   * the old GFile, and replace it with the new GFile. That means removing
+   * the group from the hashtable, changing the file field, and then
+   * reinserting with our new file key.
+   */
+
+  ifile = ide_buffer_get_file (buffer);
+  gfile = ide_file_get_file (ifile);
+
+  g_hash_table_iter_init (&iter, self->groups_by_file);
+
+  while (g_hash_table_iter_next (&iter, NULL, &value))
+    {
+      IdeDiagnosticsGroup *group = value;
+
+      if (buffer == g_weak_ref_get (&group->buffer_wr))
+        {
+          g_hash_table_steal (self->groups_by_file, group->file);
+          g_set_object (&group->file, gfile);
+          g_hash_table_insert (self->groups_by_file, group->file, group);
+          IDE_EXIT;
+        }
+    }
+
+  g_assert_not_reached ();
+
+  IDE_EXIT;
+}
+
+static void
+ide_diagnostics_manager_buffer_loaded (IdeDiagnosticsManager *self,
+                                       IdeBuffer             *buffer,
+                                       IdeBufferManager      *buffer_manager)
+{
+  IdeDiagnosticsGroup *group;
+  GtkSourceLanguage *language;
+  const gchar *language_id = NULL;
+  IdeContext *context;
+  IdeFile *ifile;
+  GFile *gfile;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (IDE_IS_BUFFER_MANAGER (buffer_manager));
+
+  /*
+   * The goal below is to setup all of our state needed for tracking
+   * diagnostics during the lifetime of the buffer. That includes tracking
+   * a few properties to update our providers at runtime, along with
+   * lifecycle tracking. At the end, we fire off a diagnosis so that we
+   * have up to date diagnostics as soon as we can.
+   */
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+
+  g_signal_connect_object (buffer,
+                           "changed",
+                           G_CALLBACK (ide_diagnostics_manager_buffer_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (buffer,
+                           "notify::file",
+                           G_CALLBACK (ide_diagnostics_manager_buffer_notify_file),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (buffer,
+                           "notify::language",
+                           G_CALLBACK (ide_diagnostics_manager_buffer_notify_language),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  ifile = ide_buffer_get_file (buffer);
+  gfile = ide_file_get_file (ifile);
+
+  group = g_hash_table_lookup (self->groups_by_file, gfile);
+
+  if (group == NULL)
+    {
+      group = ide_diagnostics_group_new (gfile);
+      g_hash_table_insert (self->groups_by_file, group->file, group);
+    }
+
+  g_weak_ref_init (&group->buffer_wr, buffer);
+
+  language = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (buffer));
+
+  if (language != NULL)
+    language_id = gtk_source_language_get_id (language);
+
+  group->diagnostics_by_provider = g_hash_table_new_full (NULL,
+                                                          NULL,
+                                                          NULL,
+                                                          free_diagnostics);
+
+  group->adapter = ide_extension_set_adapter_new (context,
+                                                  peas_engine_get_default (),
+                                                  IDE_TYPE_DIAGNOSTIC_PROVIDER,
+                                                  "Diagnostic-Provider-Languages",
+                                                  language_id);
+
+  g_signal_connect_object (group->adapter,
+                           "extension-added",
+                           G_CALLBACK (ide_diagnostics_manager_extension_added),
+                           self,
+                           0);
+
+  g_signal_connect_object (group->adapter,
+                           "extension-removed",
+                           G_CALLBACK (ide_diagnostics_manager_extension_removed),
+                           self,
+                           0);
+
+  ide_diagnostics_group_queue_diagnose (group, self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_diagnostics_manager_buffer_unloaded (IdeDiagnosticsManager *self,
+                                         IdeBuffer             *buffer,
+                                         IdeBufferManager      *buffer_manager)
+{
+  IdeDiagnosticsGroup *group;
+  gboolean has_diagnostics;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (IDE_IS_BUFFER_MANAGER (buffer_manager));
+
+  /*
+   * The goal here is to cleanup everything we can about this group that
+   * is part of a loaded buffer. We might want to keep the group around
+   * in case it is useful from other providers.
+   */
+
+  group = ide_diagnostics_manager_find_group_from_buffer (self, buffer);
+
+  /*
+   * We track if we have diagnostics now so that after we unload the
+   * the providers, we can save that bit for later.
+   */
+  has_diagnostics = ide_diagnostics_group_has_diagnostics (group);
+
+  /*
+   * Force our diagnostic providers to unload. This will cause them
+   * extension-removed signal to be called for each provider which
+   * in turn will perform per-provider cleanup including the removal
+   * of its diagnostics from all groups. (A provider can in practice
+   * affect another group since a .c file could create a diagnostic
+   * for a .h).
+   */
+  g_clear_object (&group->adapter);
+
+  /*
+   * Even after unloading the diagnostic providers, we might still have
+   * diagnostics that were created from other files (this could happen when
+   * one diagnostic is created for a header from a source file). So we don't
+   * want to wipe out the hashtable unless everything was unloaded. The other
+   * provider will cleanup during it's own destruction.
+   */
+  if (group->diagnostics_by_provider != NULL &&
+      g_hash_table_size (group->diagnostics_by_provider) == 0)
+    g_clear_pointer (&group->diagnostics_by_provider, g_hash_table_unref);
+
+  g_signal_handlers_disconnect_by_func (buffer,
+                                        G_CALLBACK (ide_diagnostics_manager_buffer_changed),
+                                        self);
+
+  g_signal_handlers_disconnect_by_func (buffer,
+                                        G_CALLBACK (ide_diagnostics_manager_buffer_notify_file),
+                                        self);
+
+  g_signal_handlers_disconnect_by_func (buffer,
+                                        G_CALLBACK (ide_diagnostics_manager_buffer_notify_language),
+                                        self);
+
+  g_weak_ref_clear (&group->buffer_wr);
+
+  group->has_diagnostics = has_diagnostics;
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_diagnostics_manager_initable_init (GInitable     *initable,
+                                       GCancellable  *cancellable,
+                                       GError       **error)
+{
+  IdeDiagnosticsManager *self = (IdeDiagnosticsManager *)initable;
+  IdeBufferManager *buffer_manager;
+  IdeContext *context;
+  guint n_items;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  buffer_manager = ide_context_get_buffer_manager (context);
+
+  g_signal_connect_object (buffer_manager,
+                           "buffer-loaded",
+                           G_CALLBACK (ide_diagnostics_manager_buffer_loaded),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (buffer_manager,
+                           "buffer-unloaded",
+                           G_CALLBACK (ide_diagnostics_manager_buffer_unloaded),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (buffer_manager));
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(IdeBuffer) buffer = NULL;
+
+      buffer = g_list_model_get_item (G_LIST_MODEL (buffer_manager), i);
+      ide_diagnostics_manager_buffer_loaded (self, buffer, buffer_manager);
+    }
+
+  IDE_RETURN (TRUE);
+}
+
+static void
+initable_iface_init (GInitableIface *iface)
+{
+  iface->init = ide_diagnostics_manager_initable_init;
+}
+
+/**
+ * ide_diagnostics_manager_get_busy:
+ *
+ * Gets if the diagnostics manager is currently executing a diagnosis.
+ *
+ * Returns: %TRUE if the #IdeDiagnosticsManager is busy diagnosing.
+ */
+gboolean
+ide_diagnostics_manager_get_busy (IdeDiagnosticsManager *self)
+{
+  GHashTableIter iter;
+  gpointer value;
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self), FALSE);
+
+  g_hash_table_iter_init (&iter, self->groups_by_file);
+
+  while (g_hash_table_iter_next (&iter, NULL, &value))
+    {
+      IdeDiagnosticsGroup *group = value;
+
+      if (group->in_diagnose > 0)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+/**
+ * ide_diagnostics_manager_get_diagnostics_for_file:
+ * @self: An #IdeDiagnosticsManager
+ * @file: A #GFile to retrieve diagnostics for
+ *
+ * This function collects all of the diagnostics that have been collected
+ * for @file and returns them as a new #IdeDiagnostics to the caller.
+ *
+ * The #IdeDiagnostics structure will contain zero items if there are
+ * no diagnostics discovered. Therefore, this function will never return
+ * a %NULL value.
+ *
+ * Returns: (transfer full): A new #IdeDiagnostics.
+ */
+IdeDiagnostics *
+ide_diagnostics_manager_get_diagnostics_for_file (IdeDiagnosticsManager *self,
+                                                  GFile                 *file)
+{
+  g_autoptr(IdeDiagnostics) ret = NULL;
+  IdeDiagnosticsGroup *group;
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self), NULL);
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+
+  ret = ide_diagnostics_new (NULL);
+
+  group = g_hash_table_lookup (self->groups_by_file, file);
+
+  if (group != NULL && group->diagnostics_by_provider != NULL)
+    {
+      GHashTableIter iter;
+      gpointer value;
+
+      g_hash_table_iter_init (&iter, group->diagnostics_by_provider);
+
+      while (g_hash_table_iter_next (&iter, NULL, &value))
+        {
+          IdeDiagnostics *diagnostics = value;
+          guint length;
+
+          if (diagnostics == NULL)
+            continue;
+
+          length = ide_diagnostics_get_size (diagnostics);
+
+          for (guint i = 0; i < length; i++)
+            {
+              IdeDiagnostic *diagnostic = ide_diagnostics_index (diagnostics, i);
+
+              ide_diagnostics_add (ret, diagnostic);
+            }
+        }
+    }
+
+  return g_steal_pointer (&ret);
+}
+
+guint
+ide_diagnostics_manager_get_sequence_for_file (IdeDiagnosticsManager *self,
+                                               GFile                 *file)
+{
+  IdeDiagnosticsGroup *group;
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self), 0);
+  g_return_val_if_fail (G_IS_FILE (file), 0);
+
+  group = g_hash_table_lookup (self->groups_by_file, file);
+
+  if (group != NULL)
+    return group->sequence;
+
+  return 0;
+}
diff --git a/libide/diagnostics/ide-diagnostics-manager.h b/libide/diagnostics/ide-diagnostics-manager.h
new file mode 100644
index 0000000..19d5c4a
--- /dev/null
+++ b/libide/diagnostics/ide-diagnostics-manager.h
@@ -0,0 +1,40 @@
+/* ide-diagnostics-manager.h
+ *
+ * Copyright (C) 2016 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/>.
+ */
+
+#ifndef IDE_DIAGNOSTICS_MANAGER_H
+#define IDE_DIAGNOSTICS_MANAGER_H
+
+#include <gio/gio.h>
+
+#include "ide-object.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DIAGNOSTICS_MANAGER (ide_diagnostics_manager_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDiagnosticsManager, ide_diagnostics_manager, IDE, DIAGNOSTICS_MANAGER, IdeObject)
+
+gboolean        ide_diagnostics_manager_get_busy                 (IdeDiagnosticsManager *self);
+IdeDiagnostics *ide_diagnostics_manager_get_diagnostics_for_file (IdeDiagnosticsManager *self,
+                                                                  GFile                 *file);
+guint           ide_diagnostics_manager_get_sequence_for_file    (IdeDiagnosticsManager *self,
+                                                                  GFile                 *file);
+
+G_END_DECLS
+
+#endif /* IDE_DIAGNOSTICS_MANAGER_H */
diff --git a/libide/ide-context.c b/libide/ide-context.c
index d86beb3..40d5523 100644
--- a/libide/ide-context.c
+++ b/libide/ide-context.c
@@ -34,6 +34,7 @@
 #include "buildsystem/ide-build-manager.h"
 #include "buildsystem/ide-build-system.h"
 #include "buildsystem/ide-configuration-manager.h"
+#include "diagnostics/ide-diagnostics-manager.h"
 #include "devices/ide-device-manager.h"
 #include "doap/ide-doap.h"
 #include "history/ide-back-forward-list-private.h"
@@ -65,6 +66,7 @@ struct _IdeContext
   IdeBuildManager          *build_manager;
   IdeBuildSystem           *build_system;
   IdeConfigurationManager  *configuration_manager;
+  IdeDiagnosticsManager    *diagnostics_manager;
   IdeDeviceManager         *device_manager;
   IdeDoap                  *doap;
   GtkRecentManager         *recent_manager;
@@ -835,6 +837,10 @@ ide_context_init (IdeContext *self)
                                       "context", self,
                                       NULL);
 
+  self->diagnostics_manager = g_object_new (IDE_TYPE_DIAGNOSTICS_MANAGER,
+                                            "context", self,
+                                            NULL);
+
   self->device_manager = g_object_new (IDE_TYPE_DEVICE_MANAGER,
                                        "context", self,
                                        NULL);
@@ -1515,6 +1521,27 @@ ide_context_init_configuration_manager (gpointer             source_object,
 }
 
 static void
+ide_context_init_diagnostics_manager (gpointer             source_object,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GTask) task = NULL;
+  IdeContext *self = source_object;
+
+  g_assert (IDE_IS_CONTEXT (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+
+  if (!g_initable_init (G_INITABLE (self->diagnostics_manager), cancellable, &error))
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    g_task_return_boolean (task, TRUE);
+}
+
+static void
 ide_context_init_loaded (gpointer             source_object,
                          GCancellable        *cancellable,
                          GAsyncReadyCallback  callback,
@@ -1560,6 +1587,7 @@ ide_context_init_async (GAsyncInitable      *initable,
                         ide_context_init_search_engine,
                         ide_context_init_runtimes,
                         ide_context_init_configuration_manager,
+                        ide_context_init_diagnostics_manager,
                         ide_context_init_loaded,
                         NULL);
 }
@@ -2239,3 +2267,18 @@ ide_context_get_transfer_manager (IdeContext *self)
 
   return self->transfer_manager;
 }
+
+/**
+ * ide_context_get_diagnostics_manager:
+ *
+ * Gets the #IdeDiagnosticsManager for the context.
+ *
+ * Returns: (transfer none): An #IdeDiagnosticsManager.
+ */
+IdeDiagnosticsManager *
+ide_context_get_diagnostics_manager (IdeContext *self)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+  return self->diagnostics_manager;
+}
diff --git a/libide/ide-context.h b/libide/ide-context.h
index 1a72a0d..b3395b8 100644
--- a/libide/ide-context.h
+++ b/libide/ide-context.h
@@ -36,6 +36,7 @@ IdeBufferManager         *ide_context_get_buffer_manager        (IdeContext
 IdeBuildManager          *ide_context_get_build_manager         (IdeContext           *self);
 IdeBuildSystem           *ide_context_get_build_system          (IdeContext           *self);
 IdeConfigurationManager  *ide_context_get_configuration_manager (IdeContext           *self);
+IdeDiagnosticsManager    *ide_context_get_diagnostics_manager   (IdeContext           *self);
 IdeDeviceManager         *ide_context_get_device_manager        (IdeContext           *self);
 IdeProject               *ide_context_get_project               (IdeContext           *self);
 GtkRecentManager         *ide_context_get_recent_manager        (IdeContext           *self);
diff --git a/libide/ide-types.h b/libide/ide-types.h
index c0ebd79..230ebaa 100644
--- a/libide/ide-types.h
+++ b/libide/ide-types.h
@@ -53,12 +53,9 @@ typedef struct _IdeDeviceManager               IdeDeviceManager;
 typedef struct _IdeDeviceProvider              IdeDeviceProvider;
 
 typedef struct _IdeDiagnostic                  IdeDiagnostic;
-
-typedef struct _IdeDiagnostics                 IdeDiagnostics;
-
-typedef struct _IdeDiagnostician               IdeDiagnostician;
-
 typedef struct _IdeDiagnosticProvider          IdeDiagnosticProvider;
+typedef struct _IdeDiagnostics                 IdeDiagnostics;
+typedef struct _IdeDiagnosticsManager          IdeDiagnosticsManager;
 
 typedef struct _IdeEnvironment                 IdeEnvironment;
 typedef struct _IdeEnvironmentVariable         IdeEnvironmentVariable;
diff --git a/libide/ide.h b/libide/ide.h
index 81a2395..d8f9f61 100644
--- a/libide/ide.h
+++ b/libide/ide.h
@@ -52,7 +52,7 @@ G_BEGIN_DECLS
 #include "devices/ide-device.h"
 #include "diagnostics/ide-diagnostic-provider.h"
 #include "diagnostics/ide-diagnostic.h"
-#include "diagnostics/ide-diagnostician.h"
+#include "diagnostics/ide-diagnostics-manager.h"
 #include "diagnostics/ide-diagnostics.h"
 #include "diagnostics/ide-source-location.h"
 #include "diagnostics/ide-source-range.h"
diff --git a/libidemm/src/libide_methods.defs b/libidemm/src/libide_methods.defs
index 8474b73..57d0424 100644
--- a/libidemm/src/libide_methods.defs
+++ b/libidemm/src/libide_methods.defs
@@ -2047,47 +2047,6 @@
 
 
 
-;; From ide-diagnostician.h
-
-(define-method get_language
-  (of-object "IdeDiagnostician")
-  (c-name "ide_diagnostician_get_language")
-  (return-type "GtkSourceLanguage*")
-)
-
-(define-method set_language
-  (of-object "IdeDiagnostician")
-  (c-name "ide_diagnostician_set_language")
-  (return-type "none")
-  (parameters
-    '("GtkSourceLanguage*" "language")
-  )
-)
-
-(define-method diagnose_async
-  (of-object "IdeDiagnostician")
-  (c-name "ide_diagnostician_diagnose_async")
-  (return-type "none")
-  (parameters
-    '("IdeFile*" "file")
-    '("GCancellable*" "cancellable")
-    '("GAsyncReadyCallback" "callback")
-    '("gpointer" "user_data")
-  )
-)
-
-(define-method diagnose_finish
-  (of-object "IdeDiagnostician")
-  (c-name "ide_diagnostician_diagnose_finish")
-  (return-type "IdeDiagnostics*")
-  (parameters
-    '("GAsyncResult*" "result")
-    '("GError**" "error")
-  )
-)
-
-
-
 ;; From ide-diagnostic-provider.h
 
 (define-method diagnose_async


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