[gnome-builder/wip/chergert/project-list-view: 4/4] libide/tree: port IdeTree to GtkListView




commit 88df7b17cf5b4e58619cf523bdd83388b088bb15
Author: Christian Hergert <chergert redhat com>
Date:   Tue Sep 20 03:22:46 2022 -0700

    libide/tree: port IdeTree to GtkListView
    
    The goal here is to preserve the main details of IdeTreeAddin, IdeTree,
    and IdeTreeNode to simplify porting efforts.
    
    There is still plenty to do, and particularly to figure out why our list
    seems to get large empty areas drawn to the screen, even though the models
    appear to be correct.

 src/libide/tree/ide-cell-renderer-status.c         |   58 -
 src/libide/tree/ide-cell-renderer-status.h         |   33 -
 src/libide/tree/ide-tree-addin.c                   |   27 +-
 src/libide/tree/ide-tree-addin.h                   |   26 +-
 src/libide/tree/ide-tree-empty.c                   |  204 +++
 .../tree/ide-tree-empty.h}                         |   12 +-
 src/libide/tree/ide-tree-model.c                   | 1599 -----------------
 src/libide/tree/ide-tree-model.h                   |   72 -
 src/libide/tree/ide-tree-node.c                    | 1857 +++++++-------------
 src/libide/tree/ide-tree-node.h                    |  142 +-
 src/libide/tree/ide-tree-private.h                 |   64 +-
 src/libide/tree/ide-tree.c                         | 1158 ++++++------
 src/libide/tree/ide-tree.h                         |   65 +-
 src/libide/tree/ide-tree.ui                        |   28 +
 src/libide/tree/libide-tree.gresource.xml          |    6 +
 src/libide/tree/libide-tree.h                      |    1 -
 src/libide/tree/meson.build                        |   49 +-
 src/plugins/codeui/codeui-plugin.c                 |    5 -
 src/plugins/codeui/gbp-codeui-tree-addin.c         |   89 -
 src/plugins/codeui/meson.build                     |    1 -
 src/plugins/grep/gbp-grep-tree-addin.c             |    8 +-
 src/plugins/project-tree/gbp-project-tree-addin.c  |   81 +-
 .../project-tree/gbp-project-tree-pane-actions.c   |   21 +-
 src/plugins/project-tree/gbp-project-tree-pane.c   |    8 +-
 src/plugins/project-tree/gbp-project-tree-pane.ui  |    8 +-
 src/plugins/project-tree/gbp-project-tree.c        |  204 +--
 src/plugins/vcsui/gbp-vcsui-tree-addin.c           |   52 +-
 27 files changed, 1686 insertions(+), 4192 deletions(-)
---
diff --git a/src/libide/tree/ide-tree-addin.c b/src/libide/tree/ide-tree-addin.c
index 92398cdf6..4fad51145 100644
--- a/src/libide/tree/ide-tree-addin.c
+++ b/src/libide/tree/ide-tree-addin.c
@@ -1,6 +1,6 @@
 /* ide-tree-addin.c
  *
- * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2022 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
@@ -219,28 +219,24 @@ ide_tree_addin_node_activated (IdeTreeAddin *self,
 
 void
 ide_tree_addin_load (IdeTreeAddin *self,
-                     IdeTree      *tree,
-                     IdeTreeModel *model)
+                     IdeTree      *tree)
 {
   g_return_if_fail (IDE_IS_TREE_ADDIN (self));
   g_return_if_fail (IDE_IS_TREE (tree));
-  g_return_if_fail (IDE_IS_TREE_MODEL (model));
 
   if (IDE_TREE_ADDIN_GET_IFACE (self)->load)
-    IDE_TREE_ADDIN_GET_IFACE (self)->load (self, tree, model);
+    IDE_TREE_ADDIN_GET_IFACE (self)->load (self, tree);
 }
 
 void
 ide_tree_addin_unload (IdeTreeAddin *self,
-                       IdeTree      *tree,
-                       IdeTreeModel *model)
+                       IdeTree      *tree)
 {
   g_return_if_fail (IDE_IS_TREE_ADDIN (self));
   g_return_if_fail (IDE_IS_TREE (tree));
-  g_return_if_fail (IDE_IS_TREE_MODEL (model));
 
   if (IDE_TREE_ADDIN_GET_IFACE (self)->unload)
-    IDE_TREE_ADDIN_GET_IFACE (self)->unload (self, tree, model);
+    IDE_TREE_ADDIN_GET_IFACE (self)->unload (self, tree);
 }
 
 void
@@ -343,16 +339,3 @@ ide_tree_addin_node_dropped_finish (IdeTreeAddin  *self,
 
   return IDE_TREE_ADDIN_GET_IFACE (self)->node_dropped_finish (self, result, error);
 }
-
-void
-ide_tree_addin_cell_data_func (IdeTreeAddin    *self,
-                               IdeTreeNode     *node,
-                               GtkCellRenderer *cell)
-{
-  g_return_if_fail (IDE_IS_TREE_ADDIN (self));
-  g_return_if_fail (IDE_IS_TREE_NODE (node));
-  g_return_if_fail (GTK_IS_CELL_RENDERER (cell));
-
-  if (IDE_TREE_ADDIN_GET_IFACE (self)->cell_data_func)
-    IDE_TREE_ADDIN_GET_IFACE (self)->cell_data_func (self, node, cell);
-}
diff --git a/src/libide/tree/ide-tree-addin.h b/src/libide/tree/ide-tree-addin.h
index 6dce9936a..4d947a9df 100644
--- a/src/libide/tree/ide-tree-addin.h
+++ b/src/libide/tree/ide-tree-addin.h
@@ -1,6 +1,6 @@
 /* ide-tree-addin.h
  *
- * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2022 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,10 +20,13 @@
 
 #pragma once
 
+#if !defined (IDE_TREE_INSIDE) && !defined (IDE_TREE_COMPILATION)
+# error "Only <libide-tree.h> can be included directly."
+#endif
+
 #include <libide-core.h>
 
 #include "ide-tree.h"
-#include "ide-tree-model.h"
 #include "ide-tree-node.h"
 
 G_BEGIN_DECLS
@@ -38,11 +41,9 @@ struct _IdeTreeAddinInterface
   GTypeInterface parent;
 
   void     (*load)                  (IdeTreeAddin         *self,
-                                     IdeTree              *tree,
-                                     IdeTreeModel         *model);
+                                     IdeTree              *tree);
   void     (*unload)                (IdeTreeAddin         *self,
-                                     IdeTree              *tree,
-                                     IdeTreeModel         *model);
+                                     IdeTree              *tree);
   void     (*build_node)            (IdeTreeAddin         *self,
                                      IdeTreeNode          *node);
   void     (*build_children)        (IdeTreeAddin         *self,
@@ -55,9 +56,6 @@ struct _IdeTreeAddinInterface
   gboolean (*build_children_finish) (IdeTreeAddin         *self,
                                      GAsyncResult         *result,
                                      GError              **error);
-  void     (*cell_data_func)        (IdeTreeAddin         *self,
-                                     IdeTreeNode          *node,
-                                     GtkCellRenderer      *cell);
   gboolean (*node_activated)        (IdeTreeAddin         *self,
                                      IdeTree              *tree,
                                      IdeTreeNode          *node);
@@ -88,12 +86,10 @@ struct _IdeTreeAddinInterface
 
 IDE_AVAILABLE_IN_ALL
 void     ide_tree_addin_load                  (IdeTreeAddin         *self,
-                                               IdeTree              *tree,
-                                               IdeTreeModel         *model);
+                                               IdeTree              *tree);
 IDE_AVAILABLE_IN_ALL
 void     ide_tree_addin_unload                (IdeTreeAddin         *self,
-                                               IdeTree              *tree,
-                                               IdeTreeModel         *model);
+                                               IdeTree              *tree);
 IDE_AVAILABLE_IN_ALL
 void     ide_tree_addin_build_node            (IdeTreeAddin         *self,
                                                IdeTreeNode          *node);
@@ -141,9 +137,5 @@ IDE_AVAILABLE_IN_ALL
 gboolean ide_tree_addin_node_dropped_finish   (IdeTreeAddin         *self,
                                                GAsyncResult         *result,
                                                GError              **error);
-IDE_AVAILABLE_IN_ALL
-void     ide_tree_addin_cell_data_func        (IdeTreeAddin         *self,
-                                               IdeTreeNode          *node,
-                                               GtkCellRenderer      *cell);
 
 G_END_DECLS
diff --git a/src/libide/tree/ide-tree-empty.c b/src/libide/tree/ide-tree-empty.c
new file mode 100644
index 000000000..52904e4b6
--- /dev/null
+++ b/src/libide/tree/ide-tree-empty.c
@@ -0,0 +1,204 @@
+/* ide-tree-empty.c
+ *
+ * Copyright 2022 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-tree-empty"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-tree-empty.h"
+#include "ide-tree-node.h"
+
+struct _IdeTreeEmpty
+{
+  GObject      parent_instance;
+  GListModel  *model;
+  IdeTreeNode *child;
+  guint        empty : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_MODEL,
+  N_PROPS
+};
+
+static GType
+ide_tree_empty_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_TREE_NODE;
+}
+
+static guint
+ide_tree_empty_get_n_items (GListModel *model)
+{
+  IdeTreeEmpty *self = IDE_TREE_EMPTY (model);
+
+  return self->empty ? 1 : g_list_model_get_n_items (self->model);
+}
+
+static gpointer
+ide_tree_empty_get_item (GListModel *model,
+                         guint       position)
+{
+  IdeTreeEmpty *self = IDE_TREE_EMPTY (model);
+
+  return self->empty ? g_object_ref (self->child) : g_list_model_get_item (self->model, position);
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = ide_tree_empty_get_item_type;
+  iface->get_item = ide_tree_empty_get_item;
+  iface->get_n_items = ide_tree_empty_get_n_items;
+}
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (IdeTreeEmpty, ide_tree_empty, G_TYPE_OBJECT,
+                               G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_tree_empty_items_changed_cb (IdeTreeEmpty *self,
+                                 guint         position,
+                                 guint         removed,
+                                 guint         added,
+                                 GListModel   *model)
+{
+  g_assert (IDE_IS_TREE_EMPTY (self));
+  g_assert (G_IS_LIST_MODEL (model));
+
+  if (added == 0 && removed == 0)
+    return;
+
+  if (self->empty)
+    {
+      g_assert (position == 0);
+      g_assert (removed == 0);
+      g_assert (added > 0);
+
+      self->empty = FALSE;
+      removed = 1;
+    }
+  else if (g_list_model_get_n_items (model) == 0)
+    {
+      g_assert (position == 0);
+      g_assert (added == 0);
+
+      self->empty = TRUE;
+      added = 1;
+    }
+
+  g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added);
+}
+
+static void
+ide_tree_empty_dispose (GObject *object)
+{
+  IdeTreeEmpty *self = (IdeTreeEmpty *)object;
+
+  g_clear_object (&self->child);
+  g_clear_object (&self->model);
+
+  G_OBJECT_CLASS (ide_tree_empty_parent_class)->dispose (object);
+}
+
+static void
+ide_tree_empty_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  IdeTreeEmpty *self = IDE_TREE_EMPTY (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      g_value_set_object (value, self->model);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_tree_empty_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  IdeTreeEmpty *self = IDE_TREE_EMPTY (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      self->model = g_value_dup_object (value);
+      self->empty = g_list_model_get_n_items (self->model) == 0;
+      g_signal_connect_object (self->model,
+                               "items-changed",
+                               G_CALLBACK (ide_tree_empty_items_changed_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_tree_empty_class_init (IdeTreeEmptyClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_tree_empty_dispose;
+  object_class->get_property = ide_tree_empty_get_property;
+  object_class->set_property = ide_tree_empty_set_property;
+
+  properties [PROP_MODEL] =
+    g_param_spec_object ("model", NULL, NULL,
+                         G_TYPE_LIST_MODEL,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_tree_empty_init (IdeTreeEmpty *self)
+{
+  self->empty = TRUE;
+  self->child = ide_tree_node_new ();
+  ide_tree_node_set_title (self->child, _("Empty"));
+}
+
+IdeTreeEmpty *
+ide_tree_empty_new (GListModel *model)
+{
+  g_return_val_if_fail (G_IS_LIST_MODEL (model), NULL);
+
+  return g_object_new (IDE_TYPE_TREE_EMPTY,
+                       "model", model,
+                       NULL);
+}
diff --git a/src/plugins/codeui/gbp-codeui-tree-addin.h b/src/libide/tree/ide-tree-empty.h
similarity index 72%
rename from src/plugins/codeui/gbp-codeui-tree-addin.h
rename to src/libide/tree/ide-tree-empty.h
index 441bc3fd4..3e7fa79fb 100644
--- a/src/plugins/codeui/gbp-codeui-tree-addin.h
+++ b/src/libide/tree/ide-tree-empty.h
@@ -1,6 +1,6 @@
-/* gbp-codeui-tree-addin.h
+/* ide-tree-empty.h
  *
- * Copyright 2019 Christian Hergert <chergert redhat com>
+ * Copyright 2022 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 <glib-object.h>
+#include <gio/gio.h>
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_CODEUI_TREE_ADDIN (gbp_codeui_tree_addin_get_type())
+#define IDE_TYPE_TREE_EMPTY (ide_tree_empty_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpCodeuiTreeAddin, gbp_codeui_tree_addin, GBP, CODEUI_TREE_ADDIN, GObject)
+G_DECLARE_FINAL_TYPE (IdeTreeEmpty, ide_tree_empty, IDE, TREE_EMPTY, GObject)
+
+IdeTreeEmpty *ide_tree_empty_new (GListModel *mode);
 
 G_END_DECLS
diff --git a/src/libide/tree/ide-tree-node.c b/src/libide/tree/ide-tree-node.c
index 4fafb6e0b..0cd6a06ab 100644
--- a/src/libide/tree/ide-tree-node.c
+++ b/src/libide/tree/ide-tree-node.c
@@ -1,6 +1,6 @@
 /* ide-tree-node.c
  *
- * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2022 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
@@ -22,218 +22,150 @@
 
 #include "config.h"
 
-#include <glib/gi18n.h>
+#include <libide-gtk.h>
+#include <libide-threading.h>
 
-#include "ide-popover-positioner.h"
-#include "ide-tree-model.h"
-#include "ide-tree-node.h"
+#include "ide-tree-addin.h"
+#include "ide-tree-enums.h"
 #include "ide-tree-private.h"
 
-/**
- * SECTION:ide-tree-node
- * @title: IdeTreeNode
- * @short_description: a node within the tree
- *
- * The #IdeTreeNode class is used to represent an item that should
- * be displayed in the tree of the Ide application. The
- * #IdeTreeAddin plugins create and maintain these nodes during the
- * lifetime of the program.
- *
- * Plugins that want to add items to the tree should implement the
- * #IdeTreeAddin interface and register it during plugin
- * initialization.
- */
-
 struct _IdeTreeNode
 {
   GObject parent_instance;
 
-  /* A pointer to the model, which is only set on the root node. */
-  IdeTreeModel *model;
-
-  /*
-   * The following are fields containing the values for various properties
-   * on the tree node. Usually, icon, display_name, and item will be set
-   * on all nodes.
-   */
-  GIcon   *icon;
-  GIcon   *expanded_icon;
-  gchar   *display_name;
-  GObject *item;
-  gchar   *tag;
-  GList   *emblems;
-
-  /*
-   * The following items are used to maintain a tree structure of
-   * nodes for which we can use O(1) operations. The link is inserted
-   * into the parents children queue. The parent pointer is unowned,
-   * and set by the parent (cleared upon removal).
-   *
-   * This also allows maintaining the tree structure with zero additional
-   * allocations beyond the nodes themselves.
-   */
   IdeTreeNode *parent;
-  GQueue       children;
-  GList        link;
-
-  /* Foreground and Background colors */
-  GdkRGBA      background;
-  GdkRGBA      foreground;
-
-  /* Flags for state cell renderer */
-  IdeTreeNodeFlags flags;
+  GQueue children;
+  GList link;
 
-  /* When did we start loading? This is used to avoid drawing "Loading..."
-   * when the tree loads really quickly. Otherwise, we risk looking janky
-   * when the loads are quite fast.
-   */
-  gint64 started_loading_at;
+  char *title;
+  GIcon *icon;
+  GIcon *expanded_icon;
 
-  /* If we're currently loading */
-  guint is_loading : 1;
-
-  /* If the node is a header (bold, etc) */
-  guint is_header : 1;
-
-  /* If this is a synthesized empty node */
-  guint is_empty : 1;
+  GObject *item;
 
-  /* If there are errors associated with the node's item */
-  guint has_error : 1;
+  IdeTask *build_children_task;
 
-  /* If the node maybe has children */
+  IdeTreeNodeFlags flags : 8;
+  guint children_built : 1;
   guint children_possible : 1;
-
-  /* If this node needs to have the children built */
-  guint needs_build_children : 1;
-
-  /* If true, we remove all children on collapse */
+  guint destroy_item : 1;
+  guint has_error : 1;
+  guint is_header : 1;
   guint reset_on_collapse : 1;
-
-  /* If pango markup should be used */
   guint use_markup : 1;
-
-  /* If true, we use ide_clear_and_destroy_object() */
-  guint destroy_item : 1;
-
-  /* If colors are set */
-  guint background_set : 1;
-  guint foreground_set : 1;
 };
 
-G_DEFINE_FINAL_TYPE (IdeTreeNode, ide_tree_node, G_TYPE_OBJECT)
-
 enum {
   PROP_0,
   PROP_CHILDREN_POSSIBLE,
   PROP_DESTROY_ITEM,
-  PROP_DISPLAY_NAME,
   PROP_EXPANDED_ICON,
   PROP_EXPANDED_ICON_NAME,
+  PROP_FLAGS,
   PROP_HAS_ERROR,
   PROP_ICON,
   PROP_ICON_NAME,
   PROP_IS_HEADER,
   PROP_ITEM,
+  PROP_PARENT,
   PROP_RESET_ON_COLLAPSE,
-  PROP_TAG,
+  PROP_TITLE,
   PROP_USE_MARKUP,
   N_PROPS
 };
 
-static GParamSpec *properties [N_PROPS];
-
-static IdeTreeModel *
-ide_tree_node_get_model (IdeTreeNode *self)
+static guint
+list_model_get_n_items (GListModel *model)
 {
-  return ide_tree_node_get_root (self)->model;
+  return IDE_TREE_NODE (model)->children.length;
 }
 
-/**
- * ide_tree_node_new:
- *
- * Create a new #IdeTreeNode.
- *
- * Returns: (transfer full): a newly created #IdeTreeNode
- */
-IdeTreeNode *
-ide_tree_node_new (void)
+static GType
+list_model_get_item_type (GListModel *model)
 {
-  return g_object_new (IDE_TYPE_TREE_NODE, NULL);
+  return IDE_TYPE_TREE_NODE;
 }
 
-static void
-ide_tree_node_emit_changed (IdeTreeNode *self)
+static gpointer
+list_model_get_item (GListModel *model,
+                     guint       position)
 {
-  g_autoptr(GtkTreePath) path = NULL;
-  IdeTreeModel *model;
-  GtkTreeIter iter = { .user_data = self };
+  IdeTreeNode *self = IDE_TREE_NODE (model);
 
-  g_assert (IDE_IS_TREE_NODE (self));
+  if (position >= self->children.length)
+    return NULL;
 
-  if (!(model = ide_tree_node_get_model (self)))
-    return;
+  if (position == 0)
+    return g_object_ref (g_queue_peek_head (&self->children));
+
+  if (position == self->children.length-1)
+    return g_object_ref (g_queue_peek_tail (&self->children));
 
-  if ((path = ide_tree_model_get_path_for_node (model, self)))
-    gtk_tree_model_row_changed (GTK_TREE_MODEL (model), path, &iter);
+  return g_object_ref (g_queue_peek_nth (&self->children, position));
 }
 
 static void
-ide_tree_node_remove_with_dispose (IdeTreeNode *self,
-                                   IdeTreeNode *child)
+list_model_iface_init (GListModelInterface *iface)
 {
-  g_object_ref (child);
-  ide_tree_node_remove (self, child);
-  g_object_run_dispose (G_OBJECT (child));
-  g_object_unref (child);
+  iface->get_n_items = list_model_get_n_items;
+  iface->get_item_type = list_model_get_item_type;
+  iface->get_item = list_model_get_item;
 }
 
