[gnome-builder/wip/chergert/docs] wip
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-builder/wip/chergert/docs] wip
- Date: Wed, 17 Jul 2019 02:48:58 +0000 (UTC)
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]