aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--fuzzyMode.js8
-rw-r--r--lib/completion.js256
2 files changed, 140 insertions, 124 deletions
diff --git a/fuzzyMode.js b/fuzzyMode.js
index cbefcde4..04fa2666 100644
--- a/fuzzyMode.js
+++ b/fuzzyMode.js
@@ -150,12 +150,12 @@ var fuzzyMode = (function() {
updateCompletions: function() {
var self = this;
- this.completer.filter(this.query, function(completions) {
- self.completions = completions.slice(0, self.maxResults);
+ this.completer.filter(this.query, this.maxResults, function(completions) {
+ self.completions = completions;
// clear completions
- self.completionList.innerHTML = self.completions.map(function(completion) {
- return '<li>' + completion.render() + '</li>';
+ self.completionList.innerHTML = completions.map(function(completion) {
+ return '<li>' + completion.html + '</li>';
}).join('');
self.completionList.style.display = self.completions.length > 0 ? 'block' : 'none';
diff --git a/lib/completion.js b/lib/completion.js
index 68f8d4bf..1915a83a 100644
--- a/lib/completion.js
+++ b/lib/completion.js
@@ -195,62 +195,54 @@ var completion = (function() {
return [ open(false), open(true, true), open(true, false) ];
}
- /** Creates a completion that renders by marking fuzzy-matched parts. */
- function createHighlightingCompletion(query, str, action, relevancy) {
- return {
- action: action,
- relevancy: relevancy,
-
- render: function() {
- // we want to match the content in HTML tags, but not the HTML tags themselves, so we remove the
- // tags and reinsert them after the matching process
- var htmlTags = {};
- str = stripHtmlTags(str, htmlTags);
- var groups = fuzzyMatcher.getMatchGroups(query, str);
- var html = '';
- var htmlOffset = 0;
-
- // this helper function adds the HTML generated _for one single character_ to the HTML output
- // and reinserts HTML tags stripped before, if they were at this position
- function addToHtml(str) {
- if (htmlOffset in htmlTags)
- html += htmlTags[htmlOffset];
- html += str;
- ++htmlOffset;
- }
-
- function addCharsWithDecoration(str, before, after) {
- before = before || '';
- after = after || '';
- for (var i = 0; i < str.length; ++i)
- addToHtml(before + str[i] + after);
- }
-
- // iterate over the match groups. They are non-matched and matched string parts, in alternating order
- for (var i = 0; i < groups.length; ++i) {
- if (i % 2 == 0)
- // we have a non-matched part, it could have several characters. We need to insert them character
- // by character, so that addToHtml can keep track of the position in the original string
- addCharsWithDecoration(groups[i]);
- else
- // we have a matched part. In addition to the characters themselves, we add some decorating HTML.
- addCharsWithDecoration(groups[i], '<span class="fuzzyMatch">', '</span>');
- };
-
- // call it another time so that a tag at the very last position is reinserted
- addToHtml('');
-
- return html;
- },
- }
- }
-
/** Creates an file-internal representation of a URL match with the given paramters */
- function createCompletionHtml(type, url, title) {
+ function createCompletionHtml(type, str, title) {
title = title || '';
// sanitize input, it could come from a malicious web site
title = title.length > 0 ? ' <span class="title">' + utils.escapeHtml(title) + '</span>' : '';
- return '<em>' + type + '</em> ' + utils.escapeHtml(url) + title;
+ return '<em>' + type + '</em> ' + utils.escapeHtml(str) + title;
+ }
+
+ /** Renders a completion by marking fuzzy-matched parts. */
+ function renderFuzzy(query, html) {
+ // we want to match the content in HTML tags, but not the HTML tags themselves, so we remove the
+ // tags and reinsert them after the matching process
+ var htmlTags = {};
+ var groups = fuzzyMatcher.getMatchGroups(query, stripHtmlTags(html, htmlTags));
+
+ html = [];
+ var htmlOffset = 0;
+
+ // this helper function adds the HTML generated _for one single character_ to the HTML output
+ // and reinserts HTML tags stripped before, if they were at this position
+ function addToHtml(str) {
+ if (htmlOffset in htmlTags)
+ html.push(htmlTags[htmlOffset]);
+ html.push(str);
+ ++htmlOffset;
+ }
+
+ function addCharsWithDecoration(str, before, after) {
+ before = before || '';
+ after = after || '';
+ for (var i = 0; i < str.length; ++i)
+ addToHtml(before + str[i] + after);
+ }
+
+ // iterate over the match groups. They are non-matched and matched string parts, in alternating order
+ for (var i = 0; i < groups.length; ++i) {
+ if (i % 2 == 0)
+ // we have a non-matched part, it could have several characters. We need to insert them character
+ // by character, so that addToHtml can keep track of the position in the original string
+ addCharsWithDecoration(groups[i]);
+ else
+ // we have a matched part. In addition to the characters themselves, we add some decorating HTML.
+ addCharsWithDecoration(groups[i], '<span class="fuzzyMatch">', '</span>');
+ };
+
+ // call it another time so that a tag at the very last position is reinserted
+ addToHtml('');
+ return html.join('');
}
/** Creates a function that returns a constant value */
@@ -258,27 +250,24 @@ var completion = (function() {
return function() { return x; }
}
+ /** A completion class that only holds a relevancy value and a function to get HTML and action
+ * properties */
+ var LazyCompletion = function(relevancy, builder) {
+ this.relevancy = relevancy;
+ this.build = builder;
+ }
+
/** Helper class to construct fuzzy completers for asynchronous data sources like history or bookmark
* matchers. */
- var AsyncFuzzyUrlCompleter = function() {
+ var AsyncCompleter = function() {
this.completions = null;
this.id = utils.createUniqueId();
this.readyCallback = this.fallbackReadyCallback = function(results) {
this.completions = results;
}
- this.extractStringFromMatch = function(match) { return stripHtmlTags(match.str); }
}
- AsyncFuzzyUrlCompleter.prototype = {
- calculateRelevancy: function(query, match) {
- return match.url.length /
- (fuzzyMatcher.calculateRelevancy(query, this.extractStringFromMatch(match)) + 1);
- },
-
- createAction: function(match) {
- return createActionOpenUrl(match.url);
- },
-
- /** Convenience function to remove shared code in the completers. Clear the completion cache, sends
+ AsyncCompleter.prototype = {
+ /** Convenience function to remove shared code in the completers. Clears the completion cache, sends
* a message to an extension port and pipes the returned message through a callback before storing it into
* the instance's completion cache.
*/
@@ -298,17 +287,44 @@ var completion = (function() {
fuzzyMatcher.invalidateFilterCache(this.id);
},
+ /** Convenience function to remove shared code in the completers. Creates an internal representation of
+ * a fuzzy completion item that is still independent of the query. The bind function will be called with
+ * the actual query as an argument later. */
+ createInternalMatch: function(type, item, action) {
+ var url = item.url;
+ var parts = [type, url, item.title];
+ var str = parts.join(' ');
+ action = action || createActionOpenUrl(url);
+
+ function createLazyCompletion(query) {
+ return new LazyCompletion(url.length / fuzzyMatcher.calculateRelevancy(query, str), function() {
+ return {
+ html: renderFuzzy(query, createCompletionHtml.apply(null, parts)),
+ action: action,
+ }});
+ }
+
+ // add one more layer of indirection: For filtering, we only need the string to match.
+ // Only after we reduced the number of possible results, we call :bind on them to get
+ // an actual completion object
+ return {
+ str: parts.join(' '),
+ bind: createLazyCompletion,
+ }
+ },
+
+ // Default to handle results using fuzzy matching. This can be overridden by subclasses.
+ processResults: function(query, results) {
+ results = fuzzyMatcher.filter(query, results, function(match) { return match.str }, this.id);
+ // bind the query-agnostic, lazy results to a query
+ return results.map(function(result) { return result.bind(query); });
+ },
+
filter: function(query, callback) {
var self = this;
var handler = function(results) {
- var filtered = fuzzyMatcher.filter(query, results, self.extractStringFromMatch, self.id);
- callback(filtered.map(function(match) {
- return createHighlightingCompletion(
- query, match.str,
- self.createAction(match),
- self.calculateRelevancy(query, match));
- }));
+ callback(self.processResults(query, results));
}
// are the results ready?
@@ -319,8 +335,8 @@ var completion = (function() {
// no: register the handler as a callback
this.readyCallback = function(results) {
handler(results);
- this.readyCallback = this.fallbackReadyCallback;
- this.readyCallback(results);
+ self.readyCallback = self.fallbackReadyCallback;
+ self.readyCallback(results);
}
}
},
@@ -344,85 +360,86 @@ var completion = (function() {
var pattern = commands[cmd][1];
var url = typeof pattern == 'function' ? pattern(term) : pattern.replace(/%s/g, term);
- return {
- render: function() { return createCompletionHtml(desc, term) },
- action: createActionOpenUrl(utils.createFullUrl(url)),
- relevancy: -2 // this will appear even before the URL/search suggestion
- };
+ // this will appear even before the URL/search suggestion
+ return new LazyCompletion(-2, function() {
+ return {
+ html: createCompletionHtml(desc, term),
+ action: createActionOpenUrl(utils.createFullUrl(url)),
+ }})
});
}
/** Checks if the input is a URL. If yes, returns a suggestion to visit it. If no, returns a suggestion
* to start a web search. */
- this.getUrlOrSearchSuggestions = function(query, suggestions) {
+ this.getUrlOrSearchSuggestion = function(query, suggestions) {
// trim query
query = query.replace(/^\s+|\s+$/g, '');
var isUrl = utils.isUrl(query);
- return [{
- render: function() { return createCompletionHtml(isUrl ? 'goto' : 'search', query); },
- action: createActionOpenUrl(isUrl ? utils.createFullUrl(query)
- : utils.createSearchUrl(query)),
- relevancy: -1, // low relevancy so this should appear at the top
- }];
+ return new LazyCompletion(-1, function() {
+ return {
+ html: createCompletionHtml(isUrl ? 'goto' : 'search', query),
+ action: createActionOpenUrl(isUrl ? utils.createFullUrl(query)
+ : utils.createSearchUrl(query)),
+ }});
}
this.filter = function(query, callback) {
- callback(this.getCommandSuggestions(query).concat(
- this.getUrlOrSearchSuggestions(query)));
+ suggestions = this.getCommandSuggestions(query);
+ suggestions.push(this.getUrlOrSearchSuggestion(query));
+ callback(suggestions);
}
}
/** A fuzzy history completer */
var FuzzyHistoryCompleter = function(maxResults) {
- AsyncFuzzyUrlCompleter.call(this);
+ AsyncCompleter.call(this);
this.maxResults = maxResults || 1000;
}
- utils.extend(AsyncFuzzyUrlCompleter, FuzzyHistoryCompleter);
+ utils.extend(AsyncCompleter, FuzzyHistoryCompleter);
+
FuzzyHistoryCompleter.prototype.refresh = function() {
- this.resetCache();
- this.fetchFromPort('getHistory', { maxResults: this.maxResults }, function(msg) {
- return msg.history.map(function(historyItem) {
- return { str: createCompletionHtml('history', historyItem.url, historyItem.title),
- url: historyItem.url };
+ var self = this;
+ self.resetCache();
+ self.fetchFromPort('getHistory', { maxResults: self.maxResults }, function(msg) {
+ return msg.history.map(function(item) {
+ return self.createInternalMatch('history', item);
});
});
}
/** A fuzzy bookmark completer */
var FuzzyBookmarkCompleter = function() {
- AsyncFuzzyUrlCompleter.call(this);
+ AsyncCompleter.call(this);
}
- utils.extend(AsyncFuzzyUrlCompleter, FuzzyBookmarkCompleter);
+ utils.extend(AsyncCompleter, FuzzyBookmarkCompleter);
+
FuzzyBookmarkCompleter.prototype.refresh = function() {
- this.resetCache();
- this.fetchFromPort('getAllBookmarks', {}, function(msg) {
+ var self = this;
+ self.resetCache();
+ self.fetchFromPort('getAllBookmarks', {}, function(msg) {
return msg.bookmarks.filter(function(bookmark) { return bookmark.url !== undefined })
.map(function(bookmark) {
- return { str: createCompletionHtml('bookmark', bookmark.url, bookmark.title),
- url: bookmark.url };
- })
+ return self.createInternalMatch('bookmark', bookmark);
+ });
});
}
/** A fuzzy tab completer */
var FuzzyTabCompleter = function() {
- AsyncFuzzyUrlCompleter.call(this);
- }
- utils.extend(AsyncFuzzyUrlCompleter, FuzzyTabCompleter);
- FuzzyTabCompleter.prototype.createAction = function(match) {
- var open = function() {
- chrome.extension.sendRequest({ handler: 'selectSpecificTab', id: match.tab.id });
- }
- return [ open, open ];
+ AsyncCompleter.call(this);
}
+ utils.extend(AsyncCompleter, FuzzyTabCompleter);
+
FuzzyTabCompleter.prototype.refresh = function() {
- this.resetCache();
- this.fetchFromPort('getTabsInCurrentWindow', {}, function(msg) {
+ var self = this;
+ self.resetCache();
+ self.fetchFromPort('getTabsInCurrentWindow', {}, function(msg) {
return msg.tabs.map(function(tab) {
- return { str: createCompletionHtml('tab', tab.url, tab.title),
- url: tab.url,
- tab: tab };
+ var open = function() {
+ chrome.extension.sendRequest({ handler: 'selectSpecificTab', id: tab.id });
+ }
+ return self.createInternalMatch('tab', tab, [open, open, open]);
});
});
}
@@ -441,7 +458,7 @@ var completion = (function() {
this.sources.forEach(function(x) { x.refresh(); });
},
- filter: function(query, callback) {
+ filter: function(query, maxResults, callback) {
if (query.length < this.queryThreshold) {
callback([]);
return;
@@ -457,10 +474,9 @@ var completion = (function() {
return;
// all sources have provided results by now, so we can sort and return
- all.sort(function(a,b) {
- return a.relevancy - b.relevancy;
- });
- callback(all);
+ all.sort(function(a,b) { return a.relevancy - b.relevancy; });
+ // evalulate lazy completions for the top n results
+ callback(all.slice(0, maxResults).map(function(result) { return result.build(); }));
});
});
}