-static void
-ide_tree_node_dispose (GObject *object)
-{
-  IdeTreeNode *self = (IdeTreeNode *)object;
+G_DEFINE_TYPE_WITH_CODE (IdeTreeNode, ide_tree_node, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
 
-  while (self->children.length > 0)
-    ide_tree_node_remove_with_dispose (self, g_queue_peek_nth (&self->children, 0));
+static GParamSpec *properties [N_PROPS];
 
-  if (self->destroy_item && IDE_IS_OBJECT (self->item))
-    ide_clear_and_destroy_object (&self->item);
-  else
-    g_clear_object (&self->item);
+IdeTree *
+_ide_tree_node_get_tree (IdeTreeNode *self)
+{
+  IdeTreeNode *root = ide_tree_node_get_root (self);
+  return IDE_TREE (g_object_get_data (G_OBJECT (root), "IDE_TREE"));
+}
 
-  g_list_free_full (self->emblems, g_object_unref);
-  self->emblems = NULL;
+void
+_ide_tree_node_collapsed (IdeTreeNode *self)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
 
-  g_clear_object (&self->icon);
-  g_clear_object (&self->expanded_icon);
-  g_clear_pointer (&self->display_name, g_free);
-  g_clear_pointer (&self->tag, g_free);
+  if (self->reset_on_collapse)
+    {
+      self->children_built = FALSE;
 
-  G_OBJECT_CLASS (ide_tree_node_parent_class)->dispose (object);
+      while (self->children.head != NULL)
+        ide_tree_node_unparent (g_queue_peek_head (&self->children));
+    }
 }
 
 static void
-ide_tree_node_finalize (GObject *object)
+ide_tree_node_dispose (GObject *object)
 {
   IdeTreeNode *self = (IdeTreeNode *)object;
 
-  g_clear_weak_pointer (&self->model);
+  while (self->children.head != NULL)
+    ide_tree_node_unparent (self->children.head->data);
 
-  g_assert (self->children.head == NULL);
-  g_assert (self->children.tail == NULL);
-  g_assert (self->children.length == 0);
+  if (self->parent != NULL)
+    ide_tree_node_unparent (self);
+
+  g_clear_pointer (&self->title, g_free);
+
+  g_clear_object (&self->icon);
+  g_clear_object (&self->expanded_icon);
 
   if (self->destroy_item && IDE_IS_OBJECT (self->item))
     ide_clear_and_destroy_object (&self->item);
   else
     g_clear_object (&self->item);
 
-  g_clear_object (&self->icon);
-  g_clear_object (&self->expanded_icon);
-  g_clear_pointer (&self->display_name, g_free);
-  g_clear_pointer (&self->tag, g_free);
+  G_OBJECT_CLASS (ide_tree_node_parent_class)->dispose (object);
 
-  G_OBJECT_CLASS (ide_tree_node_parent_class)->finalize (object);
+  g_assert (self->parent == NULL);
+  g_assert (self->children.head == NULL);
+  g_assert (self->children.length == 0);
+  g_assert (self->link.prev == NULL);
+  g_assert (self->link.next == NULL);
 }
 
 static void
@@ -254,8 +186,12 @@ ide_tree_node_get_property (GObject    *object,
       g_value_set_boolean (value, self->destroy_item);
       break;
 
-    case PROP_DISPLAY_NAME:
-      g_value_set_string (value, ide_tree_node_get_display_name (self));
+    case PROP_EXPANDED_ICON:
+      g_value_set_object (value, ide_tree_node_get_expanded_icon (self));
+      break;
+
+    case PROP_FLAGS:
+      g_value_set_flags (value, ide_tree_node_get_flags (self));
       break;
 
     case PROP_HAS_ERROR:
@@ -274,12 +210,16 @@ ide_tree_node_get_property (GObject    *object,
       g_value_set_object (value, ide_tree_node_get_item (self));
       break;
 
+    case PROP_PARENT:
+      g_value_set_object (value, ide_tree_node_get_parent (self));
+      break;
+
     case PROP_RESET_ON_COLLAPSE:
       g_value_set_boolean (value, ide_tree_node_get_reset_on_collapse (self));
       break;
 
-    case PROP_TAG:
-      g_value_set_string (value, ide_tree_node_get_tag (self));
+    case PROP_TITLE:
+      g_value_set_string (value, ide_tree_node_get_title (self));
       break;
 
     case PROP_USE_MARKUP:
@@ -309,10 +249,6 @@ ide_tree_node_set_property (GObject      *object,
       self->destroy_item = g_value_get_boolean (value);
       break;
 
-    case PROP_DISPLAY_NAME:
-      ide_tree_node_set_display_name (self, g_value_get_string (value));
-      break;
-
     case PROP_EXPANDED_ICON:
       ide_tree_node_set_expanded_icon (self, g_value_get_object (value));
       break;
@@ -321,6 +257,10 @@ ide_tree_node_set_property (GObject      *object,
       ide_tree_node_set_expanded_icon_name (self, g_value_get_string (value));
       break;
 
+    case PROP_FLAGS:
+      ide_tree_node_set_flags (self, g_value_get_flags (value));
+      break;
+
     case PROP_HAS_ERROR:
       ide_tree_node_set_has_error (self, g_value_get_boolean (value));
       break;
@@ -341,12 +281,16 @@ ide_tree_node_set_property (GObject      *object,
       ide_tree_node_set_item (self, g_value_get_object (value));
       break;
 
+    case PROP_PARENT:
+      ide_tree_node_set_parent (self, g_value_get_object (value));
+      break;
+
     case PROP_RESET_ON_COLLAPSE:
       ide_tree_node_set_reset_on_collapse (self, g_value_get_boolean (value));
       break;
 
-    case PROP_TAG:
-      ide_tree_node_set_tag (self, g_value_get_string (value));
+    case PROP_TITLE:
+      ide_tree_node_set_title (self, g_value_get_string (value));
       break;
 
     case PROP_USE_MARKUP:
@@ -364,183 +308,102 @@ ide_tree_node_class_init (IdeTreeNodeClass *klass)
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
   object_class->dispose = ide_tree_node_dispose;
-  object_class->finalize = ide_tree_node_finalize;
   object_class->get_property = ide_tree_node_get_property;
   object_class->set_property = ide_tree_node_set_property;
 
-  /**
-   * IdeTreeNode:children-possible:
-   *
-   * The "children-possible" property denotes if the node may have children
-   * even if it doesn't have children yet. This is useful for delayed loading
-   * of children nodes.
-   */
   properties [PROP_CHILDREN_POSSIBLE] =
-    g_param_spec_boolean ("children-possible",
-                          "Children Possible",
-                          "If children are possible for the node",
+    g_param_spec_boolean ("children-possible", NULL, NULL,
                           FALSE,
-                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
-  /**
-   * IdeTreeNode:destroy-item:
-   *
-   * If %TRUE and #IdeTreeNode:item is an #IdeObject, it will be destroyed
-   * when the node is destroyed.
-   */
+                          (G_PARAM_READWRITE |
+                           G_PARAM_EXPLICIT_NOTIFY |
+                           G_PARAM_STATIC_STRINGS));
+
   properties [PROP_DESTROY_ITEM] =
-    g_param_spec_boolean ("destroy-item",
-                          "Destroy Item",
-                          "If the item should be destroyed with the node.",
+    g_param_spec_boolean ("destroy-item", NULL, NULL,
                           FALSE,
-                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
-
-  /**
-   * IdeTreeNode:display-name:
-   *
-   * The "display-name" property is the name for the node as it should be
-   * displayed in the tree.
-   */
-  properties [PROP_DISPLAY_NAME] =
-    g_param_spec_string ("display-name",
-                         "Display Name",
-                         "Display name for the node in the tree",
-                         NULL,
-                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
-  /**
-   * IdeTreeNode:expanded-icon:
-   *
-   * The "expanded-icon" property is the icon that should be displayed to the
-   * user in the tree for this node.
-   */
+                          (G_PARAM_READWRITE |
+                           G_PARAM_STATIC_STRINGS));
+
   properties [PROP_EXPANDED_ICON] =
-    g_param_spec_object ("expanded-icon",
-                         "Expanded Icon",
-                         "The expanded icon to display in the tree",
+    g_param_spec_object ("expanded-icon", NULL, NULL,
                          G_TYPE_ICON,
-                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
-  /**
-   * IdeTreeNode:expanded-icon-name:
-   *
-   * The "expanded-icon-name" is a convenience property to set the
-   * #IdeTreeNode:expanded-icon property using an icon-name.
-   */
+                         (G_PARAM_READWRITE |
+                          G_PARAM_EXPLICIT_NOTIFY |
+                          G_PARAM_STATIC_STRINGS));
+
   properties [PROP_EXPANDED_ICON_NAME] =
-    g_param_spec_string ("expanded-icon-name",
-                         "Expanded Icon Name",
-                         "The expanded icon-name for the GIcon",
+    g_param_spec_string ("expanded-icon-name", NULL, NULL,
                          NULL,
                          (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
 
-  /**
-   * IdeTreeNode:has-error:
-   *
-   * The "has-error" property is true if the node should be rendered with
-   * an error styling. This is useful when errors are known by the diagnostics
-   * manager for a given file or folder.
-   */
+  properties [PROP_FLAGS] =
+    g_param_spec_flags ("flags", NULL, NULL,
+                        IDE_TYPE_TREE_NODE_FLAGS,
+                        IDE_TREE_NODE_FLAGS_NONE,
+                        (G_PARAM_READWRITE |
+                         G_PARAM_EXPLICIT_NOTIFY |
+                         G_PARAM_STATIC_STRINGS));
+
   properties [PROP_HAS_ERROR] =
-    g_param_spec_boolean ("has-error",
-                          "Has Error",
-                          "If the node has an error associated with it's item",
+    g_param_spec_boolean ("has-error", NULL, NULL,
                           FALSE,
-                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
-  /**
-   * IdeTreeNode:icon:
-   *
-   * The "icon" property is the icon that should be displayed to the
-   * user in the tree for this node.
-   */
+                          (G_PARAM_READWRITE |
+                           G_PARAM_EXPLICIT_NOTIFY |
+                           G_PARAM_STATIC_STRINGS));
+
   properties [PROP_ICON] =
-    g_param_spec_object ("icon",
-                         "Icon",
-                         "The icon to display in the tree",
+    g_param_spec_object ("icon", NULL, NULL,
                          G_TYPE_ICON,
-                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
-  /**
-   * IdeTreeNode:icon-name:
-   *
-   * The "icon-name" is a convenience property to set the #IdeTreeNode:icon
-   * property using an icon-name.
-   */
+                         (G_PARAM_READWRITE |
+                          G_PARAM_EXPLICIT_NOTIFY |
+                          G_PARAM_STATIC_STRINGS));
+
   properties [PROP_ICON_NAME] =
-    g_param_spec_string ("icon-name",
-                         "Icon Name",
-                         "The icon-name for the GIcon",
+    g_param_spec_string ("icon-name", NULL, NULL,
                          NULL,
                          (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
 
-  /**
-   * IdeTreeNode:is-header:
-   *
-   * The "is-header" property denotes the node should be styled as a group
-   * header.
-   */
   properties [PROP_IS_HEADER] =
-    g_param_spec_boolean ("is-header",
-                          "Is Header",
-                          "If the node is a header",
+    g_param_spec_boolean ("is-header", NULL, NULL,
                           FALSE,
-                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
-  /**
-   * IdeTreeNode:item:
-   *
-   * The "item" property is an optional #GObject that can be used to
-   * store information about the node, which is sometimes useful when
-   * creating #IdeTreeAddin plugins.
-   */
+                          (G_PARAM_READWRITE |
+                           G_PARAM_EXPLICIT_NOTIFY |
+                           G_PARAM_STATIC_STRINGS));
+
   properties [PROP_ITEM] =
-    g_param_spec_object ("item",
-                         "Item",
-                         "Item",
+    g_param_spec_object ("item", NULL, NULL,
+                         G_TYPE_OBJECT,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_EXPLICIT_NOTIFY |
+                          G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PARENT] =
+    g_param_spec_object ("parent", NULL, NULL,
                          G_TYPE_OBJECT,
-                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
-  /**
-   * IdeTreeNode:reset-on-collapse:
-   *
-   * The "reset-on-collapse" denotes that children should be removed when
-   * the node is collapsed.
-   */
+                         (G_PARAM_READWRITE |
+                          G_PARAM_EXPLICIT_NOTIFY |
+                          G_PARAM_STATIC_STRINGS));
+
   properties [PROP_RESET_ON_COLLAPSE] =
-    g_param_spec_boolean ("reset-on-collapse",
-                          "Reset on Collapse",
-                          "If the children are removed when the node is collapsed",
-                          TRUE,
-                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
-  /**
-   * IdeTreeNode:tag:
-   *
-   * The "tag" property can be used to denote the type of node when you do not have an
-   * object to assign to #IdeTreeNode:item.
-   *
-   * See ide_tree_node_is_tag() to match a tag when building.
-   */
-  properties [PROP_TAG] =
-    g_param_spec_string ("tag",
-                         "Tag",
-                         "The tag for the node if any",
+    g_param_spec_boolean ("reset-on-collapse", NULL, NULL,
+                          FALSE,
+                          (G_PARAM_READWRITE |
+                           G_PARAM_EXPLICIT_NOTIFY |
+                           G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title", NULL, NULL,
                          NULL,
-                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
-  /**
-   * IdeTreeNode:use-markup:
-   *
-   * If #TRUE, the "use-markup" property denotes that #IdeTreeNode:display-name
-   * contains pango markup.
-   */
+                         (G_PARAM_READWRITE |
+                          G_PARAM_EXPLICIT_NOTIFY |
+                          G_PARAM_STATIC_STRINGS));
+
   properties [PROP_USE_MARKUP] =
-    g_param_spec_boolean ("use-markup",
-                          "Use Markup",
-                          "If pango markup should be used",
+    g_param_spec_boolean ("use-markup", NULL, NULL,
                           FALSE,
-                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+                          (G_PARAM_READWRITE |
+                           G_PARAM_EXPLICIT_NOTIFY |
+                           G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 }
@@ -548,52 +411,39 @@ ide_tree_node_class_init (IdeTreeNodeClass *klass)
 static void
 ide_tree_node_init (IdeTreeNode *self)
 {
-  self->reset_on_collapse = TRUE;
   self->link.data = self;
+  self->reset_on_collapse = TRUE;
 }
 
-/**
- * ide_tree_node_get_display_name:
- * @self: a #IdeTreeNode
- *
- * Gets the #IdeTreeNode:display-name property.
- *
- * Returns: (nullable): a string containing the display name
- */
-const gchar *
-ide_tree_node_get_display_name (IdeTreeNode *self)
+IdeTreeNode *
+ide_tree_node_new (void)
+{
+  return g_object_new (IDE_TYPE_TREE_NODE, NULL);
+}
+
+const char *
+ide_tree_node_get_title (IdeTreeNode *self)
 {
   g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
 
-  return self->display_name;
+  return self->title;
 }
 
-/**
- * ide_tree_node_set_display_name:
- *
- * Sets the #IdeTreeNode:display-name property, which is the text to
- * use when displaying the item in the tree.
- */
 void
-ide_tree_node_set_display_name (IdeTreeNode *self,
-                                const gchar *display_name)
+ide_tree_node_set_title (IdeTreeNode *self,
+                         const char  *title)
 {
   g_return_if_fail (IDE_IS_TREE_NODE (self));
 
-  if (g_strcmp0 (display_name, self->display_name) != 0)
-    {
-      g_free (self->display_name);
-      self->display_name = g_strdup (display_name);
-      ide_tree_node_emit_changed (self);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DISPLAY_NAME]);
-    }
+  if (ide_set_string (&self->title, title))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
 }
 
 /**
  * ide_tree_node_get_icon:
- * @self: a #IdeTree
+ * @self: a #IdeTreeNode
  *
- * Gets the icon associated with the tree node.
+ * Gets the icon for the node.
  *
  * Returns: (transfer none) (nullable): a #GIcon or %NULL
  */
@@ -605,31 +455,22 @@ ide_tree_node_get_icon (IdeTreeNode *self)
   return self->icon;
 }
 
-/**
- * ide_tree_node_set_icon:
- * @self: a @IdeTreeNode
- * @icon: (nullable): a #GIcon or %NULL
- *
- * Sets the icon for the tree node.
- */
 void
 ide_tree_node_set_icon (IdeTreeNode *self,
                         GIcon       *icon)
 {
   g_return_if_fail (IDE_IS_TREE_NODE (self));
+  g_return_if_fail (!icon || G_IS_ICON (icon));
 
   if (g_set_object (&self->icon, icon))
-    {
-      ide_tree_node_emit_changed (self);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON]);
-    }
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON]);
 }
 
 /**
  * ide_tree_node_get_expanded_icon:
- * @self: a #IdeTree
+ * @self: a #IdeTreeNode
  *
- * Gets the expanded icon associated with the tree node.
+ * Gets the icon used when the node is expanded.
  *
  * Returns: (transfer none) (nullable): a #GIcon or %NULL
  */
@@ -638,553 +479,494 @@ ide_tree_node_get_expanded_icon (IdeTreeNode *self)
 {
   g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
 
-  return self->expanded_icon ? self->expanded_icon : self->icon;
+  return self->expanded_icon;
 }
 
-/**
- * ide_tree_node_set_expanded_icon:
- * @self: a @IdeTreeNode
- * @expanded_icon: (nullable): a #GIcon or %NULL
- *
- * Sets the expanded icon for the tree node.
- */
 void
 ide_tree_node_set_expanded_icon (IdeTreeNode *self,
                                  GIcon       *expanded_icon)
 {
   g_return_if_fail (IDE_IS_TREE_NODE (self));
+  g_return_if_fail (!expanded_icon || G_IS_ICON (expanded_icon));
 
   if (g_set_object (&self->expanded_icon, expanded_icon))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_EXPANDED_ICON]);
+}
+
+void
+ide_tree_node_set_icon_name (IdeTreeNode *self,
+                             const char  *icon_name)
+{
+  g_autoptr(GIcon) icon = NULL;
+
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  if (icon_name != NULL)
+    icon = g_themed_icon_new (icon_name);
+
+  ide_tree_node_set_icon (self, icon);
+}
+
+void
+ide_tree_node_set_expanded_icon_name (IdeTreeNode *self,
+                                      const char  *expanded_icon_name)
+{
+  g_autoptr(GIcon) expanded_icon = NULL;
+
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  if (expanded_icon_name != NULL)
+    expanded_icon = g_themed_icon_new (expanded_icon_name);
+
+  ide_tree_node_set_expanded_icon (self, expanded_icon);
+}
+
+gboolean
+ide_tree_node_get_has_error (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return self->has_error;
+}
+
+void
+ide_tree_node_set_has_error (IdeTreeNode *self,
+                             gboolean     has_error)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  has_error = !!has_error;
+
+  if (has_error != self->has_error)
     {
-      ide_tree_node_emit_changed (self);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_EXPANDED_ICON]);
+      self->has_error = has_error;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_ERROR]);
     }
 }
 
-/**
- * ide_tree_node_get_item:
- * @self: a #IdeTreeNode
- *
- * Gets the item that has been associated with the node.
- *
- * Returns: (transfer none) (type GObject.Object) (nullable): a #GObject
- *   if the item has been previously set.
- */
-gpointer
-ide_tree_node_get_item (IdeTreeNode *self)
+gboolean
+ide_tree_node_get_children_possible (IdeTreeNode *self)
 {
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
-  g_return_val_if_fail (!self->item || G_IS_OBJECT (self->item), NULL);
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
 
-  return self->item;
+  return self->children_possible;
 }
 
 void
-ide_tree_node_set_item (IdeTreeNode *self,
-                        gpointer     item)
+ide_tree_node_set_children_possible (IdeTreeNode *self,
+                                     gboolean     children_possible)
 {
   g_return_if_fail (IDE_IS_TREE_NODE (self));
-  g_return_if_fail (!item || G_IS_OBJECT (item));
 
-  if (g_set_object (&self->item, item))
+  children_possible = !!children_possible;
+
+  if (children_possible != self->children_possible)
     {
-      ide_tree_node_emit_changed (self);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ITEM]);
+      self->children_possible = children_possible;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHILDREN_POSSIBLE]);
     }
 }
 
-static IdeTreeNodeVisit
-ide_tree_node_row_inserted_traverse_cb (IdeTreeNode *node,
-                                        gpointer     user_data)
+gboolean
+ide_tree_node_get_reset_on_collapse (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return self->reset_on_collapse;
+}
+
+void
+ide_tree_node_set_reset_on_collapse (IdeTreeNode *self,
+                                     gboolean     reset_on_collapse)
 {
-  IdeTreeModel *model = user_data;
-  g_autoptr(GtkTreePath) path = NULL;
-  GtkTreeIter iter = { .user_data = node };
-
-  g_assert (IDE_IS_TREE_NODE (node));
-  g_assert (IDE_IS_TREE_MODEL (model));
-
-  /* Ignore the root node, nothing to do with that */
-  if (ide_tree_node_is_root (node))
-    return IDE_TREE_NODE_VISIT_CHILDREN;
-
-  /* It would be faster to create our paths as we traverse the tree,
-   * but that complicates the traversal. Generally this path should get
-   * hit very little (as usually it's only a single "child node").
-   */
-  if ((path = gtk_tree_model_get_path (GTK_TREE_MODEL (model), &iter)))
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  reset_on_collapse = !!reset_on_collapse;
+
+  if (reset_on_collapse != self->reset_on_collapse)
     {
-      gtk_tree_model_row_inserted (GTK_TREE_MODEL (model), path, &iter);
-
-      if (ide_tree_node_is_first (node))
-        {
-          IdeTreeNode *parent = ide_tree_node_get_parent (node);
-
-          if (!ide_tree_node_is_root (parent))
-            {
-              iter.user_data = parent;
-              gtk_tree_path_up (path);
-              gtk_tree_model_row_has_child_toggled (GTK_TREE_MODEL (model), path, &iter);
-            }
-        }
+      self->reset_on_collapse = reset_on_collapse;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RESET_ON_COLLAPSE]);
     }
+}
+
+gboolean
+ide_tree_node_get_use_markup (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
 
-  return IDE_TREE_NODE_VISIT_CHILDREN;
+  return self->use_markup;
 }
 
-static void
-ide_tree_node_row_inserted (IdeTreeNode *self,
-                            IdeTreeNode *child)
+void
+ide_tree_node_set_use_markup (IdeTreeNode *self,
+                              gboolean     use_markup)
 {
-  g_autoptr(GtkTreePath) path = NULL;
-  IdeTreeModel *model;
-  GtkTreeIter iter;
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
 
-  g_assert (IDE_IS_MAIN_THREAD ());
-  g_assert (IDE_IS_TREE_NODE (self));
-  g_assert (IDE_IS_TREE_NODE (child));
+  use_markup = !!use_markup;
 
-  if (!(model = ide_tree_node_get_model (self)) ||
-      !ide_tree_model_get_iter_for_node (model, &iter, child) ||
-      !(path = ide_tree_model_get_path_for_node (model, child)))
-    return;
+  if (use_markup != self->use_markup)
+    {
+      self->use_markup = use_markup;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_USE_MARKUP]);
+    }
+}
+
+gboolean
+ide_tree_node_get_is_header (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
 
-  ide_tree_node_traverse (child,
-                          G_PRE_ORDER,
-                          G_TRAVERSE_ALL,
-                          -1,
-                          ide_tree_node_row_inserted_traverse_cb,
-                          model);
+  return self->is_header;
 }
 
 void
-_ide_tree_node_set_model (IdeTreeNode  *self,
-                          IdeTreeModel *model)
+ide_tree_node_set_is_header (IdeTreeNode *self,
+                             gboolean     is_header)
 {
   g_return_if_fail (IDE_IS_TREE_NODE (self));
-  g_return_if_fail (!model || IDE_IS_TREE_MODEL (model));
 
-  if (g_set_weak_pointer (&self->model, model))
+  is_header = !!is_header;
+
+  if (is_header != self->is_header)
     {
-      if (self->model != NULL)
-        ide_tree_node_row_inserted (self, self);
+      self->is_header = is_header;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IS_HEADER]);
     }
 }
 
 /**
- * ide_tree_node_prepend:
+ * ide_tree_node_get_item:
  * @self: a #IdeTreeNode
- * @child: a #IdeTreeNode
  *
- * Prepends @child as a child of @self at the 0 index.
+ * Gets the #IdeTreeNode:item property.
  *
- * This operation is O(1).
+ * Returns: (transfer none) (nullable): a #GObject or %NULL
  */
-void
-ide_tree_node_prepend (IdeTreeNode *self,
-                       IdeTreeNode *child)
+gpointer
+ide_tree_node_get_item (IdeTreeNode *self)
 {
-  g_return_if_fail (IDE_IS_TREE_NODE (self));
-  g_return_if_fail (IDE_IS_TREE_NODE (child));
-  g_return_if_fail (child->parent == NULL);
-
-  child->parent = self;
-  g_object_ref (child);
-  g_queue_push_head_link (&self->children, &child->link);
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+  g_return_val_if_fail (self->item == NULL || G_IS_OBJECT (self->item), NULL);
 
-  ide_tree_node_row_inserted (self, child);
+  return self->item;
 }
 
 /**
- * ide_tree_node_append:
+ * ide_tree_node_set_item:
  * @self: a #IdeTreeNode
- * @child: a #IdeTreeNode
+ * @item: (type GObject) (nullable): a #GObject or %NULL
  *
- * Appends @child as a child of @self at the last position.
+ * Sets the #IdeTreeNode:item property.
  *
- * This operation is O(1).
+ * This item is typically used so that #IdeTreeAddin can annotate
+ * the node with additional data.
  */
 void
-ide_tree_node_append (IdeTreeNode *self,
-                      IdeTreeNode *child)
+ide_tree_node_set_item (IdeTreeNode *self,
+                        gpointer     item)
 {
   g_return_if_fail (IDE_IS_TREE_NODE (self));
-  g_return_if_fail (IDE_IS_TREE_NODE (child));
-  g_return_if_fail (child->parent == NULL);
+  g_return_if_fail (item == NULL || G_IS_OBJECT (item));
 
-  child->parent = self;
-  g_object_ref (child);
-  g_queue_push_tail_link (&self->children, &child->link);
+  if (item == self->item)
+    return;
 
-  ide_tree_node_row_inserted (self, child);
+  if (self->item)
+    {
+      if (self->destroy_item && IDE_IS_OBJECT (self->item))
+        ide_clear_and_destroy_object (&self->item);
+      else
+        g_clear_object (&self->item);
+    }
+
+  g_set_object (&self->item, item);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IS_HEADER]);
 }
 
 /**
- * ide_tree_node_insert_sorted:
- * @self: an #IdeTreeNode
- * @child: an #IdeTreeNode
- * @cmpfn: (scope call): an #IdeTreeNodeCompare
+ * ide_tree_node_get_parent:
+ * @self: a #IdeTreeNode
  *
- * Insert @child as a child of @self at the sorted position determined by @cmpfn
+ * Gets the parent node, if any.
  *
- * This operation is O(n).
+ * Returns: (transfer none) (nullable): an #IdeTreeNode or %NULL
  */
-void
-ide_tree_node_insert_sorted (IdeTreeNode        *self,
-                             IdeTreeNode        *child,
-                             IdeTreeNodeCompare  cmpfn)
+IdeTreeNode *
+ide_tree_node_get_parent (IdeTreeNode *self)
 {
-  GList *link;
-
-  g_return_if_fail (IDE_IS_TREE_NODE (self));
-  g_return_if_fail (IDE_IS_TREE_NODE (child));
-  g_return_if_fail (child->parent == NULL);
-
-  link = g_queue_find_custom (&self->children, child, (GCompareFunc)cmpfn);
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
 
-  if (link != NULL)
-    ide_tree_node_insert_before (IDE_TREE_NODE (link->data), child);
-  else
-    ide_tree_node_append (self, child);
+  return self->parent;
 }
 
-/**
- * ide_tree_node_insert_before:
- * @self: a #IdeTreeNode
- * @child: a #IdeTreeNode
- *
- * Inserts @child directly before @self by adding it to the parent of @self.
- *
- * This operation is O(1).
- */
 void
