]> git.donarmstrong.com Git - dactyl.git/blob - common/content/bookmarks.js
Import 1.0b7.1 supporting Firefox up to 8.*
[dactyl.git] / common / content / bookmarks.js
1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 "use strict";
8
9 var DEFAULT_FAVICON = "chrome://mozapps/skin/places/defaultFavicon.png";
10
11 // also includes methods for dealing with keywords and search engines
12 var Bookmarks = Module("bookmarks", {
13     init: function () {
14         this.timer = Timer(0, 100, function () {
15             this.checkBookmarked(buffer.uri);
16         }, this);
17
18         storage.addObserver("bookmark-cache", function (key, event, arg) {
19             if (["add", "change", "remove"].indexOf(event) >= 0)
20                 autocommands.trigger("Bookmark" + event[0].toUpperCase() + event.substr(1),
21                      iter({
22                          bookmark: {
23                              toString: function () "bookmarkcache.bookmarks[" + arg.id + "]",
24                              valueOf: function () arg
25                          }
26                      }, arg));
27             bookmarks.timer.tell();
28         }, window);
29     },
30
31     signals: {
32         "browser.locationChange": function (webProgress, request, uri) {
33             statusline.bookmarked = false;
34             this.checkBookmarked(uri);
35         }
36     },
37
38     get format() ({
39         anchored: false,
40         title: ["URL", "Info"],
41         keys: { text: "url", description: "title", icon: "icon", extra: "extra", tags: "tags" },
42         process: [template.icon, template.bookmarkDescription]
43     }),
44
45     // TODO: why is this a filter? --djk
46     get: function get(filter, tags, maxItems, extra) {
47         return completion.runCompleter("bookmark", filter, maxItems, tags, extra);
48     },
49
50     /**
51      * Adds a new bookmark. The first parameter should be an object with
52      * any of the following properties:
53      *
54      * @param {boolean} unfiled If true, the bookmark is added to the
55      *      Unfiled Bookmarks Folder.
56      * @param {string} title The title of the new bookmark.
57      * @param {string} url The URL of the new bookmark.
58      * @param {string} keyword The keyword of the new bookmark.
59      *      @optional
60      * @param {[string]} tags The tags for the new bookmark.
61      *      @optional
62      * @param {boolean} force If true, a new bookmark is always added.
63      *      Otherwise, if a bookmark for the given URL exists it is
64      *      updated instead.
65      *      @optional
66      * @returns {boolean} True if the bookmark was added or updated
67      *      successfully.
68      */
69     add: function add(unfiled, title, url, keyword, tags, force) {
70         // FIXME
71         if (isObject(unfiled))
72             var { unfiled, title, url, keyword, tags, post, charset, force } = unfiled;
73
74         try {
75             let uri = util.createURI(url);
76             if (!force && bookmarkcache.isBookmarked(uri))
77                 for (var bmark in bookmarkcache)
78                     if (bmark.url == uri.spec) {
79                         if (title)
80                             bmark.title = title;
81                         break;
82                     }
83
84             if (tags) {
85                 PlacesUtils.tagging.untagURI(uri, null);
86                 PlacesUtils.tagging.tagURI(uri, tags);
87             }
88             if (bmark == undefined)
89                 bmark = bookmarkcache.bookmarks[
90                     services.bookmarks.insertBookmark(
91                          services.bookmarks[unfiled ? "unfiledBookmarksFolder" : "bookmarksMenuFolder"],
92                          uri, -1, title || url)];
93             if (!bmark)
94                 return false;
95
96             if (charset !== undefined)
97                 bmark.charset = charset;
98             if (post !== undefined)
99                 bmark.post = post;
100             if (keyword)
101                 bmark.keyword = keyword;
102         }
103         catch (e) {
104             util.reportError(e);
105             return false;
106         }
107
108         return true;
109     },
110
111     /**
112      * Opens the command line in Ex mode pre-filled with a :bmark
113      * command to add a new search keyword for the given form element.
114      *
115      * @param {Element} elem A form element for which to add a keyword.
116      */
117     addSearchKeyword: function addSearchKeyword(elem) {
118         if (elem instanceof HTMLFormElement || elem.form)
119             var [url, post, charset] = util.parseForm(elem);
120         else
121             var [url, post, charset] = [elem.href || elem.src, null, elem.ownerDocument.characterSet];
122
123         let options = { "-title": "Search " + elem.ownerDocument.title };
124         if (post != null)
125             options["-post"] = post;
126         if (charset != null && charset !== "UTF-8")
127             options["-charset"] = charset;
128
129         CommandExMode().open(
130             commands.commandToString({ command: "bmark", options: options, arguments: [url] }) + " -keyword ");
131     },
132
133     checkBookmarked: function checkBookmarked(uri) {
134         if (PlacesUtils.asyncGetBookmarkIds)
135             PlacesUtils.asyncGetBookmarkIds(uri, function (ids) {
136                 statusline.bookmarked = ids.length;
137             });
138         else
139             this.timeout(function () {
140                 statusline.bookmarked = bookmarkcache.isBookmarked(uri);
141             });
142     },
143
144     /**
145      * Toggles the bookmarked state of the given URL. If the URL is
146      * bookmarked, all bookmarks for said URL are removed.
147      * If it is not, a new bookmark is added to the Unfiled Bookmarks
148      * Folder. The new bookmark has the title of the current buffer if
149      * its URL is identical to *url*, otherwise its title will be the
150      * value of *url*.
151      *
152      * @param {string} url The URL of the bookmark to toggle.
153      */
154     toggle: function toggle(url) {
155         if (!url)
156             return;
157
158         let count = this.remove(url);
159         if (count > 0)
160             dactyl.echomsg({ domains: [util.getHost(url)], message: _("bookmark.removed", url) });
161         else {
162             let title = buffer.uri.spec == url && buffer.title || url;
163             let extra = "";
164             if (title != url)
165                 extra = " (" + title + ")";
166             this.add({ unfiled: true, title: title, url: url });
167             dactyl.echomsg({ domains: [util.getHost(url)], message: _("bookmark.added", url + extra) });
168         }
169     },
170
171     isBookmarked: deprecated("bookmarkcache.isBookmarked", { get: function isBookmarked() bookmarkcache.closure.isBookmarked }),
172
173     /**
174      * Remove a bookmark or bookmarks. If *ids* is an array, removes the
175      * bookmarks with those IDs. If it is a string, removes all
176      * bookmarks whose URLs match that string.
177      *
178      * @param {string|[number]} ids The IDs or URL of the bookmarks to
179      *      remove.
180      * @returns {number} The number of bookmarks removed.
181      */
182     remove: function remove(ids) {
183         try {
184             if (!isArray(ids)) {
185                 let uri = util.newURI(ids);
186                 ids = services.bookmarks
187                               .getBookmarkIdsForURI(uri, {})
188                               .filter(bookmarkcache.closure.isRegularBookmark);
189             }
190             ids.forEach(function (id) {
191                 let bmark = bookmarkcache.bookmarks[id];
192                 if (bmark) {
193                     PlacesUtils.tagging.untagURI(bmark.uri, null);
194                     bmark.charset = null;
195                 }
196                 services.bookmarks.removeItem(id);
197             });
198             return ids.length;
199         }
200         catch (e) {
201             dactyl.reportError(e, true);
202             return 0;
203         }
204     },
205
206     getSearchEngines: deprecated("bookmarks.searchEngines", function getSearchEngines() this.searchEngines),
207     /**
208      * Returns a list of all visible search engines in the search
209      * services, augmented with keyword, title, and icon properties for
210      * use in completion functions.
211      */
212     get searchEngines() {
213         let searchEngines = [];
214         let aliases = {};
215         return iter(services.browserSearch.getVisibleEngines({})).map(function ([, engine]) {
216             let alias = engine.alias;
217             if (!alias || !/^[a-z0-9-]+$/.test(alias))
218                 alias = engine.name.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/, "").toLowerCase();
219             if (!alias)
220                 alias = "search"; // for search engines which we can't find a suitable alias
221
222             if (Set.has(aliases, alias))
223                 alias += ++aliases[alias];
224             else
225                 aliases[alias] = 0;
226
227             return [alias, { keyword: alias, __proto__: engine, title: engine.description, icon: engine.iconURI && engine.iconURI.spec }];
228         }).toObject();
229     },
230
231     /**
232      * Retrieves a list of search suggestions from the named search
233      * engine based on the given *query*. The results are always in the
234      * form of an array of strings. If *callback* is provided, the
235      * request is executed asynchronously and *callback* is called on
236      * completion. Otherwise, the request is executed synchronously and
237      * the results are returned.
238      *
239      * @param {string} engineName The name of the search engine from
240      *      which to request suggestions.
241      * @param {string} query The query string for which to request
242      *      suggestions.
243      * @param {function([string])} callback The function to call when
244      *      results are returned.
245      * @returns {[string] | null}
246      */
247     getSuggestions: function getSuggestions(engineName, query, callback) {
248         const responseType = "application/x-suggestions+json";
249
250         let engine = Set.has(this.searchEngines, engineName) && this.searchEngines[engineName];
251         if (engine && engine.supportsResponseType(responseType))
252             var queryURI = engine.getSubmission(query, responseType).uri.spec;
253         if (!queryURI)
254             return (callback || util.identity)([]);
255
256         function process(req) {
257             let results = [];
258             try {
259                 results = JSON.parse(req.responseText)[1].filter(isString);
260             }
261             catch (e) {}
262             if (callback)
263                 return callback(results);
264             return results;
265         }
266
267         let req = util.httpGet(queryURI, callback && process);
268         if (callback)
269             return req;
270         return process(req);
271     },
272
273     /**
274      * Returns an array containing a search URL and POST data for the
275      * given search string. If *useDefsearch* is true, the string is
276      * always passed to the default search engine. If it is not, the
277      * search engine name is retrieved from the first space-separated
278      * token of the given string.
279      *
280      * Returns null if no search engine is found for the passed string.
281      *
282      * @param {string} text The text for which to retrieve a search URL.
283      * @param {boolean} useDefsearch Whether to use the default search
284      *      engine.
285      * @returns {[string, string | null] | null}
286      */
287     getSearchURL: function getSearchURL(text, useDefsearch) {
288         let query = (useDefsearch ? options["defsearch"] + " " : "") + text;
289
290         // ripped from Firefox
291         var keyword = query;
292         var param = "";
293         var offset = query.indexOf(" ");
294         if (offset > 0) {
295             keyword = query.substr(0, offset);
296             param = query.substr(offset + 1);
297         }
298
299         var engine = Set.has(bookmarks.searchEngines, keyword) && bookmarks.searchEngines[keyword];
300         if (engine) {
301             if (engine.searchForm && !param)
302                 return engine.searchForm;
303             let submission = engine.getSubmission(param, null);
304             return [submission.uri.spec, submission.postData];
305         }
306
307         let [url, postData] = PlacesUtils.getURLAndPostDataForKeyword(keyword);
308         if (!url)
309             return null;
310
311         let data = window.unescape(postData || "");
312         if (/%s/i.test(url) || /%s/i.test(data)) {
313             var charset = "";
314             var matches = url.match(/^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/);
315             if (matches)
316                 [, url, charset] = matches;
317             else
318                 try {
319                     charset = services.history.getCharsetForURI(util.newURI(url));
320                 }
321                 catch (e) {}
322
323             if (charset)
324                 var encodedParam = escape(window.convertFromUnicode(charset, param));
325             else
326                 encodedParam = bookmarkcache.keywords[keyword].encodeURIComponent(param);
327
328             url = url.replace(/%s/g, encodedParam).replace(/%S/g, param);
329             if (/%s/i.test(data))
330                 postData = window.getPostDataStream(data, param, encodedParam, "application/x-www-form-urlencoded");
331         }
332         else if (param)
333             postData = null;
334
335         if (postData)
336             return [url, postData];
337         return url;
338     },
339
340     /**
341      * Lists all bookmarks whose URLs match *filter*, tags match *tags*,
342      * and other properties match the properties of *extra*. If
343      * *openItems* is true, the items are opened in tabs rather than
344      * listed.
345      *
346      * @param {string} filter A URL filter string which the URLs of all
347      *      matched items must contain.
348      * @param {[string]} tags An array of tags each of which all matched
349      *      items must contain.
350      * @param {boolean} openItems If true, items are opened rather than
351      *      listed.
352      * @param {object} extra Extra properties which must be matched.
353      */
354     list: function list(filter, tags, openItems, maxItems, extra) {
355         // FIXME: returning here doesn't make sense
356         //   Why the hell doesn't it make sense? --Kris
357         // Because it unconditionally bypasses the final error message
358         // block and does so only when listing items, not opening them. In
359         // short it breaks the :bmarks command which doesn't make much
360         // sense to me but I'm old-fashioned. --djk
361         if (!openItems)
362             return completion.listCompleter("bookmark", filter, maxItems, tags, extra);
363         let items = completion.runCompleter("bookmark", filter, maxItems, tags, extra);
364
365         if (items.length)
366             return dactyl.open(items.map(function (i) i.url), dactyl.NEW_TAB);
367
368         if (filter.length > 0 && tags.length > 0)
369             dactyl.echoerr(_("bookmark.noMatching", tags.map(String.quote), filter.quote()));
370         else if (filter.length > 0)
371             dactyl.echoerr(_("bookmark.noMatchingString", filter.quote()));
372         else if (tags.length > 0)
373             dactyl.echoerr(_("bookmark.noMatchingTags", tags.map(String.quote)));
374         else
375             dactyl.echoerr(_("bookmark.none"));
376         return null;
377     }
378 }, {
379 }, {
380     commands: function () {
381         commands.add(["ju[mps]"],
382             "Show jumplist",
383             function () {
384                 let sh = history.session;
385                 commandline.commandOutput(template.jumps(sh.index, sh));
386             },
387             { argCount: "0" });
388
389         // TODO: Clean this up.
390         const tags = {
391             names: ["-tags", "-T"],
392             description: "A comma-separated list of tags",
393             completer: function tags(context, args) {
394                 context.generate = function () array(b.tags for (b in bookmarkcache) if (b.tags)).flatten().uniq().array;
395                 context.keys = { text: util.identity, description: util.identity };
396             },
397             type: CommandOption.LIST
398         };
399
400         const title = {
401             names: ["-title", "-t"],
402             description: "Bookmark page title or description",
403             completer: function title(context, args) {
404                 let frames = buffer.allFrames();
405                 if (!args.bang)
406                     return [
407                         [win.document.title, frames.length == 1 ? /*L*/"Current Location" : /*L*/"Frame: " + win.location.href]
408                         for ([, win] in Iterator(frames))];
409                 context.keys.text = "title";
410                 context.keys.description = "url";
411                 return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: args["-keyword"], title: context.filter });
412             },
413             type: CommandOption.STRING
414         };
415
416         const post = {
417             names: ["-post", "-p"],
418             description: "Bookmark POST data",
419             completer: function post(context, args) {
420                 context.keys.text = "post";
421                 context.keys.description = "url";
422                 return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: args["-keyword"], post: context.filter });
423             },
424             type: CommandOption.STRING
425         };
426
427         const keyword = {
428             names: ["-keyword", "-k"],
429             description: "Keyword by which this bookmark may be opened (:open {keyword})",
430             completer: function keyword(context, args) {
431                 context.keys.text = "keyword";
432                 return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: context.filter, title: args["-title"] });
433             },
434             type: CommandOption.STRING,
435             validator: function (arg) /^\S+$/.test(arg)
436         };
437
438         commands.add(["bma[rk]"],
439             "Add a bookmark",
440             function (args) {
441                 let opts = {
442                     force: args.bang,
443                     unfiled: false,
444                     keyword: args["-keyword"] || null,
445                     charset: args["-charset"],
446                     post: args["-post"],
447                     tags: args["-tags"] || [],
448                     title: args["-title"] || (args.length === 0 ? buffer.title : null),
449                     url: args.length === 0 ? buffer.uri.spec : args[0]
450                 };
451
452                 if (bookmarks.add(opts)) {
453                     let extra = (opts.title == opts.url) ? "" : " (" + opts.title + ")";
454                     dactyl.echomsg({ domains: [util.getHost(opts.url)], message: _("bookmark.added", opts.url + extra) },
455                                    1, commandline.FORCE_SINGLELINE);
456                 }
457                 else
458                     dactyl.echoerr(_("bookmark.cantAdd", opts.title.quote()));
459             }, {
460                 argCount: "?",
461                 bang: true,
462                 completer: function (context, args) {
463                     if (!args.bang) {
464                         context.title = ["Page URL"];
465                         let frames = buffer.allFrames();
466                         context.completions = [
467                             [win.document.documentURI, frames.length == 1 ? /*L*/"Current Location" : /*L*/"Frame: " + win.document.title]
468                             for ([, win] in Iterator(frames))];
469                         return;
470                     }
471                     completion.bookmark(context, args["-tags"], { keyword: args["-keyword"], title: args["-title"] });
472                 },
473                 options: [keyword, title, tags, post,
474                     {
475                         names: ["-charset", "-c"],
476                         description: "The character encoding of the bookmark",
477                         type: CommandOption.STRING,
478                         completer: function (context) completion.charset(context),
479                         validator: Option.validateCompleter
480                     }
481                 ]
482             });
483
484         commands.add(["bmarks"],
485             "List or open multiple bookmarks",
486             function (args) {
487                 bookmarks.list(args.join(" "), args["-tags"] || [], args.bang, args["-max"],
488                                { keyword: args["-keyword"], title: args["-title"] });
489             },
490             {
491                 bang: true,
492                 completer: function completer(context, args) {
493                     context.filter = args.join(" ");
494                     completion.bookmark(context, args["-tags"], { keyword: args["-keyword"], title: args["-title"] });
495                 },
496                 options: [tags, keyword, title,
497                     {
498                         names: ["-max", "-m"],
499                         description: "The maximum number of items to list or open",
500                         type: CommandOption.INT
501                     }
502                 ]
503                 // Not privateData, since we don't treat bookmarks as private
504             });
505
506         commands.add(["delbm[arks]"],
507             "Delete a bookmark",
508             function (args) {
509                 if (args.bang)
510                     commandline.input(_("bookmark.prompt.deleteAll") + " ",
511                         function (resp) {
512                             if (resp && resp.match(/^y(es)?$/i)) {
513                                 bookmarks.remove(Object.keys(bookmarkcache.bookmarks));
514                                 dactyl.echomsg(_("bookmark.allDeleted"));
515                             }
516                         });
517                 else {
518                     if (!(args.length || args["-tags"] || args["-keyword"] || args["-title"]))
519                         var deletedCount = bookmarks.remove(buffer.uri.spec);
520                     else {
521                         let context = CompletionContext(args.join(" "));
522                         context.fork("bookmark", 0, completion, "bookmark",
523                                      args["-tags"], { keyword: args["-keyword"], title: args["-title"] });
524                         deletedCount = bookmarks.remove(context.allItems.items.map(function (item) item.item.id));
525                     }
526
527                     dactyl.echomsg({ message: _("bookmark.deleted", deletedCount) });
528                 }
529
530             },
531             {
532                 argCount: "?",
533                 bang: true,
534                 completer: function completer(context, args)
535                     completion.bookmark(context, args["-tags"], { keyword: args["-keyword"], title: args["-title"] }),
536                 domains: function (args) array.compact(args.map(util.getHost)),
537                 literal: 0,
538                 options: [tags, title, keyword],
539                 privateData: true
540             });
541     },
542     mappings: function () {
543         var myModes = config.browserModes;
544
545         mappings.add(myModes, ["a"],
546             "Open a prompt to bookmark the current URL",
547             function () {
548                 let options = {};
549
550                 let url = buffer.uri.spec;
551                 let bmarks = bookmarks.get(url).filter(function (bmark) bmark.url == url);
552
553                 if (bmarks.length == 1) {
554                     let bmark = bmarks[0];
555
556                     options["-title"] = bmark.title;
557                     if (bmark.charset)
558                         options["-charset"] = bmark.charset;
559                     if (bmark.keyword)
560                         options["-keyword"] = bmark.keyword;
561                     if (bmark.post)
562                         options["-post"] = bmark.post;
563                     if (bmark.tags.length > 0)
564                         options["-tags"] = bmark.tags;
565                 }
566                 else {
567                     if (buffer.title != buffer.uri.spec)
568                         options["-title"] = buffer.title;
569                     if (content.document.characterSet !== "UTF-8")
570                         options["-charset"] = content.document.characterSet;
571                 }
572
573                 CommandExMode().open(
574                     commands.commandToString({ command: "bmark", options: options, arguments: [buffer.uri.spec] }));
575             });
576
577         mappings.add(myModes, ["A"],
578             "Toggle bookmarked state of current URL",
579             function () { bookmarks.toggle(buffer.uri.spec); });
580     },
581     options: function () {
582         options.add(["defsearch", "ds"],
583             "The default search engine",
584             "string", "google",
585             {
586                 completer: function completer(context) {
587                     completion.search(context, true);
588                     context.completions = [{ keyword: "", title: "Don't perform searches by default" }].concat(context.completions);
589                 }
590             });
591
592         options.add(["suggestengines"],
593              "Search engines used for search suggestions",
594              "stringlist", "google",
595              { completer: function completer(context) completion.searchEngine(context, true), });
596     },
597
598     completion: function () {
599         completion.bookmark = function bookmark(context, tags, extra) {
600             context.title = ["Bookmark", "Title"];
601             context.format = bookmarks.format;
602             iter(extra || {}).forEach(function ([k, v]) {
603                 if (v != null)
604                     context.filters.push(function (item) item.item[k] != null && this.matchString(v, item.item[k]));
605             });
606             context.generate = function () values(bookmarkcache.bookmarks);
607             completion.urls(context, tags);
608         };
609
610         completion.search = function search(context, noSuggest) {
611             let [, keyword, space, args] = context.filter.match(/^\s*(\S*)(\s*)(.*)$/);
612             let keywords = bookmarkcache.keywords;
613             let engines = bookmarks.searchEngines;
614
615             context.title = ["Search Keywords"];
616             context.completions = iter(values(keywords), values(engines));
617             context.keys = { text: "keyword", description: "title", icon: "icon" };
618
619             if (!space || noSuggest)
620                 return;
621
622             context.fork("suggest", keyword.length + space.length, this, "searchEngineSuggest",
623                          keyword, true);
624
625             let item = keywords[keyword];
626             if (item && item.url.indexOf("%s") > -1)
627                 context.fork("keyword/" + keyword, keyword.length + space.length, null, function (context) {
628                     context.format = history.format;
629                     context.title = [/*L*/keyword + " Quick Search"];
630                     // context.background = true;
631                     context.compare = CompletionContext.Sort.unsorted;
632                     context.generate = function () {
633                         let [begin, end] = item.url.split("%s");
634
635                         return history.get({ uri: util.newURI(begin), uriIsPrefix: true }).map(function (item) {
636                             let rest = item.url.length - end.length;
637                             let query = item.url.substring(begin.length, rest);
638                             if (item.url.substr(rest) == end && query.indexOf("&") == -1)
639                                 try {
640                                     item.url = decodeURIComponent(query.replace(/#.*/, "").replace(/\+/g, " "));
641                                     return item;
642                                 }
643                                 catch (e) {}
644                             return null;
645                         }).filter(util.identity);
646                     };
647                 });
648         };
649
650         completion.searchEngine = function searchEngine(context, suggest) {
651              context.title = ["Suggest Engine", "Description"];
652              context.keys = { text: "keyword", description: "title", icon: "icon" };
653              context.completions = values(bookmarks.searchEngines);
654              if (suggest)
655                  context.filters.push(function ({ item }) item.supportsResponseType("application/x-suggestions+json"));
656
657         };
658
659         completion.searchEngineSuggest = function searchEngineSuggest(context, engineAliases, kludge) {
660             if (!context.filter)
661                 return;
662
663             let engineList = (engineAliases || options["suggestengines"].join(",") || "google").split(",");
664
665             engineList.forEach(function (name) {
666                 let engine = bookmarks.searchEngines[name];
667                 if (!engine)
668                     return;
669                 let [, word] = /^\s*(\S+)/.exec(context.filter) || [];
670                 if (!kludge && word == name) // FIXME: Check for matching keywords
671                     return;
672                 let ctxt = context.fork(name, 0);
673
674                 ctxt.title = [/*L*/engine.description + " Suggestions"];
675                 ctxt.keys = { text: util.identity, description: function () "" };
676                 ctxt.compare = CompletionContext.Sort.unsorted;
677                 ctxt.filterFunc = null;
678
679                 let words = ctxt.filter.toLowerCase().split(/\s+/g);
680                 ctxt.completions = ctxt.completions.filter(function (i) words.every(function (w) i.toLowerCase().indexOf(w) >= 0));
681
682                 ctxt.hasItems = ctxt.completions.length;
683                 ctxt.incomplete = true;
684                 ctxt.cache.request = bookmarks.getSuggestions(name, ctxt.filter, function (compl) {
685                     ctxt.incomplete = false;
686                     ctxt.completions = array.uniq(ctxt.completions.filter(function (c) compl.indexOf(c) >= 0)
687                                                       .concat(compl), true);
688                 });
689             });
690         };
691
692         completion.addUrlCompleter("S", "Suggest engines", completion.searchEngineSuggest);
693         completion.addUrlCompleter("b", "Bookmarks", completion.bookmark);
694         completion.addUrlCompleter("s", "Search engines and keyword URLs", completion.search);
695     }
696 });
697
698 // vim: set fdm=marker sw=4 ts=4 et: