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