-ide_tree_node_insert_before (IdeTreeNode *self,
-                             IdeTreeNode *child)
+ide_tree_node_set_parent (IdeTreeNode *self,
+                          IdeTreeNode *parent)
 {
+  int pos;
+
   g_return_if_fail (IDE_IS_TREE_NODE (self));
-  g_return_if_fail (IDE_IS_TREE_NODE (child));
-  g_return_if_fail (self->parent != NULL);
-  g_return_if_fail (child->parent == NULL);
+  g_return_if_fail (!parent || IDE_IS_TREE_NODE (parent));
+  g_return_if_fail (!parent || self->parent == NULL);
+  g_return_if_fail (!parent || self->link.prev == NULL);
+  g_return_if_fail (!parent || self->link.next == NULL);
+  g_return_if_fail (self->link.data == self);
+
+  if (parent == self->parent)
+    return;
+
+  if (parent == NULL)
+    {
+      ide_tree_node_unparent (self);
+      return;
+    }
 
-  child->parent = self->parent;
-  g_object_ref (child);
-  _g_queue_insert_before_link (&self->parent->children, &self->link, &child->link);
+  g_object_ref (self);
+  self->parent = parent;
+  g_queue_push_tail_link (&parent->children, &self->link);
+  pos = g_queue_link_index (&parent->children, &self->link);
 
-  ide_tree_node_row_inserted (self, child);
+  g_list_model_items_changed (G_LIST_MODEL (parent), pos, 0, 1);
 }
 
-/**
- * ide_tree_node_insert_after:
- * @self: a #IdeTreeNode
- * @child: a #IdeTreeNode
- *
- * Inserts @child directly after @self by adding it to the parent of @self.
- *
- * This operation is O(1).
- */
 void
-ide_tree_node_insert_after (IdeTreeNode *self,
-                            IdeTreeNode *child)
+ide_tree_node_unparent (IdeTreeNode *self)
 {
+  IdeTreeNode *parent;
+  int child_position;
+
   g_return_if_fail (IDE_IS_TREE_NODE (self));
-  g_return_if_fail (IDE_IS_TREE_NODE (child));
-  g_return_if_fail (self->parent != NULL);
-  g_return_if_fail (child->parent == NULL);
 
-  child->parent = self->parent;
-  g_object_ref (child);
-  _g_queue_insert_after_link (&self->parent->children, &self->link, &child->link);
+  if (self->parent == NULL)
+    return;
+
+  parent = self->parent;
+  child_position = g_queue_link_index (&parent->children, &self->link);
+  g_return_if_fail (child_position > -1);
+  g_queue_unlink (&parent->children, &self->link);
+  self->parent = NULL;
 
-  ide_tree_node_row_inserted (self, child);
+  g_list_model_items_changed (G_LIST_MODEL (parent), child_position, 1, 0);
+
+  g_object_unref (self);
 }
 
 /**
  * ide_tree_node_remove:
  * @self: a #IdeTreeNode
- * @child: a #IdeTreeNode
- *
- * Removes the child node @child from @self. @self must be the parent of @child.
  *
- * This function is O(1).
+ * Like ide_tree_node_unparent() but checks parent first.
  */
 void
 ide_tree_node_remove (IdeTreeNode *self,
                       IdeTreeNode *child)
 {
-  g_autoptr(GtkTreePath) path = NULL;
-  IdeTreeModel *model;
-
   g_return_if_fail (IDE_IS_TREE_NODE (self));
   g_return_if_fail (IDE_IS_TREE_NODE (child));
   g_return_if_fail (child->parent == self);
 
-  if ((model = ide_tree_node_get_model (self)))
-    path = ide_tree_model_get_path_for_node (model, child);
-
-  child->parent = NULL;
-  g_queue_unlink (&self->children, &child->link);
-
-  if (path != NULL)
-    gtk_tree_model_row_deleted (GTK_TREE_MODEL (model), path);
-
-  g_object_unref (child);
+  ide_tree_node_unparent (child);
 }
 
 /**
- * ide_tree_node_get_parent:
+ * ide_tree_node_get_first_child:
  * @self: a #IdeTreeNode
  *
- * Gets the parent node of @self.
+ * Gets the first child of @self.
  *
  * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
  */
 IdeTreeNode *
-ide_tree_node_get_parent (IdeTreeNode *self)
+ide_tree_node_get_first_child (IdeTreeNode *self)
 {
   g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
 
-  return self->parent;
+  return g_queue_peek_head (&self->children);
 }
 
 /**
- * ide_tree_node_get_root:
+ * ide_tree_node_get_last_child:
  * @self: a #IdeTreeNode
  *
- * Gets the root #IdeTreeNode by following the #IdeTreeNode:parent
- * properties of each node.
+ * Gets the last child of @self.
  *
  * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
  */
 IdeTreeNode *
-ide_tree_node_get_root (IdeTreeNode *self)
+ide_tree_node_get_last_child (IdeTreeNode *self)
 {
   g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
 
-  while (self->parent != NULL)
-    self = self->parent;
-
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
-
-  return self;
-}
-
-/**
- * ide_tree_node_holds:
- * @self: a #IdeTreeNode
- * @type: a #GType
- *
- * Checks to see if the #IdeTreeNode:item property matches @type
- * or is a subclass of @type.
- *
- * Returns: %TRUE if @self holds a @type item
- */
-gboolean
-ide_tree_node_holds (IdeTreeNode *self,
-                     GType        type)
-{
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
-
-  return G_TYPE_CHECK_INSTANCE_TYPE (self->item, type);
-}
-
-/**
- * ide_tree_node_get_index:
- * @self: a #IdeTreeNode
- *
- * Gets the position of the @self.
- *
- * Returns: the offset of @self with it's siblings.
- */
-guint
-ide_tree_node_get_index (IdeTreeNode *self)
-{
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), 0);
-
-  if (self->parent != NULL)
-    return g_list_position (self->parent->children.head, &self->link);
-
-  return 0;
+  return g_queue_peek_tail (&self->children);
 }
 
 /**
- * ide_tree_node_get_nth_child:
+ * ide_tree_node_get_prev_sibling:
  * @self: a #IdeTreeNode
- * @index_: the index of the child
  *
- * Gets the @nth child of the tree node or %NULL if it does not exist.
+ * Gets the previous sibling, if any.
  *
- * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ * Returns: (transfer none) (nullable): an #IdeTreeNode or %NULL
  */
 IdeTreeNode *
-ide_tree_node_get_nth_child (IdeTreeNode *self,
-                             guint        index_)
+ide_tree_node_get_prev_sibling (IdeTreeNode *self)
 {
   g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
 
-  return g_queue_peek_nth (&self->children, index_);
+  return self->link.prev ? self->link.prev->data : NULL;
 }
 
 /**
- * ide_tree_node_get_next:
+ * ide_tree_node_get_next_sibling:
  * @self: a #IdeTreeNode
  *
- * Gets the next sibling after @self.
+ * Gets the nextious sibling, if any.
  *
- * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ * Returns: (transfer none) (nullable): an #IdeTreeNode or %NULL
  */
 IdeTreeNode *
-ide_tree_node_get_next (IdeTreeNode *self)
+ide_tree_node_get_next_sibling (IdeTreeNode *self)
 {
   g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
 
-  if (self->link.next)
-    return self->link.next->data;
-
-  return NULL;
+  return self->link.next ? self->link.next->data : NULL;
 }
 
 /**
- * ide_tree_node_get_previous:
+ * ide_tree_node_get_root:
  * @self: a #IdeTreeNode
  *
- * Gets the previous sibling before @self.
+ * Gets the root #IdeTreeNode, or @self if it has no parent.
  *
- * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ * Returns: (transfer none): an #IdeTreeNode
  */
 IdeTreeNode *
-ide_tree_node_get_previous (IdeTreeNode *self)
+ide_tree_node_get_root (IdeTreeNode *self)
 {
   g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
 
-  if (self->link.prev)
-    return self->link.prev->data;
+  while (self->parent != NULL)
+    self = self->parent;
 
-  return NULL;
+  return self;
 }
 
-/**
- * ide_tree_node_get_children_possible:
- * @self: a #IdeTreeNode
- *
- * Checks if the node can have children, and if so, returns %TRUE.
- * It may not actually have children yet.
- *
- * Returns: %TRUE if the children may have children
- */
 gboolean
-ide_tree_node_get_children_possible (IdeTreeNode *self)
+ide_tree_node_holds (IdeTreeNode *self,
+                     GType        type)
 {
   g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
 
-  return self->children_possible;
+  return G_TYPE_CHECK_INSTANCE_TYPE (self->item, type);
 }
 
-/**
- * ide_tree_node_set_children_possible:
- * @self: a #IdeTreeNode
- * @children_possible: if children are possible
- *
- * Sets if the children are possible for the node.
- */
 void
-ide_tree_node_set_children_possible (IdeTreeNode *self,
-                                     gboolean     children_possible)
+ide_tree_node_insert_after (IdeTreeNode *self,
+                            IdeTreeNode *parent,
+                            IdeTreeNode *previous_sibling)
 {
-  g_return_if_fail (IDE_IS_TREE_NODE (self));
+  int child_position;
 
-  children_possible = !!children_possible;
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (parent));
+  g_return_if_fail (!previous_sibling || IDE_IS_TREE_NODE (previous_sibling));
+  g_return_if_fail (self->parent == NULL);
+  g_return_if_fail (self->link.prev == NULL);
+  g_return_if_fail (self->link.next == NULL);
+  g_return_if_fail (self->link.data == self);
 
-  if (children_possible != self->children_possible)
-    {
-      self->children_possible = children_possible;
-      self->needs_build_children = children_possible;
+  g_object_ref (self);
 
-      if (self->children_possible && self->children.length == 0)
-        {
-          g_autoptr(IdeTreeNode) child = NULL;
+  self->parent = parent;
 
-          child = g_object_new (IDE_TYPE_TREE_NODE,
-                                "display-name", _("(Empty)"),
-                                NULL);
-          child->is_empty = TRUE;
-          ide_tree_node_append (self, child);
+  if (previous_sibling != NULL)
+    g_queue_insert_after_link (&parent->children, &previous_sibling->link, &self->link);
+  else
+    g_queue_push_tail_link (&parent->children, &self->link);
 
-          g_assert (ide_tree_node_has_child (self) == children_possible);
-        }
+  child_position = g_queue_link_index (&parent->children, &self->link);
 
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHILDREN_POSSIBLE]);
-    }
+  g_list_model_items_changed (G_LIST_MODEL (parent), child_position, 0, 1);
 }
 
-/**
- * ide_tree_node_has_child:
- * @self: a #IdeTreeNode
- *
- * Checks if @self has any children.
- *
- * Returns: %TRUE if @self has one or more children.
- */
-gboolean
-ide_tree_node_has_child (IdeTreeNode *self)
+void
+ide_tree_node_insert_before (IdeTreeNode *self,
+                             IdeTreeNode *parent,
+                             IdeTreeNode *next_sibling)
 {
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+  int child_position;
+
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (parent));
+  g_return_if_fail (!next_sibling || IDE_IS_TREE_NODE (next_sibling));
+  g_return_if_fail (self->parent == NULL);
+  g_return_if_fail (self->link.prev == NULL);
+  g_return_if_fail (self->link.next == NULL);
+  g_return_if_fail (self->link.data == self);
+
+  g_object_ref (self);
+
+  self->parent = parent;
+
+  if (next_sibling != NULL)
+    g_queue_insert_before_link (&parent->children, &next_sibling->link, &self->link);
+  else
+    g_queue_push_head_link (&parent->children, &self->link);
+
+  child_position = g_queue_link_index (&parent->children, &self->link);
 
-  return self->children.length > 0;
+  g_list_model_items_changed (G_LIST_MODEL (parent), child_position, 0, 1);
 }
 
 /**
- * ide_tree_node_get_n_children:
- * @self: a #IdeTreeNode
- *
- * Gets the number of children that @self contains.
+ * ide_tree_node_insert_sorted:
+ * @self: an #IdeTreeNode
+ * @child: an #IdeTreeNode
+ * @cmpfn: (scope call): an #IdeTreeNodeCompare
  *
- * Returns: the number of children
+ * Insert @child as a child of @self at the sorted position
+ * determined by @cmpfn.
  */
-guint
-ide_tree_node_get_n_children (IdeTreeNode *self)
+void
+ide_tree_node_insert_sorted (IdeTreeNode        *self,
+                             IdeTreeNode        *child,
+                             IdeTreeNodeCompare  cmpfn)
 {
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), 0);
+  GList *link;
 
-  return self->children.length;
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (child));
+  g_return_if_fail (child->parent == NULL);
+
+  link = g_queue_find_custom (&self->children, child, (GCompareFunc)cmpfn);
+
+  if (link != NULL)
+    ide_tree_node_insert_before (child, self, link->data);
+  else
+    ide_tree_node_insert_before (child, self, NULL);
 }
 
-/**
- * ide_tree_node_get_is_header:
- * @self: a #IdeTreeNode
- *
- * Gets the #IdeTreeNode:is-header property.
- *
- * If this is %TRUE, then the node will be rendered with alternate
- * styling for group headers.
- *
- * Returns: %TRUE if @self is a header.
- */
-gboolean
-ide_tree_node_get_is_header (IdeTreeNode *self)
+IdeTreeNodeFlags
+ide_tree_node_get_flags (IdeTreeNode *self)
 {
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), 0);
 
-  return self->is_header;
+  return self->flags;
 }
 
-/**
- * ide_tree_node_set_is_header:
- * @self: a #IdeTreeNode
- *
- * Sets the #IdeTreeNode:is-header property.
- */
 void
-ide_tree_node_set_is_header (IdeTreeNode *self,
-                             gboolean     is_header)
+ide_tree_node_set_flags (IdeTreeNode *self,
+                         IdeTreeNodeFlags flags)
 {
   g_return_if_fail (IDE_IS_TREE_NODE (self));
 
-  is_header = !!is_header;
-
-  if (self->is_header != is_header)
+  if (self->flags != flags)
     {
-      self->is_header = is_header;
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IS_HEADER]);
+      self->flags = flags;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FLAGS]);
     }
 }
 
@@ -1221,7 +1003,7 @@ do_traversal (IdeTreeNode      *node,
     {
       ret = traversal->callback (node, traversal->user_data);
 
-      if (!ide_tree_node_is_root (node) &&
+      if (node->parent != NULL &&
           (ret == IDE_TREE_NODE_VISIT_CONTINUE || ret == IDE_TREE_NODE_VISIT_BREAK))
         goto finish;
     }
@@ -1287,620 +1069,223 @@ ide_tree_node_traverse (IdeTreeNode         *self,
   do_traversal (self, &traverse);
 }
 
-/**
- * ide_tree_node_is_empty:
- * @self: a #IdeTreeNode
- *
- * This function checks if @self is a synthesized "empty" node.
- *
- * Empty nodes are added to #IdeTreeNode that may have children in the
- * future, but are currently empty. It allows the tree to display the
- * "(Empty)" contents and show a proper expander arrow.
- *
- * Returns: %TRUE if @self is a synthesized empty node.
- */
-gboolean
-ide_tree_node_is_empty (IdeTreeNode *self)
-{
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
-
-  return self->is_empty;
-}
-
-gboolean
-_ide_tree_node_get_needs_build_children (IdeTreeNode *self)
-{
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
-
-  return self->needs_build_children;
-}
-
-void
-_ide_tree_node_set_needs_build_children (IdeTreeNode *self,
-                                         gboolean     needs_build_children)
-{
-  g_return_if_fail (IDE_IS_TREE_NODE (self));
-
-  self->needs_build_children = !!needs_build_children;
-}
-
-/**
- * ide_tree_node_set_icon_name:
- * @self: a #IdeTreeNode
- * @icon_name: (nullable): the name of the icon, or %NULL
- *
- * Sets the #IdeTreeNode:icon property using an icon-name.
- */
-void
-ide_tree_node_set_icon_name (IdeTreeNode *self,
-                             const gchar *icon_name)
-{
-  g_autoptr(GIcon) icon = NULL;
-
-  g_return_if_fail (IDE_IS_TREE_NODE (self));
-
-  if (icon_name != NULL)
-    icon = g_themed_icon_new (icon_name);
-  ide_tree_node_set_icon (self, icon);
-}
-
-/**
- * ide_tree_node_set_expanded_icon_name:
- * @self: a #IdeTreeNode
- * @expanded_icon_name: (nullable): the name of the icon, or %NULL
- *
- * Sets the #IdeTreeNode:icon property using an icon-name.
- */
-void
-ide_tree_node_set_expanded_icon_name (IdeTreeNode *self,
-                                      const gchar *expanded_icon_name)
-{
-  g_autoptr(GIcon) icon = NULL;
-
-  g_return_if_fail (IDE_IS_TREE_NODE (self));
-
-  if (expanded_icon_name != NULL)
-    icon = g_themed_icon_new (expanded_icon_name);
-  ide_tree_node_set_expanded_icon (self, icon);
-}
-
-/**
- * ide_tree_node_is_root:
- * @self: a #IdeTreeNode
- *
- * Checks if @self is the root node, meaning it has no parent.
- *
- * Returns: %TRUE if @self has no parent.
- */
-gboolean
-ide_tree_node_is_root (IdeTreeNode *self)
-{
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
-
-  return self->parent == NULL;
-}
-
-/**
- * ide_tree_node_is_first:
- * @self: a #IdeTreeNode
- *
- * Checks if @self is the first sibling.
- *
- * Returns: %TRUE if @self is the first sibling
- */
-gboolean
-ide_tree_node_is_first (IdeTreeNode *self)
+static void
+ide_tree_node_expand_completed_cb (IdeTreeNode *self,
+                                   GParamSpec  *pspec,
+                                   IdeTask     *task)
 {
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_NODE (self));
+  g_assert (IDE_IS_TASK (task));
 
-  return self->link.prev == NULL;
+  if (self->build_children_task == task)
+    {
+      g_clear_object (&self->build_children_task);
+      self->children_built = TRUE;
+    }
 }
 
-/**
- * ide_tree_node_is_last:
- * @self: a #IdeTreeNode
- *
- * Checks if @self is the last sibling.
- *
- * Returns: %TRUE if @self is the last sibling
- */
-gboolean
-ide_tree_node_is_last (IdeTreeNode *self)
+typedef struct
 {
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
-
-  return self->link.next == NULL;
-}
+  IdeTreeNode            *node;
+  GPtrArray              *active;
+  IdeTask                *task;
+  IdeExtensionSetAdapter *addins;
+} Expand;
 
 static void
-ide_tree_node_dump_internal (IdeTreeNode *self,
-                             gint         depth)
-{
-  g_autofree gchar *space = g_strnfill (depth * 2, ' ');
-
-  g_print ("%s%s\n", space, ide_tree_node_get_display_name (self));
-
-  g_assert (self->children.length == 0 || self->children.head);
-  g_assert (self->children.length == 0 || self->children.tail);
-  g_assert (self->children.length > 0 || !self->children.head);
-  g_assert (self->children.length > 0 || !self->children.tail);
-
-  for (const GList *iter = self->children.head; iter; iter = iter->next)
-    ide_tree_node_dump_internal (iter->data, depth + 1);
-}
-
-void
-_ide_tree_node_dump (IdeTreeNode *self)
+expand_free (Expand *state)
 {
-  ide_tree_node_dump_internal (self, 0);
+  g_clear_object (&state->node);
+  g_clear_pointer (&state->active, g_ptr_array_unref);
+  g_clear_object (&state->addins);
+  state->task = NULL;
+  g_slice_free (Expand, state);
 }
 
-gboolean
-_ide_tree_node_get_loading (IdeTreeNode *self,
-                            gint64      *started_loading_at)
+static void
+ide_tree_node_build_node_cb (IdeExtensionSetAdapter *addins,
+                             PeasPluginInfo         *plugin_info,
+                             PeasExtension          *extension,
+                             gpointer                user_data)
 {
-  g_assert (IDE_IS_TREE_NODE (self));
-  g_assert (started_loading_at != NULL);
+  IdeTreeAddin *addin = (IdeTreeAddin *)extension;
+  Expand *state = user_data;
 
-  *started_loading_at = self->started_loading_at;
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (addins));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (state != NULL);
+  g_assert (IDE_IS_TREE_NODE (state->node));
 
-  return self->is_loading;
+  for (IdeTreeNode *child = ide_tree_node_get_first_child (state->node);
+       child != NULL;
+       child = ide_tree_node_get_next_sibling (child))
+    ide_tree_addin_build_node (addin, child);
 }
 
-void
-_ide_tree_node_set_loading (IdeTreeNode *self,
-                            gboolean     loading)
+static void
+ide_tree_node_expand_build_children_cb (GObject      *object,
+                                        GAsyncResult *result,
+                                        gpointer      user_data)
 {
-  g_return_if_fail (IDE_IS_TREE_NODE (self));
-
-  self->is_loading = !!loading;
-
-  if (self->is_loading)
-    self->started_loading_at = g_get_monotonic_time ();
-
-  for (const GList *iter = self->children.head; iter; iter = iter->next)
-    {
-      IdeTreeNode *child = iter->data;
+  IdeTreeAddin *addin = (IdeTreeAddin *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  Expand *state;
 
-      if (child->is_empty)
-        {
-          if (loading)
-            ide_tree_node_set_display_name (child, _("Loading…"));
-          else
-            ide_tree_node_set_display_name (child, _("(Empty)"));
-
-          if (self->children.length > 1)
-            ide_tree_node_remove (self, child);
-
-          break;
-        }
-    }
-}
-
-void
-_ide_tree_node_remove_all (IdeTreeNode *self)
-{
-  const GList *iter;
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
 
-  g_return_if_fail (IDE_IS_TREE_NODE (self));
+  state = ide_task_get_task_data (task);
 
-  iter = self->children.head;
+  g_assert (state != NULL);
+  g_assert (state->active != NULL);
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (state->addins));
+  g_assert (IDE_IS_TREE_NODE (state->node));
+  g_assert (IDE_IS_TASK (state->task));
+  g_assert (state->task == task);
 
-  while (iter != NULL)
-    {
-      IdeTreeNode *child = iter->data;
-      iter = iter->next;
-      ide_tree_node_remove (self, child);
-    }
+  g_ptr_array_remove (state->active, addin);
 
-  if (ide_tree_node_get_children_possible (self))
+  if (!ide_tree_addin_build_children_finish (addin, result, &error))
     {
-      g_autoptr(IdeTreeNode) child = g_object_new (IDE_TYPE_TREE_NODE,
-                                                   "display-name", _("(Empty)"),
-                                                   NULL);
-      child->is_empty = TRUE;
-      ide_tree_node_append (self, child);
-      _ide_tree_node_set_needs_build_children (self, TRUE);
+      if (!ide_error_ignore (error))
+        g_warning ("%s", error->message);
     }
-}
 
-/**
- * ide_tree_node_get_reset_on_collapse:
- * @self: a #IdeTreeNode
- *
- * Checks if the node should have all children removed when collapsed.
- *
- * Returns: %TRUE if children are removed on collapse
- */
-gboolean
-ide_tree_node_get_reset_on_collapse (IdeTreeNode *self)
-{
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
-
-  return self->reset_on_collapse;
-}
-
-/**
- * ide_tree_node_set_reset_on_collapse:
- * @self: a #IdeTreeNode
- * @reset_on_collapse: if the children should be removed on collapse
- *
- * If %TRUE, then children will be removed when the row is collapsed.
- */
-void
-ide_tree_node_set_reset_on_collapse (IdeTreeNode *self,
-                                     gboolean     reset_on_collapse)
-{
-  g_return_if_fail (IDE_IS_TREE_NODE (self));
-
-  reset_on_collapse = !!reset_on_collapse;
-
-  if (reset_on_collapse != self->reset_on_collapse)
+  if (state->active->len == 0)
     {
-      self->reset_on_collapse = reset_on_collapse;
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RESET_ON_COLLAPSE]);
+      ide_extension_set_adapter_foreach (state->addins,
+                                         ide_tree_node_build_node_cb,
+                                         state);
+      ide_task_return_boolean (task, TRUE);
     }
 }
 
-/**
- * ide_tree_node_get_path:
- * @self: a #IdeTreeNode
- *
- * Gets the path for the tree node.
- *
- * Returns: (transfer full) (nullable): a path or %NULL
- */
-GtkTreePath *
-ide_tree_node_get_path (IdeTreeNode *self)
-{
-  IdeTreeModel *model;
-
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
-
-  if ((model = ide_tree_node_get_model (self)))
-    return ide_tree_model_get_path_for_node (model, self);
-
-  return NULL;
-}
-
-void
-_ide_tree_node_get_area (IdeTreeNode  *node,
-                         IdeTree      *tree,
-                         GdkRectangle *area)
-{
-  GtkTreeViewColumn *column;
-  g_autoptr(GtkTreePath) path = NULL;
-
-  g_assert (IDE_IS_TREE_NODE (node));
-  g_assert (IDE_IS_TREE (tree));
-  g_assert (area != NULL);
-
-  path = ide_tree_node_get_path (node);
-  column = gtk_tree_view_get_column (GTK_TREE_VIEW (tree), 0);
-  gtk_tree_view_get_cell_area (GTK_TREE_VIEW (tree), path, column, area);
-}
-
-typedef struct
-{
-  IdeTreeNode *self;
-  IdeTree     *tree;
-  GtkPopover  *popover;
-} PopupRequest;
-
-static gboolean
-ide_tree_node_show_popover_timeout_cb (gpointer data)
+static void
+ide_tree_node_expand_foreach_cb (IdeExtensionSetAdapter *adapter,
+                                 PeasPluginInfo         *plugin_info,
+                                 PeasExtension          *extension,
+                                 gpointer                user_data)
 {
-  PopupRequest *popreq = data;
-  GtkWidget *positioner;
-  GdkRectangle rect;
-  GtkAllocation alloc;
-
-  g_assert (popreq);
-  g_assert (IDE_IS_TREE_NODE (popreq->self));
-  g_assert (GTK_IS_POPOVER (popreq->popover));
+  IdeTreeAddin *addin = (IdeTreeAddin *)extension;
+  Expand *expand = user_data;
 
-  _ide_tree_node_get_area (popreq->self, popreq->tree, &rect);
-  gtk_widget_get_allocation (GTK_WIDGET (popreq->tree), &alloc);
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (expand != NULL);
+  g_assert (IDE_IS_TREE_NODE (expand->node));
+  g_assert (expand->active != NULL);
+  g_assert (IDE_IS_TASK (expand->task));
 
-  if ((rect.x + rect.width) > (alloc.x + alloc.width))
-    rect.width = (alloc.x + alloc.width) - rect.x;
+  g_ptr_array_add (expand->active, g_object_ref (addin));
 
-  /* FIXME: Wouldn't this be better placed in a theme? */
-  switch (gtk_popover_get_position (popreq->popover))
-    {
-    case GTK_POS_BOTTOM:
-    case GTK_POS_TOP:
-      rect.y += 3;
-      rect.height -= 6;
-      break;
-    case GTK_POS_RIGHT:
-    case GTK_POS_LEFT:
-      rect.x += 3;
-      rect.width -= 6;
-      break;
-
-    default:
-      break;
-    }
-
-  if ((positioner = gtk_widget_get_ancestor (GTK_WIDGET (popreq->tree), IDE_TYPE_POPOVER_POSITIONER)))
-    ide_popover_positioner_present (IDE_POPOVER_POSITIONER (positioner),
-                                    popreq->popover,
-                                    GTK_WIDGET (popreq->tree),
-                                    &rect);
-
-  g_clear_object (&popreq->self);
-  g_clear_object (&popreq->popover);
-  g_slice_free (PopupRequest, popreq);
-
-  return G_SOURCE_REMOVE;
+  ide_tree_addin_build_children_async (addin,
+                                       expand->node,
+                                       ide_task_get_cancellable (expand->task),
+                                       ide_tree_node_expand_build_children_cb,
+                                       g_object_ref (expand->task));
 }
 
 void
-_ide_tree_node_show_popover (IdeTreeNode *self,
-                             IdeTree     *tree,
-                             GtkPopover  *popover)
+_ide_tree_node_expand_async (IdeTreeNode            *self,
+                             IdeExtensionSetAdapter *addins,
+                             GCancellable           *cancellable,
+                             GAsyncReadyCallback     callback,
+                             gpointer                user_data)
 {
-  GdkRectangle cell_area;
-  GdkRectangle visible_rect;
-  PopupRequest *popreq;
-  GtkAdjustment *vadj;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) active = NULL;
+  Expand *state;
 
   g_return_if_fail (IDE_IS_TREE_NODE (self));
-  g_return_if_fail (IDE_IS_TREE (tree));
-  g_return_if_fail (GTK_IS_POPOVER (popover));
-
-  gtk_tree_view_get_visible_rect (GTK_TREE_VIEW (tree), &visible_rect);
-  _ide_tree_node_get_area (self, tree, &cell_area);
-  gtk_tree_view_convert_bin_window_to_tree_coords (GTK_TREE_VIEW (tree),
-                                                   cell_area.x,
-                                                   cell_area.y,
-                                                   &cell_area.x,
-                                                   &cell_area.y);
-
-  popreq = g_slice_new0 (PopupRequest);
-  popreq->self = g_object_ref (self);
-  popreq->tree = g_object_ref (tree);
-  popreq->popover = g_object_ref (popover);
-
-  vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (tree));
-
-  /* Animating to position in GTK appears to break, so just manually
-   * scroll to the right position.
-   */
-  if (cell_area.y < visible_rect.y)
-    gtk_adjustment_set_value (vadj, cell_area.y);
-  else if (cell_area.y + cell_area.height > visible_rect.y + visible_rect.height)
-    gtk_adjustment_set_value (vadj, cell_area.y + cell_area.height - visible_rect.height);
-
-  /* FIXME: We could get rid of our allocated state here now that we
-   * no longer animate because of breakage in GTK.
-   */
-  ide_tree_node_show_popover_timeout_cb (g_steal_pointer (&popreq));
-}
-
-const gchar *
-ide_tree_node_get_tag (IdeTreeNode *self)
-{
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+  g_return_if_fail (!addins || IDE_IS_EXTENSION_SET_ADAPTER (addins));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  return self->tag;
-}
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, _ide_tree_node_expand_async);
 
-void
-ide_tree_node_set_tag (IdeTreeNode *self,
-                       const gchar *tag)
-{
-  g_return_if_fail (IDE_IS_TREE_NODE (self));
-
-  if (!ide_str_equal0 (self->tag, tag))
+  if (self->children_built)
     {
-      g_free (self->tag);
-      self->tag = g_strdup (tag);
+      ide_task_return_boolean (task, TRUE);
+      return;
     }
-}
-
-gboolean
-ide_tree_node_is_tag (IdeTreeNode *self,
-                      const gchar *tag)
-{
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
-
-  return tag && ide_str_equal0 (self->tag, tag);
-}
-
-void
-ide_tree_node_add_emblem (IdeTreeNode *self,
-                          GEmblem     *emblem)
-{
-  g_return_if_fail (IDE_IS_MAIN_THREAD ());
-  g_return_if_fail (IDE_IS_TREE_NODE (self));
-
-  self->emblems = g_list_append (self->emblems, g_object_ref (emblem));
-}
-
-GIcon *
-_ide_tree_node_apply_emblems (IdeTreeNode *self,
-                              GIcon       *base)
-{
-  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
 
-  if (self->emblems != NULL)
+  if (self->build_children_task)
     {
-      g_autoptr(GIcon) emblemed = g_emblemed_icon_new (base, NULL);
-
-      for (const GList *iter = self->emblems; iter; iter = iter->next)
-        g_emblemed_icon_add_emblem (G_EMBLEMED_ICON (emblemed), iter->data);
-
-      return G_ICON (g_steal_pointer (&emblemed));
+      ide_task_chain (self->build_children_task, task);
+      return;
     }
 
-  return g_object_ref (base);
-}
-
-const GdkRGBA *
-ide_tree_node_get_foreground_rgba (IdeTreeNode *self)
-{
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
-
-  return self->foreground_set ? &self->foreground : NULL;
-}
-
-void
-ide_tree_node_set_foreground_rgba (IdeTreeNode   *self,
-                                   const GdkRGBA *foreground_rgba)
-{
-  g_return_if_fail (IDE_IS_MAIN_THREAD ());
-  g_return_if_fail (IDE_IS_TREE_NODE (self));
-
-  self->foreground_set = !!foreground_rgba;
-
-  if (foreground_rgba)
-    self->foreground = *foreground_rgba;
-
-  ide_tree_node_emit_changed (self);
-}
-
-const GdkRGBA *
-ide_tree_node_get_background_rgba (IdeTreeNode *self)
-{
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
-
-  return self->background_set ? &self->background : NULL;
-}
-
-void
-ide_tree_node_set_background_rgba (IdeTreeNode   *self,
-                                   const GdkRGBA *background_rgba)
-{
-  g_return_if_fail (IDE_IS_MAIN_THREAD ());
-  g_return_if_fail (IDE_IS_TREE_NODE (self));
-
-  self->background_set = !!background_rgba;
+  g_set_object (&self->build_children_task, task);
 
-  if (background_rgba)
-    self->background = *background_rgba;
+  g_signal_connect_object (task,
+                           "notify::completed",
+                           G_CALLBACK (ide_tree_node_expand_completed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
 
-  ide_tree_node_emit_changed (self);
-}
-
-void
-_ide_tree_node_apply_colors (IdeTreeNode     *self,
-                             GtkCellRenderer *cell)
-{
-  PangoAttrList *attrs = NULL;
-
-  g_return_if_fail (IDE_IS_TREE_NODE (self));
-
-  if (self->foreground_set)
-    {
-      if (!attrs)
-        attrs = pango_attr_list_new ();
-      pango_attr_list_insert (attrs,
-                              pango_attr_foreground_new (self->foreground.red * 65535,
-                                                         self->foreground.green * 65535,
-                                                         self->foreground.blue * 65535));
-    }
-
-  if (self->background_set)
+  if (addins == NULL)
     {
-      if (!attrs)
-        attrs = pango_attr_list_new ();
-      pango_attr_list_insert (attrs,
-                              pango_attr_background_new (self->background.red * 65535,
-                                                         self->background.green * 65535,
-                                                         self->background.blue * 65535));
+      ide_task_return_boolean (task, TRUE);
+      return;
     }
 
-  g_object_set (cell, "attributes", attrs, NULL);
-  g_clear_pointer (&attrs, pango_attr_list_unref);
-}
+  active = g_ptr_array_new_with_free_func (g_object_unref);
 
-gboolean
-ide_tree_node_is_selected (IdeTreeNode *self)
-{
-  g_autoptr(GtkTreePath) path = NULL;
-  GtkTreeSelection *selection;
-  IdeTreeModel *model;
-  IdeTree *tree;
+  state = g_slice_new0 (Expand);
+  state->active = g_ptr_array_ref (active);
+  state->addins = g_object_ref (addins);
+  state->node = g_object_ref (self);
+  state->task = task;
 
-  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+  ide_task_set_task_data (task, state, expand_free);
 
-  if ((path = ide_tree_node_get_path (self)) &&
-      (model = ide_tree_node_get_model (self)) &&
-      (tree = ide_tree_model_get_tree (model)) &&
-      (selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree))))
-    return gtk_tree_selection_path_is_selected (selection, path);
+  ide_extension_set_adapter_foreach (addins,
+                                     ide_tree_node_expand_foreach_cb,
+                                     state);
 
-  return FALSE;
+  if (active->len == 0)
+    ide_task_return_boolean (task, TRUE);
 }
 
 gboolean
-ide_tree_node_get_has_error (IdeTreeNode *self)
+_ide_tree_node_expand_finish (IdeTreeNode   *self,
+                              GAsyncResult  *result,
+                              GError       **error)
 {
   g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
 
-  return self->has_error;
-}
-
-void
-ide_tree_node_set_has_error (IdeTreeNode *self,
-                             gboolean     has_error)
-{
-  g_return_if_fail (IDE_IS_TREE_NODE (self));
-
-  has_error = !!has_error;
-
-  if (has_error != self->has_error)
-    {
-      self->has_error = has_error;
-      ide_tree_node_emit_changed (self);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_ERROR]);
-    }
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
 }
 
 gboolean
-ide_tree_node_get_use_markup (IdeTreeNode *self)
+_ide_tree_node_children_built (IdeTreeNode *self)
 {
   g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
 
-  return self->use_markup;
+  return self->children_built;
 }
 
-void
-ide_tree_node_set_use_markup (IdeTreeNode *self,
-                              gboolean     use_markup)
+guint
+_ide_tree_node_get_child_index (IdeTreeNode *parent,
+                                IdeTreeNode *child)
 {
-  g_return_if_fail (IDE_IS_TREE_NODE (self));
+  g_return_val_if_fail (IDE_IS_TREE_NODE (parent), 0);
+  g_return_val_if_fail (IDE_IS_TREE_NODE (child), 0);
 
-  use_markup = !!use_markup;
-
-  if (use_markup != self->use_markup)
-    {
-      self->use_markup = use_markup;
-      ide_tree_node_emit_changed (self);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_USE_MARKUP]);
-    }
+  return g_queue_link_index (&parent->children, &child->link);
 }
 
-IdeTreeNodeFlags
-ide_tree_node_get_flags (IdeTreeNode *self)
+guint
+ide_tree_node_get_n_children (IdeTreeNode *self)
 {
   g_return_val_if_fail (IDE_IS_TREE_NODE (self), 0);
 
-  return self->flags;
-}
-
-void
-ide_tree_node_set_flags (IdeTreeNode      *self,
-                         IdeTreeNodeFlags  flags)
-{
-  g_return_if_fail (IDE_IS_TREE_NODE (self));
-
-  if (self->flags != flags)
-    {
-      self->flags = flags;
-      ide_tree_node_emit_changed (self);
-    }
+  return self->children.length;
 }
diff --git a/src/libide/tree/ide-tree-node.h b/src/libide/tree/ide-tree-node.h
index c76dfaae0..8373d81a6 100644
--- a/src/libide/tree/ide-tree-node.h
+++ b/src/libide/tree/ide-tree-node.h
@@ -1,6 +1,6 @@
 /* ide-tree-node.h
  *
- * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2022 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,15 +20,16 @@
 
 #pragma once
 
+#if !defined (IDE_TREE_INSIDE) && !defined (IDE_TREE_COMPILATION)
+# error "Only <libide-tree.h> can be included directly."
+#endif
+
 #include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_TREE_NODE (ide_tree_node_get_type())
 
-IDE_AVAILABLE_IN_ALL
-G_DECLARE_FINAL_TYPE (IdeTreeNode, ide_tree_node, IDE, TREE_NODE, GObject)
-
 typedef enum
 {
   IDE_TREE_NODE_VISIT_BREAK    = 0,
@@ -45,6 +46,9 @@ typedef enum
   IDE_TREE_NODE_FLAGS_REMOVED    = 1 << 3,
 } IdeTreeNodeFlags;
 
+IDE_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (IdeTreeNode, ide_tree_node, IDE, TREE_NODE, GObject)
+
 /**
  * IdeTreeTraverseFunc:
  * @node: an #IdeTreeNode
@@ -57,7 +61,6 @@ typedef enum
 typedef IdeTreeNodeVisit (*IdeTreeTraverseFunc) (IdeTreeNode *node,
                                                  gpointer     user_data);
 
-
 /**
  * IdeTreeNodeCompare:
  * @node: an #IdeTreeNode that iterate over children
@@ -74,46 +77,38 @@ typedef int (*IdeTreeNodeCompare) (IdeTreeNode *node,
 IDE_AVAILABLE_IN_ALL
 IdeTreeNode      *ide_tree_node_new                    (void);
 IDE_AVAILABLE_IN_ALL
-gboolean          ide_tree_node_get_has_error          (IdeTreeNode         *self);
+guint            ide_tree_node_get_n_children          (IdeTreeNode         *self);
 IDE_AVAILABLE_IN_ALL
-void              ide_tree_node_set_has_error          (IdeTreeNode         *self,
-                                                        gboolean             has_error);
+const char       *ide_tree_node_get_title              (IdeTreeNode         *self);
 IDE_AVAILABLE_IN_ALL
-const gchar      *ide_tree_node_get_tag                (IdeTreeNode         *self);
-IDE_AVAILABLE_IN_ALL
-void              ide_tree_node_set_tag                (IdeTreeNode         *self,
-                                                        const gchar         *tag);
-IDE_AVAILABLE_IN_ALL
-gboolean          ide_tree_node_is_tag                 (IdeTreeNode         *self,
-                                                        const gchar         *tag);
-IDE_AVAILABLE_IN_ALL
-GtkTreePath      *ide_tree_node_get_path               (IdeTreeNode         *self);
-IDE_AVAILABLE_IN_ALL
-const gchar      *ide_tree_node_get_display_name       (IdeTreeNode         *self);
-IDE_AVAILABLE_IN_ALL
-void              ide_tree_node_set_display_name       (IdeTreeNode         *self,
-                                                        const gchar         *display_name);
-IDE_AVAILABLE_IN_ALL
-gboolean          ide_tree_node_get_is_header          (IdeTreeNode         *self);
-IDE_AVAILABLE_IN_ALL
-void              ide_tree_node_set_is_header          (IdeTreeNode         *self,
-                                                        gboolean             header);
+void              ide_tree_node_set_title              (IdeTreeNode         *self,
+                                                        const char          *title);
 IDE_AVAILABLE_IN_ALL
 GIcon            *ide_tree_node_get_icon               (IdeTreeNode         *self);
 IDE_AVAILABLE_IN_ALL
 void              ide_tree_node_set_icon               (IdeTreeNode         *self,
                                                         GIcon               *icon);
 IDE_AVAILABLE_IN_ALL
-void              ide_tree_node_set_icon_name          (IdeTreeNode         *self,
-                                                        const gchar         *icon_name);
-IDE_AVAILABLE_IN_ALL
 GIcon            *ide_tree_node_get_expanded_icon      (IdeTreeNode         *self);
 IDE_AVAILABLE_IN_ALL
 void              ide_tree_node_set_expanded_icon      (IdeTreeNode         *self,
                                                         GIcon               *expanded_icon);
 IDE_AVAILABLE_IN_ALL
+void              ide_tree_node_set_icon_name          (IdeTreeNode         *self,
+                                                        const char          *icon_name);
+IDE_AVAILABLE_IN_ALL
 void              ide_tree_node_set_expanded_icon_name (IdeTreeNode         *self,
-                                                        const gchar         *expanded_icon_name);
+                                                        const char          *expanded_icon_name);
+IDE_AVAILABLE_IN_ALL
+IdeTreeNodeFlags  ide_tree_node_get_flags              (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_ALL
+void              ide_tree_node_set_flags              (IdeTreeNode         *self,
+                                                        IdeTreeNodeFlags     flags);
+IDE_AVAILABLE_IN_ALL
+gboolean          ide_tree_node_get_has_error          (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_ALL
+void              ide_tree_node_set_has_error          (IdeTreeNode         *self,
+                                                        gboolean             has_error);
 IDE_AVAILABLE_IN_ALL
 gpointer          ide_tree_node_get_item               (IdeTreeNode         *self);
 IDE_AVAILABLE_IN_ALL
@@ -125,88 +120,61 @@ IDE_AVAILABLE_IN_ALL
 void              ide_tree_node_set_children_possible  (IdeTreeNode         *self,
                                                         gboolean             children_possible);
 IDE_AVAILABLE_IN_ALL
-gboolean          ide_tree_node_is_empty               (IdeTreeNode         *self);
-IDE_AVAILABLE_IN_ALL
-gboolean          ide_tree_node_has_child              (IdeTreeNode         *self);
+gboolean          ide_tree_node_get_reset_on_collapse  (IdeTreeNode         *self);
 IDE_AVAILABLE_IN_ALL
-guint             ide_tree_node_get_n_children         (IdeTreeNode         *self);
+void              ide_tree_node_set_reset_on_collapse  (IdeTreeNode         *self,
+                                                        gboolean             reset_on_collapse);
 IDE_AVAILABLE_IN_ALL
-IdeTreeNode      *ide_tree_node_get_next               (IdeTreeNode         *self);
+gboolean          ide_tree_node_get_use_markup         (IdeTreeNode         *self);
 IDE_AVAILABLE_IN_ALL
-IdeTreeNode      *ide_tree_node_get_previous           (IdeTreeNode         *self);
+void              ide_tree_node_set_use_markup         (IdeTreeNode         *self,
+                                                        gboolean             use_markup);
 IDE_AVAILABLE_IN_ALL
-guint             ide_tree_node_get_index              (IdeTreeNode         *self);
+gboolean          ide_tree_node_get_is_header          (IdeTreeNode         *self);
 IDE_AVAILABLE_IN_ALL
-IdeTreeNode      *ide_tree_node_get_nth_child          (IdeTreeNode         *self,
-                                                        guint                index_);
+void              ide_tree_node_set_is_header          (IdeTreeNode         *self,
+                                                        gboolean             is_header);
 IDE_AVAILABLE_IN_ALL
-void              ide_tree_node_prepend                (IdeTreeNode         *self,
-                                                        IdeTreeNode         *child);
+IdeTreeNode      *ide_tree_node_get_first_child        (IdeTreeNode         *self);
 IDE_AVAILABLE_IN_ALL
-void              ide_tree_node_append                 (IdeTreeNode         *self,
-                                                        IdeTreeNode         *child);
+IdeTreeNode      *ide_tree_node_get_last_child         (IdeTreeNode         *self);
 IDE_AVAILABLE_IN_ALL
-void              ide_tree_node_insert_sorted          (IdeTreeNode         *self,
-                                                        IdeTreeNode         *child,
-                                                        IdeTreeNodeCompare   cmpfn);
+IdeTreeNode      *ide_tree_node_get_prev_sibling       (IdeTreeNode         *self);
 IDE_AVAILABLE_IN_ALL
-void              ide_tree_node_insert_before          (IdeTreeNode         *self,
-                                                        IdeTreeNode         *child);
+IdeTreeNode      *ide_tree_node_get_next_sibling       (IdeTreeNode         *self);
 IDE_AVAILABLE_IN_ALL
-void              ide_tree_node_insert_after           (IdeTreeNode         *self,
-                                                        IdeTreeNode         *child);
+void              ide_tree_node_set_parent             (IdeTreeNode         *self,
+                                                        IdeTreeNode         *node);
 IDE_AVAILABLE_IN_ALL
 void              ide_tree_node_remove                 (IdeTreeNode         *self,
                                                         IdeTreeNode         *child);
 IDE_AVAILABLE_IN_ALL
-IdeTreeNode      *ide_tree_node_get_parent             (IdeTreeNode         *self);
-IDE_AVAILABLE_IN_ALL
-gboolean          ide_tree_node_is_root                (IdeTreeNode         *self);
-IDE_AVAILABLE_IN_ALL
-gboolean          ide_tree_node_is_first               (IdeTreeNode         *self);
+void              ide_tree_node_unparent               (IdeTreeNode         *self);
 IDE_AVAILABLE_IN_ALL
-gboolean          ide_tree_node_is_last                (IdeTreeNode         *self);
+IdeTreeNode      *ide_tree_node_get_parent             (IdeTreeNode         *self);
 IDE_AVAILABLE_IN_ALL
 IdeTreeNode      *ide_tree_node_get_root               (IdeTreeNode         *self);
 IDE_AVAILABLE_IN_ALL
 gboolean          ide_tree_node_holds                  (IdeTreeNode         *self,
                                                         GType                type);
 IDE_AVAILABLE_IN_ALL
+void              ide_tree_node_insert_after           (IdeTreeNode         *node,
+                                                        IdeTreeNode         *parent,
+                                                        IdeTreeNode         *previous_sibling);
+IDE_AVAILABLE_IN_ALL
+void              ide_tree_node_insert_before          (IdeTreeNode         *node,
+                                                        IdeTreeNode         *parent,
+                                                        IdeTreeNode         *next_sibling);
+IDE_AVAILABLE_IN_ALL
+void              ide_tree_node_insert_sorted          (IdeTreeNode         *self,
+                                                        IdeTreeNode         *child,
+                                                        IdeTreeNodeCompare   cmpfn);
+IDE_AVAILABLE_IN_ALL
 void              ide_tree_node_traverse               (IdeTreeNode         *self,
                                                         GTraverseType        traverse_type,
                                                         GTraverseFlags       traverse_flags,
                                                         gint                 max_depth,
                                                         IdeTreeTraverseFunc  traverse_func,
                                                         gpointer             user_data);
-IDE_AVAILABLE_IN_ALL
-void              ide_tree_node_add_emblem             (IdeTreeNode         *self,
-                                                        GEmblem             *emblem);
-IDE_AVAILABLE_IN_ALL
-gboolean          ide_tree_node_get_reset_on_collapse  (IdeTreeNode         *self);
-IDE_AVAILABLE_IN_ALL
-void              ide_tree_node_set_reset_on_collapse  (IdeTreeNode         *self,
-                                                        gboolean             reset_on_collapse);
-IDE_AVAILABLE_IN_ALL
-const GdkRGBA    *ide_tree_node_get_background_rgba    (IdeTreeNode         *self);
-IDE_AVAILABLE_IN_ALL
-void              ide_tree_node_set_background_rgba    (IdeTreeNode         *self,
-                                                        const GdkRGBA       *background_rgba);
-IDE_AVAILABLE_IN_ALL
-const GdkRGBA    *ide_tree_node_get_foreground_rgba    (IdeTreeNode         *self);
-IDE_AVAILABLE_IN_ALL
-void              ide_tree_node_set_foreground_rgba    (IdeTreeNode         *self,
-                                                        const GdkRGBA       *foreground_rgba);
-IDE_AVAILABLE_IN_ALL
-gboolean          ide_tree_node_is_selected            (IdeTreeNode         *self);
-IDE_AVAILABLE_IN_ALL
-gboolean          ide_tree_node_get_use_markup         (IdeTreeNode         *self);
-IDE_AVAILABLE_IN_ALL
-void              ide_tree_node_set_use_markup         (IdeTreeNode         *self,
-                                                        gboolean             use_markup);
-IDE_AVAILABLE_IN_ALL
-void              ide_tree_node_set_flags              (IdeTreeNode         *self,
-                                                        IdeTreeNodeFlags     flags);
-IDE_AVAILABLE_IN_ALL
-IdeTreeNodeFlags  ide_tree_node_get_flags              (IdeTreeNode         *self);
 
 G_END_DECLS
diff --git a/src/libide/tree/ide-tree-private.h b/src/libide/tree/ide-tree-private.h
index 5b60d0e63..5a871707d 100644
--- a/src/libide/tree/ide-tree-private.h
+++ b/src/libide/tree/ide-tree-private.h
@@ -1,6 +1,6 @@
 /* ide-tree-private.h
  *
- * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2022 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,54 +20,28 @@
 
 #pragma once
 
+#include <libide-plugins.h>
+
 #include "ide-tree.h"
 #include "ide-tree-node.h"
-#include "ide-tree-model.h"
 
 G_BEGIN_DECLS
 
-GdkDragAction _ide_tree_get_drop_actions              (IdeTree         *tree);
-IdeTreeModel *_ide_tree_model_new                     (IdeTree         *tree);
-IdeTreeNode  *_ide_tree_get_drop_node                 (IdeTree         *tree);
-void          _ide_tree_model_release_addins          (IdeTreeModel    *self);
-void          _ide_tree_model_selection_changed       (IdeTreeModel    *model,
-                                                       GtkTreeIter     *selection);
-void          _ide_tree_model_build_node              (IdeTreeModel    *self,
-                                                       IdeTreeNode     *node);
-gboolean      _ide_tree_model_row_activated           (IdeTreeModel    *self,
-                                                       IdeTree         *tree,
-                                                       GtkTreePath     *path);
-void          _ide_tree_model_row_expanded            (IdeTreeModel    *self,
-                                                       IdeTree         *tree,
-                                                       GtkTreePath     *path);
-void          _ide_tree_model_row_collapsed           (IdeTreeModel    *self,
-                                                       IdeTree         *tree,
-                                                       GtkTreePath     *path);
-void          _ide_tree_model_cell_data_func          (IdeTreeModel    *self,
-                                                       GtkTreeIter     *iter,
-                                                       GtkCellRenderer *cell);
-gboolean      _ide_tree_model_contains_node           (IdeTreeModel    *self,
-                                                       IdeTreeNode     *node);
-gboolean      _ide_tree_node_get_loading              (IdeTreeNode     *self,
-                                                       gint64          *loading_started_at);
-void          _ide_tree_node_set_loading              (IdeTreeNode     *self,
-                                                       gboolean         loading);
-void          _ide_tree_node_dump                     (IdeTreeNode     *self);
-void          _ide_tree_node_remove_all               (IdeTreeNode     *self);
-void          _ide_tree_node_set_model                (IdeTreeNode     *self,
-                                                       IdeTreeModel    *model);
-gboolean      _ide_tree_node_get_needs_build_children (IdeTreeNode     *self);
-void          _ide_tree_node_set_needs_build_children (IdeTreeNode     *self,
-                                                       gboolean         needs_build_children);
-void          _ide_tree_node_show_popover             (IdeTreeNode     *node,
-                                                       IdeTree         *tree,
-                                                       GtkPopover      *popover);
-GIcon        *_ide_tree_node_apply_emblems            (IdeTreeNode     *self,
-                                                       GIcon           *base);
-void          _ide_tree_node_apply_colors             (IdeTreeNode     *self,
-                                                       GtkCellRenderer *cell);
-void          _ide_tree_node_get_area                 (IdeTreeNode     *self,
-                                                       IdeTree         *tree,
-                                                       GdkRectangle    *area);
+GtkTreeListRow *_ide_tree_get_row_at_node      (IdeTree                 *self,
+                                                IdeTreeNode             *node,
+                                                gboolean                 expand_to_row);
+gboolean        _ide_tree_node_children_built  (IdeTreeNode             *self);
+guint           _ide_tree_node_get_child_index (IdeTreeNode             *parent,
+                                                IdeTreeNode             *child);
+IdeTree        *_ide_tree_node_get_tree        (IdeTreeNode             *self);
+void            _ide_tree_node_collapsed       (IdeTreeNode             *self);
+void            _ide_tree_node_expand_async    (IdeTreeNode             *self,
+                                                IdeExtensionSetAdapter  *addins,
+                                                GCancellable            *cancellable,
+                                                GAsyncReadyCallback      callback,
+                                                gpointer                 user_data);
+gboolean        _ide_tree_node_expand_finish   (IdeTreeNode             *self,
+                                                GAsyncResult            *result,
+                                                GError                 **error);
 
 G_END_DECLS
diff --git a/src/libide/tree/ide-tree.c b/src/libide/tree/ide-tree.c
index e3edb91c1..9b9855234 100644
--- a/src/libide/tree/ide-tree.c
+++ b/src/libide/tree/ide-tree.c
@@ -1,6 +1,6 @@
 /* ide-tree.c
  *
- * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2022 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
@@ -22,565 +22,392 @@
 
 #include "config.h"
 
-#include <libpeas/peas.h>
-#include <libide-core.h>
+#include <libide-gtk.h>
+#include <libide-plugins.h>
 #include <libide-threading.h>
 
-#include "ide-cell-renderer-status.h"
-#include "ide-popover-positioner.h"
 #include "ide-tree.h"
-#include "ide-tree-model.h"
-#include "ide-tree-node.h"
+#include "ide-tree-addin.h"
 #include "ide-tree-private.h"
-
-/* FIXME-GTK4:
- *
- * We still need DnD work here but that has changed so much
- * that it would help to have a plugin using this to ensure
- * we're porting it correctly.
- */
+#include "ide-tree-empty.h"
 
 typedef struct
 {
-  /* This #GCancellable will be automatically cancelled when the widget is
-   * destroyed. That is usefulf or async operations that you want to be
-   * cleaned up as the workspace is destroyed or the widget in question
-   * removed from the widget tree.
-   */
-  GCancellable *cancellable;
-
-  /* To keep rendering of common styles fast, we share these PangoAttrList
-   * so that we need not re-create them many times.
-   */
-  PangoAttrList *dim_label_attributes;
-  PangoAttrList *header_attributes;
+  IdeExtensionSetAdapter *addins;
+  GtkTreeListModel       *tree_model;
+  IdeTreeNode            *root;
+  char                   *kind;
+  GMenuModel             *menu_model;
+
+  GtkListView            *list_view;
+  GtkSingleSelection     *selection;
+} IdeTreePrivate;
 
-  /* The context menu to use for popups */
-  GMenu *context_menu;
+enum {
+  PROP_0,
+  PROP_KIND,
+  PROP_MENU_MODEL,
+  PROP_ROOT,
+  PROP_SELECTED_NODE,
+  N_PROPS
+};
 
-  /* Our context menu popover */
-  GtkPopover *popover;
+G_DEFINE_TYPE_WITH_PRIVATE (IdeTree, ide_tree, GTK_TYPE_WIDGET)
 
-  /* Stashed drop information to propagate on drop */
-  GdkDragAction drop_action;
-  GtkTreePath *drop_path;
-  GtkTreeViewDropPosition drop_pos;
-} IdeTreePrivate;
+static GParamSpec *properties [N_PROPS];
 
-G_DEFINE_TYPE_WITH_PRIVATE (IdeTree, ide_tree, GTK_TYPE_TREE_VIEW)
-
-static IdeTreeModel *
-ide_tree_get_model (IdeTree *self)
+typedef struct
 {
-  GtkTreeModel *model;
+  IdeTree     *tree;
+  IdeTreeNode *node;
+  guint        handled : 1;
+} NodeActivated;
 
-  g_assert (IDE_IS_TREE (self));
+static void
+ide_tree_node_activated_cb (IdeExtensionSetAdapter *addins,
+                            PeasPluginInfo         *plugin_info,
+                            PeasExtension          *extension,
+                            gpointer                user_data)
+{
+  IdeTreeAddin *addin = (IdeTreeAddin *)extension;
+  NodeActivated *state = user_data;
 
-  if (!(model = gtk_tree_view_get_model (GTK_TREE_VIEW (self))) ||
-      !IDE_IS_TREE_MODEL (model))
-    return NULL;
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (addins));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (state != NULL);
 
-  return IDE_TREE_MODEL (model);
+  if (!state->handled)
+    state->handled = ide_tree_addin_node_activated (addin, state->tree, state->node);
 }
 
 static void
-ide_tree_selection_changed_cb (IdeTree          *self,
-                               GtkTreeSelection *selection)
+ide_tree_activate_cb (IdeTree     *self,
+                      guint        position,
+                      GtkListView *list_view)
 {
-  IdeTreeModel *model;
-  GtkTreeIter iter;
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  g_autoptr(GtkTreeListRow) row = NULL;
+  g_autoptr(IdeTreeNode) node = NULL;
+  NodeActivated state;
+
+  IDE_ENTRY;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_TREE (self));
-  g_assert (GTK_IS_TREE_SELECTION (selection));
+  g_assert (GTK_IS_LIST_VIEW (list_view));
 
-  if (!(model = ide_tree_get_model (self)))
-    return;
+  if (!(row = g_list_model_get_item (G_LIST_MODEL (priv->selection), position)) ||
+      !(node = gtk_tree_list_row_get_item (row)))
+    IDE_EXIT;
 
-  if (gtk_tree_selection_get_selected (selection, NULL, &iter))
-    _ide_tree_model_selection_changed (model, &iter);
-  else
-    _ide_tree_model_selection_changed (model, NULL);
+  state.tree = self;
+  state.node = node;
+  state.handled = FALSE;
+
+  ide_extension_set_adapter_foreach (priv->addins,
+                                     ide_tree_node_activated_cb,
+                                     &state);
+
+  IDE_EXIT;
 }
 
 static void
-ide_tree_unselect (IdeTree *self)
+ide_tree_notify_selected_cb (IdeTree            *self,
+                             GParamSpec         *pspec,
+                             GtkSingleSelection *selection)
 {
   g_assert (IDE_IS_TREE (self));
+  g_assert (GTK_IS_SINGLE_SELECTION (selection));
 
-  gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (GTK_TREE_VIEW (self)));
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SELECTED_NODE]);
 }
 
 static void
-ide_tree_select (IdeTree     *self,
-                 IdeTreeNode *node)
+ide_tree_extension_added_cb (IdeExtensionSetAdapter *addins,
+                             PeasPluginInfo         *plugin_info,
+                             PeasExtension          *extension,
+                             gpointer                user_data)
 {
-  g_autoptr(GtkTreePath) path = NULL;
-  GtkTreeSelection *selection;
-
-  g_assert (IDE_IS_TREE (self));
-  g_assert (IDE_IS_TREE_NODE (node));
+  IdeTreeAddin *addin = (IdeTreeAddin *)extension;
+  IdeTree *self = user_data;
 
-  ide_tree_unselect (self);
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (addins));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (addin));
 
-  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self));
-  path = ide_tree_node_get_path (node);
-  gtk_tree_selection_select_path (selection, path);
-  gtk_tree_view_set_cursor (GTK_TREE_VIEW (self), path, NULL, FALSE);
+  ide_tree_addin_load (addin, self);
 }
 
 static void
-state_cell_func (GtkCellLayout   *layout,
-                 GtkCellRenderer *cell,
-                 GtkTreeModel    *model,
-                 GtkTreeIter     *iter,
-                 gpointer         user_data)
+ide_tree_extension_removed_cb (IdeExtensionSetAdapter *addins,
+                               PeasPluginInfo         *plugin_info,
+                               PeasExtension          *extension,
+                               gpointer                user_data)
 {
-  IdeTreeNodeFlags flags = 0;
-  IdeTreeNode *node;
-
-  g_assert (IDE_IS_TREE (user_data));
-  g_assert (IDE_IS_TREE_MODEL (model));
+  IdeTreeAddin *addin = (IdeTreeAddin *)extension;
+  IdeTree *self = user_data;
 
-  if ((node = ide_tree_model_get_node (IDE_TREE_MODEL (model), iter)))
-    flags = ide_tree_node_get_flags (node);
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (addins));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (addin));
 
-  ide_cell_renderer_status_set_flags (cell, flags);
+  ide_tree_addin_unload (addin, self);
 }
 
 static void
-text_cell_func (GtkCellLayout   *layout,
-                GtkCellRenderer *cell,
-                GtkTreeModel    *model,
-                GtkTreeIter     *iter,
-                gpointer         user_data)
+ide_tree_root (GtkWidget *widget)
 {
-  IdeTree *self = user_data;
+  IdeTree *self = (IdeTree *)widget;
   IdeTreePrivate *priv = ide_tree_get_instance_private (self);
-  const gchar *display_name = NULL;
-  IdeTreeNode *node;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_TREE (self));
-  g_assert (IDE_IS_TREE_MODEL (model));
 
-  g_object_set (cell,
-                "attributes", NULL,
-                "foreground-set", FALSE,
-                NULL);
+  GTK_WIDGET_CLASS (ide_tree_parent_class)->root (widget);
 
-  if (!(node = ide_tree_model_get_node (IDE_TREE_MODEL (model), iter)))
+  if (priv->addins != NULL)
     return;
 
-  _ide_tree_model_cell_data_func (IDE_TREE_MODEL (model), iter, cell);
-
-  /* If we're loading the node, avoid showing the "Loading..." text for 250
-   * milliseconds, so that we don't flash the user with information they'll
-   * never be able to read.
-   */
-  if (ide_tree_node_is_empty (node))
-    {
-      IdeTreeNode *parent = ide_tree_node_get_parent (node);
-      gint64 started_loading_at;
-
-      if (_ide_tree_node_get_loading (parent, &started_loading_at))
-        {
-          gint64 now = g_get_monotonic_time ();
-
-          if ((now - started_loading_at) < (G_USEC_PER_SEC / 4L))
-            goto set_props;
-        }
-    }
-
-  if (ide_tree_node_get_has_error (node))
-    {
-      PangoAttrList *attrs = NULL;
-      PangoAttrList *copy = NULL;
-
-      g_object_get (cell,
-                    "attributes", &attrs,
-                    NULL);
-
-      if (attrs != NULL)
-        copy = pango_attr_list_copy (attrs);
-      else
-        copy = pango_attr_list_new ();
-
-      pango_attr_list_insert (copy, pango_attr_underline_new (PANGO_UNDERLINE_ERROR));
-      pango_attr_list_insert (copy, pango_attr_underline_color_new (65535, 0, 0));
-
-      g_object_set (cell,
-                    "attributes", copy,
-                    NULL);
-
-      g_clear_pointer (&attrs, pango_attr_list_unref);
-      g_clear_pointer (&copy, pango_attr_list_unref);
-    }
-
-  if (ide_tree_node_get_is_header (node) ||
-      (ide_tree_node_get_flags (node) & IDE_TREE_NODE_FLAGS_ADDED))
-    g_object_set (cell, "attributes", priv->header_attributes, NULL);
-  else if (ide_tree_node_is_empty (node) && !ide_tree_node_is_selected (node))
-    g_object_set (cell, "attributes", priv->dim_label_attributes, NULL);
-
-  display_name = ide_tree_node_get_display_name (node);
-
-set_props:
-  if (ide_tree_node_get_use_markup (node))
-    g_object_set (cell, "markup", display_name, NULL);
-  else
-    g_object_set (cell, "text", display_name, NULL);
+  priv->addins = ide_extension_set_adapter_new (NULL,
+                                                peas_engine_get_default (),
+                                                IDE_TYPE_TREE_ADDIN,
+                                                "Tree-Kind", priv->kind);
+  g_signal_connect (priv->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_tree_extension_added_cb),
+                    self);
+  g_signal_connect (priv->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_tree_extension_removed_cb),
+                    self);
+  ide_extension_set_adapter_foreach (priv->addins,
+                                     ide_tree_extension_added_cb,
+                                     self);
+
+  if (priv->root != NULL)
+    _ide_tree_node_expand_async (priv->root, priv->addins, NULL, NULL, NULL);
 }
 
 static void
-pixbuf_cell_func (GtkCellLayout   *layout,
-                  GtkCellRenderer *cell,
-                  GtkTreeModel    *model,
-                  GtkTreeIter     *iter,
-                  gpointer         user_data)
+ide_tree_list_item_setup_cb (IdeTree                  *self,
+                             GtkListItem              *item,
+                             GtkSignalListItemFactory *factory)
 {
-  IdeTree *self = user_data;
-  g_autoptr(GtkTreePath) path = NULL;
-  g_autoptr(GIcon) emblems = NULL;
-  IdeTreeNode *node;
-  GIcon *icon;
-
   g_assert (IDE_IS_TREE (self));
-  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (GTK_IS_LIST_ITEM (item));
+  g_assert (GTK_IS_SIGNAL_LIST_ITEM_FACTORY (factory));
 
-  if (!(node = ide_tree_model_get_node (IDE_TREE_MODEL (model), iter)))
-    return;
-
-  path = gtk_tree_model_get_path (model, iter);
-
-  if (gtk_tree_view_row_expanded (GTK_TREE_VIEW (self), path))
-    icon = ide_tree_node_get_expanded_icon (node);
-  else
-    icon = ide_tree_node_get_icon (node);
-
-  if (icon != NULL)
-    emblems = _ide_tree_node_apply_emblems (node, icon);
-
-  g_object_set (cell, "gicon", emblems, NULL);
+  gtk_list_item_set_child (item, ide_tree_expander_new ());
 }
 
 static void
-ide_tree_expand_cb (GObject      *object,
-                    GAsyncResult *result,
-                    gpointer      user_data)
+ide_tree_list_item_teardown_cb (IdeTree                  *self,
+                                GtkListItem              *item,
+                                GtkSignalListItemFactory *factory)
 {
-  IdeTreeModel *model = (IdeTreeModel *)object;
-  g_autoptr(GtkTreePath) path = NULL;
-  g_autoptr(IdeTask) task = user_data;
-  IdeTreeNode *node;
-  IdeTree *self;
-
-  g_assert (IDE_IS_MAIN_THREAD ());
-  g_assert (IDE_IS_TREE_MODEL (model));
-  g_assert (G_IS_ASYNC_RESULT (result));
-  g_assert (IDE_IS_TASK (task));
-
-  self = ide_task_get_source_object (task);
-  node = ide_task_get_task_data (task);
-
   g_assert (IDE_IS_TREE (self));
-  g_assert (IDE_IS_TREE_NODE (node));
-
-  if ((path = ide_tree_model_get_path_for_node (model, node)))
-    {
-      if (ide_tree_model_expand_finish (model, result, NULL))
-        {
-          /* If node was detached during our async operation, we'll get NULL
-           * back for the GtkTreePath (in which case, we'll just ignore).
-           */
-          gtk_tree_view_expand_row (GTK_TREE_VIEW (self), path, FALSE);
-        }
-
-      _ide_tree_model_row_expanded (model, self, path);
-    }
+  g_assert (GTK_IS_LIST_ITEM (item));
+  g_assert (GTK_IS_SIGNAL_LIST_ITEM_FACTORY (factory));
 
-  ide_task_return_boolean (task, TRUE);
+  gtk_list_item_set_child (item, NULL);
 }
 
 static void
-ide_tree_row_activated (GtkTreeView       *tree_view,
-                        GtkTreePath       *path,
-                        GtkTreeViewColumn *column)
+ide_tree_row_notify_expanded_cb (IdeTree        *self,
+                                 GParamSpec     *pspec,
+                                 GtkTreeListRow *row)
 {
-  IdeTree *self = (IdeTree *)tree_view;
-  IdeTreeModel *model;
-  IdeTreeNode *node;
-  GtkTreeIter iter;
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  g_autoptr(IdeTreeNode) node = NULL;
 
   g_assert (IDE_IS_TREE (self));
-  g_assert (path != NULL);
-  g_assert (GTK_IS_TREE_VIEW_COLUMN (column));
+  g_assert (GTK_IS_TREE_LIST_ROW (row));
 
-  /* Get our model, and the node in question. Ignore everything if this
-   * is a synthesized "Empty" node.
-   */
-  if (!(model = ide_tree_get_model (self)) ||
-      !gtk_tree_model_get_iter (GTK_TREE_MODEL (model), &iter, path) ||
-      !(node = ide_tree_model_get_node (model, &iter)) ||
-      ide_tree_node_is_empty (node))
-    return;
+  node = gtk_tree_list_row_get_item (row);
 
-  if (!_ide_tree_model_row_activated (model, self, path))
+  if (gtk_tree_list_row_get_expanded (row))
     {
-      if (gtk_tree_view_row_expanded (tree_view, path))
-        gtk_tree_view_collapse_row (tree_view, path);
-      else
-        gtk_tree_view_expand_row (tree_view, path, FALSE);
+      if (!_ide_tree_node_children_built (node))
+        _ide_tree_node_expand_async (node, priv->addins, NULL, NULL, NULL);
+    }
+  else
+    {
+      if (node != NULL)
+        _ide_tree_node_collapsed (node);
     }
 }
 
 static void
-ide_tree_row_expanded (GtkTreeView *tree_view,
-                       GtkTreeIter *iter,
-                       GtkTreePath *path)
+ide_tree_list_item_bind_cb (IdeTree                  *self,
+                            GtkListItem              *item,
+                            GtkSignalListItemFactory *factory)
 {
-  IdeTree *self = (IdeTree *)tree_view;
-  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
-  g_autoptr(IdeTask) task = NULL;
-  IdeTreeModel *model;
-  IdeTreeNode *node;
+  g_autoptr(IdeTreeNode) node = NULL;
+  IdeTreeExpander *expander;
+  GtkTreeListRow *row;
 
-  g_assert (IDE_IS_TREE (tree_view));
-  g_assert (iter != NULL);
-  g_assert (IDE_IS_TREE_NODE (iter->user_data));
-  g_assert (path != NULL);
+  g_assert (IDE_IS_TREE (self));
+  g_assert (GTK_IS_LIST_ITEM (item));
+  g_assert (GTK_IS_SIGNAL_LIST_ITEM_FACTORY (factory));
 
-  if (!(model = ide_tree_get_model (self)) ||
-      !(node = ide_tree_model_get_node (model, iter)) ||
-      !ide_tree_node_get_children_possible (node))
-    return;
+  row = GTK_TREE_LIST_ROW (gtk_list_item_get_item (item));
+  expander = IDE_TREE_EXPANDER (gtk_list_item_get_child (item));
+  node = gtk_tree_list_row_get_item (row);
 
-  task = ide_task_new (self, priv->cancellable, NULL, NULL);
-  ide_task_set_source_tag (task, ide_tree_row_expanded);
-  ide_task_set_task_data (task, g_object_ref (node), g_object_unref);
+  g_assert (GTK_IS_TREE_LIST_ROW (row));
+  g_assert (IDE_IS_TREE_EXPANDER (expander));
+  g_assert (IDE_IS_TREE_NODE (node));
 
-  /* We want to expand the row if we can, but we need to ensure the
-   * children have been built first (it might only have a fake "empty"
-   * node currently). So we request that the model expand the row and
-   * then expand to the path on the callback. The model will do nothing
-   * more than complete the async request if there is nothing to build.
-   */
-  ide_tree_model_expand_async (IDE_TREE_MODEL (model),
-                               node,
-                               priv->cancellable,
-                               ide_tree_expand_cb,
-                               g_steal_pointer (&task));
-}
+  ide_tree_expander_set_list_row (expander, row);
 
