/* ide-workbench.c
 *
 * Copyright 2014-2019 Christian Hergert <chergert@redhat.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#define G_LOG_DOMAIN "ide-workbench"

#include "config.h"

#include <glib/gi18n.h>
#include <libide-debugger.h>
#include <libide-threading.h>
#include <libpeas/peas.h>

#include "ide-build-private.h"
#include "ide-context-private.h"
#include "ide-foundry-init.h"
#include "ide-thread-private.h"
#include "ide-transfer-manager-private.h"

#include "ide-application.h"
#include "ide-command-manager.h"
#include "ide-gui-global.h"
#include "ide-gui-private.h"
#include "ide-primary-workspace.h"
#include "ide-workbench.h"
#include "ide-workbench-addin.h"
#include "ide-workspace.h"

/**
 * SECTION:ide-workbench
 * @title: IdeWorkbench
 * @short_description: window group for all windows within a project
 *
 * The #IdeWorkbench is a #GtkWindowGroup containing the #IdeContext (root
 * data-structure for a project) and all of the windows associated with the
 * project.
 *
 * Usually, windows within the #IdeWorkbench are an #IdeWorkspace. They can
 * react to changes in the #IdeContext or its descendants to represent the
 * project and it's state.
 *
 * Since: 3.32
 */

struct _IdeWorkbench
{
  GtkWindowGroup    parent_instance;

  /* MRU of workspaces, link embedded in workspace */
  GQueue            mru_queue;

  /* Owned references */
  PeasExtensionSet *addins;
  GCancellable     *cancellable;
  IdeContext       *context;
  IdeBuildSystem   *build_system;
  IdeProjectInfo   *project_info;
  IdeVcs           *vcs;
  IdeVcsMonitor    *vcs_monitor;
  IdeSearchEngine  *search_engine;

  /* Various flags */
  guint             unloaded : 1;
};

typedef struct
{
  GPtrArray          *addins;
  IdeWorkbenchAddin  *preferred;
  GFile              *file;
  gchar              *hint;
  gchar              *content_type;
  IdeBufferOpenFlags  flags;
  gint                at_line;
  gint                at_line_offset;
} Open;

typedef struct
{
  IdeProjectInfo *project_info;
  GPtrArray      *addins;
  GType           workspace_type;
  gint64          present_time;
} LoadProject;

typedef struct
{
  GPtrArray *roots;
  gchar     *path;
} ResolveFile;

enum {
  PROP_0,
  PROP_CONTEXT,
  PROP_VCS,
  N_PROPS
};

static void ide_workbench_action_close       (IdeWorkbench *self,
                                              GVariant     *param);
static void ide_workbench_action_open        (IdeWorkbench *self,
                                              GVariant     *param);
static void ide_workbench_action_dump_tasks  (IdeWorkbench *self,
                                              GVariant     *param);
static void ide_workbench_action_object_tree (IdeWorkbench *self,
                                              GVariant     *param);
static void ide_workbench_action_inspector   (IdeWorkbench *self,
                                              GVariant     *param);
static void ide_workbench_action_reload_all  (IdeWorkbench *self,
                                              GVariant     *param);


DZL_DEFINE_ACTION_GROUP (IdeWorkbench, ide_workbench, {
  { "close", ide_workbench_action_close },
  { "open", ide_workbench_action_open },
  { "reload-files", ide_workbench_action_reload_all },
  { "-inspector", ide_workbench_action_inspector },
  { "-object-tree", ide_workbench_action_object_tree },
  { "-dump-tasks", ide_workbench_action_dump_tasks },
})

G_DEFINE_FINAL_TYPE_WITH_CODE (IdeWorkbench, ide_workbench, GTK_TYPE_WINDOW_GROUP,
                         G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP,
                                                ide_workbench_init_action_group))

static GParamSpec *properties [N_PROPS];

static void
load_project_free (LoadProject *lp)
{
  g_clear_object (&lp->project_info);
  g_clear_pointer (&lp->addins, g_ptr_array_unref);
  g_slice_free (LoadProject, lp);
}

static void
open_free (Open *o)
{
  g_clear_pointer (&o->addins, g_ptr_array_unref);
  g_clear_object (&o->preferred);
  g_clear_object (&o->file);
  g_clear_pointer (&o->hint, g_free);
  g_clear_pointer (&o->content_type, g_free);
  g_slice_free (Open, o);
}

static void
resolve_file_free (ResolveFile *rf)
{
  g_clear_pointer (&rf->roots, g_ptr_array_unref);
  g_clear_pointer (&rf->path, g_free);
  g_slice_free (ResolveFile, rf);
}

static gboolean
ignore_error (GError *error)
{
  return g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) ||
         g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED);
}

/**
 * ide_workbench_from_context:
 * @context: an #IdeContext
 *
 * Helper to get the #IdeWorkbench for a given context.
 *
 * Returns: (transfer none) (nullable): an #IdeWorkbench or %NULL
 *
 * Since: 3.40
 */
IdeWorkbench *
ide_workbench_from_context (IdeContext *context)
{
  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);

  return IDE_WORKBENCH (g_object_get_data (G_OBJECT (context), "WORKBENCH"));
}

static void
ide_workbench_set_context (IdeWorkbench *self,
                           IdeContext   *context)
{
  g_autoptr(IdeContext) new_context = NULL;
  g_autoptr(IdeBufferManager) bufmgr = NULL;
  IdeBuildSystem *build_system;

  g_return_if_fail (IDE_IS_WORKBENCH (self));
  g_return_if_fail (!context || IDE_IS_CONTEXT (context));

  if (context == NULL)
    context = new_context = ide_context_new ();

  /* backpointer for the workbench */
  g_object_set_data (G_OBJECT (context), "WORKBENCH", self);

  g_set_object (&self->context, context);

  /* Make sure we have access to buffer manager early */
  bufmgr = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_BUFFER_MANAGER);

  /* And use a fallback build system if one is not already available */
  if ((build_system = ide_context_peek_child_typed (context, IDE_TYPE_BUILD_SYSTEM)))
    self->build_system = g_object_ref (build_system);
  else
    self->build_system = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_FALLBACK_BUILD_SYSTEM);
}

static void
ide_workbench_addin_added_workspace_cb (IdeWorkspace      *workspace,
                                        IdeWorkbenchAddin *addin)
{
  g_assert (IDE_IS_WORKSPACE (workspace));
  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));

  ide_workbench_addin_workspace_added (addin, workspace);
}

static void
ide_workbench_addin_removed_workspace_cb (IdeWorkspace      *workspace,
                                          IdeWorkbenchAddin *addin)
{
  g_assert (IDE_IS_WORKSPACE (workspace));
  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));

  ide_workbench_addin_workspace_removed (addin, workspace);
}

static void
ide_workbench_addin_added_cb (PeasExtensionSet *set,
                              PeasPluginInfo   *plugin_info,
                              PeasExtension    *exten,
                              gpointer          user_data)
{
  IdeWorkbench *self = user_data;
  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten;

  g_assert (PEAS_IS_EXTENSION_SET (set));
  g_assert (plugin_info != NULL);
  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
  g_assert (IDE_IS_WORKBENCH (self));

  ide_workbench_addin_load (addin, self);

  /* Notify of the VCS system up-front */
  if (self->vcs != NULL)
    ide_workbench_addin_vcs_changed (addin, self->vcs);

  /*
   * If we already loaded a project, then give the plugin a
   * chance to handle that, even if it is delayed a bit.
   */

  if (self->project_info != NULL)
    ide_workbench_addin_load_project_async (addin, self->project_info, NULL, NULL, NULL);

  ide_workbench_foreach_workspace (self,
                                   (GtkCallback)ide_workbench_addin_added_workspace_cb,
                                   addin);
}

static void
ide_workbench_addin_removed_cb (PeasExtensionSet *set,
                                PeasPluginInfo   *plugin_info,
                                PeasExtension    *exten,
                                gpointer          user_data)
{
  IdeWorkbench *self = user_data;
  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten;

  g_assert (PEAS_IS_EXTENSION_SET (set));
  g_assert (plugin_info != NULL);
  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
  g_assert (IDE_IS_WORKBENCH (self));

  /* Notify of workspace removals so addins don't need to manually
   * track them for cleanup.
   */
  ide_workbench_foreach_workspace (self,
                                   (GtkCallback)ide_workbench_addin_removed_workspace_cb,
                                   addin);

  ide_workbench_addin_unload (addin, self);
}

static void
ide_workbench_notify_context_title (IdeWorkbench *self,
                                    GParamSpec   *pspec,
                                    IdeContext   *context)
{
  g_autofree gchar *formatted = NULL;
  g_autofree gchar *title = NULL;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_WORKBENCH (self));
  g_assert (IDE_IS_CONTEXT (context));

  title = ide_context_dup_title (context);
  formatted = g_strdup_printf (_("Builder — %s"), title);
  ide_workbench_foreach_workspace (self,
                                   (GtkCallback)gtk_window_set_title,
                                   formatted);
}

static void
ide_workbench_notify_context_workdir (IdeWorkbench *self,
                                      GParamSpec   *pspec,
                                      IdeContext   *context)
{
  g_autoptr(GFile) workdir = NULL;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_WORKBENCH (self));
  g_assert (IDE_IS_CONTEXT (context));

  workdir = ide_context_ref_workdir (context);
  ide_vcs_monitor_set_root (self->vcs_monitor, workdir);
}

