[gnome-builder: 41/139] libide-tree: add new libide-tree static library



commit 897ddd53e7c99ff8d2bd98672fd7d1f38282af03
Author: Christian Hergert <chergert redhat com>
Date:   Wed Jan 9 16:27:37 2019 -0800

    libide-tree: add new libide-tree static library
    
    The libide-tree static library contains a model for creating lazy-built
    trees and displaying them using content created by addins.
    
    It is similar to DzlTree but more specialized for Builder's use-case.

 src/libide/tree/ide-tree-addin.c   |  366 +++++++
 src/libide/tree/ide-tree-addin.h   |  149 +++
 src/libide/tree/ide-tree-model.c   | 1626 +++++++++++++++++++++++++++++++
 src/libide/tree/ide-tree-model.h   |   72 ++
 src/libide/tree/ide-tree-node.c    | 1863 ++++++++++++++++++++++++++++++++++++
 src/libide/tree/ide-tree-node.h    |  172 ++++
 src/libide/tree/ide-tree-private.h |   70 ++
 src/libide/tree/ide-tree.c         |  764 +++++++++++++++
 src/libide/tree/ide-tree.h         |   67 ++
 src/libide/tree/libide-tree.h      |   36 +
 src/libide/tree/meson.build        |   62 ++
 11 files changed, 5247 insertions(+)
