[gnome-builder] plugins/symbol-tree: port to GTK 4



commit 621af4a4b2bd78d8a15164acc20a1e0c8c6c48df
Author: Christian Hergert <chergert redhat com>
Date:   Mon Jul 11 23:20:25 2022 -0700

    plugins/symbol-tree: port to GTK 4
    
    This ports the symbol tree plugin to GTK 4 and also changes how it is
    presented in the workspace. It's now in the statusbar and updates as
    you move through the document like before. However there is now just a
    single symbol-tree for the workspace window.

 src/plugins/symbol-tree/gbp-symbol-frame-addin.c   | 574 ---------------------
 .../symbol-tree/gbp-symbol-hover-provider.c        |  94 ++--
 src/plugins/symbol-tree/gbp-symbol-list-model.c    | 184 +++++++
 ...mbol-tree-builder.h => gbp-symbol-list-model.h} |  15 +-
 src/plugins/symbol-tree/gbp-symbol-menu-button.c   | 330 ------------
 src/plugins/symbol-tree/gbp-symbol-menu-button.ui  |  86 ---
 src/plugins/symbol-tree/gbp-symbol-popover-row.ui  |  42 ++
 src/plugins/symbol-tree/gbp-symbol-popover.c       | 433 ++++++++++++++++
 ...p-symbol-menu-button.h => gbp-symbol-popover.h} |  21 +-
 src/plugins/symbol-tree/gbp-symbol-popover.ui      |  51 ++
 src/plugins/symbol-tree/gbp-symbol-tree-builder.c  | 263 ----------
 src/plugins/symbol-tree/gbp-symbol-util.c          | 324 ++++++++++++
 src/plugins/symbol-tree/gbp-symbol-util.h          |  42 ++
 .../symbol-tree/gbp-symbol-workspace-addin.c       | 473 +++++++++++++++++
 ...-frame-addin.h => gbp-symbol-workspace-addin.h} |  10 +-
 src/plugins/symbol-tree/gtk/keybindings.json       |   1 +
 src/plugins/symbol-tree/meson.build                |   7 +-
 src/plugins/symbol-tree/symbol-tree-plugin.c       |  15 +-
 src/plugins/symbol-tree/symbol-tree.gresource.xml  |   5 +-
 src/plugins/symbol-tree/symbol-tree.plugin         |   6 +-
 src/plugins/symbol-tree/themes/shared.css          |  20 -
 21 files changed, 1643 insertions(+), 1353 deletions(-)
---
diff --git a/src/plugins/symbol-tree/gbp-symbol-hover-provider.c 
b/src/plugins/symbol-tree/gbp-symbol-hover-provider.c
index 75f06c190..3b1dd96ea 100644
--- a/src/plugins/symbol-tree/gbp-symbol-hover-provider.c
+++ b/src/plugins/symbol-tree/gbp-symbol-hover-provider.c
@@ -22,10 +22,11 @@
 
 #include "config.h"
 
+#include <glib/gi18n.h>
+
 #include <libide-code.h>
 #include <libide-gui.h>
 #include <libide-editor.h>
-#include <glib/gi18n.h>
 
 #include "gbp-symbol-hover-provider.h"
 
@@ -41,8 +42,8 @@ on_activate_link (GtkLabel    *label,
                   const gchar *uristr,
                   IdeLocation *location)
 {
+  g_autoptr(IdePanelPosition) position = NULL;
   IdeWorkspace *workspace;
-  IdeSurface *surface;
 
   g_assert (uristr != NULL);
   g_assert (GTK_IS_LABEL (label));
@@ -51,10 +52,8 @@ on_activate_link (GtkLabel    *label,
   if (!(workspace = ide_widget_get_workspace (GTK_WIDGET (label))))
     return FALSE;
 
-  if (!(surface = ide_workspace_get_surface_by_name (workspace, "editor")))
-    return FALSE;
-
-  ide_editor_surface_focus_location (IDE_EDITOR_SURFACE (surface), location);
+  position = ide_panel_position_new ();
+  ide_editor_focus_location (workspace, position, location);
 
   return TRUE;
 }
@@ -69,9 +68,9 @@ gbp_symbol_hover_provider_get_symbol_cb (GObject      *object,
   g_autoptr(IdeSymbol) symbol = NULL;
   g_autoptr(GError) error = NULL;
   g_autofree gchar *tt = NULL;
-  IdeHoverContext *context;
+  GtkSourceHoverDisplay *display;
   const gchar *name;
-  GtkWidget *box;
+  GtkBox *box;
   struct {
     const gchar *kind;
     IdeLocation *loc;
@@ -90,16 +89,19 @@ gbp_symbol_hover_provider_get_symbol_cb (GObject      *object,
       return;
     }
 
-  context = ide_task_get_task_data (task);
-  g_assert (context != NULL);
-  g_assert (IDE_IS_HOVER_CONTEXT (context));
+  display = ide_task_get_task_data (task);
+  g_assert (display != NULL);
+  g_assert (GTK_SOURCE_IS_HOVER_DISPLAY (display));
 
   loc[0].loc = ide_symbol_get_location (symbol);
   loc[1].loc = ide_symbol_get_header_location (symbol);
 
   if (!loc[0].loc && !loc[1].loc)
     {
-      ide_task_return_boolean (task, TRUE);
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "Not supported");
       return;
     }
 
@@ -113,16 +115,17 @@ gbp_symbol_hover_provider_get_symbol_cb (GObject      *object,
     name = _("Unnamed Symbol");
 
   tt = g_strdup_printf ("<tt><span size='smaller'>%s</span></tt>", name);
-  gtk_container_add (GTK_CONTAINER (box),
-                     g_object_new (GTK_TYPE_LABEL,
-                                   "ellipsize", PANGO_ELLIPSIZE_END,
-                                   "visible", TRUE,
-                                   "xalign", 0.0f,
-                                   "margin-bottom", 3,
-                                   "selectable", TRUE,
-                                   "use-markup", TRUE,
-                                   "label", tt,
-                                   NULL));
+
+  gtk_box_append (box,
+                  g_object_new (GTK_TYPE_LABEL,
+                                "ellipsize", PANGO_ELLIPSIZE_END,
+                                "visible", TRUE,
+                                "xalign", 0.0f,
+                                "margin-bottom", 3,
+                                "selectable", TRUE,
+                                "use-markup", TRUE,
+                                "label", tt,
+                                NULL));
 
 
   for (guint i = 0; i < G_N_ELEMENTS (loc); i++)
@@ -147,47 +150,52 @@ gbp_symbol_hover_provider_get_symbol_cb (GObject      *object,
                                  g_object_ref (loc[i].loc),
                                  (GClosureNotify)g_object_unref,
                                  0);
-          gtk_container_add (GTK_CONTAINER (box), label);
+          gtk_box_append (box, label);
         }
+
     }
 
-  ide_hover_context_add_widget (context, SYMBOL_TREE_HOVER_PRIORITY, _("Symbol"), box);
+  gtk_source_hover_display_append (display, GTK_WIDGET (box));
+
   ide_task_return_boolean (task, TRUE);
 }
 
 static void
-gbp_symbol_hover_provider_hover_async (IdeHoverProvider    *provider,
-                                       IdeHoverContext     *context,
-                                       const GtkTextIter   *iter,
-                                       GCancellable        *cancellable,
-                                       GAsyncReadyCallback  callback,
-                                       gpointer             user_data)
+gbp_symbol_hover_provider_populate_async (GtkSourceHoverProvider *provider,
+                                          GtkSourceHoverContext  *context,
+                                          GtkSourceHoverDisplay  *display,
+                                          GCancellable           *cancellable,
+                                          GAsyncReadyCallback     callback,
+                                          gpointer                user_data)
 {
   GbpSymbolHoverProvider *self = (GbpSymbolHoverProvider *)provider;
   g_autoptr(IdeTask) task = NULL;
+  GtkTextIter iter;
   IdeBuffer *buffer;
 
   g_assert (GBP_IS_SYMBOL_HOVER_PROVIDER (self));
-  g_assert (IDE_IS_HOVER_CONTEXT (context));
-  g_assert (iter != NULL);
+  g_assert (GTK_SOURCE_IS_HOVER_CONTEXT (context));
+  g_assert (GTK_SOURCE_IS_HOVER_DISPLAY (display));
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = ide_task_new (self, cancellable, callback, user_data);
-  ide_task_set_source_tag (task, gbp_symbol_hover_provider_hover_async);
-  ide_task_set_task_data (task, g_object_ref (context), g_object_unref);
+  ide_task_set_source_tag (task, gbp_symbol_hover_provider_populate_async);
+  ide_task_set_task_data (task, g_object_ref (display), g_object_unref);
+
+  gtk_source_hover_context_get_iter (context, &iter);
+  buffer = IDE_BUFFER (gtk_source_hover_context_get_buffer (context));
 
-  buffer = IDE_BUFFER (gtk_text_iter_get_buffer (iter));
   ide_buffer_get_symbol_at_location_async (buffer,
-                                           iter,
+                                           &iter,
                                            cancellable,
                                            gbp_symbol_hover_provider_get_symbol_cb,
                                            g_steal_pointer (&task));
 }
 
 static gboolean