static void
ide_workbench_constructed (GObject *object)
{
  IdeWorkbench *self = (IdeWorkbench *)object;

  g_assert (IDE_IS_WORKBENCH (self));

  if (self->context == NULL)
    self->context = ide_context_new ();

  g_signal_connect_object (self->context,
                           "notify::title",
                           G_CALLBACK (ide_workbench_notify_context_title),
                           self,
                           G_CONNECT_SWAPPED);

  g_signal_connect_object (self->context,
                           "notify::workdir",
                           G_CALLBACK (ide_workbench_notify_context_workdir),
                           self,
                           G_CONNECT_SWAPPED);

  G_OBJECT_CLASS (ide_workbench_parent_class)->constructed (object);

  self->vcs_monitor = g_object_new (IDE_TYPE_VCS_MONITOR,
                                    "parent", self->context,
                                    NULL);

  self->addins = peas_extension_set_new (peas_engine_get_default (),
                                         IDE_TYPE_WORKBENCH_ADDIN,
                                         NULL);

  g_signal_connect (self->addins,
                    "extension-added",
                    G_CALLBACK (ide_workbench_addin_added_cb),
                    self);

  g_signal_connect (self->addins,
                    "extension-removed",
                    G_CALLBACK (ide_workbench_addin_removed_cb),
                    self);

  peas_extension_set_foreach (self->addins,
                              ide_workbench_addin_added_cb,
                              self);

  /* Load command providers (which may register shortcuts) */
  (void)ide_command_manager_from_context (self->context);
}

static void
ide_workbench_finalize (GObject *object)
{
  IdeWorkbench *self = (IdeWorkbench *)object;

  g_assert (IDE_IS_MAIN_THREAD ());

  if (self->context != NULL)
    g_object_set_data (G_OBJECT (self->context), "WORKBENCH", NULL);

  g_clear_object (&self->build_system);
  g_clear_object (&self->vcs);
  g_clear_object (&self->search_engine);
  g_clear_object (&self->project_info);
  g_clear_object (&self->cancellable);
  g_clear_object (&self->context);

  G_OBJECT_CLASS (ide_workbench_parent_class)->finalize (object);
}

