[gnome-builder/wip/chergert/docs] wip



commit d7e5525529889dddfb73f5036016fa85bff625be
Author: Christian Hergert <chergert redhat com>
Date:   Fri Jul 5 16:31:59 2019 -0700

    wip

 src/libide/docs/ide-docs-item.c                   | 741 ++++++++++++++++++++++
 src/libide/docs/ide-docs-item.h                   | 132 ++++
 src/libide/docs/ide-docs-library.c                | 319 ++++++++++
 src/libide/docs/ide-docs-library.h                |  49 ++
 src/libide/docs/ide-docs-provider.c               | 162 +++++
 src/libide/docs/ide-docs-provider.h               |  80 +++
 src/libide/docs/ide-docs-query.c                  | 255 ++++++++
 src/libide/docs/ide-docs-query.h                  |  52 ++
 src/libide/docs/ide-docs-search-model.c           | 306 +++++++++
 src/libide/docs/ide-docs-search-model.h           |  41 ++
 src/libide/docs/ide-docs-search-row.c             | 241 +++++++
 src/libide/docs/ide-docs-search-row.h             |  36 ++
 src/libide/docs/ide-docs-search-row.ui            |  31 +
 src/libide/docs/ide-docs-search-section.c         | 335 ++++++++++
 src/libide/docs/ide-docs-search-section.h         |  40 ++
 src/libide/docs/ide-docs-search-view.c            | 432 +++++++++++++
 src/libide/docs/ide-docs-search-view.h            |  47 ++
 src/libide/docs/ide-docs-search-view.ui           |  38 ++
 src/libide/docs/ide-docs-view.c                   |  90 +++
 src/libide/docs/ide-docs-view.h                   |  37 ++
 src/libide/docs/ide-docs-workspace.c              | 182 ++++++
 src/libide/docs/ide-docs-workspace.h              |  35 +
 src/libide/docs/ide-docs-workspace.ui             |  50 ++
 src/libide/docs/libide-docs.c                     |   0
 src/libide/docs/libide-docs.gresource.xml         |  11 +
 src/libide/docs/libide-docs.h                     |  34 +
 src/libide/docs/meson.build                       |  99 +++
 src/libide/meson.build                            |   1 +
 src/libide/themes/libide-themes.gresource.xml     |   1 +
 src/libide/themes/themes/shared.css               |   1 +
 src/libide/themes/themes/shared/shared-docs.css   |  34 +
 src/meson.build                                   |   1 +
 src/plugins/devhelp/devhelp-plugin.c              |   4 +
 src/plugins/devhelp/devhelp2-parser.c             | 329 ++++++++++
 src/plugins/devhelp/devhelp2-parser.h             |  62 ++
 src/plugins/devhelp/gbp-devhelp-docs-provider.c   | 362 +++++++++++
 src/plugins/devhelp/gbp-devhelp-docs-provider.h   |  31 +
 src/plugins/devhelp/meson.build                   |   2 +
 src/plugins/docsui/docsui-plugin.c                |  34 +
 src/plugins/docsui/docsui.gresource.xml           |   6 +
 src/plugins/docsui/docsui.plugin                  |  11 +
 src/plugins/docsui/gbp-docsui-application-addin.c | 109 ++++
 src/plugins/docsui/gbp-docsui-application-addin.h |  31 +
 src/plugins/docsui/meson.build                    |  12 +
 src/plugins/meson.build                           |   2 +
 45 files changed, 4908 insertions(+)
