[gnome-builder: 40/139] libide-projects: add libide-projects static library



commit 6c92f51477923f0bba54881e48e495c3a5efd708
Author: Christian Hergert <chergert redhat com>
Date:   Wed Jan 9 16:26:30 2019 -0800

    libide-projects: add libide-projects static library
    
    This adds the libide-projects static library which contains modules
    related to browsing, describing, and creating projects.

 src/libide/doap/OVERVIEW.md                        |  13 -
 src/libide/doap/meson.build                        |  25 -
 src/libide/{doap => projects}/ide-doap-person.c    |   2 +-
 src/libide/{doap => projects}/ide-doap-person.h    |   6 +-
 src/libide/{doap => projects}/ide-doap.c           |  15 +-
 src/libide/{doap => projects}/ide-doap.h           |  10 +-
 src/libide/projects/ide-project-edit.c             | 253 -------
 src/libide/projects/ide-project-edit.h             |  60 --
 src/libide/projects/ide-project-file.c             | 617 ++++++++++++++++++
 src/libide/projects/ide-project-file.h             | 103 +++
 src/libide/projects/ide-project-info.c             | 178 ++++-
 src/libide/projects/ide-project-info.h             |  94 +--
 src/libide/projects/ide-project-item.c             | 231 -------
 src/libide/projects/ide-project-item.h             |  52 --
 src/libide/projects/ide-project-template.c         | 188 ++++++
 src/libide/projects/ide-project-template.h         |  86 +++
 src/libide/projects/ide-project-tree-addin.c       |   2 +-
 src/libide/projects/ide-project-tree-addin.h       |   3 +-
 src/libide/projects/ide-project.c                  | 334 ++--------
 src/libide/projects/ide-project.h                  |  24 +-
 src/libide/projects/ide-projects-global.c          | 132 ++++
 ...roject-edit-private.h => ide-projects-global.h} |  19 +-
 src/libide/projects/ide-recent-projects.c          |  75 ++-
 src/libide/projects/ide-recent-projects.h          |   8 +-
 src/libide/projects/ide-template-base.c            | 724 +++++++++++++++++++++
 src/libide/projects/ide-template-base.h            |  71 ++
 src/libide/projects/ide-template-provider.c        |  61 ++
 src/libide/projects/ide-template-provider.h        |  48 ++
 src/libide/projects/libide-projects.h              |  40 ++
 src/libide/projects/meson.build                    |  86 ++-
 .../xml-reader.h => projects/xml-reader-private.h} |   2 +
 src/libide/{doap => projects}/xml-reader.c         |   4 +-
 32 files changed, 2489 insertions(+), 1077 deletions(-)
---
diff --git a/src/libide/doap/ide-doap-person.c b/src/libide/projects/ide-doap-person.c
similarity index 99%
rename from src/libide/doap/ide-doap-person.c
rename to src/libide/projects/ide-doap-person.c
index 186c35e72..7f386458d 100644
--- a/src/libide/doap/ide-doap-person.c
+++ b/src/libide/projects/ide-doap-person.c
@@ -24,7 +24,7 @@
 
 #include <glib/gi18n.h>
 
