[gnome-builder: 28/139] core: add new libide-core library



commit a9b12408642839dd9dd2e57771901c575d2f1c34
Author: Christian Hergert <chergert redhat com>
Date:   Wed Jan 9 15:38:04 2019 -0800

    core: add new libide-core library
    
    This is the center-most library that all the new static libraries will
    be based upon. It provides our core objects, and some new ways to work
    with things going forward.
    
    IdeObject is now a bit of a directed graph (without cycles). It provides
    a mechanism similar to GtkWidget to destroy hierarchies of objects without
    relying on the GObject.dispose directly upon final-unref. Doing so helps
    us ensure that we can cleanup objects even if a leak occurs somewhere and
    therefore reduce how much we leak.
    
    It also provides us a number of debugging tools going forward to get info
    about the object graph.
    
    You can encapsulate a non-IdeObject in an IdeObjectBox which allows
    associating it with a tree. IdeBuffer does this so that we can attach
    sub-objects to the buffer and have it cleaned up with the buffer. Various
    extension points use this for auto-cleanup.
    
    IdeNotification(s) is also used all over in the new design to track
    progress of operations. It has convenient UI components in libide-gui to
    make visualizing them easier in the carousel and elsewhere.
    
    The IdeContext is now just the root IdeObject. We no longer require loading
    a project into the context, which is useful in future scenarios like a
    project-less editor window.
    
    IdeObject provides more safety-checks around threading as well as built
    in support for locking parts of the graph. Plugins utilizing
    multi-threading should take advantage of this feature.

 src/libide/{ => core}/ide-build-ident.h.in         |    0
 src/libide/core/ide-context-addin.c                |  207 ++
 src/libide/core/ide-context-addin.h                |   73 +
 .../{ide-global.h => core/ide-context-private.h}   |   11 +-
 src/libide/core/ide-context.c                      |  855 ++++++
 src/libide/core/ide-context.h                      |   91 +
 src/libide/{ => core}/ide-debug.h.in               |    0
 src/libide/core/ide-global.c                       |  234 ++
 src/libide/core/ide-global.h                       |   66 +
 src/libide/core/ide-log.c                          |  380 +++
 src/libide/core/ide-log.h                          |   45 +
 src/libide/core/ide-macros.h                       |  249 ++
 src/libide/core/ide-notification.c                 | 1187 ++++++++
 src/libide/core/ide-notification.h                 |  143 +
 src/libide/core/ide-notifications.c                |  516 ++++
 src/libide/core/ide-notifications.h                |   48 +
 src/libide/core/ide-object-box.c                   |  289 ++
 src/libide/core/ide-object-box.h                   |   46 +
 src/libide/core/ide-object-notify.c                |  114 +
 src/libide/core/ide-object.c                       | 1367 +++++++++
 src/libide/core/ide-object.h                       |  156 +
 src/libide/core/ide-settings.c                     |  589 ++++
 src/libide/core/ide-settings.h                     |  111 +
 src/libide/core/ide-transfer-manager.c             |  493 ++++
 src/libide/core/ide-transfer-manager.h             |   58 +
 src/libide/core/ide-transfer.c                     |  522 ++++
 src/libide/core/ide-transfer.h                     |  101 +
 src/libide/{ => core}/ide-version-macros.h         |   15 +-
 src/libide/{ => core}/ide-version.h.in             |    0
 src/libide/core/libide-core.h                      |   43 +
 src/libide/core/meson.build                        |  124 +
 src/libide/ide-context.c                           | 3057 --------------------
 src/libide/ide-context.h                           |  158 -
 src/libide/ide-enums.c.in                          |   64 -
 src/libide/ide-enums.h.in                          |   26 -
 src/libide/ide-object.c                            |  877 ------
 src/libide/ide-object.h                            |   90 -
 src/libide/ide-pausable.c                          |  255 --
 src/libide/ide-pausable.h                          |   56 -
 src/libide/ide-service.c                           |  152 -
 src/libide/ide-service.h                           |   67 -
 src/libide/ide-types.h                             |  150 -
 src/libide/ide.c                                   |   89 -
 src/libide/ide.h                                   |  232 --
 44 files changed, 8117 insertions(+), 5289 deletions(-)
