[gnome-builder/wip/chergert/perspective] project-tree: start extracting project-tree into plugin



commit 0e62fba554d90c067cc76299724c7c59ad89d8ce
Author: Christian Hergert <chergert redhat com>
Date:   Wed Nov 11 01:58:17 2015 -0800

    project-tree: start extracting project-tree into plugin

 configure.ac                                   |    2 +
 plugins/project-tree/Makefile.am               |   57 ++
 plugins/project-tree/configure.ac              |   12 +
 plugins/project-tree/gb-new-file-popover.c     |  384 ++++++++++
 plugins/project-tree/gb-new-file-popover.h     |   39 +
 plugins/project-tree/gb-project-file.c         |  273 +++++++
 plugins/project-tree/gb-project-file.h         |   48 ++
 plugins/project-tree/gb-project-tree-actions.c |  944 ++++++++++++++++++++++++
 plugins/project-tree/gb-project-tree-actions.h |   31 +
 plugins/project-tree/gb-project-tree-addin.c   |   69 ++
 plugins/project-tree/gb-project-tree-addin.h   |   32 +
 plugins/project-tree/gb-project-tree-builder.c |  422 +++++++++++
 plugins/project-tree/gb-project-tree-builder.h |   37 +
 plugins/project-tree/gb-project-tree-private.h |   38 +
 plugins/project-tree/gb-project-tree.c         |  212 ++++++
 plugins/project-tree/gb-project-tree.h         |   42 ++
 plugins/project-tree/gb-rename-file-popover.c  |  362 +++++++++
 plugins/project-tree/gb-rename-file-popover.h  |   35 +
 plugins/project-tree/project-tree-plugin.c     |   30 +
 plugins/project-tree/project-tree.plugin       |    8 +
 20 files changed, 3077 insertions(+), 0 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 8b9dc5a..e488738 100644
--- a/configure.ac
+++ b/configure.ac
@@ -244,6 +244,7 @@ m4_include([plugins/html-completion/configure.ac])
 m4_include([plugins/html-preview/configure.ac])
 m4_include([plugins/jedi/configure.ac])
 m4_include([plugins/mingw/configure.ac])
+m4_include([plugins/project-tree/configure.ac])
 m4_include([plugins/python-gi-imports-completion/configure.ac])
 m4_include([plugins/python-pack/configure.ac])
 m4_include([plugins/symbol-tree/configure.ac])
@@ -494,6 +495,7 @@ echo "  GNOME Code Assistance ................ : ${enable_gnome_code_assistance_
 echo "  Global File Search ................... : ${enable_file_search_plugin}"
 echo "  HTML Autocompletion .................. : ${enable_html_completion_plugin}"
 echo "  HTML and Markdown Preview ............ : ${enable_html_preview_plugin}"
+echo "  Project Tree ......................... : ${enable_project_tree_plugin}"
 echo "  Python GObject Introspection ......... : ${enable_python_gi_imports_completion_plugin}"
 echo "  Python Jedi Autocompletion ........... : ${enable_jedi_plugin}"
 echo "  Python Language Pack ................. : ${enable_python_pack_plugin}"