static void
ide_workbench_get_property (GObject    *object,
                            guint       prop_id,
                            GValue     *value,
                            GParamSpec *pspec)
{
  IdeWorkbench *self = IDE_WORKBENCH (object);

  g_assert (IDE_IS_MAIN_THREAD ());

  switch (prop_id)
    {
    case PROP_CONTEXT:
      g_value_set_object (value, ide_workbench_get_context (self));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
ide_workbench_set_property (GObject      *object,
                            guint         prop_id,
                            const GValue *value,
                            GParamSpec   *pspec)
{
  IdeWorkbench *self = IDE_WORKBENCH (object);

  g_assert (IDE_IS_MAIN_THREAD ());

  switch (prop_id)
    {
    case PROP_CONTEXT:
      ide_workbench_set_context (self, g_value_get_object (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
ide_workbench_class_init (IdeWorkbenchClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->constructed = ide_workbench_constructed;
  object_class->finalize = ide_workbench_finalize;
  object_class->get_property = ide_workbench_get_property;
  object_class->set_property = ide_workbench_set_property;

  /**
   * IdeWorkbench:context:
   *
   * The "context" property is the #IdeContext for the project.
   *
   * The #IdeContext is the root #IdeObject used in the tree of
   * objects representing the project and the workings of the IDE.
   *
   * Since: 3.32
   */
  properties [PROP_CONTEXT] =
    g_param_spec_object ("context",
                         "Context",
                         "The IdeContext for the workbench",
                         IDE_TYPE_CONTEXT,
                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  /**
   * IdeWorkbench:vcs:
   *
   * The "vcs" property contains an #IdeVcs that represents the version control
   * system that is currently loaded for the project.
   *
   * The #IdeVcs is registered by an #IdeWorkbenchAddin when loading a project.
   *
   * Since: 3.32
   */
  properties [PROP_VCS] =
    g_param_spec_object ("vcs",
                         "Vcs",
                         "The version control system, if any",
                         IDE_TYPE_VCS,
                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_properties (object_class, N_PROPS, properties);
}

static void
ide_workbench_init (IdeWorkbench *self)
{
}

static void
collect_addins_cb (PeasExtensionSet *set,
                   PeasPluginInfo   *plugin_info,
                   PeasExtension    *exten,
                   gpointer          user_data)
{
  GPtrArray *ar = user_data;
  g_ptr_array_add (ar, g_object_ref (exten));
}

static GPtrArray *
ide_workbench_collect_addins (IdeWorkbench *self)
{
  g_autoptr(GPtrArray) ar = NULL;

  g_assert (IDE_IS_WORKBENCH (self));

  ar = g_ptr_array_new_with_free_func (g_object_unref);
  if (self->addins != NULL)
    peas_extension_set_foreach (self->addins, collect_addins_cb, ar);
  return g_steal_pointer (&ar);
}

static IdeWorkbenchAddin *
ide_workbench_find_addin (IdeWorkbench *self,
                          const gchar  *hint)
{
  PeasEngine *engine;
  PeasPluginInfo *plugin_info;
  PeasExtension *exten = NULL;

  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
  g_return_val_if_fail (hint != NULL, NULL);

  engine = peas_engine_get_default ();

  if ((plugin_info = peas_engine_get_plugin_info (engine, hint)))
    exten = peas_extension_set_get_extension (self->addins, plugin_info);

  return exten ? g_object_ref (IDE_WORKBENCH_ADDIN (exten)) : NULL;
}

/**
 * ide_workbench_new:
 *
 * Creates a new #IdeWorkbench.
 *
 * This does not create any windows, you'll need to request that a workspace
 * be created based on the kind of workspace you want to display to the user.
 *
 * Returns: an #IdeWorkbench
 *
 * Since: 3.32
 */
IdeWorkbench *
ide_workbench_new (void)
{
  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);

  return g_object_new (IDE_TYPE_WORKBENCH, NULL);
}

/**
 * ide_workbench_new_for_context:
 *
 * Creates a new #IdeWorkbench using @context for the #IdeWorkbench:context.
 *
 * Returns: (transfer full): an #IdeWorkbench
 *
 * Since: 3.32
 */
IdeWorkbench *
ide_workbench_new_for_context (IdeContext *context)
{
  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);

  return g_object_new (IDE_TYPE_CONTEXT,
                       "visible", TRUE,
                       NULL);
}

/**
 * ide_workbench_get_context:
 * @self: an #IdeWorkbench
 *
 * Gets the #IdeContext for the workbench.
 *
 * Returns: (transfer none): an #IdeContext
 *
 * Since: 3.32
 */
IdeContext *
ide_workbench_get_context (IdeWorkbench *self)
{
  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);

  return self->context;
}

/**
 * ide_workbench_from_widget:
 * @widget: a #GtkWidget
 *
 * Finds the #IdeWorkbench associated with a widget.
 *
 * Returns: (nullable) (transfer none): an #IdeWorkbench or %NULL
 *
 * Since: 3.32
 */
IdeWorkbench *
ide_workbench_from_widget (GtkWidget *widget)
{
  GtkWindowGroup *group;
  GtkWidget *toplevel;

  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
  g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);

  /*
   * The workbench is a window group, and the workspaces belong to us. So we
   * just need to get the toplevel window group property, and cast.
   */

  if ((toplevel = gtk_widget_get_toplevel (widget)) &&
      GTK_IS_WINDOW (toplevel) &&
      (group = gtk_window_get_group (GTK_WINDOW (toplevel))) &&
      IDE_IS_WORKBENCH (group))
    return IDE_WORKBENCH (group);

  return NULL;
}

/**
 * ide_workbench_foreach_workspace:
 * @self: an #IdeWorkbench
 * @callback: (scope call): a #GtkCallback to call for each #IdeWorkspace
 * @user_data: user data for @callback
 *
 * Iterates the available workspaces in the workbench. Workspaces are iterated
 * in most-recently-used order.
 *
 * Since: 3.32
 */
void
ide_workbench_foreach_workspace (IdeWorkbench *self,
                                 GtkCallback   callback,
                                 gpointer      user_data)
{
  GList *copy;

  g_return_if_fail (IDE_IS_MAIN_THREAD ());
  g_return_if_fail (IDE_IS_WORKBENCH (self));
  g_return_if_fail (callback != NULL);

  /* Copy for re-entrancy safety */
  copy = g_list_copy (self->mru_queue.head);

  for (const GList *iter = copy; iter; iter = iter->next)
    {
      IdeWorkspace *workspace = iter->data;
      g_assert (IDE_IS_WORKSPACE (workspace));
      callback (GTK_WIDGET (workspace), user_data);
    }

  g_list_free (copy);
}

/**
 * ide_workbench_foreach_page:
 * @self: a #IdeWorkbench
 * @callback: (scope call): a callback to execute for each page
 * @user_data: closure data for @callback
 *
 * Calls @callback for every page loaded in the workbench, by iterating
 * workspaces in order of most-recently-used.
 *
 * Since: 3.32
 */
void
ide_workbench_foreach_page (IdeWorkbench *self,
                            GtkCallback   callback,
                            gpointer      user_data)
{
  GList *copy;

  g_return_if_fail (IDE_IS_WORKBENCH (self));
  g_return_if_fail (callback != NULL);

  /* Make a copy to be safe against auto-cleanup removals */
  copy = g_list_copy (self->mru_queue.head);
  for (const GList *iter = copy; iter; iter = iter->next)
    {
      IdeWorkspace *workspace = iter->data;
      g_assert (IDE_IS_WORKSPACE (workspace));
      ide_workspace_foreach_page (workspace, callback, user_data);
    }
  g_list_free (copy);
}

static void
ide_workbench_workspace_has_toplevel_focus_cb (IdeWorkbench *self,
                                               GParamSpec   *pspec,
                                               IdeWorkspace *workspace)
{
  g_assert (IDE_IS_WORKBENCH (self));
  g_assert (IDE_IS_WORKSPACE (workspace));
  g_assert (gtk_window_get_group (GTK_WINDOW (workspace)) == GTK_WINDOW_GROUP (self));

  if (gtk_window_has_toplevel_focus (GTK_WINDOW (workspace)))
    {
      GList *mru_link = _ide_workspace_get_mru_link (workspace);

      g_queue_unlink (&self->mru_queue, mru_link);

      g_assert (mru_link->prev == NULL);
      g_assert (mru_link->next == NULL);
      g_assert (mru_link->data == (gpointer)workspace);

      g_queue_push_head_link (&self->mru_queue, mru_link);
    }
}

static void
insert_action_groups_foreach_cb (IdeWorkspace *workspace,
                                 gpointer      user_data)
{
  IdeWorkbench *self = user_data;
  struct {
    const gchar *name;
    GType        child_type;
  } groups[] = {
    { "config-manager", IDE_TYPE_CONFIG_MANAGER },
    { "build-manager", IDE_TYPE_BUILD_MANAGER },
    { "device-manager", IDE_TYPE_DEVICE_MANAGER },
    { "run-manager", IDE_TYPE_RUN_MANAGER },
    { "test-manager", IDE_TYPE_TEST_MANAGER },
  };

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_WORKBENCH (self));
  g_assert (IDE_IS_WORKSPACE (workspace));

  for (guint i = 0; i < G_N_ELEMENTS (groups); i++)
    {
      IdeObject *child;

      if ((child = ide_context_peek_child_typed (self->context, groups[i].child_type)))
        gtk_widget_insert_action_group (GTK_WIDGET (workspace),
                                        groups[i].name,
                                        G_ACTION_GROUP (child));
    }
}

/**
 * ide_workbench_add_workspace:
 * @self: an #IdeWorkbench
 * @workspace: an #IdeWorkspace
 *
 * Adds @workspace to @workbench.
 *
 * Since: 3.32
 */
void
ide_workbench_add_workspace (IdeWorkbench *self,
                             IdeWorkspace *workspace)
{
  g_autoptr(GPtrArray) addins = NULL;
  IdeCommandManager *command_manager;
  GList *mru_link;

  g_return_if_fail (IDE_IS_MAIN_THREAD ());
  g_return_if_fail (IDE_IS_WORKBENCH (self));
  g_return_if_fail (IDE_IS_WORKSPACE (workspace));

  /* Now add the window to the workspace (which takes no reference, as the
   * window will take a reference back to us.
   */
  if (gtk_window_get_group (GTK_WINDOW (workspace)) != GTK_WINDOW_GROUP (self))
    gtk_window_group_add_window (GTK_WINDOW_GROUP (self), GTK_WINDOW (workspace));

  g_assert (gtk_window_has_group (GTK_WINDOW (workspace)));
  g_assert (gtk_window_get_group (GTK_WINDOW (workspace)) == GTK_WINDOW_GROUP (self));

  /* Now place the workspace into our MRU tracking */
  mru_link = _ide_workspace_get_mru_link (workspace);

  /* New workspaces are expected to be displayed right away, so we can
   * just push the window onto the head.
   */
  g_queue_push_head_link (&self->mru_queue, mru_link);

  /* Update the context for the workspace, even if we're not loaded,
   * this IdeContext will be updated later.
   */
  _ide_workspace_set_context (workspace, self->context);

  /* This causes the workspace to get an additional reference to the group
   * (which already happens from GtkWindow:group), but IdeWorkspace will
   * remove itself in IdeWorkspace.destroy.
   */
  gtk_widget_insert_action_group (GTK_WIDGET (workspace),
                                  "workbench",
                                  G_ACTION_GROUP (self));

  /* Give the workspace access to all the action groups of the context that
   * might be useful for them to access (debug-manager, run-manager, etc).
   */
  if (self->project_info != NULL)
    insert_action_groups_foreach_cb (workspace, self);

  /* Track toplevel focus changes to maintain a most-recently-used queue. */
  g_signal_connect_object (workspace,
                           "notify::has-toplevel-focus",
                           G_CALLBACK (ide_workbench_workspace_has_toplevel_focus_cb),
                           self,
                           G_CONNECT_SWAPPED);

  /* Give access to transfer-manager */
  gtk_widget_insert_action_group (GTK_WIDGET (workspace),
                                  "transfer-manager",
                                  _ide_transfer_manager_get_actions (NULL));

  /* Notify all the addins about the new workspace. */
  if ((addins = ide_workbench_collect_addins (self)))
    {
      for (guint i = 0; i < addins->len; i++)
        {
          IdeWorkbenchAddin *addin = g_ptr_array_index (addins, i);
          ide_workbench_addin_workspace_added (addin, workspace);
        }
    }

  if (!gtk_window_get_title (GTK_WINDOW (workspace)))
    {
      g_autofree gchar *title = NULL;
      g_autofree gchar *formatted = NULL;

      title = ide_context_dup_title (self->context);
      formatted = g_strdup_printf (_("Builder — %s"), title);
      gtk_window_set_title (GTK_WINDOW (workspace), formatted);
    }

  /* Load shortcuts for commands */
  command_manager = ide_command_manager_from_context (self->context);
  _ide_command_manager_init_shortcuts (command_manager, workspace);
}

/**
 * ide_workbench_remove_workspace:
 * @self: an #IdeWorkbench
 * @workspace: an #IdeWorkspace
 *
 * Removes @workspace from @workbench.
 *
 * Since: 3.32
 */
void
ide_workbench_remove_workspace (IdeWorkbench *self,
                                IdeWorkspace *workspace)
{
  g_autoptr(GPtrArray) addins = NULL;
  IdeCommandManager *command_manager;
  GList *list;
  GList *mru_link;
  guint count = 0;

  g_return_if_fail (IDE_IS_MAIN_THREAD ());
  g_return_if_fail (IDE_IS_WORKBENCH (self));
  g_return_if_fail (IDE_IS_WORKSPACE (workspace));

  /* Stop tracking MRU changes */
  mru_link = _ide_workspace_get_mru_link (workspace);
  g_queue_unlink (&self->mru_queue, mru_link);
  g_signal_handlers_disconnect_by_func (workspace,
                                        G_CALLBACK (ide_workbench_workspace_has_toplevel_focus_cb),
                                        self);

  /* Remove any shortcuts that were registered by command providers */
  command_manager = ide_command_manager_from_context (self->context);
  _ide_command_manager_unload_shortcuts (command_manager, workspace);

  /* Notify all the addins about losing the workspace. */
  if ((addins = ide_workbench_collect_addins (self)))
    {
      for (guint i = 0; i < addins->len; i++)
        {
          IdeWorkbenchAddin *addin = g_ptr_array_index (addins, i);
          ide_workbench_addin_workspace_removed (addin, workspace);
        }
    }

  /* Clear our action group (which drops an additional back-reference) */
  gtk_widget_insert_action_group (GTK_WIDGET (workspace), "workbench", NULL);

  /* Only cleanup the group if it hasn't already been removed */
  if (gtk_window_has_group (GTK_WINDOW (workspace)))
    gtk_window_group_remove_window (GTK_WINDOW_GROUP (self), GTK_WINDOW (workspace));

  /*
   * If this is our last workspace being closed, then we want to
   * try to cleanup the workbench and shut things down.
   */

  list = gtk_window_group_list_windows (GTK_WINDOW_GROUP (self));
  for (const GList *iter = list; iter; iter = iter->next)
    {
      GtkWindow *window = iter->data;

      if (IDE_IS_WORKSPACE (window) && workspace != IDE_WORKSPACE (window))
        count++;
    }
  g_list_free (list);

  /*
   * If there are no more workspaces left, then we will want to also
   * unload the workbench opportunistically, so that the application
   * can exit cleanly.
   */
  if (count == 0 && self->unloaded == FALSE)
    ide_workbench_unload_async (self, NULL, NULL, NULL);
}

/**
 * ide_workbench_focus_workspace:
 * @self: an #IdeWorkbench
 * @workspace: an #IdeWorkspace
 *
 * Requests that @workspace be raised in the windows of @self, and
 * displayed to the user.
 *
 * Since: 3.32
 */
void
ide_workbench_focus_workspace (IdeWorkbench *self,
                               IdeWorkspace *workspace)
{
  g_return_if_fail (IDE_IS_MAIN_THREAD ());
  g_return_if_fail (IDE_IS_WORKBENCH (self));
  g_return_if_fail (IDE_IS_WORKSPACE (workspace));

  ide_gtk_window_present (GTK_WINDOW (workspace));
}

static void
ide_workbench_project_loaded_foreach_cb (PeasExtensionSet *set,
                                         PeasPluginInfo   *plugin_info,
                                         PeasExtension    *exten,
                                         gpointer          user_data)
{
  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten;
  IdeWorkbench *self = user_data;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (PEAS_IS_EXTENSION_SET (set));
  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
  g_assert (IDE_IS_WORKBENCH (self));
  g_assert (IDE_IS_PROJECT_INFO (self->project_info));

  ide_workbench_addin_project_loaded (addin, self->project_info);
}

static void
ide_workbench_load_project_completed (IdeWorkbench *self,
                                      IdeTask      *task)
{
  IdeBuildManager *build_manager;
  LoadProject *lp;

  g_assert (IDE_IS_WORKBENCH (self));
  g_assert (IDE_IS_TASK (task));

  lp = ide_task_get_task_data (task);

  g_assert (lp != NULL);
  g_assert (lp->addins != NULL);
  g_assert (lp->addins->len == 0);

  /* If we did not get a VCS as part of the loading process, set the
   * fallback VCS implementation.
   */
  if (self->vcs == NULL)
    {
      g_autoptr(GFile) workdir = ide_context_ref_workdir (self->context);
      g_autoptr(IdeDirectoryVcs) vcs = ide_directory_vcs_new (workdir);

      ide_workbench_set_vcs (self, IDE_VCS (vcs));
    }

  /* Create the search engine up-front */
  if (self->search_engine == NULL)
    self->search_engine = ide_object_ensure_child_typed (IDE_OBJECT (self->context),
                                                         IDE_TYPE_SEARCH_ENGINE);

  if (lp->workspace_type != G_TYPE_INVALID)
    {
      IdeWorkspace *workspace;

      workspace = g_object_new (lp->workspace_type,
                                "application", IDE_APPLICATION_DEFAULT,
                                NULL);
      ide_workbench_add_workspace (self, IDE_WORKSPACE (workspace));
      gtk_window_present_with_time (GTK_WINDOW (workspace), lp->present_time);
    }

  /* Give workspaces access to the various GActionGroups */
  ide_workbench_foreach_workspace (self,
                                   (GtkCallback)insert_action_groups_foreach_cb,
                                   self);

  /* Notify addins that projects have loaded */
  peas_extension_set_foreach (self->addins,
                              ide_workbench_project_loaded_foreach_cb,
                              self);

  /* Now that we have a workspace window for the project, we can allow
   * the build manager to start.
   */
  build_manager = ide_build_manager_from_context (self->context);
  _ide_build_manager_start (build_manager);

  ide_task_return_boolean (task, TRUE);
}

static void
ide_workbench_load_project_cb (GObject      *object,
                               GAsyncResult *result,
                               gpointer      user_data)
{
  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)object;
  g_autoptr(IdeTask) task = user_data;
  g_autoptr(GError) error = NULL;
  IdeWorkbench *self;
  LoadProject *lp;

  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (IDE_IS_TASK (task));

  self = ide_task_get_source_object (task);
  lp = ide_task_get_task_data (task);

  g_assert (IDE_IS_WORKBENCH (self));
  g_assert (lp != NULL);
  g_assert (IDE_IS_PROJECT_INFO (lp->project_info));
  g_assert (lp->addins != NULL);
  g_assert (lp->addins->len > 0);

  if (!ide_workbench_addin_load_project_finish (addin, result, &error))
    {
      if (!ignore_error (error))
        g_warning ("%s addin failed to load project: %s",
                   G_OBJECT_TYPE_NAME (addin), error->message);
    }

  g_ptr_array_remove (lp->addins, addin);

  if (lp->addins->len == 0)
    ide_workbench_load_project_completed (self, task);
}

static void
ide_workbench_init_foundry_cb (GObject      *object,
                               GAsyncResult *result,
                               gpointer      user_data)
{
  g_autoptr(IdeTask) task = user_data;
  g_autoptr(GError) error = NULL;
  IdeWorkbench *self;
  GCancellable *cancellable;
  LoadProject *lp;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (IDE_IS_TASK (task));

  if (!_ide_foundry_init_finish (result, &error))
    g_critical ("Failed to initialize foundry: %s", error->message);

  cancellable = ide_task_get_cancellable (task);
  self = ide_task_get_source_object (task);
  lp = ide_task_get_task_data (task);

  g_assert (IDE_IS_WORKBENCH (self));
  g_assert (lp != NULL);
  g_assert (lp->addins != NULL);
  g_assert (IDE_IS_PROJECT_INFO (lp->project_info));

  /* Now, we need to notify all of the workbench addins that we're
   * opening the project. Once they have all completed, we'll create the
   * new workspace window and attach it. That saves us the work of
   * rendering various frames of the during the intensive load process.
   */


  for (guint i = 0; i < lp->addins->len; i++)
    {
      IdeWorkbenchAddin *addin = g_ptr_array_index (lp->addins, i);

      ide_workbench_addin_load_project_async (addin,
                                              lp->project_info,
                                              cancellable,
                                              ide_workbench_load_project_cb,
                                              g_object_ref (task));
    }

  if (lp->addins->len == 0)
    ide_workbench_load_project_completed (self, task);
}

/**
 * ide_workbench_load_project_async:
 * @self: a #IdeWorkbench
 * @project_info: an #IdeProjectInfo describing the project to open
 * @cancellable: (nullable): a #GCancellable or %NULL
 * @callback: (nullable): a #GAsyncReadyCallback to execute upon completion
 * @user_data: user data for @callback
 *
 * Requests that a project be opened in the workbench.
 *
 * @project_info should contain enough information to discover and load the
 * project. Depending on the various fields of the #IdeProjectInfo,
 * different plugins may become active as part of loading the project.
 *
 * Note that this may only be called once for an #IdeWorkbench. If you need
 * to open a second project, you need to create and register a second
 * workbench first, and then open using that secondary workbench.
 *
 * @callback should call ide_workbench_load_project_finish() to obtain the
 * result of the open request.
 *
 * Since: 3.32
 */
void
ide_workbench_load_project_async (IdeWorkbench        *self,
                                  IdeProjectInfo      *project_info,
                                  GType                workspace_type,
                                  GCancellable        *cancellable,
                                  GAsyncReadyCallback  callback,
                                  gpointer             user_data)
{
  g_autoptr(IdeTask) task = NULL;
  g_autoptr(GFile) parent = NULL;
  g_autofree gchar *name = NULL;
  const gchar *project_id;
  LoadProject *lp;
  GFile *directory;
  GFile *file;

  IDE_ENTRY;

  g_return_if_fail (IDE_IS_MAIN_THREAD ());
  g_return_if_fail (IDE_IS_WORKBENCH (self));
  g_return_if_fail (IDE_IS_PROJECT_INFO (project_info));
  g_return_if_fail (workspace_type != IDE_TYPE_WORKSPACE);
  g_return_if_fail (workspace_type == G_TYPE_INVALID ||
                    g_type_is_a (workspace_type, IDE_TYPE_WORKSPACE));
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
  g_return_if_fail (self->unloaded == FALSE);

  task = ide_task_new (self, cancellable, callback, user_data);
  ide_task_set_source_tag (task, ide_workbench_load_project_async);

  if (self->project_info != NULL)
    {
      ide_task_return_new_error (task,
                                 G_IO_ERROR,
                                 G_IO_ERROR_FAILED,
                                 "Cannot load project, a project is already loaded");
      IDE_EXIT;
    }

  _ide_context_set_has_project (self->context);

  g_set_object (&self->project_info, project_info);

  /* Update context project-id based on project-info */
  if ((project_id = ide_project_info_get_id (project_info)))
    {
      g_autofree gchar *generated = ide_create_project_id (project_id);
      ide_context_set_project_id (self->context, generated);
    }

  if (!ide_project_info_get_directory (project_info) &&
      !ide_project_info_get_file (project_info))
    {
      ide_task_return_new_error (task,
                                 G_IO_ERROR,
                                 G_IO_ERROR_NOT_FOUND,
                                 "No file or directory provided to load as project");
      IDE_EXIT;
    }

  /* Fallback to using directory as file if necessary */
  if (!(file = ide_project_info_get_file (project_info)))
    {
      file = ide_project_info_get_directory (project_info);
      g_assert (G_IS_FILE (file));

      ide_project_info_set_file (project_info, file);
    }

  /*
   * Track the directory root based on project info. If we didn't get a
   * directory set, then take the parent of the project file.
   */

  if ((directory = ide_project_info_get_directory (project_info)))
    {
      ide_context_set_workdir (self->context, directory);
    }
  else
    {
      if (g_file_query_file_type (file, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL) == G_FILE_TYPE_DIRECTORY)
        {
          ide_context_set_workdir (self->context, file);
          directory = file;
        }
      else
        {
          ide_context_set_workdir (self->context, (parent = g_file_get_parent (file)));
          directory = parent;
        }

      ide_project_info_set_directory (project_info, directory);
    }

  g_assert (G_IS_FILE (directory));

  name = g_file_get_basename (directory);
  ide_context_set_title (self->context, name);

  {
    GFile *pdir = ide_project_info_get_directory (project_info);
    GFile *pfile = ide_project_info_get_file (project_info);
    const gchar *pident = ide_project_info_get_id (project_info);
    const gchar *pname = ide_project_info_get_name (project_info);

    /* Log some information to help track down project loading issues. */
    g_debug ("Loading project");
    g_debug ("    id = %s", pname);
    g_debug ("  name = %s", pident);
    g_debug ("   dir = %s", g_file_peek_path (pdir));
    g_debug ("  file = %s", g_file_peek_path (pfile));
  }

  /* If there has not been a project name set, make the default matching
   * the directory name. A plugin may update the name with more information
   * based on .doap files, etc.
   */
  if (!ide_project_info_get_name (project_info))
    ide_project_info_set_name (project_info, name);

  /* Setup some information we're going to need later on when loading the
   * individual workbench addins (and then creating the workspace).
   */
  lp = g_slice_new0 (LoadProject);
  lp->project_info = g_object_ref (project_info);
  /* HACK: Workaround for lack of last event time */
  lp->present_time = g_get_monotonic_time () / 1000L;
  lp->addins = ide_workbench_collect_addins (self);
  lp->workspace_type = workspace_type;
  ide_task_set_task_data (task, lp, load_project_free);

  /*
   * Before we load any addins, we want to register the Foundry subsystems
   * such as the device manager, diagnostics engine, configurations, etc.
   * This makes sure that we have some basics setup before addins load.
   */
  _ide_foundry_init_async (self->context,
                           cancellable,
                           ide_workbench_init_foundry_cb,
                           g_steal_pointer (&task));

  IDE_EXIT;
}

/**
 * ide_workbench_load_project_finish:
 * @self: a #IdeWorkbench
 *
 * Completes an asynchronous request to open a project using
 * ide_workbench_load_project_async().
 *
 * Returns: %TRUE if the project was successfully opened; otherwise %FALSE
 *   and @error is set.
 *
 * Since: 3.32
 */
gboolean
ide_workbench_load_project_finish (IdeWorkbench  *self,
                                   GAsyncResult  *result,
                                   GError       **error)
{
  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
  g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);

  return ide_task_propagate_boolean (IDE_TASK (result), error);
}

static void
print_object_tree (IdeObject *object,
                   gpointer   depthptr)
{
  gint depth = GPOINTER_TO_INT (depthptr);
  g_autofree gchar *space = g_strnfill (depth * 2, ' ');
  g_autofree gchar *info = ide_object_repr (object);

  g_print ("%s%s\n", space, info);
  ide_object_foreach (object,
                      (GFunc)print_object_tree,
                      GINT_TO_POINTER (depth + 1));
}

static void
ide_workbench_action_object_tree (IdeWorkbench *self,
                                  GVariant     *param)
{
  g_assert (IDE_IS_WORKBENCH (self));

  print_object_tree (IDE_OBJECT (self->context), NULL);
}

static void
ide_workbench_action_dump_tasks (IdeWorkbench *self,
                                 GVariant     *param)
{
  g_assert (IDE_IS_WORKBENCH (self));

  _ide_dump_tasks ();
}

static void
ide_workbench_action_inspector (IdeWorkbench *self,
                                GVariant     *param)
{
  gtk_window_set_interactive_debugging (TRUE);
}

static void
ide_workbench_action_close_cb (GObject      *object,
                               GAsyncResult *result,
                               gpointer      user_data)
{
  IdeWorkbench *self = (IdeWorkbench *)object;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_WORKBENCH (self));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (user_data == NULL);

  if (ide_workbench_unload_finish (self, result, NULL))
    {
      IdeApplication *app = IDE_APPLICATION_DEFAULT;
      GtkWindow *active;

      if (!(active = gtk_application_get_active_window (GTK_APPLICATION (app))))
        g_application_activate (G_APPLICATION (app));
      else
        ide_gtk_window_present (active);
    }
}