---
diff --git a/src/libide/tree/ide-tree-addin.c b/src/libide/tree/ide-tree-addin.c
new file mode 100644
index 000000000..973ce7ffa
--- /dev/null
+++ b/src/libide/tree/ide-tree-addin.c
@@ -0,0 +1,366 @@
+/* ide-tree-addin.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-tree-addin"
+
+#include "config.h"
+
+#include <libide-threading.h>
+
+#include "ide-tree-addin.h"
+
+G_DEFINE_INTERFACE (IdeTreeAddin, ide_tree_addin, G_TYPE_OBJECT)
+
+static void
+ide_tree_addin_real_build_children_async (IdeTreeAddin        *self,
+                                          IdeTreeNode         *node,
+                                          GCancellable        *cancellable,
+                                          GAsyncReadyCallback  callback,
+                                          gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_ADDIN (self));
+  g_assert (IDE_IS_TREE_NODE (node));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_tree_addin_real_build_children_async);
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->build_children)
+    IDE_TREE_ADDIN_GET_IFACE (self)->build_children (self, node);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_tree_addin_real_build_children_finish (IdeTreeAddin  *self,
+                                           GAsyncResult  *result,
+                                           GError       **error)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_ADDIN (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_tree_addin_real_node_dropped_async (IdeTreeAddin        *self,
+                                        IdeTreeNode         *drag_node,
+                                        IdeTreeNode         *drop_node,
+                                        GtkSelectionData    *selection,
+                                        GdkDragAction        actions,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_ADDIN (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  g_task_report_new_error (self, callback, user_data,
+                           ide_tree_addin_real_node_dropped_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "Addin does not support dropping nodes");
+}
+
+static gboolean
+ide_tree_addin_real_node_dropped_finish (IdeTreeAddin  *self,
+                                         GAsyncResult  *result,
+                                         GError       **error)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_ADDIN (self));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_tree_addin_default_init (IdeTreeAddinInterface *iface)
+{
+  iface->build_children_async = ide_tree_addin_real_build_children_async;
+  iface->build_children_finish = ide_tree_addin_real_build_children_finish;
+  iface->node_dropped_async = ide_tree_addin_real_node_dropped_async;
+  iface->node_dropped_finish = ide_tree_addin_real_node_dropped_finish;
+}
+
+/**
+ * ide_tree_addin_build_children_async:
+ * @self: a #IdeTreeAddin
+ * @node: a #IdeTreeNode
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a #GAsyncReadyCallback or %NULL
+ * @user_data: user data for @callback
+ *
+ * This function is called when building the children of a node. This
+ * happens when expanding an node that might have children, or building the
+ * root node.
+ *
+ * You may want to use ide_tree_node_holds() to determine if the node
+ * contains an item that you are interested in.
+ *
+ * This function will call the synchronous form of
+ * IdeTreeAddin.build_children() if no asynchronous form is available.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_addin_build_children_async (IdeTreeAddin        *self,
+                                     IdeTreeNode         *node,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (node));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_TREE_ADDIN_GET_IFACE (self)->build_children_async (self, node, cancellable, callback, user_data);
+}
+
+/**
+ * ide_tree_addin_build_children_finish:
+ * @self: a #IdeTreeAddin
+ * @result: result given to callback in ide_tree_addin_build_children_async()
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to ide_tree_addin_build_children_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_addin_build_children_finish (IdeTreeAddin  *self,
+                                      GAsyncResult  *result,
+                                      GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_TREE_ADDIN_GET_IFACE (self)->build_children_finish (self, result, error);
+}
+
+/**
+ * ide_tree_addin_build_node:
+ * @self: a #IdeTreeAddin
+ * @node: a #IdeTreeNode
+ *
+ * This function is called when preparing a node for display in the tree.
+ *
+ * Addins should adjust any state on the node that makes sense based on the
+ * addin.
+ *
+ * You may want to use ide_tree_node_holds() to determine if the node
+ * contains an item that you are interested in.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_addin_build_node (IdeTreeAddin *self,
+                           IdeTreeNode  *node)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (node));
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->build_node)
+    IDE_TREE_ADDIN_GET_IFACE (self)->build_node (self, node);
+}
+
+/**
+ * ide_tree_addin_activated:
+ * @self: an #IdeTreeAddin
+ * @tree: an #IdeTree
+ * @node: an #IdeTreeNode
+ *
+ * This function is called when a node has been activated in the tree
+ * and allows for the addin to perform any necessary operations in response
+ * to that.
+ *
+ * If the addin performs an action based on the activation request, then it
+ * should return %TRUE from this function so that no further addins may
+ * respond to the action.
+ *
+ * Returns: %TRUE if the activation was handled, otherwise %FALSE
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_addin_node_activated (IdeTreeAddin *self,
+                               IdeTree      *tree,
+                               IdeTreeNode  *node)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE_ADDIN (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE (tree), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE_NODE (node), FALSE);
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->node_activated)
+    return IDE_TREE_ADDIN_GET_IFACE (self)->node_activated (self, tree, node);
+
+  return FALSE;
+}
+
+void
+ide_tree_addin_load (IdeTreeAddin *self,
+                     IdeTree      *tree,
+                     IdeTreeModel *model)
+{
+  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);
+}
+
+void
+ide_tree_addin_unload (IdeTreeAddin *self,
+                       IdeTree      *tree,
+                       IdeTreeModel *model)
+{
+  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);
+}
+
+void
+ide_tree_addin_selection_changed (IdeTreeAddin *self,
+                                  IdeTreeNode  *selection)
+{
+  g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+  g_return_if_fail (!selection || IDE_IS_TREE_NODE (selection));
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->selection_changed)
+    IDE_TREE_ADDIN_GET_IFACE (self)->selection_changed (self, selection);
+}
+
+void
+ide_tree_addin_node_expanded (IdeTreeAddin *self,
+                              IdeTreeNode  *node)
+{
+  g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (node));
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->node_expanded)
+    IDE_TREE_ADDIN_GET_IFACE (self)->node_expanded (self, node);
+}
+
+void
+ide_tree_addin_node_collapsed (IdeTreeAddin *self,
+                               IdeTreeNode  *node)
+{
+  g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (node));
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->node_collapsed)
+    IDE_TREE_ADDIN_GET_IFACE (self)->node_collapsed (self, node);
+}
+
+gboolean
+ide_tree_addin_node_draggable (IdeTreeAddin *self,
+                               IdeTreeNode  *node)
+{
+  g_return_val_if_fail (IDE_IS_TREE_ADDIN (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE_NODE (node), FALSE);
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->node_draggable)
+    return IDE_TREE_ADDIN_GET_IFACE (self)->node_draggable (self, node);
+
+  return FALSE;
+}
+
+gboolean
+ide_tree_addin_node_droppable (IdeTreeAddin     *self,
+                               IdeTreeNode      *drag_node,
+                               IdeTreeNode      *drop_node,
+                               GtkSelectionData *selection)
+{
+  g_return_val_if_fail (IDE_IS_TREE_ADDIN (self), FALSE);
+  g_return_val_if_fail (!drag_node || IDE_IS_TREE_NODE (drag_node), FALSE);
+  g_return_val_if_fail (!drop_node || IDE_IS_TREE_NODE (drop_node), FALSE);
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->node_droppable)
+    return IDE_TREE_ADDIN_GET_IFACE (self)->node_droppable (self, drag_node, drop_node, selection);
+
+  return FALSE;
+}
+
+void
+ide_tree_addin_node_dropped_async (IdeTreeAddin        *self,
+                                   IdeTreeNode         *drag_node,
+                                   IdeTreeNode         *drop_node,
+                                   GtkSelectionData    *selection,
+                                   GdkDragAction        actions,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+  g_return_if_fail (!drag_node || IDE_IS_TREE_NODE (drag_node));
+  g_return_if_fail (!drop_node || IDE_IS_TREE_NODE (drop_node));
+  g_return_if_fail (selection != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_TREE_ADDIN_GET_IFACE (self)->node_dropped_async (self,
+                                                       drag_node,
+                                                       drop_node,
+                                                       selection,
+                                                       actions,
+                                                       cancellable,
+                                                       callback,
+                                                       user_data);
+}
+
+gboolean
+ide_tree_addin_node_dropped_finish (IdeTreeAddin  *self,
+                                    GAsyncResult  *result,
+                                    GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  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
new file mode 100644
index 000000000..92275620d
--- /dev/null
+++ b/src/libide/tree/ide-tree-addin.h
@@ -0,0 +1,149 @@
+/* ide-tree-addin.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-core.h>
+
+#include "ide-tree.h"
+#include "ide-tree-model.h"
+#include "ide-tree-node.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TREE_ADDIN (ide_tree_addin_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeTreeAddin, ide_tree_addin, IDE, TREE_ADDIN, GObject)
+
+struct _IdeTreeAddinInterface
+{
+  GTypeInterface parent;
+
+  void     (*load)                  (IdeTreeAddin         *self,
+                                     IdeTree              *tree,
+                                     IdeTreeModel         *model);
+  void     (*unload)                (IdeTreeAddin         *self,
+                                     IdeTree              *tree,
+                                     IdeTreeModel         *model);
+  void     (*build_node)            (IdeTreeAddin         *self,
+                                     IdeTreeNode          *node);
+  void     (*build_children)        (IdeTreeAddin         *self,
+                                     IdeTreeNode          *node);
+  void     (*build_children_async)  (IdeTreeAddin         *self,
+                                     IdeTreeNode          *node,
+                                     GCancellable         *cancellable,
+                                     GAsyncReadyCallback   callback,
+                                     gpointer              user_data);
+  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);
+  void     (*selection_changed)     (IdeTreeAddin         *self,
+                                     IdeTreeNode          *selection);
+  void     (*node_expanded)         (IdeTreeAddin         *self,
+                                     IdeTreeNode          *node);
+  void     (*node_collapsed)        (IdeTreeAddin         *self,
+                                     IdeTreeNode          *node);
+  gboolean (*node_draggable)        (IdeTreeAddin         *self,
+                                     IdeTreeNode          *node);
+  gboolean (*node_droppable)        (IdeTreeAddin         *self,
+                                     IdeTreeNode          *drag_node,
+                                     IdeTreeNode          *drop_node,
+                                     GtkSelectionData     *selection);
+  void     (*node_dropped_async)    (IdeTreeAddin         *self,
+                                     IdeTreeNode          *drag_node,
+                                     IdeTreeNode          *drop_node,
+                                     GtkSelectionData     *selection,
+                                     GdkDragAction         actions,
+                                     GCancellable         *cancellable,
+                                     GAsyncReadyCallback   callback,
+                                     gpointer              user_data);
+  gboolean (*node_dropped_finish)   (IdeTreeAddin         *self,
+                                     GAsyncResult         *result,
+                                     GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_load                  (IdeTreeAddin         *self,
+                                               IdeTree              *tree,
+                                               IdeTreeModel         *model);
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_unload                (IdeTreeAddin         *self,
+                                               IdeTree              *tree,
+                                               IdeTreeModel         *model);
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_build_node            (IdeTreeAddin         *self,
+                                               IdeTreeNode          *node);
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_build_children_async  (IdeTreeAddin         *self,
+                                               IdeTreeNode          *node,
+                                               GCancellable         *cancellable,
+                                               GAsyncReadyCallback   callback,
+                                               gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_addin_build_children_finish (IdeTreeAddin         *self,
+                                               GAsyncResult         *result,
+                                               GError              **error);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_addin_node_activated        (IdeTreeAddin         *self,
+                                               IdeTree              *tree,
+                                               IdeTreeNode          *node);
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_selection_changed     (IdeTreeAddin         *self,
+                                               IdeTreeNode          *selection);
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_node_expanded         (IdeTreeAddin         *self,
+                                               IdeTreeNode          *node);
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_node_collapsed        (IdeTreeAddin         *self,
+                                               IdeTreeNode          *node);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_addin_node_draggable        (IdeTreeAddin         *self,
+                                               IdeTreeNode          *node);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_addin_node_droppable        (IdeTreeAddin         *self,
+                                               IdeTreeNode          *drag_node,
+                                               IdeTreeNode          *drop_node,
+                                               GtkSelectionData     *selection);
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_node_dropped_async    (IdeTreeAddin         *self,
+                                               IdeTreeNode          *drag_node,
+                                               IdeTreeNode          *drop_node,
+                                               GtkSelectionData     *selection,
+                                               GdkDragAction         actions,
+                                               GCancellable         *cancellable,
+                                               GAsyncReadyCallback   callback,
+                                               gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_addin_node_dropped_finish   (IdeTreeAddin         *self,
+                                               GAsyncResult         *result,
+                                               GError              **error);
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_cell_data_func        (IdeTreeAddin         *self,
+                                               IdeTreeNode          *node,
+                                               GtkCellRenderer      *cell);
+
+G_END_DECLS
diff --git a/src/libide/tree/ide-tree-model.c b/src/libide/tree/ide-tree-model.c
new file mode 100644
index 000000000..aa797d645
--- /dev/null
+++ b/src/libide/tree/ide-tree-model.c
@@ -0,0 +1,1626 @@
+/* ide-tree-model.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-tree-model"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-plugins.h>
+#include <libide-threading.h>
+#include <string.h>
+
+#include "ide-tree-addin.h"
+#include "ide-tree-model.h"
+#include "ide-tree-node.h"
+#include "ide-tree-private.h"
+#include "ide-tree.h"
+
+struct _IdeTreeModel
+{
+  IdeObject               parent_instance;
+  IdeExtensionSetAdapter *addins;
+  gchar                  *kind;
+  IdeTreeNode            *root;
+  IdeTree                *tree;
+};
+
+typedef struct
+{
+  IdeTreeNode      *drag_node;
+  IdeTreeNode      *drop_node;
+  GtkSelectionData *selection;
+  GdkDragAction     actions;
+  gint              n_active;
+} DragDataReceived;
+
+static void tree_model_iface_init       (GtkTreeModelIface      *iface);
+static void tree_drag_dest_iface_init   (GtkTreeDragDestIface   *iface);
+static void tree_drag_source_iface_init (GtkTreeDragSourceIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeTreeModel, ide_tree_model, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_MODEL, tree_model_iface_init)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_DRAG_DEST, tree_drag_dest_iface_init)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_DRAG_SOURCE, tree_drag_source_iface_init))
+
+enum {
+  PROP_0,
+  PROP_KIND,
+  PROP_ROOT,
+  PROP_TREE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+drag_data_received_free (DragDataReceived *data)
+{
+  g_assert (data != NULL);
+  g_assert (!data->drag_node || IDE_IS_TREE_NODE (data->drag_node));
+  g_assert (!data->drop_node || IDE_IS_TREE_NODE (data->drop_node));
+  g_assert (data->n_active == 0);
+
+  g_clear_object (&data->drag_node);
+  g_clear_object (&data->drop_node);
+  g_clear_pointer (&data->selection, gtk_selection_data_free);
+  g_slice_free (DragDataReceived, data);
+}
+
+static IdeTreeNode *
+create_root (void)
+{
+  return g_object_new (IDE_TYPE_TREE_NODE,
+                       "children-possible", TRUE,
+                       NULL);
+}
+
+static void
+ide_tree_model_build_node_cb (IdeExtensionSetAdapter *set,
+                              PeasPluginInfo         *plugin_info,
+                              PeasExtension          *exten,
+                              gpointer                user_data)
+{
+  IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+  IdeTreeNode *node = user_data;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  ide_tree_addin_build_node (addin, node);
+}
+
+void
+_ide_tree_model_build_node (IdeTreeModel *self,
+                            IdeTreeNode  *node)
+{
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_tree_model_build_node_cb,
+                                     node);
+}
+
+static IdeTreeNodeVisit
+ide_tree_model_addin_added_traverse_cb (IdeTreeNode *node,
+                                        gpointer     user_data)
+{
+  IdeTreeAddin *addin = user_data;
+
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  if (!ide_tree_node_is_empty (node))
+    {
+      ide_tree_addin_build_node (addin, node);
+
+      if (ide_tree_node_get_children_possible (node))
+        _ide_tree_node_set_needs_build_children (node, TRUE);
+    }
+
+  return IDE_TREE_NODE_VISIT_CHILDREN;
+}
+
+static void
+ide_tree_model_addin_added_cb (IdeExtensionSetAdapter *adapter,
+                               PeasPluginInfo         *plugin_info,
+                               PeasExtension          *exten,
+                               gpointer                user_data)
+{
+  IdeTreeModel *self = user_data;
+  IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (IDE_IS_TREE (self->tree));
+
+  ide_tree_addin_load (addin, self->tree, self);
+
+  ide_tree_node_traverse (self->root,
+                          G_PRE_ORDER,
+                          G_TRAVERSE_ALL,
+                          -1,
+                          ide_tree_model_addin_added_traverse_cb,
+                          addin);
+}
+
+static void
+ide_tree_model_addin_removed_cb (IdeExtensionSetAdapter *adapter,
+                                 PeasPluginInfo         *plugin_info,
+                                 PeasExtension          *exten,
+                                 gpointer                user_data)
+{
+  IdeTreeModel *self = user_data;
+  IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TREE_MODEL (self));
+
+  ide_tree_addin_unload (addin, self->tree, self);
+}
+
+static void
+ide_tree_model_parent_set (IdeObject *object,
+                           IdeObject *parent)
+{
+  IdeTreeModel *self = (IdeTreeModel *)object;
+  g_autoptr(IdeContext) context = NULL;
+
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
+
+  if (self->addins != NULL || parent == NULL ||
+      !(context = ide_object_ref_context (IDE_OBJECT (self))))
+    return;
+
+  g_assert (IDE_IS_TREE (self->tree));
+
+  self->addins = ide_extension_set_adapter_new (IDE_OBJECT (self),
+                                                peas_engine_get_default (),
+                                                IDE_TYPE_TREE_ADDIN,
+                                                "Tree-Kind",
+                                                self->kind);
+
+  g_signal_connect_object (self->addins,
+                           "extension-added",
+                           G_CALLBACK (ide_tree_model_addin_added_cb),
+                           self,
+                           0);
+
+  g_signal_connect_object (self->addins,
+                           "extension-removed",
+                           G_CALLBACK (ide_tree_model_addin_removed_cb),
+                           self,
+                           0);
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_tree_model_addin_added_cb,
+                                     self);
+}
+
+static void
+ide_tree_model_dispose (GObject *object)
+{
+  IdeTreeModel *self = (IdeTreeModel *)object;
+
+  /* Clear the model back-pointer for root so that it cannot emit anu
+   * further signals on our tree model.
+   */
+  if (self->root != NULL)
+    _ide_tree_node_set_model (self->root, NULL);
+
+  g_clear_object (&self->tree);
+  ide_clear_and_destroy_object (&self->addins);
+  g_clear_object (&self->root);
+  g_clear_pointer (&self->kind, g_free);
+
+  G_OBJECT_CLASS (ide_tree_model_parent_class)->dispose (object);
+}
+
+static void
+ide_tree_model_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  IdeTreeModel *self = IDE_TREE_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_KIND:
+      g_value_set_string (value, ide_tree_model_get_kind (self));
+      break;
+
+    case PROP_ROOT:
+      g_value_set_object (value, ide_tree_model_get_root (self));
+      break;
+
+    case PROP_TREE:
+      g_value_set_object (value, self->tree);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_tree_model_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  IdeTreeModel *self = IDE_TREE_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_KIND:
+      ide_tree_model_set_kind (self, g_value_get_string (value));
+      break;
+
+    case PROP_ROOT:
+      ide_tree_model_set_root (self, g_value_get_object (value));
+      break;
+
+    case PROP_TREE:
+      self->tree = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_tree_model_class_init (IdeTreeModelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_tree_model_dispose;
+  object_class->get_property = ide_tree_model_get_property;
+  object_class->set_property = ide_tree_model_set_property;
+
+  i_object_class->parent_set = ide_tree_model_parent_set;
+
+  properties [PROP_TREE] =
+    g_param_spec_object ("tree",
+                         "Tree",
+                         "The tree the model belongs to",
+                         IDE_TYPE_TREE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTreeModel:root:
+   *
+   * The "root" property contains the root #IdeTreeNode that is used to build
+   * the tree. It should contain an object for the #IdeTreeNode:item property
+   * so that #IdeTreeAddin's may use it to build the node and any children.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ROOT] =
+    g_param_spec_object ("root",
+                         "Root",
+                         "The root IdeTreeNode",
+                         IDE_TYPE_TREE_NODE,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTreeModel:kind:
+   *
+   * The "kind" property is used to determine what #IdeTreeAddin plugins to
+   * load. Only plugins which match the "kind" will be loaded to extend the
+   * tree contents.
+   *
+   * For example, to extend the project-tree, plugins should set
+   * "X-Tree-Kind=project" in their .plugin manifest.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_KIND] =
+    g_param_spec_string ("kind",
+                         "Kind",
+                         "The kind of tree model that is being generated",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_tree_model_init (IdeTreeModel *self)
+{
+  self->root = create_root ();
+}
+
+IdeTreeModel *
+_ide_tree_model_new (IdeTree *tree)
+{
+  return g_object_new (IDE_TYPE_TREE_MODEL,
+                       "tree", tree,
+                       NULL);
+}
+
+void
+_ide_tree_model_release_addins (IdeTreeModel *self)
+{
+  g_assert (IDE_IS_TREE_MODEL (self));
+
+  ide_clear_and_destroy_object (&self->addins);
+}
+
+static GtkTreeModelFlags
+ide_tree_model_get_flags (GtkTreeModel *model)
+{
+  return 0;
+}
+
+static gint
+ide_tree_model_get_n_columns (GtkTreeModel *model)
+{
+  return 1;
+}
+
+static GType
+ide_tree_model_get_column_type (GtkTreeModel *model,
+                                gint          index_)
+{
+  return IDE_TYPE_TREE_NODE;
+}
+
+static GtkTreePath *
+ide_tree_model_get_path (GtkTreeModel *tree_model,
+                         GtkTreeIter  *iter)
+{
+  g_autoptr(GArray) indexes = NULL;
+  IdeTreeModel *self = (IdeTreeModel *)tree_model;
+  IdeTreeNode *node;
+
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (iter != NULL);
+  g_assert (IDE_IS_TREE_NODE (iter->user_data));
+
+  node = iter->user_data;
+
+  if (ide_tree_node_is_root (node))
+    return NULL;
+
+  indexes = g_array_new (FALSE, FALSE, sizeof (gint));
+
+  do
+    {
+      gint position;
+
+      position = ide_tree_node_get_index (node);
+      g_array_prepend_val (indexes, position);
+    }
+  while ((node = ide_tree_node_get_parent (node)) &&
+         !ide_tree_node_is_root (node));
+
+  return gtk_tree_path_new_from_indicesv (&g_array_index (indexes, gint, 0), indexes->len);
+}
+
+static gboolean
+ide_tree_model_get_iter (GtkTreeModel *model,
+                         GtkTreeIter  *iter,
+                         GtkTreePath  *path)
+{
+  IdeTreeModel *self = (IdeTreeModel *)model;
+  IdeTreeNode *node;
+  gint *indices;
+  gint depth = 0;
+
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (iter != NULL);
+  g_assert (path != NULL);
+
+  memset (iter, 0, sizeof *iter);
+
+  if (self->root == NULL)
+    return FALSE;
+
+  indices = gtk_tree_path_get_indices_with_depth (path, &depth);
+
+  node = self->root;
+
+  for (gint i = 0; i < depth; i++)
+    {
+      if (!(node = ide_tree_node_get_nth_child (node, indices[i])))
+        return FALSE;
+    }
+
+  if (ide_tree_node_is_root (node))
+    return FALSE;
+
+  iter->user_data = node;
+  return TRUE;
+}
+
+static void
+ide_tree_model_get_value (GtkTreeModel *model,
+                          GtkTreeIter  *iter,
+                          gint          column,
+                          GValue       *value)
+{
+  g_value_init (value, IDE_TYPE_TREE_NODE);
+  g_value_set_object (value, iter->user_data);
+}
+
+static gboolean
+ide_tree_model_iter_next (GtkTreeModel *model,
+                          GtkTreeIter  *iter)
+{
+  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (!iter->user_data || IDE_IS_TREE_NODE (iter->user_data));
+
+  if (iter->user_data)
+    iter->user_data = ide_tree_node_get_next (iter->user_data);
+
+  return IDE_IS_TREE_NODE (iter->user_data);
+}
+
+static gboolean
+ide_tree_model_iter_previous (GtkTreeModel *model,
+                              GtkTreeIter  *iter)
+{
+  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (!iter->user_data || IDE_IS_TREE_NODE (iter->user_data));
+
+  if (iter->user_data)
+    iter->user_data = ide_tree_node_get_previous (iter->user_data);
+
+  return IDE_IS_TREE_NODE (iter->user_data);
+}
+
+static gboolean
+ide_tree_model_iter_nth_child (GtkTreeModel *model,
+                               GtkTreeIter  *iter,
+                               GtkTreeIter  *parent,
+                               gint          n)
+{
+  IdeTreeModel *self = (IdeTreeModel  *)model;
+  IdeTreeNode *pnode;
+
+  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+
+  if (self->root == NULL)
+    return FALSE;
+
+  g_assert (parent == NULL || IDE_IS_TREE_NODE (parent->user_data));
+
+  n = CLAMP (n, 0, G_MAXINT);
+
+  memset (iter, 0, sizeof *iter);
+
+  if (parent == NULL)
+    pnode = self->root;
+  else
+    pnode = parent->user_data;
+  g_assert (IDE_IS_TREE_NODE (pnode));
+
+  iter->user_data = ide_tree_node_get_nth_child (pnode, n);
+  g_assert (!iter->user_data || IDE_IS_TREE_NODE (iter->user_data));
+
+  return IDE_IS_TREE_NODE (iter->user_data);
+}
+
+static gboolean
+ide_tree_model_iter_children (GtkTreeModel *model,
+                              GtkTreeIter  *iter,
+                              GtkTreeIter  *parent)
+{
+  return ide_tree_model_iter_nth_child (model, iter, parent, 0);
+}
+
+static gboolean
+ide_tree_model_iter_has_child (GtkTreeModel *model,
+                               GtkTreeIter  *iter)
+{
+  gboolean ret;
+
+  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (IDE_IS_TREE_NODE (iter->user_data));
+
+  ret = ide_tree_node_has_child (iter->user_data);
+
+  IDE_TRACE_MSG ("%s has child -> %s",
+                 ide_tree_node_get_display_name (iter->user_data),
+                 ret ? "yes" : "no");
+
+  return ret;
+}
+
+static gint
+ide_tree_model_iter_n_children (GtkTreeModel *model,
+                                GtkTreeIter  *iter)
+{
+  IdeTreeModel *self = (IdeTreeModel *)model;
+  gint ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (self != NULL);
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (IDE_IS_TREE_NODE (self->root));
+  g_assert (iter == NULL || IDE_IS_TREE_NODE (iter->user_data));
+
+  if (iter == NULL)
+    ret = ide_tree_node_get_n_children (self->root);
+  else if (iter->user_data)
+    ret = ide_tree_node_get_n_children (iter->user_data);
+  else
+    ret = 0;
+
+  IDE_RETURN (ret);
+}
+
+static gboolean
+ide_tree_model_iter_parent (GtkTreeModel *model,
+                            GtkTreeIter  *iter,
+                            GtkTreeIter  *child)
+{
+  IdeTreeModel *self = (IdeTreeModel *)model;
+
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (iter != NULL);
+  g_assert (child != NULL);
+  g_assert (IDE_IS_TREE_NODE (child->user_data));
+
+  memset (iter, 0, sizeof *iter);
+
+  iter->user_data = ide_tree_node_get_parent (child->user_data);
+
+  return !ide_tree_node_is_root (iter->user_data);
+}
+
+static void
+ide_tree_model_row_inserted (GtkTreeModel *model,
+                             GtkTreePath  *path,
+                             GtkTreeIter  *iter)
+{
+  IdeTreeModel *self = (IdeTreeModel *)model;
+  IdeTreeNode *node;
+
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (path != NULL);
+  g_assert (iter != NULL);
+
+  node = iter->user_data;
+
+  g_assert (IDE_IS_TREE_NODE (node));
+
+#if 0
+  g_print ("Building %s (child of %s)\n",
+           ide_tree_node_get_display_name (node),
+           ide_tree_node_get_display_name (ide_tree_node_get_parent (node)));
+#endif
+
+  /*
+   * If this node holds an IdeObject which is not rooted on our object
+   * tree, add it to the object tree beneath us so that it can get destroy
+   * propagation and access to the IdeContext.
+   */
+  if (ide_tree_node_holds (node, IDE_TYPE_OBJECT))
+    {
+      IdeObject *object = ide_tree_node_get_item (node);
+
+      if (!ide_object_get_parent (object))
+        ide_object_append (IDE_OBJECT (self), object);
+    }
+
+  _ide_tree_model_build_node (self, node);
+}
+
+static void
+ide_tree_model_ref_node (GtkTreeModel *model,
+                         GtkTreeIter  *iter)
+{
+  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (!iter->user_data || IDE_IS_TREE_NODE (iter->user_data));
+
+  if (iter->user_data)
+    g_object_ref (iter->user_data);
+}
+
+static void
+ide_tree_model_unref_node (GtkTreeModel *model,
+                           GtkTreeIter  *iter)
+{
+  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (!iter->user_data || IDE_IS_TREE_NODE (iter->user_data));
+
+  if (iter->user_data)
+    g_object_unref (iter->user_data);
+}
+
+static void
+tree_model_iface_init (GtkTreeModelIface *iface)
+{
+  iface->get_flags = ide_tree_model_get_flags;
+  iface->get_n_columns = ide_tree_model_get_n_columns;
+  iface->get_column_type = ide_tree_model_get_column_type;
+  iface->get_iter = ide_tree_model_get_iter;
+  iface->get_path = ide_tree_model_get_path;
+  iface->get_value = ide_tree_model_get_value;
+  iface->iter_next = ide_tree_model_iter_next;
+  iface->iter_previous = ide_tree_model_iter_previous;
+  iface->iter_children = ide_tree_model_iter_children;
+  iface->iter_has_child = ide_tree_model_iter_has_child;
+  iface->iter_n_children = ide_tree_model_iter_n_children;
+  iface->iter_nth_child = ide_tree_model_iter_nth_child;
+  iface->iter_parent = ide_tree_model_iter_parent;
+  iface->row_inserted = ide_tree_model_row_inserted;
+  iface->ref_node = ide_tree_model_ref_node;
+  iface->unref_node = ide_tree_model_unref_node;
+}
+
+/**
+ * ide_tree_model_get_path_for_node:
+ * @self: an #IdeTreeModel
+ * @node: an #IdeTreeNode
+ *
+ * Gets the #GtkTreePath pointing at @node.
+ *
+ * Returns: (transfer full) (nullable): a new #GtkTreePath
+ *
+ * Since: 3.32
+ */
+GtkTreePath *
+ide_tree_model_get_path_for_node (IdeTreeModel *self,
+                                  IdeTreeNode  *node)
+{
+  GtkTreeIter iter;
+
+  g_return_val_if_fail (IDE_IS_TREE_MODEL (self), NULL);
+  g_return_val_if_fail (IDE_IS_TREE_NODE (node), NULL);
+
+  if (ide_tree_model_get_iter_for_node (self, &iter, node))
+    return gtk_tree_model_get_path (GTK_TREE_MODEL (self), &iter);
+
+  return NULL;
+}
+
+/**
+ * ide_tree_model_get_iter_for_node:
+ * @self: an #IdeTreeModel
+ * @iter: (out): a #GtkTreeIter
+ * @node: an #IdeTreeNode
+ *
+ * Gets a #GtkTreeIter that points at @node.
+ *
+ * Returns: %TRUE if @iter was set; otherwise %FALSE
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_model_get_iter_for_node (IdeTreeModel *self,
+                                  GtkTreeIter  *iter,
+                                  IdeTreeNode  *node)
+{
+  g_return_val_if_fail (IDE_IS_TREE_MODEL (self), FALSE);
+
+  if (_ide_tree_model_contains_node (self, node))
+    {
+      memset (iter, 0, sizeof *iter);
+      iter->user_data = node;
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+/**
+ * ide_tree_model_get_root:
+ * @self: a #IdeTreeModel
+ *
+ * Gets the root #IdeTreeNode. This node is never visualized in the tree, but
+ * is used to build the immediate children which are displayed in the tree.
+ *
+ * Returns: (transfer none) (not nullable): an #IdeTreeNode
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_model_get_root (IdeTreeModel *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_MODEL (self), NULL);
+
+  return self->root;
+}
+
+static IdeTreeNodeVisit
+ide_tree_model_remove_all_cb (IdeTreeNode *node,
+                              gpointer     user_data)
+{
+  IdeTreeModel *self = user_data;
+
+  g_assert (IDE_IS_TREE_NODE (node));
+  g_assert (IDE_IS_TREE_MODEL (self));
+
+  if (node != self->root)
+    {
+      GtkTreePath *tree_path;
+
+      tree_path = ide_tree_model_get_path_for_node (self, node);
+      gtk_tree_model_row_deleted (GTK_TREE_MODEL (self), tree_path);
+      gtk_tree_path_free (tree_path);
+    }
+
+  return IDE_TREE_NODE_VISIT_CHILDREN;
+}
+
+static void
+ide_tree_model_remove_all (IdeTreeModel *self)
+{
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+
+  ide_tree_node_traverse (self->root,
+                          G_POST_ORDER,
+                          G_TRAVERSE_ALL,
+                          -1,
+                          ide_tree_model_remove_all_cb,
+                          self);
+}
+
+void
+ide_tree_model_set_root (IdeTreeModel *self,
+                         IdeTreeNode  *root)
+{
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+  g_return_if_fail (!root || IDE_IS_TREE_NODE (root));
+
+  if (root != self->root)
+    {
+      ide_tree_model_remove_all (self);
+      g_clear_object (&self->root);
+
+      if (root != NULL)
+        self->root = g_object_ref (root);
+      else
+        self->root = create_root ();
+
+      _ide_tree_node_set_model (self->root, self);
+
+      /* Root always requires building children */
+      if (!ide_tree_node_get_children_possible (self->root))
+        ide_tree_node_set_children_possible (self->root, TRUE);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ROOT]);
+    }
+}
+
+/**
+ * ide_tree_model_get_kind:
+ * @self: a #IdeTreeModel
+ *
+ * Gets the kind of model that is being generated. See #IdeTreeModel:kind
+ * for more information.
+ *
+ * Returns: (nullable): a string containing the kind, or %NULL
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_tree_model_get_kind (IdeTreeModel *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_MODEL (self), NULL);
+
+  return self->kind;
+}
+
+/**
+ * ide_tree_model_set_kind:
+ * @self: a #IdeTreeModel
+ * @kind: a string describing the kind of model
+ *
+ * Sets the kind of model that is being created. This determines what plugins
+ * are used to generate the tree contents.
+ *
+ * This should be set before adding the #IdeTreeModel to an #IdeObject to
+ * ensure the tree builds the proper contents.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_model_set_kind (IdeTreeModel *self,
+                         const gchar  *kind)
+{
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+
+  if (!ide_str_equal0 (kind, self->kind))
+    {
+      g_free (self->kind);
+      self->kind = g_strdup (kind);
+
+      if (self->addins != NULL)
+        ide_extension_set_adapter_set_value (self->addins, kind);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_KIND]);
+    }
+}
+
+typedef struct
+{
+  IdeTreeNode *node;
+  IdeTree     *tree;
+  gboolean     handled;
+} RowActivated;
+
+static void
+ide_tree_model_row_activated_cb (IdeExtensionSetAdapter *set,
+                                 PeasPluginInfo         *plugin_info,
+                                 PeasExtension          *exten,
+                                 gpointer                user_data)
+{
+  IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+  RowActivated *state = user_data;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (state != NULL);
+
+  if (state->handled)
+    return;
+
+  state->handled = ide_tree_addin_node_activated (addin, state->tree, state->node);
+}
+
+gboolean
+_ide_tree_model_row_activated (IdeTreeModel *self,
+                               IdeTree      *tree,
+                               GtkTreePath  *path)
+{
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (IDE_IS_TREE (tree));
+  g_assert (path != NULL);
+
+  if (gtk_tree_model_get_iter (GTK_TREE_MODEL (self), &iter, path))
+    {
+      RowActivated state = {
+        .node = iter.user_data,
+        .tree = tree,
+        .handled = FALSE,
+      };
+
+      ide_extension_set_adapter_foreach (self->addins,
+                                         ide_tree_model_row_activated_cb,
+                                         &state);
+
+      return state.handled;
+    }
+
+  return FALSE;
+}
+
+/**
+ * ide_tree_model_get_node:
+ * @self: a #IdeTreeModel
+ * @iter: a #GtkTreeIter
+ *
+ * Gets the #IdeTreeNode found at @iter.
+ *
+ * Returns: (transfer none) (nullable): an #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_model_get_node (IdeTreeModel *self,
+                         GtkTreeIter  *iter)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_TREE_MODEL (self), NULL);
+  g_return_val_if_fail (iter != NULL, NULL);
+
+  if (IDE_IS_TREE_NODE (iter->user_data))
+    return iter->user_data;
+
+  return NULL;
+}
+
+gboolean
+_ide_tree_model_contains_node (IdeTreeModel *self,
+                               IdeTreeNode  *node)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE_MODEL (self), FALSE);
+  g_return_val_if_fail (!node || IDE_IS_TREE_NODE (node), FALSE);
+
+  if (node == NULL)
+    return FALSE;
+
+  return self->root == ide_tree_node_get_root (node);
+}
+
+static void
+inc_active (IdeTask *task)
+{
+  gint n_active = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "N_ACTIVE"));
+  n_active++;
+  g_object_set_data (G_OBJECT (task), "N_ACTIVE", GINT_TO_POINTER (n_active));
+}
+
+static gboolean
+dec_active_and_test (IdeTask *task)
+{
+  gint n_active = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "N_ACTIVE"));
+  n_active--;
+  g_object_set_data (G_OBJECT (task), "N_ACTIVE", GINT_TO_POINTER (n_active));
+  return n_active == 0;
+}
+
+static void
+ide_tree_model_addin_build_children_cb (GObject      *object,
+                                        GAsyncResult *result,
+                                        gpointer      user_data)
+{
+  IdeTreeAddin *addin = (IdeTreeAddin *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  ide_tree_addin_build_children_finish (addin, result, &error);
+
+  if (dec_active_and_test (task))
+    {
+#if 0
+      {
+        IdeTreeNode *node = ide_task_get_task_data (task);
+        _ide_tree_node_dump (ide_tree_node_get_root (node));
+      }
+#endif
+
+      ide_task_return_boolean (task, TRUE);
+    }
+}
+
+static void
+ide_tree_model_expand_foreach_cb (IdeExtensionSetAdapter *set,
+                                  PeasPluginInfo         *plugin_info,
+                                  PeasExtension          *exten,
+                                  gpointer                user_data)
+{
+  IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+  IdeTreeNode *node;
+  IdeTask *task = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TASK (task));
+
+  node = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  inc_active (task);
+
+  ide_tree_addin_build_children_async (addin,
+                                       node,
+                                       ide_task_get_cancellable (task),
+                                       ide_tree_model_addin_build_children_cb,
+                                       g_object_ref (task));
+
+  _ide_tree_node_set_needs_build_children (node, FALSE);
+}
+
+static void
+ide_tree_model_expand_completed (IdeTreeNode *node,
+                                 GParamSpec  *pspec,
+                                 IdeTask     *task)
+{
+  g_assert (IDE_IS_TREE_NODE (node));
+  g_assert (pspec != NULL);
+  g_assert (IDE_IS_TASK (task));
+
+  _ide_tree_node_set_loading (node, FALSE);
+}
+
+void
+ide_tree_model_expand_async (IdeTreeModel        *self,
+                             IdeTreeNode         *node,
+                             GCancellable        *cancellable,
+                             GAsyncReadyCallback  callback,
+                             gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (node));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_tree_model_expand_async);
+  ide_task_set_task_data (task, g_object_ref (node), g_object_unref);
+
+  g_signal_connect_object (task,
+                           "notify::completed",
+                           G_CALLBACK (ide_tree_model_expand_completed),
+                           node,
+                           G_CONNECT_SWAPPED);
+
+  /* If no building is necessary, then just skip any work here */
+  if (!_ide_tree_node_get_needs_build_children (node) ||
+      ide_extension_set_adapter_get_n_extensions (self->addins) == 0)
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  _ide_tree_node_set_loading (node, TRUE);
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_tree_model_expand_foreach_cb,
+                                     task);
+}
+
+gboolean
+ide_tree_model_expand_finish (IdeTreeModel  *self,
+                              GAsyncResult  *result,
+                              GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE_MODEL (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static IdeTreeNodeVisit
+ide_tree_model_invalidate_traverse_cb (IdeTreeNode *node,
+                                       gpointer     user_data)
+{
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  if (!ide_tree_node_is_root (node))
+    ide_tree_node_remove (ide_tree_node_get_parent (node), node);
+
+  return IDE_TREE_NODE_VISIT_CHILDREN;
+}
+
+/**
+ * ide_tree_model_invalidate:
+ * @self: a #IdeTreeModel
+ * @node: (nullable): an #IdeTreeNode or %NULL
+ *
+ * Invalidates @model starting from @node so that those items
+ * are rebuilt using the configured tree addins.
+ *
+ * If @node is %NULL, the root of the tree is invalidated.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_model_invalidate (IdeTreeModel *self,
+                           IdeTreeNode  *node)
+{
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+  g_return_if_fail (!node || IDE_IS_TREE_NODE (node));
+
+  if (node == NULL)
+    node = self->root;
+
+  ide_tree_node_traverse (node,
+                          G_POST_ORDER,
+                          G_TRAVERSE_ALL,
+                          -1,
+                          ide_tree_model_invalidate_traverse_cb,
+                          NULL);
+
+  _ide_tree_node_set_needs_build_children (node, TRUE);
+  ide_tree_model_expand_async (self, node, NULL, NULL, NULL);
+}
+
+static void
+ide_tree_model_propagate_selection_changed_cb (IdeExtensionSetAdapter *set,
+                                               PeasPluginInfo         *plugin_info,
+                                               PeasExtension          *exten,
+                                               gpointer                user_data)
+{
+  IdeTreeNode *node = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (exten));
+  g_assert (!node || IDE_IS_TREE_NODE (node));
+
+  ide_tree_addin_selection_changed (IDE_TREE_ADDIN (exten), node);
+}
+
+void
+_ide_tree_model_selection_changed (IdeTreeModel *self,
+                                   GtkTreeIter  *iter)
+{
+  IdeTreeNode *node = NULL;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+  g_return_if_fail (!iter || IDE_IS_TREE_NODE (iter->user_data));
+
+  if (self->addins == NULL)
+    return;
+
+  if (iter != NULL)
+    node = ide_tree_model_get_node (self, iter);
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_tree_model_propagate_selection_changed_cb,
+                                     node);
+}
+
+static void
+ide_tree_model_propagate_node_expanded_cb (IdeExtensionSetAdapter *set,
+                                           PeasPluginInfo         *plugin_info,
+                                           PeasExtension          *exten,
+                                           gpointer                user_data)
+{
+  IdeTreeNode *node = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (exten));
+  g_assert (!node || IDE_IS_TREE_NODE (node));
+
+  ide_tree_addin_node_expanded (IDE_TREE_ADDIN (exten), node);
+}
+
+void
+_ide_tree_model_row_expanded (IdeTreeModel *self,
+                              IdeTree      *tree,
+                              GtkTreePath  *path)
+{
+  GtkTreeIter iter;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+  g_return_if_fail (IDE_IS_TREE (tree));
+  g_return_if_fail (path != NULL);
+
+  if (self->addins == NULL)
+    return;
+
+  if (ide_tree_model_get_iter (GTK_TREE_MODEL (self), &iter, path))
+    {
+      IdeTreeNode *node = ide_tree_model_get_node (self, &iter);
+
+      ide_extension_set_adapter_foreach (self->addins,
+                                         ide_tree_model_propagate_node_expanded_cb,
+                                         node);
+    }
+}
+
+static void
+ide_tree_model_propagate_node_collapsed_cb (IdeExtensionSetAdapter *set,
+                                            PeasPluginInfo         *plugin_info,
+                                            PeasExtension          *exten,
+                                            gpointer                user_data)
+{
+  IdeTreeNode *node = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (exten));
+  g_assert (!node || IDE_IS_TREE_NODE (node));
+
+  ide_tree_addin_node_collapsed (IDE_TREE_ADDIN (exten), node);
+}
+
+void
+_ide_tree_model_row_collapsed (IdeTreeModel *self,
+                               IdeTree      *tree,
+                               GtkTreePath  *path)
+{
+  GtkTreeIter iter;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+  g_return_if_fail (IDE_IS_TREE (tree));
+  g_return_if_fail (path != NULL);
+
+  if (self->addins == NULL)
+    return;
+
+  if (ide_tree_model_get_iter (GTK_TREE_MODEL (self), &iter, path))
+    {
+      IdeTreeNode *node = ide_tree_model_get_node (self, &iter);
+
+      ide_extension_set_adapter_foreach (self->addins,
+                                         ide_tree_model_propagate_node_collapsed_cb,
+                                         node);
+    }
+}
+
+/**
+ * ide_tree_model_get_tree:
+ * @self: a #IdeTreeModel
+ *
+ * Returns: (transfer none): an #IdeTree
+ *
+ * Since: 3.32
+ */
+IdeTree *
+ide_tree_model_get_tree (IdeTreeModel *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_MODEL (self), NULL);
+
+  return self->tree;
+}
+
+static void
+ide_tree_model_cell_data_func_cb (IdeExtensionSetAdapter *set,
+                                  PeasPluginInfo         *plugin_info,
+                                  PeasExtension          *exten,
+                                  gpointer                user_data)
+{
+  struct {
+    IdeTreeNode     *node;
+    GtkCellRenderer *cell;
+  } *state = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (exten));
+  g_assert (state != NULL);
+  g_assert (IDE_IS_TREE_NODE (state->node));
+  g_assert (GTK_IS_CELL_RENDERER (state->cell));
+
+  ide_tree_addin_cell_data_func (IDE_TREE_ADDIN (exten), state->node, state->cell);
+}
+
+void
+_ide_tree_model_cell_data_func (IdeTreeModel    *self,
+                                GtkTreeIter     *iter,
+                                GtkCellRenderer *cell)
+{
+  struct {
+    IdeTreeNode     *node;
+    GtkCellRenderer *cell;
+  } state;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+  g_return_if_fail (iter != NULL);
+  g_return_if_fail (GTK_IS_CELL_RENDERER (cell));
+
+  state.node = iter->user_data;
+  state.cell = cell;
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_tree_model_cell_data_func_cb,
+                                     &state);
+}
+
+static void
+ide_tree_model_row_draggable_cb (IdeExtensionSetAdapter *set,
+                                 PeasPluginInfo         *plugin_info,
+                                 PeasExtension          *exten,
+                                 gpointer                user_data)
+{
+  struct {
+    IdeTreeNode *node;
+    gboolean     draggable;
+  } *state = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (exten));
+  g_assert (state != NULL);
+  g_assert (IDE_IS_TREE_NODE (state->node));
+
+  state->draggable |= ide_tree_addin_node_draggable (IDE_TREE_ADDIN (exten), state->node);
+}
+
+static gboolean
+ide_tree_model_row_draggable (GtkTreeDragSource *source,
+                              GtkTreePath       *path)
+{
+  IdeTreeModel *self = (IdeTreeModel *)source;
+  GtkTreeIter iter;
+  struct {
+    IdeTreeNode *node;
+    gboolean     draggable;
+  } state;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_MODEL (self));
+
+  if (!ide_tree_model_get_iter (GTK_TREE_MODEL (source), &iter, path))
+    return FALSE;
+
+  if (!IDE_IS_TREE_NODE (iter.user_data))
+    return FALSE;
+
+  state.node = iter.user_data;
+  state.draggable = FALSE;
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_tree_model_row_draggable_cb,
+                                     &state);
+
+  return state.draggable;
+}
+
+static gboolean
+ide_tree_model_drag_data_get (GtkTreeDragSource *source,
+                              GtkTreePath       *path,
+                              GtkSelectionData  *selection)
+{
+  IdeTreeModel *self = (IdeTreeModel *)source;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (path != NULL);
+  g_assert (selection != NULL);
+
+  return gtk_tree_set_row_drag_data (selection, GTK_TREE_MODEL (self), path);
+}
+
+static gboolean
+ide_tree_model_drag_data_delete (GtkTreeDragSource *source,
+                                 GtkTreePath       *path)
+{
+  IdeTreeModel *self = (IdeTreeModel *)source;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (path != NULL);
+
+  return FALSE;
+}
+
+static void
+tree_drag_source_iface_init (GtkTreeDragSourceIface *iface)
+{
+  iface->row_draggable = ide_tree_model_row_draggable;
+  iface->drag_data_get = ide_tree_model_drag_data_get;
+  iface->drag_data_delete = ide_tree_model_drag_data_delete;
+}
+
+static void
+ide_tree_model_drag_data_received_addin_cb (GObject      *object,
+                                            GAsyncResult *result,
+                                            gpointer      user_data)
+{
+  IdeTreeAddin *addin = (IdeTreeAddin *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  DragDataReceived *state;
+
+  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));
+
+  if (!ide_tree_addin_node_dropped_finish (addin, result, &error))
+    {
+      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
+        g_warning ("%s: %s", G_OBJECT_TYPE_NAME (addin), error->message);
+    }
+
+  state = ide_task_get_task_data (task);
+  g_assert (state != NULL);
+  g_assert (!state->drag_node || IDE_IS_TREE_NODE (state->drag_node));
+  g_assert (!state->drop_node || IDE_IS_TREE_NODE (state->drop_node));
+  g_assert (state->n_active > 0);
+
+  state->n_active--;
+
+  if (state->n_active == 0)
+    ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_tree_model_drag_data_received_cb (IdeExtensionSetAdapter *set,
+                                      PeasPluginInfo         *plugin_info,
+                                      PeasExtension          *exten,
+                                      gpointer                user_data)
+{
+  IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+  IdeTask *task = user_data;
+  DragDataReceived *state;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TASK (task));
+
+  state = ide_task_get_task_data (task);
+  g_assert (state != NULL);
+  g_assert (!state->drag_node || IDE_IS_TREE_NODE (state->drag_node));
+  g_assert (!state->drop_node || IDE_IS_TREE_NODE (state->drop_node));
+
+  state->n_active++;
+
+  ide_tree_addin_node_dropped_async (addin,
+                                     state->drag_node,
+                                     state->drop_node,
+                                     state->selection,
+                                     state->actions,
+                                     NULL,
+                                     ide_tree_model_drag_data_received_addin_cb,
+                                     g_object_ref (task));
+}
+
+static gboolean
+ide_tree_model_drag_data_received (GtkTreeDragDest  *dest,
+                                   GtkTreePath      *path,
+                                   GtkSelectionData *selection)
+{
+  IdeTreeModel *self = (IdeTreeModel *)dest;
+  g_autoptr(GtkTreePath) source_path = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  GtkTreeModel *source_model = NULL;
+  DragDataReceived *state;
+  IdeTreeNode *drag_node = NULL;
+  IdeTreeNode *drop_node = NULL;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (path != NULL);
+  g_assert (selection != NULL);
+
+  if (gtk_tree_get_row_drag_data (selection, &source_model, &source_path))
+    {
+      if (IDE_IS_TREE_MODEL (source_model))
+        {
+          if (ide_tree_model_get_iter (source_model, &iter, source_path))
+            drag_node = IDE_TREE_NODE (iter.user_data);
+        }
+    }
+
+  drop_node = _ide_tree_get_drop_node (self->tree);
+
+  state = g_slice_new0 (DragDataReceived);
+  g_set_object (&state->drag_node, drag_node);
+  g_set_object (&state->drop_node, drop_node);
+  state->selection = gtk_selection_data_copy (selection);
+  state->actions = _ide_tree_get_drop_actions (self->tree);
+
+
+  task = ide_task_new (self, NULL, NULL, NULL);
+  ide_task_set_source_tag (task, ide_tree_model_drag_data_received);
+  ide_task_set_task_data (task, state, drag_data_received_free);
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_tree_model_drag_data_received_cb,
+                                     task);
+
+  if (state->n_active == 0)
+    ide_task_return_boolean (task, TRUE);
+
+  return TRUE;
+}
+
+static void
+ide_tree_model_row_drop_possible_cb (IdeExtensionSetAdapter *set,
+                                     PeasPluginInfo         *plugin_info,
+                                     PeasExtension          *exten,
+                                     gpointer                user_data)
+{
+  struct {
+    IdeTreeNode      *drag_node;
+    IdeTreeNode      *drop_node;
+    GtkSelectionData *selection;
+    gboolean          drop_possible;
+  } *state = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (exten));
+  g_assert (state != NULL);
+  g_assert (state->selection != NULL);
+
+  state->drop_possible |= ide_tree_addin_node_droppable (IDE_TREE_ADDIN (exten),
+                                                         state->drag_node,
+                                                         state->drop_node,
+                                                         state->selection);
+}
+
+static gboolean
+ide_tree_model_row_drop_possible (GtkTreeDragDest  *dest,
+                                  GtkTreePath      *path,
+                                  GtkSelectionData *selection)
+{
+  IdeTreeModel *self = (IdeTreeModel *)dest;
+  g_autoptr(GtkTreePath) source_path = NULL;
+  GtkTreeModel *source_model = NULL;
+  IdeTreeNode *drag_node = NULL;
+  IdeTreeNode *drop_node = NULL;
+  GtkTreeIter iter = {0};
+  struct {
+    IdeTreeNode      *drag_node;
+    IdeTreeNode      *drop_node;
+    GtkSelectionData *selection;
+    gboolean          drop_possible;
+  } state;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (path != NULL);
+  g_assert (selection != NULL);
+
+  if (gtk_tree_get_row_drag_data (selection, &source_model, &source_path))
+    {
+      if (IDE_IS_TREE_MODEL (source_model))
+        {
+          if (ide_tree_model_get_iter (source_model, &iter, source_path))
+            drag_node = IDE_TREE_NODE (iter.user_data);
+        }
+    }
+
+  if (ide_tree_model_get_iter (GTK_TREE_MODEL (self), &iter, path))
+    {
+      drop_node = IDE_TREE_NODE (iter.user_data);
+    }
+  else
+    {
+      g_autoptr(GtkTreePath) copy = gtk_tree_path_copy (path);
+
+      gtk_tree_path_up (copy);
+
+      if (ide_tree_model_get_iter (GTK_TREE_MODEL (self), &iter, copy))
+        drop_node = IDE_TREE_NODE (iter.user_data);
+    }
+
+  state.drag_node = drag_node;
+  state.drop_node = drop_node;
+  state.selection = selection;
+  state.drop_possible = FALSE;
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_tree_model_row_drop_possible_cb,
+                                     &state);
+
+  return state.drop_possible;
+}
+
+static void
+tree_drag_dest_iface_init (GtkTreeDragDestIface *iface)
+{
+  iface->drag_data_received = ide_tree_model_drag_data_received;
+  iface->row_drop_possible = ide_tree_model_row_drop_possible;
+}
diff --git a/src/libide/tree/ide-tree-model.h b/src/libide/tree/ide-tree-model.h
new file mode 100644
index 000000000..5590b8943
--- /dev/null
+++ b/src/libide/tree/ide-tree-model.h
@@ -0,0 +1,72 @@
+/* ide-tree-model.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+#include "ide-tree.h"
+#include "ide-tree-node.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TREE_MODEL (ide_tree_model_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeTreeModel, ide_tree_model, IDE, TREE_MODEL, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeTree      *ide_tree_model_get_tree          (IdeTreeModel *self);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode  *ide_tree_model_get_root          (IdeTreeModel *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_tree_model_set_root          (IdeTreeModel *self,
+                                                IdeTreeNode  *root);
+IDE_AVAILABLE_IN_3_32
+const gchar  *ide_tree_model_get_kind          (IdeTreeModel *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_tree_model_set_kind          (IdeTreeModel *self,
+                                                const gchar  *kind);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode  *ide_tree_model_get_node          (IdeTreeModel *self,
+                                                GtkTreeIter  *iter);
+IDE_AVAILABLE_IN_3_32
+GtkTreePath  *ide_tree_model_get_path_for_node (IdeTreeModel *self,
+                                                IdeTreeNode  *node);
+IDE_AVAILABLE_IN_3_32
+gboolean      ide_tree_model_get_iter_for_node (IdeTreeModel *self,
+                                                GtkTreeIter  *iter,
+                                                IdeTreeNode  *node);
+IDE_AVAILABLE_IN_3_32
+void          ide_tree_model_invalidate        (IdeTreeModel *self,
+                                                IdeTreeNode  *node);
+IDE_AVAILABLE_IN_3_32
+void          ide_tree_model_expand_async      (IdeTreeModel         *self,
+                                                IdeTreeNode          *node,
+                                                GCancellable         *cancellable,
+                                                GAsyncReadyCallback   callback,
+                                                gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean      ide_tree_model_expand_finish     (IdeTreeModel         *self,
+                                                GAsyncResult         *result,
+                                                GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/tree/ide-tree-node.c b/src/libide/tree/ide-tree-node.c
new file mode 100644
index 000000000..aeda3c25b
--- /dev/null
+++ b/src/libide/tree/ide-tree-node.c
@@ -0,0 +1,1863 @@
+/* ide-tree-node.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-tree-node"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-tree-model.h"
+#include "ide-tree-node.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.
+ *
+ * Since: 3.32
+ */
+
+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;
+
+  /* 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;
+
+  /* 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;
+
+  /* If the node maybe has children */
+  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 reset_on_collapse : 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_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_ICON,
+  PROP_ICON_NAME,
+  PROP_IS_HEADER,
+  PROP_ITEM,
+  PROP_RESET_ON_COLLAPSE,
+  PROP_TAG,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static IdeTreeModel *
+ide_tree_node_get_model (IdeTreeNode *self)
+{
+  return ide_tree_node_get_root (self)->model;
+}
+
+/**
+ * ide_tree_node_new:
+ *
+ * Create a new #IdeTreeNode.
+ *
+ * Returns: (transfer full): a newly created #IdeTreeNode
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_new (void)
+{
+  return g_object_new (IDE_TYPE_TREE_NODE, NULL);
+}
+
+static void
+ide_tree_node_emit_changed (IdeTreeNode *self)
+{
+  g_autoptr(GtkTreePath) path = NULL;
+  IdeTreeModel *model;
+  GtkTreeIter iter = { .user_data = self };
+
+  g_assert (IDE_IS_TREE_NODE (self));
+
+  if (!(model = ide_tree_node_get_model (self)))
+    return;
+
+  if ((path = ide_tree_model_get_path_for_node (model, self)))
+    gtk_tree_model_row_changed (GTK_TREE_MODEL (model), path, &iter);
+}
+
+static void
+ide_tree_node_remove_with_dispose (IdeTreeNode *self,
+                                   IdeTreeNode *child)
+{
+  g_object_ref (child);
+  ide_tree_node_remove (self, child);
+  g_object_run_dispose (G_OBJECT (child));
+  g_object_unref (child);
+}
+
+static void
+ide_tree_node_dispose (GObject *object)
+{
+  IdeTreeNode *self = (IdeTreeNode *)object;
+
+  while (self->children.length > 0)
+    ide_tree_node_remove_with_dispose (self, g_queue_peek_nth (&self->children, 0));
+
+  if (self->destroy_item && IDE_IS_OBJECT (self->item))
+    ide_clear_and_destroy_object (&self->item);
+  else
+    g_clear_object (&self->item);
+
+  g_list_free_full (self->emblems, g_object_unref);
+  self->emblems = NULL;
+
+  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);
+}
+
+static void
+ide_tree_node_finalize (GObject *object)
+{
+  IdeTreeNode *self = (IdeTreeNode *)object;
+
+  g_clear_weak_pointer (&self->model);
+
+  g_assert (self->children.head == NULL);
+  g_assert (self->children.tail == NULL);
+  g_assert (self->children.length == 0);
+
+  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)->finalize (object);
+}
+
+static void
+ide_tree_node_get_property (GObject    *object,
+                            guint       prop_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
+{
+  IdeTreeNode *self = IDE_TREE_NODE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CHILDREN_POSSIBLE:
+      g_value_set_boolean (value, ide_tree_node_get_children_possible (self));
+      break;
+
+    case PROP_DESTROY_ITEM:
+      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));
+      break;
+
+    case PROP_ICON:
+      g_value_set_object (value, ide_tree_node_get_icon (self));
+      break;
+
+    case PROP_IS_HEADER:
+      g_value_set_boolean (value, ide_tree_node_get_is_header (self));
+      break;
+
+    case PROP_ITEM:
+      g_value_set_object (value, ide_tree_node_get_item (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));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_tree_node_set_property (GObject      *object,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  IdeTreeNode *self = IDE_TREE_NODE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CHILDREN_POSSIBLE:
+      ide_tree_node_set_children_possible (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_DESTROY_ITEM:
+      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;
+
+    case PROP_EXPANDED_ICON_NAME:
+      ide_tree_node_set_expanded_icon_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_ICON:
+      ide_tree_node_set_icon (self, g_value_get_object (value));
+      break;
+
+    case PROP_ICON_NAME:
+      ide_tree_node_set_icon_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_IS_HEADER:
+      ide_tree_node_set_is_header (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_ITEM:
+      ide_tree_node_set_item (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));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+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.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_CHILDREN_POSSIBLE] =
+    g_param_spec_boolean ("children-possible",
+                          "Children Possible",
+                          "If children are possible for the node",
+                          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.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_DESTROY_ITEM] =
+    g_param_spec_boolean ("destroy-item",
+                          "Destroy Item",
+                          "If the item should be destroyed with the node.",
+                          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.
+   *
+   * Since: 3.32
+   */
+  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.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_EXPANDED_ICON] =
+    g_param_spec_object ("expanded-icon",
+                         "Expanded Icon",
+                         "The expanded icon to display in the tree",
+                         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.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_EXPANDED_ICON_NAME] =
+    g_param_spec_string ("expanded-icon-name",
+                         "Expanded Icon Name",
+                         "The expanded icon-name for the GIcon",
+                         NULL,
+                         (G_PARAM_WRITABLE | 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.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ICON] =
+    g_param_spec_object ("icon",
+                         "Icon",
+                         "The icon to display in the tree",
+                         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.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         "Icon Name",
+                         "The icon-name for the GIcon",
+                         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.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_IS_HEADER] =
+    g_param_spec_boolean ("is-header",
+                          "Is Header",
+                          "If the node is a header",
+                          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.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ITEM] =
+    g_param_spec_object ("item",
+                         "Item",
+                         "Item",
+                         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.
+   *
+   * Since: 3.32
+   */
+  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.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_TAG] =
+    g_param_spec_string ("tag",
+                         "Tag",
+                         "The tag for the node if any",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_tree_node_init (IdeTreeNode *self)
+{
+  self->reset_on_collapse = TRUE;
+  self->link.data = self;
+}
+
+/**
+ * ide_tree_node_get_display_name:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the #IdeTreeNode:display-name property.
+ *
+ * Returns: (nullable): a string containing the display name
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_tree_node_get_display_name (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  return self->display_name;
+}
+
+/**
+ * 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.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_display_name (IdeTreeNode *self,
+                                const gchar *display_name)
+{
+  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]);
+    }
+}
+
+/**
+ * ide_tree_node_get_icon:
+ * @self: a #IdeTree
+ *
+ * Gets the icon associated with the tree node.
+ *
+ * Returns: (transfer none) (nullable): a #GIcon or %NULL
+ *
+ * Since: 3.32
+ */
+GIcon *
+ide_tree_node_get_icon (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  return self->icon;
+}
+
+/**
+ * ide_tree_node_set_icon:
+ * @self: a @IdeTreeNode
+ * @icon: (nullable): a #GIcon or %NULL
+ *
+ * Sets the icon for the tree node.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_icon (IdeTreeNode *self,
+                        GIcon       *icon)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  if (g_set_object (&self->icon, icon))
+    {
+      ide_tree_node_emit_changed (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON]);
+    }
+}
+
+/**
+ * ide_tree_node_get_expanded_icon:
+ * @self: a #IdeTree
+ *
+ * Gets the expanded icon associated with the tree node.
+ *
+ * Returns: (transfer none) (nullable): a #GIcon or %NULL
+ *
+ * Since: 3.32
+ */
+GIcon *
+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;
+}
+
+/**
+ * ide_tree_node_set_expanded_icon:
+ * @self: a @IdeTreeNode
+ * @expanded_icon: (nullable): a #GIcon or %NULL
+ *
+ * Sets the expanded icon for the tree node.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_expanded_icon (IdeTreeNode *self,
+                                 GIcon       *expanded_icon)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  if (g_set_object (&self->expanded_icon, expanded_icon))
+    {
+      ide_tree_node_emit_changed (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_EXPANDED_ICON]);
+    }
+}
+
+/**
+ * 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.
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_tree_node_get_item (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);
+
+  return self->item;
+}
+
+void
+ide_tree_node_set_item (IdeTreeNode *self,
+                        gpointer     item)
+{
+  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))
+    {
+      ide_tree_node_emit_changed (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ITEM]);
+    }
+}
+
+static IdeTreeNodeVisit
+ide_tree_node_row_inserted_traverse_cb (IdeTreeNode *node,
+                                        gpointer     user_data)
+{
+  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)))
+    {
+      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);
+            }
+        }
+    }
+
+  return IDE_TREE_NODE_VISIT_CHILDREN;
+}
+
+static void
+ide_tree_node_row_inserted (IdeTreeNode *self,
+                            IdeTreeNode *child)
+{
+  g_autoptr(GtkTreePath) path = NULL;
+  IdeTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_NODE (self));
+  g_assert (IDE_IS_TREE_NODE (child));
+
+  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;
+
+  ide_tree_node_traverse (child,
+                          G_PRE_ORDER,
+                          G_TRAVERSE_ALL,
+                          -1,
+                          ide_tree_node_row_inserted_traverse_cb,
+                          model);
+}
+
+void
+_ide_tree_node_set_model (IdeTreeNode  *self,
+                          IdeTreeModel *model)
+{
+  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))
+    {
+      if (self->model != NULL)
+        ide_tree_node_row_inserted (self, self);
+    }
+}
+
+/**
+ * ide_tree_node_prepend:
+ * @self: a #IdeTreeNode
+ * @child: a #IdeTreeNode
+ *
+ * Prepends @child as a child of @self at the 0 index.
+ *
+ * This operation is O(1).
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_prepend (IdeTreeNode *self,
+                       IdeTreeNode *child)
+{
+  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);
+
+  ide_tree_node_row_inserted (self, child);
+}
+
+/**
+ * ide_tree_node_append:
+ * @self: a #IdeTreeNode
+ * @child: a #IdeTreeNode
+ *
+ * Appends @child as a child of @self at the last position.
+ *
+ * This operation is O(1).
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_append (IdeTreeNode *self,
+                      IdeTreeNode *child)
+{
+  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_tail_link (&self->children, &child->link);
+
+  ide_tree_node_row_inserted (self, child);
+}
+
+/**
+ * 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).
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_insert_before (IdeTreeNode *self,
+                             IdeTreeNode *child)
+{
+  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_before_link (&self->parent->children, &self->link, &child->link);
+
+  ide_tree_node_row_inserted (self, child);
+}
+
+/**
+ * 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).
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_insert_after (IdeTreeNode *self,
+                            IdeTreeNode *child)
+{
+  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);
+
+  ide_tree_node_row_inserted (self, child);
+}
+
+/**
+ * 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).
+ *
+ * Since: 3.32
+ */
+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_get_parent:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the parent node of @self.
+ *
+ * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_get_parent (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  return self->parent;
+}
+
+/**
+ * ide_tree_node_get_root:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the root #IdeTreeNode by following the #IdeTreeNode:parent
+ * properties of each node.
+ *
+ * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_get_root (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
+ *
+ * Since: 3.32
+ */
+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.
+ *
+ * Since: 3.32
+ */
+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;
+}
+
+/**
+ * ide_tree_node_get_nth_child:
+ * @self: a #IdeTreeNode
+ * @index_: the index of the child
+ *
+ * Gets the @nth child of the tree node or %NULL if it does not exist.
+ *
+ * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_get_nth_child (IdeTreeNode *self,
+                             guint        index_)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  return g_queue_peek_nth (&self->children, index_);
+}
+
+/**
+ * ide_tree_node_get_next:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the next sibling after @self.
+ *
+ * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_get_next (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  if (self->link.next)
+    return self->link.next->data;
+
+  return NULL;
+}
+
+/**
+ * ide_tree_node_get_previous:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the previous sibling before @self.
+ *
+ * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_get_previous (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  if (self->link.prev)
+    return self->link.prev->data;
+
+  return NULL;
+}
+
+/**
+ * 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
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_get_children_possible (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return self->children_possible;
+}
+
+/**
+ * ide_tree_node_set_children_possible:
+ * @self: a #IdeTreeNode
+ * @children_possible: if children are possible
+ *
+ * Sets if the children are possible for the node.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_children_possible (IdeTreeNode *self,
+                                     gboolean     children_possible)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  children_possible = !!children_possible;
+
+  if (children_possible != self->children_possible)
+    {
+      self->children_possible = children_possible;
+      self->needs_build_children = children_possible;
+
+      if (self->children_possible && self->children.length == 0)
+        {
+          g_autoptr(IdeTreeNode) child = NULL;
+
+          child = g_object_new (IDE_TYPE_TREE_NODE,
+                                "display-name", _("(Empty)"),
+                                NULL);
+          child->is_empty = TRUE;
+          ide_tree_node_append (self, child);
+
+          g_assert (ide_tree_node_has_child (self) == children_possible);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHILDREN_POSSIBLE]);
+    }
+}
+
+/**
+ * ide_tree_node_has_child:
+ * @self: a #IdeTreeNode
+ *
+ * Checks if @self has any children.
+ *
+ * Returns: %TRUE if @self has one or more children.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_has_child (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return self->children.length > 0;
+}
+
+/**
+ * ide_tree_node_get_n_children:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the number of children that @self contains.
+ *
+ * Returns: the number of children
+ *
+ * Since: 3.32
+ */
+guint
+ide_tree_node_get_n_children (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), 0);
+
+  return self->children.length;
+}
+
+/**
+ * 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.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_get_is_header (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return self->is_header;
+}
+
+/**
+ * ide_tree_node_set_is_header:
+ * @self: a #IdeTreeNode
+ *
+ * Sets the #IdeTreeNode:is-header property.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_is_header (IdeTreeNode *self,
+                             gboolean     is_header)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  is_header = !!is_header;
+
+  if (self->is_header != is_header)
+    {
+      self->is_header = is_header;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IS_HEADER]);
+    }
+}
+
+typedef struct
+{
+  GTraverseType       type;
+  GTraverseFlags      flags;
+  gint                depth;
+  IdeTreeTraverseFunc callback;
+  gpointer            user_data;
+} IdeTreeTraversal;
+
+static inline gboolean
+can_callback_node (IdeTreeNode    *node,
+                   GTraverseFlags  flags)
+{
+  return ((flags & G_TRAVERSE_LEAVES) && node->children.length == 0) ||
+         ((flags & G_TRAVERSE_NON_LEAVES) && node->children.length > 0);
+}
+
+static gboolean
+do_traversal (IdeTreeNode      *node,
+              IdeTreeTraversal *traversal)
+{
+  const GList *iter;
+  IdeTreeNodeVisit ret = IDE_TREE_NODE_VISIT_BREAK;
+
+  if (traversal->depth < 0)
+    return IDE_TREE_NODE_VISIT_CONTINUE;
+
+  traversal->depth--;
+
+  if (traversal->type == G_PRE_ORDER && can_callback_node (node, traversal->flags))
+    {
+      ret = traversal->callback (node, traversal->user_data);
+
+      if (!ide_tree_node_is_root (node) &&
+          (ret == IDE_TREE_NODE_VISIT_CONTINUE || ret == IDE_TREE_NODE_VISIT_BREAK))
+        goto finish;
+    }
+
+  iter = node->children.head;
+
+  while (iter != NULL)
+    {
+      IdeTreeNode *child = iter->data;
+
+      iter = iter->next;
+
+      ret = do_traversal (child, traversal);
+
+      if (ret == IDE_TREE_NODE_VISIT_BREAK)
+        goto finish;
+    }
+
+  if (traversal->type == G_POST_ORDER && can_callback_node (node, traversal->flags))
+    ret = traversal->callback (node, traversal->user_data);
+
+finish:
+  traversal->depth++;
+
+  return ret;
+}
+
+/**
+ * ide_tree_node_traverse:
+ * @self: a #IdeTreeNode
+ * @traverse_type: the type of traversal, pre and post supported
+ * @traverse_flags: the flags for what nodes to match
+ * @max_depth: the max depth for the traversal or -1 for all
+ * @traverse_func: (scope call): the callback for each matching node
+ * @user_data: user data for @traverse_func
+ *
+ * Calls @traverse_func for each node that matches the requested
+ * type, flags, and depth.
+ *
+ * Traversal is stopped if @traverse_func returns %TRUE.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_traverse (IdeTreeNode         *self,
+                        GTraverseType        traverse_type,
+                        GTraverseFlags       traverse_flags,
+                        gint                 max_depth,
+                        IdeTreeTraverseFunc  traverse_func,
+                        gpointer             user_data)
+{
+  IdeTreeTraversal traverse;
+
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+  g_return_if_fail (traverse_type == G_PRE_ORDER ||
+                    traverse_type == G_POST_ORDER);
+  g_return_if_fail (traverse_func != NULL);
+
+  traverse.type = traverse_type;
+  traverse.flags = traverse_flags;
+  traverse.depth = max_depth < 0 ? G_MAXINT : max_depth;
+  traverse.callback = traverse_func;
+  traverse.user_data = user_data;
+
+  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.
+ *
+ * Since: 3.32
+ */
+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.
+ *
+ * Since: 3.32
+ */
+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.
+ *
+ * Since: 3.32
+ */
+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.
+ *
+ * Since: 3.32
+ */
+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
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_is_first (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return self->link.prev == NULL;
+}
+
+/**
+ * ide_tree_node_is_last:
+ * @self: a #IdeTreeNode
+ *
+ * Checks if @self is the last sibling.
+ *
+ * Returns: %TRUE if @self is the last sibling
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_is_last (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return self->link.next == NULL;
+}
+
+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)
+{
+  ide_tree_node_dump_internal (self, 0);
+}
+
+gboolean
+_ide_tree_node_get_loading (IdeTreeNode *self,
+                            gint64      *started_loading_at)
+{
+  g_assert (IDE_IS_TREE_NODE (self));
+  g_assert (started_loading_at != NULL);
+
+  *started_loading_at = self->started_loading_at;
+
+  return self->is_loading;
+}
+
+void
+_ide_tree_node_set_loading (IdeTreeNode *self,
+                            gboolean     loading)
+{
+  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;
+
+      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_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  iter = self->children.head;
+
+  while (iter != NULL)
+    {
+      IdeTreeNode *child = iter->data;
+      iter = iter->next;
+      ide_tree_node_remove (self, child);
+    }
+
+  if (ide_tree_node_get_children_possible (self))
+    {
+      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);
+    }
+}
+
+/**
+ * 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
+ *
+ * Since: 3.32
+ */
+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.
+ *
+ * Since: 3.32
+ */
+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)
+    {
+      self->reset_on_collapse = reset_on_collapse;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RESET_ON_COLLAPSE]);
+    }
+}
+
+/**
+ * ide_tree_node_get_path:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the path for the tree node.
+ *
+ * Returns: (transfer full) (nullable): a path or %NULL
+ *
+ * Since: 3.32
+ */
+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;
+}
+
+static 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)
+{
+  PopupRequest *popreq = data;
+  GdkRectangle rect;
+  GtkAllocation alloc;
+
+  g_assert (popreq);
+  g_assert (IDE_IS_TREE_NODE (popreq->self));
+  g_assert (GTK_IS_POPOVER (popreq->popover));
+
+  ide_tree_node_get_area (popreq->self, popreq->tree, &rect);
+  gtk_widget_get_allocation (GTK_WIDGET (popreq->tree), &alloc);
+
+  if ((rect.x + rect.width) > (alloc.x + alloc.width))
+    rect.width = (alloc.x + alloc.width) - rect.x;
+
+  /* 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;
+    }
+
+  gtk_popover_set_relative_to (popreq->popover, GTK_WIDGET (popreq->tree));
+  gtk_popover_set_pointing_to (popreq->popover, &rect);
+  gtk_popover_popup (popreq->popover);
+
+  g_clear_object (&popreq->self);
+  g_clear_object (&popreq->popover);
+  g_slice_free (PopupRequest, popreq);
+
+  return G_SOURCE_REMOVE;
+}
+
+void
+_ide_tree_node_show_popover (IdeTreeNode *self,
+                             IdeTree     *tree,
+                             GtkPopover  *popover)
+{
+  GdkRectangle cell_area;
+  GdkRectangle visible_rect;
+  PopupRequest *popreq;
+
+  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);
+
+  /*
+   * If the node is not on screen, we need to animate until we get there.
+   */
+  if ((cell_area.y < visible_rect.y) ||
+      ((cell_area.y + cell_area.height) >
+       (visible_rect.y + visible_rect.height)))
+    {
+      GtkTreePath *path;
+
+      path = ide_tree_node_get_path (self);
+      gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (tree), path, NULL, FALSE, 0, 0);
+      g_clear_pointer (&path, gtk_tree_path_free);
+
+      /*
+       * FIXME: Time period comes from gtk animation duration.
+       *        Not curently available in pubic API.
+       *        We need to be greater than the max timeout it
+       *        could take to move, since we must have it
+       *        on screen by then.
+       *
+       *        One alternative might be to check the result
+       *        and if we are still not on screen, then just
+       *        pin it to a row-height from the top or bottom.
+       */
+      g_timeout_add (300,
+                     ide_tree_node_show_popover_timeout_cb,
+                     popreq);
+
+      return;
+    }
+
+  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);
+
+  return self->tag;
+}
+
+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))
+    {
+      g_free (self->tag);
+      self->tag = g_strdup (tag);
+    }
+}
+
+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)
+    {
+      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));
+    }
+
+  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;
+
+  if (background_rgba)
+    self->background = *background_rgba;
+
+  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 (!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));
+    }
+
+  g_object_set (cell, "attributes", attrs, NULL);
+  g_clear_pointer (&attrs, pango_attr_list_unref);
+}
+
+gboolean
+ide_tree_node_is_selected (IdeTreeNode *self)
+{
+  g_autoptr(GtkTreePath) path = NULL;
+  GtkTreeSelection *selection;
+  IdeTreeModel *model;
+  IdeTree *tree;
+
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  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);
+
+  return FALSE;
+}
diff --git a/src/libide/tree/ide-tree-node.h b/src/libide/tree/ide-tree-node.h
new file mode 100644
index 000000000..2a0339dd2
--- /dev/null
+++ b/src/libide/tree/ide-tree-node.h
@@ -0,0 +1,172 @@
+/* ide-tree-node.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TREE_NODE (ide_tree_node_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeTreeNode, ide_tree_node, IDE, TREE_NODE, GObject)
+
+typedef enum
+{
+  IDE_TREE_NODE_VISIT_BREAK    = 0,
+  IDE_TREE_NODE_VISIT_CONTINUE = 0x1,
+  IDE_TREE_NODE_VISIT_CHILDREN = 0x3,
+} IdeTreeNodeVisit;
+
+/**
+ * IdeTreeTraverseFunc:
+ * @node: an #IdeTreeNode
+ * @user_data: closure data provided to ide_tree_node_traverse()
+ *
+ * This function prototype is used to traverse a tree of #IdeTreeNode.
+ *
+ * Returns: #IdeTreeNodeVisit, %IDE_TREE_NODE_VISIT_BREAK to stop traversal.
+ *
+ * Since: 3.32
+ */
+typedef IdeTreeNodeVisit (*IdeTreeTraverseFunc) (IdeTreeNode *node,
+                                                 gpointer     user_data);
+
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode   *ide_tree_node_new                    (void);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_tree_node_get_tag                (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_tag                (IdeTreeNode         *self,
+                                                     const gchar         *tag);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_is_tag                 (IdeTreeNode         *self,
+                                                     const gchar         *tag);
+IDE_AVAILABLE_IN_3_32
+GtkTreePath   *ide_tree_node_get_path               (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_tree_node_get_display_name       (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_display_name       (IdeTreeNode         *self,
+                                                     const gchar         *display_name);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_get_is_header          (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_is_header          (IdeTreeNode         *self,
+                                                     gboolean             header);
+IDE_AVAILABLE_IN_3_32
+GIcon         *ide_tree_node_get_icon               (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_icon               (IdeTreeNode         *self,
+                                                     GIcon               *icon);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_icon_name          (IdeTreeNode         *self,
+                                                     const gchar         *icon_name);
+IDE_AVAILABLE_IN_3_32
+GIcon         *ide_tree_node_get_expanded_icon      (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_expanded_icon      (IdeTreeNode         *self,
+                                                     GIcon               *expanded_icon);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_expanded_icon_name (IdeTreeNode         *self,
+                                                     const gchar         *expanded_icon_name);
+IDE_AVAILABLE_IN_3_32
+gpointer       ide_tree_node_get_item               (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_item               (IdeTreeNode         *self,
+                                                     gpointer             item);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_get_children_possible  (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_children_possible  (IdeTreeNode         *self,
+                                                     gboolean             children_possible);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_is_empty               (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_has_child              (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+guint          ide_tree_node_get_n_children         (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode   *ide_tree_node_get_next               (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode   *ide_tree_node_get_previous           (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+guint          ide_tree_node_get_index              (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode   *ide_tree_node_get_nth_child          (IdeTreeNode         *self,
+                                                     guint                index_);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_prepend                (IdeTreeNode         *self,
+                                                     IdeTreeNode         *child);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_append                 (IdeTreeNode         *self,
+                                                     IdeTreeNode         *child);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_insert_before          (IdeTreeNode         *self,
+                                                     IdeTreeNode         *child);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_insert_after           (IdeTreeNode         *self,
+                                                     IdeTreeNode         *child);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_remove                 (IdeTreeNode         *self,
+                                                     IdeTreeNode         *child);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode   *ide_tree_node_get_parent             (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_is_root                (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_is_first               (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_is_last                (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode   *ide_tree_node_get_root               (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_holds                  (IdeTreeNode         *self,
+                                                     GType                type);
+IDE_AVAILABLE_IN_3_32
+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_3_32
+void           ide_tree_node_add_emblem             (IdeTreeNode         *self,
+                                                     GEmblem             *emblem);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_get_reset_on_collapse  (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_reset_on_collapse  (IdeTreeNode         *self,
+                                                     gboolean             reset_on_collapse);
+IDE_AVAILABLE_IN_3_32
+const GdkRGBA *ide_tree_node_get_background_rgba    (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_background_rgba    (IdeTreeNode         *self,
+                                                     const GdkRGBA       *background_rgba);
+IDE_AVAILABLE_IN_3_32
+const GdkRGBA *ide_tree_node_get_foreground_rgba    (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_foreground_rgba    (IdeTreeNode         *self,
+                                                     const GdkRGBA       *foreground_rgba);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_is_selected            (IdeTreeNode         *self);
+
+G_END_DECLS
diff --git a/src/libide/tree/ide-tree-private.h b/src/libide/tree/ide-tree-private.h
new file mode 100644
index 000000000..77968a522
--- /dev/null
+++ b/src/libide/tree/ide-tree-private.h
@@ -0,0 +1,70 @@
+/* ide-tree-private.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "ide-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);
+
+G_END_DECLS
diff --git a/src/libide/tree/ide-tree.c b/src/libide/tree/ide-tree.c
new file mode 100644
index 000000000..24d557da1
--- /dev/null
+++ b/src/libide/tree/ide-tree.c
@@ -0,0 +1,764 @@
+/* ide-tree.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-tree"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-core.h>
+#include <libide-threading.h>
+
+#include "ide-tree.h"
+#include "ide-tree-model.h"
+#include "ide-tree-node.h"
+#include "ide-tree-private.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;
+
+  /* The context menu to use for popups */
+  GMenu *context_menu;
+
+  /* Our context menu popover */
+  GtkPopover *popover;
+
+  /* Stashed drop information to propagate on drop */
+  GdkDragAction drop_action;
+  GtkTreePath *drop_path;
+  GtkTreeViewDropPosition drop_pos;
+} IdeTreePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeTree, ide_tree, GTK_TYPE_TREE_VIEW)
+
+static IdeTreeModel *
+ide_tree_get_model (IdeTree *self)
+{
+  GtkTreeModel *model;
+
+  g_assert (IDE_IS_TREE (self));
+
+  if (!(model = gtk_tree_view_get_model (GTK_TREE_VIEW (self))) ||
+      !IDE_IS_TREE_MODEL (model))
+    return NULL;
+
+  return IDE_TREE_MODEL (model);
+}
+
+static void
+ide_tree_selection_changed_cb (IdeTree          *self,
+                               GtkTreeSelection *selection)
+{
+  IdeTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_TREE (self));
+  g_assert (GTK_IS_TREE_SELECTION (selection));
+
+  if (!(model = ide_tree_get_model (self)))
+    return;
+
+  if (gtk_tree_selection_get_selected (selection, NULL, &iter))
+    _ide_tree_model_selection_changed (model, &iter);
+  else
+    _ide_tree_model_selection_changed (model, NULL);
+}
+
+static void
+ide_tree_unselect (IdeTree *self)
+{
+  g_assert (IDE_IS_TREE (self));
+
+  gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (GTK_TREE_VIEW (self)));
+}
+
+static void
+ide_tree_select (IdeTree     *self,
+                 IdeTreeNode *node)
+{
+  g_autoptr(GtkTreePath) path = NULL;
+  GtkTreeSelection *selection;
+
+  g_assert (IDE_IS_TREE (self));
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  ide_tree_unselect (self);
+
+  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self));
+  path = ide_tree_node_get_path (node);
+  gtk_tree_selection_select_path (selection, path);
+}
+
+static void
+text_cell_func (GtkCellLayout   *layout,
+                GtkCellRenderer *cell,
+                GtkTreeModel    *model,
+                GtkTreeIter     *iter,
+                gpointer         user_data)
+{
+  IdeTree *self = user_data;
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  const gchar *display_name = NULL;
+  IdeTreeNode *node;
+
+  g_assert (IDE_IS_TREE (self));
+  g_assert (IDE_IS_TREE_MODEL (model));
+
+  g_object_set (cell,
+                "attributes", NULL,
+                "foreground-set", FALSE,
+                NULL);
+
+  if (!(node = ide_tree_model_get_node (IDE_TREE_MODEL (model), iter)))
+    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;
+        }
+    }
+
+  /* Only apply styling if the node isn't selected */
+  if (!ide_tree_node_is_selected (node))
+    {
+      if (ide_tree_node_get_is_header (node))
+        g_object_set (cell, "attributes", priv->header_attributes, NULL);
+      else if (ide_tree_node_is_empty (node))
+        g_object_set (cell, "attributes", priv->dim_label_attributes, NULL);
+    }
+
+  display_name = ide_tree_node_get_display_name (node);
+
+set_props:
+  g_object_set (cell,
+                "text", display_name,
+                NULL);
+}
+
+static void
+pixbuf_cell_func (GtkCellLayout   *layout,
+                  GtkCellRenderer *cell,
+                  GtkTreeModel    *model,
+                  GtkTreeIter     *iter,
+                  gpointer         user_data)
+{
+  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));
+
+  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);
+}
+
+static void
+ide_tree_expand_cb (GObject      *object,
+                    GAsyncResult *result,
+                    gpointer      user_data)
+{
+  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);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_tree_row_activated (GtkTreeView       *tree_view,
+                        GtkTreePath       *path,
+                        GtkTreeViewColumn *column)
+{
+  IdeTree *self = (IdeTree *)tree_view;
+  IdeTreeModel *model;
+  IdeTreeNode *node;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_TREE (self));
+  g_assert (path != NULL);
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (column));
+
+  /* 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;
+
+  if (!_ide_tree_model_row_activated (model, self, path))
+    {
+      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);
+    }
+}
+
+static void
+ide_tree_row_expanded (GtkTreeView *tree_view,
+                       GtkTreeIter *iter,
+                       GtkTreePath *path)
+{
+  IdeTree *self = (IdeTree *)tree_view;
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+  IdeTreeModel *model;
+  IdeTreeNode *node;
+
+  g_assert (IDE_IS_TREE (tree_view));
+  g_assert (iter != NULL);
+  g_assert (IDE_IS_TREE_NODE (iter->user_data));
+  g_assert (path != NULL);
+
+  if (!(model = ide_tree_get_model (self)) ||
+      !(node = ide_tree_model_get_node (model, iter)) ||
+      !ide_tree_node_get_children_possible (node))
+    return;
+
+  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);
+
+  /* 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));
+}
+
+static void
+ide_tree_row_collapsed (GtkTreeView *tree_view,
+                        GtkTreeIter *iter,
+                        GtkTreePath *path)
+{
+  IdeTree *self = (IdeTree *)tree_view;
+  IdeTreeModel *model;
+  IdeTreeNode *node;
+
+  g_assert (IDE_IS_TREE (self));
+  g_assert (iter != NULL);
+  g_assert (path != NULL);
+
+  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);
+
+  _ide_tree_model_row_collapsed (model, self, path);
+}
+
+static void
+ide_tree_popup (IdeTree        *self,
+                IdeTreeNode    *node,
+                GdkEventButton *event,
+                gint            target_x,
+                gint            target_y)
+{
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  const GdkRectangle area = { target_x, target_y, 0, 0 };
+  GtkTextDirection dir;
+
+  g_assert (IDE_IS_TREE (self));
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  if (priv->context_menu == NULL)
+    return;
+
+  dir = gtk_widget_get_direction (GTK_WIDGET (self));
+
+  if (priv->popover == NULL)
+    {
+      priv->popover = GTK_POPOVER (gtk_popover_new_from_model (GTK_WIDGET (self),
+                                                               G_MENU_MODEL (priv->context_menu)));
+      g_signal_connect (priv->popover,
+                        "destroy",
+                        G_CALLBACK (gtk_widget_destroyed),
+                        &priv->popover);
+    }
+
+  gtk_popover_set_pointing_to (priv->popover, &area);
+  gtk_popover_set_position (priv->popover, dir == GTK_TEXT_DIR_LTR ? GTK_POS_RIGHT : GTK_POS_LEFT);
+
+  ide_tree_show_popover_at_node (self, node, priv->popover);
+}
+
+static gboolean
+ide_tree_button_press_event (GtkWidget      *widget,
+                             GdkEventButton *event)
+{
+  IdeTree *self = (IdeTree *)widget;
+  IdeTreeModel *model;
+
+  g_assert (IDE_IS_TREE (self));
+  g_assert (event != NULL);
+
+  if ((model = ide_tree_get_model (self)) &&
+      (event->type == GDK_BUTTON_PRESS) &&
+      (event->button == GDK_BUTTON_SECONDARY))
+    {
+      g_autoptr(GtkTreePath) path = NULL;
+      gint cell_y;
+
+      if (!gtk_widget_has_focus (GTK_WIDGET (self)))
+        gtk_widget_grab_focus (GTK_WIDGET (self));
+
+      gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (self),
+                                     event->x,
+                                     event->y,
+                                     &path,
+                                     NULL,
+                                     NULL,
+                                     &cell_y);
+
+      if (path == NULL)
+        {
+          ide_tree_unselect (self);
+        }
+      else
+        {
+          GtkAllocation alloc;
+          GtkTreeIter iter;
+
+          gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+          if (gtk_tree_model_get_iter (GTK_TREE_MODEL (model), &iter, path))
+            {
+              IdeTreeNode *node;
+
+              node = ide_tree_model_get_node (IDE_TREE_MODEL (model), &iter);
+              ide_tree_select (self, node);
+              ide_tree_popup (self, node, event, alloc.x + alloc.width, event->y - cell_y);
+            }
+        }
+
+      return GDK_EVENT_STOP;
+    }
+
+  return GTK_WIDGET_CLASS (ide_tree_parent_class)->button_press_event (widget, event);
+}
+
+static gboolean
+ide_tree_drag_motion (GtkWidget      *widget,
+                      GdkDragContext *context,
+                      gint            x,
+                      gint            y,
+                      guint           time_)
+{
+  IdeTree *self = (IdeTree *)widget;
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  gboolean ret;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE (self));
+  g_assert (context != NULL);
+
+  ret = GTK_WIDGET_CLASS (ide_tree_parent_class)->drag_motion (widget, context, x, y, time_);
+
+  /*
+   * Cache the current drop position so we can use it
+   * later to determine how to drop on a given node.
+   */
+  g_clear_pointer (&priv->drop_path, gtk_tree_path_free);
+  gtk_tree_view_get_drag_dest_row (GTK_TREE_VIEW (self), &priv->drop_path, &priv->drop_pos);
+
+  /* Save the drag action for builders dispatch */
+  priv->drop_action = gdk_drag_context_get_selected_action (context);
+
+  return ret;
+}
+
+static void
+ide_tree_destroy (GtkWidget *widget)
+{
+  IdeTree *self = (IdeTree *)widget;
+  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);
+
+  if (priv->popover != NULL)
+    gtk_widget_destroy (GTK_WIDGET (priv->popover));
+
+  gtk_tree_view_set_model (GTK_TREE_VIEW (self), NULL);
+
+  g_cancellable_cancel (priv->cancellable);
+  g_clear_object (&priv->cancellable);
+
+  g_clear_object (&priv->context_menu);
+
+  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);
+
+  GTK_WIDGET_CLASS (ide_tree_parent_class)->destroy (widget);
+}
+
+static void
+ide_tree_class_init (IdeTreeClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
+
+  widget_class->destroy = ide_tree_destroy;
+  widget_class->button_press_event = ide_tree_button_press_event;
+  widget_class->drag_motion = ide_tree_drag_motion;
+
+  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;
+}
+
+static void
+ide_tree_init (IdeTree *self)
+{
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  GtkCellRenderer *cell;
+  GtkTreeViewColumn *column;
+
+  priv->cancellable = g_cancellable_new ();
+
+  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 ();
+  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 = gtk_cell_renderer_text_new ();
+  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_tree_view_append_column (GTK_TREE_VIEW (self), column);
+
+  priv->dim_label_attributes = pango_attr_list_new ();
+  pango_attr_list_insert (priv->dim_label_attributes,
+                          pango_attr_foreground_alpha_new (65535 * 0.55));
+
+  priv->header_attributes = pango_attr_list_new ();
+  pango_attr_list_insert (priv->header_attributes,
+                          pango_attr_weight_new (PANGO_WEIGHT_BOLD));
+}
+
+GtkWidget *
+ide_tree_new (void)
+{
+  return g_object_new (IDE_TYPE_TREE, NULL);
+}
+
+void
+ide_tree_set_context_menu (IdeTree *self,
+                           GMenu   *menu)
+{
+  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));
+
+  if (g_set_object (&priv->context_menu, menu))
+    {
+      if (priv->popover != NULL)
+        gtk_widget_destroy (GTK_WIDGET (priv->popover));
+    }
+
+  g_return_if_fail (priv->popover == NULL);
+}
+
+void
+ide_tree_show_popover_at_node (IdeTree     *self,
+                               IdeTreeNode *node,
+                               GtkPopover  *popover)
+{
+  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));
+
+  _ide_tree_node_show_popover (node, self, popover);
+}
+
+/**
+ * ide_tree_get_selected_node:
+ * @self: a #IdeTree
+ *
+ * Gets the currently selected node, or %NULL
+ *
+ * Returns: (transfer none) (nullable): an #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_get_selected_node (IdeTree *self)
+{
+  GtkTreeSelection *selection;
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  g_return_val_if_fail (IDE_IS_TREE (self), NULL);
+
+  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self));
+
+  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);
+
+  return NULL;
+}
+
+void
+ide_tree_select_node (IdeTree     *self,
+                      IdeTreeNode *node)
+{
+  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);
+}
+
+static void
+ide_tree_expand_node_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  IdeTreeModel *model = (IdeTreeModel *)object;
+  g_autoptr(IdeTask) task = user_data;
+
+  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;
+      IdeTreeNode *node;
+      IdeTree *self;
+
+      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_node_get_path (node)))
+        gtk_tree_view_expand_row (GTK_TREE_VIEW (self), path, FALSE);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+void
+ide_tree_expand_node (IdeTree     *self,
+                      IdeTreeNode *node)
+{
+  g_autoptr(IdeTask) task = NULL;
+  IdeTreeModel *model;
+
+  g_return_if_fail (IDE_IS_TREE (self));
+
+  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);
+
+  ide_tree_model_expand_async (model,
+                               node,
+                               NULL,
+                               ide_tree_expand_node_cb,
+                               g_steal_pointer (&task));
+}
+
+gboolean
+ide_tree_node_expanded (IdeTree     *self,
+                        IdeTreeNode *node)
+{
+  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);
+
+  if (node == NULL)
+    return FALSE;
+
+  if (!(path = ide_tree_node_get_path (node)))
+    return FALSE;
+
+  return gtk_tree_view_row_expanded (GTK_TREE_VIEW (self), path);
+}
+
+void
+ide_tree_collapse_node (IdeTree     *self,
+                        IdeTreeNode *node)
+{
+  IdeTreeModel *model;
+  g_autoptr(GtkTreePath) path = NULL;
+
+  g_return_if_fail (IDE_IS_TREE (self));
+
+  if (!(model = ide_tree_get_model (self)))
+    return;
+
+  if ((path = ide_tree_node_get_path (node)))
+    gtk_tree_view_collapse_row (GTK_TREE_VIEW (self), path);
+}
+
+GdkDragAction
+_ide_tree_get_drop_actions (IdeTree *self)
+{
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TREE (self), 0);
+
+  return priv->drop_action;
+}
+
+IdeTreeNode *
+_ide_tree_get_drop_node (IdeTree *self)
+{
+  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;
+    }
+
+  return NULL;
+}
diff --git a/src/libide/tree/ide-tree.h b/src/libide/tree/ide-tree.h
new file mode 100644
index 000000000..0b4c16b6e
--- /dev/null
+++ b/src/libide/tree/ide-tree.h
@@ -0,0 +1,67 @@
+/* ide-tree.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+#include "ide-tree-node.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TREE (ide_tree_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeTree, ide_tree, IDE, TREE, GtkTreeView)
+
+struct _IdeTreeClass
+{
+  GtkTreeViewClass parent_type;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget   *ide_tree_new                  (void);
+IDE_AVAILABLE_IN_3_32
+void         ide_tree_set_context_menu     (IdeTree     *self,
+                                            GMenu       *menu);
+IDE_AVAILABLE_IN_3_32
+void         ide_tree_show_popover_at_node (IdeTree     *self,
+                                            IdeTreeNode *node,
+                                            GtkPopover  *popover);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode *ide_tree_get_selected_node    (IdeTree     *self);
+IDE_AVAILABLE_IN_3_32
+void         ide_tree_select_node          (IdeTree     *self,
+                                            IdeTreeNode *node);
+IDE_AVAILABLE_IN_3_32
+void         ide_tree_expand_node          (IdeTree     *self,
+                                            IdeTreeNode *node);
+IDE_AVAILABLE_IN_3_32
+void         ide_tree_collapse_node        (IdeTree     *self,
+                                            IdeTreeNode *node);
+IDE_AVAILABLE_IN_3_32
+gboolean     ide_tree_node_expanded        (IdeTree     *self,
+                                            IdeTreeNode *node);
+
+G_END_DECLS
diff --git a/src/libide/tree/libide-tree.h b/src/libide/tree/libide-tree.h
new file mode 100644
index 000000000..515828d11
--- /dev/null
+++ b/src/libide/tree/libide-tree.h
@@ -0,0 +1,36 @@
+/* libide-tree.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TREE_INSIDE
+
+#include "ide-tree.h"
+#include "ide-tree-addin.h"
+#include "ide-tree-model.h"
+#include "ide-tree-node.h"
+
+#undef IDE_TREE_INSIDE
+
+G_END_DECLS
diff --git a/src/libide/tree/meson.build b/src/libide/tree/meson.build
new file mode 100644
index 000000000..8e47f196d
--- /dev/null
+++ b/src/libide/tree/meson.build
@@ -0,0 +1,62 @@
+libide_tree_header_subdir = join_paths(libide_header_subdir, 'tree')
+libide_include_directories += include_directories('.')
+
+#
+# Public API Headers
+#
+
+libide_tree_public_headers = [
+  'ide-tree.h',
+  'ide-tree-addin.h',
+  'ide-tree-model.h',
+  'ide-tree-node.h',
+  'libide-tree.h',
+]
+
+install_headers(libide_tree_public_headers, subdir: libide_tree_header_subdir)
+
+#
+# Sources
+#
+
+libide_tree_public_sources = [
+  'ide-tree.c',
+  'ide-tree-addin.c',
+  'ide-tree-model.c',
+  'ide-tree-node.c',
+]
+
+libide_tree_sources = libide_tree_public_sources
+
+#
+# Dependencies
+#
+
+libide_tree_deps = [
+  libgtk_dep,
+  libpeas_dep,
+
+  libide_core_dep,
+  libide_plugins_dep,
+  libide_threading_dep,
+]
+
+#
+# Library Definitions
+#
+
+libide_tree = static_library('ide-tree-' + libide_api_version, libide_tree_sources,
+   dependencies: libide_tree_deps,
+         c_args: libide_args + release_args + ['-DIDE_TREE_COMPILATION'],
+)
+
+libide_tree_dep = declare_dependency(
+         dependencies: libide_tree_deps,
+           link_whole: libide_tree,
+  include_directories: include_directories('.'),
+)
+
+gnome_builder_public_sources += files(libide_tree_public_sources)
+gnome_builder_public_headers += files(libide_tree_public_headers)
+gnome_builder_include_subdirs += libide_tree_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-tree.h', '-DIDE_TREE_COMPILATION']


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