---
diff --git a/src/libide/docs/ide-docs-item.c b/src/libide/docs/ide-docs-item.c
new file mode 100644
index 000000000..d0d670a1e
--- /dev/null
+++ b/src/libide/docs/ide-docs-item.c
@@ -0,0 +1,741 @@
+/* ide-docs-item.c
+ *
+ * Copyright 2019 Christian Hergert <unknown domain org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-docs-item"
+
+#include "config.h"
+
+#include "ide-docs-enums.h"
+#include "ide-docs-item.h"
+
+typedef struct
+{
+  IdeDocsItem     *parent;
+  GHashTable      *children_index;
+  GQueue           children;
+  GList            link;
+  gchar           *id;
+  gchar           *title;
+  gchar           *display_name;
+  gchar           *since;
+  gchar           *url;
+  IdeDocsItemKind  kind;
+  gint             priority;
+  guint            deprecated : 1;
+} IdeDocsItemPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeDocsItem, ide_docs_item, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_DEPRECATED,
+  PROP_DISPLAY_NAME,
+  PROP_ID,
+  PROP_KIND,
+  PROP_PRIORITY,
+  PROP_SINCE,
+  PROP_TITLE,
+  PROP_URL,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * ide_docs_item_new:
+ *
+ * Create a new #IdeDocsItem.
+ *
+ * Returns: (transfer full): a newly created #IdeDocsItem
+ *
+ * Since: 3.34
+ */
+IdeDocsItem *
+ide_docs_item_new (void)
+{
+  return g_object_new (IDE_TYPE_DOCS_ITEM, NULL);
+}
+
+void
+ide_docs_item_remove (IdeDocsItem *self,
+                      IdeDocsItem *child)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+  IdeDocsItemPrivate *child_priv = ide_docs_item_get_instance_private (child);
+  const gchar *id;
+
+  g_return_if_fail (IDE_IS_DOCS_ITEM (self));
+  g_return_if_fail (IDE_IS_DOCS_ITEM (child));
+  g_return_if_fail (child_priv->parent == self);
+
+  if ((id = ide_docs_item_get_id (child)))
+    g_hash_table_remove (priv->children_index, id);
+
+  g_queue_unlink (&priv->children, &child_priv->link);
+  child_priv->parent = NULL;
+  g_object_unref (child);
+}
+
+static void
+ide_docs_item_dispose (GObject *object)
+{
+  IdeDocsItem *self = (IdeDocsItem *)object;
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  if (priv->parent != NULL)
+    ide_docs_item_remove (priv->parent, self);
+
+  G_OBJECT_CLASS (ide_docs_item_parent_class)->dispose (object);
+}
+
+static void
+ide_docs_item_finalize (GObject *object)
+{
+  IdeDocsItem *self = (IdeDocsItem *)object;
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_clear_pointer (&priv->id, g_free);
+  g_clear_pointer (&priv->since, g_free);
+  g_clear_pointer (&priv->display_name, g_free);
+  g_clear_pointer (&priv->title, g_free);
+  g_clear_pointer (&priv->url, g_free);
+
+  G_OBJECT_CLASS (ide_docs_item_parent_class)->finalize (object);
+}
+
+static void
+ide_docs_item_get_property (GObject    *object,
+                            guint       prop_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
+{
+  IdeDocsItem *self = IDE_DOCS_ITEM (object);
+
+  switch (prop_id)
+    {
+    case PROP_ID:
+      g_value_set_string (value, ide_docs_item_get_id (self));
+      break;
+
+    case PROP_DISPLAY_NAME:
+      g_value_set_string (value, ide_docs_item_get_display_name (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, ide_docs_item_get_title (self));
+      break;
+
+    case PROP_SINCE:
+      g_value_set_string (value, ide_docs_item_get_since (self));
+      break;
+
+    case PROP_KIND:
+      g_value_set_enum (value, ide_docs_item_get_kind (self));
+      break;
+
+    case PROP_DEPRECATED:
+      g_value_set_boolean (value, ide_docs_item_get_deprecated (self));
+      break;
+
+    case PROP_URL:
+      g_value_set_string (value, ide_docs_item_get_url (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_docs_item_set_property (GObject      *object,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  IdeDocsItem *self = IDE_DOCS_ITEM (object);
+
+  switch (prop_id)
+    {
+    case PROP_ID:
+      ide_docs_item_set_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_DISPLAY_NAME:
+      ide_docs_item_set_display_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_TITLE:
+      ide_docs_item_set_title (self, g_value_get_string (value));
+      break;
+
+    case PROP_SINCE:
+      ide_docs_item_set_since (self, g_value_get_string (value));
+      break;
+
+    case PROP_KIND:
+      ide_docs_item_set_kind (self, g_value_get_enum (value));
+      break;
+
+    case PROP_DEPRECATED:
+      ide_docs_item_set_deprecated (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_URL:
+      ide_docs_item_set_url (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_docs_item_class_init (IdeDocsItemClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_docs_item_dispose;
+  object_class->finalize = ide_docs_item_finalize;
+  object_class->get_property = ide_docs_item_get_property;
+  object_class->set_property = ide_docs_item_set_property;
+
+  properties [PROP_ID] =
+    g_param_spec_string ("id",
+                         "Id",
+                         "The identifier for the item, if any",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_DISPLAY_NAME] =
+    g_param_spec_string ("display-name",
+                         "Display Name",
+                         "The display-name of the item, possibily containing pango markup",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the item",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SINCE] =
+    g_param_spec_string ("since",
+                         "Since",
+                         "When the item is added",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_DEPRECATED] =
+    g_param_spec_string ("deprecated",
+                         "Deprecated",
+                         "When the item was deprecated",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_URL] =
+    g_param_spec_string ("url",
+                         "Url",
+                         "The url for the documentation",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_KIND] =
+    g_param_spec_enum ("kind",
+                       "Kind",
+                       "The kind of item",
+                       IDE_TYPE_DOCS_ITEM_KIND,
+                       IDE_DOCS_ITEM_KIND_NONE,
+                       (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PRIORITY] =
+    g_param_spec_int ("priority",
+                      "Priority",
+                      "The priority of the item",
+                      G_MININT, G_MAXINT, 0,
+                      (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+  
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_docs_item_init (IdeDocsItem *self)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  priv->link.data = self;
+}
+
+const gchar *
+ide_docs_item_get_id (IdeDocsItem *self)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DOCS_ITEM (self), NULL);
+
+  return priv->id;
+}
+
+void
+ide_docs_item_set_id (IdeDocsItem *self,
+                      const gchar *id)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DOCS_ITEM (self));
+  g_return_if_fail (priv->parent == NULL);
+
+  if (g_strcmp0 (id, priv->id) != 0)
+    {
+      g_free (priv->id);
+      priv->id = g_strdup (id);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ID]);
+    }
+}
+
+const gchar *
+ide_docs_item_get_display_name (IdeDocsItem *self)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DOCS_ITEM (self), NULL);
+
+  return priv->display_name;
+}
+
+void
+ide_docs_item_set_display_name (IdeDocsItem *self,
+                                const gchar *display_name)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DOCS_ITEM (self));
+
+  if (g_strcmp0 (display_name, priv->display_name) != 0)
+    {
+      g_free (priv->display_name);
+      priv->display_name = g_strdup (display_name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DISPLAY_NAME]);
+    }
+}
+
+const gchar *
+ide_docs_item_get_title (IdeDocsItem *self)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DOCS_ITEM (self), NULL);
+
+  return priv->title;
+}
+
+void
+ide_docs_item_set_title (IdeDocsItem *self,
+                         const gchar *title)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DOCS_ITEM (self));
+
+  if (g_strcmp0 (title, priv->title) != 0)
+    {
+      g_free (priv->title);
+      priv->title = g_strdup (title);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+    }
+}
+
+const gchar *
+ide_docs_item_get_url (IdeDocsItem *self)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DOCS_ITEM (self), NULL);
+
+  return priv->url;
+}
+
+void
+ide_docs_item_set_url (IdeDocsItem *self,
+                       const gchar *url)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DOCS_ITEM (self));
+
+  if (g_strcmp0 (url, priv->url) != 0)
+    {
+      g_free (priv->url);
+      priv->url = g_strdup (url);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_URL]);
+    }
+}
+
+const gchar *
+ide_docs_item_get_since (IdeDocsItem *self)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DOCS_ITEM (self), NULL);
+
+  return priv->since;
+}
+
+void
+ide_docs_item_set_since (IdeDocsItem *self,
+                         const gchar *since)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DOCS_ITEM (self));
+
+  if (g_strcmp0 (since, priv->since) != 0)
+    {
+      g_free (priv->since);
+      priv->since = g_strdup (since);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SINCE]);
+    }
+}
+
+gboolean
+ide_docs_item_get_deprecated (IdeDocsItem *self)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DOCS_ITEM (self), FALSE);
+
+  return priv->deprecated;
+}
+
+void
+ide_docs_item_set_deprecated (IdeDocsItem *self,
+                              gboolean     deprecated)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DOCS_ITEM (self));
+
+  deprecated = !!deprecated;
+
+  if (deprecated != priv->deprecated)
+    {
+      priv->deprecated = deprecated;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DEPRECATED]);
+    }
+}
+
+IdeDocsItemKind
+ide_docs_item_get_kind (IdeDocsItem *self)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DOCS_ITEM (self), FALSE);
+
+  return priv->kind;
+}
+
+void
+ide_docs_item_set_kind (IdeDocsItem     *self,
+                        IdeDocsItemKind  kind)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DOCS_ITEM (self));
+
+  if (kind != priv->kind)
+    {
+      priv->kind = kind;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_KIND]);
+    }
+}
+
+gboolean
+ide_docs_item_has_child (IdeDocsItem *self)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DOCS_ITEM (self), FALSE);
+
+  return priv->children.length > 0;
+}
+
+gboolean
+ide_docs_item_is_root (IdeDocsItem *self)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DOCS_ITEM (self), FALSE);
+
+  return priv->parent == NULL;
+}
+
+/**
+ * ide_docs_item_get_parent:
+ *
+ * Get the parent #IdeDocsItem if set.
+ *
+ * Returns: (transfer none): an #IdeDocsItem or %NULL
+ *
+ * Since: 3.34
+ */
+IdeDocsItem *
+ide_docs_item_get_parent (IdeDocsItem *self)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DOCS_ITEM (self), NULL);
+
+  return priv->parent;
+}
+
+/**
+ * ide_docs_item_get_n_children:
+ * @self: a #IdeDocsItem
+ *
+ * Gets the nubmer of children #IdeDocsItem contained by @self.
+ *
+ * Returns: the number of children
+ *
+ * Since: 3.34
+ */
+guint
+ide_docs_item_get_n_children (IdeDocsItem *self)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DOCS_ITEM (self), 0);
+
+  return priv->children.length;
+}
+
+/**
+ * ide_docs_item_get_children:
+ * @self: an #IdeDocsItem
+ *
+ * Gets a #GList of #IdeDocsItem that are direct children of @self.
+ *
+ * The result may not be modified or freed.
+ *
+ * Returns: (transfer none) (element-type Ide.DocsItem): a #GList of
+ *   #IdeDocsItem.
+ *
+ * Since: 3.34
+ */
+const GList *
+ide_docs_item_get_children (IdeDocsItem *self)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DOCS_ITEM (self), NULL);
+
+  return priv->children.head;
+}
+
+static void
+maybe_index (IdeDocsItem *self,
+             IdeDocsItem *child)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+  const gchar *id = ide_docs_item_get_id (child);
+
+  if (id != NULL)
+    {
+      if (priv->children_index == NULL)
+        priv->children_index = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+      g_hash_table_insert (priv->children_index, g_strdup (id), child);
+    }
+}
+
+void
+ide_docs_item_append (IdeDocsItem *self,
+                      IdeDocsItem *child)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+  IdeDocsItemPrivate *child_priv = ide_docs_item_get_instance_private (child);
+
+  g_return_if_fail (IDE_IS_DOCS_ITEM (self));
+  g_return_if_fail (IDE_IS_DOCS_ITEM (child));
+  g_return_if_fail (child_priv->parent == NULL);
+  g_return_if_fail (child_priv->link.prev == NULL);
+  g_return_if_fail (child_priv->link.next == NULL);
+
+  g_object_ref (child);
+  child_priv->parent = self;
+  g_queue_push_tail_link (&priv->children, &child_priv->link);
+  maybe_index (self, child);
+}
+
+void
+ide_docs_item_prepend (IdeDocsItem *self,
+                       IdeDocsItem *child)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+  IdeDocsItemPrivate *child_priv = ide_docs_item_get_instance_private (child);
+
+  g_return_if_fail (IDE_IS_DOCS_ITEM (self));
+  g_return_if_fail (IDE_IS_DOCS_ITEM (child));
+  g_return_if_fail (child_priv->parent == NULL);
+  g_return_if_fail (child_priv->link.prev == NULL);
+  g_return_if_fail (child_priv->link.next == NULL);
+
+  g_object_ref (child);
+  child_priv->parent = self;
+  g_queue_push_head_link (&priv->children, &child_priv->link);
+  maybe_index (self, child);
+}
+
+gint
+ide_docs_item_get_priority (IdeDocsItem *self)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DOCS_ITEM (self), 0);
+
+  return priv->priority;
+}
+
+void
+ide_docs_item_set_priority (IdeDocsItem *self,
+                            gint         priority)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DOCS_ITEM (self));
+
+  if (priority != priv->priority)
+    {
+      priv->priority = priority;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PRIORITY]);
+    }
+}
+
+/**
+ * ide_docs_item_find_child_by_id:
+ * @self: a #IdeDocsItem
+ * @id: the id of the child to locate
+ *
+ * Finds a child item based on the id of the child.
+ *
+ * Returns: (transfer none) (nullable): an #IdeDocsItem or %NULL
+ *
+ * Since: 3.34
+ */
+IdeDocsItem *
+ide_docs_item_find_child_by_id (IdeDocsItem *self,
+                                const gchar *id)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+  IdeDocsItem *child;
+
+  g_return_val_if_fail (IDE_IS_DOCS_ITEM (self), NULL);
+
+  if (id == NULL)
+    return NULL;
+
+  if (priv->children_index != NULL &&
+      (child = g_hash_table_lookup (priv->children_index, id)))
+    return child;
+
+  for (const GList *iter = priv->children.head;
+       iter != NULL;
+       iter = iter->next)
+    {
+      IdeDocsItemPrivate *child_priv;
+
+      child = iter->data;
+      child_priv = ide_docs_item_get_instance_private (child);
+
+      g_assert (IDE_IS_DOCS_ITEM (child));
+
+      if (g_strcmp0 (child_priv->id, id) == 0)
+        return child;
+    }
+
+  return NULL;
+}
+
+static gint
+sort_by_priority (IdeDocsItem *a,
+                  IdeDocsItem *b)
+{
+  gint prio_a = ide_docs_item_get_priority (a);
+  gint prio_b = ide_docs_item_get_priority (b);
+
+  if (prio_a < prio_b)
+    return -1;
+  else if (prio_a > prio_b)
+    return 1;
+  else
+    return 0;
+}
+
+void
+ide_docs_item_sort_by_priority (IdeDocsItem *self)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DOCS_ITEM (self));
+
+  if (priv->children.length != 0)
+    g_queue_sort (&priv->children,
+                  (GCompareDataFunc) sort_by_priority,
+                  NULL);
+}
+
+void
+ide_docs_item_truncate (IdeDocsItem *self,
+                        guint        max_items)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DOCS_ITEM (self));
+
+  if (max_items == 0)
+    return;
+
+  if (max_items >= priv->children.length)
+    return;
+
+  while (priv->children.length > max_items)
+    ide_docs_item_remove (self, priv->children.tail->data);
+}
+
+/**
+ * ide_docs_item_get_nth_child:
+ * @self: a #IdeDocsItem
+ * @nth: the index (starting from zero) of the child
+ *
+ * Gets the @nth item from the children.
+ *
+ * Returns: (transfer none) (nullable): an #IdeDocsItem or %NULL
+ *
+ * Since: 3.34
+ */
+IdeDocsItem *
+ide_docs_item_get_nth_child (IdeDocsItem *self,
+                             guint        nth)
+{
+  IdeDocsItemPrivate *priv = ide_docs_item_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DOCS_ITEM (self), NULL);
+
+  return g_list_nth_data (priv->children.head, nth);
+}
diff --git a/src/libide/docs/ide-docs-item.h b/src/libide/docs/ide-docs-item.h
new file mode 100644
index 000000000..0ad2ae762
--- /dev/null
+++ b/src/libide/docs/ide-docs-item.h
@@ -0,0 +1,132 @@
+/* ide-docs-item.h
+ *
+ * Copyright 2019 Christian Hergert <unknown domain org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DOCS_ITEM (ide_docs_item_get_type())
+
+typedef enum
+{
+  IDE_DOCS_ITEM_KIND_NONE = 0,
+  IDE_DOCS_ITEM_KIND_COLLECTION,
+  IDE_DOCS_ITEM_KIND_BOOK,
+  IDE_DOCS_ITEM_KIND_CHAPTER,
+  IDE_DOCS_ITEM_KIND_CLASS,
+  IDE_DOCS_ITEM_KIND_CONSTANT,
+  IDE_DOCS_ITEM_KIND_ENUM,
+  IDE_DOCS_ITEM_KIND_FUNCTION,
+  IDE_DOCS_ITEM_KIND_MACRO,
+  IDE_DOCS_ITEM_KIND_MEMBER,
+  IDE_DOCS_ITEM_KIND_METHOD,
+  IDE_DOCS_ITEM_KIND_PROPERTY,
+  IDE_DOCS_ITEM_KIND_SIGNAL,
+  IDE_DOCS_ITEM_KIND_STRUCT,
+  IDE_DOCS_ITEM_KIND_UNION,
+} IdeDocsItemKind;
+
+IDE_AVAILABLE_IN_3_34
+G_DECLARE_DERIVABLE_TYPE (IdeDocsItem, ide_docs_item, IDE, DOCS_ITEM, GObject)
+
+struct _IdeDocsItemClass
+{
+  GObjectClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_34
+IdeDocsItem     *ide_docs_item_new              (void);
+IDE_AVAILABLE_IN_3_34
+const gchar     *ide_docs_item_get_id           (IdeDocsItem     *self);
+IDE_AVAILABLE_IN_3_34
+void             ide_docs_item_set_id           (IdeDocsItem     *self,
+                                                 const gchar     *id);
+IDE_AVAILABLE_IN_3_34
+IdeDocsItem     *ide_docs_item_find_child_by_id (IdeDocsItem     *self,
+                                                 const gchar     *id);
+IDE_AVAILABLE_IN_3_34
+const gchar     *ide_docs_item_get_title        (IdeDocsItem     *self);
+IDE_AVAILABLE_IN_3_34
+void             ide_docs_item_set_title        (IdeDocsItem     *self,
+                                                 const gchar     *title);
+IDE_AVAILABLE_IN_3_34
+const gchar     *ide_docs_item_get_display_name (IdeDocsItem     *self);
+IDE_AVAILABLE_IN_3_34
+void             ide_docs_item_set_display_name (IdeDocsItem     *self,
+                                                 const gchar     *display_name);
+IDE_AVAILABLE_IN_3_34
+const gchar     *ide_docs_item_get_url          (IdeDocsItem     *self);
+IDE_AVAILABLE_IN_3_34
+void             ide_docs_item_set_url          (IdeDocsItem     *self,
+                                                 const gchar     *url);
+IDE_AVAILABLE_IN_3_34
+IdeDocsItemKind  ide_docs_item_get_kind         (IdeDocsItem     *self);
+IDE_AVAILABLE_IN_3_34
+void             ide_docs_item_set_kind         (IdeDocsItem     *self,
+                                                 IdeDocsItemKind  kind);
+IDE_AVAILABLE_IN_3_34
+gboolean         ide_docs_item_get_deprecated   (IdeDocsItem     *self);
+IDE_AVAILABLE_IN_3_34
+void             ide_docs_item_set_deprecated   (IdeDocsItem     *self,
+                                                 gboolean         deprecated);
+IDE_AVAILABLE_IN_3_34
+const gchar     *ide_docs_item_get_since        (IdeDocsItem     *self);
+IDE_AVAILABLE_IN_3_34
+void             ide_docs_item_set_since        (IdeDocsItem     *self,
+                                                 const gchar     *since);
+IDE_AVAILABLE_IN_3_34
+gint             ide_docs_item_get_priority     (IdeDocsItem     *self);
+IDE_AVAILABLE_IN_3_34
+void             ide_docs_item_set_priority     (IdeDocsItem     *self,
+                                                 gint             priority);
+IDE_AVAILABLE_IN_3_34
+void             ide_docs_item_append           (IdeDocsItem     *self,
+                                                 IdeDocsItem     *child);
+IDE_AVAILABLE_IN_3_34
+void             ide_docs_item_prepend          (IdeDocsItem     *self,
+                                                 IdeDocsItem     *child);
+IDE_AVAILABLE_IN_3_34
+void             ide_docs_item_remove           (IdeDocsItem     *self,
+                                                 IdeDocsItem     *child);
+IDE_AVAILABLE_IN_3_34
+gboolean         ide_docs_item_has_child        (IdeDocsItem     *self);
+IDE_AVAILABLE_IN_3_34
+IdeDocsItem     *ide_docs_item_get_nth_child    (IdeDocsItem     *self,
+                                                 guint            nth);
+IDE_AVAILABLE_IN_3_34
+guint            ide_docs_item_get_n_children   (IdeDocsItem     *self);
+IDE_AVAILABLE_IN_3_34
+const GList     *ide_docs_item_get_children     (IdeDocsItem     *self);
+IDE_AVAILABLE_IN_3_34
+gboolean         ide_docs_item_is_root          (IdeDocsItem     *self);
+IDE_AVAILABLE_IN_3_34
+IdeDocsItem     *ide_docs_item_get_parent       (IdeDocsItem     *self);
+IDE_AVAILABLE_IN_3_34
+void             ide_docs_item_sort_by_priority (IdeDocsItem     *self);
+IDE_AVAILABLE_IN_3_34
+void             ide_docs_item_truncate         (IdeDocsItem     *self,
+                                                 guint            max_items);
+
+G_END_DECLS
diff --git a/src/libide/docs/ide-docs-library.c b/src/libide/docs/ide-docs-library.c
new file mode 100644
index 000000000..7afd39598
--- /dev/null
+++ b/src/libide/docs/ide-docs-library.c
@@ -0,0 +1,319 @@
+/* ide-docs-library.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-docs-library"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-plugins.h>
+#include <libide-threading.h>
+
+#include "ide-docs-library.h"
+#include "ide-docs-provider.h"
+
+struct _IdeDocsLibrary
+{
+  IdeObject               parent_instance;
+  IdeExtensionSetAdapter *providers;
+};
+
+typedef struct
+{
+  GCancellable *cancellable;
+  IdeDocsQuery *query;
+  IdeDocsItem  *results;
+  guint         n_active;
+} Search;
+
+G_DEFINE_TYPE (IdeDocsLibrary, ide_docs_library, IDE_TYPE_OBJECT)
+
+static void
+search_free (Search *search)
+{
+  g_clear_object (&search->results);
+  g_clear_object (&search->cancellable);
+  g_clear_object (&search->query);
+  g_slice_free (Search, search);
+}
+
+static void
+ide_docs_library_init_provider_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  IdeDocsProvider *provider = (IdeDocsProvider *)object;
+  g_autoptr(IdeDocsLibrary) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_DOCS_PROVIDER (provider));
+  g_assert (G_IS_ASYNC_INITABLE (provider));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_DOCS_LIBRARY (self));
+
+  if (!g_async_initable_init_finish (G_ASYNC_INITABLE (provider), result, &error))
+    g_warning ("%s failed to initialize: %s", G_OBJECT_TYPE_NAME (provider), error->message);
+}
+
+static void
+on_extension_added_cb (IdeExtensionSetAdapter *adapter,
+                       PeasPluginInfo         *plugin,
+                       PeasExtension          *exten,
+                       gpointer                user_data)
+{
+  IdeDocsProvider *provider = (IdeDocsProvider *)exten;
+  IdeDocsLibrary *self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin != NULL);
+  g_assert (IDE_IS_DOCS_PROVIDER (provider));
+  g_assert (IDE_IS_DOCS_LIBRARY (self));
+
+  if (G_IS_INITABLE (provider) && !g_initable_init (G_INITABLE (provider), NULL, &error))
+    g_warning ("%s failed to initialize: %s", G_OBJECT_TYPE_NAME (provider), error->message);
+  else if (G_IS_ASYNC_INITABLE (provider))
+    g_async_initable_init_async (G_ASYNC_INITABLE (provider),
+                                 G_PRIORITY_DEFAULT,
+                                 NULL,
+                                 ide_docs_library_init_provider_cb,
+                                 g_object_ref (self));
+}
+
+static void
+ide_docs_library_parent_set (IdeObject *object,
+                             IdeObject *parent)
+{
+  IdeDocsLibrary *self = (IdeDocsLibrary *)object;
+
+  g_assert (IDE_IS_OBJECT (object));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
+
+  if (parent == NULL)
+    return;
+
+  self->providers = ide_extension_set_adapter_new (object,
+                                                   peas_engine_get_default (),
+                                                   IDE_TYPE_DOCS_PROVIDER,
+                                                   NULL, NULL);
+  g_signal_connect (self->providers,
+                    "extension-added",
+                    G_CALLBACK (on_extension_added_cb),
+                    self);
+  ide_extension_set_adapter_foreach (self->providers, on_extension_added_cb, self);
+}
+
+static void
+ide_docs_library_destroy (IdeObject *object)
+{
+  IdeDocsLibrary *self = (IdeDocsLibrary *)object;
+
+  ide_clear_and_destroy_object (&self->providers);
+
+  IDE_OBJECT_CLASS (ide_docs_library_parent_class)->destroy (object);
+}
+
+static void
+ide_docs_library_class_init (IdeDocsLibraryClass *klass)
+{
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  i_object_class->parent_set = ide_docs_library_parent_set;
+  i_object_class->destroy = ide_docs_library_destroy;
+}
+
+static void
+ide_docs_library_init (IdeDocsLibrary *self)
+{
+}
+
+/**
+ * ide_docs_library_from_context:
+ *
+ * Gets the #IdeDocsLibrary for the context.
+ *
+ * Returns: (transfer none): an #IdeDocsLibrary
+ *
+ * Since: 3.34
+ */
+IdeDocsLibrary *
+ide_docs_library_from_context (IdeContext *context)
+{
+  g_autoptr(IdeDocsLibrary) ensured = NULL;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  if ((ensured = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_DOCS_LIBRARY)))
+    return ide_context_peek_child_typed (context, IDE_TYPE_DOCS_LIBRARY);
+
+  return NULL;
+}
+
+static void
+ide_docs_library_search_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  IdeDocsProvider *provider = (IdeDocsProvider *)object;
+  g_autoptr(GListModel) model = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  Search *search;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_DOCS_PROVIDER (provider));
+
+  search = ide_task_get_task_data (task);
+
+  if (!ide_docs_provider_search_finish (provider, result, &error))
+    {
+      if (!ide_error_ignore (error))
+        g_warning ("Search failed: %s: %s",
+                   G_OBJECT_TYPE_NAME (provider), error->message);
+    }
+
+  search->n_active--;
+
+  if (search->n_active == 0)
+    {
+      if (!ide_task_return_error_if_cancelled (task))
+        ide_task_return_boolean (task, TRUE);
+    }
+}
+
+static void
+ide_docs_library_search_foreach_cb (IdeExtensionSetAdapter *adapter,
+                                    PeasPluginInfo         *plugin,
+                                    PeasExtension          *exten,
+                                    gpointer                user_data)
+{
+  IdeDocsProvider *provider = (IdeDocsProvider *)exten;
+  IdeTask *task = user_data;
+  Search *search;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin != NULL);
+  g_assert (IDE_IS_DOCS_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (task));
+
+  search = ide_task_get_task_data (task);
+  search->n_active++;
+
+  ide_docs_provider_search_async (provider,
+                                  search->query,
+                                  search->results,
+                                  search->cancellable,
+                                  ide_docs_library_search_cb,
+                                  g_object_ref (task));
+}
+
+/**
+ * ide_docs_library_search_async:
+ * @self: an #IdeDocsLibrary
+ * @query: an #IdeDocsQuery
+ * @results: an #IdeDocsItem to place results
+ * @cancellable: (nullable): a #GCancellable, or %NULL
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Asynchronously queries the documentation providers for docs that
+ * match @query.
+ *
+ * @callback should call ide_docs_library_search_finish() to obtain
+ * the result.
+ *
+ * Since: 3.34
+ */
+void
+ide_docs_library_search_async (IdeDocsLibrary      *self,
+                               IdeDocsQuery        *query,
+                               IdeDocsItem         *results,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  Search *search;
+  static const struct {
+    const gchar *id;
+    const gchar *title;
+  } default_groups[] = {
+    { "api", N_("API") },
+    { "tutorials", N_("Tutorials and Guides") },
+    { "guidelines", N_("Guidelines") },
+    { "other", N_("Other") },
+  };
+
+  g_return_if_fail (IDE_IS_DOCS_LIBRARY (self));
+  g_return_if_fail (IDE_IS_DOCS_QUERY (query));
+  g_return_if_fail (IDE_IS_DOCS_ITEM (results));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  search = g_slice_new0 (Search);
+  search->results = g_object_ref (results);
+  search->query = g_object_ref (query);
+  search->cancellable = cancellable ? g_object_ref (cancellable) : NULL;
+  search->n_active = 0;
+
+  for (guint i = 0; i < G_N_ELEMENTS (default_groups); i++)
+    {
+      const gchar *id = default_groups[i].id;
+      const gchar *group = g_dgettext (GETTEXT_PACKAGE, default_groups[i].title);
+      g_autoptr(IdeDocsItem) child = NULL;
+
+      child = ide_docs_item_new ();
+      ide_docs_item_set_id (child, id);
+      ide_docs_item_set_title (child, group);
+      ide_docs_item_append (results, child);
+    }
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_docs_library_search_async);
+  ide_task_set_task_data (task, search, search_free);
+
+  ide_extension_set_adapter_foreach (self->providers,
+                                     ide_docs_library_search_foreach_cb,
+                                     task);
+
+  if (search->n_active == 0)
+    ide_task_return_boolean (task, TRUE);
+}
+
+/**
+ * ide_docs_library_search_finish:
+ * @self: an #IdeDocsLibrary
+ * @result: a #GAsyncResult provided to callack
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes a request to search the library.
+ *
+ * Since: 3.34
+ */
+gboolean
+ide_docs_library_search_finish (IdeDocsLibrary  *self,
+                                GAsyncResult    *result,
+                                GError         **error)
+{
+  g_return_val_if_fail (IDE_IS_DOCS_LIBRARY (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
diff --git a/src/libide/docs/ide-docs-library.h b/src/libide/docs/ide-docs-library.h
new file mode 100644
index 000000000..8942f920b
--- /dev/null
+++ b/src/libide/docs/ide-docs-library.h
@@ -0,0 +1,49 @@
+/* ide-docs-library.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-core.h>
+
+#include "ide-docs-item.h"
+#include "ide-docs-query.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DOCS_LIBRARY (ide_docs_library_get_type())
+
+IDE_AVAILABLE_IN_3_34
+G_DECLARE_FINAL_TYPE (IdeDocsLibrary, ide_docs_library, IDE, DOCS_LIBRARY, IdeObject)
+
+IDE_AVAILABLE_IN_3_34
+IdeDocsLibrary *ide_docs_library_from_context  (IdeContext           *context);
+IDE_AVAILABLE_IN_3_34
+void            ide_docs_library_search_async  (IdeDocsLibrary       *self,
+                                                IdeDocsQuery         *query,
+                                                IdeDocsItem          *results,
+                                                GCancellable         *cancellable,
+                                                GAsyncReadyCallback   callback,
+                                                gpointer              user_data);
+IDE_AVAILABLE_IN_3_34
+gboolean        ide_docs_library_search_finish (IdeDocsLibrary       *self,
+                                                GAsyncResult         *result,
+                                                GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/docs/ide-docs-provider.c b/src/libide/docs/ide-docs-provider.c
new file mode 100644
index 000000000..3afd7a790
--- /dev/null
+++ b/src/libide/docs/ide-docs-provider.c
@@ -0,0 +1,162 @@
+/* ide-docs-provider.c
+ *
+ * Copyright 2019 Christian Hergert <unknown domain org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-docs-provider"
+
+#include "config.h"
+
+#include <libide-threading.h>
+
+#include "ide-docs-provider.h"
+
+G_DEFINE_INTERFACE (IdeDocsProvider, ide_docs_provider, G_TYPE_OBJECT)
+
+static void
+ide_docs_provider_real_populate_async (IdeDocsProvider     *provider,
+                                       IdeDocsItem         *item,
+                                       GCancellable        *cancellable,
+                                       GAsyncReadyCallback  callback,
+                                       gpointer             user_data)
+{
+  ide_task_report_new_error (provider, callback, user_data,
+                             ide_docs_provider_real_populate_async,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_SUPPORTED,
+                             "Not supported");
+}
+
+static gboolean
+ide_docs_provider_real_populate_finish (IdeDocsProvider  *provider,
+                                        GAsyncResult     *result,
+                                        GError          **error)
+{
+  g_assert (IDE_IS_DOCS_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_docs_provider_real_search_async (IdeDocsProvider     *provider,
+                                     IdeDocsQuery        *query,
+                                     IdeDocsItem         *results,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
+{
+  ide_task_report_new_error (provider, callback, user_data,
+                             ide_docs_provider_real_search_async,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_SUPPORTED,
+                             "Not supported");
+}
+
+static gboolean
+ide_docs_provider_real_search_finish (IdeDocsProvider  *provider,
+                                       GAsyncResult     *result,
+                                       GError          **error)
+{
+  g_assert (IDE_IS_DOCS_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_docs_provider_default_init (IdeDocsProviderInterface *iface)
+{
+  iface->populate_async = ide_docs_provider_real_populate_async;
+  iface->populate_finish = ide_docs_provider_real_populate_finish;
+  iface->search_async = ide_docs_provider_real_search_async;
+  iface->search_finish = ide_docs_provider_real_search_finish;
+}
+
+void
+ide_docs_provider_populate_async (IdeDocsProvider     *self,
+                                  IdeDocsItem         *item,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_DOCS_PROVIDER (self));
+  g_return_if_fail (IDE_IS_DOCS_ITEM (item));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_DOCS_PROVIDER_GET_IFACE (self)->populate_async (self, item, cancellable, callback, user_data);
+}
+
+gboolean
+ide_docs_provider_populate_finish (IdeDocsProvider  *self,
+                                   GAsyncResult     *result,
+                                   GError          **error)
+{
+  g_return_val_if_fail (IDE_IS_DOCS_PROVIDER (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_DOCS_PROVIDER_GET_IFACE (self)->populate_finish (self, result, error);
+}
+
+/**
+ * ide_docs_provider_search_async:
+ * @self: an #IdeDocsProvider
+ * @query: an #IdeDocsQuery
+ * @results: an #IdeDocsItem
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute
+ * @user_data: closure data for @callback
+ *
+ * Asynchronously queries the documentation provider. The results will be placed
+ * into @results. @results should contain a series of "sections" for the results
+ * and then "groups" within those.
+ *
+ * You may not use @results outside of the main-thread.
+ *
+ * Since: 3.34
+ */
+void
+ide_docs_provider_search_async (IdeDocsProvider     *self,
+                                IdeDocsQuery        *query,
+                                IdeDocsItem         *results,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_DOCS_PROVIDER (self));
+  g_return_if_fail (IDE_IS_DOCS_QUERY (query));
+  g_return_if_fail (IDE_IS_DOCS_ITEM (results));
+
+  IDE_DOCS_PROVIDER_GET_IFACE (self)->search_async (self, query, results, cancellable, callback, user_data);
+}
+
+/**
+ * ide_docs_provider_search_finish:
+ *
+ * Since: 3.34
+ */
+gboolean
+ide_docs_provider_search_finish (IdeDocsProvider  *self,
+                                 GAsyncResult     *result,
+                                 GError          **error)
+{
+  g_return_val_if_fail (IDE_IS_DOCS_PROVIDER (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_DOCS_PROVIDER_GET_IFACE (self)->search_finish (self, result, error);
+}
diff --git a/src/libide/docs/ide-docs-provider.h b/src/libide/docs/ide-docs-provider.h
new file mode 100644
index 000000000..149a096bb
--- /dev/null
+++ b/src/libide/docs/ide-docs-provider.h
@@ -0,0 +1,80 @@
+/* ide-docs-provider.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-core.h>
+
+#include "ide-docs-item.h"
+#include "ide-docs-query.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DOCS_PROVIDER (ide_docs_provider_get_type ())
+
+IDE_AVAILABLE_IN_3_34
+G_DECLARE_INTERFACE (IdeDocsProvider, ide_docs_provider, IDE, DOCS_PROVIDER, GObject)
+
+struct _IdeDocsProviderInterface
+{
+  GTypeInterface parent;
+
+  void        (*populate_async)  (IdeDocsProvider      *self,
+                                  IdeDocsItem          *parent,
+                                  GCancellable         *cancellable,
+                                  GAsyncReadyCallback   callback,
+                                  gpointer              user_data);
+  gboolean    (*populate_finish) (IdeDocsProvider      *self,
+                                  GAsyncResult         *result,
+                                  GError              **error);
+  void        (*search_async)    (IdeDocsProvider      *self,
+                                  IdeDocsQuery         *query,
+                                  IdeDocsItem          *results,
+                                  GCancellable         *cancellable,
+                                  GAsyncReadyCallback   callback,
+                                  gpointer              user_data);
+  gboolean    (*search_finish)   (IdeDocsProvider      *self,
+                                  GAsyncResult         *result,
+                                  GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_34
+void        ide_docs_provider_populate_async  (IdeDocsProvider      *self,
+                                               IdeDocsItem          *item,
+                                               GCancellable         *cancellable,
+                                               GAsyncReadyCallback   callback,
+                                               gpointer              user_data);
+IDE_AVAILABLE_IN_3_34
+gboolean    ide_docs_provider_populate_finish (IdeDocsProvider      *self,
+                                               GAsyncResult         *result,
+                                               GError              **error);
+IDE_AVAILABLE_IN_3_34
+void        ide_docs_provider_search_async    (IdeDocsProvider      *self,
+                                               IdeDocsQuery         *query,
+                                               IdeDocsItem          *results,
+                                               GCancellable         *cancellable,
+                                               GAsyncReadyCallback   callback,
+                                               gpointer              user_data);
+IDE_AVAILABLE_IN_3_34
+gboolean    ide_docs_provider_search_finish   (IdeDocsProvider      *self,
+                                               GAsyncResult         *result,
+                                               GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/docs/ide-docs-query.c b/src/libide/docs/ide-docs-query.c
new file mode 100644
index 000000000..5f611503b
--- /dev/null
+++ b/src/libide/docs/ide-docs-query.c
@@ -0,0 +1,255 @@
+/* ide-docs-query.c
+ *
+ * Copyright 2019 Christian Hergert <unknown domain org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-docs-query"
+
+#include "config.h"
+
+#include "ide-docs-query.h"
+
+struct _IdeDocsQuery
+{
+  GObject parent_instance;
+  gchar *keyword;
+  gchar *fuzzy;
+  gchar *sdk;
+  gchar *language;
+};
+
+G_DEFINE_TYPE (IdeDocsQuery, ide_docs_query, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_KEYWORD,
+  PROP_SDK,
+  PROP_LANGUAGE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * ide_docs_query_new:
+ *
+ * Create a new #IdeDocsQuery.
+ *
+ * Returns: (transfer full): a newly created #IdeDocsQuery
+ */
+IdeDocsQuery *
+ide_docs_query_new (void)
+{
+  return g_object_new (IDE_TYPE_DOCS_QUERY, NULL);
+}
+
+static void
+ide_docs_query_finalize (GObject *object)
+{
+  IdeDocsQuery *self = (IdeDocsQuery *)object;
+
+  g_clear_pointer (&self->keyword, g_free);
+  g_clear_pointer (&self->fuzzy, g_free);
+  g_clear_pointer (&self->sdk, g_free);
+  g_clear_pointer (&self->language, g_free);
+
+  G_OBJECT_CLASS (ide_docs_query_parent_class)->finalize (object);
+}
+
+static void
+ide_docs_query_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  IdeDocsQuery *self = IDE_DOCS_QUERY (object);
+
+  switch (prop_id)
+    {
+    case PROP_KEYWORD:
+      g_value_set_string (value, ide_docs_query_get_keyword (self));
+      break;
+
+    case PROP_LANGUAGE:
+      g_value_set_string (value, ide_docs_query_get_language (self));
+      break;
+
+    case PROP_SDK:
+      g_value_set_string (value, ide_docs_query_get_sdk (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_docs_query_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  IdeDocsQuery *self = IDE_DOCS_QUERY (object);
+
+  switch (prop_id)
+    {
+    case PROP_KEYWORD:
+      ide_docs_query_set_keyword (self, g_value_get_string (value));
+      break;
+
+    case PROP_LANGUAGE:
+      ide_docs_query_set_language (self, g_value_get_string (value));
+      break;
+
+    case PROP_SDK:
+      ide_docs_query_set_sdk (self, g_value_get_string (value));
+      break;
+      
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_docs_query_class_init (IdeDocsQueryClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_docs_query_finalize;
+  object_class->get_property = ide_docs_query_get_property;
+  object_class->set_property = ide_docs_query_set_property;
+  
+  properties [PROP_KEYWORD] =
+    g_param_spec_string ("keyword",
+                         "Keyword",
+                         "Keyword",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+  
+  properties [PROP_LANGUAGE] =
+    g_param_spec_string ("language",
+                         "Language",
+                         "Language",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+  
+  properties [PROP_SDK] =
+    g_param_spec_string ("sdk",
+                         "SDK",
+                         "SDK",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+  
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_docs_query_init (IdeDocsQuery *self)
+{
+}
+
+const gchar *
+ide_docs_query_get_keyword (IdeDocsQuery *self)
+{
+  g_return_val_if_fail (IDE_IS_DOCS_QUERY (self), NULL);
+
+  return self->keyword;
+}
+
+void
+ide_docs_query_set_keyword (IdeDocsQuery *self,
+                            const gchar  *keyword)
+{
+  g_return_if_fail (IDE_IS_DOCS_QUERY (self));
+
+  if (g_strcmp0 (keyword, self->keyword) != 0)
+    {
+      g_free (self->keyword);
+      self->keyword = g_strdup (keyword);
+
+      if (keyword != NULL)
+        {
+          GString *str = g_string_new (NULL);
+
+          for (; *keyword; keyword = g_utf8_next_char (keyword))
+            {
+              gunichar ch = g_utf8_get_char (keyword);
+
+              if (!g_unichar_isspace (ch))
+                g_string_append_unichar (str, ch);
+            }
+
+          g_free (self->fuzzy);
+          self->fuzzy = g_string_free (str, FALSE);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_KEYWORD]);
+    }
+}
+
+const gchar *
+ide_docs_query_get_sdk (IdeDocsQuery *self)
+{
+  g_return_val_if_fail (IDE_IS_DOCS_QUERY (self), NULL);
+
+  return self->sdk;
+}
+
+void
+ide_docs_query_set_sdk (IdeDocsQuery *self,
+                        const gchar  *sdk)
+{
+  g_return_if_fail (IDE_IS_DOCS_QUERY (self));
+
+  if (g_strcmp0 (sdk, self->sdk) != 0)
+    {
+      g_free (self->sdk);
+      self->sdk = g_strdup (sdk);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SDK]);
+    }
+}
+
+const gchar *
+ide_docs_query_get_language (IdeDocsQuery *self)
+{
+  g_return_val_if_fail (IDE_IS_DOCS_QUERY (self), NULL);
+
+  return self->language;
+}
+
+void
+ide_docs_query_set_language (IdeDocsQuery *self,
+                             const gchar  *language)
+{
+  g_return_if_fail (IDE_IS_DOCS_QUERY (self));
+
+  if (g_strcmp0 (language, self->language) != 0)
+    {
+      g_free (self->language);
+      self->language = g_strdup (language);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LANGUAGE]);
+    }
+}
+
+const gchar *
+ide_docs_query_get_fuzzy (IdeDocsQuery *self)
+{
+  g_return_val_if_fail (IDE_IS_DOCS_QUERY (self), NULL);
+
+  return self->fuzzy;
+}
diff --git a/src/libide/docs/ide-docs-query.h b/src/libide/docs/ide-docs-query.h
new file mode 100644
index 000000000..8672079ec
--- /dev/null
+++ b/src/libide/docs/ide-docs-query.h
@@ -0,0 +1,52 @@
+/* ide-docs-query.h
+ *
+ * Copyright 2019 Christian Hergert <unknown domain org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DOCS_QUERY (ide_docs_query_get_type())
+
+IDE_AVAILABLE_IN_3_34
+G_DECLARE_FINAL_TYPE (IdeDocsQuery, ide_docs_query, IDE, DOCS_QUERY, GObject)
+
+IDE_AVAILABLE_IN_3_34
+IdeDocsQuery *ide_docs_query_new          (void);
+IDE_AVAILABLE_IN_3_34
+const gchar  *ide_docs_query_get_keyword  (IdeDocsQuery *self);
+IDE_AVAILABLE_IN_3_34
+void          ide_docs_query_set_keyword  (IdeDocsQuery *self,
+                                           const gchar  *keyword);
+IDE_AVAILABLE_IN_3_34
+const gchar  *ide_docs_query_get_fuzzy    (IdeDocsQuery *self);
+IDE_AVAILABLE_IN_3_34
+const gchar  *ide_docs_query_get_sdk      (IdeDocsQuery *self);
+IDE_AVAILABLE_IN_3_34
+void          ide_docs_query_set_sdk      (IdeDocsQuery *self,
+                                           const gchar  *sdk);
+IDE_AVAILABLE_IN_3_34
+const gchar  *ide_docs_query_get_language (IdeDocsQuery *self);
+IDE_AVAILABLE_IN_3_34
+void          ide_docs_query_set_language (IdeDocsQuery *self,
+                                           const gchar  *language);
+
+G_END_DECLS
diff --git a/src/libide/docs/ide-docs-search-model.c b/src/libide/docs/ide-docs-search-model.c
new file mode 100644
index 000000000..f0c13b7a8
--- /dev/null
+++ b/src/libide/docs/ide-docs-search-model.c
@@ -0,0 +1,306 @@
+/* ide-docs-search-model.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-docs-search-model"
+
+#include "config.h"
+
+#include "ide-docs-search-model.h"
+
+#define DEFAULT_MAX_CHILDREN 3
+
+struct _IdeDocsSearchModel
+{
+  GObject  parent_instance;
+
+  GArray  *groups;
+
+  guint    max_children;
+};
+
+typedef struct
+{
+  IdeDocsItem *group;
+  guint        expanded : 1;
+} Group;
+
+static GType
+ide_docs_search_model_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_DOCS_ITEM;
+}
+
+static guint
+ide_docs_search_model_get_n_items (GListModel *model)
+{
+  IdeDocsSearchModel *self = (IdeDocsSearchModel *)model;
+  guint n_items = 0;
+
+  g_assert (IDE_IS_DOCS_SEARCH_MODEL (self));
+
+  for (guint i = 0; i < self->groups->len; i++)
+    {
+      const Group *g = &g_array_index (self->groups, Group, i);
+      guint n_children = ide_docs_item_get_n_children (g->group);
+
+      /* Add the group title */
+      n_items++;
+
+      /* Add the items (depending on expanded state) */
+      if (g->expanded)
+        n_items += n_children;
+      else
+        n_items += MIN (n_children, self->max_children);
+    }
+
+  return n_items;
+}
+
+static gpointer
+ide_docs_search_model_get_item (GListModel *model,
+                                guint       position)
+{
+  IdeDocsSearchModel *self = (IdeDocsSearchModel *)model;
+
+  g_assert (IDE_IS_DOCS_SEARCH_MODEL (self));
+  g_assert (position < ide_docs_search_model_get_n_items (model));
+
+  for (guint i = 0; i < self->groups->len; i++)
+    {
+      const Group *g = &g_array_index (self->groups, Group, i);
+      guint n_children = ide_docs_item_get_n_children (g->group);
+
+      if (position == 0)
+        return g_object_ref (g->group);
+
+      position--;
+
+      if (!g->expanded)
+        n_children = MIN (n_children, self->max_children);
+
+      if (position >= n_children)
+        {
+          position -= n_children;
+          continue;
+        }
+
+      return g_object_ref (ide_docs_item_get_nth_child (g->group, position));
+    }
+
+  g_return_val_if_reached (NULL);
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = ide_docs_search_model_get_item_type;
+  iface->get_n_items = ide_docs_search_model_get_n_items;
+  iface->get_item = ide_docs_search_model_get_item;
+}
+
+G_DEFINE_TYPE_WITH_CODE (IdeDocsSearchModel, ide_docs_search_model, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static void
+clear_group_cb (gpointer data)
+{
+  Group *g = data;
+  g_clear_object (&g->group);
+}
+
+static void
+ide_docs_search_model_finalize (GObject *object)
+{
+  IdeDocsSearchModel *self = (IdeDocsSearchModel *)object;
+
+  g_clear_pointer (&self->groups, g_array_unref);
+
+  G_OBJECT_CLASS (ide_docs_search_model_parent_class)->finalize (object);
+}
+
+static void
+ide_docs_search_model_class_init (IdeDocsSearchModelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_docs_search_model_finalize;
+}
+
+static void
+ide_docs_search_model_init (IdeDocsSearchModel *self)
+{
+  self->max_children = DEFAULT_MAX_CHILDREN;
+  self->groups = g_array_new (FALSE, FALSE, sizeof (Group));
+  g_array_set_clear_func (self->groups, clear_group_cb);
+}
+
+IdeDocsSearchModel *
+ide_docs_search_model_new (void)
+{
+  return g_object_new (IDE_TYPE_DOCS_SEARCH_MODEL, NULL);
+}
+
+void
+ide_docs_search_model_add_group (IdeDocsSearchModel *self,
+                                 IdeDocsItem        *group)
+{
+  Group to_add = {0};
+  guint n_children;
+  guint position = 0;
+  guint added;
+  gint priority;
+
+  g_return_if_fail (IDE_IS_DOCS_SEARCH_MODEL (self));
+  g_return_if_fail (IDE_IS_DOCS_ITEM (group));
+
+  if (ide_docs_item_get_n_children (group) == 0)
+    return;
+
+  to_add.group = g_object_ref (group);
+  to_add.expanded = FALSE;
+
+  priority = ide_docs_item_get_priority (group);
+  added = n_children = ide_docs_item_get_n_children (group);
+  if (added > self->max_children)
+    added = self->max_children;
+
+  /* This is a hacky way to let the row know how many children were
+   * in the group when creating this model so that it can show the
+   * proper +N header.
+   */
+  g_object_set_data (G_OBJECT (group),
+                     "N_INVISIBLE",
+                     GUINT_TO_POINTER (n_children - added));
+
+  /* Add the group header */
+  added++;
+
+  for (guint i = 0; i < self->groups->len; i++)
+    {
+      const Group *g = &g_array_index (self->groups, Group, i);
+
+      if (ide_docs_item_get_priority (g->group) > priority)
+        {
+          g_array_insert_val (self->groups, i, to_add);
+          g_list_model_items_changed (G_LIST_MODEL (self), position, 0, added);
+          return;
+        }
+
+      /* Skip the group header */
+      position++;
+
+      n_children = ide_docs_item_get_n_children (g->group);
+
+      if (g->expanded)
+        position += n_children;
+      else
+        position += MIN (n_children, self->max_children);
+    }
+
+  g_assert (position == ide_docs_search_model_get_n_items (G_LIST_MODEL (self)));
+
+  g_array_append_val (self->groups, to_add);
+  g_list_model_items_changed (G_LIST_MODEL (self), position, 0, added);
+}
+
+static void
+ide_docs_search_model_toggle (IdeDocsSearchModel *self,
+                              IdeDocsItem        *group,
+                              gboolean            expanded)
+{
+  guint position = 0;
+
+  g_return_if_fail (IDE_IS_DOCS_SEARCH_MODEL (self));
+  g_return_if_fail (IDE_IS_DOCS_ITEM (group));
+
+  for (guint i = 0; i < self->groups->len; i++)
+    {
+      Group *g = &g_array_index (self->groups, Group, i);
+      guint n_children = ide_docs_item_get_n_children (g->group);
+      guint removed = 0;
+      guint added = 0;
+
+      /* Skip the group header */
+      position++;
+
+      if (g->group != group)
+        {
+          if (g->expanded)
+            position += n_children;
+          else
+            position += MIN (self->max_children, n_children);
+
+          continue;
+        }
+
+      if (g->expanded == expanded)
+        return;
+
+      g->expanded = !g->expanded;
+
+      if (g->expanded)
+        {
+          /* expanding */
+          removed = MIN (self->max_children, n_children);
+          added = n_children;
+        }
+      else
+        {
+          /* collapsing */
+          removed = n_children;
+          added = MIN (self->max_children, n_children);
+        }
+
+      g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added);
+      break;
+    }
+}
+
+void
+ide_docs_search_model_collapse_group (IdeDocsSearchModel *self,
+                                      IdeDocsItem        *group)
+{
+  g_return_if_fail (IDE_IS_DOCS_SEARCH_MODEL (self));
+  g_return_if_fail (IDE_IS_DOCS_ITEM (group));
+
+  ide_docs_search_model_toggle (self, group, FALSE);
+}
+
+void
+ide_docs_search_model_expand_group (IdeDocsSearchModel *self,
+                                    IdeDocsItem        *group)
+{
+  g_return_if_fail (IDE_IS_DOCS_SEARCH_MODEL (self));
+  g_return_if_fail (IDE_IS_DOCS_ITEM (group));
+
+  ide_docs_search_model_toggle (self, group, TRUE);
+}
+
+void
+ide_docs_search_model_set_max_children (IdeDocsSearchModel *self,
+                                        guint               max_children)
+{
+  g_return_if_fail (IDE_IS_DOCS_SEARCH_MODEL (self));
+
+  if (max_children == 0)
+    self->max_children = DEFAULT_MAX_CHILDREN;
+  else
+    self->max_children = max_children;
+}
diff --git a/src/libide/docs/ide-docs-search-model.h b/src/libide/docs/ide-docs-search-model.h
new file mode 100644
index 000000000..140814854
--- /dev/null
+++ b/src/libide/docs/ide-docs-search-model.h
@@ -0,0 +1,41 @@
+/* ide-docs-search-model.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "ide-docs-item.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DOCS_SEARCH_MODEL (ide_docs_search_model_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDocsSearchModel, ide_docs_search_model, IDE, DOCS_SEARCH_MODEL, GObject)
+
+IdeDocsSearchModel *ide_docs_search_model_new              (void);
+void                ide_docs_search_model_set_max_children (IdeDocsSearchModel *self,
+                                                            guint               max_children);
+void                ide_docs_search_model_add_group        (IdeDocsSearchModel *self,
+                                                            IdeDocsItem        *group);
+void                ide_docs_search_model_collapse_group   (IdeDocsSearchModel *self,
+                                                            IdeDocsItem        *group);
+void                ide_docs_search_model_expand_group     (IdeDocsSearchModel *self,
+                                                            IdeDocsItem        *group);
+
+G_END_DECLS
diff --git a/src/libide/docs/ide-docs-search-row.c b/src/libide/docs/ide-docs-search-row.c
new file mode 100644
index 000000000..4e5c872f9
--- /dev/null
+++ b/src/libide/docs/ide-docs-search-row.c
@@ -0,0 +1,241 @@
+/* ide-docs-search-row.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-docs-search-row"
+
+#include "config.h"
+
+#include "ide-docs-search-row.h"
+
+struct _IdeDocsSearchRow
+{
+  DzlListBoxRow parent_instance;
+
+  IdeDocsItem *item;
+
+  /* Template Widgets */
+  GtkLabel *label;
+  GtkImage *image;
+};
+
+enum {
+  PROP_0,
+  PROP_ITEM,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeDocsSearchRow, ide_docs_search_row, DZL_TYPE_LIST_BOX_ROW)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_docs_search_row_set_item (IdeDocsSearchRow *self,
+                              IdeDocsItem      *item)
+{
+  g_autofree gchar *with_size = NULL;
+  GtkStyleContext *style_context;
+  const gchar *icon_name;
+  const gchar *title;
+  IdeDocsItemKind kind;
+  gboolean use_markup;
+
+  g_return_if_fail (IDE_IS_DOCS_SEARCH_ROW (self));
+  g_return_if_fail (!item || IDE_IS_DOCS_ITEM (item));
+
+  g_set_object (&self->item, item);
+
+  if (item == NULL)
+    return;
+
+  kind = ide_docs_item_get_kind (self->item);
+
+  switch (kind)
+    {
+    case IDE_DOCS_ITEM_KIND_FUNCTION:
+      icon_name = "lang-function-symbolic";
+      break;
+
+    case IDE_DOCS_ITEM_KIND_METHOD:
+      icon_name = "lang-method-symbolic";
+      break;
+
+    case IDE_DOCS_ITEM_KIND_CLASS:
+      icon_name = "lang-class-symbolic";
+      break;
+
+    case IDE_DOCS_ITEM_KIND_ENUM:
+      icon_name = "lang-enum-symbolic";
+      break;
+
+    case IDE_DOCS_ITEM_KIND_CONSTANT:
+      icon_name = "lang-enum-value-symbolic";
+      break;
+
+    case IDE_DOCS_ITEM_KIND_MACRO:
+      icon_name = "lang-define-symbolic";
+      break;
+
+    case IDE_DOCS_ITEM_KIND_STRUCT:
+      icon_name = "lang-struct-symbolic";
+      break;
+
+    case IDE_DOCS_ITEM_KIND_UNION:
+      icon_name = "lang-union-symbolic";
+      break;
+
+    case IDE_DOCS_ITEM_KIND_PROPERTY:
+      icon_name = "lang-variable-symbolic";
+      break;
+
+    case IDE_DOCS_ITEM_KIND_BOOK:
+    case IDE_DOCS_ITEM_KIND_CHAPTER:
+    case IDE_DOCS_ITEM_KIND_COLLECTION:
+    case IDE_DOCS_ITEM_KIND_MEMBER:
+    case IDE_DOCS_ITEM_KIND_NONE:
+    case IDE_DOCS_ITEM_KIND_SIGNAL:
+    default:
+      icon_name = NULL;
+      break;
+    }
+
+  gtk_label_set_use_markup (self->label, FALSE);
+
+  if ((title = ide_docs_item_get_display_name (self->item)))
+    use_markup = TRUE;
+  else
+    title = ide_docs_item_get_title (self->item);
+
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
+
+  if (kind == IDE_DOCS_ITEM_KIND_BOOK || ide_docs_item_has_child (item))
+    {
+      guint n_invisible = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (item), "N_INVISIBLE"));
+
+      gtk_style_context_add_class (style_context, "header");
+
+      if (n_invisible > 0)
+        title = with_size = g_strdup_printf ("%s     +%u", title, n_invisible);
+    }
+  else
+    {
+      gtk_style_context_remove_class (style_context, "header");
+    }
+
+  g_object_set (self->image, "icon-name", icon_name, NULL);
+
+  gtk_label_set_label (self->label, title);
+  gtk_label_set_use_markup (self->label, use_markup);
+}
+
+static void
+ide_docs_search_row_finalize (GObject *object)
+{
+  IdeDocsSearchRow *self = (IdeDocsSearchRow *)object;
+
+  g_clear_object (&self->item);
+
+  G_OBJECT_CLASS (ide_docs_search_row_parent_class)->finalize (object);
+}
+
+static void
+ide_docs_search_row_get_property (GObject    *object,
+                                  guint       prop_id,
+                                  GValue     *value,
+                                  GParamSpec *pspec)
+{
+  IdeDocsSearchRow *self = IDE_DOCS_SEARCH_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_ITEM:
+      g_value_set_object (value, self->item);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_docs_search_row_set_property (GObject      *object,
+                                  guint         prop_id,
+                                  const GValue *value,
+                                  GParamSpec   *pspec)
+{
+  IdeDocsSearchRow *self = IDE_DOCS_SEARCH_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_ITEM:
+      ide_docs_search_row_set_item (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_docs_search_row_class_init (IdeDocsSearchRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_docs_search_row_finalize;
+  object_class->get_property = ide_docs_search_row_get_property;
+  object_class->set_property = ide_docs_search_row_set_property;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-docs/ui/ide-docs-search-row.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeDocsSearchRow, image);
+  gtk_widget_class_bind_template_child (widget_class, IdeDocsSearchRow, label);
+
+  properties [PROP_ITEM] =
+    g_param_spec_object ("item",
+                         "Item",
+                         "The item to display",
+                         IDE_TYPE_DOCS_ITEM,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+  
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_docs_search_row_init (IdeDocsSearchRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+GtkWidget *
+ide_docs_search_row_new (IdeDocsItem *item)
+{
+  g_return_val_if_fail (IDE_IS_DOCS_ITEM (item), NULL);
+
+  return g_object_new (IDE_TYPE_DOCS_SEARCH_ROW,
+                       "item", item,
+                       NULL);
+}
+
+IdeDocsItem *
+ide_docs_search_row_get_item (IdeDocsSearchRow *self)
+{
+  g_return_val_if_fail (IDE_IS_DOCS_SEARCH_ROW (self), NULL);
+
+  return self->item;
+}
diff --git a/src/libide/docs/ide-docs-search-row.h b/src/libide/docs/ide-docs-search-row.h
new file mode 100644
index 000000000..220c68f24
--- /dev/null
+++ b/src/libide/docs/ide-docs-search-row.h
@@ -0,0 +1,36 @@
+/* ide-docs-search-row.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <dazzle.h>
+
+#include "ide-docs-item.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DOCS_SEARCH_ROW (ide_docs_search_row_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDocsSearchRow, ide_docs_search_row, IDE, DOCS_SEARCH_ROW, DzlListBoxRow)
+
+GtkWidget   *ide_docs_search_row_new      (IdeDocsItem      *item);
+IdeDocsItem *ide_docs_search_row_get_item (IdeDocsSearchRow *self);
+
+G_END_DECLS
diff --git a/src/libide/docs/ide-docs-search-row.ui b/src/libide/docs/ide-docs-search-row.ui
new file mode 100644
index 000000000..ed198e3ed
--- /dev/null
+++ b/src/libide/docs/ide-docs-search-row.ui
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeDocsSearchRow" parent="DzlListBoxRow">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">horizontal</property>
+        <property name="spacing">12</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkLabel" id="label">
+            <property name="ellipsize">end</property>
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+            <property name="width-chars">60</property>
+            <property name="max-width-chars">55</property>
+            <property name="xalign">0</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkImage" id="image">
+            <property name="pixel-size">16</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/docs/ide-docs-search-section.c b/src/libide/docs/ide-docs-search-section.c
new file mode 100644
index 000000000..beadfa4a4
--- /dev/null
+++ b/src/libide/docs/ide-docs-search-section.c
@@ -0,0 +1,335 @@
+/* ide-docs-search-section.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-docs-search-section"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include "ide-docs-search-model.h"
+#include "ide-docs-search-row.h"
+#include "ide-docs-search-section.h"
+
+#define MAX_ALLOWED_BY_GROUP 1000
+
+struct _IdeDocsSearchSection
+{
+  GtkBin              parent_instance;
+
+  DzlListBox         *groups;
+
+  gchar              *title;
+
+  gint                priority;
+
+  guint               max_children;
+
+  guint               show_all_results : 1;
+};
+
+G_DEFINE_TYPE (IdeDocsSearchSection, ide_docs_search_section, GTK_TYPE_BIN)
+
+enum {
+  PROP_0,
+  PROP_PRIORITY,
+  PROP_MAX_CHILDREN,
+  PROP_SHOW_ALL_RESULTS,
+  PROP_TITLE,
+  N_PROPS
+};
+
+enum {
+  ITEM_ACTIVATED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+on_row_activated_cb (IdeDocsSearchSection *self,
+                     IdeDocsSearchRow     *row,
+                     DzlListBox           *list_box)
+{
+  IdeDocsItem *item;
+
+  g_assert (IDE_IS_DOCS_SEARCH_SECTION (self));
+  g_assert (IDE_IS_DOCS_SEARCH_ROW (row));
+  g_assert (DZL_IS_LIST_BOX (list_box));
+
+  item = ide_docs_search_row_get_item (row);
+
+  g_signal_emit (self, signals [ITEM_ACTIVATED], 0, item);
+}
+
+static void
+ide_docs_search_section_finalize (GObject *object)
+{
+  IdeDocsSearchSection *self = (IdeDocsSearchSection *)object;
+
+  g_clear_pointer (&self->title, g_free);
+
+  G_OBJECT_CLASS (ide_docs_search_section_parent_class)->finalize (object);
+}
+
+static void
+ide_docs_search_section_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  IdeDocsSearchSection *self = IDE_DOCS_SEARCH_SECTION (object);
+
+  switch (prop_id)
+    {
+    case PROP_MAX_CHILDREN:
+      g_value_set_uint (value, self->max_children);
+      break;
+
+    case PROP_PRIORITY:
+      g_value_set_int (value, ide_docs_search_section_get_priority (self));
+      break;
+
+    case PROP_SHOW_ALL_RESULTS:
+      g_value_set_boolean (value, self->show_all_results);
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, self->title);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_docs_search_section_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  IdeDocsSearchSection *self = IDE_DOCS_SEARCH_SECTION (object);
+
+  switch (prop_id)
+    {
+    case PROP_MAX_CHILDREN:
+      self->max_children = g_value_get_uint (value);
+      break;
+
+    case PROP_PRIORITY:
+      ide_docs_search_section_set_priority (self, g_value_get_int (value));
+      break;
+
+    case PROP_SHOW_ALL_RESULTS:
+      self->show_all_results = g_value_get_boolean (value);
+      break;
+
+    case PROP_TITLE:
+      self->title = g_value_dup_string (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_docs_search_section_class_init (IdeDocsSearchSectionClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_docs_search_section_finalize;
+  object_class->get_property = ide_docs_search_section_get_property;
+  object_class->set_property = ide_docs_search_section_set_property;
+
+  properties [PROP_MAX_CHILDREN] =
+    g_param_spec_uint ("max-children",
+                       "Max Children",
+                       "Max children per group",
+                       0, G_MAXUINT, 3,
+                       (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PRIORITY] =
+    g_param_spec_int ("priority",
+                      "Priority",
+                      "THe priority of the section",
+                      G_MININT, G_MAXINT, 0,
+                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SHOW_ALL_RESULTS] =
+    g_param_spec_boolean ("show-all-results",
+                          "Show All Results",
+                          "Show all of the results from groups",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the section",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+  
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_css_name (widget_class, "IdeDocsSearchSection");
+
+  signals [ITEM_ACTIVATED] =
+    g_signal_new ("item-activated",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL,
+                  NULL,
+                  G_TYPE_NONE, 1, IDE_TYPE_DOCS_ITEM);
+}
+
+static void
+ide_docs_search_section_init (IdeDocsSearchSection *self)
+{
+  self->max_children = 3;
+  self->groups = g_object_new (DZL_TYPE_LIST_BOX,
+                               "row-type", IDE_TYPE_DOCS_SEARCH_ROW,
+                               "property-name", "item",
+                               "selection-mode", GTK_SELECTION_NONE,
+                               "visible", TRUE,
+                               NULL);
+  dzl_list_box_set_recycle_max (self->groups, 100);
+  g_signal_connect_object (self->groups,
+                           "row-activated",
+                           G_CALLBACK (on_row_activated_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->groups));
+}
+
+GtkWidget *
+ide_docs_search_section_new (const gchar *title)
+{
+  return g_object_new (IDE_TYPE_DOCS_SEARCH_SECTION,
+                       "title", title,
+                       NULL);
+}
+
+const gchar *
+ide_docs_search_section_get_title (IdeDocsSearchSection *self)
+{
+  g_return_val_if_fail (IDE_IS_DOCS_SEARCH_SECTION (self), NULL);
+
+  return self->title;
+}
+
+gint
+ide_docs_search_section_get_priority (IdeDocsSearchSection *self)
+{
+  g_return_val_if_fail (IDE_IS_DOCS_SEARCH_SECTION (self), 0);
+
+  return self->priority;
+}
+
+void
+ide_docs_search_section_set_priority (IdeDocsSearchSection *self,
+                                      gint                  priority)
+{
+  g_return_if_fail (IDE_IS_DOCS_SEARCH_SECTION (self));
+
+  if (priority != self->priority)
+    {
+      self->priority = priority;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PRIORITY]);
+    }
+}
+
+void
+ide_docs_search_section_add_groups (IdeDocsSearchSection *self,
+                                    IdeDocsItem          *parent)
+{
+  g_return_if_fail (IDE_IS_DOCS_SEARCH_SECTION (self));
+  g_return_if_fail (IDE_IS_DOCS_ITEM (parent));
+
+  /* Clear state before we add new stuff, so we get a chance to
+   * re-use cached listbox rows.
+   */
+  dzl_list_box_set_model (self->groups, NULL);
+  gtk_widget_hide (GTK_WIDGET (self->groups));
+
+  if (self->show_all_results)
+    {
+      g_autoptr(GListStore) model = g_list_store_new (IDE_TYPE_DOCS_ITEM);
+      g_autoptr(IdeDocsItem) copy = NULL;
+
+      /* Make a fake title with no children so we don't get
+       * the +123 items in the header.
+       */
+      copy = ide_docs_item_new ();
+      ide_docs_item_set_title (copy, ide_docs_item_get_title (parent));
+      ide_docs_item_set_kind (copy, IDE_DOCS_ITEM_KIND_BOOK);
+      g_list_store_append (model, copy);
+
+      for (const GList *iter = ide_docs_item_get_children (parent);
+           iter != NULL;
+           iter = iter->next)
+        {
+          IdeDocsItem *child = iter->data;
+
+          g_assert (IDE_IS_DOCS_ITEM (child));
+
+          g_list_store_append (model, child);
+        }
+
+      dzl_list_box_set_model (self->groups, G_LIST_MODEL (model));
+    }
+  else
+    {
+      g_autoptr(IdeDocsSearchModel) model = ide_docs_search_model_new ();
+
+      if (self->max_children)
+        ide_docs_search_model_set_max_children (model, self->max_children);
+
+      for (const GList *iter = ide_docs_item_get_children (parent);
+           iter != NULL;
+           iter = iter->next)
+        {
+          IdeDocsItem *child = iter->data;
+
+          g_assert (IDE_IS_DOCS_ITEM (child));
+
+          /* Truncate to a reasonable number to avoid very large lists */
+          ide_docs_item_truncate (child, MAX_ALLOWED_BY_GROUP);
+
+          ide_docs_search_model_add_group (model, child);
+
+          dzl_list_box_set_model (self->groups, G_LIST_MODEL (model));
+        }
+    }
+
+  gtk_widget_show (GTK_WIDGET (self->groups));
+}
+
+gboolean
+ide_docs_search_section_get_show_all_results (IdeDocsSearchSection *self)
+{
+  g_return_val_if_fail (IDE_IS_DOCS_SEARCH_SECTION (self), FALSE);
+
+  return self->show_all_results;
+}
diff --git a/src/libide/docs/ide-docs-search-section.h b/src/libide/docs/ide-docs-search-section.h
new file mode 100644
index 000000000..b17b1c901
--- /dev/null
+++ b/src/libide/docs/ide-docs-search-section.h
@@ -0,0 +1,40 @@
+/* ide-docs-search-section.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DOCS_SEARCH_SECTION (ide_docs_search_section_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDocsSearchSection, ide_docs_search_section, IDE, DOCS_SEARCH_SECTION, GtkBin)
+
+GtkWidget   *ide_docs_search_section_new                  (const gchar          *title);
+const gchar *ide_docs_search_section_get_title            (IdeDocsSearchSection *self);
+gint         ide_docs_search_section_get_priority         (IdeDocsSearchSection *self);
+void         ide_docs_search_section_set_priority         (IdeDocsSearchSection *self,
+                                                           gint                  priority);
+gboolean     ide_docs_search_section_get_show_all_results (IdeDocsSearchSection *self);
+void         ide_docs_search_section_add_groups           (IdeDocsSearchSection *self,
+                                                           IdeDocsItem          *parent);
+
+G_END_DECLS
diff --git a/src/libide/docs/ide-docs-search-view.c b/src/libide/docs/ide-docs-search-view.c
new file mode 100644
index 000000000..e7cc87853
--- /dev/null
+++ b/src/libide/docs/ide-docs-search-view.c
@@ -0,0 +1,432 @@
+/* ide-docs-search-view.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-docs-search-view"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-gui.h>
+#include <libide-threading.h>
+
+#include "ide-docs-library.h"
+#include "ide-docs-search-section.h"
+#include "ide-docs-search-view.h"
+
+#define SINGLE_GROUP_N_CHILDREN 3
+#define MULTI_GROUP_N_CHILDREN  15
+
+struct _IdeDocsSearchView
+{
+  GtkBin             parent_instance;
+
+  /* We keep the last cancellable used to search so that we can
+   * cancel it when a second search has arrived which would cause
+   * the previous one to be ignored.
+   */
+  GCancellable      *search_cancellable;
+
+  /* The most recent full result set, so that we can go back after
+   * viewing a specific set and going backwards.
+   */
+  IdeDocsItem       *full_set;
+
+  GtkScrolledWindow *scroller;
+  DzlPriorityBox    *sections;
+  DzlPriorityBox    *titles;
+
+  /* Track our current search number so we can discard successful
+   * results that are not the current search.
+   */
+  guint              sequence;
+};
+
+G_DEFINE_TYPE (IdeDocsSearchView, ide_docs_search_view, GTK_TYPE_BIN)
+
+enum {
+  ITEM_ACTIVATED,
+  N_SIGNALS
+};
+
+static guint signals [N_SIGNALS];
+
+static void
+on_go_previous_clicked_cb (IdeDocsSearchView *self,
+                           GtkButton         *button)
+{
+  g_assert (IDE_IS_DOCS_SEARCH_VIEW (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  if (self->full_set != NULL)
+    ide_docs_search_view_add_sections (self, self->full_set);
+}
+
+static void
+ide_docs_search_view_add (GtkContainer *container,
+                          GtkWidget    *child)
+{
+  IdeDocsSearchView *self = (IdeDocsSearchView *)container;
+
+  g_assert (IDE_IS_DOCS_SEARCH_VIEW (self));
+  g_assert (GTK_IS_WIDGET (child));
+
+  if (IDE_IS_DOCS_SEARCH_SECTION (child))
+    {
+      const gchar *title = ide_docs_search_section_get_title (IDE_DOCS_SEARCH_SECTION (child));
+      gint priority = ide_docs_search_section_get_priority (IDE_DOCS_SEARCH_SECTION (child));
+      GtkSizeGroup *group;
+      GtkLabel *label;
+      GtkBox *box;
+      GtkBox *vbox;
+
+      gtk_container_add_with_properties (GTK_CONTAINER (self->sections), child,
+                                         "priority", priority,
+                                         NULL);
+
+      vbox = g_object_new (GTK_TYPE_BOX,
+                           "orientation", GTK_ORIENTATION_VERTICAL,
+                           "visible", TRUE,
+                           NULL);
+      gtk_container_add (GTK_CONTAINER (self->titles), GTK_WIDGET (vbox));
+
+      box = g_object_new (GTK_TYPE_BOX,
+                          "orientation", GTK_ORIENTATION_HORIZONTAL,
+                          "spacing", 6,
+                          "visible", TRUE,
+                          NULL);
+
+      if (ide_docs_search_section_get_show_all_results (IDE_DOCS_SEARCH_SECTION (child)))
+        {
+          GtkImage *image;
+          GtkButton *button;
+
+          button = g_object_new (GTK_TYPE_BUTTON,
+                                 "focus-on-click", FALSE,
+                                 "halign", GTK_ALIGN_END,
+                                 "valign", GTK_ALIGN_START,
+                                 "visible", TRUE,
+                                 NULL);
+          g_signal_connect_object (button,
+                                   "clicked",
+                                   G_CALLBACK (on_go_previous_clicked_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+          dzl_gtk_widget_add_style_class (GTK_WIDGET (button), "image-button");
+          dzl_gtk_widget_add_style_class (GTK_WIDGET (button), "flat");
+          gtk_container_add (GTK_CONTAINER (vbox), GTK_WIDGET (button));
+          gtk_container_add (GTK_CONTAINER (button), GTK_WIDGET (box));
+
+          image = g_object_new (GTK_TYPE_IMAGE,
+                                "icon-name", "go-previous-symbolic",
+                                "pixel-size", 16,
+                                "visible", TRUE,
+                                NULL);
+          gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (image));
+        }
+      else
+        {
+          gtk_container_add (GTK_CONTAINER (vbox), GTK_WIDGET (box));
+        }
+
+      label = g_object_new (GTK_TYPE_LABEL,
+                            "label", title,
+                            "visible", TRUE,
+                            NULL);
+      gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (label));
+
+      group = gtk_size_group_new (GTK_SIZE_GROUP_VERTICAL);
+      gtk_size_group_add_widget (group, GTK_WIDGET (vbox));
+      gtk_size_group_add_widget (group, GTK_WIDGET (child));
+      g_object_unref (group);
+
+      gtk_adjustment_set_value (gtk_scrolled_window_get_vadjustment (self->scroller), 0);
+
+      return;
+    }
+
+  GTK_CONTAINER_CLASS (ide_docs_search_view_parent_class)->add (container, child);
+}
+
+static void
+ide_docs_search_view_finalize (GObject *object)
+{
+  IdeDocsSearchView *self = (IdeDocsSearchView *)object;
+
+  g_assert (IDE_IS_DOCS_SEARCH_VIEW (self));
+
+  g_clear_object (&self->search_cancellable);
+  g_clear_object (&self->full_set);
+
+  G_OBJECT_CLASS (ide_docs_search_view_parent_class)->finalize (object);
+}
+
+static void
+ide_docs_search_view_class_init (IdeDocsSearchViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  object_class->finalize = ide_docs_search_view_finalize;
+
+  container_class->add = ide_docs_search_view_add;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-docs/ui/ide-docs-search-view.ui");
+  gtk_widget_class_set_css_name (widget_class, "IdeDocsSearchView");
+  gtk_widget_class_bind_template_child (widget_class, IdeDocsSearchView, scroller);
+  gtk_widget_class_bind_template_child (widget_class, IdeDocsSearchView, sections);
+  gtk_widget_class_bind_template_child (widget_class, IdeDocsSearchView, titles);
+
+  /**
+   * IdeDocsSearchView::item-activated:
+   * @self: an #IdeDocsSearchView
+   * @item: an #IdeDocsItem
+   *
+   * The "item-activated" signal is emitted when a documentation item
+   * has been activated and should be displayed to the user.
+   *
+   * Since: 3.34
+   */
+  signals [ITEM_ACTIVATED] =
+    g_signal_new ("item-activated",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL,
+                  NULL,
+                  G_TYPE_NONE, 1, IDE_TYPE_DOCS_ITEM);
+}
+
+static void
+ide_docs_search_view_init (IdeDocsSearchView *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+static void
+ide_docs_search_view_clear (IdeDocsSearchView *self)
+{
+  g_assert (IDE_IS_DOCS_SEARCH_VIEW (self));
+
+  gtk_container_foreach (GTK_CONTAINER (self->sections),
+                         (GtkCallback) gtk_widget_destroy,
+                         NULL);
+  gtk_container_foreach (GTK_CONTAINER (self->titles),
+                         (GtkCallback) gtk_widget_destroy,
+                         NULL);
+}
+
+static void
+on_item_activated_cb (IdeDocsSearchView    *self,
+                      IdeDocsItem          *item,
+                      IdeDocsSearchSection *old_section)
+{
+  g_assert (IDE_IS_DOCS_SEARCH_VIEW (self));
+  g_assert (IDE_IS_DOCS_ITEM (item));
+  g_assert (IDE_IS_DOCS_SEARCH_SECTION (old_section));
+
+  if (ide_docs_item_has_child (item))
+    {
+      IdeDocsSearchSection *section;
+
+      ide_docs_search_view_clear (self);
+
+      section = g_object_new (IDE_TYPE_DOCS_SEARCH_SECTION,
+                              "show-all-results", TRUE,
+                              "title", _("All Search Results"),
+                              NULL);
+      ide_docs_search_section_add_groups (section, item);
+      g_signal_connect_object (section,
+                               "item-activated",
+                               G_CALLBACK (on_item_activated_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+      gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (section));
+      gtk_widget_show (GTK_WIDGET (section));
+    }
+  else
+    {
+      g_signal_emit (self, signals [ITEM_ACTIVATED], 0, item);
+    }
+}
+
+void
+ide_docs_search_view_add_sections (IdeDocsSearchView *self,
+                                   IdeDocsItem       *item)
+{
+  guint count = 0;
+
+  g_assert (IDE_IS_DOCS_SEARCH_VIEW (self));
+  g_assert (!item || IDE_IS_DOCS_ITEM (item));
+
+  ide_docs_search_view_clear (self);
+
+  g_set_object (&self->full_set, item);
+
+  if (item == NULL)
+    return;
+
+  /* Count number of groups with children */
+  for (const GList *iter = ide_docs_item_get_children (item);
+       iter != NULL;
+       iter = iter->next)
+    count += ide_docs_item_get_n_children (iter->data);
+
+  /* The root IdeDocsItem contains the children which are groups,
+   * each containing the children within that category.
+   *
+   * For each group, we create a new searchgroup to contain the
+   * children items. If there are too many items to display, we
+   * let the user know how many items are in the group and provide
+   * a button to click to show the additional items.
+   */
+
+  for (const GList *iter = ide_docs_item_get_children (item);
+       iter != NULL;
+       iter = iter->next)
+    {
+      IdeDocsItem *child = iter->data;
+      IdeDocsSearchSection *section;
+      const gchar *title;
+      gint priority;
+
+      g_assert (IDE_IS_DOCS_ITEM (child));
+
+      /* Ignore children that have no items */
+      if (ide_docs_item_get_n_children (child) == 0)
+        continue;
+
+      /* Create a new group with the title */
+      title = ide_docs_item_get_title (child);
+      priority = ide_docs_item_get_priority (child);
+      section = g_object_new (IDE_TYPE_DOCS_SEARCH_SECTION,
+                              "title", title,
+                              "priority", priority,
+                              "max-children", count == 1 ? MULTI_GROUP_N_CHILDREN
+                                                         : SINGLE_GROUP_N_CHILDREN,
+                              NULL);
+      ide_docs_search_section_add_groups (section, child);
+      g_signal_connect_object (section,
+                               "item-activated",
+                               G_CALLBACK (on_item_activated_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+      gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (section));
+      gtk_widget_show (GTK_WIDGET (section));
+    }
+
+  gtk_adjustment_set_value (gtk_scrolled_window_get_vadjustment (self->scroller), 0);
+}
+
+GtkWidget *
+ide_docs_search_view_new (void)
+{
+  return g_object_new (IDE_TYPE_DOCS_SEARCH_VIEW, NULL);
+}
+
+static void
+ide_docs_search_view_search_cb (GObject      *object,
+                                GAsyncResult *result,
+                                gpointer      user_data)
+{
+  IdeDocsLibrary *library = (IdeDocsLibrary *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(IdeDocsItem) res = NULL;
+  g_autoptr(GError) error = NULL;
+  IdeDocsSearchView *self;
+  IdeDocsItem *results;
+  guint sequence;
+
+  g_assert (IDE_IS_DOCS_LIBRARY (library));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_docs_library_search_finish (library, result, &error))
+    {
+      /* Don't clear results in case of error (which might just
+       * be a cancellation).
+       */
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  self = ide_task_get_source_object (task);
+  results = ide_task_get_task_data (task);
+  sequence = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "SEARCH_SEQUENCE"));
+
+  if (sequence == self->sequence)
+    ide_docs_search_view_add_sections (self, results);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+void
+ide_docs_search_view_search_async (IdeDocsSearchView   *self,
+                                   IdeDocsQuery        *query,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(IdeDocsItem) results = NULL;
+  IdeDocsLibrary *library;
+  IdeContext *context;
+
+  g_return_if_fail (IDE_IS_DOCS_SEARCH_VIEW (self));
+  g_return_if_fail (IDE_IS_DOCS_QUERY (query));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  g_cancellable_cancel (self->search_cancellable);
+  g_set_object (&self->search_cancellable, cancellable);
+
+  if (self->search_cancellable == NULL)
+    self->search_cancellable = g_cancellable_new ();
+
+  context = ide_widget_get_context (GTK_WIDGET (self));
+  library = ide_docs_library_from_context (context);
+  results = ide_docs_item_new ();
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_docs_search_view_search_async);
+  ide_task_set_task_data (task, g_object_ref (results), g_object_unref);
+
+  g_object_set_data (G_OBJECT (task),
+                     "SEARCH_SEQUENCE",
+                     GINT_TO_POINTER (++self->sequence));
+
+  ide_docs_library_search_async (library,
+                                 query,
+                                 results,
+                                 self->search_cancellable,
+                                 ide_docs_search_view_search_cb,
+                                 g_steal_pointer (&task));
+}
+
+gboolean
+ide_docs_search_view_search_finish (IdeDocsSearchView  *self,
+                                    GAsyncResult       *result,
+                                    GError            **error)
+{
+  g_return_val_if_fail (IDE_IS_DOCS_SEARCH_VIEW (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
diff --git a/src/libide/docs/ide-docs-search-view.h b/src/libide/docs/ide-docs-search-view.h
new file mode 100644
index 000000000..4a7b86161
--- /dev/null
+++ b/src/libide/docs/ide-docs-search-view.h
@@ -0,0 +1,47 @@
+/* ide-docs-search-view.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "ide-docs-item.h"
+#include "ide-docs-query.h"
+#include "ide-docs-search-view.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DOCS_SEARCH_VIEW (ide_docs_search_view_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDocsSearchView, ide_docs_search_view, IDE, DOCS_SEARCH_VIEW, GtkBin)
+
+GtkWidget *ide_docs_search_view_new           (void);
+void       ide_docs_search_view_add_sections  (IdeDocsSearchView    *self,
+                                               IdeDocsItem          *parent);
+void       ide_docs_search_view_search_async  (IdeDocsSearchView    *self,
+                                               IdeDocsQuery         *query,
+                                               GCancellable         *cancellable,
+                                               GAsyncReadyCallback   callback,
+                                               gpointer              user_data);
+gboolean   ide_docs_search_view_search_finish (IdeDocsSearchView    *self,
+                                               GAsyncResult         *result,
+                                               GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/docs/ide-docs-search-view.ui b/src/libide/docs/ide-docs-search-view.ui
new file mode 100644
index 000000000..442716f76
--- /dev/null
+++ b/src/libide/docs/ide-docs-search-view.ui
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeDocsSearchView" parent="GtkBin">
+    <child>
+      <object class="GtkScrolledWindow" id="scroller">
+        <property name="hscrollbar-policy">never</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkBox">
+            <property name="margin">48</property>
+            <property name="spacing">24</property>
+            <property name="orientation">horizontal</property>
+            <property name="visible">true</property>
+            <child type="center">
+              <object class="DzlPriorityBox" id="sections">
+                <property name="orientation">vertical</property>
+                <property name="visible">true</property>
+                <property name="spacing">24</property>
+              </object>
+            </child>
+            <child>
+              <object class="DzlPriorityBox" id="titles">
+                <property name="halign">end</property>
+                <property name="hexpand">true</property>
+                <property name="orientation">vertical</property>
+                <property name="spacing">24</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="titles"/>
+                </style>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/docs/ide-docs-view.c b/src/libide/docs/ide-docs-view.c
new file mode 100644
index 000000000..541089c1b
--- /dev/null
+++ b/src/libide/docs/ide-docs-view.c
@@ -0,0 +1,90 @@
+/* ide-docs-view.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-docs-view"
+
+#include "config.h"
+
+#include <string.h>
+#include <webkit2/webkit2.h>
+
+#include "ide-docs-view.h"
+
+struct _IdeDocsView
+{
+  GtkBin parent_instance;
+
+  WebKitWebView *web_view;
+};
+
+G_DEFINE_TYPE (IdeDocsView, ide_docs_view, GTK_TYPE_BIN)
+
+static void
+ide_docs_view_class_init (IdeDocsViewClass *klass)
+{
+}
+
+static void
+ide_docs_view_init (IdeDocsView *self)
+{
+  self->web_view = g_object_new (WEBKIT_TYPE_WEB_VIEW,
+                                 "expand", TRUE,
+                                 "visible", TRUE,
+                                 NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->web_view));
+}
+
+GtkWidget *
+ide_docs_view_new (void)
+{
+  return g_object_new (IDE_TYPE_DOCS_VIEW, NULL);
+}
+
+void
+ide_docs_view_set_item (IdeDocsView *self,
+                        IdeDocsItem *item)
+{
+  const gchar *url;
+
+  g_return_if_fail (IDE_IS_DOCS_VIEW (self));
+  g_return_if_fail (IDE_IS_DOCS_ITEM (item));
+
+  if ((url = ide_docs_item_get_url (item)))
+    {
+      g_autofree gchar *generated = NULL;
+
+      if (strstr (url, "://") == NULL)
+        {
+          IdeDocsItem *parent;
+
+          if ((parent = ide_docs_item_get_parent (item)))
+            {
+              const gchar *purl = ide_docs_item_get_url (parent);
+
+              if (purl != NULL)
+                url = generated = g_strdup_printf ("file://%s/%s", purl, url);
+            }
+        }
+
+      g_print ("%s\n", url);
+
+      webkit_web_view_load_uri (self->web_view, url);
+    }
+}
diff --git a/src/libide/docs/ide-docs-view.h b/src/libide/docs/ide-docs-view.h
new file mode 100644
index 000000000..7fa694d72
--- /dev/null
+++ b/src/libide/docs/ide-docs-view.h
@@ -0,0 +1,37 @@
+/* ide-docs-view.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "ide-docs-item.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DOCS_VIEW (ide_docs_view_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDocsView, ide_docs_view, IDE, DOCS_VIEW, GtkBin)
+
+GtkWidget *ide_docs_view_new      (void);
+void       ide_docs_view_set_item (IdeDocsView *self,
+                                   IdeDocsItem *item);
+
+G_END_DECLS
diff --git a/src/libide/docs/ide-docs-workspace.c b/src/libide/docs/ide-docs-workspace.c
new file mode 100644
index 000000000..40b0ed717
--- /dev/null
+++ b/src/libide/docs/ide-docs-workspace.c
@@ -0,0 +1,182 @@
+/* ide-docs-workspace.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-docs-workspace"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-plugins.h>
+#include <libide-threading.h>
+
+#include "ide-docs-item.h"
+#include "ide-docs-library.h"
+#include "ide-docs-provider.h"
+#include "ide-docs-search-view.h"
+#include "ide-docs-view.h"
+#include "ide-docs-workspace.h"
+
+struct _IdeDocsWorkspace
+{
+  IdeWorkspace            parent_instance;
+
+  guint                   queued_search;
+
+  /* Template Widgets */
+  GtkStack               *stack;
+  IdeDocsSearchView      *search_view;
+  IdeDocsView            *view;
+  GtkEntry               *entry;
+};
+
+G_DEFINE_TYPE (IdeDocsWorkspace, ide_docs_workspace, IDE_TYPE_WORKSPACE)
+
+/**
+ * ide_docs_workspace_new:
+ *
+ * Create a new #IdeDocsWorkspace.
+ *
+ * Returns: (transfer full): a newly created #IdeDocsWorkspace
+ */
+IdeWorkspace *
+ide_docs_workspace_new (IdeApplication *application)
+{
+  return g_object_new (IDE_TYPE_DOCS_WORKSPACE,
+                       "application", application,
+                       "default-width", 800,
+                       "default-height", 600,
+                       NULL);
+}
+
+static gboolean
+ide_docs_workspace_do_search (IdeDocsWorkspace *self)
+{
+  g_autoptr(IdeDocsQuery) query = NULL;
+  const gchar *text;
+
+  g_assert (IDE_IS_DOCS_WORKSPACE (self));
+
+  self->queued_search = 0;
+
+  text = gtk_entry_get_text (self->entry);
+  if (ide_str_empty0 (text))
+    return G_SOURCE_REMOVE;
+
+  query = ide_docs_query_new ();
+  ide_docs_query_set_keyword (query, text);
+  ide_docs_search_view_search_async (self->search_view, query, NULL, NULL, NULL);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_docs_workspace_queue_search (IdeDocsWorkspace *self)
+{
+  g_assert (IDE_IS_DOCS_WORKSPACE (self));
+
+  if (self->queued_search != 0)
+    g_clear_handle_id (&self->queued_search, g_source_remove);
+
+  self->queued_search =
+    gdk_threads_add_timeout_full (G_PRIORITY_LOW,
+                                  50,
+                                  (GSourceFunc)ide_docs_workspace_do_search,
+                                  g_object_ref (self),
+                                  g_object_unref);
+}
+
+static void
+on_search_entry_changed_cb (IdeDocsWorkspace *self,
+                            GtkEntry         *entry)
+{
+  g_assert (IDE_IS_DOCS_WORKSPACE (self));
+  g_assert (GTK_IS_ENTRY (entry));
+
+  gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->search_view));
+  ide_docs_workspace_queue_search (self);
+}
+
+static void
+on_search_view_item_activated_cb (IdeDocsWorkspace  *self,
+                                  IdeDocsItem       *item,
+                                  IdeDocsSearchView *view)
+{
+  g_assert (IDE_IS_DOCS_WORKSPACE (self));
+  g_assert (IDE_IS_DOCS_ITEM (item));
+  g_assert (IDE_IS_DOCS_SEARCH_VIEW (view));
+
+  g_print ("Activate view for %s at %s\n",
+           ide_docs_item_get_title (item),
+           ide_docs_item_get_url (item));
+
+  ide_docs_view_set_item (self->view, item);
+  gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->view));
+}
+
+static void
+ide_docs_workspace_destroy (GtkWidget *widget)
+{
+  IdeDocsWorkspace *self = (IdeDocsWorkspace *)widget;
+
+  g_assert (IDE_IS_DOCS_WORKSPACE (self));
+
+  g_clear_handle_id (&self->queued_search, g_source_remove);
+
+  GTK_WIDGET_CLASS (ide_docs_workspace_parent_class)->destroy (widget);
+}
+
+static void
+ide_docs_workspace_class_init (IdeDocsWorkspaceClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  IdeWorkspaceClass *workspace_class = IDE_WORKSPACE_CLASS (klass);
+
+  widget_class->destroy = ide_docs_workspace_destroy;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-docs/ui/ide-docs-workspace.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeDocsWorkspace, entry);
+  gtk_widget_class_bind_template_child (widget_class, IdeDocsWorkspace, search_view);
+  gtk_widget_class_bind_template_child (widget_class, IdeDocsWorkspace, stack);
+  gtk_widget_class_bind_template_child (widget_class, IdeDocsWorkspace, view);
+
+  ide_workspace_class_set_kind (workspace_class, "docs");
+
+  g_type_ensure (IDE_TYPE_DOCS_SEARCH_VIEW);
+  g_type_ensure (IDE_TYPE_DOCS_VIEW);
+}
+
+static void
+ide_docs_workspace_init (IdeDocsWorkspace *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (self->search_view,
+                           "item-activated",
+                           G_CALLBACK (on_search_view_item_activated_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->entry,
+                           "changed",
+                           G_CALLBACK (on_search_entry_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
diff --git a/src/libide/docs/ide-docs-workspace.h b/src/libide/docs/ide-docs-workspace.h
new file mode 100644
index 000000000..e71123a60
--- /dev/null
+++ b/src/libide/docs/ide-docs-workspace.h
@@ -0,0 +1,35 @@
+/* ide-docs-workspace.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-gui.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DOCS_WORKSPACE (ide_docs_workspace_get_type())
+
+IDE_AVAILABLE_IN_3_34
+G_DECLARE_FINAL_TYPE (IdeDocsWorkspace, ide_docs_workspace, IDE, DOCS_WORKSPACE, IdeWorkspace)
+
+IDE_AVAILABLE_IN_3_34
+IdeWorkspace *ide_docs_workspace_new (IdeApplication *application);
+
+G_END_DECLS
diff --git a/src/libide/docs/ide-docs-workspace.ui b/src/libide/docs/ide-docs-workspace.ui
new file mode 100644
index 000000000..8de573d28
--- /dev/null
+++ b/src/libide/docs/ide-docs-workspace.ui
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeDocsWorkspace" parent="IdeWorkspace">
+    <child type="titlebar">
+      <object class="GtkHeaderBar" id="headerbar">
+        <property name="show-close-button">true</property>
+        <property name="visible">true</property>
+        <child type="title">
+          <object class="GtkEntry" id="entry">
+            <property name="visible">true</property>
+            <property name="max-width-chars">40</property>
+            <property name="placeholder-text" translatable="yes">Search Documentation (Ctrl+K)</property>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child internal-child="surfaces">
+      <object class="GtkStack">
+        <child>
+          <object class="IdeSurface" id="docs_surface">
+            <property name="visible">true</property>
+            <child type="left">
+              <object class="GtkBox">
+                <property name="width-request">200</property>
+                <property name="orientation">vertical</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkStack" id="stack">
+                <property name="visible">true</property>
+                <property name="expand">true</property>
+                <child>
+                  <object class="IdeDocsSearchView" id="search_view">
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="IdeDocsView" id="view">
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/docs/libide-docs.c b/src/libide/docs/libide-docs.c
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/libide/docs/libide-docs.gresource.xml b/src/libide/docs/libide-docs.gresource.xml
new file mode 100644
index 000000000..454619f2a
--- /dev/null
+++ b/src/libide/docs/libide-docs.gresource.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/libide-docs">
+    <!-- file preprocess="xml-stripblanks">gtk/menus.ui</file -->
+  </gresource>
+  <gresource prefix="/org/gnome/libide-docs/ui">
+    <file preprocess="xml-stripblanks">ide-docs-search-row.ui</file>
+    <file preprocess="xml-stripblanks">ide-docs-search-view.ui</file>
+    <file preprocess="xml-stripblanks">ide-docs-workspace.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/libide/docs/libide-docs.h b/src/libide/docs/libide-docs.h
new file mode 100644
index 000000000..797d13b8e
--- /dev/null
+++ b/src/libide/docs/libide-docs.h
@@ -0,0 +1,34 @@
+/* libide-docs.h
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-core.h>
+#include <libide-threading.h>
+
+#define IDE_DOCS_INSIDE
+
+#include "ide-docs-item.h"
+#include "ide-docs-library.h"
+#include "ide-docs-provider.h"
+#include "ide-docs-query.h"
+#include "ide-docs-workspace.h"
+
+#undef IDE_DOCS_INSIDE
diff --git a/src/libide/docs/meson.build b/src/libide/docs/meson.build
new file mode 100644
index 000000000..158add6fb
--- /dev/null
+++ b/src/libide/docs/meson.build
@@ -0,0 +1,99 @@
+libide_docs_header_dir = join_paths(libide_header_dir, 'docs')
+libide_docs_header_subdir = join_paths(libide_header_subdir, 'docs')
+libide_include_directories += include_directories('.')
+
+#
+# Public API Headers
+#
+
+libide_docs_public_headers = [
+  'libide-docs.h',
+  'ide-docs-item.h',
+  'ide-docs-library.h',
+  'ide-docs-provider.h',
+  'ide-docs-query.h',
+  'ide-docs-workspace.h',
+]
+
+install_headers(libide_docs_public_headers, subdir: libide_docs_header_subdir)
+
+#
+# Sources
+#
+
+libide_docs_public_sources = [
+  'libide-docs.c',
+  'ide-docs-item.c',
+  'ide-docs-library.c',
+  'ide-docs-provider.c',
+  'ide-docs-query.c',
+  'ide-docs-search-model.c',
+  'ide-docs-search-row.c',
+  'ide-docs-search-section.c',
+  'ide-docs-search-view.c',
+  'ide-docs-view.c',
+  'ide-docs-workspace.c',
+]
+
+libide_docs_sources = libide_docs_public_sources
+
+#
+# Enum generation
+#
+
+libide_docs_enum_headers = [
+  'ide-docs-item.h',
+]
+
+libide_docs_enums = gnome.mkenums_simple('ide-docs-enums',
+     body_prefix: '#include "config.h"',
+   header_prefix: '#include <libide-core.h>',
+       decorator: '_IDE_EXTERN',
+         sources: libide_docs_enum_headers,
+  install_header: true,
+     install_dir: libide_docs_header_dir,
+)
+
+#
+# Generated Resource Files
+#
+
+libide_docs_resources = gnome.compile_resources(
+  'ide-docs-resources',
+  'libide-docs.gresource.xml',
+  c_name: 'ide_docs',
+)
+libide_docs_sources += libide_docs_resources
+
+#
+# Dependencies
+#
+
+libide_docs_deps = [
+  libgio_dep,
+  libpeas_dep,
+  libide_core_dep,
+  libide_threading_dep,
+  libide_gui_dep,
+]
+
+#
+# Library Definitions
+#
+
+libide_docs = static_library('ide-docs-' + libide_api_version,
+  libide_docs_sources + libide_docs_enums,
+   dependencies: libide_docs_deps,
+         c_args: libide_args + release_args + ['-DIDE_DOCS_COMPILATION'],
+)
+
+libide_docs_dep = declare_dependency(
+         dependencies: libide_docs_deps,
+           link_whole: libide_docs,
+  include_directories: include_directories('.'),
+)
+
+gnome_builder_public_sources += files(libide_docs_public_sources)
+gnome_builder_public_headers += files(libide_docs_public_headers)
+gnome_builder_include_subdirs += libide_docs_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-docs.h', '-DIDE_DOCS_COMPILATION']
diff --git a/src/libide/meson.build b/src/libide/meson.build
index 3412c50c9..8e8337a29 100644
--- a/src/libide/meson.build
+++ b/src/libide/meson.build
@@ -14,6 +14,7 @@ subdir('foundry')
 subdir('debugger')
 subdir('themes')
 subdir('gui')
+subdir('docs')
 subdir('terminal')
 subdir('sourceview')
 subdir('lsp')
diff --git a/src/libide/themes/libide-themes.gresource.xml b/src/libide/themes/libide-themes.gresource.xml
index 2d22cd6b7..9eed61bba 100644
--- a/src/libide/themes/libide-themes.gresource.xml
+++ b/src/libide/themes/libide-themes.gresource.xml
@@ -19,6 +19,7 @@
     <file compressed="true">themes/shared/shared-buildui.css</file>
     <file compressed="true">themes/shared/shared-completion.css</file>
     <file compressed="true">themes/shared/shared-debugger.css</file>
+    <file compressed="true">themes/shared/shared-docs.css</file>
     <file compressed="true">themes/shared/shared-editor.css</file>
     <file compressed="true">themes/shared/shared-greeter.css</file>
     <file compressed="true">themes/shared/shared-hoverer.css</file>
diff --git a/src/libide/themes/themes/shared.css b/src/libide/themes/themes/shared.css
index fcf9ea9c9..4abb305ef 100644
--- a/src/libide/themes/themes/shared.css
+++ b/src/libide/themes/themes/shared.css
@@ -1,6 +1,7 @@
 @import url("resource:///org/gnome/builder/themes/shared/shared-buildui.css");
 @import url("resource:///org/gnome/builder/themes/shared/shared-completion.css");
 @import url("resource:///org/gnome/builder/themes/shared/shared-debugger.css");
+@import url("resource:///org/gnome/builder/themes/shared/shared-docs.css");
 @import url("resource:///org/gnome/builder/themes/shared/shared-layout.css");
 @import url("resource:///org/gnome/builder/themes/shared/shared-editor.css");
 @import url("resource:///org/gnome/builder/themes/shared/shared-greeter.css");
diff --git a/src/libide/themes/themes/shared/shared-docs.css b/src/libide/themes/themes/shared/shared-docs.css
new file mode 100644
index 000000000..49f3391af
--- /dev/null
+++ b/src/libide/themes/themes/shared/shared-docs.css
@@ -0,0 +1,34 @@
+IdeDocsSearchView label.section-title {
+  margin-top: 3px;
+  }
+IdeDocsSearchSection list {
+  background-color: transparent;
+  }
+IdeDocsSearchSection list row.header:not(:first-child) {
+  margin-top: 16px;
+  }
+IdeDocsSearchSection list row.header box {
+  padding: 7px;
+  }
+IdeDocsSearchSection list row.header box label {
+  font-size: 0.8333em;
+  font-weight: bold;
+  color: @theme_selected_bg_color;
+  }
+IdeDocsSearchSection list row:selected.header box label {
+  color: @theme_selected_fg_color;
+  }
+IdeDocsSearchView box.titles label {
+  font-size: 1.3em;
+  font-weight: 500;
+  color: alpha(currentColor, 0.5);
+  }
+IdeDocsSearchSection list row {
+  border-bottom: 1px solid alpha(@borders, 0.5);
+  }
+IdeDocsSearchSection list row > box {
+  padding: 7px;
+  }
+IdeDocsSearchSection list row > box image:last-child {
+  min-width: 16px;
+  }
diff --git a/src/meson.build b/src/meson.build
index bc7f91841..ae03f4e5d 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -29,6 +29,7 @@ gnome_builder_deps = [
 
   libide_code_dep,
   libide_core_dep,
+  libide_docs_dep,
   libide_debugger_dep,
   libide_editor_dep,
   libide_foundry_dep,
diff --git a/src/plugins/devhelp/devhelp-plugin.c b/src/plugins/devhelp/devhelp-plugin.c
index 72c5b2fed..bc93f83b9 100644
--- a/src/plugins/devhelp/devhelp-plugin.c
+++ b/src/plugins/devhelp/devhelp-plugin.c
@@ -23,6 +23,7 @@
 #include <libide-editor.h>
 #include <libpeas/peas.h>
 
+#include "gbp-devhelp-docs-provider.h"
 #include "gbp-devhelp-editor-addin.h"
 #include "gbp-devhelp-hover-provider.h"
 #include "gbp-devhelp-frame-addin.h"
@@ -30,6 +31,9 @@
 _IDE_EXTERN void
 _gbp_devhelp_register_types (PeasObjectModule *module)
 {
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_DOCS_PROVIDER,
+                                              GBP_TYPE_DEVHELP_DOCS_PROVIDER);
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_EDITOR_ADDIN,
                                               GBP_TYPE_DEVHELP_EDITOR_ADDIN);
diff --git a/src/plugins/devhelp/devhelp2-parser.c b/src/plugins/devhelp/devhelp2-parser.c
new file mode 100644
index 000000000..e78b4c331
--- /dev/null
+++ b/src/plugins/devhelp/devhelp2-parser.c
@@ -0,0 +1,329 @@
+/* devhelp2-parser.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <unistd.h>
+
+#include "devhelp2-parser.h"
+
+static const gchar *
+devhelp2_parser_intern (Devhelp2Parser *state,
+                        const gchar    *str)
+{
+  if (str == NULL)
+    return NULL;
+
+  return g_string_chunk_insert_const (state->strings, str);
+}
+
+static Chapter *
+chapter_new (const gchar *name,
+             const gchar *link)
+{
+  Chapter *chapter;
+
+  chapter = g_slice_new0 (Chapter);
+  chapter->list.data = chapter;
+  chapter->name = name;
+  chapter->link = link;
+
+  return chapter;
+}
+
+static void
+chapter_free (Chapter *chapter)
+{
+  if (chapter->parent)
+    g_queue_unlink (&chapter->parent->children, &chapter->list);
+
+  chapter->parent = NULL;
+
+  while (chapter->children.head)
+    chapter_free (chapter->children.head->data);
+
+  g_assert (chapter->parent == NULL);
+  g_assert (chapter->list.prev == NULL);
+  g_assert (chapter->list.next == NULL);
+  g_assert (chapter->children.length == 0);
+
+  g_slice_free (Chapter, chapter);
+}
+
+static void
+chapter_append (Chapter *chapter,
+                Chapter *child)
+{
+  g_assert (chapter != NULL);
+  g_assert (child != NULL);
+  g_assert (child->parent == NULL);
+
+  child->parent = chapter;
+  g_queue_push_tail_link (&chapter->children, &child->list);
+}
+
+static void
+devhelp2_parser_start_element (GMarkupParseContext  *context G_GNUC_UNUSED,
+                               const gchar          *element_name,
+                               const gchar         **attribute_names,
+                               const gchar         **attribute_values,
+                               gpointer              user_data,
+                               GError              **error)
+{
+  Devhelp2Parser *state = user_data;
+
+  g_assert (element_name != NULL);
+  g_assert (state != NULL);
+  g_assert (state->context != NULL);
+
+  if (g_str_equal (element_name, "book"))
+    {
+      const gchar *title = NULL;
+      const gchar *link = NULL;
+      const gchar *author = NULL;
+      const gchar *name = NULL;
+      const gchar *version = NULL;
+      const gchar *language = NULL;
+      const gchar *online = NULL;
+      const gchar *xmlns = NULL;
+
+      if (!g_markup_collect_attributes (element_name,
+                                        attribute_names,
+                                        attribute_values,
+                                        error,
+                                        G_MARKUP_COLLECT_STRING, "title", &title,
+                                        G_MARKUP_COLLECT_STRING, "link", &link,
+                                        G_MARKUP_COLLECT_STRING, "name", &name,
+                                        G_MARKUP_COLLECT_OPTIONAL | G_MARKUP_COLLECT_STRING, "author", 
&author,
+                                        G_MARKUP_COLLECT_OPTIONAL | G_MARKUP_COLLECT_STRING, "version", 
&version,
+                                        G_MARKUP_COLLECT_OPTIONAL | G_MARKUP_COLLECT_STRING, "language", 
&language,
+                                        G_MARKUP_COLLECT_OPTIONAL | G_MARKUP_COLLECT_STRING, "online", 
&online,
+                                        G_MARKUP_COLLECT_OPTIONAL | G_MARKUP_COLLECT_STRING, "xmlns", &xmlns,
+                                        G_MARKUP_COLLECT_INVALID))
+        return;
+
+      state->book.title = devhelp2_parser_intern (state, title);
+      state->book.link = devhelp2_parser_intern (state, link);
+      state->book.author = devhelp2_parser_intern (state, author);
+      state->book.name = devhelp2_parser_intern (state, name);
+      state->book.version = devhelp2_parser_intern (state, version);
+      state->book.language = devhelp2_parser_intern (state, language);
+      state->book.online = devhelp2_parser_intern (state, online);
+    }
+  else if (g_str_equal (element_name, "sub"))
+    {
+      const gchar *name = NULL;
+      const gchar *link = NULL;
+      Chapter *chapter;
+
+      if (!g_markup_collect_attributes (element_name,
+                                        attribute_names,
+                                        attribute_values,
+                                        error,
+                                        G_MARKUP_COLLECT_STRING, "name", &name,
+                                        G_MARKUP_COLLECT_STRING, "link", &link,
+                                        G_MARKUP_COLLECT_INVALID))
+        return;
+
+      chapter = chapter_new (devhelp2_parser_intern (state, name),
+                             devhelp2_parser_intern (state, link));
+
+      if (state->chapter != NULL)
+        chapter_append (state->chapter, chapter);
+
+      state->chapter = chapter;
+    }
+  else if (g_str_equal (element_name, "keyword"))
+    {
+      Keyword keyword;
+      const gchar *name = NULL;
+      const gchar *link = NULL;
+      const gchar *type = NULL;
+      const gchar *since = NULL;
+      const gchar *deprecated = NULL;
+      const gchar *stability = NULL;
+
+      if (!g_markup_collect_attributes (element_name,
+                                        attribute_names,
+                                        attribute_values,
+                                        error,
+                                        G_MARKUP_COLLECT_STRING, "type", &type,
+                                        G_MARKUP_COLLECT_STRING, "name", &name,
+                                        G_MARKUP_COLLECT_STRING, "link", &link,
+                                        G_MARKUP_COLLECT_OPTIONAL | G_MARKUP_COLLECT_STRING, "deprecated", 
&deprecated,
+                                        G_MARKUP_COLLECT_OPTIONAL | G_MARKUP_COLLECT_STRING, "since", &since,
+                                        G_MARKUP_COLLECT_OPTIONAL | G_MARKUP_COLLECT_STRING, "stability", 
&stability,
+                                        G_MARKUP_COLLECT_INVALID))
+        return;
+
+      if (g_str_has_prefix (name, "struct "))
+        name += strlen ("struct ");
+      else if (g_str_has_prefix (name, "enum "))
+        name += strlen ("enum ");
+      else if (g_str_has_prefix (name, "union "))
+        name += strlen ("union ");
+
+      if (!*name)
+        return;
+
+      keyword.backptr = state;
+      keyword.type = devhelp2_parser_intern (state, type);
+      keyword.kind = GPOINTER_TO_UINT (g_hash_table_lookup (state->kinds, keyword.type));
+      keyword.name = devhelp2_parser_intern (state, name);
+      keyword.link = devhelp2_parser_intern (state, link);
+      keyword.since = devhelp2_parser_intern (state, since);
+      keyword.deprecated = devhelp2_parser_intern (state, deprecated);
+      keyword.stability = devhelp2_parser_intern (state, stability);
+
+      g_array_append_val (state->keywords, keyword);
+    }
+}
+
+static void
+devhelp2_parser_end_element (GMarkupParseContext  *context G_GNUC_UNUSED,
+                             const gchar          *element_name,
+                             gpointer              user_data,
+                             GError              **error G_GNUC_UNUSED)
+{
+  Devhelp2Parser *state = user_data;
+
+  if (g_str_equal (element_name, "sub"))
+    {
+      if (state->chapter->parent)
+        state->chapter = state->chapter->parent;
+    }
+}
+
+static GMarkupParser devhelp2_parser = {
+  .start_element = devhelp2_parser_start_element,
+  .end_element = devhelp2_parser_end_element,
+};
+
+Devhelp2Parser *
+devhelp2_parser_new (void)
+{
+  Devhelp2Parser *state;
+
+  state = g_slice_new0 (Devhelp2Parser);
+  state->kinds = g_hash_table_new (NULL, NULL);
+  state->strings = g_string_chunk_new (4096*4L);
+  state->keywords = g_array_new (FALSE, FALSE, sizeof (Keyword));
+  state->context = g_markup_parse_context_new (&devhelp2_parser,
+                                               G_MARKUP_IGNORE_QUALIFIED,
+                                               state, NULL);
+
+#define ADD_KIND(k, v) \
+  g_hash_table_insert (state->kinds, \
+                       (gchar *)g_string_chunk_insert_const (state->strings, k), \
+                       GUINT_TO_POINTER(v))
+
+  ADD_KIND ("function", IDE_DOCS_ITEM_KIND_FUNCTION);
+  ADD_KIND ("struct", IDE_DOCS_ITEM_KIND_STRUCT);
+  ADD_KIND ("enum", IDE_DOCS_ITEM_KIND_ENUM);
+  ADD_KIND ("property", IDE_DOCS_ITEM_KIND_PROPERTY);
+  ADD_KIND ("signal", IDE_DOCS_ITEM_KIND_SIGNAL);
+  ADD_KIND ("macro", IDE_DOCS_ITEM_KIND_MACRO);
+  ADD_KIND ("member", IDE_DOCS_ITEM_KIND_MEMBER);
+  ADD_KIND ("method", IDE_DOCS_ITEM_KIND_METHOD);
+  ADD_KIND ("constant", IDE_DOCS_ITEM_KIND_CONSTANT);
+
+#undef ADD_KIND
+
+  return state;
+}
+
+void
+devhelp2_parser_free (Devhelp2Parser *state)
+{
+  g_clear_pointer (&state->kinds, g_hash_table_unref);
+  g_clear_pointer (&state->context, g_markup_parse_context_free);
+  g_clear_pointer (&state->strings, g_string_chunk_free);
+  g_clear_pointer (&state->keywords, g_array_unref);
+  g_clear_pointer (&state->chapter, chapter_free);
+  g_clear_pointer (&state->directory, g_free);
+  g_slice_free (Devhelp2Parser, state);
+}
+
+gboolean
+devhelp2_parser_parse_file (Devhelp2Parser  *state,
+                            const gchar     *filename,
+                            GError         **error)
+{
+  g_autofree gchar *contents = NULL;
+  gboolean ret = FALSE;
+  gint fd;
+
+  g_assert (state != NULL);
+  g_assert (filename != NULL);
+  g_assert (state->directory == NULL);
+
+  state->directory = g_path_get_dirname (filename);
+
+  fd = g_open (filename, O_RDONLY, 0);
+
+  if (fd == -1)
+    {
+      int errsv = errno;
+      g_set_error (error,
+                   G_FILE_ERROR,
+                   g_file_error_from_errno (errsv),
+                   "%s",
+                   g_strerror (errno));
+      goto failure;
+    }
+
+  for (;;)
+    {
+      gchar buf[4096*4L];
+      gssize n_read;
+
+      n_read = read (fd, buf, sizeof buf);
+
+      if (n_read > 0)
+        {
+          if (!g_markup_parse_context_parse (state->context, buf, n_read, error))
+            goto failure;
+        }
+      else if (n_read < 0)
+        {
+          int errsv = errno;
+          g_set_error (error,
+                       G_FILE_ERROR,
+                       g_file_error_from_errno (errsv),
+                       "%s",
+                       g_strerror (errno));
+          goto failure;
+        }
+      else
+        {
+          g_assert (n_read == 0);
+          break;
+        }
+    }
+
+  ret = g_markup_parse_context_end_parse (state->context, error);
+
+failure:
+  close (fd);
+
+  return ret;
+}
diff --git a/src/plugins/devhelp/devhelp2-parser.h b/src/plugins/devhelp/devhelp2-parser.h
new file mode 100644
index 000000000..4bfb1e203
--- /dev/null
+++ b/src/plugins/devhelp/devhelp2-parser.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include <glib.h>
+
+#include <libide-docs.h>
+
+G_BEGIN_DECLS
+
+typedef struct _Devhelp2Parser Devhelp2Parser;
+typedef struct _Chapter Chapter;
+typedef struct _Keyword Keyword;
+
+struct _Chapter
+{
+  struct _Chapter *parent;
+  GQueue           children;
+  GList            list;
+  const gchar     *name;
+  const gchar     *link;
+};
+
+struct _Keyword
+{
+  Devhelp2Parser  *backptr;
+  GList            list;
+  const gchar     *type;
+  const gchar     *name;
+  const gchar     *link;
+  const gchar     *since;
+  const gchar     *deprecated;
+  const gchar     *stability;
+  IdeDocsItemKind  kind;
+};
+
+struct _Devhelp2Parser
+{
+  GMarkupParseContext *context;
+  GStringChunk        *strings;
+  GHashTable          *kinds;
+  Chapter             *chapter;
+  GArray              *keywords;
+  gchar               *directory;
+  struct {
+    const gchar       *title;
+    const gchar       *link;
+    const gchar       *author;
+    const gchar       *name;
+    const gchar       *version;
+    const gchar       *language;
+    const gchar       *online;
+  } book;
+};
+
+Devhelp2Parser *devhelp2_parser_new        (void);
+void            devhelp2_parser_free       (Devhelp2Parser *state);
+gboolean        devhelp2_parser_parse_file (Devhelp2Parser  *state,
+                                            const gchar     *filename,
+                                            GError         **error);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (Devhelp2Parser, devhelp2_parser_free)
+
+G_END_DECLS
diff --git a/src/plugins/devhelp/gbp-devhelp-docs-provider.c b/src/plugins/devhelp/gbp-devhelp-docs-provider.c
new file mode 100644
index 000000000..0d4285df4
--- /dev/null
+++ b/src/plugins/devhelp/gbp-devhelp-docs-provider.c
@@ -0,0 +1,362 @@
+/* gbp-devhelp-docs-provider.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-devhelp-docs-provider"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <libide-sourceview.h>
+#include <libide-threading.h>
+
+#include "devhelp2-parser.h"
+#include "gbp-devhelp-docs-provider.h"
+
+struct _GbpDevhelpDocsProvider
+{
+  GObject               parent_instance;
+  GPtrArray            *parsers;
+  DzlFuzzyMutableIndex *index;
+};
+
+typedef struct
+{
+  IdeDocsQuery *query;
+  IdeDocsItem  *results;
+  IdeDocsItem  *internal_results;
+  gchar        *fuzzy;
+} Search;
+
+static void
+search_free (Search *search)
+{
+  g_clear_object (&search->query);
+  g_clear_object (&search->results);
+  g_clear_object (&search->internal_results);
+  g_clear_pointer (&search->fuzzy, g_free);
+  g_slice_free (Search, search);
+}
+
+static void
+gbp_devhelp_docs_provider_populate_async (IdeDocsProvider     *provider,
+                                          IdeDocsItem         *item,
+                                          GCancellable        *cancellable,
+                                          GAsyncReadyCallback  callback,
+                                          gpointer             user_data)
+{
+  GbpDevhelpDocsProvider *self = (GbpDevhelpDocsProvider *)provider;
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (GBP_IS_DEVHELP_DOCS_PROVIDER (self));
+  g_assert (IDE_IS_DOCS_ITEM (item));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_devhelp_docs_provider_populate_async);
+  ide_task_set_task_data (task, g_object_ref (item), g_object_unref);
+
+  if (ide_docs_item_is_root (item))
+    {
+      g_autoptr(IdeDocsItem) child = NULL;
+
+      child = ide_docs_item_new ();
+      ide_docs_item_set_title (child, "Books");
+      ide_docs_item_set_kind (child, IDE_DOCS_ITEM_KIND_COLLECTION);
+
+      ide_docs_item_append (item, child);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gbp_devhelp_docs_provider_populate_finish (IdeDocsProvider  *provider,
+                                           GAsyncResult     *result,
+                                           GError          **error)
+{
+  g_assert (GBP_IS_DEVHELP_DOCS_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+gbp_devhelp_docs_provider_search_worker (IdeTask      *task,
+                                         gpointer      source_object,
+                                         gpointer      task_data,
+                                         GCancellable *cancellable)
+{
+  g_autofree gchar *needle = NULL;
+  g_autoptr(GArray) matches = NULL;
+  GbpDevhelpDocsProvider *self = source_object;
+  Search *search = task_data;
+  IdeDocsItem *group;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (GBP_IS_DEVHELP_DOCS_PROVIDER (self));
+  g_assert (search != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  needle = g_utf8_casefold (search->fuzzy, -1);
+  matches = dzl_fuzzy_mutable_index_match (self->index, needle, 300);
+
+  for (guint i = 0; i < matches->len; i++)
+    {
+      DzlFuzzyMutableIndexMatch *m = &g_array_index (matches, DzlFuzzyMutableIndexMatch, i);
+      const Keyword *kw = m->value;
+      Devhelp2Parser *parser = kw->backptr;
+      g_autofree gchar *highlight = NULL;
+      g_autoptr(IdeDocsItem) child = NULL;
+
+      /* TODO: We could/should probably do this only for the visible
+       *       items to save us some overhead.
+       */
+      highlight = ide_completion_fuzzy_highlight (kw->name, search->fuzzy);
+
+      child = ide_docs_item_new ();
+      ide_docs_item_set_title (child, kw->name);
+      ide_docs_item_set_display_name (child, highlight);
+      ide_docs_item_set_kind (child, kw->kind);
+      ide_docs_item_set_url (child, kw->link);
+      ide_docs_item_set_priority (child, G_MAXINT32 - (m->score * G_MAXINT32));
+
+      if ((group = ide_docs_item_find_child_by_id (search->internal_results, parser->book.name)))
+        {
+          ide_docs_item_append (group, child);
+          continue;
+        }
+
+      /* Less common path, also check for cancellation */
+      if (ide_task_return_error_if_cancelled (task))
+        return;
+
+      group = ide_docs_item_new ();
+      ide_docs_item_set_id (group, parser->book.name);
+      ide_docs_item_set_title (group, parser->book.title);
+      ide_docs_item_set_kind (group, IDE_DOCS_ITEM_KIND_BOOK);
+      ide_docs_item_set_url (group, parser->directory);
+      ide_docs_item_append (group, child);
+      ide_docs_item_append (search->internal_results, group);
+      g_object_unref (group);
+    }
+
+  for (const GList *iter = ide_docs_item_get_children (search->internal_results);
+       iter != NULL;
+       iter = iter->next)
+    {
+      group = iter->data;
+      ide_docs_item_sort_by_priority (group);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+gbp_devhelp_docs_provider_search_async (IdeDocsProvider     *provider,
+                                        IdeDocsQuery        *query,
+                                        IdeDocsItem         *results,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+  GbpDevhelpDocsProvider *self = (GbpDevhelpDocsProvider *)provider;
+  g_autoptr(IdeTask) task = NULL;
+  Search *search;
+
+  g_assert (GBP_IS_DEVHELP_DOCS_PROVIDER (self));
+  g_assert (IDE_IS_DOCS_QUERY (query));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  search = g_slice_new0 (Search);
+  search->query = g_object_ref (query);
+  search->results = g_object_ref (results);
+  search->internal_results = ide_docs_item_new ();
+  search->fuzzy = g_strdup (ide_docs_query_get_fuzzy (query));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_devhelp_docs_provider_search_async);
+  ide_task_set_task_data (task, search, search_free);
+
+  if (self->index == NULL ||
+      ide_str_empty0 (search->fuzzy))
+    ide_task_return_boolean (task, TRUE);
+  else
+    ide_task_run_in_thread (task, gbp_devhelp_docs_provider_search_worker);
+}
+
+static gboolean
+gbp_devhelp_docs_provider_search_finish (IdeDocsProvider  *provider,
+                                         GAsyncResult     *result,
+                                         GError          **error)
+{
+  g_assert (GBP_IS_DEVHELP_DOCS_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (result));
+
+  if (ide_task_propagate_boolean (IDE_TASK (result), error))
+    {
+      Search *search = ide_task_get_task_data (IDE_TASK (result));
+      IdeDocsItem *parent = search->internal_results;
+      IdeDocsItem *results = search->results;
+      IdeDocsItem *api = ide_docs_item_find_child_by_id (results, "api");
+
+      g_assert (IDE_IS_DOCS_ITEM (parent));
+      g_assert (IDE_IS_DOCS_ITEM (results));
+      g_assert (IDE_IS_DOCS_ITEM (api));
+
+      while (ide_docs_item_has_child (parent))
+        {
+          IdeDocsItem *child = ide_docs_item_get_nth_child (parent, 0);
+
+          g_object_ref (child);
+          ide_docs_item_remove (parent, child);
+          ide_docs_item_append (api, child);
+          g_object_unref (child);
+        }
+
+      ide_docs_item_sort_by_priority (api);
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+docs_provider_iface_init (IdeDocsProviderInterface *iface)
+{
+  iface->populate_async = gbp_devhelp_docs_provider_populate_async;
+  iface->populate_finish = gbp_devhelp_docs_provider_populate_finish;
+  iface->search_async = gbp_devhelp_docs_provider_search_async;
+  iface->search_finish = gbp_devhelp_docs_provider_search_finish;
+}
+
+static void
+gbp_devhelp_docs_provider_init_async (GAsyncInitable      *initable,
+                                      gint                 io_priority,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  GbpDevhelpDocsProvider *self = (GbpDevhelpDocsProvider *)initable;
+  g_autoptr(IdeTask) task = NULL;
+  static const gchar *dirs[] = {
+    "/app/share/gtk-doc/html",
+    "/usr/share/gtk-doc/html",
+  };
+
+  g_assert (GBP_IS_DEVHELP_DOCS_PROVIDER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_devhelp_docs_provider_init_async);
+
+  self->parsers = g_ptr_array_new_with_free_func ((GDestroyNotify)devhelp2_parser_free);
+
+  if (self->index == NULL)
+    self->index = dzl_fuzzy_mutable_index_new (FALSE);
+
+  dzl_fuzzy_mutable_index_begin_bulk_insert (self->index);
+
+  for (guint i = 0; i < G_N_ELEMENTS (dirs); i++)
+    {
+      g_autoptr(GDir) dir = g_dir_open (dirs[i], 0, NULL);
+      const gchar *name;
+
+      if (dir == NULL)
+        continue;
+
+      while ((name = g_dir_read_name (dir)))
+        {
+          g_autofree gchar *index = g_strdup_printf ("%s.devhelp2", name);
+          g_autofree gchar *path = g_build_filename (dirs[i], name, index, NULL);
+          Devhelp2Parser *parser;
+
+          if (!g_file_test (path, G_FILE_TEST_EXISTS))
+            continue;
+
+          parser = devhelp2_parser_new ();
+
+          if (devhelp2_parser_parse_file (parser, path, NULL))
+            {
+              for (guint j = 0; j < parser->keywords->len; j++)
+                {
+                  Keyword *kw = &g_array_index (parser->keywords, Keyword, j);
+                  dzl_fuzzy_mutable_index_insert (self->index, kw->name, kw);
+                }
+
+              g_ptr_array_add (self->parsers, parser);
+            }
+          else
+            {
+              devhelp2_parser_free (parser);
+            }
+        }
+    }
+
+  dzl_fuzzy_mutable_index_end_bulk_insert (self->index);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gbp_devhelp_docs_provider_init_finish (GAsyncInitable  *initable,
+                                       GAsyncResult    *result,
+                                       GError         **error)
+{
+  g_assert (GBP_IS_DEVHELP_DOCS_PROVIDER (initable));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+async_initable_iface_init (GAsyncInitableIface *iface)
+{
+  iface->init_async = gbp_devhelp_docs_provider_init_async;
+  iface->init_finish = gbp_devhelp_docs_provider_init_finish;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpDevhelpDocsProvider, gbp_devhelp_docs_provider, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, async_initable_iface_init)
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_DOCS_PROVIDER, docs_provider_iface_init))
+
+static void
+gbp_devhelp_docs_provider_finalize (GObject *object)
+{
+  GbpDevhelpDocsProvider *self = (GbpDevhelpDocsProvider *)object;
+
+  g_clear_pointer (&self->parsers, g_ptr_array_unref);
+  g_clear_pointer (&self->index, dzl_fuzzy_mutable_index_unref);
+
+  G_OBJECT_CLASS (gbp_devhelp_docs_provider_parent_class)->finalize (object);
+}
+
+static void
+gbp_devhelp_docs_provider_class_init (GbpDevhelpDocsProviderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gbp_devhelp_docs_provider_finalize;
+}
+
+static void
+gbp_devhelp_docs_provider_init (GbpDevhelpDocsProvider *self)
+{
+}
diff --git a/src/plugins/devhelp/gbp-devhelp-docs-provider.h b/src/plugins/devhelp/gbp-devhelp-docs-provider.h
new file mode 100644
index 000000000..fc2a9d8ad
--- /dev/null
+++ b/src/plugins/devhelp/gbp-devhelp-docs-provider.h
@@ -0,0 +1,31 @@
+/* gbp-devhelp-docs-provider.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-docs.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_DEVHELP_DOCS_PROVIDER (gbp_devhelp_docs_provider_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpDevhelpDocsProvider, gbp_devhelp_docs_provider, GBP, DEVHELP_DOCS_PROVIDER, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/devhelp/meson.build b/src/plugins/devhelp/meson.build
index e4cbc0223..baa851f1e 100644
--- a/src/plugins/devhelp/meson.build
+++ b/src/plugins/devhelp/meson.build
@@ -6,6 +6,8 @@ plugins_deps += [
 
 plugins_sources += files([
   'devhelp-plugin.c',
+  'devhelp2-parser.c',
+  'gbp-devhelp-docs-provider.c',
   'gbp-devhelp-editor-addin.c',
   'gbp-devhelp-frame-addin.c',
   'gbp-devhelp-hover-provider.c',
diff --git a/src/plugins/docsui/docsui-plugin.c b/src/plugins/docsui/docsui-plugin.c
new file mode 100644
index 000000000..c43ff1d69
--- /dev/null
+++ b/src/plugins/docsui/docsui-plugin.c
@@ -0,0 +1,34 @@
+/* docsui-plugin.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "config.h"
+
+#include <libide-docs.h>
+#include <libpeas/peas.h>
+
+#include "gbp-docsui-application-addin.h"
+
+_IDE_EXTERN void
+_gbp_docsui_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_APPLICATION_ADDIN,
+                                              GBP_TYPE_DOCSUI_APPLICATION_ADDIN);
+}
diff --git a/src/plugins/docsui/docsui.gresource.xml b/src/plugins/docsui/docsui.gresource.xml
new file mode 100644
index 000000000..f2b600497
--- /dev/null
+++ b/src/plugins/docsui/docsui.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/docsui">
+    <file>docsui.plugin</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/docsui/docsui.plugin b/src/plugins/docsui/docsui.plugin
new file mode 100644
index 000000000..c69d2b468
--- /dev/null
+++ b/src/plugins/docsui/docsui.plugin
@@ -0,0 +1,11 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2015-2018 Christian Hergert
+Depends=editor;webkit;
+Description=Documentation UI integration
+Embedded=_gbp_docsui_register_types
+Hidden=true
+Module=docsui
+Name=Docs UI
+X-At-Startup=true
diff --git a/src/plugins/docsui/gbp-docsui-application-addin.c 
b/src/plugins/docsui/gbp-docsui-application-addin.c
new file mode 100644
index 000000000..a170abdaa
--- /dev/null
+++ b/src/plugins/docsui/gbp-docsui-application-addin.c
@@ -0,0 +1,109 @@
+/* gbp-docsui-application-addin.c
+ *
+ * Copyright 2019 Christian Hergert <unknown domain org>
+ *
+ * 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-docsui-application-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-gui.h>
+#include <libide-docs.h>
+
+#include "gbp-docsui-application-addin.h"
+
+struct _GbpDocsuiApplicationAddin
+{
+  GObject parent_instance;
+};
+
+static void
+gbp_docsui_application_addin_handle_command_line (IdeApplicationAddin     *addin,
+                                                  IdeApplication          *application,
+                                                  GApplicationCommandLine *cmdline)
+{
+  g_autoptr(IdeWorkbench) workbench = NULL;
+  IdeApplication *app = (IdeApplication *)application;
+  g_autoptr(GFile) workdir = NULL;
+  GVariantDict *options;
+
+  g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+  g_assert (IDE_IS_APPLICATION (application));
+  g_assert (G_IS_APPLICATION_COMMAND_LINE (cmdline));
+
+  if ((options = g_application_command_line_get_options_dict (cmdline)) &&
+      g_variant_dict_contains (options, "docs"))
+    {
+      IdeWorkspace *workspace;
+      IdeContext *context;
+
+      workdir = g_application_command_line_create_file_for_arg (cmdline, ".");
+      ide_application_set_command_line_handled (application, cmdline, TRUE);
+
+      workbench = ide_workbench_new ();
+      ide_application_add_workbench (app, workbench);
+
+      context = ide_workbench_get_context (workbench);
+      ide_context_set_workdir (context, workdir);
+
+      workspace = ide_docs_workspace_new (application);
+      ide_workbench_add_workspace (workbench, IDE_WORKSPACE (workspace));
+
+      ide_workbench_focus_workspace (workbench, IDE_WORKSPACE (workspace));
+
+      return;
+    }
+}
+
+static void
+gbp_docsui_application_addin_add_option_entries (IdeApplicationAddin *addin,
+                                                   IdeApplication      *app)
+{
+  g_assert (GBP_IS_DOCSUI_APPLICATION_ADDIN (addin));
+  g_assert (G_IS_APPLICATION (app));
+
+  g_application_add_main_option (G_APPLICATION (app),
+                                 "docs",
+                                 'd',
+                                 G_OPTION_FLAG_IN_MAIN,
+                                 G_OPTION_ARG_NONE,
+                                 _("Open documentation window"),
+                                 NULL);
+}
+
+static void
+application_addin_iface_init (IdeApplicationAddinInterface *iface)
+{
+  iface->add_option_entries = gbp_docsui_application_addin_add_option_entries;
+  iface->handle_command_line = gbp_docsui_application_addin_handle_command_line;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpDocsuiApplicationAddin, gbp_docsui_application_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_APPLICATION_ADDIN,
+                                                application_addin_iface_init))
+
+static void
+gbp_docsui_application_addin_class_init (GbpDocsuiApplicationAddinClass *klass)
+{
+}
+
+static void
+gbp_docsui_application_addin_init (GbpDocsuiApplicationAddin *self)
+{
+}
diff --git a/src/plugins/docsui/gbp-docsui-application-addin.h 
b/src/plugins/docsui/gbp-docsui-application-addin.h
new file mode 100644
index 000000000..61b14620a
--- /dev/null
+++ b/src/plugins/docsui/gbp-docsui-application-addin.h
@@ -0,0 +1,31 @@
+/* gbp-docsui-application-addin.h
+ *
+ * Copyright 2018 Christian Hergert <unknown domain org>
+ *
+ * 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 <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_DOCSUI_APPLICATION_ADDIN (gbp_docsui_application_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpDocsuiApplicationAddin, gbp_docsui_application_addin, GBP, 
DOCSUI_APPLICATION_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/docsui/meson.build b/src/plugins/docsui/meson.build
new file mode 100644
index 000000000..877532722
--- /dev/null
+++ b/src/plugins/docsui/meson.build
@@ -0,0 +1,12 @@
+plugins_sources += files([
+  'docsui-plugin.c',
+  'gbp-docsui-application-addin.c',
+])
+
+plugin_docsui_resources = gnome.compile_resources(
+  'docsui-resources',
+  'docsui.gresource.xml',
+  c_name: 'gbp_docsui',
+)
+
+plugins_sources += plugin_docsui_resources
diff --git a/src/plugins/meson.build b/src/plugins/meson.build
index 17583bf9e..d626708fe 100644
--- a/src/plugins/meson.build
+++ b/src/plugins/meson.build
@@ -14,6 +14,7 @@ plugins_deps = [
 
   libide_code_dep,
   libide_core_dep,
+  libide_docs_dep,
   libide_debugger_dep,
   libide_editor_dep,
   libide_foundry_dep,
@@ -56,6 +57,7 @@ subdir('devhelp')
 subdir('deviceui')
 subdir('deviced')
 subdir('doap')
+subdir('docsui')
 subdir('dspy')
 subdir('editor')
 subdir('editorconfig')


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