static void
ide_workbench_action_close (IdeWorkbench *self,
                            GVariant     *param)
{
  g_assert (IDE_IS_WORKBENCH (self));
  g_assert (param == NULL);

  if (self->unloaded == FALSE)
    ide_workbench_unload_async (self,
                                NULL,
                                ide_workbench_action_close_cb,
                                NULL);
}

static void
ide_workbench_action_reload_all (IdeWorkbench *self,
                                 GVariant     *param)
{
  IdeBufferManager *bufmgr;
  IdeContext *context;

  g_assert (IDE_IS_WORKBENCH (self));
  g_assert (param == NULL);

  context = ide_workbench_get_context (self);
  bufmgr = ide_buffer_manager_from_context (context);
  ide_buffer_manager_reload_all_async (bufmgr, NULL, NULL, NULL);
}

static void
ide_workbench_action_open (IdeWorkbench *self,
                           GVariant     *param)
{
  GtkFileChooserNative *chooser;
  IdeWorkspace *workspace;
  gint ret;

  g_assert (IDE_IS_WORKBENCH (self));
  g_assert (param == NULL);

  workspace = ide_workbench_get_current_workspace (self);

  chooser = gtk_file_chooser_native_new (_("Open File…"),
                                         GTK_WINDOW (workspace),
                                         GTK_FILE_CHOOSER_ACTION_OPEN,
                                         _("_Open"),
                                         _("_Cancel"));
  gtk_native_dialog_set_modal (GTK_NATIVE_DIALOG (chooser), FALSE);
  gtk_file_chooser_set_local_only (GTK_FILE_CHOOSER (chooser), FALSE);
  gtk_file_chooser_set_select_multiple (GTK_FILE_CHOOSER (chooser), TRUE);

  ret = gtk_native_dialog_run (GTK_NATIVE_DIALOG (chooser));

  if (ret == GTK_RESPONSE_ACCEPT)
    {
      g_autoslist(GFile) files = gtk_file_chooser_get_files (GTK_FILE_CHOOSER (chooser));

      for (const GSList *iter = files; iter; iter = iter->next)
        {
          GFile *file = iter->data;

          g_assert (G_IS_FILE (file));

          ide_workbench_open_async (self, file, NULL, 0, NULL, NULL, NULL);
        }
    }

  gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (chooser));
}

