[gnome-shell] Add search.js, rebase search system on top



commit b7646d18ae34a38dd3103421cfbff053e907cfa1
Author: Colin Walters <walters verbum org>
Date:   Sun Nov 29 17:45:30 2009 -0500

    Add search.js, rebase search system on top
    
    The high level goal is to separate the concern of searching for
    things with display of those things; for example in newer mockups,
    applications are displayed exactly the same as they look in the
    AppWell.
    
    Another goal was optimizing for speed; for example,
    application search was pushed mostly down into C, and we avoid
    lowercasing and normalizing every item over and over.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=603523

 data/theme/gnome-shell.css |   34 ++-
 js/misc/docInfo.js         |   57 +++++-
 js/ui/appDisplay.js        |   92 +++++++-
 js/ui/dash.js              |  521 ++++++++++++++++++++++++--------------------
 js/ui/docDisplay.js        |   50 ++++-
 js/ui/overview.js          |    1 +
 js/ui/placeDisplay.js      |  228 +++++++++----------
 js/ui/search.js            |  272 +++++++++++++++++++++++
 src/shell-app-system.c     |  443 ++++++++++++++++++++++++--------------
 src/shell-app-system.h     |   23 +-
 10 files changed, 1159 insertions(+), 562 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 8b42630..7fc91ff 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -152,17 +152,6 @@ StTooltip {
     spacing: 12px;
 }
 
-.dash-search-section-header {
-    padding: 6px 0px;
-    spacing: 4px;
-    font-size: 12px;
-    color: #bbbbbb;
-}
-
-.dash-search-section-title, dash-search-section-count {
-    font-weight: bold;
-}
-
 #searchEntry {
     padding: 4px;
     border-bottom: 1px solid #262626;
@@ -237,6 +226,29 @@ StTooltip {
     height: 16px;
 }
 