-static void
-ide_tree_row_collapsed (GtkTreeView *tree_view,
-                        GtkTreeIter *iter,
-                        GtkTreePath *path)
-{
-  IdeTree *self = (IdeTree *)tree_view;
-  IdeTreeModel *model;
-  IdeTreeNode *node;
+#define BIND_PROPERTY(name) \
+  G_STMT_START { \
+    GBinding *binding = g_object_bind_property (node, name, expander, name, G_BINDING_SYNC_CREATE); \
+    g_object_set_data (G_OBJECT (expander), "BINDING_" name, binding); \
+  } G_STMT_END
 
-  g_assert (IDE_IS_TREE (self));
-  g_assert (iter != NULL);
-  g_assert (path != NULL);
+  BIND_PROPERTY ("expanded-icon");
+  BIND_PROPERTY ("icon");
+  BIND_PROPERTY ("title");
 
-  if (!(model = ide_tree_get_model (self)) ||
-      !(node = ide_tree_model_get_node (IDE_TREE_MODEL (model), iter)))
-    return;
-
-  /*
-   * If we are collapsing a row that requests to have its children removed
-   * and the dummy node re-inserted, go ahead and do so now.
-   */
-  if (ide_tree_node_get_reset_on_collapse (node))
-    _ide_tree_node_remove_all (node);
+#undef BIND_PROPERTY
 
-  _ide_tree_model_row_collapsed (model, self, path);
+  g_signal_connect_object (row,
+                           "notify::expanded",
+                           G_CALLBACK (ide_tree_row_notify_expanded_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
 }
 
 static void
-ide_tree_popup (IdeTree        *self,
-                IdeTreeNode    *node,
-                double          target_x,
-                double          target_y)
+ide_tree_list_item_unbind_cb (IdeTree                  *self,
+                              GtkListItem              *item,
+                              GtkSignalListItemFactory *factory)
 {
-  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
-  GdkRectangle area;
-  GtkTextDirection dir;
-  GtkWidget *positioner;
+  IdeTreeExpander *expander;
+  GtkTreeListRow *row;
 
   g_assert (IDE_IS_TREE (self));
-  g_assert (IDE_IS_TREE_NODE (node));
+  g_assert (GTK_IS_LIST_ITEM (item));
+  g_assert (GTK_IS_SIGNAL_LIST_ITEM_FACTORY (factory));
 
-  if (priv->context_menu == NULL)
-    return;
+  row = GTK_TREE_LIST_ROW (gtk_list_item_get_item (item));
+  expander = IDE_TREE_EXPANDER (gtk_list_item_get_child (item));
 
-  if (!(positioner = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_POPOVER_POSITIONER)))
-    return;
+  g_signal_handlers_disconnect_by_func (row,
+                                        G_CALLBACK (ide_tree_row_notify_expanded_cb),
+                                        self);
+
+#define UNBIND_PROPERTY(name) \
+  G_STMT_START { \
+    GBinding *binding = g_object_steal_data (G_OBJECT (expander), "BINDING_" name); \
+    g_binding_unbind (binding); \
+  } G_STMT_END
 
-  priv->popover = GTK_POPOVER (gtk_popover_menu_new_from_model (G_MENU_MODEL (priv->context_menu)));
-  g_object_ref_sink (priv->popover);
+  UNBIND_PROPERTY ("expanded-icon");
+  UNBIND_PROPERTY ("icon");
+  UNBIND_PROPERTY ("title");
 
-  dir = gtk_widget_get_direction (GTK_WIDGET (self));
-  gtk_popover_set_position (priv->popover, dir == GTK_TEXT_DIR_LTR ? GTK_POS_RIGHT : GTK_POS_LEFT);
+#undef UNBIND_PROPERTY
 
-  _ide_tree_node_get_area (node, self, &area);
+  g_object_set (expander,
+                "expanded-icon", NULL,
+                "icon", NULL,
+                "title", NULL,
+                NULL);
 
-  ide_popover_positioner_present (IDE_POPOVER_POSITIONER (positioner),
-                                  priv->popover,
-                                  GTK_WIDGET (self),
-                                  &area);
+  ide_tree_expander_set_list_row (expander, NULL);
 }
 
 static void
-ide_tree_click_pressed_cb (IdeTree         *self,
-                           int              n_presses,
-                           double           x,
-                           double           y,
-                           GtkGestureClick *gesture)
+ide_tree_dispose (GObject *object)
 {
-  g_autoptr(GtkTreePath) path = NULL;
-  IdeTreeModel *model;
-  int cell_y;
-
-  g_assert (IDE_IS_TREE (self));
-  g_assert (GTK_IS_GESTURE_CLICK (gesture));
-
-  if (!(model = ide_tree_get_model (self)))
-    return;
+  IdeTree *self = (IdeTree *)object;
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
 
-  if (!gtk_widget_has_focus (GTK_WIDGET (self)))
-    gtk_widget_grab_focus (GTK_WIDGET (self));
+  ide_clear_and_destroy_object (&priv->addins);
 
-  gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (self),
-                                 x,
-                                 y,
-                                 &path,
-                                 NULL,
-                                 NULL,
-                                 &cell_y);
+  g_clear_pointer ((GtkWidget **)&priv->list_view, gtk_widget_unparent);
 
-  if (path == NULL)
-    {
-      ide_tree_unselect (self);
-    }
-  else
-    {
-      GtkAllocation alloc;
-      GtkTreeIter iter;
+  ide_tree_set_root (self, NULL);
 
-      gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+  if (priv->selection != NULL)
+    gtk_single_selection_set_model (priv->selection, NULL);
 
-      if (gtk_tree_model_get_iter (GTK_TREE_MODEL (model), &iter, path))
-        {
-          IdeTreeNode *node;
+  g_clear_object (&priv->tree_model);
+  g_clear_object (&priv->menu_model);
 
-          node = ide_tree_model_get_node (IDE_TREE_MODEL (model), &iter);
-          ide_tree_select (self, node);
-          ide_tree_popup (self, node, alloc.x + alloc.width, y - cell_y);
-        }
-    }
+  g_clear_pointer (&priv->kind, g_free);
 
-  gtk_gesture_set_state (GTK_GESTURE (gesture),  GTK_EVENT_SEQUENCE_CLAIMED);
+  G_OBJECT_CLASS (ide_tree_parent_class)->dispose (object);
 }
 
 static void
-show_context_menu_action (GtkWidget  *widget,
-                          const char *action_name,
-                          GVariant   *param)
+ide_tree_get_property (GObject    *object,
+                       guint       prop_id,
+                       GValue     *value,
+                       GParamSpec *pspec)
 {
-  IdeTree *self = (IdeTree *)widget;
-  g_autoptr(GtkTreePath) path = NULL;
-  IdeTreeModel *model;
-  IdeTreeNode *node;
-  GtkAllocation alloc;
-  GdkRectangle cell_area;
-
-  g_assert (IDE_IS_TREE (self));
-
-  if (!(model = ide_tree_get_model (self)))
-    return;
-
-  if (!gtk_widget_has_focus (GTK_WIDGET (self)))
-    gtk_widget_grab_focus (GTK_WIDGET (self));
-
-  if (!(node = ide_tree_get_selected_node (self)))
-    return;
-
-  if (!(path = ide_tree_node_get_path (node)))
-    return;
+  IdeTree *self = IDE_TREE (object);
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
 
-  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
-  gtk_tree_view_get_cell_area (GTK_TREE_VIEW (self),
-                               path,
-                               NULL,
-                               &cell_area);
+  switch (prop_id)
+    {
+    case PROP_KIND:
+      g_value_set_string (value, priv->kind);
+      break;
 
-  ide_tree_popup (self, node, alloc.x + alloc.width, cell_area.y + (cell_area.height/2));
-}
+    case PROP_MENU_MODEL:
+      g_value_set_object (value, ide_tree_get_menu_model (self));
+      break;
 
-static gboolean
-ide_tree_query_tooltip (GtkWidget  *widget,
-                        gint        x,
-                        gint        y,
-                        gboolean    keyboard_mode,
-                        GtkTooltip *tooltip)
-{
-  GtkTreeView *tree_view = (GtkTreeView *)widget;
-  g_autoptr(GtkTreePath) path = NULL;
+    case PROP_ROOT:
+      g_value_set_object (value, ide_tree_get_root (self));
+      break;
 
-  g_assert (IDE_IS_TREE (tree_view));
-  g_assert (GTK_IS_TOOLTIP (tooltip));
+    case PROP_SELECTED_NODE:
+      g_value_set_object (value, ide_tree_get_selected_node (self));
+      break;
 
-  if (gtk_tree_view_get_path_at_pos (tree_view, x, y, &path, NULL, NULL, NULL))
-    {
-      GtkTreeModel *model = gtk_tree_view_get_model (tree_view);
-      GtkTreeIter iter;
-
-      if (gtk_tree_model_get_iter (model, &iter, path))
-        {
-          IdeTreeNode *node;
-
-          node = ide_tree_model_get_node (IDE_TREE_MODEL (model), &iter);
-
-          if (node != NULL)
-            {
-              gtk_tree_view_set_tooltip_row (tree_view, tooltip, path);
-              gtk_tooltip_set_markup (tooltip,
-                                      ide_tree_node_get_display_name (node));
-              return TRUE;
-            }
-        }
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
     }
-
-  return FALSE;
 }
 
 static void
-ide_tree_dispose (GObject *object)
+ide_tree_set_property (GObject      *object,
+                       guint         prop_id,
+                       const GValue *value,
+                       GParamSpec   *pspec)
 {
-  IdeTree *self = (IdeTree *)object;
+  IdeTree *self = IDE_TREE (object);
   IdeTreePrivate *priv = ide_tree_get_instance_private (self);
-  IdeTreeModel *model;
-
-  g_assert (IDE_IS_MAIN_THREAD ());
 
-  if ((model = ide_tree_get_model (self)))
-    _ide_tree_model_release_addins (model);
-
-  g_clear_object (&priv->popover);
-
-  gtk_tree_view_set_model (GTK_TREE_VIEW (self), NULL);
+  switch (prop_id)
+    {
+    case PROP_KIND:
+      priv->kind = g_value_dup_string (value);
+      break;
 
-  g_cancellable_cancel (priv->cancellable);
-  g_clear_object (&priv->cancellable);
+    case PROP_MENU_MODEL:
+      ide_tree_set_menu_model (self, g_value_get_object (value));
+      break;
 
-  g_clear_object (&priv->context_menu);
+    case PROP_ROOT:
+      ide_tree_set_root (self, g_value_get_object (value));
+      break;
 
-  g_clear_pointer (&priv->dim_label_attributes, pango_attr_list_unref);
-  g_clear_pointer (&priv->header_attributes, pango_attr_list_unref);
-  g_clear_pointer (&priv->drop_path, gtk_tree_path_free);
+    case PROP_SELECTED_NODE:
+      ide_tree_set_selected_node (self, g_value_get_object (value));
+      break;
 
-  G_OBJECT_CLASS (ide_tree_parent_class)->dispose (object);
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
 }
 
 static void
@@ -588,99 +415,133 @@ ide_tree_class_init (IdeTreeClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
-  GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
 
   object_class->dispose = ide_tree_dispose;
-
-  widget_class->query_tooltip = ide_tree_query_tooltip;
-
-  tree_view_class->row_activated = ide_tree_row_activated;
-  tree_view_class->row_expanded = ide_tree_row_expanded;
-  tree_view_class->row_collapsed = ide_tree_row_collapsed;
-
-  gtk_widget_class_install_action (widget_class, "tree.popup-context-menu", NULL, show_context_menu_action);
-  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_F10, GDK_SHIFT_MASK, "tree.popup-context-menu", 
NULL);
-  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_Menu, 0, "tree.popup-context-menu", NULL);
+  object_class->get_property = ide_tree_get_property;
+  object_class->set_property = ide_tree_set_property;
+
+  widget_class->root = ide_tree_root;
+
+  properties[PROP_KIND] =
+    g_param_spec_string ("kind", NULL, NULL,
+                         NULL,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_MENU_MODEL] =
+    g_param_spec_object ("menu-model", NULL, NULL,
+                         G_TYPE_MENU_MODEL,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_EXPLICIT_NOTIFY |
+                          G_PARAM_STATIC_STRINGS));
+
+  properties[PROP_ROOT] =
+    g_param_spec_object ("root", NULL, NULL,
+                         IDE_TYPE_TREE_NODE,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SELECTED_NODE] =
+    g_param_spec_object ("selected-node", NULL, NULL,
+                         IDE_TYPE_TREE_NODE,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-tree/ide-tree.ui");
+  gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeTree, list_view);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeTree, selection);
+
+  gtk_widget_class_bind_template_callback (widget_class, ide_tree_activate_cb);
+  gtk_widget_class_bind_template_callback (widget_class, ide_tree_notify_selected_cb);
+  gtk_widget_class_bind_template_callback (widget_class, ide_tree_list_item_bind_cb);
+  gtk_widget_class_bind_template_callback (widget_class, ide_tree_list_item_unbind_cb);
+  gtk_widget_class_bind_template_callback (widget_class, ide_tree_list_item_setup_cb);
+  gtk_widget_class_bind_template_callback (widget_class, ide_tree_list_item_teardown_cb);
 }
 
 static void
 ide_tree_init (IdeTree *self)
 {
-  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
-  GtkGesture *gesture;
-  GtkCellRenderer *cell;
-  GtkTreeViewColumn *column;
-
-  /* Show popover on right-click */
-  gesture = gtk_gesture_click_new ();
-  gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), 3);
-  g_signal_connect_object (gesture,
-                           "pressed",
-                           G_CALLBACK (ide_tree_click_pressed_cb),
-                           self,
-                           G_CONNECT_SWAPPED);
-  gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (gesture));
-
-  priv->cancellable = g_cancellable_new ();
-
-  gtk_widget_set_has_tooltip (GTK_WIDGET (self), TRUE);
-
-  g_signal_connect_object (gtk_tree_view_get_selection (GTK_TREE_VIEW (self)),
-                           "changed",
-                           G_CALLBACK (ide_tree_selection_changed_cb),
-                           self,
-                           G_CONNECT_SWAPPED);
-
-  gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (self), FALSE);
-  gtk_tree_view_set_activate_on_single_click (GTK_TREE_VIEW (self), TRUE);
-
-  column = gtk_tree_view_column_new ();
-  gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
-  cell = g_object_new (GTK_TYPE_CELL_RENDERER_PIXBUF,
-                       "xpad", 6,
-                       NULL);
-  gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, FALSE);
-  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column), cell, pixbuf_cell_func, self, NULL);
-
-  cell = g_object_new (GTK_TYPE_CELL_RENDERER_TEXT,
-                       "ellipsize", PANGO_ELLIPSIZE_END,
-                       "ypad", 6,
-                       NULL);
-  gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, TRUE);
-  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column), cell, text_cell_func, self, NULL);
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
 
-  cell = ide_cell_renderer_status_new ();
-  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column), cell, state_cell_func, self, NULL);
-  gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, FALSE);
+static GListModel *
+ide_tree_create_child_model_cb (gpointer item,
+                                gpointer user_data)
+{
+  IdeTreeNode *node = item;
 
-  gtk_tree_view_append_column (GTK_TREE_VIEW (self), column);
+  g_assert (IDE_IS_TREE_NODE (node));
+  g_assert (user_data == NULL);
 
-  priv->dim_label_attributes = pango_attr_list_new ();
-  pango_attr_list_insert (priv->dim_label_attributes,
-                          pango_attr_foreground_alpha_new (65535 * 0.55));
+  if (ide_tree_node_get_children_possible (node))
+    return G_LIST_MODEL (ide_tree_empty_new (G_LIST_MODEL (node)));
 
-  priv->header_attributes = pango_attr_list_new ();
-  pango_attr_list_insert (priv->header_attributes,
-                          pango_attr_weight_new (PANGO_WEIGHT_BOLD));
+  return NULL;
 }
 
-GtkWidget *
-ide_tree_new (void)
+/**
+ * ide_tree_get_root:
+ * @self: a #IdeTree
+ *
+ * Gets the root node.
+ *
+ * Returns: (transfer none) (nullable): an IdeTreeNode or %NULL
+ */
+IdeTreeNode *
+ide_tree_get_root (IdeTree *self)
 {
-  return g_object_new (IDE_TYPE_TREE, NULL);
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TREE (self), NULL);
+
+  return priv->root;
 }
 
 void
-ide_tree_set_context_menu (IdeTree *self,
-                           GMenu   *menu)
+ide_tree_set_root (IdeTree     *self,
+                   IdeTreeNode *root)
 {
   IdeTreePrivate *priv = ide_tree_get_instance_private (self);
 
   g_return_if_fail (IDE_IS_TREE (self));
-  g_return_if_fail (!menu || G_IS_MENU (menu));
+  g_return_if_fail (!root || IDE_IS_TREE_NODE (root));
+
+  if (priv->root == root)
+    return;
+
+  gtk_single_selection_set_model (priv->selection, NULL);
+  g_clear_object (&priv->tree_model);
+
+  if (priv->root != NULL)
+    {
+      g_object_set_data (G_OBJECT (priv->root), "IDE_TREE", NULL);
+      g_clear_object (&priv->root);
+    }
 
-  if (g_set_object (&priv->context_menu, menu))
-    g_clear_object (&priv->popover);
+  g_set_object (&priv->root, root);
+
+  if (priv->root != NULL)
+    {
+      GListModel *base_model = G_LIST_MODEL (priv->root);
+
+      g_object_set_data (G_OBJECT (priv->root), "IDE_TREE", self);
+
+      priv->tree_model = gtk_tree_list_model_new (g_object_ref (base_model),
+                                                  FALSE, /* Passthrough */
+                                                  FALSE,  /* Autoexpand */
+                                                  ide_tree_create_child_model_cb,
+                                                  NULL, NULL);
+      gtk_single_selection_set_model (priv->selection, G_LIST_MODEL (priv->tree_model));
+
+      if (priv->addins != NULL)
+        _ide_tree_node_expand_async (priv->root, priv->addins, NULL, NULL, NULL);
+    }
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ROOT]);
 }
 
 void
@@ -691,52 +552,156 @@ ide_tree_show_popover_at_node (IdeTree     *self,
   g_return_if_fail (IDE_IS_TREE (self));
   g_return_if_fail (IDE_IS_TREE_NODE (node));
   g_return_if_fail (GTK_IS_POPOVER (popover));
-  g_return_if_fail (gtk_widget_get_parent (GTK_WIDGET (popover)) == NULL);
 
-#if 0
-  /* Once this is in GTK, uncomment */
-  gtk_widget_set_action_parent (GTK_WIDGET (popover), GTK_WIDGET (self));
-#endif
+  g_critical ("TODO: show popover at node");
+}
 
-  _ide_tree_node_show_popover (node, self, popover);
+static GtkTreeListRow *
+_ide_tree_get_row_at_node_recurse (IdeTree     *self,
+                                   IdeTreeNode *node,
+                                   gboolean     expand_to_node)
+{
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  g_autoptr(GtkTreeListRow) row = NULL;
+  IdeTreeNode *parent;
+  guint index;
+
+  g_assert (IDE_IS_TREE (self));
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  /* The root node cannot have a GtkTreeListRow */
+  if (!(parent = ide_tree_node_get_parent (node)))
+    return NULL;
+
+  /* Get our index for offset use within models */
+  index = _ide_tree_node_get_child_index (parent, node);
+
+  /* Handle children of the root specially by getting their
+   * row from the GtkTreeListModel.
+   */
+  if (parent == priv->root)
+    return gtk_tree_list_model_get_child_row (priv->tree_model, index);
+
+  /* Expand the parent row and use the resulting row to locate
+   * the child within that.
+   */
+  if ((row = _ide_tree_get_row_at_node_recurse (self, parent, expand_to_node)))
+    {
+      if (expand_to_node)
+        gtk_tree_list_row_set_expanded (row, TRUE);
+      return gtk_tree_list_row_get_child_row (row, index);
+    }
+
+  /* Failed to get row, probably due to something not expanded */
+  return NULL;
+}
+
+GtkTreeListRow *
+_ide_tree_get_row_at_node (IdeTree     *self,
+                           IdeTreeNode *node,
+                           gboolean     expand_to_node)
+{
+  g_return_val_if_fail (IDE_IS_TREE (self), NULL);
+  g_return_val_if_fail (!node || IDE_IS_TREE_NODE (node), NULL);
+
+  if (node == NULL)
+    return NULL;
+
+  return _ide_tree_get_row_at_node_recurse (self, node, expand_to_node);
+}
+
+gboolean
+ide_tree_is_node_expanded (IdeTree     *self,
+                           IdeTreeNode *node)
+{
+  g_autoptr(GtkTreeListRow) row = NULL;
+
+  g_return_val_if_fail (IDE_IS_TREE (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE_NODE (node), FALSE);
+
+  if ((row = _ide_tree_get_row_at_node (self, node, FALSE)))
+    return gtk_tree_list_row_get_expanded (row);
+
+  return FALSE;
 }
 
 /**
  * ide_tree_get_selected_node:
  * @self: a #IdeTree
  *
- * Gets the currently selected node, or %NULL
+ * Gets the selected item.
  *
  * Returns: (transfer none) (nullable): an #IdeTreeNode or %NULL
  */
 IdeTreeNode *
 ide_tree_get_selected_node (IdeTree *self)
 {
-  GtkTreeSelection *selection;
-  GtkTreeModel *model;
-  GtkTreeIter iter;
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  GtkTreeListRow *row;
 
   g_return_val_if_fail (IDE_IS_TREE (self), NULL);
 
-  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self));
+  if ((row = gtk_single_selection_get_selected_item (priv->selection)))
+    {
+      g_autoptr(GObject) item = gtk_tree_list_row_get_item (row);
 
-  if (gtk_tree_selection_get_selected (selection, &model, &iter) && IDE_IS_TREE_MODEL (model))
-    return ide_tree_model_get_node (IDE_TREE_MODEL (model), &iter);
+      g_assert (IDE_IS_TREE_NODE (item));
+
+      /* Return borrowed instance, which we are sure will stick
+       * around after the unref as it's part of node tree.
+       */
+      return IDE_TREE_NODE (item);
+    }
 
   return NULL;
 }
 
+/**
+ * ide_tree_set_selected_node:
+ * @self: a #IdeTree
+ * @node: (nullable): an #IdeTreeNode or %NULL
+ *
+ * Sets the selected item in the tree.
+ *
+ * If @node is %NULL, the current selection is cleared.
+ */
 void
-ide_tree_select_node (IdeTree     *self,
-                      IdeTreeNode *node)
+ide_tree_set_selected_node (IdeTree     *self,
+                            IdeTreeNode *node)
 {
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  g_autoptr(GtkTreeListRow) row = NULL;
+  guint position = GTK_INVALID_LIST_POSITION;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
   g_return_if_fail (IDE_IS_TREE (self));
   g_return_if_fail (!node || IDE_IS_TREE_NODE (node));
 
-  if (node == NULL)
-    ide_tree_unselect (self);
-  else
-    ide_tree_select (self, node);
+  if ((row = _ide_tree_get_row_at_node (self, node, TRUE)))
+    position = gtk_tree_list_row_get_position (row);
+
+  gtk_single_selection_set_selected (priv->selection, position);
+}
+
+void
+ide_tree_invalidate_all (IdeTree *self)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE (self));
+
+  g_critical ("TODO: implement invalidate all");
+}
+
+void
+ide_tree_expand_to_node (IdeTree     *self,
+                         IdeTreeNode *node)
+{
+  g_autoptr(GtkTreeListRow) row = NULL;
+
+  g_return_if_fail (IDE_IS_TREE (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (node));
+
+  row = _ide_tree_get_row_at_node (self, node, TRUE);
 }
 
 static void
@@ -744,135 +709,122 @@ ide_tree_expand_node_cb (GObject      *object,
                          GAsyncResult *result,
                          gpointer      user_data)
 {
-  IdeTreeModel *model = (IdeTreeModel *)object;
+  IdeTreeNode *node = (IdeTreeNode *)object;
+  g_autoptr(GtkTreeListRow) row = NULL;
   g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeTree *self;
 
-  g_assert (IDE_IS_TREE_MODEL (model));
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TREE_NODE (node));
   g_assert (G_IS_ASYNC_RESULT (result));
   g_assert (IDE_IS_TASK (task));
 
-  if (ide_tree_model_expand_finish (model, result, NULL))
+  if (!_ide_tree_node_expand_finish (node, result, &error))
     {
-      g_autoptr(GtkTreePath) path = NULL;
-      IdeTreeNode *node;
-      IdeTree *self;
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
 
-      self = ide_task_get_source_object (task);
-      node = ide_task_get_task_data (task);
+  self = ide_task_get_source_object (task);
 
-      g_assert (IDE_IS_TREE (self));
-      g_assert (IDE_IS_TREE_NODE (node));
-
-      if ((path = ide_tree_node_get_path (node)))
-        gtk_tree_view_expand_row (GTK_TREE_VIEW (self), path, FALSE);
-    }
+  if ((row = _ide_tree_get_row_at_node (self, node, TRUE)))
+    gtk_tree_list_row_set_expanded (row, TRUE);
 
   ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
 }
 
 void