/**
 * ide_workbench_get_search_engine:
 * @self: a #IdeWorkbench
 *
 * Gets the search engine for the workbench, if any.
 *
 * Returns: (transfer none): an #IdeSearchEngine
 *
 * Since: 3.32
 */
IdeSearchEngine *
ide_workbench_get_search_engine (IdeWorkbench *self)
{
  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
  g_return_val_if_fail (self->context != NULL, NULL);

  if (self->search_engine == NULL)
    self->search_engine = ide_object_ensure_child_typed (IDE_OBJECT (self->context),
                                                         IDE_TYPE_SEARCH_ENGINE);

  return self->search_engine;
}

/**
 * ide_workbench_get_project_info:
 * @self: a #IdeWorkbench
 *
 * Gets the #IdeProjectInfo for the workbench, if a project has been or is
 * currently, loading.
 *
 * Returns: (transfer none) (nullable): an #IdeProjectInfo or %NULL
 *
 * Since: 3.32
 */
IdeProjectInfo *
ide_workbench_get_project_info (IdeWorkbench *self)
{
  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);

  return self->project_info;
}

static void
ide_workbench_unload_foundry_cb (GObject      *object,
                                 GAsyncResult *result,
                                 gpointer      user_data)
{
  g_autoptr(IdeTask) task = user_data;
  g_autoptr(GError) error = NULL;
  IdeWorkbench *self;

  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (IDE_IS_TASK (task));

  self = ide_task_get_source_object (task);

  if (!_ide_foundry_unload_finish (result, &error))
    ide_task_return_error (task, g_steal_pointer (&error));
  else
    ide_task_return_boolean (task, TRUE);

  if (self->context != NULL)
    {
      ide_object_destroy (IDE_OBJECT (self->context));
      g_clear_object (&self->context);
    }
}

static void
ide_workbench_unload_project_completed (IdeWorkbench *self,
                                        IdeTask      *task)
{
  g_assert (IDE_IS_WORKBENCH (self));
  g_assert (IDE_IS_TASK (task));

  g_clear_object (&self->addins);
  ide_workbench_foreach_workspace (self, (GtkCallback)gtk_widget_destroy, NULL);

  _ide_foundry_unload_async (self->context,
                             ide_task_get_cancellable (task),
                             ide_workbench_unload_foundry_cb,
                             g_object_ref (task));
}
static void
ide_workbench_unload_project_cb (GObject      *object,
                                 GAsyncResult *result,
                                 gpointer      user_data)
{
  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)object;
  g_autoptr(IdeTask) task = user_data;
  g_autoptr(GError) error = NULL;
  IdeWorkbench *self;
  GPtrArray *addins;

  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (IDE_IS_TASK (task));

  self = ide_task_get_source_object (task);
  addins = ide_task_get_task_data (task);

  g_assert (IDE_IS_WORKBENCH (self));
  g_assert (addins != NULL);
  g_assert (addins->len > 0);

  if (!ide_workbench_addin_unload_project_finish (addin, result, &error))
    {
      if (!ignore_error (error))
        g_warning ("%s failed to unload project: %s",
                   G_OBJECT_TYPE_NAME (addin), error->message);
    }

  g_ptr_array_remove (addins, addin);

  if (addins->len == 0)
    ide_workbench_unload_project_completed (self, task);
}

/**
 * ide_workbench_unload_async:
 * @self: an #IdeWorkbench
 * @cancellable: (nullable): a #GCancellable
 * @callback: a #GAsyncReadyCallback to execute upon completion
 * @user_data: closure data for @callback
 *
 * Asynchronously unloads the workbench.
 *
 * All #IdeWorkspace windows will be closed after calling this
 * function.
 *
 * Since: 3.32
 */
void
ide_workbench_unload_async (IdeWorkbench        *self,
                            GCancellable        *cancellable,
                            GAsyncReadyCallback  callback,
                            gpointer             user_data)
{
  g_autoptr(IdeTask) task = NULL;
  g_autoptr(GPtrArray) addins = NULL;
  GApplication *app;

  g_return_if_fail (IDE_IS_WORKBENCH (self));
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));

  task = ide_task_new (self, cancellable, callback, user_data);
  ide_task_set_source_tag (task, ide_workbench_unload_async);

  if (self->unloaded)
    {
      ide_task_return_boolean (task, TRUE);
      return;
    }

  self->unloaded = TRUE;

  /* Keep the GApplication alive for the lifetime of the task */
  app = g_application_get_default ();
  g_signal_connect_object (task,
                           "notify::completed",
                           G_CALLBACK (g_application_release),
                           app,
                           G_CONNECT_SWAPPED);
  g_application_hold (app);

  /*
   * Remove our workbench from the application, so that no new
   * open-file requests can keep us alive while we're shutting
   * down.
   */

  ide_application_remove_workbench (IDE_APPLICATION (app), self);

  /* If we haven't loaded a project, then there is nothing to
   * do right now, just let ide_workbench_addin_unload() be called
   * when the workbench disposes.
   */
  if (self->project_info == NULL)
    {
      ide_workbench_unload_project_completed (self, g_steal_pointer (&task));
      return;
    }

  addins = ide_workbench_collect_addins (self);
  ide_task_set_task_data (task, g_ptr_array_ref (addins), g_ptr_array_unref);

  if (addins->len == 0)
    {
      ide_workbench_unload_project_completed (self, task);
      return;
    }

  for (guint i = 0; i < addins->len; i++)
    {
      IdeWorkbenchAddin *addin = g_ptr_array_index (addins, i);

      ide_workbench_addin_unload_project_async (addin,
                                                self->project_info,
                                                ide_task_get_cancellable (task),
                                                ide_workbench_unload_project_cb,
                                                g_object_ref (task));
    }

  /* Since the g_steal_pointer() just before doesn't always run, ensure the
   * task isn't freed while it hasn't yet finished running asynchronously.
   */
  task = NULL;
}