-#include "doap/ide-doap-person.h"
+#include "ide-doap-person.h"
 
 struct _IdeDoapPerson
 {
diff --git a/src/libide/doap/ide-doap-person.h b/src/libide/projects/ide-doap-person.h
similarity index 90%
rename from src/libide/doap/ide-doap-person.h
rename to src/libide/projects/ide-doap-person.h
index df2eec58d..2d77305ad 100644
--- a/src/libide/doap/ide-doap-person.h
+++ b/src/libide/projects/ide-doap-person.h
@@ -20,9 +20,11 @@
 
 #pragma once
 
-#include <glib-object.h>
+#if !defined (IDE_PROJECTS_INSIDE) && !defined (IDE_PROJECTS_COMPILATION)
+# error "Only <libide-projects.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/libide/doap/ide-doap.c b/src/libide/projects/ide-doap.c
similarity index 98%
rename from src/libide/doap/ide-doap.c
rename to src/libide/projects/ide-doap.c
index bfa1b7845..542c80bc2 100644
--- a/src/libide/doap/ide-doap.c
+++ b/src/libide/projects/ide-doap.c
@@ -1,6 +1,6 @@
 /* ide-doap.c
  *
- * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,17 +24,14 @@
 
 #include <glib/gi18n.h>
 
-#include "doap/ide-doap.h"
+#include "ide-doap.h"
+#include "xml-reader-private.h"
 
-#include "doap/xml-reader.h"
-
-/*
- * TODO: We don't do any XMLNS checking or anything here.
- */
+/* TODO: We don't do any XMLNS checking or anything here. */
 
 struct _IdeDoap
 {
-  GObject parent_instance;
+  GObject    parent_instance;
 
   gchar     *bug_database;
   gchar     *category;
@@ -249,7 +246,7 @@ ide_doap_set_shortdesc (IdeDoap     *self,
  *
  *
  *
- * Returns: (transfer none) (element-type Ide.DoapPerson): a #GList of #IdeDoapPerson.
+ * Returns: (transfer none) (element-type IdeDoapPerson): a #GList of #IdeDoapPerson.
  *
  * Since: 3.32
  */
diff --git a/src/libide/doap/ide-doap.h b/src/libide/projects/ide-doap.h
similarity index 90%
rename from src/libide/doap/ide-doap.h
rename to src/libide/projects/ide-doap.h
index 038e67bd2..2e65d96e0 100644
--- a/src/libide/doap/ide-doap.h
+++ b/src/libide/projects/ide-doap.h
@@ -1,6 +1,6 @@
 /* ide-doap.h
  *
- * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,11 +20,13 @@
 
 #pragma once
 
-#include <gio/gio.h>
+#if !defined (IDE_PROJECTS_INSIDE) && !defined (IDE_PROJECTS_COMPILATION)
+# error "Only <libide-projects.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
-#include "doap/ide-doap-person.h"
+#include "ide-doap-person.h"
 
 G_BEGIN_DECLS
 
diff --git a/src/libide/projects/ide-project-file.c b/src/libide/projects/ide-project-file.c
new file mode 100644
index 000000000..ebe18fa10
--- /dev/null
+++ b/src/libide/projects/ide-project-file.c
@@ -0,0 +1,617 @@
+/* ide-project-file.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-project-file"
+
+#include "config.h"
+
+#include <libide-threading.h>
+#include <string.h>
+
+#include "ide-project.h"
+#include "ide-project-file.h"
+
+typedef struct
+{
+  GFile     *directory;
+  GFileInfo *info;
+  guint      checked_for_icon_override : 1;
+} IdeProjectFilePrivate;
+
+enum {
+  PROP_0,
+  PROP_DIRECTORY,
+  PROP_FILE,
+  PROP_INFO,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeProjectFile, ide_project_file, IDE_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static gchar *
+ide_project_file_repr (IdeObject *object)
+{
+  IdeProjectFile *self = (IdeProjectFile *)object;
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_assert (IDE_IS_PROJECT_FILE (self));
+
+  if (priv->info && priv->directory)
+    return g_strdup_printf ("%s name=\"%s\" directory=\"%s\"",
+                            G_OBJECT_TYPE_NAME (self),
+                            g_file_info_get_display_name (priv->info),
+                            g_file_peek_path (priv->directory));
+  else
+    return IDE_OBJECT_CLASS (ide_project_file_parent_class)->repr (object);
+}
+
+static void
+ide_project_file_dispose (GObject *object)
+{
+  IdeProjectFile *self = (IdeProjectFile *)object;
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_clear_object (&priv->directory);
+  g_clear_object (&priv->info);
+
+  G_OBJECT_CLASS (ide_project_file_parent_class)->dispose (object);
+}
+
+static void
+ide_project_file_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  IdeProjectFile *self = IDE_PROJECT_FILE (object);
+
+  switch (prop_id)
+    {
+    case PROP_DIRECTORY:
+      g_value_set_object (value, ide_project_file_get_directory (self));
+      break;
+
+    case PROP_FILE:
+      g_value_take_object (value, ide_project_file_ref_file (self));
+      break;
+
+    case PROP_INFO:
+      g_value_set_object (value, ide_project_file_get_info (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_project_file_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  IdeProjectFile *self = IDE_PROJECT_FILE (object);
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_DIRECTORY:
+      priv->directory = g_value_dup_object (value);
+      break;
+
+    case PROP_INFO:
+      priv->info = g_value_dup_object (value);
+      if (priv->info &&
+          g_file_info_has_attribute (priv->info, G_FILE_ATTRIBUTE_STANDARD_NAME))
+        break;
+      /* Fall-through */
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_project_file_class_init (IdeProjectFileClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_project_file_dispose;
+  object_class->get_property = ide_project_file_get_property;
+  object_class->set_property = ide_project_file_set_property;
+
+  i_object_class->repr = ide_project_file_repr;
+
+  properties [PROP_DIRECTORY] =
+    g_param_spec_object ("directory",
+                         "Directory",
+                         "The directory containing the file",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_INFO] =
+    g_param_spec_object ("info",
+                         "Info",
+                         "The file info the file",
+                         G_TYPE_FILE_INFO,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_FILE] =
+    g_param_spec_object ("file",
+                         "File",
+                         "The file",
+                         G_TYPE_FILE,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_project_file_init (IdeProjectFile *self)
+{
+}
+
+/**
+ * ide_project_file_get_directory:
+ * @self: a #IdeProjectFile
+ *
+ * Gets the project file.
+ *
+ * Returns: (transfer none): an #IdeProjectFile
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_project_file_get_directory (IdeProjectFile *self)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), NULL);
+
+  return priv->directory;
+}
+
+/**
+ * ide_project_file_ref_file:
+ * @self: a #IdeProjectFile
+ *
+ * Gets the file for the #IdeProjectFile.
+ *
+ * Returns: (transfer full): a #GFile
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_project_file_ref_file (IdeProjectFile *self)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), NULL);
+
+  return g_file_get_child (priv->directory, g_file_info_get_name (priv->info));
+}
+
+/**
+ * ide_project_file_get_info:
+ * @self: a #IdeProjectFile
+ *
+ * Gets the #GFileInfo for the file. This combined with
+ * #IdeProjectFile:directory can be used to determine the underlying
+ * file, such as via #IdeProjectFile:file.
+ *
+ * Returns: (transfer none): a #GFileInfo
+ *
+ * Since: 3.32
+ */
+GFileInfo *
+ide_project_file_get_info (IdeProjectFile *self)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), NULL);
+
+  return priv->info;
+}
+
+/**
+ * ide_project_file_get_name:
+ * @self: a #IdeProjectFile
+ *
+ * Gets the name for the file, which matches the encoding on disk.
+ *
+ * Returns: a string containing the name
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_project_file_get_name (IdeProjectFile *self)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), NULL);
+
+  return g_file_info_get_name (priv->info);
+}
+
+/**
+ * ide_project_file_get_display_name:
+ * @self: a #IdeProjectFile
+ *
+ * Gets the display-name for the file, which should be shown to users.
+ *
+ * Returns: a string containing the display name
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_project_file_get_display_name (IdeProjectFile *self)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), NULL);
+
+  return g_file_info_get_display_name (priv->info);
+}
+
+/**
+ * ide_project_file_is_directory:
+ * @self: a #IdeProjectFile
+ *
+ * Checks if @self represents a directory. If ide_project_file_is_symlink() is
+ * %TRUE, this may still return %TRUE.
+ *
+ * Returns: %TRUE if @self is a directory, or symlink to a directory
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_project_file_is_directory (IdeProjectFile *self)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), FALSE);
+
+  return g_file_info_get_file_type (priv->info) == G_FILE_TYPE_DIRECTORY;
+}
+
+
+/**
+ * ide_project_file_is_symlink:
+ * @self: a #IdeProjectFile
+ *
+ * Checks if @self represents a symlink.
+ *
+ * Returns: %TRUE if @self is a symlink
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_project_file_is_symlink (IdeProjectFile *self)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), FALSE);
+
+  return g_file_info_get_is_symlink (priv->info);
+}
+
+gint
+ide_project_file_compare (IdeProjectFile *a,
+                          IdeProjectFile *b)
+{
+  GFileInfo *info_a = ide_project_file_get_info (a);
+  GFileInfo *info_b = ide_project_file_get_info (b);
+  const gchar *display_name_a = g_file_info_get_display_name (info_a);
+  const gchar *display_name_b = g_file_info_get_display_name (info_b);
+  gchar *casefold_a = NULL;
+  gchar *casefold_b = NULL;
+  gboolean ret;
+
+  casefold_a = g_utf8_collate_key_for_filename (display_name_a, -1);
+  casefold_b = g_utf8_collate_key_for_filename (display_name_b, -1);
+
+  ret = strcmp (casefold_a, casefold_b);
+
+  g_free (casefold_a);
+  g_free (casefold_b);
+
+  return ret;
+}
+
+gint
+ide_project_file_compare_directories_first (IdeProjectFile *a,
+                                            IdeProjectFile *b)
+{
+  GFileInfo *info_a = ide_project_file_get_info (a);
+  GFileInfo *info_b = ide_project_file_get_info (b);
+  GFileType file_type_a = g_file_info_get_file_type (info_a);
+  GFileType file_type_b = g_file_info_get_file_type (info_b);
+  gint dir_a = (file_type_a == G_FILE_TYPE_DIRECTORY);
+  gint dir_b = (file_type_b == G_FILE_TYPE_DIRECTORY);
+  gint ret;
+
+  ret = dir_b - dir_a;
+  if (ret == 0)
+    ret = ide_project_file_compare (a, b);
+
+  return ret;
+}
+
+/**
+ * ide_project_file_get_symbolic_icon:
+ * @self: a #IdeProjectFile
+ *
+ * Gets the symbolic icon to represent the file.
+ *
+ * Returns: (transfer none) (nullable): a #GIcon or %NULL
+ *
+ * Since: 3.32
+ */
+GIcon *
+ide_project_file_get_symbolic_icon (IdeProjectFile *self)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), NULL);
+
+  /*
+   * We might want to override the symbolic icon based on an override
+   * icon we ship with Builder.
+   */
+  if (!priv->checked_for_icon_override)
+    {
+      const gchar *content_type;
+
+      priv->checked_for_icon_override = TRUE;
+
+      if ((content_type = g_file_info_get_content_type (priv->info)))
+        {
+          g_autoptr(GIcon) override = NULL;
+
+          if ((override = ide_g_content_type_get_symbolic_icon (content_type)))
+            g_file_info_set_symbolic_icon (priv->info, override);
+        }
+    }
+
+  return g_file_info_get_symbolic_icon (priv->info);
+}
+
+static void
+ide_project_file_list_children_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  GFile *parent = (GFile *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GPtrArray) files = NULL;
+  g_autoptr(GPtrArray) ret = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_FILE (parent));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!(files = ide_g_file_get_children_finish (parent, result, &error)))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  IDE_PTR_ARRAY_SET_FREE_FUNC (files, g_object_unref);
+
+  ret = g_ptr_array_new_full (files->len, g_object_unref);
+
+  for (guint i = 0; i < files->len; i++)
+    {
+      GFileInfo *info = g_ptr_array_index (files, i);
+      IdeProjectFile *project_file;
+
+      project_file = g_object_new (IDE_TYPE_PROJECT_FILE,
+                                   "info", info,
+                                   "directory", parent,
+                                   NULL);
+      g_ptr_array_add (ret, g_steal_pointer (&project_file));
+    }
+
+  ide_task_return_pointer (task,
+                           g_steal_pointer (&ret),
+                           (GDestroyNotify)g_ptr_array_unref);
+}
+
+/**
+ * ide_project_file_list_children_async:
+ * @self: a #IdeProjectFile
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: callback to execute upon completion
+ * @user_data: user data for @callback
+ *
+ * List the children of @self.
+ *
+ * Call ide_project_file_list_children_finish() to get the result
+ * of this operation.
+ *
+ * Since: 3.32
+ */
+void
+ide_project_file_list_children_async (IdeProjectFile      *self,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GFile) file = NULL;
+
+  g_assert (IDE_IS_PROJECT_FILE (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_project_file_list_children_async);
+
+  file = ide_project_file_ref_file (self);
+
+  ide_g_file_get_children_async (file,
+                                 IDE_PROJECT_FILE_ATTRIBUTES,
+                                 G_FILE_QUERY_INFO_NONE,
+                                 G_PRIORITY_DEFAULT,
+                                 cancellable,
+                                 ide_project_file_list_children_cb,
+                                 g_steal_pointer (&task));
+}
+
+/**
+ * ide_project_file_list_children_finish:
+ * @self: a #IdeProjectFile
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError or %NULL
+ *
+ * Completes an asynchronous request to
+ * ide_project_file_list_children_async().
+ *
+ * Returns: (transfer full) (element-type IdeProjectFile): a #GPtrArray
+ *   of #IdeProjectFile
+ *
+ * Since: 3.32
+ */
+GPtrArray *
+ide_project_file_list_children_finish (IdeProjectFile  *self,
+                                       GAsyncResult    *result,
+                                       GError         **error)
+{
+  GPtrArray *ret;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+
+  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  return IDE_PTR_ARRAY_STEAL_FULL (&ret);
+}
+
+static void
+ide_project_file_trash_cb (GObject      *object,
+                           GAsyncResult *result,
+                           gpointer      user_data)
+{
+  IdeProject *project = (IdeProject *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_PROJECT (project));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_project_trash_file_finish (project, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+}
+
+void
+ide_project_file_trash_async (IdeProjectFile      *self,
+                              GCancellable        *cancellable,
+                              GAsyncReadyCallback  callback,
+                              gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(IdeContext) context = NULL;
+  g_autoptr(IdeProject) project = NULL;
+  g_autoptr(GFile) file = NULL;
+
+  g_return_if_fail (IDE_IS_PROJECT_FILE (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_project_file_trash_async);
+
+  context = ide_object_ref_context (IDE_OBJECT (self));
+  project = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_PROJECT);
+  file = ide_project_file_ref_file (self);
+
+  ide_project_trash_file_async (project,
+                                file,
+                                cancellable,
+                                ide_project_file_trash_cb,
+                                g_steal_pointer (&task));
+}
+
+gboolean
+ide_project_file_trash_finish (IdeProjectFile  *self,
+                               GAsyncResult    *result,
+                               GError         **error)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+/**
+ * ide_project_file_create_child:
+ * @self: a #IdeProjectFile
+ * @info: a #GFileInfo
+ *
+ * Creates a new child project file of @self.
+ *
+ * Returns: (transfer full): an #IdeProjectFile
+ *
+ * Since: 3.32
+ */
+IdeProjectFile *
+ide_project_file_create_child (IdeProjectFile *self,
+                               GFileInfo      *info)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), NULL);
+  g_return_val_if_fail (G_IS_FILE_INFO (info), NULL);
+
+  return g_object_new (IDE_TYPE_PROJECT_FILE,
+                       "directory", priv->directory,
+                       "info", info,
+                       NULL);
+}
+
+/**
+ * ide_project_file_new:
+ * @directory: a #GFile
+ * @info: a #GFileInfo
+ *
+ * Creates a new project file for a child of @directory.
+ *
+ * Returns: (transfer full): an #IdeProjectFile
+ *
+ * Since: 3.32
+ */
+IdeProjectFile *
+ide_project_file_new (GFile     *directory,
+                      GFileInfo *info)
+{
+  g_return_val_if_fail (G_IS_FILE (directory), NULL);
+  g_return_val_if_fail (G_IS_FILE_INFO (info), NULL);
+
+  return g_object_new (IDE_TYPE_PROJECT_FILE,
+                       "directory", directory,
+                       "info", info,
+                       NULL);
+}
diff --git a/src/libide/projects/ide-project-file.h b/src/libide/projects/ide-project-file.h
new file mode 100644
index 000000000..36023e831
--- /dev/null
+++ b/src/libide/projects/ide-project-file.h
@@ -0,0 +1,103 @@
+/* ide-project-file.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_PROJECTS_INSIDE) && !defined (IDE_PROJECTS_COMPILATION)
+# error "Only <libide-projects.h> can be included directly."
+#endif
+
+#include <libide-code.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PROJECT_FILE (ide_project_file_get_type())
+
+
+#define IDE_PROJECT_FILE_ATTRIBUTES \
+  G_FILE_ATTRIBUTE_STANDARD_NAME"," \
+  G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME"," \
+  G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE"," \
+  G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON"," \
+  G_FILE_ATTRIBUTE_STANDARD_TYPE"," \
+  G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK"," \
+  G_FILE_ATTRIBUTE_ACCESS_CAN_READ"," \
+  G_FILE_ATTRIBUTE_ACCESS_CAN_RENAME"," \
+  G_FILE_ATTRIBUTE_ACCESS_CAN_TRASH
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeProjectFile, ide_project_file, IDE, PROJECT_FILE, IdeObject)
+
+struct _IdeProjectFileClass
+{
+  IdeObjectClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeProjectFile *ide_project_file_new                       (GFile                *directory,
+                                                            GFileInfo            *info);
+IDE_AVAILABLE_IN_3_32
+GFile          *ide_project_file_get_directory             (IdeProjectFile       *self);
+IDE_AVAILABLE_IN_3_32
+GFileInfo      *ide_project_file_get_info                  (IdeProjectFile       *self);
+IDE_AVAILABLE_IN_3_32
+GFile          *ide_project_file_ref_file                  (IdeProjectFile       *self);
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_project_file_get_display_name          (IdeProjectFile       *self);
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_project_file_get_name                  (IdeProjectFile       *self);
+IDE_AVAILABLE_IN_3_32
+gboolean        ide_project_file_is_directory              (IdeProjectFile       *self);
+IDE_AVAILABLE_IN_3_32
+gboolean        ide_project_file_is_symlink                (IdeProjectFile       *self);
+IDE_AVAILABLE_IN_3_32
+gint            ide_project_file_compare_directories_first (IdeProjectFile       *a,
+                                                            IdeProjectFile       *b);
+IDE_AVAILABLE_IN_3_32
+gint            ide_project_file_compare                   (IdeProjectFile       *a,
+                                                            IdeProjectFile       *b);
+IDE_AVAILABLE_IN_3_32
+GIcon          *ide_project_file_get_symbolic_icon         (IdeProjectFile       *self);
+IDE_AVAILABLE_IN_3_32
+IdeProjectFile *ide_project_file_create_child              (IdeProjectFile       *self,
+                                                            GFileInfo            *info);
+IDE_AVAILABLE_IN_3_32
+void            ide_project_file_list_children_async       (IdeProjectFile       *self,
+                                                            GCancellable         *cancellable,
+                                                            GAsyncReadyCallback   callback,
+                                                            gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+GPtrArray      *ide_project_file_list_children_finish      (IdeProjectFile       *self,
+                                                            GAsyncResult         *result,
+                                                            GError              **error);
+IDE_AVAILABLE_IN_3_32
+void            ide_project_file_trash_async               (IdeProjectFile       *self,
+                                                            GCancellable         *cancellable,
+                                                            GAsyncReadyCallback   callback,
+                                                            gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean        ide_project_file_trash_finish              (IdeProjectFile       *self,
+                                                            GAsyncResult         *result,
+                                                            GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/projects/ide-project-info.c b/src/libide/projects/ide-project-info.c
index 7c1e05844..290e2b64b 100644
--- a/src/libide/projects/ide-project-info.c
+++ b/src/libide/projects/ide-project-info.c
@@ -26,11 +26,10 @@
 
 #include "config.h"
 
-#include <dazzle.h>
 #include <glib/gi18n.h>
 #include <string.h>
 
-#include "projects/ide-project-info.h"
+#include "ide-project-info.h"
 
 /**
  * SECTION:ideprojectinfo:
@@ -47,6 +46,7 @@ struct _IdeProjectInfo
 {
   GObject     parent_instance;
 
+  gchar      *id;
   IdeDoap    *doap;
   GDateTime  *last_modified_at;
   GFile      *directory;
@@ -55,7 +55,7 @@ struct _IdeProjectInfo
   gchar      *name;
   gchar      *description;
   gchar     **languages;
-  IdeVcsUri  *vcs_uri;
+  gchar      *vcs_uri;
 
   gint        priority;
 
@@ -71,6 +71,7 @@ enum {
   PROP_DIRECTORY,
   PROP_DOAP,
   PROP_FILE,
+  PROP_ID,
   PROP_IS_RECENT,
   PROP_LANGUAGES,
   PROP_LAST_MODIFIED_AT,
@@ -224,7 +225,7 @@ ide_project_info_set_build_system_name (IdeProjectInfo *self,
 {
   g_return_if_fail (IDE_IS_PROJECT_INFO (self));
 
-  if (!dzl_str_equal0 (self->build_system_name, build_system_name))
+  if (!ide_str_equal0 (self->build_system_name, build_system_name))
     {
       g_free (self->build_system_name);
       self->build_system_name = g_strdup (build_system_name);
@@ -246,7 +247,7 @@ ide_project_info_set_description (IdeProjectInfo *self,
 {
   g_return_if_fail (IDE_IS_PROJECT_INFO (self));
 
-  if (!dzl_str_equal0 (self->description, description))
+  if (!ide_str_equal0 (self->description, description))
     {
       g_free (self->description);
       self->description = g_strdup (description);
@@ -268,7 +269,7 @@ ide_project_info_set_name (IdeProjectInfo *self,
 {
   g_return_if_fail (IDE_IS_PROJECT_INFO (self));
 
-  if (!dzl_str_equal0 (self->name, name))
+  if (!ide_str_equal0 (self->name, name))
     {
       g_free (self->name);
       self->name = g_strdup (name);
@@ -340,6 +341,7 @@ ide_project_info_finalize (GObject *object)
 {
   IdeProjectInfo *self = (IdeProjectInfo *)object;
 
+  g_clear_pointer (&self->id, g_free);
   g_clear_pointer (&self->last_modified_at, g_date_time_unref);
   g_clear_pointer (&self->build_system_name, g_free);
   g_clear_pointer (&self->description, g_free);
@@ -381,6 +383,10 @@ ide_project_info_get_property (GObject    *object,
       g_value_set_object (value, ide_project_info_get_file (self));
       break;
 
+    case PROP_ID:
+      g_value_set_string (value, ide_project_info_get_id (self));
+      break;
+
     case PROP_IS_RECENT:
       g_value_set_boolean (value, ide_project_info_get_is_recent (self));
       break;
@@ -402,7 +408,7 @@ ide_project_info_get_property (GObject    *object,
       break;
 
     case PROP_VCS_URI:
-      g_value_set_boxed (value, ide_project_info_get_vcs_uri (self));
+      g_value_set_string (value, ide_project_info_get_vcs_uri (self));
       break;
 
     default:
@@ -440,6 +446,10 @@ ide_project_info_set_property (GObject      *object,
       ide_project_info_set_file (self, g_value_get_object (value));
       break;
 
+    case PROP_ID:
+      ide_project_info_set_id (self, g_value_get_string (value));
+      break;
+
     case PROP_IS_RECENT:
       ide_project_info_set_is_recent (self, g_value_get_boolean (value));
       break;
@@ -461,7 +471,7 @@ ide_project_info_set_property (GObject      *object,
       break;
 
     case PROP_VCS_URI:
-      ide_project_info_set_vcs_uri (self, g_value_get_boxed (value));
+      ide_project_info_set_vcs_uri (self, g_value_get_string (value));
       break;
 
     default:
@@ -483,63 +493,70 @@ ide_project_info_class_init (IdeProjectInfoClass *klass)
                          "Build System name",
                          "Build System name",
                          NULL,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_DESCRIPTION] =
     g_param_spec_string ("description",
                          "Description",
                          "The project description.",
                          NULL,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ID] =
+    g_param_spec_string ("id",
+                         "Id",
+                         "The identifier for the project",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_NAME] =
     g_param_spec_string ("name",
                          "Name",
                          "The project name.",
                          NULL,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_DIRECTORY] =
     g_param_spec_object ("directory",
                          "Directory",
                          "The project directory.",
                          G_TYPE_FILE,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_DOAP] =
     g_param_spec_object ("doap",
                          "DOAP",
                          "A DOAP describing the project.",
                          IDE_TYPE_DOAP,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_FILE] =
     g_param_spec_object ("file",
                          "File",
                          "The toplevel project file.",
                          G_TYPE_FILE,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_IS_RECENT] =
     g_param_spec_boolean ("is-recent",
                           "Is Recent",
                           "Is Recent",
                           FALSE,
-                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_LANGUAGES] =
     g_param_spec_boxed ("languages",
                         "Languages",
                         "Languages",
                         G_TYPE_STRV,
-                        (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_LAST_MODIFIED_AT] =
     g_param_spec_boxed ("last-modified-at",
                         "Last Modified At",
                         "Last Modified At",
                         G_TYPE_DATE_TIME,
-                        (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_PRIORITY] =
     g_param_spec_int ("priority",
@@ -548,14 +565,14 @@ ide_project_info_class_init (IdeProjectInfoClass *klass)
                       G_MININT,
                       G_MAXINT,
                       0,
-                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                      (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_VCS_URI] =
-    g_param_spec_boxed ("vcs-uri",
-                        "Vcs Uri",
-                        "The vcs uri of the project, in case it is not local",
-                        IDE_TYPE_VCS_URI,
-                        (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+    g_param_spec_string ("vcs-uri",
+                         "Vcs Uri",
+                         "The VCS URI of the project, in case it is not local",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, LAST_PROP, properties);
 }
@@ -580,6 +597,9 @@ ide_project_info_compare (IdeProjectInfo *info1,
   g_assert (IDE_IS_PROJECT_INFO (info1));
   g_assert (IDE_IS_PROJECT_INFO (info2));
 
+  if (info1 == info2)
+    return 0;
+
   prio1 = ide_project_info_get_priority (info1);
   prio2 = ide_project_info_get_priority (info2);
 
@@ -609,7 +629,7 @@ ide_project_info_compare (IdeProjectInfo *info1,
  * ide_project_info_get_vcs_uri:
  * @self: an #IdeProjectInfo
  *
- * Gets the #IdeVcsUri for the project info. This should be set with the
+ * Gets the VCS URI for the project info. This should be set with the
  * remote URI for the version control system. It can be used to clone the
  * project when activated from the greeter.
  *
@@ -617,7 +637,7 @@ ide_project_info_compare (IdeProjectInfo *info1,
  *
  * Since: 3.32
  */