---
diff --git a/src/libide/ide-build-ident.h.in b/src/libide/core/ide-build-ident.h.in
similarity index 100%
rename from src/libide/ide-build-ident.h.in
rename to src/libide/core/ide-build-ident.h.in
diff --git a/src/libide/core/ide-context-addin.c b/src/libide/core/ide-context-addin.c
new file mode 100644
index 000000000..3f3071ffc
--- /dev/null
+++ b/src/libide/core/ide-context-addin.c
@@ -0,0 +1,207 @@
+/* ide-context-addin.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-context-addin"
+
+#include "config.h"
+
+#include "ide-context-addin.h"
+
+G_DEFINE_INTERFACE (IdeContextAddin, ide_context_addin, G_TYPE_OBJECT)
+
+enum {
+  PROJECT_LOADED,
+  N_SIGNALS
+};
+
+static guint signals [N_SIGNALS];
+
+static void
+ide_context_addin_real_load_project_async (IdeContextAddin     *addin,
+                                           IdeContext          *context,
+                                           GCancellable        *cancellable,
+                                           GAsyncReadyCallback  callback,
+                                           gpointer             user_data)
+{
+  g_autoptr(GTask) task = g_task_new (addin, cancellable, callback, user_data);
+  g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_context_addin_real_load_project_finish (IdeContextAddin  *addin,
+                                            GAsyncResult     *result,
+                                            GError          **error)
+{
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_context_addin_default_init (IdeContextAddinInterface *iface)
+{
+  iface->load_project_async = ide_context_addin_real_load_project_async;
+  iface->load_project_finish = ide_context_addin_real_load_project_finish;
+
+  /**
+   * IdeContextAddin::project-loaded:
+   * @self: an #IdeContextAddin
+   * @context: an #IdeContext
+   *
+   * The "project-loaded" signal is emitted after a project has been loaded
+   * in the #IdeContext.
+   *
+   * You might use this to setup any runtime features that rely on the project
+   * being successfully loaded first. Every addin's
+   * ide_context_addin_load_project_async() will have been called and completed
+   * before this signal is emitted.
+   *
+   * Since: 3.32
+   */
+  signals [PROJECT_LOADED] =
+    g_signal_new ("project-loaded",
+                  G_TYPE_FROM_INTERFACE (iface),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeContextAddinInterface, project_loaded),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_CONTEXT);
+  g_signal_set_va_marshaller (signals [PROJECT_LOADED],
+                              G_TYPE_FROM_INTERFACE (iface),
+                              g_cclosure_marshal_VOID__OBJECTv);
+}
+
+/**
+ * ide_context_addin_load_project_async:
+ * @self: an #IdeContextAddin
+ * @context: an #IdeContext
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Requests to load a project with the #IdeContextAddin.
+ *
+ * This function is called when the #IdeContext requests loading a project.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_addin_load_project_async (IdeContextAddin     *self,
+                                      IdeContext          *context,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_CONTEXT_ADDIN (self));
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_CONTEXT_ADDIN_GET_IFACE (self)->load_project_async (self, context, cancellable, callback, user_data);
+}
+
+/**
+ * ide_context_addin_load_project_finish:
+ * @self: an #IdeContextAddin
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes a request to load a project with the #IdeContextAddin.
+ *
+ * This function will be called from the callback provided to
+ * ide_context_addin_load_project_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_context_addin_load_project_finish (IdeContextAddin  *self,
+                                       GAsyncResult     *result,
+                                       GError          **error)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_CONTEXT_ADDIN_GET_IFACE (self)->load_project_finish (self, result, error);
+}
+
+/**
+ * ide_context_addin_load:
+ * @self: an #IdeContextAddin
+ * @context: an #IdeContext
+ *
+ * Requests that the #IdeContextAddin loads any necessary runtime features.
+ *
+ * This is called when the #IdeContext is created. If you would rather wait
+ * until a project is loaded, then use #IdeContextAddin::project-loaded to
+ * load runtime features.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_addin_load (IdeContextAddin *self,
+                        IdeContext      *context)
+{
+  g_return_if_fail (IDE_IS_CONTEXT_ADDIN (self));
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+
+  if (IDE_CONTEXT_ADDIN_GET_IFACE (self)->load)
+    IDE_CONTEXT_ADDIN_GET_IFACE (self)->load (self, context);
+}
+
+/**
+ * ide_context_addin_unload:
+ * @self: an #IdeContextAddin
+ * @context: an #IdeContext
+ *
+ * Requests that the #IdeContextAddin unloads any previously loaded
+ * resources.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_addin_unload (IdeContextAddin *self,
+                          IdeContext      *context)
+{
+  g_return_if_fail (IDE_IS_CONTEXT_ADDIN (self));
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+
+  if (IDE_CONTEXT_ADDIN_GET_IFACE (self)->unload)
+    IDE_CONTEXT_ADDIN_GET_IFACE (self)->unload (self, context);
+}
+
+/**
+ * ide_context_addin_project_loaded:
+ * @self: an #IdeContextAddin
+ * @context: an #IdeContext
+ *
+ * Emits the #IdeContextAddin::project-loaded signal.
+ *
+ * This is called when the context has completed loading a project.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_addin_project_loaded (IdeContextAddin *self,
+                                  IdeContext      *context)
+{
+  g_return_if_fail (IDE_IS_CONTEXT_ADDIN (self));
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+
+  g_signal_emit (self, signals [PROJECT_LOADED], 0, context);
+}
diff --git a/src/libide/core/ide-context-addin.h b/src/libide/core/ide-context-addin.h
new file mode 100644
index 000000000..ea4e3b575
--- /dev/null
+++ b/src/libide/core/ide-context-addin.h
@@ -0,0 +1,73 @@
+/* ide-context-addin.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "ide-context.h"
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CONTEXT_ADDIN (ide_context_addin_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeContextAddin, ide_context_addin, IDE, CONTEXT_ADDIN, GObject)
+
+struct _IdeContextAddinInterface
+{
+  GTypeInterface parent_iface;
+
+  void     (*load)                (IdeContextAddin      *self,
+                                   IdeContext           *context);
+  void     (*unload)              (IdeContextAddin      *self,
+                                   IdeContext           *context);
+  void     (*load_project_async)  (IdeContextAddin      *self,
+                                   IdeContext           *context,
+                                   GCancellable         *cancellable,
+                                   GAsyncReadyCallback   callback,
+                                   gpointer              user_data);
+  gboolean (*load_project_finish) (IdeContextAddin      *self,
+                                   GAsyncResult         *result,
+                                   GError              **error);
+  void     (*project_loaded)      (IdeContextAddin      *self,
+                                   IdeContext           *context);
+};
+
+IDE_AVAILABLE_IN_3_32
+void     ide_context_addin_load_project_async  (IdeContextAddin      *self,
+                                                IdeContext           *context,
+                                                GCancellable         *cancellable,
+                                                GAsyncReadyCallback   callback,
+                                                gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_context_addin_load_project_finish (IdeContextAddin      *self,
+                                                GAsyncResult         *result,
+                                                GError              **error);
+IDE_AVAILABLE_IN_3_32
+void     ide_context_addin_load                (IdeContextAddin      *self,
+                                                IdeContext           *context);
+IDE_AVAILABLE_IN_3_32
+void     ide_context_addin_unload              (IdeContextAddin      *self,
+                                                IdeContext           *context);
+IDE_AVAILABLE_IN_3_32
+void     ide_context_addin_project_loaded      (IdeContextAddin      *self,
+                                                IdeContext           *context);
+
+G_END_DECLS
diff --git a/src/libide/ide-global.h b/src/libide/core/ide-context-private.h
similarity index 74%
rename from src/libide/ide-global.h
rename to src/libide/core/ide-context-private.h
index a9b422157..e554b9952 100644
--- a/src/libide/ide-global.h
+++ b/src/libide/core/ide-context-private.h
@@ -1,6 +1,6 @@
-/* ide-global.h
+/* ide-context-private.h
  *
- * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ * Copyright 2018-2019 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
@@ -20,13 +20,10 @@
 
 #pragma once
 
-#include "ide-version-macros.h"
+#include "ide-context.h"
 
 G_BEGIN_DECLS
 
-IDE_AVAILABLE_IN_3_32
-const gchar *ide_get_program_name (void);
-IDE_AVAILABLE_IN_3_32
-void         ide_set_program_name (const gchar *program_name);
+void _ide_context_set_has_project (IdeContext *self);
 
 G_END_DECLS
diff --git a/src/libide/core/ide-context.c b/src/libide/core/ide-context.c
new file mode 100644
index 000000000..4f6cc3144
--- /dev/null
+++ b/src/libide/core/ide-context.c
@@ -0,0 +1,855 @@
+/* ide-context.c
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-context"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libpeas/peas.h>
+
+#include "ide-context.h"
+#include "ide-context-private.h"
+#include "ide-context-addin.h"
+#include "ide-macros.h"
+#include "ide-notifications.h"
+
+/**
+ * SECTION:ide-context
+ * @title: IdeContext
+ * @short_description: the root object for a project
+ *
+ * The #IdeContext object is the root object for a project. Everything
+ * in a project is contained by this object.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeContext
+{
+  IdeObject         parent_instance;
+  PeasExtensionSet *addins;
+  gchar            *project_id;
+  gchar            *title;
+  GFile            *workdir;
+  guint             project_loaded : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_PROJECT_ID,
+  PROP_TITLE,
+  PROP_WORKDIR,
+  N_PROPS
+};
+
+enum {
+  LOG,
+  N_SIGNALS
+};
+
+G_DEFINE_TYPE (IdeContext, ide_context, IDE_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+ide_context_addin_load_project_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  IdeContextAddin *addin = (IdeContextAddin *)object;
+  g_autoptr(IdeContext) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_CONTEXT_ADDIN (addin));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_CONTEXT (self));
+
+  if (ide_context_addin_load_project_finish (addin, result, &error))
+    ide_context_addin_project_loaded (addin, self);
+  else
+    g_warning ("%s context addin failed to load project: %s",
+               G_OBJECT_TYPE_NAME (addin), error->message);
+}
+
+static void
+ide_context_addin_added_cb (PeasExtensionSet *set,
+                            PeasPluginInfo   *plugin_info,
+                            PeasExtension    *exten,
+                            gpointer          user_data)
+{
+  IdeContextAddin *addin = (IdeContextAddin *)exten;
+  IdeContext *self = user_data;
+  g_autoptr(GCancellable) cancellable = NULL;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_CONTEXT_ADDIN (addin));
+
+  /* Ignore any request during shutdown */
+  cancellable = ide_object_ref_cancellable (IDE_OBJECT (self));
+  if (g_cancellable_is_cancelled (cancellable))
+    return;
+
+  ide_context_addin_load (addin, self);
+
+  if (self->project_loaded)
+    ide_context_addin_load_project_async (addin,
+                                          self,
+                                          cancellable,
+                                          ide_context_addin_load_project_cb,
+                                          g_object_ref (self));
+}
+
+static void
+ide_context_addin_removed_cb (PeasExtensionSet *set,
+                              PeasPluginInfo   *plugin_info,
+                              PeasExtension    *exten,
+                              gpointer          user_data)
+{
+  IdeContextAddin *addin = (IdeContextAddin *)exten;
+  IdeContext *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_CONTEXT_ADDIN (addin));
+
+  ide_context_addin_unload (addin, self);
+}
+
+static void
+ide_context_real_log (IdeContext     *self,
+                      GLogLevelFlags  level,
+                      const gchar    *domain,
+                      const gchar    *message)
+{
+  g_log (domain, level, "%s", message);
+}
+
+static gchar *
+ide_context_repr (IdeObject *object)
+{
+  IdeContext *self = IDE_CONTEXT (object);
+
+  return g_strdup_printf ("%s workdir=\"%s\" has_project=%d",
+                          G_OBJECT_TYPE_NAME (self),
+                          g_file_peek_path (self->workdir),
+                          self->project_loaded);
+}
+
+static void
+ide_context_constructed (GObject *object)
+{
+  IdeContext *self = (IdeContext *)object;
+
+  g_assert (IDE_IS_OBJECT (object));
+
+  self->addins = peas_extension_set_new (peas_engine_get_default (),
+                                         IDE_TYPE_CONTEXT_ADDIN,
+                                         NULL);
+
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_context_addin_added_cb),
+                    self);
+
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_context_addin_removed_cb),
+                    self);
+
+  peas_extension_set_foreach (self->addins,
+                              ide_context_addin_added_cb,
+                              self);
+
+  G_OBJECT_CLASS (ide_context_parent_class)->constructed (object);
+}
+
+static void
+ide_context_destroy (IdeObject *object)
+{
+  IdeContext *self = (IdeContext *)object;
+
+  g_assert (IDE_IS_OBJECT (object));
+
+  g_clear_object (&self->addins);
+
+  IDE_OBJECT_CLASS (ide_context_parent_class)->destroy (object);
+}
+
+static void
+ide_context_finalize (GObject *object)
+{
+  IdeContext *self = (IdeContext *)object;
+
+  g_clear_object (&self->workdir);
+  g_clear_pointer (&self->project_id, g_free);
+  g_clear_pointer (&self->title, g_free);
+
+  G_OBJECT_CLASS (ide_context_parent_class)->finalize (object);
+}
+
+static void
+ide_context_get_property (GObject    *object,
+                          guint       prop_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+  IdeContext *self = IDE_CONTEXT (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROJECT_ID:
+      g_value_take_string (value, ide_context_dup_project_id (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_take_string (value, ide_context_dup_title (self));
+      break;
+
+    case PROP_WORKDIR:
+      g_value_take_object (value, ide_context_ref_workdir (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_context_set_property (GObject      *object,
+                          guint         prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  IdeContext *self = IDE_CONTEXT (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROJECT_ID:
+      ide_context_set_project_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_TITLE:
+      ide_context_set_title (self, g_value_get_string (value));
+      break;
+
+    case PROP_WORKDIR:
+      ide_context_set_workdir (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_context_class_init (IdeContextClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->constructed = ide_context_constructed;
+  object_class->finalize = ide_context_finalize;
+  object_class->get_property = ide_context_get_property;
+  object_class->set_property = ide_context_set_property;
+
+  i_object_class->destroy = ide_context_destroy;
+  i_object_class->repr = ide_context_repr;
+
+  /**
+   * IdeContext:project-id:
+   *
+   * The "project-id" property is the identifier to use when creating
+   * files and folders for this project. It has a mutated form of either
+   * the directory or some other discoverable trait of the project.
+   *
+   * It has also been modified to remove spaces and other unsafe
+   * characters for file-systems.
+   *
+   * This may change during runtime, but usually only once when the
+   * project has been initialize loaded.
+   *
+   * Before any project has loaded, this is "empty" to allow flexibility
+   * for non-project use.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PROJECT_ID] =
+    g_param_spec_string ("project-id",
+                         "Project Id",
+                         "The project identifier used when creating files and folders",
+                         "empty",
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeContext:title:
+   *
+   * The "title" property is a descriptive name for the project.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the project",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeContext:workdir:
+   *
+   * The "workdir" property is the best guess at the working directory for the
+   * context. This may be discovered using a common parent if multiple files
+   * are opened without a project.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_WORKDIR] =
+    g_param_spec_object ("workdir",
+                         "Working Directory",
+                         "The working directory for the project",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeContext::log:
+   * @self: an #IdeContext
+   * @severity: the log severity
+   * @domain: the log domain
+   * @message: the log message
+   *
+   * This signal is emitted when a log item has been added for the context.
+   *
+   * Since: 3.32
+   */
+  signals [LOG] =
+    g_signal_new_class_handler ("log",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (ide_context_real_log),
+                                NULL, NULL, NULL,
+                                G_TYPE_NONE,
+                                3,
+                                G_TYPE_UINT,
+                                G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE,
+                                G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+}
+
+static void
+ide_context_init (IdeContext *self)
+{
+  g_autoptr(IdeNotifications) notifs = NULL;
+
+  self->workdir = g_file_new_for_path (g_get_home_dir ());
+  self->project_id = g_strdup ("empty");
+  self->title = g_strdup (_("Untitled"));
+
+  notifs = ide_notifications_new ();
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (notifs));
+}
+
+/**
+ * ide_context_new:
+ *
+ * Creates a new #IdeContext.
+ *
+ * This only creates the context object. After creating the object you need
+ * to set a number of properties and then initialize asynchronously using
+ * g_async_initable_init_async().
+ *
+ * Returns: (transfer full): an #IdeContext
+ *
+ * Since: 3.32
+ */
+IdeContext *
+ide_context_new (void)
+{
+  return ide_object_new (IDE_TYPE_CONTEXT, NULL);
+}
+
+static void
+ide_context_peek_child_typed_cb (IdeObject *object,
+                                 gpointer   user_data)
+{
+  struct {
+    IdeObject *ret;
+    GType      type;
+  } *lookup = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  if (lookup->ret != NULL)
+    return;
+
+  /* Take a borrowed instance, we're in the main thread so
+   * we can ensure it's not fully destroyed.
+   */
+  if (G_TYPE_CHECK_INSTANCE_TYPE (object, lookup->type))
+    lookup->ret = object;
+}
+
+/**
+ * ide_context_peek_child_typed:
+ * @self: a #IdeContext
+ * @type: the #GType of the child
+ *
+ * Looks for the first child matching @type, and returns it. No reference is
+ * taken to the child, so you should avoid using this except as used by
+ * compatability functions.
+ *
+ * This may only be called from the main thread or you risk the objects
+ * being finalized before your caller has a chance to reference them.
+ *
+ * Returns: (transfer none) (type IdeObject) (nullable): an #IdeObject that
+ *   matches @type if successful; otherwise %NULL
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_context_peek_child_typed (IdeContext *self,
+                              GType       type)
+{
+  struct {
+    IdeObject *ret;
+    GType      type;
+  } lookup = { NULL, type };
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ide_object_foreach (IDE_OBJECT (self), (GFunc)ide_context_peek_child_typed_cb, &lookup);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return lookup.ret;
+}
+
+/**
+ * ide_context_dup_project_id:
+ * @self: a #IdeContext
+ *
+ * Copies the project-id and returns it to the caller.
+ *
+ * Returns: (transfer full): a project-id as a string
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_context_dup_project_id (IdeContext *self)
+{
+  gchar *ret;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = g_strdup (self->project_id);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  g_return_val_if_fail (ret != NULL, NULL);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_context_set_project_id:
+ * @self: a #IdeContext
+ *
+ * Sets the project-id for the context.
+ *
+ * Generally, this should only be done once after loading a project.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_set_project_id (IdeContext  *self,
+                            const gchar *project_id)
+{
+  g_return_if_fail (IDE_IS_CONTEXT (self));
+
+  if (ide_str_empty0 (project_id))
+    project_id = "empty";
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (!ide_str_equal0 (self->project_id, project_id))
+    {
+      g_free (self->project_id);
+      self->project_id = g_strdup (project_id);
+      ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_PROJECT_ID]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_context_ref_workdir:
+ * @self: a #IdeContext
+ *
+ * Gets the working-directory of the context and increments the
+ * reference count by one.
+ *
+ * Returns: (transfer full): a #GFile
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_context_ref_workdir (IdeContext *self)
+{
+  GFile *ret;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = g_object_ref (self->workdir);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_context_set_workdir:
+ * @self: a #IdeContext
+ * @workdir: a #GFile
+ *
+ * Sets the working directory for the project.
+ *
+ * This should generally only be set once after checking out the project.
+ *
+ * In future releases, changes may be made to change this in support of
+ * git-worktrees or similar workflows.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_set_workdir (IdeContext *self,
+                         GFile      *workdir)
+{
+  g_return_if_fail (IDE_IS_CONTEXT (self));
+  g_return_if_fail (G_IS_FILE (workdir));
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (g_set_object (&self->workdir, workdir))
+    ide_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WORKDIR]);
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_context_cache_file:
+ * @self: a #IdeContext
+ * @first_part: The first part of the path
+ *
+ * Like ide_context_cache_filename() but returns a #GFile.
+ *
+ * Returns: (transfer full): a #GFile for the cache file
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_context_cache_file (IdeContext  *self,
+                        const gchar *first_part,
+                        ...)
+{
+  g_autoptr(GPtrArray) ar = NULL;
+  g_autofree gchar *path = NULL;
+  g_autofree gchar *project_id = NULL;
+  const gchar *part = first_part;
+  va_list args;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+  g_return_val_if_fail (first_part != NULL, NULL);
+
+  project_id = ide_context_dup_project_id (self);
+
+  ar = g_ptr_array_new ();
+  g_ptr_array_add (ar, (gchar *)g_get_user_cache_dir ());
+  g_ptr_array_add (ar, (gchar *)ide_get_program_name ());
+  g_ptr_array_add (ar, (gchar *)"projects");
+  g_ptr_array_add (ar, (gchar *)project_id);
+
+  va_start (args, first_part);
+  do
+    {
+      g_ptr_array_add (ar, (gchar *)part);
+      part = va_arg (args, const gchar *);
+    }
+  while (part != NULL);
+  va_end (args);
+
+  g_ptr_array_add (ar, NULL);
+
+  path = g_build_filenamev ((gchar **)ar->pdata);
+
+  return g_file_new_for_path (path);
+}
+
+/**
+ * ide_context_cache_filename:
+ * @self: a #IdeContext
+ * @first_part: the first part of the filename
+ *
+ * Creates a new filename that will be located in the projects cache directory.
+ * This makes it convenient to remove files when a project is deleted as all
+ * cache files will share a unified parent directory.
+ *
+ * The file will be located in a directory similar to
+ * ~/.cache/gnome-builder/project_name. This may change based on the value
+ * of g_get_user_cache_dir().
+ *
+ * Returns: (transfer full): A new string containing the cache filename
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_context_cache_filename (IdeContext  *self,
+                            const gchar *first_part,
+                            ...)
+{
+  g_autofree gchar *project_id = NULL;
+  g_autofree gchar *base = NULL;
+  va_list args;
+  gchar *ret;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+  g_return_val_if_fail (first_part != NULL, NULL);
+
+  project_id = ide_context_dup_project_id (self);
+
+  g_return_val_if_fail (project_id != NULL, NULL);
+
+  base = g_build_filename (g_get_user_cache_dir (),
+                           ide_get_program_name (),
+                           "projects",
+                           project_id,
+                           first_part,
+                           NULL);
+
+  va_start (args, first_part);
+  ret = g_build_filename_valist (base, &args);
+  va_end (args);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_context_build_file:
+ * @self: a #IdeContext
+ * @path: (nullable): a path to the file
+ *
+ * Creates a new #GFile for the path.
+ *
+ * - If @path is %NULL, #IdeContext:workdir is returned.
+ * - If @path is absolute, a new #GFile to the absolute path is returned.
+ * - Otherwise, a #GFile child of #IdeContext:workdir is returned.
+ *
+ * Returns: (transfer full): a #GFile
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_context_build_file (IdeContext  *self,
+                        const gchar *path)
+{
+  g_autoptr(GFile) ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+  if (path == NULL)
+    ret = g_file_dup (self->workdir);
+  else if (g_path_is_absolute (path))
+    ret = g_file_new_for_path (path);
+  else
+    ret = g_file_get_child (self->workdir, path);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_context_build_filename:
+ * @self: a #IdeContext
+ * @first_part: first path part
+ *
+ * Creates a new path that starts from the working directory of the
+ * loaded project.
+ *
+ * Returns: (transfer full): a string containing the new path
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_context_build_filename (IdeContext  *self,
+                            const gchar *first_part,
+                            ...)
+{
+  g_autoptr(GPtrArray) ar = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  const gchar *part = first_part;
+  const gchar *base;
+  va_list args;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+  g_return_val_if_fail (first_part != NULL, NULL);
+
+  workdir = ide_context_ref_workdir (self);
+  base = g_file_peek_path (workdir);
+
+  ar = g_ptr_array_new ();
+
+  /* If first part is absolute, just use that as our root */
+  if (!g_path_is_absolute (first_part))
+    g_ptr_array_add (ar, (gchar *)base);
+
+  va_start (args, first_part);
+  do
+    {
+      g_ptr_array_add (ar, (gchar *)part);
+      part = va_arg (args, const gchar *);
+    }
+  while (part != NULL);
+  va_end (args);
+
+  g_ptr_array_add (ar, NULL);
+
+  return g_build_filenamev ((gchar **)ar->pdata);
+}
+
+/**
+ * ide_context_ref_project_settings:
+ * @self: a #IdeContext
+ *
+ * Gets an org.gnome.builder.project #GSettings.
+ *
+ * This creates a new #GSettings instance for the project.
+ *
+ * Returns: (transfer full): a #GSettings
+ *
+ * Since: 3.32
+ */
+GSettings *
+ide_context_ref_project_settings (IdeContext *self)
+{
+  g_autofree gchar *path = NULL;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  path = g_strdup_printf ("/org/gnome/builder/projects/%s/", self->project_id);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_settings_new_with_path ("org.gnome.builder.project", path);
+}
+
+/**
+ * ide_context_dup_title:
+ * @self: a #IdeContext
+ *
+ * Returns: (transfer full): a string containing the title
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_context_dup_title (IdeContext *self)
+{
+  gchar *ret;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = g_strdup (self->title);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_context_set_title:
+ * @self: an #IdeContext
+ * @title: (nullable): the title for the project or %NULL
+ *
+ * Sets the #IdeContext:title property. This is used by various
+ * components to show the user the name of the project. This may
+ * include the omnibar and the window title.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_set_title (IdeContext  *self,
+                       const gchar *title)
+{
+  g_return_if_fail (IDE_IS_CONTEXT (self));
+
+  if (ide_str_empty0 (title))
+    title = _("Untitled");
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (!ide_str_equal0 (self->title, title))
+    {
+      g_free (self->title);
+      self->title = g_strdup (title);
+      ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_TITLE]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+void
+ide_context_log (IdeContext     *self,
+                 GLogLevelFlags  level,
+                 const gchar    *domain,
+                 const gchar    *message)
+{
+  g_assert (IDE_IS_CONTEXT (self));
+
+  g_signal_emit (self, signals [LOG], 0, level, domain, message);
+}
+
+/**
+ * ide_context_has_project:
+ * @self: a #IdeContext
+ *
+ * Checks to see if a project has been loaded in @context.
+ *
+ * Returns: %TRUE if a project has been, or is currently, loading.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_context_has_project (IdeContext *self)
+{
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), FALSE);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = self->project_loaded;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+void
+_ide_context_set_has_project (IdeContext *self)
+{
+  g_return_if_fail (IDE_IS_CONTEXT (self));
+
+  ide_object_lock (IDE_OBJECT (self));
+  self->project_loaded = TRUE;
+  ide_object_unlock (IDE_OBJECT (self));
+}
diff --git a/src/libide/core/ide-context.h b/src/libide/core/ide-context.h
new file mode 100644
index 000000000..065ac4563
--- /dev/null
+++ b/src/libide/core/ide-context.h
@@ -0,0 +1,91 @@
+/* ide-context.h
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include "ide-object.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CONTEXT (ide_context_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeContext, ide_context, IDE, CONTEXT, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeContext *ide_context_new                  (void);
+IDE_AVAILABLE_IN_3_32
+gboolean    ide_context_has_project          (IdeContext     *self);
+IDE_AVAILABLE_IN_3_32
+gpointer    ide_context_peek_child_typed     (IdeContext     *self,
+                                              GType           type);
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_context_dup_project_id       (IdeContext     *self);
+IDE_AVAILABLE_IN_3_32
+void        ide_context_set_project_id       (IdeContext     *self,
+                                              const gchar    *project_id);
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_context_dup_title            (IdeContext     *self);
+IDE_AVAILABLE_IN_3_32
+void        ide_context_set_title            (IdeContext     *self,
+                                              const gchar    *title);
+IDE_AVAILABLE_IN_3_32
+GFile      *ide_context_ref_workdir          (IdeContext     *self);
+IDE_AVAILABLE_IN_3_32
+void        ide_context_set_workdir          (IdeContext     *self,
+                                              GFile          *workdir);
+IDE_AVAILABLE_IN_3_32
+GFile      *ide_context_build_file           (IdeContext     *self,
+                                              const gchar    *path);
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_context_build_filename       (IdeContext     *self,
+                                              const gchar    *first_part,
+                                              ...) G_GNUC_NULL_TERMINATED;
+IDE_AVAILABLE_IN_3_32
+GFile      *ide_context_cache_file           (IdeContext     *self,
+                                              const gchar    *first_part,
+                                              ...) G_GNUC_NULL_TERMINATED;
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_context_cache_filename       (IdeContext     *self,
+                                              const gchar    *first_part,
+                                              ...) G_GNUC_NULL_TERMINATED;
+IDE_AVAILABLE_IN_3_32
+GSettings  *ide_context_ref_project_settings (IdeContext     *self);
+IDE_AVAILABLE_IN_3_32
+IdeContext *ide_object_ref_context           (IdeObject      *self);
+IDE_AVAILABLE_IN_3_32
+IdeContext *ide_object_get_context           (IdeObject      *object);
+IDE_AVAILABLE_IN_3_32
+void        ide_object_set_context           (IdeObject      *object,
+                                              IdeContext     *context);
+IDE_AVAILABLE_IN_3_32
+void        ide_context_log                  (IdeContext     *self,
+                                              GLogLevelFlags  level,
+                                              const gchar    *domain,
+                                              const gchar    *message);
+
+#define ide_context_warning(instance, format, ...) \
+  ide_object_log(instance, G_LOG_LEVEL_WARNING, G_LOG_DOMAIN, format __VA_OPT__(,) __VA_ARGS__)
+
+G_END_DECLS
diff --git a/src/libide/ide-debug.h.in b/src/libide/core/ide-debug.h.in
similarity index 100%
rename from src/libide/ide-debug.h.in
rename to src/libide/core/ide-debug.h.in
diff --git a/src/libide/core/ide-global.c b/src/libide/core/ide-global.c
new file mode 100644
index 000000000..46133cc56
--- /dev/null
+++ b/src/libide/core/ide-global.c
@@ -0,0 +1,234 @@
+/* ide-global.c
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-global"
+
+#include "config.h"
+
+#include <gio/gio.h>
+#include <glib/gi18n.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/user.h>
+#include <sys/utsname.h>
+#include <unistd.h>
+
+#include "../../gconstructor.h"
+
+#include "ide-macros.h"
+#include "ide-global.h"
+
+static GThread *main_thread;
+static const gchar *application_id = "org.gnome.Builder";
+static IdeProcessKind kind = IDE_PROCESS_KIND_HOST;
+
+#if defined (G_HAS_CONSTRUCTORS)
+# ifdef G_DEFINE_CONSTRUCTOR_NEEDS_PRAGMA
+#  pragma G_DEFINE_CONSTRUCTOR_PRAGMA_ARGS(ide_init_ctor)
+# endif
+G_DEFINE_CONSTRUCTOR(ide_init_ctor)
+#else
+# error Your platform/compiler is missing constructor support
+#endif
+
+static void
+ide_init_ctor (void)
+{
+  main_thread = g_thread_self ();
+
+  if (g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS))
+    kind = IDE_PROCESS_KIND_FLATPAK;
+}
+
+/**
+ * ide_get_main_thread
+ *
+ * Gets #GThread of the main thread.
+ *
+ * Generally this is used by macros to determine what thread they code is
+ * currently running within.
+ *
+ * Returns: (transfer none): a #GThread
+ *
+ * Since: 3.32
+ */
+GThread *
+ide_get_main_thread (void)
+{
+  return main_thread;
+}
+
+/**
+ * ide_get_process_kind:
+ *
+ * Gets the kind of process we're running as.
+ *
+ * Returns: an #IdeProcessKind
+ *
+ * Since: 3.32
+ */
+IdeProcessKind
+ide_get_process_kind (void)
+{
+  return kind;
+}
+
+const gchar *
+ide_get_application_id (void)
+{
+  return application_id;
+}
+
+/**
+ * ide_set_application_id:
+ * @app_id: the application id
+ *
+ * Sets the application id that will be used.
+ *
+ * This must be set at application startup before any GApplication
+ * has connected to the D-Bus.
+ *
+ * The default is "org.gnome.Builder".
+ *
+ * Since: 3.32
+ */
+void
+ide_set_application_id (const gchar *app_id)
+{
+  g_return_if_fail (app_id != NULL);
+
+  application_id = g_intern_string (app_id);
+}
+
+const gchar *
+ide_get_program_name (void)
+{
+  return "gnome-builder";
+}
+
+gchar *
+ide_create_host_triplet (const gchar *arch,
+                         const gchar *kernel,
+                         const gchar *system)
+{
+  if (arch == NULL || kernel == NULL)
+    return g_strdup (ide_get_system_type ());
+  else if (system == NULL)
+    return g_strdup_printf ("%s-%s", arch, kernel);
+  else
+    return g_strdup_printf ("%s-%s-%s", arch, kernel, system);
+}
+
+const gchar *
+ide_get_system_type (void)
+{
+  static gchar *system_type;
+  g_autofree gchar *os_lower = NULL;
+  const gchar *machine = NULL;
+  struct utsname u;
+
+  if (system_type != NULL)
+    return system_type;
+
+  if (uname (&u) < 0)
+    return g_strdup ("unknown");
+
+  os_lower = g_utf8_strdown (u.sysname, -1);
+
+  /* config.sub doesn't accept amd64-OS */
+  machine = strcmp (u.machine, "amd64") ? u.machine : "x86_64";
+
+  /*
+   * TODO: Clearly we want to discover "gnu", but that should be just fine
+   *       for a default until we try to actually run on something non-gnu.
+   *       Which seems unlikely at the moment. If you run FreeBSD, you can
+   *       probably fix this for me :-) And while you're at it, make the
+   *       uname() call more portable.
+   */
+
+#ifdef __GLIBC__
+  system_type = g_strdup_printf ("%s-%s-%s", machine, os_lower, "gnu");
+#else
+  system_type = g_strdup_printf ("%s-%s", machine, os_lower);
+#endif
+
+  return system_type;
+}
+
+gchar *
+ide_get_system_arch (void)
+{
+  struct utsname u;
+  const char *machine;
+
+  if (uname (&u) < 0)
+    return g_strdup ("unknown");
+
+  /* config.sub doesn't accept amd64-OS */
+  machine = strcmp (u.machine, "amd64") ? u.machine : "x86_64";
+
+  return g_strdup (machine);
+}
+
+gsize
+ide_get_system_page_size (void)
+{
+  return sysconf (_SC_PAGE_SIZE);
+}
+
+static gchar *
+get_base_path (const gchar *name)
+{
+  g_autoptr(GKeyFile) keyfile = g_key_file_new ();
+
+  if (g_key_file_load_from_file (keyfile, "/.flatpak-info", 0, NULL))
+    return g_key_file_get_string (keyfile, "Instance", name, NULL);
+
+  return NULL;
+}
+
+/**
+ * ide_get_relocatable_path:
+ * @path: a relocatable path
+ *
+ * Gets the path to a resource that may be relocatable at runtime.
+ *
+ * Returns: (transfer full): a new string containing the path
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_get_relocatable_path (const gchar *path)
+{
+  static gchar *base_path;
+
+  if G_UNLIKELY (base_path == NULL)
+    base_path = get_base_path ("app-path");
+
+  return g_build_filename (base_path, path, NULL);
+}
+
+const gchar *
+ide_gettext (const gchar *message)
+{
+  if (message != NULL)
+    return g_dgettext (GETTEXT_PACKAGE, message);
+  return NULL;
+}
diff --git a/src/libide/core/ide-global.h b/src/libide/core/ide-global.h
new file mode 100644
index 000000000..28adbdfb8
--- /dev/null
+++ b/src/libide/core/ide-global.h
@@ -0,0 +1,66 @@
+/* ide-global.h
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include <glib.h>
+
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+  IDE_PROCESS_KIND_HOST    = 0,
+  IDE_PROCESS_KIND_FLATPAK = 1,
+} IdeProcessKind;
+
+#define ide_is_flatpak() (ide_get_process_kind() == IDE_PROCESS_KIND_FLATPAK)
+
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_gettext             (const gchar *message);
+IDE_AVAILABLE_IN_3_32
+GThread        *ide_get_main_thread     (void);
+IDE_AVAILABLE_IN_3_32
+IdeProcessKind  ide_get_process_kind    (void);
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_get_application_id  (void);
+IDE_AVAILABLE_IN_3_32
+void            ide_set_application_id  (const gchar *app_id);
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_get_program_name    (void);
+IDE_AVAILABLE_IN_3_32
+gchar          *ide_get_system_arch     (void);
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_get_system_type     (void);
+IDE_AVAILABLE_IN_3_32
+gchar          *ide_create_host_triplet (const gchar *arch,
+                                         const gchar *kernel,
+                                         const gchar *system);
+IDE_AVAILABLE_IN_3_32
+gsize          ide_get_system_page_size (void) G_GNUC_CONST;
+IDE_AVAILABLE_IN_3_32
+gchar         *ide_get_relocatable_path (const gchar *path);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-log.c b/src/libide/core/ide-log.c
new file mode 100644
index 000000000..69a992df6
--- /dev/null
+++ b/src/libide/core/ide-log.c
@@ -0,0 +1,380 @@
+/* ide-log.c
+ *
+ * Copyright 2015-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-log"
+
+#include "config.h"
+
+#ifndef _GNU_SOURCE
+# define _GNU_SOURCE
+#endif
+
+#ifdef __linux__
+# include <sys/types.h>
+# include <sys/syscall.h>
+#endif
+
+#include <glib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "ide-debug.h"
+#include "ide-log.h"
+#include "ide-macros.h"
+
+/**
+ * SECTION:ide-log
+ * @title: Logging
+ * @short_description: Standard logging facilities for Builder
+ *
+ * This module manages the logging facilities in Builder. It involves
+ * formatting the standard output and error logs as well as filtering
+ * logs based on their #GLogLevelFlags.
+ *
+ * Generally speaking, you want to continue using the GLib logging API
+ * such as g_debug(), g_warning(), g_message(), or g_error(). These functions
+ * will redirect their logging information to this module who will format
+ * the log message appropriately.
+ *
+ * If you are writing code for Builder that is in C, you want to ensure you
+ * set the %G_LOG_DOMAIN define at the top of your file (after the license)
+ * as such:
+ *
+ * ## Logging from C
+ *
+ * |[
+ * #define G_LOG_DOMAIN "my-module"
+ * ...
+ * static void
+ * some_function (void)
+ * {
+ *   g_debug ("Use normal logging facilities");
+ * }
+ * ]|
+ *
+ * ## Logging from Python
+ *
+ * If you are writing an extension to Builder from Python, you may use the
+ * helper functions provided by our Ide python module.
+ *
+ * |[<!-- Language="py" -->
+ * from gi.repository import Ide
+ *
+ * Ide.warning("This is a warning")
+ * Ide.debug("This is a debug")
+ * Ide.error("This is a fatal error")
+ * ]|
+ *
+ * Since: 3.32
+ */
+
+typedef const gchar *(*IdeLogLevelStrFunc) (GLogLevelFlags log_level);
+
+static GPtrArray          *channels;
+static GLogFunc            last_handler;
+static int                 log_verbosity;
+static IdeLogLevelStrFunc  log_level_str_func;
+static gchar              *domains;
+static gboolean            has_domains;
+
+G_LOCK_DEFINE (channels_lock);
+
+/**
+ * ide_log_get_thread:
+ *
+ * Retrieves task id for the current thread. This is only supported on Linux.
+ * On other platforms, the current thread pointer is retrieved.
+ *
+ * Returns: The task id.
+ *
+ * Since: 3.32
+ */
+static inline gint
+ide_log_get_thread (void)
+{
+#ifdef __linux__
+  return (gint) syscall (SYS_gettid);
+#else
+  return GPOINTER_TO_INT (g_thread_self ());
+#endif /* __linux__ */
+}
+
+/**
+ * ide_log_level_str:
+ * @log_level: a #GLogLevelFlags.
+ *
+ * Retrieves the log level as a string.
+ *
+ * Returns: A string which shouldn't be modified or freed.
+ * Side effects: None.
+ *
+ * Since: 3.32
+ */
+static const gchar *
+ide_log_level_str (GLogLevelFlags log_level)
+{
+  switch (((gulong)log_level & G_LOG_LEVEL_MASK))
+    {
+    case G_LOG_LEVEL_ERROR:    return "   ERROR";
+    case G_LOG_LEVEL_CRITICAL: return "CRITICAL";
+    case G_LOG_LEVEL_WARNING:  return " WARNING";
+    case G_LOG_LEVEL_MESSAGE:  return " MESSAGE";
+    case G_LOG_LEVEL_INFO:     return "    INFO";
+    case G_LOG_LEVEL_DEBUG:    return "   DEBUG";
+    case IDE_LOG_LEVEL_TRACE:  return "   TRACE";
+
+    default:
+      return " UNKNOWN";
+    }
+}
+
+static const gchar *
+ide_log_level_str_with_color (GLogLevelFlags log_level)
+{
+  switch (((gulong)log_level & G_LOG_LEVEL_MASK))
+    {
+    case G_LOG_LEVEL_ERROR:    return "   \033[1;31mERROR\033[0m";
+    case G_LOG_LEVEL_CRITICAL: return "\033[1;35mCRITICAL\033[0m";
+    case G_LOG_LEVEL_WARNING:  return " \033[1;33mWARNING\033[0m";
+    case G_LOG_LEVEL_MESSAGE:  return " \033[1;32mMESSAGE\033[0m";
+    case G_LOG_LEVEL_INFO:     return "    \033[1;32mINFO\033[0m";
+    case G_LOG_LEVEL_DEBUG:    return "   \033[1;32mDEBUG\033[0m";
+    case IDE_LOG_LEVEL_TRACE:  return "   \033[1;36mTRACE\033[0m";
+
+    default:
+      return " UNKNOWN";
+    }
+}
+
+/**
+ * ide_log_write_to_channel:
+ * @channel: a #GIOChannel.
+ * @message: A string log message.
+ *
+ * Writes @message to @channel and flushes the channel.
+ *
+ * Since: 3.32
+ */
+static void
+ide_log_write_to_channel (GIOChannel  *channel,
+                          const gchar *message)
+{
+  g_io_channel_write_chars (channel, message, -1, NULL, NULL);
+  g_io_channel_flush (channel, NULL);
+}
+
+/**
+ * ide_log_handler:
+ * @log_domain: A string containing the log section.
+ * @log_level: a #GLogLevelFlags.
+ * @message: The string message.
+ * @user_data: User data supplied to g_log_set_default_handler().
+ *
+ * Default log handler that will dispatch log messages to configured logging
+ * destinations.
+ *
+ * Since: 3.32
+ */
+static void
+ide_log_handler (const gchar    *log_domain,
+                 GLogLevelFlags  log_level,
+                 const gchar    *message,
+                 gpointer        user_data)
+{
+  GTimeVal tv;
+  struct tm tt;
+  time_t t;
+  const gchar *level;
+  gchar ftime[32];
+  gchar *buffer;
+  gboolean is_debug_level;
+
+  if (G_LIKELY (channels->len))
+    {
+      is_debug_level = (log_level == G_LOG_LEVEL_DEBUG || log_level == IDE_LOG_LEVEL_TRACE);
+      if (is_debug_level &&
+          has_domains &&
+          (log_domain == NULL || strstr (domains, log_domain) == NULL))
+        return;
+
+      switch ((int)log_level)
+        {
+        case G_LOG_LEVEL_MESSAGE:
+          if (log_verbosity < 1)
+            return;
+          break;
+
+        case G_LOG_LEVEL_INFO:
+          if (log_verbosity < 2)
+            return;
+          break;
+
+        case G_LOG_LEVEL_DEBUG:
+          if (log_verbosity < 3)
+            return;
+          break;
+
+        case IDE_LOG_LEVEL_TRACE:
+          if (log_verbosity < 4)
+            return;
+          break;
+
+        default:
+          break;
+        }
+
+      level = log_level_str_func (log_level);
+      g_get_current_time (&tv);
+      t = (time_t) tv.tv_sec;
+      tt = *localtime (&t);
+      strftime (ftime, sizeof (ftime), "%H:%M:%S", &tt);
+      buffer = g_strdup_printf ("%s.%04ld  %40s[% 5d]: %s: %s\n",
+                                ftime,
+                                tv.tv_usec / 1000,
+                                log_domain,
+                                ide_log_get_thread (),
+                                level,
+                                message);
+      G_LOCK (channels_lock);
+      g_ptr_array_foreach (channels, (GFunc) ide_log_write_to_channel, buffer);
+      G_UNLOCK (channels_lock);
+      g_free (buffer);
+    }
+}
+
+/**
+ * ide_log_init:
+ * @stdout_: Indicates logging should be written to stdout.
+ * @filename: An optional file in which to store logs.
+ *
+ * Initializes the logging subsystem. This should be called from
+ * the application entry point only. Secondary calls to this function
+ * will do nothing.
+ *
+ * Since: 3.32
+ */
+void
+ide_log_init (gboolean     stdout_,
+              const gchar *filename)
+{
+  static gsize initialized = FALSE;
+  GIOChannel *channel;
+
+  if (g_once_init_enter (&initialized))
+    {
+      log_level_str_func = ide_log_level_str;
+      channels = g_ptr_array_new ();
+      if (filename)
+        {
+          channel = g_io_channel_new_file (filename, "a", NULL);
+          g_ptr_array_add (channels, channel);
+        }
+      if (stdout_)
+        {
+          channel = g_io_channel_unix_new (STDOUT_FILENO);
+          g_ptr_array_add (channels, channel);
+          if ((filename == NULL) && isatty (STDOUT_FILENO))
+            log_level_str_func = ide_log_level_str_with_color;
+        }
+
+      domains = g_strdup (g_getenv ("G_MESSAGES_DEBUG"));
+      if (!ide_str_empty0 (domains) && strcmp (domains, "all") != 0)
+        has_domains = TRUE;
+
+      g_log_set_default_handler (ide_log_handler, NULL);
+      g_once_init_leave (&initialized, TRUE);
+    }
+}
+
+/**
+ * ide_log_shutdown:
+ *
+ * Cleans up after the logging subsystem and restores the original
+ * log handler.
+ *
+ * Since: 3.32
+ */
+void
+ide_log_shutdown (void)
+{
+  if (last_handler)
+    {
+      g_log_set_default_handler (last_handler, NULL);
+      last_handler = NULL;
+    }
+
+  g_clear_pointer (&domains, g_free);
+}
+
+/**
+ * ide_log_increase_verbosity:
+ *
+ * Increases the amount of logging that will occur. By default, only
+ * warning and above will be displayed.
+ *
+ * Calling this once will cause %G_LOG_LEVEL_MESSAGE to be displayed.
+ * Calling this twice will cause %G_LOG_LEVEL_INFO to be displayed.
+ * Calling this thrice will cause %G_LOG_LEVEL_DEBUG to be displayed.
+ * Calling this four times will cause %IDE_LOG_LEVEL_TRACE to be displayed.
+ *
+ * Note that many DEBUG and TRACE level log messages are only compiled into
+ * debug builds, and therefore will not be available in release builds.
+ *
+ * This method is meant to be called for every -v provided on the command
+ * line.
+ *
+ * Calling this method more than four times is acceptable.
+ *
+ * Since: 3.32
+ */
+void
+ide_log_increase_verbosity (void)
+{
+  log_verbosity++;
+}
+
+/**
+ * ide_log_get_verbosity:
+ *
+ * Retrieves the log verbosity, which is the number of times -v was
+ * provided on the command line.
+ *
+ * Since: 3.32
+ */
+gint
+ide_log_get_verbosity (void)
+{
+  return log_verbosity;
+}
+
+/**
+ * ide_log_set_verbosity:
+ *
+ * Sets the explicit verbosity. Generally you want to use
+ * ide_log_increase_verbosity() instead of this function.
+ *
+ * Since: 3.32
+ */
+void
+ide_log_set_verbosity (gint level)
+{
+  log_verbosity = level;
+}
diff --git a/src/libide/core/ide-log.h b/src/libide/core/ide-log.h
new file mode 100644
index 000000000..c8052dca4
--- /dev/null
+++ b/src/libide/core/ide-log.h
@@ -0,0 +1,45 @@
+/* ide-log.h
+ *
+ * Copyright 2015-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include <glib.h>
+
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+IDE_AVAILABLE_IN_3_32
+void ide_log_init               (gboolean     stdout_,
+                                 const gchar *filename);
+IDE_AVAILABLE_IN_3_32
+void ide_log_increase_verbosity (void);
+IDE_AVAILABLE_IN_3_32
+gint ide_log_get_verbosity      (void);
+IDE_AVAILABLE_IN_3_32
+void ide_log_set_verbosity      (gint         level);
+IDE_AVAILABLE_IN_3_32
+void ide_log_shutdown           (void);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-macros.h b/src/libide/core/ide-macros.h
new file mode 100644
index 000000000..6519df04b
--- /dev/null
+++ b/src/libide/core/ide-macros.h
@@ -0,0 +1,249 @@
+/* ide-macros.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#ifndef __GI_SCANNER__
+
+#include <glib.h>
+
+#include "ide-global.h"
+#include "ide-object.h"
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define ide_str_empty0(str)       (!(str) || !*(str))
+#define ide_str_equal0(str1,str2) (g_strcmp0(str1,str2)==0)
+#define ide_strv_empty0(strv)     (((strv) == NULL) || ((strv)[0] == NULL))
+#define ide_set_string(ptr,str)   (ide_take_string((ptr), g_strdup(str)))
+
+#define ide_clear_param(pptr, pval) \
+  G_STMT_START { if (pptr) { *(pptr) = pval; }; } G_STMT_END
+
+#define IDE_IS_MAIN_THREAD() (g_thread_self() == ide_get_main_thread())
+
+#define IDE_PTR_ARRAY_CLEAR_FREE_FUNC(ar)                       \
+  IDE_PTR_ARRAY_SET_FREE_FUNC(ar, NULL)
+#define IDE_PTR_ARRAY_SET_FREE_FUNC(ar, func)                   \
+  G_STMT_START {                                                \
+    if ((ar) != NULL)                                           \
+      g_ptr_array_set_free_func ((ar), (GDestroyNotify)(func)); \
+  } G_STMT_END
+#define IDE_PTR_ARRAY_STEAL_FULL(arptr)        \
+  ({ IDE_PTR_ARRAY_CLEAR_FREE_FUNC (*(arptr)); \
+     g_steal_pointer ((arptr)); })
+
+static inline void
+_g_object_unref0 (gpointer instance)
+{
+  if (instance)
+    g_object_unref (instance);
+}
+
+static inline gboolean
+ide_take_string (gchar **ptr,
+                 gchar  *str)
+{
+  if (*ptr != str)
+    {
+      g_free (*ptr);
+      *ptr = str;
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static inline void
+ide_clear_string (gchar **ptr)
+{
+  g_free (*ptr);
+  *ptr = NULL;
+}
+
+static inline GList *
+_g_list_insert_before_link (GList *list,
+                            GList *sibling,
+                            GList *link_)
+{
+  g_return_val_if_fail (link_ != NULL, list);
+
+  if (!list)
+    {
+      g_return_val_if_fail (sibling == NULL, list);
+      return link_;
+    }
+  else if (sibling)
+    {
+      link_->prev = sibling->prev;
+      link_->next = sibling;
+      sibling->prev = link_;
+      if (link_->prev)
+        {
+          link_->prev->next = link_;
+          return list;
+        }
+      else
+        {
+          g_return_val_if_fail (sibling == list, link_);
+          return link_;
+        }
+    }
+  else
+    {
+      GList *last;
+
+      last = list;
+      while (last->next)
+        last = last->next;
+
+      last->next = link_;
+      last->next->prev = last;
+      last->next->next = NULL;
+
+      return list;
+    }
+}
+
+static inline void
+_g_queue_insert_before_link (GQueue *queue,
+                             GList  *sibling,
+                             GList  *link_)
+{
+  g_return_if_fail (queue != NULL);
+  g_return_if_fail (link_ != NULL);
+
+  if G_UNLIKELY (sibling == NULL)
+    {
+      /* We don't use g_list_insert_before_link() with a NULL sibling because it
+       * would be a O(n) operation and we would need to update manually the tail
+       * pointer.
+       */
+      g_queue_push_tail_link (queue, link_);
+    }
+  else
+    {
+      queue->head = _g_list_insert_before_link (queue->head, sibling, link_);
+      queue->length++;
+    }
+}
+
+static inline void
+_g_queue_insert_after_link (GQueue *queue,
+                            GList  *sibling,
+                            GList  *link_)
+{
+  g_return_if_fail (queue != NULL);
+  g_return_if_fail (link_ != NULL);
+
+  if (sibling == NULL)
+    g_queue_push_head_link (queue, link_);
+  else
+    _g_queue_insert_before_link (queue, sibling->next, link_);
+}
+
+static inline GPtrArray *
+_g_ptr_array_copy_objects (GPtrArray *ar)
+{
+  if (ar != NULL)
+    {
+      GPtrArray *copy = g_ptr_array_new_full (ar->len, g_object_unref);
+      for (guint i = 0; i < ar->len; i++)
+        g_ptr_array_add (copy, g_object_ref (g_ptr_array_index (ar, i)));
+      return g_steal_pointer (&copy);
+    }
+
+  return NULL;
+}
+
+static void
+ide_object_unref_and_destroy (IdeObject *object)
+{
+  if (object != NULL)
+    {
+      if (!ide_object_in_destruction (object))
+        ide_object_destroy (object);
+      g_object_unref (object);
+    }
+}
+
+typedef GPtrArray IdeObjectArray;
+
+static inline void
+ide_clear_and_destroy_object (gpointer pptr)
+{
+  IdeObject **ptr = pptr;
+
+  if (ptr && *ptr)
+    {
+      if (!ide_object_in_destruction (*ptr))
+        ide_object_destroy (*ptr);
+      g_clear_object (ptr);
+    }
+}
+
+static inline GPtrArray *
+ide_object_array_new (void)
+{
+  return g_ptr_array_new_with_free_func ((GDestroyNotify)ide_object_unref_and_destroy);
+}
+
+static inline gpointer
+ide_object_array_steal_index (IdeObjectArray *array,
+                              guint           position)
+{
+  gpointer ret = g_ptr_array_index (array, position);
+  g_ptr_array_index (array, position) = NULL;
+  g_ptr_array_remove_index (array, position);
+  return ret;
+}
+
+static inline gpointer
+ide_object_array_index (IdeObjectArray *array,
+                        guint           position)
+{
+  return g_ptr_array_index (array, position);
+}
+
+static inline void
+ide_object_array_add (IdeObjectArray *ar,
+                      gpointer        instance)
+{
+  g_ptr_array_add (ar, g_object_ref (IDE_OBJECT (instance)));
+}
+
+static inline void
+ide_object_array_unref (IdeObjectArray *ar)
+{
+  g_ptr_array_unref (ar);
+}
+
+#define IDE_OBJECT_ARRAY_STEAL_FULL(ar) IDE_PTR_ARRAY_STEAL_FULL(ar)
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeObjectArray, g_ptr_array_unref)
+
+G_END_DECLS
+
+#endif /* __GI_SCANNER__ */
diff --git a/src/libide/core/ide-notification.c b/src/libide/core/ide-notification.c
new file mode 100644
index 000000000..f41c4ed29
--- /dev/null
+++ b/src/libide/core/ide-notification.c
@@ -0,0 +1,1187 @@
+/* ide-notification.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-notification"
+
+#include "config.h"
+
+#include "ide-macros.h"
+#include "ide-notification.h"
+#include "ide-notifications.h"
+
+typedef struct
+{
+  gchar    *id;
+  gchar    *title;
+  gchar    *body;
+  GIcon    *icon;
+  gchar    *default_action;
+  GVariant *default_target;
+  GArray   *buttons;
+  gdouble   progress;
+  gint      priority;
+  guint     has_progress : 1;
+  guint     progress_is_imprecise : 1;
+  guint     urgent : 1;
+} IdeNotificationPrivate;
+
+typedef struct
+{
+  gchar    *label;
+  GIcon    *icon;
+  gchar    *action;
+  GVariant *target;
+} Button;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeNotification, ide_notification, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_BODY,
+  PROP_HAS_PROGRESS,
+  PROP_ICON,
+  PROP_ICON_NAME,
+  PROP_ID,
+  PROP_PRIORITY,
+  PROP_PROGRESS,
+  PROP_PROGRESS_IS_IMPRECISE,
+  PROP_TITLE,
+  PROP_URGENT,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+clear_button (Button *button)
+{
+  g_clear_pointer (&button->label, g_free);
+  g_clear_pointer (&button->action, g_free);
+  g_clear_pointer (&button->target, g_variant_unref);
+  g_clear_object (&button->icon);
+}
+
+static void
+ide_notification_destroy (IdeObject *object)
+{
+  IdeNotification *self = (IdeNotification *)object;
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_clear_pointer (&priv->title, g_free);
+  g_clear_pointer (&priv->body, g_free);
+  g_clear_pointer (&priv->default_action, g_free);
+  g_clear_pointer (&priv->default_target, g_variant_unref);
+  g_clear_pointer (&priv->buttons, g_array_unref);
+  g_clear_object (&priv->icon);
+
+  IDE_OBJECT_CLASS (ide_notification_parent_class)->destroy (object);
+}
+
+static void
+ide_notification_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  IdeNotification *self = IDE_NOTIFICATION (object);
+
+  switch (prop_id)
+    {
+    case PROP_BODY:
+      g_value_take_string (value, ide_notification_dup_body (self));
+      break;
+
+    case PROP_HAS_PROGRESS:
+      g_value_set_boolean (value, ide_notification_get_has_progress (self));
+      break;
+
+    case PROP_ICON:
+      g_value_take_object (value, ide_notification_ref_icon (self));
+      break;
+
+    case PROP_ID:
+      g_value_take_string (value, ide_notification_dup_id (self));
+      break;
+
+    case PROP_PRIORITY:
+      g_value_set_int (value, ide_notification_get_priority (self));
+      break;
+
+    case PROP_PROGRESS:
+      g_value_set_double (value, ide_notification_get_progress (self));
+      break;
+
+    case PROP_PROGRESS_IS_IMPRECISE:
+      g_value_set_boolean (value, ide_notification_get_progress_is_imprecise (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_take_string (value, ide_notification_dup_title (self));
+      break;
+
+    case PROP_URGENT:
+      g_value_set_boolean (value, ide_notification_get_urgent (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_notification_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  IdeNotification *self = IDE_NOTIFICATION (object);
+
+  switch (prop_id)
+    {
+    case PROP_BODY:
+      ide_notification_set_body (self, g_value_get_string (value));
+      break;
+
+    case PROP_HAS_PROGRESS:
+      ide_notification_set_has_progress (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_ICON:
+      ide_notification_set_icon (self, g_value_get_object (value));
+      break;
+
+    case PROP_ICON_NAME:
+      ide_notification_set_icon_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_ID:
+      ide_notification_set_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_PRIORITY:
+      ide_notification_set_priority (self, g_value_get_int (value));
+      break;
+
+    case PROP_PROGRESS:
+      ide_notification_set_progress (self, g_value_get_double (value));
+      break;
+
+    case PROP_PROGRESS_IS_IMPRECISE:
+      ide_notification_set_progress_is_imprecise (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_TITLE:
+      ide_notification_set_title (self, g_value_get_string (value));
+      break;
+
+    case PROP_URGENT:
+      ide_notification_set_urgent (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_notification_class_init (IdeNotificationClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *ide_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->get_property = ide_notification_get_property;
+  object_class->set_property = ide_notification_set_property;
+
+  ide_object_class->destroy = ide_notification_destroy;
+
+  /**
+   * IdeNotification:body:
+   *
+   * The "body" property is the main body of text for the notification.
+   * Not all notifications need this, but more complex notifications might.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_BODY] =
+    g_param_spec_string ("body",
+                         "Body",
+                         "The body of the notification",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:has-progress:
+   *
+   * The "has-progress" property denotes the notification will receive
+   * updates to the #IdeNotification:progress property.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_HAS_PROGRESS] =
+    g_param_spec_boolean ("has-progress",
+                          "Has Progress",
+                          "If the notification supports progress updates",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:icon:
+   *
+   * The "icon" property is an optional icon that may be shown next to
+   * the notification title and body under certain senarios.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ICON] =
+    g_param_spec_object ("icon",
+                         "Icon",
+                         "The icon for the notification, if any",
+                         G_TYPE_ICON,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:icon-name:
+   *
+   * The "icon-name" property is a helper to make setting #IdeNotification:icon
+   * more convenient.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         "Icon Name",
+                         "An icon-name to use to set IdeNotification:icon",
+                         NULL,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:id:
+   *
+   * The "id" property is an optional identifier that can be used to locate
+   * the notification later.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ID] =
+    g_param_spec_string ("id",
+                         "Id",
+                         "An optional identifier for the notification",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:priority:
+   *
+   * The "priority" property is used to sort the notification in order of
+   * importance when displaying to the user.
+   *
+   * You may also use the #IdeNotification:urgent property to raise the
+   * importance of a message to the user.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PRIORITY] =
+    g_param_spec_int ("priority",
+                      "Priority",
+                      "The priority of the notification",
+                      G_MININT, G_MAXINT, 0,
+                      (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:progress:
+   *
+   * The "progress" property is a value between 0.0 and 1.0 describing the progress of
+   * the operation for which the notification represents.
+   *
+   * This property is ignored if #IdeNotification:has-progress is unset.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PROGRESS] =
+    g_param_spec_double ("progress",
+                         "Progress",
+                         "The progress for the notification, if any",
+                         0.0, 1.0, 0.0,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:progress-is-imprecise:
+   *
+   * The "progress-is-imprecise" property indicates that the notification has
+   * progress, but it is imprecise.
+   *
+   * The UI may show a bouncing progress bar if set.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PROGRESS_IS_IMPRECISE] =
+    g_param_spec_boolean ("progress-is-imprecise",
+                          "Progress is Imprecise",
+                          "If the notification supports progress, but is imprecise",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:title:
+   *
+   * The "title" property is the main text to show the user. It may be
+   * displayed more prominently such as in the titlebar.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the notification",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:urgent:
+   *
+   * If the notification is urgent. These notifications will be displayed with
+   * higher priority than those without the urgent property set.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_URGENT] =
+    g_param_spec_boolean ("urgent",
+                          "Urgent",
+                          "If it is urgent the user see the notification",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_notification_init (IdeNotification *self)
+{
+}
+
+/**
+ * ide_notification_new:
+ *
+ * Creates a new #IdeNotification.
+ *
+ * To "send" the notification, you should attach it to the #IdeNotifications
+ * object which can be found under the root #IdeObject. To simplify this,
+ * the ide_notification_attach() function is provided to locate the
+ * #IdeNotifications object using any #IdeObject you have access to.
+ *
+ * ```
+ * IdeNotification *notif = ide_notification_new ();
+ * setup_notification (notify);
+ * ide_notification_attach (notif, IDE_OBJECT (some_object));
+ * ```
+ *
+ * Since: 3.32
+ */
+IdeNotification *
+ide_notification_new (void)
+{
+  return g_object_new (IDE_TYPE_NOTIFICATION, NULL);
+}
+
+/**
+ * ide_notification_attach:
+ * @self: an #IdeNotifications
+ * @object: an #IdeObject
+ *
+ * This function will locate the #IdeNotifications object starting from
+ * @object and attach @self as a child to that object.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_attach (IdeNotification *self,
+                         IdeObject       *object)
+{
+  g_autoptr(IdeObject) root = NULL;
+  g_autoptr(IdeObject) child = NULL;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+  g_return_if_fail (IDE_IS_OBJECT (object));
+
+  root = ide_object_ref_root (object);
+  child = ide_object_get_child_typed (root, IDE_TYPE_NOTIFICATIONS);
+
+  if (child != NULL)
+    ide_notifications_add_notification (IDE_NOTIFICATIONS (child), self);
+  else
+    g_warning ("Failed to locate IdeNotifications from %s", G_OBJECT_TYPE_NAME (object));
+}
+
+/**
+ * ide_notification_dup_id:
+ *
+ * Copies the id of the notification and returns it to the caller after locking
+ * the object. A copy is used to avoid thread-races.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_notification_dup_id (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gchar *ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = g_strdup (priv->id);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_notification_set_id:
+ * @self: an #IdeNotification
+ * @id: (nullable): a string containing the id, or %NULL
+ *
+ * Sets the #IdeNotification:id property.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_set_id (IdeNotification *self,
+                         const gchar     *id)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (!ide_str_equal0 (priv->id, id))
+    {
+      g_free (priv->id);
+      priv->id = g_strdup (id);
+      ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_ID]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_notification_dup_title:
+ *
+ * Copies the current title and returns it to the caller after locking the
+ * object. A copy is used to avoid thread-races.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_notification_dup_title (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gchar *ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = g_strdup (priv->title);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_notification_set_title:
+ * @self: an #IdeNotification
+ * @title: (nullable): a string containing the title text, or %NULL
+ *
+ * Sets the #IdeNotification:title property.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_set_title (IdeNotification *self,
+                            const gchar     *title)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (!ide_str_equal0 (priv->title, title))
+    {
+      g_free (priv->title);
+      priv->title = g_strdup (title);
+      ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_TITLE]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_notification_dup_body:
+ *
+ * Copies the current body and returns it to the caller after locking the
+ * object. A copy is used to avoid thread-races.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_notification_dup_body (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gchar *ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = g_strdup (priv->body);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_notification_set_body:
+ * @self: an #IdeNotification
+ * @body: (nullable): a string containing the body text, or %NULL
+ *
+ * Sets the #IdeNotification:body property.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_set_body (IdeNotification *self,
+                           const gchar     *body)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (!ide_str_equal0 (priv->body, body))
+    {
+      g_free (priv->body);
+      priv->body = g_strdup (body);
+      ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_BODY]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_notification_ref_icon:
+ *
+ * Gets the icon for the notification, and returns a new reference
+ * to the #GIcon.
+ *
+ * Returns: (transfer full) (nullable): a #GIcon or %NULL
+ *
+ * Since: 3.32
+ */
+GIcon *
+ide_notification_ref_icon (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  GIcon *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  g_set_object (&ret, priv->icon);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+void
+ide_notification_set_icon (IdeNotification *self,
+                           GIcon           *icon)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+  g_return_if_fail (!icon || G_IS_ICON (icon));
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (g_set_object (&priv->icon, icon))
+    ide_object_notify_by_pspec (self, properties [PROP_ICON]);
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+void
+ide_notification_set_icon_name (IdeNotification *self,
+                                const gchar     *icon_name)
+{
+  g_autoptr(GIcon) icon = NULL;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+  g_return_if_fail (!icon || G_IS_ICON (icon));
+
+  if (icon_name != NULL)
+    icon = g_themed_icon_new (icon_name);
+  ide_notification_set_icon (self, icon);
+}
+
+gint
+ide_notification_get_priority (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gint ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), 0);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = priv->priority;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+void
+ide_notification_set_priority (IdeNotification *self,
+                               gint             priority)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->priority != priority)
+    {
+      priv->priority = priority;
+      ide_object_notify_by_pspec (self, properties [PROP_PRIORITY]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+gboolean
+ide_notification_get_urgent (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), FALSE);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = priv->urgent;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+void
+ide_notification_set_urgent (IdeNotification *self,
+                             gboolean         urgent)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  urgent = !!urgent;
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->urgent != urgent)
+    {
+      priv->urgent = urgent;
+      ide_object_notify_by_pspec (self, properties [PROP_URGENT]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+guint
+ide_notification_get_n_buttons (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  guint ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), FALSE);
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->buttons != NULL)
+    ret = priv->buttons->len;
+  else
+    ret = 0;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+/**
+ * ide_notification_get_button:
+ * @self: an #IdeNotification
+ * @label: (out) (optional): a location for the button label
+ * @icon: (out) (optional): a location for the button icon
+ * @action: (out) (optional): a location for the button action name
+ * @target: (out) (optional): a location for the button action target
+ *
+ * Gets the button indexed by @button, and stores information about the
+ * button into the various out parameters @label, @icon, @action, and @target.
+ *
+ * Caller should check for the number of buttons using
+ * ide_notification_get_n_buttons() to determine the numerical range of
+ * indexes to provide for @button.
+ *
+ * To avoid racing with threads modifying notifications, the caller can
+ * hold a recursive lock across the function calls using ide_object_lock()
+ * and ide_object_unlock().
+ *
+ * Returns: %TRUE if @button was found; otherwise %FALSE
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_notification_get_button (IdeNotification  *self,
+                             guint             button,
+                             gchar           **label,
+                             GIcon           **icon,
+                             gchar           **action,
+                             GVariant        **target)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gboolean ret = FALSE;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), FALSE);
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->buttons != NULL)
+    {
+      if (button < priv->buttons->len)
+        {
+          Button *b = &g_array_index (priv->buttons, Button, button);
+
+          if (label)
+            *label = g_strdup (b->label);
+          if (icon)
+            g_set_object (icon, b->icon);
+          if (action)
+            *action = g_strdup (b->action);
+          if (target)
+            *target = b->target ? g_variant_ref (b->target) : NULL;
+          ret = TRUE;
+        }
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+/**
+ * ide_notification_add_button:
+ * @self: an #IdeNotification
+ * @label: the label for the button
+ * @icon: (nullable): an optional icon for the button
+ * @detailed_action: a detailed action name (See #GAction)
+ *
+ * Adds a new button that may be displayed with the notification.
+ *
+ * See also: ide_notification_add_button_with_target_value().
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_add_button (IdeNotification *self,
+                             const gchar     *label,
+                             GIcon           *icon,
+                             const gchar     *detailed_action)
+{
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GVariant) target_value = NULL;
+  g_autofree gchar *action_name = NULL;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+  g_return_if_fail (label || icon);
+  g_return_if_fail (!icon || G_IS_ICON (icon));
+  g_return_if_fail (detailed_action != NULL);
+
+  if (!g_action_parse_detailed_name (detailed_action, &action_name, &target_value, &error))
+    g_warning ("Failed to parse detailed_action: %s", error->message);
+  else
+    ide_notification_add_button_with_target_value (self, label, icon, action_name, target_value);
+}
+
+/**
+ * ide_notification_add_button_with_target_value:
+ * @self: an #IdeNotification
+ * @label: the label for the button
+ * @icon: (nullable): an optional icon for the button
+ * @action: an action name (See #GAction)
+ * @target: (nullable): an optional #GVariant for the action target
+ *
+ * Adds a new button, used the parsed #GVariant format for the action
+ * target.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_add_button_with_target_value (IdeNotification *self,
+                                               const gchar     *label,
+                                               GIcon           *icon,
+                                               const gchar     *action,
+                                               GVariant        *target)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  Button b = {0};
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+  g_return_if_fail (label || icon);
+  g_return_if_fail (action != NULL);
+
+  b.label = g_strdup (label);
+  g_set_object (&b.icon, icon);
+  b.action = g_strdup (action);
+  b.target = target ? g_variant_ref (target) : NULL;
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->buttons == NULL)
+    {
+      priv->buttons = g_array_new (FALSE, FALSE, sizeof b);
+      g_array_set_clear_func (priv->buttons, (GDestroyNotify)clear_button);
+    }
+  g_array_append_val (priv->buttons, b);
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+gboolean
+ide_notification_get_default_action (IdeNotification  *self,
+                                     gchar           **action,
+                                     GVariant        **target)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gboolean ret = FALSE;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), FALSE);
+
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->default_action != NULL)
+    {
+      if (action)
+        *action = g_strdup (priv->default_action);
+      if (target)
+        *target = priv->default_target ? g_variant_ref (priv->default_target) : NULL;
+      ret = TRUE;
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+void
+ide_notification_set_default_action (IdeNotification *self,
+                                     const gchar     *detailed_action)
+{
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GVariant) target_value = NULL;
+  g_autofree gchar *action_name = NULL;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+  g_return_if_fail (detailed_action != NULL);
+
+  if (!g_action_parse_detailed_name (detailed_action, &action_name, &target_value, &error))
+    g_warning ("Failed to parse detailed_action: %s", error->message);
+  else
+    ide_notification_set_default_action_and_target_value (self, action_name, target_value);
+}
+
+void
+ide_notification_set_default_action_and_target_value (IdeNotification *self,
+                                                      const gchar     *action,
+                                                      GVariant        *target)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+  g_return_if_fail (action != NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+
+  if (!ide_str_equal0 (priv->default_action, action))
+    {
+      g_free (priv->default_action);
+      priv->default_action = g_strdup (action);
+    }
+
+  if (priv->default_target != NULL &&
+      target != NULL &&
+      g_variant_equal (priv->default_target, target))
+    goto unlock;
+
+  g_clear_pointer (&priv->default_target, g_variant_unref);
+  priv->default_target = target ? g_variant_ref (target) : NULL;
+
+unlock:
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+gint
+ide_notification_compare (IdeNotification *a,
+                          IdeNotification *b)
+{
+  IdeNotificationPrivate *a_priv = ide_notification_get_instance_private (a);
+  IdeNotificationPrivate *b_priv = ide_notification_get_instance_private (b);
+
+  if (a_priv->urgent)
+    {
+      if (!b_priv->urgent)
+        return -1;
+    }
+
+  if (b_priv->urgent)
+    {
+      if (!a_priv->urgent)
+        return 1;
+    }
+
+  return a_priv->priority - b_priv->priority;
+}
+
+/**
+ * ide_notification_get_progress:
+ * @self: a #IdeNotification
+ *
+ * Gets the progress for the notification.
+ *
+ * Returns: a value between 0.0 and 1.0
+ *
+ * Since: 3.32
+ */
+gdouble
+ide_notification_get_progress (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gdouble ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), 0.0);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = priv->progress;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+/**
+ * ide_notification_set_progress:
+ * @self: a #IdeNotification
+ * @progress: a value between 0.0 and 1.0
+ *
+ * Sets the progress for the notification.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_set_progress (IdeNotification *self,
+                               gdouble          progress)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  progress = CLAMP (progress, 0.0, 1.0);
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->progress != progress)
+    {
+      priv->progress = progress;
+      ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_PROGRESS]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_notification_get_has_progress:
+ * @self: a #IdeNotification
+ *
+ * Gets if the notification supports progress updates.
+ *
+ * Returns: %TRUE if progress updates are supported.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_notification_get_has_progress (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), 0.0);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = priv->has_progress;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+/**
+ * ide_notification_set_has_progress:
+ * @self: a #IdeNotification
+ * @has_progress: if @notification supports progress
+ *
+ * Set to %TRUE if the notification supports progress updates.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_set_has_progress (IdeNotification *self,
+                                   gboolean         has_progress)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  has_progress = !!has_progress;
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->has_progress != has_progress)
+    {
+      priv->has_progress = has_progress;
+      ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_HAS_PROGRESS]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+gboolean
+ide_notification_get_progress_is_imprecise (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), FALSE);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = priv->progress_is_imprecise;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+void
+ide_notification_set_progress_is_imprecise (IdeNotification *self,
+                                            gboolean         progress_is_imprecise)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  progress_is_imprecise = !!progress_is_imprecise;
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->progress_is_imprecise != progress_is_imprecise)
+    {
+      priv->progress_is_imprecise = progress_is_imprecise;
+      ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_PROGRESS_IS_IMPRECISE]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_notification_withdraw:
+ * @self: a #IdeNotification
+ *
+ * Withdraws the notification by removing it from the #IdeObject parent it
+ * belongs to.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_withdraw (IdeNotification *self)
+{
+  g_autoptr(IdeObject) parent = NULL;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  g_object_ref (self);
+  ide_object_lock (IDE_OBJECT (self));
+
+  if ((parent = ide_object_ref_parent (IDE_OBJECT (self))))
+    ide_object_remove (parent, IDE_OBJECT (self));
+
+  ide_object_unlock (IDE_OBJECT (self));
+  g_object_unref (self);
+}
+
+static gboolean
+do_withdrawal (gpointer data)
+{
+  ide_notification_withdraw (data);
+  return FALSE;
+}
+
+/**
+ * ide_notification_withdraw_in_seconds:
+ * @self: a #IdeNotification
+ * @seconds: number of seconds to withdraw after, or less than zero for a
+ *   sensible default.
+ *
+ * Withdraws @self from it's #IdeObject parent after @seconds have passed.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_withdraw_in_seconds (IdeNotification *self,
+                                      gint             seconds)
+{
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  if (seconds < 0)
+    seconds = 15;
+
+  g_timeout_add_seconds_full (G_PRIORITY_DEFAULT,
+                              seconds,
+                              do_withdrawal,
+                              g_object_ref (self),
+                              g_object_unref);
+}
+
+/**
+ * ide_notification_file_progress_callback:
+ *
+ * This function is a #GFileProgressCallback helper that will update the
+ * #IdeNotification:fraction property. @user_data must be an #IdeNotification.
+ *
+ * Remember to make sure to unref the #IdeNotification instance with
+ * g_object_unref() during the #GDestroyNotify.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_file_progress_callback (goffset  current_num_bytes,
+                                         goffset  total_num_bytes,
+                                         gpointer user_data)
+{
+  IdeNotification *self = user_data;
+  gdouble fraction = 0.0;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  if (total_num_bytes)
+    fraction = (gdouble)current_num_bytes / (gdouble)total_num_bytes;
+
+  ide_notification_set_progress (self, fraction);
+}
+
+void
+ide_notification_flatpak_progress_callback (const char *status,
+                                            guint       notification,
+                                            gboolean    estimating,
+                                            gpointer    user_data)
+{
+  IdeNotification *self = user_data;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  ide_notification_set_body (self, status);
+  ide_notification_set_progress (self, (gdouble)notification / 100.0);
+}
diff --git a/src/libide/core/ide-notification.h b/src/libide/core/ide-notification.h
new file mode 100644
index 000000000..fdb763a67
--- /dev/null
+++ b/src/libide/core/ide-notification.h
@@ -0,0 +1,143 @@
+/* ide-notification.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "ide-object.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_NOTIFICATION (ide_notification_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeNotification, ide_notification, IDE, NOTIFICATION, IdeObject)
+
+struct _IdeNotificationClass
+{
+  IdeObjectClass parent_class;
+
+  /*< private */
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeNotification *ide_notification_new                                 (void);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_attach                              (IdeNotification  *self,
+                                                                       IdeObject        *object);
+IDE_AVAILABLE_IN_3_32
+gchar           *ide_notification_dup_id                              (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_id                              (IdeNotification  *self,
+                                                                       const gchar      *id);
+IDE_AVAILABLE_IN_3_32
+gchar           *ide_notification_dup_title                           (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_title                           (IdeNotification  *self,
+                                                                       const gchar      *title);
+IDE_AVAILABLE_IN_3_32
+GIcon           *ide_notification_ref_icon                            (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_icon                            (IdeNotification  *self,
+                                                                       GIcon            *icon);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_icon_name                       (IdeNotification  *self,
+                                                                       const gchar      *icon_name);
+IDE_AVAILABLE_IN_3_32
+gchar           *ide_notification_dup_body                            (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_body                            (IdeNotification  *self,
+                                                                       const gchar      *body);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_notification_get_has_progress                    (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_has_progress                    (IdeNotification  *self,
+                                                                       gboolean          has_progress);
+IDE_AVAILABLE_IN_3_32
+gint             ide_notification_get_priority                        (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_priority                        (IdeNotification  *self,
+                                                                       gint              priority);
+IDE_AVAILABLE_IN_3_32
+gdouble          ide_notification_get_progress                        (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_progress                        (IdeNotification  *self,
+                                                                       gdouble           progress);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_notification_get_progress_is_imprecise           (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_progress_is_imprecise           (IdeNotification  *self,
+                                                                       gboolean          
progress_is_imprecise);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_notification_get_urgent                          (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_urgent                          (IdeNotification  *self,
+                                                                       gboolean          urgent);
+IDE_AVAILABLE_IN_3_32
+guint            ide_notification_get_n_buttons                       (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_notification_get_button                          (IdeNotification  *self,
+                                                                       guint             button,
+                                                                       gchar           **label,
+                                                                       GIcon           **icon,
+                                                                       gchar           **action,
+                                                                       GVariant        **target);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_add_button                          (IdeNotification  *self,
+                                                                       const gchar      *label,
+                                                                       GIcon            *icon,
+                                                                       const gchar      *detailed_action);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_add_button_with_target_value        (IdeNotification  *self,
+                                                                       const gchar      *label,
+                                                                       GIcon            *icon,
+                                                                       const gchar      *action,
+                                                                       GVariant         *target);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_notification_get_default_action                  (IdeNotification  *self,
+                                                                       gchar           **action,
+                                                                       GVariant        **target);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_default_action                  (IdeNotification  *self,
+                                                                       const gchar      *detailed_action);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_default_action_and_target_value (IdeNotification  *self,
+                                                                       const gchar      *action,
+                                                                       GVariant         *target);
+IDE_AVAILABLE_IN_3_32
+gint             ide_notification_compare                             (IdeNotification  *a,
+                                                                       IdeNotification  *b);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_withdraw                            (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_withdraw_in_seconds                 (IdeNotification  *self,
+                                                                       gint              seconds);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_file_progress_callback              (goffset           current_num_bytes,
+                                                                       goffset           total_num_bytes,
+                                                                       gpointer          user_data);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_flatpak_progress_callback           (const char       *status,
+                                                                       guint             notification,
+                                                                       gboolean          estimating,
+                                                                       gpointer          user_data);
+
+
+G_END_DECLS
diff --git a/src/libide/core/ide-notifications.c b/src/libide/core/ide-notifications.c
new file mode 100644
index 000000000..9c09da832
--- /dev/null
+++ b/src/libide/core/ide-notifications.c
@@ -0,0 +1,516 @@
+/* ide-notifications.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-notifications"
+
+#include "config.h"
+
+#include "ide-macros.h"
+#include "ide-notifications.h"
+
+struct _IdeNotifications
+{
+  IdeObject parent_instance;
+};
+
+typedef struct
+{
+  gdouble progress;
+  guint   total;
+  guint   imprecise;
+} Progress;
+
+typedef struct
+{
+  const gchar     *id;
+  IdeNotification *notif;
+} Find;
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeNotifications, ide_notifications, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+enum {
+  PROP_0,
+  PROP_HAS_PROGRESS,
+  PROP_PROGRESS,
+  PROP_PROGRESS_IS_IMPRECISE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_notifications_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeNotifications *self = IDE_NOTIFICATIONS (object);
+
+  switch (prop_id)
+    {
+    case PROP_HAS_PROGRESS:
+      g_value_set_boolean (value, ide_notifications_get_has_progress (self));
+      break;
+
+    case PROP_PROGRESS:
+      g_value_set_double (value, ide_notifications_get_progress (self));
+      break;
+
+    case PROP_PROGRESS_IS_IMPRECISE:
+      g_value_set_boolean (value, ide_notifications_get_progress_is_imprecise (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_notifications_child_notify_progress_cb (IdeNotifications *self,
+                                            GParamSpec       *pspec,
+                                            IdeNotification  *child)
+{
+  g_assert (IDE_IS_NOTIFICATIONS (self));
+  g_assert (IDE_IS_NOTIFICATION (child));
+
+  ide_object_notify_by_pspec (self, properties [PROP_PROGRESS]);
+}
+
+static void
+ide_notifications_add (IdeObject         *object,
+                       IdeObject         *sibling,
+                       IdeObject         *child,
+                       IdeObjectLocation  location)
+{
+  IdeNotifications *self = (IdeNotifications *)object;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_NOTIFICATIONS (object));
+  g_assert (IDE_IS_OBJECT (child));
+
+  if (!IDE_IS_NOTIFICATION (child))
+    {
+      g_warning ("Attempt to add something other than an IdeNotification is not allowed");
+      return;
+    }
+
+  g_signal_connect_object (child,
+                           "notify::progress",
+                           G_CALLBACK (ide_notifications_child_notify_progress_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  IDE_OBJECT_CLASS (ide_notifications_parent_class)->add (object, sibling, child, location);
+
+  g_list_model_items_changed (G_LIST_MODEL (object), ide_object_get_position (child), 0, 1);
+  ide_object_notify_by_pspec (self, properties [PROP_HAS_PROGRESS]);
+  ide_object_notify_by_pspec (self, properties [PROP_PROGRESS_IS_IMPRECISE]);
+  ide_object_notify_by_pspec (self, properties [PROP_PROGRESS]);
+}
+
+static void
+ide_notifications_remove (IdeObject *object,
+                          IdeObject *child)
+{
+  IdeNotifications *self = (IdeNotifications *)object;
+  guint position;
+
+  g_assert (IDE_IS_NOTIFICATIONS (self));
+  g_assert (IDE_IS_OBJECT (child));
+
+  g_signal_handlers_disconnect_by_func (child,
+                                        G_CALLBACK (ide_notifications_child_notify_progress_cb),
+                                        self);
+
+  position = ide_object_get_position (child);
+
+  IDE_OBJECT_CLASS (ide_notifications_parent_class)->remove (object, child);
+
+  g_list_model_items_changed (G_LIST_MODEL (object), position, 1, 0);
+  ide_object_notify_by_pspec (self, properties [PROP_HAS_PROGRESS]);
+  ide_object_notify_by_pspec (self, properties [PROP_PROGRESS_IS_IMPRECISE]);
+  ide_object_notify_by_pspec (self, properties [PROP_PROGRESS]);
+}
+
+static void
+ide_notifications_class_init (IdeNotificationsClass *klass)
+{
+  GObjectClass *g_object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *object_class = IDE_OBJECT_CLASS (klass);
+
+  g_object_class->get_property = ide_notifications_get_property;
+
+  object_class->add = ide_notifications_add;
+  object_class->remove = ide_notifications_remove;
+
+  /**
+   * IdeNotifications:has-progress:
+   *
+   * The "has-progress" property denotes if any of the notifications
+   * have progress supported.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_HAS_PROGRESS] =
+    g_param_spec_boolean ("has-progress",
+                          "Has Progress",
+                          "If any of the notifications have progress",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotifications:progress:
+   *
+   * The "progress" property is the combination of all of the notifications
+   * currently monitored. It is updated when child notifications progress
+   * changes.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PROGRESS] =
+    g_param_spec_double ("progress",
+                         "Progress",
+                         "The combined process of all child notifications",
+                         0.0, 1.0, 0.0,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotifications:progress-is-imprecise:
+   *
+   * The "progress-is-imprecise" property indicates that all progress-bearing
+   * notifications are imprecise.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PROGRESS_IS_IMPRECISE] =
+    g_param_spec_boolean ("progress-is-imprecise",
+                          "Progress is Imprecise",
+                          "If all of the notifications have imprecise progress",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (g_object_class, N_PROPS, properties);
+}
+
+static void
+ide_notifications_init (IdeNotifications *self)
+{
+#if 0
+  g_autoptr(IdeNotification) notif = NULL;
+  g_autoptr(IdeNotification) notif2 = NULL;
+  g_autoptr(IdeNotification) notif3 = NULL;
+  g_autoptr(IdeNotification) notif4 = NULL;
+  g_autoptr(IdeNotification) notif5 = NULL;
+  g_autoptr(IdeNotification) notif6 = NULL;
+  g_autoptr(IdeNotification) notif7 = NULL;
+  g_autoptr(GIcon) icon1 = NULL;
+  g_autoptr(GIcon) icon2 = NULL;
+  g_autoptr(GIcon) icon4 = NULL;
+  g_autoptr(GIcon) icon5 = NULL;
+  g_autoptr(GIcon) icon6 = NULL;
+  g_autoptr(GIcon) icon7 = NULL;
+
+  notif = ide_notification_new ();
+  ide_notification_set_title (notif, "Builder ready.");
+  ide_notification_set_has_progress (notif, FALSE);
+  ide_notification_add_button (notif, "Foo", (icon1 = g_icon_new_for_string 
("media-playback-pause-symbolic", NULL)), "debugger.pause");
+  ide_notifications_add_notification (self, notif);
+
+  notif2 = ide_notification_new ();
+  ide_notification_set_title (notif2, "Downloading libdazzle…");
+  ide_notification_set_has_progress (notif2, TRUE);
+  ide_notification_set_progress (notif2, .75);
+  ide_notification_set_default_action (notif2, "win.close");
+  ide_notification_add_button (notif2, "Foo", (icon2 = g_icon_new_for_string ("process-stop-symbolic", 
NULL)), "build-manager.stop");
+  ide_notifications_add_notification (self, notif2);
+
+  notif3 = ide_notification_new ();
+  ide_notification_set_title (notif3, "SDK Not Installed");
+  ide_notification_set_body (notif3, "The org.gnome.Calculator.json build profile requires the 
org.gnome.Platform runtime. Install it to allow this project to be built.");
+  ide_notification_set_has_progress (notif3, FALSE);
+  ide_notification_set_progress (notif3, 0);
+  ide_notification_add_button (notif3, "Download and Install", NULL, "win.close");
+  ide_notification_set_default_action (notif3, "win.close");
+  ide_notification_set_urgent (notif3, TRUE);
+  ide_notifications_add_notification (self, notif3);
+
+  notif4 = ide_notification_new ();
+  ide_notification_set_title (notif4, "Code Analytics Unavailable");
+  ide_notification_set_body (notif4, "Code highlighting, error detection, and macros are not fully 
available, due to this project not being built recently. Rebuild to fully enable these features.");
+  ide_notification_set_has_progress (notif4, FALSE);
+  ide_notification_set_progress (notif4, 0);
+  ide_notification_set_default_action (notif4, "win.close");
+  ide_notifications_add_notification (self, notif4);
+
+  notif5 = ide_notification_new ();
+  ide_notification_set_title (notif5, "Running Partial Build");
+  ide_notification_set_body (notif5, "Diagnostics and autocompletion may be limited until complete.");
+  ide_notification_set_has_progress (notif5, TRUE);
+  ide_notification_set_progress_is_imprecise (notif5, TRUE);
+  ide_notification_set_progress (notif5, 0);
+  ide_notification_add_button (notif5, NULL, (icon5 = g_icon_new_for_string ("process-stop-symbolic", 
NULL)), "win.close");
+  ide_notifications_add_notification (self, notif5);
+
+  notif6 = ide_notification_new ();
+  ide_notification_set_title (notif6, "Indexing Source Code");
+  ide_notification_set_body (notif6, "Search, diagnostics, and autocompletion may be limited until 
complete.");
+  ide_notification_set_has_progress (notif6, TRUE);
+  ide_notification_set_progress (notif6, 0);
+  ide_notification_set_progress_is_imprecise (notif6, TRUE);
+  ide_notification_add_button (notif6, NULL, (icon6 = g_icon_new_for_string 
("media-playback-pause-symbolic", NULL)), "win.close");
+  ide_notifications_add_notification (self, notif6);
+
+  notif7 = ide_notification_new ();
+  ide_notification_set_title (notif7, "Downloading org.gnome.Platform");
+  ide_notification_set_body (notif7, "3 minutes remaining");
+  ide_notification_set_has_progress (notif7, TRUE);
+  ide_notification_set_progress (notif7, 0);
+  ide_notification_add_button (notif7, NULL, (icon7 = g_icon_new_for_string ("process-stop-symbolic", 
NULL)), "win.close");
+  ide_notifications_add_notification (self, notif7);
+
+  ide_notification_withdraw_in_seconds (notif, 10);
+  ide_notification_withdraw_in_seconds (notif2, 12);
+  ide_notification_withdraw_in_seconds (notif3, 14);
+  ide_notification_withdraw_in_seconds (notif4, 16);
+  ide_notification_withdraw_in_seconds (notif5, 18);
+#endif
+}
+
+/**
+ * ide_notifications_new:
+ *
+ * Create a new #IdeNotifications.
+ *
+ * Usually, creating this is not necessary, as the #IdeContext root
+ * #IdeObject will create it automatically.
+ *
+ * Returns: (transfer full): a newly created #IdeNotifications
+ *
+ * Since: 3.32
+ */
+IdeNotifications *
+ide_notifications_new (void)
+{
+  return g_object_new (IDE_TYPE_NOTIFICATIONS, NULL);
+}
+
+/**
+ * ide_notifications_add_notification:
+ * @self: an #IdeNotifications
+ * @notification: an #IdeNotification
+ *
+ * Adds @notification as a child of @self, sorting it by priority
+ * and urgency.
+ *
+ * Since: 3.32
+ */
+void
+ide_notifications_add_notification (IdeNotifications *self,
+                                    IdeNotification  *notification)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_NOTIFICATIONS (self));
+  g_return_if_fail (IDE_IS_NOTIFICATION (notification));
+
+  ide_object_insert_sorted (IDE_OBJECT (self),
+                            IDE_OBJECT (notification),
+                            (GCompareDataFunc)ide_notification_compare,
+                            NULL);
+}
+
+static GType
+ide_notifications_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_NOTIFICATION;
+}
+
+static guint
+ide_notifications_get_n_items (GListModel *model)
+{
+  return ide_object_get_n_children (IDE_OBJECT (model));
+}
+
+static gpointer
+ide_notifications_get_item (GListModel *model,
+                            guint       position)
+{
+  return ide_object_get_nth_child (IDE_OBJECT (model), position);
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = ide_notifications_get_item_type;
+  iface->get_n_items = ide_notifications_get_n_items;
+  iface->get_item = ide_notifications_get_item;
+}
+
+static void
+collect_progress_cb (gpointer item,
+                     gpointer user_data)
+{
+  IdeNotification *notif = item;
+  Progress *prog = user_data;
+
+  g_assert (IDE_IS_NOTIFICATION (notif));
+  g_assert (prog != NULL);
+
+  if (ide_notification_get_has_progress (notif))
+    {
+      if (ide_notification_get_progress_is_imprecise (notif))
+        prog->imprecise++;
+      else
+        prog->progress += ide_notification_get_progress (notif);
+
+      prog->total++;
+    }
+}
+
+/**
+ * ide_notifications_get_progress:
+ * @self: a #IdeNotifications
+ *
+ * Gets the combined progress of the notifications contained in this
+ * #IdeNotifications object.
+ *
+ * Returns: A double between 0.0 and 1.0
+ *
+ * Since: 3.32
+ */
+gdouble
+ide_notifications_get_progress (IdeNotifications *self)
+{
+  Progress prog = {0};
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATIONS (self), 0.0);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ide_object_foreach (IDE_OBJECT (self), collect_progress_cb, &prog);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  if (prog.total > 0)
+    {
+      if (prog.imprecise != prog.total)
+        return prog.progress / (gdouble)(prog.total - prog.imprecise);
+      else
+        return prog.progress / (gdouble)prog.total;
+    }
+
+  return 0.0;
+}
+
+/**
+ * ide_notifications_get_has_progress:
+ * @self: a #IdeNotifications
+ *
+ * Gets if any of the notification support progress updates.
+ *
+ * Returns: %TRUE if any notification has progress
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_notifications_get_has_progress (IdeNotifications *self)
+{
+  Progress prog = {0};
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATIONS (self), 0.0);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ide_object_foreach (IDE_OBJECT (self), collect_progress_cb, &prog);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return prog.total > 0;
+}
+
+/**
+ * ide_notifications_get_progress_is_imprecise:
+ * @self: a #IdeNotifications
+ *
+ * Checks if all of the notifications with progress are imprecise.
+ *
+ * Returns: %TRUE if all progress-supporting notifications are imprecise.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_notifications_get_progress_is_imprecise (IdeNotifications *self)
+{
+  Progress prog = {0};
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATIONS (self), 0.0);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ide_object_foreach (IDE_OBJECT (self), collect_progress_cb, &prog);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  if (prog.total > 0)
+    return prog.imprecise == prog.total;
+
+  return FALSE;
+}
+
+static void
+find_by_id (gpointer item,
+            gpointer user_data)
+{
+  IdeNotification *notif = item;
+  Find *find = user_data;
+  g_autofree gchar *id = NULL;
+
+  if (find->notif)
+    return;
+
+  id = ide_notification_dup_id (notif);
+
+  if (ide_str_equal0 (find->id, id))
+    find->notif = g_object_ref (notif);
+}
+
+/**
+ * ide_notifications_find_by_id:
+ * @self: a #IdeNotifications
+ * @id: the id of the notification
+ *
+ * Finds the first #IdeNotification registered with @self with
+ * #IdeNotification:id of @id.
+ *
+ * Returns: (transfer full) (nullable): an #IdeNotification or %NULL
+ *
+ * Since: 3.32
+ */
+IdeNotification *
+ide_notifications_find_by_id (IdeNotifications *self,
+                              const gchar      *id)
+{
+  Find find = { id, NULL };
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATIONS (self), NULL);
+  g_return_val_if_fail (id != NULL, NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ide_object_foreach (IDE_OBJECT (self), find_by_id, &find);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&find.notif);
+}
diff --git a/src/libide/core/ide-notifications.h b/src/libide/core/ide-notifications.h
new file mode 100644
index 000000000..fc482cfe4
--- /dev/null
+++ b/src/libide/core/ide-notifications.h
@@ -0,0 +1,48 @@
+/* ide-notifications.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "ide-object.h"
+#include "ide-notification.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_NOTIFICATIONS (ide_notifications_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeNotifications, ide_notifications, IDE, NOTIFICATIONS, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeNotifications *ide_notifications_new                       (void);
+IDE_AVAILABLE_IN_3_32
+void              ide_notifications_add_notification          (IdeNotifications *self,
+                                                               IdeNotification  *notification);
+IDE_AVAILABLE_IN_3_32
+gdouble           ide_notifications_get_progress              (IdeNotifications *self);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_notifications_get_has_progress          (IdeNotifications *self);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_notifications_get_progress_is_imprecise (IdeNotifications *self);
+IDE_AVAILABLE_IN_3_32
+IdeNotification  *ide_notifications_find_by_id                (IdeNotifications *self,
+                                                               const gchar      *id);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-object-box.c b/src/libide/core/ide-object-box.c
new file mode 100644
index 000000000..3a3ad2383
--- /dev/null
+++ b/src/libide/core/ide-object-box.c
@@ -0,0 +1,289 @@
+/* ide-object-box.c
+ *
+ * Copyright 2018 Christian Hergert <unknown domain org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-object-box"
+
+#include "config.h"
+
+#include "ide-object-box.h"
+#include "ide-macros.h"
+
+struct _IdeObjectBox
+{
+  IdeObject  parent_instance;
+  GObject   *object;
+  guint      propagate_disposal : 1;
+};
+
+G_DEFINE_TYPE (IdeObjectBox, ide_object_box, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_OBJECT,
+  PROP_PROPAGATE_DISPOSAL,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_object_box_set_object (IdeObjectBox *self,
+                           GObject      *object)
+{
+  g_return_if_fail (IDE_IS_OBJECT_BOX (self));
+  g_return_if_fail (G_IS_OBJECT (object));
+  g_return_if_fail (g_object_get_data (object, "IDE_OBJECT_BOX") == NULL);
+
+  self->object = g_object_ref (object);
+  g_object_set_data (self->object, "IDE_OBJECT_BOX", self);
+}
+
+/**
+ * ide_object_box_new:
+ *
+ * Create a new #IdeObjectBox.
+ *
+ * Returns: (transfer full): a newly created #IdeObjectBox
+ *
+ * Since: 3.32
+ */
+IdeObjectBox *
+ide_object_box_new (GObject *object)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+
+  return g_object_new (IDE_TYPE_OBJECT_BOX,
+                       "object", object,
+                       NULL);
+}
+
+static gchar *
+ide_object_box_repr (IdeObject *object)
+{
+  g_autoptr(GObject) obj = ide_object_box_ref_object (IDE_OBJECT_BOX (object));
+
+  if (obj != NULL)
+    return g_strdup_printf ("%s object=\"%s\"",
+                            G_OBJECT_TYPE_NAME (object),
+                            G_OBJECT_TYPE_NAME (obj));
+  else
+    return IDE_OBJECT_CLASS (ide_object_box_parent_class)->repr (object);
+}
+
+static void
+ide_object_box_destroy (IdeObject *object)
+{
+  IdeObjectBox *self = (IdeObjectBox *)object;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_OBJECT (self));
+
+  g_object_ref (self);
+
+  /* Clear the backpointer before any disposal to the object, since that
+   * will possibly result in the object calling back into this peer object.
+   */
+  if (self->object)
+    {
+      g_object_set_data (G_OBJECT (self->object), "IDE_OBJECT_BOX", NULL);
+      if (self->propagate_disposal)
+        g_object_run_dispose (G_OBJECT (self->object));
+    }
+
+  IDE_OBJECT_CLASS (ide_object_box_parent_class)->destroy (object);
+
+  g_clear_object (&self->object);
+
+  g_object_unref (self);
+}
+
+static void
+ide_object_box_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  IdeObjectBox *self = IDE_OBJECT_BOX (object);
+
+  switch (prop_id)
+    {
+    case PROP_OBJECT:
+      g_value_take_object (value, ide_object_box_ref_object (self));
+      break;
+
+    case PROP_PROPAGATE_DISPOSAL:
+      g_value_set_boolean (value, self->propagate_disposal);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_object_box_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  IdeObjectBox *self = IDE_OBJECT_BOX (object);
+
+  switch (prop_id)
+    {
+    case PROP_OBJECT:
+      ide_object_box_set_object (self, g_value_get_object (value));
+      break;
+
+    case PROP_PROPAGATE_DISPOSAL:
+      self->propagate_disposal = g_value_get_boolean (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_object_box_class_init (IdeObjectBoxClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->get_property = ide_object_box_get_property;
+  object_class->set_property = ide_object_box_set_property;
+
+  i_object_class->destroy = ide_object_box_destroy;
+  i_object_class->repr = ide_object_box_repr;
+
+  /**
+   * IdeObjectBox:object:
+   *
+   * The "object" property contains the object that is boxed and
+   * placed onto the object graph using this box.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_OBJECT] =
+    g_param_spec_object ("object",
+                         "Object",
+                         "The boxed object",
+                         G_TYPE_OBJECT,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeObjectBox:propagate-disposal:
+   *
+   * The "propagate-disposal" property denotes if the #IdeObject:object
+   * property contents should have g_object_run_dispose() called when the
+   * #IdeObjectBox is destroyed.
+   *
+   * This is useful when you want to force disposal of an external object
+   * when @self is removed from the object tree.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PROPAGATE_DISPOSAL] =
+    g_param_spec_boolean ("propagate-disposal",
+                          "Propagate Disposal",
+                          "If the object should be disposed when the box is destroyed",
+                          TRUE,
+                          (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_object_box_init (IdeObjectBox *self)
+{
+  self->propagate_disposal = TRUE;
+}
+
+/**
+ * ide_object_box_ref_object:
+ * @self: an #IdeObjectBox
+ *
+ * Gets the boxed object.
+ *
+ * Returns: (transfer full) (nullable) (type GObject): a #GObject or %NULL
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_object_box_ref_object (IdeObjectBox *self)
+{
+  GObject *ret;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_OBJECT_BOX (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = self->object ? g_object_ref (self->object) : NULL;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_box_from_object:
+ * @object: a #GObject
+ *
+ * Gets the #IdeObjectBox that contains @object, if any.
+ *
+ * This function may only be called from the main thread.
+ *
+ * Returns: (transfer none): an #IdeObjectBox
+ *
+ * Since: 3.32
+ */
+IdeObjectBox *
+ide_object_box_from_object (GObject *object)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (G_IS_OBJECT (object), NULL);
+
+  return g_object_get_data (G_OBJECT (object), "IDE_OBJECT_BOX");
+}
+
+/**
+ * ide_object_box_contains:
+ * @self: a #IdeObjectBox
+ * @instance: (type GObject) (nullable): a #GObject or %NULL
+ *
+ * Checks if @self contains @instance.
+ *
+ * Returns: %TRUE if #IdeObjectBox:object matches @instance
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_object_box_contains (IdeObjectBox *self,
+                         gpointer      instance)
+{
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_OBJECT_BOX (self), FALSE);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = (instance == (gpointer)self->object);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
diff --git a/src/libide/core/ide-object-box.h b/src/libide/core/ide-object-box.h
new file mode 100644
index 000000000..eb81085fb
--- /dev/null
+++ b/src/libide/core/ide-object-box.h
@@ -0,0 +1,46 @@
+/* ide-object-box.h
+ *
+ * Copyright 2018 Christian Hergert <unknown domain org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include "ide-object.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_OBJECT_BOX (ide_object_box_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeObjectBox, ide_object_box, IDE, OBJECT_BOX, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeObjectBox *ide_object_box_new         (GObject      *object);
+IDE_AVAILABLE_IN_3_32
+gpointer      ide_object_box_ref_object  (IdeObjectBox *self);
+IDE_AVAILABLE_IN_3_32
+IdeObjectBox *ide_object_box_from_object (GObject      *object);
+IDE_AVAILABLE_IN_3_32
+gboolean      ide_object_box_contains    (IdeObjectBox *self,
+                                          gpointer      instance);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-object-notify.c b/src/libide/core/ide-object-notify.c
new file mode 100644
index 000000000..42cfe92ed
--- /dev/null
+++ b/src/libide/core/ide-object-notify.c
@@ -0,0 +1,114 @@
+/* ide-object-notify.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-object-notify"
+
+#include "config.h"
+
+#include "ide-object.h"
+#include "ide-macros.h"
+
+typedef struct
+{
+  GObject    *object;
+  GParamSpec *pspec;
+} NotifyInMain;
+
+static gboolean
+ide_object_notify_in_main_cb (gpointer data)
+{
+  NotifyInMain *notify = data;
+
+  g_assert (notify != NULL);
+  g_assert (G_IS_OBJECT (notify->object));
+  g_assert (notify->pspec != NULL);
+
+  g_object_notify_by_pspec (notify->object, notify->pspec);
+
+  g_object_unref (notify->object);
+  g_param_spec_unref (notify->pspec);
+  g_slice_free (NotifyInMain, notify);
+
+  return G_SOURCE_REMOVE;
+}
+
+/**
+ * ide_object_notify_by_pspec:
+ * @instance: a #IdeObjectNotify
+ * @pspec: a #GParamSpec
+ *
+ * Like g_object_notify_by_pspec() if the caller is in the main-thread.
+ * Otherwise, the request is deferred to the main thread.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_notify_by_pspec (gpointer    instance,
+                            GParamSpec *pspec)
+{
+  NotifyInMain *notify;
+
+  g_return_if_fail (G_IS_OBJECT (instance));
+  g_return_if_fail (G_IS_PARAM_SPEC (pspec));
+
+  if G_LIKELY (IDE_IS_MAIN_THREAD ())
+    {
+      g_object_notify_by_pspec (instance, pspec);
+      return;
+    }
+
+  notify = g_slice_new0 (NotifyInMain);
+  notify->pspec = g_param_spec_ref (pspec);
+  notify->object = g_object_ref (instance);
+
+  g_timeout_add (0, ide_object_notify_in_main_cb, g_steal_pointer (&notify));
+}
+
+/**
+ * ide_object_notify_in_main:
+ * @instance: (type GObject.Object): a #GObject
+ * @pspec: a #GParamSpec
+ *
+ * This helper will perform a g_object_notify_by_pspec() with the
+ * added requirement that it is run from the applications main thread.
+ *
+ * You may want to do this when modifying state from a thread, but only
+ * notify from the Gtk+ thread.
+ *
+ * This will *always* return to the default main context, and never
+ * emit ::notify immediately.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_notify_in_main (gpointer    instance,
+                           GParamSpec *pspec)
+{
+  NotifyInMain *notify;
+
+  g_return_if_fail (G_IS_OBJECT (instance));
+  g_return_if_fail (G_IS_PARAM_SPEC (pspec));
+
+  notify = g_slice_new0 (NotifyInMain);
+  notify->pspec = g_param_spec_ref (pspec);
+  notify->object = g_object_ref (instance);
+
+  g_timeout_add (0, ide_object_notify_in_main_cb, g_steal_pointer (&notify));
+}
diff --git a/src/libide/core/ide-object.c b/src/libide/core/ide-object.c
new file mode 100644
index 000000000..ffabce957
--- /dev/null
+++ b/src/libide/core/ide-object.c
@@ -0,0 +1,1367 @@
+/* ide-object.c
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-object"
+
+#include "config.h"
+
+#include "ide-context.h"
+#include "ide-object.h"
+#include "ide-macros.h"
+
+/**
+ * SECTION:ide-object
+ * @title: IdeObject
+ * @short_description: Base object with support for object trees
+ *
+ * #IdeObject is a specialized #GObject for use in Builder. It provides a
+ * hierarchy of objects using a specialized tree similar to a DOM. You can
+ * insert/append/prepend objects to a parent node, and track their lifetime
+ * as part of the tree.
+ *
+ * When an object is removed from the tree, it can automatically be destroyed
+ * via the #IdeObject::destroy signal. This is useful as it may cause the
+ * children of that object to be removed, recursively destroying the objects
+ * descendants. This behavior is ideal when you want a large amount of objects
+ * to be reclaimed once an ancestor is no longer necessary.
+ *
+ * #IdeObject's may also have a #GCancellable associated with them. The
+ * cancellable is created on demand when ide_object_ref_cancellable() is
+ * called. When the object is destroyed, the #GCancellable::cancel signal
+ * is emitted. This allows automatic cleanup of asynchronous operations
+ * when used properly.
+ *
+ * Since: 3.32
+ */
+
+typedef struct
+{
+  GRecMutex     mutex;
+  GCancellable *cancellable;
+  IdeObject    *parent;
+  GQueue        children;
+  GList         link;
+  guint         in_destruction : 1;
+  guint         destroyed : 1;
+} IdeObjectPrivate;
+
+typedef struct
+{
+  GType      type;
+  IdeObject *child;
+} GetChildTyped;
+
+typedef struct
+{
+  GType      type;
+  GPtrArray *array;
+} GetChildrenTyped;
+
+enum {
+  PROP_0,
+  PROP_CANCELLABLE,
+  PROP_PARENT,
+  N_PROPS
+};
+
+enum {
+  DESTROY,
+  N_SIGNALS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeObject, ide_object, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static inline void
+ide_object_private_lock (IdeObjectPrivate *priv)
+{
+  g_rec_mutex_lock (&priv->mutex);
+}
+
+static inline void
+ide_object_private_unlock (IdeObjectPrivate *priv)
+{
+  g_rec_mutex_unlock (&priv->mutex);
+}
+
+static gboolean
+check_disposition (IdeObject        *child,
+                   IdeObject        *parent,
+                   IdeObjectPrivate *sibling_priv)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (child);
+
+  if (priv->parent != NULL)
+    {
+      g_critical ("Attempt to add %s to %s, but it already has a parent",
+                  G_OBJECT_TYPE_NAME (child),
+                  G_OBJECT_TYPE_NAME (parent));
+      return FALSE;
+    }
+
+  if (sibling_priv && sibling_priv->parent != parent)
+    {
+      g_critical ("Attempt to add child relative to sibling of another parent");
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+static gchar *
+ide_object_real_repr (IdeObject *self)
+{
+  return g_strdup (G_OBJECT_TYPE_NAME (self));
+}
+
+static void
+ide_object_real_add (IdeObject         *self,
+                     IdeObject         *sibling,
+                     IdeObject         *child,
+                     IdeObjectLocation  location)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  IdeObjectPrivate *child_priv = ide_object_get_instance_private (child);
+  IdeObjectPrivate *sibling_priv = ide_object_get_instance_private (sibling);
+
+  g_assert (IDE_IS_OBJECT (self));
+  g_assert (IDE_IS_OBJECT (child));
+  g_assert (!sibling || IDE_IS_OBJECT (sibling));
+
+  if (location == IDE_OBJECT_BEFORE_SIBLING ||
+      location == IDE_OBJECT_AFTER_SIBLING)
+    g_return_if_fail (IDE_IS_OBJECT (sibling));
+
+  ide_object_private_lock (priv);
+  ide_object_private_lock (child_priv);
+
+  if (sibling)
+    ide_object_private_lock (sibling_priv);
+
+  if (!check_disposition (child, self, NULL))
+    goto unlock;
+
+  switch (location)
+    {
+    case IDE_OBJECT_START:
+      g_queue_push_head_link (&priv->children, &child_priv->link);
+      break;
+
+    case IDE_OBJECT_END:
+      g_queue_push_tail_link (&priv->children, &child_priv->link);
+      break;
+
+    case IDE_OBJECT_BEFORE_SIBLING:
+      _g_queue_insert_before_link (&priv->children, &sibling_priv->link, &child_priv->link);
+      break;
+
+    case IDE_OBJECT_AFTER_SIBLING:
+      _g_queue_insert_after_link (&priv->children, &sibling_priv->link, &child_priv->link);
+      break;
+
+    default:
+      g_critical ("Invalid location to add object child");
+      goto unlock;
+    }
+
+  child_priv->parent = self;
+  g_object_ref (child);
+
+  if (IDE_OBJECT_GET_CLASS (child)->parent_set)
+    IDE_OBJECT_GET_CLASS (child)->parent_set (child, self);
+
+unlock:
+  if (sibling)
+    ide_object_private_unlock (sibling_priv);
+  ide_object_private_unlock (child_priv);
+  ide_object_private_unlock (priv);
+}
+
+static void
+ide_object_real_remove (IdeObject *self,
+                        IdeObject *child)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  IdeObjectPrivate *child_priv = ide_object_get_instance_private (child);
+
+  g_assert (IDE_IS_OBJECT (self));
+  g_assert (IDE_IS_OBJECT (child));
+
+  ide_object_private_lock (priv);
+  ide_object_private_lock (child_priv);
+
+  g_assert (child_priv->parent == self);
+
+  if (child_priv->parent != self)
+    {
+      g_critical ("Attempt to remove child object from incorrect parent");
+      ide_object_private_unlock (child_priv);
+      ide_object_private_unlock (priv);
+      return;
+    }
+
+  g_queue_unlink (&priv->children, &child_priv->link);
+  child_priv->parent = NULL;
+
+  if (IDE_OBJECT_GET_CLASS (child)->parent_set)
+    IDE_OBJECT_GET_CLASS (child)->parent_set (child, NULL);
+
+  ide_object_private_unlock (child_priv);
+  ide_object_private_unlock (priv);
+
+  g_object_unref (child);
+}
+
+static void
+ide_object_real_destroy (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  IdeObject *hold = NULL;
+
+  g_assert (IDE_IS_OBJECT (self));
+
+  /* We already hold the instance lock, for destroy */
+
+  g_cancellable_cancel (priv->cancellable);
+
+  if (priv->parent != NULL)
+    {
+      hold = g_object_ref (self);
+      ide_object_remove (priv->parent, self);
+    }
+
+  g_assert (priv->parent == NULL);
+  g_assert (priv->link.prev == NULL);
+  g_assert (priv->link.next == NULL);
+
+  while (priv->children.head != NULL)
+    {
+      IdeObject *child = priv->children.head->data;
+
+      ide_object_destroy (child);
+    }
+
+  g_assert (priv->children.tail == NULL);
+  g_assert (priv->children.head == NULL);
+  g_assert (priv->children.length == 0);
+
+  g_assert (priv->parent == NULL);
+  g_assert (priv->link.prev == NULL);
+  g_assert (priv->link.next == NULL);
+
+  priv->destroyed = TRUE;
+
+  if (hold != NULL)
+    g_object_unref (hold);
+}
+
+static gboolean
+ide_object_destroy_in_main_cb (IdeObject *object)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_OBJECT (object));
+
+  ide_object_destroy (object);
+
+  return G_SOURCE_REMOVE;
+}
+
+void
+ide_object_destroy (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_OBJECT (self));
+
+  g_object_ref (self);
+  ide_object_private_lock (priv);
+
+  /* If we are not on the main thread, we want to detach from the
+   * object tree and then dispatch the rest of the destroy to the
+   * main thread (so no threaded cleanup can occur).
+   */
+
+  if (IDE_IS_MAIN_THREAD ())
+    {
+      g_cancellable_cancel (priv->cancellable);
+      if (!priv->in_destruction && !priv->destroyed)
+        g_object_run_dispose (G_OBJECT (self));
+    }
+  else
+    {
+      g_autoptr(IdeObject) parent = NULL;
+
+      if ((parent = ide_object_ref_parent (self)))
+        ide_object_remove (parent, self);
+
+      g_idle_add_full (G_PRIORITY_LOW + 1000,
+                       (GSourceFunc)ide_object_destroy_in_main_cb,
+                       g_object_ref (self),
+                       g_object_unref);
+    }
+
+  ide_object_private_unlock (priv);
+  g_object_unref (self);
+}
+
+static gboolean
+ide_object_dispose_from_main_cb (gpointer user_data)
+{
+  IdeObject *self = user_data;
+  g_object_run_dispose (G_OBJECT (self));
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_object_dispose (GObject *object)
+{
+  IdeObject *self = (IdeObject *)object;
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  if (!IDE_IS_MAIN_THREAD ())
+    {
+      /* We are not on the main thread and might lose our last reference count.
+       * Pass this object to the main thread for disposal. This usually only
+       * happens when an object was temporarily created/destroyed on a thread.
+       */
+      g_idle_add_full (G_PRIORITY_LOW + 1000,
+                       ide_object_dispose_from_main_cb,
+                       g_object_ref (self),
+                       g_object_unref);
+      return;
+    }
+
+  g_assert (IDE_IS_OBJECT (object));
+
+  ide_object_private_lock (priv);
+
+  if (!priv->in_destruction)
+    {
+      priv->in_destruction = TRUE;
+      g_signal_emit (self, signals [DESTROY], 0);
+      priv->in_destruction = FALSE;
+    }
+
+  ide_object_private_unlock (priv);
+
+  G_OBJECT_CLASS (ide_object_parent_class)->dispose (object);
+}
+
+static void
+ide_object_finalize (GObject *object)
+{
+  IdeObject *self = (IdeObject *)object;
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  if (!IDE_IS_MAIN_THREAD ())
+    {
+      g_critical ("Attempt to finalize %s on a thread which is not allowed. Leaking instead.",
+                  G_OBJECT_TYPE_NAME (object));
+      return;
+    }
+
+  g_assert (priv->parent == NULL);
+  g_assert (priv->children.length == 0);
+  g_assert (priv->children.head == NULL);
+  g_assert (priv->children.tail == NULL);
+  g_assert (priv->link.prev == NULL);
+  g_assert (priv->link.next == NULL);
+
+  g_clear_object (&priv->cancellable);
+  g_rec_mutex_clear (&priv->mutex);
+
+  G_OBJECT_CLASS (ide_object_parent_class)->finalize (object);
+}
+
+static void
+ide_object_get_property (GObject    *object,
+                         guint       prop_id,
+                         GValue     *value,
+                         GParamSpec *pspec)
+{
+  IdeObject *self = IDE_OBJECT (object);
+
+  switch (prop_id)
+    {
+    case PROP_PARENT:
+      g_value_take_object (value, ide_object_ref_parent (self));
+      break;
+
+    case PROP_CANCELLABLE:
+      g_value_take_object (value, ide_object_ref_cancellable (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_object_set_property (GObject      *object,
+                         guint         prop_id,
+                         const GValue *value,
+                         GParamSpec   *pspec)
+{
+  IdeObject *self = IDE_OBJECT (object);
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_CANCELLABLE:
+      priv->cancellable = g_value_dup_object (value);
+      break;
+
+    case PROP_PARENT:
+      {
+        IdeObject *parent = g_value_get_object (value);
+        if (parent != NULL)
+          ide_object_append (parent, IDE_OBJECT (self));
+      }
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_object_class_init (IdeObjectClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_object_dispose;
+  object_class->finalize = ide_object_finalize;
+  object_class->get_property = ide_object_get_property;
+  object_class->set_property = ide_object_set_property;
+
+  klass->add = ide_object_real_add;
+  klass->remove = ide_object_real_remove;
+  klass->destroy = ide_object_real_destroy;
+  klass->repr = ide_object_real_repr;
+
+  /**
+   * IdeObject:parent:
+   *
+   * The parent #IdeObject, if any.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PARENT] =
+    g_param_spec_object ("parent",
+                         "Parent",
+                         "The parent IdeObject",
+                         IDE_TYPE_OBJECT,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeObject:cancellable:
+   *
+   * The "cancellable" property is a #GCancellable that can be used by operations
+   * that will be cancelled when the #IdeObject::destroy signal is emitted on @self.
+   *
+   * This is convenient when you want operations to automatically be cancelled when
+   * part of teh object tree is segmented.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_CANCELLABLE] =
+    g_param_spec_object ("cancellable",
+                         "Cancellable",
+                         "A GCancellable for the object to use in operations",
+                         G_TYPE_CANCELLABLE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeObject::destroy:
+   *
+   * The "destroy" signal is emitted when the object should destroy itself
+   * and cleanup any state that is no longer necessary. This happens when
+   * the object has been removed from the because it was requested to be
+   * destroyed, or because a parent object is being destroyed.
+   *
+   * If you do not want to receive the "destroy" signal, then you must
+   * manually remove the object from the tree using ide_object_remove()
+   * while holding a reference to the object.
+   *
+   * Since: 3.32
+   */
+  signals [DESTROY] =
+    g_signal_new ("destroy",
+                  G_TYPE_FROM_CLASS (klass),
+                  (G_SIGNAL_RUN_CLEANUP | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS),
+                  G_STRUCT_OFFSET (IdeObjectClass, destroy),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__VOID,
+                  G_TYPE_NONE, 0);
+  g_signal_set_va_marshaller (signals [DESTROY],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__VOIDv);
+}
+
+static void
+ide_object_init (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  priv->link.data = self;
+
+  g_rec_mutex_init (&priv->mutex);
+}
+
+/**
+ * ide_object_new:
+ * @type: a #GType of an #IdeObject derived object
+ * @parent: (nullable): an optional #IdeObject parent
+ *
+ * This is a convenience function for creating an #IdeObject and appending it
+ * to a parent.
+ *
+ * This function may only be called from the main-thread, as calling from any
+ * other thread would potentially risk being disposed before returning.
+ *
+ * Returns: (transfer full) (type IdeObject): a new #IdeObject
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_object_new (GType      type,
+                IdeObject *parent)
+{
+  IdeObject *ret;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (g_type_is_a (type, IDE_TYPE_OBJECT), NULL);
+  g_return_val_if_fail (!parent || IDE_IS_OBJECT (parent), NULL);
+
+  ret = g_object_new (type, NULL);
+  if (parent != NULL)
+    ide_object_append (parent, ret);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_get_n_children:
+ * @self: a #IdeObject
+ *
+ * Gets the number of children for an object.
+ *
+ * Returns: the number of children
+ *
+ * Since: 3.32
+ */
+guint
+ide_object_get_n_children (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  guint ret;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), 0);
+
+  ide_object_private_lock (priv);
+  ret = priv->children.length;
+  ide_object_private_unlock (priv);
+
+  return ret;
+}
+
+/**
+ * ide_object_get_nth_child:
+ * @self: a #IdeObject
+ * @nth: position of child to fetch
+ *
+ * Gets the @nth child of @self.
+ *
+ * A full reference to the child is returned.
+ *
+ * Returns: (transfer full) (nullable): an #IdeObject or %NULL
+ *
+ * Since: 3.32
+ */
+IdeObject *
+ide_object_get_nth_child (IdeObject *self,
+                          guint      nth)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  IdeObject *ret;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), 0);
+
+  ide_object_private_lock (priv);
+  ret = g_list_nth_data (priv->children.head, nth);
+  if (ret != NULL)
+    g_object_ref (ret);
+  ide_object_private_unlock (priv);
+
+  g_return_val_if_fail (!ret || IDE_IS_OBJECT (ret), NULL);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_get_position:
+ * @self: a #IdeObject
+ *
+ * Gets the position of @self within the parent node.
+ *
+ * Returns: the position, starting from 0
+ *
+ * Since: 3.32
+ */
+guint
+ide_object_get_position (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  guint ret = 0;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), 0);
+
+  ide_object_private_lock (priv);
+
+  if (priv->parent != NULL)
+    {
+      IdeObjectPrivate *parent_priv = ide_object_get_instance_private (priv->parent);
+      ret = g_list_position (parent_priv->children.head, &priv->link);
+    }
+
+  ide_object_private_unlock (priv);
+
+  return ret;
+}
+
+/**
+ * ide_object_lock:
+ * @self: a #IdeObject
+ *
+ * Acquires the lock for @self. This can be useful when you need to do
+ * multi-threaded work with @self and want to ensure exclusivity.
+ *
+ * Call ide_object_unlock() to release the lock.
+ *
+ * The synchronization used is a #GRecMutex.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_lock (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_OBJECT (self));
+
+  ide_object_private_lock (priv);
+}
+
+/**
+ * ide_object_unlock:
+ * @self: a #IdeObject
+ *
+ * Releases a previously acuiqred lock from ide_object_lock().
+ *
+ * The synchronization used is a #GRecMutex.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_unlock (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_OBJECT (self));
+
+  ide_object_private_unlock (priv);
+}
+
+/**
+ * ide_object_ref_cancellable:
+ * @self: a #IdeObject
+ *
+ * Gets a #GCancellable for the object.
+ *
+ * Returns: (transfer none) (not nullable): a #GCancellable
+ *
+ * Since: 3.32
+ */
+GCancellable *
+ide_object_ref_cancellable (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  GCancellable *ret;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+  ide_object_private_lock (priv);
+  if (priv->cancellable == NULL)
+    priv->cancellable = g_cancellable_new ();
+  ret = g_object_ref (priv->cancellable);
+  ide_object_private_unlock (priv);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_get_parent:
+ * @self: a #IdeObject
+ *
+ * Gets the parent #IdeObject, if any.
+ *
+ * This function may only be called from the main thread.
+ *
+ * Returns: (transfer none) (nullable): an #IdeObject or %NULL
+ *
+ * Since: 3.32
+ */
+IdeObject *
+ide_object_get_parent (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  IdeObject *ret;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+  ide_object_private_lock (priv);
+  ret = priv->parent;
+  ide_object_private_unlock (priv);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_ref_parent:
+ * @self: a #IdeObject
+ *
+ * Gets the parent #IdeObject, if any.
+ *
+ * Returns: (transfer full) (nullable): an #IdeObject or %NULL
+ *
+ * Since: 3.32
+ */
+IdeObject *
+ide_object_ref_parent (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  IdeObject *ret;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+  ide_object_private_lock (priv);
+  ret = priv->parent ? g_object_ref (priv->parent) : NULL;
+  ide_object_private_unlock (priv);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_is_root:
+ * @self: a #IdeObject
+ *
+ * Checks if @self is root, meaning it has no parent.
+ *
+ * Returns: %TRUE if @self has no parent
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_object_is_root (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), FALSE);
+
+  ide_object_private_lock (priv);
+  ret = priv->parent == NULL;
+  ide_object_private_unlock (priv);
+
+  return ret;
+}
+
+/**
+ * ide_object_add:
+ * @self: an #IdeObject
+ * @sibling: (nullable): an #IdeObject or %NULL
+ * @child: an #IdeObject
+ * @location: location for child
+ *
+ * Adds @child to @self, with location dependent on @location.
+ *
+ * Generally, it is simpler to use the helper functions such as
+ * ide_object_append(), ide_object_prepend(), ide_object_insert_before(),
+ * or ide_object_insert_after().
+ *
+ * This function is primarily meant for consumers that don't know the
+ * relative position they need until runtime.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_add (IdeObject         *self,
+                IdeObject         *sibling,
+                IdeObject         *child,
+                IdeObjectLocation  location)
+{
+  g_return_if_fail (IDE_IS_OBJECT (self));
+  g_return_if_fail (IDE_IS_OBJECT (child));
+
+  if (location == IDE_OBJECT_BEFORE_SIBLING ||
+      location == IDE_OBJECT_AFTER_SIBLING)
+    g_return_if_fail (IDE_IS_OBJECT (sibling));
+  else
+    g_return_if_fail (sibling == NULL);
+
+  IDE_OBJECT_GET_CLASS (self)->add (self, sibling, child, location);
+}
+
+/**
+ * ide_object_remove:
+ * @self: an #IdeObject
+ * @child: an #IdeObject
+ *
+ * Removes @child from @self.
+ *
+ * If @child is a borrowed reference, it may be finalized before this
+ * function returns.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_remove (IdeObject *self,
+                   IdeObject *child)
+{
+  g_return_if_fail (IDE_IS_OBJECT (self));
+  g_return_if_fail (IDE_IS_OBJECT (child));
+
+  IDE_OBJECT_GET_CLASS (self)->remove (self, child);
+}
+
+/**
+ * ide_object_append:
+ * @self: an #IdeObject
+ * @child: an #IdeObject
+ *
+ * Inserts @child as the last child of @self.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_append (IdeObject *self,
+                   IdeObject *child)
+{
+  ide_object_add (self, NULL, child, IDE_OBJECT_END);
+}
+
+/**
+ * ide_object_prepend:
+ * @self: an #IdeObject
+ * @child: an #IdeObject
+ *
+ * Inserts @child as the first child of @self.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_prepend (IdeObject *self,
+                    IdeObject *child)
+{
+  ide_object_add (self, NULL, child, IDE_OBJECT_START);
+}
+
+/**
+ * ide_object_insert_before:
+ * @self: an #IdeObject
+ * @sibling: an #IdeObject
+ * @child: an #IdeObject
+ *
+ * Inserts @child into @self's children, directly before @sibling.
+ *
+ * @sibling MUST BE a child of @self.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_insert_before (IdeObject *self,
+                          IdeObject *sibling,
+                          IdeObject *child)
+{
+  ide_object_add (self, sibling, child, IDE_OBJECT_BEFORE_SIBLING);
+}
+
+/**
+ * ide_object_insert_after:
+ * @self: an #IdeObject
+ * @sibling: an #IdeObject
+ * @child: an #IdeObject
+ *
+ * Inserts @child into @self's children, directly after @sibling.
+ *
+ * @sibling MUST BE a child of @self.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_insert_after (IdeObject *self,
+                         IdeObject *sibling,
+                         IdeObject *child)
+{
+  ide_object_add (self, sibling, child, IDE_OBJECT_AFTER_SIBLING);
+}
+
+/**
+ * ide_object_insert_sorted:
+ * @self: a #IdeObject
+ * @child: an #IdeObject
+ * @func: (scope call): a #GCompareDataFunc that can be used to locate the
+ *    proper sibling
+ * @user_data: user data for @func
+ *
+ * Locates the proper sibling for @child by using @func amongst @self's
+ * children #IdeObject. Those objects must already be sorted.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_insert_sorted (IdeObject        *self,
+                          IdeObject        *child,
+                          GCompareDataFunc  func,
+                          gpointer          user_data)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_OBJECT (self));
+  g_return_if_fail (IDE_IS_OBJECT (child));
+  g_return_if_fail (func != NULL);
+
+  ide_object_lock (self);
+
+  if (priv->children.length == 0)
+    {
+      ide_object_prepend (self, child);
+      goto unlock;
+    }
+
+  g_assert (priv->children.head != NULL);
+  g_assert (priv->children.tail != NULL);
+
+  for (GList *iter = priv->children.tail; iter; iter = iter->prev)
+    {
+      IdeObject *other = iter->data;
+
+      g_assert (IDE_IS_OBJECT (other));
+
+      if (func (child, other, user_data) <= 0)
+        {
+          ide_object_insert_after (self, other, child);
+          goto unlock;
+        }
+    }
+
+  ide_object_append (self, child);
+
+unlock:
+  ide_object_unlock (self);
+}
+
+/**
+ * ide_object_foreach:
+ * @self: a #IdeObject
+ * @callback: (scope call): a #GFunc to call for each child
+ * @user_data: closure data for @callback
+ *
+ * Calls @callback for each child of @self.
+ *
+ * @callback is allowed to remove children from @self, but only as long as they are
+ * the child passed to callback (or child itself). See g_queue_foreach() for more
+ * details about what is allowed.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_foreach (IdeObject *self,
+                    GFunc      callback,
+                    gpointer   user_data)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_OBJECT (self));
+  g_return_if_fail (callback != NULL);
+
+  ide_object_private_lock (priv);
+  g_queue_foreach (&priv->children, callback, user_data);
+  ide_object_private_unlock (priv);
+}
+
+static void
+get_child_typed_cb (gpointer data,
+                    gpointer user_data)
+{
+  IdeObject *child = data;
+  GetChildTyped *q = user_data;
+
+  if (q->child != NULL)
+    return;
+
+  if (G_TYPE_CHECK_INSTANCE_TYPE (child, q->type))
+    q->child = g_object_ref (child);
+}
+
+/**
+ * ide_object_get_child_typed:
+ * @self: a #IdeObject
+ * @type: the #GType of the child to match
+ *
+ * Finds the first child of @self that is of @type.
+ *
+ * Returns: (transfer full) (type IdeObject) (nullable): an #IdeObject or %NULL
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_object_get_child_typed (IdeObject *self,
+                            GType      type)
+{
+  GetChildTyped q = { type, NULL };
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+  g_return_val_if_fail (g_type_is_a (type, IDE_TYPE_OBJECT), NULL);
+
+  ide_object_foreach (self, get_child_typed_cb, &q);
+
+  return g_steal_pointer (&q.child);
+}
+
+static void
+get_children_typed_cb (gpointer data,
+                       gpointer user_data)
+{
+  IdeObject *child = data;
+  GetChildrenTyped *q = user_data;
+
+  if (G_TYPE_CHECK_INSTANCE_TYPE (child, q->type))
+    g_ptr_array_add (q->array, g_object_ref (child));
+}
+
+/**
+ * ide_object_get_children_typed:
+ * @self: a #IdeObject
+ * @type: a #GType
+ *
+ * Gets all children matching @type.
+ *
+ * Returns: (transfer full) (element-type IdeObject): a #GPtrArray of
+ *   #IdeObject matching @type.
+ *
+ * Since: 3.32
+ */
+GPtrArray *
+ide_object_get_children_typed (IdeObject *self,
+                               GType      type)
+{
+  g_autoptr(GPtrArray) ar = NULL;
+  GetChildrenTyped q;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+  g_return_val_if_fail (g_type_is_a (type, IDE_TYPE_OBJECT), NULL);
+
+  ar = g_ptr_array_new ();
+
+  q.type = type;
+  q.array = ar;
+
+  ide_object_foreach (self, get_children_typed_cb, &q);
+
+  return g_steal_pointer (&ar);
+}
+
+/**
+ * ide_object_ref_root:
+ * @self: a #IdeObject
+ *
+ * Finds and returns the toplevel object in the tree.
+ *
+ * Returns: (transfer full): an #IdeObject
+ *
+ * Since: 3.32
+ */
+IdeObject *
+ide_object_ref_root (IdeObject *self)
+{
+  IdeObject *cur;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+  cur = g_object_ref (self);
+
+  while (!ide_object_is_root (cur))
+    {
+      IdeObject *tmp = cur;
+      cur = ide_object_ref_parent (tmp);
+      g_object_unref (tmp);
+    }
+
+  return g_steal_pointer (&cur);
+}
+
+static void
+ide_object_async_init_cb (GObject      *object,
+                          GAsyncResult *result,
+                          gpointer      user_data)
+{
+  GAsyncInitable *initable = (GAsyncInitable *)object;
+  g_autoptr(IdeObject) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_ASYNC_INITABLE (initable));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_OBJECT (self));
+
+  if (!g_async_initable_init_finish (initable, result, &error))
+    {
+      g_warning ("Failed to initialize %s: %s",
+                 G_OBJECT_TYPE_NAME (initable),
+                 error->message);
+      ide_object_destroy (IDE_OBJECT (initable));
+    }
+}
+
+/**
+ * ide_object_ensure_child_typed:
+ * @self: a #IdeObject
+ * @type: the #GType of the child
+ *
+ * Like ide_object_get_child_typed() except that it creates an object of
+ * @type if it is missing.
+ *
+ * Returns: (transfer full) (nullable) (type IdeObject): an #IdeObject or %NULL
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_object_ensure_child_typed (IdeObject *self,
+                               GType      type)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  g_autoptr(IdeObject) ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+  g_return_val_if_fail (g_type_is_a (type, IDE_TYPE_OBJECT), NULL);
+  g_return_val_if_fail (!ide_object_in_destruction (self), NULL);
+
+  ide_object_private_lock (priv);
+  if (!(ret = ide_object_get_child_typed (self, type)))
+    {
+      g_autoptr(GError) error = NULL;
+
+      ret = ide_object_new (type, self);
+
+      if (G_IS_INITABLE (ret))
+        {
+          if (!g_initable_init (G_INITABLE (ret), NULL, &error))
+            g_warning ("Failed to initialize %s: %s",
+                       G_OBJECT_TYPE_NAME (ret), error->message);
+        }
+      else if (G_IS_ASYNC_INITABLE (ret))
+        {
+          g_async_initable_init_async (G_ASYNC_INITABLE (ret),
+                                       G_PRIORITY_DEFAULT,
+                                       priv->cancellable,
+                                       ide_object_async_init_cb,
+                                       g_object_ref (self));
+        }
+    }
+  ide_object_private_unlock (priv);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_destroyed:
+ * @self: a #IdeObject
+ *
+ * This function sets *object_pointer to NULL if object_pointer != NULL. It's
+ * intended to be used as a callback connected to the "destroy" signal of a
+ * object. You connect ide_object_destroyed() as a signal handler, and pass the
+ * address of your object variable as user data. Then when the object is
+ * destroyed, the variable will be set to NULL. Useful for example to avoid
+ * multiple copies of the same dialog.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_destroyed (IdeObject **object_pointer)
+{
+  if (object_pointer != NULL)
+    *object_pointer = NULL;
+}
+
+/* compat for now to ease porting */
+void
+ide_object_set_context (IdeObject  *object,
+                        IdeContext *context)
+{
+  ide_object_append (IDE_OBJECT (context), object);
+}
+
+static gboolean dummy (gpointer p) { return G_SOURCE_REMOVE; }
+
+/**
+ * ide_object_get_context:
+ * @object: a #IdeObject
+ *
+ * Gets the #IdeContext for the object.
+ *
+ * Returns: (transfer none) (nullable): an #IdeContext
+ *
+ * Since: 3.32
+ */
+IdeContext *
+ide_object_get_context (IdeObject *object)
+{
+  g_autoptr(IdeObject) root = ide_object_ref_root (object);
+  IdeContext *ret = NULL;
+  GSource *source;
+
+  if (IDE_IS_CONTEXT (root))
+    ret = IDE_CONTEXT (root);
+
+  /* We can just return a borrowed instance if in main thread,
+   * otherwise we need to queue the object to the main loop.
+   */
+  if (IDE_IS_MAIN_THREAD ())
+    return ret;
+
+  source = g_idle_source_new ();
+  g_source_set_name (source, "context-release");
+  g_source_set_callback (source, dummy, g_steal_pointer (&root), g_object_unref);
+  g_source_attach (source, g_main_context_get_thread_default ());
+  g_source_unref (source);
+
+  return ret;
+}
+
+/**
+ * ide_object_ref_context:
+ * @self: a #IdeContext
+ *
+ * Gets the root #IdeContext for the object, if any.
+ *
+ * Returns: (transfer full) (nullable): an #IdeContext or %NULL
+ *
+ * Since: 3.32
+ */
+IdeContext *
+ide_object_ref_context (IdeObject *self)
+{
+  g_autoptr(IdeObject) root = NULL;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+  if ((root = ide_object_ref_root (self)) && IDE_IS_CONTEXT (root))
+    return IDE_CONTEXT (g_steal_pointer (&root));
+
+  return NULL;
+}
+
+gboolean
+ide_object_in_destruction (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), FALSE);
+
+  ide_object_lock (self);
+  ret = priv->in_destruction || priv->destroyed;
+  ide_object_unlock (self);
+
+  return ret;
+}
+
+/**
+ * ide_object_repr:
+ * @self: a #IdeObject
+ *
+ * This function is similar to Python's `repr()` which gives a string
+ * representation for the object. It is useful when debugging Builder
+ * or when writing plugins.
+ *
+ * Returns: (transfer full): a string containing the string representation
+ *   of the #IdeObject
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_object_repr (IdeObject *self)
+{
+  g_autofree gchar *str = NULL;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+  str = IDE_OBJECT_GET_CLASS (self)->repr (self);
+
+  return g_strdup_printf ("<%s at %p>", str, self);
+}
+
+gboolean
+ide_object_set_error_if_destroyed (IdeObject  *self,
+                                   GError    **error)
+{
+  g_return_val_if_fail (IDE_IS_OBJECT (self), FALSE);
+
+  if (ide_object_in_destruction (self))
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_CANCELLED,
+                   "The object was destroyed");
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+void
+ide_object_log (gpointer        instance,
+                GLogLevelFlags  level,
+                const gchar    *domain,
+                const gchar    *format,
+                ...)
+{
+  g_autoptr(IdeObject) root = NULL;
+  va_list args;
+
+  g_assert (IDE_IS_OBJECT (instance));
+
+  root = ide_object_ref_root (instance);
+
+  if (IDE_IS_CONTEXT (root))
+    {
+      g_autofree gchar *message = NULL;
+
+      va_start (args, format);
+      message = g_strdup_vprintf (format, args);
+      ide_context_log (IDE_CONTEXT (root), level, domain, message);
+      va_end (args);
+    }
+}
diff --git a/src/libide/core/ide-object.h b/src/libide/core/ide-object.h
new file mode 100644
index 000000000..a0ec78c78
--- /dev/null
+++ b/src/libide/core/ide-object.h
@@ -0,0 +1,156 @@
+/* ide-object.h
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include <gio/gio.h>
+
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_OBJECT (ide_object_get_type())
+
+typedef enum
+{
+  IDE_OBJECT_START,
+  IDE_OBJECT_END,
+  IDE_OBJECT_BEFORE_SIBLING,
+  IDE_OBJECT_AFTER_SIBLING,
+} IdeObjectLocation;
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeObject, ide_object, IDE, OBJECT, GObject)
+
+struct _IdeObjectClass
+{
+  GObjectClass parent_class;
+
+  void     (*destroy)    (IdeObject         *self);
+  void     (*add)        (IdeObject         *self,
+                          IdeObject         *sibling,
+                          IdeObject         *child,
+                          IdeObjectLocation  location);
+  void     (*remove)     (IdeObject         *self,
+                          IdeObject         *child);
+  void     (*parent_set) (IdeObject         *self,
+                          IdeObject         *parent);
+  gchar  *(*repr)        (IdeObject         *self);
+
+  /*< private */
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+gpointer      ide_object_new                    (GType               type,
+                                                 IdeObject          *parent) G_GNUC_WARN_UNUSED_RESULT;
+IDE_AVAILABLE_IN_3_32
+GCancellable *ide_object_ref_cancellable        (IdeObject          *self) G_GNUC_WARN_UNUSED_RESULT;
+IDE_AVAILABLE_IN_3_32
+IdeObject    *ide_object_get_parent             (IdeObject          *self) G_GNUC_WARN_UNUSED_RESULT;
+IDE_AVAILABLE_IN_3_32
+IdeObject    *ide_object_ref_parent             (IdeObject          *self) G_GNUC_WARN_UNUSED_RESULT;
+IDE_AVAILABLE_IN_3_32
+IdeObject    *ide_object_ref_root               (IdeObject          *self) G_GNUC_WARN_UNUSED_RESULT;
+IDE_AVAILABLE_IN_3_32
+gboolean      ide_object_is_root                (IdeObject          *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_lock                   (IdeObject          *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_unlock                 (IdeObject          *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_add                    (IdeObject          *self,
+                                                 IdeObject          *sibling,
+                                                 IdeObject          *child,
+                                                 IdeObjectLocation   location);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_append                 (IdeObject          *self,
+                                                 IdeObject          *child);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_prepend                (IdeObject          *self,
+                                                 IdeObject          *child);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_insert_before          (IdeObject          *self,
+                                                 IdeObject          *sibling,
+                                                 IdeObject          *child);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_insert_after           (IdeObject          *self,
+                                                 IdeObject          *sibling,
+                                                 IdeObject          *child);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_insert_sorted          (IdeObject          *self,
+                                                 IdeObject          *child,
+                                                 GCompareDataFunc    func,
+                                                 gpointer            user_data);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_remove                 (IdeObject          *self,
+                                                 IdeObject          *child);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_foreach                (IdeObject          *self,
+                                                 GFunc               callback,
+                                                 gpointer            user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean      ide_object_set_error_if_destroyed (IdeObject          *self,
+                                                 GError            **error);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_destroy                (IdeObject          *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_destroyed              (IdeObject         **self);
+IDE_AVAILABLE_IN_3_32
+guint         ide_object_get_position           (IdeObject          *self);
+IDE_AVAILABLE_IN_3_32
+guint         ide_object_get_n_children         (IdeObject          *self);
+IDE_AVAILABLE_IN_3_32
+IdeObject    *ide_object_get_nth_child          (IdeObject          *self,
+                                                 guint               nth);
+IDE_AVAILABLE_IN_3_32
+gpointer      ide_object_get_child_typed        (IdeObject          *self,
+                                                 GType               type);
+IDE_AVAILABLE_IN_3_32
+GPtrArray    *ide_object_get_children_typed     (IdeObject          *self,
+                                                 GType               type);
+IDE_AVAILABLE_IN_3_32
+gpointer      ide_object_ensure_child_typed     (IdeObject          *self,
+                                                 GType               type);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_notify_in_main         (gpointer            instance,
+                                                 GParamSpec         *pspec);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_notify_by_pspec        (gpointer            instance,
+                                                 GParamSpec         *pspec);
+IDE_AVAILABLE_IN_3_32
+gboolean      ide_object_in_destruction         (IdeObject          *self);
+IDE_AVAILABLE_IN_3_32
+gchar        *ide_object_repr                   (IdeObject          *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_log                    (gpointer            instance,
+                                                 GLogLevelFlags      level,
+                                                 const gchar        *domain,
+                                                 const gchar        *format,
+                                                 ...) G_GNUC_PRINTF (4, 5);
+
+#define ide_object_message(instance, format, ...) ide_object_log(instance, G_LOG_LEVEL_MESSAGE, 
G_LOG_DOMAIN, format __VA_OPT__(,) __VA_ARGS__)
+#define ide_object_warning(instance, format, ...) ide_object_log(instance, G_LOG_LEVEL_WARNING, 
G_LOG_DOMAIN, format __VA_OPT__(,) __VA_ARGS__)
+
+G_END_DECLS
diff --git a/src/libide/core/ide-settings.c b/src/libide/core/ide-settings.c
new file mode 100644
index 000000000..80b851da7
--- /dev/null
+++ b/src/libide/core/ide-settings.c
@@ -0,0 +1,589 @@
+/* ide-settings.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-settings"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <stdlib.h>
+
+#include "ide-settings.h"
+
+/**
+ * SECTION:ide-settings
+ * @title: IdeSettings
+ * @short_description: Settings with per-project overrides
+ *
+ * In Builder, we need support for settings at the user level (their chosen
+ * defaults) as well as defaults for a project. #IdeSettings attempts to
+ * simplify this by providing a layered approach to settings.
+ *
+ * If a setting has been set for the current project, it will be returned. If
+ * not, the users preference will be returned. Setting a preference via
+ * #IdeSettings will always modify the projects setting, not the users default
+ * settings.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeSettings
+{
+  GObject              parent_instance;
+
+  DzlSettingsSandwich *settings_sandwich;
+  gchar               *relative_path;
+  gchar               *schema_id;
+  gchar               *project_id;
+  guint                ignore_project_settings : 1;
+};
+
+G_DEFINE_TYPE (IdeSettings, ide_settings, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_RELATIVE_PATH,
+  PROP_SCHEMA_ID,
+  PROP_IGNORE_PROJECT_SETTINGS,
+  PROP_PROJECT_ID,
+  N_PROPS
+};
+
+enum {
+  CHANGED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+ide_settings_set_ignore_project_settings (IdeSettings *self,
+                                          gboolean     ignore_project_settings)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+
+  ignore_project_settings = !!ignore_project_settings;
+
+  if (ignore_project_settings != self->ignore_project_settings)
+    {
+      self->ignore_project_settings = ignore_project_settings;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IGNORE_PROJECT_SETTINGS]);
+    }
+}
+
+static void
+ide_settings_set_relative_path (IdeSettings *self,
+                                const gchar *relative_path)
+{
+  g_assert (IDE_IS_SETTINGS (self));
+  g_assert (relative_path != NULL);
+
+  if (*relative_path == '/')
+    relative_path++;
+
+  if (!ide_str_equal0 (relative_path, self->relative_path))
+    {
+      g_free (self->relative_path);
+      self->relative_path = g_strdup (relative_path);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RELATIVE_PATH]);
+    }
+}
+
+static void
+ide_settings_set_schema_id (IdeSettings *self,
+                            const gchar *schema_id)
+{
+  g_assert (IDE_IS_SETTINGS (self));
+  g_assert (schema_id != NULL);
+
+  if (!ide_str_equal0 (schema_id, self->schema_id))
+    {
+      g_free (self->schema_id);
+      self->schema_id = g_strdup (schema_id);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SCHEMA_ID]);
+    }
+}
+
+static void
+ide_settings_constructed (GObject *object)
+{
+  IdeSettings *self = (IdeSettings *)object;
+  g_autofree gchar *full_path = NULL;
+  GSettings *settings;
+  gchar *path;
+
+  IDE_ENTRY;
+
+  G_OBJECT_CLASS (ide_settings_parent_class)->constructed (object);
+
+  if (self->schema_id == NULL)
+    {
+      g_error ("You must provide IdeSettings:schema-id");
+      abort ();
+    }
+
+  if (self->relative_path == NULL)
+    {
+      g_autoptr(GSettingsSchema) schema = NULL;
+      GSettingsSchemaSource *source;
+      const gchar *schema_path;
+
+      source = g_settings_schema_source_get_default ();
+      schema = g_settings_schema_source_lookup (source, self->schema_id, TRUE);
+
+      if (schema == NULL)
+        {
+          g_error ("Could not locate schema %s", self->schema_id);
+          abort ();
+        }
+
+      schema_path = g_settings_schema_get_path (schema);
+
+      if ((schema_path != NULL) && !g_str_has_prefix (schema_path, "/org/gnome/builder/"))
+        {
+          g_error ("Schema path MUST be under /org/gnome/builder/");
+          abort ();
+        }
+      else if (schema_path == NULL)
+        {
+          self->relative_path = g_strdup ("");
+        }
+      else
+        {
+          self->relative_path = g_strdup (schema_path + strlen ("/org/gnome/builder/"));
+        }
+    }
+
+  g_assert (self->relative_path != NULL);
+  g_assert (self->relative_path [0] != '/');
+  g_assert ((self->relative_path [0] == 0) || g_str_has_suffix (self->relative_path, "/"));
+
+  full_path = g_strdup_printf ("/org/gnome/builder/%s", self->relative_path);
+  self->settings_sandwich = dzl_settings_sandwich_new (self->schema_id, full_path);
+
+  /* Add our project relative settings */
+  if (self->ignore_project_settings == FALSE)
+    {
+      path = g_strdup_printf ("/org/gnome/builder/projects/%s/%s",
+                              self->project_id, self->relative_path);
+      settings = g_settings_new_with_path (self->schema_id, path);
+      dzl_settings_sandwich_append (self->settings_sandwich, settings);
+      g_clear_object (&settings);
+      g_free (path);
+    }
+
+  /* Add our application global (user defaults) settings */
+  settings = g_settings_new_with_path (self->schema_id, full_path);
+  dzl_settings_sandwich_append (self->settings_sandwich, settings);
+  g_clear_object (&settings);
+
+  IDE_EXIT;
+}
+
+static void
+ide_settings_finalize (GObject *object)
+{
+  IdeSettings *self = (IdeSettings *)object;
+
+  g_clear_object (&self->settings_sandwich);
+  g_clear_pointer (&self->relative_path, g_free);
+  g_clear_pointer (&self->schema_id, g_free);
+  g_clear_pointer (&self->project_id, g_free);
+
+  G_OBJECT_CLASS (ide_settings_parent_class)->finalize (object);
+}
+
+static void
+ide_settings_get_property (GObject    *object,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  IdeSettings *self = IDE_SETTINGS (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROJECT_ID:
+      g_value_set_string (value, self->project_id);
+      break;
+
+    case PROP_SCHEMA_ID:
+      g_value_set_string (value, ide_settings_get_schema_id (self));
+      break;
+
+    case PROP_RELATIVE_PATH:
+      g_value_set_string (value, ide_settings_get_relative_path (self));
+      break;
+
+    case PROP_IGNORE_PROJECT_SETTINGS:
+      g_value_set_boolean (value, ide_settings_get_ignore_project_settings (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_settings_set_property (GObject      *object,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+  IdeSettings *self = IDE_SETTINGS (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROJECT_ID:
+      self->project_id = g_value_dup_string (value);
+      break;
+
+    case PROP_SCHEMA_ID:
+      ide_settings_set_schema_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_RELATIVE_PATH:
+      ide_settings_set_relative_path (self, g_value_get_string (value));
+      break;
+
+    case PROP_IGNORE_PROJECT_SETTINGS:
+      ide_settings_set_ignore_project_settings (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_settings_class_init (IdeSettingsClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = ide_settings_constructed;
+  object_class->finalize = ide_settings_finalize;
+  object_class->get_property = ide_settings_get_property;
+  object_class->set_property = ide_settings_set_property;
+
+  properties [PROP_PROJECT_ID] =
+    g_param_spec_string ("project-id",
+                         "Project Id",
+                         "The identifier for the project",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_IGNORE_PROJECT_SETTINGS] =
+    g_param_spec_boolean ("ignore-project-settings",
+                         "Ignore Project Settings",
+                         "If project settings should be ignored.",
+                         FALSE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_RELATIVE_PATH] =
+    g_param_spec_string ("relative-path",
+                         "Relative Path",
+                         "Relative Path",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SCHEMA_ID] =
+    g_param_spec_string ("schema-id",
+                         "Schema ID",
+                         "Schema ID",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [CHANGED] =
+    g_signal_new ("changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  1,
+                  G_TYPE_STRING);
+}
+
+static void
+ide_settings_init (IdeSettings *self)
+{
+}
+
+IdeSettings *
+ide_settings_new (const gchar *project_id,
+                  const gchar *schema_id,
+                  const gchar *relative_path,
+                  gboolean     ignore_project_settings)
+{
+  IdeSettings *ret;
+
+  IDE_ENTRY;
+
+  g_assert (project_id != NULL);
+  g_assert (schema_id != NULL);
+  g_assert (relative_path != NULL);
+
+  ret = g_object_new (IDE_TYPE_SETTINGS,
+                      "project-id", project_id,
+                      "ignore-project-settings", ignore_project_settings,
+                      "relative-path", relative_path,
+                      "schema-id", schema_id,
+                      NULL);
+
+  IDE_RETURN (ret);
+}
+
+const gchar *
+ide_settings_get_schema_id (IdeSettings *self)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+
+  return self->schema_id;
+}
+
+const gchar *
+ide_settings_get_relative_path (IdeSettings *self)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+
+  return self->relative_path;
+}
+
+gboolean
+ide_settings_get_ignore_project_settings (IdeSettings *self)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), FALSE);
+
+  return self->ignore_project_settings;
+}
+
+GVariant *
+ide_settings_get_default_value (IdeSettings *self,
+                                const gchar *key)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+  g_return_val_if_fail (key != NULL, NULL);
+
+  return dzl_settings_sandwich_get_default_value (self->settings_sandwich, key);
+}
+
+GVariant *
+ide_settings_get_user_value (IdeSettings *self,
+                             const gchar *key)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+  g_return_val_if_fail (key != NULL, NULL);
+
+  return dzl_settings_sandwich_get_user_value (self->settings_sandwich, key);
+}
+
+GVariant *
+ide_settings_get_value (IdeSettings *self,
+                        const gchar *key)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+  g_return_val_if_fail (key != NULL, NULL);
+
+  return dzl_settings_sandwich_get_value (self->settings_sandwich, key);
+}
+
+void
+ide_settings_set_value (IdeSettings *self,
+                        const gchar *key,
+                        GVariant    *value)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (key != NULL);
+
+  return dzl_settings_sandwich_set_value (self->settings_sandwich, key, value);
+}
+
+gboolean
+ide_settings_get_boolean (IdeSettings *self,
+                          const gchar *key)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), FALSE);
+  g_return_val_if_fail (key != NULL, FALSE);
+
+  return dzl_settings_sandwich_get_boolean (self->settings_sandwich, key);
+}
+
+gdouble
+ide_settings_get_double (IdeSettings *self,
+                         const gchar *key)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), 0.0);
+  g_return_val_if_fail (key != NULL, 0.0);
+
+  return dzl_settings_sandwich_get_double (self->settings_sandwich, key);
+}
+
+gint
+ide_settings_get_int (IdeSettings *self,
+                      const gchar *key)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), 0);
+  g_return_val_if_fail (key != NULL, 0);
+
+  return dzl_settings_sandwich_get_int (self->settings_sandwich, key);
+}
+
+gchar *
+ide_settings_get_string (IdeSettings *self,
+                         const gchar *key)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+  g_return_val_if_fail (key != NULL, NULL);
+
+  return dzl_settings_sandwich_get_string (self->settings_sandwich, key);
+}
+
+guint
+ide_settings_get_uint (IdeSettings *self,
+                       const gchar *key)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), 0);
+  g_return_val_if_fail (key != NULL, 0);
+
+  return dzl_settings_sandwich_get_uint (self->settings_sandwich, key);
+}
+
+void
+ide_settings_set_boolean (IdeSettings *self,
+                          const gchar *key,
+                          gboolean     val)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (key != NULL);
+
+  dzl_settings_sandwich_set_boolean (self->settings_sandwich, key, val);
+}
+
+void
+ide_settings_set_double (IdeSettings *self,
+                         const gchar *key,
+                         gdouble      val)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (key != NULL);
+
+  dzl_settings_sandwich_set_double (self->settings_sandwich, key, val);
+}
+
+void
+ide_settings_set_int (IdeSettings *self,
+                      const gchar *key,
+                      gint         val)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (key != NULL);
+
+  dzl_settings_sandwich_set_int (self->settings_sandwich, key, val);
+}
+
+void
+ide_settings_set_string (IdeSettings *self,
+                         const gchar *key,
+                         const gchar *val)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (key != NULL);
+
+  dzl_settings_sandwich_set_string (self->settings_sandwich, key, val);
+}
+
+void
+ide_settings_set_uint (IdeSettings *self,
+                       const gchar *key,
+                       guint        val)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (key != NULL);
+
+  dzl_settings_sandwich_set_uint (self->settings_sandwich, key, val);
+}
+
+void
+ide_settings_bind (IdeSettings        *self,
+                   const gchar        *key,
+                   gpointer            object,
+                   const gchar        *property,
+                   GSettingsBindFlags  flags)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (key != NULL);
+  g_return_if_fail (G_IS_OBJECT (object));
+  g_return_if_fail (property != NULL);
+
+  dzl_settings_sandwich_bind (self->settings_sandwich, key, object, property, flags);
+}
+
+/**
+ * ide_settings_bind_with_mapping:
+ * @self: An #IdeSettings
+ * @key: The settings key
+ * @object: the object to bind to
+ * @property: the property of @object to bind to
+ * @flags: flags for the binding
+ * @get_mapping: (allow-none) (scope notified): variant to value mapping
+ * @set_mapping: (allow-none) (scope notified): value to variant mapping
+ * @user_data: user data for @get_mapping and @set_mapping
+ * @destroy: destroy function to cleanup @user_data.
+ *
+ * Like ide_settings_bind() but allows transforming to and from settings storage using
+ * @get_mapping and @set_mapping transformation functions.
+ *
+ * Call ide_settings_unbind() to unbind the mapping.
+ *
+ * Since: 3.32
+ */
+void
+ide_settings_bind_with_mapping (IdeSettings             *self,
+                                const gchar             *key,
+                                gpointer                 object,
+                                const gchar             *property,
+                                GSettingsBindFlags       flags,
+                                GSettingsBindGetMapping  get_mapping,
+                                GSettingsBindSetMapping  set_mapping,
+                                gpointer                 user_data,
+                                GDestroyNotify           destroy)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (key != NULL);
+  g_return_if_fail (G_IS_OBJECT (object));
+  g_return_if_fail (property != NULL);
+
+  dzl_settings_sandwich_bind_with_mapping (self->settings_sandwich, key, object, property, flags,
+                                           get_mapping, set_mapping, user_data, destroy);
+}
+
+void
+ide_settings_unbind (IdeSettings *self,
+                     const gchar *property)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (property != NULL);
+
+  dzl_settings_sandwich_unbind (self->settings_sandwich, property);
+}
diff --git a/src/libide/core/ide-settings.h b/src/libide/core/ide-settings.h
new file mode 100644
index 000000000..ab61a0043
--- /dev/null
+++ b/src/libide/core/ide-settings.h
@@ -0,0 +1,111 @@
+/* ide-settings.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SETTINGS (ide_settings_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeSettings, ide_settings, IDE, SETTINGS, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeSettings *ide_settings_new                         (const gchar             *project_id,
+                                                       const gchar             *schema_id,
+                                                       const gchar             *relative_path,
+                                                       gboolean                 ignore_project_settings);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_settings_get_relative_path           (IdeSettings             *self);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_settings_get_schema_id               (IdeSettings             *self);
+IDE_AVAILABLE_IN_3_32
+gboolean     ide_settings_get_ignore_project_settings (IdeSettings             *self);
+IDE_AVAILABLE_IN_3_32
+GVariant    *ide_settings_get_default_value           (IdeSettings             *self,
+                                                       const gchar             *key);
+IDE_AVAILABLE_IN_3_32
+GVariant    *ide_settings_get_user_value              (IdeSettings             *self,
+                                                       const gchar             *key);
+IDE_AVAILABLE_IN_3_32
+GVariant    *ide_settings_get_value                   (IdeSettings             *self,
+                                                       const gchar             *key);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_set_value                   (IdeSettings             *self,
+                                                       const gchar             *key,
+                                                       GVariant                *value);
+IDE_AVAILABLE_IN_3_32
+gboolean     ide_settings_get_boolean                 (IdeSettings             *self,
+                                                       const gchar             *key);
+IDE_AVAILABLE_IN_3_32
+gdouble      ide_settings_get_double                  (IdeSettings             *self,
+                                                       const gchar             *key);
+IDE_AVAILABLE_IN_3_32
+gint         ide_settings_get_int                     (IdeSettings             *self,
+                                                       const gchar             *key);
+IDE_AVAILABLE_IN_3_32
+gchar       *ide_settings_get_string                  (IdeSettings             *self,
+                                                       const gchar             *key);
+IDE_AVAILABLE_IN_3_32
+guint        ide_settings_get_uint                    (IdeSettings             *self,
+                                                       const gchar             *key);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_set_boolean                 (IdeSettings             *self,
+                                                       const gchar             *key,
+                                                       gboolean                 val);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_set_double                  (IdeSettings             *self,
+                                                       const gchar             *key,
+                                                       gdouble                  val);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_set_int                     (IdeSettings             *self,
+                                                       const gchar             *key,
+                                                       gint                     val);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_set_string                  (IdeSettings             *self,
+                                                       const gchar             *key,
+                                                       const gchar             *val);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_set_uint                    (IdeSettings             *self,
+                                                       const gchar             *key,
+                                                       guint                    val);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_bind                        (IdeSettings             *self,
+                                                       const gchar             *key,
+                                                       gpointer                 object,
+                                                       const gchar             *property,
+                                                       GSettingsBindFlags       flags);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_bind_with_mapping           (IdeSettings             *self,
+                                                       const gchar             *key,
+                                                       gpointer                 object,
+                                                       const gchar             *property,
+                                                       GSettingsBindFlags       flags,
+                                                       GSettingsBindGetMapping  get_mapping,
+                                                       GSettingsBindSetMapping  set_mapping,
+                                                       gpointer                 user_data,
+                                                       GDestroyNotify           destroy);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_unbind                      (IdeSettings             *self,
+                                                       const gchar             *property);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-transfer-manager.c b/src/libide/core/ide-transfer-manager.c
new file mode 100644
index 000000000..66011bf71
--- /dev/null
+++ b/src/libide/core/ide-transfer-manager.c
@@ -0,0 +1,493 @@
+/* ide-transfer-manager.c
+ *
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-transfer-manager"
+
+#include "config.h"
+
+#include "ide-context.h"
+#include "ide-debug.h"
+#include "ide-macros.h"
+
+#include "ide-transfer.h"
+#include "ide-transfer-manager.h"
+
+struct _IdeTransferManager
+{
+  GObject    parent_instance;
+  GPtrArray *transfers;
+};
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeTransferManager, ide_transfer_manager, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+enum {
+  PROP_0,
+  PROP_HAS_ACTIVE,
+  PROP_PROGRESS,
+  N_PROPS
+};
+
+enum {
+  TRANSFER_COMPLETED,
+  TRANSFER_FAILED,
+  ALL_TRANSFERS_COMPLETED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+/**
+ * ide_transfer_manager_get_has_active:
+ *
+ * Gets if there are active transfers.
+ *
+ * Returns: %TRUE if there are active transfers.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_transfer_manager_get_has_active (IdeTransferManager *self)
+{
+  g_return_val_if_fail (IDE_IS_TRANSFER_MANAGER (self), FALSE);
+
+  for (guint i = 0; i < self->transfers->len; i++)
+    {
+      IdeTransfer *transfer = g_ptr_array_index (self->transfers, i);
+
+      if (ide_transfer_get_active (transfer))
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+ide_transfer_manager_finalize (GObject *object)
+{
+  IdeTransferManager *self = (IdeTransferManager *)object;
+
+  g_clear_pointer (&self->transfers, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (ide_transfer_manager_parent_class)->finalize (object);
+}
+
+static void
+ide_transfer_manager_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  IdeTransferManager *self = IDE_TRANSFER_MANAGER (object);
+
+  switch (prop_id)
+    {
+    case PROP_HAS_ACTIVE:
+      g_value_set_boolean (value, ide_transfer_manager_get_has_active (self));
+      break;
+
+    case PROP_PROGRESS:
+      g_value_set_double (value, ide_transfer_manager_get_progress (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_transfer_manager_class_init (IdeTransferManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_transfer_manager_finalize;
+  object_class->get_property = ide_transfer_manager_get_property;
+
+  /**
+   * IdeTransferManager:has-active:
+   *
+   * If there are transfers active, this will be set.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_HAS_ACTIVE] =
+    g_param_spec_boolean ("has-active",
+                          "Has Active",
+                          "Has Active",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTransferManager:progress:
+   *
+   * A double between and including 0.0 and 1.0 describing the progress of
+   * all tasks.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PROGRESS] =
+    g_param_spec_double ("progress",
+                         "Progress",
+                         "Progress",
+                         0.0,
+                         1.0,
+                         0.0,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeTransferManager::all-transfers-completed:
+   *
+   * This signal is emitted when all of the transfers have completed or failed.
+   *
+   * Since: 3.32
+   */
+  signals [ALL_TRANSFERS_COMPLETED] =
+    g_signal_new ("all-transfers-completed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  /**
+   * IdeTransferManager::transfer-completed:
+   * @self: An #IdeTransferManager
+   * @transfer: An #IdeTransfer
+   *
+   * This signal is emitted when a transfer has completed successfully.
+   *
+   * Since: 3.32
+   */
+  signals [TRANSFER_COMPLETED] =
+    g_signal_new ("transfer-completed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 1, IDE_TYPE_TRANSFER);
+
+  /**
+   * IdeTransferManager::transfer-failed:
+   * @self: An #IdeTransferManager
+   * @transfer: An #IdeTransfer
+   * @reason: (in): The reason for the failure.
+   *
+   * This signal is emitted when a transfer has failed to complete
+   * successfully.
+   *
+   * Since: 3.32
+   */
+  signals [TRANSFER_FAILED] =
+    g_signal_new ("transfer-failed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 2, IDE_TYPE_TRANSFER, G_TYPE_ERROR);
+}
+
+static void
+ide_transfer_manager_init (IdeTransferManager *self)
+{
+  self->transfers = g_ptr_array_new_with_free_func (g_object_unref);
+}
+
+static void
+ide_transfer_manager_notify_progress (IdeTransferManager *self,
+                                      GParamSpec         *pspec,
+                                      IdeTransfer        *transfer)
+{
+  g_assert (IDE_IS_TRANSFER_MANAGER (self));
+  g_assert (IDE_IS_TRANSFER (transfer));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]);
+}
+
+static gboolean
+ide_transfer_manager_append (IdeTransferManager *self,
+                             IdeTransfer        *transfer)
+{
+  guint position;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_TRANSFER_MANAGER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TRANSFER (transfer), FALSE);
+
+  for (guint i = 0; i < self->transfers->len; i++)
+    {
+      if (transfer == (IdeTransfer *)g_ptr_array_index (self->transfers, i))
+        IDE_RETURN (FALSE);
+    }
+
+  g_signal_connect_object (transfer,
+                           "notify::progress",
+                           G_CALLBACK (ide_transfer_manager_notify_progress),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  position = self->transfers->len;
+  g_ptr_array_add (self->transfers, g_object_ref (transfer));
+  g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
+
+  IDE_RETURN (TRUE);
+}
+
+void
+ide_transfer_manager_cancel_all (IdeTransferManager *self)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_TRANSFER_MANAGER (self));
+
+  for (guint i = 0; i < self->transfers->len; i++)
+    {
+      IdeTransfer *transfer = g_ptr_array_index (self->transfers, i);
+
+      ide_transfer_cancel (transfer);
+    }
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_transfer_manager_clear:
+ *
+ * Removes all transfers from the manager that are completed.
+ *
+ * Since: 3.32
+ */
+void
+ide_transfer_manager_clear (IdeTransferManager *self)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_TRANSFER_MANAGER (self));
+
+  for (guint i = self->transfers->len; i > 0; i--)
+    {
+      IdeTransfer *transfer = g_ptr_array_index (self->transfers, i - 1);
+
+      if (!ide_transfer_get_active (transfer))
+        {
+          g_ptr_array_remove_index (self->transfers, i - 1);
+          g_list_model_items_changed (G_LIST_MODEL (self), i - 1, 1, 0);
+        }
+    }
+
+  IDE_EXIT;
+}
+
+static GType
+ide_transfer_manager_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_TRANSFER;
+}
+
+static guint
+ide_transfer_manager_get_n_items (GListModel *model)
+{
+  IdeTransferManager *self = (IdeTransferManager *)model;
+
+  g_assert (IDE_IS_TRANSFER_MANAGER (self));
+
+  return self->transfers->len;
+}
+
+static gpointer
+ide_transfer_manager_get_item (GListModel *model,
+                               guint       position)
+{
+  IdeTransferManager *self = (IdeTransferManager *)model;
+
+  g_assert (IDE_IS_TRANSFER_MANAGER (self));
+
+  if G_UNLIKELY (position >= self->transfers->len)
+    return NULL;
+
+  return g_object_ref (g_ptr_array_index (self->transfers, position));
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = ide_transfer_manager_get_item_type;
+  iface->get_n_items = ide_transfer_manager_get_n_items;
+  iface->get_item = ide_transfer_manager_get_item;
+}
+
+gdouble
+ide_transfer_manager_get_progress (IdeTransferManager *self)
+{
+  gdouble total = 0.0;
+
+  g_return_val_if_fail (IDE_IS_TRANSFER_MANAGER (self), 0.0);
+
+  if (self->transfers->len > 0)
+    {
+      guint count = 0;
+
+      for (guint i = 0; i < self->transfers->len; i++)
+        {
+          IdeTransfer *transfer = g_ptr_array_index (self->transfers, i);
+          gdouble progress = ide_transfer_get_progress (transfer);
+
+          if (ide_transfer_get_completed (transfer) || ide_transfer_get_active (transfer))
+            {
+              total += MAX (0.0, MIN (1.0, progress));
+              count++;
+            }
+        }
+
+      if (count != 0)
+        total /= (gdouble)count;
+    }
+
+  return total;
+}
+
+static void
+ide_transfer_manager_execute_cb (GObject      *object,
+                                 GAsyncResult *result,
+                                 gpointer      user_data)
+{
+  IdeTransfer *transfer = (IdeTransfer *)object;
+  IdeTransferManager *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TRANSFER (transfer));
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+
+  if (!ide_transfer_execute_finish (transfer, result, &error))
+    {
+      g_signal_emit (self, signals[TRANSFER_FAILED], 0, transfer, error);
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_GOTO (notify_properties);
+    }
+  else
+    {
+      g_signal_emit (self, signals[TRANSFER_COMPLETED], 0, transfer);
+      g_task_return_boolean (task, TRUE);
+    }
+
+  if (!ide_transfer_manager_get_has_active (self))
+    g_signal_emit (self, signals[ALL_TRANSFERS_COMPLETED], 0);
+
+notify_properties:
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_ACTIVE]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_transfer_manager_execute_async:
+ * @self: An #IdeTransferManager
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: (nullable): A callback or %NULL
+ * @user_data: user data for @callback
+ *
+ * This is a convenience function that will queue @transfer into the transfer
+ * manager and execute callback upon completion of the transfer. The success
+ * or failure #GError will be propagated to the caller via
+ * ide_transfer_manager_execute_finish().
+ *
+ * Since: 3.32
+ */
+void
+ide_transfer_manager_execute_async (IdeTransferManager  *self,
+                                    IdeTransfer         *transfer,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (!self || IDE_IS_TRANSFER_MANAGER (self));
+  g_return_if_fail (IDE_IS_TRANSFER (transfer));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (self == NULL)
+    self = ide_transfer_manager_get_default ();
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_transfer_manager_execute_async);
+
+  if (!ide_transfer_manager_append (self, transfer))
+    {
+      if (ide_transfer_get_active (transfer))
+        {
+          g_warning ("%s is already active, ignoring transfer request",
+                     G_OBJECT_TYPE_NAME (transfer));
+          IDE_EXIT;
+        }
+    }
+
+  ide_transfer_execute_async (transfer,
+                              cancellable,
+                              ide_transfer_manager_execute_cb,
+                              g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_transfer_manager_execute_finish (IdeTransferManager  *self,
+                                     GAsyncResult        *result,
+                                     GError             **error)
+{
+  g_return_val_if_fail (IDE_IS_TRANSFER_MANAGER (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+/**
+ * ide_transfer_manager_get_default:
+ *
+ * Gets the #IdeTransferManager singleton.
+ *
+ * Returns: (transfer none): an #IdeTransferManager
+ *
+ * Since: 3.32
+ */
+IdeTransferManager *
+ide_transfer_manager_get_default (void)
+{
+  static IdeTransferManager *instance;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (!instance || IDE_IS_TRANSFER_MANAGER (instance));
+
+  if (g_once_init_enter (&instance))
+    g_once_init_leave (&instance, g_object_new (IDE_TYPE_TRANSFER_MANAGER, NULL));
+
+  return instance;
+}
diff --git a/src/libide/core/ide-transfer-manager.h b/src/libide/core/ide-transfer-manager.h
new file mode 100644
index 000000000..a3adccb54
--- /dev/null
+++ b/src/libide/core/ide-transfer-manager.h
@@ -0,0 +1,58 @@
+/* ide-transfer-manager.h
+ *
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include "ide-transfer.h"
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TRANSFER_MANAGER (ide_transfer_manager_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeTransferManager, ide_transfer_manager, IDE, TRANSFER_MANAGER, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeTransferManager *ide_transfer_manager_get_default    (void);
+IDE_AVAILABLE_IN_3_32
+gdouble             ide_transfer_manager_get_progress   (IdeTransferManager   *self);
+IDE_AVAILABLE_IN_3_32
+gboolean            ide_transfer_manager_get_has_active (IdeTransferManager   *self);
+IDE_AVAILABLE_IN_3_32
+void                ide_transfer_manager_cancel_all     (IdeTransferManager   *self);
+IDE_AVAILABLE_IN_3_32
+void                ide_transfer_manager_clear          (IdeTransferManager   *self);
+IDE_AVAILABLE_IN_3_32
+void                ide_transfer_manager_execute_async  (IdeTransferManager   *self,
+                                                         IdeTransfer          *transfer,
+                                                         GCancellable         *cancellable,
+                                                         GAsyncReadyCallback   callback,
+                                                         gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean            ide_transfer_manager_execute_finish (IdeTransferManager   *self,
+                                                         GAsyncResult         *result,
+                                                         GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-transfer.c b/src/libide/core/ide-transfer.c
new file mode 100644
index 000000000..a3c35b3e0
--- /dev/null
+++ b/src/libide/core/ide-transfer.c
@@ -0,0 +1,522 @@
+/* ide-transfer.c
+ *
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-transfer"
+
+#include "config.h"
+
+#include "ide-debug.h"
+#include "ide-macros.h"
+#include "ide-transfer.h"
+
+typedef struct
+{
+  gchar *icon_name;
+  gchar *status;
+  gchar *title;
+  GCancellable *cancellable;
+  gdouble progress;
+  guint active : 1;
+  guint completed : 1;
+} IdeTransferPrivate;
+
+enum {
+  PROP_0,
+  PROP_ACTIVE,
+  PROP_COMPLETED,
+  PROP_ICON_NAME,
+  PROP_PROGRESS,
+  PROP_STATUS,
+  PROP_TITLE,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeTransfer, ide_transfer, IDE_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_transfer_real_execute_async (IdeTransfer         *self,
+                                 GCancellable        *cancellable,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  g_assert (IDE_IS_TRANSFER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_transfer_real_execute_finish (IdeTransfer   *self,
+                                  GAsyncResult  *result,
+                                  GError       **error)
+{
+  g_assert (IDE_IS_TRANSFER (self));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_transfer_finalize (GObject *object)
+{
+  IdeTransfer *self = (IdeTransfer *)object;
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_clear_pointer (&priv->icon_name, g_free);
+  g_clear_pointer (&priv->status, g_free);
+  g_clear_pointer (&priv->title, g_free);
+  g_clear_object (&priv->cancellable);
+
+  G_OBJECT_CLASS (ide_transfer_parent_class)->finalize (object);
+}
+
+static void
+ide_transfer_get_property (GObject    *object,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  IdeTransfer *self = IDE_TRANSFER (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACTIVE:
+      g_value_set_boolean (value, ide_transfer_get_active (self));
+      break;
+
+    case PROP_COMPLETED:
+      g_value_set_boolean (value, ide_transfer_get_completed (self));
+      break;
+
+    case PROP_ICON_NAME:
+      g_value_set_string (value, ide_transfer_get_icon_name (self));
+      break;
+
+    case PROP_PROGRESS:
+      g_value_set_double (value, ide_transfer_get_progress (self));
+      break;
+
+    case PROP_STATUS:
+      g_value_set_string (value, ide_transfer_get_status (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, ide_transfer_get_title (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_transfer_set_property (GObject      *object,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+  IdeTransfer *self = IDE_TRANSFER (object);
+
+  switch (prop_id)
+    {
+    case PROP_ICON_NAME:
+      ide_transfer_set_icon_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_PROGRESS:
+      ide_transfer_set_progress (self, g_value_get_double (value));
+      break;
+
+    case PROP_STATUS:
+      ide_transfer_set_status (self, g_value_get_string (value));
+      break;
+
+    case PROP_TITLE:
+      ide_transfer_set_title (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_transfer_class_init (IdeTransferClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_transfer_finalize;
+  object_class->get_property = ide_transfer_get_property;
+  object_class->set_property = ide_transfer_set_property;
+
+  klass->execute_async = ide_transfer_real_execute_async;
+  klass->execute_finish = ide_transfer_real_execute_finish;
+
+  properties [PROP_ACTIVE] =
+    g_param_spec_boolean ("active",
+                          "Active",
+                          "If the transfer is active",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_COMPLETED] =
+    g_param_spec_boolean ("completed",
+                          "Completed",
+                          "If the transfer has completed successfully",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         "Icon Name",
+                         "The icon to display next to the transfer",
+                         "folder-download-symbolic",
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PROGRESS] =
+    g_param_spec_double ("progress",
+                         "Progress",
+                         "The progress for the transfer between 0 adn 1",
+                         0.0,
+                         1.0,
+                         0.0,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_STATUS] =
+    g_param_spec_string ("status",
+                         "Status",
+                         "The status message for the transfer",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the transfer",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_transfer_init (IdeTransfer *self)
+{
+}
+
+static void
+ide_transfer_execute_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  IdeTransfer *self = (IdeTransfer *)object;
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TRANSFER (self));
+  g_assert (G_IS_TASK (task));
+
+  priv->active = FALSE;
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACTIVE]);
+
+  ide_transfer_set_progress (self, 1.0);
+
+  if (!IDE_TRANSFER_GET_CLASS (self)->execute_finish (self, result, &error))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  priv->completed = TRUE;
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COMPLETED]);
+
+  g_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+void
+ide_transfer_execute_async (IdeTransfer         *self,
+                            GCancellable        *cancellable,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+  g_autoptr(GTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TRANSFER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /*
+   * We already create our own wrapper task so that we can track completion
+   * cleanly from the subclass implementation. It also allows us to ensure
+   * that the subclasses execute_finish() is guaranteed to be called.
+   */
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_transfer_execute_async);
+
+  /*
+   * Wrap our own cancellable so that we can gracefully control
+   * the cancellation of the underlying transfer without affecting
+   * the callers cancellation state.
+   */
+  g_clear_object (&priv->cancellable);
+  priv->cancellable = g_cancellable_new ();
+
+  if (cancellable != NULL)
+    g_signal_connect_object (cancellable,
+                             "cancelled",
+                             G_CALLBACK (g_cancellable_cancel),
+                             priv->cancellable,
+                             G_CONNECT_SWAPPED);
+
+  priv->active = TRUE;
+  priv->completed = FALSE;
+
+  IDE_TRANSFER_GET_CLASS (self)->execute_async (self,
+                                                priv->cancellable,
+                                                ide_transfer_execute_cb,
+                                                g_steal_pointer (&task));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACTIVE]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COMPLETED]);
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_transfer_execute_finish (IdeTransfer   *self,
+                             GAsyncResult  *result,
+                             GError       **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_TRANSFER (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  ret = g_task_propagate_boolean (G_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+const gchar *
+ide_transfer_get_icon_name (IdeTransfer *self)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TRANSFER (self), NULL);
+
+  return priv->icon_name ?: "folder-download-symbolic";
+}
+
+void
+ide_transfer_set_icon_name (IdeTransfer *self,
+                            const gchar *icon_name)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TRANSFER (self));
+
+  if (g_strcmp0 (priv->icon_name, icon_name) != 0)
+    {
+      g_free (priv->icon_name);
+      priv->icon_name = g_strdup (icon_name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON_NAME]);
+    }
+}
+
+gdouble
+ide_transfer_get_progress (IdeTransfer *self)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TRANSFER (self), 0.0);
+
+  return priv->progress;
+}
+
+void
+ide_transfer_set_progress (IdeTransfer *self,
+                           gdouble      progress)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TRANSFER (self));
+
+  if (progress != priv->progress)
+    {
+      priv->progress = progress;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]);
+    }
+}
+
+const gchar *
+ide_transfer_get_status (IdeTransfer *self)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TRANSFER (self), NULL);
+
+  return priv->status;
+}
+
+void
+ide_transfer_set_status (IdeTransfer *self,
+                         const gchar *status)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TRANSFER (self));
+
+  if (g_strcmp0 (priv->status, status) != 0)
+    {
+      g_free (priv->status);
+      priv->status = g_strdup (status);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STATUS]);
+    }
+}
+
+const gchar *
+ide_transfer_get_title (IdeTransfer *self)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TRANSFER (self), NULL);
+
+  return priv->title;
+}
+
+void
+ide_transfer_set_title (IdeTransfer *self,
+                        const gchar *title)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TRANSFER (self));
+
+  if (g_strcmp0 (priv->title, title) != 0)
+    {
+      g_free (priv->title);
+      priv->title = g_strdup (title);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+    }
+}
+
+void
+ide_transfer_cancel (IdeTransfer *self)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TRANSFER (self));
+
+  if (!g_cancellable_is_cancelled (priv->cancellable))
+    g_cancellable_cancel (priv->cancellable);
+}
+
+gboolean
+ide_transfer_get_completed (IdeTransfer *self)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TRANSFER (self), FALSE);
+
+  return priv->completed;
+}
+
+gboolean
+ide_transfer_get_active (IdeTransfer *self)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TRANSFER (self), FALSE);
+
+  return priv->active;
+}
+
+GQuark
+ide_transfer_error_quark (void)
+{
+  return g_quark_from_static_string ("ide-transfer-error-quark");
+}
+
+static void
+ide_transfer_notification_notify_completed (IdeTransfer     *self,
+                                            GParamSpec      *pspec,
+                                            IdeNotification *notif)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TRANSFER (self));
+  g_assert (IDE_IS_NOTIFICATION (notif));
+
+  ide_notification_withdraw_in_seconds (notif, 10);
+}
+
+/**
+ * ide_transfer_create_notification:
+ * @self: a #IdeTransfer
+ *
+ * Creates a new #IdeNotification that is updated with the progress
+ * of the #IdeTransfer. This is useful when you need to bridge an
+ * #IdeTransfer into something that can be displayed to the user.
+ *
+ * If the transfer has completed, %NULL is returned.
+ *
+ * Returns: (transfer full) (nullable): an #IdeNotification or %NULL
+ *
+ * Since: 3.32
+ */
+IdeNotification *
+ide_transfer_create_notification (IdeTransfer *self)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+  g_autoptr(IdeNotification) notif = NULL;
+
+  g_return_val_if_fail (IDE_IS_TRANSFER (self), NULL);
+
+  if (priv->completed)
+    return NULL;
+
+  notif = ide_notification_new ();
+  ide_notification_set_has_progress (notif, TRUE);
+  g_object_bind_property (self, "title", notif, "title", G_BINDING_SYNC_CREATE);
+  g_object_bind_property (self, "status", notif, "body", G_BINDING_SYNC_CREATE);
+  g_object_bind_property (self, "progress", notif, "progress", G_BINDING_SYNC_CREATE);
+  g_object_bind_property (self, "icon-name", notif, "icon-name", G_BINDING_SYNC_CREATE);
+
+  g_signal_connect_object (self,
+                           "notify::completed",
+                           G_CALLBACK (ide_transfer_notification_notify_completed),
+                           notif,
+                           0);
+
+  return g_steal_pointer (&notif);
+}
diff --git a/src/libide/core/ide-transfer.h b/src/libide/core/ide-transfer.h
new file mode 100644
index 000000000..380161773
--- /dev/null
+++ b/src/libide/core/ide-transfer.h
@@ -0,0 +1,101 @@
+/* ide-transfer.h
+ *
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include "ide-notification.h"
+#include "ide-object.h"
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TRANSFER  (ide_transfer_get_type())
+#define IDE_TRANSFER_ERROR (ide_transfer_error_quark())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeTransfer, ide_transfer, IDE, TRANSFER, IdeObject)
+
+struct _IdeTransferClass
+{
+  IdeObjectClass parent_class;
+
+  void     (*execute_async)  (IdeTransfer          *self,
+                              GCancellable         *cancellable,
+                              GAsyncReadyCallback   callback,
+                              gpointer              user_data);
+  gboolean (*execute_finish) (IdeTransfer          *self,
+                              GAsyncResult         *result,
+                              GError              **error);
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+typedef enum
+{
+  IDE_TRANSFER_ERROR_UNKNOWN = 0,
+  IDE_TRANSFER_ERROR_CONNECTION_IS_METERED = 1,
+} IdeTransferError;
+
+IDE_AVAILABLE_IN_3_32
+GQuark           ide_transfer_error_quark         (void);
+IDE_AVAILABLE_IN_3_32
+void             ide_transfer_cancel              (IdeTransfer          *self);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_transfer_get_completed       (IdeTransfer          *self);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_transfer_get_active          (IdeTransfer          *self);
+IDE_AVAILABLE_IN_3_32
+const gchar     *ide_transfer_get_icon_name       (IdeTransfer          *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_transfer_set_icon_name       (IdeTransfer          *self,
+                                                   const gchar          *icon_name);
+IDE_AVAILABLE_IN_3_32
+gdouble          ide_transfer_get_progress        (IdeTransfer          *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_transfer_set_progress        (IdeTransfer          *self,
+                                                   gdouble               progress);
+IDE_AVAILABLE_IN_3_32
+const gchar     *ide_transfer_get_status          (IdeTransfer          *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_transfer_set_status          (IdeTransfer          *self,
+                                                   const gchar          *status);
+IDE_AVAILABLE_IN_3_32
+const gchar     *ide_transfer_get_title           (IdeTransfer          *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_transfer_set_title           (IdeTransfer          *self,
+                                                   const gchar          *title);
+IDE_AVAILABLE_IN_3_32
+void             ide_transfer_execute_async       (IdeTransfer          *self,
+                                                   GCancellable         *cancellable,
+                                                   GAsyncReadyCallback   callback,
+                                                   gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_transfer_execute_finish      (IdeTransfer          *self,
+                                                   GAsyncResult         *result,
+                                                   GError              **error);
+IDE_AVAILABLE_IN_3_32
+IdeNotification *ide_transfer_create_notification (IdeTransfer          *self);
+
+G_END_DECLS
diff --git a/src/libide/ide-version-macros.h b/src/libide/core/ide-version-macros.h
similarity index 94%
rename from src/libide/ide-version-macros.h
rename to src/libide/core/ide-version-macros.h
index 95ef7b632..4e7af41a3 100644
--- a/src/libide/ide-version-macros.h
+++ b/src/libide/core/ide-version-macros.h
@@ -1,6 +1,6 @@
 /* ide-version-macros.h
  *
- * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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,11 +18,10 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#ifndef IDE_VERSION_MACROS_H
-#define IDE_VERSION_MACROS_H
+#pragma once
 
-#if !defined(IDE_INSIDE) && !defined(IDE_COMPILATION)
-# error "Only <ide.h> can be included directly."
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
 #endif
 
 #include <glib.h>
@@ -78,7 +77,7 @@
  * it is possible to use this symbol to avoid the compiler warnings
  * without disabling warning for every deprecated function.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 #ifndef IDE_VERSION_MIN_REQUIRED
 # define IDE_VERSION_MIN_REQUIRED (IDE_VERSION_CUR_STABLE)
@@ -99,7 +98,7 @@
  * it is possible to use this symbol to get compiler warnings when
  * trying to use that function.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 #ifndef IDE_VERSION_MAX_ALLOWED
 # if IDE_VERSION_MIN_REQUIRED > IDE_VERSION_PREV_STABLE
@@ -159,5 +158,3 @@
 #else
 # define IDE_AVAILABLE_IN_3_32                 _IDE_EXTERN
 #endif
-
-#endif /* IDE_VERSION_MACROS_H */
diff --git a/src/libide/ide-version.h.in b/src/libide/core/ide-version.h.in
similarity index 100%
rename from src/libide/ide-version.h.in
rename to src/libide/core/ide-version.h.in
diff --git a/src/libide/core/libide-core.h b/src/libide/core/libide-core.h
new file mode 100644
index 000000000..42d4373ea
--- /dev/null
+++ b/src/libide/core/libide-core.h
@@ -0,0 +1,43 @@
+/* ide-core.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+#define IDE_CORE_INSIDE
+
+#include "ide-build-ident.h"
+#include "ide-context.h"
+#include "ide-debug.h"
+#include "ide-global.h"
+#include "ide-log.h"
+#include "ide-macros.h"
+#include "ide-notification.h"
+#include "ide-notifications.h"
+#include "ide-object.h"
+#include "ide-object-box.h"
+#include "ide-settings.h"
+#include "ide-transfer.h"
+#include "ide-transfer-manager.h"
+#include "ide-version.h"
+#include "ide-version-macros.h"
+
+#undef IDE_CORE_INSIDE
diff --git a/src/libide/core/meson.build b/src/libide/core/meson.build
new file mode 100644
index 000000000..155d9ea6c
--- /dev/null
+++ b/src/libide/core/meson.build
@@ -0,0 +1,124 @@
+libide_core_header_dir = join_paths(libide_header_dir, 'core')
+libide_core_header_subdir = join_paths(libide_header_subdir, 'core')
+libide_include_directories += include_directories('.')
+
+#
+# Versioning that all libide libraries (re)use
+#
+
+version_data = configuration_data()
+version_data.set('MAJOR_VERSION', MAJOR_VERSION)
+version_data.set('MINOR_VERSION', MINOR_VERSION)
+version_data.set('MICRO_VERSION', MICRO_VERSION)
+version_data.set('VERSION', meson.project_version())
+version_data.set_quoted('BUILD_CHANNEL', get_option('with_channel'))
+version_data.set_quoted('BUILD_TYPE', get_option('buildtype'))
+
+libide_core_version_h = configure_file(
+          input: 'ide-version.h.in',
+         output: 'ide-version.h',
+    install_dir: libide_core_header_dir,
+        install: true,
+  configuration: version_data)
+
+libide_core_generated_headers = [libide_core_version_h]
+
+libide_build_ident_h = vcs_tag(
+     fallback: meson.project_version(),
+        input: 'ide-build-ident.h.in',
+       output: 'ide-build-ident.h',
+)
+libide_core_generated_headers += [libide_build_ident_h]
+
+#
+# Debugging and Tracing Support
+#
+
+libide_core_conf = configuration_data()
+libide_core_conf.set10('ENABLE_TRACING', get_option('tracing'))
+libide_core_conf.set('BUGREPORT_URL', 'https://gitlab.gnome.org/GNOME/gnome-builder/issues')
+
+libide_debug_h = configure_file(
+         input: 'ide-debug.h.in',
+         output: 'ide-debug.h',
+  configuration: libide_core_conf,
+        install: true,
+    install_dir: libide_core_header_dir,
+)
+
+libide_core_generated_headers += [libide_debug_h]
+
+#
+# Public API Headers
+#
+
+libide_core_public_headers = [
+  'ide-context.h',
+  'ide-context-addin.h',
+  'ide-global.h',
+  'ide-log.h',
+  'ide-macros.h',
+  'ide-notification.h',
+  'ide-notifications.h',
+  'ide-object.h',
+  'ide-object-box.h',
+  'ide-settings.h',
+  'ide-transfer.h',
+  'ide-transfer-manager.h',
+  'ide-version-macros.h',
+  'libide-core.h',
+]
+
+install_headers(libide_core_public_headers, subdir: libide_core_header_subdir)
+
+#
+# Sources
+#
+
+libide_core_public_sources = [
+  'ide-context.c',
+  'ide-context-addin.c',
+  'ide-global.c',
+  'ide-log.c',
+  'ide-notification.c',
+  'ide-notifications.c',
+  'ide-object.c',
+  'ide-object-box.c',
+  'ide-object-notify.c',
+  'ide-settings.c',
+  'ide-transfer.c',
+  'ide-transfer-manager.c',
+]
+
+libide_core_sources = []
+libide_core_sources += libide_core_generated_headers
+libide_core_sources += libide_core_public_sources
+
+#
+# Library Definitions
+#
+
+libide_core_deps = [
+  libgio_dep,
+  libgtk_dep,
+  libdazzle_dep,
+  libpeas_dep,
+]
+
+libide_core = static_library('ide-core-' + libide_api_version, libide_core_sources,
+   dependencies: libide_core_deps,
+         c_args: libide_args + release_args + ['-DIDE_CORE_COMPILATION'],
+)
+
+libide_core_dep = declare_dependency(
+              sources: libide_core_generated_headers,
+         dependencies: libide_core_deps,
+           link_whole: libide_core,
+  include_directories: include_directories('.'),
+)
+
+gnome_builder_public_sources += files(libide_core_public_sources)
+gnome_builder_public_headers += files(libide_core_public_headers)
+gnome_builder_generated_headers += libide_core_generated_headers
+gnome_builder_include_subdirs += libide_core_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-core.h', '-DIDE_CORE_COMPILATION']



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