[gnome-builder/wip/search] search: implement new search design



commit d86dd7103ed4ef4f88af1264b961c866fad290f1
Author: Christian Hergert <christian hergert me>
Date:   Sat Jan 17 15:09:09 2015 -0800

    search: implement new search design
    
                 **This is not ready for general consumption**
    
    The new search design was laid out on the Builder mailing list[1]. This
    is the beginning of that implementation.
    
    ## Pending Work
    
     * Keyboard navigation is not solved
     * "X More" result navigation is not implemented
     * We need a plaholder page when no search is performed
     * Additional search providers.
     * Connecting activation signals
     * etc ...
    
    ## Performance Notes
    
    In particular, the old search felt slow for a couple of reasons.
    
     * We inserted delays to slow down search which were too large.
       Well at least when the search layer is faster.
     * We created way too many result objects. Since these were also
       GtkWidgets, the overhead was huge.
     * We were adding these widgets and sorting them in GtkListstore,
       which was not meant to handle the load. realize() costs alone
       where too much.
    
    So this adds a new structure called GbSearchReducer. It's purpose is to
    help us avoid creating objects if we know they will simply be discarded
    based on the expected number of results. Since most search layers will
    calculate the score as they walk through the result set, all we need is
    the score to determine if we need to inflate a full result object.
    
    This alone makes search much faster when walking through the Git trie.
    
    [1] https://mail.gnome.org/archives/builder-list/2015-January/msg00002.html

 src/git/gb-git-search-provider.c            |   89 +++---
 src/git/gb-git-search-provider.h            |    4 +-
 src/git/gb-git-search-result.c              |  224 ------------
 src/git/gb-git-search-result.h              |   55 ---
 src/gnome-builder.mk                        |    6 +-
 src/resources/css/builder.Adwaita.css       |    9 +
 src/resources/gnome-builder.gresource.xml   |    3 +-
 src/resources/ui/gb-git-search-result.ui    |   48 ---
 src/resources/ui/gb-search-box.ui           |   12 +-
 src/resources/ui/gb-search-display-group.ui |   59 ++++
 src/resources/ui/gb-search-display.ui       |   27 --
 src/search/gb-search-box.c                  |   11 +-
 src/search/gb-search-context.c              |  275 +++++----------
 src/search/gb-search-context.h              |   43 ++-
 src/search/gb-search-display-group.c        |  457 ++++++++++++++++++++++++
 src/search/gb-search-display-group.h        |   62 ++++
 src/search/gb-search-display.c              |  500 +++++++++++++++++----------
 src/search/gb-search-display.h              |   16 +-
 src/search/gb-search-manager.c              |  131 +++-----
 src/search/gb-search-manager.h              |   26 +-
 src/search/gb-search-provider.c             |  127 ++++++--
 src/search/gb-search-provider.h             |   51 ++-
 src/search/gb-search-reducer.c              |   98 ++++++
 src/search/gb-search-reducer.h              |   49 +++
 src/search/gb-search-result.c               |  128 ++++---
 src/search/gb-search-result.h               |   25 +-
 src/search/gb-search-types.h                |   45 ++-
 27 files changed, 1545 insertions(+), 1035 deletions(-)
---
diff --git a/src/git/gb-git-search-provider.c b/src/git/gb-git-search-provider.c
index dc0846c..bec6d76 100644
--- a/src/git/gb-git-search-provider.c
+++ b/src/git/gb-git-search-provider.c
@@ -23,9 +23,8 @@
 
 #include "fuzzy.h"
 #include "gb-git-search-provider.h"
-#include "gb-git-search-result.h"
-#include "gb-log.h"
 #include "gb-search-context.h"
+#include "gb-search-reducer.h"
 #include "gb-search-result.h"
 
 #define GB_GIT_SEARCH_PROVIDER_MAX_MATCHES 1000
@@ -38,15 +37,9 @@ struct _GbGitSearchProviderPrivate
   gchar          *repository_shorthand;
 };
 
-static void search_provider_init (GbSearchProviderInterface *iface);
-
-G_DEFINE_TYPE_EXTENDED (GbGitSearchProvider,
-                        gb_git_search_provider,
-                        G_TYPE_OBJECT,
-                        0,
-                        G_ADD_PRIVATE (GbGitSearchProvider)
-                        G_IMPLEMENT_INTERFACE (GB_TYPE_SEARCH_PROVIDER,
-                                               search_provider_init))
+G_DEFINE_TYPE_WITH_PRIVATE (GbGitSearchProvider,
+                            gb_git_search_provider,
+                            GB_TYPE_SEARCH_PROVIDER)
 
 enum {
   PROP_0,
@@ -112,8 +105,6 @@ gb_git_search_provider_build_file_index (GTask        *task,
   guint count;
   guint i;
 
-  ENTRY;
-
   g_return_if_fail (G_IS_FILE (repository_dir));
 
   /*
@@ -131,7 +122,7 @@ gb_git_search_provider_build_file_index (GTask        *task,
   if (!repository)
     {
       g_task_return_error (task, error);
-      GOTO (cleanup);
+      goto cleanup;
     }
 
   ref = ggit_repository_get_head (repository, NULL);
@@ -152,7 +143,7 @@ gb_git_search_provider_build_file_index (GTask        *task,
   if (!index)
     {
       g_task_return_error (task, error);
-      GOTO (cleanup);
+      goto cleanup;
     }
 
   entries = ggit_index_get_entries (index);
@@ -196,8 +187,6 @@ cleanup:
   g_clear_pointer (&entries, ggit_index_entries_unref);
   g_clear_object (&index);
   g_clear_object (&repository);
-
-  EXIT;
 }
 
 static gchar *
@@ -243,10 +232,11 @@ split_path (const gchar  *path,
 static void
 gb_git_search_provider_populate (GbSearchProvider *provider,
                                  GbSearchContext  *context,
+                                 const gchar      *search_terms,
+                                 gsize             max_results,
                                  GCancellable     *cancellable)
 {
   GbGitSearchProvider *self = (GbGitSearchProvider *)provider;
-  GList *list = NULL;
 
   g_return_if_fail (GB_IS_GIT_SEARCH_PROVIDER (self));
   g_return_if_fail (GB_IS_SEARCH_CONTEXT (context));
@@ -255,14 +245,13 @@ gb_git_search_provider_populate (GbSearchProvider *provider,
   if (self->priv->file_index)
     {
       GString *str = g_string_new (NULL);
-      const gchar *search_text;
+      GbSearchReducer reducer = { 0 };
       gchar *delimited;
       GArray *matches;
       guint i;
       guint truncate_len;
 
-      search_text = gb_search_context_get_search_text (context);
-      delimited = remove_spaces (search_text);
+      delimited = remove_spaces (search_terms);
       matches = fuzzy_match (self->priv->file_index, delimited,
                              GB_GIT_SEARCH_PROVIDER_MAX_MATCHES);
 
@@ -301,38 +290,38 @@ gb_git_search_provider_populate (GbSearchProvider *provider,
 
       truncate_len = str->len;
 
+      gb_search_reducer_init (&reducer, context, provider);
+
       for (i = 0; i < matches->len; i++)
         {
           FuzzyMatch *match;
-          GtkWidget *widget;
           gchar *shortname = NULL;
           gchar **parts;
           guint j;
 
           match = &g_array_index (matches, FuzzyMatch, i);
 
-          parts = split_path (match->value, &shortname);
-          for (j = 0; parts [j]; j++)
-            g_string_append_printf (str, " / %s", parts [j]);
-
-          /* TODO: Make a git file search result */
-          widget = g_object_new (GB_TYPE_GIT_SEARCH_RESULT,
-                                 "visible", TRUE,
-                                 "score", match->score,
-                                 "repository-name", str->str,
-                                 "path", match->value,
-                                 "display-name", shortname,
-                                 NULL);
-          list = g_list_prepend (list, widget);
-
-          g_free (shortname);
-          g_strfreev (parts);
-          g_string_truncate (str, truncate_len);
+          if (gb_search_reducer_accepts (&reducer, match->score))
+            {
+              GbSearchResult *result;
+
+              parts = split_path (match->value, &shortname);
+              for (j = 0; parts [j]; j++)
+                g_string_append_printf (str, " / %s", parts [j]);
+
+              result = gb_search_result_new (match->value, match->score);
+              gb_search_reducer_push (&reducer, result);
+              g_object_unref (result);
+
+              g_free (shortname);
+              g_strfreev (parts);
+              g_string_truncate (str, truncate_len);
+            }
         }
 
-      list = g_list_reverse (list);
-      gb_search_context_add_results (context, provider, list, TRUE);
+      gb_search_context_set_provider_count (context, provider, matches->len);
 
+      gb_search_reducer_destroy (&reducer);
       g_array_unref (matches);
       g_free (delimited);
       g_string_free (str, TRUE);
@@ -380,6 +369,14 @@ gb_git_search_provider_set_repository (GbGitSearchProvider *provider,
     }
 }
 