-IdeVcsUri *
+const gchar *
 ide_project_info_get_vcs_uri (IdeProjectInfo *self)
 {
   g_return_val_if_fail (IDE_IS_PROJECT_INFO (self), NULL);
@@ -627,14 +647,114 @@ ide_project_info_get_vcs_uri (IdeProjectInfo *self)
 
 void
 ide_project_info_set_vcs_uri (IdeProjectInfo *self,
-                              IdeVcsUri      *vcs_uri)
+                              const gchar    *vcs_uri)
 {
   g_return_if_fail (IDE_IS_PROJECT_INFO (self));
 
-  if (self->vcs_uri != vcs_uri)
+  if (!ide_str_equal0 (self->vcs_uri, vcs_uri))
     {
-      g_clear_pointer (&self->vcs_uri, ide_vcs_uri_unref);
-      self->vcs_uri = ide_vcs_uri_ref (vcs_uri);
+      g_free (self->vcs_uri);
+      self->vcs_uri = g_strdup (vcs_uri);
       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VCS_URI]);
     }
 }
+
+IdeProjectInfo *
+ide_project_info_new (void)
+{
+  return g_object_new (IDE_TYPE_PROJECT_INFO, NULL);
+}
+
+/**
+ * ide_project_info_equal:
+ * @self: a #IdeProjectInfo
+ * @other: a #IdeProjectInfo
+ *
+ * This function will check to see if information about @self and @other are
+ * similar enough that a request to open @other would instead activate
+ * @self. This is useful when a user tries to open the same project twice.
+ *
+ * However, some case is taken to ensure that things like the build system
+ * are the same so that a project may be opened twice with two build systems
+ * as is sometimes necessary when projects are porting to a new build
+ * system.
+ *
+ * Returns: %TRUE if @self and @other are the same project and similar
+ *   enough to be considered equal.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_project_info_equal (IdeProjectInfo *self,
+                        IdeProjectInfo *other)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_INFO (self), FALSE);
+  g_return_val_if_fail (IDE_IS_PROJECT_INFO (other), FALSE);
+
+  if (!self->file || !other->file ||
+      !g_file_equal (self->file, other->file))
+    {
+      if (!self->directory || !other->directory ||
+          !g_file_equal (self->directory, other->directory))
+        return FALSE;
+    }
+
+  /* build-system only set in one of the project-info?
+   * That's fine, we'll consider them the same to avoid over
+   * activating a second workbench
+   */
+  if ((!self->build_system_name && other->build_system_name) ||
+      (self->build_system_name && !other->build_system_name))
+    return TRUE;
+
+  return ide_str_equal0 (self->build_system_name, other->build_system_name);
+}
+
+const gchar *
+ide_project_info_get_id (IdeProjectInfo *self)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_INFO (self), NULL);
+
+  if (!self->id && self->directory)
+    self->id = g_file_get_basename (self->directory);
+
+  if (!self->id && self->file)
+    {
+      g_autoptr(GFile) parent = g_file_get_parent (self->file);
+      self->id = g_file_get_basename (parent);
+    }
+
+  if (!self->id && self->doap)
+    self->id = g_strdup (ide_doap_get_name (self->doap));
+
+  if (!self->id && self->vcs_uri)
+    {
+      const gchar *path = self->vcs_uri;
+
+      if (strstr (path, "//"))
+        path = strstr (path, "//") + 1;
+
+      if (strchr (path, '/'))
+        path = strchr (path, '/');
+      else if (strrchr (path, ':'))
+        path = strrchr (path, ':');
+
+      self->id = g_path_get_basename (path);
+    }
+
+  return self->id;
+}
+
+void
+ide_project_info_set_id (IdeProjectInfo *self,
+                         const gchar    *id)
+{
+  g_return_if_fail (IDE_IS_PROJECT_INFO (self));
+
+  if (!ide_str_equal0 (id, self->id))
+    {
+      g_free (self->id);
+      self->id = g_strdup (id);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ID]);
+    }
+}
diff --git a/src/libide/projects/ide-project-info.h b/src/libide/projects/ide-project-info.h
index 799e51361..b47307b19 100644
--- a/src/libide/projects/ide-project-info.h
+++ b/src/libide/projects/ide-project-info.h
@@ -1,6 +1,6 @@
 /* ide-project-info.h
  *
- * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,12 +20,14 @@
 
 #pragma once
 
-#include <gio/gio.h>
+#if !defined (IDE_PROJECTS_INSIDE) && !defined (IDE_PROJECTS_COMPILATION)
+# error "Only <libide-projects.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <gio/gio.h>
+#include <libide-core.h>
 
-#include "doap/ide-doap.h"
-#include "vcs/ide-vcs-uri.h"
+#include "ide-doap.h"
 
 G_BEGIN_DECLS
 
@@ -35,63 +37,73 @@ IDE_AVAILABLE_IN_3_32
 G_DECLARE_FINAL_TYPE (IdeProjectInfo, ide_project_info, IDE, PROJECT_INFO, GObject)
 
 IDE_AVAILABLE_IN_3_32
-gint         ide_project_info_compare                (IdeProjectInfo  *info1,
-                                                      IdeProjectInfo  *info2);
+IdeProjectInfo      *ide_project_info_new                   (void);
+IDE_AVAILABLE_IN_3_32
+const gchar         *ide_project_info_get_id                (IdeProjectInfo  *self);
+IDE_AVAILABLE_IN_3_32
+void                 ide_project_info_set_id                (IdeProjectInfo  *self,
+                                                             const gchar     *id);
 IDE_AVAILABLE_IN_3_32
-GFile        *ide_project_info_get_file              (IdeProjectInfo  *self);
+gint                 ide_project_info_compare               (IdeProjectInfo  *info1,
+                                                             IdeProjectInfo  *info2);
 IDE_AVAILABLE_IN_3_32
-IdeDoap      *ide_project_info_get_doap              (IdeProjectInfo  *self);
+gboolean             ide_project_info_equal                 (IdeProjectInfo  *self,
+                                                             IdeProjectInfo  *other);
 IDE_AVAILABLE_IN_3_32
-void          ide_project_info_set_doap              (IdeProjectInfo  *self,
-                                                      IdeDoap         *doap);
+GFile               *ide_project_info_get_file              (IdeProjectInfo  *self);
 IDE_AVAILABLE_IN_3_32
-const gchar  *ide_project_info_get_build_system_name (IdeProjectInfo  *self);
+IdeDoap             *ide_project_info_get_doap              (IdeProjectInfo  *self);
 IDE_AVAILABLE_IN_3_32
-const gchar  *ide_project_info_get_description       (IdeProjectInfo  *self);
+void                 ide_project_info_set_doap              (IdeProjectInfo  *self,
+                                                             IdeDoap         *doap);
 IDE_AVAILABLE_IN_3_32
-GFile        *ide_project_info_get_directory         (IdeProjectInfo  *self);
+const gchar         *ide_project_info_get_build_system_name (IdeProjectInfo  *self);
 IDE_AVAILABLE_IN_3_32
-gboolean      ide_project_info_get_is_recent         (IdeProjectInfo  *self);
+const gchar         *ide_project_info_get_description       (IdeProjectInfo  *self);
 IDE_AVAILABLE_IN_3_32
-gint          ide_project_info_get_priority          (IdeProjectInfo  *self);
+GFile               *ide_project_info_get_directory         (IdeProjectInfo  *self);
 IDE_AVAILABLE_IN_3_32
-GDateTime    *ide_project_info_get_last_modified_at  (IdeProjectInfo  *self);
+gboolean             ide_project_info_get_is_recent         (IdeProjectInfo  *self);
 IDE_AVAILABLE_IN_3_32
-void          ide_project_info_set_last_modified_at  (IdeProjectInfo  *self,
-                                                      GDateTime       *modified_at);
+gint                 ide_project_info_get_priority          (IdeProjectInfo  *self);
 IDE_AVAILABLE_IN_3_32
-const gchar * const *
-              ide_project_info_get_languages         (IdeProjectInfo  *self);
+GDateTime           *ide_project_info_get_last_modified_at  (IdeProjectInfo  *self);
 IDE_AVAILABLE_IN_3_32
-const gchar  *ide_project_info_get_name              (IdeProjectInfo  *self);
+void                 ide_project_info_set_last_modified_at  (IdeProjectInfo  *self,
+                                                             GDateTime       *modified_at);
 IDE_AVAILABLE_IN_3_32
-IdeVcsUri    *ide_project_info_get_vcs_uri           (IdeProjectInfo  *self);
+const gchar * const *ide_project_info_get_languages         (IdeProjectInfo  *self);
 IDE_AVAILABLE_IN_3_32
-void          ide_project_info_set_file              (IdeProjectInfo  *self,
-                                                      GFile           *file);
+const gchar         *ide_project_info_get_name              (IdeProjectInfo  *self);
 IDE_AVAILABLE_IN_3_32
-void          ide_project_info_set_build_system_name (IdeProjectInfo  *self,
-                                                      const gchar     *build_system_name);
+const gchar         *ide_project_info_get_vcs_uri           (IdeProjectInfo  *self);
 IDE_AVAILABLE_IN_3_32
-void          ide_project_info_set_description       (IdeProjectInfo  *self,
-                                                      const gchar     *description);
+void                 ide_project_info_set_file              (IdeProjectInfo  *self,
+                                                             GFile           *file);
 IDE_AVAILABLE_IN_3_32
-void          ide_project_info_set_directory         (IdeProjectInfo  *self,
-                                                      GFile           *directory);
+void                 ide_project_info_set_build_system_name (IdeProjectInfo  *self,
+                                                             const gchar     *build_system_name);
 IDE_AVAILABLE_IN_3_32
-void          ide_project_info_set_is_recent         (IdeProjectInfo  *self,
-                                                      gboolean         is_recent);
+void                 ide_project_info_set_description       (IdeProjectInfo  *self,
+                                                             const gchar     *description);
 IDE_AVAILABLE_IN_3_32
-void          ide_project_info_set_languages         (IdeProjectInfo  *self,
-                                                      gchar          **languages);
+void                 ide_project_info_set_directory         (IdeProjectInfo  *self,
+                                                             GFile           *directory);
 IDE_AVAILABLE_IN_3_32
-void          ide_project_info_set_name              (IdeProjectInfo  *self,
-                                                      const gchar     *name);
+void                 ide_project_info_set_is_recent         (IdeProjectInfo  *self,
+                                                             gboolean         is_recent);
 IDE_AVAILABLE_IN_3_32
-void          ide_project_info_set_priority          (IdeProjectInfo  *self,
-                                                      gint             priority);
+void                 ide_project_info_set_languages         (IdeProjectInfo  *self,
+                                                             gchar          **languages);
 IDE_AVAILABLE_IN_3_32
-void          ide_project_info_set_vcs_uri           (IdeProjectInfo  *self,
-                                                      IdeVcsUri       *uri);
+void                 ide_project_info_set_name              (IdeProjectInfo  *self,
+                                                             const gchar     *name);
+IDE_AVAILABLE_IN_3_32
+void                 ide_project_info_set_priority          (IdeProjectInfo  *self,
+                                                             gint             priority);
+IDE_AVAILABLE_IN_3_32
+void                 ide_project_info_set_vcs_uri           (IdeProjectInfo  *self,
+                                                             const gchar     *vcs_uri);
+
 
 G_END_DECLS
diff --git a/src/libide/projects/ide-project-template.c b/src/libide/projects/ide-project-template.c
new file mode 100644
index 000000000..ac95b5e8c
--- /dev/null
+++ b/src/libide/projects/ide-project-template.c
@@ -0,0 +1,188 @@
+/* ide-project-template.c
+ *
+ * Copyright 2015-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-project-template"
+
+#include "config.h"
+
+#include "ide-project-template.h"
+
+G_DEFINE_INTERFACE (IdeProjectTemplate, ide_project_template, G_TYPE_OBJECT)
+
+static void
+ide_project_template_default_init (IdeProjectTemplateInterface *iface)
+{
+}
+
+gchar *
+ide_project_template_get_id (IdeProjectTemplate *self)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (self), NULL);
+
+  return IDE_PROJECT_TEMPLATE_GET_IFACE (self)->get_id (self);
+}
+
+gchar *
+ide_project_template_get_name (IdeProjectTemplate *self)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (self), NULL);
+
+  return IDE_PROJECT_TEMPLATE_GET_IFACE (self)->get_name (self);
+}
+
+gchar *
+ide_project_template_get_description (IdeProjectTemplate *self)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (self), NULL);
+
+  return IDE_PROJECT_TEMPLATE_GET_IFACE (self)->get_description (self);
+}
+
+/**
+ * ide_project_template_get_widget:
+ * @self: An #IdeProjectTemplate
+ *
+ * Get's the configuration widget for the template if there is one.
+ *
+ * Returns: (transfer none): a #GtkWidget.
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_project_template_get_widget (IdeProjectTemplate *self)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (self), NULL);
+
+  return IDE_PROJECT_TEMPLATE_GET_IFACE (self)->get_widget (self);
+}
+
+/**
+ * ide_project_template_get_languages:
+ * @self: an #IdeProjectTemplate
+ *
+ * Gets the list of languages that this template can support when generating
+ * the project.
+ *
+ * Returns: (transfer full): A newly allocated, NULL terminated list of
+ *   supported languages.
+ *
+ * Since: 3.32
+ */
+gchar **
+ide_project_template_get_languages (IdeProjectTemplate *self)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (self), NULL);
+
+  return IDE_PROJECT_TEMPLATE_GET_IFACE (self)->get_languages (self);
+}
+
+gchar *
+ide_project_template_get_icon_name (IdeProjectTemplate *self)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (self), NULL);
+
+  return IDE_PROJECT_TEMPLATE_GET_IFACE (self)->get_icon_name (self);
+}
+
+/**
+ * ide_project_template_expand_async:
+ * @self: an #IdeProjectTemplate
+ * @params: (element-type utf8 GLib.Variant): A hashtable of template parameters.
+ * @cancellable: (nullable): a #GCancellable or %NULL.
+ * @callback: the callback for the asynchronous operation.
+ * @user_data: user data for @callback.
+ *
+ * Asynchronously requests expansion of the template.
+ *
+ * This may involve creating files and directories on disk as well as
+ * expanding files based on the contents of @params.
+ *
+ * It is expected that this method is only called once on an #IdeProjectTemplate.
+ *
+ * Since: 3.32
+ */
+void
+ide_project_template_expand_async (IdeProjectTemplate  *self,
+                                   GHashTable          *params,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_PROJECT_TEMPLATE (self));
+  g_return_if_fail (params != NULL);
+  g_return_if_fail (g_hash_table_contains (params, "name"));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_PROJECT_TEMPLATE_GET_IFACE (self)->expand_async (self, params, cancellable, callback, user_data);
+}
+
+gboolean
+ide_project_template_expand_finish (IdeProjectTemplate  *self,
+                                    GAsyncResult        *result,
+                                    GError             **error)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_PROJECT_TEMPLATE_GET_IFACE (self)->expand_finish (self, result, error);
+}
+
+/**
+ * ide_project_template_get_priority:
+ * @self: a #IdeProjectTemplate
+ *
+ * Gets the priority of the template. This can be used to sort the templates
+ * in the "new project" view.
+ *
+ * Returns: the priority of the template
+ *
+ * Since: 3.32
+ */
+gint
+ide_project_template_get_priority (IdeProjectTemplate *self)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (self), 0);
+
+  if (IDE_PROJECT_TEMPLATE_GET_IFACE (self)->get_priority)
+    return IDE_PROJECT_TEMPLATE_GET_IFACE (self)->get_priority (self);
+
+  return 0;
+}
+
+gint
+ide_project_template_compare (IdeProjectTemplate *a,
+                              IdeProjectTemplate *b)
+{
+  gint ret;
+
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (a), 0);
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (b), 0);
+
+  ret = ide_project_template_get_priority (a) - ide_project_template_get_priority (b);
+
+  if (ret == 0)
+    {
+      g_autofree gchar *a_name = ide_project_template_get_name (a);
+      g_autofree gchar *b_name = ide_project_template_get_name (b);
+      ret = g_utf8_collate (a_name, b_name);
+    }
+
+  return ret;
+}
diff --git a/src/libide/projects/ide-project-template.h b/src/libide/projects/ide-project-template.h
new file mode 100644
index 000000000..91a342dcc
--- /dev/null
+++ b/src/libide/projects/ide-project-template.h
@@ -0,0 +1,86 @@
+/* ide-project-template.h
+ *
+ * Copyright 2015-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_PROJECTS_INSIDE) && !defined (IDE_PROJECTS_COMPILATION)
+# error "Only <libide-projects.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PROJECT_TEMPLATE (ide_project_template_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeProjectTemplate, ide_project_template, IDE, PROJECT_TEMPLATE, GObject)
+
+struct _IdeProjectTemplateInterface
+{
+  GTypeInterface parent;
+
+  gchar      *(*get_id)          (IdeProjectTemplate   *self);
+  gchar      *(*get_name)        (IdeProjectTemplate   *self);
+  gchar      *(*get_description) (IdeProjectTemplate   *self);
+  GtkWidget  *(*get_widget)      (IdeProjectTemplate   *self);
+  gchar     **(*get_languages)   (IdeProjectTemplate   *self);
+  gchar      *(*get_icon_name)   (IdeProjectTemplate   *self);
+  void        (*expand_async)    (IdeProjectTemplate   *self,
+                                  GHashTable           *params,
+                                  GCancellable         *cancellable,
+                                  GAsyncReadyCallback   callback,
+                                  gpointer              user_data);
+  gboolean    (*expand_finish)   (IdeProjectTemplate   *self,
+                                  GAsyncResult         *result,
+                                  GError              **error);
+  gint        (*get_priority)    (IdeProjectTemplate   *self);
+};
+
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_project_template_get_id          (IdeProjectTemplate  *self);
+IDE_AVAILABLE_IN_3_32
+gint        ide_project_template_get_priority    (IdeProjectTemplate  *self);
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_project_template_get_name        (IdeProjectTemplate  *self);
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_project_template_get_description (IdeProjectTemplate  *self);
+IDE_AVAILABLE_IN_3_32
+GtkWidget  *ide_project_template_get_widget      (IdeProjectTemplate  *self);
+IDE_AVAILABLE_IN_3_32
+gchar     **ide_project_template_get_languages   (IdeProjectTemplate  *self);
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_project_template_get_icon_name   (IdeProjectTemplate  *self);
+IDE_AVAILABLE_IN_3_32
+void        ide_project_template_expand_async    (IdeProjectTemplate   *self,
+                                                  GHashTable           *params,
+                                                  GCancellable         *cancellable,
+                                                  GAsyncReadyCallback   callback,
+                                                  gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean    ide_project_template_expand_finish   (IdeProjectTemplate   *self,
+                                                  GAsyncResult         *result,
+                                                  GError              **error);
+IDE_AVAILABLE_IN_3_32
+gint        ide_project_template_compare         (IdeProjectTemplate   *a,
+                                                  IdeProjectTemplate   *b);
+
+G_END_DECLS
diff --git a/src/libide/projects/ide-project-tree-addin.c b/src/libide/projects/ide-project-tree-addin.c
index 6d6730277..7289dd94c 100644
--- a/src/libide/projects/ide-project-tree-addin.c
+++ b/src/libide/projects/ide-project-tree-addin.c
@@ -22,7 +22,7 @@
 
 #include "config.h"
 
-#include "projects/ide-project-tree-addin.h"
+#include "ide-project-tree-addin.h"
 
 /**
  * SECTION:ide-project-tree-addin
diff --git a/src/libide/projects/ide-project-tree-addin.h b/src/libide/projects/ide-project-tree-addin.h
index 7b6a81ca5..33412fb99 100644
--- a/src/libide/projects/ide-project-tree-addin.h
+++ b/src/libide/projects/ide-project-tree-addin.h
@@ -21,8 +21,7 @@
 #pragma once
 
 #include <dazzle.h>
-
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/libide/projects/ide-project.c b/src/libide/projects/ide-project.c
index 1a2b28dfc..7b7b2ebfb 100644
--- a/src/libide/projects/ide-project.c
+++ b/src/libide/projects/ide-project.c
@@ -1,6 +1,6 @@
 /* ide-project.c
  *
- * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -23,30 +23,15 @@
 #include "config.h"
 
 #include <glib/gi18n.h>
+#include <libide-code.h>
 
-#include "ide-context.h"
-#include "ide-debug.h"
-
-#include "application/ide-application.h"
-#include "buffers/ide-buffer.h"
-#include "buffers/ide-buffer-manager.h"
-#include "files/ide-file.h"
-#include "projects/ide-project-item.h"
-#include "projects/ide-project.h"
-#include "subprocess/ide-subprocess.h"
-#include "subprocess/ide-subprocess-launcher.h"
-#include "util/ide-flatpak.h"
-#include "vcs/ide-vcs.h"
-#include "threading/ide-task.h"
+#include "ide-buffer-private.h"
+
+#include "ide-project.h"
 
 struct _IdeProject
 {
-  IdeObject       parent_instance;
-
-  GRWLock         rw_lock;
-  IdeProjectItem *root;
-  gchar          *name;
-  gchar          *id;
+  IdeObject parent_instance;
 };
 
 typedef struct
@@ -56,250 +41,19 @@ typedef struct
   IdeBuffer *buffer;
 } RenameFile;
 
-G_DEFINE_TYPE (IdeProject, ide_project, IDE_TYPE_OBJECT)
-
-enum {
-  PROP_0,
-  PROP_ID,
-  PROP_NAME,
-  PROP_ROOT,
-  LAST_PROP
-};
-
 enum {
   FILE_RENAMED,
   FILE_TRASHED,
-  LAST_SIGNAL
+  N_SIGNALS
 };
 
-static GParamSpec *properties [LAST_PROP];
-static guint signals [LAST_SIGNAL];
-
-void
-ide_project_reader_lock (IdeProject *self)
-{
-  g_return_if_fail (IDE_IS_PROJECT (self));
-
-  g_rw_lock_reader_lock (&self->rw_lock);
-}
-
-void
-ide_project_reader_unlock (IdeProject *self)
-{
-  g_return_if_fail (IDE_IS_PROJECT (self));
-
-  g_rw_lock_reader_unlock (&self->rw_lock);
-}
-
-void
-ide_project_writer_lock (IdeProject *self)
-{
-  g_return_if_fail (IDE_IS_PROJECT (self));
-
-  g_rw_lock_writer_lock (&self->rw_lock);
-}
-
-void
-ide_project_writer_unlock (IdeProject *self)
-{
-  g_return_if_fail (IDE_IS_PROJECT (self));
-
-  g_rw_lock_writer_unlock (&self->rw_lock);
-}
-
-/**
- * ide_project_create_id:
- * @name: the name of the project
- *
- * Escapes the project name into something suitable using as an id.
- * This can be uesd to determine the directory name when the project
- * name should be used.
- *
- * Returns: (transfer full): a new string
- *
- * Since: 3.32
- */
-gchar *
-ide_project_create_id (const gchar *name)
-{
-  g_return_val_if_fail (name != NULL, NULL);
-
-  return g_strdelimit (g_strdup (name), " /|<>\n\t", '-');
-}
-
-const gchar *
-ide_project_get_id (IdeProject *self)
-{
-  g_return_val_if_fail (IDE_IS_PROJECT (self), NULL);
-
-  return self->id;
-}
-
-const gchar *
-ide_project_get_name (IdeProject *self)
-{
-  g_return_val_if_fail (IDE_IS_PROJECT (self), NULL);
-
-  return self->name;
-}
-
-void
-_ide_project_set_name (IdeProject  *self,
-                       const gchar *name)
-{
-  g_return_if_fail (IDE_IS_PROJECT (self));
-
-  if (self->name != name)
-    {
-      g_free (self->name);
-      self->name = g_strdup (name);
-      self->id = ide_project_create_id (name);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_NAME]);
-    }
-}
-
-/**
- * ide_project_get_root:
- *
- * Retrieves the root item of the project tree.
- *
- * You must be holding the reader lock while calling and using the result of
- * this function. Other thread may be accessing or modifying the tree without
- * your knowledge. See ide_project_reader_lock() and ide_project_reader_unlock()
- * for more information.
- *
- * If you need to modify the tree, you must hold a writer lock that has been
- * acquired with ide_project_writer_lock() and released with
- * ide_project_writer_unlock() when you are no longer modifiying the tree.
- *
- * Returns: (transfer none): An #IdeProjectItem.
- *
- * Since: 3.32
- */
-IdeProjectItem *
-ide_project_get_root (IdeProject *self)
-{
-  g_return_val_if_fail (IDE_IS_PROJECT (self),  NULL);
-
-  return self->root;
-}
-
-static void
-ide_project_set_root (IdeProject     *self,
-                      IdeProjectItem *root)
-{
-  g_autoptr(IdeProjectItem) allocated = NULL;
-  IdeContext *context;
-
-  g_return_if_fail (IDE_IS_PROJECT (self));
-  g_return_if_fail (!root || IDE_IS_PROJECT_ITEM (root));
-
-  context = ide_object_get_context (IDE_OBJECT (self));
-
-  if (!root)
-    {
-      allocated = g_object_new (IDE_TYPE_PROJECT_ITEM,
-                                "context", context,
-                                NULL);
-      root = allocated;
-    }
-
-  if (g_set_object (&self->root, root))
-    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ROOT]);
-}
-
-static void
-ide_project_finalize (GObject *object)
-{
-  IdeProject *self = (IdeProject *)object;
-
-  g_clear_object (&self->root);
-  g_clear_pointer (&self->name, g_free);
-  g_rw_lock_clear (&self->rw_lock);
-
-  G_OBJECT_CLASS (ide_project_parent_class)->finalize (object);
-}
-
-static void
-ide_project_get_property (GObject    *object,
-                          guint       prop_id,
-                          GValue     *value,
-                          GParamSpec *pspec)
-{
-  IdeProject *self = IDE_PROJECT (object);
-
-  switch (prop_id)
-    {
-    case PROP_ID:
-      g_value_set_string (value, ide_project_get_id (self));
-      break;
-
-    case PROP_NAME:
-      g_value_set_string (value, ide_project_get_name (self));
-      break;
-
-    case PROP_ROOT:
-      g_value_set_object (value, ide_project_get_root (self));
-      break;
-
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-    }
-}
-
-static void
-ide_project_set_property (GObject      *object,
-                          guint         prop_id,
-                          const GValue *value,
-                          GParamSpec   *pspec)
-{
-  IdeProject *self = IDE_PROJECT (object);
+G_DEFINE_TYPE (IdeProject, ide_project, IDE_TYPE_OBJECT)
 
-  switch (prop_id)
-    {
-    case PROP_ROOT:
-      ide_project_set_root (self, g_value_get_object (value));
-      break;
-
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-    }
-}
+static guint signals [N_SIGNALS];
 
 static void
 ide_project_class_init (IdeProjectClass *klass)
 {
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
-
-  object_class->finalize = ide_project_finalize;
-  object_class->get_property = ide_project_get_property;
-  object_class->set_property = ide_project_set_property;
-
-  properties [PROP_ID] =
-    g_param_spec_string ("id",
-                         "ID",
-                         "The unique project identifier.",
-                         NULL,
-                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
-
-  properties [PROP_NAME] =
-    g_param_spec_string ("name",
-                         "Name",
-                         "The name of the project.",
-                         NULL,
-                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
-
-  properties [PROP_ROOT] =
-    g_param_spec_object ("root",
-                         "Root",
-                         "The root object for the project.",
-                         IDE_TYPE_PROJECT_ITEM,
-                         (G_PARAM_READWRITE |
-                          G_PARAM_CONSTRUCT_ONLY |
-                          G_PARAM_STATIC_STRINGS));
-
-  g_object_class_install_properties (object_class, LAST_PROP, properties);
-
   signals [FILE_RENAMED] =
     g_signal_new ("file-renamed",
                   G_TYPE_FROM_CLASS (klass),
@@ -318,7 +72,31 @@ ide_project_class_init (IdeProjectClass *klass)
 static void
 ide_project_init (IdeProject *self)
 {
-  g_rw_lock_init (&self->rw_lock);
+}
+
+/**
+ * ide_project_from_context:
+ * @context: #IdeContext
+ *
+ * Gets the project for an #IdeContext.
+ *
+ * Returns: (transfer none): an #IdeProject
+ *
+ * Since: 3.32
+ */
+IdeProject *
+ide_project_from_context (IdeContext *context)
+{
+  IdeProject *self;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  /* Return borrowed reference */
+  self = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_PROJECT);
+  g_object_unref (self);
+
+  return self;
 }
 
 static void
@@ -372,11 +150,10 @@ ide_project_rename_file_worker (IdeTask      *task,
   IdeProject *self = source_object;
   g_autofree gchar *path = NULL;
   g_autoptr(GFile) parent = NULL;
+  g_autoptr(GFile) workdir = NULL;
   g_autoptr(GError) error = NULL;
   RenameFile *op = task_data;
   IdeContext *context;
-  IdeVcs *vcs;
-  GFile *workdir;
 
   g_assert (IDE_IS_PROJECT (self));
   g_assert (op != NULL);
@@ -385,8 +162,7 @@ ide_project_rename_file_worker (IdeTask      *task,
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
-  workdir = ide_vcs_get_working_directory (vcs);
+  workdir = ide_context_ref_workdir (context);
   path = g_file_get_relative_path (workdir, op->new_file);
 
 #ifdef IDE_ENABLE_TRACE
@@ -440,19 +216,17 @@ ide_project_rename_buffer_save_cb (GObject      *object,
                                    GAsyncResult *result,
                                    gpointer      user_data)
 {
-  IdeBufferManager *bufmgr = (IdeBufferManager *)object;
+  IdeBuffer *buffer = (IdeBuffer *)object;
   g_autoptr(IdeTask) task = user_data;
-  g_autoptr(IdeFile) file = NULL;
   g_autoptr(GError) error = NULL;
-  IdeContext *context;
   RenameFile *rf;
 
   g_assert (IDE_IS_MAIN_THREAD ());
-  g_assert (IDE_IS_BUFFER_MANAGER (bufmgr));
+  g_assert (IDE_IS_BUFFER (buffer));
   g_assert (G_IS_ASYNC_RESULT (result));
   g_assert (IDE_IS_TASK (task));
 
-  if (!ide_buffer_manager_save_file_finish (bufmgr, result, &error))
+  if (!ide_buffer_save_file_finish (buffer, result, &error))
     {
       ide_task_return_error (task, g_steal_pointer (&error));
       return;
@@ -468,9 +242,7 @@ ide_project_rename_buffer_save_cb (GObject      *object,
    * Change the filename in the buffer so that the user doesn't continue
    * to edit the file under the old name.
    */
-  context = ide_object_get_context (IDE_OBJECT (bufmgr));
-  file = ide_file_new (context, rf->new_file);
-  ide_buffer_set_file (rf->buffer, file);
+  _ide_buffer_set_file (rf->buffer, rf->new_file);
 
   ide_task_run_in_thread (task, ide_project_rename_file_worker);
 }
@@ -500,7 +272,7 @@ ide_project_rename_file_async (IdeProject          *self,
   ide_task_set_priority (task, G_PRIORITY_LOW);
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  bufmgr = ide_context_get_buffer_manager (context);
+  bufmgr = ide_buffer_manager_from_context (context);
   buffer = ide_buffer_manager_find_buffer (bufmgr, orig_file);
 
   op = g_slice_new0 (RenameFile);
@@ -515,22 +287,18 @@ ide_project_rename_file_async (IdeProject          *self,
    */
   if (buffer != NULL)
     {
-      g_autoptr(IdeFile) from = ide_file_new (context, orig_file);
-      g_autoptr(IdeFile) to = ide_file_new (context, new_file);
-
       if (gtk_text_buffer_get_modified (GTK_TEXT_BUFFER (buffer)))
         {
-          ide_buffer_manager_save_file_async (bufmgr,
-                                              buffer,
-                                              from,
-                                              NULL,
-                                              NULL,
-                                              ide_project_rename_buffer_save_cb,
-                                              g_steal_pointer (&task));
+          ide_buffer_save_file_async (buffer,
+                                      orig_file,
+                                      NULL,
+                                      NULL,
+                                      ide_project_rename_buffer_save_cb,
+                                      g_steal_pointer (&task));
           return;
         }
 
-      ide_buffer_set_file (buffer, to);
+      _ide_buffer_set_file (buffer, new_file);
     }
 
   ide_task_run_in_thread (task, ide_project_rename_file_worker);
@@ -626,9 +394,8 @@ ide_project_trash_file_async (IdeProject          *self,
                               gpointer             user_data)
 {
   g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GFile) workdir = NULL;
   IdeContext *context;
-  IdeVcs *vcs;
-  GFile *workdir;
 
   IDE_ENTRY;
 
@@ -636,8 +403,7 @@ ide_project_trash_file_async (IdeProject          *self,
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
-  workdir = ide_vcs_get_working_directory (vcs);
+  workdir = ide_context_ref_workdir (context);
 
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_source_tag (task, ide_project_trash_file_async);
diff --git a/src/libide/projects/ide-project.h b/src/libide/projects/ide-project.h
index f3240ac24..ebf2b28b0 100644
--- a/src/libide/projects/ide-project.h
+++ b/src/libide/projects/ide-project.h
@@ -1,6 +1,6 @@
 /* ide-project.h
  *
- * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,9 +20,7 @@
 
 #pragma once
 
-#include "ide-version-macros.h"
-
-#include "ide-object.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
@@ -32,21 +30,7 @@ IDE_AVAILABLE_IN_3_32
 G_DECLARE_FINAL_TYPE (IdeProject, ide_project, IDE, PROJECT, IdeObject)
 
 IDE_AVAILABLE_IN_3_32
-gchar           *ide_project_create_id          (const gchar          *name);
-IDE_AVAILABLE_IN_3_32
-IdeProjectItem  *ide_project_get_root           (IdeProject           *self);
-IDE_AVAILABLE_IN_3_32
-const gchar     *ide_project_get_name           (IdeProject           *self);
-IDE_AVAILABLE_IN_3_32
-const gchar     *ide_project_get_id             (IdeProject           *self);
-IDE_AVAILABLE_IN_3_32
-void             ide_project_reader_lock        (IdeProject           *self);
-IDE_AVAILABLE_IN_3_32
-void             ide_project_reader_unlock      (IdeProject           *self);
-IDE_AVAILABLE_IN_3_32
-void             ide_project_writer_lock        (IdeProject           *self);
-IDE_AVAILABLE_IN_3_32
-void             ide_project_writer_unlock      (IdeProject           *self);
+IdeProject      *ide_project_from_context       (IdeContext           *context);
 IDE_AVAILABLE_IN_3_32
 void             ide_project_rename_file_async  (IdeProject           *self,
                                                  GFile                *orig_file,
@@ -68,7 +52,5 @@ IDE_AVAILABLE_IN_3_32
 gboolean         ide_project_trash_file_finish  (IdeProject           *self,
                                                  GAsyncResult         *result,
                                                  GError              **error);
-void             _ide_project_set_name          (IdeProject           *project,
-                                                 const gchar          *name) G_GNUC_INTERNAL;
 
 G_END_DECLS
diff --git a/src/libide/projects/ide-projects-global.c b/src/libide/projects/ide-projects-global.c
new file mode 100644
index 000000000..d762d072c
--- /dev/null
+++ b/src/libide/projects/ide-projects-global.c
@@ -0,0 +1,132 @@
+/* ide-projects-global.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-projects-global"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-io.h>
+
+#include "ide-projects-global.h"
+
+static GSettings *g_settings;
+static gchar *projects_directory;
+
+static void
+on_projects_directory_changed_cb (GSettings   *settings,
+                                  const gchar *key,
+                                  gpointer     user_data)
+{
+  g_assert (G_IS_SETTINGS (settings));
+  g_assert (key != NULL);
+
+  g_clear_pointer (&projects_directory, g_free);
+}
+
+/**
+ * ide_get_projects_dir:
+ *
+ * Gets the directory to store projects within.
+ *
+ * First, this checks GSettings for a directory. If that directory exists,
+ * it is returned.
+ *
+ * If not, it then checks for the non-translated name "Projects" in the
+ * users home directory. If it exists, that is returned.
+ *
+ * If that does not exist, and a GSetting path existed, but was non-existant
+ * that is returned.
+ *
+ * If the GSetting was empty, the translated name "Projects" is returned.
+ *
+ * Returns: (not nullable) (transfer full): a #GFile
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_get_projects_dir (void)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+
+  if G_UNLIKELY (g_settings == NULL)
+    {
+      g_settings = g_settings_new ("org.gnome.builder");
+      g_signal_connect (g_settings,
+                        "changed::projects-directory",
+                        G_CALLBACK (on_projects_directory_changed_cb),
+                        NULL);
+    }
+
+  if G_UNLIKELY (projects_directory == NULL)
+    {
+      g_autofree gchar *dir = g_settings_get_string (g_settings, "projects-directory");
+      g_autofree gchar *expanded = ide_path_expand (dir);
+      g_autofree gchar *projects = NULL;
+      g_autofree gchar *translated = NULL;
+
+      if (g_file_test (expanded, G_FILE_TEST_IS_DIR))
+        {
+          projects_directory = g_steal_pointer (&expanded);
+          goto completed;
+        }
+
+      projects = g_build_filename (g_get_home_dir (), "Projects", NULL);
+
+      if (g_file_test (projects, G_FILE_TEST_IS_DIR))
+        {
+          projects_directory = g_steal_pointer (&projects);
+          goto completed;
+        }
+
+      if (!ide_str_empty0 (dir) && !ide_str_empty0 (expanded))
+        {
+          projects_directory = g_steal_pointer (&expanded);
+          goto completed;
+        }
+
+      translated = g_build_filename (g_get_home_dir (), _("Projects"), NULL);
+      projects_directory = g_steal_pointer (&translated);
+    }
+
+completed:
+
+  return projects_directory;
+}
+
+/**
+ * ide_create_project_id:
+ * @name: the name of the project
+ *
+ * Escapes the project name into something suitable using as an id.
+ * This can be uesd to determine the directory name when the project
+ * name should be used.
+ *
+ * Returns: (transfer full): a new string
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_create_project_id (const gchar *name)
+{
+  g_return_val_if_fail (name != NULL, NULL);
+
+  return g_strdelimit (g_strdup (name), " /|<>\n\t", '-');
+}
diff --git a/src/libide/projects/ide-project-edit-private.h b/src/libide/projects/ide-projects-global.h
similarity index 65%
rename from src/libide/projects/ide-project-edit-private.h
rename to src/libide/projects/ide-projects-global.h
index 0faba79b1..f32401be4 100644
--- a/src/libide/projects/ide-project-edit-private.h
+++ b/src/libide/projects/ide-projects-global.h
@@ -1,6 +1,6 @@
-/* ide-project-edit-private.h
+/* ide-projects-global.h
  *
- * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,14 +20,17 @@
 
 #pragma once
 
-#include "buffers/ide-buffer.h"
-#include "projects/ide-project-edit.h"
+#if !defined (IDE_PROJECTS_INSIDE) && !defined (IDE_PROJECTS_COMPILATION)
+# error "Only <libide-projects.h> can be included directly."
+#endif
+
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
-void _ide_project_edit_prepare (IdeProjectEdit *self,
-                                IdeBuffer      *buffer);
-void _ide_project_edit_apply   (IdeProjectEdit *self,
-                                IdeBuffer      *buffer);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_get_projects_dir  (void);
+IDE_AVAILABLE_IN_3_32
+gchar       *ide_create_project_id (const gchar *name);
 
 G_END_DECLS
diff --git a/src/libide/projects/ide-recent-projects.c b/src/libide/projects/ide-recent-projects.c
index 7ace9d079..53ccbfca5 100644
--- a/src/libide/projects/ide-recent-projects.c
+++ b/src/libide/projects/ide-recent-projects.c
@@ -1,6 +1,6 @@
 /* ide-recent-projects.c
  *
- * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,10 +24,9 @@
 
 #include <glib/gi18n.h>
 #include <gtk/gtk.h>
+#include <libide-core.h>
 
-#include "ide-global.h"
-
-#include "projects/ide-recent-projects.h"
+#include "ide-recent-projects.h"
 
 struct _IdeRecentProjects
 {
@@ -53,6 +52,29 @@ ide_recent_projects_new (void)
   return g_object_new (IDE_TYPE_RECENT_PROJECTS, NULL);
 }
 
+/**
+ * ide_recent_projects_get_default:
+ *
+ * Gets a shared #IdeRecentProjects instance.
+ *
+ * If this instance is unref'd, a new instance will be created on the next
+ * request to get the default #IdeRecentProjects instance.
+ *
+ * Returns: (transfer none): an #IdeRecentProjects
+ *
+ * Since: 3.32
+ */
+IdeRecentProjects *
+ide_recent_projects_get_default (void)
+{
+  static IdeRecentProjects *instance;
+
+  if (instance == NULL)
+    g_set_weak_pointer (&instance, ide_recent_projects_new ());
+
+  return instance;
+}
+
 static void
 ide_recent_projects_added (IdeRecentProjects *self,
                            IdeProjectInfo    *project_info)
@@ -87,22 +109,23 @@ static GBookmarkFile *
 ide_recent_projects_get_bookmarks (IdeRecentProjects  *self,
                                    GError            **error)
 {
-  GBookmarkFile *bookmarks;
+  g_autoptr(GBookmarkFile) bookmarks = NULL;
+  g_autoptr(GError) local_error = NULL;
 
   g_assert (IDE_IS_RECENT_PROJECTS (self));
 
   bookmarks = g_bookmark_file_new ();
 
-  if (!g_bookmark_file_load_from_file (bookmarks, self->file_uri, error))
+  if (!g_bookmark_file_load_from_file (bookmarks, self->file_uri, &local_error))
     {
-      if (!g_error_matches (*error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+      if (!g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
         {
-          g_object_unref (bookmarks);
+          g_propagate_error (error, g_steal_pointer (&local_error));
           return NULL;
         }
     }
 
-  return bookmarks;
+  return g_steal_pointer (&bookmarks);
 }
 
 static void
@@ -112,13 +135,10 @@ ide_recent_projects_load_recent (IdeRecentProjects *self)
   g_autoptr(GError) error = NULL;
   gboolean needs_sync = FALSE;
   gchar **uris;
-  gssize z;
 
   g_assert (IDE_IS_RECENT_PROJECTS (self));
 
-  projects_file = ide_recent_projects_get_bookmarks (self, &error);
-
-  if (projects_file == NULL)
+  if (!(projects_file = ide_recent_projects_get_bookmarks (self, &error)))
     {
       g_warning ("Unable to open recent projects file: %s", error->message);
       return;
@@ -126,7 +146,7 @@ ide_recent_projects_load_recent (IdeRecentProjects *self)
 
   uris = g_bookmark_file_get_uris (projects_file, NULL);
 
-  for (z = 0; uris[z]; z++)
+  for (gsize z = 0; uris[z]; z++)
     {
       g_autoptr(GDateTime) last_modified_at = NULL;
       g_autoptr(GFile) project_file = NULL;
@@ -137,14 +157,14 @@ ide_recent_projects_load_recent (IdeRecentProjects *self)
       g_autofree gchar *description = NULL;
       const gchar *build_system_name = NULL;
       const gchar *uri = uris[z];
+      const gchar *diruri = NULL;
       time_t modified;
       g_auto(GStrv) groups = NULL;
       gsize len;
-      gsize i;
 
       groups = g_bookmark_file_get_groups (projects_file, uri, &len, NULL);
 
-      for (i = 0; i < len; i++)
+      for (gsize i = 0; i < len; i++)
         {
           if (g_str_equal (groups [i], IDE_RECENT_PROJECTS_GROUP))
             goto is_project;
@@ -166,10 +186,20 @@ ide_recent_projects_load_recent (IdeRecentProjects *self)
       description = g_bookmark_file_get_description (projects_file, uri, NULL);
       modified = g_bookmark_file_get_modified  (projects_file, uri, NULL);
       last_modified_at = g_date_time_new_from_unix_local (modified);
-      directory = g_file_get_parent (project_file);
+
+      for (gsize i = 0; i < len; i++)
+        {
+          if (g_str_has_prefix (groups [i], IDE_RECENT_PROJECTS_DIRECTORY))
+            diruri = groups [i] + strlen (IDE_RECENT_PROJECTS_DIRECTORY);
+        }
+
+      if (diruri == NULL)
+        directory = g_file_get_parent (project_file);
+      else
+        directory = g_file_new_for_uri (diruri);
 
       languages = g_ptr_array_new ();
-      for (i = 0; i < len; i++)
+      for (gsize i = 0; i < len; i++)
         {
           if (g_str_has_prefix (groups [i], IDE_RECENT_PROJECTS_LANGUAGE_GROUP_PREFIX))
             g_ptr_array_add (languages, groups [i] + strlen (IDE_RECENT_PROJECTS_LANGUAGE_GROUP_PREFIX));
@@ -282,7 +312,8 @@ ide_recent_projects_init (IdeRecentProjects *self)
 /**
  * ide_recent_projects_remove:
  * @self: An #IdeRecentProjects
- * @project_infos: (transfer none) (element-type Ide.ProjectInfo): a #GList of #IdeProjectInfo.
+ * @project_infos: (transfer none) (element-type IdeProjectInfo): a #GList
+ *   of #IdeProjectInfo.
  *
  * Removes the provided projects from the recent projects file.
  *
@@ -298,9 +329,7 @@ ide_recent_projects_remove (IdeRecentProjects *self,
 
   g_return_if_fail (IDE_IS_RECENT_PROJECTS (self));
 
-  projects_file = ide_recent_projects_get_bookmarks (self, &error);
-
-  if (projects_file == NULL)
+  if (!(projects_file = ide_recent_projects_get_bookmarks (self, &error)))
     {
       g_warning ("Failed to load bookmarks file: %s", error->message);
       return;
@@ -375,7 +404,7 @@ ide_recent_projects_find_by_directory (IdeRecentProjects *self,
   if (!g_file_test (directory, G_FILE_TEST_IS_DIR))
     return NULL;
 
-  if (NULL == (bookmarks = ide_recent_projects_get_bookmarks (self, NULL)))
+  if (!(bookmarks = ide_recent_projects_get_bookmarks (self, NULL)))
     return NULL;
 
   uris = g_bookmark_file_get_uris (bookmarks, &len);
diff --git a/src/libide/projects/ide-recent-projects.h b/src/libide/projects/ide-recent-projects.h
index ee3005d49..d5f4c2435 100644
--- a/src/libide/projects/ide-recent-projects.h
+++ b/src/libide/projects/ide-recent-projects.h
@@ -20,9 +20,8 @@
 
 #pragma once
 
-#include "ide-version-macros.h"
-
-#include "projects/ide-project-info.h"
+#include <libide-core.h>
+#include <libide-projects.h>
 
 G_BEGIN_DECLS
 
@@ -31,11 +30,14 @@ G_BEGIN_DECLS
 #define IDE_RECENT_PROJECTS_GROUP                     "X-GNOME-Builder-Project"
 #define IDE_RECENT_PROJECTS_LANGUAGE_GROUP_PREFIX     "X-GNOME-Builder-Language:"
 #define IDE_RECENT_PROJECTS_BUILD_SYSTEM_GROUP_PREFIX "X-GNOME-Builder-Build-System:"
+#define IDE_RECENT_PROJECTS_DIRECTORY                 "X-GNOME-Builder-Directory:"
 #define IDE_RECENT_PROJECTS_BOOKMARK_FILENAME         "recent-projects.xbel"
 
 IDE_AVAILABLE_IN_3_32
 G_DECLARE_FINAL_TYPE (IdeRecentProjects, ide_recent_projects, IDE, RECENT_PROJECTS, GObject)
 
+IDE_AVAILABLE_IN_3_32
+IdeRecentProjects *ide_recent_projects_get_default       (void);
 IDE_AVAILABLE_IN_3_32
 IdeRecentProjects *ide_recent_projects_new               (void);
 IDE_AVAILABLE_IN_3_32
diff --git a/src/libide/projects/ide-template-base.c b/src/libide/projects/ide-template-base.c
new file mode 100644
index 000000000..81879f3da
--- /dev/null
+++ b/src/libide/projects/ide-template-base.c
@@ -0,0 +1,724 @@
+/* ide-template-base.c
+ *
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-template-base"
+
+#include "config.h"
+
+#include <glib/gstdio.h>
+#include <errno.h>
+#include <libide-threading.h>
+#include <string.h>
+
+#include "ide-template-base.h"
+
+#define TIMEOUT_INTERVAL_MSEC 17
+#define TIMEOUT_DURATION_MSEC  2
+
+typedef struct
+{
+  TmplTemplateLocator *locator;
+  GArray              *files;
+
+  guint                has_expanded : 1;
+} IdeTemplateBasePrivate;
+
+typedef struct
+{
+  GFile        *file;
+  GInputStream *stream;
+  TmplScope    *scope;
+  GFile        *destination;
+  TmplTemplate *template;
+  gchar        *result;
+  gint          mode;
+} FileExpansion;
+
+typedef struct
+{
+  GArray    *files;
+  guint      index;
+  guint      completed;
+} ExpansionTask;
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (IdeTemplateBase, ide_template_base, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_LOCATOR,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+static void
+ide_template_base_mkdirs_worker (IdeTask      *task,
+                                 gpointer      source_object,
+                                 gpointer      task_data,
+                                 GCancellable *cancellable)
+{
+  IdeTemplateBase *self = source_object;
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_TEMPLATE_BASE (self));
+
+  for (guint i = 0; i < priv->files->len; i++)
+    {
+      FileExpansion *fexp = &g_array_index (priv->files, FileExpansion, i);
+      g_autoptr(GFile) directory = NULL;
+      g_autoptr(GError) error = NULL;
+
+      directory = g_file_get_parent (fexp->destination);
+
+      if (!g_file_make_directory_with_parents (directory, cancellable, &error))
+        {
+          if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
+            {
+              ide_task_return_error (task, g_steal_pointer (&error));
+              return;
+            }
+        }
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_template_base_mkdirs_async (IdeTemplateBase     *self,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_TEMPLATE_BASE (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_run_in_thread (task, ide_template_base_mkdirs_worker);
+}
+
+static gboolean
+ide_template_base_mkdirs_finish (IdeTemplateBase  *self,
+                                 GAsyncResult     *result,
+                                 GError          **error)
+{
+  g_assert (IDE_IS_TEMPLATE_BASE (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+/**
+ * ide_template_base_get_locator:
+ * @self: An #IdeTemplateBase
+ *
+ * Fetches the #TmplTemplateLocator used for resolving templates.
+ *
+ * Returns: (transfer none) (nullable): a #TmplTemplateLocator or %NULL.
+ *
+ * Since: 3.32
+ */
+TmplTemplateLocator *
+ide_template_base_get_locator (IdeTemplateBase *self)
+{
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TEMPLATE_BASE (self), NULL);
+
+  return priv->locator;
+}
+
+void
+ide_template_base_set_locator (IdeTemplateBase     *self,
+                               TmplTemplateLocator *locator)
+{
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TEMPLATE_BASE (self));
+  g_return_if_fail (!locator || TMPL_IS_TEMPLATE_LOCATOR (locator));
+
+  if (priv->has_expanded)
+    {
+      g_warning ("Cannot change template locator after "
+                 "ide_template_base_expand_all_async() has been called.");
+      return;
+    }
+
+  if (g_set_object (&priv->locator, locator))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LOCATOR]);
+}
+
+static void
+clear_file_expansion (gpointer data)
+{
+  FileExpansion *expansion = data;
+
+  g_clear_object (&expansion->file);
+  g_clear_object (&expansion->stream);
+  g_clear_pointer (&expansion->scope, tmpl_scope_unref);
+  g_clear_object (&expansion->destination);
+  g_clear_object (&expansion->template);
+  g_clear_pointer (&expansion->result, g_free);
+}
+
+static void
+ide_template_base_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeTemplateBase *self = IDE_TEMPLATE_BASE(object);
+
+  switch (prop_id)
+    {
+    case PROP_LOCATOR:
+      g_value_set_object (value, ide_template_base_get_locator (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+ide_template_base_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  IdeTemplateBase *self = IDE_TEMPLATE_BASE(object);
+
+  switch (prop_id)
+    {
+    case PROP_LOCATOR:
+      ide_template_base_set_locator (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+ide_template_base_finalize (GObject *object)
+{
+  IdeTemplateBase *self = (IdeTemplateBase *)object;
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+
+  g_clear_pointer (&priv->files, g_array_unref);
+  g_clear_object (&priv->locator);
+
+  G_OBJECT_CLASS (ide_template_base_parent_class)->finalize (object);
+}
+
+static void
+ide_template_base_class_init (IdeTemplateBaseClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_template_base_finalize;
+  object_class->get_property = ide_template_base_get_property;
+  object_class->set_property = ide_template_base_set_property;
+
+  /**
+   * IdeTemplateBase:locator:
+   *
+   * The #IdeTemplateBase:locator property contains the #TmplTemplateLocator
+   * that should be used to resolve template includes. If %NULL, templates
+   * will not be allowed to include other templates.
+   * directive.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_LOCATOR] =
+    g_param_spec_object ("locator",
+                         "Locator",
+                         "Locator",
+                         TMPL_TYPE_TEMPLATE_LOCATOR,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+ide_template_base_init (IdeTemplateBase *self)
+{
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+
+  priv->files = g_array_new (FALSE, TRUE, sizeof (FileExpansion));
+  g_array_set_clear_func (priv->files, clear_file_expansion);
+}
+
+static void
+ide_template_base_parse_worker (IdeTask      *task,
+                                gpointer      source_object,
+                                gpointer      task_data,
+                                GCancellable *cancellable)
+{
+  IdeTemplateBase *self = source_object;
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_TEMPLATE_BASE (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  for (guint i = 0; i < priv->files->len; i++)
+    {
+      FileExpansion *fexp = &g_array_index (priv->files, FileExpansion, i);
+      g_autoptr(TmplTemplate) template = NULL;
+      g_autoptr(GError) error = NULL;
+
+      if (fexp->template != NULL)
+        continue;
+
+      template = tmpl_template_new (priv->locator);
+
+      if (!tmpl_template_parse_file (template, fexp->file, cancellable, &error))
+        {
+          ide_task_return_error (task, g_steal_pointer (&error));
+          return;
+        }
+
+      fexp->template = g_object_ref (template);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_template_base_parse_async (IdeTemplateBase     *self,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (IDE_IS_TEMPLATE_BASE (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_run_in_thread (task, ide_template_base_parse_worker);
+}
+
+static gboolean
+ide_template_base_parse_finish (IdeTemplateBase  *self,
+                                GAsyncResult     *result,
+                                GError          **error)
+{
+  g_assert (IDE_IS_TEMPLATE_BASE (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_template_base_replace_cb (GObject      *object,
+                              GAsyncResult *result,
+                              gpointer      user_data)
+{
+  GFile *file = (GFile *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  ExpansionTask *expansion;
+  FileExpansion *fexp = NULL;
+  guint i;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  expansion = ide_task_get_task_data (task);
+
+  g_assert (expansion != NULL);
+  g_assert (expansion->files != NULL);
+
+  expansion->completed++;
+
+  /*
+   * Complete the file replacement operation.
+   */
+  if (!g_file_replace_contents_finish (file, result, NULL, &error))
+    {
+      if (!ide_task_get_completed (task))
+        ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  /*
+   * Locate the FileExpansion. We could remove this by tracking some
+   * state in the callback, but that is more complex than it's worth
+   * since we share the task between all the callbacks.
+   */
+  for (i = 0; i < expansion->files->len; i++)
+    {
+      FileExpansion *item = &g_array_index (expansion->files, FileExpansion, i);
+
+      if (g_file_equal (item->destination, file))
+        {
+          fexp = item;
+          break;
+        }
+    }
+
+  /*
+   * Unfortunately, we don't have a nice portable API to define modes.
+   * So we limit our ability to chmod() to the local file-system.
+   * This still works for things like FUSE, so much as they support
+   * the posix chmod() API.
+   */
+  if ((fexp != NULL) && (fexp->mode != 0) && g_file_is_native (file))
+    {
+      g_autofree gchar *path = g_file_get_path (file);
+
+      if (0 != g_chmod (path, fexp->mode))
+        g_warning ("chmod(\"%s\", 0%o) failed with: %s",
+                   path, fexp->mode, strerror (errno));
+    }
+
+  if (expansion->completed == expansion->files->len)
+    {
+      if (!ide_task_get_completed (task))
+        ide_task_return_boolean (task, TRUE);
+    }
+}
+
+static gboolean
+ide_template_base_expand (IdeTask *task)
+{
+  ExpansionTask *expansion;
+  gint64 end;
+  gint64 now;
+
+  g_assert (IDE_IS_TASK (task));
+
+  expansion = ide_task_get_task_data (task);
+
+  g_assert (expansion != NULL);
+  g_assert (expansion->files != NULL);
+
+  /*
+   * We will only run for up to 2 milliseconds before we want to yield
+   * back to the main loop and schedule future expansions as low-priority
+   * so that we do not block the frame-clock;
+   */
+  for (end = (now = g_get_monotonic_time ()) + ((G_USEC_PER_SEC / 1000) * TIMEOUT_DURATION_MSEC);
+       now < end;
+       now = g_get_monotonic_time ())
+    {
+      FileExpansion *fexp;
+      g_autoptr(GError) error = NULL;
+
+      g_assert (expansion->index <= expansion->files->len);
+
+      if (expansion->index == expansion->files->len)
+        break;
+
+      fexp = &g_array_index (expansion->files, FileExpansion, expansion->index);
+
+      g_assert (fexp != NULL);
+      g_assert (fexp->template != NULL);
+      g_assert (fexp->scope != NULL);
+      g_assert (fexp->result == NULL);
+
+      fexp->result = tmpl_template_expand_string (fexp->template, fexp->scope, &error);
+
+      if (fexp->result == NULL)
+        {
+          ide_task_return_error (task, g_steal_pointer (&error));
+          return G_SOURCE_REMOVE;
+        }
+
+      expansion->index++;
+    }
+
+  /*
+   * If we have completed expanding all the templates, we need to start
+   * writing the results to the destination files asynchronously, and in
+   * parallel. When all of the async operations have completed, we will
+   * cleanup and complete the task.
+   */
+  if (expansion->index == expansion->files->len)
+    {
+      guint i;
+
+      expansion->completed = 0;
+
+      //ide_template_base_make_directories (task);
+
+      for (i = 0; i < expansion->files->len; i++)
+        {
+          FileExpansion *fexp = &g_array_index (expansion->files, FileExpansion, i);
+
+          g_assert (fexp != NULL);
+          g_assert (G_IS_FILE (fexp->destination));
+          g_assert (fexp->result != NULL);
+
+          g_file_replace_contents_async (fexp->destination,
+                                         fexp->result,
+                                         strlen (fexp->result),
+                                         NULL,
+                                         FALSE,
+                                         G_FILE_CREATE_REPLACE_DESTINATION,
+                                         ide_task_get_cancellable (task),
+                                         ide_template_base_replace_cb,
+                                         g_object_ref (task));
+        }
+
+      return G_SOURCE_REMOVE;
+    }
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+ide_template_base_expand_parse_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  IdeTemplateBase *self = (IdeTemplateBase *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_TEMPLATE_BASE (self));
+
+  if (!ide_template_base_parse_finish (self, result, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  g_timeout_add_full (G_PRIORITY_LOW,
+                      TIMEOUT_INTERVAL_MSEC,
+                      (GSourceFunc)ide_template_base_expand,
+                      g_object_ref (task),
+                      g_object_unref);
+}
+
+static void
+ide_template_base_expand_mkdirs_cb (GObject      *object,
+                                    GAsyncResult *result,
+                                    gpointer      user_data)
+{
+  IdeTemplateBase *self = (IdeTemplateBase *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  GCancellable *cancellable;
+
+  g_assert (IDE_IS_TEMPLATE_BASE (self));
+  g_assert (IDE_IS_TASK (task));
+
+  cancellable = ide_task_get_cancellable (task);
+
+  if (!ide_template_base_mkdirs_finish (self, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_template_base_parse_async (self,
+                                   cancellable,
+                                   ide_template_base_expand_parse_cb,
+                                   g_steal_pointer (&task));
+}
+
+void
+ide_template_base_expand_all_async (IdeTemplateBase     *self,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+  ExpansionTask *task_data;
+
+  g_return_if_fail (IDE_IS_TEMPLATE_BASE (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task_data = g_new0 (ExpansionTask, 1);
+  task_data->files = priv->files;
+  task_data->index = 0;
+  task_data->completed = 0;
+
+  /*
+   * The expand process will need to call tmpl_template_expand() and we want
+   * that to happen in the main loop so that all scoped objects need not be
+   * thread-safe.
+   *
+   * Therefore, the first step is to asynchronously load all of the templates
+   * from storage. After that, we will expand the templates into memory,
+   * being careful about how long we run per-cycle in the main-loop. If we
+   * run too long, we risk adding jitter to the frame-clock and causing UI
+   * elements to feel sluggish.
+   *
+   * Once we have all of our templates expanded, we progress to asynchronously
+   * write them to the requested underlying storage.
+   */
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_task_data (task, task_data, g_free);
+
+  /*
+   * You can only call ide_template_base_expand_all_async() once, since we maintain
+   * a bunch of state inline.
+   */
+  if (priv->has_expanded)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_PENDING,
+                                 "%s() has already been called.",
+                                 G_STRFUNC);
+      return;
+    }
+
+  priv->has_expanded = TRUE;
+
+  /*
+   * If we have nothing to do, we still need to preserve our "executed" state.
+   * So if there is nothing to do, short circuit now.
+   */
+  if (priv->files->len == 0)
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  ide_template_base_mkdirs_async (self,
+                                  cancellable,
+                                  ide_template_base_expand_mkdirs_cb,
+                                  g_object_ref (task));
+}
+
+gboolean
+ide_template_base_expand_all_finish (IdeTemplateBase  *self,
+                                     GAsyncResult     *result,
+                                     GError          **error)
+{
+  g_return_val_if_fail (IDE_IS_TEMPLATE_BASE (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static TmplScope *
+create_scope (IdeTemplateBase *self,
+              TmplScope       *parent,
+              GFile           *destination)
+{
+  TmplScope *scope;
+  TmplSymbol *symbol;
+  g_autofree gchar *filename = NULL;
+  g_autofree gchar *year = NULL;
+  g_autoptr(GDateTime) now = NULL;
+
+  g_assert (IDE_IS_TEMPLATE_BASE (self));
+  g_assert (G_IS_FILE (destination));
+
+  scope = tmpl_scope_new_with_parent (parent);
+
+  symbol = tmpl_scope_get (scope, "filename");
+  filename = g_file_get_basename (destination);
+  tmpl_symbol_assign_string (symbol, filename);
+
+  now = g_date_time_new_now_local ();
+  year = g_date_time_format (now, "%Y");
+  symbol = tmpl_scope_get (scope, "year");
+  tmpl_symbol_assign_string (symbol, year);
+
+  return scope;
+}
+
+void
+ide_template_base_add_resource (IdeTemplateBase *self,
+                                const gchar     *resource_path,
+                                GFile           *destination,
+                                TmplScope       *scope,
+                                gint             mode)
+{
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+  FileExpansion expansion = { 0 };
+  g_autofree gchar *uri = NULL;
+
+  g_return_if_fail (IDE_IS_TEMPLATE_BASE (self));
+  g_return_if_fail (resource_path != NULL);
+  g_return_if_fail (G_IS_FILE (destination));
+
+  if (priv->has_expanded)
+    {
+      g_warning ("%s() called after ide_template_base_expand_all_async(). "
+                 "Ignoring request to add resource.",
+                 G_STRFUNC);
+      return;
+    }
+
+  uri = g_strdup_printf ("resource://%s", resource_path);
+
+  expansion.file = g_file_new_for_uri (uri);
+  expansion.stream = NULL;
+  expansion.scope = create_scope (self, scope, destination);
+  expansion.destination = g_object_ref (destination);
+  expansion.result = NULL;
+  expansion.mode = mode;
+
+  g_array_append_val (priv->files, expansion);
+}
+
+void
+ide_template_base_add_path (IdeTemplateBase *self,
+                            const gchar     *path,
+                            GFile           *destination,
+                            TmplScope       *scope,
+                            gint             mode)
+{
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+  FileExpansion expansion = { 0 };
+
+  g_return_if_fail (IDE_IS_TEMPLATE_BASE (self));
+  g_return_if_fail (path != NULL);
+  g_return_if_fail (G_IS_FILE (destination));
+
+  if (priv->has_expanded)
+    {
+      g_warning ("%s() called after ide_template_base_expand_all_async(). "
+                 "Ignoring request to add resource.",
+                 G_STRFUNC);
+      return;
+    }
+
+  expansion.file = g_file_new_for_path (path);
+  expansion.stream = NULL;
+  expansion.scope = create_scope (self, scope, destination);
+  expansion.destination = g_object_ref (destination);
+  expansion.result = NULL;
+  expansion.mode = mode;
+
+  g_array_append_val (priv->files, expansion);
+}
+
+void
+ide_template_base_reset (IdeTemplateBase *self)
+{
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TEMPLATE_BASE (self));
+
+  g_clear_pointer (&priv->files, g_array_unref);
+  priv->files = g_array_new (FALSE, TRUE, sizeof (FileExpansion));
+
+  priv->has_expanded = FALSE;
+}
diff --git a/src/libide/projects/ide-template-base.h b/src/libide/projects/ide-template-base.h
new file mode 100644
index 000000000..83df2affd
--- /dev/null
+++ b/src/libide/projects/ide-template-base.h
@@ -0,0 +1,71 @@
+/* ide-template-base.h
+ *
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_PROJECTS_INSIDE) && !defined (IDE_PROJECTS_COMPILATION)
+# error "Only <libide-projects.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <tmpl-glib.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TEMPLATE_BASE (ide_template_base_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeTemplateBase, ide_template_base, IDE, TEMPLATE_BASE, GObject)
+
+struct _IdeTemplateBaseClass
+{
+  GObjectClass parent_class;
+};
+
+IDE_AVAILABLE_IN_3_32
+TmplTemplateLocator *ide_template_base_get_locator       (IdeTemplateBase       *self);
+IDE_AVAILABLE_IN_3_32
+void                 ide_template_base_set_locator       (IdeTemplateBase       *self,
+                                                          TmplTemplateLocator   *locator);
+IDE_AVAILABLE_IN_3_32
+void                 ide_template_base_add_resource      (IdeTemplateBase       *self,
+                                                          const gchar           *resource_path,
+                                                          GFile                 *destination,
+                                                          TmplScope             *scope,
+                                                          gint                   mode);
+IDE_AVAILABLE_IN_3_32
+void                 ide_template_base_add_path          (IdeTemplateBase       *self,
+                                                          const gchar           *path,
+                                                          GFile                 *destination,
+                                                          TmplScope             *scope,
+                                                          gint                   mode);
+IDE_AVAILABLE_IN_3_32
+void                 ide_template_base_expand_all_async  (IdeTemplateBase       *self,
+                                                          GCancellable          *cancellable,
+                                                          GAsyncReadyCallback    callback,
+                                                          gpointer               user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean             ide_template_base_expand_all_finish (IdeTemplateBase       *self,
+                                                          GAsyncResult          *result,
+                                                          GError               **error);
+IDE_AVAILABLE_IN_3_32
+void                 ide_template_base_reset             (IdeTemplateBase       *self);
+
+G_END_DECLS
diff --git a/src/libide/projects/ide-template-provider.c b/src/libide/projects/ide-template-provider.c
new file mode 100644
index 000000000..58014d433
--- /dev/null
+++ b/src/libide/projects/ide-template-provider.c
@@ -0,0 +1,61 @@
+/* ide-template-provider.c
+ *
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-template-provider"
+
+#include "config.h"
+
+#include "ide-template-provider.h"
+
+G_DEFINE_INTERFACE (IdeTemplateProvider, ide_template_provider, G_TYPE_OBJECT)
+
+static GList *
+ide_template_provider_real_get_project_templates (IdeTemplateProvider *self)
+{
+  return NULL;
+}
+
+static void
+ide_template_provider_default_init (IdeTemplateProviderInterface *iface)
+{
+  iface->get_project_templates = ide_template_provider_real_get_project_templates;
+}
+
+/**
+ * ide_template_provider_get_project_templates:
+ * @self: An #IdeTemplateProvider
+ *
+ * Gets a list of templates for this provider.
+ *
+ * Plugins should implement this interface to feed #IdeProjectTemplate's into
+ * the project creation workflow.
+ *
+ * Returns: (transfer full) (element-type Ide.ProjectTemplate): a #GList of
+ *   #IdeProjectTemplate instances.
+ *
+ * Since: 3.32
+ */
+GList *
+ide_template_provider_get_project_templates (IdeTemplateProvider *self)
+{
+  g_return_val_if_fail (IDE_IS_TEMPLATE_PROVIDER (self), NULL);
+
+  return IDE_TEMPLATE_PROVIDER_GET_IFACE (self)->get_project_templates (self);
+}
diff --git a/src/libide/projects/ide-template-provider.h b/src/libide/projects/ide-template-provider.h
new file mode 100644
index 000000000..8325794ed
--- /dev/null
+++ b/src/libide/projects/ide-template-provider.h
@@ -0,0 +1,48 @@
+/* ide-template-provider.h
+ *
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_PROJECTS_INSIDE) && !defined (IDE_PROJECTS_COMPILATION)
+# error "Only <libide-projects.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-project-template.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TEMPLATE_PROVIDER (ide_template_provider_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeTemplateProvider, ide_template_provider, IDE, TEMPLATE_PROVIDER, GObject)
+
+struct _IdeTemplateProviderInterface
+{
+  GTypeInterface parent_iface;
+
+  GList *(*get_project_templates) (IdeTemplateProvider *self);
+};
+
+IDE_AVAILABLE_IN_3_32
+GList *ide_template_provider_get_project_templates (IdeTemplateProvider *self);
+
+G_END_DECLS
diff --git a/src/libide/projects/libide-projects.h b/src/libide/projects/libide-projects.h
new file mode 100644
index 000000000..f1f7db00d
--- /dev/null
+++ b/src/libide/projects/libide-projects.h
@@ -0,0 +1,40 @@
+/* ide-projects.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-core.h>
+#include <libide-io.h>
+
+#define IDE_PROJECTS_INSIDE
+
+#include "ide-doap.h"
+#include "ide-doap-person.h"
+#include "ide-project.h"
+#include "ide-project-info.h"
+#include "ide-project-file.h"
+#include "ide-project-template.h"
+#include "ide-project-tree-addin.h"
+#include "ide-projects-global.h"
+#include "ide-recent-projects.h"
+#include "ide-template-base.h"
+#include "ide-template-provider.h"
+
+#undef IDE_PROJECTS_INSIDE
diff --git a/src/libide/projects/meson.build b/src/libide/projects/meson.build
index ab7587693..0a68c01cb 100644
--- a/src/libide/projects/meson.build
+++ b/src/libide/projects/meson.build
@@ -1,27 +1,85 @@
-projects_headers = [
-  'ide-project-edit.h',
-  'ide-project-info.h',
-  'ide-project-item.h',
+libide_projects_header_subdir = join_paths(libide_header_subdir, 'projects')
+libide_include_directories += include_directories('.')
+
+#
+# Public API Headers
+#
+
+libide_projects_public_headers = [
+  'ide-doap.h',
+  'ide-doap-person.h',
   'ide-project.h',
+  'ide-project-info.h',
+  'ide-project-file.h',
+  'ide-projects-global.h',
+  'ide-project-template.h',
   'ide-project-tree-addin.h',
   'ide-recent-projects.h',
+  'ide-template-base.h',
+  'ide-template-provider.h',
+  'libide-projects.h',
 ]
 
-projects_sources = [
-  'ide-project-edit.c',
-  'ide-project-info.c',
-  'ide-project-item.c',
+install_headers(libide_projects_public_headers, subdir: libide_projects_header_subdir)
+
+#
+# Sources
+#
+
+libide_projects_private_headers = [ 'xml-reader-private.h', ]
+libide_projects_private_sources = [ 'xml-reader.c', ]
+
+libide_projects_public_sources = [
+  'ide-doap.c',
+  'ide-doap-person.c',
   'ide-project.c',
+  'ide-project-info.c',
+  'ide-project-file.c',
+  'ide-projects-global.c',
+  'ide-project-template.c',
   'ide-project-tree-addin.c',
   'ide-recent-projects.c',
+  'ide-template-base.c',
+  'ide-template-provider.c',
 ]
 
-projects_private_sources = [
-  'ide-project-edit-private.h',
+libide_projects_sources = libide_projects_public_sources + libide_projects_private_sources
+
+#
+# Dependencies
+#
+
+libide_projects_deps = [
+  libgio_dep,
+  libgtk_dep,
+  libdazzle_dep,
+  libtemplate_glib_dep,
+  libxml2_dep,
+
+  libide_core_dep,
+  libide_io_dep,
+  libide_threading_dep,
+  libide_code_dep,
+  libide_vcs_dep,
 ]
 
-libide_public_headers += files(projects_headers)
-libide_public_sources += files(projects_sources)
-libide_private_sources += files(projects_private_sources)
+#
+# Library Definitions
+#
+
+libide_projects = static_library('ide-projects-' + libide_api_version, libide_projects_sources,
+   dependencies: libide_projects_deps,
+         c_args: libide_args + release_args + ['-DIDE_PROJECTS_COMPILATION'],
+)
+
+libide_projects_dep = declare_dependency(
+              sources: libide_projects_private_headers,
+         dependencies: libide_projects_deps,
+           link_whole: libide_projects,
+  include_directories: include_directories('.'),
+)
 
-install_headers(projects_headers, subdir: join_paths(libide_header_subdir, 'projects'))
+gnome_builder_public_sources += files(libide_projects_public_sources)
+gnome_builder_public_headers += files(libide_projects_public_headers)
+gnome_builder_include_subdirs += libide_projects_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-projects.h', '-DIDE_PROJECTS_COMPILATION']
diff --git a/src/libide/doap/xml-reader.h b/src/libide/projects/xml-reader-private.h
similarity index 99%
rename from src/libide/doap/xml-reader.h
rename to src/libide/projects/xml-reader-private.h
index f4be11b22..0c7e574de 100644
--- a/src/libide/doap/xml-reader.h
+++ b/src/libide/projects/xml-reader-private.h
@@ -17,6 +17,8 @@
  *
  * Based upon work by:
  *   Emmanuele Bassi
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #pragma once
diff --git a/src/libide/doap/xml-reader.c b/src/libide/projects/xml-reader.c
similarity index 99%
rename from src/libide/doap/xml-reader.c
rename to src/libide/projects/xml-reader.c
index dc4b6baf8..bcd9ca042 100644
--- a/src/libide/doap/xml-reader.c
+++ b/src/libide/projects/xml-reader.c
@@ -14,13 +14,15 @@
  *
  * Author:
  *   Christian Hergert  <chris dronelabs com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #include <glib/gi18n.h>
 #include <string.h>
 #include <libxml/xmlreader.h>
 
-#include "doap/xml-reader.h"
+#include "xml-reader-private.h"
 
 #define XML_TO_CHAR(s)  ((char *) (s))
 #define CHAR_TO_XML(s)  ((unsigned char *) (s))


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