/**
 * ide_workbench_unload_finish:
 * @self: an #IdeWorkbench
 * @result: a #GAsyncResult provided to callback
 * @error: a location for a #GError, or %NULL

 * Completes a request to unload the workbench.
 *
 * Returns: %TRUE if the workbench was unloaded successfully,
 *   otherwise %FALSE and @error is set.
 *
 * Since: 3.32
 */
gboolean
ide_workbench_unload_finish (IdeWorkbench *self,
                             GAsyncResult *result,
                             GError **error)
{
  g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);

  return ide_task_propagate_boolean (IDE_TASK (result), error);
}

static void
ide_workbench_open_all_cb (GObject      *object,
                           GAsyncResult *result,
                           gpointer      user_data)
{
  IdeWorkbench *self = (IdeWorkbench *)object;
  g_autoptr(IdeTask) task = user_data;
  g_autoptr(GError) error = NULL;
  gint *n_active;

  g_assert (IDE_IS_WORKBENCH (self));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (IDE_IS_TASK (task));

  if (!ide_workbench_open_finish (self, result, &error))
    g_message ("Failed to open file: %s", error->message);

  n_active = ide_task_get_task_data (task);
  g_assert (n_active != NULL);

  (*n_active)--;

  if (*n_active == 0)
    ide_task_return_boolean (task, TRUE);
}

/**
 * ide_workbench_open_all_async:
 * @self: an #IdeWorkbench
 * @files: (array length=n_files): an array of #GFile
 * @n_files: number of #GFiles contained in @files
 * @hint: (nullable): an optional hint about what addin to use
 * @cancellable: (nullable): a #GCancellable
 * @callback: a #GAsyncReadyCallback to execute upon completion
 * @user_data: closure data for @callback
 *
 * Requests that the workbench open all of the #GFile denoted by @files.
 *
 * If @hint is provided, that will be used to determine what workbench
 * addin to use when opening the file. The @hint name should match the
 * module name of the plugin.
 *
 * Call ide_workbench_open_finish() from @callback to complete this
 * operation.
 *
 * Since: 3.32
 */
void
ide_workbench_open_all_async (IdeWorkbench         *self,
                              GFile               **files,
                              guint                 n_files,
                              const gchar          *hint,
                              GCancellable         *cancellable,
                              GAsyncReadyCallback   callback,
                              gpointer              user_data)
{
  g_autoptr(IdeTask) task = NULL;
  g_autoptr(GPtrArray) ar = NULL;
  gint *n_active;

  g_return_if_fail (IDE_IS_WORKBENCH (self));
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));

  task = ide_task_new (self, cancellable, callback, user_data);
  ide_task_set_source_tag (task, ide_workbench_open_all_async);

  if (n_files == 0)
    {
      ide_task_return_boolean (task, TRUE);
      return;
    }

  ar = g_ptr_array_new_full (n_files, g_object_unref);
  for (guint i = 0; i < n_files; i++)
    g_ptr_array_add (ar, g_object_ref (files[i]));

  n_active = g_new0 (gint, 1);
  *n_active = ar->len;
  ide_task_set_task_data (task, n_active, g_free);

  for (guint i = 0; i < ar->len; i++)
    {
      GFile *file = g_ptr_array_index (ar, i);

      ide_workbench_open_async (self,
                                file,
                                hint,
                                IDE_BUFFER_OPEN_FLAGS_NONE,
                                cancellable,
                                ide_workbench_open_all_cb,
                                g_object_ref (task));
    }
}

/**
 * ide_workbench_open_async:
 * @self: an #IdeWorkbench
 * @file: a #GFile
 * @hint: (nullable): an optional hint about what addin to use
 * @flags: optional flags when opening the file
 * @cancellable: (nullable): a #GCancellable
 * @callback: a #GAsyncReadyCallback to execute upon completion
 * @user_data: closure data for @callback
 *
 * Requests that the workbench open @file.
 *
 * If @hint is provided, that will be used to determine what workbench
 * addin to use when opening the file. The @hint name should match the
 * module name of the plugin.
 *
 * @flags may be ignored by some backends.
 *
 * Since: 3.32
 */
void
ide_workbench_open_async (IdeWorkbench        *self,
                          GFile               *file,
                          const gchar         *hint,
                          IdeBufferOpenFlags   flags,
                          GCancellable        *cancellable,
                          GAsyncReadyCallback  callback,
                          gpointer             user_data)
{
  g_return_if_fail (IDE_IS_WORKBENCH (self));
  g_return_if_fail (G_IS_FILE (file));
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));

  ide_workbench_open_at_async (self,
                               file,
                               hint,
                               -1,
                               -1,
                               flags,
                               cancellable,
                               callback,
                               user_data);
}

static void
ide_workbench_open_cb (GObject      *object,
                       GAsyncResult *result,
                       gpointer      user_data)
{
  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)object;
  IdeWorkbenchAddin *next;
  g_autoptr(IdeTask) task = user_data;
  g_autoptr(GError) error = NULL;
  GCancellable *cancellable;
  Open *o;

  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (IDE_IS_TASK (task));

  cancellable = ide_task_get_cancellable (task);
  o = ide_task_get_task_data (task);

  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
  g_assert (o != NULL);
  g_assert (o->addins != NULL);
  g_assert (o->addins->len > 0);

  if (ide_workbench_addin_open_finish (addin, result, &error))
    {
      ide_task_return_boolean (task, TRUE);
      return;
    }

  g_debug ("%s did not open the file, trying next.",
           G_OBJECT_TYPE_NAME (addin));

  g_ptr_array_remove (o->addins, addin);

  /*
   * We failed to open the file, try the next addin that is
   * left which said it supported the content-type.
   */

  if (o->addins->len == 0)
    {
      ide_task_return_new_error (task,
                                 G_IO_ERROR,
                                 G_IO_ERROR_FAILED,
                                 "Failed to locate addin supporting file");
      return;
    }

  next = g_ptr_array_index (o->addins, 0);

  ide_workbench_addin_open_at_async (next,
                                     o->file,
                                     o->content_type,
                                     o->at_line,
                                     o->at_line_offset,
                                     o->flags,
                                     cancellable,
                                     ide_workbench_open_cb,
                                     g_steal_pointer (&task));
}

static gint
sort_by_priority (gconstpointer a,
                  gconstpointer b,
                  gpointer      user_data)
{
  IdeWorkbenchAddin *addin_a = *(IdeWorkbenchAddin **)a;
  IdeWorkbenchAddin *addin_b = *(IdeWorkbenchAddin **)b;
  Open *o = user_data;
  gint prio_a = 0;
  gint prio_b = 0;

  if (!ide_workbench_addin_can_open (addin_a, o->file, o->content_type, &prio_a))
    return 1;

  if (!ide_workbench_addin_can_open (addin_b, o->file, o->content_type, &prio_b))
    return -1;

  if (prio_a < prio_b)
    return -1;
  else if (prio_a > prio_b)
    return 1;
  else
    return 0;
}

static void
ide_workbench_open_query_info_cb (GObject      *object,
                                  GAsyncResult *result,
                                  gpointer      user_data)
{
  GFile *file = (GFile *)object;
  g_autoptr(IdeTask) task = user_data;
  g_autoptr(GFileInfo) info = NULL;
  g_autoptr(GError) error = NULL;
  IdeWorkbenchAddin *first;
  GCancellable *cancellable;
  Open *o;

  g_assert (G_IS_FILE (file));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (IDE_IS_TASK (task));

  cancellable = ide_task_get_cancellable (task);
  o = ide_task_get_task_data (task);

  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
  g_assert (o != NULL);
  g_assert (o->addins != NULL);
  g_assert (o->addins->len > 0);

  if ((info = g_file_query_info_finish (file, result, &error)))
    o->content_type = g_strdup (g_file_info_get_content_type (info));

  /* Remove unsupported addins while iterating backwards so that
   * we can preserve the ordering of the array as we go.
   */
  for (guint i = o->addins->len; i > 0; i--)
    {
      IdeWorkbenchAddin *addin = g_ptr_array_index (o->addins, i - 1);
      gint prio = G_MAXINT;

      if (!ide_workbench_addin_can_open (addin, o->file, o->content_type, &prio))
        {
          g_ptr_array_remove_index_fast (o->addins, i - 1);
          if (o->preferred == addin)
            g_clear_object (&o->preferred);
        }
    }

  if (o->addins->len == 0)
    {
      ide_task_return_new_error (task,
                                 G_IO_ERROR,
                                 G_IO_ERROR_FAILED,
                                 "No addins can open the file");
      return;
    }

  /*
   * Now sort the addins by priority, so that we can attempt to load them
   * in the preferred ordering.
   */
  g_ptr_array_sort_with_data (o->addins, sort_by_priority, o);

  /*
   * Ensure that we place the preferred at the head of the array, so
   * that it gets preference over default priorities.
   */
  if (o->preferred != NULL)
    {
      g_ptr_array_insert (o->addins, 0, g_object_ref (o->preferred));

      for (guint i = 1; i < o->addins->len; i++)
        {
          if (g_ptr_array_index (o->addins, i) == (gpointer)o->preferred)
            {
              g_ptr_array_remove_index (o->addins, i);
              break;
            }
        }
    }

  /* Now start requesting that addins attempt to load the file. */

  first = g_ptr_array_index (o->addins, 0);

  ide_workbench_addin_open_at_async (first,
                                     o->file,
                                     o->content_type,
                                     o->at_line,
                                     o->at_line_offset,
                                     o->flags,
                                     cancellable,
                                     ide_workbench_open_cb,
                                     g_steal_pointer (&task));
}