+const gchar *
+gb_git_search_provider_get_verb (GbSearchProvider *provider)
+{
+  g_return_val_if_fail (GB_IS_GIT_SEARCH_PROVIDER (provider), NULL);
+
+  return _("Switch To");
+}
+
 static void
 gb_git_search_provider_finalize (GObject *object)
 {
@@ -435,11 +432,15 @@ static void
 gb_git_search_provider_class_init (GbGitSearchProviderClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GbSearchProviderClass *provider_class = GB_SEARCH_PROVIDER_CLASS (klass);
 
   object_class->finalize = gb_git_search_provider_finalize;
   object_class->get_property = gb_git_search_provider_get_property;
   object_class->set_property = gb_git_search_provider_set_property;
 
+  provider_class->populate = gb_git_search_provider_populate;
+  provider_class->get_verb = gb_git_search_provider_get_verb;
+
   /**
    * GbGitSearchProvider:repository:
    *
@@ -463,9 +464,3 @@ gb_git_search_provider_init (GbGitSearchProvider *self)
 {
   self->priv = gb_git_search_provider_get_instance_private (self);
 }
-
-static void
-search_provider_init (GbSearchProviderInterface *iface)
-{
-  iface->populate = gb_git_search_provider_populate;
-}
diff --git a/src/git/gb-git-search-provider.h b/src/git/gb-git-search-provider.h
index 882b924..b330710 100644
--- a/src/git/gb-git-search-provider.h
+++ b/src/git/gb-git-search-provider.h
@@ -40,7 +40,7 @@ typedef struct _GbGitSearchProviderPrivate GbGitSearchProviderPrivate;
 
 struct _GbGitSearchProvider
 {
-  GObject parent;
+  GbSearchProvider parent;
 
   /*< private >*/
   GbGitSearchProviderPrivate *priv;
@@ -48,7 +48,7 @@ struct _GbGitSearchProvider
 
 struct _GbGitSearchProviderClass
 {
-  GObjectClass parent;
+  GbSearchProviderClass parent;
 };
 
 GType             gb_git_search_provider_get_type       (void);
diff --git a/src/gnome-builder.mk b/src/gnome-builder.mk
index 35af49c..b1df7df 100644
--- a/src/gnome-builder.mk
+++ b/src/gnome-builder.mk
@@ -113,8 +113,6 @@ libgnome_builder_la_SOURCES = \
        src/gedit/gedit-menu-stack-switcher.h \
        src/git/gb-git-search-provider.c \
        src/git/gb-git-search-provider.h \
-       src/git/gb-git-search-result.c \
-       src/git/gb-git-search-result.h \
        src/html/gb-html-completion-provider.c \
        src/html/gb-html-completion-provider.h \
        src/html/gb-html-document.c \
@@ -151,10 +149,14 @@ libgnome_builder_la_SOURCES = \
        src/search/gb-search-context.h \
        src/search/gb-search-display.c \
        src/search/gb-search-display.h \
+       src/search/gb-search-display-group.c \
+       src/search/gb-search-display-group.h \
        src/search/gb-search-manager.c \
        src/search/gb-search-manager.h \
        src/search/gb-search-provider.c \
        src/search/gb-search-provider.h \
+       src/search/gb-search-reducer.c \
+       src/search/gb-search-reducer.h \
        src/search/gb-search-result.c \
        src/search/gb-search-result.h \
        src/search/gb-search-types.h \
diff --git a/src/resources/css/builder.Adwaita.css b/src/resources/css/builder.Adwaita.css
index 44742c6..55f3433 100644
--- a/src/resources/css/builder.Adwaita.css
+++ b/src/resources/css/builder.Adwaita.css
@@ -200,3 +200,12 @@ GbSourceStyleSchemeWidget GtkSourceView {
 GtkScrolledWindow.gb-linked-scroller {
     border-top: none;
 }
+
+
+/*
+ * Rows inside of global search. They have a really confusing transition
+ * by default.
+ */
+GbSearchDisplayGroup GtkListBox .list-row {
+    transition: none;
+}
diff --git a/src/resources/gnome-builder.gresource.xml b/src/resources/gnome-builder.gresource.xml
index 255e6a8..2c564b3 100644
--- a/src/resources/gnome-builder.gresource.xml
+++ b/src/resources/gnome-builder.gresource.xml
@@ -42,7 +42,6 @@
     <file>ui/gb-editor-tweak-widget.ui</file>
     <file>ui/gb-editor-view.ui</file>
     <file>ui/gb-editor-workspace.ui</file>
-    <file>ui/gb-git-search-result.ui</file>
     <file>ui/gb-html-view.ui</file>
     <file>ui/gb-preferences-window.ui</file>
     <file>ui/gb-preferences-page-editor.ui</file>
@@ -50,7 +49,7 @@
     <file>ui/gb-preferences-page-language.ui</file>
     <file>ui/gb-preferences-page-vim.ui</file>
     <file>ui/gb-search-box.ui</file>
-    <file>ui/gb-search-display.ui</file>
+    <file>ui/gb-search-display-group.ui</file>
     <file>ui/gb-workbench.ui</file>
   </gresource>
 </gresources>
diff --git a/src/resources/ui/gb-search-box.ui b/src/resources/ui/gb-search-box.ui
index 0c4ceed..5d9f52a 100644
--- a/src/resources/ui/gb-search-box.ui
+++ b/src/resources/ui/gb-search-box.ui
@@ -35,8 +35,16 @@
     <property name="modal">false</property>
     <property name="relative-to">entry</property>
     <child>
-      <object class="GbSearchDisplay" id="display">
-        <property name="visible">true</property>
+      <object class="GbScrolledWindow" id="scroller">
+        <property name="max-content-height">800</property>
+        <property name="min-content-height">300</property>
+        <property name="min-content-width">750</property>
+        <property name="visible">True</property>
+        <child>
+          <object class="GbSearchDisplay" id="display">
+            <property name="visible">true</property>
+          </object>
+        </child>
       </object>
     </child>
   </object>
diff --git a/src/resources/ui/gb-search-display-group.ui b/src/resources/ui/gb-search-display-group.ui
new file mode 100644
index 0000000..cf93be8
--- /dev/null
+++ b/src/resources/ui/gb-search-display-group.ui
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.8 -->
+  <template class="GbSearchDisplayGroup" parent="GtkBox">
+    <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+    <child>
+      <object class="GtkLabel" id="label">
+        <property name="expand">False</property>
+        <property name="visible">True</property>
+        <property name="xalign">1.0</property>
+        <property name="yalign">0.0</property>
+        <property name="xpad">6</property>
+        <property name="ypad">3</property>
+        <property name="margin_start">24</property>
+        <style>
+          <class name="dim-label"/>
+        </style>
+      </object>
+    </child>
+    <child>
+      <object class="GtkSeparator">
+        <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+        <property name="visible">True</property>
+      </object>
+    </child>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+        <property name="visible">True</property>
+        <style>
+          <class name="view"/>
+        </style>
+        <child>
+          <object class="GtkListBox" id="rows">
+            <property name="hexpand">True</property>
+            <property name="visible">True</property>
+            <child>
+              <object class="GtkListBoxRow" id="more_row">
+                <child>
+                  <object class="GtkLabel" id="more_label">
+                    <property name="visible">True</property>
+                    <property name="halign">end</property>
+                    <property name="hexpand">False</property>
+                    <property name="use-markup">True</property>
+                    <property name="ypad">3</property>
+                    <property name="xpad">6</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/search/gb-search-box.c b/src/search/gb-search-box.c
index ba66582..e468daa 100644
--- a/src/search/gb-search-box.c
+++ b/src/search/gb-search-box.c
@@ -21,14 +21,17 @@
 #include <glib/gi18n.h>
 
 #include "gb-glib.h"
+#include "gb-scrolled-window.h"
 #include "gb-search-box.h"
+#include "gb-search-context.h"
 #include "gb-search-display.h"
 #include "gb-search-manager.h"
+#include "gb-search-result.h"
 #include "gb-widget.h"
 #include "gb-workbench.h"
 
-#define SHORT_DELAY_TIMEOUT_MSEC 250
-#define LONG_DELAY_TIMEOUT_MSEC  500
+#define SHORT_DELAY_TIMEOUT_MSEC 20
+#define LONG_DELAY_TIMEOUT_MSEC  250
 
 struct _GbSearchBoxPrivate
 {
@@ -113,8 +116,9 @@ gb_search_box_delay_cb (gpointer user_data)
   if (!search_text)
     return G_SOURCE_REMOVE;
 
-  context = gb_search_manager_search (box->priv->search_manager, search_text);
+  context = gb_search_manager_search (box->priv->search_manager, NULL, search_text); /* TODO: Remove search 
text */
   gb_search_display_set_context (box->priv->display, context);
+  gb_search_context_execute (context, search_text);
   g_object_unref (context);
 
   return G_SOURCE_REMOVE;
@@ -448,6 +452,7 @@ gb_search_box_class_init (GbSearchBoxClass *klass)
   GB_WIDGET_CLASS_BIND (klass, GbSearchBox, popover);
 
   g_type_ensure (GB_TYPE_SEARCH_DISPLAY);
+  g_type_ensure (GB_TYPE_SCROLLED_WINDOW);
 }
 
 static void
diff --git a/src/search/gb-search-context.c b/src/search/gb-search-context.c
index 04a0798..8281a9c 100644
--- a/src/search/gb-search-context.c
+++ b/src/search/gb-search-context.c
@@ -1,6 +1,6 @@
 /* gb-search-context.c
  *
- * Copyright (C) 2014 Christian Hergert <christian hergert me>
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,11 +16,9 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#define G_LOG_DOMAIN "search-context"
-
 #include <glib/gi18n.h>
+#include <gio/gio.h>
 
-#include "gb-log.h"
 #include "gb-search-context.h"
 #include "gb-search-provider.h"
 #include "gb-search-result.h"
@@ -29,8 +27,6 @@ struct _GbSearchContextPrivate
 {
   GCancellable *cancellable;
   GList        *providers;
-  gchar        *search_text;
-  GList        *results;
   guint         executed : 1;
 };
 
@@ -39,183 +35,114 @@ G_DEFINE_TYPE_WITH_PRIVATE (GbSearchContext, gb_search_context, G_TYPE_OBJECT)
 enum {
   PROP_0,
   PROP_PROVIDERS,
-  PROP_SEARCH_TEXT,
   LAST_PROP
 };
 
 enum {
-  RESULTS_ADDED,
+  COUNT_SET,
+  RESULT_ADDED,
+  RESULT_REMOVED,
   LAST_SIGNAL
 };
 
-static guint       gSignals [LAST_SIGNAL];
 static GParamSpec *gParamSpecs [LAST_PROP];
+static guint       gSignals [LAST_SIGNAL];
 
-/**
- * gb_search_context_new:
- * @providers: (element-type GbSearchProvider*) (transfer none): A #GList
- *
- * Creates a new search context with the provided search providers.
- *
- * Returns: (transfer full): A newly allocated #GbSearchContext.
- */
 GbSearchContext *
-gb_search_context_new (const GList *providers,
-                       const gchar *search_text)
+gb_search_context_new (void)
 {
-  return g_object_new (GB_TYPE_SEARCH_CONTEXT,
-                       "providers", providers,
-                       "search-text", search_text,
-                       NULL);
+  return g_object_new (GB_TYPE_SEARCH_CONTEXT, NULL);
 }
 
-void
-gb_search_context_execute (GbSearchContext *context)
-{
-  GbSearchContextPrivate *priv;
-  GList *iter;
-
-  g_return_if_fail (GB_IS_SEARCH_CONTEXT (context));
-
-  priv = context->priv;
-
-  if (priv->executed)
-    {
-      g_warning ("GbSearchContext has already been executed.");
-      return;
-    }
-
-  priv->executed = 1;
-
-  for (iter = priv->providers; iter; iter = iter->next)
-    gb_search_provider_populate (iter->data, context, priv->cancellable);
-}
-
-/**
- * gb_search_context_get_cancellable:
- * @context: A #GbSearchContext
- *
- * Retrieves the cancellable to cancel the search request. If the search has
- * completed, this will return NULL.
- *
- * Returns: (transfer none): A #GCancellable or %NULL.
- */
-GCancellable *
-gb_search_context_get_cancellable (GbSearchContext *context)
+const GList *
+gb_search_context_get_providers (GbSearchContext *context)
 {
   g_return_val_if_fail (GB_IS_SEARCH_CONTEXT (context), NULL);
 
-  return context->priv->cancellable;
+  return context->priv->providers;
 }
 
 void
-gb_search_context_cancel (GbSearchContext *context)
+gb_search_context_add_result (GbSearchContext  *context,
+                              GbSearchProvider *provider,
+                              GbSearchResult   *result)
 {
   g_return_if_fail (GB_IS_SEARCH_CONTEXT (context));
+  g_return_if_fail (GB_IS_SEARCH_PROVIDER (provider));
+  g_return_if_fail (GB_IS_SEARCH_RESULT (result));
 
-  g_cancellable_cancel (context->priv->cancellable);
+  g_signal_emit (context, gSignals [RESULT_ADDED], 0, provider, result);
 }
 
-/**
- * gb_search_context_get_results:
- * @context: A #GbSearchContext
- *
- * Fetches the current results.
- *
- * Returns: (transfer none): A #GList of current results.
- */
-const GList *
-gb_search_context_get_results (GbSearchContext *context)
+void
+gb_search_context_remove_result (GbSearchContext  *context,
+                                 GbSearchProvider *provider,
+                                 GbSearchResult   *result)
 {
-  g_return_val_if_fail (GB_IS_SEARCH_CONTEXT (context), NULL);
+  g_return_if_fail (GB_IS_SEARCH_CONTEXT (context));
+  g_return_if_fail (GB_IS_SEARCH_PROVIDER (provider));
+  g_return_if_fail (GB_IS_SEARCH_RESULT (result));
 
-  return context->priv->results;
+  g_signal_emit (context, gSignals [RESULT_REMOVED], 0, provider, result);
 }
 
-static void
-gb_search_context_results_added (GbSearchContext  *context,
-                                 GbSearchProvider *provider,
-                                 GList            *results,
-                                 gboolean          finished)
+void
+gb_search_context_set_provider_count (GbSearchContext  *context,
+                                      GbSearchProvider *provider,
+                                      guint64           count)
 {
-  ENTRY;
-
   g_return_if_fail (GB_IS_SEARCH_CONTEXT (context));
   g_return_if_fail (GB_IS_SEARCH_PROVIDER (provider));
 
-  /* TODO: how should we deal with priority? */
-
-  context->priv->results = g_list_concat (context->priv->results, results);
-
-  EXIT;
+  g_signal_emit (context, gSignals [COUNT_SET], 0, provider, count);
 }
 
-/**
- * gb_search_context_add_results:
- * @results: (transfer full) (element-type GbSearchResult*): A #GList or %NULL
- * @finished: if the provider is finished adding results.
- *
- * This function will add a list of results to the context. Ownership of
- * @results and the contained elements will be transfered to @context.
- */
 void
-gb_search_context_add_results (GbSearchContext  *context,
-                               GbSearchProvider *provider,
-                               GList            *results,
-                               gboolean          finished)
+gb_search_context_execute (GbSearchContext *context,
+                           const gchar     *search_terms)
 {
-  ENTRY;
+  GList *iter;
 
   g_return_if_fail (GB_IS_SEARCH_CONTEXT (context));
-  g_return_if_fail (GB_IS_SEARCH_PROVIDER (provider));
+  g_return_if_fail (!context->priv->executed);
+  g_return_if_fail (search_terms);
 
-  g_list_foreach (results, (GFunc)g_object_ref_sink, NULL);
+  context->priv->executed = TRUE;
 
-  g_signal_emit (context, gSignals [RESULTS_ADDED], 0,
-                 provider, results, finished);
+  for (iter = context->priv->providers; iter; iter = iter->next)
+    {
+      gsize max_results = 0;
 
-  EXIT;
-}
+      /* TODO: Get the max results for this provider */
 
-const gchar *
-gb_search_context_get_search_text (GbSearchContext *context)
-{
-  g_return_val_if_fail (GB_IS_SEARCH_CONTEXT (context), NULL);
-
-  return context->priv->search_text;
+      gb_search_provider_populate (iter->data,
+                                   context,
+                                   search_terms,
+                                   max_results,
+                                   context->priv->cancellable);
+    }
 }
 
-static void
-gb_search_context_set_search_text (GbSearchContext *context,
-                                   const gchar     *search_text)
+void
+gb_search_context_cancel (GbSearchContext *context)
 {
   g_return_if_fail (GB_IS_SEARCH_CONTEXT (context));
-  g_return_if_fail (search_text);
 
-  if (search_text != context->priv->search_text)
-    {
-      g_free (context->priv->search_text);
-      context->priv->search_text = g_strdup (search_text);
-      g_object_notify_by_pspec (G_OBJECT (context),
-                                gParamSpecs [PROP_SEARCH_TEXT]);
-    }
+  if (!g_cancellable_is_cancelled (context->priv->cancellable))
+    g_cancellable_cancel (context->priv->cancellable);
 }
 
-static void
-gb_search_context_set_providers (GbSearchContext *context,
-                                 const GList     *providers)
+void
+gb_search_context_add_provider (GbSearchContext  *context,
+                                GbSearchProvider *provider,
+                                gsize             max_results)
 {
-  GbSearchContextPrivate *priv;
-
   g_return_if_fail (GB_IS_SEARCH_CONTEXT (context));
+  g_return_if_fail (GB_IS_SEARCH_PROVIDER (provider));
+  g_return_if_fail (!context->priv->executed);
 
-  priv = context->priv;
-
-  g_list_foreach (priv->providers, (GFunc)g_object_unref, NULL);
-  g_list_free (priv->providers);
-
-  priv->providers = g_list_copy ((GList *)providers);
-  g_list_foreach (priv->providers, (GFunc)g_object_ref, NULL);
+  context->priv->providers = g_list_append (context->priv->providers,
+                                            g_object_ref (provider));
 }
 
 static void
@@ -223,13 +150,10 @@ gb_search_context_finalize (GObject *object)
 {
   GbSearchContextPrivate *priv = GB_SEARCH_CONTEXT (object)->priv;
 
-  g_list_foreach (priv->providers, (GFunc)g_object_unref, NULL);
-  g_clear_pointer (&priv->providers, g_list_free);
-
-  g_list_foreach (priv->results, (GFunc)g_object_unref, NULL);
-  g_clear_pointer (&priv->results, g_list_free);
+  g_clear_object (&priv->cancellable);
 
-  g_clear_pointer (&priv->search_text, g_free);
+  g_list_foreach (priv->providers, (GFunc)g_object_unref, NULL);
+  g_list_free (priv->providers);
 
   G_OBJECT_CLASS (gb_search_context_parent_class)->finalize (object);
 }
@@ -244,10 +168,6 @@ gb_search_context_get_property (GObject    *object,
 
   switch (prop_id)
     {
-    case PROP_SEARCH_TEXT:
-      g_value_set_string (value, gb_search_context_get_search_text (self));
-      break;
-
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
     }
@@ -263,14 +183,6 @@ gb_search_context_set_property (GObject      *object,
 
   switch (prop_id)
     {
-    case PROP_PROVIDERS:
-      gb_search_context_set_providers (self, g_value_get_pointer (value));
-      break;
-
-    case PROP_SEARCH_TEXT:
-      gb_search_context_set_search_text (self, g_value_get_string (value));
-      break;
-
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
     }
@@ -285,48 +197,49 @@ gb_search_context_class_init (GbSearchContextClass *klass)
   object_class->get_property = gb_search_context_get_property;
   object_class->set_property = gb_search_context_set_property;
 
-  klass->results_added = gb_search_context_results_added;
-
-  gParamSpecs [PROP_SEARCH_TEXT] =
-    g_param_spec_string ("search-text",
-                         _("Search Text"),
-                         _("The search text for the context."),
-                         NULL,
-                         (G_PARAM_READWRITE |
-                          G_PARAM_CONSTRUCT_ONLY |
-                          G_PARAM_STATIC_STRINGS));
-  g_object_class_install_property (object_class, PROP_SEARCH_TEXT,
-                                   gParamSpecs [PROP_SEARCH_TEXT]);
-
-  gParamSpecs [PROP_PROVIDERS] =
-    g_param_spec_pointer ("providers",
-                          _("Providers"),
-                          _("The providers for the search context."),
-                          (G_PARAM_READWRITE |
-                           G_PARAM_CONSTRUCT_ONLY |
-                           G_PARAM_STATIC_STRINGS));
-  g_object_class_install_property (object_class, PROP_PROVIDERS,
-                                   gParamSpecs [PROP_PROVIDERS]);
-
-  gSignals [RESULTS_ADDED] =
-    g_signal_new ("results-added",
-                  GB_TYPE_SEARCH_CONTEXT,
+  gSignals [COUNT_SET] =
+    g_signal_new ("count-set",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  g_cclosure_marshal_generic,
+                  G_TYPE_NONE,
+                  2,
+                  GB_TYPE_SEARCH_PROVIDER,
+                  G_TYPE_UINT64);
+
+  gSignals [RESULT_ADDED] =
+    g_signal_new ("result-added",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  g_cclosure_marshal_generic,
+                  G_TYPE_NONE,
+                  2,
+                  GB_TYPE_SEARCH_PROVIDER,
+                  GB_TYPE_SEARCH_RESULT);
+
+  gSignals [RESULT_REMOVED] =
+    g_signal_new ("result-removed",
+                  G_TYPE_FROM_CLASS (klass),
                   G_SIGNAL_RUN_LAST,
-                  G_STRUCT_OFFSET (GbSearchContextClass, results_added),
+                  0,
                   NULL,
                   NULL,
                   g_cclosure_marshal_generic,
                   G_TYPE_NONE,
-                  3,
+                  2,
                   GB_TYPE_SEARCH_PROVIDER,
-                  G_TYPE_POINTER,
-                  G_TYPE_BOOLEAN);
+                  GB_TYPE_SEARCH_RESULT);
 }
 
 static void
 gb_search_context_init (GbSearchContext *self)
 {
-  ENTRY;
   self->priv = gb_search_context_get_instance_private (self);
-  EXIT;
+  self->priv->cancellable = g_cancellable_new ();
 }
diff --git a/src/search/gb-search-context.h b/src/search/gb-search-context.h
index 5aae19a..688bc1e 100644
--- a/src/search/gb-search-context.h
+++ b/src/search/gb-search-context.h
@@ -1,6 +1,6 @@
 /* gb-search-context.h
  *
- * Copyright (C) 2014 Christian Hergert <christian hergert me>
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -19,13 +19,12 @@
 #ifndef GB_SEARCH_CONTEXT_H
 #define GB_SEARCH_CONTEXT_H
 
-#include <gio/gio.h>
+#include <glib-object.h>
 
 #include "gb-search-types.h"
 
 G_BEGIN_DECLS
 
-#define GB_TYPE_SEARCH_CONTEXT            (gb_search_context_get_type())
 #define GB_SEARCH_CONTEXT(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GB_TYPE_SEARCH_CONTEXT, 
GbSearchContext))
 #define GB_SEARCH_CONTEXT_CONST(obj)      (G_TYPE_CHECK_INSTANCE_CAST ((obj), GB_TYPE_SEARCH_CONTEXT, 
GbSearchContext const))
 #define GB_SEARCH_CONTEXT_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  GB_TYPE_SEARCH_CONTEXT, 
GbSearchContextClass))
@@ -45,23 +44,31 @@ struct _GbSearchContextClass
 {
   GObjectClass parent;
 
-  void (*results_added) (GbSearchContext  *context,
-                         GbSearchProvider *provider,
-                         GList            *results,
-                         gboolean          finished);
+  void (*result_added)   (GbSearchContext  *context,
+                          GbSearchProvider *provider,
+                          GbSearchResult   *result);
+  void (*result_removed) (GbSearchContext  *context,
+                          GbSearchProvider *provider,
+                          GbSearchResult   *result);
 };
 
-GType            gb_search_context_get_type        (void);
-GbSearchContext *gb_search_context_new             (const GList      *providers,
-                                                    const gchar      *search_text);
-void             gb_search_context_cancel          (GbSearchContext  *context);
-const GList     *gb_search_context_get_results     (GbSearchContext  *context);
-void             gb_search_context_add_results     (GbSearchContext  *context,
-                                                    GbSearchProvider *provider,
-                                                    GList            *results,
-                                                    gboolean          finished);
-void             gb_search_context_execute         (GbSearchContext  *context);
-const gchar     *gb_search_context_get_search_text (GbSearchContext *context);
+GbSearchContext *gb_search_context_new                (void);
+const GList     *gb_search_context_get_providers      (GbSearchContext  *context);
+void             gb_search_context_add_provider       (GbSearchContext  *context,
+                                                       GbSearchProvider *provider,
+                                                       gsize             max_results);
+void             gb_search_context_add_result         (GbSearchContext  *context,
+                                                       GbSearchProvider *provider,
+                                                       GbSearchResult   *result);
+void             gb_search_context_remove_result      (GbSearchContext  *context,
+                                                       GbSearchProvider *provider,
+                                                       GbSearchResult   *result);
+void             gb_search_context_cancel             (GbSearchContext  *context);
+void             gb_search_context_execute            (GbSearchContext  *context,
+                                                       const gchar      *search_terms);
+void             gb_search_context_set_provider_count (GbSearchContext  *context,
+                                                       GbSearchProvider *provider,
+                                                       guint64           count);
 
 G_END_DECLS
 
diff --git a/src/search/gb-search-display-group.c b/src/search/gb-search-display-group.c
new file mode 100644
index 0000000..611e787
--- /dev/null
+++ b/src/search/gb-search-display-group.c
@@ -0,0 +1,457 @@
+/* gb-search-display-group.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "gb-search-display-group.h"
+#include "gb-search-provider.h"
+#include "gb-search-result.h"
+#include "gb-widget.h"
+
+struct _GbSearchDisplayGroupPrivate
+{
+  /* References owned by instance */
+  GbSearchProvider *provider;
+
+  /* References owned by template */
+  GtkLabel         *more_label;
+  GtkListBoxRow    *more_row;
+  GtkLabel         *label;
+  GtkListBox       *rows;
+
+  guint64           count;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (GbSearchDisplayGroup,
+                            gb_search_display_group,
+                            GTK_TYPE_BOX)
+
+enum {
+  PROP_0,
+  PROP_MAX_RESULTS,
+  PROP_PROVIDER,
+  PROP_SIZE_GROUP,
+  LAST_PROP
+};
+
+enum {
+  RESULT_SELECTED,
+  LAST_SIGNAL
+};
+
+static GQuark      gQuarkResult;
+static GQuark      gQuarkRow;
+static GParamSpec *gParamSpecs [LAST_PROP];
+static guint       gSignals [LAST_SIGNAL];
+
+GbSearchProvider *
+gb_search_display_group_get_provider (GbSearchDisplayGroup *group)
+{
+  g_return_val_if_fail (GB_IS_SEARCH_DISPLAY_GROUP (group), NULL);
+
+  return group->priv->provider;
+}
+
+static void
+gb_search_display_group_set_provider (GbSearchDisplayGroup *group,
+                                      GbSearchProvider     *provider)
+{
+  const gchar *verb;
+
+  g_return_if_fail (GB_IS_SEARCH_DISPLAY_GROUP (group));
+  g_return_if_fail (!provider || GB_IS_SEARCH_PROVIDER (provider));
+
+  if (provider)
+    {
+      group->priv->provider = g_object_ref (provider);
+      verb = gb_search_provider_get_verb (provider);
+      gtk_label_set_label (group->priv->label, verb);
+    }
+}
+
+static void
+gb_search_display_group_set_size_group (GbSearchDisplayGroup *group,
+                                        GtkSizeGroup         *size_group)
+{
+  g_return_if_fail (GB_IS_SEARCH_DISPLAY_GROUP (group));
+  g_return_if_fail (!size_group || GTK_IS_SIZE_GROUP (size_group));
+
+  if (size_group)
+    gtk_size_group_add_widget (size_group, GTK_WIDGET (group->priv->label));
+}
+
+GtkWidget *
+gb_search_display_group_create_row (GbSearchResult *result)
+{
+  GtkProgressBar *progress;
+  GtkListBoxRow *row;
+  const gchar *markup;
+  GtkLabel *label;
+  GtkBox *box;
+  gfloat score;
+
+  g_return_val_if_fail (GB_IS_SEARCH_RESULT (result), NULL);
+
+  row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                      "visible", TRUE,
+                      NULL);
+  g_object_set_qdata_full (G_OBJECT (row), gQuarkResult,
+                           g_object_ref (result), g_object_unref);
+  g_object_set_qdata (G_OBJECT (result), gQuarkRow, row);
+  box = g_object_new (GTK_TYPE_BOX,
+                      "orientation", GTK_ORIENTATION_HORIZONTAL,
+                      "spacing", 6,
+                      "visible", TRUE,
+                      NULL);
+  gtk_container_add (GTK_CONTAINER (row), GTK_WIDGET (box));
+  markup = gb_search_result_get_markup (result);
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "hexpand", TRUE,
+                        "label", markup,
+                        "use-markup", TRUE,
+                        "visible", TRUE,
+                        "xalign", 0.0f,
+                        "xpad", 6,
+                        "ypad", 3,
+                        NULL);
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (label));
+  score = gb_search_result_get_score (result);
+  progress = g_object_new (GTK_TYPE_PROGRESS_BAR,
+                           "fraction", score,
+                           "hexpand", FALSE,
+                           "inverted", TRUE,
+                           "visible", TRUE,
+                           "width-request", 30,
+                           "valign", GTK_ALIGN_CENTER,
+                           "margin-start", 6,
+                           "margin-end", 6,
+                           NULL);
+  gtk_style_context_add_class (
+    gtk_widget_get_style_context (GTK_WIDGET (progress)), "osd");
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (progress));
+
+  return GTK_WIDGET (row);
+}
+
+void
+gb_search_display_group_remove_result (GbSearchDisplayGroup *group,
+                                       GbSearchResult       *result)
+{
+  GtkWidget *row;
+
+  g_return_if_fail (GB_IS_SEARCH_DISPLAY_GROUP (group));
+  g_return_if_fail (GB_IS_SEARCH_RESULT (result));
+
+  row = g_object_get_qdata (G_OBJECT (result), gQuarkRow);
+
+  if (row)
+    gtk_container_remove (GTK_CONTAINER (group->priv->rows), row);
+}
+
+void
+gb_search_display_group_add_result (GbSearchDisplayGroup *group,
+                                    GbSearchResult       *result)
+{
+  GtkWidget *row;
+
+  g_return_if_fail (GB_IS_SEARCH_DISPLAY_GROUP (group));
+  g_return_if_fail (GB_IS_SEARCH_RESULT (result));
+
+  row = gb_search_display_group_create_row (result);
+  gtk_container_add (GTK_CONTAINER (group->priv->rows), row);
+
+  gtk_list_box_invalidate_sort (group->priv->rows);
+
+  group->priv->count++;
+}
+
+void
+gb_search_display_group_set_count (GbSearchDisplayGroup *group,
+                                   guint64               count)
+{
+  GtkWidget *parent;
+  gchar *markup;
+
+  g_return_if_fail (GB_IS_SEARCH_DISPLAY_GROUP (group));
+
+  markup = g_strdup_printf (_("%"G_GUINT64_FORMAT" more"), count);
+  gtk_label_set_label (group->priv->more_label, markup);
+  g_free (markup);
+
+  parent = GTK_WIDGET (group->priv->more_row);
+
+  if ((count - group->priv->count) > 0)
+    gtk_widget_show (parent);
+  else
+    gtk_widget_hide (parent);
+}
+
+static gint
+compare_cb (GtkListBoxRow *row1,
+            GtkListBoxRow *row2,
+            gpointer       user_data)
+{
+  GtkListBoxRow *more_row = user_data;
+  GbSearchResult *result1;
+  GbSearchResult *result2;
+  gfloat score1;
+  gfloat score2;
+
+  if (row1 == more_row)
+    return 1;
+  else if (row2 == more_row)
+    return -1;
+
+  result1 = g_object_get_qdata (G_OBJECT (row1), gQuarkResult);
+  result2 = g_object_get_qdata (G_OBJECT (row2), gQuarkResult);
+
+  score1 = gb_search_result_get_score (result1);
+  score2 = gb_search_result_get_score (result2);
+
+  if (score1 < score2)
+    return 1;
+  else if (score1 > score2)
+    return -1;
+  else
+    return 0;
+}
+
+void
+gb_search_display_group_unselect (GbSearchDisplayGroup *group)
+{
+  g_return_if_fail (GB_IS_SEARCH_DISPLAY_GROUP (group));
+
+  gtk_list_box_unselect_all (group->priv->rows);
+}
+
+static void
+gb_search_display_group_row_selected (GbSearchDisplayGroup *group,
+                                      GtkListBoxRow        *row,
+                                      GtkListBox           *list_box)
+{
+  g_return_if_fail (GB_IS_SEARCH_DISPLAY_GROUP (group));
+  g_return_if_fail (!row || GTK_IS_LIST_BOX_ROW (row));
+  g_return_if_fail (GTK_IS_LIST_BOX (list_box));
+
+  if (row)
+    {
+      GbSearchResult *result;
+
+      result = g_object_get_qdata (G_OBJECT (row), gQuarkResult);
+      g_signal_emit (group, gSignals [RESULT_SELECTED], 0, result);
+    }
+}
+
+void
+gb_search_display_group_focus_first (GbSearchDisplayGroup *group)
+{
+  GtkListBoxRow *row;
+
+  g_return_if_fail (GB_IS_SEARCH_DISPLAY_GROUP (group));
+
+  row = gtk_list_box_get_row_at_y (group->priv->rows, 1);
+
+  if (row)
+    {
+      gtk_list_box_unselect_all (group->priv->rows);
+      gtk_widget_child_focus (GTK_WIDGET (group->priv->rows), GTK_DIR_DOWN);
+    }
+}
+
+void
+gb_search_display_group_focus_last (GbSearchDisplayGroup *group)
+{
+  GtkAllocation alloc;
+  GtkListBoxRow *row;
+
+  g_return_if_fail (GB_IS_SEARCH_DISPLAY_GROUP (group));
+
+  gtk_widget_get_allocation (GTK_WIDGET (group->priv->rows), &alloc);
+  row = gtk_list_box_get_row_at_y (group->priv->rows, alloc.height - 2);
+
+  if (row)
+    {
+      gtk_list_box_unselect_all (group->priv->rows);
+      gtk_widget_child_focus (GTK_WIDGET (group->priv->rows), GTK_DIR_UP);
+    }
+}
+
+static void
+gb_search_display_group_header_cb (GtkListBoxRow *row,
+                                   GtkListBoxRow *before,
+                                   gpointer       user_data)
+{
+  g_return_if_fail (GTK_IS_LIST_BOX_ROW (row));
+
+  if (row)
+    {
+      GtkWidget *header;
+
+      header = g_object_new (GTK_TYPE_SEPARATOR,
+                             "orientation", GTK_ORIENTATION_HORIZONTAL,
+                             "visible", TRUE,
+                             NULL);
+      gtk_list_box_row_set_header (row, header);
+    }
+}
+
+static gboolean
+gb_search_display_group_keynav_failed (GbSearchDisplayGroup *group,
+                                       GtkDirectionType      dir,
+                                       GtkListBox           *list_box)
+{
+  gboolean ret = FALSE;
+
+  g_return_val_if_fail (GB_IS_SEARCH_DISPLAY_GROUP (group), FALSE);
+  g_return_val_if_fail (GTK_IS_LIST_BOX (list_box), FALSE);
+
+  g_signal_emit_by_name (group, "keynav-failed", dir, &ret);
+
+  return ret;
+}
+
+static void
+gb_search_display_group_finalize (GObject *object)
+{
+  GbSearchDisplayGroupPrivate *priv = GB_SEARCH_DISPLAY_GROUP (object)->priv;
+
+  g_clear_object (&priv->provider);
+
+  G_OBJECT_CLASS (gb_search_display_group_parent_class)->finalize (object);
+}
+
+static void
+gb_search_display_group_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  GbSearchDisplayGroup *self = GB_SEARCH_DISPLAY_GROUP (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROVIDER:
+      g_value_set_object (value, gb_search_display_group_get_provider (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_search_display_group_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  GbSearchDisplayGroup *self = GB_SEARCH_DISPLAY_GROUP (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROVIDER:
+      gb_search_display_group_set_provider (self, g_value_get_object (value));
+      break;
+
+    case PROP_SIZE_GROUP:
+      gb_search_display_group_set_size_group (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_search_display_group_class_init (GbSearchDisplayGroupClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = gb_search_display_group_finalize;
+  object_class->get_property = gb_search_display_group_get_property;
+  object_class->set_property = gb_search_display_group_set_property;
+
+  gParamSpecs [PROP_PROVIDER] =
+    g_param_spec_object ("provider",
+                         _("Provider"),
+                         _("The search provider"),
+                         GB_TYPE_SEARCH_PROVIDER,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+  g_object_class_install_property (object_class, PROP_PROVIDER,
+                                   gParamSpecs [PROP_PROVIDER]);
+
+  gParamSpecs [PROP_SIZE_GROUP] =
+    g_param_spec_object ("size-group",
+                         _("Size Group"),
+                         _("The size group for the label."),
+                         GTK_TYPE_SIZE_GROUP,
+                         (G_PARAM_WRITABLE |
+                          G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+  g_object_class_install_property (object_class, PROP_SIZE_GROUP,
+                                   gParamSpecs [PROP_SIZE_GROUP]);
+
+  gSignals [RESULT_SELECTED] =
+    g_signal_new ("result-selected",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  g_cclosure_marshal_generic,
+                  G_TYPE_NONE,
+                  1,
+                  GB_TYPE_SEARCH_RESULT);
+
+  GB_WIDGET_CLASS_TEMPLATE (widget_class, "gb-search-display-group.ui");
+  GB_WIDGET_CLASS_BIND (widget_class, GbSearchDisplayGroup, more_label);
+  GB_WIDGET_CLASS_BIND (widget_class, GbSearchDisplayGroup, more_row);
+  GB_WIDGET_CLASS_BIND (widget_class, GbSearchDisplayGroup, label);
+  GB_WIDGET_CLASS_BIND (widget_class, GbSearchDisplayGroup, rows);
+
+  gQuarkResult = g_quark_from_static_string ("GB_SEARCH_RESULT");
+  gQuarkRow = g_quark_from_static_string ("GB_SEARCH_DISPLAY_ROW");
+}
+
+static void
+gb_search_display_group_init (GbSearchDisplayGroup *self)
+{
+  self->priv = gb_search_display_group_get_instance_private (self);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_list_box_set_sort_func (self->priv->rows, compare_cb,
+                              self->priv->more_row, NULL);
+
+  g_signal_connect_object (self->priv->rows,
+                           "keynav-failed",
+                           G_CALLBACK (gb_search_display_group_keynav_failed),
+                           self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (self->priv->rows,
+                           "row-selected",
+                           G_CALLBACK (gb_search_display_group_row_selected),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_list_box_set_header_func (self->priv->rows,
+                                gb_search_display_group_header_cb,
+                                NULL, NULL);
+}
diff --git a/src/search/gb-search-display-group.h b/src/search/gb-search-display-group.h
new file mode 100644
index 0000000..ec17842
--- /dev/null
+++ b/src/search/gb-search-display-group.h
@@ -0,0 +1,62 @@
+/* gb-search-display-group.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GB_SEARCH_DISPLAY_GROUP_H
+#define GB_SEARCH_DISPLAY_GROUP_H
+
+#include <gtk/gtk.h>
+
+#include "gb-search-types.h"
+
+G_BEGIN_DECLS
+
+#define GB_SEARCH_DISPLAY_GROUP(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), 
GB_TYPE_SEARCH_DISPLAY_GROUP, GbSearchDisplayGroup))
+#define GB_SEARCH_DISPLAY_GROUP_CONST(obj)      (G_TYPE_CHECK_INSTANCE_CAST ((obj), 
GB_TYPE_SEARCH_DISPLAY_GROUP, GbSearchDisplayGroup const))
+#define GB_SEARCH_DISPLAY_GROUP_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  
GB_TYPE_SEARCH_DISPLAY_GROUP, GbSearchDisplayGroupClass))
+#define GB_IS_SEARCH_DISPLAY_GROUP(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), 
GB_TYPE_SEARCH_DISPLAY_GROUP))
+#define GB_IS_SEARCH_DISPLAY_GROUP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  
GB_TYPE_SEARCH_DISPLAY_GROUP))
+#define GB_SEARCH_DISPLAY_GROUP_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  
GB_TYPE_SEARCH_DISPLAY_GROUP, GbSearchDisplayGroupClass))
+
+struct _GbSearchDisplayGroup
+{
+  GtkBox parent;
+
+  /*< private >*/
+  GbSearchDisplayGroupPrivate *priv;
+};
+
+struct _GbSearchDisplayGroupClass
+{
+  GtkBoxClass parent;
+};
+
+void              gb_search_display_group_clear         (GbSearchDisplayGroup *group);
+GbSearchProvider *gb_search_display_group_get_provider  (GbSearchDisplayGroup *group);
+void              gb_search_display_group_add_result    (GbSearchDisplayGroup *group,
+                                                         GbSearchResult       *result);
+void              gb_search_display_group_remove_result (GbSearchDisplayGroup *group,
+                                                         GbSearchResult       *result);
+void              gb_search_display_group_set_count     (GbSearchDisplayGroup *group,
+                                                         gsize                 count);
+void              gb_search_display_group_unselect      (GbSearchDisplayGroup *group);
+void              gb_search_display_group_focus_first   (GbSearchDisplayGroup *group);
+void              gb_search_display_group_focus_last    (GbSearchDisplayGroup *group);
+
+G_END_DECLS
+
+#endif /* GB_SEARCH_DISPLAY_GROUP_H */
diff --git a/src/search/gb-search-display.c b/src/search/gb-search-display.c
index af16d71..0ce09cc 100644
--- a/src/search/gb-search-display.c
+++ b/src/search/gb-search-display.c
@@ -1,6 +1,6 @@
 /* gb-search-display.c
  *
- * Copyright (C) 2014 Christian Hergert <christian hergert me>
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,28 +16,28 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#define G_LOG_DOMAIN "search-display"
-
 #include <glib/gi18n.h>
 
-#include "gb-log.h"
-#include "gb-scrolled-window.h"
 #include "gb-search-display.h"
+#include "gb-search-display-group.h"
 #include "gb-search-provider.h"
 #include "gb-search-result.h"
-#include "gb-widget.h"
 
 struct _GbSearchDisplayPrivate
 {
-  /* References owned by widget */
-  GbSearchContext  *context;
-
-  /* References owned by Gtk template */
-  GtkListBox       *list_box;
-  GbScrolledWindow *scroller;
+  GbSearchContext      *context;
+  GArray               *providers;
+  GtkSizeGroup         *size_group;
+  GbSearchDisplayGroup *last_group;
 };
 
-G_DEFINE_TYPE_WITH_PRIVATE (GbSearchDisplay, gb_search_display, GTK_TYPE_BIN)
+typedef struct
+{
+  GbSearchProvider     *provider;
+  GbSearchDisplayGroup *group;
+} ProviderEntry;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GbSearchDisplay, gb_search_display, GTK_TYPE_BOX)
 
 enum {
   PROP_0,
@@ -53,6 +53,29 @@ enum {
 static GParamSpec *gParamSpecs [LAST_PROP];
 static guint       gSignals [LAST_SIGNAL];
 
+static void
+provider_entry_destroy (gpointer data)
+{
+  ProviderEntry *entry = data;
+
+  g_clear_object (&entry->provider);
+}
+
+static gint
+provider_entry_sort (gconstpointer ptra,
+                     gconstpointer ptrb)
+{
+  const ProviderEntry *entrya = ptra;
+  const ProviderEntry *entryb = ptrb;
+  gint a;
+  gint b;
+
+  a = gb_search_provider_get_priority ((GB_SEARCH_PROVIDER (entrya->provider)));
+  b = gb_search_provider_get_priority ((GB_SEARCH_PROVIDER (entryb->provider)));
+
+  return a - b;
+}
+
 GtkWidget *
 gb_search_display_new (void)
 {
@@ -60,263 +83,353 @@ gb_search_display_new (void)
 }
 
 static void
-gb_search_display_results_added (GbSearchDisplay  *display,
-                                 GbSearchProvider *provider,
-                                 GList            *results,
-                                 gboolean          finished)
+gb_search_display_result_selected (GbSearchDisplay      *display,
+                                   GbSearchResult       *result,
+                                   GbSearchDisplayGroup *group)
 {
-  GList *iter;
-
-  ENTRY;
+  guint i;
 
   g_return_if_fail (GB_IS_SEARCH_DISPLAY (display));
-  g_return_if_fail (GB_IS_SEARCH_PROVIDER (provider));
-
-  for (iter = results; iter; iter = iter->next)
-    gtk_list_box_insert (display->priv->list_box, iter->data, -1);
+  g_return_if_fail (!result || GB_IS_SEARCH_RESULT (result));
+  g_return_if_fail (GB_IS_SEARCH_DISPLAY_GROUP (group));
 
-  gtk_list_box_invalidate_sort (display->priv->list_box);
+  for (i = 0; i < display->priv->providers->len; i++)
+    {
+      ProviderEntry *ptr;
 
-  EXIT;
+      ptr = &g_array_index (display->priv->providers, ProviderEntry, i);
+      if (ptr->group != group)
+        gb_search_display_group_unselect (ptr->group);
+    }
 }
 
-GbSearchContext *
-gb_search_display_get_context (GbSearchDisplay *display)
+static gboolean
+gb_search_display_keynav_failed (GbSearchDisplay      *display,
+                                 GtkDirectionType      dir,
+                                 GbSearchDisplayGroup *group)
 {
-  g_return_val_if_fail (GB_IS_SEARCH_DISPLAY (display), NULL);
+  GList *list;
+  GList *iter;
+  gint position = -1;
 
-  return display->priv->context;
+  g_return_val_if_fail (GB_IS_SEARCH_DISPLAY (display), FALSE);
+  g_return_val_if_fail (GB_IS_SEARCH_DISPLAY_GROUP (group), FALSE);
+
+  gtk_container_child_get (GTK_CONTAINER (display), GTK_WIDGET (group),
+                           "position", &position,
+                           NULL);
+
+  if (dir == GTK_DIR_DOWN)
+    {
+      list = gtk_container_get_children (GTK_CONTAINER (display));
+      iter = g_list_nth (list, position + 1);
+      if (iter && (iter->data != display->priv->last_group))
+        {
+          gb_search_display_group_unselect (group);
+          gb_search_display_group_focus_first (iter->data);
+          return TRUE;
+        }
+    }
+  else if (dir == GTK_DIR_UP && position > 0)
+    {
+      list = gtk_container_get_children (GTK_CONTAINER (display));
+      iter = g_list_nth (list, position - 1);
+      if (iter)
+        {
+          gb_search_display_group_unselect (group);
+          gb_search_display_group_focus_last (iter->data);
+          return TRUE;
+        }
+    }
+
+  return FALSE;
 }
 
-static void
-gb_search_display_connect (GbSearchDisplay *display,
-                           GbSearchContext *context)
+void
+gb_search_display_activate (GbSearchDisplay *display)
 {
-  GbSearchDisplayPrivate *priv;
-  const GList *list;
-  const GList *iter;
+  g_warning ("TODO: implement display_activate()");
+}
 
-  ENTRY;
+static void
+gb_search_display_add_provider (GbSearchDisplay  *display,
+                                GbSearchProvider *provider)
+{
+  ProviderEntry entry = { 0 };
+  guint i;
 
   g_return_if_fail (GB_IS_SEARCH_DISPLAY (display));
-  g_return_if_fail (GB_IS_SEARCH_CONTEXT (context));
+  g_return_if_fail (GB_IS_SEARCH_PROVIDER (provider));
 
-  priv = display->priv;
+  /*
+   * Make sure we don't add an item twice. Probably can assert here, but
+   * warning will do for now.
+   */
+  for (i = 0; i < display->priv->providers->len; i++)
+    {
+      ProviderEntry *ptr;
 
-  g_signal_connect_object (context,
-                           "results-added",
-                           G_CALLBACK (gb_search_display_results_added),
+      ptr = &g_array_index (display->priv->providers, ProviderEntry, i);
+
+      if (ptr->provider == provider)
+        {
+          g_warning (_("Cannot add provider more than once."));
+          return;
+        }
+    }
+
+  /*
+   * Add the entry to our array and sort the array to determine our target
+   * widget packing position.
+   */
+  entry.provider = g_object_ref (provider);
+  entry.group = g_object_new (GB_TYPE_SEARCH_DISPLAY_GROUP,
+                              "size-group", display->priv->size_group,
+                              "provider", provider,
+                              "visible", FALSE,
+                              NULL);
+  g_signal_connect_object (entry.group,
+                           "result-selected",
+                           G_CALLBACK (gb_search_display_result_selected),
+                           display,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (entry.group,
+                           "keynav-failed",
+                           G_CALLBACK (gb_search_display_keynav_failed),
                            display,
                            G_CONNECT_SWAPPED);
+  g_array_append_val (display->priv->providers, entry);
+  g_array_sort (display->priv->providers, provider_entry_sort);
 
-  list = gb_search_context_get_results (context);
-  for (iter = list; iter; iter = iter->next)
-    gtk_list_box_insert (priv->list_box, iter->data, -1);
-  gtk_list_box_invalidate_sort (display->priv->list_box);
+  /*
+   * Find the location of the entry and use the index to pack the display
+   * group widget.
+   */
+  for (i = 0; i < display->priv->providers->len; i++)
+    {
+      ProviderEntry *ptr;
 
-  EXIT;
+      ptr = &g_array_index (display->priv->providers, ProviderEntry, i);
+
+      if (ptr->provider == provider)
+        {
+          gtk_container_add_with_properties (GTK_CONTAINER (display),
+                                             GTK_WIDGET (entry.group),
+                                             "position", i,
+                                             NULL);
+          break;
+        }
+    }
 }
 
 static void
-gb_search_display_disconnect (GbSearchDisplay *display,
-                              GbSearchContext *context)
+gb_search_display_remove_provider (GbSearchDisplay  *display,
+                                   GbSearchProvider *provider)
 {
-  GbSearchDisplayPrivate *priv;
-  GList *children;
-  GList *iter;
-
-  ENTRY;
+  guint i;
 
   g_return_if_fail (GB_IS_SEARCH_DISPLAY (display));
-  g_return_if_fail (GB_IS_SEARCH_CONTEXT (context));
+  g_return_if_fail (GB_IS_SEARCH_PROVIDER (provider));
 
-  priv = display->priv;
+  for (i = 0; i < display->priv->providers->len; i++)
+    {
+      ProviderEntry *ptr;
 
-  g_signal_handlers_disconnect_by_func (context,
-                                        G_CALLBACK (gb_search_display_results_added),
-                                        display);
+      ptr = &g_array_index (display->priv->providers, ProviderEntry, i);
 
-  children = gtk_container_get_children (GTK_CONTAINER (priv->list_box));
-  for (iter = children; iter; iter = iter->next)
-    gtk_container_remove (GTK_CONTAINER (priv->list_box), iter->data);
-  g_list_free (children);
+      if (ptr->provider == provider)
+        {
+          gtk_container_remove (GTK_CONTAINER (display),
+                                GTK_WIDGET (ptr->group));
+          g_array_remove_index (display->priv->providers, i);
+          return;
+        }
+    }
 
-  EXIT;
+  g_warning (_("The provider could not be found."));
 }
 
-void
-gb_search_display_set_context (GbSearchDisplay *display,
-                               GbSearchContext *context)
+static void
+gb_search_display_result_added (GbSearchDisplay  *display,
+                                GbSearchProvider *provider,
+                                GbSearchResult   *result,
+                                GbSearchContext  *context)
 {
-  ENTRY;
+  guint i;
 
   g_return_if_fail (GB_IS_SEARCH_DISPLAY (display));
-  g_return_if_fail (!context || GB_IS_SEARCH_CONTEXT (context));
+  g_return_if_fail (GB_IS_SEARCH_PROVIDER (provider));
+  g_return_if_fail (GB_IS_SEARCH_RESULT (result));
+  g_return_if_fail (GB_IS_SEARCH_CONTEXT (context));
 
-  if (display->priv->context != context)
+  for (i = 0; i < display->priv->providers->len; i++)
     {
-      if (display->priv->context)
-        {
-          gb_search_display_disconnect (display, display->priv->context);
-          g_clear_object (&display->priv->context);
-        }
+      ProviderEntry *ptr;
 
-      if (context)
+      ptr = &g_array_index (display->priv->providers, ProviderEntry, i);
+
+      if (ptr->provider == provider)
         {
-          display->priv->context = g_object_ref (context);
-          gb_search_display_connect (display, context);
+          gb_search_display_group_add_result (ptr->group, result);
+          gtk_widget_show (GTK_WIDGET (ptr->group));
+          break;
         }
-
-      g_object_notify_by_pspec (G_OBJECT (display),
-                                gParamSpecs [PROP_CONTEXT]);
     }
-
-  EXIT;
 }
 
 static void
-gb_search_display_emit_result_activated (GbSearchDisplay *display,
-                                         GbSearchResult  *result)
+gb_search_display_result_removed (GbSearchDisplay  *display,
+                                  GbSearchProvider *provider,
+                                  GbSearchResult   *result,
+                                  GbSearchContext  *context)
 {
+  guint i;
+
   g_return_if_fail (GB_IS_SEARCH_DISPLAY (display));
+  g_return_if_fail (GB_IS_SEARCH_PROVIDER (provider));
   g_return_if_fail (GB_IS_SEARCH_RESULT (result));
+  g_return_if_fail (GB_IS_SEARCH_CONTEXT (context));
 
-  gb_search_result_activate (result);
-  g_signal_emit (display, gSignals [RESULT_ACTIVATED], 0, result);
+  for (i = 0; i < display->priv->providers->len; i++)
+    {
+      ProviderEntry *ptr;
+
+      ptr = &g_array_index (display->priv->providers, ProviderEntry, i);
+
+      if (ptr->provider == provider)
+        {
+          gb_search_display_group_remove_result (ptr->group, result);
+          break;
+        }
+    }
 }
 
 static void
-gb_search_display_row_activated (GbSearchDisplay *display,
-                                 GtkListBoxRow   *row,
-                                 GtkListBox      *list_box)
+gb_search_display_count_set (GbSearchDisplay  *display,
+                             GbSearchProvider *provider,
+                             guint64           count,
+                             GbSearchContext  *context)
 {
-  GtkWidget *child;
+  guint i;
 
   g_return_if_fail (GB_IS_SEARCH_DISPLAY (display));
-  g_return_if_fail (GTK_IS_LIST_BOX_ROW (row));
-  g_return_if_fail (GTK_IS_LIST_BOX (list_box));
+  g_return_if_fail (GB_IS_SEARCH_PROVIDER (provider));
+  g_return_if_fail (GB_IS_SEARCH_CONTEXT (context));
 
-  child = gtk_bin_get_child (GTK_BIN (row));
+  for (i = 0; i < display->priv->providers->len; i++)
+    {
+      ProviderEntry *ptr;
 
-  if (GB_IS_SEARCH_RESULT (child))
-    gb_search_display_emit_result_activated (display, GB_SEARCH_RESULT (child));
+      ptr = &g_array_index (display->priv->providers, ProviderEntry, i);
+
+      if (ptr->provider == provider)
+        {
+          gb_search_display_group_set_count (ptr->group, count);
+          break;
+        }
+    }
 }
 
-void
-gb_search_display_activate (GbSearchDisplay *display)
+static void
+gb_search_display_connect_context (GbSearchDisplay *display,
+                                   GbSearchContext *context)
 {
-  GtkListBoxRow *row;
+  const GList *providers;
+  const GList *iter;
 
   g_return_if_fail (GB_IS_SEARCH_DISPLAY (display));
+  g_return_if_fail (GB_IS_SEARCH_CONTEXT (context));
 
-  row = gtk_list_box_get_selected_row (display->priv->list_box);
-
-  /*
-   * WORKAROUND:
-   *
-   * Workaround since get_index() does not take into account sorts and
-   * a y of 0 doesn't currently work.
-   */
-  if (!row)
-    row = gtk_list_box_get_row_at_y (display->priv->list_box, 5);
-
-  if (row)
-    gb_search_display_row_activated (display, row, display->priv->list_box);
-}
-
-static gint
-gb_search_display_sort_cb (GtkListBoxRow *row1,
-                           GtkListBoxRow *row2,
-                           gpointer       user_data)
-{
-  GtkWidget *child1;
-  GtkWidget *child2;
+  providers = gb_search_context_get_providers (context);
 
-  child1 = gtk_bin_get_child (GTK_BIN (row1));
-  child2 = gtk_bin_get_child (GTK_BIN (row2));
+  for (iter = providers; iter; iter = iter->next)
+    gb_search_display_add_provider (display, iter->data);
 
-  return gb_search_result_compare_func (GB_SEARCH_RESULT (child1),
-                                        GB_SEARCH_RESULT (child2));
+  g_signal_connect_object (context,
+                           "result-added",
+                           G_CALLBACK (gb_search_display_result_added),
+                           display,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (context,
+                           "result-removed",
+                           G_CALLBACK (gb_search_display_result_removed),
+                           display,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (context,
+                           "count-set",
+                           G_CALLBACK (gb_search_display_count_set),
+                           display,
+                           G_CONNECT_SWAPPED);
 }
 
 static void
-gb_search_display_grab_focus (GtkWidget *widget)
+gb_search_display_disconnect_context (GbSearchDisplay *display,
+                                      GbSearchContext *context)
 {
-  GbSearchDisplay *display = (GbSearchDisplay *)widget;
-  GtkListBoxRow *row;
-
   g_return_if_fail (GB_IS_SEARCH_DISPLAY (display));
+  g_return_if_fail (GB_IS_SEARCH_CONTEXT (context));
 
-  /*
-   * WORKAROUND:
-   *
-   * Getting the row at y of 0 does not work (returns NULL). And getting the
-   * row at index 0 does not take into account sort.
-   *
-   * https://bugzilla.gnome.org/show_bug.cgi?id=741208
-   */
-  row = gtk_list_box_get_row_at_y (display->priv->list_box, 1);
-
-  if (row)
+  while (display->priv->providers->len)
     {
-      gtk_list_box_select_row (display->priv->list_box, row);
-      gtk_widget_child_focus (GTK_WIDGET (display->priv->list_box),
-                              GTK_DIR_TAB_FORWARD);
+      ProviderEntry *ptr;
+
+      ptr = &g_array_index (display->priv->providers, ProviderEntry,
+                            display->priv->providers->len - 1);
+      gb_search_display_remove_provider (display, ptr->provider);
     }
-  else
-    GTK_WIDGET_CLASS (gb_search_display_parent_class)->grab_focus (widget);
+
+  g_signal_handlers_disconnect_by_func (context,
+                                        G_CALLBACK (gb_search_display_result_added),
+                                        display);
 }
 
-static void
-gb_search_display_header_func (GtkListBoxRow *row,
-                               GtkListBoxRow *before,
-                               gpointer       user_data)
+GbSearchContext *
+gb_search_display_get_context (GbSearchDisplay *display)
 {
-  if (before)
-    {
-      GtkWidget *header;
+  g_return_val_if_fail (GB_IS_SEARCH_DISPLAY (display), NULL);
 
-      header = g_object_new (GTK_TYPE_SEPARATOR,
-                             "orientation", GTK_ORIENTATION_HORIZONTAL,
-                             "visible", TRUE,
-                             NULL);
-      gtk_list_box_row_set_header (row, header);
-    }
+  return display->priv->context;
 }
 
-static void
-gb_search_display_constructed (GObject *object)
+void
+gb_search_display_set_context (GbSearchDisplay *display,
+                               GbSearchContext *context)
 {
-  GbSearchDisplay *self = (GbSearchDisplay *)object;
-
-  g_return_if_fail (GB_IS_SEARCH_DISPLAY (self));
+  GbSearchDisplayPrivate *priv;
 
-  G_OBJECT_CLASS (gb_search_display_parent_class)->constructed (object);
+  g_return_if_fail (GB_IS_SEARCH_DISPLAY (display));
+  g_return_if_fail (!context || GB_IS_SEARCH_CONTEXT (context));
 
-  gtk_list_box_set_header_func (self->priv->list_box,
-                                gb_search_display_header_func,
-                                NULL, NULL);
+  priv = display->priv;
 
+  if (priv->context != context)
+    {
+      if (priv->context)
+        {
+          gb_search_display_disconnect_context (display, priv->context);
+          g_clear_object (&display->priv->context);
+        }
 
-  g_signal_connect_object (self->priv->list_box,
-                           "row-activated",
-                           G_CALLBACK (gb_search_display_row_activated),
-                           self,
-                           G_CONNECT_SWAPPED);
+      if (context)
+        {
+          priv->context = g_object_ref (context);
+          gb_search_display_connect_context (display, priv->context);
+        }
 
-  gtk_list_box_set_sort_func (self->priv->list_box,
-                              gb_search_display_sort_cb,
-                              NULL, NULL);
+      g_object_notify_by_pspec (G_OBJECT (display), gParamSpecs [PROP_CONTEXT]);
+    }
 }
 
 static void
-gb_search_display_finalize (GObject *object)
+gb_search_display_dispose (GObject *object)
 {
   GbSearchDisplayPrivate *priv = GB_SEARCH_DISPLAY (object)->priv;
 
+  g_clear_pointer (&priv->providers, g_array_unref);
   g_clear_object (&priv->context);
+  g_clear_object (&priv->size_group);
 
-  G_OBJECT_CLASS (gb_search_display_parent_class)->finalize (object);
+  G_OBJECT_CLASS (gb_search_display_parent_class)->dispose (object);
 }
 
 static void
@@ -330,7 +443,7 @@ gb_search_display_get_property (GObject    *object,
   switch (prop_id)
     {
     case PROP_CONTEXT:
-      g_value_set_object (value, self->priv->context);
+      g_value_set_object (value, gb_search_display_get_context (self));
       break;
 
     default:
@@ -361,19 +474,15 @@ static void
 gb_search_display_class_init (GbSearchDisplayClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
-  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
 
-  object_class->constructed = gb_search_display_constructed;
-  object_class->finalize = gb_search_display_finalize;
+  object_class->dispose = gb_search_display_dispose;
   object_class->get_property = gb_search_display_get_property;
   object_class->set_property = gb_search_display_set_property;
 
-  widget_class->grab_focus = gb_search_display_grab_focus;
-
   gParamSpecs [PROP_CONTEXT] =
     g_param_spec_object ("context",
                          _("Context"),
-                         _("The search context."),
+                         _("The active search context."),
                          GB_TYPE_SEARCH_CONTEXT,
                          (G_PARAM_READWRITE |
                           G_PARAM_STATIC_STRINGS));
@@ -391,12 +500,6 @@ gb_search_display_class_init (GbSearchDisplayClass *klass)
                   G_TYPE_NONE,
                   1,
                   GB_TYPE_SEARCH_RESULT);
-
-  GB_WIDGET_CLASS_TEMPLATE (klass, "gb-search-display.ui");
-  GB_WIDGET_CLASS_BIND (klass, GbSearchDisplay, list_box);
-  GB_WIDGET_CLASS_BIND (klass, GbSearchDisplay, scroller);
-
-  g_type_ensure (GB_TYPE_SCROLLED_WINDOW);
 }
 
 static void
@@ -404,5 +507,20 @@ gb_search_display_init (GbSearchDisplay *self)
 {
   self->priv = gb_search_display_get_instance_private (self);
 
-  gtk_widget_init_template (GTK_WIDGET (self));
+  self->priv->providers = g_array_new (FALSE, FALSE, sizeof (ProviderEntry));
+  g_array_set_clear_func (self->priv->providers,
+                          (GDestroyNotify)provider_entry_destroy);
+
+  self->priv->size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+
+  gtk_orientable_set_orientation (GTK_ORIENTABLE (self),
+                                  GTK_ORIENTATION_VERTICAL);
+
+  self->priv->last_group = g_object_new (GB_TYPE_SEARCH_DISPLAY_GROUP,
+                                         "size-group", self->priv->size_group,
+                                         "visible", TRUE,
+                                         "vexpand", TRUE,
+                                         NULL);
+  gtk_container_add (GTK_CONTAINER (self),
+                     GTK_WIDGET (self->priv->last_group));
 }
diff --git a/src/search/gb-search-display.h b/src/search/gb-search-display.h
index bd4e39d..7958c21 100644
--- a/src/search/gb-search-display.h
+++ b/src/search/gb-search-display.h
@@ -1,6 +1,6 @@
 /* gb-search-display.h
  *
- * Copyright (C) 2014 Christian Hergert <christian hergert me>
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,12 +22,9 @@
 #include <gtk/gtk.h>
 
 #include "gb-search-types.h"
-#include "gb-search-context.h"
-#include "gb-search-result.h"
 
 G_BEGIN_DECLS
 
-#define GB_TYPE_SEARCH_DISPLAY            (gb_search_display_get_type())
 #define GB_SEARCH_DISPLAY(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GB_TYPE_SEARCH_DISPLAY, 
GbSearchDisplay))
 #define GB_SEARCH_DISPLAY_CONST(obj)      (G_TYPE_CHECK_INSTANCE_CAST ((obj), GB_TYPE_SEARCH_DISPLAY, 
GbSearchDisplay const))
 #define GB_SEARCH_DISPLAY_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  GB_TYPE_SEARCH_DISPLAY, 
GbSearchDisplayClass))
@@ -35,13 +32,9 @@ G_BEGIN_DECLS
 #define GB_IS_SEARCH_DISPLAY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  GB_TYPE_SEARCH_DISPLAY))
 #define GB_SEARCH_DISPLAY_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  GB_TYPE_SEARCH_DISPLAY, 
GbSearchDisplayClass))
 
-typedef struct _GbSearchDisplay        GbSearchDisplay;
-typedef struct _GbSearchDisplayClass   GbSearchDisplayClass;
-typedef struct _GbSearchDisplayPrivate GbSearchDisplayPrivate;
-
 struct _GbSearchDisplay
 {
-  GtkBin parent;
+  GtkBox parent;
 
   /*< private >*/
   GbSearchDisplayPrivate *priv;
@@ -49,18 +42,17 @@ struct _GbSearchDisplay
 
 struct _GbSearchDisplayClass
 {
-  GtkBinClass parent;
+  GtkBoxClass parent;
 
   void (*result_activated) (GbSearchDisplay *display,
                             GbSearchResult  *result);
 };
 
-GType            gb_search_display_get_type    (void);
 GtkWidget       *gb_search_display_new         (void);
+void             gb_search_display_activate    (GbSearchDisplay *display);
 GbSearchContext *gb_search_display_get_context (GbSearchDisplay *display);
 void             gb_search_display_set_context (GbSearchDisplay *display,
                                                 GbSearchContext *context);
-void             gb_search_display_activate    (GbSearchDisplay *display);
 
 G_END_DECLS
 
diff --git a/src/search/gb-search-manager.c b/src/search/gb-search-manager.c
index a7c3862..afbf6aa 100644
--- a/src/search/gb-search-manager.c
+++ b/src/search/gb-search-manager.c
@@ -1,6 +1,6 @@
 /* gb-search-manager.c
  *
- * Copyright (C) 2014 Christian Hergert <christian hergert me>
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,13 +16,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#define G_LOG_DOMAIN "search-manager"
-
-#include <glib/gi18n.h>
-
-#include "gb-search-context.h"
 #include "gb-search-manager.h"
-#include "gb-search-provider.h"
 
 struct _GbSearchManagerPrivate
 {
@@ -32,11 +26,11 @@ struct _GbSearchManagerPrivate
 G_DEFINE_TYPE_WITH_PRIVATE (GbSearchManager, gb_search_manager, G_TYPE_OBJECT)
 
 enum {
-  PROP_0,
-  LAST_PROP
+  PROVIDER_ADDED,
+  LAST_SIGNAL
 };
 
-//static GParamSpec *gParamSpecs [LAST_PROP];
+static guint gSignals [LAST_SIGNAL];
 
 GbSearchManager *
 gb_search_manager_new (void)
@@ -44,28 +38,45 @@ gb_search_manager_new (void)
   return g_object_new (GB_TYPE_SEARCH_MANAGER, NULL);
 }
 
-GbSearchManager *
-gb_search_manager_get_default (void)
+GbSearchContext *
+gb_search_manager_search (GbSearchManager *manager,
+                          const GList     *providers,
+                          const gchar     *search_terms)
 {
-  static GbSearchManager *instance;
+  GbSearchContext *context;
+  const GList *iter;
+
+  g_return_val_if_fail (GB_IS_SEARCH_MANAGER (manager), NULL);
+  g_return_val_if_fail (search_terms, NULL);
+
+  if (!providers)
+    providers = manager->priv->providers;
+
+  if (!providers)
+    return NULL;
 
-  if (!instance)
-    instance = gb_search_manager_new ();
+  context = gb_search_context_new ();
 
-  return instance;
+  for (iter = providers; iter; iter = iter->next)
+    gb_search_context_add_provider (context, iter->data, 0);
+
+  return context;
 }
 
-static gint
-sort_provider (gconstpointer a,
-               gconstpointer b)
+/**
+ * gb_search_manager_get_providers:
+ *
+ * Returns the providers attached to the search manager.
+ *
+ * Returns: (transfer container) (element-type GbSearchProvider*): A #GList of
+ *   #GbSearchProvider.
+ */
+GList *
+gb_search_manager_get_providers (GbSearchManager *manager)
 {
-  gint prio1;
-  gint prio2;
-
-  prio1 = gb_search_provider_get_priority ((GbSearchProvider *)a);
-  prio2 = gb_search_provider_get_priority ((GbSearchProvider *)b);
+  g_return_val_if_fail (GB_IS_SEARCH_MANAGER (manager), NULL);
 
-  return prio1 - prio2;
+  return g_list_copy (manager->priv->providers);
 }
 
 void
@@ -75,26 +86,9 @@ gb_search_manager_add_provider (GbSearchManager  *manager,
   g_return_if_fail (GB_IS_SEARCH_MANAGER (manager));
   g_return_if_fail (GB_IS_SEARCH_PROVIDER (provider));
 
-  manager->priv->providers =
-    g_list_sort (g_list_prepend (manager->priv->providers,
-                                 g_object_ref (provider)),
-                 sort_provider);
-}
-
-GbSearchContext *
-gb_search_manager_search (GbSearchManager *manager,
-                          const gchar     *search_text)
-{
-  GbSearchContext *context;
-
-  g_return_val_if_fail (GB_IS_SEARCH_MANAGER (manager), NULL);
-  g_return_val_if_fail (search_text, NULL);
-
-  context = gb_search_context_new (manager->priv->providers, search_text);
-
-  gb_search_context_execute (context);
-
-  return context;
+  manager->priv->providers = g_list_append (manager->priv->providers,
+                                            g_object_ref (provider));
+  g_signal_emit (manager, gSignals [PROVIDER_ADDED], 0, provider);
 }
 
 static void
@@ -103,49 +97,30 @@ gb_search_manager_finalize (GObject *object)
   GbSearchManagerPrivate *priv = GB_SEARCH_MANAGER (object)->priv;
 
   g_list_foreach (priv->providers, (GFunc)g_object_unref, NULL);
-  g_clear_pointer (&priv->providers, g_list_free);
+  g_list_free (priv->providers);
+  priv->providers = NULL;
 
   G_OBJECT_CLASS (gb_search_manager_parent_class)->finalize (object);
 }
 
 static void
-gb_search_manager_get_property (GObject    *object,
-                                guint       prop_id,
-                                GValue     *value,
-                                GParamSpec *pspec)
-{
-  //GbSearchManager *self = GB_SEARCH_MANAGER (object);
-
-  switch (prop_id)
-    {
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-    }
-}
-
-static void
-gb_search_manager_set_property (GObject      *object,
-                                guint         prop_id,
-                                const GValue *value,
-                                GParamSpec   *pspec)
-{
-  //GbSearchManager *self = GB_SEARCH_MANAGER (object);
-
-  switch (prop_id)
-    {
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-    }
-}
-
-static void
 gb_search_manager_class_init (GbSearchManagerClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
   object_class->finalize = gb_search_manager_finalize;
-  object_class->get_property = gb_search_manager_get_property;
-  object_class->set_property = gb_search_manager_set_property;
+
+  gSignals [PROVIDER_ADDED] =
+    g_signal_new ("provider-added",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  g_cclosure_marshal_generic,
+                  G_TYPE_NONE,
+                  1,
+                  GB_TYPE_SEARCH_PROVIDER);
 }
 
 static void
diff --git a/src/search/gb-search-manager.h b/src/search/gb-search-manager.h
index 5c862fe..4683448 100644
--- a/src/search/gb-search-manager.h
+++ b/src/search/gb-search-manager.h
@@ -1,6 +1,6 @@
 /* gb-search-manager.h
  *
- * Copyright (C) 2014 Christian Hergert <christian hergert me>
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -19,13 +19,13 @@
 #ifndef GB_SEARCH_MANAGER_H
 #define GB_SEARCH_MANAGER_H
 
-#include <gio/gio.h>
+#include <glib-object.h>
 
-#include "gb-search-types.h"
+#include "gb-search-context.h"
+#include "gb-search-provider.h"
 
 G_BEGIN_DECLS
 
-#define GB_TYPE_SEARCH_MANAGER            (gb_search_manager_get_type())
 #define GB_SEARCH_MANAGER(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GB_TYPE_SEARCH_MANAGER, 
GbSearchManager))
 #define GB_SEARCH_MANAGER_CONST(obj)      (G_TYPE_CHECK_INSTANCE_CAST ((obj), GB_TYPE_SEARCH_MANAGER, 
GbSearchManager const))
 #define GB_SEARCH_MANAGER_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  GB_TYPE_SEARCH_MANAGER, 
GbSearchManagerClass))
@@ -33,10 +33,6 @@ G_BEGIN_DECLS
 #define GB_IS_SEARCH_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  GB_TYPE_SEARCH_MANAGER))
 #define GB_SEARCH_MANAGER_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  GB_TYPE_SEARCH_MANAGER, 
GbSearchManagerClass))
 
-typedef struct _GbSearchManager        GbSearchManager;
-typedef struct _GbSearchManagerClass   GbSearchManagerClass;
-typedef struct _GbSearchManagerPrivate GbSearchManagerPrivate;
-
 struct _GbSearchManager
 {
   GObject parent;
@@ -50,13 +46,13 @@ struct _GbSearchManagerClass
   GObjectClass parent;
 };
 
-GType            gb_search_manager_get_type     (void);
-GbSearchManager *gb_search_manager_new          (void);
-GbSearchManager *gb_search_manager_get_default  (void);
-void             gb_search_manager_add_provider (GbSearchManager  *manager,
-                                                 GbSearchProvider *provider);
-GbSearchContext *gb_search_manager_search       (GbSearchManager  *manager,
-                                                 const gchar      *search_text);
+GbSearchManager *gb_search_manager_new           (void);
+GList           *gb_search_manager_get_providers (GbSearchManager  *manager);
+void             gb_search_manager_add_provider  (GbSearchManager  *manager,
+                                                  GbSearchProvider *provider);
+GbSearchContext *gb_search_manager_search        (GbSearchManager  *manager,
+                                                  const GList      *providers,
+                                                  const gchar      *search_terms);
 
 G_END_DECLS
 
diff --git a/src/search/gb-search-provider.c b/src/search/gb-search-provider.c
index d43cab8..345ddff 100644
--- a/src/search/gb-search-provider.c
+++ b/src/search/gb-search-provider.c
@@ -1,6 +1,6 @@
 /* gb-search-provider.c
  *
- * Copyright (C) 2014 Christian Hergert <christian hergert me>
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,54 +16,121 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#include "gb-search-context.h"
+#include <glib/gi18n.h>
+
 #include "gb-search-provider.h"
 
-G_DEFINE_INTERFACE (GbSearchProvider, gb_search_provider, G_TYPE_OBJECT)
+G_DEFINE_ABSTRACT_TYPE (GbSearchProvider, gb_search_provider, G_TYPE_OBJECT)
 
-static void
-gb_search_provider_default_init (GbSearchProviderInterface *iface)
+const gchar *
+gb_search_provider_get_verb (GbSearchProvider *provider)
 {
+  g_return_val_if_fail (GB_IS_SEARCH_PROVIDER (provider), NULL);
+
+  if (GB_SEARCH_PROVIDER_GET_CLASS (provider)->get_verb)
+    return GB_SEARCH_PROVIDER_GET_CLASS (provider)->get_verb (provider);
+
+  return NULL;
 }
 
-/**
- * gb_search_provider_get_priority:
- * @provider: A #GbSearchProvider
- *
- * Retrieves the priority of the search provider. Lower integral values are
- * of higher priority.
- *
- * Returns: An integer.
- */
 gint
 gb_search_provider_get_priority (GbSearchProvider *provider)
 {
-  g_return_val_if_fail (GB_IS_SEARCH_PROVIDER (provider), 0);
+  g_return_val_if_fail (GB_IS_SEARCH_PROVIDER (provider), NULL);
+
+  if (GB_SEARCH_PROVIDER_GET_CLASS (provider)->get_priority)
+    return GB_SEARCH_PROVIDER_GET_CLASS (provider)->get_priority (provider);
 
-  if (GB_SEARCH_PROVIDER_GET_INTERFACE (provider)->get_priority)
-    return GB_SEARCH_PROVIDER_GET_INTERFACE (provider)->get_priority (provider);
-  return 0;
+  return G_MAXINT;
+}
+
+gunichar
+gb_search_provider_get_prefix (GbSearchProvider *provider)
+{
+  g_return_val_if_fail (GB_IS_SEARCH_PROVIDER (provider), NULL);
+
+  if (GB_SEARCH_PROVIDER_GET_CLASS (provider)->get_prefix)
+    return GB_SEARCH_PROVIDER_GET_CLASS (provider)->get_prefix (provider);
+
+  return '\0';
 }
 
-/**
- * gb_search_provider_populate:
- * @provider: A #GbSearchProvider
- * @context: A #GbSearchContext
- * @cancelalble: An optional #GCancellable to cancel the request.
- *
- * Requests that a search provider start populating @context with results.
- * If @cancellable is not %NULL, then it may be used to cancel the request.
- */
 void
 gb_search_provider_populate (GbSearchProvider *provider,
                              GbSearchContext  *context,
+                             const gchar      *search_terms,
+                             gsize             max_results,
                              GCancellable     *cancellable)
 {
   g_return_if_fail (GB_IS_SEARCH_PROVIDER (provider));
   g_return_if_fail (GB_IS_SEARCH_CONTEXT (context));
+  g_return_if_fail (search_terms);
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  if (GB_SEARCH_PROVIDER_GET_INTERFACE (provider)->populate)
-    GB_SEARCH_PROVIDER_GET_INTERFACE (provider)->populate (provider, context,
-                                                           cancellable);
+  if (GB_SEARCH_PROVIDER_GET_CLASS (provider)->populate)
+    {
+      GB_SEARCH_PROVIDER_GET_CLASS (provider)->populate (provider,
+                                                         context,
+                                                         search_terms,
+                                                         max_results,
+                                                         cancellable);
+      return;
+    }
+
+  g_warning ("%s does not implement populate vfunc",
+             g_type_name (G_TYPE_FROM_INSTANCE (provider)));
+}
+
+static void
+gb_search_provider_finalize (GObject *object)
+{
+  GbSearchProviderPrivate *priv = GB_SEARCH_PROVIDER (object)->priv;
+
+  G_OBJECT_CLASS (gb_search_provider_parent_class)->finalize (object);
+}
+
+static void
+gb_search_provider_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  GbSearchProvider *self = GB_SEARCH_PROVIDER (object);
+
+  switch (prop_id)
+    {
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_search_provider_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  GbSearchProvider *self = GB_SEARCH_PROVIDER (object);
+
+  switch (prop_id)
+    {
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_search_provider_class_init (GbSearchProviderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gb_search_provider_finalize;
+  object_class->get_property = gb_search_provider_get_property;
+  object_class->set_property = gb_search_provider_set_property;
+}
+
+static void
+gb_search_provider_init (GbSearchProvider *self)
+{
+  self->priv = gb_search_provider_get_instance_private (self);
 }
diff --git a/src/search/gb-search-provider.h b/src/search/gb-search-provider.h
index f209d52..78300e3 100644
--- a/src/search/gb-search-provider.h
+++ b/src/search/gb-search-provider.h
@@ -1,6 +1,6 @@
 /* gb-search-provider.h
  *
- * Copyright (C) 2014 Christian Hergert <christian hergert me>
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,30 +21,47 @@
 
 #include <gio/gio.h>
 
-#include "gb-search-types.h"
+#include "gb-search-context.h"
 
 G_BEGIN_DECLS
 
-#define GB_TYPE_SEARCH_PROVIDER               (gb_search_provider_get_type ())
-#define GB_SEARCH_PROVIDER(obj)               (G_TYPE_CHECK_INSTANCE_CAST ((obj),    
GB_TYPE_SEARCH_PROVIDER, GbSearchProvider))
-#define GB_IS_SEARCH_PROVIDER(obj)            (G_TYPE_CHECK_INSTANCE_TYPE ((obj),    
GB_TYPE_SEARCH_PROVIDER))
-#define GB_SEARCH_PROVIDER_GET_INTERFACE(obj) (G_TYPE_INSTANCE_GET_INTERFACE ((obj), 
GB_TYPE_SEARCH_PROVIDER, GbSearchProviderInterface))
+#define GB_SEARCH_PROVIDER(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GB_TYPE_SEARCH_PROVIDER, 
GbSearchProvider))
+#define GB_SEARCH_PROVIDER_CONST(obj)      (G_TYPE_CHECK_INSTANCE_CAST ((obj), GB_TYPE_SEARCH_PROVIDER, 
GbSearchProvider const))
+#define GB_SEARCH_PROVIDER_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  GB_TYPE_SEARCH_PROVIDER, 
GbSearchProviderClass))
+#define GB_IS_SEARCH_PROVIDER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GB_TYPE_SEARCH_PROVIDER))
+#define GB_IS_SEARCH_PROVIDER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  GB_TYPE_SEARCH_PROVIDER))
+#define GB_SEARCH_PROVIDER_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  GB_TYPE_SEARCH_PROVIDER, 
GbSearchProviderClass))
 
-struct _GbSearchProviderInterface
+struct _GbSearchProvider
 {
-  GTypeInterface parent;
+  GObject parent;
 
-  gint  (*get_priority) (GbSearchProvider *provider);
-  void  (*populate)     (GbSearchProvider *provider,
-                         GbSearchContext  *context,
-                         GCancellable     *cancellable);
+  /*< private >*/
+  GbSearchProviderPrivate *priv;
 };
 
-GType gb_search_provider_get_type     (void);
-gint  gb_search_provider_get_priority (GbSearchProvider *provider);
-void  gb_search_provider_populate     (GbSearchProvider *provider,
-                                       GbSearchContext  *context,
-                                       GCancellable     *cancellable);
+struct _GbSearchProviderClass
+{
+  GObjectClass parent;
+
+  gunichar     (*get_prefix)   (GbSearchProvider *provider);
+  gint         (*get_priority) (GbSearchProvider *provider);
+  const gchar *(*get_verb)     (GbSearchProvider *provider);
+  void         (*populate)     (GbSearchProvider *provider,
+                                GbSearchContext  *context,
+                                const gchar      *search_terms,
+                                gsize             max_results,
+                                GCancellable     *cancellable);
+};
+
+gunichar     gb_search_provider_get_prefix   (GbSearchProvider *provider);
+gint         gb_search_provider_get_priority (GbSearchProvider *provider);
+const gchar *gb_search_provider_get_verb     (GbSearchProvider *provider);
+void         gb_search_provider_populate     (GbSearchProvider *provider,
+                                              GbSearchContext  *context,
+                                              const gchar      *search_terms,
+                                              gsize             max_results,
+                                              GCancellable     *cancellable);
 
 G_END_DECLS
 
diff --git a/src/search/gb-search-reducer.c b/src/search/gb-search-reducer.c
new file mode 100644
index 0000000..a20b84e
--- /dev/null
+++ b/src/search/gb-search-reducer.c
@@ -0,0 +1,98 @@
+/* gb-search-reducer.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "gb-search-context.h"
+#include "gb-search-provider.h"
+#include "gb-search-reducer.h"
+#include "gb-search-result.h"
+
+void
+gb_search_reducer_init (GbSearchReducer  *reducer,
+                        GbSearchContext  *context,
+                        GbSearchProvider *provider)
+{
+  g_return_if_fail (reducer);
+  g_return_if_fail (GB_IS_SEARCH_CONTEXT (context));
+  g_return_if_fail (GB_IS_SEARCH_PROVIDER (provider));
+
+  reducer->context = context;
+  reducer->provider = provider;
+  reducer->sequence = g_sequence_new (g_object_unref);
+  reducer->max_results = 15;
+  reducer->count = 0;
+}
+
+void
+gb_search_reducer_destroy (GbSearchReducer *reducer)
+{
+  g_return_if_fail (reducer);
+
+  g_sequence_free (reducer->sequence);
+}
+
+void
+gb_search_reducer_push (GbSearchReducer *reducer,
+                        GbSearchResult  *result)
+{
+  g_return_if_fail (reducer);
+  g_return_if_fail (GB_IS_SEARCH_RESULT (result));
+
+  if (reducer->max_results <= g_sequence_get_length (reducer->sequence))
+    {
+      GSequenceIter *iter;
+      GbSearchResult *lowest;
+
+      /* Remove lowest score */
+      iter = g_sequence_get_begin_iter (reducer->sequence);
+      lowest = g_sequence_get (iter);
+      gb_search_context_remove_result (reducer->context, reducer->provider,
+                                       lowest);
+      g_sequence_remove (iter);
+    }
+
+  g_sequence_insert_sorted (reducer->sequence,
+                            g_object_ref (result),
+                            (GCompareDataFunc)gb_search_result_compare,
+                            NULL);
+  gb_search_context_add_result (reducer->context, reducer->provider, result);
+}
+
+gboolean
+gb_search_reducer_accepts (GbSearchReducer *reducer,
+                           gfloat           score)
+{
+  GSequenceIter *iter;
+
+  g_return_val_if_fail (reducer, FALSE);
+
+  if (g_sequence_get_length (reducer->sequence) < reducer->max_results)
+    return TRUE;
+
+  iter = g_sequence_get_begin_iter (reducer->sequence);
+
+  if (iter)
+    {
+      GbSearchResult *result;
+
+      result = g_sequence_get (iter);
+      if (result)
+        return score > gb_search_result_get_score (result);
+    }
+
+  return FALSE;
+}
diff --git a/src/search/gb-search-reducer.h b/src/search/gb-search-reducer.h
new file mode 100644
index 0000000..461ee02
--- /dev/null
+++ b/src/search/gb-search-reducer.h
@@ -0,0 +1,49 @@
+/* gb-search-reducer.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GB_SEARCH_REDUCER_H
+#define GB_SEARCH_REDUCER_H
+
+#include <glib.h>
+
+#include "gb-search-types.h"
+
+G_BEGIN_DECLS
+
+typedef struct
+{
+  GbSearchContext  *context;
+  GbSearchProvider *provider;
+  GSequence        *sequence;
+  gsize             max_results;
+  gsize             count;
+} GbSearchReducer;
+
+void     gb_search_reducer_init    (GbSearchReducer  *reducer,
+                                    GbSearchContext  *context,
+                                    GbSearchProvider *provider);
+gboolean gb_search_reducer_accepts (GbSearchReducer  *reducer,
+                                    gfloat            score);
+void     gb_search_reducer_push    (GbSearchReducer  *reducer,
+                                    GbSearchResult   *result);
+void     gb_search_reducer_destroy (GbSearchReducer  *reducer);
+
+
+G_END_DECLS
+
+#endif /* GB_SEARCH_REDUCER_H */
diff --git a/src/search/gb-search-result.c b/src/search/gb-search-result.c
index 3de3c56..da9fe60 100644
--- a/src/search/gb-search-result.c
+++ b/src/search/gb-search-result.c
@@ -1,6 +1,6 @@
 /* gb-search-result.c
  *
- * Copyright (C) 2014 Christian Hergert <christian hergert me>
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,86 +22,90 @@
 
 struct _GbSearchResultPrivate
 {
-  gint   priority;
-  gfloat score;
+  gchar  *markup;
+  gfloat  score;
 };
 
-G_DEFINE_TYPE_WITH_PRIVATE (GbSearchResult, gb_search_result, GTK_TYPE_BIN)
+G_DEFINE_TYPE_WITH_PRIVATE (GbSearchResult, gb_search_result, G_TYPE_OBJECT)
 
 enum {
   PROP_0,
+  PROP_MARKUP,
   PROP_SCORE,
   LAST_PROP
 };
 
-enum {
-  ACTIVATE,
-  LAST_SIGNAL
-};
-
 static GParamSpec *gParamSpecs [LAST_PROP];
-static guint       gSignals [LAST_SIGNAL];
 
-GtkWidget *
-gb_search_result_new (void)
+gint
+gb_search_result_compare (const GbSearchResult *a,
+                          const GbSearchResult *b)
 {
-  return g_object_new (GB_TYPE_SEARCH_RESULT, NULL);
+  if (a->priv->score < b->priv->score)
+    return -1;
+  else if (a->priv->score > b->priv->score)
+    return 1;
+  else
+    return 0;
 }
 
-gfloat
-gb_search_result_get_score (GbSearchResult *result)
+GbSearchResult *
+gb_search_result_new (const gchar *markup,
+                      gfloat       score)
 {
-  g_return_val_if_fail (GB_IS_SEARCH_RESULT (result), 0.0f);
+  return g_object_new (GB_TYPE_SEARCH_RESULT,
+                       "markup", markup,
+                       "score", score,
+                       NULL);
+}
 
-  return result->priv->score;
+const gchar *
+gb_search_result_get_markup (GbSearchResult *result)
+{
+  g_return_val_if_fail (GB_IS_SEARCH_RESULT (result), NULL);
+
+  return result->priv->markup;
 }
 
-void
-gb_search_result_set_score (GbSearchResult *result,
-                            gfloat          score)
+static void
+gb_search_result_set_markup (GbSearchResult *result,
+                             const gchar    *markup)
 {
   g_return_if_fail (GB_IS_SEARCH_RESULT (result));
 
-  if (result->priv->score != score)
+  if (result->priv->markup != markup)
     {
-      result->priv->score = score;
-      g_object_notify_by_pspec (G_OBJECT (result),
-                                gParamSpecs [PROP_SCORE]);
+      g_free (result->priv->markup);
+      result->priv->markup = g_strdup (markup);
     }
 }
 
-gint
-gb_search_result_compare_func (gconstpointer result1,
-                               gconstpointer result2)
+gfloat
+gb_search_result_get_score (GbSearchResult *result)
 {
-  const GbSearchResult *r1 = result1;
-  const GbSearchResult *r2 = result2;
-  gint ret;
-
-  ret = r2->priv->priority - r1->priv->priority;
-
-  if (ret == 0)
-    {
-      if (r2->priv->score > r1->priv->score)
-        return 1;
-      else if (r2->priv->score < r1->priv->score)
-        return -1;
-    }
+  g_return_val_if_fail (GB_IS_SEARCH_RESULT (result), 0.0f);
 
-  return ret;
+  return result->priv->score;
 }
 
-void
-gb_search_result_activate (GbSearchResult *result)
+static void
+gb_search_result_set_score (GbSearchResult *result,
+                            gfloat          score)
 {
   g_return_if_fail (GB_IS_SEARCH_RESULT (result));
+  g_return_if_fail (score >= 0.0);
+  g_return_if_fail (score <= 1.0);
 
-  g_signal_emit (result, gSignals [ACTIVATE], 0);
+  result->priv->score = score;
 }
 
 static void
 gb_search_result_finalize (GObject *object)
 {
+  GbSearchResultPrivate *priv = GB_SEARCH_RESULT (object)->priv;
+
+  g_clear_pointer (&priv->markup, g_free);
+
   G_OBJECT_CLASS (gb_search_result_parent_class)->finalize (object);
 }
 
@@ -115,6 +119,10 @@ gb_search_result_get_property (GObject    *object,
 
   switch (prop_id)
     {
+    case PROP_MARKUP:
+      g_value_set_string (value, gb_search_result_get_markup (self));
+      break;
+
     case PROP_SCORE:
       g_value_set_float (value, gb_search_result_get_score (self));
       break;
@@ -134,6 +142,10 @@ gb_search_result_set_property (GObject      *object,
 
   switch (prop_id)
     {
+    case PROP_MARKUP:
+      gb_search_result_set_markup (self, g_value_get_string (value));
+      break;
+
     case PROP_SCORE:
       gb_search_result_set_score (self, g_value_get_float (value));
       break;
@@ -152,27 +164,29 @@ gb_search_result_class_init (GbSearchResultClass *klass)
   object_class->get_property = gb_search_result_get_property;
   object_class->set_property = gb_search_result_set_property;
 
+  gParamSpecs [PROP_MARKUP] =
+    g_param_spec_string ("markup",
+                         _("Markup"),
+                         _("The pango markup to be rendered."),
+                         NULL,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+  g_object_class_install_property (object_class, PROP_MARKUP,
+                                   gParamSpecs [PROP_MARKUP]);
+
   gParamSpecs [PROP_SCORE] =
     g_param_spec_float ("score",
                         _("Score"),
-                        _("The score for the result."),
+                        _("The result match score."),
                         0.0f,
                         1.0f,
                         0.0f,
-                        (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                        (G_PARAM_READWRITE |
+                         G_PARAM_CONSTRUCT_ONLY |
+                         G_PARAM_STATIC_STRINGS));
   g_object_class_install_property (object_class, PROP_SCORE,
                                    gParamSpecs [PROP_SCORE]);
-
-  gSignals [ACTIVATE] =
-    g_signal_new ("activate",
-                  G_TYPE_FROM_CLASS (klass),
-                  G_SIGNAL_RUN_LAST,
-                  G_STRUCT_OFFSET (GbSearchResultClass, activate),
-                  NULL,
-                  NULL,
-                  g_cclosure_marshal_VOID__VOID,
-                  G_TYPE_NONE,
-                  0);
 }
 
 static void
diff --git a/src/search/gb-search-result.h b/src/search/gb-search-result.h
index db24fec..63515a2 100644
--- a/src/search/gb-search-result.h
+++ b/src/search/gb-search-result.h
@@ -1,6 +1,6 @@
 /* gb-search-result.h
  *
- * Copyright (C) 2014 Christian Hergert <christian hergert me>
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -19,13 +19,12 @@
 #ifndef GB_SEARCH_RESULT_H
 #define GB_SEARCH_RESULT_H
 
-#include <gtk/gtk.h>
+#include <glib-object.h>
 
 #include "gb-search-types.h"
 
 G_BEGIN_DECLS
 
-#define GB_TYPE_SEARCH_RESULT            (gb_search_result_get_type())
 #define GB_SEARCH_RESULT(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GB_TYPE_SEARCH_RESULT, 
GbSearchResult))
 #define GB_SEARCH_RESULT_CONST(obj)      (G_TYPE_CHECK_INSTANCE_CAST ((obj), GB_TYPE_SEARCH_RESULT, 
GbSearchResult const))
 #define GB_SEARCH_RESULT_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  GB_TYPE_SEARCH_RESULT, 
GbSearchResultClass))
@@ -35,7 +34,7 @@ G_BEGIN_DECLS
 
 struct _GbSearchResult
 {
-  GtkBin parent;
+  GObject parent;
 
   /*< private >*/
   GbSearchResultPrivate *priv;
@@ -43,19 +42,15 @@ struct _GbSearchResult
 
 struct _GbSearchResultClass
 {
-  GtkBinClass parent;
-
-  void (*activate) (GbSearchResult *result);
+  GObjectClass parent;
 };
 
-void       gb_search_result_activate     (GbSearchResult *result);
-gint       gb_search_result_compare_func (gconstpointer   result1,
-                                          gconstpointer   result2);
-GType      gb_search_result_get_type     (void);
-GtkWidget *gb_search_result_new          (void);
-gfloat     gb_search_result_get_score    (GbSearchResult *result);
-void       gb_search_result_set_score    (GbSearchResult *result,
-                                          gfloat          score);
+GbSearchResult *gb_search_result_new        (const gchar          *markup,
+                                             gfloat                score);
+gfloat          gb_search_result_get_score  (GbSearchResult       *result);
+const gchar    *gb_search_result_get_markup (GbSearchResult       *result);
+gint            gb_search_result_compare    (const GbSearchResult *a,
+                                             const GbSearchResult *b);
 
 G_END_DECLS
 
diff --git a/src/search/gb-search-types.h b/src/search/gb-search-types.h
index d6f8cd2..160c1c8 100644
--- a/src/search/gb-search-types.h
+++ b/src/search/gb-search-types.h
@@ -1,6 +1,6 @@
 /* gb-search-types.h
  *
- * Copyright (C) 2014 Christian Hergert <christian hergert me>
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -23,16 +23,43 @@
 
 G_BEGIN_DECLS
 
-typedef struct _GbSearchContext           GbSearchContext;
-typedef struct _GbSearchContextClass      GbSearchContextClass;
-typedef struct _GbSearchContextPrivate    GbSearchContextPrivate;
+#define GB_TYPE_SEARCH_CONTEXT       (gb_search_context_get_type())
+#define GB_TYPE_SEARCH_DISPLAY       (gb_search_display_get_type())
+#define GB_TYPE_SEARCH_DISPLAY_GROUP (gb_search_display_group_get_type())
+#define GB_TYPE_SEARCH_MANAGER       (gb_search_manager_get_type())
+#define GB_TYPE_SEARCH_PROVIDER      (gb_search_provider_get_type())
+#define GB_TYPE_SEARCH_RESULT        (gb_search_result_get_type())
 
-typedef struct _GbSearchProvider          GbSearchProvider;
-typedef struct _GbSearchProviderInterface GbSearchProviderInterface;
+typedef struct _GbSearchContext         GbSearchContext;
+typedef struct _GbSearchContextClass    GbSearchContextClass;
+typedef struct _GbSearchContextPrivate  GbSearchContextPrivate;
 
-typedef struct _GbSearchResult            GbSearchResult;
-typedef struct _GbSearchResultClass       GbSearchResultClass;
-typedef struct _GbSearchResultPrivate     GbSearchResultPrivate;
+typedef struct _GbSearchDisplay        GbSearchDisplay;
+typedef struct _GbSearchDisplayClass   GbSearchDisplayClass;
+typedef struct _GbSearchDisplayPrivate GbSearchDisplayPrivate;
+
+typedef struct _GbSearchDisplayGroup        GbSearchDisplayGroup;
+typedef struct _GbSearchDisplayGroupClass   GbSearchDisplayGroupClass;
+typedef struct _GbSearchDisplayGroupPrivate GbSearchDisplayGroupPrivate;
+
+typedef struct _GbSearchProvider        GbSearchProvider;
+typedef struct _GbSearchProviderClass   GbSearchProviderClass;
+typedef struct _GbSearchProviderPrivate GbSearchProviderPrivate;
+
+typedef struct _GbSearchManager         GbSearchManager;
+typedef struct _GbSearchManagerClass    GbSearchManagerClass;
+typedef struct _GbSearchManagerPrivate  GbSearchManagerPrivate;
+
+typedef struct _GbSearchResult          GbSearchResult;
+typedef struct _GbSearchResultClass     GbSearchResultClass;
+typedef struct _GbSearchResultPrivate   GbSearchResultPrivate;
+
+GType gb_search_context_get_type       (void);
+GType gb_search_display_get_type       (void);
+GType gb_search_display_group_get_type (void);
+GType gb_search_manager_get_type       (void);
+GType gb_search_provider_get_type      (void);
+GType gb_search_result_get_type        (void);
 
 G_END_DECLS
 


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