+.dash-search-section-header {
+    padding: 6px 0px;
+    spacing: 4px;
+}
+
+.dash-search-section-results {
+    color: #ffffff;
+    padding-left: 4px;
+}
+
+.dash-search-section-list-results {
+    spacing: 4px;
+}
+
+.dash-search-result-content {
+    padding: 2px;
+}
+
+.dash-search-result-content:selected {
+    padding: 1px;
+    border: 1px solid #262626;
+}
+
 /* GenericDisplay */
 
 .generic-display-container {
diff --git a/js/misc/docInfo.js b/js/misc/docInfo.js
index e60d7ca..559e9ec 100644
--- a/js/misc/docInfo.js
+++ b/js/misc/docInfo.js
@@ -7,6 +7,7 @@ const Shell = imports.gi.Shell;
 
 const Lang = imports.lang;
 const Signals = imports.signals;
+const Search = imports.ui.search;
 const Main = imports.ui.main;
 
 const THUMBNAIL_ICON_MARGIN = 2;
@@ -23,6 +24,7 @@ DocInfo.prototype = {
         // correctly. See http://bugzilla.gnome.org/show_bug.cgi?id=567094
         this.timestamp = recentInfo.get_modified().getTime() / 1000;
         this.name = recentInfo.get_display_name();
+        this._lowerName = this.name.toLowerCase();
         this.uri = recentInfo.get_uri();
         this.mimeType = recentInfo.get_mime_type();
     },
@@ -35,8 +37,24 @@ DocInfo.prototype = {
         Shell.DocSystem.get_default().open(this.recentInfo);
     },
 
-    exists : function() {
-        return this.recentInfo.exists();
+    matchTerms: function(terms) {
+        let mtype = Search.MatchType.NONE;
+        for (let i = 0; i < terms.length; i++) {
+            let term = terms[i];
+            let idx = this._lowerName.indexOf(term);
+            if (idx == 0) {
+                if (mtype != Search.MatchType.NONE)
+                    return Search.MatchType.MULTIPLE;
+                mtype = Search.MatchType.PREFIX;
+            } else if (idx > 0) {
+                if (mtype != Search.MatchType.NONE)
+                    return Search.MatchType.MULTIPLE;
+                mtype = Search.MatchType.SUBSTRING;
+            } else {
+                continue;
+            }
+        }
+        return mtype;
     }
 };
 
@@ -93,6 +111,41 @@ DocManager.prototype = {
 
     queueExistenceCheck: function(count) {
         return this._docSystem.queue_existence_check(count);
+    },
+
+    initialSearch: function(terms) {
+        let multipleMatches = [];
+        let prefixMatches = [];
+        let substringMatches = [];
+        for (let i = 0; i < this._infosByTimestamp.length; i++) {
+            let item = this._infosByTimestamp[i];
+            let mtype = item.matchTerms(terms);
+            if (mtype == Search.MatchType.MULTIPLE)
+                multipleMatches.push(item.uri);
+            else if (mtype == Search.MatchType.PREFIX)
+                prefixMatches.push(item.uri);
+            else if (mtype == Search.MatchType.SUBSTRING)
+                substringMatches.push(item.uri);
+         }
+        return multipleMatches.concat(prefixMatches.concat(substringMatches));
+    },
+
+    subsearch: function(previousResults, terms) {
+        let multipleMatches = [];
+        let prefixMatches = [];
+        let substringMatches = [];
+        for (let i = 0; i < previousResults.length; i++) {
+            let uri = previousResults[i];
+            let item = this._infosByUri[uri];
+            let mtype = item.matchTerms(terms);
+            if (mtype == Search.MatchType.MULTIPLE)
+                multipleMatches.push(uri);
+            else if (mtype == Search.MatchType.PREFIX)
+                prefixMatches.push(uri);
+            else if (mtype == Search.MatchType.SUBSTRING)
+                substringMatches.push(uri);
+        }
+        return multipleMatches.concat(prefixMatches.concat(substringMatches));
     }
 }
 
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index 651ab3a..e7c372a 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -18,6 +18,7 @@ const AppFavorites = imports.ui.appFavorites;
 const DND = imports.ui.dnd;
 const GenericDisplay = imports.ui.genericDisplay;
 const Main = imports.ui.main;
+const Search = imports.ui.search;
 const Workspaces = imports.ui.workspaces;
 
 const APPICON_SIZE = 48;
@@ -134,17 +135,10 @@ AppDisplay.prototype = {
                 this._addApp(app);
             }
         } else {
-            // Loop over the toplevel menu items, load the set of desktop file ids
-            // associated with each one.
-            let allMenus = this._appSystem.get_menus();
-            for (let i = 0; i < allMenus.length; i++) {
-                let menu = allMenus[i];
-                let menuApps = this._appSystem.get_applications_for_menu(menu.id);
-
-                for (let j = 0; j < menuApps.length; j++) {
-                    let app = menuApps[j];
-                    this._addApp(app);
-                }
+            let apps = this._appSystem.get_flattened_apps();
+            for (let i = 0; i < apps.length; i++) {
+                let app = apps[i];
+                this._addApp(app);
             }
         }
 
@@ -220,6 +214,82 @@ AppDisplay.prototype = {
 
 Signals.addSignalMethods(AppDisplay.prototype);
 
+function BaseAppSearchProvider() {
+    this._init();
+}
+
+BaseAppSearchProvider.prototype = {
+    __proto__: Search.SearchProvider.prototype,
+
+    _init: function(name) {
+        Search.SearchProvider.prototype._init.call(this, name);
+        this._appSys = Shell.AppSystem.get_default();
+    },
+
+    getResultMeta: function(resultId) {
+        let app = this._appSys.get_app(resultId);
+        if (!app)
+            return null;
+        return { 'id': resultId,
+                 'name': app.get_name(),
+                 'icon': app.create_icon_texture(Search.RESULT_ICON_SIZE)};
+    },
+
+    activateResult: function(id) {
+        let app = this._appSys.get_app(id);
+        app.launch();
+    }
+};
+
+function AppSearchProvider() {
+    this._init();
+}
+
+AppSearchProvider.prototype = {
+    __proto__: BaseAppSearchProvider.prototype,
+
+    _init: function() {
+         BaseAppSearchProvider.prototype._init.call(this, _("APPLICATIONS"));
+    },
+
+    getInitialResultSet: function(terms) {
+        return this._appSys.initial_search(false, terms);
+    },
+
+    getSubsearchResultSet: function(previousResults, terms) {
+        return this._appSys.subsearch(false, previousResults, terms);
+    },
+
+    expandSearch: function(terms) {
+        log("TODO expand search");
+    }
+}
+
+function PrefsSearchProvider() {
+    this._init();
+}
+
+PrefsSearchProvider.prototype = {
+    __proto__: BaseAppSearchProvider.prototype,
+
+    _init: function() {
+        BaseAppSearchProvider.prototype._init.call(this, _("PREFERENCES"));
+    },
+
+    getInitialResultSet: function(terms) {
+        return this._appSys.initial_search(true, terms);
+    },
+
+    getSubsearchResultSet: function(previousResults, terms) {
+        return this._appSys.subsearch(true, previousResults, terms);
+    },
+
+    expandSearch: function(terms) {
+        let controlCenter = this._appSys.load_from_desktop_file('gnomecc.desktop');
+        controlCenter.launch();
+        Main.overview.hide();
+    }
+}
 
 function AppIcon(app) {
     this._init(app);
diff --git a/js/ui/dash.js b/js/ui/dash.js
index 1fc2184..3fe7e95 100644
--- a/js/ui/dash.js
+++ b/js/ui/dash.js
@@ -17,6 +17,10 @@ const DocDisplay = imports.ui.docDisplay;
 const PlaceDisplay = imports.ui.placeDisplay;
 const GenericDisplay = imports.ui.genericDisplay;
 const Main = imports.ui.main;
+const Search = imports.ui.search;
+
+// 25 search results (per result type) should be enough for everyone
+const MAX_RENDERED_SEARCH_RESULTS = 25;
 
 const DEFAULT_PADDING = 4;
 const DEFAULT_SPACING = 4;
@@ -332,6 +336,254 @@ SearchEntry.prototype = {
 };
 Signals.addSignalMethods(SearchEntry.prototype);
 
+function SearchResult(provider, metaInfo, terms) {
+    this._init(provider, metaInfo, terms);
+}
+
+SearchResult.prototype = {
+    _init: function(provider, metaInfo, terms) {
+        this.provider = provider;
+        this.metaInfo = metaInfo;
+        this.actor = new St.Clickable({ style_class: 'dash-search-result',
+                                        reactive: true,
+                                        x_align: St.Align.START,
+                                        x_fill: true,
+                                        y_fill: true });
+        this.actor._delegate = this;
+
+        let content = provider.createResultActor(metaInfo, terms);
+        if (content == null) {
+            content = new St.BoxLayout({ style_class: 'dash-search-result-content' });
+            let title = new St.Label({ text: this.metaInfo['name'] });
+            let icon = this.metaInfo['icon'];
+            content.add(icon, { y_fill: false });
+            content.add(title, { expand: true, y_fill: false });
+        }
+        this._content = content;
+        this.actor.set_child(content);
+
+        this.actor.connect('clicked', Lang.bind(this, this._onResultClicked));
+    },
+
+    setSelected: function(selected) {
+        this._content.set_style_pseudo_class(selected ? 'selected' : null);
+    },
+
+    activate: function() {
+        this.provider.activateResult(this.metaInfo.id);
+        Main.overview.toggle();
+    },
+
+    _onResultClicked: function(actor, event) {
+        this.activate();
+    }
+}
+
+function OverflowSearchResults(provider) {
+    this._init(provider);
+}
+
+OverflowSearchResults.prototype = {
+    __proto__: Search.SearchResultDisplay.prototype,
+
+    _init: function(provider) {
+        Search.SearchResultDisplay.prototype._init.call(this, provider);
+        this.actor = new St.OverflowBox({ style_class: 'dash-search-section-list-results' });
+    },
+
+    renderResults: function(results, terms) {
+        for (let i = 0; i < results.length && i < MAX_RENDERED_SEARCH_RESULTS; i++) {
+            let result = results[i];
+            let meta = this.provider.getResultMeta(result);
+            let display = new SearchResult(this.provider, meta, terms);
+            this.actor.add_actor(display.actor);
+        }
+    },
+
+    getVisibleCount: function() {
+        return this.actor.get_n_visible();
+    },
+
+    selectIndex: function(index) {
+        let nVisible = this.actor.get_n_visible();
+        let children = this.actor.get_children();
+        if (this.selectionIndex >= 0) {
+            let prevActor = children[this.selectionIndex];
+            prevActor._delegate.setSelected(false);
+        }
+        this.selectionIndex = -1;
+        if (index >= nVisible)
+            return false;
+        else if (index < 0)
+            return false;
+        let targetActor = children[index];
+        targetActor._delegate.setSelected(true);
+        this.selectionIndex = index;
+        return true;
+    }
+}
+
+function SearchResults(searchSystem) {
+    this._init(searchSystem);
+}
+
+SearchResults.prototype = {
+    _init: function(searchSystem) {
+        this._searchSystem = searchSystem;
+
+        this.actor = new St.BoxLayout({ name: 'dashSearchResults',
+                                        vertical: true });
+        this._searchingNotice = new St.Label({ style_class: 'dash-search-starting',
+                                               text: _("Searching...") });
+        this.actor.add(this._searchingNotice);
+        this._selectedProvider = -1;
+        this._providers = this._searchSystem.getProviders();
+        this._providerMeta = [];
+        for (let i = 0; i < this._providers.length; i++) {
+            let provider = this._providers[i];
+            let providerBox = new St.BoxLayout({ style_class: 'dash-search-section',
+                                                  vertical: true });
+            let titleButton = new St.Button({ style_class: 'dash-search-section-header',
+                                              reactive: true,
+                                              x_fill: true,
+                                              y_fill: true });
+            titleButton.connect('clicked', Lang.bind(this, function () { this._onHeaderClicked(provider); }));
+            providerBox.add(titleButton);
+            let titleBox = new St.BoxLayout();
+            titleButton.set_child(titleBox);
+            let title = new St.Label({ text: provider.title });
+            let count = new St.Label();
+            titleBox.add(title, { expand: true });
+            titleBox.add(count);
+
+            let resultDisplayBin = new St.Bin({ style_class: 'dash-search-section-results',
+                                                x_fill: true,
+                                                y_fill: true });
+            providerBox.add(resultDisplayBin, { expand: true });
+            let resultDisplay = provider.createResultContainerActor();
+            if (resultDisplay == null) {
+                resultDisplay = new OverflowSearchResults(provider);
+            }
+            resultDisplayBin.set_child(resultDisplay.actor);
+
+            this._providerMeta.push({ actor: providerBox,
+                                      resultDisplay: resultDisplay,
+                                      count: count });
+            this.actor.add(providerBox);
+        }
+    },
+
+    _clearDisplay: function() {
+        this._selectedProvider = -1;
+        this._visibleResultsCount = 0;
+        for (let i = 0; i < this._providerMeta.length; i++) {
+            let meta = this._providerMeta[i];
+            meta.resultDisplay.clear();
+            meta.actor.hide();
+        }
+    },
+
+    reset: function() {
+        this._searchSystem.reset();
+        this._searchingNotice.hide();
+        this._clearDisplay();
+    },
+
+    startingSearch: function() {
+        this.reset();
+        this._searchingNotice.show();
+    },
+
+    _metaForProvider: function(provider) {
+        return this._providerMeta[this._providers.indexOf(provider)];
+    },
+
+    updateSearch: function (searchString) {
+        let results = this._searchSystem.updateSearch(searchString);
+
+        this._searchingNotice.hide();
+        this._clearDisplay();
+
+        let terms = this._searchSystem.getTerms();
+
+        for (let i = 0; i < results.length; i++) {
+            let [provider, providerResults] = results[i];
+            let meta = this._metaForProvider(provider);
+            meta.actor.show();
+            meta.resultDisplay.renderResults(providerResults, terms);
+            meta.count.set_text(""+providerResults.length);
+        }
+
+        this.selectDown();
+
+        return true;
+    },
+
+    _onHeaderClicked: function(provider) {
+        provider.expandSearch(this._searchSystem.getTerms());
+    },
+
+    _modifyActorSelection: function(resultDisplay, up) {
+        let success;
+        let index = resultDisplay.getSelectionIndex();
+        if (up && index == -1)
+            index = resultDisplay.getVisibleCount() - 1;
+        else if (up)
+            index = index - 1;
+        else
+            index = index + 1;
+        return resultDisplay.selectIndex(index);
+    },
+
+    selectUp: function() {
+        for (let i = this._selectedProvider; i >= 0; i--) {
+            let meta = this._providerMeta[i];
+            if (!meta.actor.visible)
+                continue;
+            let success = this._modifyActorSelection(meta.resultDisplay, true);
+            if (success) {
+                this._selectedProvider = i;
+                return;
+            }
+        }
+        if (this._providerMeta.length > 0) {
+            this._selectedProvider = this._providerMeta.length - 1;
+            this.selectUp();
+        }
+    },
+
+    selectDown: function() {
+        let current = this._selectedProvider;
+        if (current == -1)
+            current = 0;
+        for (let i = current; i < this._providerMeta.length; i++) {
+            let meta = this._providerMeta[i];
+            if (!meta.actor.visible)
+                continue;
+            let success = this._modifyActorSelection(meta.resultDisplay, false);
+            if (success) {
+                this._selectedProvider = i;
+                return;
+            }
+        }
+        if (this._providerMeta.length > 0) {
+            this._selectedProvider = 0;
+            this.selectDown();
+        }
+    },
+
+    activateSelected: function() {
+        let current = this._selectedProvider;
+        if (current < 0)
+            return;
+        let meta = this._providerMeta[current];
+        let resultDisplay = meta.resultDisplay;
+        let children = resultDisplay.actor.get_children();
+        let targetActor = children[resultDisplay.getSelectionIndex()];
+        targetActor._delegate.activate();
+    }
+}
+
 function MoreLink() {
     this._init();
 }
@@ -500,9 +752,9 @@ Dash.prototype = {
                                         vertical: true,
                                         reactive: true });
 
-        // Size for this one explicitly set from overlay.js
-        this.searchArea = new Big.Box({ y_align: Big.BoxAlignment.CENTER });
-
+        // The searchArea just holds the entry
+        this.searchArea = new St.BoxLayout({ name: "dashSearchArea",
+                                             vertical: true });
         this.sectionArea = new St.BoxLayout({ name: "dashSections",
                                                vertical: true });
 
@@ -517,16 +769,35 @@ Dash.prototype = {
         this._searchActive = false;
         this._searchPending = false;
         this._searchEntry = new SearchEntry();
-        this.searchArea.append(this._searchEntry.actor, Big.BoxPackFlags.EXPAND);
+        this.searchArea.add(this._searchEntry.actor, { y_fill: false, expand: true });
+
+        this._searchSystem = new Search.SearchSystem();
+        this._searchSystem.registerProvider(new AppDisplay.AppSearchProvider());
+        this._searchSystem.registerProvider(new AppDisplay.PrefsSearchProvider());
+        this._searchSystem.registerProvider(new PlaceDisplay.PlaceSearchProvider());
+        this._searchSystem.registerProvider(new DocDisplay.DocSearchProvider());
+
+        this.searchResults = new SearchResults(this._searchSystem);
+        this.actor.add(this.searchResults.actor);
+        this.searchResults.actor.hide();
 
         this._searchTimeoutId = 0;
         this._searchEntry.entry.connect('text-changed', Lang.bind(this, function (se, prop) {
             let text = this._searchEntry.getText();
-            text = text.replace(/^\s+/g, "").replace(/\s+$/g, "")
+            text = text.replace(/^\s+/g, "").replace(/\s+$/g, "");
             let searchPreviouslyActive = this._searchActive;
             this._searchActive = text != '';
             this._searchPending = this._searchActive && !searchPreviouslyActive;
-            this._updateDashActors();
+            if (this._searchPending) {
+                this.searchResults.startingSearch();
+            }
+            if (this._searchActive) {
+                this.searchResults.actor.show();
+                this.sectionArea.hide();
+            } else {
+                this.searchResults.actor.hide();
+                this.sectionArea.show();
+            }
             if (!this._searchActive) {
                 if (this._searchTimeoutId > 0) {
                     Mainloop.source_remove(this._searchTimeoutId);
@@ -543,24 +814,15 @@ Dash.prototype = {
                 Mainloop.source_remove(this._searchTimeoutId);
                 this._doSearch();
             }
-            // Only one of the displays will have an item selected, so it's ok to
-            // call activateSelected() on all of them.
-            for (var i = 0; i < this._searchSections.length; i++) {
-                let section = this._searchSections[i];
-                section.resultArea.display.activateSelected();
-            }
+            this.searchResults.activateSelected();
             return true;
         }));
         this._searchEntry.entry.connect('key-press-event', Lang.bind(this, function (se, e) {
-            let text = this._searchEntry.getText();
             let symbol = e.get_key_symbol();
             if (symbol == Clutter.Escape) {
                 // Escape will keep clearing things back to the desktop.
-                // If we are showing a particular section of search, go back to all sections.
-                if (this._searchResultsSingleShownSection != null)
-                    this._showAllSearchSections();
                 // If we have an active search, we remove it.
-                else if (this._searchActive)
+                if (this._searchActive)
                     this._searchEntry.reset();
                 // Next, if we're in one of the "more" modes or showing the details pane, close them
                 else if (this._activePane != null)
@@ -572,44 +834,14 @@ Dash.prototype = {
             } else if (symbol == Clutter.Up) {
                 if (!this._searchActive)
                     return true;
-                // selectUp and selectDown wrap around in their respective displays
-                // too, but there doesn't seem to be any flickering if we first select
-                // something in one display, but then unset the selection, and move
-                // it to the other display, so it's ok to do that.
-                for (var i = 0; i < this._searchSections.length; i++) {
-                    let section = this._searchSections[i];
-                    if (section.resultArea.display.hasSelected() && !section.resultArea.display.selectUp()) {
-                        if (this._searchResultsSingleShownSection != section.type) {
-                            // We need to move the selection to the next section above this section that has items,
-                            // wrapping around at the bottom, if necessary.
-                            let newSectionIndex = this._findAnotherSectionWithItems(i, -1);
-                            if (newSectionIndex >= 0) {
-                                this._searchSections[newSectionIndex].resultArea.display.selectLastItem();
-                                section.resultArea.display.unsetSelected();
-                            }
-                        }
-                        break;
-                    }
-                }
+                this.searchResults.selectUp();
+
                 return true;
             } else if (symbol == Clutter.Down) {
                 if (!this._searchActive)
                     return true;
-                for (var i = 0; i < this._searchSections.length; i++) {
-                    let section = this._searchSections[i];
-                    if (section.resultArea.display.hasSelected() && !section.resultArea.display.selectDown()) {
-                        if (this._searchResultsSingleShownSection != section.type) {
-                            // We need to move the selection to the next section below this section that has items,
-                            // wrapping around at the top, if necessary.
-                            let newSectionIndex = this._findAnotherSectionWithItems(i, 1);
-                            if (newSectionIndex >= 0) {
-                                this._searchSections[newSectionIndex].resultArea.display.selectFirstItem();
-                                section.resultArea.display.unsetSelected();
-                            }
-                        }
-                        break;
-                    }
-                }
+
+                this.searchResults.selectDown();
                 return true;
             }
             return false;
@@ -666,102 +898,12 @@ Dash.prototype = {
         this._docDisplay.emit('changed');
 
         this.sectionArea.add(this._docsSection.actor, { expand: true });
-
-        /***** Search Results *****/
-
-        this._searchResultsSection = new Section(_("SEARCH RESULTS"), true);
-
-        this._searchResultsSingleShownSection = null;
-
-        this._searchResultsSection.header.connect('back-link-activated', Lang.bind(this, function () {
-            this._showAllSearchSections();
-        }));
-
-        this._searchSections = [
-            { type: APPS,
-              title: _("APPLICATIONS"),
-              header: null,
-              resultArea: null
-            },
-            { type: PREFS,
-              title: _("PREFERENCES"),
-              header: null,
-              resultArea: null
-            },
-            { type: DOCS,
-              title: _("RECENT DOCUMENTS"),
-              header: null,
-              resultArea: null
-            },
-            { type: PLACES,
-              title: _("PLACES"),
-              header: null,
-              resultArea: null
-            }
-        ];
-
-        for (var i = 0; i < this._searchSections.length; i++) {
-            let section = this._searchSections[i];
-            section.header = new SearchSectionHeader(section.title,
-                                                     Lang.bind(this,
-                                                               function () {
-                                                                   this._showSingleSearchSection(section.type);
-                                                               }));
-            this._searchResultsSection.content.add(section.header.actor);
-            section.resultArea = new ResultArea(section.type, GenericDisplay.GenericDisplayFlags.DISABLE_VSCROLLING);
-            this._searchResultsSection.content.add(section.resultArea.actor, { expand: true });
-            createPaneForDetails(this, section.resultArea.display);
-        }
-
-        this.sectionArea.add(this._searchResultsSection.actor, { expand: true });
-        this._searchResultsSection.actor.hide();
     },
 
     _doSearch: function () {
         this._searchTimeoutId = 0;
         let text = this._searchEntry.getText();
-        text = text.replace(/^\s+/g, "").replace(/\s+$/g, "");
-
-        let selectionSet = false;
-
-        for (var i = 0; i < this._searchSections.length; i++) {
-            let section = this._searchSections[i];
-            section.resultArea.display.setSearch(text);
-            let itemCount = section.resultArea.display.getMatchedItemsCount();
-            let itemCountText = itemCount + "";
-            section.header.countText.text = itemCountText;
-
-            if (this._searchResultsSingleShownSection == section.type) {
-                this._searchResultsSection.header.setCountText(itemCountText);
-                if (itemCount == 0) {
-                    section.resultArea.actor.hide();
-                } else {
-                    section.resultArea.actor.show();
-                }
-            } else if (this._searchResultsSingleShownSection == null) {
-                // Don't show the section if it has no results
-                if (itemCount == 0) {
-                    section.header.actor.hide();
-                    section.resultArea.actor.hide();
-                } else {
-                    section.header.actor.show();
-                    section.resultArea.actor.show();
-                }
-            }
-
-            // Refresh the selection when a new search is applied.
-            section.resultArea.display.unsetSelected();
-            if (!selectionSet && section.resultArea.display.hasItems() &&
-                (this._searchResultsSingleShownSection == null || this._searchResultsSingleShownSection == section.type)) {
-                section.resultArea.display.selectFirstItem();
-                selectionSet = true;
-            }
-        }
-
-        // Here work around a bug that I never quite tracked down
-        // the root cause of; it appeared that the search results
-        // section was getting a 0 height allocation.
-        this._searchResultsSection.content.queue_relayout();
+        this.searchResults.updateSearch(text);
 
         return false;
     },
@@ -794,101 +936,6 @@ Dash.prototype = {
             }
         }));
         Main.overview.addPane(pane);
-    },
-
-    _updateDashActors: function() {
-        if (this._searchPending) {
-            this._searchResultsSection.actor.show();
-            // We initially hide all sections when we start a search. When the search timeout
-            // first runs, the sections that have matching results are shown. As the search
-            // is refined, only the sections that have matching results will be shown.
-            for (let i = 0; i < this._searchSections.length; i++) {
-                let section = this._searchSections[i];
-                section.header.actor.hide();
-                section.resultArea.actor.hide();
-            }
-            this._appsSection.actor.hide();
-            this._placesSection.actor.hide();
-            this._docsSection.actor.hide();
-        } else if (!this._searchActive) {
-            this._showAllSearchSections();
-            this._searchResultsSection.actor.hide();
-            this._appsSection.actor.show();
-            this._placesSection.actor.show();
-            this._docsSection.actor.show();
-        }
-    },
-
-    _showSingleSearchSection: function(type) {
-        // We currently don't allow going from showing one section to showing another section.
-        if (this._searchResultsSingleShownSection != null) {
-            throw new Error("We were already showing a single search section: '" + this._searchResultsSingleShownSection
-                            + "' when _showSingleSearchSection() was called for '" + type + "'");
-        }
-        for (var i = 0; i < this._searchSections.length; i++) {
-            let section = this._searchSections[i];
-            if (section.type == type) {
-                // This will be the only section shown.
-                section.resultArea.display.selectFirstItem();
-                let itemCount = section.resultArea.display.getMatchedItemsCount();
-                let itemCountText = itemCount + "";
-                section.header.actor.hide();
-                this._searchResultsSection.header.setTitle(section.title);
-                this._searchResultsSection.header.setBackLinkVisible(true);
-                this._searchResultsSection.header.setCountText(itemCountText);
-            } else {
-                // We need to hide this section.
-                section.header.actor.hide();
-                section.resultArea.actor.hide();
-                section.resultArea.display.unsetSelected();
-            }
-        }
-        this._searchResultsSingleShownSection = type;
-    },
-
-    _showAllSearchSections: function() {
-        if (this._searchResultsSingleShownSection != null) {
-            let selectionSet = false;
-            for (var i = 0; i < this._searchSections.length; i++) {
-                let section = this._searchSections[i];
-                if (section.type == this._searchResultsSingleShownSection) {
-                    // This will no longer be the only section shown.
-                    let itemCount = section.resultArea.display.getMatchedItemsCount();
-                    if (itemCount != 0) {
-                        section.header.actor.show();
-                        section.resultArea.display.selectFirstItem();
-                        selectionSet = true;
-                    }
-                    this._searchResultsSection.header.setTitle(_("SEARCH RESULTS"));
-                    this._searchResultsSection.header.setBackLinkVisible(false);
-                    this._searchResultsSection.header.setCountText("");
-                } else {
-                    // We need to restore this section.
-                    let itemCount = section.resultArea.display.getMatchedItemsCount();
-                    if (itemCount != 0) {
-                        section.header.actor.show();
-                        section.resultArea.actor.show();
-                        // This ensures that some other section will have the selection if the
-                        // single section that was being displayed did not have any items.
-                        if (!selectionSet) {
-                            section.resultArea.display.selectFirstItem();
-                            selectionSet = true;
-                        }
-                    }
-                }
-            }
-            this._searchResultsSingleShownSection = null;
-        }
-    },
-
-    _findAnotherSectionWithItems: function(index, increment) {
-        let pos = _getIndexWrapped(index, increment, this._searchSections.length);
-        while (pos != index) {
-            if (this._searchSections[pos].resultArea.display.hasItems())
-                return pos;
-            pos = _getIndexWrapped(pos, increment, this._searchSections.length);
-        }
-        return -1;
     }
 };
 Signals.addSignalMethods(Dash.prototype);
diff --git a/js/ui/docDisplay.js b/js/ui/docDisplay.js
index 9ba6c4c..ee4fa7e 100644
--- a/js/ui/docDisplay.js
+++ b/js/ui/docDisplay.js
@@ -10,11 +10,14 @@ const Shell = imports.gi.Shell;
 const Signals = imports.signals;
 const St = imports.gi.St;
 const Mainloop = imports.mainloop;
+const Gettext = imports.gettext.domain('gnome-shell');
+const _ = Gettext.gettext;
 
 const DocInfo = imports.misc.docInfo;
 const DND = imports.ui.dnd;
 const GenericDisplay = imports.ui.genericDisplay;
 const Main = imports.ui.main;
+const Search = imports.ui.search;
 
 const MAX_DASH_DOCS = 50;
 const DASH_DOCS_ICON_SIZE = 16;
@@ -179,13 +182,8 @@ DocDisplay.prototype = {
         this._matchedItemKeys = [];
         let docIdsToRemove = [];
         for (docId in this._allItems) {
-            // this._allItems[docId].exists() checks if the resource still exists
-            if (this._allItems[docId].exists()) {
-                this._matchedItems[docId] = 1;
-                this._matchedItemKeys.push(docId);
-            } else {
-                docIdsToRemove.push(docId);
-            }
+            this._matchedItems[docId] = 1;
+            this._matchedItemKeys.push(docId);
         }
 
         for (docId in docIdsToRemove) {
@@ -479,3 +477,41 @@ DashDocDisplay.prototype = {
 
 Signals.addSignalMethods(DashDocDisplay.prototype);
 
+function DocSearchProvider() {
+    this._init();
+}
+
+DocSearchProvider.prototype = {
+    __proto__: Search.SearchProvider.prototype,
+
+    _init: function(name) {
+        Search.SearchProvider.prototype._init.call(this, _("DOCUMENTS"));
+        this._docManager = DocInfo.getDocManager();
+    },
+
+    getResultMeta: function(resultId) {
+        let docInfo = this._docManager.lookupByUri(resultId);
+        if (!docInfo)
+            return null;
+        return { 'id': resultId,
+                 'name': docInfo.name,
+                 'icon': docInfo.createIcon(Search.RESULT_ICON_SIZE)};
+    },
+
+    activateResult: function(id) {
+        let docInfo = this._docManager.lookupByUri(id);
+        docInfo.launch();
+    },
+
+    getInitialResultSet: function(terms) {
+        return this._docManager.initialSearch(terms);
+    },
+
+    getSubsearchResultSet: function(previousResults, terms) {
+        return this._docManager.subsearch(previousResults, terms);
+    },
+
+    expandSearch: function(terms) {
+        log("TODO expand docs search");
+    }
+};
diff --git a/js/ui/overview.js b/js/ui/overview.js
index 74859a7..a9697ff 100644
--- a/js/ui/overview.js
+++ b/js/ui/overview.js
@@ -184,6 +184,7 @@ Overview.prototype = {
         this._dash.actor.set_size(displayGridColumnWidth, contentHeight);
         this._dash.searchArea.height = this._workspacesY - contentY;
         this._dash.sectionArea.height = this._workspacesHeight;
+        this._dash.searchResults.actor.height = this._workspacesHeight;
 
         // place the 'Add Workspace' button in the bottom row of the grid
         addRemoveButtonSize = Math.floor(displayGridRowHeight * 3/5);
diff --git a/js/ui/placeDisplay.js b/js/ui/placeDisplay.js
index 82a3674..868ad11 100644
--- a/js/ui/placeDisplay.js
+++ b/js/ui/placeDisplay.js
@@ -15,7 +15,7 @@ const _ = Gettext.gettext;
 
 const DND = imports.ui.dnd;
 const Main = imports.ui.main;
-const GenericDisplay = imports.ui.genericDisplay;
+const Search = imports.ui.search;
 
 const NAUTILUS_PREFS_DIR = '/apps/nautilus/preferences';
 const DESKTOP_IS_HOME_KEY = NAUTILUS_PREFS_DIR + '/desktop_is_home_dir';
@@ -30,16 +30,30 @@ const PLACES_ICON_SIZE = 16;
  * @iconFactory: A JavaScript callback which will create an icon texture given a size parameter
  * @launch: A JavaScript callback to launch the entry
  */
-function PlaceInfo(name, iconFactory, launch) {
-    this._init(name, iconFactory, launch);
+function PlaceInfo(id, name, iconFactory, launch) {
+    this._init(id, name, iconFactory, launch);
 }
 
 PlaceInfo.prototype = {
-    _init: function(name, iconFactory, launch) {
+    _init: function(id, name, iconFactory, launch) {
+        this.id = id;
         this.name = name;
+        this._lowerName = name.toLowerCase();
         this.iconFactory = iconFactory;
         this.launch = launch;
-        this.id = null;
+    },
+
+    matchTerms: function(terms) {
+        let mtype = Search.MatchType.NONE;
+        for (let i = 0; i < terms.length; i++) {
+            let term = terms[i];
+            let idx = this._lowerName.indexOf(term);
+            if (idx == 0)
+                return Search.MatchType.PREFIX;
+            else if (idx > 0)
+                mtype = Search.MatchType.SUBSTRING;
+        }
+        return mtype;
     }
 }
 
@@ -52,6 +66,7 @@ PlacesManager.prototype = {
         let gconf = Shell.GConf.get_default();
         gconf.watch_directory(NAUTILUS_PREFS_DIR);
 
+        this._defaultPlaces = [];
         this._mounts = [];
         this._bookmarks = [];
         this._isDesktopHome = false;
@@ -60,7 +75,7 @@ PlacesManager.prototype = {
         let homeUri = homeFile.get_uri();
         let homeLabel = Shell.util_get_label_for_uri (homeUri);
         let homeIcon = Shell.util_get_icon_for_uri (homeUri);
-        this._home = new PlaceInfo(homeLabel,
+        this._home = new PlaceInfo('special:home', homeLabel,
             function(size) {
                 return Shell.TextureCache.get_default().load_gicon(homeIcon, size);
             },
@@ -73,7 +88,7 @@ PlacesManager.prototype = {
         let desktopUri = desktopFile.get_uri();
         let desktopLabel = Shell.util_get_label_for_uri (desktopUri);
         let desktopIcon = Shell.util_get_icon_for_uri (desktopUri);
-        this._desktopMenu = new PlaceInfo(desktopLabel,
+        this._desktopMenu = new PlaceInfo('special:desktop', desktopLabel,
             function(size) {
                 return Shell.TextureCache.get_default().load_gicon(desktopIcon, size);
             },
@@ -81,7 +96,7 @@ PlacesManager.prototype = {
                 Gio.app_info_launch_default_for_uri(desktopUri, global.create_app_launch_context());
             });
 
-        this._connect = new PlaceInfo(_("Connect to..."),
+        this._connect = new PlaceInfo('special:connect', _("Connect to..."),
             function (size) {
                 return Shell.TextureCache.get_default().load_icon_name("applications-internet", size);
             },
@@ -101,7 +116,7 @@ PlacesManager.prototype = {
         }
 
         if (networkApp != null) {
-            this._network = new PlaceInfo(networkApp.get_name(),
+            this._network = new PlaceInfo('special:network', networkApp.get_name(),
                 function(size) {
                     return networkApp.create_icon_texture(size);
                 },
@@ -110,6 +125,16 @@ PlacesManager.prototype = {
                 });
         }
 
+        this._defaultPlaces.push(this._home);
+
+        if (!this._isDesktopHome)
+            this._defaultPlaces.push(this._desktopMenu);
+
+        if (this._network)
+            this._defaultPlaces.push(this._network);
+
+        this._defaultPlaces.push(this._connect);
+
         /*
         * Show devices, code more or less ported from nautilus-places-sidebar.c
         */
@@ -238,7 +263,7 @@ PlacesManager.prototype = {
                 continue;
             let icon = Shell.util_get_icon_for_uri(bookmark);
 
-            let item = new PlaceInfo(label,
+            let item = new PlaceInfo('bookmark:' + bookmark, label,
                 function(size) {
                     return Shell.TextureCache.get_default().load_gicon(icon, size);
                 },
@@ -267,7 +292,8 @@ PlacesManager.prototype = {
         let mountIcon = mount.get_icon();
         let root = mount.get_root();
         let mountUri = root.get_uri();
-        let devItem = new PlaceInfo(mountLabel,
+        let devItem = new PlaceInfo('mount:' + mountUri,
+                                     mountLabel,
                function(size) {
                         return Shell.TextureCache.get_default().load_gicon(mountIcon, size);
                },
@@ -282,16 +308,7 @@ PlacesManager.prototype = {
     },
 
     getDefaultPlaces: function () {
-        let places = [this._home];
-
-        if (!this._isDesktopHome)
-            places.push(this._desktopMenu);
-
-        if (this._network)
-            places.push(this._network);
-
-        places.push(this._connect);
-        return places;
+        return this._defaultPlaces;
     },
 
     getBookmarks: function () {
@@ -300,6 +317,28 @@ PlacesManager.prototype = {
 
     getMounts: function () {
         return this._mounts;
+    },
+
+    _lookupById: function(sourceArray, id) {
+        for (let i = 0; i < sourceArray.length; i++) {
+            let place = sourceArray[i];
+            if (place.id == id)
+                return place;
+        }
+        return null;
+    },
+
+    lookupPlaceById: function(id) {
+        let colonIdx = id.indexOf(':');
+        let type = id.substring(0, colonIdx);
+        let sourceArray = null;
+        if (type == 'special')
+            sourceArray = this._defaultPlaces;
+        else if (type == 'mount')
+            sourceArray = this._mounts;
+        else if (type == 'bookmark')
+            sourceArray = this._bookmarks;
+        return this._lookupById(sourceArray, id);
     }
 };
 
@@ -421,120 +460,67 @@ DashPlaceDisplay.prototype = {
 
 Signals.addSignalMethods(DashPlaceDisplay.prototype);
 
-
-function PlaceDisplayItem(placeInfo) {
-    this._init(placeInfo);
+function PlaceSearchProvider() {
+    this._init();
 }
 
-PlaceDisplayItem.prototype = {
-    __proto__: GenericDisplay.GenericDisplayItem.prototype,
-
-    _init : function(placeInfo) {
-        GenericDisplay.GenericDisplayItem.prototype._init.call(this);
-        this._info = placeInfo;
-
-        this._setItemInfo(placeInfo.name, '');
-    },
+PlaceSearchProvider.prototype = {
+    __proto__: Search.SearchProvider.prototype,
 
-    //// Public method overrides ////
-
-    // Opens an application represented by this display item.
-    launch : function() {
-        this._info.launch();
+    _init: function() {
+        Search.SearchProvider.prototype._init.call(this, _("PLACES"));
     },
 
-    shellWorkspaceLaunch: function() {
-        this._info.launch();
+    getResultMeta: function(resultId) {
+        let placeInfo = Main.placesManager.lookupPlaceById(resultId);
+        if (!placeInfo)
+            return null;
+        return { 'id': resultId,
+                 'name': placeInfo.name,
+                 'icon': placeInfo.iconFactory(Search.RESULT_ICON_SIZE) };
     },
 
-    //// Protected method overrides ////
-
-    // Returns an icon for the item.
-    _createIcon: function() {
-        return this._info.iconFactory(GenericDisplay.ITEM_DISPLAY_ICON_SIZE);
+    activateResult: function(id) {
+        let placeInfo = Main.placesManager.lookupPlaceById(id);
+        placeInfo.launch();
     },
 
-    // Returns a preview icon for the item.
-    _createPreviewIcon: function() {
-        return this._info.iconFactory(GenericDisplay.PREVIEW_ICON_SIZE);
-    }
-
-};
-
-function PlaceDisplay(flags) {
-    this._init(flags);
-}
-
-PlaceDisplay.prototype = {
-    __proto__:  GenericDisplay.GenericDisplay.prototype,
-
-    _init: function(flags) {
-        GenericDisplay.GenericDisplay.prototype._init.call(this, flags);
-        this._stale = true;
-        Main.placesManager.connect('places-updated', Lang.bind(this, function (e) {
-            this._stale = true;
-        }));
+    _compareResultMeta: function (idA, idB) {
+        let infoA = Main.placesManager.lookupPlaceById(idA);
+        let infoB = Main.placesManager.lookupPlaceById(idB);
+        return infoA.name.localeCompare(infoB.name);
     },
 
-    //// Protected method overrides ////
-    _refreshCache: function () {
-        if (!this._stale)
-            return true;
-        this._allItems = {};
-        let array = Main.placesManager.getAllPlaces();
-        for (let i = 0; i < array.length; i ++) {
-            // We are using an array id as placeInfo id because placeInfo doesn't have any
-            // other information piece that can be used as a unique id. There are different
-            // types of placeInfo, such as devices and directories that would result in differently
-            // structured ids. Also the home directory can show up in both the default places and in
-            // bookmarks which means its URI can't be used as a unique id. (This does mean it can
-            // appear twice in search results, though that doesn't happen at the moment because we
-            // name it "Home Folder" in default places and it's named with the user's system name
-            // if it appears as a bookmark.)
-            let placeInfo = array[i];
-            placeInfo.id = i;
-            this._allItems[i] = placeInfo;
+    _searchPlaces: function(places, terms) {
+        let multipleResults = [];
+        let prefixResults = [];
+        let substringResults = [];
+
+        terms = terms.map(String.toLowerCase);
+
+        for (let i = 0; i < places.length; i++) {
+            let place = places[i];
+            let mtype = place.matchTerms(terms);
+            if (mtype == Search.MatchType.MULTIPLE)
+                multipleResults.push(place.id);
+            else if (mtype == Search.MatchType.PREFIX)
+                prefixResults.push(place.id);
+            else if (mtype == Search.MatchType.SUBSTRING)
+                substringResults.push(place.id);
         }
-        this._stale = false;
-        return false;
+        multipleResults.sort(this._compareResultMeta);
+        prefixResults.sort(this._compareResultMeta);
+        substringResults.sort(this._compareResultMeta);
+        return multipleResults.concat(prefixResults.concat(substringResults));
     },
 
-    // Sets the list of the displayed items.
-    _setDefaultList: function() {
-        this._matchedItems = {};
-        this._matchedItemKeys = [];
-        for (id in this._allItems) {
-            this._matchedItems[id] = 1;
-            this._matchedItemKeys.push(id);
-        }
-        this._matchedItemKeys.sort(Lang.bind(this, this._compareItems));
+    getInitialResultSet: function(terms) {
+        let places = Main.placesManager.getAllPlaces();
+        return this._searchPlaces(places, terms);
     },
 
-    // Checks if the item info can be a match for the search string by checking
-    // the name of the place. Item info is expected to be PlaceInfo.
-    // Returns a boolean flag indicating if itemInfo is a match.
-    _isInfoMatching: function(itemInfo, search) {
-        if (search == null || search == '')
-            return true;
-
-        let name = itemInfo.name.toLowerCase();
-        if (name.indexOf(search) >= 0)
-            return true;
-
-        return false;
-    },
-
-    // Compares items associated with the item ids based on the alphabetical order
-    // of the item names.
-    // Returns an integer value indicating the result of the comparison.
-    _compareItems: function(itemIdA, itemIdB) {
-        let placeA = this._allItems[itemIdA];
-        let placeB = this._allItems[itemIdB];
-        return placeA.name.localeCompare(placeB.name);
-    },
-
-    // Creates a PlaceDisplayItem based on itemInfo, which is expected to be a PlaceInfo object.
-    _createDisplayItem: function(itemInfo) {
-        return new PlaceDisplayItem(itemInfo);
+    getSubsearchResultSet: function(previousResults, terms) {
+        let places = previousResults.map(function (id) { return Main.placesManager.lookupPlaceById(id); });
+        return this._searchPlaces(places, terms);
     }
-};
+}
diff --git a/js/ui/search.js b/js/ui/search.js
new file mode 100644
index 0000000..2b73852
--- /dev/null
+++ b/js/ui/search.js
@@ -0,0 +1,272 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Signals = imports.signals;
+const St = imports.gi.St;
+
+const RESULT_ICON_SIZE = 24;
+
+// Not currently referenced by the search API, but
+// this enumeration can be useful for provider
+// implementations.
+const MatchType = {
+    NONE: 0,
+    MULTIPLE: 1,
+    PREFIX: 2,
+    SUBSTRING: 3
+};
+
+function SearchResultDisplay(provider) {
+    this._init(provider);
+}
+
+SearchResultDisplay.prototype = {
+    _init: function(provider) {
+        this.provider = provider;
+        this.actor = null;
+        this.selectionIndex = -1;
+    },
+
+    /**
+     * renderResults:
+     * @results: List of identifier strings
+     * @terms: List of search term strings
+     *
+     * Display the given search matches which resulted
+     * from the given terms.  It's expected that not
+     * all results will fit in the space for the container
+     * actor; in this case, show as many as makes sense
+     * for your result type.
+     *
+     * The terms are useful for search match highlighting.
+     */
+    renderResults: function(results, terms) {
+        throw new Error("not implemented");
+    },
+
+    /**
+     * clear:
+     * Remove all results from this display and reset the selection index.
+     */
+    clear: function() {
+        this.actor.get_children().forEach(function (actor) { actor.destroy(); });
+        this.selectionIndex = -1;
+    },
+
+    /**
+     * getSelectionIndex:
+     *
+     * Returns the index of the selected actor, or -1 if none.
+     */
+    getSelectionIndex: function() {
+        return this.selectionIndex;
+    },
+
+    /**
+     * getVisibleResultCount:
+     *
+     * Returns: The number of actors visible.
+     */
+    getVisibleResultCount: function() {
+        throw new Error("not implemented");
+    },
+
+    /**
+     * selectIndex:
+     * @index: Integer index
+     *
+     * Move selection to the given index.
+     * Return true if successful, false if no more results
+     * available.
+     */
+    selectIndex: function() {
+        throw new Error("not implemented");
+    }
+};
+
+/**
+ * SearchProvider:
+ *
+ * Subclass this object to add a new result type
+ * to the search system, then call registerProvider()
+ * in SearchSystem with an instance.
+ */
+function SearchProvider(title) {
+    this._init(title);
+}
+
+SearchProvider.prototype = {
+    _init: function(title) {
+        this.title = title;
+    },
+
+    /**
+     * getInitialResultSet:
+     * @terms: Array of search terms, treated as logical OR
+     *
+     * Called when the user first begins a search (most likely
+     * therefore a single term of length one or two), or when
+     * a new term is added.
+     *
+     * Should return an array of result identifier strings representing
+     * items which match the given search terms.  This
+     * is expected to be a substring match on the metadata for a given
+     * item.  Ordering of returned results is up to the discretion of the provider,
+     * but you should follow these heruistics:
+     *
+     *  * Put items which match multiple search terms before single matches
+     *  * Put items which match on a prefix before non-prefix substring matches
+     *
+     * This function should be fast; do not perform unindexed full-text searches
+     * or network queries.
+     */
+    getInitialResultSet: function(terms) {
+        throw new Error("not implemented");
+    },
+
+    /**
+     * getSubsearchResultSet:
+     * @previousResults: Array of item identifiers
+     * @newTerms: Updated search terms
+     *
+     * Called when a search is performed which is a "subsearch" of
+     * the previous search; i.e. when every search term has exactly
+     * one corresponding term in the previous search which is a prefix
+     * of the new term.
+     *
+     * This allows search providers to only search through the previous
+     * result set, rather than possibly performing a full re-query.
+     */
+    getSubsearchResultSet: function(previousResults, newTerms) {
+        throw new Error("not implemented");
+    },
+
+    /**
+     * getResultInfo:
+     * @id: Result identifier string
+     *
+     * Return an object with 'id', 'name', (both strings) and 'icon' (Clutter.Texture)
+     * properties which describe the given search result.
+     */
+    getResultMeta: function(id) {
+        throw new Error("not implemented");
+    },
+
+    /**
+     * createResultContainer:
+     *
+     * Search providers may optionally override this to render their
+     * results in a custom fashion.  The default implementation
+     * will create a vertical list.
+     *
+     * Returns: An instance of SearchResultDisplay.
+     */
+    createResultContainerActor: function() {
+        return null;
+    },
+
+    /**
+     * createResultActor:
+     * @resultMeta: Object with result metadata
+     * @terms: Array of search terms, should be used for highlighting
+     *
+     * Search providers may optionally override this to render a
+     * particular serch result in a custom fashion.  The default
+     * implementation will show the icon next to the name.
+     *
+     * The actor should be an instance of St.Widget, with the style class
+     * 'dash-search-result-content'.
+     */
+    createResultActor: function(resultMeta, terms) {
+        return null;
+    },
+
+    /**
+     * activateResult:
+     * @id: Result identifier string
+     *
+     * Called when the user chooses a given result.
+     */
+    activateResult: function(id) {
+        throw new Error("not implemented");
+    },
+
+    /**
+     * expandSearch:
+     *
+     * Called when the user clicks on the header for this
+     * search section.  Should typically launch an external program
+     * displaying search results for that item type.
+     */
+    expandSearch: function(terms) {
+        throw new Error("not implemented");
+    }
+}
+Signals.addSignalMethods(SearchProvider.prototype);
+
+function SearchSystem() {
+    this._init();
+}
+
+SearchSystem.prototype = {
+    _init: function() {
+        this._providers = [];
+        this.reset();
+    },
+
+    registerProvider: function (provider) {
+        this._providers.push(provider);
+    },
+
+    getProviders: function() {
+        return this._providers;
+    },
+
+    getTerms: function() {
+        return this._previousTerms;
+    },
+
+    reset: function() {
+        this._previousTerms = [];
+        this._previousResults = [];
+    },
+
+    updateSearch: function(searchString) {
+        searchString = searchString.replace(/^\s+/g, "").replace(/\s+$/g, "");
+        if (searchString == '')
+            return null;
+
+        let terms = searchString.split(/\s+/);
+        let isSubSearch = terms.length == this._previousTerms.length;
+        if (isSubSearch) {
+            for (let i = 0; i < terms.length; i++) {
+                if (terms[i].indexOf(this._previousTerms[i]) != 0) {
+                    isSubSearch = false;
+                    break;
+                }
+            }
+        }
+
+        let results = [];
+        if (isSubSearch) {
+            for (let i = 0; i < this._previousResults.length; i++) {
+                let [provider, previousResults] = this._previousResults[i];
+                let providerResults = provider.getSubsearchResultSet(previousResults, terms);
+                if (providerResults.length > 0)
+                    results.push([provider, providerResults]);
+            }
+        } else {
+            for (let i = 0; i < this._providers.length; i++) {
+                let provider = this._providers[i];
+                let providerResults = provider.getInitialResultSet(terms);
+                if (providerResults.length > 0)
+                    results.push([provider, providerResults]);
+            }
+        }
+
+        this._previousTerms = terms;
+        this._previousResults = results;
+
+        return results;
+    }
+}
+Signals.addSignalMethods(SearchSystem.prototype);
diff --git a/src/shell-app-system.c b/src/shell-app-system.c
index 0f61576..ffe3ccc 100644
--- a/src/shell-app-system.c
+++ b/src/shell-app-system.c
@@ -48,9 +48,7 @@ struct _ShellAppSystemPrivate {
   GHashTable *app_id_to_info;
   GHashTable *app_id_to_app;
 
-  GHashTable *cached_menu_contents;  /* <char *id, GSList<ShellAppInfo*>> */
-  GSList *cached_app_menus; /* ShellAppMenuEntry */
-
+  GSList *cached_flattened_apps; /* ShellAppInfo */
   GSList *cached_settings; /* ShellAppInfo */
 
   gint app_monitor_id;
@@ -58,7 +56,6 @@ struct _ShellAppSystemPrivate {
   guint app_change_timeout_id;
 };
 
-static void free_appinfo_gslist (gpointer list);
 static void shell_app_system_finalize (GObject *object);
 static gboolean on_tree_changed (gpointer user_data);
 static void on_tree_changed_cb (GMenuTree *tree, gpointer user_data);
@@ -83,6 +80,10 @@ struct _ShellAppInfo {
    */
   guint refcount;
 
+  char *casefolded_name;
+  char *name_collation_key;
+  char *casefolded_description;
+
   GMenuTreeItem *entry;
 
   GKeyFile *keyfile;
@@ -104,6 +105,11 @@ shell_app_info_unref (ShellAppInfo *info)
 {
   if (--info->refcount > 0)
     return;
+
+  g_free (info->casefolded_name);
+  g_free (info->name_collation_key);
+  g_free (info->casefolded_description);
+
   switch (info->type)
   {
   case SHELL_APP_INFO_TYPE_ENTRY:
@@ -129,7 +135,7 @@ shell_app_info_new_from_tree_item (GMenuTreeItem *item)
   if (!item)
     return NULL;
 
-  info = g_slice_alloc (sizeof (ShellAppInfo));
+  info = g_slice_alloc0 (sizeof (ShellAppInfo));
   info->type = SHELL_APP_INFO_TYPE_ENTRY;
   info->refcount = 1;
   info->entry = gmenu_tree_item_ref (item);
@@ -141,7 +147,7 @@ shell_app_info_new_from_window (MetaWindow *window)
 {
   ShellAppInfo *info;
 
-  info = g_slice_alloc (sizeof (ShellAppInfo));
+  info = g_slice_alloc0 (sizeof (ShellAppInfo));
   info->type = SHELL_APP_INFO_TYPE_WINDOW;
   info->refcount = 1;
   info->window = g_object_ref (window);
@@ -159,7 +165,7 @@ shell_app_info_new_from_keyfile_take_ownership (GKeyFile   *keyfile,
 {
   ShellAppInfo *info;
 
-  info = g_slice_alloc (sizeof (ShellAppInfo));
+  info = g_slice_alloc0 (sizeof (ShellAppInfo));
   info->type = SHELL_APP_INFO_TYPE_DESKTOP_FILE;
   info->refcount = 1;
   info->keyfile = keyfile;
@@ -167,29 +173,6 @@ shell_app_info_new_from_keyfile_take_ownership (GKeyFile   *keyfile,
   return info;
 }
 
-static gpointer
-shell_app_menu_entry_copy (gpointer entryp)
-{
-  ShellAppMenuEntry *entry;
-  ShellAppMenuEntry *copy;
-  entry = entryp;
-  copy = g_new0 (ShellAppMenuEntry, 1);
-  copy->name = g_strdup (entry->name);
-  copy->id = g_strdup (entry->id);
-  copy->icon = g_strdup (entry->icon);
-  return copy;
-}
-
-static void
-shell_app_menu_entry_free (gpointer entryp)
-{
-  ShellAppMenuEntry *entry = entryp;
-  g_free (entry->name);
-  g_free (entry->id);
-  g_free (entry->icon);
-  g_free (entry);
-}
-
 static void shell_app_system_class_init(ShellAppSystemClass *klass)
 {
   GObjectClass *gobject_class = (GObjectClass *)klass;
@@ -225,9 +208,6 @@ shell_app_system_init (ShellAppSystem *self)
   /* Key is owned by info */
   priv->app_id_to_app = g_hash_table_new (g_str_hash, g_str_equal);
 
-  priv->cached_menu_contents = g_hash_table_new_full (g_str_hash, g_str_equal,
-                                                      g_free, free_appinfo_gslist);
-
   /* For now, we want to pick up Evince, Nautilus, etc.  We'll
    * handle NODISPLAY semantics at a higher level or investigate them
    * case by case.
@@ -257,15 +237,12 @@ shell_app_system_finalize (GObject *object)
   gmenu_tree_unref (priv->apps_tree);
   gmenu_tree_unref (priv->settings_tree);
 
-  g_hash_table_destroy (priv->cached_menu_contents);
-
   g_hash_table_destroy (priv->app_id_to_info);
   g_hash_table_destroy (priv->app_id_to_app);
 
-  g_slist_foreach (priv->cached_app_menus, (GFunc)shell_app_menu_entry_free, NULL);
-  g_slist_free (priv->cached_app_menus);
-  priv->cached_app_menus = NULL;
-
+  g_slist_foreach (priv->cached_flattened_apps, (GFunc)shell_app_info_unref, NULL);
+  g_slist_free (priv->cached_flattened_apps);
+  priv->cached_flattened_apps = NULL;
   g_slist_foreach (priv->cached_settings, (GFunc)shell_app_info_unref, NULL);
   g_slist_free (priv->cached_settings);
   priv->cached_settings = NULL;
@@ -273,60 +250,10 @@ shell_app_system_finalize (GObject *object)
   G_OBJECT_CLASS (shell_app_system_parent_class)->finalize(object);
 }
 
-static void
-free_appinfo_gslist (gpointer listp)
-{
-  GSList *list = listp;
-  g_slist_foreach (list, (GFunc) shell_app_info_unref, NULL);
-  g_slist_free (list);
-}
-
-static void
-reread_directories (ShellAppSystem *self, GSList **cache, GMenuTree *tree)
-{
-  GMenuTreeDirectory *trunk;
-  GSList *entries;
-  GSList *iter;
-
-  trunk = gmenu_tree_get_root_directory (tree);
-  entries = gmenu_tree_directory_get_contents (trunk);
-
-  g_slist_foreach (*cache, (GFunc)shell_app_menu_entry_free, NULL);
-  g_slist_free (*cache);
-  *cache = NULL;
-
-  for (iter = entries; iter; iter = iter->next)
-    {
-      GMenuTreeItem *item = iter->data;
-
-      switch (gmenu_tree_item_get_type (item))
-        {
-          case GMENU_TREE_ITEM_DIRECTORY:
-            {
-              GMenuTreeDirectory *dir = iter->data;
-              ShellAppMenuEntry *shell_entry = g_new0 (ShellAppMenuEntry, 1);
-              shell_entry->name = g_strdup (gmenu_tree_directory_get_name (dir));
-              shell_entry->id = g_strdup (gmenu_tree_directory_get_menu_id (dir));
-              shell_entry->icon = g_strdup (gmenu_tree_directory_get_icon (dir));
-
-              *cache = g_slist_prepend (*cache, shell_entry);
-            }
-            break;
-          default:
-            break;
-        }
-
-      gmenu_tree_item_unref (item);
-    }
-  *cache = g_slist_reverse (*cache);
-
-  g_slist_free (entries);
-  gmenu_tree_item_unref (trunk);
-}
-
 static GSList *
 gather_entries_recurse (ShellAppSystem     *monitor,
                         GSList             *apps,
+                        GHashTable         *unique,
                         GMenuTreeDirectory *root)
 {
   GSList *contents;
@@ -342,13 +269,17 @@ gather_entries_recurse (ShellAppSystem     *monitor,
           case GMENU_TREE_ITEM_ENTRY:
             {
               ShellAppInfo *app = shell_app_info_new_from_tree_item (item);
-              apps = g_slist_prepend (apps, app);
+              if (!g_hash_table_lookup (unique, shell_app_info_get_id (app)))
+                {
+                  apps = g_slist_prepend (apps, app);
+                  g_hash_table_insert (unique, (char*)shell_app_info_get_id (app), app);
+                }
             }
             break;
           case GMENU_TREE_ITEM_DIRECTORY:
             {
               GMenuTreeDirectory *dir = (GMenuTreeDirectory*)item;
-              apps = gather_entries_recurse (monitor, apps, dir);
+              apps = gather_entries_recurse (monitor, apps, unique, dir);
             }
             break;
           default:
@@ -365,6 +296,7 @@ gather_entries_recurse (ShellAppSystem     *monitor,
 static void
 reread_entries (ShellAppSystem     *self,
                 GSList            **cache,
+                GHashTable         *unique,
                 GMenuTree          *tree)
 {
   GMenuTreeDirectory *trunk;
@@ -375,46 +307,40 @@ reread_entries (ShellAppSystem     *self,
   g_slist_free (*cache);
   *cache = NULL;
 
-  *cache = gather_entries_recurse (self, *cache, trunk);
+  *cache = gather_entries_recurse (self, *cache, unique, trunk);
 
   gmenu_tree_item_unref (trunk);
 }
 
 static void
-cache_by_id (ShellAppSystem *self, GSList *apps, gboolean ref)
+cache_by_id (ShellAppSystem *self, GSList *apps)
 {
   GSList *iter;
 
   for (iter = apps; iter; iter = iter->next)
     {
       ShellAppInfo *info = iter->data;
-      if (ref)
-        shell_app_info_ref (info);
+      shell_app_info_ref (info);
       /* the name is owned by the info itself */
-      g_hash_table_insert (self->priv->app_id_to_info, (char*)shell_app_info_get_id (info),
-                           info);
+      g_hash_table_replace (self->priv->app_id_to_info, (char*)shell_app_info_get_id (info),
+                            info);
     }
 }
 
 static void
 reread_menus (ShellAppSystem *self)
 {
-  GSList *apps;
-  GMenuTreeDirectory *trunk;
+  GHashTable *unique = g_hash_table_new (g_str_hash, g_str_equal);
 
-  reread_directories (self, &(self->priv->cached_app_menus), self->priv->apps_tree);
+  reread_entries (self, &(self->priv->cached_flattened_apps), unique, self->priv->apps_tree);
+  g_hash_table_remove_all (unique);
+  reread_entries (self, &(self->priv->cached_settings), unique, self->priv->settings_tree);
+  g_hash_table_destroy (unique);
 
-  reread_entries (self, &(self->priv->cached_settings), self->priv->settings_tree);
-
-  /* Now loop over applications.menu and settings.menu, inserting each by desktop file
-   * ID into a hash */
   g_hash_table_remove_all (self->priv->app_id_to_info);
-  trunk = gmenu_tree_get_root_directory (self->priv->apps_tree);
-  apps = gather_entries_recurse (self, NULL, trunk);
-  gmenu_tree_item_unref (trunk);
-  cache_by_id (self, apps, FALSE);
-  g_slist_free (apps);
-  cache_by_id (self, self->priv->cached_settings, TRUE);
+
+  cache_by_id (self, self->priv->cached_flattened_apps);
+  cache_by_id (self, self->priv->cached_settings);
 }
 
 static gboolean
@@ -423,7 +349,6 @@ on_tree_changed (gpointer user_data)
   ShellAppSystem *self = SHELL_APP_SYSTEM (user_data);
 
   reread_menus (self);
-  g_hash_table_remove_all (self->priv->cached_menu_contents);
 
   g_signal_emit (self, signals[INSTALLED_CHANGED], 0);
 
@@ -469,21 +394,8 @@ shell_app_info_get_type (void)
   return gtype;
 }
 
-GType
-shell_app_menu_entry_get_type (void)
-{
-  static GType gtype = G_TYPE_INVALID;
-  if (gtype == G_TYPE_INVALID)
-    {
-      gtype = g_boxed_type_register_static ("ShellAppMenuEntry",
-          shell_app_menu_entry_copy,
-          shell_app_menu_entry_free);
-    }
-  return gtype;
-}
-
 /**
- * shell_app_system_get_applications_for_menu:
+ * shell_app_system_get_flattened_apps:
  *
  * Traverses a toplevel menu, and returns all items under it.  Nested items
  * are flattened.  This value is computed on initial call and cached thereafter
@@ -492,41 +404,9 @@ shell_app_menu_entry_get_type (void)
  * Return value: (transfer none) (element-type ShellAppInfo): List of applications
  */
 GSList *
-shell_app_system_get_applications_for_menu (ShellAppSystem *self,
-                                            const char *menu)
+shell_app_system_get_flattened_apps (ShellAppSystem *self)
 {
-  GSList *apps;
-
-  apps = g_hash_table_lookup (self->priv->cached_menu_contents, menu);
-  if (!apps)
-    {
-      char *path;
-      GMenuTreeDirectory *menu_entry;
-      path = g_strdup_printf ("/%s", menu);
-      menu_entry = gmenu_tree_get_directory_from_path (self->priv->apps_tree, path);
-      g_free (path);
-      g_assert (menu_entry != NULL);
-
-      apps = gather_entries_recurse (self, NULL, menu_entry);
-      g_hash_table_insert (self->priv->cached_menu_contents, g_strdup (menu), apps);
-
-      gmenu_tree_item_unref (menu_entry);
-    }
-
-  return apps;
-}
-
-/**
- * shell_app_system_get_menus:
- *
- * Returns a list of toplevel #ShellAppMenuEntry items
- *
- * Return value: (transfer none) (element-type AppMenuEntry): List of toplevel menus
- */
-GSList *
-shell_app_system_get_menus (ShellAppSystem *monitor)
-{
-  return monitor->priv->cached_app_menus;
+  return self->priv->cached_flattened_apps;
 }
 
 /**
@@ -711,6 +591,249 @@ shell_app_system_lookup_heuristic_basename (ShellAppSystem *system,
   return NULL;
 }
 
+typedef enum {
+  MATCH_NONE,
+  MATCH_MULTIPLE, /* Matches multiple terms */
+  MATCH_PREFIX, /* Strict prefix */
+  MATCH_SUBSTRING /* Not prefix, substring */
+} ShellAppInfoSearchMatch;
+
+static char *
+normalize_and_casefold (const char *str)
+{
+  char *normalized, *result;
+
+  if (str == NULL)
+    return NULL;
+
+  normalized = g_utf8_normalize (str, -1, G_NORMALIZE_ALL);
+  result = g_utf8_casefold (normalized, -1);
+  g_free (normalized);
+  return result;
+}
+
+static void
+shell_app_info_init_search_data (ShellAppInfo *info)
+{
+  const char *name;
+  const char *comment;
+
+  g_assert (info->type == SHELL_APP_INFO_TYPE_ENTRY);
+
+  name = gmenu_tree_entry_get_name ((GMenuTreeEntry*)info->entry);
+  info->casefolded_name = normalize_and_casefold (name);
+  info->name_collation_key = g_utf8_collate_key (name, -1);
+
+  comment = gmenu_tree_entry_get_comment ((GMenuTreeEntry*)info->entry);
+  info->casefolded_description = normalize_and_casefold (comment);
+}
+
+static ShellAppInfoSearchMatch
+shell_app_info_match_terms (ShellAppInfo  *info,
+                            GSList        *terms)
+{
+  GSList *iter;
+  ShellAppInfoSearchMatch match;
+
+  if (G_UNLIKELY(!info->casefolded_name))
+    shell_app_info_init_search_data (info);
+
+  match = MATCH_NONE;
+  for (iter = terms; iter; iter = iter->next)
+    {
+      const char *term = iter->data;
+      const char *p;
+
+      p = strstr (info->casefolded_name, term);
+      if (p == info->casefolded_name)
+        {
+          if (match != MATCH_NONE)
+            return MATCH_MULTIPLE;
+          else
+            match = MATCH_PREFIX;
+         }
+      else if (p != NULL)
+        match = MATCH_SUBSTRING;
+
+      if (!info->casefolded_description)
+        continue;
+      p = strstr (info->casefolded_description, term);
+      if (p != NULL)
+        match = MATCH_SUBSTRING;
+    }
+  return match;
+}
+
+static gint
+shell_app_info_compare (gconstpointer a,
+                        gconstpointer b,
+                        gpointer      data)
+{
+  ShellAppSystem *system = data;
+  const char *id_a = a;
+  const char *id_b = b;
+  ShellAppInfo *info_a = g_hash_table_lookup (system->priv->app_id_to_info, id_a);
+  ShellAppInfo *info_b = g_hash_table_lookup (system->priv->app_id_to_info, id_b);
+
+  return strcmp (info_a->name_collation_key, info_b->name_collation_key);
+}
+
+static GSList *
+sort_and_concat_results (ShellAppSystem *system,
+                         GSList         *multiple_matches,
+                         GSList         *prefix_matches,
+                         GSList         *substring_matches)
+{
+  multiple_matches = g_slist_sort_with_data (multiple_matches, shell_app_info_compare, system);
+  prefix_matches = g_slist_sort_with_data (prefix_matches, shell_app_info_compare, system);
+  substring_matches = g_slist_sort_with_data (substring_matches, shell_app_info_compare, system);
+  return g_slist_concat (multiple_matches, g_slist_concat (prefix_matches, substring_matches));
+}
+
+/**
+ * normalize_terms:
+ * @terms: (element-type utf8): Input search terms
+ *
+ * Returns: (element-type utf8) (transfer full): Unicode-normalized and lowercased terms
+ */
+static GSList *
+normalize_terms (GSList *terms)
+{
+  GSList *normalized_terms = NULL;
+  GSList *iter;
+  for (iter = terms; iter; iter = iter->next)
+    {
+      const char *term = iter->data;
+      normalized_terms = g_slist_prepend (normalized_terms, normalize_and_casefold (term));
+    }
+  return normalized_terms;
+}
+
+static inline void
+shell_app_system_do_match (ShellAppSystem   *system,
+                           ShellAppInfo     *info,
+                           GSList           *terms,
+                           GSList          **multiple_results,
+                           GSList          **prefix_results,
+                           GSList          **substring_results)
+{
+  const char *id = shell_app_info_get_id (info);
+  ShellAppInfoSearchMatch match;
+
+  if (shell_app_info_get_is_nodisplay (info))
+    return;
+
+  match = shell_app_info_match_terms (info, terms);
+  switch (match)
+    {
+      case MATCH_NONE:
+        break;
+      case MATCH_MULTIPLE:
+        *multiple_results = g_slist_prepend (*multiple_results, (char *) id);
+        break;
+      case MATCH_PREFIX:
+        *prefix_results = g_slist_prepend (*prefix_results, (char *) id);
+        break;
+      case MATCH_SUBSTRING:
+        *substring_results = g_slist_prepend (*substring_results, (char *) id);
+        break;
+    }
+}
+
+static GSList *
+shell_app_system_initial_search_internal (ShellAppSystem  *self,
+                                          GSList          *terms,
+                                          GSList          *source)
+{
+  GSList *multiple_results = NULL;
+  GSList *prefix_results = NULL;
+  GSList *substring_results = NULL;
+  GSList *iter;
+  GSList *normalized_terms = normalize_terms (terms);
+
+  for (iter = source; iter; iter = iter->next)
+    {
+      ShellAppInfo *info = iter->data;
+
+      shell_app_system_do_match (self, info, normalized_terms, &multiple_results, &prefix_results, &substring_results);
+    }
+  g_slist_foreach (normalized_terms, (GFunc)g_free, NULL);
+  g_slist_free (normalized_terms);
+
+  return sort_and_concat_results (self, multiple_results, prefix_results, substring_results);
+}
+
+/**
+ * shell_app_system_initial_search:
+ * @self: A #ShellAppSystem
+ * @prefs: %TRUE iff we should search preferences instead of apps
+ * @terms: (element-type utf8): List of terms, logical OR
+ *
+ * Search through applications for the given search terms.  Note that returned
+ * strings are only valid until a return to the main loop.
+ *
+ * Returns: (transfer container) (element-type utf8): List of application identifiers
+ */
+GSList *
+shell_app_system_initial_search (ShellAppSystem  *self,
+                                 gboolean         prefs,
+                                 GSList          *terms)
+{
+  return shell_app_system_initial_search_internal (self, terms,
+            prefs ? self->priv->cached_settings : self->priv->cached_flattened_apps);
+}
+
+/**
+ * shell_app_system_subsearch:
+ * @self: A #ShellAppSystem
+ * @prefs: %TRUE iff we should search preferences instead of apps
+ * @previous_results: (element-type utf8): List of previous results
+ * @terms: (element-type utf8): List of terms, logical OR
+ *
+ * Search through a previous result set; for more information, see
+ * js/ui/search.js.  Note the value of @prefs must be
+ * the same as passed to shell_app_system_initial_search().  Note that returned
+ * strings are only valid until a return to the main loop.
+ *
+ * Returns: (transfer container) (element-type utf8): List of application identifiers
+ */
+GSList *
+shell_app_system_subsearch (ShellAppSystem   *system,
+                            gboolean          prefs,
+                            GSList           *previous_results,
+                            GSList           *terms)
+{
+  GSList *iter;
+  GSList *multiple_results = NULL;
+  GSList *prefix_results = NULL;
+  GSList *substring_results = NULL;
+  GSList *normalized_terms = normalize_terms (terms);
+
+  /* Note prefs is deliberately ignored; both apps and prefs are in app_id_to_app,
+   * but we have the parameter for consistency and in case in the future
+   * they're not in the same data structure.
+   */
+
+  for (iter = previous_results; iter; iter = iter->next)
+    {
+      const char *id = iter->data;
+      ShellAppInfo *info;
+
+      info = g_hash_table_lookup (system->priv->app_id_to_info, id);
+      if (!info)
+        continue;
+
+      shell_app_system_do_match (system, info, normalized_terms, &multiple_results, &prefix_results, &substring_results);
+    }
+  g_slist_foreach (normalized_terms, (GFunc)g_free, NULL);
+  g_slist_free (normalized_terms);
+
+  /* Note that a shorter term might have matched as a prefix, but
+     when extended only as a substring, so we have to redo the
+     sort rather than reusing the existing ordering */
+  return sort_and_concat_results (system, multiple_results, prefix_results, substring_results);
+}
+
 const char *
 shell_app_info_get_id (ShellAppInfo *info)
 {
diff --git a/src/shell-app-system.h b/src/shell-app-system.h
index 3e3bee1..d6c7581 100644
--- a/src/shell-app-system.h
+++ b/src/shell-app-system.h
@@ -37,18 +37,6 @@ struct _ShellAppSystemClass
 GType shell_app_system_get_type (void) G_GNUC_CONST;
 ShellAppSystem* shell_app_system_get_default(void);
 
-GSList *shell_app_system_get_applications_for_menu (ShellAppSystem *system, const char *menu);
-
-typedef struct _ShellAppMenuEntry ShellAppMenuEntry;
-
-struct _ShellAppMenuEntry {
-  char *name;
-  char *id;
-  char *icon;
-};
-
-GType shell_app_menu_entry_get_type (void);
-
 typedef struct _ShellAppInfo ShellAppInfo;
 
 #define SHELL_TYPE_APP_INFO (shell_app_info_get_type ())
@@ -85,8 +73,17 @@ ShellApp *shell_app_system_lookup_heuristic_basename (ShellAppSystem *system, co
 
 ShellAppInfo *shell_app_system_create_from_window (ShellAppSystem *system, MetaWindow *window);
 
-GSList *shell_app_system_get_menus (ShellAppSystem *system);
+GSList *shell_app_system_get_flattened_apps (ShellAppSystem *system);
 
 GSList *shell_app_system_get_all_settings (ShellAppSystem *system);
 
+GSList *shell_app_system_initial_search (ShellAppSystem *system,
+                                         gboolean        prefs,
+                                         GSList         *terms);
+
+GSList *shell_app_system_subsearch (ShellAppSystem   *system,
+                                    gboolean          prefs,
+                                    GSList           *previous_results,
+                                    GSList           *terms);
+
 #endif /* __SHELL_APP_SYSTEM_H__ */



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