-ide_tree_expand_node (IdeTree     *self,
-                      IdeTreeNode *node)
+ide_tree_expand_node_async (IdeTree             *self,
+                            IdeTreeNode         *node,
+                            GCancellable        *cancellable,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
 {
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  g_autoptr(GtkTreeListRow) row = NULL;
   g_autoptr(IdeTask) task = NULL;
-  IdeTreeModel *model;
 
   g_return_if_fail (IDE_IS_TREE (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (node));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  if (!(model = ide_tree_get_model (self)))
-    return;
-
-  task = ide_task_new (self, NULL, NULL, NULL);
-  ide_task_set_source_tag (task, ide_tree_expand_node);
-  ide_task_set_task_data (task, g_object_ref (node), g_object_unref);
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_tree_expand_node_async);
 
-  ide_tree_model_expand_async (model,
-                               node,
-                               NULL,
+  _ide_tree_node_expand_async (node,
+                               priv->addins,
+                               cancellable,
                                ide_tree_expand_node_cb,
                                g_steal_pointer (&task));
 }
 
 gboolean
-ide_tree_node_expanded (IdeTree     *self,
-                        IdeTreeNode *node)
+ide_tree_expand_node_finish (IdeTree       *self,
+                             GAsyncResult  *result,
+                             GError       **error)
 {
-  g_autoptr(GtkTreePath) path = NULL;
-
   g_return_val_if_fail (IDE_IS_TREE (self), FALSE);
-  g_return_val_if_fail (!node || IDE_IS_TREE_NODE (node), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
 
-  if (node == NULL)
-    return FALSE;
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
 
-  if (!(path = ide_tree_node_get_path (node)))
-    return FALSE;
+void
+ide_tree_expand_node (IdeTree     *self,
+                      IdeTreeNode *node)
+{
+  g_return_if_fail (IDE_IS_TREE (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (node));
 
-  return gtk_tree_view_row_expanded (GTK_TREE_VIEW (self), path);
+  ide_tree_expand_node_async (self, node, NULL, NULL, NULL);
 }
 
 void
 ide_tree_collapse_node (IdeTree     *self,
                         IdeTreeNode *node)
 {
-  IdeTreeModel *model;
-  g_autoptr(GtkTreePath) path = NULL;
+  g_autoptr(GtkTreeListRow) row = NULL;
 
-  g_return_if_fail (IDE_IS_TREE (self));
-
-  if (!(model = ide_tree_get_model (self)))
-    return;
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE (self));
+  g_assert (IDE_IS_TREE_NODE (node));
+  g_assert (node != ide_tree_get_root (self));
 
-  if ((path = ide_tree_node_get_path (node)))
-    gtk_tree_view_collapse_row (GTK_TREE_VIEW (self), path);
+  if ((row = _ide_tree_get_row_at_node (self, node, FALSE)))
+    gtk_tree_list_row_set_expanded (row, FALSE);
 }
 
-GdkDragAction
-_ide_tree_get_drop_actions (IdeTree *self)
+/**
+ * ide_tree_get_menu_model:
+ * @self: a #IdeTree
+ *
+ * Gets the menu model for the tree.
+ *
+ * Returns: (transfer none) (nullable): a #GMenuModel or %NULL
+ */
+GMenuModel *
+ide_tree_get_menu_model (IdeTree *self)
 {
   IdeTreePrivate *priv = ide_tree_get_instance_private (self);
 
-  g_return_val_if_fail (IDE_IS_TREE (self), 0);
+  g_return_val_if_fail (IDE_IS_TREE (self), NULL);
 
-  return priv->drop_action;
+  return priv->menu_model;
 }
 
-IdeTreeNode *
-_ide_tree_get_drop_node (IdeTree *self)
+void
+ide_tree_set_menu_model (IdeTree    *self,
+                         GMenuModel *menu_model)
 {
   IdeTreePrivate *priv = ide_tree_get_instance_private (self);
-  g_autoptr(GtkTreePath) copy = NULL;
-  GtkTreeModel *model;
-  GtkTreeIter iter;
-
-  g_return_val_if_fail (IDE_IS_TREE (self), NULL);
-
-  if (priv->drop_path == NULL)
-    return NULL;
 
-  copy = gtk_tree_path_copy (priv->drop_path);
-
-  if (priv->drop_pos == GTK_TREE_VIEW_DROP_BEFORE ||
-      priv->drop_pos == GTK_TREE_VIEW_DROP_AFTER)
-    {
-      if (gtk_tree_path_get_depth (copy) > 1)
-        gtk_tree_path_up (copy);
-    }
-
-  model = gtk_tree_view_get_model (GTK_TREE_VIEW (self));
-
-  if (gtk_tree_model_get_iter (model, &iter, copy))
-    {
-      IdeTreeNode *node = iter.user_data;
-
-      if (IDE_IS_TREE_NODE (node))
-        {
-          if (ide_tree_node_is_empty (node))
-            node = ide_tree_node_get_parent (node);
-        }
-
-      return node;
-    }
+  g_return_if_fail (IDE_IS_TREE (self));
+  g_return_if_fail (!menu_model || G_IS_MENU_MODEL (menu_model));
 
-  return NULL;
+  if (g_set_object (&priv->menu_model, menu_model))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MENU_MODEL]);
 }
diff --git a/src/libide/tree/ide-tree.h b/src/libide/tree/ide-tree.h
index c9e3d8a51..01353c507 100644
--- a/src/libide/tree/ide-tree.h
+++ b/src/libide/tree/ide-tree.h
@@ -1,6 +1,6 @@
 /* ide-tree.h
  *
- * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2022 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,6 +20,10 @@
 
 #pragma once
 
+#if !defined (IDE_TREE_INSIDE) && !defined (IDE_TREE_COMPILATION)
+# error "Only <libide-tree.h> can be included directly."
+#endif
+
 #include <gtk/gtk.h>
 
 #include <libide-core.h>
@@ -31,38 +35,57 @@ G_BEGIN_DECLS
 #define IDE_TYPE_TREE (ide_tree_get_type())
 
 IDE_AVAILABLE_IN_ALL
-G_DECLARE_DERIVABLE_TYPE (IdeTree, ide_tree, IDE, TREE, GtkTreeView)
+G_DECLARE_DERIVABLE_TYPE (IdeTree, ide_tree, IDE, TREE, GtkWidget)
 
 struct _IdeTreeClass
 {
-  GtkTreeViewClass parent_type;
-
-  /*< private >*/
-  gpointer _reserved[8];
+  GtkWidgetClass parent_class;
 };
 
 IDE_AVAILABLE_IN_ALL
-GtkWidget     *ide_tree_new                  (void);
+IdeTree     *ide_tree_new                  (void);
+IDE_AVAILABLE_IN_ALL
+IdeTreeNode *ide_tree_get_root             (IdeTree              *self);
+IDE_AVAILABLE_IN_ALL
+void         ide_tree_set_root             (IdeTree              *self,
+                                            IdeTreeNode          *root);
+IDE_AVAILABLE_IN_ALL
+GMenuModel  *ide_tree_get_menu_model       (IdeTree              *self);
+IDE_AVAILABLE_IN_ALL
+void         ide_tree_set_menu_model       (IdeTree              *self,
+                                            GMenuModel           *menu_model);
+IDE_AVAILABLE_IN_ALL
+void         ide_tree_show_popover_at_node (IdeTree              *self,
+                                            IdeTreeNode          *node,
+                                            GtkPopover           *popover);
+IDE_AVAILABLE_IN_ALL
+gboolean     ide_tree_is_node_expanded     (IdeTree              *self,
+                                            IdeTreeNode          *node);
+IDE_AVAILABLE_IN_ALL
+void         ide_tree_collapse_node        (IdeTree              *self,
+                                            IdeTreeNode          *node);
 IDE_AVAILABLE_IN_ALL
-void           ide_tree_set_context_menu     (IdeTree     *self,
-                                              GMenu       *menu);
+void         ide_tree_expand_to_node       (IdeTree              *self,
+                                            IdeTreeNode          *node);
 IDE_AVAILABLE_IN_ALL
-void           ide_tree_show_popover_at_node (IdeTree     *self,
-                                              IdeTreeNode *node,
-                                              GtkPopover  *popover);
+void         ide_tree_expand_node          (IdeTree              *self,
+                                            IdeTreeNode          *node);
 IDE_AVAILABLE_IN_ALL
-IdeTreeNode   *ide_tree_get_selected_node    (IdeTree     *self);
+void         ide_tree_expand_node_async    (IdeTree              *self,
+                                            IdeTreeNode          *node,
+                                            GCancellable         *cancellable,
+                                            GAsyncReadyCallback   callback,
+                                            gpointer              user_data);
 IDE_AVAILABLE_IN_ALL
-void           ide_tree_select_node          (IdeTree     *self,
-                                              IdeTreeNode *node);
+gboolean     ide_tree_expand_node_finish   (IdeTree              *self,
+                                            GAsyncResult         *result,
+                                            GError              **error);
 IDE_AVAILABLE_IN_ALL
-void           ide_tree_expand_node          (IdeTree     *self,
-                                              IdeTreeNode *node);
+IdeTreeNode *ide_tree_get_selected_node    (IdeTree              *self);
 IDE_AVAILABLE_IN_ALL
-void           ide_tree_collapse_node        (IdeTree     *self,
-                                              IdeTreeNode *node);
+void         ide_tree_set_selected_node    (IdeTree              *self,
+                                            IdeTreeNode          *node);
 IDE_AVAILABLE_IN_ALL
-gboolean       ide_tree_node_expanded        (IdeTree     *self,
-                                              IdeTreeNode *node);
+void         ide_tree_invalidate_all       (IdeTree              *self);
 
 G_END_DECLS
diff --git a/src/libide/tree/ide-tree.ui b/src/libide/tree/ide-tree.ui
new file mode 100644
index 000000000..e6bdb3d91
--- /dev/null
+++ b/src/libide/tree/ide-tree.ui
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeTree" parent="GtkWidget">
+    <child>
+      <object class="GtkListView" id="list_view">
+        <signal name="activate" handler="ide_tree_activate_cb" swapped="true" object="IdeTree"/>
+        <property name="orientation">vertical</property>
+        <property name="single-click-activate">true</property>
+        <property name="model">
+          <object class="GtkSingleSelection" id="selection">
+            <signal name="notify::selected" handler="ide_tree_notify_selected_cb" swapped="true" 
object="IdeTree"/>
+          </object>
+        </property>
+        <property name="factory">
+          <object class="GtkSignalListItemFactory">
+            <signal name="bind" handler="ide_tree_list_item_bind_cb" swapped="true" object="IdeTree"/>
+            <signal name="setup" handler="ide_tree_list_item_setup_cb" swapped="true" object="IdeTree"/>
+            <signal name="teardown" handler="ide_tree_list_item_teardown_cb" swapped="true" 
object="IdeTree"/>
+            <signal name="unbind" handler="ide_tree_list_item_unbind_cb" swapped="true" object="IdeTree"/>
+          </object>
+        </property>
+        <style>
+          <class name="navigation-sidebar"/>
+        </style>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/tree/libide-tree.gresource.xml b/src/libide/tree/libide-tree.gresource.xml
new file mode 100644
index 000000000..55775b9d6
--- /dev/null
+++ b/src/libide/tree/libide-tree.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/libide-tree">
+    <file preprocess="xml-stripblanks">ide-tree.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/libide/tree/libide-tree.h b/src/libide/tree/libide-tree.h
index be4915a14..ab6636efd 100644
--- a/src/libide/tree/libide-tree.h
+++ b/src/libide/tree/libide-tree.h
@@ -28,7 +28,6 @@ G_BEGIN_DECLS
 # include "ide-popover-positioner.h"
 # include "ide-tree.h"
 # include "ide-tree-addin.h"
-# include "ide-tree-model.h"
 # include "ide-tree-node.h"
 #undef IDE_TREE_INSIDE
 
diff --git a/src/libide/tree/meson.build b/src/libide/tree/meson.build
index d0a99b73d..259a0a605 100644
--- a/src/libide/tree/meson.build
+++ b/src/libide/tree/meson.build
@@ -1,3 +1,4 @@
+libide_tree_header_dir = join_paths(libide_header_dir, 'tree')
 libide_tree_header_subdir = join_paths(libide_header_subdir, 'tree')
 libide_include_directories += include_directories('.')
 
@@ -9,7 +10,6 @@ libide_tree_public_headers = [
   'ide-popover-positioner.h',
   'ide-tree.h',
   'ide-tree-addin.h',
-  'ide-tree-model.h',
   'ide-tree-node.h',
   'libide-tree.h',
 ]
@@ -24,20 +24,46 @@ libide_tree_public_sources = [
   'ide-popover-positioner.c',
   'ide-tree.c',
   'ide-tree-addin.c',
-  'ide-tree-model.c',
   'ide-tree-node.c',
 ]
 
-libide_tree_private_headers = [
-  'ide-cell-renderer-status.h',
-]
-
 libide_tree_private_sources = [
-  'ide-cell-renderer-status.c',
+  'ide-tree-empty.c',
 ]
 
 libide_tree_sources = libide_tree_public_sources + libide_tree_private_sources
 
+#
+# Enum generation
+#
+
+libide_tree_enum_headers = [
+  'ide-tree-node.h',
+]
+
+libide_tree_enums = gnome.mkenums_simple('ide-tree-enums',
+     body_prefix: '#include "config.h"',
+   header_prefix: '#include <libide-core.h>',
+       decorator: '_IDE_EXTERN',
+         sources: libide_tree_enum_headers,
+  install_header: true,
+     install_dir: libide_tree_header_dir,
+)
+libide_tree_generated_sources = [libide_tree_enums[0]]
+libide_tree_generated_headers = [libide_tree_enums[1]]
+
+#
+# Generated Resource Files
+#
+
+libide_tree_resources = gnome.compile_resources(
+  'ide-tree-resources',
+  'libide-tree.gresource.xml',
+  c_name: 'ide_tree',
+)
+libide_tree_generated_headers += [libide_tree_resources[1]]
+libide_tree_generated_sources += [libide_tree_resources[0]]
+
 #
 # Dependencies
 #
@@ -47,6 +73,7 @@ libide_tree_deps = [
   libpeas_dep,
 
   libide_core_dep,
+  libide_gtk_dep,
   libide_plugins_dep,
   libide_threading_dep,
 ]
@@ -55,13 +82,13 @@ libide_tree_deps = [
 # Library Definitions
 #
 
-libide_tree = static_library('ide-tree-' + libide_api_version, libide_tree_sources,
+libide_tree = static_library('ide-tree-' + libide_api_version,
+  libide_tree_sources + libide_tree_enums + libide_tree_resources,
    dependencies: libide_tree_deps,
          c_args: libide_args + release_args + ['-DIDE_TREE_COMPILATION'],
 )
 
 libide_tree_dep = declare_dependency(
-              sources: libide_tree_private_headers,
          dependencies: libide_tree_deps,
             link_with: libide_tree,
   include_directories: include_directories('.'),
@@ -69,7 +96,7 @@ libide_tree_dep = declare_dependency(
 
 gnome_builder_public_sources += files(libide_tree_public_sources)
 gnome_builder_public_headers += files(libide_tree_public_headers)
-gnome_builder_private_sources += files(libide_tree_private_sources)
-gnome_builder_private_headers += files(libide_tree_private_headers)
+gnome_builder_generated_headers += libide_tree_generated_headers
+gnome_builder_generated_sources += libide_tree_generated_sources
 gnome_builder_include_subdirs += libide_tree_header_subdir
 gnome_builder_gir_extra_args += ['--c-include=libide-tree.h', '-DIDE_TREE_COMPILATION']
diff --git a/src/plugins/codeui/codeui-plugin.c b/src/plugins/codeui/codeui-plugin.c
index d0237be13..2667ea793 100644
--- a/src/plugins/codeui/codeui-plugin.c
+++ b/src/plugins/codeui/codeui-plugin.c
@@ -24,11 +24,9 @@
 
 #include <libide-code.h>
 #include <libide-sourceview.h>
-#include <libide-tree.h>
 
 #include "gbp-codeui-buffer-addin.h"
 #include "gbp-codeui-hover-provider.h"
-#include "gbp-codeui-tree-addin.h"
 
 _IDE_EXTERN void
 _gbp_codeui_register_types (PeasObjectModule *module)
@@ -39,7 +37,4 @@ _gbp_codeui_register_types (PeasObjectModule *module)
   peas_object_module_register_extension_type (module,
                                               GTK_SOURCE_TYPE_HOVER_PROVIDER,
                                               GBP_TYPE_CODEUI_HOVER_PROVIDER);
-  peas_object_module_register_extension_type (module,
-                                              IDE_TYPE_TREE_ADDIN,
-                                              GBP_TYPE_CODEUI_TREE_ADDIN);
 }
diff --git a/src/plugins/codeui/meson.build b/src/plugins/codeui/meson.build
index f13c5c37c..4a3fffa9d 100644
--- a/src/plugins/codeui/meson.build
+++ b/src/plugins/codeui/meson.build
@@ -2,7 +2,6 @@ plugins_sources += files([
   'codeui-plugin.c',
   'gbp-codeui-buffer-addin.c',
   'gbp-codeui-hover-provider.c',
-  'gbp-codeui-tree-addin.c',
 ])
 
 plugin_codeui_resources = gnome.compile_resources(
diff --git a/src/plugins/grep/gbp-grep-tree-addin.c b/src/plugins/grep/gbp-grep-tree-addin.c
index c9d561251..f43795eb7 100644
--- a/src/plugins/grep/gbp-grep-tree-addin.c
+++ b/src/plugins/grep/gbp-grep-tree-addin.c
@@ -70,8 +70,7 @@ find_in_files_action (GSimpleAction *action,
 
 static void
 gbp_grep_tree_addin_load (IdeTreeAddin *addin,
-                          IdeTree      *tree,
-                          IdeTreeModel *model)
+                          IdeTree      *tree)
 {
   static const GActionEntry actions[] = {
     { "find-in-files", find_in_files_action },
@@ -83,7 +82,6 @@ gbp_grep_tree_addin_load (IdeTreeAddin *addin,
   g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (GBP_IS_GREP_TREE_ADDIN (self));
   g_assert (IDE_IS_TREE (tree));
-  g_assert (IDE_IS_TREE_MODEL (model));
 
   self->tree = tree;
 
@@ -102,15 +100,13 @@ gbp_grep_tree_addin_load (IdeTreeAddin *addin,
 
 static void
 gbp_grep_tree_addin_unload (IdeTreeAddin *addin,
-                            IdeTree      *tree,
-                            IdeTreeModel *model)
+                            IdeTree      *tree)
 {
   GbpGrepTreeAddin *self = (GbpGrepTreeAddin *)addin;
 
   g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (GBP_IS_GREP_TREE_ADDIN (self));
   g_assert (IDE_IS_TREE (tree));
-  g_assert (IDE_IS_TREE_MODEL (model));
 
   gtk_widget_insert_action_group (GTK_WIDGET (tree), "grep", NULL);
   g_clear_object (&self->group);
diff --git a/src/plugins/project-tree/gbp-project-tree-addin.c 
b/src/plugins/project-tree/gbp-project-tree-addin.c
index cf062d3ea..53e4970fc 100644
--- a/src/plugins/project-tree/gbp-project-tree-addin.c
+++ b/src/plugins/project-tree/gbp-project-tree-addin.c
@@ -39,7 +39,6 @@ struct _GbpProjectTreeAddin
   GObject       parent_instance;
 
   IdeTree      *tree;
-  IdeTreeModel *model;
   GSettings    *settings;
 
   guint         sort_directories_first : 1;
@@ -92,7 +91,7 @@ create_file_node (IdeProjectFile *file)
 
   child = ide_tree_node_new ();
   ide_tree_node_set_item (child, G_OBJECT (file));
-  ide_tree_node_set_display_name (child, ide_project_file_get_display_name (file));
+  ide_tree_node_set_title (child, ide_project_file_get_display_name (file));
   ide_tree_node_set_icon (child, ide_project_file_get_symbolic_icon (file));
   g_object_set (child, "destroy-item", TRUE, NULL);
 
@@ -158,9 +157,9 @@ gbp_project_tree_addin_file_list_children_cb (GObject      *object,
       child = create_file_node (file);
 
       if (last == NULL)
-        ide_tree_node_append (node, child);
+        ide_tree_node_insert_before (child, node, NULL);
       else
-        ide_tree_node_insert_after (last, child);
+        ide_tree_node_insert_after (child, node, last);
 
       last = child;
     }
@@ -205,11 +204,11 @@ gbp_project_tree_addin_build_children_async (IdeTreeAddin        *addin,
       g_file_info_set_is_symlink (info, FALSE);
       root_file = ide_project_file_new (parent, info);
       files = create_file_node (root_file);
-      ide_tree_node_set_display_name (files, _("Files"));
+      ide_tree_node_set_title (files, _("Files"));
       ide_tree_node_set_icon_name (files, "view-list-symbolic");
       ide_tree_node_set_expanded_icon_name (files, "view-list-symbolic");
       ide_tree_node_set_is_header (files, TRUE);
-      ide_tree_node_append (node, files);
+      ide_tree_node_insert_before (files, node, NULL);
     }
   else if (ide_tree_node_holds (node, IDE_TYPE_PROJECT_FILE))
     {
@@ -296,7 +295,6 @@ static IdeTreeNode *
 find_file_node (IdeTree *tree,
                 GFile   *file)
 {
-  GtkTreeModel *model;
   IdeTreeNode *root;
   FindFileNode find;
 
@@ -304,8 +302,7 @@ find_file_node (IdeTree *tree,
   g_assert (IDE_IS_TREE (tree));
   g_assert (G_IS_FILE (file));
 
-  model = gtk_tree_view_get_model (GTK_TREE_VIEW (tree));
-  root = ide_tree_model_get_root (IDE_TREE_MODEL (model));
+  root = ide_tree_get_root (tree);
 
   find.file = file;
   find.node = NULL;
@@ -364,8 +361,8 @@ node_compare_directories_first (IdeTreeNode *node,
       ide_tree_node_get_children_possible (node))
     return 1;
 
-  child_name = ide_tree_node_get_display_name (child);
-  node_name = ide_tree_node_get_display_name (node);
+  child_name = ide_tree_node_get_title (child);
+  node_name = ide_tree_node_get_title (node);
 
   collated_child = g_utf8_collate_key_for_filename (child_name, -1);
   collated_node = g_utf8_collate_key_for_filename (node_name, -1);
@@ -387,8 +384,8 @@ node_compare (IdeTreeNode *node,
   g_assert (IDE_IS_TREE_NODE (node));
   g_assert (IDE_IS_TREE_NODE (child));
 
-  child_name = ide_tree_node_get_display_name (child);
-  node_name = ide_tree_node_get_display_name (node);
+  child_name = ide_tree_node_get_title (child);
+  node_name = ide_tree_node_get_title (node);
 
   collated_child = g_utf8_collate_key_for_filename (child_name, -1);
   collated_node = g_utf8_collate_key_for_filename (node_name, -1);
@@ -440,18 +437,8 @@ gbp_project_tree_addin_add_file (GbpProjectTreeAddin *self,
 
       if ((parent = find_file_node (self->tree, item)))
         {
-          IdeTreeNode *child;
-
-          if (!ide_tree_node_expanded (self->tree, parent))
+          if (!ide_tree_is_node_expanded (self->tree, parent))
             IDE_EXIT;
-
-          /* Remove empty children if necessary */
-          if (ide_tree_node_get_n_children (parent) == 1 &&
-              (child = ide_tree_node_get_nth_child (parent, 0)) &&
-              ide_tree_node_is_empty (child))
-            ide_tree_node_remove (parent, child);
-
-          continue;
         }
 
       directory = g_file_get_parent (item);
@@ -477,7 +464,7 @@ gbp_project_tree_addin_add_file (GbpProjectTreeAddin *self,
       else
         ide_tree_node_insert_sorted (parent, node, node_compare);
 
-      if (!ide_tree_node_expanded (self->tree, parent))
+      if (!ide_tree_is_node_expanded (self->tree, parent))
         ide_tree_expand_node (self->tree, parent);
     }
 
@@ -508,11 +495,6 @@ gbp_project_tree_addin_remove_file (GbpProjectTreeAddin *self,
       IdeTreeNode *parent = ide_tree_node_get_parent (selected);
 
       ide_tree_node_remove (parent, selected);
-
-      /* Force the parent node to re-add the Empty child */
-      if (ide_tree_node_get_children_possible (parent) &&
-          ide_tree_node_get_n_children (parent) == 0)
-        _ide_tree_node_remove_all (parent);
     }
 
   IDE_EXIT;
@@ -550,20 +532,16 @@ gbp_project_tree_addin_reloaded_cb (GbpProjectTreeAddin *self,
 
 static void
 gbp_project_tree_addin_load (IdeTreeAddin *addin,
-                             IdeTree      *tree,
-                             IdeTreeModel *model)
+                             IdeTree      *tree)
 {
   GbpProjectTreeAddin *self = (GbpProjectTreeAddin *)addin;
   IdeVcsMonitor *monitor;
   IdeWorkbench *workbench;
-  g_autoptr(GdkContentFormats) formats = NULL;
-  GdkContentFormatsBuilder *builder;
 
   g_assert (GBP_IS_PROJECT_TREE_ADDIN (self));
-  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (IDE_IS_TREE (tree));
 
   self->tree = tree;
-  self->model = model;
 
   workbench = ide_widget_get_workbench (GTK_WIDGET (tree));
   monitor = ide_workbench_get_vcs_monitor (workbench);
@@ -579,33 +557,18 @@ gbp_project_tree_addin_load (IdeTreeAddin *addin,
                            G_CALLBACK (gbp_project_tree_addin_reloaded_cb),
                            self,
                            G_CONNECT_SWAPPED);
-
-  builder = gdk_content_formats_builder_new ();
-  gdk_content_formats_builder_add_gtype (builder, GDK_TYPE_FILE_LIST);
-  gdk_content_formats_builder_add_gtype (builder, GTK_TYPE_TREE_ROW_DATA);
-  formats = gdk_content_formats_builder_free_to_formats (builder);
-
-  gtk_tree_view_enable_model_drag_source (GTK_TREE_VIEW (tree),
-                                          GDK_BUTTON1_MASK,
-                                          formats,
-                                          GDK_ACTION_COPY | GDK_ACTION_MOVE);
-  gtk_tree_view_enable_model_drag_dest (GTK_TREE_VIEW (tree),
-                                        formats,
-                                        GDK_ACTION_COPY | GDK_ACTION_MOVE);
 }
 
 static void
 gbp_project_tree_addin_unload (IdeTreeAddin *addin,
-                               IdeTree      *tree,
-                               IdeTreeModel *model)
+                               IdeTree      *tree)
 {
   GbpProjectTreeAddin *self = (GbpProjectTreeAddin *)addin;
 
   g_assert (GBP_IS_PROJECT_TREE_ADDIN (self));
-  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (IDE_IS_TREE (tree));
 
   self->tree = NULL;
-  self->model = NULL;
 }
 
 static gboolean
@@ -753,7 +716,7 @@ gbp_project_tree_addin_transfer_cb (GObject      *object,
            * open it up in IdeProject.
            */
 
-          context = ide_object_get_context (IDE_OBJECT (self->model));
+          context = ide_widget_get_context (GTK_WIDGET (self->tree));
           project = ide_project_from_context (context);
 
           for (guint i = 0; i < sources->len; i++)
@@ -880,7 +843,7 @@ gbp_project_tree_addin_node_dropped_async (IdeTreeAddin        *addin,
                            notif,
                            0);
 
-  context = ide_object_get_context (IDE_OBJECT (self->model));
+  context = ide_widget_get_context (GTK_WIDGET (self->tree));
   buffer_manager = ide_buffer_manager_from_context (context);
 
   for (guint i = 0; i < srcs->len; i++)
@@ -928,7 +891,7 @@ gbp_project_tree_addin_node_dropped_async (IdeTreeAddin        *addin,
   ide_notification_set_title (notif, _("Copying files…"));
   ide_notification_set_body (notif, _("Files will be copied in a moment"));
   ide_notification_set_has_progress (notif, TRUE);
-  ide_notification_attach (notif, IDE_OBJECT (self->model));
+  ide_notification_attach (notif, IDE_OBJECT (context));
   ide_task_set_task_data (task, g_object_ref (notif), g_object_unref);
 
   ide_file_transfer_execute_async (transfer,
@@ -983,12 +946,12 @@ gbp_project_tree_addin_settings_changed (GbpProjectTreeAddin *self,
   self->sort_directories_first = g_settings_get_boolean (self->settings, "sort-directories-first");
   self->show_ignored_files = g_settings_get_boolean (self->settings, "show-ignored-files");
 
-  if (self->model != NULL)
-    ide_tree_model_invalidate (self->model, NULL);
+  if (self->tree != NULL)
+    ide_tree_invalidate_all (self->tree);
 }
 
 G_DEFINE_FINAL_TYPE_WITH_CODE (GbpProjectTreeAddin, gbp_project_tree_addin, G_TYPE_OBJECT,
-                         G_IMPLEMENT_INTERFACE (IDE_TYPE_TREE_ADDIN, tree_addin_iface_init))
+                               G_IMPLEMENT_INTERFACE (IDE_TYPE_TREE_ADDIN, tree_addin_iface_init))
 
 static void
 gbp_project_tree_addin_dispose (GObject *object)
diff --git a/src/plugins/project-tree/gbp-project-tree-pane-actions.c 
b/src/plugins/project-tree/gbp-project-tree-pane-actions.c
index 6f3bfefd4..17b8a847c 100644
--- a/src/plugins/project-tree/gbp-project-tree-pane-actions.c
+++ b/src/plugins/project-tree/gbp-project-tree-pane-actions.c
@@ -209,7 +209,7 @@ gbp_project_tree_pane_actions_new (GbpProjectTreePane *self,
       project_file = ide_tree_node_get_item (parent);
       selected = parent;
 
-      ide_tree_select_node (self->tree, parent);
+      ide_tree_set_selected_node (self->tree, parent);
     }
 
   /* Now create our async task to keep track of everything during
@@ -226,7 +226,7 @@ gbp_project_tree_pane_actions_new (GbpProjectTreePane *self,
 
 
   state = g_slice_new0 (NewState);
-  state->needs_collapse = !ide_tree_node_expanded (self->tree, selected);
+  state->needs_collapse = !ide_tree_is_node_expanded (self->tree, selected);
   state->file_type = file_type;
   state->node = g_object_ref (selected);
 
@@ -682,25 +682,18 @@ action_map_set (GActionMap *map,
 void
 _gbp_project_tree_pane_update_actions (GbpProjectTreePane *self)
 {
-  GtkTreeSelection *selection;
+  IdeTreeNode *node;
   gboolean is_file = FALSE;
   gboolean is_dir = FALSE;
 
   g_assert (GBP_IS_PROJECT_TREE_PANE (self));
 
-  if ((selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self->tree))))
+  if ((node = ide_tree_get_selected_node (self->tree)))
     {
-      GtkTreeIter iter;
-
-      if (gtk_tree_selection_get_selected (selection, NULL, &iter))
-        {
-          IdeTreeModel *model = IDE_TREE_MODEL (gtk_tree_view_get_model (GTK_TREE_VIEW (self->tree)));
-          IdeTreeNode *node = ide_tree_model_get_node (model, &iter);
-          GObject *item = ide_tree_node_get_item (node);
+      GObject *item = ide_tree_node_get_item (node);
 
-          if ((is_file = IDE_IS_PROJECT_FILE (item)))
-            is_dir = ide_project_file_is_directory (IDE_PROJECT_FILE (item));
-        }
+      if ((is_file = IDE_IS_PROJECT_FILE (item)))
+        is_dir = ide_project_file_is_directory (IDE_PROJECT_FILE (item));
     }
 
   action_map_set (G_ACTION_MAP (self->actions), "new-file",
diff --git a/src/plugins/project-tree/gbp-project-tree-pane.c 
b/src/plugins/project-tree/gbp-project-tree-pane.c
index c9321104d..68425ef58 100644
--- a/src/plugins/project-tree/gbp-project-tree-pane.c
+++ b/src/plugins/project-tree/gbp-project-tree-pane.c
@@ -49,7 +49,6 @@ gbp_project_tree_pane_class_init (GbpProjectTreePaneClass *klass)
 static void
 gbp_project_tree_pane_init (GbpProjectTreePane *self)
 {
-  GtkTreeSelection *selection;
   IdeApplication *app;
   GMenu *menu;
 
@@ -57,11 +56,10 @@ gbp_project_tree_pane_init (GbpProjectTreePane *self)
 
   app = IDE_APPLICATION_DEFAULT;
   menu = ide_application_get_menu_by_id (IDE_APPLICATION (app), "project-tree-menu");
-  ide_tree_set_context_menu (self->tree, menu);
+  ide_tree_set_menu_model (self->tree, G_MENU_MODEL (menu));
 
-  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self->tree));
-  g_signal_connect_object (selection,
-                           "changed",
+  g_signal_connect_object (self->tree,
+                           "notify::selected-node",
                            G_CALLBACK (_gbp_project_tree_pane_update_actions),
                            self,
                            G_CONNECT_SWAPPED);
diff --git a/src/plugins/project-tree/gbp-project-tree-pane.ui 
b/src/plugins/project-tree/gbp-project-tree-pane.ui
index b14982cb4..3b55e1c8e 100644
--- a/src/plugins/project-tree/gbp-project-tree-pane.ui
+++ b/src/plugins/project-tree/gbp-project-tree-pane.ui
@@ -3,15 +3,9 @@
   <template class="GbpProjectTreePane" parent="IdePane">
     <child>
       <object class="GtkScrolledWindow">
-        <property name="visible">true</property>
         <child>
           <object class="GbpProjectTree" id="tree">
-            <property name="level-indentation">16</property>
-            <property name="visible">true</property>
-            <style>
-              <class name="project-tree"/>
-              <class name="navigation-sidebar"/>
-            </style>
+            <property name="kind">project-tree</property>
           </object>
         </child>
       </object>
diff --git a/src/plugins/project-tree/gbp-project-tree.c b/src/plugins/project-tree/gbp-project-tree.c
index 985229d9f..f001f6af0 100644
--- a/src/plugins/project-tree/gbp-project-tree.c
+++ b/src/plugins/project-tree/gbp-project-tree.c
@@ -51,54 +51,22 @@ locate_project_files (IdeTreeNode *node,
   return IDE_TREE_NODE_VISIT_CONTINUE;
 }
 
-static void
-project_files_expanded_cb (GObject      *object,
-                           GAsyncResult *result,
-                           gpointer      user_data)
-{
-  IdeTreeModel *model = (IdeTreeModel *)object;
-  g_autoptr(IdeTask) task = user_data;
-
-  g_assert (IDE_IS_MAIN_THREAD ());
-  g_assert (IDE_IS_TREE_MODEL (model));
-  g_assert (G_IS_ASYNC_RESULT (result));
-  g_assert (IDE_IS_TASK (task));
-
-  if (ide_tree_model_expand_finish (model, result, NULL))
-    {
-      g_autoptr(GtkTreePath) path = NULL;
-      GbpProjectTree *self;
-      IdeTreeNode *node;
-
-      self = ide_task_get_source_object (task);
-      node = ide_task_get_task_data (task);
-
-      g_assert (GBP_IS_PROJECT_TREE (self));
-      g_assert (IDE_IS_TREE_NODE (node));
-
-      if ((path = ide_tree_node_get_path (node)))
-        gtk_tree_view_expand_row (GTK_TREE_VIEW (self), path, FALSE);
-    }
-
-  ide_task_return_boolean (task, TRUE);
-}
-
 static void
 gbp_project_tree_expand_cb (GObject      *object,
                             GAsyncResult *result,
                             gpointer      user_data)
 {
-  IdeTreeModel *model = (IdeTreeModel *)object;
-  g_autoptr(IdeTask) task = user_data;
+  GbpProjectTree *self = (GbpProjectTree *)object;
+  g_autoptr(IdeTreeNode) root = user_data;
+  g_autoptr(GError) error = NULL;
 
   g_assert (IDE_IS_MAIN_THREAD ());
-  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (GBP_IS_PROJECT_TREE (self));
   g_assert (G_IS_ASYNC_RESULT (result));
-  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_TREE_NODE (root));
 
-  if (ide_tree_model_expand_finish (model, result, NULL))
+  if (ide_tree_expand_node_finish (IDE_TREE (self), result, &error))
     {
-      IdeTreeNode *root = ide_tree_model_get_root (model);
       IdeTreeNode *node = NULL;
 
       ide_tree_node_traverse (root,
@@ -108,22 +76,9 @@ gbp_project_tree_expand_cb (GObject      *object,
                               locate_project_files,
                               &node);
 
-      if (node == NULL)
-        goto cleanup;
-
-      ide_task_set_task_data (task, g_object_ref (node), g_object_unref);
-
-      ide_tree_model_expand_async (model,
-                                   node,
-                                   NULL,
-                                   project_files_expanded_cb,
-                                   g_steal_pointer (&task));
-
-      return;
+      if (node != NULL)
+        ide_tree_expand_node (IDE_TREE (self), node);
     }
-
-cleanup:
-  ide_task_return_boolean (task, TRUE);
 }
 
 static void
@@ -132,8 +87,6 @@ gbp_project_tree_context_set (GtkWidget  *widget,
 {
   GbpProjectTree *self = (GbpProjectTree *)widget;
   g_autoptr(IdeTreeNode) root = NULL;
-  g_autoptr(IdeTreeModel) model = NULL;
-  g_autoptr(IdeTask) task = NULL;
 
   g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (GBP_IS_PROJECT_TREE (self));
@@ -143,25 +96,14 @@ gbp_project_tree_context_set (GtkWidget  *widget,
     return;
 
   root = ide_tree_node_new ();
-
-  model = g_object_new (IDE_TYPE_TREE_MODEL,
-                        "kind", "project-tree",
-                        "tree", self,
-                        NULL);
-  gtk_tree_view_set_model (GTK_TREE_VIEW (self), GTK_TREE_MODEL (model));
-
   ide_tree_node_set_item (root, context);
-  ide_object_append (IDE_OBJECT (context), IDE_OBJECT (model));
-  ide_tree_model_set_root (model, root);
-
-  task = ide_task_new (self, NULL, NULL, NULL);
-  ide_task_set_source_tag (task, gbp_project_tree_context_set);
+  ide_tree_set_root (IDE_TREE (self), root);
 
-  ide_tree_model_expand_async (model,
-                               root,
-                               NULL,
-                               gbp_project_tree_expand_cb,
-                               g_steal_pointer (&task));
+  ide_tree_expand_node_async (IDE_TREE (self),
+                              root,
+                              NULL,
+                              gbp_project_tree_expand_cb,
+                              g_object_ref (root));
 }
 
 static void
@@ -174,20 +116,16 @@ gbp_project_tree_init (GbpProjectTree *self)
 {
   ide_widget_set_context_handler (GTK_WIDGET (self),
                                   gbp_project_tree_context_set);
-
-  gtk_tree_view_set_show_expanders (GTK_TREE_VIEW (self), FALSE);
 }
 
 static IdeTreeNode *
 gbp_project_tree_get_project_files (GbpProjectTree *self)
 {
-  IdeTreeModel *model;
   IdeTreeNode *project_files = NULL;
 
   g_assert (GBP_IS_PROJECT_TREE (self));
 
-  model = IDE_TREE_MODEL (gtk_tree_view_get_model (GTK_TREE_VIEW (self)));
-  ide_tree_node_traverse (ide_tree_model_get_root (model),
+  ide_tree_node_traverse (ide_tree_get_root (IDE_TREE (self)),
                           G_PRE_ORDER,
                           G_TRAVERSE_ALL,
                           1,
@@ -220,20 +158,50 @@ reveal_next_cb (GObject      *object,
                 GAsyncResult *result,
                 gpointer      user_data)
 {
-  IdeTreeModel *model = (IdeTreeModel *)object;
+  IdeTree *tree = (IdeTree *)object;
   Reveal *r = user_data;
 
-  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (IDE_IS_TREE (tree));
   g_assert (G_IS_ASYNC_RESULT (result));
   g_assert (r != NULL);
   g_assert (GBP_IS_PROJECT_TREE (r->tree));
   g_assert (IDE_IS_TREE_NODE (r->node));
   g_assert (G_IS_FILE (r->file));
 
-  if (!ide_tree_model_expand_finish (model, result, NULL))
-    reveal_free (r);
-  else
-    reveal_next (g_steal_pointer (&r));
+  if (!ide_tree_expand_node_finish (tree, result, NULL))
+    {
+      reveal_free (r);
+      return;
+    }
+
+  for (IdeTreeNode *child = ide_tree_node_get_first_child (r->node);
+       child != NULL;
+       child = ide_tree_node_get_next_sibling (child))
+    {
+      IdeProjectFile *pf;
+      g_autoptr(GFile) file = NULL;
+
+      if (ide_tree_node_holds (child, IDE_TYPE_PROJECT_FILE) &&
+          (pf = ide_tree_node_get_item (child)) &&
+          IDE_IS_PROJECT_FILE (pf) &&
+          (file = ide_project_file_ref_file (pf)))
+      {
+        if (g_file_has_prefix (r->file, file))
+          {
+            g_set_object (&r->node, child);
+            reveal_next (r);
+            return;
+          }
+        else if (g_file_equal (r->file, file))
+          {
+            ide_tree_set_selected_node (IDE_TREE (r->tree), child);
+            gtk_widget_grab_focus (GTK_WIDGET (r->tree));
+            break;
+          }
+      }
+    }
+
+  reveal_free (r);
 }
 
 static void
@@ -255,79 +223,23 @@ reveal_next (Reveal *r)
 
   if (g_file_has_prefix (r->file, file))
     {
-      IdeTreeNode *child;
-
       /* If this node cannot have children, then there is no way we
        * can expect to find the child there.
        */
       if (!ide_tree_node_get_children_possible (r->node))
         goto failure;
 
-      /* If this node needs to be built, then build it before we
-       * continue processing.
-       */
-      if (_ide_tree_node_get_needs_build_children (r->node))
-        {
-          IdeTreeModel *model;
-
-          if (!(model = IDE_TREE_MODEL (gtk_tree_view_get_model (GTK_TREE_VIEW (r->tree)))))
-            goto failure;
-
-          ide_tree_model_expand_async (model,
-                                       r->node,
-                                       NULL,
-                                       reveal_next_cb,
-                                       r);
-          return;
-        }
-
-      /* Tree to find the first child which is equal to or is a prefix
-       * for the target file.
-       */
-      if (!(child = ide_tree_node_get_nth_child (r->node, 0)))
-        goto failure;
+      ide_tree_expand_node_async (IDE_TREE (r->tree),
+                                  r->node,
+                                  NULL,
+                                  reveal_next_cb,
+                                  r);
 
-      do
-        {
-          IdeProjectFile *cpf;
-          g_autoptr(GFile) cf = NULL;
-
-          if (!ide_tree_node_holds (child, IDE_TYPE_PROJECT_FILE) ||
-              !(cpf = ide_tree_node_get_item (child)) ||
-              !IDE_IS_PROJECT_FILE (cpf) ||
-              !(cf = ide_project_file_ref_file (cpf)) ||
-              !G_IS_FILE (cf))
-            continue;
-
-          if (g_file_has_prefix (r->file, cf) || g_file_equal (r->file, cf))
-            {
-              g_set_object (&r->node, child);
-              reveal_next (r);
-              return;
-            }
-        }
-      while ((child = ide_tree_node_get_next (child)));
+      return;
     }
   else if (g_file_equal (r->file, file))
     {
-      g_autoptr(GtkTreePath) path = ide_tree_node_get_path (r->node);
-      gtk_tree_view_expand_to_path (GTK_TREE_VIEW (r->tree), path);
-      ide_tree_select_node (IDE_TREE (r->tree), r->node);
-      gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (r->tree),
-                                    path, NULL, FALSE, 0, 0);
-
-      /* Due to the way tree views work, we need to use this function to
-       * grab keyboard focus on a particular row in the treeview. This allows
-       * pressing the menu key and navigate with the arrow keys in the tree
-       * view without leaving the keyboard :)
-       */
-      gtk_tree_view_set_cursor (GTK_TREE_VIEW (r->tree),
-                                path,
-                                NULL,
-                                FALSE);
-      /* We still need to grab the focus on the tree view widget as suggested
-       * by the documentation.
-       */
+      ide_tree_set_selected_node (IDE_TREE (r->tree), r->node);
       gtk_widget_grab_focus (GTK_WIDGET (r->tree));
     }
 
diff --git a/src/plugins/vcsui/gbp-vcsui-tree-addin.c b/src/plugins/vcsui/gbp-vcsui-tree-addin.c
index 6c8c2ffb6..ec954235e 100644
--- a/src/plugins/vcsui/gbp-vcsui-tree-addin.c
+++ b/src/plugins/vcsui/gbp-vcsui-tree-addin.c
@@ -37,15 +37,13 @@ struct _GbpVcsuiTreeAddin
   GObject        parent_instance;
 
   IdeTree       *tree;
-  IdeTreeModel  *model;
   IdeVcs        *vcs;
   IdeVcsMonitor *monitor;
 };
 
 static void
 gbp_vcsui_tree_addin_load (IdeTreeAddin *addin,
-                           IdeTree      *tree,
-                           IdeTreeModel *model)
+                           IdeTree      *tree)
 {
   GbpVcsuiTreeAddin *self = (GbpVcsuiTreeAddin *)addin;
   IdeVcsMonitor *monitor;
@@ -55,9 +53,7 @@ gbp_vcsui_tree_addin_load (IdeTreeAddin *addin,
   g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (GBP_IS_VCSUI_TREE_ADDIN (self));
   g_assert (IDE_IS_TREE (tree));
-  g_assert (IDE_IS_TREE_MODEL (model));
 
-  self->model = model;
   self->tree = tree;
 
   if ((workbench = ide_widget_get_workbench (GTK_WIDGET (tree))) &&
@@ -76,67 +72,23 @@ gbp_vcsui_tree_addin_load (IdeTreeAddin *addin,
 
 static void
 gbp_vcsui_tree_addin_unload (IdeTreeAddin *addin,
-                             IdeTree      *tree,
-                             IdeTreeModel *model)
+                             IdeTree      *tree)
 {
   GbpVcsuiTreeAddin *self = (GbpVcsuiTreeAddin *)addin;
 
   g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (GBP_IS_VCSUI_TREE_ADDIN (self));
   g_assert (IDE_IS_TREE (tree));
-  g_assert (IDE_IS_TREE_MODEL (model));
 
   g_clear_object (&self->monitor);
   g_clear_object (&self->vcs);
 
-  self->model = NULL;
   self->tree = NULL;
 }
 
-static void
-gbp_vcsui_tree_addin_cell_data_func (IdeTreeAddin    *addin,
-                                     IdeTreeNode     *node,
-                                     GtkCellRenderer *cell)
-{
-  GbpVcsuiTreeAddin *self = (GbpVcsuiTreeAddin *)addin;
-  g_autoptr(IdeVcsFileInfo) info = NULL;
-  g_autoptr(GFile) file = NULL;
-  IdeProjectFile *project_file;
-  IdeTreeNodeFlags flags = 0;
-
-  g_assert (GBP_IS_VCSUI_TREE_ADDIN (self));
-  g_assert (IDE_IS_TREE_NODE (node));
-  g_assert (GTK_IS_CELL_RENDERER (cell));
-
-  if (self->monitor == NULL)
-    return;
-
-  if (!ide_tree_node_holds (node, IDE_TYPE_PROJECT_FILE))
-    return;
-
-  project_file = ide_tree_node_get_item (node);
-  file = ide_project_file_ref_file (project_file);
-
-  if ((info = ide_vcs_monitor_ref_info (self->monitor, file)))
-    {
-      IdeVcsFileStatus status = ide_vcs_file_info_get_status (info);
-
-      if (status == IDE_VCS_FILE_STATUS_ADDED)
-        flags = IDE_TREE_NODE_FLAGS_ADDED;
-      else if (status == IDE_VCS_FILE_STATUS_CHANGED)
-        flags = IDE_TREE_NODE_FLAGS_CHANGED;
-
-      if (flags && ide_tree_node_has_child (node))
-        flags |= IDE_TREE_NODE_FLAGS_DESCENDANT;
-    }
-
-  ide_tree_node_set_flags (node, flags);
-}
-
 static void
 tree_addin_iface_init (IdeTreeAddinInterface *iface)
 {
-  iface->cell_data_func = gbp_vcsui_tree_addin_cell_data_func;
   iface->load = gbp_vcsui_tree_addin_load;
   iface->unload = gbp_vcsui_tree_addin_unload;
 }


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