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