aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNiklas Baumstark2012-01-21 00:23:59 +0100
committerNiklas Baumstark2012-04-10 23:54:35 +0200
commit6f589fedcc826b125884e3a5884c9791802afb7f (patch)
treeeb117a99558b5456b0367157a09a12f887db8630
parent269042a28230bb35406d1447fac8955ca1a5c0b3 (diff)
downloadvimium-6f589fedcc826b125884e3a5884c9791802afb7f.tar.bz2
add fuzzy mode
-rw-r--r--background_page.html55
-rw-r--r--commands.js9
-rw-r--r--fuzzyMode.js166
-rw-r--r--lib/completion.js324
-rw-r--r--lib/keyboardUtils.js3
-rw-r--r--lib/utils.js67
-rw-r--r--manifest.json3
-rw-r--r--vimium.css70
8 files changed, 676 insertions, 21 deletions
diff --git a/background_page.html b/background_page.html
index d8d3f75b..bca446a6 100644
--- a/background_page.html
+++ b/background_page.html
@@ -26,7 +26,9 @@
returnScrollPosition: handleReturnScrollPosition,
getCurrentTabUrl: getCurrentTabUrl,
settings: handleSettings,
- getBookmarks: getBookmarks
+ getBookmarks: getBookmarks,
+ getAllBookmarks: getAllBookmarks,
+ getHistory: getHistory,
};
var sendRequestHandlers = {
@@ -276,6 +278,57 @@
})
}
+ function getAllBookmarks(args, port) {
+ function traverseTree(bookmarks, callback) {
+ for (var i = 0; i < bookmarks.length; ++i) {
+ callback(bookmarks[i]);
+ if (typeof bookmarks[i].children === "undefined")
+ continue;
+ traverseTree(bookmarks[i].children, callback);
+ }
+ };
+
+ chrome.bookmarks.getTree(function(bookmarks) {
+ var results = [];
+ traverseTree(bookmarks, function(bookmark) {
+ if (typeof bookmark.url === "undefined")
+ return;
+ results.push(bookmark);
+ });
+ port.postMessage({bookmarks:results});
+ });
+ };
+
+ function getHistory(args, port) {
+ chrome.history.search({ text: '',
+ maxResults: args.maxResults || 1000 },
+ function(history) {
+ // sort by visit cound descending
+ history.sort(function(a, b) {
+ // visitCount may be undefined
+ var visitCountForA = a.visitCount || 0;
+ var visitCountForB = a.visitCount || 0;
+ return visitCountForB - visitCountForA;
+ });
+ var results = [];
+ for (var i = 0; i < history.length; ++i) {
+ results.push(history[i]);
+ }
+ port.postMessage({history:results});
+ });
+ };
+
+ /*
+ * Used by everyone to get settings from local storage.
+ */
+ function getSettingFromLocalStorage(setting) {
+ if (localStorage[setting] != "" && !localStorage[setting]) {
+ return defaultSettings[setting];
+ } else {
+ return localStorage[setting];
+ }
+ }
+
function getCurrentTimeInSeconds() { Math.floor((new Date()).getTime() / 1000); }
chrome.tabs.onSelectionChanged.addListener(function(tabId, selectionInfo) {
diff --git a/commands.js b/commands.js
index c961a901..1c19e26a 100644
--- a/commands.js
+++ b/commands.js
@@ -144,7 +144,10 @@ function clearKeyMappingsAndSetDefaults() {
"b": "activateBookmarkFindMode",
"B": "activateBookmarkFindModeToOpenInNewTab",
- "gf": "nextFrame"
+ "o": "fuzzyMode.activate",
+ "O": "fuzzyMode.activateNewTab",
+
+ "gf": "nextFrame",
};
for (var key in defaultKeyMappings)
@@ -211,6 +214,9 @@ var commandDescriptions = {
activateBookmarkFindMode: ["Open a bookmark in the current tab"],
activateBookmarkFindModeToOpenInNewTab: ["Open a bookmark in a new tab"],
+ 'fuzzyMode.activate': ["Open URL, bookmark, history entry or a custom search (fuzzy)"],
+ 'fuzzyMode.activateNewTab': ["Open URL, bookmark, history entry or a custom search (fuzzy, new tab)"],
+
nextFrame: ["Cycle forward to the next frame on the page", { background: true, passCountToFunction: true }]
};
@@ -230,6 +236,7 @@ var commandGroups = {
"enterInsertMode", "focusInput",
"linkHints.activateMode", "linkHints.activateModeToOpenInNewTab", "linkHints.activateModeWithQueue",
"activateBookmarkFindMode", "activateBookmarkFindModeToOpenInNewTab",
+ "fuzzyMode.activate", "fuzzyMode.activateNewTab",
"goPrevious", "goNext", "nextFrame"],
findCommands: ["enterFindMode", "performFind", "performBackwardsFind"],
historyNavigation:
diff --git a/fuzzyMode.js b/fuzzyMode.js
new file mode 100644
index 00000000..ec2e351b
--- /dev/null
+++ b/fuzzyMode.js
@@ -0,0 +1,166 @@
+var fuzzyMode = (function() {
+ /** Trigger the fuzzy mode dialog */
+ var fuzzyBox = null;
+ function start(newTab) {
+ if (!fuzzyBox) {
+ var completer = new completion.MergingCompleter([
+ new completion.SmartCompleter(),
+ new completion.FuzzyHistoryCompleter(1000),
+ new completion.FuzzyBookmarkCompleter(),
+ ]);
+ fuzzyBox = new FuzzyBox(completer);
+ }
+ fuzzyBox.show(newTab);
+ }
+
+ /** User interface for fuzzy completion */
+ var FuzzyBox = function(completer, reverseAction) {
+ this.prompt = '> ';
+ this.completer = completer;
+ this.initDom();
+ this.reset();
+ }
+ FuzzyBox.prototype = {
+ show: function(reverseAction) {
+ this.reverseAction = reverseAction;
+ this.completer.refresh();
+ this.update();
+ this.box.style.display = 'block';
+ var self = this;
+ handlerStack.push({ keydown: function(event) { self.onKeydown(event); }});
+ },
+
+ hide: function() {
+ this.box.style.display = 'none';
+ handlerStack.pop();
+ },
+
+ reset: function() {
+ this.query = '';
+ this.completions = [];
+ this.selection = 0;
+ this.update();
+ },
+
+ updateSelection: function() {
+ var items = this.completionList.childNodes;
+ for (var i = 0; i < items.length; ++i) {
+ items[i].className = (i == this.selection) ? 'selected' : '';
+ }
+ },
+
+ onKeydown: function(event) {
+ var keyChar = getKeyChar(event);
+
+ if (isEscape(event)) {
+ this.hide();
+ }
+
+ // move selection with Up/Down, Tab/Shift-Tab, Ctrl-k/Ctrl-j
+ else if (keyChar === 'up' || (event.keyCode == 9 && event.shiftKey)
+ || (keyChar === 'k' && event.ctrlKey)) {
+ if (this.selection > 0)
+ this.selection -= 1;
+ this.updateSelection();
+ }
+ else if (keyChar === 'down' || (event.keyCode == 9 && !event.shiftKey)
+ || (keyChar === 'j' && isPrimaryModifierKey(event))) {
+ if (this.selection < this.completions.length - 1)
+ this.selection += 1;
+ this.updateSelection();
+ }
+
+ else if (event.keyCode == keyCodes.backspace) {
+ if (this.query.length > 0) {
+ this.query = this.query.substr(0, this.query.length-1);
+ this.update();
+ }
+ }
+
+ // use primary action with Enter. Holding down Shift/Ctrl uses the alternative action
+ // (opening in new tab)
+ else if (event.keyCode == keyCodes.enter) {
+ var alternative = (event.shiftKey || isPrimaryModifierKey(event));
+ if (this.reverseAction)
+ alternative = !alternative;
+ this.completions[this.selection].action[alternative ? 1 : 0]();
+ this.hide();
+ this.reset();
+ }
+
+ else if (keyChar.length == 1) {
+ this.query += keyChar;
+ this.update();
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+ return true;
+ },
+
+ update: function() {
+ this.query = this.query.replace(/^\s*/, '');
+ this.input.textContent = this.query;
+
+ // clear completions
+ this.completions = [];
+ while (this.completionList.hasChildNodes())
+ this.completionList.removeChild(this.completionList.firstChild);
+
+ if (this.query.length == 0) {
+ this.completionList.style.display = 'none';
+ return;
+ }
+
+ this.completionList.style.display = 'block';
+
+ var li;
+ var counter = 0;
+ var self = this;
+ this.completer.filter(this.query, function(completion) {
+ self.completions.push(completion);
+ li = document.createElement('li');
+ li.innerHTML = completion.render();
+ self.completionList.appendChild(li);
+ return ++counter < 10;
+ });
+
+ this.updateSelection();
+ },
+
+ initDom: function() {
+ this.box = document.createElement('div');
+ this.box.id = 'fuzzybox';
+ this.box.className = 'vimiumReset';
+
+ var inputBox = document.createElement('div');
+ inputBox.className = 'input';
+
+ var promptSpan = document.createElement('span');
+ promptSpan.className = 'prompt';
+ promptSpan.textContent = this.prompt;
+
+ this.input = document.createElement('span');
+ this.input.className = 'query';
+
+ inputBox.appendChild(promptSpan);
+ inputBox.appendChild(this.input);
+
+ this.completionList = document.createElement('ul');
+
+ this.box.appendChild(inputBox);
+ this.box.appendChild(this.completionList);
+
+ this.hide();
+ document.body.appendChild(this.box);
+ },
+ }
+
+ // public interface
+ return {
+ activate: function() { start(false); },
+ activateNewTab: function() { start(true); },
+ }
+
+})();
+
diff --git a/lib/completion.js b/lib/completion.js
new file mode 100644
index 00000000..cad7d455
--- /dev/null
+++ b/lib/completion.js
@@ -0,0 +1,324 @@
+var completion = (function() {
+
+ //============ Helper functions and objects ============//
+
+ /** Singleton object that provides helpers and caching for fuzzy completion. */
+ var fuzzyMatcher = (function() {
+ var self = {};
+
+ self.matcherCacheSize = 300;
+ self.regexNonWord = /\W*/g;
+
+ // cache generated regular expressions
+ self.matcherCache = {};
+ // cache filtered results from recent queries
+ self.filterCache = {};
+
+ /** Normalizes the string specified in :query. Strips any non-word characters and converts
+ * to lower case. */
+ self.normalize = function(query) {
+ return query.replace(self.regexNonWord, '').toLowerCase();
+ }
+
+ /** Calculates a very simple similarity value between a :query and a :string. The current
+ * implementation simply returns the length of the longest prefix of :query that is found within :str.
+ */
+ self.calculateRelevancy = function(query, str) {
+ query = self.normalize(query);
+ str = self.normalize(str);
+ for (var i = query.length; i >= 0; --i) {
+ if (str.indexOf(query.slice(0, i)) >= 0)
+ return i;
+ }
+ return 0;
+ }
+
+ /** Trims the size of the regex cache to the configured size using a FIFO algorithm. */
+ self.cleanMatcherCache = function() {
+ // remove old matchers
+ queries = Object.keys(self.matcherCache);
+ for (var i = 0; i < queries.length - self.matcherCacheSize; ++i)
+ delete self.matcherCache(queries[i]);
+ }
+
+ /** Returns a regex that matches a string using a fuzzy :query. Example: The :query "abc" would result
+ * in a regex like /^([^a])*(a)([^b])*(b)([^c])*(c)(.*)$/
+ */
+ self.getMatcher = function(query) {
+ query = self.normalize(query);
+ if (!(query in self.matcherCache)) {
+ // build up a regex for fuzzy matching
+ var regex = '^';
+ for (var i = 0; i < query.length; ++i)
+ regex += '([^' + query[i] + ']*)(' + query[i] + ')';
+ self.matcherCache[query] = new RegExp(regex + '(.*)$', 'i');
+ }
+ return self.matcherCache[query];
+ }
+
+ /** Clears the filter cache with the given ID. */
+ self.clearFilterCache = function(id) {
+ if (id in self.filterCache)
+ delete self.filterCache[id];
+ }
+
+ /** Filters a list :ary using fuzzy matching against an input string :query. If a query with a less
+ * specific query was issued before (e.g. if the user added a letter to the query), the cached results
+ * of the last filtering are used as a starting point, instead of :ary.
+ */
+ self.filter = function(query, ary, getValue, id, callback) {
+ var filtered = [];
+ var source = ary;
+
+ if (!(id in self.filterCache))
+ self.filterCache[id] = {};
+
+ // find the most specific list of sources in the cache
+ var maxSpecificity = 0;
+ for (key in self.filterCache[id]) {
+ if (!self.filterCache[id].hasOwnProperty(key))
+ continue;
+
+ if ((query.indexOf(key) != 0 && key.indexOf(query) != 0) || key.length > query.length) {
+ // cache entry no longer needed
+ delete self.filterCache[id][key];
+ continue;
+ }
+
+ // is this cache entry the most specific so far?
+ var specificity = self.filterCache[id][key].length;
+ if (query.indexOf(key) == 0 && specificity > maxSpecificity) {
+ source = self.filterCache[id][key];
+ maxSpecificity = specificity;
+ }
+ }
+
+ // clean up
+ self.cleanMatcherCache();
+
+ var matcher = self.getMatcher(query);
+ for (var i = 0; i < source.length; ++i) {
+ if (!matcher.test(getValue(source[i])))
+ continue;
+ filtered.push(source[i]);
+ callback(source[i]);
+ }
+ self.filterCache[id][query] = filtered;
+ }
+
+ return self;
+ })();
+
+ /** Creates an action that opens :url in the current tab by default or in a new tab as an alternative. */
+ function createActionOpenUrl(url) {
+ return [
+ function() { window.location = url; },
+ function() { window.open(url); },
+ ]
+ }
+
+ /** Creates a completion that renders by marking fuzzy-matched parts. */
+ function createHighlightingCompletion(query, str, action, relevancy) {
+ return {
+ render: function() {
+ var match = fuzzyMatcher.getMatcher(query).exec(str);
+ var html = '';
+ for (var i = 1; i < match.length; ++i) {
+ if (i % 2 == 1)
+ html += match[i];
+ else
+ html += '<strong>' + match[i] + '</strong>';
+ };
+ return html;
+ },
+ action: action,
+ relevancy: relevancy,
+ }
+ }
+
+ /** Helper class to construct fuzzy completers for asynchronous data sources like history or bookmark
+ * matchers. */
+ var AsyncFuzzyUrlCompleter = function() {
+ this.completions = null;
+ this.id = utils.createUniqueId();
+ this.readyCallback = function(results) {
+ this.completions = results;
+ }
+ }
+ AsyncFuzzyUrlCompleter.prototype = {
+ // to be implemented by subclasses
+ refresh: function() { },
+
+ calculateRelevancy: function(query, match) {
+ return match.url.length * 10 / (fuzzyMatcher.calculateRelevancy(query, match.str)+1);
+ },
+
+ filter: function(query, callback) {
+ var self = this;
+
+ var handler = function(results) {
+ var filtered = [];
+ fuzzyMatcher.filter(query,
+ self.completions, function(comp) { return comp.str },
+ self.id,
+ function(match) {
+ filtered.push(createHighlightingCompletion(
+ query, match.str,
+ createActionOpenUrl(match.url),
+ self.calculateRelevancy(query, match)));
+ });
+ callback(filtered);
+ }
+
+ // are the results ready?
+ if (this.completions !== null) {
+ // yes: call the callback synchronously
+ handler(this.completions);
+ } else {
+ // no: push our handler to the handling chain
+ var oldReadyCallback = this.readyCallback;
+ this.readyCallback = function(results) {
+ handler(results);
+ this.readyCallback = oldReadyCallback;
+ oldReadyCallback(results);
+ }
+ }
+ },
+ }
+
+ //========== Completer implementations ===========//
+
+ /** A simple completer that suggests to open the input string as an URL or to trigger a web search for the
+ * given term, depending on whether it thinks the input is an URL or not. */
+ var SmartCompleter = function() {
+ this.refresh = function() { };
+
+ this.filter = function(query, callback) {
+ var url;
+ var str;
+
+ if (utils.isUrl(query)) {
+ url = utils.createFullUrl(query);
+ str = '<em>goto</em> ' + url;
+ } else {
+ url = utils.createSearchUrl(query);
+ str = '<em>search</em> ' + query;
+ }
+
+ // call back with exactly one suggestion
+ callback([{
+ render: function() { return str; },
+ action: createActionOpenUrl(url),
+ // relevancy will always be the lowest one, so the suggestion is at the top
+ relevancy: -1,
+ }]);
+ };
+ }
+
+ /** A fuzzy history completer */
+ var FuzzyHistoryCompleter = function(maxEntries) {
+ AsyncFuzzyUrlCompleter.call(this);
+ this.maxEntries = maxEntries || 1000;
+ }
+ FuzzyHistoryCompleter.prototype = new AsyncFuzzyUrlCompleter;
+ FuzzyHistoryCompleter.prototype.refresh = function() {
+ this.completions = null; // reset completions
+
+ // asynchronously fetch history items
+ var port = chrome.extension.connect({ name: "getHistory" }) ;
+ var self = this;
+ port.onMessage.addListener(function(msg) {
+ var results = [];
+
+ for (var i = 0; i < msg.history.length; ++i) {
+ var historyItem = msg.history[i];
+ var title = '';
+
+ if (historyItem.title.length > 0)
+ title = ' (' + historyItem.title + ')';
+
+ results.push({
+ str: 'history: ' + historyItem.url + title,
+ url: historyItem.url,
+ });
+ }
+ port = null;
+ self.readyCallback(results);
+ });
+ port.postMessage({ maxResults: this.maxEntries });
+ }
+
+ /** A fuzzy bookmark completer */
+ var FuzzyBookmarkCompleter = function() {
+ AsyncFuzzyUrlCompleter.call(this);
+ }
+ FuzzyBookmarkCompleter.prototype = new AsyncFuzzyUrlCompleter;
+ FuzzyBookmarkCompleter.prototype.refresh = function() {
+ this.completions = null; // reset completions
+
+ var port = chrome.extension.connect({ name: "getAllBookmarks" }) ;
+ var self = this;
+ port.onMessage.addListener(function(msg) {
+ var results = [];
+
+ for (var i = 0; i < msg.bookmarks.length; ++i) {
+ var bookmark = msg.bookmarks[i];
+
+ var title = '';
+ if (bookmark.title.length > 0)
+ title = ' (' + bookmark.title + ')';
+
+ results.push({
+ str: 'bookmark: ' + bookmark.url + title,
+ url: bookmark.url,
+ });
+ }
+ port = null;
+ self.readyCallback(results);
+ });
+ port.postMessage();
+ }
+
+ /** A meta-completer that delegates queries and merges and sorts the results of a collection of other
+ * completer instances. */
+ var MergingCompleter = function(sources) {
+ this.sources = sources;
+ }
+ MergingCompleter.prototype = {
+ refresh: function() {
+ for (var i = 0; i < this.sources.length; ++i)
+ this.sources[i].refresh();
+ },
+
+ filter: function(query, callback) {
+ var all = [];
+ var counter = this.sources.length;
+
+ for (var i = 0; i < this.sources.length; ++i) {
+ this.sources[i].filter(query, function(results) {
+ all = all.concat(results);
+ if (--counter > 0)
+ return;
+
+ // all sources have provided results by now, so we can sort and return
+ all.sort(function(a,b) {
+ return a.relevancy - b.relevancy;
+ });
+ for (var i = 0; i < all.length; ++i) {
+ if (!callback(all[i]))
+ // the caller doesn't want any more results
+ return;
+ }
+ });
+ }
+ }
+ }
+
+ // public interface
+ return {
+ FuzzyHistoryCompleter: FuzzyHistoryCompleter,
+ FuzzyBookmarkCompleter: FuzzyBookmarkCompleter,
+ SmartCompleter: SmartCompleter,
+ MergingCompleter: MergingCompleter,
+ };
+})();
diff --git a/lib/keyboardUtils.js b/lib/keyboardUtils.js
index 98725d95..84e860ba 100644
--- a/lib/keyboardUtils.js
+++ b/lib/keyboardUtils.js
@@ -47,7 +47,8 @@ function getKeyChar(event) {
keyIdentifier = event.shiftKey ? correctedIdentifiers[1] : correctedIdentifiers[0];
}
var unicodeKeyInHex = "0x" + keyIdentifier.substring(2);
- return String.fromCharCode(parseInt(unicodeKeyInHex)).toLowerCase();
+ var character = String.fromCharCode(parseInt(unicodeKeyInHex)).toLowerCase();
+ return event.shiftKey ? character.toUpperCase() : character;
}
function isPrimaryModifierKey(event) {
diff --git a/lib/utils.js b/lib/utils.js
index 8aada3a1..922e7db3 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -21,17 +21,26 @@ var utils = {
},
/**
- * Creates a search URL from the given :query.
+ * Generates a unique ID
*/
- createSearchUrl: function(query) {
- return "http://www.google.com/search?q=" + query;
+ createUniqueId: (function() {
+ id = 0;
+ return function() { return ++id; };
+ })(),
+
+ /**
+ * Completes a partial URL (without scheme)
+ */
+ createFullUrl: function(partialUrl) {
+ if (!/^[a-z]{3,}:\/\//.test(partialUrl))
+ partialUrl = 'http://' + partialUrl;
+ return partialUrl
},
/**
- * Tries to convert :str into a valid URL.
- * We don't bother with escaping characters, however, as Chrome will do that for us.
+ * Tries to detect, whether :str is a valid URL.
*/
- ensureUrl: function(str) {
+ isUrl: function(str) {
// more or less RFC compliant URL host part parsing. This should be sufficient
// for our needs
var urlRegex = new RegExp(
@@ -55,38 +64,60 @@ var utils = {
// it starts with a scheme, so it's definitely an URL
if (/^[a-z]{3,}:\/\//.test(str))
- return str;
- var strWithScheme = 'http://' + str;
+ return true;
- // definitely not a valid URL; treat as search query
+ // spaces => definitely not a valid URL
if (str.indexOf(' ') >= 0)
- return utils.createSearchUrl(str);
+ return false;
// assuming that this is an URL, try to parse it into its meaningful parts. If matching fails, we're
// pretty sure that we don't have some kind of URL here.
var match = urlRegex.exec(str.split('/')[0]);
if (!match)
- return utils.createSearchUrl(str);
+ return false;
var hostname = match[3];
// allow known special host names
if (specialHostNames.indexOf(hostname) >= 0)
- return strWithScheme;
+ return true;
// allow IPv6 addresses (need to be wrapped in brackets, as required by RFC). It is sufficient to check
// for a colon here, as the regex wouldn't match colons in the host name unless it's an v6 address
if (hostname.indexOf(':') >= 0)
- return strWithScheme;
+ return true;
// at this point we have to make a decision. As a heuristic, we check if the input has dots in it. If
- // yes, and if the last part could be a TLD, treat it as an URL
+ // yes, and if the last part could be a TLD, treat it as an URL.
var dottedParts = hostname.split('.');
var lastPart = dottedParts[dottedParts.length-1];
- if (dottedParts.length > 1 && (lastPart.length <= 3 || longTlds.indexOf(lastPart) >= 0))
- return strWithScheme;
+ if (dottedParts.length > 1 && ((lastPart.length >= 2 && lastPart.length <= 3)
+ || longTlds.indexOf(lastPart) >= 0))
+ return true;
+
+ // also allow IPv4 addresses
+ if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostname))
+ return true;
+
+ // fallback: no URL
+ return false
+ },
- // fallback: use as search query
- return utils.createSearchUrl(str);
+ /**
+ * Creates a search URL from the given :query.
+ */
+ createSearchUrl: function(query) {
+ return "http://www.google.com/search?q=" + query;
+ },
+
+ /**
+ * Tries to convert :str into a valid URL.
+ * We don't bother with escaping characters, however, as Chrome will do that for us.
+ */
+ ensureUrl: function(str) {
+ if (utils.isUrl(str))
+ return utils.createFullUrl(str);
+ else
+ return utils.createSearchUrl(str);
},
};
diff --git a/manifest.json b/manifest.json
index 72631a94..a14e2fef 100644
--- a/manifest.json
+++ b/manifest.json
@@ -10,6 +10,7 @@
"permissions": [
"tabs",
"bookmarks",
+ "history",
"clipboardRead",
"<all_urls>"
],
@@ -20,7 +21,9 @@
"lib/keyboardUtils.js",
"lib/domUtils.js",
"lib/clipboard.js",
+ "lib/completion.js",
"linkHints.js",
+ "fuzzyMode.js",
"vimiumFrontend.js",
"completionDialog.js",
"bookmarks.js"
diff --git a/vimium.css b/vimium.css
index ef33428c..35a4473b 100644
--- a/vimium.css
+++ b/vimium.css
@@ -282,4 +282,74 @@ div.vimium-completions div.vimium-noResults{
body.vimiumFindMode ::selection {
background: #ff9632;
+};
+
+/* fuzzymode CSS */
+
+#fuzzybox ol, #fuzzybox ul {
+ list-style: none;
+}
+
+#fuzzybox {
+ position: fixed;
+ width: 80%;
+ top: 70px;
+ left: 50%;
+ margin: 0 0 0 -40%;
+ background: black;
+ color: white;
+ font-family: sans-serif;
+ font-size: 30px;
+ text-align: left;
+ padding: 7px 20px 7px 20px;
+ opacity: 0.9;
+ border-radius: 10px;
+ box-shadow: 5px 5px 5px rgba(0,0,0,0.5);
+ z-index: 99999998;
+}
+
+#fuzzybox ul {
+ list-style: none;
+ padding: 7px 0 0 0;
+ margin: 7px 0 0 0;
+ border-top: 2px solid #444;
+}
+
+#fuzzybox li {
+ border-bottom: 1px solid #111;
+ line-height: 1.1em;
+ padding: 7px;
+ margin: 0 -7px 0 -7px;
+ font-size: 18px;
+ color: #aaa;
+}
+
+#fuzzybox li strong {
+ color: red;
+}
+
+#fuzzybox li em {
+ color: #444;
+ font-weight: normal;
+ font-style: italic;
+}
+
+#fuzzybox li.selected {
+ background: #222;
+ color: #ccc;
+ border-radius: 4px;
+}
+#fuzzybox li.selected em {
+ color: #666;
+}
+
+#fuzzybox .input {
+ font-size: 28px;
+ padding: 0;
+ margin: 0;
+}
+
+#fuzzybox .input .prompt {
+ color: orange;
+ content: '>> ';
}