/**
 * ide_workbench_open_at_async:
 * @self: an #IdeWorkbench
 * @file: a #GFile
 * @hint: (nullable): an optional hint about what addin to use
 * @at_line: the line number to open at, or -1 to ignore
 * @at_line_offset: the line offset to open at, or -1 to ignore
 * @flags: optional #IdeBufferOpenFlags
 * @cancellable: (nullable): a #GCancellable
 * @callback: a #GAsyncReadyCallback to execute upon completion
 * @user_data: closure data for @callback
 *
 * Like ide_workbench_open_async(), this allows opening a file
 * within the workbench. However, it also allows specifying a
 * line and column offset within the file to focus. Usually, this
 * only makes sense for files that can be opened in an editor.
 *
 * @at_line and @at_line_offset may be < 0 to ignore the parameters.
 *
 * @flags may be ignored by some backends
 *
 * Use ide_workbench_open_finish() to receive teh result of this
 * asynchronous operation.
 *
 * Since: 3.32
 */
void
ide_workbench_open_at_async (IdeWorkbench        *self,
                             GFile               *file,
                             const gchar         *hint,
                             gint                 at_line,
                             gint                 at_line_offset,
                             IdeBufferOpenFlags   flags,
                             GCancellable        *cancellable,
                             GAsyncReadyCallback  callback,
                             gpointer             user_data)
{
  g_autoptr(IdeTask) task = NULL;
  g_autoptr(GPtrArray) addins = NULL;
  IdeWorkbench *other;
  Open *o;

  g_return_if_fail (IDE_IS_WORKBENCH (self));
  g_return_if_fail (G_IS_FILE (file));
  g_return_if_fail (self->unloaded == FALSE);
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));

  /* Possibly re-route opening the file to another workbench if we
   * discover the file is a better fit over there.
   */
  other = ide_application_find_workbench_for_file (IDE_APPLICATION_DEFAULT, file);

  if (other != NULL && other != self)
    {
      ide_workbench_open_at_async (other,
                                   file,
                                   hint,
                                   at_line,
                                   at_line_offset,
                                   flags,
                                   cancellable,
                                   callback,
                                   user_data);
      return;
    }

  /* Canonicalize parameters */
  if (at_line < 0)
    at_line = -1;
  if (at_line_offset < 0)
    at_line_offset = -1;

  task = ide_task_new (self, cancellable, callback, user_data);
  ide_task_set_source_tag (task, ide_workbench_open_at_async);

  /*
   * Make sure we might have an addin to load after discovering
   * the files content-type.
   */
  if (!(addins = ide_workbench_collect_addins (self)) || addins->len == 0)
    {
      ide_task_return_new_error (task,
                                 G_IO_ERROR,
                                 G_IO_ERROR_FAILED,
                                 "No addins could open the file");
      return;
    }

  o = g_slice_new0 (Open);
  o->addins = g_ptr_array_ref (addins);
  if (hint != NULL)
    o->preferred = ide_workbench_find_addin (self, hint);
  o->file = g_object_ref (file);
  o->hint = g_strdup (hint);
  o->flags = flags;
  o->at_line = at_line;
  o->at_line_offset = at_line_offset;
  ide_task_set_task_data (task, o, open_free);

  g_file_query_info_async (file,
                           G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
                           G_FILE_QUERY_INFO_NONE,
                           G_PRIORITY_DEFAULT,
                           cancellable,
                           ide_workbench_open_query_info_cb,
                           g_steal_pointer (&task));
}

/**
 * ide_workbench_open_finish:
 * @self: an #IdeWorkbench
 * @result: a #GAsyncResult provided to callback
 * @error: a location for a #GError, or %NULL
 *
 * Completes a request to open a file using either
 * ide_workbench_open_async() or ide_workbench_open_at_async().
 *
 * Returns: %TRUE if the file was successfully opened; otherwise
 *   %FALSE and @error is set.
 *
 * Since: 3.32
 */
gboolean
ide_workbench_open_finish (IdeWorkbench  *self,
                           GAsyncResult  *result,
                           GError       **error)
{
  g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);

  return ide_task_propagate_boolean (IDE_TASK (result), error);
}

/**
 * ide_workbench_get_current_workspace:
 * @self: a #IdeWorkbench
 *
 * Gets the most recently focused workspace, which may be used to
 * deliver events such as opening new pages.
 *
 * Returns: (transfer none) (nullable): an #IdeWorkspace or %NULL
 *
 * Since: 3.32
 */
IdeWorkspace *
ide_workbench_get_current_workspace (IdeWorkbench *self)
{
  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);

  if (self->mru_queue.length > 0)
    return IDE_WORKSPACE (self->mru_queue.head->data);

  return NULL;
}

/**
 * ide_workbench_activate:
 * @self: a #IdeWorkbench
 *
 * This function will attempt to raise the most recently focused workspace.
 *
 * Since: 3.32
 */
void
ide_workbench_activate (IdeWorkbench *self)
{
  IdeWorkspace *workspace;

  g_return_if_fail (IDE_IS_WORKBENCH (self));

  if ((workspace = ide_workbench_get_current_workspace (self)))
    ide_workbench_focus_workspace (self, workspace);
}

static void
ide_workbench_propagate_vcs_cb (PeasExtensionSet *set,
                                PeasPluginInfo   *plugin_info,
                                PeasExtension    *exten,
                                gpointer          user_data)
{
  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten;
  IdeVcs *vcs = user_data;

  g_assert (PEAS_IS_EXTENSION_SET (set));
  g_assert (plugin_info != NULL);
  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
  g_assert (!vcs || IDE_IS_VCS (vcs));

  ide_workbench_addin_vcs_changed (addin, vcs);
}

/**
 * ide_workbench_get_vcs:
 * @self: a #IdeWorkbench
 *
 * Gets the #IdeVcs that has been loaded for the workbench, if any.
 *
 * Returns: (transfer none) (nullable): an #IdeVcs or %NULL
 *
 * Since: 3.32
 */
IdeVcs *
ide_workbench_get_vcs (IdeWorkbench *self)
{
  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);

  return self->vcs;
}

/**
 * ide_workbench_get_vcs_monitor:
 * @self: a #IdeWorkbench
 *
 * Gets the #IdeVcsMonitor for the workbench, if any.
 *
 * Returns: (transfer none) (nullable): an #IdeVcsMonitor or %NULL
 *
 * Since: 3.32
 */
IdeVcsMonitor *
ide_workbench_get_vcs_monitor (IdeWorkbench *self)
{
  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);

  return self->vcs_monitor;
}

static void
remove_non_matching_vcs_cb (IdeObject *child,
                            IdeVcs    *vcs)
{
  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_OBJECT (child));
  g_assert (IDE_IS_VCS (vcs));

  if (IDE_IS_VCS (child) && IDE_VCS (child) != vcs)
    ide_object_destroy (child);
}

static void
ide_workbench_vcs_notify_branch_name_cb (IdeWorkbench *self,
                                         GParamSpec   *pspec,
                                         IdeVcs       *vcs)
{
  IdeBuildManager *build_manager;

  IDE_ENTRY;

  g_assert (IDE_IS_WORKBENCH (self));
  g_assert (IDE_IS_VCS (vcs));

  if (!ide_workbench_has_project (self))
    IDE_EXIT;

  build_manager = ide_build_manager_from_context (self->context);
  ide_build_manager_invalidate (build_manager);

  IDE_EXIT;
}

/**
 * ide_workbench_set_vcs:
 * @self: a #IdeWorkbench
 * @vcs: (nullable): an #IdeVcs
 *
 * Sets the #IdeVcs for the workbench.
 *
 * Since: 3.32
 */
void
ide_workbench_set_vcs (IdeWorkbench *self,
                       IdeVcs       *vcs)
{
  g_autoptr(IdeVcs) local_vcs = NULL;
  g_autoptr(GFile) local_workdir = NULL;
  g_autoptr(GFile) workdir = NULL;

  g_return_if_fail (IDE_IS_MAIN_THREAD ());
  g_return_if_fail (IDE_IS_WORKBENCH (self));
  g_return_if_fail (!vcs || IDE_IS_VCS (vcs));

  if (self->vcs != NULL && vcs == self->vcs)
    return;

  if (vcs == NULL)
    {
      local_workdir = ide_context_ref_workdir (self->context);
      vcs = local_vcs = IDE_VCS (ide_directory_vcs_new (local_workdir));
    }

  g_set_object (&self->vcs, vcs);
  ide_object_append (IDE_OBJECT (self->context), IDE_OBJECT (vcs));
  ide_object_foreach (IDE_OBJECT (self->context),
                      (GFunc)remove_non_matching_vcs_cb,
                      vcs);

  if ((workdir = ide_vcs_get_workdir (vcs)))
    ide_context_set_workdir (self->context, workdir);

  ide_vcs_monitor_set_vcs (self->vcs_monitor, self->vcs);

  peas_extension_set_foreach (self->addins,
                              ide_workbench_propagate_vcs_cb,
                              self->vcs);

  g_signal_connect_object (vcs,
                           "notify::branch-name",
                           G_CALLBACK (ide_workbench_vcs_notify_branch_name_cb),
                           self,
                           G_CONNECT_SWAPPED);

  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VCS]);
}

