[gnome-builder] threading: add IdeTask as basis for new tasking design



commit 6b9b5a28a2206eb00e0ac00560c5704d86af3a1d
Author: Christian Hergert <chergert redhat com>
Date:   Mon Mar 12 14:09:42 2018 -0700

    threading: add IdeTask as basis for new tasking design
    
    We are going to have to move away from GTask, at least in the short term
    because it cannot provide some fundamental ownership and life-cycle
    guarantees we require.
    
    This is the start of a new IdeTask replacement which allows us to ensure
    that task data, source objects, and results are finalized only within the
    GMainContext attached to the task.
    
    It also starts adding some new convenience API that we want for propagating
    results between multiple tasks.

 src/libide/ide-enums.c.in        |    2 +
 src/libide/ide.h                 |    1 +
 src/libide/threading/ide-task.c  | 2003 ++++++++++++++++++++++++++++++++++++++
 src/libide/threading/ide-task.h  |  174 ++++
 src/libide/threading/meson.build |    3 +
 src/tests/meson.build            |    8 +
 src/tests/test-ide-task.c        |  735 ++++++++++++++
 7 files changed, 2926 insertions(+)
---
diff --git a/src/libide/ide-enums.c.in b/src/libide/ide-enums.c.in
index 62ef5a0d9..8aad2d4a6 100644
--- a/src/libide/ide-enums.c.in
+++ b/src/libide/ide-enums.c.in
@@ -20,6 +20,7 @@
 #include "sourceview/ide-source-view.h"
 #include "symbols/ide-symbol.h"
 #include "testing/ide-test.h"
+#include "threading/ide-task.h"
 #include "threading/ide-thread-pool.h"
 #include "transfers/ide-transfer.h"
 #include "vcs/ide-vcs-config.h"
@@ -58,3 +59,4 @@ GType
 
 /*** END file-tail ***/
 
+
diff --git a/src/libide/ide.h b/src/libide/ide.h
index 5d224edd7..aaf6d2824 100644
--- a/src/libide/ide.h
+++ b/src/libide/ide.h
@@ -174,6 +174,7 @@ G_BEGIN_DECLS
 #include "testing/ide-test.h"
 #include "testing/ide-test-manager.h"
 #include "testing/ide-test-provider.h"
+#include "threading/ide-task.h"
 #include "threading/ide-thread-pool.h"
 #include "terminal/ide-terminal.h"
 #include "terminal/ide-terminal-search.h"