diff --git a/plugins/project-tree/Makefile.am b/plugins/project-tree/Makefile.am
new file mode 100644
index 0000000..cf09162
--- /dev/null
+++ b/plugins/project-tree/Makefile.am
@@ -0,0 +1,57 @@
+if ENABLE_DEVHELP_PLUGIN
+
+DISTCLEANFILES =
+BUILT_SOURCES =
+CLEANFILES =
+EXTRA_DIST = $(plugin_DATA)
+
+plugindir = $(libdir)/gnome-builder/plugins
+plugin_LTLIBRARIES = libproject-tree-plugin.la
+dist_plugin_DATA = project-tree.plugin
+
+libproject_tree_plugin_la_SOURCES = \
+       gb-new-file-popover.c \
+       gb-new-file-popover.h \
+       gb-project-file.c \
+       gb-project-file.h \
+       gb-project-tree-actions.c \
+       gb-project-tree-actions.h \
+       gb-project-tree-builder.c \
+       gb-project-tree-builder.h \
+       gb-project-tree.c \
+       gb-project-tree.h \
+       gb-project-tree-private.h \
+       gb-rename-file-popover.c \
+       gb-rename-file-popover.h \
+       gb-project-tree-addin.c \
+       gb-project-tree-addin.h \
+       project-tree-plugin.c \
+       $(NULL)
+
+libproject_tree_plugin_la_CFLAGS = \
+       $(LIBIDE_CFLAGS) \
+       $(BUILDER_CFLAGS) \
+       $(OPTIMIZE_CFLAGS) \
+       $(DEBUG_CFLAGS) \
+       -I$(top_srcdir)/libide \
+       -I$(top_srcdir)/src/tree \
+       -I$(top_srcdir)/src/util \
+       $(NULL)
+
+libproject_tree_plugin_la_LIBADD = \
+       $(LIBIDE_LIBS) \
+       $(BUILDER_LIBS) \
+       $(top_builddir)/libide/libide-1.0.la \
+       $(NULL)
+
+libproject_tree_plugin_la_LDFLAGS = \
+       $(OPTIMIZE_LDFLAGS) \
+       -avoid-version \
+       -module \
+       $(NULL)
+
+include $(top_srcdir)/plugins/Makefile.plugin
+
+endif
+
+-include $(top_srcdir)/git.mk
diff --git a/plugins/project-tree/configure.ac b/plugins/project-tree/configure.ac
new file mode 100644
index 0000000..ec37e7c
--- /dev/null
+++ b/plugins/project-tree/configure.ac
@@ -0,0 +1,12 @@
+# --enable-project-tree-plugin=yes/no
+AC_ARG_ENABLE([project-tree-plugin],
+              [AS_HELP_STRING([--enable-project-tree-plugin=@<:@yes/no@:>@],
+                              [Build with support for the project tree.])],
+              [enable_project_tree_plugin=$enableval],
+              [enable_project_tree_plugin=yes])
+
+# for if ENABLE_PROJECT_TREE_PLUGIN in Makefile.am
+AM_CONDITIONAL(ENABLE_PROJECT_TREE_PLUGIN, test x$enable_project_tree_plugin = xyes)
+
+# Ensure our makefile is generated by autoconf
+AC_CONFIG_FILES([plugins/project-tree/Makefile])
diff --git a/plugins/project-tree/gb-new-file-popover.c b/plugins/project-tree/gb-new-file-popover.c
new file mode 100644
index 0000000..25ece48
--- /dev/null
+++ b/plugins/project-tree/gb-new-file-popover.c
@@ -0,0 +1,384 @@
+/* gb-new-file-popover.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+#include <ide.h>
+
+#include "gb-new-file-popover.h"
+
+struct _GbNewFilePopover
+{
+  GtkPopover    parent_instance;
+
+  GFileType     file_type;
+  GFile        *directory;
+  GCancellable *cancellable;
+
+  GtkButton    *button;
+  GtkEntry     *entry;
+  GtkLabel     *message;
+  GtkLabel     *title;
+};
+
+G_DEFINE_TYPE (GbNewFilePopover, gb_new_file_popover, GTK_TYPE_POPOVER)
+
+enum {
+  PROP_0,
+  PROP_DIRECTORY,
+  PROP_FILE_TYPE,
+  LAST_PROP
+};
+
+enum {
+  CREATE_FILE,
+  LAST_SIGNAL
+};
+
+static GParamSpec *properties [LAST_PROP];
+static guint       signals [LAST_SIGNAL];
+
+static void
+gb_new_file_popover__button_clicked (GbNewFilePopover *self,
+                                     GtkButton        *button)
+{
+  g_autoptr(GFile) file = NULL;
+  const gchar *path;
+
+  g_assert (GB_IS_NEW_FILE_POPOVER (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  if (self->directory == NULL)
+    return;
+
+  path = gtk_entry_get_text (self->entry);
+  if (ide_str_empty0 (path))
+    return;
+
+  file = g_file_get_child (self->directory, path);
+
+  g_signal_emit (self, signals [CREATE_FILE], 0, file, self->file_type);
+}
+
+static void
+gb_new_file_popover__entry_activate (GbNewFilePopover *self,
+                                     GtkEntry         *entry)
+{
+  g_assert (GB_IS_NEW_FILE_POPOVER (self));
+  g_assert (GTK_IS_ENTRY (entry));
+
+  if (gtk_widget_get_sensitive (GTK_WIDGET (self->button)))
+    gtk_widget_activate (GTK_WIDGET (self->button));
+}
+
+static void
+gb_new_file_popover__query_info_cb (GObject      *object,
+                                    GAsyncResult *result,
+                                    gpointer      user_data)
+{
+  GFile *file = (GFile *)object;
+  g_autoptr(GFileInfo) file_info = NULL;
+  g_autoptr(GbNewFilePopover) self = user_data;
+  g_autoptr(GError) error = NULL;
+  GFileType file_type;
+
+  file_info = g_file_query_info_finish (file, result, &error);
+
+  if (file_info == NULL &&
+      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+    return;
+
+  if ((file_info == NULL) &&
+      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+    {
+      gtk_label_set_label (self->message, NULL);
+      gtk_widget_set_sensitive (GTK_WIDGET (self->button), TRUE);
+      return;
+    }
+
+  if (file_info == NULL)
+    {
+      gtk_label_set_label (self->message, error->message);
+      return;
+    }
+
+  file_type = g_file_info_get_file_type (file_info);
+
+  if (file_type == G_FILE_TYPE_DIRECTORY)
+    gtk_label_set_label (self->message,
+                         _("A folder with that name already exists."));
+  else
+    gtk_label_set_label (self->message,
+                         _("A file with that name already exists."));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->button), FALSE);
+}
+
+static void
+gb_new_file_popover_check_exists (GbNewFilePopover *self,
+                                  GFile            *directory,
+                                  const gchar      *path)
+{
+  g_autoptr(GFile) child = NULL;
+
+  g_assert (GB_IS_NEW_FILE_POPOVER (self));
+  g_assert (!directory || G_IS_FILE (directory));
+
+  if (self->cancellable != NULL)
+    {
+      if (!g_cancellable_is_cancelled (self->cancellable))
+        g_cancellable_cancel (self->cancellable);
+      g_clear_object (&self->cancellable);
+    }
+
+  gtk_label_set_label (self->message, NULL);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->button), FALSE);
+
+  if (directory == NULL)
+    return;
+
+  if (ide_str_empty0 (path))
+    return;
+
+  child = g_file_get_child (directory, path);
+
+  self->cancellable = g_cancellable_new ();
+
+  g_file_query_info_async (child,
+                           G_FILE_ATTRIBUTE_STANDARD_TYPE,
+                           G_FILE_QUERY_INFO_NONE,
+                           G_PRIORITY_DEFAULT,
+                           self->cancellable,
+                           gb_new_file_popover__query_info_cb,
+                           g_object_ref (self));
+
+}
+
+static void
+gb_new_file_popover__entry_changed (GbNewFilePopover *self,
+                                    GtkEntry         *entry)
+{
+  const gchar *text;
+
+  g_assert (GB_IS_NEW_FILE_POPOVER (self));
+  g_assert (GTK_IS_ENTRY (entry));
+
+  text = gtk_entry_get_text (entry);
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->button), !ide_str_empty0 (text));
+
+  gb_new_file_popover_check_exists (self, self->directory, text);
+}
+
+static void
+gb_new_file_popover_finalize (GObject *object)
+{
+  GbNewFilePopover *self = (GbNewFilePopover *)object;
+
+  if (self->cancellable && !g_cancellable_is_cancelled (self->cancellable))
+    g_cancellable_cancel (self->cancellable);
+
+  g_clear_object (&self->cancellable);
+  g_clear_object (&self->directory);
+
+  G_OBJECT_CLASS (gb_new_file_popover_parent_class)->finalize (object);
+}
+
+static void
+gb_new_file_popover_get_property (GObject    *object,
+                                  guint       prop_id,
+                                  GValue     *value,
+                                  GParamSpec *pspec)
+{
+  GbNewFilePopover *self = GB_NEW_FILE_POPOVER(object);
+
+  switch (prop_id)
+    {
+    case PROP_DIRECTORY:
+      g_value_set_object (value, gb_new_file_popover_get_directory (self));
+      break;
+
+    case PROP_FILE_TYPE:
+      g_value_set_enum (value, gb_new_file_popover_get_file_type (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+/**
+ * gb_new_file_popover_set_property:
+ * @object: (in): A #GObject.
+ * @prop_id: (in): The property identifier.
+ * @value: (in): The given property.
+ * @pspec: (in): A #ParamSpec.
+ *
+ * Set a given #GObject property.
+ */
+static void
+gb_new_file_popover_set_property (GObject      *object,
+                                  guint         prop_id,
+                                  const GValue *value,
+                                  GParamSpec   *pspec)
+{
+  GbNewFilePopover *self = GB_NEW_FILE_POPOVER(object);
+
+  switch (prop_id)
+    {
+    case PROP_DIRECTORY:
+      gb_new_file_popover_set_directory (self, g_value_get_object (value));
+      break;
+
+    case PROP_FILE_TYPE:
+      gb_new_file_popover_set_file_type (self, g_value_get_enum (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+gb_new_file_popover_class_init (GbNewFilePopoverClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = gb_new_file_popover_finalize;
+  object_class->get_property = gb_new_file_popover_get_property;
+  object_class->set_property = gb_new_file_popover_set_property;
+
+  properties [PROP_DIRECTORY] =
+    g_param_spec_object ("directory",
+                         "Directory",
+                         "Directory",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_FILE_TYPE] =
+    g_param_spec_enum ("file-type",
+                       "File Type",
+                       "The file type to create.",
+                       G_TYPE_FILE_TYPE,
+                       G_FILE_TYPE_REGULAR,
+                       (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  signals [CREATE_FILE] =
+    g_signal_new ("create-file",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_FIRST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  2,
+                  G_TYPE_FILE,
+                  G_TYPE_FILE_TYPE);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/builder/ui/gb-new-file-popover.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbNewFilePopover, button);
+  gtk_widget_class_bind_template_child (widget_class, GbNewFilePopover, entry);
+  gtk_widget_class_bind_template_child (widget_class, GbNewFilePopover, message);
+  gtk_widget_class_bind_template_child (widget_class, GbNewFilePopover, title);
+}
+
+static void
+gb_new_file_popover_init (GbNewFilePopover *self)
+{
+  self->file_type = G_FILE_TYPE_REGULAR;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (self->entry,
+                           "activate",
+                           G_CALLBACK (gb_new_file_popover__entry_activate),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->entry,
+                           "changed",
+                           G_CALLBACK (gb_new_file_popover__entry_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->button,
+                           "clicked",
+                           G_CALLBACK (gb_new_file_popover__button_clicked),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+GFileType
+gb_new_file_popover_get_file_type (GbNewFilePopover *self)
+{
+  g_return_val_if_fail (GB_IS_NEW_FILE_POPOVER (self), 0);
+
+  return self->file_type;
+}
+
+void
+gb_new_file_popover_set_file_type (GbNewFilePopover *self,
+                                   GFileType         file_type)
+{
+  g_return_if_fail (GB_IS_NEW_FILE_POPOVER (self));
+  g_return_if_fail ((file_type == G_FILE_TYPE_REGULAR) ||
+                    (file_type == G_FILE_TYPE_DIRECTORY));
+
+  if (file_type != self->file_type)
+    {
+      self->file_type = file_type;
+
+      if (file_type == G_FILE_TYPE_REGULAR)
+        gtk_label_set_label (self->title, _("File Name"));
+      else
+        gtk_label_set_label (self->title, _("Folder Name"));
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FILE_TYPE]);
+    }
+}
+
+void
+gb_new_file_popover_set_directory (GbNewFilePopover *self,
+                                   GFile            *directory)
+{
+  g_return_if_fail (GB_IS_NEW_FILE_POPOVER (self));
+  g_return_if_fail (G_IS_FILE (directory));
+
+  if (g_set_object (&self->directory, directory))
+    {
+      const gchar *path;
+
+      path = gtk_entry_get_text (self->entry);
+      gb_new_file_popover_check_exists (self, directory, path);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DIRECTORY]);
+    }
+}
+
+/**
+ * gb_new_file_popover_get_directory:
+ *
+ * Returns: (transfer none) (nullable): A #GFile or %NULL.
+ */
+GFile *
+gb_new_file_popover_get_directory (GbNewFilePopover *self)
+{
+  g_return_val_if_fail (GB_IS_NEW_FILE_POPOVER (self), NULL);
+
+  return self->directory;
+}
diff --git a/plugins/project-tree/gb-new-file-popover.h b/plugins/project-tree/gb-new-file-popover.h
new file mode 100644
index 0000000..106cdea
--- /dev/null
+++ b/plugins/project-tree/gb-new-file-popover.h
@@ -0,0 +1,39 @@
+/* gb-new-file-popover.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#ifndef GB_NEW_FILE_POPOVER_H
+#define GB_NEW_FILE_POPOVER_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_NEW_FILE_POPOVER (gb_new_file_popover_get_type())
+
+G_DECLARE_FINAL_TYPE (GbNewFilePopover, gb_new_file_popover, GB, NEW_FILE_POPOVER, GtkPopover)
+
+GFileType  gb_new_file_popover_get_file_type (GbNewFilePopover *self);
+void       gb_new_file_popover_set_file_type (GbNewFilePopover *self,
+                                              GFileType         file_type);
+void       gb_new_file_popover_set_directory (GbNewFilePopover *self,
+                                              GFile            *directory);
+GFile     *gb_new_file_popover_get_directory (GbNewFilePopover *self);
+
+G_END_DECLS
+
+#endif /* GB_NEW_FILE_POPOVER_H */
diff --git a/plugins/project-tree/gb-project-file.c b/plugins/project-tree/gb-project-file.c
new file mode 100644
index 0000000..787adde
--- /dev/null
+++ b/plugins/project-tree/gb-project-file.c
@@ -0,0 +1,273 @@
+/* gb-project-file.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "gb-project-file.h"
+
+struct _GbProjectFile
+{
+  GObject    parent_instance;
+
+  GFile     *file;
+  GFileInfo *file_info;
+};
+
+G_DEFINE_TYPE (GbProjectFile, gb_project_file, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_DISPLAY_NAME,
+  PROP_FILE,
+  PROP_FILE_INFO,
+  PROP_ICON_NAME,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+gint
+gb_project_file_compare (GbProjectFile *a,
+                         GbProjectFile *b)
+{
+  const gchar *display_name_a = g_file_info_get_display_name (a->file_info);
+  const gchar *display_name_b = g_file_info_get_display_name (b->file_info);
+  gchar *casefold_a = NULL;
+  gchar *casefold_b = NULL;
+  gboolean ret;
+
+  casefold_a = g_utf8_collate_key_for_filename (display_name_a, -1);
+  casefold_b = g_utf8_collate_key_for_filename (display_name_b, -1);
+
+  ret = strcmp (casefold_a, casefold_b);
+
+  g_free (casefold_a);
+  g_free (casefold_b);
+
+  return ret;
+}
+
+gint
+gb_project_file_compare_directories_first (GbProjectFile *a,
+                                           GbProjectFile *b)
+{
+  GFileType file_type_a = g_file_info_get_file_type (a->file_info);
+  GFileType file_type_b = g_file_info_get_file_type (b->file_info);
+  gint dir_a = (file_type_a == G_FILE_TYPE_DIRECTORY);
+  gint dir_b = (file_type_b == G_FILE_TYPE_DIRECTORY);
+  gint ret;
+
+  ret = dir_b - dir_a;
+  if (ret == 0)
+    ret = gb_project_file_compare (a, b);
+
+  return ret;
+}
+
+
+static void
+gb_project_file_finalize (GObject *object)
+{
+  GbProjectFile *self = (GbProjectFile *)object;
+
+  g_clear_object (&self->file);
+  g_clear_object (&self->file_info);
+
+  G_OBJECT_CLASS (gb_project_file_parent_class)->finalize (object);
+}
+
+static void
+gb_project_file_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  GbProjectFile *self = GB_PROJECT_FILE (object);
+
+  switch (prop_id)
+    {
+    case PROP_DISPLAY_NAME:
+      g_value_set_string (value, gb_project_file_get_display_name (self));
+      break;
+
+    case PROP_ICON_NAME:
+      g_value_set_static_string (value, gb_project_file_get_icon_name (self));
+      break;
+
+    case PROP_FILE:
+      g_value_set_object (value, gb_project_file_get_file (self));
+      break;
+
+    case PROP_FILE_INFO:
+      g_value_set_object (value, gb_project_file_get_file_info (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_project_file_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  GbProjectFile *self = GB_PROJECT_FILE (object);
+
+  switch (prop_id)
+    {
+    case PROP_FILE:
+      gb_project_file_set_file (self, g_value_get_object (value));
+      break;
+
+    case PROP_FILE_INFO:
+      gb_project_file_set_file_info (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_project_file_class_init (GbProjectFileClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gb_project_file_finalize;
+  object_class->get_property = gb_project_file_get_property;
+  object_class->set_property = gb_project_file_set_property;
+
+  properties [PROP_DISPLAY_NAME] =
+    g_param_spec_string ("display-name",
+                         "Display Name",
+                         "Display Name",
+                         NULL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         "Icon Name",
+                         "Icon Name",
+                         NULL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_FILE] =
+    g_param_spec_object ("file",
+                         "File",
+                         "File",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_FILE_INFO] =
+    g_param_spec_object ("file-info",
+                         "File Info",
+                         "File Info",
+                         G_TYPE_FILE_INFO,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+gb_project_file_init (GbProjectFile *self)
+{
+}
+
+GbProjectFile *
+gb_project_file_new (GFile     *file,
+                     GFileInfo *file_info)
+{
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+  g_return_val_if_fail (G_IS_FILE_INFO (file_info), NULL);
+
+  return g_object_new (GB_TYPE_PROJECT_FILE,
+                       "file", file,
+                       "file-info", file_info,
+                       NULL);
+}
+
+GFile *
+gb_project_file_get_file (GbProjectFile *self)
+{
+  g_return_val_if_fail (GB_IS_PROJECT_FILE (self), NULL);
+
+  return self->file;
+}
+
+void
+gb_project_file_set_file (GbProjectFile *self,
+                          GFile         *file)
+{
+  g_return_if_fail (GB_IS_PROJECT_FILE (self));
+  g_return_if_fail (!file || G_IS_FILE (file));
+
+  if (g_set_object (&self->file, file))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FILE]);
+}
+
+GFileInfo *
+gb_project_file_get_file_info (GbProjectFile *self)
+{
+  g_return_val_if_fail (GB_IS_PROJECT_FILE (self), NULL);
+
+  return self->file_info;
+}
+
+void
+gb_project_file_set_file_info (GbProjectFile *self,
+                               GFileInfo     *file_info)
+{
+  g_return_if_fail (GB_IS_PROJECT_FILE (self));
+  g_return_if_fail (!file_info || G_IS_FILE_INFO (file_info));
+
+  if (g_set_object (&self->file_info, file_info))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FILE_INFO]);
+}
+
+gboolean
+gb_project_file_get_is_directory (GbProjectFile *self)
+{
+  g_return_val_if_fail (GB_IS_PROJECT_FILE (self), FALSE);
+
+  if (self->file_info != NULL)
+    return g_file_info_get_file_type (self->file_info) == G_FILE_TYPE_DIRECTORY;
+
+  return G_FILE_TYPE_UNKNOWN;
+}
+
+const gchar *
+gb_project_file_get_icon_name (GbProjectFile *self)
+{
+  if (gb_project_file_get_is_directory (self))
+    return "folder-symbolic";
+
+  return "text-x-generic-symbolic";
+}
+
+const gchar *
+gb_project_file_get_display_name (GbProjectFile *self)
+{
+  g_return_val_if_fail (GB_IS_PROJECT_FILE (self), NULL);
+
+  if (self->file_info != NULL)
+    return g_file_info_get_display_name (self->file_info);
+
+  return NULL;
+}
diff --git a/plugins/project-tree/gb-project-file.h b/plugins/project-tree/gb-project-file.h
new file mode 100644
index 0000000..a9eebad
--- /dev/null
+++ b/plugins/project-tree/gb-project-file.h
@@ -0,0 +1,48 @@
+/* gb-project-file.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#ifndef GB_PROJECT_FILE_H
+#define GB_PROJECT_FILE_H
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_PROJECT_FILE (gb_project_file_get_type())
+
+G_DECLARE_FINAL_TYPE (GbProjectFile, gb_project_file, GB, PROJECT_FILE, GObject)
+
+GbProjectFile *gb_project_file_new                       (GFile         *directory,
+                                                          GFileInfo     *file_info);
+GFile         *gb_project_file_get_file                  (GbProjectFile *self);
+void           gb_project_file_set_file                  (GbProjectFile *self,
+                                                          GFile         *file);
+GFileInfo     *gb_project_file_get_file_info             (GbProjectFile *self);
+void           gb_project_file_set_file_info             (GbProjectFile *self,
+                                                          GFileInfo     *file_info);
+gboolean       gb_project_file_get_is_directory          (GbProjectFile *self);
+const gchar   *gb_project_file_get_display_name          (GbProjectFile *self);
+const gchar   *gb_project_file_get_icon_name             (GbProjectFile *self);
+gint           gb_project_file_compare_directories_first (GbProjectFile *a,
+                                                          GbProjectFile *b);
+gint           gb_project_file_compare                   (GbProjectFile *a,
+                                                          GbProjectFile *b);
+
+G_END_DECLS
+
+#endif /* GB_PROJECT_FILE_H */
diff --git a/plugins/project-tree/gb-project-tree-actions.c b/plugins/project-tree/gb-project-tree-actions.c
new file mode 100644
index 0000000..aa62505
--- /dev/null
+++ b/plugins/project-tree/gb-project-tree-actions.c
@@ -0,0 +1,944 @@
+/* gb-project-tree-actions.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#define G_LOG_DOMAIN "gb-project-tree-actions"
+
+#include <glib/gi18n.h>
+#include <gio/gdesktopappinfo.h>
+#include <ide.h>
+
+#ifdef HAVE_VTE
+# include <vte/vte.h>
+#endif
+
+#include "gb-file-manager.h"
+#include "gb-new-file-popover.h"
+#include "gb-project-file.h"
+#include "gb-project-tree.h"
+#include "gb-project-tree-actions.h"
+#include "gb-project-tree-private.h"
+#include "gb-rename-file-popover.h"
+
+#if 0
+#include "gb-view-stack.h"
+#endif
+
+#if 0
+static void
+action_set (GActionGroup *group,
+            const gchar  *action_name,
+            const gchar  *first_param,
+            ...)
+{
+  GAction *action;
+  va_list args;
+
+  g_assert (G_IS_ACTION_GROUP (group));
+  g_assert (G_IS_ACTION_MAP (group));
+
+  action = g_action_map_lookup_action (G_ACTION_MAP (group), action_name);
+  g_assert (G_IS_SIMPLE_ACTION (action));
+
+  va_start (args, first_param);
+  g_object_set_valist (G_OBJECT (action), first_param, args);
+  va_end (args);
+}
+
+static gboolean
+project_file_is_directory (GObject *object)
+{
+  g_assert (!object || G_IS_OBJECT (object));
+
+  return (GB_IS_PROJECT_FILE (object) &&
+          gb_project_file_get_is_directory (GB_PROJECT_FILE (object)));
+}
+
+static void
+gb_project_tree_actions_refresh (GSimpleAction *action,
+                                 GVariant      *variant,
+                                 gpointer       user_data)
+{
+  GbProjectTree *self = user_data;
+  GbTreeNode *selected;
+  GObject *item = NULL;
+
+  g_assert (GB_IS_PROJECT_TREE (self));
+
+  if ((selected = gb_tree_get_selected (GB_TREE (self))))
+    {
+      item = gb_tree_node_get_item (selected);
+      if (item != NULL)
+        g_object_ref (item);
+    }
+
+  gb_tree_rebuild (GB_TREE (self));
+
+  if (item != NULL)
+    {
+      selected = gb_tree_find_item (GB_TREE (self), item);
+      if (selected != NULL)
+        {
+          gb_tree_node_expand (selected, TRUE);
+          gb_tree_node_select (selected);
+          gb_tree_scroll_to_node (GB_TREE (self), selected);
+        }
+      g_object_unref (item);
+    }
+}
+
+static void
+gb_project_tree_actions_collapse_all_nodes (GSimpleAction *action,
+                                            GVariant      *variant,
+                                            gpointer       user_data)
+{
+  GbProjectTree *self = user_data;
+
+  g_assert (GB_IS_PROJECT_TREE (self));
+
+  gtk_tree_view_collapse_all (GTK_TREE_VIEW (self));
+}
+
+static void
+gb_project_tree_actions_open (GSimpleAction *action,
+                              GVariant      *variant,
+                              gpointer       user_data)
+{
+#if 0
+  GbProjectTree *self = user_data;
+  IdeWorkbench *workbench;
+  GbTreeNode *selected;
+  GObject *item;
+
+  g_assert (GB_IS_PROJECT_TREE (self));
+
+  workbench = gb_widget_get_workbench (GTK_WIDGET (self));
+  g_assert (GB_IS_WORKBENCH (workbench));
+
+  if (!(selected = gb_tree_get_selected (GB_TREE (self))) ||
+      !(item = gb_tree_node_get_item (selected)))
+    return;
+
+  item = gb_tree_node_get_item (selected);
+
+  if (GB_IS_PROJECT_FILE (item))
+    {
+      GFileInfo *file_info;
+      GFile *file;
+
+      file_info = gb_project_file_get_file_info (GB_PROJECT_FILE (item));
+      if (!file_info)
+        return;
+
+      if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_DIRECTORY)
+        return;
+
+      file = gb_project_file_get_file (GB_PROJECT_FILE (item));
+      if (!file)
+        return;
+
+      ide_workbench_open (workbench, file);
+    }
+#endif
+}
+
+static void
+gb_project_tree_actions_open_with (GSimpleAction *action,
+                                   GVariant      *variant,
+                                   gpointer       user_data)
+{
+#if 0
+  g_autoptr(GDesktopAppInfo) app_info = NULL;
+  g_autoptr(GdkAppLaunchContext) launch_context = NULL;
+  GbProjectTree *self = user_data;
+  GbTreeNode *selected;
+  IdeWorkbench *workbench;
+  GdkDisplay *display;
+  GFileInfo *file_info;
+  GFile *file;
+  const gchar *app_id;
+  GObject *item;
+  GList *files;
+
+  g_assert (GB_IS_PROJECT_TREE (self));
+  g_assert (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING));
+
+  if (!(workbench = gb_widget_get_workbench (GTK_WIDGET (self))) ||
+      !(selected = gb_tree_get_selected (GB_TREE (self))) ||
+      !(item = gb_tree_node_get_item (selected)) ||
+      !GB_IS_PROJECT_FILE (item) ||
+      !(app_id = g_variant_get_string (variant, NULL)) ||
+      !(file_info = gb_project_file_get_file_info (GB_PROJECT_FILE (item))) ||
+      !(file = gb_project_file_get_file (GB_PROJECT_FILE (item))) ||
+      !(app_info = g_desktop_app_info_new (app_id)))
+    return;
+
+  display = gtk_widget_get_display (GTK_WIDGET (self));
+  launch_context = gdk_display_get_app_launch_context (display);
+
+  files = g_list_append (NULL, file);
+  g_app_info_launch (G_APP_INFO (app_info), files, G_APP_LAUNCH_CONTEXT (launch_context), NULL);
+  g_list_free (files);
+#endif
+}
+
+static void
+gb_project_tree_actions_open_with_editor (GSimpleAction *action,
+                                          GVariant      *variant,
+                                          gpointer       user_data)
+{
+#if 0
+  IdeWorkbench *workbench;
+  GbProjectTree *self = user_data;
+  GFileInfo *file_info;
+  GFile *file;
+  GbTreeNode *selected;
+  GObject *item;
+
+  g_assert (GB_IS_PROJECT_TREE (self));
+
+  if (!(selected = gb_tree_get_selected (GB_TREE (self))) ||
+      !(item = gb_tree_node_get_item (selected)) ||
+      !GB_IS_PROJECT_FILE (item) ||
+      !(file_info = gb_project_file_get_file_info (GB_PROJECT_FILE (item))) ||
+      (g_file_info_get_file_type (file_info) == G_FILE_TYPE_DIRECTORY) ||
+      !(file = gb_project_file_get_file (GB_PROJECT_FILE (item))) ||
+      !(workbench = gb_widget_get_workbench (GTK_WIDGET (self))))
+    return;
+
+  ide_workbench_open_with_editor (workbench, file);
+#endif
+}
+
+static void
+gb_project_tree_actions_open_containing_folder (GSimpleAction *action,
+                                                GVariant      *variant,
+                                                gpointer       user_data)
+{
+  GbProjectTree *self = user_data;
+  GbTreeNode *selected;
+  GObject *item;
+  GFile *file;
+
+  g_assert (GB_IS_PROJECT_TREE (self));
+
+  if (!(selected = gb_tree_get_selected (GB_TREE (self))) ||
+      !(item = gb_tree_node_get_item (selected)) ||
+      !GB_IS_PROJECT_FILE (item))
+    return;
+
+  file = gb_project_file_get_file (GB_PROJECT_FILE (item));
+
+  gb_file_manager_show (file, NULL);
+}
+
+/* Based on gdesktopappinfo.c in GIO */
+static gchar *
+find_terminal_executable (void)
+{
+  gsize i;
+  gchar *path = NULL;
+  g_autoptr(GSettings) terminal_settings = NULL;
+  g_autofree gchar *gsettings_terminal = NULL;
+  const gchar *terminals[] = {
+    NULL,                     /* GSettings */
+    "x-terminal-emulator",    /* Debian's alternative system */
+    "gnome-terminal",
+    NULL,                     /* getenv ("TERM") */
+    "nxterm", "color-xterm",
+    "rxvt", "xterm", "dtterm"
+  };
+
+  /* This is deprecated, but at least the user can specify it! */
+  terminal_settings = g_settings_new ("org.gnome.desktop.default-applications.terminal");
+  gsettings_terminal = g_settings_get_string (terminal_settings, "exec");
+  terminals[0] = gsettings_terminal;
+
+  /* This is generally one of the fallback terminals */
+  terminals[3] = g_getenv ("TERM");
+
+  for (i = 0; i < G_N_ELEMENTS (terminals) && path == NULL; ++i)
+    {
+      if (terminals[i] != NULL)
+        {
+          G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+          path = g_find_program_in_path (terminals[i]);
+          G_GNUC_END_IGNORE_DEPRECATIONS
+        }
+    }
+
+  return path;
+}
+
+static void
+gb_project_tree_actions_open_in_terminal (GSimpleAction *action,
+                                          GVariant      *variant,
+                                          gpointer       user_data)
+{
+  GbProjectTree *self = user_data;
+  GbTreeNode *selected;
+  GObject *item;
+  GFile *file;
+  g_autofree gchar *workdir = NULL;
+  g_autofree gchar *terminal_executable = NULL;
+  const gchar *argv[] = { NULL, NULL };
+  g_auto(GStrv) env = NULL;
+  GError *error = NULL;
+
+  g_assert (GB_IS_PROJECT_TREE (self));
+
+  if (!(selected = gb_tree_get_selected (GB_TREE (self))) ||
+      !(item = gb_tree_node_get_item (selected)) ||
+      !GB_IS_PROJECT_FILE (item))
+    return;
+
+  file = gb_project_file_get_file (GB_PROJECT_FILE (item));
+
+  if (!gb_project_file_get_is_directory (GB_PROJECT_FILE (item)))
+    {
+      g_autoptr(GFile) parent;
+
+      parent = g_file_get_parent (file);
+      workdir = g_file_get_path (parent);
+    }
+  else
+    {
+      workdir = g_file_get_path (file);
+    }
+
+  if (workdir == NULL)
+    {
+      g_warning ("Cannot load non-native file in terminal.");
+      return;
+    }
+
+  terminal_executable = find_terminal_executable ();
+  argv[0] = terminal_executable;
+  g_return_if_fail (terminal_executable != NULL);
+
+#ifdef HAVE_VTE
+  {
+    /*
+     * Overwrite SHELL to the users default shell.
+     * Failure to do so typically results in /bin/sh being used.
+     */
+    gchar *shell;
+
+    shell = vte_get_user_shell ();
+    g_setenv ("SHELL", shell, TRUE);
+    g_free (shell);
+  }
+#endif
+
+  env = g_get_environ ();
+
+  /* Can't use GdkAppLaunchContext as
+   * we cannot set the working directory.
+   */
+  if (!g_spawn_async (workdir, (gchar **)argv, env,
+                      G_SPAWN_STDERR_TO_DEV_NULL,
+                      NULL, NULL, NULL, &error))
+    {
+      g_warning ("%s", error->message);
+      g_clear_error (&error);
+      return;
+    }
+}
+
+static void
+gb_project_tree_actions__make_directory_cb (GObject      *object,
+                                            GAsyncResult *result,
+                                            gpointer      user_data)
+{
+  GFile *file = (GFile *)object;
+  g_autoptr(GbTreeNode) node = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (GB_IS_TREE_NODE (node));
+
+  if (!g_file_make_directory_finish (file, result, &error))
+    {
+      /* todo: show error messsage */
+      return;
+    }
+
+  gb_tree_node_invalidate (node);
+  gb_tree_node_expand (node, FALSE);
+  gb_tree_node_select (node);
+}
+
+static void
+gb_project_tree_actions__create_cb (GObject      *object,
+                                    GAsyncResult *result,
+                                    gpointer      user_data)
+{
+  GFile *file = (GFile *)object;
+  g_autoptr(GbTreeNode) node = user_data;
+  g_autoptr(GError) error = NULL;
+  GbProjectTree *self;
+  IdeWorkbench *workbench;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (GB_IS_TREE_NODE (node));
+
+  if (!g_file_create_finish (file, result, &error))
+    {
+      /* todo: show error messsage */
+      return;
+    }
+
+  self = GB_PROJECT_TREE (gb_tree_node_get_tree (node));
+  if (self == NULL)
+    return;
+
+  workbench = gb_widget_get_workbench (GTK_WIDGET (self));
+  if (workbench == NULL)
+    return;
+
+  ide_workbench_open (workbench, file);
+
+  gb_tree_node_invalidate (node);
+  gb_tree_node_expand (node, FALSE);
+  gb_tree_node_select (node);
+}
+
+static void
+gb_project_tree_actions__popover_create_file_cb (GbProjectTree    *self,
+                                                 GFile            *file,
+                                                 GFileType         file_type,
+                                                 GbNewFilePopover *popover)
+{
+  GbTreeNode *selected;
+
+  g_assert (GB_IS_PROJECT_TREE (self));
+  g_assert (G_IS_FILE (file));
+  g_assert ((file_type == G_FILE_TYPE_DIRECTORY) ||
+            (file_type == G_FILE_TYPE_REGULAR));
+  g_assert (GB_IS_NEW_FILE_POPOVER (popover));
+
+  selected = gb_tree_get_selected (GB_TREE (self));
+
+  g_assert (selected != NULL);
+  g_assert (GB_IS_TREE_NODE (selected));
+
+  if (file_type == G_FILE_TYPE_DIRECTORY)
+    {
+      g_file_make_directory_async (file,
+                                   G_PRIORITY_DEFAULT,
+                                   NULL, /* cancellable */
+                                   gb_project_tree_actions__make_directory_cb,
+                                   g_object_ref (selected));
+    }
+  else if (file_type == G_FILE_TYPE_REGULAR)
+    {
+      g_file_create_async (file,
+                           G_FILE_CREATE_NONE,
+                           G_PRIORITY_DEFAULT,
+                           NULL, /* cancellable */
+                           gb_project_tree_actions__create_cb,
+                           g_object_ref (selected));
+    }
+  else
+    {
+      g_assert_not_reached ();
+    }
+
+  self->expanded_in_new = FALSE;
+
+  gtk_widget_hide (GTK_WIDGET (popover));
+  gtk_widget_destroy (GTK_WIDGET (popover));
+}
+
+static void
+gb_project_tree_actions__popover_closed_cb (GbProjectTree *self,
+                                            GtkPopover    *popover)
+{
+  GbTreeNode *selected;
+
+  g_assert (GB_IS_PROJECT_TREE (self));
+  g_assert (GTK_IS_POPOVER (popover));
+
+  if (!(selected = gb_tree_get_selected (GB_TREE (self))) || !self->expanded_in_new)
+    return;
+
+  gb_tree_node_collapse (selected);
+}
+
+static void
+gb_project_tree_actions_new (GbProjectTree *self,
+                             GFileType      file_type)
+{
+  GbTreeNode *selected;
+  GObject *item;
+  GtkPopover *popover;
+  GbProjectFile *project_file;
+  GFile *file = NULL;
+  gboolean is_dir = FALSE;
+
+  g_assert (GB_IS_PROJECT_TREE (self));
+  g_assert ((file_type == G_FILE_TYPE_DIRECTORY) ||
+            (file_type == G_FILE_TYPE_REGULAR));
+
+again:
+  if (!(selected = gb_tree_get_selected (GB_TREE (self))) ||
+      !(item = gb_tree_node_get_item (selected)) ||
+      !GB_IS_PROJECT_FILE (item))
+    return;
+
+  if (!(project_file = GB_PROJECT_FILE (item)) ||
+      !(file = gb_project_file_get_file (project_file)))
+    return;
+
+  is_dir = project_file_is_directory (item);
+
+  g_assert (G_IS_FILE (file));
+
+  /*
+   * If this item is an GbProjectFile and not a directory, then we really
+   * want to create a sibling.
+   */
+  if (!is_dir)
+    {
+      GtkTreePath *path;
+
+      selected = gb_tree_node_get_parent (selected);
+      gb_tree_node_select (selected);
+      path = gb_tree_node_get_path (selected);
+      gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (self), path, NULL, FALSE, 0, 0);
+      gtk_tree_path_free (path);
+
+      goto again;
+    }
+
+  if ((self->expanded_in_new = !gb_tree_node_get_expanded (selected)))
+    gb_tree_node_expand (selected, FALSE);
+
+  popover = g_object_new (GB_TYPE_NEW_FILE_POPOVER,
+                          "directory", file,
+                          "file-type", file_type,
+                          "position", GTK_POS_RIGHT,
+                          NULL);
+  g_signal_connect_object (popover,
+                           "create-file",
+                           G_CALLBACK (gb_project_tree_actions__popover_create_file_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (popover,
+                           "closed",
+                           G_CALLBACK (gb_project_tree_actions__popover_closed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gb_tree_node_show_popover (selected, popover);
+}
+
+static void
+gb_project_tree_actions_new_directory (GSimpleAction *action,
+                                       GVariant      *variant,
+                                       gpointer       user_data)
+{
+  GbProjectTree *self = user_data;
+
+  g_assert (GB_IS_PROJECT_TREE (self));
+
+  gb_project_tree_actions_new (self, G_FILE_TYPE_DIRECTORY);
+}
+
+static void
+gb_project_tree_actions_new_file (GSimpleAction *action,
+                                  GVariant      *variant,
+                                  gpointer       user_data)
+{
+  GbProjectTree *self = user_data;
+
+  g_assert (GB_IS_PROJECT_TREE (self));
+
+  gb_project_tree_actions_new (self, G_FILE_TYPE_REGULAR);
+}
+
+static gboolean
+find_child_node (GbTree     *tree,
+                 GbTreeNode *parent,
+                 GbTreeNode *child,
+                 gpointer    user_data)
+{
+  GObject *item = gb_tree_node_get_item (child);
+  GFile *target = user_data;
+  GFile *child_file;
+
+  if (GB_IS_PROJECT_FILE (item) &&
+      (child_file = gb_project_file_get_file (GB_PROJECT_FILE (item))) &&
+      g_file_equal (child_file, target))
+    return TRUE;
+
+  return FALSE;
+}
+
+static void
+gb_project_tree_actions__project_rename_file_cb (GObject      *object,
+                                                 GAsyncResult *result,
+                                                 gpointer      user_data)
+{
+  IdeProject *project = (IdeProject *)object;
+  g_autoptr(GbRenameFilePopover) popover = user_data;
+  g_autoptr(GError) error = NULL;
+  GbTreeNode *node;
+  GbTreeNode *parent;
+  GFile *file;
+  GbTree *tree;
+
+  g_assert (IDE_IS_PROJECT (project));
+  g_assert (GB_IS_RENAME_FILE_POPOVER (popover));
+
+  if (!ide_project_rename_file_finish (project, result, &error))
+    {
+      /* todo: display error */
+      g_warning ("%s", error->message);
+      return;
+    }
+
+  file = g_object_get_data (G_OBJECT (popover), "G_FILE");
+  tree = GB_TREE (gtk_popover_get_relative_to (GTK_POPOVER (popover)));
+
+  g_assert (G_IS_FILE (file));
+  g_assert (GB_IS_TREE (tree));
+
+  node = gb_tree_get_selected (tree);
+  if (node == NULL)
+    goto cleanup;
+
+  parent = gb_tree_node_get_parent (node);
+
+  gb_tree_node_invalidate (parent);
+  gb_tree_node_expand (parent, FALSE);
+
+  node = gb_tree_find_child_node (tree, parent, find_child_node, file);
+
+  if (node != NULL)
+    gb_tree_node_select (node);
+  else
+    gb_tree_node_select (parent);
+
+cleanup:
+  gtk_widget_hide (GTK_WIDGET (popover));
+  gtk_widget_destroy (GTK_WIDGET (popover));
+}
+
+static void
+gb_project_tree_actions__rename_file_cb (GbProjectTree       *self,
+                                         GFile               *orig_file,
+                                         GFile               *new_file,
+                                         GbRenameFilePopover *popover)
+{
+  IdeWorkbench *workbench;
+  IdeContext *context;
+  IdeProject *project;
+
+  g_assert (GB_IS_PROJECT_TREE (self));
+  g_assert (G_IS_FILE (orig_file));
+  g_assert (G_IS_FILE (new_file));
+  g_assert (GTK_IS_POPOVER (popover));
+
+  workbench = gb_widget_get_workbench (GTK_WIDGET (self));
+  context = ide_workbench_get_context (workbench);
+  project = ide_context_get_project (context);
+
+  /* todo: set busy spinner in popover */
+
+  g_object_set_data_full (G_OBJECT (popover),
+                          "G_FILE",
+                          g_object_ref (new_file),
+                          g_object_unref);
+
+  ide_project_rename_file_async (project, orig_file, new_file, NULL,
+                                 gb_project_tree_actions__project_rename_file_cb,
+                                 g_object_ref (popover));
+}
+
+static void
+gb_project_tree_actions_rename_file (GSimpleAction *action,
+                                     GVariant      *variant,
+                                     gpointer       user_data)
+{
+  GbProjectTree *self = user_data;
+  GbTreeNode *selected;
+  GtkPopover *popover;
+  GObject *item;
+  GFile *file;
+  GFileInfo *file_info;
+  gboolean is_dir;
+
+  g_assert (GB_IS_PROJECT_TREE (self));
+
+  if (!(selected = gb_tree_get_selected (GB_TREE (self))) ||
+      !(item = gb_tree_node_get_item (selected)) ||
+      !GB_IS_PROJECT_FILE (item) ||
+      !(file = gb_project_file_get_file (GB_PROJECT_FILE (item))) ||
+      !(file_info = gb_project_file_get_file_info (GB_PROJECT_FILE (item))))
+    return;
+
+  is_dir = (g_file_info_get_file_type (file_info) == G_FILE_TYPE_DIRECTORY);
+
+  popover = g_object_new (GB_TYPE_RENAME_FILE_POPOVER,
+                          "file", file,
+                          "is-directory", is_dir,
+                          "position", GTK_POS_RIGHT,
+                          NULL);
+  g_signal_connect_object (popover,
+                           "rename-file",
+                           G_CALLBACK (gb_project_tree_actions__rename_file_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gb_tree_node_show_popover (selected, popover);
+}
+
+static void
+gb_project_tree_actions__trash_file_cb (GObject      *object,
+                                        GAsyncResult *result,
+                                        gpointer      user_data)
+{
+  IdeProject *project = (IdeProject *)object;
+  g_autoptr(GbTreeNode) node = user_data;
+  g_autoptr(GError) error = NULL;
+  GbTreeNode *parent;
+
+  g_assert (IDE_IS_PROJECT (project));
+  g_assert (GB_IS_TREE_NODE (node));
+
+  if (!ide_project_trash_file_finish (project, result, &error))
+    {
+      /* todo: warning dialog */
+      g_warning ("%s", error->message);
+      return;
+    }
+
+  if ((parent = gb_tree_node_get_parent (node)))
+    {
+      gb_tree_node_invalidate (parent);
+      gb_tree_node_expand (parent, FALSE);
+      gb_tree_node_select (parent);
+    }
+}
+
+typedef struct
+{
+  GbDocument *document;
+  GList      *views;
+} ViewsRemoval;
+
+static void
+gb_project_tree_actions_close_views_cb (GtkWidget *widget,
+                                        gpointer   user_data)
+{
+  GbDocument *document;
+  ViewsRemoval *removal = user_data;
+  IdeView *view = (IdeView *)widget;
+
+  g_assert (GB_IS_VIEW (view));
+  g_assert (removal != NULL);
+  g_assert (GB_IS_DOCUMENT (removal->document));
+
+  document = gb_view_get_document (view);
+
+  if (document == removal->document)
+    removal->views = g_list_prepend (removal->views, g_object_ref (view));
+}
+
+static void
+gb_project_tree_actions_move_to_trash (GSimpleAction *action,
+                                       GVariant      *param,
+                                       gpointer       user_data)
+{
+  GbProjectTree *self = user_data;
+  IdeBufferManager *buffer_manager;
+  IdeWorkbench *workbench;
+  IdeContext *context;
+  ViewsRemoval removal = { 0 };
+  IdeBuffer *buffer;
+  IdeProject *project;
+  GbTreeNode *node;
+  GFile *file;
+  GObject *item;
+  GList *iter;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GB_IS_PROJECT_TREE (self));
+
+  workbench = gb_widget_get_workbench (GTK_WIDGET (self));
+  context = ide_workbench_get_context (workbench);
+  project = ide_context_get_project (context);
+  buffer_manager = ide_context_get_buffer_manager (context);
+
+  if (!(node = gb_tree_get_selected (GB_TREE (self))) ||
+      !(item = gb_tree_node_get_item (node)) ||
+      !GB_IS_PROJECT_FILE (item) ||
+      !(file = gb_project_file_get_file (GB_PROJECT_FILE (item))))
+    return;
+
+  /*
+   * Find all of the views that contain this file.
+   * We do not close them until we leave the foreach callback.
+   */
+  if ((buffer = ide_buffer_manager_find_buffer (buffer_manager, file)))
+    {
+      removal.document = g_object_ref (buffer);
+      ide_workbench_views_foreach (workbench,
+                                   gb_project_tree_actions_close_views_cb,
+                                   &removal);
+      g_object_unref (removal.document);
+    }
+
+  /*
+   * Close all of the views that match the document.
+   */
+  for (iter = removal.views; iter; iter = iter->next)
+    {
+      GtkWidget *stack;
+
+      stack = gtk_widget_get_ancestor (iter->data, GB_TYPE_VIEW_STACK);
+      if (stack != NULL)
+        gb_view_stack_remove (GB_VIEW_STACK (stack), iter->data);
+    }
+
+  g_list_free_full (removal.views, g_object_unref);
+
+  /*
+   * Now move the file to the trash.
+   */
+  ide_project_trash_file_async (project,
+                                file,
+                                NULL,
+                                gb_project_tree_actions__trash_file_cb,
+                                g_object_ref (node));
+}
+
+static gboolean
+is_files_node (GbTreeNode *node)
+{
+  if (node != NULL)
+    {
+      GObject *item = gb_tree_node_get_item (node);
+      GbTreeNode *parent = gb_tree_node_get_parent (node);
+      GObject *parent_item = gb_tree_node_get_item (parent);
+
+      return (GB_IS_PROJECT_FILE (item) && !GB_IS_PROJECT_FILE (parent_item));
+    }
+
+  return FALSE;
+}
+
+static GActionEntry GbProjectTreeActions[] = {
+  { "collapse-all-nodes",     gb_project_tree_actions_collapse_all_nodes },
+  { "move-to-trash",          gb_project_tree_actions_move_to_trash },
+  { "new-directory",          gb_project_tree_actions_new_directory },
+  { "new-file",               gb_project_tree_actions_new_file },
+  { "open",                   gb_project_tree_actions_open },
+  { "open-containing-folder", gb_project_tree_actions_open_containing_folder },
+  { "open-in-terminal",       gb_project_tree_actions_open_in_terminal },
+  { "open-with",              gb_project_tree_actions_open_with, "s" },
+  { "open-with-editor",       gb_project_tree_actions_open_with_editor },
+  { "refresh",                gb_project_tree_actions_refresh },
+  { "rename-file",            gb_project_tree_actions_rename_file },
+};
+#endif
+
+void
+gb_project_tree_actions_init (GbProjectTree *self)
+{
+#if 0
+  g_autoptr(GSettings) settings = NULL;
+  g_autoptr(GSettings) tree_settings = NULL;
+  g_autoptr(GSimpleActionGroup) actions = NULL;
+  GAction *action;
+
+  actions = g_simple_action_group_new ();
+
+  settings = g_settings_new ("org.gtk.Settings.FileChooser");
+  action = g_settings_create_action (settings, "sort-directories-first");
+  g_action_map_add_action (G_ACTION_MAP (actions), action);
+  g_clear_object (&action);
+
+  g_action_map_add_action_entries (G_ACTION_MAP (actions),
+                                   GbProjectTreeActions,
+                                   G_N_ELEMENTS (GbProjectTreeActions),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self),
+                                  "project-tree",
+                                  G_ACTION_GROUP (actions));
+
+  tree_settings = g_settings_new ("org.gnome.builder.project-tree");
+
+  action = g_settings_create_action (tree_settings, "show-ignored-files");
+  g_action_map_add_action (G_ACTION_MAP (actions), action);
+  g_clear_object (&action);
+
+  action = g_settings_create_action (tree_settings, "show-icons");
+  g_action_map_add_action (G_ACTION_MAP (actions), action);
+  g_clear_object (&action);
+
+  gb_project_tree_actions_update (self);
+#endif
+}
+
+void
+gb_project_tree_actions_update (GbProjectTree *self)
+{
+#if 0
+  GActionGroup *group;
+  GbTreeNode *selection;
+  GObject *item = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (GB_IS_PROJECT_TREE (self));
+
+  group = gtk_widget_get_action_group (GTK_WIDGET (self), "project-tree");
+  g_assert (G_IS_SIMPLE_ACTION_GROUP (group));
+
+  selection = gb_tree_get_selected (GB_TREE (self));
+  if (selection != NULL)
+    item = gb_tree_node_get_item (selection);
+
+  action_set (group, "new-file",
+              "enabled", GB_IS_PROJECT_FILE (item),
+              NULL);
+  action_set (group, "new-directory",
+              "enabled", GB_IS_PROJECT_FILE (item),
+              NULL);
+  action_set (group, "open",
+              "enabled", (GB_IS_PROJECT_FILE (item) && !project_file_is_directory (item)),
+              NULL);
+  action_set (group, "open-with-editor",
+              "enabled", (GB_IS_PROJECT_FILE (item) && !project_file_is_directory (item)),
+              NULL);
+  action_set (group, "open-containing-folder",
+              "enabled", GB_IS_PROJECT_FILE (item),
+              NULL);
+  action_set (group, "open-in-terminal",
+              "enabled", GB_IS_PROJECT_FILE (item),
+              NULL);
+  action_set (group, "rename-file",
+              "enabled", (GB_IS_PROJECT_FILE (item) && !is_files_node (selection)),
+              NULL);
+  action_set (group, "move-to-trash",
+              "enabled", (GB_IS_PROJECT_FILE (item) && !is_files_node (selection)),
+              NULL);
+
+  IDE_EXIT;
+#endif
+}
diff --git a/plugins/project-tree/gb-project-tree-actions.h b/plugins/project-tree/gb-project-tree-actions.h
new file mode 100644
index 0000000..2837c84
--- /dev/null
+++ b/plugins/project-tree/gb-project-tree-actions.h
@@ -0,0 +1,31 @@
+/* gb-project-tree-actions.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#ifndef GB_PROJECT_TREE_ACTIONS_H
+#define GB_PROJECT_TREE_ACTIONS_H
+
+#include "gb-project-tree.h"
+
+G_BEGIN_DECLS
+
+void gb_project_tree_actions_init   (GbProjectTree *self);
+void gb_project_tree_actions_update (GbProjectTree *self);
+
+G_END_DECLS
+
+#endif /* GB_PROJECT_TREE_ACTIONS_H */
diff --git a/plugins/project-tree/gb-project-tree-addin.c b/plugins/project-tree/gb-project-tree-addin.c
new file mode 100644
index 0000000..c4d5c1d
--- /dev/null
+++ b/plugins/project-tree/gb-project-tree-addin.c
@@ -0,0 +1,69 @@
+/* gb-project-tree-addin.c
+ *
+ * Copyright (C) 2015 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+#include <ide.h>
+
+#include "gb-project-tree.h"
+#include "gb-project-tree-addin.h"
+
+static void workbench_addin_iface_init (IdeWorkbenchAddinInterface *iface);
+
+struct _GbProjectTreeAddin
+{
+  GObject parent_instance;
+};
+
+G_DEFINE_TYPE_EXTENDED (GbProjectTreeAddin, gb_project_tree_addin, G_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKBENCH_ADDIN, workbench_addin_iface_init))
+
+static void
+gb_project_tree_addin_load (IdeWorkbenchAddin *addin,
+                            IdeWorkbench      *workbench)
+{
+  IdePerspective *editor;
+  GtkWidget *pane;
+  GtkWidget *tree;
+
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+  g_assert (IDE_IS_WORKBENCH (workbench));
+
+  editor = ide_workbench_get_perspective_by_name (workbench, "editor");
+  pane = ide_layout_get_left_pane (IDE_LAYOUT (editor));
+  tree = g_object_new (GB_TYPE_PROJECT_TREE,
+                       "headers-visible", FALSE,
+                       "visible", TRUE,
+                       NULL);
+  ide_layout_pane_add_page (IDE_LAYOUT_PANE (pane), tree, _("Project Tree"), "folder-symbolic");
+}
+
+static void
+workbench_addin_iface_init (IdeWorkbenchAddinInterface *iface)
+{
+  iface->load = gb_project_tree_addin_load;
+}
+
+static void
+gb_project_tree_addin_class_init (GbProjectTreeAddinClass *klass)
+{
+}
+
+static void
+gb_project_tree_addin_init (GbProjectTreeAddin *addin)
+{
+}
diff --git a/plugins/project-tree/gb-project-tree-addin.h b/plugins/project-tree/gb-project-tree-addin.h
new file mode 100644
index 0000000..2b107b8
--- /dev/null
+++ b/plugins/project-tree/gb-project-tree-addin.h
@@ -0,0 +1,32 @@
+/* gb-project-tree-addin.h
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#ifndef GB_PROJECT_TREE_ADDIN_H
+#define GB_PROJECT_TREE_ADDIN_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_PROJECT_TREE_ADDIN (gb_project_tree_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbProjectTreeAddin, gb_project_tree_addin, GB, PROJECT_TREE_ADDIN, GObject)
+
+G_END_DECLS
+
+#endif /* GB_PROJECT_TREE_ADDIN_H */
diff --git a/plugins/project-tree/gb-project-tree-builder.c b/plugins/project-tree/gb-project-tree-builder.c
new file mode 100644
index 0000000..4e9304b
--- /dev/null
+++ b/plugins/project-tree/gb-project-tree-builder.c
@@ -0,0 +1,422 @@
+/* gb-project-tree-builder.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+#include <ide.h>
+
+#include "gb-project-file.h"
+#include "gb-project-tree.h"
+#include "gb-project-tree-builder.h"
+#include "gb-tree.h"
+
+struct _GbProjectTreeBuilder
+{
+  GbTreeBuilder  parent_instance;
+
+  GSettings     *file_chooser_settings;
+
+  guint          sort_directories_first : 1;
+};
+
+G_DEFINE_TYPE (GbProjectTreeBuilder, gb_project_tree_builder, GB_TYPE_TREE_BUILDER)
+
+GbTreeBuilder *
+gb_project_tree_builder_new (void)
+{
+  return g_object_new (GB_TYPE_PROJECT_TREE_BUILDER, NULL);
+}
+
+static void
+build_context (GbProjectTreeBuilder *self,
+               GbTreeNode           *node)
+{
+  g_autoptr(GbProjectFile) item = NULL;
+  g_autoptr(GFileInfo) file_info = NULL;
+  g_autofree gchar *name = NULL;
+  GbTreeNode *child;
+  IdeContext *context;
+  IdeVcs *vcs;
+  GFile *workdir;
+
+  g_return_if_fail (GB_IS_PROJECT_TREE_BUILDER (self));
+  g_return_if_fail (GB_IS_TREE_NODE (node));
+
+  context = IDE_CONTEXT (gb_tree_node_get_item (node));
+  vcs = ide_context_get_vcs (context);
+  workdir = ide_vcs_get_working_directory (vcs);
+
+  file_info = g_file_info_new ();
+
+  g_file_info_set_file_type (file_info, G_FILE_TYPE_DIRECTORY);
+
+  name = g_file_get_basename (workdir);
+  g_file_info_set_name (file_info, name);
+  g_file_info_set_display_name (file_info, name);
+
+  item = g_object_new (GB_TYPE_PROJECT_FILE,
+                       "file", workdir,
+                       "file-info", file_info,
+                       NULL);
+
+  child = g_object_new (GB_TYPE_TREE_NODE,
+                        "item", item,
+                        "text", _("Files"),
+                        "icon-name", "folder-symbolic",
+                        NULL);
+  gb_tree_node_append (node, child);
+}
+
+static IdeVcs *
+get_vcs (GbTreeNode *node)
+{
+  GbTree *tree;
+  GbTreeNode *root;
+  IdeContext *context;
+
+  g_assert (GB_IS_TREE_NODE (node));
+
+  tree = gb_tree_node_get_tree (node);
+  root = gb_tree_get_root (tree);
+  context = IDE_CONTEXT (gb_tree_node_get_item (root));
+
+  return ide_context_get_vcs (context);
+}
+
+static gint
+compare_nodes_func (GbTreeNode *a,
+                    GbTreeNode *b,
+                    gpointer    user_data)
+{
+  GbProjectFile *file_a = GB_PROJECT_FILE (gb_tree_node_get_item (a));
+  GbProjectFile *file_b = GB_PROJECT_FILE (gb_tree_node_get_item (b));
+  GbProjectTreeBuilder *self = user_data;
+
+  if (self->sort_directories_first)
+    return gb_project_file_compare_directories_first (file_a, file_b);
+  else
+    return gb_project_file_compare (file_a, file_b);
+}
+
+static void
+build_file (GbProjectTreeBuilder *self,
+            GbTreeNode           *node)
+{
+  g_autoptr(GFileEnumerator) enumerator = NULL;
+  GbProjectFile *project_file;
+  gpointer file_info_ptr;
+  IdeVcs *vcs;
+  GFile *file;
+  GbTree *tree;
+  gboolean show_ignored_files;
+
+  g_return_if_fail (GB_IS_PROJECT_TREE_BUILDER (self));
+  g_return_if_fail (GB_IS_TREE_NODE (node));
+
+  project_file = GB_PROJECT_FILE (gb_tree_node_get_item (node));
+
+  tree = gb_tree_builder_get_tree (GB_TREE_BUILDER (self));
+  show_ignored_files = gb_project_tree_get_show_ignored_files (GB_PROJECT_TREE (tree));
+
+  vcs = get_vcs (node);
+
+  /*
+   * TODO: Make this all async.
+   */
+
+  if (!gb_project_file_get_is_directory (project_file))
+    return;
+
+  file = gb_project_file_get_file (project_file);
+
+  enumerator = g_file_enumerate_children (file,
+                                          G_FILE_ATTRIBUTE_STANDARD_NAME","
+                                          G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME","
+                                          G_FILE_ATTRIBUTE_STANDARD_TYPE,
+                                          G_FILE_QUERY_INFO_NONE,
+                                          NULL,
+                                          NULL);
+
+  if (enumerator == NULL)
+    return;
+
+  while ((file_info_ptr = g_file_enumerator_next_file (enumerator, NULL, NULL)))
+    {
+      g_autoptr(GFileInfo) item_file_info = file_info_ptr;
+      g_autoptr(GFile) item_file = NULL;
+      g_autoptr(GbProjectFile) item = NULL;
+      GbTreeNode *child;
+      const gchar *name;
+      const gchar *display_name;
+      const gchar *icon_name;
+      gboolean ignored;
+
+      name = g_file_info_get_name (item_file_info);
+      item_file = g_file_get_child (file, name);
+
+      ignored = ide_vcs_is_ignored (vcs, item_file, NULL);
+      if (ignored && !show_ignored_files)
+        continue;
+
+      item = gb_project_file_new (item_file, item_file_info);
+
+      display_name = gb_project_file_get_display_name (item);
+      icon_name = gb_project_file_get_icon_name (item);
+
+      child = g_object_new (GB_TYPE_TREE_NODE,
+                            "icon-name", icon_name,
+                            "text", display_name,
+                            "item", item,
+                            "use-dim-label", ignored,
+                            NULL);
+
+      gb_tree_node_insert_sorted (node, child, compare_nodes_func, self);
+
+      if (g_file_info_get_file_type (item_file_info) == G_FILE_TYPE_DIRECTORY)
+        gb_tree_node_set_children_possible (child, TRUE);
+    }
+}
+
+static void
+gb_project_tree_builder_build_node (GbTreeBuilder *builder,
+                                    GbTreeNode    *node)
+{
+  GbProjectTreeBuilder *self = (GbProjectTreeBuilder *)builder;
+  GObject *item;
+
+  g_return_if_fail (GB_IS_PROJECT_TREE_BUILDER (self));
+
+  item = gb_tree_node_get_item (node);
+
+  if (IDE_IS_CONTEXT (item))
+    build_context (self, node);
+  else if (GB_IS_PROJECT_FILE (item))
+    build_file (self, node);
+}
+
+static gchar *
+get_content_type (GFile *file)
+{
+  g_autofree gchar *name = NULL;
+
+  g_assert (G_IS_FILE (file));
+
+  name = g_file_get_basename (file);
+
+  return g_content_type_guess (name, NULL, 0, NULL);
+}
+
+static void
+populate_mime_handlers (GMenu         *menu,
+                        GbProjectFile *project_file)
+{
+  g_autofree gchar *content_type = NULL;
+  GList *list;
+  GList *iter;
+  GFile *file;
+
+  g_assert (G_IS_MENU (menu));
+  g_assert (GB_IS_PROJECT_FILE (project_file));
+
+  g_menu_remove_all (menu);
+
+  file = gb_project_file_get_file (project_file);
+  if (file == NULL)
+    return;
+
+  content_type = get_content_type (file);
+  if (content_type == NULL)
+    return;
+
+  list = g_app_info_get_all_for_type (content_type);
+
+  for (iter = list; iter; iter = iter->next)
+    {
+      g_autoptr(GMenuItem) menu_item = NULL;
+      g_autofree gchar *detailed_action = NULL;
+      GAppInfo *app_info = iter->data;
+      const gchar *display_name;
+      const gchar *app_id;
+
+      display_name = g_app_info_get_display_name (app_info);
+      app_id = g_app_info_get_id (app_info);
+
+      detailed_action = g_strdup_printf ("project-tree.open-with('%s')", app_id);
+      menu_item = g_menu_item_new (display_name, detailed_action);
+
+      g_menu_append_item (menu, menu_item);
+    }
+
+  g_list_free_full (list, g_object_unref);
+}
+
+static void
+gb_project_tree_builder_node_popup (GbTreeBuilder *builder,
+                                    GbTreeNode    *node,
+                                    GMenu         *menu)
+{
+  GtkApplication *app;
+  GObject *item;
+  GMenu *submenu;
+  IdeVcs *vcs;
+  GFile *workdir;
+  GFile *file;
+
+  g_assert (GB_IS_PROJECT_TREE_BUILDER (builder));
+  g_assert (GB_IS_TREE_NODE (node));
+  g_assert (G_IS_MENU (menu));
+
+  app = GTK_APPLICATION (g_application_get_default ());
+  item = gb_tree_node_get_item (node);
+
+  if (GB_IS_PROJECT_FILE (item))
+    {
+      submenu = gtk_application_get_menu_by_id (app, "gb-project-tree-build");
+      g_menu_prepend_section (menu, NULL, G_MENU_MODEL (submenu));
+    }
+
+  vcs = get_vcs (node);
+  workdir = ide_vcs_get_working_directory (vcs);
+
+  if (GB_IS_PROJECT_FILE (item) &&
+      (file = gb_project_file_get_file (GB_PROJECT_FILE (item))) &&
+      !g_file_equal (file, workdir))
+    {
+      submenu = gtk_application_get_menu_by_id (app, "gb-project-tree-move-to-trash");
+      g_menu_prepend_section (menu, NULL, G_MENU_MODEL (submenu));
+
+      submenu = gtk_application_get_menu_by_id (app, "gb-project-tree-rename");
+      g_menu_prepend_section (menu, NULL, G_MENU_MODEL (submenu));
+
+      submenu = gtk_application_get_menu_by_id (app, "gb-project-tree-open-containing");
+      g_menu_prepend_section (menu, NULL, G_MENU_MODEL (submenu));
+
+      submenu = gtk_application_get_menu_by_id (app, "gb-project-tree-open");
+      g_menu_prepend_section (menu, NULL, G_MENU_MODEL (submenu));
+
+      submenu = gtk_application_get_menu_by_id (app, "gb-project-tree-open-by-mime-section");
+      populate_mime_handlers (submenu, GB_PROJECT_FILE (item));
+
+      submenu = gtk_application_get_menu_by_id (app, "gb-project-tree-new");
+      g_menu_prepend_section (menu, NULL, G_MENU_MODEL (submenu));
+    }
+  else if (GB_IS_PROJECT_FILE (item))
+    {
+      submenu = gtk_application_get_menu_by_id (app, "gb-project-tree-open-containing");
+      g_menu_prepend_section (menu, NULL, G_MENU_MODEL (submenu));
+
+      submenu = gtk_application_get_menu_by_id (app, "gb-project-tree-new");
+      g_menu_prepend_section (menu, NULL, G_MENU_MODEL (submenu));
+    }
+
+  submenu = gtk_application_get_menu_by_id (app, "gb-project-tree-display-options");
+  g_menu_append_section (menu, NULL, G_MENU_MODEL (submenu));
+}
+
+static gboolean
+gb_project_tree_builder_node_activated (GbTreeBuilder *builder,
+                                        GbTreeNode    *node)
+{
+  GObject *item;
+
+  g_assert (GB_IS_PROJECT_TREE_BUILDER (builder));
+
+  item = gb_tree_node_get_item (node);
+
+  if (GB_IS_PROJECT_FILE (item))
+    {
+      GtkWidget *workbench;
+      GbTree *tree;
+      GFile *file;
+
+      if (gb_project_file_get_is_directory (GB_PROJECT_FILE (item)))
+        goto failure;
+
+      file = gb_project_file_get_file (GB_PROJECT_FILE (item));
+      if (!file)
+        goto failure;
+
+      tree = gb_tree_node_get_tree (node);
+      if (!tree)
+        goto failure;
+
+      workbench = gtk_widget_get_ancestor (GTK_WIDGET (tree), IDE_TYPE_WORKBENCH);
+      ide_workbench_open_async (IDE_WORKBENCH (workbench), &file, 1, NULL, NULL, NULL);
+
+      return TRUE;
+    }
+
+failure:
+  return FALSE;
+}
+
+static void
+gb_project_tree_builder_rebuild (GSettings            *settings,
+                                 const gchar          *key,
+                                 GbProjectTreeBuilder *self)
+{
+  GbTree *tree;
+  gboolean sort_directories_first;
+
+  g_assert (G_IS_SETTINGS (settings));
+  g_assert (GB_IS_PROJECT_TREE_BUILDER (self));
+
+  sort_directories_first = g_settings_get_boolean (settings, "sort-directories-first");
+
+  if (sort_directories_first != self->sort_directories_first)
+    {
+      self->sort_directories_first = sort_directories_first;
+      if ((tree = gb_tree_builder_get_tree (GB_TREE_BUILDER (self))))
+        gb_tree_rebuild (tree);
+    }
+}
+
+static void
+gb_project_tree_builder_finalize (GObject *object)
+{
+  GbProjectTreeBuilder *self = (GbProjectTreeBuilder *)object;
+
+  g_clear_object (&self->file_chooser_settings);
+
+  G_OBJECT_CLASS (gb_project_tree_builder_parent_class)->finalize (object);
+}
+
+static void
+gb_project_tree_builder_class_init (GbProjectTreeBuilderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GbTreeBuilderClass *tree_builder_class = GB_TREE_BUILDER_CLASS (klass);
+
+  object_class->finalize = gb_project_tree_builder_finalize;
+
+  tree_builder_class->build_node = gb_project_tree_builder_build_node;
+  tree_builder_class->node_activated = gb_project_tree_builder_node_activated;
+  tree_builder_class->node_popup = gb_project_tree_builder_node_popup;
+}
+
+static void
+gb_project_tree_builder_init (GbProjectTreeBuilder *self)
+{
+  self->file_chooser_settings = g_settings_new ("org.gtk.Settings.FileChooser");
+  self->sort_directories_first = g_settings_get_boolean (self->file_chooser_settings,
+                                                         "sort-directories-first");
+
+  g_signal_connect (self->file_chooser_settings,
+                    "changed::sort-directories-first",
+                    G_CALLBACK (gb_project_tree_builder_rebuild),
+                    self);
+}
diff --git a/plugins/project-tree/gb-project-tree-builder.h b/plugins/project-tree/gb-project-tree-builder.h
new file mode 100644
index 0000000..2924185
--- /dev/null
+++ b/plugins/project-tree/gb-project-tree-builder.h
@@ -0,0 +1,37 @@
+/* gb-project-tree-builder.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#ifndef GB_PROJECT_TREE_BUILDER_H
+#define GB_PROJECT_TREE_BUILDER_H
+
+#include <ide.h>
+
+#include "gb-tree-builder.h"
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_PROJECT_TREE_BUILDER (gb_project_tree_builder_get_type())
+
+G_DECLARE_FINAL_TYPE (GbProjectTreeBuilder, gb_project_tree_builder,
+                      GB, PROJECT_TREE_BUILDER, GbTreeBuilder)
+
+GbTreeBuilder  *gb_project_tree_builder_new (void);
+
+G_END_DECLS
+
+#endif /* GB_PROJECT_TREE_BUILDER_H */
diff --git a/plugins/project-tree/gb-project-tree-private.h b/plugins/project-tree/gb-project-tree-private.h
new file mode 100644
index 0000000..d63b95f
--- /dev/null
+++ b/plugins/project-tree/gb-project-tree-private.h
@@ -0,0 +1,38 @@
+/* gb-project-tree-private.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#ifndef GB_PROJECT_TREE_PRIVATE_H
+#define GB_PROJECT_TREE_PRIVATE_H
+
+#include "gb-tree.h"
+
+G_BEGIN_DECLS
+
+struct _GbProjectTree
+{
+  GbTree     parent_instance;
+
+  GSettings *settings;
+
+  guint      expanded_in_new : 1;
+  guint      show_ignored_files : 1;
+};
+
+G_END_DECLS
+
+#endif /* GB_PROJECT_TREE_PRIVATE_H */
diff --git a/plugins/project-tree/gb-project-tree.c b/plugins/project-tree/gb-project-tree.c
new file mode 100644
index 0000000..b6b8545
--- /dev/null
+++ b/plugins/project-tree/gb-project-tree.c
@@ -0,0 +1,212 @@
+/* gb-project-tree.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "gb-project-tree.h"
+#include "gb-project-tree-actions.h"
+#include "gb-project-tree-builder.h"
+#include "gb-project-tree-private.h"
+
+G_DEFINE_TYPE (GbProjectTree, gb_project_tree, GB_TYPE_TREE)
+
+enum {
+  PROP_0,
+  PROP_SHOW_IGNORED_FILES,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+GtkWidget *
+gb_project_tree_new (void)
+{
+  return g_object_new (GB_TYPE_PROJECT_TREE, NULL);
+}
+
+IdeContext *
+gb_project_tree_get_context (GbProjectTree *self)
+{
+  GbTreeNode *root;
+  GObject *item;
+
+  g_return_val_if_fail (GB_IS_PROJECT_TREE (self), NULL);
+
+  if ((root = gb_tree_get_root (GB_TREE (self))) &&
+      (item = gb_tree_node_get_item (root)) &&
+      IDE_IS_OBJECT (item))
+    return ide_object_get_context (IDE_OBJECT (item));
+
+  return NULL;
+}
+
+void
+gb_project_tree_set_context (GbProjectTree *self,
+                             IdeContext    *context)
+{
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+  GbTreeNode *root;
+
+  g_return_if_fail (GB_IS_PROJECT_TREE (self));
+  g_return_if_fail (!context || IDE_IS_CONTEXT (context));
+
+  model = gtk_tree_view_get_model (GTK_TREE_VIEW (self));
+
+  root = gb_tree_node_new ();
+  gb_tree_node_set_item (root, G_OBJECT (context));
+  gb_tree_set_root (GB_TREE (self), root);
+
+  /*
+   * If we only have one toplevel item (underneath root), expand it.
+   */
+  if ((gtk_tree_model_iter_n_children (model, NULL) == 1) &&
+      gtk_tree_model_get_iter_first (model, &iter))
+    {
+      g_autoptr(GbTreeNode) node = NULL;
+
+      gtk_tree_model_get (model, &iter, 0, &node, -1);
+      if (node != NULL)
+        gb_tree_node_expand (node, FALSE);
+    }
+}
+
+
+static void
+gb_project_tree_notify_selection (GbProjectTree *self)
+{
+  g_assert (GB_IS_PROJECT_TREE (self));
+
+  gb_project_tree_actions_update (self);
+}
+
+static void
+gb_project_tree_finalize (GObject *object)
+{
+  GbProjectTree *self = (GbProjectTree *)object;
+
+  g_clear_object (&self->settings);
+
+  G_OBJECT_CLASS (gb_project_tree_parent_class)->finalize (object);
+}
+
+static void
+gb_project_tree_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  GbProjectTree *self = GB_PROJECT_TREE(object);
+
+  switch (prop_id)
+    {
+    case PROP_SHOW_IGNORED_FILES:
+      g_value_set_boolean (value, gb_project_tree_get_show_ignored_files (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_project_tree_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  GbProjectTree *self = GB_PROJECT_TREE(object);
+
+  switch (prop_id)
+    {
+    case PROP_SHOW_IGNORED_FILES:
+      gb_project_tree_set_show_ignored_files (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_project_tree_class_init (GbProjectTreeClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gb_project_tree_finalize;
+  object_class->get_property = gb_project_tree_get_property;
+  object_class->set_property = gb_project_tree_set_property;
+
+  properties [PROP_SHOW_IGNORED_FILES] =
+    g_param_spec_boolean ("show-ignored-files",
+                          "Show Ignored Files",
+                          "If files ignored by the VCS should be displayed.",
+                         FALSE,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+gb_project_tree_init (GbProjectTree *self)
+{
+  GbTreeBuilder *builder;
+
+  self->settings = g_settings_new ("org.gnome.builder.project-tree");
+
+  g_settings_bind (self->settings, "show-icons",
+                   self, "show-icons",
+                   G_SETTINGS_BIND_DEFAULT);
+  g_settings_bind (self->settings, "show-ignored-files",
+                   self, "show-ignored-files",
+                   G_SETTINGS_BIND_DEFAULT);
+
+  builder = gb_project_tree_builder_new ();
+  gb_tree_add_builder (GB_TREE (self), builder);
+
+  g_signal_connect (self,
+                    "notify::selection",
+                    G_CALLBACK (gb_project_tree_notify_selection),
+                    NULL);
+
+  gb_project_tree_actions_init (self);
+}
+
+gboolean
+gb_project_tree_get_show_ignored_files (GbProjectTree *self)
+{
+  g_return_val_if_fail (GB_IS_PROJECT_TREE (self), FALSE);
+
+  return self->show_ignored_files;
+}
+
+void
+gb_project_tree_set_show_ignored_files (GbProjectTree *self,
+                                        gboolean       show_ignored_files)
+{
+  g_return_if_fail (GB_IS_PROJECT_TREE (self));
+
+  show_ignored_files = !!show_ignored_files;
+
+  if (show_ignored_files != self->show_ignored_files)
+    {
+      self->show_ignored_files = show_ignored_files;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_IGNORED_FILES]);
+      gb_tree_rebuild (GB_TREE (self));
+    }
+}
diff --git a/plugins/project-tree/gb-project-tree.h b/plugins/project-tree/gb-project-tree.h
new file mode 100644
index 0000000..9e7358e
--- /dev/null
+++ b/plugins/project-tree/gb-project-tree.h
@@ -0,0 +1,42 @@
+/* gb-project-tree.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#ifndef GB_PROJECT_TREE_H
+#define GB_PROJECT_TREE_H
+
+#include <ide.h>
+
+#include "gb-tree.h"
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_PROJECT_TREE (gb_project_tree_get_type())
+
+G_DECLARE_FINAL_TYPE (GbProjectTree, gb_project_tree, GB, PROJECT_TREE, GbTree)
+
+GtkWidget  *gb_project_tree_new                    (void);
+void        gb_project_tree_set_context            (GbProjectTree *self,
+                                                    IdeContext    *context);
+IdeContext *gb_project_tree_get_context            (GbProjectTree *self);
+gboolean    gb_project_tree_get_show_ignored_files (GbProjectTree *self);
+void        gb_project_tree_set_show_ignored_files (GbProjectTree *self,
+                                                    gboolean       show_ignored_files);
+
+G_END_DECLS
+
+#endif /* GB_PROJECT_TREE_H */
diff --git a/plugins/project-tree/gb-rename-file-popover.c b/plugins/project-tree/gb-rename-file-popover.c
new file mode 100644
index 0000000..4532300
--- /dev/null
+++ b/plugins/project-tree/gb-rename-file-popover.c
@@ -0,0 +1,362 @@
+/* gb-rename-file-popover.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+#include <ide.h>
+
+#include "gb-rename-file-popover.h"
+
+struct _GbRenameFilePopover
+{
+  GtkPopover    parent_instance;
+
+  GCancellable *cancellable;
+  GFile        *file;
+
+  GtkEntry     *entry;
+  GtkButton    *button;
+  GtkLabel     *label;
+  GtkLabel     *message;
+
+  guint         is_directory : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_FILE,
+  PROP_IS_DIRECTORY,
+  LAST_PROP
+};
+
+enum {
+  RENAME_FILE,
+  LAST_SIGNAL
+};
+
+G_DEFINE_TYPE (GbRenameFilePopover, gb_rename_file_popover, GTK_TYPE_POPOVER)
+
+static GParamSpec *properties [LAST_PROP];
+static guint signals [LAST_SIGNAL];
+
+GFile *
+gb_rename_file_popover_get_file (GbRenameFilePopover *self)
+{
+  g_return_val_if_fail (GB_IS_RENAME_FILE_POPOVER (self), NULL);
+
+  return self->file;
+}
+
+static void
+gb_rename_file_popover_set_file (GbRenameFilePopover *self,
+                                 GFile               *file)
+{
+  g_return_if_fail (GB_IS_RENAME_FILE_POPOVER (self));
+  g_return_if_fail (G_IS_FILE (file));
+
+  if (g_set_object (&self->file, file))
+    {
+      if (file != NULL)
+        {
+          gchar *name;
+          gchar *label;
+
+          name = g_file_get_basename (file);
+          label = g_strdup_printf (_("Rename %s"), name);
+
+          gtk_label_set_label (self->label, label);
+
+          g_free (name);
+          g_free (label);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FILE]);
+    }
+}
+
+static void
+gb_rename_file_popover_set_is_directory (GbRenameFilePopover *self,
+                                         gboolean             is_directory)
+{
+  g_return_if_fail (GB_IS_RENAME_FILE_POPOVER (self));
+
+  is_directory = !!is_directory;
+
+  if (is_directory != self->is_directory)
+    {
+      self->is_directory = is_directory;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IS_DIRECTORY]);
+    }
+}
+
+static void
+gb_rename_file_popover__file_query_info (GObject      *object,
+                                         GAsyncResult *result,
+                                         gpointer      user_data)
+{
+  GFile *file = (GFile *)object;
+  g_autoptr(GFileInfo) file_info = NULL;
+  g_autoptr(GbRenameFilePopover) self = user_data;
+  g_autoptr(GError) error = NULL;
+  GFileType file_type;
+
+  file_info = g_file_query_info_finish (file, result, &error);
+
+  if (file_info == NULL &&
+      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+    return;
+
+  if ((file_info == NULL) &&
+      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+    {
+      gtk_label_set_label (self->message, NULL);
+      gtk_widget_set_sensitive (GTK_WIDGET (self->button), TRUE);
+      return;
+    }
+
+  if (file_info == NULL)
+    {
+      gtk_label_set_label (self->message, error->message);
+      return;
+    }
+
+  file_type = g_file_info_get_file_type (file_info);
+
+  if (file_type == G_FILE_TYPE_DIRECTORY)
+    gtk_label_set_label (self->message,
+                         _("A folder with that name already exists."));
+  else
+    gtk_label_set_label (self->message,
+                         _("A file with that name already exists."));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->button), FALSE);
+}
+
+static void
+gb_rename_file_popover__entry_changed (GbRenameFilePopover *self,
+                                       GtkEntry            *entry)
+{
+  g_autoptr(GFile) parent = NULL;
+  g_autoptr(GFile) file = NULL;
+  const gchar *text;
+
+  g_assert (GB_IS_RENAME_FILE_POPOVER (self));
+  g_assert (GTK_IS_ENTRY (entry));
+  g_assert (self->file != NULL);
+  g_assert (G_IS_FILE (self->file));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->button), FALSE);
+  gtk_label_set_label (self->message, NULL);
+
+  text = gtk_entry_get_text (entry);
+  if (ide_str_empty0 (text))
+    return;
+
+  if (strchr (text, G_DIR_SEPARATOR) != NULL)
+    {
+      gtk_label_set_label (self->message,
+                           _("File name must not contain subdirectories."));
+      return;
+    }
+
+  if (self->cancellable)
+    {
+      g_cancellable_cancel (self->cancellable);
+      g_clear_object (&self->cancellable);
+    }
+
+  self->cancellable = g_cancellable_new ();
+
+  parent = g_file_get_parent (self->file);
+  file = g_file_get_child (parent, text);
+
+  g_file_query_info_async (file,
+                           G_FILE_ATTRIBUTE_STANDARD_TYPE,
+                           G_FILE_QUERY_INFO_NONE,
+                           G_PRIORITY_DEFAULT,
+                           self->cancellable,
+                           gb_rename_file_popover__file_query_info,
+                           g_object_ref (self));
+}
+
+static void
+gb_rename_file_popover__entry_activate (GbRenameFilePopover *self,
+                                        GtkEntry            *entry)
+{
+  g_assert (GB_IS_RENAME_FILE_POPOVER (self));
+  g_assert (GTK_IS_ENTRY (entry));
+
+  if (gtk_widget_get_sensitive (GTK_WIDGET (self->button)))
+    gtk_widget_activate (GTK_WIDGET (self->button));
+}
+
+static void
+gb_rename_file_popover__button_clicked (GbRenameFilePopover *self,
+                                        GtkButton           *button)
+{
+  g_autoptr(GFile) file = NULL;
+  g_autoptr(GFile) parent = NULL;
+  const gchar *path;
+
+  g_assert (GB_IS_RENAME_FILE_POPOVER (self));
+  g_assert (GTK_IS_BUTTON (button));
+  g_assert (self->file != NULL);
+  g_assert (G_IS_FILE (self->file));
+
+  path = gtk_entry_get_text (self->entry);
+  if (ide_str_empty0 (path))
+    return;
+
+  parent = g_file_get_parent (self->file);
+  file = g_file_get_child (parent, path);
+
+  /* only activate once */
+  gtk_widget_set_sensitive (GTK_WIDGET (self->button), FALSE);
+
+  g_signal_emit (self, signals [RENAME_FILE], 0, self->file, file);
+}
+
+static void
+gb_rename_file_popover_finalize (GObject *object)
+{
+  GbRenameFilePopover *self = (GbRenameFilePopover *)object;
+
+  if (self->cancellable != NULL)
+    {
+      if (!g_cancellable_is_cancelled (self->cancellable))
+        g_cancellable_cancel (self->cancellable);
+      g_clear_object (&self->cancellable);
+    }
+
+  g_clear_object (&self->file);
+
+  G_OBJECT_CLASS (gb_rename_file_popover_parent_class)->finalize (object);
+}
+
+static void
+gb_rename_file_popover_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  GbRenameFilePopover *self = GB_RENAME_FILE_POPOVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_FILE:
+      g_value_set_object (value, self->file);
+      break;
+
+    case PROP_IS_DIRECTORY:
+      g_value_set_boolean (value, self->is_directory);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_rename_file_popover_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  GbRenameFilePopover *self = GB_RENAME_FILE_POPOVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_FILE:
+      gb_rename_file_popover_set_file (self, g_value_get_object (value));
+      break;
+
+    case PROP_IS_DIRECTORY:
+      gb_rename_file_popover_set_is_directory (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_rename_file_popover_class_init (GbRenameFilePopoverClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = gb_rename_file_popover_finalize;
+  object_class->get_property = gb_rename_file_popover_get_property;
+  object_class->set_property = gb_rename_file_popover_set_property;
+
+  properties [PROP_FILE] =
+    g_param_spec_object ("file",
+                         "File",
+                         "File",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_IS_DIRECTORY] =
+    g_param_spec_boolean ("is-directory",
+                          "Is Directory",
+                          "Is Directory",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  signals [RENAME_FILE] =
+    g_signal_new ("rename-file",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  2,
+                  G_TYPE_FILE,
+                  G_TYPE_FILE);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/gb-rename-file-popover.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbRenameFilePopover, button);
+  gtk_widget_class_bind_template_child (widget_class, GbRenameFilePopover, entry);
+  gtk_widget_class_bind_template_child (widget_class, GbRenameFilePopover, label);
+  gtk_widget_class_bind_template_child (widget_class, GbRenameFilePopover, message);
+}
+
+static void
+gb_rename_file_popover_init (GbRenameFilePopover *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (self->entry,
+                           "changed",
+                           G_CALLBACK (gb_rename_file_popover__entry_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->entry,
+                           "activate",
+                           G_CALLBACK (gb_rename_file_popover__entry_activate),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->button,
+                           "clicked",
+                           G_CALLBACK (gb_rename_file_popover__button_clicked),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
diff --git a/plugins/project-tree/gb-rename-file-popover.h b/plugins/project-tree/gb-rename-file-popover.h
new file mode 100644
index 0000000..8c2d771
--- /dev/null
+++ b/plugins/project-tree/gb-rename-file-popover.h
@@ -0,0 +1,35 @@
+/* gb-rename-file-popover.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#ifndef GB_RENAME_FILE_POPOVER_H
+#define GB_RENAME_FILE_POPOVER_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_RENAME_FILE_POPOVER (gb_rename_file_popover_get_type())
+
+G_DECLARE_FINAL_TYPE (GbRenameFilePopover, gb_rename_file_popover,
+                      GB, RENAME_FILE_POPOVER, GtkPopover)
+
+GFile *gb_rename_file_popover_get_file (GbRenameFilePopover *self);
+
+G_END_DECLS
+
+#endif /* GB_RENAME_FILE_POPOVER_H */
diff --git a/plugins/project-tree/project-tree-plugin.c b/plugins/project-tree/project-tree-plugin.c
new file mode 100644
index 0000000..4e8bbce
--- /dev/null
+++ b/plugins/project-tree/project-tree-plugin.c
@@ -0,0 +1,30 @@
+/* project-tree-plugin.c
+ *
+ * Copyright (C) 2015 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <libpeas/peas.h>
+#include <ide.h>
+
+#include "gb-project-tree-addin.h"
+
+void
+peas_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKBENCH_ADDIN,
+                                              GB_TYPE_PROJECT_TREE_ADDIN);
+}
diff --git a/plugins/project-tree/project-tree.plugin b/plugins/project-tree/project-tree.plugin
new file mode 100644
index 0000000..f610ba7
--- /dev/null
+++ b/plugins/project-tree/project-tree.plugin
@@ -0,0 +1,8 @@
+[Plugin]
+Module=project-tree-plugin
+Name=Project Tree
+Description=Provides a project tree
+Authors=Christian Hergert <christian hergert me>
+Copyright=Copyright © 2015 Christian Hergert
+Builtin=true
+Hidden=true


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