-gbp_symbol_hover_provider_hover_finish (IdeHoverProvider  *provider,
-                                        GAsyncResult      *result,
-                                        GError           **error)
+gbp_symbol_hover_provider_populate_finish (GtkSourceHoverProvider  *provider,
+                                           GAsyncResult            *result,
+                                           GError                 **error)
 {
   g_assert (GBP_IS_SYMBOL_HOVER_PROVIDER (provider));
   g_assert (IDE_IS_TASK (result));
@@ -196,14 +204,14 @@ gbp_symbol_hover_provider_hover_finish (IdeHoverProvider  *provider,
 }
 
 static void
-hover_provider_iface_init (IdeHoverProviderInterface *iface)
+hover_provider_iface_init (GtkSourceHoverProviderInterface *iface)
 {
-  iface->hover_async = gbp_symbol_hover_provider_hover_async;
-  iface->hover_finish = gbp_symbol_hover_provider_hover_finish;
+  iface->populate_async = gbp_symbol_hover_provider_populate_async;
+  iface->populate_finish = gbp_symbol_hover_provider_populate_finish;
 }
 
 G_DEFINE_FINAL_TYPE_WITH_CODE (GbpSymbolHoverProvider, gbp_symbol_hover_provider, G_TYPE_OBJECT,
-                         G_IMPLEMENT_INTERFACE (IDE_TYPE_HOVER_PROVIDER, hover_provider_iface_init))
+                               G_IMPLEMENT_INTERFACE (GTK_SOURCE_TYPE_HOVER_PROVIDER, 
hover_provider_iface_init))
 
 static void
 gbp_symbol_hover_provider_class_init (GbpSymbolHoverProviderClass *klass)