diff --git a/src/libide/threading/ide-task.c b/src/libide/threading/ide-task.c
new file mode 100644
index 000000000..2806d555c
--- /dev/null
+++ b/src/libide/threading/ide-task.c
@@ -0,0 +1,2003 @@
+/* ide-task.c
+ *
+ * Copyright 2018 Christian Hergert
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#define G_LOG_DOMAIN "ide-task"
+
+#include <dazzle.h>
+
+#include "threading/ide-task.h"
+#include "threading/ide-thread-pool.h"
+
+/**
+ * SECTION:ide-task
+ * @title: IdeTask
+ * @short_description: asynchronous task management
+ *
+ * #IdeTask is meant to be an improved form of #GTask. There are a few
+ * deficiencies in #GTask that have made it unsuitable for certain use cases.
+ *
+ * #GTask does not provide a way to guarnatee that the source object,
+ * task data, and unused results are freed with in a given #GMainContext.
+ * #IdeTask addresses this by having a more flexible result and object
+ * ownership control.
+ *
+ * Furthermore, #IdeTask allows consumers to force disposal from a given
+ * thread so that the data is released there.
+ *
+ * #IdeTask also supports chaining tasks together which makes it simpler
+ * to avoid doing duplicate work by instead simply chaining the tasks.
+ *
+ * There are some costs to this design. It uses the main context a bit
+ * more than #GTask may use it. Also, an equivalent to
+ * g_task_get_source_object() cannot be provided because we do not control
+ * the exact lifecycle of that object. g_async_result_get_source_object()
+ * may be used which provides a full reference instead of a borrowed
+ * reference.
+ *
+ * #IdeTask uses it's own #GThreadPool, however the threads are not exclusive
+ * to increase the chances of thread sharing with other pools.
+ *
+ * The number of threads in the pool is equivalent to N_CPU*2+1.
+ *
+ * Because #IdeTask needs more control over result life-cycles (for chaining
+ * results), additional return methods have been provided. Consumers should
+ * use ide_task_return_boxed() when working with boxed types as it allows us
+ * to copy the result to another task. Additionally, ide_task_return_object()
+ * provides a simplified API over ide_task_return_pointer() which also allows
+ * copying the result to chained tasks.
+ *
+ * Since: 3.28
+ */
+
+typedef struct
+{
+  /*
+   * The pointer we were provided.
+   */
+  gpointer data;
+
+  /*
+   * The destroy notify for @data. We should only call this from the
+   * main context associated with the task.
+   */
+  GDestroyNotify data_destroy;
+} IdeTaskData;
+
+typedef enum
+{
+  IDE_TASK_RESULT_NONE,
+  IDE_TASK_RESULT_CANCELLED,
+  IDE_TASK_RESULT_BOOLEAN,
+  IDE_TASK_RESULT_INT,
+  IDE_TASK_RESULT_ERROR,
+  IDE_TASK_RESULT_OBJECT,
+  IDE_TASK_RESULT_BOXED,
+  IDE_TASK_RESULT_POINTER,
+} IdeTaskResultType;
+
+typedef struct
+{
+  /*
+   * The type of result stored in our union @u.
+   */
+  IdeTaskResultType type;
+
+  /*
+   * To ensure that we can pass ownership back to the main context
+   * from our worker thread, we need to be able to stash the reference
+   * here in our result. It is also convenient as we need access to it
+   * from the main context callback anyway.
+   */
+  IdeTask *task;
+
+  /*
+   * Additionally, we need to allow passing our main context reference
+   * back so that it cannot be finalized in our thread.
+   */
+  GMainContext *main_context;
+
+  /*
+   * Priority for our GSource attached to @main_context.
+   */
+  gint priority;
+
+  /*
+   * The actual result information, broken down by result @type.
+   */
+  union {
+    gboolean  v_bool;
+    gssize    v_int;
+    GError   *v_error;
+    GObject  *v_object;
+    struct {
+      GType    type;
+      gpointer pointer;
+    } v_boxed;
+    struct {
+      gpointer       pointer;
+      GDestroyNotify destroy;
+    } v_pointer;
+  } u;
+} IdeTaskResult;
+
+typedef struct
+{
+  IdeTask      *task;
+  GMainContext *main_context;
+  gint          priority;
+} IdeTaskCancel;
+
+typedef struct
+{
+  /*
+   * Controls access to our private data. We only access structure
+   * data while holding this mutex to ensure that we have consistency
+   * between threads which could be accessing internals.
+   */
+  GMutex mutex;
+
+  /*
+   * The source object for the GAsyncResult interface. If we have set
+   * release_on_propagate, this will be released when the task propagate
+   * function is called.
+   */
+  gpointer source_object;
+
+  /*
+   * The cancellable that we're monitoring for task cancellation.
+   */
+  GCancellable *cancellable;
+
+  /*
+   * If ide_task_set_return_on_cancel() has been set, then we might be
+   * listening for changes. Handling this will queue a completion
+   */
+  gulong cancel_handler;
+
+  /*
+   * The callback to execute upon completion of the operation. It will
+   * be called from @main_contect after the operation completes.
+   */
+  GAsyncReadyCallback callback;
+  gpointer user_data;
+
+  /*
+   * The name for the task. This string is interned so you should not
+   * use dynamic names. They are meant to simplify the process of
+   * debugging what task failed.
+   */
+  const gchar *name;
+
+  /*
+   * The GMainContext that was the thread default when the task was
+   * created. Most operations are proxied back to this context so that
+   * the consumer does not need to worry about thread safety.
+   */
+  GMainContext *main_context;
+
+  /*
+   * The task data that has been set for the task. Task data is released
+   * from a callback in the #GMainContext if changed outside the main
+   * context.
+   */
+  IdeTaskData *task_data;
+
+  /*
+   * The result for the task. If release_on_propagate as set to %FALSE,
+   * then this may be kept around so that ide_task_chain() can be used to
+   * duplicate the result to another task. This is convenient when multiple
+   * async funcs race to do some work, allowing just a single winner with all
+   * the callers getting the same result.
+   */
+  IdeTaskResult *result;
+
+  /*
+   * ide_task_chain() allows us to propagate the result of this task to
+   * another task (for a limited number of result types). This is the
+   * list of those tasks.
+   */
+  GPtrArray *chained;
+
+  /*
+   * If ide_task_run_in_thread() is called, this will be set to the func
+   * that should be called from within the thread.
+   */
+  IdeTaskThreadFunc thread_func;
+
+  /*
+   * If we're running in a thread, we'll stash the value here until we
+   * can complete things cleanly and pass ownership back as one operation.
+   */
+  IdeTaskResult *thread_result;
+
+  /*
+   * The source tag for the task, which can be used to determine what
+   * the task is from a debugger as well as to verify correctness
+   * in async finish functions.
+   */
+  gpointer source_tag;
+
+  /*
+   * Our priority for scheduling tasks in the particular workqueue.
+   */
+  gint priority;
+
+  /*
+   * While we're waiting for our return callback, this is set to our
+   * source id. We use that to know we need to block on the main loop
+   * in case the user calls ide_task_propagate_*() synchronously without
+   * round-triping to the main loop.
+   */
+  guint return_source;
+
+  /*
+   * Our kind of task, which is used to determine what thread pool we
+   * can use when running threaded work. This can be used to help choke
+   * lots of work down to a relatively small number of threads.
+   */
+  IdeTaskKind kind : 8;
+
+  /*
+   * If the task has been completed, which is to say that the callback
+   * dispatch has occurred in @main_context.
+   */
+  guint completed : 1;
+
+  /*
+   * If we should check @cancellable before returning the result. If set
+   * to true, and the cancellable was cancelled, an error will be returned
+   * even if the task completed successfully.
+   */
+  guint check_cancellable : 1;
+
+  /*
+   * If we should synthesize completion from a GCancellable::cancelled
+   * event instead of waiting for the task to complete normally.
+   */
+  guint return_on_cancel : 1;
+
+  /*
+   * If we should release the source object and task data after we've
+   * dispatched the callback (or the callback was NULL). This allows us
+   * to ensure that various dependent data are released in the main
+   * context. This is the default and helps ensure thread-safety.
+   */
+  guint release_on_propagate : 1;
+
+  /*
+   * Protect against multiple return calls, and given the developer a good
+   * warning so they catch this early.
+   */
+  guint return_called : 1;
+
+  /*
+   * If we got a result that was a cancellation, then we mark it here so
+   * that we can deal with it cleanly later.
+   */
+  guint got_cancel : 1;
+
+  /*
+   * If we have dispatched to a thread already.
+   */
+  guint thread_called : 1;
+
+} IdeTaskPrivate;
+
+static void     async_result_init_iface (GAsyncResultIface *iface);
+static void     ide_task_data_free      (IdeTaskData       *task_data);
+static void     ide_task_result_free    (IdeTaskResult     *result);
+static gboolean ide_task_return_cb      (gpointer           user_data);
+static void     ide_task_release        (IdeTask           *self,
+                                         gboolean           force);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeTaskData, ide_task_data_free);
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeTaskResult, ide_task_result_free);
+
+DZL_DEFINE_COUNTER (instances, "Tasks", "Instances", "Number of active tasks")
+
+G_DEFINE_TYPE_WITH_CODE (IdeTask, ide_task, G_TYPE_OBJECT,
+                         G_ADD_PRIVATE (IdeTask)
+                         G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_RESULT, async_result_init_iface))
+
+enum {
+  PROP_0,
+  PROP_COMPLETED,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_task_cancel_free (IdeTaskCancel *cancel)
+{
+  g_clear_pointer (&cancel->main_context, g_main_context_unref);
+  g_clear_object (&cancel->task);
+  g_slice_free (IdeTaskCancel, cancel);
+}
+
+static const gchar *
+result_type_name (IdeTaskResultType type)
+{
+  switch (type)
+    {
+    case IDE_TASK_RESULT_NONE:
+      return "none";
+
+    case IDE_TASK_RESULT_CANCELLED:
+      return "cancelled";
+
+    case IDE_TASK_RESULT_INT:
+      return "int";
+
+    case IDE_TASK_RESULT_POINTER:
+      return "pointer";
+
+    case IDE_TASK_RESULT_OBJECT:
+      return "object";
+
+    case IDE_TASK_RESULT_BOXED:
+      return "boxed";
+
+    case IDE_TASK_RESULT_BOOLEAN:
+      return "boolean";
+
+    case IDE_TASK_RESULT_ERROR:
+      return "error";
+
+    default:
+      return NULL;
+    }
+}
+
+static void
+ide_task_data_free (IdeTaskData *task_data)
+{
+  if (task_data->data_destroy != NULL)
+    task_data->data_destroy (task_data->data);
+  g_slice_free (IdeTaskData, task_data);
+}
+
+static IdeTaskResult *
+ide_task_result_copy (const IdeTaskResult *src)
+{
+  IdeTaskResult *dst;
+
+  dst = g_slice_new0 (IdeTaskResult);
+  dst->type = src->type;
+
+  switch (src->type)
+    {
+    case IDE_TASK_RESULT_INT:
+      dst->u.v_int = src->u.v_int;
+      break;
+
+    case IDE_TASK_RESULT_BOOLEAN:
+      dst->u.v_bool = src->u.v_bool;
+      break;
+
+    case IDE_TASK_RESULT_ERROR:
+      dst->u.v_error = g_error_copy (src->u.v_error);
+      break;
+
+    case IDE_TASK_RESULT_OBJECT:
+      dst->u.v_object = src->u.v_object ? g_object_ref (src->u.v_object) : NULL;
+      break;
+
+    case IDE_TASK_RESULT_BOXED:
+      dst->u.v_boxed.type = src->u.v_boxed.type;
+      dst->u.v_boxed.pointer = g_boxed_copy (src->u.v_boxed.type, src->u.v_boxed.pointer);
+      break;
+
+    case IDE_TASK_RESULT_POINTER:
+      g_critical ("Cannot proxy raw pointers for task results");
+      break;
+
+    case IDE_TASK_RESULT_CANCELLED:
+    case IDE_TASK_RESULT_NONE:
+    default:
+      break;
+    }
+
+  return g_steal_pointer (&dst);
+}
+
+static void
+ide_task_result_free (IdeTaskResult *result)
+{
+  if (result == NULL)
+    return;
+
+  switch (result->type)
+    {
+    case IDE_TASK_RESULT_POINTER:
+      if (result->u.v_pointer.destroy)
+        result->u.v_pointer.destroy (result->u.v_pointer.pointer);
+      break;
+
+    case IDE_TASK_RESULT_ERROR:
+      g_error_free (result->u.v_error);
+      break;
+
+    case IDE_TASK_RESULT_BOXED:
+      if (result->u.v_boxed.pointer)
+        g_boxed_free (result->u.v_boxed.type, result->u.v_boxed.pointer);
+      break;
+
+    case IDE_TASK_RESULT_OBJECT:
+      g_clear_object (&result->u.v_object);
+      break;
+
+    case IDE_TASK_RESULT_BOOLEAN:
+    case IDE_TASK_RESULT_INT:
+    case IDE_TASK_RESULT_NONE:
+    case IDE_TASK_RESULT_CANCELLED:
+    default:
+      break;
+    }
+
+  g_clear_object (&result->task);
+  g_clear_pointer (&result->main_context, g_main_context_unref);
+  g_slice_free (IdeTaskResult, result);
+}
+
+/*
+ * ide_task_complete:
+ * @result: (transfer full): the result to complete
+ *
+ * queues the completion for the task. make sure that you've
+ * set the result->task, main_context, and priority first.
+ *
+ * This is designed to allow stealing the last reference from
+ * a worker thread and pass it back to the main context.
+ *
+ * Returns: a gsource identifier
+ */
+static guint
+ide_task_complete (IdeTaskResult *result)
+{
+  GSource *source;
+  guint ret;
+
+  g_assert (result != NULL);
+  g_assert (IDE_IS_TASK (result->task));
+  g_assert (result->main_context);
+
+  source = g_idle_source_new ();
+  g_source_set_name (source, "[ide-task] complete result");
+  g_source_set_ready_time (source, -1);
+  g_source_set_callback (source, ide_task_return_cb, result, NULL);
+  g_source_set_priority (source, result->priority);
+  ret = g_source_attach (source, result->main_context);
+  g_source_unref (source);
+
+  return ret;
+}
+
+static void
+ide_task_thread_func (gpointer data)
+{
+  g_autoptr(GObject) source_object = NULL;
+  g_autoptr(GCancellable) cancellable = NULL;
+  g_autoptr(IdeTask) task = data;
+  IdeTaskPrivate *priv = ide_task_get_instance_private (task);
+  gpointer task_data = NULL;
+  IdeTaskThreadFunc thread_func;
+
+  g_assert (IDE_IS_TASK (task));
+
+  g_mutex_lock (&priv->mutex);
+  source_object = priv->source_object ? g_object_ref (priv->source_object) : NULL;
+  cancellable = priv->cancellable ? g_object_ref (priv->cancellable) : NULL;
+  if (priv->task_data)
+    task_data = priv->task_data->data;
+  thread_func = priv->thread_func;
+  priv->thread_func = NULL;
+  g_mutex_unlock (&priv->mutex);
+
+  g_assert (thread_func != NULL);
+
+  thread_func (task, source_object, task_data, cancellable);
+
+  g_clear_object (&source_object);
+  g_clear_object (&cancellable);
+
+  g_mutex_lock (&priv->mutex);
+
+  /*
+   * We've delayed our ide_task_return() until we reach here, so now
+   * we can steal our object instance and complete the task along with
+   * ensuring the object wont be finalized from this thread.
+   */
+  if (priv->thread_result)
+    {
+      IdeTaskResult *result = g_steal_pointer (&priv->thread_result);
+
+      g_assert (result->task == task);
+      g_clear_object (&result->task);
+      result->task = g_steal_pointer (&task);
+
+      priv->return_source = ide_task_complete (g_steal_pointer (&result));
+
+      g_assert (source_object == NULL);
+      g_assert (cancellable == NULL);
+      g_assert (task == NULL);
+    }
+  else
+    {
+      /* The task did not return a value while in the thread func!  GTask
+       * doesn't support this, but its useful to us in a number of ways, so
+       * we'll begrudgingly support it but the best we can do is drop our
+       * reference from the thread.
+       */
+    }
+
+  g_mutex_unlock (&priv->mutex);
+
+  g_assert (source_object == NULL);
+  g_assert (cancellable == NULL);
+  g_assert (task == NULL);
+}
+
+static void
+ide_task_dispose (GObject *object)
+{
+  IdeTask *self = (IdeTask *)object;
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+
+  g_assert (IDE_IS_TASK (self));
+
+  ide_task_release (self, TRUE);
+
+  g_mutex_lock (&priv->mutex);
+  g_clear_pointer (&priv->result, ide_task_result_free);
+  g_mutex_unlock (&priv->mutex);
+
+  G_OBJECT_CLASS (ide_task_parent_class)->dispose (object);
+}
+
+static void
+ide_task_finalize (GObject *object)
+{
+  IdeTask *self = (IdeTask *)object;
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+
+  if (!priv->return_called)
+    g_critical ("%s [%s] finalized before completing",
+                G_OBJECT_TYPE_NAME (self),
+                priv->name ?: "unnamed");
+  else if (priv->chained && priv->chained->len)
+    g_critical ("%s [%s] finalized before dependents were notified",
+                G_OBJECT_TYPE_NAME (self),
+                priv->name ?: "unnamed");
+  else if (priv->thread_func)
+    g_critical ("%s [%s] finalized while thread_func is active",
+                G_OBJECT_TYPE_NAME (self),
+                priv->name ?: "unnamed");
+  else if (!priv->completed)
+    g_critical ("%s [%s] finalized before completion",
+                G_OBJECT_TYPE_NAME (self),
+                priv->name ?: "unnamed");
+
+  g_assert (priv->return_source == 0);
+  g_assert (priv->result == NULL);
+  g_assert (priv->task_data == NULL);
+  g_assert (priv->source_object == NULL);
+  g_assert (priv->chained == NULL);
+  g_assert (priv->thread_result == NULL);
+
+  g_clear_pointer (&priv->main_context, g_main_context_unref);
+  g_clear_object (&priv->cancellable);
+  g_mutex_clear (&priv->mutex);
+
+  G_OBJECT_CLASS (ide_task_parent_class)->finalize (object);
+
+  DZL_COUNTER_DEC (instances);
+}
+
+static void
+ide_task_get_property (GObject    *object,
+                       guint       prop_id,
+                       GValue     *value,
+                       GParamSpec *pspec)
+{
+  IdeTask *self = IDE_TASK (object);
+
+  switch (prop_id)
+    {
+    case PROP_COMPLETED:
+      g_value_set_boolean (value, ide_task_get_completed (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_task_class_init (IdeTaskClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_task_dispose;
+  object_class->finalize = ide_task_finalize;
+  object_class->get_property = ide_task_get_property;
+
+  properties [PROP_COMPLETED] =
+    g_param_spec_boolean ("completed",
+                          "Completed",
+                          "If the task has completed",
+                          FALSE,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_task_init (IdeTask *self)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+
+  DZL_COUNTER_INC (instances);
+
+  g_mutex_init (&priv->mutex);
+
+  priv->check_cancellable = TRUE;
+  priv->release_on_propagate = TRUE;
+  priv->priority = G_PRIORITY_DEFAULT;
+  priv->main_context = g_main_context_ref_thread_default ();
+}
+
+/**
+ * ide_task_get_source_object: (skip)
+ * @self: a #IdeTask
+ *
+ * Gets the #GObject used when creating the source object.
+ *
+ * As this does not provide ownership transfer of the #GObject, it is a
+ * programmer error to call this function outside of a thread worker called
+ * from ide_task_run_in_thread() or outside the #GMainContext that is
+ * associated with the task.
+ *
+ * If you need to access the object in other scenarios, you must use the
+ * g_async_result_get_source_object() which provides a full reference to the
+ * source object, safely. You are responsible for ensuring that you do not
+ * release the object in a manner that is unsafe for the source object.
+ *
+ * Returns: (transfer none) (nullable) (type GObject.Object): a #GObject or %NULL
+ *
+ * Since: 3.28
+ */
+gpointer
+ide_task_get_source_object (IdeTask *self)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  gpointer ret;
+
+  g_return_val_if_fail (IDE_IS_TASK (self), NULL);
+
+  g_mutex_lock (&priv->mutex);
+  ret = priv->source_object;
+  g_mutex_unlock (&priv->mutex);
+
+  return ret;
+}
+
+/**
+ * ide_task_new:
+ * @source_object: (type GObject.Object) (nullable): a #GObject or %NULL
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: (scope async) (nullable): a #GAsyncReadyCallback or %NULL
+ * @user_data: closure data for @callback
+ *
+ * Creates a new #IdeTask.
+ *
+ * #IdeTask is similar to #GTask but provides some additional guarantees
+ * such that by default, the source object, task data, and unused results
+ * are guaranteed to be finalized in the #GMainContext associated with
+ * the task itself.
+ *
+ * Returns: (transfer full): an #IdeTask
+ *
+ * Since: 3.28
+ */
+IdeTask *
+(ide_task_new) (gpointer             source_object,
+                GCancellable        *cancellable,
+                GAsyncReadyCallback  callback,
+                gpointer             user_data)
+{
+  g_autoptr(IdeTask) self = NULL;
+  IdeTaskPrivate *priv;
+
+  g_return_val_if_fail (!source_object || G_IS_OBJECT (source_object), NULL);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), NULL);
+
+  self = g_object_new (IDE_TYPE_TASK, NULL);
+  priv = ide_task_get_instance_private (self);
+
+  priv->source_object = source_object ? g_object_ref (source_object) : NULL;
+  priv->cancellable = cancellable ? g_object_ref (cancellable) : NULL;
+  priv->callback = callback;
+  priv->user_data = user_data;
+
+  return g_steal_pointer (&self);
+}
+
+/**
+ * ide_task_is_valid:
+ * @self: (nullable) (type Ide.Task): a #IdeTask
+ * @source_object: (nullable): a #GObject or %NULL
+ *
+ * Checks if @source_object matches the object the task was created with.
+ *
+ * Returns: %TRUE is source_object matches
+ *
+ * Since: 3.28
+ */
+gboolean
+ide_task_is_valid (gpointer self,
+                   gpointer source_object)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+
+  return IDE_IS_TASK (self) && priv->source_object == source_object;
+}
+
+/**
+ * ide_task_get_completed:
+ * @self: a #IdeTask
+ *
+ * Gets the "completed" property. This is %TRUE after the callback used when
+ * creating the task has been executed.
+ *
+ * The property will be notified using g_object_notify() exactly once in the
+ * same #GMainContext as the callback.
+ *
+ * Returns: %TRUE if the task has completed
+ *
+ * Since: 3.28
+ */
+gboolean
+ide_task_get_completed (IdeTask *self)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_TASK (self), FALSE);
+
+  g_mutex_lock (&priv->mutex);
+  ret = priv->completed;
+  g_mutex_unlock (&priv->mutex);
+
+  return ret;
+}
+
+gint
+ide_task_get_priority (IdeTask *self)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  gint ret;
+
+  g_return_val_if_fail (IDE_IS_TASK (self), 0);
+
+  g_mutex_lock (&priv->mutex);
+  ret = priv->priority;
+  g_mutex_unlock (&priv->mutex);
+
+  return ret;
+}
+
+void
+ide_task_set_priority (IdeTask *self,
+                       gint     priority)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TASK (self));
+
+  g_mutex_lock (&priv->mutex);
+  priv->priority = priority;
+  g_mutex_unlock (&priv->mutex);
+}
+
+/**
+ * ide_task_get_cancellable:
+ * @self: a #IdeTask
+ *
+ * Gets the #GCancellable for the task.
+ *
+ * Returns: (transfer none) (nullable): a #GCancellable or %NULL
+ *
+ * Since: 3.28
+ */
+GCancellable *
+ide_task_get_cancellable (IdeTask *self)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TASK (self), NULL);
+
+  return priv->cancellable;
+}
+
+static void
+ide_task_deliver_result (IdeTask       *self,
+                         IdeTaskResult *result)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+
+  g_assert (IDE_IS_TASK (self));
+  g_assert (result != NULL);
+  g_assert (result->task == NULL);
+  g_assert (result->main_context == NULL);
+
+  /* This task was chained from another task. This completes the result
+   * and we should dispatch the callback. To simplify the dispatching and
+   * help prevent any re-entrancy issues, we defer back to the main context
+   * to complete the operation.
+   */
+
+  result->task = g_object_ref (self);
+  result->main_context = g_main_context_ref (priv->main_context);
+  result->priority = priv->priority;
+
+  g_mutex_lock (&priv->mutex);
+
+  priv->return_called = TRUE;
+  priv->return_source = ide_task_complete (g_steal_pointer (&result));
+
+  g_mutex_unlock (&priv->mutex);
+}
+
+static void
+ide_task_release (IdeTask  *self,
+                  gboolean  force)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  g_autoptr(IdeTaskData) task_data = NULL;
+  g_autoptr(GObject) source_object = NULL;
+  g_autoptr(GPtrArray) chained = NULL;
+
+  g_assert (IDE_IS_TASK (self));
+
+  g_mutex_lock (&priv->mutex);
+  if (force || priv->release_on_propagate)
+    {
+      source_object = g_steal_pointer (&priv->source_object);
+      task_data = g_steal_pointer (&priv->task_data);
+      chained = g_steal_pointer (&priv->chained);
+    }
+  g_mutex_unlock (&priv->mutex);
+
+  if (chained)
+    {
+      for (guint i = 0; i < chained->len; i++)
+        {
+          IdeTask *task = g_ptr_array_index (chained, i);
+
+          ide_task_return_new_error (task,
+                                     G_IO_ERROR,
+                                     G_IO_ERROR_FAILED,
+                                     "Error synthesized for task, parent task disposed");
+        }
+    }
+}
+
+static gboolean
+ide_task_return_cb (gpointer user_data)
+{
+  g_autoptr(IdeTask) self = NULL;
+  g_autoptr(IdeTaskResult) result = user_data;
+  g_autoptr(IdeTaskResult) result_copy = NULL;
+  g_autoptr(GCancellable) cancellable = NULL;
+  g_autoptr(GObject) source_object = NULL;
+  g_autoptr(GPtrArray) chained = NULL;
+  GAsyncReadyCallback callback = NULL;
+  gpointer callback_data = NULL;
+  IdeTaskPrivate *priv;
+
+  g_assert (result != NULL);
+  g_assert (IDE_IS_TASK (result->task));
+
+  /* We steal the task object, because we only stash it in the result
+   * structure to get it here. And if we held onto it, we would have
+   * a reference cycle.
+   */
+  self = g_steal_pointer (&result->task);
+  priv = ide_task_get_instance_private (self);
+
+  g_mutex_lock (&priv->mutex);
+
+  g_assert (priv->return_source != 0);
+
+  priv->return_source = 0;
+
+  if (priv->got_cancel && priv->result != NULL)
+    {
+      /* We can discard this since we already handled a result for the
+       * task. We delivered this here just so that we could finalize
+       * any objects back inside them main context.
+       */
+      g_mutex_unlock (&priv->mutex);
+      return G_SOURCE_REMOVE;
+    }
+
+  g_assert (priv->result == NULL);
+  g_assert (priv->return_called == TRUE);
+
+  priv->result = g_steal_pointer (&result);
+
+  callback = priv->callback;
+  callback_data = priv->user_data;
+
+  priv->callback = NULL;
+  priv->user_data = NULL;
+
+  source_object = priv->source_object ? g_object_ref (priv->source_object) : NULL;
+  cancellable = priv->cancellable ? g_object_ref (priv->cancellable) : NULL;
+
+  chained = g_steal_pointer (&priv->chained);
+
+  /* Make a private copy of the result data if we're going to need to notify
+   * other tasks of our result. We can't guarantee the result in @task will
+   * stay alive during our dispatch callbacks, so we need to have a copy.
+   */
+  if (chained != NULL && chained->len > 0)
+    result_copy = ide_task_result_copy (priv->result);
+
+  g_mutex_unlock (&priv->mutex);
+
+  if (callback)
+    callback (source_object, G_ASYNC_RESULT (self), callback_data);
+
+  if (chained)
+    {
+      for (guint i = 0; i < chained->len; i++)
+        {
+          IdeTask *other = g_ptr_array_index (chained, i);
+          g_autoptr(IdeTaskResult) other_result = ide_task_result_copy (result_copy);
+
+          ide_task_deliver_result (other, g_steal_pointer (&other_result));
+        }
+    }
+
+  g_mutex_lock (&priv->mutex);
+  priv->completed = TRUE;
+  g_mutex_unlock (&priv->mutex);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COMPLETED]);
+
+  ide_task_release (self, FALSE);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_task_return (IdeTask       *self,
+                 IdeTaskResult *result)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  g_autoptr(GMutexLocker) locker = NULL;
+
+  g_assert (IDE_IS_TASK (self));
+  g_assert (result != NULL);
+  g_assert (result->task == NULL);
+
+  locker = g_mutex_locker_new (&priv->mutex);
+
+  if (priv->cancel_handler && priv->cancellable)
+    {
+      g_cancellable_disconnect (priv->cancellable, priv->cancel_handler);
+      priv->cancel_handler = 0;
+    }
+
+  if (priv->return_called)
+    {
+      if (result->type == IDE_TASK_RESULT_CANCELLED)
+        {
+          /* We handled the cancel signal but already raced to get a result
+           * in the mean time. So we can just drop this result immediately
+           * as it has no state ot propagate.
+           */
+          ide_task_result_free (result);
+          return;
+        }
+
+      /*
+       * We already failed this task, but we need to ensure that the data
+       * is released from the main context rather than potentially on a
+       * thread (as we could be now).
+       *
+       * Since this can only happen when return_on_cancel is set, we can
+       * be assured the main context is alive because that is our contract
+       * with the API consumer.
+       */
+      if (!priv->got_cancel)
+        {
+          /* leak result to ensure we don't free anything in the wrong context */
+          g_critical ("Attempted to set task result multiple times, leaking result");
+          return;
+        }
+    }
+
+  priv->return_called = TRUE;
+
+  if (result->type == IDE_TASK_RESULT_CANCELLED)
+    priv->got_cancel = TRUE;
+
+  result->task = g_object_ref (self);
+  result->main_context = g_main_context_ref (priv->main_context);
+  result->priority = priv->priority;
+
+  /* We can queue the result immediately if we're not being called
+   * while we're inside of a ide_task_run_in_thread() callback. Otherwise,
+   * that thread cleanup must complete this to ensure objects cannot
+   * be finalized in that thread.
+   */
+  if (!priv->thread_called)
+    priv->return_source = ide_task_complete (result);
+  else if (priv->return_on_cancel && result->type == IDE_TASK_RESULT_CANCELLED)
+    priv->return_source = ide_task_complete (result);
+  else
+    priv->thread_result = result;
+}
+
+/**
+ * ide_task_return_int:
+ * @self: a #IdeTask
+ * @result: the result for the task
+ *
+ * Sets the result of the task to @result.
+ *
+ * Other tasks depending on the result will be notified after returning
+ * to the #GMainContext of the task.
+ *
+ * Since: 3.28
+ */
+void
+ide_task_return_int (IdeTask *self,
+                     gssize   result)
+{
+  IdeTaskResult *ret;
+
+  g_return_if_fail (IDE_IS_TASK (self));
+
+  ret = g_slice_new0 (IdeTaskResult);
+  ret->type = IDE_TASK_RESULT_INT;
+  ret->u.v_int = result;
+
+  ide_task_return (self, g_steal_pointer (&ret));
+}
+
+/**
+ * ide_task_return_boolean:
+ * @self: a #IdeTask
+ * @result: the result for the task
+ *
+ * Sets the result of the task to @result.
+ *
+ * Other tasks depending on the result will be notified after returning
+ * to the #GMainContext of the task.
+ *
+ * Since: 3.28
+ */
+void
+ide_task_return_boolean (IdeTask  *self,
+                         gboolean  result)
+{
+  IdeTaskResult *ret;
+
+  g_return_if_fail (IDE_IS_TASK (self));
+
+  ret = g_slice_new0 (IdeTaskResult);
+  ret->type = IDE_TASK_RESULT_BOOLEAN;
+  ret->u.v_bool = !!result;
+
+  ide_task_return (self, g_steal_pointer (&ret));
+}
+
+void
+ide_task_return_boxed (IdeTask  *self,
+                       GType     result_type,
+                       gpointer  result)
+{
+  IdeTaskResult *ret;
+
+  g_return_if_fail (IDE_IS_TASK (self));
+  g_return_if_fail (result_type != G_TYPE_INVALID);
+  g_return_if_fail (G_TYPE_IS_BOXED (result_type));
+
+  ret = g_slice_new0 (IdeTaskResult);
+  ret->type = IDE_TASK_RESULT_BOXED;
+  ret->u.v_boxed.type = result_type;
+  ret->u.v_boxed.pointer = result;
+
+  ide_task_return (self, g_steal_pointer (&ret));
+}
+
+/**
+ * ide_task_return_object:
+ * @self: a #IdeTask
+ * @instance: (transfer full): a #GObject instance
+ *
+ * Returns a new object instance.
+ *
+ * Takes ownership of @instance to allow saving a reference increment and
+ * decrement by the caller.
+ *
+ * Since: 3.28
+ */
+void
+ide_task_return_object (IdeTask  *self,
+                        gpointer  instance)
+{
+  IdeTaskResult *ret;
+
+  g_return_if_fail (IDE_IS_TASK (self));
+  g_return_if_fail (!instance || G_IS_OBJECT (instance));
+
+  ret = g_slice_new0 (IdeTaskResult);
+  ret->type = IDE_TASK_RESULT_OBJECT;
+  ret->u.v_object = instance;
+
+  ide_task_return (self, g_steal_pointer (&ret));
+}
+
+/**
+ * ide_task_return_pointer: (skip)
+ * @self: a #IdeTask
+ * @data: the data to return
+ * @destroy: an optional #GDestroyNotify to cleanup data if no handler
+ *   propagates the result
+ *
+ * Returns a new raw pointer.
+ *
+ * Note that pointers cannot be chained to other tasks, so you may not
+ * use ide_task_chain() in conjunction with a task returning a pointer
+ * using ide_task_return_pointer().
+ *
+ * If you need task chaining with pointers, see ide_task_return_boxed()
+ * or ide_task_return_object().
+ *
+ * Since: 3.28
+ */
+void
+ide_task_return_pointer (IdeTask        *self,
+                         gpointer        data,
+                         GDestroyNotify  destroy)
+{
+  IdeTaskResult *ret;
+
+  g_return_if_fail (IDE_IS_TASK (self));
+
+  ret = g_slice_new0 (IdeTaskResult);
+  ret->type = IDE_TASK_RESULT_POINTER;
+  ret->u.v_pointer.pointer = data;
+  ret->u.v_pointer.destroy = destroy;
+
+  ide_task_return (self, g_steal_pointer (&ret));
+}
+
+/**
+ * ide_task_return_error:
+ * @self: a #IdeTask
+ * @error: (transfer full): a #GError
+ *
+ * Sets @error as the result of the #IdeTask
+ *
+ * Since: 3.28
+ */
+void
+ide_task_return_error (IdeTask *self,
+                       GError  *error)
+{
+  IdeTaskResult *ret;
+
+  g_return_if_fail (IDE_IS_TASK (self));
+
+  ret = g_slice_new0 (IdeTaskResult);
+  ret->type = IDE_TASK_RESULT_ERROR;
+  ret->u.v_error = error;
+
+  ide_task_return (self, g_steal_pointer (&ret));
+}
+
+/**
+ * ide_task_return_new_error:
+ * @self: a #IdeTask
+ * @error_domain: the error domain of the #GError
+ * @error_code: the error code for the #GError
+ * @format: the printf-style format string
+ *
+ * Creates a new #GError and sets it as the result for the task.
+ *
+ * Since: 3.28
+ */
+void
+ide_task_return_new_error (IdeTask     *self,
+                           GQuark       error_domain,
+                           gint         error_code,
+                           const gchar *format,
+                           ...)
+{
+  GError *error;
+  va_list args;
+
+  g_return_if_fail (IDE_IS_TASK (self));
+
+  va_start (args, format);
+  error = g_error_new_valist (error_domain, error_code, format, args);
+  va_end (args);
+
+  ide_task_return_error (self, g_steal_pointer (&error));
+}
+
+/**
+ * ide_task_return_error_if_cancelled:
+ * @self: a #IdeTask
+ *
+ * Returns a new #GError if the cancellable associated with the task
+ * has been cancelled. If so, %TRUE is returned, otherwise %FALSE.
+ *
+ * Returns: %TRUE if the task was cancelled and error returned.
+ *
+ * Since: 3.28
+ */
+gboolean
+ide_task_return_error_if_cancelled (IdeTask *self)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  gboolean failed;
+
+  g_return_val_if_fail (IDE_IS_TASK (self), FALSE);
+
+  g_mutex_lock (&priv->mutex);
+  failed = g_cancellable_is_cancelled (priv->cancellable);
+  g_mutex_unlock (&priv->mutex);
+
+  if (failed)
+    ide_task_return_new_error (self,
+                               G_IO_ERROR,
+                               G_IO_ERROR_CANCELLED,
+                               "The task was cancelled");
+
+  return failed;
+}
+
+/**
+ * ide_task_set_release_on_propagate:
+ * @self: a #IdeTask
+ * @release_on_propagate: if data should be released on propagate
+ *
+ * Setting this to %TRUE (the default) ensures that the task will release all
+ * task data and source_object references after executing the configured
+ * callback. This is useful to ensure that dependent objects are finalized
+ * in the thread-default #GMainContext the task was created in.
+ *
+ * Generally, you want to leave this as %TRUE to ensure thread-safety on the
+ * dependent objects and task data.
+ *
+ * Since: 3.28
+ */
+void
+ide_task_set_release_on_propagate (IdeTask  *self,
+                                   gboolean  release_on_propagate)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TASK (self));
+
+  release_on_propagate = !!release_on_propagate;
+
+  g_mutex_lock (&priv->mutex);
+  priv->release_on_propagate = release_on_propagate;
+  g_mutex_unlock (&priv->mutex);
+}
+
+/**
+ * ide_task_set_source_tag:
+ * @self: a #IdeTask
+ * @source_tag: a tag to identify the task, usual a function pointer
+ *
+ * Sets the source tag for the task. Generally this is a function pointer
+ * of the function that created the task.
+ *
+ * Since: 3.28
+ */
+void
+ide_task_set_source_tag (IdeTask  *self,
+                         gpointer  source_tag)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TASK (self));
+
+  g_mutex_lock (&priv->mutex);
+  priv->source_tag = source_tag;
+  g_mutex_unlock (&priv->mutex);
+}
+
+/**
+ * ide_task_set_check_cancellable:
+ * @self: a #IdeTask
+ * @check_cancellable: %TRUE if the cancellable should be checked
+ *
+ * Setting @check_cancellable to %TRUE (the default) ensures that the
+ * #GCancellable used when creating the #IdeTask is checked for cancellation
+ * before propagating a result. If cancelled, an error will be returned
+ * instead of the result.
+ *
+ * Since: 3.28
+ */
+void
+ide_task_set_check_cancellable (IdeTask  *self,
+                                gboolean  check_cancellable)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TASK (self));
+
+  check_cancellable = !!check_cancellable;
+
+  g_mutex_lock (&priv->mutex);
+  priv->check_cancellable = check_cancellable;
+  g_mutex_unlock (&priv->mutex);
+}
+
+/**
+ * ide_task_run_in_thread: (skip)
+ * @self: a #IdeTask
+ * @thread_func: a function to execute on a worker thread
+ *
+ * Scheules @thread_func to be executed on a worker thread.
+ *
+ * @thread_func must complete the task from the worker thread using one of
+ * ide_task_return_boolean(), ide_task_return_int(), or
+ * ide_task_return_pointer().
+ *
+ * Since: 3.28
+ */
+void
+ide_task_run_in_thread (IdeTask           *self,
+                        IdeTaskThreadFunc  thread_func)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  g_autoptr(GError) error = NULL;
+
+  g_return_if_fail (IDE_IS_TASK (self));
+  g_return_if_fail (thread_func != NULL);
+
+  g_mutex_lock (&priv->mutex);
+
+  if (priv->completed == TRUE)
+    {
+      g_critical ("Task already completed, cannot run in thread");
+      goto unlock;
+    }
+
+  if (priv->thread_called)
+    {
+      g_critical ("Run in thread already called, cannot run again");
+      goto unlock;
+    }
+
+  priv->thread_called = TRUE;
+  priv->thread_func = thread_func;
+
+  ide_thread_pool_push ((IdeThreadPoolKind)priv->kind,
+                        ide_task_thread_func,
+                        g_object_ref (self));
+
+unlock:
+  g_mutex_unlock (&priv->mutex);
+
+  if (error != NULL)
+    ide_task_return_error (self, g_steal_pointer (&error));
+}
+
+/**
+ * ide_task_run_in_thread_sync: (skip)
+ */
+void
+ide_task_run_in_thread_sync (IdeTask           *self,
+                             IdeTaskThreadFunc  thread_func)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  g_autoptr(GMainContext) main_context = NULL;
+
+  g_return_if_fail (IDE_IS_TASK (self));
+  g_return_if_fail (thread_func != NULL);
+
+  g_mutex_lock (&priv->mutex);
+  main_context = g_main_context_ref (priv->main_context);
+  g_mutex_unlock (&priv->mutex);
+
+  ide_task_run_in_thread (self, thread_func);
+
+  while (!ide_task_get_completed (self))
+    g_main_context_iteration (main_context, TRUE);
+}
+
+static IdeTaskResult *
+ide_task_propagate_locked (IdeTask            *self,
+                           IdeTaskResultType   expected_type,
+                           GError            **error)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  IdeTaskResult *ret = NULL;
+
+  g_assert (IDE_IS_TASK (self));
+  g_assert (expected_type > IDE_TASK_RESULT_NONE);
+
+  if (priv->result == NULL)
+    {
+      g_autoptr(GMainContext) context = g_main_context_ref (priv->main_context);
+
+      while (priv->return_source)
+        {
+          g_mutex_unlock (&priv->mutex);
+          g_main_context_iteration (context, FALSE);
+          g_mutex_lock (&priv->mutex);
+        }
+    }
+
+  if (priv->result == NULL)
+    {
+      g_set_error_literal (error,
+                           G_IO_ERROR,
+                           G_IO_ERROR_FAILED,
+                           "No result available for task");
+    }
+  else if (priv->result->type == IDE_TASK_RESULT_ERROR)
+    {
+      if (error != NULL)
+        *error = g_error_copy (priv->result->u.v_error);
+    }
+  else if ((priv->check_cancellable && g_cancellable_is_cancelled (priv->cancellable)) ||
+           priv->result->type == IDE_TASK_RESULT_CANCELLED)
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_CANCELLED,
+                   "The operation was cancelled");
+    }
+  else if (priv->result->type != expected_type)
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_FAILED,
+                   "Task expected result of %s got %s",
+                   result_type_name (expected_type),
+                   result_type_name (priv->result->type));
+    }
+  else
+    {
+      g_assert (priv->result != NULL);
+      g_assert (priv->result->type == expected_type);
+
+      if (priv->release_on_propagate)
+        ret = g_steal_pointer (&priv->result);
+      else
+        ret = ide_task_result_copy (priv->result);
+
+      g_assert (ret != NULL);
+    }
+
+  return ret;
+}
+
+gboolean
+ide_task_propagate_boolean (IdeTask  *self,
+                            GError  **error)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  g_autoptr(GMutexLocker) locker = NULL;
+  g_autoptr(IdeTaskResult) res = NULL;
+
+  g_return_val_if_fail (IDE_IS_TASK (self), FALSE);
+
+  locker = g_mutex_locker_new (&priv->mutex);
+
+  if (!(res = ide_task_propagate_locked (self, IDE_TASK_RESULT_BOOLEAN, error)))
+    return FALSE;
+
+  return res->u.v_bool;
+}
+
+gpointer
+ide_task_propagate_boxed (IdeTask  *self,
+                          GError  **error)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  g_autoptr(GMutexLocker) locker = NULL;
+  g_autoptr(IdeTaskResult) res = NULL;
+
+  g_return_val_if_fail (IDE_IS_TASK (self), NULL);
+
+  locker = g_mutex_locker_new (&priv->mutex);
+
+  if (!(res = ide_task_propagate_locked (self, IDE_TASK_RESULT_BOXED, error)))
+    return NULL;
+
+  return g_boxed_copy (res->u.v_boxed.type, res->u.v_boxed.pointer);
+}
+
+gssize
+ide_task_propagate_int (IdeTask  *self,
+                        GError  **error)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  g_autoptr(GMutexLocker) locker = NULL;
+  g_autoptr(IdeTaskResult) res = NULL;
+
+  g_return_val_if_fail (IDE_IS_TASK (self), 0);
+
+  locker = g_mutex_locker_new (&priv->mutex);
+
+  if (!(res = ide_task_propagate_locked (self, IDE_TASK_RESULT_INT, error)))
+    return 0;
+
+  return res->u.v_int;
+}
+
+gpointer
+ide_task_propagate_object (IdeTask  *self,
+                           GError  **error)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  g_autoptr(GMutexLocker) locker = NULL;
+  g_autoptr(IdeTaskResult) res = NULL;
+
+  g_return_val_if_fail (IDE_IS_TASK (self), NULL);
+
+  locker = g_mutex_locker_new (&priv->mutex);
+
+  if (!(res = ide_task_propagate_locked (self, IDE_TASK_RESULT_OBJECT, error)))
+    return NULL;
+
+  return g_steal_pointer (&res->u.v_object);
+}
+
+gpointer
+ide_task_propagate_pointer (IdeTask  *self,
+                            GError  **error)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  g_autoptr(GMutexLocker) locker = NULL;
+  g_autoptr(IdeTaskResult) res = NULL;
+  gpointer ret;
+
+  g_return_val_if_fail (IDE_IS_TASK (self), NULL);
+
+  locker = g_mutex_locker_new (&priv->mutex);
+
+  if (!(res = ide_task_propagate_locked (self, IDE_TASK_RESULT_POINTER, error)))
+    return NULL;
+
+  ret = g_steal_pointer (&res->u.v_pointer.pointer);
+  res->u.v_pointer.destroy = NULL;
+
+  return ret;
+}
+
+/**
+ * ide_task_chain:
+ * @self: a #IdeTask
+ * @other_task: a #IdeTask
+ *
+ * Causes the result of @self to also be delivered to @other_task.
+ *
+ * This API is useful in situations when you want to avoid doing the same
+ * work multiple times, and can share the result between mutliple async
+ * operations requesting the same work.
+ *
+ * Users of this API must make sure one of two things is true. Either they
+ * have called ide_task_set_release_on_propagate() with @self and set
+ * release_on_propagate to %FALSE, or @self has not yet completed.
+ *
+ * Since: 3.28
+ */
+void
+ide_task_chain (IdeTask *self,
+                IdeTask *other_task)
+{
+  IdeTaskPrivate *self_priv = ide_task_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TASK (self));
+  g_return_if_fail (IDE_IS_TASK (other_task));
+  g_return_if_fail (self != other_task);
+
+  g_mutex_lock (&self_priv->mutex);
+
+  if (self_priv->result)
+    {
+      IdeTaskResult *copy = ide_task_result_copy (self_priv->result);
+
+      if (copy != NULL)
+        ide_task_deliver_result (other_task, g_steal_pointer (&copy));
+      else
+        ide_task_return_new_error (other_task,
+                                   G_IO_ERROR,
+                                   G_IO_ERROR_FAILED,
+                                   "Result could not be copied to task");
+    }
+  else
+    {
+      if (self_priv->chained == NULL)
+        self_priv->chained = g_ptr_array_new_with_free_func (g_object_unref);
+      g_ptr_array_add (self_priv->chained, g_object_ref (other_task));
+    }
+
+  g_mutex_unlock (&self_priv->mutex);
+}
+
+gpointer
+ide_task_get_source_tag (IdeTask *self)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  gpointer ret;
+
+  g_return_val_if_fail (IDE_IS_TASK (self), NULL);
+
+  g_mutex_lock (&priv->mutex);
+  ret = priv->source_tag;
+  g_mutex_unlock (&priv->mutex);
+
+  return ret;
+}
+
+IdeTaskKind
+ide_task_get_kind (IdeTask *self)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  IdeTaskKind kind;
+
+  g_return_val_if_fail (IDE_IS_TASK (self), 0);
+
+  g_mutex_lock (&priv->mutex);
+  kind = priv->kind;
+  g_mutex_unlock (&priv->mutex);
+
+  return kind;
+}
+
+void
+ide_task_set_kind (IdeTask     *self,
+                   IdeTaskKind  kind)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TASK (self));
+  g_return_if_fail (kind >= IDE_TASK_KIND_DEFAULT);
+  g_return_if_fail (kind < IDE_TASK_KIND_LAST);
+
+  g_mutex_lock (&priv->mutex);
+  priv->kind = kind;
+  g_mutex_unlock (&priv->mutex);
+}
+
+/**
+ * ide_task_get_task_data: (skip)
+ * @self: a #IdeTask
+ *
+ * Gets the task data previously set with ide_task_set_task_data().
+ *
+ * Returns: (transfer none): previously registered task data or %NULL
+ *
+ * Since: 3.28
+ */
+gpointer
+ide_task_get_task_data (IdeTask *self)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  gpointer task_data = NULL;
+
+  g_assert (IDE_IS_TASK (self));
+
+  g_mutex_lock (&priv->mutex);
+  if (priv->task_data)
+    task_data = priv->task_data->data;
+  g_mutex_unlock (&priv->mutex);
+
+  return task_data;
+}
+
+static gboolean
+ide_task_set_task_data_cb (gpointer data)
+{
+  IdeTaskData *task_data = data;
+  ide_task_data_free (task_data);
+  return G_SOURCE_REMOVE;
+}
+
+void
+ide_task_set_task_data (IdeTask        *self,
+                        gpointer        task_data,
+                        GDestroyNotify  task_data_destroy)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  g_autoptr(IdeTaskData) old_task_data = NULL;
+  g_autoptr(IdeTaskData) new_task_data = NULL;
+
+  g_return_if_fail (IDE_IS_TASK (self));
+
+  new_task_data = g_slice_new0 (IdeTaskData);
+  new_task_data->data = task_data;
+  new_task_data->data_destroy = task_data_destroy;
+
+  g_mutex_lock (&priv->mutex);
+
+  if (priv->return_called)
+    {
+      g_critical ("Cannot set task data after returning value");
+      goto unlock;
+    }
+
+  old_task_data = g_steal_pointer (&priv->task_data);
+  priv->task_data = g_steal_pointer (&new_task_data);
+
+  if (priv->thread_called && old_task_data)
+    {
+      GSource *source;
+
+      source = g_idle_source_new ();
+      g_source_set_name (source, "[ide-task] finalize task data");
+      g_source_set_ready_time (source, -1);
+      g_source_set_callback (source,
+                             ide_task_set_task_data_cb,
+                             NULL, NULL);
+      g_source_set_priority (source, priv->priority);
+      g_source_attach (source, priv->main_context);
+      g_source_unref (source);
+    }
+
+unlock:
+  g_mutex_unlock (&priv->mutex);
+}
+
+static gboolean
+ide_task_cancel_cb (gpointer user_data)
+{
+  IdeTask *self = user_data;
+  IdeTaskResult *ret;
+
+  g_assert (IDE_IS_TASK (self));
+
+  ret = g_slice_new0 (IdeTaskResult);
+  ret->type = IDE_TASK_RESULT_CANCELLED;
+
+  ide_task_return (self, g_steal_pointer (&ret));
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_task_cancellable_cancelled_cb (GCancellable  *cancellable,
+                                   IdeTaskCancel *cancel)
+{
+  GSource *source;
+
+  g_assert (G_IS_CANCELLABLE (cancellable));
+  g_assert (cancel != NULL);
+  g_assert (IDE_IS_TASK (cancel->task));
+  g_assert (cancel->main_context != NULL);
+
+  /*
+   * This can be called synchronously from g_cancellable_connect(), which
+   * could still be holding priv->mutex. So we need to queue the cancellation
+   * request back through the main context.
+   */
+
+  source = g_idle_source_new ();
+  g_source_set_name (source, "[ide-task] cancel task");
+  g_source_set_ready_time (source, -1);
+  g_source_set_callback (source, ide_task_cancel_cb, g_object_ref (cancel->task), g_object_unref);
+  g_source_set_priority (source, cancel->priority);
+  g_source_attach (source, cancel->main_context);
+  g_source_unref (source);
+}
+
+/**
+ * ide_task_set_return_on_cancel:
+ * @self: a #IdeTask
+ * @return_on_cancel: if the task should return immediately when the
+ *   #GCancellable has been cancelled.
+ *
+ * Setting @return_on_cancel to %TRUE ensures that the task will cancel
+ * immediately when #GCancellable::cancelled is emitted by the configured
+ * cancellable.
+ *
+ * Setting this requires that the caller can ensure the configured #GMainContext
+ * will outlive the threaded worker so that task state can be freed in a delayed
+ * fashion.
+ *
+ * Since: 3.28
+ */
+void
+ide_task_set_return_on_cancel (IdeTask  *self,
+                               gboolean  return_on_cancel)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  g_autoptr(GMutexLocker) locker = NULL;
+
+  g_return_if_fail (IDE_IS_TASK (self));
+
+  locker = g_mutex_locker_new (&priv->mutex);
+
+  if (priv->cancellable == NULL)
+    return;
+
+  return_on_cancel = !!return_on_cancel;
+
+  if (priv->return_on_cancel != return_on_cancel)
+    {
+      priv->return_on_cancel = return_on_cancel;
+
+      if (return_on_cancel)
+        {
+          IdeTaskCancel *cancel;
+
+          /* This creates a reference cycle, but it gets destroyed when the
+           * appropriate ide_task_return() API is called.
+           */
+          cancel = g_slice_new0 (IdeTaskCancel);
+          cancel->main_context = g_main_context_ref (priv->main_context);
+          cancel->task = g_object_ref (self);
+          cancel->priority = priv->priority;
+
+          priv->cancel_handler =
+            g_cancellable_connect (priv->cancellable,
+                                   G_CALLBACK (ide_task_cancellable_cancelled_cb),
+                                   g_steal_pointer (&cancel),
+                                   (GDestroyNotify)ide_task_cancel_free);
+
+        }
+      else
+        {
+          if (priv->cancel_handler)
+            {
+              g_cancellable_disconnect (priv->cancellable, priv->cancel_handler);
+              priv->cancel_handler = 0;
+            }
+        }
+    }
+}
+
+void
+ide_task_report_new_error (gpointer              source_object,
+                           GAsyncReadyCallback   callback,
+                           gpointer              callback_data,
+                           gpointer              source_tag,
+                           GQuark                domain,
+                           gint                  code,
+                           const gchar          *format,
+                           ...)
+{
+  g_autoptr(IdeTask) task = NULL;
+  GError *error;
+  va_list args;
+
+  va_start (args, format);
+  error = g_error_new_valist (domain, code, format, args);
+  va_end (args);
+
+  task = ide_task_new (source_object, NULL, callback, callback_data);
+  ide_task_set_source_tag (task, source_tag);
+  ide_task_return_error (task, g_steal_pointer (&error));
+}
+
+/**
+ * ide_task_get_name:
+ * @self: a #IdeTask
+ *
+ * Gets the name assigned for the task.
+ *
+ * Returns: (nullable): a string or %NULL
+ *
+ * Since: 3.28
+ */
+const gchar *
+ide_task_get_name (IdeTask *self)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+  const gchar *ret;
+
+  g_return_val_if_fail (IDE_IS_TASK (self), NULL);
+
+  g_mutex_lock (&priv->mutex);
+  ret = priv->name;
+  g_mutex_unlock (&priv->mutex);
+
+  return ret;
+}
+
+/**
+ * ide_task_set_name:
+ * @self: a #IdeTask
+ *
+ * Sets a useful name for the task.
+ *
+ * This string is interned, so it is best to avoid dynamic names as
+ * that can result in lots of unnecessary strings being interned for
+ * the lifetime of the process.
+ *
+ * This name may be used in various g_critical() messages which can
+ * be useful in troubleshooting.
+ *
+ * If using #IdeTask from C, a default name is set using the source
+ * file name and line number.
+ *
+ * Since: 3.28
+ */
+void
+ide_task_set_name (IdeTask *self,
+                   const gchar *name)
+{
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TASK (self));
+
+  name = g_intern_string (name);
+
+  g_mutex_lock (&priv->mutex);
+  priv->name = name;
+  g_mutex_unlock (&priv->mutex);
+}
+
+static gpointer
+ide_task_get_user_data (GAsyncResult *result)
+{
+  IdeTask *self = (IdeTask *)result;
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+
+  g_assert (IDE_IS_TASK (self));
+
+  return priv->user_data;
+}
+
+static GObject *
+ide_task_get_source_object_full (GAsyncResult *result)
+{
+  IdeTask *self = (IdeTask *)result;
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+
+  g_assert (IDE_IS_TASK (self));
+
+  return priv->source_object ? g_object_ref (priv->source_object) : NULL;
+}
+
+static gboolean
+ide_task_is_tagged (GAsyncResult *result,
+                    gpointer      source_tag)
+{
+  IdeTask *self = (IdeTask *)result;
+  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
+
+  g_assert (IDE_IS_TASK (self));
+
+  return source_tag == priv->source_tag;
+}
+
+static void
+async_result_init_iface (GAsyncResultIface *iface)
+{
+  iface->get_user_data = ide_task_get_user_data;
+  iface->get_source_object = ide_task_get_source_object_full;
+  iface->is_tagged = ide_task_is_tagged;
+}
diff --git a/src/libide/threading/ide-task.h b/src/libide/threading/ide-task.h
new file mode 100644
index 000000000..318b91d43
--- /dev/null
+++ b/src/libide/threading/ide-task.h
@@ -0,0 +1,174 @@
+/* ide-task.h
+ *
+ * Copyright 2018 Christian Hergert
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TASK (ide_task_get_type())
+
+IDE_AVAILABLE_IN_3_28
+G_DECLARE_DERIVABLE_TYPE (IdeTask, ide_task, IDE, TASK, GObject)
+
+typedef void (*IdeTaskThreadFunc) (IdeTask      *task,
+                                   gpointer      source_object,
+                                   gpointer      task_data,
+                                   GCancellable *cancellable);
+
+typedef enum
+{
+  IDE_TASK_KIND_DEFAULT,
+  IDE_TASK_KIND_COMPILER,
+  IDE_TASK_KIND_INDEXER,
+  IDE_TASK_KIND_IO,
+  IDE_TASK_KIND_LAST
+} IdeTaskKind;
+
+struct _IdeTaskClass
+{
+  GObjectClass parent;
+
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_28
+IdeTask      *ide_task_new                       (gpointer              source_object,
+                                                  GCancellable         *cancellable,
+                                                  GAsyncReadyCallback   callback,
+                                                  gpointer              user_data);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_chain                     (IdeTask              *self,
+                                                  IdeTask              *other_task);
+IDE_AVAILABLE_IN_3_28
+GCancellable *ide_task_get_cancellable           (IdeTask              *self);
+IDE_AVAILABLE_IN_3_28
+gboolean      ide_task_get_completed             (IdeTask              *self);
+IDE_AVAILABLE_IN_3_28
+IdeTaskKind   ide_task_get_kind                  (IdeTask              *self);
+IDE_AVAILABLE_IN_3_28
+const gchar  *ide_task_get_name                  (IdeTask              *self);
+IDE_AVAILABLE_IN_3_28
+gint          ide_task_get_priority              (IdeTask              *self);
+IDE_AVAILABLE_IN_3_28
+gpointer      ide_task_get_source_object         (IdeTask              *self);
+IDE_AVAILABLE_IN_3_28
+gpointer      ide_task_get_source_tag            (IdeTask              *self);
+IDE_AVAILABLE_IN_3_28
+gpointer      ide_task_get_task_data             (IdeTask              *self);
+IDE_AVAILABLE_IN_3_28
+gboolean      ide_task_is_valid                  (gpointer              self,
+                                                  gpointer              source_object);
+IDE_AVAILABLE_IN_3_28
+gboolean      ide_task_propagate_boolean         (IdeTask              *self,
+                                                  GError              **error);
+IDE_AVAILABLE_IN_3_28
+gpointer      ide_task_propagate_boxed           (IdeTask              *self,
+                                                  GError              **error);
+IDE_AVAILABLE_IN_3_28
+gssize        ide_task_propagate_int             (IdeTask              *self,
+                                                  GError              **error);
+IDE_AVAILABLE_IN_3_28
+gpointer      ide_task_propagate_object          (IdeTask              *self,
+                                                  GError              **error);
+IDE_AVAILABLE_IN_3_28
+gpointer      ide_task_propagate_pointer         (IdeTask              *self,
+                                                  GError              **error);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_return_boolean            (IdeTask              *self,
+                                                  gboolean              result);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_return_boxed              (IdeTask              *self,
+                                                  GType                 result_type,
+                                                  gpointer              result);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_return_error              (IdeTask              *self,
+                                                  GError               *error);
+IDE_AVAILABLE_IN_3_28
+gboolean      ide_task_return_error_if_cancelled (IdeTask              *self);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_return_int                (IdeTask              *self,
+                                                  gssize                result);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_return_new_error          (IdeTask              *self,
+                                                  GQuark                error_domain,
+                                                  gint                  error_code,
+                                                  const gchar          *format,
+                                                  ...) G_GNUC_PRINTF (4, 5);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_return_object             (IdeTask              *self,
+                                                  gpointer              instance);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_return_pointer            (IdeTask              *self,
+                                                  gpointer              data,
+                                                  GDestroyNotify        destroy);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_run_in_thread             (IdeTask              *self,
+                                                  IdeTaskThreadFunc     thread_func);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_run_in_thread_sync        (IdeTask              *self,
+                                                  IdeTaskThreadFunc     thread_func);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_set_check_cancellable     (IdeTask              *self,
+                                                  gboolean              check_cancellable);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_set_kind                  (IdeTask              *self,
+                                                  IdeTaskKind           kind);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_set_name                  (IdeTask              *self,
+                                                  const gchar          *name);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_set_priority              (IdeTask              *self,
+                                                  gint                  priority);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_set_release_on_propagate  (IdeTask              *self,
+                                                  gboolean              release_on_propagate);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_set_return_on_cancel      (IdeTask              *self,
+                                                  gboolean              return_on_cancel);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_set_source_tag            (IdeTask              *self,
+                                                  gpointer              source_tag);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_set_task_data             (IdeTask              *self,
+                                                  gpointer              task_data,
+                                                  GDestroyNotify        task_data_destroy);
+IDE_AVAILABLE_IN_3_28
+void          ide_task_report_new_error          (gpointer              source_object,
+                                                  GAsyncReadyCallback   callback,
+                                                  gpointer              callback_data,
+                                                  gpointer              source_tag,
+                                                  GQuark                domain,
+                                                  gint                  code,
+                                                  const gchar          *format,
+                                                  ...) G_GNUC_PRINTF (7, 8);
+
+#ifdef __GNUC__
+# define ide_task_new(self, cancellable, callback, user_data)                      \
+  ({                                                                               \
+    IdeTask *__ide_task = (ide_task_new) (self, cancellable, callback, user_data); \
+    ide_task_set_name (__ide_task, G_STRLOC);                                      \
+    __ide_task;                                                                    \
+  })
+#endif
+
+G_END_DECLS
diff --git a/src/libide/threading/meson.build b/src/libide/threading/meson.build
index 44fc6ffcc..201e188e0 100644
--- a/src/libide/threading/meson.build
+++ b/src/libide/threading/meson.build
@@ -1,13 +1,16 @@
 threading_headers = [
   'ide-thread-pool.h',
+  'ide-task.h',
 ]
 
 threading_sources = [
   'ide-thread-pool.c',
+  'ide-task.c',
 ]
 
 threading_enums = [
   'ide-thread-pool.h',
+  'ide-task.h',
 ]
 
 libide_public_headers += files(threading_headers)
diff --git a/src/tests/meson.build b/src/tests/meson.build
index c4273d32f..ca04f7af1 100644
--- a/src/tests/meson.build
+++ b/src/tests/meson.build
@@ -169,8 +169,16 @@ test_ide_glib = executable('test-ide-glib', 'test-ide-glib.c',
 )
 test('test-ide-glib', test_ide_glib, env: ide_test_env)
 
+
 test_line_reader = executable('test-line-reader', 'test-line-reader.c',
   c_args: ide_test_cflags,
   dependencies: [ ide_test_deps ],
 )
 test('test-line-reader', test_line_reader, env: ide_test_env)
+
+
+test_ide_task = executable('test-ide-task', 'test-ide-task.c',
+  c_args: ide_test_cflags,
+  dependencies: [ ide_test_deps ],
+)
+test('test-ide-task', test_ide_task, env: ide_test_env)
diff --git a/src/tests/test-ide-task.c b/src/tests/test-ide-task.c
new file mode 100644
index 000000000..03f0aeda1
--- /dev/null
+++ b/src/tests/test-ide-task.c
@@ -0,0 +1,735 @@
+/* test-ide-task.c
+ *
+ * Copyright 2018 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <ide.h>
+
+#include "../libide/threading/ide-thread-private.h"
+
+static gboolean
+complete_int (gpointer data)
+{
+  IdeTask *task = data;
+  ide_task_return_int (task, -123);
+  return G_SOURCE_REMOVE;
+}
+
+static void
+check_int (GObject      *object,
+           GAsyncResult *result,
+           gpointer      user_data)
+{
+  g_autoptr(GMainLoop) main_loop = user_data;
+  g_autoptr(GError) error = NULL;
+  gssize ret;
+
+  g_assert (!object || G_IS_OBJECT (object));
+  g_assert (IDE_IS_TASK (result));
+  g_assert (main_loop != NULL);
+
+  ret = ide_task_propagate_int (IDE_TASK (result), &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (ret, ==, -123);
+
+  /* shoudln't switch to true until callback has exited */
+  g_assert_false (ide_task_get_completed (IDE_TASK (result)));
+
+  g_main_loop_quit (main_loop);
+}
+
+static void
+test_ide_task_chain (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  IdeTask *task = ide_task_new (NULL, NULL, NULL, NULL);
+  IdeTask *task2 = ide_task_new (NULL, NULL, check_int, g_main_loop_ref (main_loop));
+
+  /* tests that we can chain the result from the first task to the
+   * second task and get the same answer.
+   */
+
+  g_object_add_weak_pointer (G_OBJECT (task), (gpointer *)&task);
+  g_object_add_weak_pointer (G_OBJECT (task2), (gpointer *)&task2);
+
+  ide_task_chain (task, task2);
+
+  g_timeout_add (0, complete_int, task);
+  g_main_loop_run (main_loop);
+
+  g_assert_true (ide_task_get_completed (task));
+  g_assert_true (ide_task_get_completed (task2));
+
+  g_object_unref (task);
+  g_object_unref (task2);
+
+  g_assert_null (task);
+  g_assert_null (task2);
+}
+
+static void
+test_ide_task_basic (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  IdeTask *task = ide_task_new (NULL, NULL, check_int, g_main_loop_ref (main_loop));
+
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  ide_task_set_source_tag (task, test_ide_task_basic);
+  g_assert (ide_task_get_source_tag (task) == test_ide_task_basic);
+
+  g_object_add_weak_pointer (G_OBJECT (task), (gpointer *)&task);
+
+  g_timeout_add (0, complete_int, task);
+  g_main_loop_run (main_loop);
+
+  g_assert_true (ide_task_get_completed (task));
+  g_object_unref (task);
+
+  g_assert_null (task);
+}
+
+static void
+test_ide_task_no_release (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  IdeTask *task = ide_task_new (NULL, NULL, check_int, g_main_loop_ref (main_loop));
+
+  ide_task_set_release_on_propagate (task, FALSE);
+
+  g_object_add_weak_pointer (G_OBJECT (task), (gpointer *)&task);
+
+  g_timeout_add (0, complete_int, task);
+  g_main_loop_run (main_loop);
+
+  g_assert_true (ide_task_get_completed (task));
+  g_object_unref (task);
+
+  g_assert_null (task);
+}
+
+static void
+test_ide_task_serial (void)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+  gboolean r;
+
+  /*
+   * This tests creating a task, returning, and propagating a value
+   * serially without returning to the main loop. (the task will advance
+   * the main context to make this work.
+   */
+
+  task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_assert_false (ide_task_get_completed (task));
+  ide_task_return_boolean (task, TRUE);
+  g_assert_false (ide_task_get_completed (task));
+  r = ide_task_propagate_boolean (task, &error);
+  g_assert_true (ide_task_get_completed (task));
+  g_assert_no_error (error);
+  g_assert_true (r);
+}
+
+static void
+test_ide_task_delayed_chain (void)
+{
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(IdeTask) task2 = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(GObject) obj2 = NULL;
+  g_autoptr(GError) error = NULL;
+
+  /* finish task 1, but it won't release resources since still need them
+   * for future chaining.
+   */
+  ide_task_set_release_on_propagate (task, FALSE);
+  ide_task_return_object (task, g_steal_pointer (&obj));
+  g_assert_null (obj);
+  obj = ide_task_propagate_object (task, &error);
+  g_assert_nonnull (obj);
+
+  /* try to chain a task, it should succeed since task still has the obj */
+  ide_task_chain (task, task2);
+  obj2 = ide_task_propagate_object (task2, &error);
+  g_assert_no_error (error);
+  g_assert_nonnull (obj2);
+}
+
+static void
+test_ide_task_delayed_chain_fail (void)
+{
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(IdeTask) task2 = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(GObject) obj2 = NULL;
+  g_autoptr(GError) error = NULL;
+
+  /* complete a task with an object, with release_on_propagate set */
+  ide_task_return_object (task, g_steal_pointer (&obj));
+  g_assert_null (obj);
+  obj = ide_task_propagate_object (task, &error);
+  g_assert_nonnull (obj);
+
+  /* try to chain a task, it should fail since task released the obj */
+  ide_task_chain (task, task2);
+  obj2 = ide_task_propagate_object (task2, &error);
+  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED);
+  g_assert_null (obj2);
+}
+
+static void
+test_ide_task_null_object (void)
+{
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(IdeTask) task2 = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(GObject) obj = NULL;
+  g_autoptr(GObject) obj2 = NULL;
+  g_autoptr(GError) error = NULL;
+
+  /* Create a task, return a NULL object for a result. Ensure we got
+   * NULL when propagating and no error.
+   */
+  ide_task_set_release_on_propagate (task, FALSE);
+  ide_task_return_object (task, NULL);
+  obj = ide_task_propagate_object (task, &error);
+  g_assert_null (obj);
+  g_assert_no_error (error);
+
+  /* Now try to chain it, and make sure it is the same */
+  ide_task_chain (task, task2);
+  obj2 = ide_task_propagate_object (task2, &error);
+  g_assert_no_error (error);
+  g_assert_null (obj2);
+}
+
+typedef gchar FooStr;
+G_DEFINE_BOXED_TYPE (FooStr, foo_str, g_strdup, g_free)
+
+static void
+test_ide_task_boxed (void)
+{
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *ret = NULL;
+
+  ide_task_return_boxed (task, foo_str_get_type (), g_strdup ("Hi there"));
+  ret = ide_task_propagate_boxed (task, &error);
+  g_assert_no_error (error);
+  g_assert_cmpstr (ret, ==, "Hi there");
+}
+
+static void
+test_ide_task_get_cancellable (void)
+{
+  g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+  g_autoptr(IdeTask) task = ide_task_new (NULL, cancellable, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+
+  g_assert (cancellable == ide_task_get_cancellable (task));
+  ide_task_return_int (task, 123);
+  g_assert (cancellable == ide_task_get_cancellable (task));
+  ide_task_propagate_int (task, &error);
+  g_assert_no_error (error);
+  g_assert (cancellable == ide_task_get_cancellable (task));
+}
+
+static void
+test_ide_task_is_valid (void)
+{
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(IdeTask) task2 = ide_task_new (obj, NULL, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+
+  g_assert (ide_task_is_valid (task, NULL));
+  g_assert (!ide_task_is_valid (task, obj));
+  g_assert (!ide_task_is_valid (task2, NULL));
+  g_assert (ide_task_is_valid (task2, obj));
+
+  ide_task_return_int (task, 123);
+  ide_task_return_int (task2, 123);
+}
+
+static void
+test_ide_task_source_object (void)
+{
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(GObject) obj2 = NULL;
+  g_autoptr(IdeTask) task = ide_task_new (obj, NULL, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+
+  obj2 = g_async_result_get_source_object (G_ASYNC_RESULT (task));
+  g_assert (obj == obj2);
+
+  ide_task_return_boolean (task, TRUE);
+  g_assert_true (ide_task_propagate_boolean (task, &error));
+  g_assert_no_error (error);
+
+  /* default release-on-propagate, source object released now */
+  g_assert_null (g_async_result_get_source_object (G_ASYNC_RESULT (task)));
+}
+
+static void
+test_ide_task_error (void)
+{
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+
+  ide_task_return_new_error (task,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_CONNECTED,
+                             "Not connected");
+  g_assert_false (ide_task_propagate_boolean (task, &error));
+  g_assert_error (error,
+                  G_IO_ERROR,
+                  G_IO_ERROR_NOT_CONNECTED);
+}
+
+static void
+typical_cb (GObject      *object,
+            GAsyncResult *result,
+            gpointer      user_data)
+{
+  g_autoptr(GMainLoop) main_loop = user_data;
+  g_autoptr(GError) error = NULL;
+  gboolean r;
+
+  g_assert (object == NULL);
+  g_assert (IDE_IS_TASK (result));
+  g_assert (main_loop);
+
+  r = ide_task_propagate_boolean (IDE_TASK (result), &error);
+  g_assert_true (r);
+
+  g_main_loop_quit (main_loop);
+}
+
+static gboolean
+complete_in_main (gpointer data)
+{
+  g_autoptr(IdeTask) task = data;
+  g_assert (IDE_IS_TASK (task));
+  ide_task_return_boolean (task, TRUE);
+  return G_SOURCE_REMOVE;
+}
+
+static void
+test_ide_task_typical (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  g_autoptr(IdeTask) task = NULL;
+  IdeTask *finalize_check = NULL;
+
+  task = ide_task_new (NULL, NULL, typical_cb, g_main_loop_ref (main_loop));
+
+  /* life-cycle tracking */
+  finalize_check = task;
+  g_object_add_weak_pointer (G_OBJECT (task), (gpointer *)&finalize_check);
+
+  /* simulate some async call */
+  g_timeout_add (0, complete_in_main, g_steal_pointer (&task));
+
+  g_main_loop_run (main_loop);
+
+  g_assert_null (finalize_check);
+}
+
+static void
+test_ide_task_thread_cb (IdeTask      *task,
+                         gpointer      source_object,
+                         gpointer      task_data,
+                         GCancellable *cancellable)
+{
+  g_assert_nonnull (task);
+  g_assert (IDE_IS_TASK (task));
+  g_assert_nonnull (source_object);
+  g_assert (G_IS_OBJECT (source_object));
+  g_assert (G_IS_CANCELLABLE (cancellable));
+
+  ide_task_return_int (task, -123);
+}
+
+static void
+test_ide_task_thread (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+  g_autoptr(IdeTask) task = ide_task_new (obj, cancellable, check_int, g_main_loop_ref (main_loop));
+
+  ide_task_run_in_thread (task, test_ide_task_thread_cb);
+  g_main_loop_run (main_loop);
+}
+
+static void
+test_ide_task_thread_chained (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+  g_autoptr(IdeTask) task = ide_task_new (obj, cancellable, check_int, g_main_loop_ref (main_loop));
+  g_autoptr(IdeTask) task2 = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+  gssize ret;
+
+  ide_task_chain (task, task2);
+  ide_task_run_in_thread (task, test_ide_task_thread_cb);
+  g_main_loop_run (main_loop);
+
+  ret = ide_task_propagate_int (task2, &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (ret, ==, -123);
+}
+
+static void
+inc_completed (IdeTask    *task,
+               GParamSpec *pspec,
+               gpointer    user_data)
+{
+  g_autoptr(GMainContext) context = NULL;
+  guint *count = user_data;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert_nonnull (pspec);
+  g_assert_cmpstr (pspec->name, ==, "completed");
+  g_assert_nonnull (count);
+
+  context = g_main_context_ref_thread_default ();
+  g_assert (g_main_context_default () == context);
+
+  (*count)++;
+}
+
+static void
+test_ide_task_completed (void)
+{
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+  guint count = 0;
+
+  g_signal_connect (task, "notify::completed", G_CALLBACK (inc_completed), &count);
+  ide_task_return_boolean (task, TRUE);
+  g_assert_cmpint (count, ==, 0);
+  g_assert_true (ide_task_propagate_boolean (task, &error));
+  g_assert_no_error (error);
+  g_assert_cmpint (count, ==, 1);
+}
+
+static void
+test_ide_task_completed_threaded (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+  g_autoptr(IdeTask) task = ide_task_new (obj, cancellable, check_int, g_main_loop_ref (main_loop));
+  g_autoptr(GError) error = NULL;
+  guint count = 0;
+
+  g_signal_connect (task, "notify::completed", G_CALLBACK (inc_completed), &count);
+  ide_task_run_in_thread (task, test_ide_task_thread_cb);
+  g_main_loop_run (main_loop);
+  g_assert_cmpint (count, ==, 1);
+}
+
+static void
+test_ide_task_task_data (void)
+{
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+  gint *n = g_new0 (gint, 1);
+
+  ide_task_set_task_data (task, n, g_free);
+  g_assert (ide_task_get_task_data (task) == (gpointer)n);
+  ide_task_return_boolean (task, TRUE);
+  g_assert_nonnull (ide_task_get_task_data (task));
+  g_assert_true (ide_task_propagate_boolean (task, &error));
+  g_assert_no_error (error);
+  /* after propagation, task data should be freed */
+  g_assert_null (ide_task_get_task_data (task));
+}
+
+static void
+test_ide_task_task_data_threaded (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+  g_autoptr(IdeTask) task = ide_task_new (obj, cancellable, check_int, g_main_loop_ref (main_loop));
+  g_autoptr(GError) error = NULL;
+  gint *n = g_new0 (gint, 1);
+
+  ide_task_set_task_data (task, n, g_free);
+  ide_task_run_in_thread (task, test_ide_task_thread_cb);
+  g_main_loop_run (main_loop);
+  g_assert_null (ide_task_get_task_data (task));
+}
+
+static void
+set_in_thread_cb (GObject      *object,
+                  GAsyncResult *result,
+                  gpointer      user_data)
+{
+  g_autoptr(GMainLoop) main_loop = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeTask *task = (IdeTask *)result;
+
+  g_assert (object == NULL);
+  g_assert (IDE_IS_TASK (task));
+  g_assert (main_loop != NULL);
+
+  g_assert (ide_task_get_task_data (task) == GINT_TO_POINTER (0x1234));
+  g_assert_true (ide_task_propagate_boolean (task, &error));
+  g_assert_no_error (error);
+
+  /* we should have this cleared until we return from this func */
+  g_assert_nonnull (ide_task_get_task_data (task));
+  g_assert (ide_task_get_task_data (task) == GINT_TO_POINTER (0x1234));
+
+  g_main_loop_quit (main_loop);
+}
+
+static void
+set_in_thread_worker (IdeTask      *task,
+                      gpointer      source_object,
+                      gpointer      task_data,
+                      GCancellable *cancellable)
+{
+  g_assert (IDE_IS_TASK (task));
+  g_assert (source_object == NULL);
+  g_assert (task_data == NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /* its invalid to call set_task_data() after return, but okay here.
+   * this obviously invalidates @task_data.
+   */
+  ide_task_set_task_data (task, GINT_TO_POINTER (0x1234), NULL);
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+test_ide_task_task_data_set_in_thread (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, set_in_thread_cb, g_main_loop_ref (main_loop));
+  g_autoptr(GError) error = NULL;
+
+  ide_task_run_in_thread (task, set_in_thread_worker);
+  g_main_loop_run (main_loop);
+
+  /* and now it should be cleared */
+  g_assert_null (ide_task_get_task_data (task));
+}
+
+static void
+test_ide_task_get_source_object (void)
+{
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(IdeTask) task = ide_task_new (obj, NULL, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+
+  g_assert_nonnull (ide_task_get_source_object (task));
+  g_assert (obj == ide_task_get_source_object (task));
+
+  ide_task_return_boolean (task, TRUE);
+
+  g_assert_nonnull (ide_task_get_source_object (task));
+  g_assert (obj == ide_task_get_source_object (task));
+
+  g_assert_true (ide_task_propagate_boolean (task, &error));
+  g_assert_null (ide_task_get_source_object (task));
+}
+
+static void
+test_ide_task_check_cancellable (void)
+{
+  g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+  g_autoptr(IdeTask) task = ide_task_new (NULL, cancellable, NULL, NULL);
+  g_autoptr(IdeTask) task2 = ide_task_new (NULL, cancellable, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+
+  ide_task_set_check_cancellable (task2, FALSE);
+
+  g_cancellable_cancel (cancellable);
+  ide_task_return_boolean (task, TRUE);
+  ide_task_return_boolean (task2, TRUE);
+  g_assert_false (ide_task_propagate_boolean (task, &error));
+  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
+  g_clear_error (&error);
+  g_assert_true (ide_task_propagate_boolean (task2, &error));
+  g_assert_no_error (error);
+}
+
+G_LOCK_DEFINE (cancel_lock);
+
+static void
+test_ide_task_return_on_cancel_worker (IdeTask      *task,
+                                       gpointer      source_object,
+                                       gpointer      task_data,
+                                       GCancellable *cancellable)
+{
+  g_assert (IDE_IS_TASK (task));
+  g_assert (source_object == NULL);
+  g_assert (G_IS_CANCELLABLE (cancellable));
+
+  G_LOCK (cancel_lock);
+  ide_task_return_boolean (task, TRUE);
+  G_UNLOCK (cancel_lock);
+}
+
+static gboolean
+idle_main_loop_quit (gpointer data)
+{
+  GMainLoop *main_loop = data;
+  g_main_loop_quit (main_loop);
+  return G_SOURCE_REMOVE;
+}
+
+static void
+test_ide_task_return_on_cancel_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  g_autoptr(GMainLoop) main_loop = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (object == NULL);
+  g_assert (IDE_IS_TASK (result));
+
+  g_assert_false (ide_task_propagate_boolean (IDE_TASK (result), &error));
+  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
+
+  G_UNLOCK (cancel_lock);
+
+  /* sleep a bit to give the task a chance to hit error paths */
+  g_timeout_add_full (50,
+                      G_PRIORITY_DEFAULT,
+                      idle_main_loop_quit,
+                      g_steal_pointer (&main_loop),
+                      (GDestroyNotify)g_main_loop_unref);
+}
+
+static void
+test_ide_task_return_on_cancel (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+  g_autoptr(IdeTask) task = ide_task_new (NULL, cancellable,
+                                          test_ide_task_return_on_cancel_cb,
+                                          g_main_loop_ref (main_loop));
+
+  G_LOCK (cancel_lock);
+
+  ide_task_set_return_on_cancel (task, TRUE);
+  ide_task_run_in_thread (task, test_ide_task_return_on_cancel_worker);
+
+  g_cancellable_cancel (cancellable);
+  g_main_loop_run (main_loop);
+}
+
+static void
+test_ide_task_report_new_error_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  g_autoptr(GMainLoop) main_loop = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert_null (object);
+  g_assert (IDE_IS_TASK (result));
+
+  g_assert_false (ide_task_propagate_boolean (IDE_TASK (result), &error));
+  g_assert_error (error, G_IO_ERROR, 1234);
+
+  g_main_loop_quit (main_loop);
+}
+
+static void
+test_ide_task_report_new_error (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+
+  ide_task_report_new_error (NULL,
+                             test_ide_task_report_new_error_cb,
+                             g_main_loop_ref (main_loop),
+                             test_ide_task_report_new_error,
+                             G_IO_ERROR,
+                             1234,
+                             "Failure message");
+  g_main_loop_run (main_loop);
+}
+
+static void
+test_ide_task_run_in_thread_sync_cb (IdeTask      *task,
+                                     gpointer      source_object,
+                                     gpointer      task_data,
+                                     GCancellable *cancellable)
+{
+  g_assert (IDE_IS_TASK (task));
+  g_assert_null (source_object);
+  g_assert_null (task_data);
+  g_assert_null (cancellable);
+
+  ide_task_return_int (task, 774);
+}
+
+static void
+test_ide_task_run_in_thread_sync (void)
+{
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+
+  ide_task_run_in_thread_sync (task, test_ide_task_run_in_thread_sync_cb);
+  g_assert_cmpint (774, ==, ide_task_propagate_int (task, &error));
+  g_assert_no_error (error);
+}
+
+gint
+main (gint   argc,
+      gchar *argv[])
+{
+  _ide_thread_pool_init (FALSE);
+
+  g_test_init (&argc, &argv, NULL);
+
+  g_test_add_func ("/Ide/Task/typical", test_ide_task_typical);
+  g_test_add_func ("/Ide/Task/basic", test_ide_task_basic);
+  g_test_add_func ("/Ide/Task/get-cancellable", test_ide_task_get_cancellable);
+  g_test_add_func ("/Ide/Task/is-valid", test_ide_task_is_valid);
+  g_test_add_func ("/Ide/Task/source-object", test_ide_task_source_object);
+  g_test_add_func ("/Ide/Task/chain", test_ide_task_chain);
+  g_test_add_func ("/Ide/Task/delayed-chain", test_ide_task_delayed_chain);
+  g_test_add_func ("/Ide/Task/delayed-chain-fail", test_ide_task_delayed_chain_fail);
+  g_test_add_func ("/Ide/Task/no-release", test_ide_task_no_release);
+  g_test_add_func ("/Ide/Task/serial", test_ide_task_serial);
+  g_test_add_func ("/Ide/Task/null-object", test_ide_task_null_object);
+  g_test_add_func ("/Ide/Task/boxed", test_ide_task_boxed);
+  g_test_add_func ("/Ide/Task/error", test_ide_task_error);
+  g_test_add_func ("/Ide/Task/thread", test_ide_task_thread);
+  g_test_add_func ("/Ide/Task/thread-chained", test_ide_task_thread_chained);
+  g_test_add_func ("/Ide/Task/completed", test_ide_task_completed);
+  g_test_add_func ("/Ide/Task/completed-threaded", test_ide_task_completed_threaded);
+  g_test_add_func ("/Ide/Task/task-data", test_ide_task_task_data);
+  g_test_add_func ("/Ide/Task/task-data-threaded", test_ide_task_task_data_threaded);
+  g_test_add_func ("/Ide/Task/task-data-set-in-thread", test_ide_task_task_data_set_in_thread);
+  g_test_add_func ("/Ide/Task/get-source-object", test_ide_task_get_source_object);
+  g_test_add_func ("/Ide/Task/check-cancellable", test_ide_task_check_cancellable);
+  g_test_add_func ("/Ide/Task/return-on-cancel", test_ide_task_return_on_cancel);
+  g_test_add_func ("/Ide/Task/report-new-error", test_ide_task_report_new_error);
+  g_test_add_func ("/Ide/Task/run-in-thread-sync", test_ide_task_run_in_thread_sync);
+
+  return g_test_run ();
+}



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