[gnome-builder] hover: start on hover abstraction for sourceview



commit 4e0612ecad3f974a57c927eef8d8ff3c940edc8e
Author: Christian Hergert <chergert redhat com>
Date:   Fri Jun 29 23:55:21 2018 -0700

    hover: start on hover abstraction for sourceview
    
    The IdeHoverProvider and IdeHoverContext abstractions are used as a
    means to communicate information to be displayed in a popover above
    the IdeSourceView similar to that of a tooltip.
    
    However, the popovers can stick around for long enough for the user to
    enter them and manipulate information within the popover. When the
    pointer leaves or goes out of the grace area, the popover is
    immediately dismissed similar to a tooltip.

 data/themes/shared.css                             |   1 +
 data/themes/shared/shared-hoverer.css              |  19 +
 meson.build                                        |   2 +
 po/POTFILES.in                                     |   2 +
 src/libide/debugger/ide-debugger-hover-controls.c  | 199 +++++
 src/libide/debugger/ide-debugger-hover-controls.h  |  35 +
 src/libide/debugger/ide-debugger-hover-controls.ui |  34 +
 src/libide/debugger/ide-debugger-hover-provider.c  | 115 +++
 src/libide/debugger/ide-debugger-hover-provider.h  |  29 +
 src/libide/debugger/ide-debugger-plugin.c          |   5 +
 src/libide/debugger/meson.build                    |  12 +-
 src/libide/editor/ide-editor-hover-provider.c      | 109 +++
 src/libide/editor/ide-editor-hover-provider.h      |  29 +
 src/libide/editor/ide-editor-plugin.c              |  12 +-
 src/libide/editor/meson.build                      |   8 +-
 src/libide/hover/ide-hover-context-private.h       |  48 ++
 src/libide/hover/ide-hover-context.c               | 253 ++++++
 src/libide/hover/ide-hover-context.h               |  48 ++
 src/libide/hover/ide-hover-popover-private.h       |  38 +
 src/libide/hover/ide-hover-popover.c               | 283 +++++++
 src/libide/hover/ide-hover-private.h               |  37 +
 src/libide/hover/ide-hover-provider.c              | 148 ++++
 src/libide/hover/ide-hover-provider.h              |  74 ++
 src/libide/hover/ide-hover.c                       | 640 +++++++++++++++
 src/libide/hover/ide-marked-content.c              | 230 ++++++
 src/libide/hover/ide-marked-content.h              |  65 ++
 src/libide/hover/ide-marked-view.c                 | 116 +++
 src/libide/hover/ide-marked-view.h                 |  37 +
 src/libide/hover/meson.build                       |  29 +
 src/libide/ide-enums.c.in                          |   1 +
 src/libide/ide.h                                   |   4 +
 src/libide/libide.gresource.xml                    |   2 +
 src/libide/meson.build                             |   1 +
 src/libide/object-modules.h                        |   5 +
 src/libide/sourceview/ide-source-view.c            |  67 +-
 src/libide/util/gs-markdown.c                      | 870 +++++++++++++++++++++
 src/libide/util/gs-markdown.h                      |  58 ++
 src/libide/util/meson.build                        |   1 +
 38 files changed, 3599 insertions(+), 67 deletions(-)
---
diff --git a/data/themes/shared.css b/data/themes/shared.css
index 2b8decf6e..a6367725d 100644
--- a/data/themes/shared.css
+++ b/data/themes/shared.css
@@ -4,6 +4,7 @@
 @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");
+@import url("resource:///org/gnome/builder/themes/shared/shared-hoverer.css");
 @import url("resource:///org/gnome/builder/themes/shared/shared-omnibar.css");
 @import url("resource:///org/gnome/builder/themes/shared/shared-search.css");
 @import url("resource:///org/gnome/builder/themes/shared/shared-treeview.css");
diff --git a/data/themes/shared/shared-hoverer.css b/data/themes/shared/shared-hoverer.css
new file mode 100644
index 000000000..af166e4f9
--- /dev/null
+++ b/data/themes/shared/shared-hoverer.css
@@ -0,0 +1,19 @@
+popover.hoverer {
+  padding: 8px 12px;
+}
+
+popover.hoverer > box > :not(:first-child) {
+  margin-top: 8px;
+}
+
+popover.hoverer > box > box > .title {
+  opacity: 0.55;
+  font-weight: bold;
+  font-size: 0.8333em;
+  margin-bottom: 2px;
+}
+
+popover.hoverer > box > box box.linked button {
+  padding: 0px 6px;
+  font-size: 0.9em;
+}
diff --git a/meson.build b/meson.build
index 186f73b12..72428d5cf 100644
--- a/meson.build
+++ b/meson.build
@@ -98,6 +98,8 @@ config_h.set10('ENABLE_NLS', true) # Always enabled
 config_h.set_quoted('SRCDIR', meson.source_root())
 config_h.set_quoted('BUILDDIR', meson.build_root())
 
+config_h.set10('ENABLE_WEBKIT', get_option('with_webkit'))
+
 add_global_arguments([
   '-DHAVE_CONFIG_H',
   '-I' + meson.build_root(), # config.h
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 5e93efb79..e2bbbfa38 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -51,6 +51,7 @@ src/libide/debugger/ide-debugger-breakpoints-view.ui
 src/libide/debugger/ide-debugger-controls.ui
 src/libide/debugger/ide-debugger-disassembly-view.ui
 src/libide/debugger/ide-debugger-editor-addin.c
+src/libide/debugger/ide-debugger-hover-provider.c
 src/libide/debugger/ide-debugger-libraries-view.ui
 src/libide/debugger/ide-debugger-locals-view.c
 src/libide/debugger/ide-debugger-locals-view.ui
@@ -63,6 +64,7 @@ src/libide/directory/ide-directory-vcs.c
 src/libide/doap/xml-reader.c
 src/libide/editorconfig/ide-editorconfig-file-settings.c
 src/libide/editor/gtk/menus.ui
+src/libide/editor/ide-editor-hover-provider.c
 src/libide/editor/ide-editor-layout-stack-controls.c
 src/libide/editor/ide-editor-layout-stack-controls.ui
 src/libide/editor/ide-editor-perspective-actions.c
diff --git a/src/libide/debugger/ide-debugger-hover-controls.c 
b/src/libide/debugger/ide-debugger-hover-controls.c
new file mode 100644
index 000000000..10cae5c72
--- /dev/null
+++ b/src/libide/debugger/ide-debugger-hover-controls.c
@@ -0,0 +1,199 @@
+/* ide-debugger-hover-controls.c
+ *
+ * Copyright 2018 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#define G_LOG_DOMAIN "ide-debugger-hover-controls"
+
+#include <dazzle.h>
+
+#include "debugger/ide-debugger-hover-controls.h"
+#include "debugger/ide-debugger-breakpoints.h"
+#include "debugger/ide-debugger-private.h"
+#include "debugger/ide-debug-manager.h"
+#include "sourceview/ide-source-view.h"
+
+struct _IdeDebuggerHoverControls
+{
+  GtkBin parent_instance;
+
+  IdeDebugManager *debug_manager;
+  GFile *file;
+  guint line;
+
+  GtkToggleButton *nobreak;
+  GtkToggleButton *breakpoint;
+  GtkToggleButton *countpoint;
+};
+
+G_DEFINE_TYPE (IdeDebuggerHoverControls, ide_debugger_hover_controls, GTK_TYPE_BIN)
+
+static void
+ide_debugger_hover_controls_destroy (GtkWidget *widget)
+{
+  IdeDebuggerHoverControls *self = (IdeDebuggerHoverControls *)widget;
+
+  g_clear_object (&self->debug_manager);
+  g_clear_object (&self->file);
+
+  GTK_WIDGET_CLASS (ide_debugger_hover_controls_parent_class)->destroy (widget);
+}
+
+static void
+ide_debugger_hover_controls_class_init (IdeDebuggerHoverControlsClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  widget_class->destroy = ide_debugger_hover_controls_destroy;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/ide-debugger-hover-controls.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerHoverControls, nobreak);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerHoverControls, breakpoint);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerHoverControls, countpoint);
+}
+
+static void
+ide_debugger_hover_controls_init (IdeDebuggerHoverControls *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+static void
+on_toggle_cb (GtkToggleButton          *button,
+              IdeDebuggerHoverControls *self)
+{
+  g_autoptr(IdeDebuggerBreakpoints) breakpoints = NULL;
+  IdeDebuggerBreakMode break_type = IDE_DEBUGGER_BREAK_NONE;
+  IdeDebuggerBreakpoint *breakpoint;
+  GtkWidget *view;
+
+  g_assert (GTK_IS_TOGGLE_BUTTON (button));
+  g_assert (IDE_IS_DEBUGGER_HOVER_CONTROLS (self));
+
+  g_signal_handlers_block_by_func (self->nobreak, G_CALLBACK (on_toggle_cb), self);
+  g_signal_handlers_block_by_func (self->breakpoint, G_CALLBACK (on_toggle_cb), self);
+  g_signal_handlers_block_by_func (self->countpoint, G_CALLBACK (on_toggle_cb), self);
+
+  breakpoints = ide_debug_manager_get_breakpoints_for_file (self->debug_manager, self->file);
+  breakpoint = ide_debugger_breakpoints_get_line (breakpoints, self->line);
+
+  if (button == self->nobreak)
+    break_type = IDE_DEBUGGER_BREAK_NONE;
+  else if (button == self->breakpoint)
+    break_type = IDE_DEBUGGER_BREAK_BREAKPOINT;
+  else if (button == self->countpoint)
+    break_type = IDE_DEBUGGER_BREAK_COUNTPOINT;
+
+  if (breakpoint != NULL)
+    {
+      _ide_debug_manager_remove_breakpoint (self->debug_manager, breakpoint);
+      breakpoint = NULL;
+    }
+
+  switch (break_type)
+    {
+    default:
+    case IDE_DEBUGGER_BREAK_NONE:
+      gtk_toggle_button_set_active (self->nobreak, TRUE);
+      gtk_toggle_button_set_active (self->breakpoint, FALSE);
+      gtk_toggle_button_set_active (self->countpoint, FALSE);
+      break;
+
+    case IDE_DEBUGGER_BREAK_BREAKPOINT:
+    case IDE_DEBUGGER_BREAK_COUNTPOINT:
+      {
+        g_autoptr(IdeDebuggerBreakpoint) to_insert = NULL;
+        g_autofree gchar *path = g_file_get_path (self->file);
+
+        to_insert = ide_debugger_breakpoint_new (NULL);
+
+        ide_debugger_breakpoint_set_line (to_insert, self->line);
+        ide_debugger_breakpoint_set_file (to_insert, path);
+        ide_debugger_breakpoint_set_mode (to_insert, break_type);
+        ide_debugger_breakpoint_set_enabled (to_insert, TRUE);
+
+        _ide_debug_manager_add_breakpoint (self->debug_manager, to_insert);
+
+        gtk_toggle_button_set_active (self->nobreak, FALSE);
+        gtk_toggle_button_set_active (self->breakpoint, break_type == IDE_DEBUGGER_BREAK_BREAKPOINT);
+        gtk_toggle_button_set_active (self->countpoint, break_type == IDE_DEBUGGER_BREAK_COUNTPOINT);
+      }
+      break;
+
+    case IDE_DEBUGGER_BREAK_WATCHPOINT:
+      /* TODO: watchpoint not yet supported */
+      gtk_toggle_button_set_active (self->nobreak, FALSE);
+      gtk_toggle_button_set_active (self->breakpoint, FALSE);
+      gtk_toggle_button_set_active (self->countpoint, FALSE);
+      break;
+    }
+
+  view = dzl_gtk_widget_get_relative (GTK_WIDGET (self), IDE_TYPE_SOURCE_VIEW);
+  gtk_widget_queue_draw (view);
+
+  g_signal_handlers_unblock_by_func (self->nobreak, G_CALLBACK (on_toggle_cb), self);
+  g_signal_handlers_unblock_by_func (self->breakpoint, G_CALLBACK (on_toggle_cb), self);
+  g_signal_handlers_unblock_by_func (self->countpoint, G_CALLBACK (on_toggle_cb), self);
+}
+
+GtkWidget *
+ide_debugger_hover_controls_new (IdeDebugManager *debug_manager,
+                                 GFile           *file,
+                                 guint            line)
+{
+  g_autoptr(IdeDebuggerBreakpoints) breakpoints = NULL;
+  IdeDebuggerHoverControls *self;
+
+  self = g_object_new (IDE_TYPE_DEBUGGER_HOVER_CONTROLS, NULL);
+  self->debug_manager = g_object_ref (debug_manager);
+  self->file = g_object_ref (file);
+  self->line = line;
+
+  if ((breakpoints = ide_debug_manager_get_breakpoints_for_file (debug_manager, file)))
+    {
+      IdeDebuggerBreakMode mode;
+
+      mode = ide_debugger_breakpoints_get_line_mode (breakpoints, line);
+
+      switch (mode)
+        {
+        default:
+        case IDE_DEBUGGER_BREAK_NONE:
+          gtk_toggle_button_set_active (self->nobreak, TRUE);
+          break;
+
+        case IDE_DEBUGGER_BREAK_BREAKPOINT:
+          gtk_toggle_button_set_active (self->breakpoint, TRUE);
+          break;
+
+        case IDE_DEBUGGER_BREAK_COUNTPOINT:
+          gtk_toggle_button_set_active (self->countpoint, TRUE);
+          break;
+
+        case IDE_DEBUGGER_BREAK_WATCHPOINT:
+          /* TODO: not currently supported */
+          break;
+        }
+    }
+
+  g_signal_connect (self->nobreak, "toggled", G_CALLBACK (on_toggle_cb), self);
+  g_signal_connect (self->breakpoint, "toggled", G_CALLBACK (on_toggle_cb), self);
+  g_signal_connect (self->countpoint, "toggled", G_CALLBACK (on_toggle_cb), self);
+
+  return GTK_WIDGET (self);
+}
diff --git a/src/libide/debugger/ide-debugger-hover-controls.h 
b/src/libide/debugger/ide-debugger-hover-controls.h
new file mode 100644
index 000000000..eadc4e729
--- /dev/null
+++ b/src/libide/debugger/ide-debugger-hover-controls.h
@@ -0,0 +1,35 @@
+/* ide-debugger-hover-controls.h
+ *
+ * Copyright 2018 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "debugger/ide-debug-manager.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DEBUGGER_HOVER_CONTROLS (ide_debugger_hover_controls_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDebuggerHoverControls, ide_debugger_hover_controls, IDE, DEBUGGER_HOVER_CONTROLS, 
GtkBin)
+
+GtkWidget *ide_debugger_hover_controls_new (IdeDebugManager *debug_manager,
+                                            GFile           *file,
+                                            guint            line);
+
+G_END_DECLS
diff --git a/src/libide/debugger/ide-debugger-hover-controls.ui 
b/src/libide/debugger/ide-debugger-hover-controls.ui
new file mode 100644
index 000000000..f0fb71d4a
--- /dev/null
+++ b/src/libide/debugger/ide-debugger-hover-controls.ui
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeDebuggerHoverControls" parent="GtkBin">
+    <child>
+      <object class="GtkBox">
+        <property name="visible">true</property>
+        <property name="homogeneous">true</property>
+        <property name="halign">center</property>
+        <property name="orientation">horizontal</property>
+        <style>
+          <class name="linked"/>
+        </style>
+        <child>
+          <object class="GtkToggleButton" id="nobreak">
+            <property name="visible">true</property>
+            <property name="label" translatable="yes">No break</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkToggleButton" id="breakpoint">
+            <property name="visible">true</property>
+            <property name="label" translatable="yes">Breakpoint</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkToggleButton" id="countpoint">
+            <property name="visible">true</property>
+            <property name="label" translatable="yes">Countpoint</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/debugger/ide-debugger-hover-provider.c 
b/src/libide/debugger/ide-debugger-hover-provider.c
new file mode 100644
index 000000000..b5f8e6876
--- /dev/null
+++ b/src/libide/debugger/ide-debugger-hover-provider.c
@@ -0,0 +1,115 @@
+/* ide-debugger-hover-provider.c
+ *
+ * Copyright 2018 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#define G_LOG_DOMAIN "ide-debugger-hover-provider"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+
+#include "ide-context.h"
+
+#include "buffers/ide-buffer.h"
+#include "debugger/ide-debug-manager.h"
+#include "debugger/ide-debugger-hover-controls.h"
+#include "debugger/ide-debugger-hover-provider.h"
+#include "files/ide-file.h"
+#include "hover/ide-marked-content.h"
+#include "threading/ide-task.h"
+
+struct _IdeDebuggerHoverProvider
+{
+  GObject parent_instance;
+};
+
+static void
+ide_debugger_hover_provider_hover_async (IdeHoverProvider    *provider,
+                                         IdeHoverContext     *context,
+                                         const GtkTextIter   *iter,
+                                         GCancellable        *cancellable,
+                                         GAsyncReadyCallback  callback,
+                                         gpointer             user_data)
+{
+  IdeDebuggerHoverProvider *self = (IdeDebuggerHoverProvider *)provider;
+  g_autoptr(IdeTask) task = NULL;
+  IdeDebugManager *dbgmgr;
+  const gchar *lang_id;
+  IdeContext *icontext;
+  IdeBuffer *buffer;
+  IdeFile *file;
+  GFile *gfile;
+  guint line;
+
+  g_assert (IDE_IS_DEBUGGER_HOVER_PROVIDER (provider));
+  g_assert (IDE_IS_HOVER_CONTEXT (context));
+  g_assert (iter != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_debugger_hover_provider_hover_async);
+
+  buffer = IDE_BUFFER (gtk_text_iter_get_buffer (iter));
+  lang_id = ide_buffer_get_language_id (buffer);
+  icontext = ide_buffer_get_context (buffer);
+  dbgmgr = ide_context_get_debug_manager (icontext);
+  file = ide_buffer_get_file (buffer);
+  gfile = ide_file_get_file (file);
+  line = gtk_text_iter_get_line (iter);
+
+  if (ide_debug_manager_supports_language (dbgmgr, lang_id))
+    {
+      GtkWidget *controls;
+
+      controls = ide_debugger_hover_controls_new (dbgmgr, gfile, line + 1);
+      ide_hover_context_add_widget (context, _("Debugger"), controls);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_debugger_hover_provider_hover_finish (IdeHoverProvider  *provider,
+                                          GAsyncResult      *result,
+                                          GError           **error)
+{
+  g_assert (IDE_IS_DEBUGGER_HOVER_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+hover_provider_iface_init (IdeHoverProviderInterface *iface)
+{
+  iface->hover_async = ide_debugger_hover_provider_hover_async;
+  iface->hover_finish = ide_debugger_hover_provider_hover_finish;
+}
+
+G_DEFINE_TYPE_WITH_CODE (IdeDebuggerHoverProvider, ide_debugger_hover_provider, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_HOVER_PROVIDER, hover_provider_iface_init))
+
+static void
+ide_debugger_hover_provider_class_init (IdeDebuggerHoverProviderClass *klass)
+{
+}
+
+static void
+ide_debugger_hover_provider_init (IdeDebuggerHoverProvider *self)
+{
+}
diff --git a/src/libide/debugger/ide-debugger-hover-provider.h 
b/src/libide/debugger/ide-debugger-hover-provider.h
new file mode 100644
index 000000000..1a348f9e0
--- /dev/null
+++ b/src/libide/debugger/ide-debugger-hover-provider.h
@@ -0,0 +1,29 @@
+/* ide-debugger-hover-provider.h
+ *
+ * Copyright 2018 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/>.
+ */
+
+#pragma once
+
+#include "hover/ide-hover-provider.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DEBUGGER_HOVER_PROVIDER (ide_debugger_hover_provider_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDebuggerHoverProvider, ide_debugger_hover_provider, IDE, DEBUGGER_HOVER_PROVIDER, 
GObject)
+
+G_END_DECLS
diff --git a/src/libide/debugger/ide-debugger-plugin.c b/src/libide/debugger/ide-debugger-plugin.c
index 956bf1492..f708c80b2 100644
--- a/src/libide/debugger/ide-debugger-plugin.c
+++ b/src/libide/debugger/ide-debugger-plugin.c
@@ -23,8 +23,10 @@
 #include "object-modules.h"
 
 #include "debugger/ide-debugger-editor-addin.h"