/**
 * ide_workbench_get_build_system:
 * @self: a #IdeWorkbench
 *
 * Gets the #IdeBuildSystem for the workbench, if any.
 *
 * Returns: (transfer none) (nullable): an #IdeBuildSystem or %NULL
 *
 * Since: 3.32
 */
IdeBuildSystem *
ide_workbench_get_build_system (IdeWorkbench *self)
{
  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);

  return self->build_system;
}

static void
remove_non_matching_build_systems_cb (IdeObject      *child,
                                      IdeBuildSystem *build_system)
{
  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_OBJECT (child));
  g_assert (IDE_IS_BUILD_SYSTEM (build_system));

  if (IDE_IS_BUILD_SYSTEM (child) && IDE_BUILD_SYSTEM (child) != build_system)
    ide_object_destroy (child);
}

/**
 * ide_workbench_set_build_system:
 * @self: a #IdeWorkbench
 * @build_system: (nullable): an #IdeBuildSystem or %NULL
 *
 * Sets the #IdeBuildSystem for the workbench.
 *
 * If @build_system is %NULL, then a fallback build system will be used
 * instead. It does not provide building capabilities, but allows for some
 * components that require a build system to continue functioning.
 *
 * Since: 3.32
 */
void
ide_workbench_set_build_system (IdeWorkbench   *self,
                                IdeBuildSystem *build_system)
{
  g_autoptr(IdeBuildSystem) local_build_system = NULL;
  IdeBuildManager *build_manager;

  g_return_if_fail (IDE_IS_WORKBENCH (self));
  g_return_if_fail (!build_system || IDE_IS_BUILD_SYSTEM (build_system));

  if (build_system == self->build_system)
    return;

  /* We want there to always be a build system available so that various
   * plugins don't need lots of extra code to handle the %NULL case. So
   * if @build_system is %NULL, then we'll create a fallback build system
   * and assign that instead.
   */

  if (build_system == NULL)
    build_system = local_build_system = ide_fallback_build_system_new ();

  /* We want to add our new build system before removing the old build
   * system to ensure there is always an #IdeBuildSystem child of the
   * IdeContext.
   */
  g_set_object (&self->build_system, build_system);
  ide_object_append (IDE_OBJECT (self->context), IDE_OBJECT (build_system));

  /* Now remove any previous build-system from the context */
  ide_object_foreach (IDE_OBJECT (self->context),
                      (GFunc)remove_non_matching_build_systems_cb,
                      build_system);

  /* Ask the build-manager to setup a new pipeline */
  if ((build_manager = ide_context_peek_child_typed (self->context, IDE_TYPE_BUILD_MANAGER)))
    ide_build_manager_invalidate (build_manager);
}

/**
 * ide_workbench_get_workspace_by_type:
 * @self: a #IdeWorkbench
 * @type: a #GType of a subclass of #IdeWorkspace
 *
 * Gets the most-recently-used workspace that matches @type.
 *
 * Returns: (transfer none) (nullable): an #IdeWorkspace or %NULL
 *
 * Since: 3.32
 */
IdeWorkspace *
ide_workbench_get_workspace_by_type (IdeWorkbench *self,
                                     GType         type)
{
  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
  g_return_val_if_fail (g_type_is_a (type, IDE_TYPE_WORKSPACE), NULL);

  for (const GList *iter = self->mru_queue.head; iter; iter = iter->next)
    {
      if (G_TYPE_CHECK_INSTANCE_TYPE (iter->data, type))
        return IDE_WORKSPACE (iter->data);
    }

  return NULL;
}

gboolean
_ide_workbench_is_last_workspace (IdeWorkbench *self,
                                  IdeWorkspace *workspace)
{
  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
  g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);

  return self->mru_queue.length == 1 &&
         g_queue_peek_head (&self->mru_queue) == (gpointer)workspace;
}

/**
 * ide_workbench_has_project:
 * @self: a #IdeWorkbench
 *
 * Returns %TRUE if a project is loaded (or currently loading) in the
 * workbench.
 *
 * Returns: %TRUE if the workbench has a project
 *
 * Since: 3.32
 */
gboolean
ide_workbench_has_project (IdeWorkbench *self)
{
  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
  g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);

  return self->project_info != NULL;
}

/**
 * ide_workbench_addin_find_by_module_name:
 * @workbench: an #IdeWorkbench
 * @module_name: the name of the addin module
 *
 * Finds the addin (if any) matching the plugin's @module_name.
 *
 * Returns: (transfer none) (nullable): an #IdeWorkbenchAddin or %NULL
 *
 * Since: 3.32
 */
IdeWorkbenchAddin *
ide_workbench_addin_find_by_module_name (IdeWorkbench *workbench,
                                         const gchar  *module_name)
{
  PeasPluginInfo *plugin_info;
  PeasExtension *ret = NULL;
  PeasEngine *engine;

  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
  g_return_val_if_fail (IDE_IS_WORKBENCH (workbench), NULL);
  g_return_val_if_fail (module_name != NULL, NULL);

  if (workbench->addins == NULL)
    return NULL;

  engine = peas_engine_get_default ();

  if ((plugin_info = peas_engine_get_plugin_info (engine, module_name)))
    ret = peas_extension_set_get_extension (workbench->addins, plugin_info);

  return IDE_WORKBENCH_ADDIN (ret);
}

static void
ide_workbench_resolve_file_worker (IdeTask      *task,
                                   gpointer      source_object,
                                   gpointer      task_data,
                                   GCancellable *cancellable)
{
  ResolveFile *rf = task_data;
  g_autofree gchar *basename = NULL;

  g_assert (IDE_IS_TASK (task));
  g_assert (IDE_IS_WORKBENCH (source_object));
  g_assert (rf != NULL);
  g_assert (rf->roots != NULL);
  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));

  basename = g_path_get_basename (rf->path);

  for (guint i = 0; i < rf->roots->len; i++)
    {
      GFile *root = g_ptr_array_index (rf->roots, i);
      g_autoptr(GFile) child = g_file_get_child (root, rf->path);
      g_autoptr(GPtrArray) found = NULL;

      if (g_file_query_exists (child, cancellable))
        {
          ide_task_return_pointer (task, g_steal_pointer (&child), g_object_unref);
          return;
        }

      found = ide_g_file_find_with_depth (root, basename, 0, cancellable);
      IDE_PTR_ARRAY_SET_FREE_FUNC (found, g_object_unref);

      if (found != NULL && found->len > 0)
        {
          GFile *match = g_ptr_array_index (found, 0);
          ide_task_return_pointer (task, g_object_ref (match), g_object_unref);
          return;
        }
    }

  ide_task_return_new_error (task,
                             G_IO_ERROR,
                             G_IO_ERROR_NOT_FOUND,
                             "Failed to locate file %s",
                             basename);
}

/**
 * ide_workbench_resolve_file_async:
 * @self: a #IdeWorkbench
 * @filename: the filename to discover
 *
 * This function will try to locate a given file based on the filename,
 * possibly resolving it from a build directory, or source directory.
 *
 * If no file was discovered, some attempt will be made to locate a file
 * that matches appropriately.
 *
 * Since: 3.32
 */
void
ide_workbench_resolve_file_async (IdeWorkbench        *self,
                                  const gchar         *filename,
                                  GCancellable        *cancellable,
                                  GAsyncReadyCallback  callback,
                                  gpointer             user_data)
{
  g_autoptr(IdeTask) task = NULL;
  ResolveFile *rf;

  IDE_ENTRY;

  g_return_if_fail (IDE_IS_MAIN_THREAD ());
  g_return_if_fail (IDE_IS_WORKBENCH (self));
  g_return_if_fail (filename != NULL);

  task = ide_task_new (self, cancellable, callback, user_data);
  ide_task_set_source_tag (task, ide_workbench_resolve_file_async);

  rf = g_slice_new0 (ResolveFile);
  rf->roots = g_ptr_array_new_with_free_func (g_object_unref);
  rf->path = g_strdup (filename);

  g_ptr_array_add (rf->roots, ide_context_ref_workdir (self->context));

  if (ide_workbench_has_project (self))
    {
      IdeBuildManager *build_manager = ide_build_manager_from_context (self->context);
      IdePipeline *pipeline = ide_build_manager_get_pipeline (build_manager);

      if (pipeline != NULL)
        {
          const gchar *builddir = ide_pipeline_get_builddir (pipeline);

          g_ptr_array_add (rf->roots, g_file_new_for_path (builddir));
        }
    }

  ide_task_set_task_data (task, rf, resolve_file_free);
  ide_task_run_in_thread (task, ide_workbench_resolve_file_worker);

  IDE_EXIT;
}

/**
 * ide_workbench_resolve_file_finish:
 * @self: a #IdeWorkbench
 * @result: a #GAsyncResult
 * @error: a location for a #GError
 *
 * Completes an asynchronous request to ide_workbench_resolve_file_async().
 *
 * Returns: (transfer full): a #GFile, or %NULL and @error is set
 *
 * Since: 3.32
 */
GFile *
ide_workbench_resolve_file_finish (IdeWorkbench  *self,
                                   GAsyncResult  *result,
                                   GError       **error)
{
  GFile *ret;

  IDE_ENTRY;

  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
  g_return_val_if_fail (IDE_IS_TASK (result), NULL);

  ret = ide_task_propagate_pointer (IDE_TASK (result), error);

  IDE_RETURN (g_steal_pointer (&ret));
}