diff --git a/src/plugins/symbol-tree/gbp-symbol-list-model.c b/src/plugins/symbol-tree/gbp-symbol-list-model.c
new file mode 100644
index 000000000..bde035a76
--- /dev/null
+++ b/src/plugins/symbol-tree/gbp-symbol-list-model.c
@@ -0,0 +1,184 @@
+/* gbp-symbol-list-model.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-symbol-list-model"
+
+#include "config.h"
+
+#include "gbp-symbol-list-model.h"
+
+struct _GbpSymbolListModel
+{
+  GObject        parent_instance;
+  IdeSymbolTree *tree;
+  IdeSymbolNode *parent;
+};
+
+enum {
+  PROP_0,
+  PROP_TREE,
+  PROP_PARENT,
+  N_PROPS
+};
+
+static GType
+gbp_symbol_list_model_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_SYMBOL_NODE;
+}
+
+static guint
+gbp_symbol_list_model_get_n_items (GListModel *model)
+{
+  GbpSymbolListModel *self = GBP_SYMBOL_LIST_MODEL (model);
+
+  return ide_symbol_tree_get_n_children (self->tree, self->parent);
+}
+
+static gpointer
+gbp_symbol_list_model_get_item (GListModel *model,
+                                guint       position)
+{
+  GbpSymbolListModel *self = GBP_SYMBOL_LIST_MODEL (model);
+
+  if (position >= ide_symbol_tree_get_n_children (self->tree, self->parent))
+    return NULL;
+
+  return ide_symbol_tree_get_nth_child (self->tree, self->parent, position);
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item = gbp_symbol_list_model_get_item;
+  iface->get_n_items = gbp_symbol_list_model_get_n_items;
+  iface->get_item_type = gbp_symbol_list_model_get_item_type;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpSymbolListModel, gbp_symbol_list_model, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gbp_symbol_list_model_dispose (GObject *object)
+{
+  GbpSymbolListModel *self = (GbpSymbolListModel *)object;
+
+  g_clear_object (&self->tree);
+  g_clear_object (&self->parent);
+
+  G_OBJECT_CLASS (gbp_symbol_list_model_parent_class)->dispose (object);
+}
+
+static void
+gbp_symbol_list_model_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  GbpSymbolListModel *self = GBP_SYMBOL_LIST_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_TREE:
+      g_value_set_object (value, self->tree);
+      break;
+
+    case PROP_PARENT:
+      g_value_set_object (value, self->parent);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_symbol_list_model_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  GbpSymbolListModel *self = GBP_SYMBOL_LIST_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_TREE:
+      self->tree = g_value_dup_object (value);
+      break;
+
+    case PROP_PARENT:
+      self->parent = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_symbol_list_model_class_init (GbpSymbolListModelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = gbp_symbol_list_model_dispose;
+  object_class->get_property = gbp_symbol_list_model_get_property;
+  object_class->set_property = gbp_symbol_list_model_set_property;
+
+  properties [PROP_TREE] =
+    g_param_spec_object ("tree",
+                         "Tree",
+                         "The tree of nodes",
+                         IDE_TYPE_SYMBOL_TREE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PARENT] =
+    g_param_spec_object ("parent",
+                         "Parent",
+                         "The parent node",
+                         IDE_TYPE_SYMBOL_NODE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gbp_symbol_list_model_init (GbpSymbolListModel *self)
+{
+}
+
+IdeSymbolTree *
+gbp_symbol_list_model_get_tree (GbpSymbolListModel *self)
+{
+  g_return_val_if_fail (GBP_IS_SYMBOL_LIST_MODEL (self), NULL);
+
+  return self->tree;
+}
+
+GbpSymbolListModel *
+gbp_symbol_list_model_new (IdeSymbolTree *tree,
+                           IdeSymbolNode *parent)
+{
+  return g_object_new (GBP_TYPE_SYMBOL_LIST_MODEL,
+                       "tree", tree,
+                       "parent", parent,
+                       NULL);
+}
diff --git a/src/plugins/symbol-tree/gbp-symbol-tree-builder.h 
b/src/plugins/symbol-tree/gbp-symbol-list-model.h
similarity index 59%
rename from src/plugins/symbol-tree/gbp-symbol-tree-builder.h
rename to src/plugins/symbol-tree/gbp-symbol-list-model.h
index f999f822c..55eb8b986 100644
--- a/src/plugins/symbol-tree/gbp-symbol-tree-builder.h
+++ b/src/plugins/symbol-tree/gbp-symbol-list-model.h
@@ -1,6 +1,6 @@
-/* gbp-symbol-tree-builder.h
+/* gbp-symbol-list-model.h
  *
- * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ * Copyright 2022 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,15 +20,16 @@
 
 #pragma once
 
-#include <libide-gui.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_SYMBOL_TREE_BUILDER (gbp_symbol_tree_builder_get_type())
+#define GBP_TYPE_SYMBOL_LIST_MODEL (gbp_symbol_list_model_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpSymbolTreeBuilder, gbp_symbol_tree_builder, GBP, SYMBOL_TREE_BUILDER, 
DzlTreeBuilder)
+G_DECLARE_FINAL_TYPE (GbpSymbolListModel, gbp_symbol_list_model, GBP, SYMBOL_LIST_MODEL, GObject)
 
-void gbp_symbol_tree_builder_set_filter (GbpSymbolTreeBuilder *self,
-                                         const gchar          *filter);
+GbpSymbolListModel *gbp_symbol_list_model_new      (IdeSymbolTree      *tree,
+                                                    IdeSymbolNode      *parent);
+IdeSymbolTree      *gbp_symbol_list_model_get_tree (GbpSymbolListModel *self);
 
 G_END_DECLS
diff --git a/src/plugins/symbol-tree/gbp-symbol-popover-row.ui 
b/src/plugins/symbol-tree/gbp-symbol-popover-row.ui
new file mode 100644
index 000000000..3e339354e
--- /dev/null
+++ b/src/plugins/symbol-tree/gbp-symbol-popover-row.ui
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GtkListItem">
+    <property name="child">
+      <object class="GtkTreeExpander" id="expander">
+        <binding name="list-row">
+          <lookup name="item">GtkListItem</lookup>
+        </binding>
+        <property name="child">
+          <object class="GtkBox">
+            <property name="spacing">6</property>
+            <property name="margin-top">3</property>
+            <property name="margin-bottom">3</property>
+            <property name="margin-start">3</property>
+            <property name="margin-end">3</property>
+            <child>
+              <object class="GtkImage">
+                <binding name="icon-name">
+                  <lookup name="icon-name" type="IdeSymbolNode">
+                    <lookup name="item">expander</lookup>
+                  </lookup>
+                </binding>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="halign">start</property>
+                <property name="use-markup">true</property>
+                <binding name="label">
+                  <lookup name="name" type="IdeSymbolNode">
+                    <lookup name="item">expander</lookup>
+                  </lookup>
+                </binding>
+              </object>
+            </child>
+          </object>
+        </property>
+      </object>
+    </property>
+  </template>
+</interface>
+
diff --git a/src/plugins/symbol-tree/gbp-symbol-popover.c b/src/plugins/symbol-tree/gbp-symbol-popover.c
new file mode 100644
index 000000000..87c231f37
--- /dev/null
+++ b/src/plugins/symbol-tree/gbp-symbol-popover.c
@@ -0,0 +1,433 @@
+/* gbp-symbol-popover.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-symbol-popover"
+
+#include "config.h"
+
+#include <libide-editor.h>
+#include <libide-gui.h>
+
+#include "gbp-symbol-list-model.h"
+#include "gbp-symbol-popover.h"
+
+struct _GbpSymbolPopover
+{
+  GtkPopover       parent_instance;
+
+  IdeSymbolTree   *symbol_tree;
+  GtkFilter       *filter;
+
+  GtkSearchEntry  *search_entry;
+  GtkListView     *list_view;
+
+  char           **search_needle;
+};
+
+enum {
+  PROP_0,
+  PROP_SYMBOL_TREE,
+  N_PROPS
+};
+
+G_DEFINE_FINAL_TYPE (GbpSymbolPopover, gbp_symbol_popover, GTK_TYPE_POPOVER)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gbp_symbol_popover_get_location_cb (GObject      *object,
+                                    GAsyncResult *result,
+                                    gpointer      user_data)
+{
+  IdeSymbolNode *node = (IdeSymbolNode *)object;
+  g_autoptr(GbpSymbolPopover) self = user_data;
+  g_autoptr(IdeLocation) location = NULL;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SYMBOL_NODE (node));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (GBP_IS_SYMBOL_POPOVER (self));
+
+  if ((location = ide_symbol_node_get_location_finish (node, result, &error)))
+    {
+      IdeWorkspace *workspace = ide_widget_get_workspace (GTK_WIDGET (self));
+      g_autoptr(IdePanelPosition) position = ide_panel_position_new ();
+
+      ide_editor_focus_location (workspace, position, location);
+
+      gtk_popover_popdown (GTK_POPOVER (self));
+    }
+
+  IDE_EXIT;
+}
+
+static void
+gbp_symbol_popover_activate_cb (GbpSymbolPopover *self,
+                                guint             position,
+                                GtkListView      *list_view)
+{
+  GtkTreeListRow *row;
+  IdeSymbolNode *node;
+  GListModel *model;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SYMBOL_POPOVER (self));
+  g_assert (GTK_IS_LIST_VIEW (list_view));
+
+  g_debug ("Activating symbol row at position %u", position);
+
+  model = G_LIST_MODEL (gtk_list_view_get_model (list_view));
+  row = g_list_model_get_item (model, position);
+  node = gtk_tree_list_row_get_item (row);
+
+  g_assert (IDE_IS_SYMBOL_NODE (node));
+
+  ide_symbol_node_get_location_async (node,
+                                      NULL,
+                                      gbp_symbol_popover_get_location_cb,
+                                      g_object_ref (self));
+
+  IDE_EXIT;
+}
+
+static void
+gbp_symbol_popover_search_activate_cb (GbpSymbolPopover *self,
+                                       GtkSearchEntry   *search_entry)
+{
+  guint position;
+
+  g_assert (GBP_IS_SYMBOL_POPOVER (self));
+  g_assert (GTK_IS_SEARCH_ENTRY (search_entry));
+
+  if (ide_gtk_list_view_get_selected_row (self->list_view, &position))
+    gbp_symbol_popover_activate_cb (self, position, self->list_view);
+}
+
+static void
+gbp_symbol_popover_search_changed_cb (GbpSymbolPopover *self,
+                                      GtkSearchEntry   *search_entry)
+{
+  const char *text;
+
+  g_assert (GBP_IS_SYMBOL_POPOVER (self));
+  g_assert (GTK_IS_SEARCH_ENTRY (search_entry));
+
+  text = gtk_editable_get_text (GTK_EDITABLE (search_entry));
+  g_clear_pointer (&self->search_needle, g_strfreev);
+
+  if (text && *text)
+    self->search_needle = g_str_tokenize_and_fold (text, NULL, NULL);
+
+  if (self->filter != NULL)
+    gtk_filter_changed (self->filter, GTK_FILTER_CHANGE_DIFFERENT);
+}
+
+static gboolean
+gbp_symbol_popover_grab_focus (GtkWidget *widget)
+{
+  GbpSymbolPopover *self = (GbpSymbolPopover *)widget;
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SYMBOL_POPOVER (self));
+
+  ret = gtk_widget_grab_focus (GTK_WIDGET (self->search_entry));
+  gtk_editable_select_region (GTK_EDITABLE (self->search_entry), 0, -1);
+
+  IDE_RETURN (ret);
+}
+
+static gboolean
+on_search_key_pressed_cb (GbpSymbolPopover      *self,
+                          guint                  keyval,
+                          guint                  keycode,
+                          GdkModifierType        state,
+                          GtkEventControllerKey *key)
+{
+  g_assert (GBP_IS_SYMBOL_POPOVER (self));
+  g_assert (GTK_IS_EVENT_CONTROLLER_KEY (key));
+
+  if ((state & (GDK_CONTROL_MASK | GDK_ALT_MASK)) == 0)
+    {
+      switch (keyval)
+        {
+        case GDK_KEY_Escape:
+          {
+            IdeWorkspace *workspace = ide_widget_get_workspace (GTK_WIDGET (self));
+            IdePage *page = ide_workspace_get_most_recent_page (workspace);
+
+            gtk_popover_popdown (GTK_POPOVER (self));
+            if (page)
+              gtk_widget_grab_focus (GTK_WIDGET (page));
+
+            return TRUE;
+          }
+
+        case GDK_KEY_Up:
+        case GDK_KEY_KP_Up:
+          ide_gtk_list_view_move_previous (self->list_view);
+          return TRUE;
+
+        case GDK_KEY_Down:
+        case GDK_KEY_KP_Down:
+          ide_gtk_list_view_move_next (self->list_view);
+          return TRUE;
+
+        default:
+          break;
+        }
+    }
+
+  return FALSE;
+}
+
+static void
+gbp_symbol_popover_dispose (GObject *object)
+{
+  GbpSymbolPopover *self = (GbpSymbolPopover *)object;
+
+  g_clear_object (&self->filter);
+  g_clear_object (&self->symbol_tree);
+  g_clear_pointer (&self->search_needle, g_strfreev);
+
+  G_OBJECT_CLASS (gbp_symbol_popover_parent_class)->dispose (object);
+}
+
+static void
+gbp_symbol_popover_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  GbpSymbolPopover *self = GBP_SYMBOL_POPOVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_SYMBOL_TREE:
+      g_value_set_object (value, gbp_symbol_popover_get_symbol_tree (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_symbol_popover_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  GbpSymbolPopover *self = GBP_SYMBOL_POPOVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_SYMBOL_TREE:
+      gbp_symbol_popover_set_symbol_tree (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_symbol_popover_class_init (GbpSymbolPopoverClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = gbp_symbol_popover_dispose;
+  object_class->get_property = gbp_symbol_popover_get_property;
+  object_class->set_property = gbp_symbol_popover_set_property;
+
+  widget_class->grab_focus = gbp_symbol_popover_grab_focus;
+
+  properties [PROP_SYMBOL_TREE] =
+    g_param_spec_object ("symbol-tree",
+                         "Symbol Tree",
+                         "The symbol tree to display",
+                         IDE_TYPE_SYMBOL_TREE,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/symbol-tree/gbp-symbol-popover.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpSymbolPopover, list_view);
+  gtk_widget_class_bind_template_child (widget_class, GbpSymbolPopover, search_entry);
+  gtk_widget_class_bind_template_callback (widget_class, gbp_symbol_popover_activate_cb);
+  gtk_widget_class_bind_template_callback (widget_class, gbp_symbol_popover_search_activate_cb);
+  gtk_widget_class_bind_template_callback (widget_class, gbp_symbol_popover_search_changed_cb);
+  gtk_widget_class_bind_template_callback (widget_class, on_search_key_pressed_cb);
+}
+
+static void
+gbp_symbol_popover_init (GbpSymbolPopover *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+GtkWidget *
+gbp_symbol_popover_new (void)
+{
+  return g_object_new (GBP_TYPE_SYMBOL_POPOVER, NULL);
+}
+
+IdeSymbolTree *
+gbp_symbol_popover_get_symbol_tree (GbpSymbolPopover *self)
+{
+  g_return_val_if_fail (GBP_IS_SYMBOL_POPOVER (self), NULL);
+  g_return_val_if_fail (!self->symbol_tree ||
+                        IDE_IS_SYMBOL_TREE (self->symbol_tree), NULL);
+
+  return self->symbol_tree;
+}
+
+static GListModel *
+get_child_model (gpointer item,
+                 gpointer user_data)
+{
+  IdeSymbolNode *node = item;
+  IdeSymbolTree *tree = user_data;
+
+  g_assert (IDE_IS_SYMBOL_NODE (node));
+  g_assert (IDE_IS_SYMBOL_TREE (tree));
+
+  if (ide_symbol_tree_get_n_children (tree, node) == 0)
+    return NULL;
+
+  return G_LIST_MODEL (gbp_symbol_list_model_new (tree, node));
+}
+
+static gboolean
+filter_node (IdeSymbolNode      *node,
+             const char * const *search_needle)
+{
+  /* Show only if the name matches every needle */
+  for (guint i = 0; search_needle[i]; i++)
+    {
+      const char *name = ide_symbol_node_get_name (node);
+
+      if (!name)
+        return FALSE;
+
+      if (g_str_match_string (search_needle[i], name, TRUE))
+        continue;
+
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+static gboolean
+filter_by_name (gpointer item,
+                gpointer user_data)
+{
+  GbpSymbolPopover *self = user_data;
+  GtkTreeListRow *row = item;
+  GtkTreeListRow *parent;
+  IdeSymbolNode *node;
+  GListModel *children;
+  guint i, n;
+
+  g_assert (GTK_IS_TREE_LIST_ROW (row));
+  g_assert (GBP_IS_SYMBOL_POPOVER (self));
+
+  /* Show all items if search is empty */
+  if (!self->search_needle || !self->search_needle[0] || !*self->search_needle[0])
+    return TRUE;
+
+  /* Show a row if itself or any parent matches */
+  for (parent = row; parent; parent = gtk_tree_list_row_get_parent (parent))
+    {
+      node = gtk_tree_list_row_get_item (parent);
+      g_assert (IDE_IS_SYMBOL_NODE (node));
+
+      if (filter_node (node, (const char * const *)self->search_needle))
+        return TRUE;
+    }
+
+  /* Show a row if any child matches */
+  if ((children = gtk_tree_list_row_get_children (row)))
+    {
+      n = g_list_model_get_n_items (children);
+      for (i = 0; i < n; i++)
+        {
+          gboolean ret;
+
+          node = g_list_model_get_item (children, i);
+          g_assert (IDE_IS_SYMBOL_NODE (node));
+
+          ret = filter_node (node, (const char * const *)self->search_needle);
+          g_object_unref (node);
+          if (ret)
+            return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+void
+gbp_symbol_popover_set_symbol_tree (GbpSymbolPopover *self,
+                                    IdeSymbolTree    *symbol_tree)
+{
+  g_return_if_fail (GBP_IS_SYMBOL_POPOVER (self));
+  g_return_if_fail (!symbol_tree || IDE_IS_SYMBOL_TREE (symbol_tree));
+
+  if (g_set_object (&self->symbol_tree, symbol_tree))
+    {
+      gtk_list_view_set_model (self->list_view, NULL);
+
+      if (symbol_tree != NULL)
+        {
+          GbpSymbolListModel *model;
+          GtkTreeListModel *tree_model;
+          GtkFilterListModel *filter_model;
+          GtkSingleSelection *selection;
+          GtkFilter *filter;
+
+          model = gbp_symbol_list_model_new (symbol_tree, NULL);
+          tree_model = gtk_tree_list_model_new (G_LIST_MODEL (model),
+                                                FALSE,
+                                                TRUE,
+                                                get_child_model,
+                                                g_object_ref (symbol_tree),
+                                                g_object_unref);
+
+          filter_model = gtk_filter_list_model_new (G_LIST_MODEL (tree_model), NULL);
+          filter = GTK_FILTER (gtk_custom_filter_new (filter_by_name, self, NULL));
+          gtk_filter_list_model_set_filter (filter_model, filter);
+          g_set_object (&self->filter, filter);
+          g_object_unref (filter);
+
+          selection = gtk_single_selection_new (G_LIST_MODEL (filter_model));
+          gtk_list_view_set_model (self->list_view, GTK_SELECTION_MODEL (selection));
+          g_object_unref (selection);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SYMBOL_TREE]);
+    }
+}
diff --git a/src/plugins/symbol-tree/gbp-symbol-menu-button.h b/src/plugins/symbol-tree/gbp-symbol-popover.h
similarity index 51%
rename from src/plugins/symbol-tree/gbp-symbol-menu-button.h
rename to src/plugins/symbol-tree/gbp-symbol-popover.h
index ebc5503f9..3ccb30074 100644
--- a/src/plugins/symbol-tree/gbp-symbol-menu-button.h
+++ b/src/plugins/symbol-tree/gbp-symbol-popover.h
@@ -1,6 +1,6 @@
-/* gbp-symbol-menu-button.h
+/* gbp-symbol-popover.h
  *
- * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2022 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,18 +20,19 @@
 
 #pragma once
 
-#include <libide-gui.h>
+#include <gtk/gtk.h>
+
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_SYMBOL_MENU_BUTTON (gbp_symbol_menu_button_get_type())
+#define GBP_TYPE_SYMBOL_POPOVER (gbp_symbol_popover_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpSymbolMenuButton, gbp_symbol_menu_button, GBP, SYMBOL_MENU_BUTTON, GtkMenuButton)
+G_DECLARE_FINAL_TYPE (GbpSymbolPopover, gbp_symbol_popover, GBP, SYMBOL_POPOVER, GtkPopover)
 
-void           gbp_symbol_menu_button_set_symbol      (GbpSymbolMenuButton *self,
-                                                       IdeSymbol           *symbol);
-IdeSymbolTree *gbp_symbol_menu_button_get_symbol_tree (GbpSymbolMenuButton *self);
-void           gbp_symbol_menu_button_set_symbol_tree (GbpSymbolMenuButton *self,
-                                                       IdeSymbolTree       *symbol_tree);
+GtkWidget     *gbp_symbol_popover_new             (void);
+IdeSymbolTree *gbp_symbol_popover_get_symbol_tree (GbpSymbolPopover *self);
+void           gbp_symbol_popover_set_symbol_tree (GbpSymbolPopover *self,
+                                                   IdeSymbolTree    *symbol_tree);
 
 G_END_DECLS
diff --git a/src/plugins/symbol-tree/gbp-symbol-popover.ui b/src/plugins/symbol-tree/gbp-symbol-popover.ui
new file mode 100644
index 000000000..c99333589
--- /dev/null
+++ b/src/plugins/symbol-tree/gbp-symbol-popover.ui
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpSymbolPopover" parent="GtkPopover">
+    <property name="width-request">400</property>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="spacing">6</property>
+        <child>
+          <object class="GtkFrame">
+            <child>
+              <object class="GtkScrolledWindow" id="scroller">
+                <property name="propagate-natural-height">true</property>
+                <property name="propagate-natural-width">true</property>
+                <property name="min-content-height">100</property>
+                <property name="max-content-height">600</property>
+                <property name="min-content-width">400</property>
+                <property name="max-content-width">400</property>
+                <child>
+                  <object class="GtkListView" id="list_view">
+                    <signal name="activate" handler="gbp_symbol_popover_activate_cb" swapped="true" 
object="GbpSymbolPopover"/>
+                    <property name="orientation">vertical</property>
+                    <property name="single-click-activate">True</property>
+                    <property name="factory">
+                      <object class="GtkBuilderListItemFactory">
+                        <property name="resource">/plugins/symbol-tree/gbp-symbol-popover-row.ui</property>
+                      </object>
+                    </property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkSearchEntry" id="search_entry">
+            <property name="placeholder-text" translatable="yes">Filter Symbols…</property>
+            <signal name="activate" handler="gbp_symbol_popover_search_activate_cb" swapped="true" 
object="GbpSymbolPopover"/>
+            <signal name="search-changed" handler="gbp_symbol_popover_search_changed_cb" swapped="true" 
object="GbpSymbolPopover"/>
+            <child>
+              <object class="GtkEventControllerKey">
+                <property name="propagation-phase">capture</property>
+                <signal name="key-pressed" handler="on_search_key_pressed_cb" swapped="true" 
object="GbpSymbolPopover"/>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/symbol-tree/gbp-symbol-util.c b/src/plugins/symbol-tree/gbp-symbol-util.c
new file mode 100644
index 000000000..f82a752dd
--- /dev/null
+++ b/src/plugins/symbol-tree/gbp-symbol-util.c
@@ -0,0 +1,324 @@
+/* gbp-symbol-util.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-symbol-util"
+
+#include "config.h"
+
+#include <libide-threading.h>
+
+#include "gbp-symbol-util.h"
+
+typedef struct
+{
+  GPtrArray   *resolvers;
+  IdeBuffer   *buffer;
+  IdeLocation *location;
+} FindNearestScope;
+
+typedef struct
+{
+  GPtrArray *resolvers;
+  IdeBuffer *buffer;
+  GFile     *file;
+  GBytes    *contents;
+} GetSymbolTree;
+
+static void
+get_symbol_tree_free (GetSymbolTree *data)
+{
+  g_assert (data != NULL);
+  g_assert (data->resolvers != NULL);
+  g_assert (data->buffer != NULL);
+  g_assert (IDE_IS_BUFFER (data->buffer));
+  g_assert (G_IS_FILE (data->file));
+  g_assert (data->contents != NULL);
+
+  g_clear_pointer (&data->resolvers, g_ptr_array_unref);
+  g_clear_pointer (&data->buffer, ide_buffer_release);
+  g_clear_pointer (&data->contents, g_bytes_unref);
+  g_clear_object (&data->file);
+  g_slice_free (GetSymbolTree, data);
+}
+
+static void
+find_nearest_scope_free (FindNearestScope *data)
+{
+  g_assert (data != NULL);
+  g_assert (data->resolvers != NULL);
+  g_assert (data->buffer != NULL);
+  g_assert (IDE_IS_BUFFER (data->buffer));
+  g_assert (IDE_IS_LOCATION (data->location));
+
+  g_clear_pointer (&data->resolvers, g_ptr_array_unref);
+  g_clear_pointer (&data->buffer, ide_buffer_release);
+  g_clear_object (&data->location);
+  g_slice_free (FindNearestScope, data);
+}
+
+static void
+gbp_symbol_find_nearest_scope_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  IdeSymbolResolver *resolver = (IdeSymbolResolver *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(IdeSymbol) symbol = NULL;
+  g_autoptr(GError) error = NULL;
+  FindNearestScope *data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SYMBOL_RESOLVER (resolver));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if ((symbol = ide_symbol_resolver_find_nearest_scope_finish (resolver, result, &error)))
+    {
+      ide_task_return_object (task, g_steal_pointer (&symbol));
+      IDE_EXIT;
+    }
+
+  data = ide_task_get_task_data (task);
+
+  g_assert (data != NULL);
+  g_assert (data->resolvers != NULL);
+  g_assert (data->resolvers->len > 0);
+  g_assert (IDE_IS_LOCATION (data->location));
+  g_assert (IDE_IS_BUFFER (data->buffer));
+
+  g_ptr_array_remove_index (data->resolvers,
+                            data->resolvers->len - 1);
+
+  if (data->resolvers->len == 0)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "No resolvers could locate the nearest scope");
+      IDE_EXIT;
+    }
+
+  ide_symbol_resolver_find_nearest_scope_async (g_ptr_array_index (data->resolvers, data->resolvers->len - 
1),
+                                                data->location,
+                                                ide_task_get_cancellable (task),
+                                                gbp_symbol_find_nearest_scope_cb,
+                                                g_object_ref (task));
+
+  IDE_EXIT;
+}
+
+void
+gbp_symbol_find_nearest_scope_async (IdeBuffer           *buffer,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) resolvers = NULL;
+  FindNearestScope *data;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (buffer, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_symbol_find_nearest_scope_async);
+
+  resolvers = ide_buffer_get_symbol_resolvers (buffer);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (resolvers, g_object_unref);
+
+  if (resolvers == NULL || resolvers->len == 0)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "No symbol resolvers available");
+      IDE_EXIT;
+    }
+
+  data = g_slice_new0 (FindNearestScope);
+  data->resolvers = g_steal_pointer (&resolvers);
+  data->buffer = ide_buffer_hold (buffer);
+  data->location = ide_buffer_get_insert_location (buffer);
+  ide_task_set_task_data (task, data, find_nearest_scope_free);
+
+  ide_symbol_resolver_find_nearest_scope_async (g_ptr_array_index (data->resolvers, data->resolvers->len - 
1),
+                                                data->location,
+                                                cancellable,
+                                                gbp_symbol_find_nearest_scope_cb,
+                                                g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+IdeSymbol *
+gbp_symbol_find_nearest_scope_finish (IdeBuffer     *buffer,
+                                      GAsyncResult  *result,
+                                      GError       **error)
+{
+  IdeSymbol *ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUFFER (buffer), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+
+  ret = ide_task_propagate_object (IDE_TASK (result), error);
+
+  g_return_val_if_fail (!ret || IDE_IS_SYMBOL (ret), NULL);
+
+  IDE_RETURN (ret);
+}
+
+static void
+gbp_symbol_get_symbol_tree_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  IdeSymbolResolver *resolver = (IdeSymbolResolver *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(IdeSymbolTree) tree = NULL;
+  g_autoptr(GError) error = NULL;
+  GetSymbolTree *data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SYMBOL_RESOLVER (resolver));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if ((tree = ide_symbol_resolver_get_symbol_tree_finish (resolver, result, &error)))
+    {
+      ide_task_return_object (task, g_steal_pointer (&tree));
+      IDE_EXIT;
+    }
+
+  data = ide_task_get_task_data (task);
+
+  g_assert (data != NULL);
+  g_assert (data->resolvers != NULL);
+  g_assert (data->resolvers->len > 0);
+  g_assert (IDE_IS_BUFFER (data->buffer));
+  g_assert (G_IS_FILE (data->file));
+  g_assert (data->contents != NULL);
+
+  g_ptr_array_remove_index (data->resolvers,
+                            data->resolvers->len - 1);
+
+  if (data->resolvers->len == 0)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "No resolvers could create a symbol tree");
+      IDE_EXIT;
+    }
+
+  ide_symbol_resolver_get_symbol_tree_async (g_ptr_array_index (data->resolvers, data->resolvers->len - 1),
+                                             data->file,
+                                             data->contents,
+                                             ide_task_get_cancellable (task),
+                                             gbp_symbol_get_symbol_tree_cb,
+                                             g_object_ref (task));
+
+  IDE_EXIT;
+}
+
+void
+gbp_symbol_get_symbol_tree_async (IdeBuffer           *buffer,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) resolvers = NULL;
+  g_autoptr(GBytes) contents = NULL;
+  GetSymbolTree *data;
+  GFile *file;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (buffer, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_symbol_get_symbol_tree_async);
+
+  file = ide_buffer_get_file (buffer);
+  contents = ide_buffer_dup_content (buffer);
+
+  if (file == NULL || contents == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "Symbol resolvers require both a file and buffer contents");
+      IDE_EXIT;
+    }
+
+  resolvers = ide_buffer_get_symbol_resolvers (buffer);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (resolvers, g_object_unref);
+
+  if (resolvers == NULL || resolvers->len == 0)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "No symbol resolvers available");
+      IDE_EXIT;
+    }
+
+  data = g_slice_new0 (GetSymbolTree);
+  data->resolvers = g_steal_pointer (&resolvers);
+  data->buffer = ide_buffer_hold (buffer);
+  data->file = g_object_ref (file);
+  data->contents = g_steal_pointer (&contents);
+  ide_task_set_task_data (task, data, get_symbol_tree_free);
+
+  ide_symbol_resolver_get_symbol_tree_async (g_ptr_array_index (data->resolvers, data->resolvers->len - 1),
+                                             data->file,
+                                             data->contents,
+                                             cancellable,
+                                             gbp_symbol_get_symbol_tree_cb,
+                                             g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+IdeSymbolTree *
+gbp_symbol_get_symbol_tree_finish (IdeBuffer     *buffer,
+                                   GAsyncResult  *result,
+                                   GError       **error)
+{
+  IdeSymbolTree *ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUFFER (buffer), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+
+  ret = ide_task_propagate_object (IDE_TASK (result), error);
+
+  g_return_val_if_fail (!ret || IDE_IS_SYMBOL_TREE (ret), NULL);
+
+  IDE_RETURN (ret);
+}
diff --git a/src/plugins/symbol-tree/gbp-symbol-util.h b/src/plugins/symbol-tree/gbp-symbol-util.h
new file mode 100644
index 000000000..ae1a35f04
--- /dev/null
+++ b/src/plugins/symbol-tree/gbp-symbol-util.h
@@ -0,0 +1,42 @@
+/* gbp-symbol-util.h
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-code.h>
+
+G_BEGIN_DECLS
+
+void           gbp_symbol_find_nearest_scope_async  (IdeBuffer            *buffer,
+                                                     GCancellable         *cancellable,
+                                                     GAsyncReadyCallback   callback,
+                                                     gpointer              user_data);
+IdeSymbol     *gbp_symbol_find_nearest_scope_finish (IdeBuffer            *buffer,
+                                                     GAsyncResult         *result,
+                                                     GError              **error);
+void           gbp_symbol_get_symbol_tree_async     (IdeBuffer            *buffer,
+                                                     GCancellable         *cancellable,
+                                                     GAsyncReadyCallback   callback,
+                                                     gpointer              user_data);
+IdeSymbolTree *gbp_symbol_get_symbol_tree_finish    (IdeBuffer            *buffer,
+                                                     GAsyncResult         *result,
+                                                     GError              **error);
+
+G_END_DECLS
diff --git a/src/plugins/symbol-tree/gbp-symbol-workspace-addin.c 
b/src/plugins/symbol-tree/gbp-symbol-workspace-addin.c
new file mode 100644
index 000000000..2737b2bb2
--- /dev/null
+++ b/src/plugins/symbol-tree/gbp-symbol-workspace-addin.c
@@ -0,0 +1,473 @@
+/* gbp-symbol-workspace-addin.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-symbol-workspace-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include <libide-code.h>
+#include <libide-editor.h>
+#include <libide-gui.h>
+
+#include "gbp-symbol-popover.h"
+#include "gbp-symbol-workspace-addin.h"
+#include "gbp-symbol-util.h"
+
+#define NEAREST_SCOPE_DELAY_MSEC 500
+#define SYMBOL_TREE_DELAY_MSEC   1000
+
+struct _GbpSymbolWorkspaceAddin
+{
+  GObject           parent_instance;
+
+  IdeWorkspace     *workspace;
+  PanelStatusbar   *statusbar;
+
+  GtkMenuButton    *menu_button;
+  GtkLabel         *menu_label;
+  GtkImage         *menu_image;
+  GbpSymbolPopover *popover;
+
+  IdeSignalGroup   *buffer_signals;
+  guint             nearest_scope_timeout_source;
+  guint             symbol_tree_timeout_source;
+};
+
+static void
+gbp_symbol_workspace_addin_set_symbol (GbpSymbolWorkspaceAddin *self,
+                                       IdeSymbol               *symbol)
+{
+  const char *label = NULL;
+  const char *icon_name = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SYMBOL_WORKSPACE_ADDIN (self));
+  g_assert (!symbol || IDE_IS_SYMBOL (symbol));
+
+  if (symbol != NULL)
+    {
+      icon_name = ide_symbol_kind_get_icon_name (ide_symbol_get_kind (symbol));
+      label = ide_symbol_get_name (symbol);
+
+      if (ide_str_empty0 (label))
+        label = NULL;
+    }
+
+  if (label == NULL)
+    {
+      gtk_label_set_label (self->menu_label, _("Select Symbol…"));
+      gtk_image_set_from_icon_name (self->menu_image, NULL);
+      gtk_widget_hide (GTK_WIDGET (self->menu_image));
+      IDE_EXIT;
+    }
+
+  gtk_label_set_label (self->menu_label, label);
+  gtk_image_set_from_icon_name (self->menu_image, icon_name);
+  gtk_widget_set_visible (GTK_WIDGET (self->menu_image), icon_name != NULL);
+
+  IDE_EXIT;
+}
+
+static void
+gbp_symbol_workspace_addin_find_nearest_scope_cb (GObject      *object,
+                                                  GAsyncResult *result,
+                                                  gpointer      user_data)
+{
+  IdeBuffer *buffer = (IdeBuffer *)object;
+  g_autoptr(GbpSymbolWorkspaceAddin) self = user_data;
+  g_autoptr(IdeSymbol) symbol = NULL;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (GBP_IS_SYMBOL_WORKSPACE_ADDIN (self));
+
+  if (!(symbol = gbp_symbol_find_nearest_scope_finish (buffer, result, &error)))
+    {
+      if (!ide_error_ignore (error))
+        g_warning ("Failed to get symbol at location: %s", error->message);
+      IDE_GOTO (failure);
+    }
+
+  if ((gpointer)buffer != ide_signal_group_get_target (self->buffer_signals))
+    IDE_GOTO (failure);
+
+  gbp_symbol_workspace_addin_set_symbol (self, symbol);
+  gtk_widget_show (GTK_WIDGET (self->menu_button));
+
+  IDE_EXIT;
+
+failure:
+
+  /* Raced against another query or cleanup and lost, just bail */
+  if (ide_signal_group_get_target (self->buffer_signals) != buffer)
+    IDE_EXIT;
+
+  gbp_symbol_workspace_addin_set_symbol (self, NULL);
+  gtk_widget_show (GTK_WIDGET (self->menu_button));
+
+  IDE_EXIT;
+}
+
+static void
+gbp_symbol_workspace_addin_get_symbol_tree_cb (GObject      *object,
+                                               GAsyncResult *result,
+                                               gpointer      user_data)
+{
+  IdeBuffer *buffer = (IdeBuffer *)object;
+  g_autoptr(GbpSymbolWorkspaceAddin) self = user_data;
+  g_autoptr(IdeSymbolTree) tree = NULL;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (GBP_IS_SYMBOL_WORKSPACE_ADDIN (self));
+
+  if (!(tree = gbp_symbol_get_symbol_tree_finish (buffer, result, &error)))
+    {
+      if (!ide_error_ignore (error))
+        g_warning ("Failed to get symbol tree: %s", error->message);
+      IDE_GOTO (failure);
+    }
+
+  if ((gpointer)buffer != ide_signal_group_get_target (self->buffer_signals))
+    IDE_GOTO (failure);
+
+  gbp_symbol_popover_set_symbol_tree (self->popover, tree);
+
+  IDE_EXIT;
+
+failure:
+
+  /* Raced against another query and lost, just bail */
+  if ((gpointer)buffer != ide_signal_group_get_target (self->buffer_signals))
+    IDE_EXIT;
+
+  gbp_symbol_popover_set_symbol_tree (self->popover, NULL);
+
+  IDE_EXIT;
+}
+
+static void
+gbp_symbol_workspace_addin_update_nearest_scope (GbpSymbolWorkspaceAddin *self,
+                                                 IdeBuffer               *buffer)
+{
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SYMBOL_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  if (!ide_buffer_has_symbol_resolvers (buffer))
+    {
+      gbp_symbol_workspace_addin_set_symbol (self, NULL);
+      gtk_widget_hide (GTK_WIDGET (self->menu_button));
+      IDE_EXIT;
+    }
+
+  gbp_symbol_find_nearest_scope_async (buffer,
+                                       NULL,
+                                       gbp_symbol_workspace_addin_find_nearest_scope_cb,
+                                       g_object_ref (self));
+
+  IDE_EXIT;
+}
+
+static void
+gbp_symbol_workspace_addin_update_symbol_tree (GbpSymbolWorkspaceAddin *self,
+                                               IdeBuffer               *buffer)
+{
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SYMBOL_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  if (!ide_buffer_has_symbol_resolvers (buffer))
+    {
+      gbp_symbol_popover_set_symbol_tree (self->popover, NULL);
+      IDE_EXIT;
+    }
+
+  gbp_symbol_get_symbol_tree_async (buffer,
+                                    NULL,
+                                    gbp_symbol_workspace_addin_get_symbol_tree_cb,
+                                    g_object_ref (self));
+
+  IDE_EXIT;
+}
+
+static void
+gbp_symbol_workspace_addin_buffer_bind_cb (GbpSymbolWorkspaceAddin *self,
+                                           IdeBuffer               *buffer,
+                                           IdeSignalGroup          *signal_group)
+{
+  g_assert (GBP_IS_SYMBOL_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (IDE_IS_SIGNAL_GROUP (signal_group));
+
+  gbp_symbol_workspace_addin_update_nearest_scope (self, buffer);
+  gbp_symbol_workspace_addin_update_symbol_tree (self, buffer);
+}
+
+static gboolean
+gbp_symbol_workspace_addin_nearest_scope_timeout (gpointer data)
+{
+  GbpSymbolWorkspaceAddin *self = data;
+  IdeBuffer *buffer;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SYMBOL_WORKSPACE_ADDIN (self));
+
+  self->nearest_scope_timeout_source = 0;
+
+  if ((buffer = ide_signal_group_get_target (self->buffer_signals)))
+    gbp_symbol_workspace_addin_update_nearest_scope (self, buffer);
+  else
+    gtk_widget_hide (GTK_WIDGET (self->menu_button));
+
+  IDE_RETURN (G_SOURCE_REMOVE);
+}
+
+static void
+gbp_symbol_workspace_addin_buffer_cursor_moved_cb (GbpSymbolWorkspaceAddin *self,
+                                                   IdeBuffer               *buffer)
+{
+  g_assert (GBP_IS_SYMBOL_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  if (self->nearest_scope_timeout_source == 0)
+    self->nearest_scope_timeout_source = g_timeout_add (NEAREST_SCOPE_DELAY_MSEC,
+                                                        gbp_symbol_workspace_addin_nearest_scope_timeout,
+                                                        self);
+}
+
+static gboolean
+gbp_symbol_workspace_addin_symbol_tree_timeout (gpointer data)
+{
+  GbpSymbolWorkspaceAddin *self = data;
+  IdeBuffer *buffer;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SYMBOL_WORKSPACE_ADDIN (self));
+
+  self->symbol_tree_timeout_source = 0;
+
+  if ((buffer = ide_signal_group_get_target (self->buffer_signals)))
+    gbp_symbol_workspace_addin_update_symbol_tree (self, buffer);
+  else
+    gbp_symbol_popover_set_symbol_tree (self->popover, NULL);
+
+  IDE_RETURN (G_SOURCE_REMOVE);
+}
+
+static void
+gbp_symbol_workspace_addin_buffer_changed_cb (GbpSymbolWorkspaceAddin *self,
+                                              IdeBuffer               *buffer)
+{
+  g_assert (GBP_IS_SYMBOL_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  g_clear_handle_id (&self->symbol_tree_timeout_source, g_source_remove);
+  self->symbol_tree_timeout_source = g_timeout_add (SYMBOL_TREE_DELAY_MSEC,
+                                                    gbp_symbol_workspace_addin_symbol_tree_timeout,
+                                                    self);
+}
+
+static void
+focus_symbol_tree (GSimpleAction *action,
+                   GVariant      *param,
+                   gpointer       user_data)
+{
+  GbpSymbolWorkspaceAddin *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_SYMBOL_WORKSPACE_ADDIN (self));
+
+  if (gtk_widget_get_visible (GTK_WIDGET (self->menu_button)))
+    {
+      gtk_menu_button_popup (self->menu_button);
+      gtk_widget_grab_focus (GTK_WIDGET (self->popover));
+    }
+}
+
+static const GActionEntry actions[] = {
+  { "focus-symbol-tree", focus_symbol_tree },
+};
+
+static void
+gbp_symbol_workspace_addin_load (IdeWorkspaceAddin *addin,
+                                 IdeWorkspace      *workspace)
+{
+  GbpSymbolWorkspaceAddin *self = (GbpSymbolWorkspaceAddin *)addin;
+  GtkBox *box;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SYMBOL_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  self->workspace = workspace;
+  self->statusbar = ide_workspace_get_statusbar (workspace);
+
+  g_action_map_add_action_entries (G_ACTION_MAP (workspace),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+
+  box = g_object_new (GTK_TYPE_BOX,
+                      "orientation", GTK_ORIENTATION_HORIZONTAL,
+                      "spacing", 6,
+                      NULL);
+  self->menu_image = g_object_new (GTK_TYPE_IMAGE,
+                                   "icon-name", "lang-function-symbolic",
+                                   NULL);
+  gtk_box_append (box, GTK_WIDGET (self->menu_image));
+  self->menu_label = g_object_new (GTK_TYPE_LABEL,
+                                   "label", _("Select Symbol…"),
+                                   "xalign", 0.0f,
+                                   "ellipsize", PANGO_ELLIPSIZE_END,
+                                   NULL);
+  gtk_box_append (box, GTK_WIDGET (self->menu_label));
+  self->popover = GBP_SYMBOL_POPOVER (gbp_symbol_popover_new ());
+  self->menu_button = g_object_new (GTK_TYPE_MENU_BUTTON,
+                                    "child", box,
+                                    "direction", GTK_ARROW_UP,
+                                    "popover", self->popover,
+                                    "visible", FALSE,
+                                    NULL);
+
+  panel_statusbar_add_suffix (self->statusbar, 20000, GTK_WIDGET (self->menu_button));
+
+  IDE_EXIT;
+}
+
+static void
+gbp_symbol_workspace_addin_unload (IdeWorkspaceAddin *addin,
+                                   IdeWorkspace      *workspace)
+{
+  GbpSymbolWorkspaceAddin *self = (GbpSymbolWorkspaceAddin *)addin;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SYMBOL_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+  g_assert (PANEL_IS_STATUSBAR (self->statusbar));
+  g_assert (workspace == self->workspace);
+
+  for (guint i = 0; i < G_N_ELEMENTS (actions); i++)
+    g_action_map_remove_action (G_ACTION_MAP (workspace), actions[i].name);
+
+  ide_signal_group_set_target (self->buffer_signals, NULL);
+
+  g_clear_handle_id (&self->nearest_scope_timeout_source, g_source_remove);
+  g_clear_handle_id (&self->symbol_tree_timeout_source, g_source_remove);
+
+  panel_statusbar_remove (self->statusbar, GTK_WIDGET (self->menu_button));
+
+  self->menu_button = NULL;
+  self->menu_label = NULL;
+  self->menu_image = NULL;
+  self->workspace = NULL;
+  self->statusbar = NULL;
+
+  IDE_EXIT;
+}
+
+static void
+gbp_symbol_workspace_addin_page_changed (IdeWorkspaceAddin *addin,
+                                         IdePage           *page)
+{
+  GbpSymbolWorkspaceAddin *self = (GbpSymbolWorkspaceAddin *)addin;
+  IdeBuffer *buffer = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SYMBOL_WORKSPACE_ADDIN (self));
+  g_assert (!page || IDE_IS_PAGE (page));
+
+  gbp_symbol_workspace_addin_set_symbol (self, NULL);
+  gtk_widget_hide (GTK_WIDGET (self->menu_button));
+
+  if (IDE_IS_EDITOR_PAGE (page))
+    buffer = ide_editor_page_get_buffer (IDE_EDITOR_PAGE (page));
+
+  ide_signal_group_set_target (self->buffer_signals, buffer);
+
+  IDE_EXIT;
+}
+
+static void
+workspace_addin_iface_init (IdeWorkspaceAddinInterface *iface)
+{
+  iface->load = gbp_symbol_workspace_addin_load;
+  iface->unload = gbp_symbol_workspace_addin_unload;
+  iface->page_changed = gbp_symbol_workspace_addin_page_changed;
+}
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (GbpSymbolWorkspaceAddin, gbp_symbol_workspace_addin, G_TYPE_OBJECT,
+                               G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKSPACE_ADDIN, workspace_addin_iface_init))
+
+static void
+gbp_symbol_workspace_addin_finalize (GObject *object)
+{
+  GbpSymbolWorkspaceAddin *self = (GbpSymbolWorkspaceAddin *)object;
+
+  g_clear_object (&self->buffer_signals);
+
+  G_OBJECT_CLASS (gbp_symbol_workspace_addin_parent_class)->finalize (object);
+}
+
+static void
+gbp_symbol_workspace_addin_class_init (GbpSymbolWorkspaceAddinClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gbp_symbol_workspace_addin_finalize;
+}
+
+static void
+gbp_symbol_workspace_addin_init (GbpSymbolWorkspaceAddin *self)
+{
+  self->buffer_signals = ide_signal_group_new (IDE_TYPE_BUFFER);
+  g_signal_connect_object (self->buffer_signals,
+                           "bind",
+                           G_CALLBACK (gbp_symbol_workspace_addin_buffer_bind_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  ide_signal_group_connect_object (self->buffer_signals,
+                                   "cursor-moved",
+                                   G_CALLBACK (gbp_symbol_workspace_addin_buffer_cursor_moved_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  ide_signal_group_connect_object (self->buffer_signals,
+                                   "changed",
+                                   G_CALLBACK (gbp_symbol_workspace_addin_buffer_changed_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+}
diff --git a/src/plugins/symbol-tree/gbp-symbol-frame-addin.h 
b/src/plugins/symbol-tree/gbp-symbol-workspace-addin.h
similarity index 70%
rename from src/plugins/symbol-tree/gbp-symbol-frame-addin.h
rename to src/plugins/symbol-tree/gbp-symbol-workspace-addin.h
index db160ac53..e0cd9e580 100644
--- a/src/plugins/symbol-tree/gbp-symbol-frame-addin.h
+++ b/src/plugins/symbol-tree/gbp-symbol-workspace-addin.h
@@ -1,6 +1,6 @@
-/* gbp-symbol-frame-addin.h
+/* gbp-symbol-workspace-addin.h
  *
- * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2022 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,12 +20,12 @@
 
 #pragma once
 
-#include <libide-gui.h>
+#include <glib-object.h>
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_SYMBOL_FRAME_ADDIN (gbp_symbol_frame_addin_get_type())
+#define GBP_TYPE_SYMBOL_WORKSPACE_ADDIN (gbp_symbol_workspace_addin_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpSymbolFrameAddin, gbp_symbol_frame_addin, GBP, SYMBOL_FRAME_ADDIN, GObject)
+G_DECLARE_FINAL_TYPE (GbpSymbolWorkspaceAddin, gbp_symbol_workspace_addin, GBP, SYMBOL_WORKSPACE_ADDIN, 
GObject)
 
 G_END_DECLS
diff --git a/src/plugins/symbol-tree/gtk/keybindings.json b/src/plugins/symbol-tree/gtk/keybindings.json
new file mode 100644
index 000000000..00758dae5
--- /dev/null
+++ b/src/plugins/symbol-tree/gtk/keybindings.json
@@ -0,0 +1 @@
+{ "trigger" : "<Control><Shift>k", "action" : "win.focus-symbol-tree", "when" : "canEdit()", "phase" : 
"capture" },
diff --git a/src/plugins/symbol-tree/meson.build b/src/plugins/symbol-tree/meson.build
index 1eeb61f54..94f64dc50 100644
--- a/src/plugins/symbol-tree/meson.build
+++ b/src/plugins/symbol-tree/meson.build
@@ -1,8 +1,9 @@
 plugins_sources += files([
   'gbp-symbol-hover-provider.c',
-  'gbp-symbol-frame-addin.c',
-  'gbp-symbol-menu-button.c',
-  'gbp-symbol-tree-builder.c',
+  'gbp-symbol-list-model.c',
+  'gbp-symbol-popover.c',
+  'gbp-symbol-util.c',
+  'gbp-symbol-workspace-addin.c',
   'symbol-tree-plugin.c',
 ])
 
diff --git a/src/plugins/symbol-tree/symbol-tree-plugin.c b/src/plugins/symbol-tree/symbol-tree-plugin.c
index 0408a034e..b641650a3 100644
--- a/src/plugins/symbol-tree/symbol-tree-plugin.c
+++ b/src/plugins/symbol-tree/symbol-tree-plugin.c
@@ -1,6 +1,6 @@
 /* symbol-tree-plugin.c
  *
- * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2022 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,19 +21,20 @@
 #include "config.h"
 
 #include <libpeas/peas.h>
-#include <libide-sourceview.h>
+
 #include <libide-gui.h>
+#include <libide-sourceview.h>
 
-#include "gbp-symbol-frame-addin.h"
 #include "gbp-symbol-hover-provider.h"
+#include "gbp-symbol-workspace-addin.h"
 
 _IDE_EXTERN void
 _gbp_symbol_tree_register_types (PeasObjectModule *module)
 {
   peas_object_module_register_extension_type (module,
-                                              IDE_TYPE_FRAME_ADDIN,
-                                              GBP_TYPE_SYMBOL_FRAME_ADDIN);
-  peas_object_module_register_extension_type (module,
-                                              IDE_TYPE_HOVER_PROVIDER,
+                                              GTK_SOURCE_TYPE_HOVER_PROVIDER,
                                               GBP_TYPE_SYMBOL_HOVER_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKSPACE_ADDIN,
+                                              GBP_TYPE_SYMBOL_WORKSPACE_ADDIN);
 }
diff --git a/src/plugins/symbol-tree/symbol-tree.gresource.xml 
b/src/plugins/symbol-tree/symbol-tree.gresource.xml
index 44bbb417c..63075fa12 100644
--- a/src/plugins/symbol-tree/symbol-tree.gresource.xml
+++ b/src/plugins/symbol-tree/symbol-tree.gresource.xml
@@ -2,7 +2,8 @@
 <gresources>
   <gresource prefix="/plugins/symbol-tree">
     <file>symbol-tree.plugin</file>
-    <file>gbp-symbol-menu-button.ui</file>
-    <file>themes/shared.css</file>
+    <file>gtk/keybindings.json</file>
+    <file preprocess="xml-stripblanks">gbp-symbol-popover.ui</file>
+    <file preprocess="xml-stripblanks">gbp-symbol-popover-row.ui</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/symbol-tree/symbol-tree.plugin b/src/plugins/symbol-tree/symbol-tree.plugin
index cc49c678d..923746413 100644
--- a/src/plugins/symbol-tree/symbol-tree.plugin
+++ b/src/plugins/symbol-tree/symbol-tree.plugin
@@ -1,10 +1,10 @@
 [Plugin]
 Authors=Christian Hergert <christian hergert me>
 Builtin=true
-Copyright=Copyright © 2015-2018 Christian Hergert
-Depends=editor;
+Copyright=Copyright © 2015-2022 Christian Hergert
 Description=Provides a Symbol Tree for the currently focused document
 Embedded=_gbp_symbol_tree_register_types
-Hidden=true
 Module=symbol-tree
 Name=Symbol Tree
+X-Category=editing
+X-Workspace-Kind=primary;editor;


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