+#include "debugger/ide-debugger-hover-provider.h"
 #include "editor/ide-editor-addin.h"
 #include "editor/ide-editor-view-addin.h"
+#include "hover/ide-hover-provider.h"
 
 void
 ide_debugger_register_types (PeasObjectModule *module)
@@ -32,4 +34,7 @@ ide_debugger_register_types (PeasObjectModule *module)
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_EDITOR_ADDIN,
                                               IDE_TYPE_DEBUGGER_EDITOR_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_HOVER_PROVIDER,
+                                              IDE_TYPE_DEBUGGER_HOVER_PROVIDER);
 }
diff --git a/src/libide/debugger/meson.build b/src/libide/debugger/meson.build
index 5e828d39c..353daf92c 100644
--- a/src/libide/debugger/meson.build
+++ b/src/libide/debugger/meson.build
@@ -31,26 +31,18 @@ debugger_sources = [
 debugger_private_sources = [
   'ide-debugger-actions.c',
   'ide-debugger-address-map.c',
-  'ide-debugger-address-map.h',
   'ide-debugger-breakpoints-view.c',
-  'ide-debugger-breakpoints-view.h',
   'ide-debugger-controls.c',
-  'ide-debugger-controls.h',
   'ide-debugger-disassembly-view.c',
-  'ide-debugger-disassembly-view.h',
   'ide-debugger-editor-addin.c',
-  'ide-debugger-editor-addin.h',
   'ide-debugger-fallbacks.c',
+  'ide-debugger-hover-controls.c',
+  'ide-debugger-hover-provider.c',
   'ide-debugger-libraries-view.c',
-  'ide-debugger-libraries-view.h',
   'ide-debugger-locals-view.c',
-  'ide-debugger-locals-view.h',
   'ide-debugger-plugin.c',
-  'ide-debugger-private.h',
   'ide-debugger-registers-view.c',
-  'ide-debugger-registers-view.h',
   'ide-debugger-threads-view.c',
-  'ide-debugger-threads-view.h',
 ]
 
 libide_public_headers += files(debugger_headers)
diff --git a/src/libide/editor/ide-editor-hover-provider.c b/src/libide/editor/ide-editor-hover-provider.c
new file mode 100644
index 000000000..89236d35b
--- /dev/null
+++ b/src/libide/editor/ide-editor-hover-provider.c
@@ -0,0 +1,109 @@
+/* ide-editor-hover-provider.c
+ *
+ * Copyright 2018 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#define G_LOG_DOMAIN "ide-editor-hover-provider"
+
+#include <glib/gi18n.h>
+
+#include "buffers/ide-buffer.h"
+#include "diagnostics/ide-diagnostic.h"
+#include "editor/ide-editor-hover-provider.h"
+#include "threading/ide-task.h"
+
+struct _IdeEditorHoverProvider
+{
+  GObject parent_instance;
+};
+
+static void
+ide_editor_hover_provider_hover_async (IdeHoverProvider    *provider,
+                                       IdeHoverContext     *context,
+                                       const GtkTextIter   *iter,
+                                       GCancellable        *cancellable,
+                                       GAsyncReadyCallback  callback,
+                                       gpointer             user_data)
+{
+  IdeEditorHoverProvider *self = (IdeEditorHoverProvider *)provider;
+  g_autoptr(IdeTask) task = NULL;
+  GtkTextBuffer *buffer;
+
+  g_assert (IDE_IS_EDITOR_HOVER_PROVIDER (self));
+  g_assert (IDE_IS_HOVER_CONTEXT (context));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_editor_hover_provider_hover_async);
+
+  buffer = gtk_text_iter_get_buffer (iter);
+
+  if (IDE_IS_BUFFER (buffer))
+    {
+      IdeDiagnostic *diag;
+
+      diag = ide_buffer_get_diagnostic_at_iter (IDE_BUFFER (buffer), iter);
+
+      if (diag != NULL)
+        {
+          g_autoptr(IdeMarkedContent) content = NULL;
+          g_autofree gchar *text = ide_diagnostic_get_text_for_display (diag);
+
+          content = ide_marked_content_new_from_data (text,
+                                                      strlen (text),
+                                                      IDE_MARKED_KIND_PLAINTEXT);
+          ide_hover_context_add_content (context, _("Diagnostics"), content);
+        }
+    }
+
+  ide_task_return_new_error (task,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_SUPPORTED,
+                             "No information to display");
+}
+
+static gboolean
+ide_editor_hover_provider_hover_finish (IdeHoverProvider  *self,
+                                        GAsyncResult      *result,
+                                        GError           **error)
+{
+  g_assert (IDE_IS_HOVER_PROVIDER (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+hover_provider_iface_init (IdeHoverProviderInterface *iface)
+{
+  iface->hover_async = ide_editor_hover_provider_hover_async;
+  iface->hover_finish = ide_editor_hover_provider_hover_finish;
+}
+
+G_DEFINE_TYPE_WITH_CODE (IdeEditorHoverProvider, ide_editor_hover_provider, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_HOVER_PROVIDER, hover_provider_iface_init))
+
+static void
+ide_editor_hover_provider_class_init (IdeEditorHoverProviderClass *klass)
+{
+}
+
+static void
+ide_editor_hover_provider_init (IdeEditorHoverProvider *self)
+{
+}
diff --git a/src/libide/editor/ide-editor-hover-provider.h b/src/libide/editor/ide-editor-hover-provider.h
new file mode 100644
index 000000000..0252ab4ca
--- /dev/null
+++ b/src/libide/editor/ide-editor-hover-provider.h
@@ -0,0 +1,29 @@
+/* ide-editor-hover-provider.h
+ *
+ * Copyright 2018 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/>.
+ */
+
+#pragma once
+
+#include "hover/ide-hover-provider.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_EDITOR_HOVER_PROVIDER (ide_editor_hover_provider_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeEditorHoverProvider, ide_editor_hover_provider, IDE, EDITOR_HOVER_PROVIDER, GObject)
+
+G_END_DECLS
diff --git a/src/libide/editor/ide-editor-plugin.c b/src/libide/editor/ide-editor-plugin.c
index d7435f3c1..f6536be0a 100644
--- a/src/libide/editor/ide-editor-plugin.c
+++ b/src/libide/editor/ide-editor-plugin.c
@@ -22,12 +22,20 @@
 
 #include "object-modules.h"
 
+#include "editor/ide-editor-hover-provider.h"
 #include "editor/ide-editor-layout-stack-addin.h"
 #include "editor/ide-editor-workbench-addin.h"
 
 void
 ide_editor_register_types (PeasObjectModule *module)
 {
-  peas_object_module_register_extension_type (module, IDE_TYPE_LAYOUT_STACK_ADDIN, 
IDE_TYPE_EDITOR_LAYOUT_STACK_ADDIN);
-  peas_object_module_register_extension_type (module, IDE_TYPE_WORKBENCH_ADDIN, 
IDE_TYPE_EDITOR_WORKBENCH_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_HOVER_PROVIDER,
+                                              IDE_TYPE_EDITOR_HOVER_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_LAYOUT_STACK_ADDIN,
+                                              IDE_TYPE_EDITOR_LAYOUT_STACK_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKBENCH_ADDIN,
+                                              IDE_TYPE_EDITOR_WORKBENCH_ADDIN);
 }
diff --git a/src/libide/editor/meson.build b/src/libide/editor/meson.build
index 60c39b8fa..762c36938 100644
--- a/src/libide/editor/meson.build
+++ b/src/libide/editor/meson.build
@@ -19,26 +19,20 @@ editor_sources = [
 ]
 
 editor_private_sources = [
+  'ide-editor-hover-provider.c',
   'ide-editor-layout-stack-addin.c',
-  'ide-editor-layout-stack-addin.h',
   'ide-editor-layout-stack-controls.c',
-  'ide-editor-layout-stack-controls.h',
   'ide-editor-perspective-actions.c',
   'ide-editor-perspective-shortcuts.c',
   'ide-editor-plugin.c',
   'ide-editor-print-operation.c',
-  'ide-editor-print-operation.h',
-  'ide-editor-private.h',
   'ide-editor-properties.c',
-  'ide-editor-properties.h',
   'ide-editor-search-bar.c',
   'ide-editor-search-bar-shortcuts.c',
-  'ide-editor-search-bar.h',
   'ide-editor-view-actions.c',
   'ide-editor-view-settings.c',
   'ide-editor-view-shortcuts.c',
   'ide-editor-workbench-addin.c',
-  'ide-editor-workbench-addin.h',
 ]
 
 libide_public_headers += files(editor_headers)
diff --git a/src/libide/hover/ide-hover-context-private.h b/src/libide/hover/ide-hover-context-private.h
new file mode 100644
index 000000000..e099e04f7
--- /dev/null
+++ b/src/libide/hover/ide-hover-context-private.h
@@ -0,0 +1,48 @@
+/* ide-hover-context-private.h
+ *
+ * Copyright 2018 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "hover/ide-hover-context.h"
+#include "hover/ide-hover-provider.h"
+#include "hover/ide-marked-content.h"
+
+G_BEGIN_DECLS
+
+typedef void (*IdeHoverContextForeach) (const gchar      *title,
+                                        IdeMarkedContent *content,
+                                        GtkWidget        *widget,
+                                        gpointer          user_data);
+
+void     _ide_hover_context_add_provider  (IdeHoverContext         *context,
+                                           IdeHoverProvider        *provider);
+void     _ide_hover_context_query_async   (IdeHoverContext         *self,
+                                           const GtkTextIter       *iter,
+                                           GCancellable            *cancellable,
+                                           GAsyncReadyCallback      callback,
+                                           gpointer                 user_data);
+gboolean _ide_hover_context_query_finish  (IdeHoverContext         *self,
+                                           GAsyncResult            *result,
+                                           GError                 **error);
+void     _ide_hover_context_foreach       (IdeHoverContext         *self,
+                                           IdeHoverContextForeach   foreach,
+                                           gpointer                 foreach_data);
+
+G_END_DECLS
diff --git a/src/libide/hover/ide-hover-context.c b/src/libide/hover/ide-hover-context.c
new file mode 100644
index 000000000..73d95c4f4
--- /dev/null
+++ b/src/libide/hover/ide-hover-context.c
@@ -0,0 +1,253 @@
+/* ide-hover-context.c
+ *
+ * Copyright 2018 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"
+
+#define G_LOG_DOMAIN "ide-hover-context"
+
+#include "hover/ide-hover-context.h"
+#include "hover/ide-hover-context-private.h"
+#include "hover/ide-hover-provider.h"
+#include "threading/ide-task.h"
+
+struct _IdeHoverContext
+{
+  GObject    parent_instance;
+  GPtrArray *providers;
+  GArray    *content;
+};
+
+typedef struct
+{
+  gchar            *title;
+  IdeMarkedContent *content;
+  GtkWidget        *widget;
+} Item;
+
+typedef struct
+{
+  guint active;
+} Query;
+
+G_DEFINE_TYPE (IdeHoverContext, ide_hover_context, G_TYPE_OBJECT)
+
+static void
+clear_item (Item *item)
+{
+  g_clear_pointer (&item->title, g_free);
+  g_clear_pointer (&item->content, ide_marked_content_unref);
+  g_clear_object (&item->widget);
+}
+
+static void
+ide_hover_context_dispose (GObject *object)
+{
+  IdeHoverContext *self = (IdeHoverContext *)object;
+
+  g_clear_pointer (&self->content, g_array_unref);
+
+  G_OBJECT_CLASS (ide_hover_context_parent_class)->dispose (object);
+}
+
+static void
+ide_hover_context_class_init (IdeHoverContextClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_hover_context_dispose;
+}
+
+static void
+ide_hover_context_init (IdeHoverContext *self)
+{
+  self->providers = g_ptr_array_new_with_free_func (g_object_unref);
+
+  self->content = g_array_new (FALSE, FALSE, sizeof (Item));
+  g_array_set_clear_func (self->content, (GDestroyNotify) clear_item);
+}
+
+void
+ide_hover_context_add_content (IdeHoverContext  *self,
+                               const gchar      *title,
+                               IdeMarkedContent *content)
+{
+  Item item = {0};
+
+  g_return_if_fail (IDE_IS_HOVER_CONTEXT (self));
+  g_return_if_fail (content != NULL);
+
+  item.title = g_strdup (title);
+  item.content = ide_marked_content_ref (content);
+  item.widget = NULL;
+
+  g_array_append_val (self->content, item);
+}
+
+void
+ide_hover_context_add_widget (IdeHoverContext *self,
+                              const gchar     *title,
+                              GtkWidget       *widget)
+{
+  Item item = {0};
+
+  g_return_if_fail (IDE_IS_HOVER_CONTEXT (self));
+  g_return_if_fail (widget != NULL);
+
+  item.title = g_strdup (title);
+  item.content = NULL;
+  item.widget = g_object_ref_sink (widget);
+
+  g_array_append_val (self->content, item);
+}
+
+void
+_ide_hover_context_add_provider (IdeHoverContext  *self,
+                                 IdeHoverProvider *provider)
+{
+  g_return_if_fail (IDE_IS_HOVER_CONTEXT (self));
+  g_return_if_fail (IDE_IS_HOVER_PROVIDER (provider));
+
+  g_ptr_array_add (self->providers, g_object_ref (provider));
+}
+
+static void
+query_free (Query *q)
+{
+  g_slice_free (Query, q);
+}
+
+static void
+ide_hover_context_query_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  IdeHoverProvider *provider = (IdeHoverProvider *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  Query *q;
+
+  g_assert (IDE_IS_HOVER_PROVIDER (provider));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  q = ide_task_get_task_data (task);
+  g_assert (q != NULL);
+  g_assert (q->active > 0);
+
+  if (!ide_hover_provider_hover_finish (provider, result, &error))
+    g_debug ("%s: %s", G_OBJECT_TYPE_NAME (provider), error->message);
+
+  q->active--;
+
+  if (q->active == 0)
+    ide_task_return_boolean (task, TRUE);
+}
+
+void
+_ide_hover_context_query_async (IdeHoverContext     *self,
+                                const GtkTextIter   *iter,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  Query *q;
+
+  g_return_if_fail (IDE_IS_HOVER_CONTEXT (self));
+  g_return_if_fail (iter != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, _ide_hover_context_query_async);
+
+  q = g_slice_new0 (Query);
+  q->active = self->providers->len;
+  ide_task_set_task_data (task, q, (GDestroyNotify)query_free);
+
+  for (guint i = 0; i < self->providers->len; i++)
+    {
+      IdeHoverProvider *provider = g_ptr_array_index (self->providers, i);
+
+      ide_hover_provider_hover_async (provider,
+                                      self,
+                                      iter,
+                                      cancellable,
+                                      ide_hover_context_query_cb,
+                                      g_object_ref (task));
+    }
+
+  if (q->active == 0)
+    ide_task_return_boolean (task, TRUE);
+}
+
+/**
+ * ide_hover_context_query_finish:
+ * @self: an #IdeHoverContext
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes a request to query providers.
+ *
+ * Returns: %TRUE if successful, otherwise %FALSE and @error.
+ *
+ * Since: 3.30
+ */
+gboolean
+_ide_hover_context_query_finish (IdeHoverContext  *self,
+                                 GAsyncResult     *result,
+                                 GError          **error)
+{
+  g_return_val_if_fail (IDE_IS_HOVER_CONTEXT (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+gboolean
+ide_hover_context_has_content (IdeHoverContext *self)
+{
+  g_return_val_if_fail (IDE_IS_HOVER_CONTEXT (self), FALSE);
+
+  return self->content != NULL && self->content->len > 0;
+}
+
+void
+_ide_hover_context_foreach (IdeHoverContext        *self,
+                            IdeHoverContextForeach  foreach,
+                            gpointer                foreach_data)
+{
+  g_return_if_fail (IDE_IS_HOVER_CONTEXT (self));
+  g_return_if_fail (foreach != NULL);
+
+  if (self->content == NULL || self->content->len == 0)
+    return;
+
+  /* Iterate backwards to allow mutation */
+  for (guint i = self->content->len; i > 0; i--)
+    {
+      const Item *item = &g_array_index (self->content, Item, i - 1);
+
+      foreach (item->title, item->content, item->widget, foreach_data);
+
+      /* Widgets are consumed to prevent double use */
+      if (item->widget != NULL)
+        g_array_remove_index (self->content, i - 1);
+    }
+}
diff --git a/src/libide/hover/ide-hover-context.h b/src/libide/hover/ide-hover-context.h
new file mode 100644
index 000000000..45ccc6188
--- /dev/null
+++ b/src/libide/hover/ide-hover-context.h
@@ -0,0 +1,48 @@
+/* ide-hover-context.h
+ *
+ * Copyright 2018 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 <gio/gio.h>
+#include <gtk/gtk.h>
+
+#include "ide-version-macros.h"
+
+#include "ide-marked-content.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_HOVER_CONTEXT (ide_hover_context_get_type())
+
+IDE_AVAILABLE_IN_3_30
+G_DECLARE_FINAL_TYPE (IdeHoverContext, ide_hover_context, IDE, HOVER_CONTEXT, GObject)
+
+IDE_AVAILABLE_IN_3_30
+void     ide_hover_context_add_content  (IdeHoverContext      *self,
+                                         const gchar          *title,
+                                         IdeMarkedContent     *content);
+IDE_AVAILABLE_IN_3_30
+void     ide_hover_context_add_widget   (IdeHoverContext      *self,
+                                         const gchar          *title,
+                                         GtkWidget            *widget);
+IDE_AVAILABLE_IN_3_30
+gboolean ide_hover_context_has_content  (IdeHoverContext      *self);
+
+G_END_DECLS
diff --git a/src/libide/hover/ide-hover-popover-private.h b/src/libide/hover/ide-hover-popover-private.h
new file mode 100644
index 000000000..f4a90efbe
--- /dev/null
+++ b/src/libide/hover/ide-hover-popover-private.h
@@ -0,0 +1,38 @@
+/* ide-hover-popover-private.h
+ *
+ * Copyright 2018 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "ide-hover-context.h"
+#include "ide-hover-provider.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_HOVER_POPOVER (ide_hover_popover_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeHoverPopover, ide_hover_popover, IDE, HOVER_POPOVER, GtkPopover)
+
+IdeHoverContext *_ide_hover_popover_get_context  (IdeHoverPopover  *self);
+void             _ide_hover_popover_add_provider (IdeHoverPopover  *self,
+                                                  IdeHoverProvider *provider);
+void             _ide_hover_popover_show         (IdeHoverPopover  *self);
+void             _ide_hover_popover_hide         (IdeHoverPopover  *self);
+
+G_END_DECLS
diff --git a/src/libide/hover/ide-hover-popover.c b/src/libide/hover/ide-hover-popover.c
new file mode 100644
index 000000000..40f771770
--- /dev/null
+++ b/src/libide/hover/ide-hover-popover.c
@@ -0,0 +1,283 @@
+/* ide-hover-popover.c
+ *
+ * Copyright 2018 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#define G_LOG_DOMAIN "ide-hover-popover"
+
+#include <dazzle.h>
+
+#include "hover/ide-hover-context-private.h"
+#include "hover/ide-hover-popover-private.h"
+#include "hover/ide-marked-view.h"
+
+struct _IdeHoverPopover
+{
+  GtkPopover parent_instance;
+
+  /*
+   * A vertical box containing all of our marked content/widgets that
+   * were provided by the context.
+   */
+  GtkBox *box;
+
+  /*
+   * Our context to be observed. As items are added to the context,
+   * we add them to the popver (creating or re-using the widget) based
+   * on the kind of content.
+   */
+  IdeHoverContext *context;
+
+  /*
+   * This is our cancellable to cancel any in-flight requests to the
+   * hover providers when the popover is withdrawn. That could happen
+   * before we've even really been displayed to the user.
+   */
+  GCancellable *cancellable;
+
+  /*
+   * If we've had any providers added, so that we can short-circuit
+   * in that case without having to display the popover.
+   */
+  guint has_providers : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_CONTEXT,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeHoverPopover, ide_hover_popover, GTK_TYPE_POPOVER)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_hover_popover_add_content (const gchar      *title,
+                               IdeMarkedContent *content,
+                               GtkWidget        *widget,
+                               gpointer          user_data)
+{
+  IdeHoverPopover *self = user_data;
+  GtkBox *box;
+
+  g_assert (content != NULL || widget != NULL);
+  g_assert (!widget || GTK_IS_WIDGET (widget));
+
+  box = g_object_new (GTK_TYPE_BOX,
+                      "orientation", GTK_ORIENTATION_VERTICAL,
+                      "visible", TRUE,
+                      NULL);
+  gtk_container_add (GTK_CONTAINER (self->box), GTK_WIDGET (box));
+
+  if (!dzl_str_empty0 (title))
+    {
+      GtkWidget *label;
+
+      label = g_object_new (GTK_TYPE_LABEL,
+                            "xalign", 0.0f,
+                            "label", title,
+                            "use-markup", FALSE,
+                            "visible", TRUE,
+                            NULL);
+      dzl_gtk_widget_add_style_class (label, "title");
+      gtk_container_add (GTK_CONTAINER (box), label);
+    }
+
+  if (content != NULL)
+    {
+      GtkWidget *view = ide_marked_view_new (content);
+
+      if (view != NULL)
+        {
+          gtk_container_add (GTK_CONTAINER (box), view);
+          gtk_widget_show (view);
+        }
+    }
+
+  if (widget != NULL)
+    {
+      gtk_container_add (GTK_CONTAINER (box), widget);
+      gtk_widget_show (widget);
+    }
+}
+
+static void
+ide_hover_popover_query_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  IdeHoverContext *context = (IdeHoverContext *)object;
+  g_autoptr(IdeHoverPopover) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_HOVER_CONTEXT (context));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_HOVER_POPOVER (self));
+
+  if (!_ide_hover_context_query_finish (context, result, &error) ||
+      !ide_hover_context_has_content (context))
+    {
+      gtk_widget_destroy (GTK_WIDGET (self));
+      return;
+    }
+
+  _ide_hover_context_foreach (context,
+                              ide_hover_popover_add_content,
+                              self);
+
+  gtk_widget_show (GTK_WIDGET (self));
+}
+
+static void
+ide_hover_popover_destroy (GtkWidget *widget)
+{
+  IdeHoverPopover *self = (IdeHoverPopover *)widget;
+
+  g_cancellable_cancel (self->cancellable);
+
+  g_clear_object (&self->context);
+  g_clear_object (&self->cancellable);
+
+  GTK_WIDGET_CLASS (ide_hover_popover_parent_class)->destroy (widget);
+}
+
+static void
+ide_hover_popover_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeHoverPopover *self = IDE_HOVER_POPOVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_value_set_object (value, _ide_hover_popover_get_context (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_hover_popover_class_init (IdeHoverPopoverClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = ide_hover_popover_get_property;
+
+  widget_class->destroy = ide_hover_popover_destroy;
+
+  properties [PROP_CONTEXT] =
+    g_param_spec_object ("context",
+                         "Context",
+                         "The hover context to display to the user",
+                         IDE_TYPE_HOVER_CONTEXT,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+  
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_hover_popover_init (IdeHoverPopover *self)
+{
+  GtkStyleContext *style_context;
+
+  self->context = g_object_new (IDE_TYPE_HOVER_CONTEXT, NULL);
+  self->cancellable = g_cancellable_new ();
+
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  gtk_style_context_add_class (style_context, "hoverer");
+
+  self->box = g_object_new (GTK_TYPE_BOX,
+                            "orientation", GTK_ORIENTATION_VERTICAL,
+                            "visible", TRUE,
+                            NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->box));
+}
+
+IdeHoverContext *
+_ide_hover_popover_get_context (IdeHoverPopover *self)
+{
+  g_return_val_if_fail (IDE_IS_HOVER_POPOVER (self), NULL);
+
+  return self->context;
+}
+
+void
+_ide_hover_popover_add_provider (IdeHoverPopover  *self,
+                                 IdeHoverProvider *provider)
+{
+  g_return_if_fail (IDE_IS_HOVER_POPOVER (self));
+  g_return_if_fail (IDE_IS_HOVER_PROVIDER (provider));
+
+  _ide_hover_context_add_provider (self->context, provider);
+
+  self->has_providers = TRUE;
+}
+
+void
+_ide_hover_popover_show (IdeHoverPopover *self)
+{
+  GdkRectangle rect;
+  GtkWidget *view;
+
+  g_return_if_fail (IDE_IS_HOVER_POPOVER (self));
+  g_return_if_fail (self->context != NULL);
+
+  if (self->has_providers &&
+      !g_cancellable_is_cancelled (self->cancellable) &&
+      (view = gtk_popover_get_relative_to (GTK_POPOVER (self))) &&
+      GTK_IS_TEXT_VIEW (view) &&
+      gtk_popover_get_pointing_to (GTK_POPOVER (self), &rect))
+    {
+      GtkTextIter iter;
+      gint x, y;
+
+      /* Get the center of the box */
+      x = rect.x + (rect.width / 2);
+      y = rect.y + (rect.height / 2);
+
+      gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (view),
+                                             GTK_TEXT_WINDOW_WIDGET,
+                                             x, y, &x, &y);
+      gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), &iter, x, y);
+
+      _ide_hover_context_query_async (self->context,
+                                      &iter,
+                                      self->cancellable,
+                                      ide_hover_popover_query_cb,
+                                      g_object_ref (self));
+
+      return;
+    }
+
+  /* Cancel this popover immediately, we have nothing to do */
+  gtk_widget_destroy (GTK_WIDGET (self));
+}
+
+void
+_ide_hover_popover_hide (IdeHoverPopover *self)
+{
+  g_return_if_fail (IDE_IS_HOVER_POPOVER (self));
+
+  gtk_widget_destroy (GTK_WIDGET (self));
+}
diff --git a/src/libide/hover/ide-hover-private.h b/src/libide/hover/ide-hover-private.h
new file mode 100644
index 000000000..3d45b6fca
--- /dev/null
+++ b/src/libide/hover/ide-hover-private.h
@@ -0,0 +1,37 @@
+/* ide-hover-private.h
+ *
+ * Copyright 2018 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/>.
+ */
+
+#pragma once
+
+#include "ide-context.h"
+
+#include "sourceview/ide-source-view.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_HOVER (ide_hover_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeHover, ide_hover, IDE, HOVER, GObject)
+
+IdeHover *_ide_hover_new          (IdeSourceView *view);
+void      _ide_hover_set_context  (IdeHover      *self,
+                                   IdeContext    *context);
+void      _ide_hover_set_language (IdeHover      *self,
+                                   const gchar   *language);
+
+G_END_DECLS
diff --git a/src/libide/hover/ide-hover-provider.c b/src/libide/hover/ide-hover-provider.c
new file mode 100644
index 000000000..a5bd0b308
--- /dev/null
+++ b/src/libide/hover/ide-hover-provider.c
@@ -0,0 +1,148 @@
+/* ide-hover-provider.c
+ *
+ * Copyright 2018 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "config.h"
+
+#define G_LOG_DOMAIN "ide-hover-provider"
+
+#include "hover/ide-hover-provider.h"
+#include "sourceview/ide-source-view.h"
+
+G_DEFINE_INTERFACE (IdeHoverProvider, ide_hover_provider, G_TYPE_OBJECT)
+
+static void
+ide_hover_provider_real_hover_async (IdeHoverProvider    *self,
+                                     IdeHoverContext     *context,
+                                     const GtkTextIter   *location,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
+{
+  g_task_report_new_error (self, callback, user_data,
+                           ide_hover_provider_real_hover_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "Hovering not supported");
+}
+
+static gboolean
+ide_hover_provider_real_hover_finish (IdeHoverProvider  *self,
+                                      GAsyncResult      *result,
+                                      GError           **error)
+{
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_hover_provider_default_init (IdeHoverProviderInterface *iface)
+{
+  iface->hover_async = ide_hover_provider_real_hover_async;
+  iface->hover_finish = ide_hover_provider_real_hover_finish;
+}
+
+/**
+ * ide_hover_provider_load:
+ * @self: an #IdeHoverProvider
+ * @view: an #IdeSourceView
+ *
+ * This method is used to load an #IdeHoverProvider.
+ * Providers should perform any startup work from here.
+ *
+ * Since: 3.30
+ */
+void
+ide_hover_provider_load (IdeHoverProvider *self,
+                         IdeSourceView    *view)
+{
+  g_return_if_fail (IDE_IS_HOVER_PROVIDER (self));
+  g_return_if_fail (IDE_IS_SOURCE_VIEW (view));
+
+  if (IDE_HOVER_PROVIDER_GET_IFACE (self)->load)
+    IDE_HOVER_PROVIDER_GET_IFACE (self)->load (self, view);
+}
+
+/**
+ * ide_hover_provider_unload:
+ * @self: an #IdeHoverProvider
+ * @view: an #IdeSourceView
+ *
+ * This method is used to unload an #IdeHoverProvider.
+ * Providers should cleanup any state they've allocated.
+ *
+ * Since: 3.30
+ */
+void
+ide_hover_provider_unload (IdeHoverProvider *self,
+                           IdeSourceView    *view)
+{
+  g_return_if_fail (IDE_IS_HOVER_PROVIDER (self));
+  g_return_if_fail (IDE_IS_SOURCE_VIEW (view));
+
+  if (IDE_HOVER_PROVIDER_GET_IFACE (self)->unload)
+    IDE_HOVER_PROVIDER_GET_IFACE (self)->unload (self, view);
+}
+
+/**
+ * ide_hover_provider_hover_async:
+ * @self: an #IdeHoverProvider
+ * @location: a #GtkTextIter
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * 
+ * Since: 3.30
+ */
+void
+ide_hover_provider_hover_async (IdeHoverProvider    *self,
+                                IdeHoverContext     *context,
+                                const GtkTextIter   *location,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_HOVER_PROVIDER (self));
+  g_return_if_fail (IDE_IS_HOVER_CONTEXT (context));
+  g_return_if_fail (location != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_HOVER_PROVIDER_GET_IFACE (self)->hover_async (self, context, location, cancellable, callback, 
user_data);
+}
+
+/**
+ * ide_hover_provider_hover_finish:
+ * @self: an #IdeHoverProvider
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.30
+ */
+gboolean
+ide_hover_provider_hover_finish (IdeHoverProvider  *self,
+                                 GAsyncResult      *result,
+                                 GError           **error)
+{
+  g_return_val_if_fail (IDE_IS_HOVER_PROVIDER (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_HOVER_PROVIDER_GET_IFACE (self)->hover_finish (self, result, error);
+}
diff --git a/src/libide/hover/ide-hover-provider.h b/src/libide/hover/ide-hover-provider.h
new file mode 100644
index 000000000..8ff86a83d
--- /dev/null
+++ b/src/libide/hover/ide-hover-provider.h
@@ -0,0 +1,74 @@
+/* ide-hover-provider.h
+ *
+ * Copyright 2018 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "ide-version-macros.h"
+
+#include "hover/ide-hover-context.h"
+#include "sourceview/ide-source-view.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_HOVER_PROVIDER (ide_hover_provider_get_type ())
+
+IDE_AVAILABLE_IN_3_30
+G_DECLARE_INTERFACE (IdeHoverProvider, ide_hover_provider, IDE, HOVER_PROVIDER, GObject)
+
+struct _IdeHoverProviderInterface
+{
+  GTypeInterface parent;
+
+  void     (*load)         (IdeHoverProvider     *self,
+                            IdeSourceView        *view);
+  void     (*unload)       (IdeHoverProvider     *self,
+                            IdeSourceView        *view);
+  void     (*hover_async)  (IdeHoverProvider     *self,
+                            IdeHoverContext      *context,
+                            const GtkTextIter    *location,
+                            GCancellable         *cancellable,
+                            GAsyncReadyCallback   callback,
+                            gpointer              user_data);
+  gboolean (*hover_finish) (IdeHoverProvider     *self,
+                            GAsyncResult         *result,
+                            GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_30
+void     ide_hover_provider_load         (IdeHoverProvider     *self,
+                                          IdeSourceView        *view);
+IDE_AVAILABLE_IN_3_30
+void     ide_hover_provider_unload       (IdeHoverProvider     *self,
+                                          IdeSourceView        *view);
+IDE_AVAILABLE_IN_3_30
+void     ide_hover_provider_hover_async  (IdeHoverProvider     *self,
+                                          IdeHoverContext      *context,
+                                          const GtkTextIter    *location,
+                                          GCancellable         *cancellable,
+                                          GAsyncReadyCallback   callback,
+                                          gpointer              user_data);
+IDE_AVAILABLE_IN_3_30
+gboolean ide_hover_provider_hover_finish (IdeHoverProvider     *self,
+                                          GAsyncResult         *result,
+                                          GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/hover/ide-hover.c b/src/libide/hover/ide-hover.c
new file mode 100644
index 000000000..620e0c73c
--- /dev/null
+++ b/src/libide/hover/ide-hover.c
@@ -0,0 +1,640 @@
+/* ide-hover.c
+ *
+ * Copyright 2018 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#define G_LOG_DOMAIN "ide-hover"
+
+#include <dazzle.h>
+#include <libpeas/peas.h>
+#include <string.h>
+
+#include "hover/ide-hover-popover-private.h"
+#include "hover/ide-hover-private.h"
+#include "hover/ide-hover-provider.h"
+#include "plugins/ide-extension-set-adapter.h"
+#include "sourceview/ide-source-iter.h"
+#include "util/ide-gtk.h"
+
+#define GRACE_X 20
+#define GRACE_Y 20
+#define MOTION_SETTLE_TIMEOUT_MSEC 500
+
+typedef enum
+{
+  IDE_HOVER_STATE_INITIAL,
+  IDE_HOVER_STATE_DISPLAY,
+  IDE_HOVER_STATE_IN_POPOVER,
+} IdeHoverState;
+
+struct _IdeHover
+{
+  GObject parent_instance;
+
+  /*
+   * Our signal group to handle the number of events on the textview so that
+   * we can update the hover provider and associated content.
+   */
+  DzlSignalGroup *signals;
+
+  /*
+   * Our plugins that can populate our IdeHoverContext with content to be
+   * displayed.
+   */
+  IdeExtensionSetAdapter *providers;
+
+  /*
+   * Our popover that will display content once the cursor has settled
+   * somewhere of importance.
+   */
+  IdeHoverPopover *popover;
+
+  /*
+   * Our last motion position, used to calculate where we should find
+   * our iter to display the popover.
+   */
+  gdouble motion_x;
+  gdouble motion_y;
+
+  /*
+   * Our state so that we can handle events in a sane manner without
+   * stomping all over things.
+   */
+  IdeHoverState state;
+
+  /*
+   * Our source which is continually delayed until the motion event has
+   * settled somewhere we can potentially display a popover.
+   */
+  guint delay_display_source;
+
+  /*
+   * We need to introduce some delay when we get a leave-notify-event
+   * because we might be entering the popover next.
+   */
+  guint dismiss_source;
+};
+
+G_DEFINE_TYPE (IdeHover, ide_hover, G_TYPE_OBJECT)
+
+static void
+ide_hover_popover_closed_cb (IdeHover        *self,
+                             IdeHoverPopover *popover)
+{
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (IDE_IS_HOVER_POPOVER (popover));
+
+  self->state = IDE_HOVER_STATE_INITIAL;
+  gtk_widget_destroy (GTK_WIDGET (popover));
+  dzl_clear_source (&self->dismiss_source);
+  dzl_clear_source (&self->delay_display_source);
+
+  g_assert (self->popover == NULL);
+  g_assert (self->state == IDE_HOVER_STATE_INITIAL);
+  g_assert (self->dismiss_source == 0);
+  g_assert (self->delay_display_source == 0);
+}
+
+static gboolean
+ide_hover_popover_enter_notify_event_cb (IdeHover               *self,
+                                         const GdkEventCrossing *event,
+                                         IdeHoverPopover        *popover)
+{
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (event != NULL);
+  g_assert (IDE_IS_HOVER_POPOVER (popover));
+  g_assert (self->state == IDE_HOVER_STATE_DISPLAY);
+
+  self->state = IDE_HOVER_STATE_IN_POPOVER;
+
+  dzl_clear_source (&self->dismiss_source);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+ide_hover_popover_leave_notify_event_cb (IdeHover               *self,
+                                         const GdkEventCrossing *event,
+                                         IdeHoverPopover        *popover)
+{
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (event != NULL);
+  g_assert (IDE_IS_HOVER_POPOVER (popover));
+
+  if (self->state == IDE_HOVER_STATE_IN_POPOVER)
+    self->state = IDE_HOVER_STATE_DISPLAY;
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+ide_hover_providers_foreach_cb (IdeExtensionSetAdapter *set,
+                                PeasPluginInfo         *plugin_info,
+                                PeasExtension          *exten,
+                                gpointer                user_data)
+{
+  IdeHoverPopover *popover = user_data;
+  IdeHoverProvider *provider = (IdeHoverProvider *)exten;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_HOVER_PROVIDER (provider));
+  g_assert (IDE_IS_HOVER_POPOVER (popover));
+
+  _ide_hover_popover_add_provider (popover, provider);
+}
+
+static void
+ide_hover_popover_destroy_cb (IdeHover        *self,
+                              IdeHoverPopover *popover)
+{
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (IDE_IS_HOVER_POPOVER (popover));
+
+  self->popover = NULL;
+  self->state = IDE_HOVER_STATE_INITIAL;
+}
+
+static void
+ide_hover_get_bounds (IdeHover    *self,
+                      GtkTextIter *begin,
+                      GtkTextIter *end)
+{
+  GtkTextView *view;
+  GtkTextIter iter;
+  gint x, y;
+
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
+
+  if (!(view = dzl_signal_group_get_target (self->signals)))
+    {
+      memset (begin, 0, sizeof *begin);
+      memset (end, 0, sizeof *end);
+      return;
+    }
+
+  g_assert (GTK_IS_TEXT_VIEW (view));
+
+  gtk_text_view_window_to_buffer_coords (view,
+                                         GTK_TEXT_WINDOW_WIDGET,
+                                         self->motion_x,
+                                         self->motion_y,
+                                         &x, &y);
+
+  gtk_text_view_get_iter_at_location (view, &iter, x, y);
+
+  if (!_ide_source_iter_inside_word (&iter))
+    {
+      *begin = iter;
+      gtk_text_iter_set_line_offset (begin, 0);
+
+      *end = *begin;
+      gtk_text_iter_forward_to_line_end (end);
+
+      return;
+    }
+
+  if (!_ide_source_iter_starts_full_word (&iter))
+    _ide_source_iter_backward_full_word_start (&iter);
+
+  *begin = iter;
+  *end = iter;
+
+  _ide_source_iter_forward_full_word_end (end);
+}
+
+static gboolean
+ide_hover_motion_timeout_cb (gpointer data)
+{
+  IdeHover *self = data;
+  IdeSourceView *view;
+  GdkRectangle rect;
+  GdkRectangle begin_rect;
+  GdkRectangle end_rect;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (IDE_IS_HOVER (self));
+
+  self->delay_display_source = 0;
+
+  if (!(view = dzl_signal_group_get_target (self->signals)))
+    return G_SOURCE_REMOVE;
+
+  /* Ignore signal if we're already processing */
+  if (self->state != IDE_HOVER_STATE_INITIAL)
+    return G_SOURCE_REMOVE;
+
+  if (self->popover == NULL)
+    {
+      self->popover = g_object_new (IDE_TYPE_HOVER_POPOVER,
+                                    "modal", FALSE,
+                                    "position", GTK_POS_TOP,
+                                    "relative-to", view,
+                                    NULL);
+
+      g_signal_connect_object (self->popover,
+                               "destroy",
+                               G_CALLBACK (ide_hover_popover_destroy_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+
+      g_signal_connect_object (self->popover,
+                               "closed",
+                               G_CALLBACK (ide_hover_popover_closed_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+
+      g_signal_connect_object (self->popover,
+                               "enter-notify-event",
+                               G_CALLBACK (ide_hover_popover_enter_notify_event_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+
+      g_signal_connect_object (self->popover,
+                               "leave-notify-event",
+                               G_CALLBACK (ide_hover_popover_leave_notify_event_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+
+      if (self->providers != NULL)
+        ide_extension_set_adapter_foreach (self->providers,
+                                           ide_hover_providers_foreach_cb,
+                                           self->popover);
+    }
+
+  self->state = IDE_HOVER_STATE_DISPLAY;
+
+  ide_hover_get_bounds (self, &begin, &end);
+  gtk_text_view_get_iter_location (GTK_TEXT_VIEW (view), &begin, &begin_rect);
+  gtk_text_view_get_iter_location (GTK_TEXT_VIEW (view), &end, &end_rect);
+  gdk_rectangle_union (&begin_rect, &end_rect, &rect);
+
+  gtk_text_view_buffer_to_window_coords (GTK_TEXT_VIEW (view),
+                                         GTK_TEXT_WINDOW_WIDGET,
+                                         rect.x, rect.y, &rect.x, &rect.y);
+
+  if (gtk_text_iter_equal (&begin, &end) &&
+      gtk_text_iter_starts_line (&begin))
+    {
+      rect.width = 1;
+      gtk_popover_set_pointing_to (GTK_POPOVER (self->popover), &rect);
+      gtk_popover_set_position (GTK_POPOVER (self->popover), GTK_POS_RIGHT);
+    }
+  else
+    {
+      gtk_popover_set_pointing_to (GTK_POPOVER (self->popover), &rect);
+      gtk_popover_set_position (GTK_POPOVER (self->popover), GTK_POS_TOP);
+    }
+
+  _ide_hover_popover_show (self->popover);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_hover_delay_display (IdeHover *self)
+{
+  g_assert (IDE_IS_HOVER (self));
+
+  if (self->delay_display_source)
+    g_source_remove (self->delay_display_source);
+
+  self->delay_display_source =
+    gdk_threads_add_timeout_full (G_PRIORITY_LOW,
+                                  MOTION_SETTLE_TIMEOUT_MSEC,
+                                  ide_hover_motion_timeout_cb,
+                                  self, NULL);
+}
+
+static inline gboolean
+should_ignore_event (IdeSourceView  *view,
+                     GdkWindow      *event_window)
+{
+  GdkWindow *window;
+
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  window = gtk_text_view_get_window (GTK_TEXT_VIEW (view), GTK_TEXT_WINDOW_WIDGET);
+  return window != event_window;
+}
+
+static gboolean
+ide_hover_key_press_event_cb (IdeHover          *self,
+                              const GdkEventKey *event,
+                              IdeSourceView     *view)
+{
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (event != NULL);
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  if (self->popover != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->popover));
+
+  dzl_clear_source (&self->delay_display_source);
+  dzl_clear_source (&self->dismiss_source);
+
+  g_assert (self->popover == NULL);
+  g_assert (self->state == IDE_HOVER_STATE_INITIAL);
+  g_assert (self->delay_display_source == 0);
+  g_assert (self->dismiss_source == 0);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+ide_hover_enter_notify_event_cb (IdeHover               *self,
+                                 const GdkEventCrossing *event,
+                                 IdeSourceView          *view)
+{
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (event != NULL);
+  g_assert (event->type == GDK_ENTER_NOTIFY);
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  if (should_ignore_event (view, event->window))
+    return GDK_EVENT_PROPAGATE;
+
+  dzl_clear_source (&self->dismiss_source);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+ide_hover_dismiss_cb (gpointer data)
+{
+  IdeHover *self = data;
+
+  g_assert (IDE_IS_HOVER (self));
+
+  self->dismiss_source = 0;
+
+  switch (self->state)
+    {
+    case IDE_HOVER_STATE_DISPLAY:
+      g_assert (IDE_IS_HOVER_POPOVER (self->popover));
+
+      _ide_hover_popover_hide (self->popover);
+
+      g_assert (self->state == IDE_HOVER_STATE_INITIAL);
+      g_assert (self->popover == NULL);
+
+      break;
+
+    case IDE_HOVER_STATE_INITIAL:
+    case IDE_HOVER_STATE_IN_POPOVER:
+    default:
+      dzl_clear_source (&self->delay_display_source);
+      break;
+    }
+
+  return G_SOURCE_REMOVE;
+}
+
+static gboolean
+ide_hover_leave_notify_event_cb (IdeHover               *self,
+                                 const GdkEventCrossing *event,
+                                 IdeSourceView          *view)
+{
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (event != NULL);
+  g_assert (event->type == GDK_LEAVE_NOTIFY);
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  if (should_ignore_event (view, event->window))
+    return GDK_EVENT_PROPAGATE;
+
+  if (self->dismiss_source)
+    g_source_remove (self->dismiss_source);
+
+  /*
+   * Give ourselves just enough time to get the crossing event
+   * into the popover before we try to dismiss the popover.
+   */
+  self->dismiss_source =
+    gdk_threads_add_timeout_full (G_PRIORITY_HIGH,
+                                  1,
+                                  ide_hover_dismiss_cb,
+                                  self, NULL);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+ide_hover_motion_notify_event_cb (IdeHover             *self,
+                                  const GdkEventMotion *event,
+                                  IdeSourceView        *view)
+{
+  GdkWindow *window;
+  gint width;
+
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (event != NULL);
+  g_assert (event->type == GDK_MOTION_NOTIFY);
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  window = gtk_text_view_get_window (GTK_TEXT_VIEW (view), GTK_TEXT_WINDOW_LEFT);
+  width = gdk_window_get_width (window);
+
+  self->motion_x = event->x + width;
+  self->motion_y = event->y;
+
+  /*
+   * If we have a popover displayed, get it's allocation so that
+   * we can detect if our x/y coordinate is outside the threshold
+   * of the rectangle + grace area. If so, we'll dismiss the popover
+   * immediately.
+   */
+
+  if (self->popover != NULL)
+    {
+      GtkAllocation alloc;
+      GdkRectangle pointing_to;
+
+      gtk_widget_get_allocation (GTK_WIDGET (self->popover), &alloc);
+      gtk_widget_translate_coordinates (GTK_WIDGET (self->popover),
+                                        GTK_WIDGET (view),
+                                        alloc.x, alloc.y,
+                                        &alloc.x, &alloc.y);
+      gtk_popover_get_pointing_to (GTK_POPOVER (self->popover), &pointing_to);
+
+      alloc.x -= GRACE_X;
+      alloc.width += GRACE_X * 2;
+      alloc.y -= GRACE_Y;
+      alloc.height += GRACE_Y * 2;
+
+      gdk_rectangle_union (&alloc, &pointing_to, &alloc);
+
+      if (event->x < alloc.x ||
+          event->x > (alloc.x + alloc.width) ||
+          event->y < alloc.y ||
+          event->y > (alloc.y + alloc.height))
+        {
+          _ide_hover_popover_hide (self->popover);
+
+          g_assert (self->popover == NULL);
+          g_assert (self->state == IDE_HOVER_STATE_INITIAL);
+        }
+    }
+
+  dzl_clear_source (&self->dismiss_source);
+
+  ide_hover_delay_display (self);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+ide_hover_destroy_cb (IdeHover      *self,
+                      IdeSourceView *view)
+{
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+  g_assert (!self->popover || IDE_IS_HOVER_POPOVER (self->popover));
+
+  dzl_clear_source (&self->delay_display_source);
+
+  if (self->popover != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->popover));
+
+  g_assert (self->popover == NULL);
+  g_assert (self->delay_display_source == 0);
+}
+
+static void
+ide_hover_dispose (GObject *object)
+{
+  IdeHover *self = (IdeHover *)object;
+
+  g_clear_object (&self->providers);
+
+  dzl_clear_source (&self->delay_display_source);
+  dzl_clear_source (&self->dismiss_source);
+  dzl_signal_group_set_target (self->signals, NULL);
+
+  if (self->popover != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->popover));
+
+  G_OBJECT_CLASS (ide_hover_parent_class)->dispose (object);
+
+  g_assert (self->popover == NULL);
+  g_assert (self->delay_display_source == 0);
+  g_assert (self->dismiss_source == 0);
+}
+
+static void
+ide_hover_finalize (GObject *object)
+{
+  IdeHover *self = (IdeHover *)object;
+
+  g_clear_object (&self->signals);
+
+  g_assert (self->signals == NULL);
+  g_assert (self->popover == NULL);
+  g_assert (self->providers == NULL);
+
+  g_assert (self->delay_display_source == 0);
+  g_assert (self->dismiss_source == 0);
+
+  G_OBJECT_CLASS (ide_hover_parent_class)->finalize (object);
+}
+
+static void
+ide_hover_class_init (IdeHoverClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_hover_dispose;
+  object_class->finalize = ide_hover_finalize;
+}
+
+static void
+ide_hover_init (IdeHover *self)
+{
+  self->signals = dzl_signal_group_new (IDE_TYPE_SOURCE_VIEW);
+
+  dzl_signal_group_connect_object (self->signals,
+                                   "key-press-event",
+                                   G_CALLBACK (ide_hover_key_press_event_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->signals,
+                                   "enter-notify-event",
+                                   G_CALLBACK (ide_hover_enter_notify_event_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->signals,
+                                   "leave-notify-event",
+                                   G_CALLBACK (ide_hover_leave_notify_event_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->signals,
+                                   "motion-notify-event",
+                                   G_CALLBACK (ide_hover_motion_notify_event_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->signals,
+                                   "destroy",
+                                   G_CALLBACK (ide_hover_destroy_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+}
+
+IdeHover *
+_ide_hover_new (IdeSourceView *view)
+{
+  IdeHover *self;
+
+  self = g_object_new (IDE_TYPE_HOVER, NULL);
+  dzl_signal_group_set_target (self->signals, view);
+
+  return self;
+}
+
+void
+_ide_hover_set_context (IdeHover   *self,
+                        IdeContext *context)
+{
+  g_return_if_fail (IDE_IS_HOVER (self));
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+
+  if (self->providers != NULL)
+    return;
+
+  self->providers = ide_extension_set_adapter_new (context,
+                                                   peas_engine_get_default (),
+                                                   IDE_TYPE_HOVER_PROVIDER,
+                                                   "Hover-Provider-Languages",
+                                                   NULL);
+}
+
+void
+_ide_hover_set_language (IdeHover    *self,
+                         const gchar *language)
+{
+  g_return_if_fail (IDE_IS_HOVER (self));
+
+  if (self->providers != NULL)
+    ide_extension_set_adapter_set_value (self->providers, language);
+}
diff --git a/src/libide/hover/ide-marked-content.c b/src/libide/hover/ide-marked-content.c
new file mode 100644
index 000000000..0dd7d267e
--- /dev/null
+++ b/src/libide/hover/ide-marked-content.c
@@ -0,0 +1,230 @@
+/* ide-marked-content.c
+ *
+ * Copyright 2018 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"
+
+#define G_LOG_DOMAIN "ide-marked-content"
+
+#include "ide-marked-content.h"
+
+#define IDE_MARKED_CONTENT_MAGIC 0x81124633
+
+struct _IdeMarkedContent
+{
+  guint          magic;
+  IdeMarkedKind  kind;
+  GBytes        *data;
+  volatile gint  ref_count;
+};
+
+G_DEFINE_BOXED_TYPE (IdeMarkedContent,
+                     ide_marked_content,
+                     ide_marked_content_ref,
+                     ide_marked_content_unref)
+
+/**
+ * ide_marked_content_new:
+ * @content: a #GBytes containing the markup
+ * @kind: an #IdeMakredKind describing the markup kind
+ *
+ * Creates a new #IdeMarkedContent using the bytes provided.
+ *
+ * Returns: (transfer full): an #IdeMarkedContent
+ */
+IdeMarkedContent *
+ide_marked_content_new (GBytes        *content,
+                        IdeMarkedKind  kind)
+{
+  IdeMarkedContent *self;
+
+  g_return_val_if_fail (content != NULL, NULL);
+
+  self = g_slice_new0 (IdeMarkedContent);
+  self->magic = IDE_MARKED_CONTENT_MAGIC;
+  self->ref_count = 1;
+  self->data = g_bytes_ref (content);
+  self->kind = kind;
+
+  return self;
+}
+
+/**
+ * ide_marked_content_new_plaintext:
+ * @plaintext: (nullable): a string containing the plaintext
+ *
+ * Creates a new #IdeMarkedContent of type %IDE_MARKED_KIND_PLAINTEXT
+ * with the contents of @string.
+ *
+ * Returns: (transfer full): an #IdeMarkedContent
+ *
+ * Since: 3.30
+ */
+IdeMarkedContent *
+ide_marked_content_new_plaintext (const gchar *plaintext)
+{
+  if (plaintext == NULL)
+    plaintext = "";
+
+  return ide_marked_content_new_from_data (plaintext, -1, IDE_MARKED_KIND_PLAINTEXT);
+}
+
+/**
+ * ide_marked_content_new_from_data:
+ * @data: the data for the content
+ * @len: the length of the data, or -1 to strlen() @data
+ * @kind: the kind of markup
+ *
+ * Creates a new #IdeMarkedContent from the provided data.
+ *
+ * Returns: (transfer full): an #IdeMarkedContent
+ *
+ * Since: 3.30
+ */
+IdeMarkedContent *
+ide_marked_content_new_from_data (const gchar   *data,
+                                  gssize         len,
+                                  IdeMarkedKind  kind)
+{
+  g_autoptr(GBytes) bytes = NULL;
+
+  if (len < 0)
+    len = strlen (data);
+
+  bytes = g_bytes_new (data, len);
+
+  return ide_marked_content_new (bytes, kind);
+}
+
+/**
+ * ide_marked_content_ref:
+ * @self: an #IdeMarkedContent
+ *
+ * Increments the reference count of @self by one.
+ *
+ * When a #IdeMarkedContent reaches a reference count of zero, by using
+ * ide_marked_content_unref(), it will be freed.
+ *
+ * Returns: (transfer full): @self with the reference count incremented
+ *
+ * Since: 3.30
+ */
+IdeMarkedContent *
+ide_marked_content_ref (IdeMarkedContent *self)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+  g_return_val_if_fail (self->magic == IDE_MARKED_CONTENT_MAGIC, NULL);
+  g_return_val_if_fail (self->ref_count > 0, NULL);
+
+  g_atomic_int_inc (&self->ref_count);
+
+  return self;
+}
+
+/**
+ * ide_marked_content_unref:
+ * @self: an #IdeMarkedContent
+ *
+ * Decrements the reference count of @self by one.
+ *
+ * When the reference count of @self reaches zero, it will be freed.
+ *
+ * Since: 3.30
+ */
+void
+ide_marked_content_unref (IdeMarkedContent *self)
+{
+  g_return_if_fail (self != NULL);
+  g_return_if_fail (self->magic == IDE_MARKED_CONTENT_MAGIC);
+  g_return_if_fail (self->ref_count > 0);
+
+  if (g_atomic_int_dec_and_test (&self->ref_count))
+    {
+      self->magic = 0;
+      self->kind = 0;
+      g_clear_pointer (&self->data, g_bytes_unref);
+      g_slice_free (IdeMarkedContent, self);
+    }
+}
+
+/**
+ * ide_marked_content_get_kind:
+ * @self: an #IdeMarkedContent
+ *
+ * Gets the kind of markup that @self contains.
+ *
+ * This is used to display the content appropriately.
+ *
+ * Returns:
+ */
+IdeMarkedKind
+ide_marked_content_get_kind (IdeMarkedContent *self)
+{
+  g_return_val_if_fail (self != NULL, 0);
+  g_return_val_if_fail (self->magic == IDE_MARKED_CONTENT_MAGIC, 0);
+  g_return_val_if_fail (self->ref_count > 0, 0);
+
+  return self->kind;
+}
+
+/**
+ * ide_marked_content_get_bytes:
+ *
+ * Gets the bytes for the marked content.
+ *
+ * Returns: (transfer none): a #GBytes
+ */
+GBytes *
+ide_marked_content_get_bytes (IdeMarkedContent *self)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+  g_return_val_if_fail (self->magic == IDE_MARKED_CONTENT_MAGIC, NULL);
+  g_return_val_if_fail (self->ref_count > 0, NULL);
+
+  return self->data;
+}
+
+/**
+ * ide_marked_content_as_string:
+ * @self: a #IdeMarkedContent
+ *
+ * Gets the contents of the marked content as a newly allcoated C string.
+ *
+ * Returns: (nullable): a newly allocated string or %NULL
+ *
+ * Since: 3.30
+ */
+gchar *
+ide_marked_content_as_string (IdeMarkedContent *self)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+  g_return_val_if_fail (self->magic == IDE_MARKED_CONTENT_MAGIC, NULL);
+  g_return_val_if_fail (self->ref_count > 0, NULL);
+
+  if (self->data != NULL)
+    {
+      const gchar *buf;
+      gsize len;
+
+      if ((buf = g_bytes_get_data (self->data, &len)))
+        return g_strndup (buf, len);
+    }
+
+  return NULL;
+}
diff --git a/src/libide/hover/ide-marked-content.h b/src/libide/hover/ide-marked-content.h
new file mode 100644
index 000000000..f002a5461
--- /dev/null
+++ b/src/libide/hover/ide-marked-content.h
@@ -0,0 +1,65 @@
+/* ide-marked-content.h
+ *
+ * Copyright 2018 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 <glib-object.h>
+
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_MARKED_CONTENT (ide_marked_content_get_type())
+
+typedef struct _IdeMarkedContent IdeMarkedContent;
+
+typedef enum
+{
+  IDE_MARKED_KIND_PLAINTEXT = 0,
+  IDE_MARKED_KIND_MARKDOWN  = 1,
+  IDE_MARKED_KIND_HTML      = 2,
+  IDE_MARKED_KIND_PANGO     = 3,
+} IdeMarkedKind;
+
+IDE_AVAILABLE_IN_3_30
+GType             ide_marked_content_get_type      (void);
+IDE_AVAILABLE_IN_3_30
+IdeMarkedContent *ide_marked_content_new           (GBytes           *content,
+                                                    IdeMarkedKind     kind);
+IDE_AVAILABLE_IN_3_30
+IdeMarkedContent *ide_marked_content_new_plaintext (const gchar      *plaintext);
+IDE_AVAILABLE_IN_3_30
+IdeMarkedContent *ide_marked_content_new_from_data (const gchar      *data,
+                                                    gssize            len,
+                                                    IdeMarkedKind     kind);
+IDE_AVAILABLE_IN_3_30
+GBytes           *ide_marked_content_get_bytes     (IdeMarkedContent *self);
+IDE_AVAILABLE_IN_3_30
+IdeMarkedKind     ide_marked_content_get_kind      (IdeMarkedContent *self);
+IDE_AVAILABLE_IN_3_30
+gchar            *ide_marked_content_as_string     (IdeMarkedContent *self);
+IDE_AVAILABLE_IN_3_30
+IdeMarkedContent *ide_marked_content_ref           (IdeMarkedContent *self);
+IDE_AVAILABLE_IN_3_30
+void              ide_marked_content_unref         (IdeMarkedContent *self);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeMarkedContent, ide_marked_content_unref)
+
+G_END_DECLS
diff --git a/src/libide/hover/ide-marked-view.c b/src/libide/hover/ide-marked-view.c
new file mode 100644
index 000000000..d7fb68cbd
--- /dev/null
+++ b/src/libide/hover/ide-marked-view.c
@@ -0,0 +1,116 @@
+/* ide-marked-view.c
+ *
+ * Copyright 2018 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#define G_LOG_DOMAIN "ide-marked-view"
+
+#include <dazzle.h>
+
+#if ENABLE_WEBKIT
+# include <webkit2/webkit2.h>
+#endif
+
+#include "hover/ide-marked-view.h"
+#include "util/gs-markdown.h"
+
+struct _IdeMarkedView
+{
+  GtkBin parent_instance;
+};
+
+G_DEFINE_TYPE (IdeMarkedView, ide_marked_view, GTK_TYPE_BIN)
+
+static void
+ide_marked_view_class_init (IdeMarkedViewClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_css_name (widget_class, "markedview");
+}
+
+static void
+ide_marked_view_init (IdeMarkedView *self)
+{
+}
+
+GtkWidget *
+ide_marked_view_new (IdeMarkedContent *content)
+{
+  g_autofree gchar *markup = NULL;
+  GtkWidget *child = NULL;
+  IdeMarkedView *self;
+  IdeMarkedKind kind;
+
+  g_return_val_if_fail (content != NULL, NULL);
+
+  self = g_object_new (IDE_TYPE_MARKED_VIEW, NULL);
+  kind = ide_marked_content_get_kind (content);
+  markup = ide_marked_content_as_string (content);
+
+  switch (kind)
+    {
+    default:
+    case IDE_MARKED_KIND_PLAINTEXT:
+    case IDE_MARKED_KIND_PANGO:
+      child = g_object_new (GTK_TYPE_LABEL,
+                            "max-width-chars", 80,
+                            "wrap", TRUE,
+                            "xalign", 0.0f,
+                            "visible", TRUE,
+                            "use-markup", kind == IDE_MARKED_KIND_PANGO,
+                            "label", markup,
+                            NULL);
+      break;
+
+    case IDE_MARKED_KIND_HTML:
+#if ENABLE_WEBKIT
+      child = g_object_new (WEBKIT_TYPE_WEB_VIEW,
+                            "visible", TRUE,
+                            NULL);
+      webkit_web_view_load_html (WEBKIT_WEB_VIEW (child), markup, NULL);
+#endif
+      break;
+
+    case IDE_MARKED_KIND_MARKDOWN:
+      {
+        g_autoptr(GsMarkdown) md = gs_markdown_new (GS_MARKDOWN_OUTPUT_PANGO);
+        g_autofree gchar *parsed = NULL;
+
+        gs_markdown_set_smart_quoting (md, TRUE);
+        gs_markdown_set_autocode (md, TRUE);
+        gs_markdown_set_autolinkify (md, TRUE);
+
+        if ((parsed = gs_markdown_parse (md, markup)))
+          child = g_object_new (GTK_TYPE_LABEL,
+                                "max-width-chars", 80,
+                                "wrap", TRUE,
+                                "xalign", 0.0f,
+                                "visible", TRUE,
+                                "use-markup", TRUE,
+                                "label", parsed,
+                                NULL);
+      }
+      break;
+    }
+
+  if (child != NULL)
+    gtk_container_add (GTK_CONTAINER (self), child);
+
+  return GTK_WIDGET (self);
+}
diff --git a/src/libide/hover/ide-marked-view.h b/src/libide/hover/ide-marked-view.h
new file mode 100644
index 000000000..836df12d8
--- /dev/null
+++ b/src/libide/hover/ide-marked-view.h
@@ -0,0 +1,37 @@
+/* ide-marked-view.h
+ *
+ * Copyright 2018 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "ide-version-macros.h"
+
+#include "hover/ide-marked-content.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_MARKED_VIEW (ide_marked_view_get_type())
+
+IDE_AVAILABLE_IN_3_30
+G_DECLARE_FINAL_TYPE (IdeMarkedView, ide_marked_view, IDE, MARKED_VIEW, GtkBin)
+
+IDE_AVAILABLE_IN_3_30
+GtkWidget *ide_marked_view_new (IdeMarkedContent *content);
+
+G_END_DECLS
diff --git a/src/libide/hover/meson.build b/src/libide/hover/meson.build
new file mode 100644
index 000000000..ecffdbfff
--- /dev/null
+++ b/src/libide/hover/meson.build
@@ -0,0 +1,29 @@
+hover_headers = [
+  'ide-hover-context.h',
+  'ide-hover-provider.h',
+  'ide-marked-content.h',
+  'ide-marked-view.h',
+]
+
+hover_sources = [
+  'ide-hover-context.c',
+  'ide-hover-provider.c',
+  'ide-marked-content.c',
+  'ide-marked-view.c',
+]
+
+hover_private_sources = [
+  'ide-hover.c',
+  'ide-hover-popover.c',
+]
+
+hover_enums = [
+  'ide-marked-content.h',
+]
+
+libide_public_headers += files(hover_headers)
+libide_public_sources += files(hover_sources)
+libide_private_sources += files(hover_private_sources)
+libide_enum_headers += files(hover_enums)
+
+install_headers(hover_headers, subdir: join_paths(libide_header_subdir, 'hover'))
diff --git a/src/libide/ide-enums.c.in b/src/libide/ide-enums.c.in
index c03c5c2be..ac8d1a091 100644
--- a/src/libide/ide-enums.c.in
+++ b/src/libide/ide-enums.c.in
@@ -16,6 +16,7 @@
 #include "files/ide-indent-style.h"
 #include "files/ide-spaces-style.h"
 #include "highlighting/ide-highlighter.h"
+#include "hover/ide-marked-content.h"
 #include "langserv/ide-langserv-types.h"
 #include "runtimes/ide-runtime.h"
 #include "sourceview/ide-source-view.h"
diff --git a/src/libide/ide.h b/src/libide/ide.h
index 9ff3a0ac5..3c8013c93 100644
--- a/src/libide/ide.h
+++ b/src/libide/ide.h
@@ -118,6 +118,10 @@ G_BEGIN_DECLS
 #include "highlighting/ide-highlight-engine.h"
 #include "highlighting/ide-highlight-index.h"
 #include "highlighting/ide-highlighter.h"
+#include "hover/ide-hover-context.h"
+#include "hover/ide-hover-provider.h"
+#include "hover/ide-marked-content.h"
+#include "hover/ide-marked-view.h"
 #include "langserv/ide-langserv-client.h"
 #include "langserv/ide-langserv-completion-item.h"
 #include "langserv/ide-langserv-completion-provider.h"
diff --git a/src/libide/libide.gresource.xml b/src/libide/libide.gresource.xml
index b1cb42045..72bb47c9a 100644
--- a/src/libide/libide.gresource.xml
+++ b/src/libide/libide.gresource.xml
@@ -41,6 +41,7 @@
     <file compressed="true" 
alias="shared/shared-debugger.css">../../data/themes/shared/shared-debugger.css</file>
     <file compressed="true" 
alias="shared/shared-editor.css">../../data/themes/shared/shared-editor.css</file>
     <file compressed="true" 
alias="shared/shared-greeter.css">../../data/themes/shared/shared-greeter.css</file>
+    <file compressed="true" 
alias="shared/shared-hoverer.css">../../data/themes/shared/shared-hoverer.css</file>
     <file compressed="true" 
alias="shared/shared-layout.css">../../data/themes/shared/shared-layout.css</file>
     <file compressed="true" 
alias="shared/shared-omnibar.css">../../data/themes/shared/shared-omnibar.css</file>
     <file compressed="true" 
alias="shared/shared-search.css">../../data/themes/shared/shared-search.css</file>
@@ -71,6 +72,7 @@
     <file preprocess="xml-stripblanks" 
alias="ide-debugger-breakpoints-view.ui">debugger/ide-debugger-breakpoints-view.ui</file>
     <file preprocess="xml-stripblanks" 
alias="ide-debugger-controls.ui">debugger/ide-debugger-controls.ui</file>
     <file preprocess="xml-stripblanks" 
alias="ide-debugger-disassembly-view.ui">debugger/ide-debugger-disassembly-view.ui</file>
+    <file preprocess="xml-stripblanks" 
alias="ide-debugger-hover-controls.ui">debugger/ide-debugger-hover-controls.ui</file>
     <file preprocess="xml-stripblanks" 
alias="ide-debugger-libraries-view.ui">debugger/ide-debugger-libraries-view.ui</file>
     <file preprocess="xml-stripblanks" 
alias="ide-debugger-locals-view.ui">debugger/ide-debugger-locals-view.ui</file>
     <file preprocess="xml-stripblanks" 
alias="ide-debugger-registers-view.ui">debugger/ide-debugger-registers-view.ui</file>
diff --git a/src/libide/meson.build b/src/libide/meson.build
index 6dcc0579f..1b114787d 100644
--- a/src/libide/meson.build
+++ b/src/libide/meson.build
@@ -69,6 +69,7 @@ subdir('genesis')
 subdir('greeter')
 subdir('gsettings')
 subdir('highlighting')
+subdir('hover')
 subdir('keybindings')
 subdir('langserv')
 subdir('layout')
diff --git a/src/libide/object-modules.h b/src/libide/object-modules.h
index 1e95d0772..900cbe419 100644
--- a/src/libide/object-modules.h
+++ b/src/libide/object-modules.h
@@ -18,6 +18,8 @@
 
 #pragma once
 
+#include "config.h"
+
 #include <libpeas/peas.h>
 
 #include "ide-version-macros.h"
@@ -30,6 +32,9 @@ _IDE_EXTERN void ide_debugger_register_types    (PeasObjectModule *module);
 _IDE_EXTERN void ide_directory_register_types   (PeasObjectModule *module);
 _IDE_EXTERN void ide_editor_register_types      (PeasObjectModule *module);
 _IDE_EXTERN void ide_test_register_types        (PeasObjectModule *module);
+
+#if ENABLE_WEBKIT
 _IDE_EXTERN void ide_webkit_register_types      (PeasObjectModule *module);
+#endif
 
 G_END_DECLS
diff --git a/src/libide/sourceview/ide-source-view.c b/src/libide/sourceview/ide-source-view.c
index 49377a56c..3896da61c 100644
--- a/src/libide/sourceview/ide-source-view.c
+++ b/src/libide/sourceview/ide-source-view.c
@@ -41,6 +41,7 @@
 #include "diagnostics/ide-source-range.h"
 #include "files/ide-file-settings.h"
 #include "files/ide-file.h"
+#include "hover/ide-hover-private.h"
 #include "plugins/ide-extension-adapter.h"
 #include "plugins/ide-extension-set-adapter.h"
 #include "rename/ide-rename-provider.h"
@@ -113,6 +114,7 @@ typedef struct
   IdeOmniGutterRenderer       *omni_renderer;
 
   IdeCompletion               *completion;
+  IdeHover                    *hover;
 
   DzlBindingGroup             *file_setting_bindings;
   DzlSignalGroup              *buffer_signals;
@@ -845,21 +847,21 @@ ide_source_view__buffer_notify_language_cb (IdeSourceView *self,
                                             IdeBuffer     *buffer)
 {
   IdeSourceViewPrivate *priv = ide_source_view_get_instance_private (self);
-  GtkSourceLanguage *language;
-  const gchar *lang_id = NULL;
+  const gchar *lang_id;
 
   g_assert (IDE_IS_SOURCE_VIEW (self));
   g_assert (IDE_IS_BUFFER (buffer));
 
-  if ((language = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (buffer))))
-    lang_id = gtk_source_language_get_id (language);
+  lang_id = ide_buffer_get_language_id (buffer);
 
-  /*
-   * Update the indenter, which is provided by a plugin.
-   */
+  /* Update the indenter, which is provided by a plugin. */
   if (priv->indenter_adapter != NULL)
     ide_extension_adapter_set_value (priv->indenter_adapter, lang_id);
   ide_source_view_update_auto_indent_override (self);
+
+  /* Reload hover providers by language */
+  _ide_hover_set_language (priv->hover, lang_id);
+
   g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_INDENTER]);
 }
 
@@ -1334,6 +1336,8 @@ ide_source_view_bind_buffer (IdeSourceView  *self,
 
   context = ide_buffer_get_context (buffer);
 
+  _ide_hover_set_context (priv->hover, context);
+
   priv->indenter_adapter = ide_extension_adapter_new (context,
                                                       peas_engine_get_default (),
                                                       IDE_TYPE_INDENTER,
@@ -2642,44 +2646,6 @@ cleanup:
   return ret;
 }
 
-static gboolean
-ide_source_view_query_tooltip (GtkWidget  *widget,
-                               gint        x,
-                               gint        y,
-                               gboolean    keyboard_mode,
-                               GtkTooltip *tooltip)
-{
-  IdeSourceView *self = (IdeSourceView *)widget;
-  IdeSourceViewPrivate *priv = ide_source_view_get_instance_private (self);
-  GtkTextView *text_view = (GtkTextView *)widget;
-
-  g_assert (IDE_IS_SOURCE_VIEW (self));
-  g_assert (GTK_IS_TEXT_VIEW (text_view));
-  g_assert (GTK_IS_TOOLTIP (tooltip));
-
-  if (priv->buffer != NULL)
-    {
-      IdeDiagnostic *diagnostic;
-      GtkTextIter iter;
-
-      gtk_text_view_window_to_buffer_coords (text_view, GTK_TEXT_WINDOW_WIDGET, x, y, &x, &y);
-      gtk_text_view_get_iter_at_location (text_view, &iter, x, y);
-      diagnostic = ide_buffer_get_diagnostic_at_iter (priv->buffer, &iter);
-
-      if (diagnostic)
-        {
-          g_autofree gchar *str = NULL;
-
-          str = ide_diagnostic_get_text_for_display (diagnostic);
-          gtk_tooltip_set_text (tooltip, str);
-
-          return TRUE;
-        }
-    }
-
-  return FALSE;
-}
-
 static void
 ide_source_view_real_add_cursor (IdeSourceView *self,
                                  IdeCursorType  type)
@@ -5396,6 +5362,7 @@ ide_source_view_dispose (GObject *object)
       priv->delay_size_allocate_chainup = 0;
     }
 
+  g_clear_object (&priv->hover);
   g_clear_object (&priv->completion);
   g_clear_object (&priv->capture);
   g_clear_object (&priv->indenter_adapter);
@@ -5618,7 +5585,6 @@ ide_source_view_class_init (IdeSourceViewClass *klass)
   widget_class->focus_out_event = ide_source_view_focus_out_event;
   widget_class->key_press_event = ide_source_view_key_press_event;
   widget_class->key_release_event = ide_source_view_key_release_event;
-  widget_class->query_tooltip = ide_source_view_query_tooltip;
   widget_class->scroll_event = ide_source_view_scroll_event;
   widget_class->size_allocate = ide_source_view_size_allocate;
   widget_class->style_updated = ide_source_view_real_style_updated;
@@ -6516,13 +6482,16 @@ ide_source_view_init (IdeSourceView *self)
 {
   IdeSourceViewPrivate *priv = ide_source_view_get_instance_private (self);
 
+  DZL_COUNTER_INC (instances);
+
+  gtk_widget_add_events (GTK_WIDGET (self), GDK_ENTER_NOTIFY_MASK);
+  gtk_widget_set_has_tooltip (GTK_WIDGET (self), FALSE);
+
   priv->include_regex = g_regex_new (INCLUDE_STATEMENTS,
                                      G_REGEX_OPTIMIZE,
                                      0,
                                      NULL);
 
-  DZL_COUNTER_INC (instances);
-
   priv->target_line_column = 0;
   priv->snippets = g_queue_new ();
   priv->selections = g_queue_new ();
@@ -6530,6 +6499,8 @@ ide_source_view_init (IdeSourceView *self)
   priv->command_str = g_string_sized_new (32);
   priv->overscroll_num_lines = DEFAULT_OVERSCROLL_NUM_LINES;
 
+  priv->hover = _ide_hover_new (self);
+
   priv->file_setting_bindings = dzl_binding_group_new ();
   dzl_binding_group_bind (priv->file_setting_bindings, "auto-indent",
                           self, "auto-indent", G_BINDING_SYNC_CREATE);
diff --git a/src/libide/util/gs-markdown.c b/src/libide/util/gs-markdown.c
new file mode 100644
index 000000000..6ce956653
--- /dev/null
+++ b/src/libide/util/gs-markdown.c
@@ -0,0 +1,870 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Richard Hughes <richard hughsie com>
+ * Copyright (C) 2015 Kalev Lember <klember redhat com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <glib.h>
+
+#include "gs-markdown.h"
+
+/*******************************************************************************
+ *
+ * This is a simple Markdown parser.
+ * It can output to Pango, HTML or plain text. The following limitations are
+ * already known, and properly deliberate:
+ *
+ * - No code section support
+ * - No ordered list support
+ * - No blockquote section support
+ * - No image support
+ * - No links or email support
+ * - No backslash escapes support
+ * - No HTML escaping support
+ * - Auto-escapes certain word patterns, like http://
+ *
+ * It does support the rest of the standard pretty well, although it's not
+ * been run against any conformance tests. The parsing is single pass, with
+ * a simple enumerated interpretor mode and a single line back-memory.
+ *
+ ******************************************************************************/
+
+typedef enum {
+       GS_MARKDOWN_MODE_BLANK,
+       GS_MARKDOWN_MODE_RULE,
+       GS_MARKDOWN_MODE_BULLETT,
+       GS_MARKDOWN_MODE_PARA,
+       GS_MARKDOWN_MODE_H1,
+       GS_MARKDOWN_MODE_H2,
+       GS_MARKDOWN_MODE_UNKNOWN
+} GsMarkdownMode;
+
+typedef struct {
+       const gchar *em_start;
+       const gchar *em_end;
+       const gchar *strong_start;
+       const gchar *strong_end;
+       const gchar *code_start;
+       const gchar *code_end;
+       const gchar *h1_start;
+       const gchar *h1_end;
+       const gchar *h2_start;
+       const gchar *h2_end;
+       const gchar *bullet_start;
+       const gchar *bullet_end;
+       const gchar *rule;
+} GsMarkdownTags;
+
+struct _GsMarkdown {
+       GObject                  parent_instance;
+
+       GsMarkdownMode           mode;
+       GsMarkdownTags           tags;
+       GsMarkdownOutputKind     output;
+       gint                     max_lines;
+       gint                     line_count;
+       gboolean                 smart_quoting;
+       gboolean                 escape;
+       gboolean                 autocode;
+       gboolean                 autolinkify;
+       GString                 *pending;
+       GString                 *processed;
+};
+
+G_DEFINE_TYPE (GsMarkdown, gs_markdown, G_TYPE_OBJECT)
+
+/*
+ * gs_markdown_to_text_line_is_rule:
+ *
+ * Horizontal rules are created by placing three or more hyphens, asterisks,
+ * or underscores on a line by themselves.
+ * You may use spaces between the hyphens or asterisks.
+ **/
+static gboolean
+gs_markdown_to_text_line_is_rule (const gchar *line)
+{
+       guint i;
+       guint len;
+       guint count = 0;
+       g_autofree gchar *copy = NULL;
+
+       len = (guint) strlen (line);
+       if (len == 0)
+               return FALSE;
+
+       /* replace non-rule chars with ~ */
+       copy = g_strdup (line);
+       g_strcanon (copy, "-*_ ", '~');
+       for (i = 0; i < len; i++) {
+               if (copy[i] == '~')
+                       return FALSE;
+               if (copy[i] != ' ')
+                       count++;
+       }
+
+       /* if we matched, return true */
+       if (count >= 3)
+               return TRUE;
+       return FALSE;
+}
+
+static gboolean
+gs_markdown_to_text_line_is_bullet (const gchar *line)
+{
+       return (g_str_has_prefix (line, "- ") ||
+               g_str_has_prefix (line, "* ") ||
+               g_str_has_prefix (line, "+ ") ||
+               g_str_has_prefix (line, " - ") ||
+               g_str_has_prefix (line, " * ") ||
+               g_str_has_prefix (line, " + "));
+}
+
+static gboolean
+gs_markdown_to_text_line_is_header1 (const gchar *line)
+{
+       return g_str_has_prefix (line, "# ");
+}
+
+static gboolean
+gs_markdown_to_text_line_is_header2 (const gchar *line)
+{
+       return g_str_has_prefix (line, "## ");
+}
+
+static gboolean
+gs_markdown_to_text_line_is_header1_type2 (const gchar *line)
+{
+       return g_str_has_prefix (line, "===");
+}
+
+static gboolean
+gs_markdown_to_text_line_is_header2_type2 (const gchar *line)
+{
+       return g_str_has_prefix (line, "---");
+}
+
+#if 0
+static gboolean
+gs_markdown_to_text_line_is_code (const gchar *line)
+{
+       return (g_str_has_prefix (line, "    ") ||
+               g_str_has_prefix (line, "\t"));
+}
+
+static gboolean
+gs_markdown_to_text_line_is_blockquote (const gchar *line)
+{
+       return (g_str_has_prefix (line, "> "));
+}
+#endif
+
+static gboolean
+gs_markdown_to_text_line_is_blank (const gchar *line)
+{
+       guint i;
+       guint len;
+
+       /* a line with no characters is blank by definition */
+       len = (guint) strlen (line);
+       if (len == 0)
+               return TRUE;
+
+       /* find if there are only space chars */
+       for (i = 0; i < len; i++) {
+               if (line[i] != ' ' && line[i] != '\t')
+                       return FALSE;
+       }
+
+       /* if we matched, return true */
+       return TRUE;
+}
+
+static gchar *
+gs_markdown_replace (const gchar *haystack,
+                    const gchar *needle,
+                    const gchar *replace)
+{
+       g_auto(GStrv) split = NULL;
+       split = g_strsplit (haystack, needle, -1);
+       return g_strjoinv (replace, split);
+}
+
+static gchar *
+gs_markdown_strstr_spaces (const gchar *haystack, const gchar *needle)
+{
+       gchar *found;
+       const gchar *haystack_new = haystack;
+
+retry:
+       /* don't find if surrounded by spaces */
+       found = strstr (haystack_new, needle);
+       if (found == NULL)
+               return NULL;
+
+       /* start of the string, always valid */
+       if (found == haystack)
+               return found;
+
+       /* end of the string, always valid */
+       if (*(found-1) == ' ' && *(found+1) == ' ') {
+               haystack_new = found+1;
+               goto retry;
+       }
+       return found;
+}
+
+static gchar *
+gs_markdown_to_text_line_formatter (const gchar *line,
+                                   const gchar *formatter,
+                                   const gchar *left,
+                                   const gchar *right)
+{
+       guint len;
+       gchar *str1;
+       gchar *str2;
+       gchar *start = NULL;
+       gchar *middle = NULL;
+       gchar *end = NULL;
+       g_autofree gchar *copy = NULL;
+
+       /* needed to know for shifts */
+       len = (guint) strlen (formatter);
+       if (len == 0)
+               return NULL;
+
+       /* find sections */
+       copy = g_strdup (line);
+       str1 = gs_markdown_strstr_spaces (copy, formatter);
+       if (str1 != NULL) {
+               *str1 = '\0';
+               str2 = gs_markdown_strstr_spaces (str1+len, formatter);
+               if (str2 != NULL) {
+                       *str2 = '\0';
+                       middle = str1 + len;
+                       start = copy;
+                       end = str2 + len;
+               }
+       }
+
+       /* if we found, replace and keep looking for the same string */
+       if (start != NULL && middle != NULL && end != NULL) {
+               g_autofree gchar *temp = NULL;
+               temp = g_strdup_printf ("%s%s%s%s%s", start, left, middle, right, end);
+               /* recursive */
+               return gs_markdown_to_text_line_formatter (temp, formatter, left, right);
+       }
+
+       /* not found, keep return as-is */
+       return g_strdup (line);
+}
+
+static gchar *
+gs_markdown_to_text_line_format_sections (GsMarkdown *self, const gchar *line)
+{
+       gchar *data = g_strdup (line);
+       gchar *temp;
+
+       /* bold1 */
+       temp = data;
+       data = gs_markdown_to_text_line_formatter (temp, "**",
+                                                  self->tags.strong_start,
+                                                  self->tags.strong_end);
+       g_free (temp);
+
+       /* bold2 */
+       temp = data;
+       data = gs_markdown_to_text_line_formatter (temp, "__",
+                                                  self->tags.strong_start,
+                                                  self->tags.strong_end);
+       g_free (temp);
+
+       /* italic1 */
+       temp = data;
+       data = gs_markdown_to_text_line_formatter (temp, "*",
+                                                  self->tags.em_start,
+                                                  self->tags.em_end);
+       g_free (temp);
+
+       /* italic2 */
+       temp = data;
+       data = gs_markdown_to_text_line_formatter (temp, "_",
+                                                  self->tags.em_start,
+                                                  self->tags.em_end);
+       g_free (temp);
+
+       /* em-dash */
+       temp = data;
+       data = gs_markdown_replace (temp, " -- ", " — ");
+       g_free (temp);
+
+       /* smart quoting */
+       if (self->smart_quoting) {
+               temp = data;
+               data = gs_markdown_to_text_line_formatter (temp, "\"", "“", "”");
+               g_free (temp);
+
+               temp = data;
+               data = gs_markdown_to_text_line_formatter (temp, "'", "‘", "’");
+               g_free (temp);
+       }
+
+       return data;
+}
+
+static gchar *
+gs_markdown_to_text_line_format (GsMarkdown *self, const gchar *line)
+{
+       GString *string;
+       gboolean mode = FALSE;
+       gchar *text;
+       guint i;
+       g_auto(GStrv) codes = NULL;
+
+       /* optimise the trivial case where we don't have any code tags */
+       text = strstr (line, "`");
+       if (text == NULL)
+               return gs_markdown_to_text_line_format_sections (self, line);
+
+       /* we want to parse the code sections without formatting */
+       codes = g_strsplit (line, "`", -1);
+       string = g_string_new ("");
+       for (i = 0; codes[i] != NULL; i++) {
+               if (!mode) {
+                       text = gs_markdown_to_text_line_format_sections (self, codes[i]);
+                       g_string_append (string, text);
+                       g_free (text);
+                       mode = TRUE;
+               } else {
+                       /* just append without formatting */
+                       g_string_append (string, self->tags.code_start);
+                       g_string_append (string, codes[i]);
+                       g_string_append (string, self->tags.code_end);
+                       mode = FALSE;
+               }
+       }
+       return g_string_free (string, FALSE);
+}
+
+static gboolean
+gs_markdown_add_pending (GsMarkdown *self, const gchar *line)
+{
+       g_autofree gchar *copy = NULL;
+
+       /* would put us over the limit */
+       if (self->max_lines > 0 && self->line_count >= self->max_lines)
+               return FALSE;
+
+       copy = g_strdup (line);
+
+       /* strip leading and trailing spaces */
+       g_strstrip (copy);
+
+       /* append */
+       g_string_append_printf (self->pending, "%s ", copy);
+       return TRUE;
+}
+
+static gboolean
+gs_markdown_add_pending_header (GsMarkdown *self, const gchar *line)
+{
+       g_autofree gchar *copy = NULL;
+
+       /* strip trailing # */
+       copy = g_strdup (line);
+       g_strdelimit (copy, "#", ' ');
+       return gs_markdown_add_pending (self, copy);
+}
+
+static guint
+gs_markdown_count_chars_in_word (const gchar *text, gchar find)
+{
+       guint i;
+       guint len;
+       guint count = 0;
+
+       /* get length */
+       len = (guint) strlen (text);
+       if (len == 0)
+               return 0;
+
+       /* find matching chars */
+       for (i = 0; i < len; i++) {
+               if (text[i] == find)
+                       count++;
+       }
+       return count;
+}
+
+static gboolean
+gs_markdown_word_is_code (const gchar *text)
+{
+       /* already code */
+       if (g_str_has_prefix (text, "`"))
+               return FALSE;
+       if (g_str_has_suffix (text, "`"))
+               return FALSE;
+
+       /* paths */
+       if (g_str_has_prefix (text, "/"))
+               return TRUE;
+
+       /* bugzillas */
+       if (g_str_has_prefix (text, "#"))
+               return TRUE;
+
+       /* patch files */
+       if (g_strrstr (text, ".patch") != NULL)
+               return TRUE;
+       if (g_strrstr (text, ".diff") != NULL)
+               return TRUE;
+
+       /* function names */
+       if (g_strrstr (text, "()") != NULL)
+               return TRUE;
+
+       /* email addresses */
+       if (g_strrstr (text, "@") != NULL)
+               return TRUE;
+
+       /* compiler defines */
+       if (text[0] != '_' &&
+           gs_markdown_count_chars_in_word (text, '_') > 1)
+               return TRUE;
+
+       /* nothing special */
+       return FALSE;
+}
+
+static gchar *
+gs_markdown_word_auto_format_code (const gchar *text)
+{
+       guint i;
+       gchar *temp;
+       gboolean ret = FALSE;
+       g_auto(GStrv) words = NULL;
+
+       /* split sentence up with space */
+       words = g_strsplit (text, " ", -1);
+
+       /* search each word */
+       for (i = 0; words[i] != NULL; i++) {
+               if (gs_markdown_word_is_code (words[i])) {
+                       temp = g_strdup_printf ("`%s`", words[i]);
+                       g_free (words[i]);
+                       words[i] = temp;
+                       ret = TRUE;
+               }
+       }
+
+       /* no replacements, so just return a copy */
+       if (!ret)
+               return g_strdup (text);
+
+       /* join the array back into a string */
+       return g_strjoinv (" ", words);
+}
+
+static gboolean
+gs_markdown_word_is_url (const gchar *text)
+{
+       if (g_str_has_prefix (text, "http://";))
+               return TRUE;
+       if (g_str_has_prefix (text, "https://";))
+               return TRUE;
+       if (g_str_has_prefix (text, "ftp://";))
+               return TRUE;
+       return FALSE;
+}
+
+static gchar *
+gs_markdown_word_auto_format_urls (const gchar *text)
+{
+       guint i;
+       gchar *temp;
+       gboolean ret = FALSE;
+       g_auto(GStrv) words = NULL;
+
+       /* split sentence up with space */
+       words = g_strsplit (text, " ", -1);
+
+       /* search each word */
+       for (i = 0; words[i] != NULL; i++) {
+               if (gs_markdown_word_is_url (words[i])) {
+                       temp = g_strdup_printf ("<a href=\"%s\">%s</a>",
+                                               words[i], words[i]);
+                       g_free (words[i]);
+                       words[i] = temp;
+                       ret = TRUE;
+               }
+       }
+
+       /* no replacements, so just return a copy */
+       if (!ret)
+               return g_strdup (text);
+
+       /* join the array back into a string */
+       return g_strjoinv (" ", words);
+}
+
+static void
+gs_markdown_flush_pending (GsMarkdown *self)
+{
+       g_autofree gchar *copy = NULL;
+       g_autofree gchar *temp = NULL;
+
+       /* no data yet */
+       if (self->mode == GS_MARKDOWN_MODE_UNKNOWN)
+               return;
+
+       /* remove trailing spaces */
+       while (g_str_has_suffix (self->pending->str, " "))
+               g_string_set_size (self->pending, self->pending->len - 1);
+
+       /* pango requires escaping */
+       copy = g_strdup (self->pending->str);
+       if (!self->escape && self->output == GS_MARKDOWN_OUTPUT_PANGO) {
+               g_strdelimit (copy, "<", '(');
+               g_strdelimit (copy, ">", ')');
+               g_strdelimit (copy, "&", '+');
+       }
+
+       /* check words for code */
+       if (self->autocode &&
+           (self->mode == GS_MARKDOWN_MODE_PARA ||
+            self->mode == GS_MARKDOWN_MODE_BULLETT)) {
+               temp = gs_markdown_word_auto_format_code (copy);
+               g_free (copy);
+               copy = temp;
+       }
+
+       /* escape */
+       if (self->escape) {
+               temp = g_markup_escape_text (copy, -1);
+               g_free (copy);
+               copy = temp;
+       }
+
+       /* check words for URLS */
+       if (self->autolinkify &&
+           self->output == GS_MARKDOWN_OUTPUT_PANGO &&
+           (self->mode == GS_MARKDOWN_MODE_PARA ||
+            self->mode == GS_MARKDOWN_MODE_BULLETT)) {
+               temp = gs_markdown_word_auto_format_urls (copy);
+               g_free (copy);
+               copy = temp;
+       }
+
+       /* do formatting */
+       temp = gs_markdown_to_text_line_format (self, copy);
+       if (self->mode == GS_MARKDOWN_MODE_BULLETT) {
+               g_string_append_printf (self->processed, "%s%s%s\n",
+                                       self->tags.bullet_start,
+                                       temp,
+                                       self->tags.bullet_end);
+               self->line_count++;
+       } else if (self->mode == GS_MARKDOWN_MODE_H1) {
+               g_string_append_printf (self->processed, "%s%s%s\n",
+                                       self->tags.h1_start,
+                                       temp,
+                                       self->tags.h1_end);
+       } else if (self->mode == GS_MARKDOWN_MODE_H2) {
+               g_string_append_printf (self->processed, "%s%s%s\n",
+                                       self->tags.h2_start,
+                                       temp,
+                                       self->tags.h2_end);
+       } else if (self->mode == GS_MARKDOWN_MODE_PARA ||
+                  self->mode == GS_MARKDOWN_MODE_RULE) {
+               g_string_append_printf (self->processed, "%s\n", temp);
+               self->line_count++;
+       }
+
+       /* clear */
+       g_string_truncate (self->pending, 0);
+}
+
+static gboolean
+gs_markdown_to_text_line_process (GsMarkdown *self, const gchar *line)
+{
+       gboolean ret;
+
+       /* blank */
+       ret = gs_markdown_to_text_line_is_blank (line);
+       if (ret) {
+               gs_markdown_flush_pending (self);
+               /* a new line after a list is the end of list, not a gap */
+               if (self->mode != GS_MARKDOWN_MODE_BULLETT)
+                       ret = gs_markdown_add_pending (self, "\n");
+               self->mode = GS_MARKDOWN_MODE_BLANK;
+               goto out;
+       }
+
+       /* header1_type2 */
+       ret = gs_markdown_to_text_line_is_header1_type2 (line);
+       if (ret) {
+               if (self->mode == GS_MARKDOWN_MODE_PARA)
+                       self->mode = GS_MARKDOWN_MODE_H1;
+               goto out;
+       }
+
+       /* header2_type2 */
+       ret = gs_markdown_to_text_line_is_header2_type2 (line);
+       if (ret) {
+               if (self->mode == GS_MARKDOWN_MODE_PARA)
+                       self->mode = GS_MARKDOWN_MODE_H2;
+               goto out;
+       }
+
+       /* rule */
+       ret = gs_markdown_to_text_line_is_rule (line);
+       if (ret) {
+               gs_markdown_flush_pending (self);
+               self->mode = GS_MARKDOWN_MODE_RULE;
+               ret = gs_markdown_add_pending (self, self->tags.rule);
+               goto out;
+       }
+
+       /* bullet */
+       ret = gs_markdown_to_text_line_is_bullet (line);
+       if (ret) {
+               gs_markdown_flush_pending (self);
+               self->mode = GS_MARKDOWN_MODE_BULLETT;
+               ret = gs_markdown_add_pending (self, &line[2]);
+               goto out;
+       }
+
+       /* header1 */
+       ret = gs_markdown_to_text_line_is_header1 (line);
+       if (ret) {
+               gs_markdown_flush_pending (self);
+               self->mode = GS_MARKDOWN_MODE_H1;
+               ret = gs_markdown_add_pending_header (self, &line[2]);
+               goto out;
+       }
+
+       /* header2 */
+       ret = gs_markdown_to_text_line_is_header2 (line);
+       if (ret) {
+               gs_markdown_flush_pending (self);
+               self->mode = GS_MARKDOWN_MODE_H2;
+               ret = gs_markdown_add_pending_header (self, &line[3]);
+               goto out;
+       }
+
+       /* paragraph */
+       if (self->mode == GS_MARKDOWN_MODE_BLANK ||
+           self->mode == GS_MARKDOWN_MODE_UNKNOWN) {
+               gs_markdown_flush_pending (self);
+               self->mode = GS_MARKDOWN_MODE_PARA;
+       }
+
+       /* add to pending */
+       ret = gs_markdown_add_pending (self, line);
+out:
+       /* if we failed to add, we don't know the mode */
+       if (!ret)
+               self->mode = GS_MARKDOWN_MODE_UNKNOWN;
+       return ret;
+}
+
+static void
+gs_markdown_set_output_kind (GsMarkdown *self, GsMarkdownOutputKind output)
+{
+       g_return_if_fail (GS_IS_MARKDOWN (self));
+
+       self->output = output;
+       switch (output) {
+       case GS_MARKDOWN_OUTPUT_PANGO:
+               /* PangoMarkup */
+               self->tags.em_start = "<i>";
+               self->tags.em_end = "</i>";
+               self->tags.strong_start = "<b>";
+               self->tags.strong_end = "</b>";
+               self->tags.code_start = "<tt>";
+               self->tags.code_end = "</tt>";
+               self->tags.h1_start = "<big>";
+               self->tags.h1_end = "</big>";
+               self->tags.h2_start = "<b>";
+               self->tags.h2_end = "</b>";
+               self->tags.bullet_start = "• ";
+               self->tags.bullet_end = "";
+               self->tags.rule = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯\n";
+               self->escape = TRUE;
+               self->autolinkify = TRUE;
+               break;
+       case GS_MARKDOWN_OUTPUT_HTML:
+               /* XHTML */
+               self->tags.em_start = "<em>";
+               self->tags.em_end = "<em>";
+               self->tags.strong_start = "<strong>";
+               self->tags.strong_end = "</strong>";
+               self->tags.code_start = "<code>";
+               self->tags.code_end = "</code>";
+               self->tags.h1_start = "<h1>";
+               self->tags.h1_end = "</h1>";
+               self->tags.h2_start = "<h2>";
+               self->tags.h2_end = "</h2>";
+               self->tags.bullet_start = "<li>";
+               self->tags.bullet_end = "</li>";
+               self->tags.rule = "<hr>";
+               self->escape = TRUE;
+               self->autolinkify = TRUE;
+               break;
+       case GS_MARKDOWN_OUTPUT_TEXT:
+               /* plain text */
+               self->tags.em_start = "";
+               self->tags.em_end = "";
+               self->tags.strong_start = "";
+               self->tags.strong_end = "";
+               self->tags.code_start = "";
+               self->tags.code_end = "";
+               self->tags.h1_start = "[";
+               self->tags.h1_end = "]";
+               self->tags.h2_start = "-";
+               self->tags.h2_end = "-";
+               self->tags.bullet_start = "* ";
+               self->tags.bullet_end = "";
+               self->tags.rule = " ----- \n";
+               self->escape = FALSE;
+               self->autolinkify = FALSE;
+               break;
+  case GS_MARKDOWN_OUTPUT_LAST:
+       default:
+               g_warning ("unknown output enum");
+               break;
+       }
+}
+
+void
+gs_markdown_set_max_lines (GsMarkdown *self, gint max_lines)
+{
+       g_return_if_fail (GS_IS_MARKDOWN (self));
+       self->max_lines = max_lines;
+}
+
+void
+gs_markdown_set_smart_quoting (GsMarkdown *self, gboolean smart_quoting)
+{
+       g_return_if_fail (GS_IS_MARKDOWN (self));
+       self->smart_quoting = smart_quoting;
+}
+
+void
+gs_markdown_set_escape (GsMarkdown *self, gboolean escape)
+{
+       g_return_if_fail (GS_IS_MARKDOWN (self));
+       self->escape = escape;
+}
+
+void
+gs_markdown_set_autocode (GsMarkdown *self, gboolean autocode)
+{
+       g_return_if_fail (GS_IS_MARKDOWN (self));
+       self->autocode = autocode;
+}
+
+void
+gs_markdown_set_autolinkify (GsMarkdown *self, gboolean autolinkify)
+{
+       g_return_if_fail (GS_IS_MARKDOWN (self));
+       self->autolinkify = autolinkify;
+}
+
+gchar *
+gs_markdown_parse (GsMarkdown *self, const gchar *markdown)
+{
+       gboolean ret;
+       gchar *temp;
+       guint i;
+       guint len;
+       g_auto(GStrv) lines = NULL;
+
+       g_return_val_if_fail (GS_IS_MARKDOWN (self), NULL);
+
+       /* process */
+       self->mode = GS_MARKDOWN_MODE_UNKNOWN;
+       self->line_count = 0;
+       g_string_truncate (self->pending, 0);
+       g_string_truncate (self->processed, 0);
+       lines = g_strsplit (markdown, "\n", -1);
+       len = g_strv_length (lines);
+
+       /* process each line */
+       for (i = 0; i < len; i++) {
+               ret = gs_markdown_to_text_line_process (self, lines[i]);
+               if (!ret)
+                       break;
+       }
+       gs_markdown_flush_pending (self);
+
+       /* remove trailing \n */
+       while (g_str_has_suffix (self->processed->str, "\n"))
+               g_string_set_size (self->processed, self->processed->len - 1);
+
+       /* get a copy */
+       temp = g_strdup (self->processed->str);
+       g_string_truncate (self->pending, 0);
+       g_string_truncate (self->processed, 0);
+       return temp;
+}
+
+static void
+gs_markdown_finalize (GObject *object)
+{
+       GsMarkdown *self;
+
+       g_return_if_fail (GS_IS_MARKDOWN (object));
+
+       self = GS_MARKDOWN (object);
+
+       g_string_free (self->pending, TRUE);
+       g_string_free (self->processed, TRUE);
+
+       G_OBJECT_CLASS (gs_markdown_parent_class)->finalize (object);
+}
+
+static void
+gs_markdown_class_init (GsMarkdownClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       object_class->finalize = gs_markdown_finalize;
+}
+
+static void
+gs_markdown_init (GsMarkdown *self)
+{
+       self->mode = GS_MARKDOWN_MODE_UNKNOWN;
+       self->pending = g_string_new ("");
+       self->processed = g_string_new ("");
+       self->max_lines = -1;
+       self->smart_quoting = FALSE;
+       self->escape = FALSE;
+       self->autocode = FALSE;
+}
+
+GsMarkdown *
+gs_markdown_new (GsMarkdownOutputKind output)
+{
+       GsMarkdown *self;
+       self = g_object_new (GS_TYPE_MARKDOWN, NULL);
+       gs_markdown_set_output_kind (self, output);
+       return GS_MARKDOWN (self);
+}
diff --git a/src/libide/util/gs-markdown.h b/src/libide/util/gs-markdown.h
new file mode 100644
index 000000000..833929db4
--- /dev/null
+++ b/src/libide/util/gs-markdown.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008-2013 Richard Hughes <richard hughsie com>
+ * Copyright (C) 2015 Kalev Lember <klember redhat com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __GS_MARKDOWN_H
+#define __GS_MARKDOWN_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_MARKDOWN (gs_markdown_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsMarkdown, gs_markdown, GS, MARKDOWN, GObject)
+
+typedef enum {
+       GS_MARKDOWN_OUTPUT_TEXT,
+       GS_MARKDOWN_OUTPUT_PANGO,
+       GS_MARKDOWN_OUTPUT_HTML,
+       GS_MARKDOWN_OUTPUT_LAST
+} GsMarkdownOutputKind;
+
+GsMarkdown     *gs_markdown_new                        (GsMarkdownOutputKind    output);
+void            gs_markdown_set_max_lines              (GsMarkdown             *self,
+                                                        gint                    max_lines);
+void            gs_markdown_set_smart_quoting          (GsMarkdown             *self,
+                                                        gboolean                smart_quoting);
+void            gs_markdown_set_escape                 (GsMarkdown             *self,
+                                                        gboolean                escape);
+void            gs_markdown_set_autocode               (GsMarkdown             *self,
+                                                        gboolean                autocode);
+void            gs_markdown_set_autolinkify            (GsMarkdown             *self,
+                                                        gboolean                autolinkify);
+gchar          *gs_markdown_parse                      (GsMarkdown             *self,
+                                                        const gchar            *text);
+
+G_END_DECLS
+
+#endif /* __GS_MARKDOWN_H */
+
diff --git a/src/libide/util/meson.build b/src/libide/util/meson.build
index 46d0f51c0..00eca698b 100644
--- a/src/libide/util/meson.build
+++ b/src/libide/util/meson.build
@@ -30,6 +30,7 @@ util_sources = [
 ]
 
 util_private_sources = [
+  'gs-markdown.c',
   'ide-async-helper.c',
   'ide-battery-monitor.c',
   'ide-doc-seq.c',


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