]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/completion.jsm
finalize changelog for 7904
[dactyl.git] / common / modules / completion.jsm
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 defineModule("completion", {
10     exports: ["CompletionContext", "Completion", "completion"]
11 }, this);
12
13 lazyRequire("dom", ["DOM"]);
14 lazyRequire("messages", ["_", "messages"]);
15 lazyRequire("template", ["template"]);
16
17 /**
18  * Creates a new completion context.
19  *
20  * @class A class to provide contexts for command completion.
21  * Manages the filtering and formatting of completions, and keeps
22  * track of the positions and quoting of replacement text. Allows for
23  * the creation of sub-contexts with different headers and quoting
24  * rules.
25  *
26  * @param {nsIEditor} editor The editor for which completion is
27  *     intended. May be a {CompletionContext} when forking a context,
28  *     or a {string} when creating a new one.
29  * @param {string} name The name of this context. Used when the
30  *     context is forked.
31  * @param {number} offset The offset from the parent context.
32  * @author Kris Maglione <maglione.k@gmail.com>
33  * @constructor
34  */
35 var CompletionContext = Class("CompletionContext", {
36     init: function cc_init(editor, name="", offset=0) {
37         let self = this;
38         if (editor instanceof this.constructor) {
39             let parent = editor;
40             name = parent.name + "/" + name;
41
42             if (this.options) {
43                 this.autoComplete = this.options.get("autocomplete").getKey(name);
44                 this.sortResults  = this.options.get("wildsort").getKey(name);
45                 this.wildcase     = this.options.get("wildcase").getKey(name);
46             }
47
48             this.contexts = parent.contexts;
49             if (name in this.contexts)
50                 self = this.contexts[name];
51             else
52                 self.contexts[name] = this;
53
54             /**
55              * @property {CompletionContext} This context's parent. {null} when
56              *     this is a top-level context.
57              */
58             self.parent = parent;
59
60             ["filters", "keys", "process", "title", "quote"]
61                 .forEach(key => self[key] = parent[key] && util.cloneObject(parent[key]));
62             ["anchored", "compare", "editor", "_filter", "filterFunc", "forceAnchored", "top"]
63                 .forEach(key => self[key] = parent[key]);
64
65             self.__defineGetter__("value", function get_value() this.top.value);
66
67             self.offset = parent.offset;
68             self.advance(offset);
69
70             /**
71              * @property {boolean} Specifies that this context is not finished
72              *     generating results.
73              * @default false
74              */
75             self.incomplete = false;
76             self.message = null;
77             /**
78              * @property {boolean} Specifies that this context is waiting for the
79              *     user to press <Tab>. Useful when fetching completions could be
80              *     dangerous or slow, and the user has enabled autocomplete.
81              */
82             self.waitingForTab = false;
83
84             self.hasItems = null;
85
86             delete self._generate;
87             delete self.ignoreCase;
88             if (self != this)
89                 return self;
90             ["_caret", "contextList", "maxItems", "onUpdate", "selectionTypes", "tabPressed", "updateAsync", "value"].forEach(function fe(key) {
91                 self.__defineGetter__(key, function () this.top[key]);
92                 self.__defineSetter__(key, function (val) this.top[key] = val);
93             });
94         }
95         else {
96             if (typeof editor == "string")
97                 this._value = editor;
98             else
99                 this.editor = editor;
100             /**
101              * @property {boolean} Specifies whether this context results must
102              *     match the filter at the beginning of the string.
103              * @default true
104              */
105             this.anchored = true;
106             this.forceAnchored = null;
107
108             this.compare = function compare(a, b) String.localeCompare(a.text, b.text);
109             /**
110              * @property {function} This function is called when we close
111              *     a completion window with Esc or Ctrl-c. Usually this callback
112              *     is only needed for long, asynchronous completions
113              */
114             this.cancel = null;
115             /**
116              * @property {[CompletionContext]} A list of active
117              *     completion contexts, in the order in which they were
118              *     instantiated.
119              */
120             this.contextList = [];
121             /**
122              * @property {Object} A map of all contexts, keyed on their names.
123              *    Names are assigned when a context is forked, with its specified
124              *    name appended, after a '/', to its parent's name. May
125              *    contain inactive contexts. For active contexts, see
126              *    {@link #contextList}.
127              */
128             this.contexts = { "": this };
129             /**
130              * @property {function} The function used to filter the results.
131              * @default Selects all results which match every predicate in the
132              *     {@link #filters} array.
133              */
134             this.filterFunc = function filterFunc(items) {
135                 return this.filters
136                            .reduce((res, filter)
137                                         => res.filter((item) => filter.call(this, item)),
138                                    items);
139             };
140             /**
141              * @property {Array} An array of predicates on which to filter the
142              *     results.
143              */
144             this.filters = [CompletionContext.Filter.text];
145             /**
146              * @property {Object} A mapping of keys, for {@link #getKey}. Given
147              *      { key: value }, getKey(item, key) will return values as such:
148              *      if value is a string, it will return item.item[value]. If it's a
149              *      function, it will return value(item.item).
150              */
151             this.keys = { text: 0, description: 1, icon: "icon" };
152             /**
153              * @property {number} This context's offset from the beginning of
154              *     {@link #editor}'s value.
155              */
156             this.offset = offset;
157             /**
158              * @property {function} A function which is called when any subcontext
159              *     changes its completion list. Only called when
160              *     {@link #updateAsync} is true.
161              */
162             this.onUpdate = function onUpdate() true;
163
164             this.runCount = 0;
165
166             /**
167              * @property {CompletionContext} The top-level completion context.
168              */
169             this.top = this;
170             this.__defineGetter__("incomplete", function get_incomplete() this._incomplete
171                 || this.contextList.some(c => c.parent && c.incomplete));
172             this.__defineGetter__("waitingForTab", function get_waitingForTab() this._waitingForTab
173                 || this.contextList.some(c => c.parent && c.waitingForTab));
174             this.__defineSetter__("incomplete", function get_incomplete(val) { this._incomplete = val; });
175             this.__defineSetter__("waitingForTab", function get_waitingForTab(val) { this._waitingForTab = val; });
176             this.reset();
177         }
178         /**
179          * @property {Object} A general-purpose store for functions which need to
180          *     cache data between calls.
181          */
182         this.cache = {};
183         this._cache = {};
184         /**
185          * @private
186          * @property {Object} A cache for return values of {@link #generate}.
187          */
188         this.itemCache = {};
189         /**
190          * @property {string} A key detailing when the cached value of
191          *     {@link #generate} may be used. Every call to
192          *     {@link #generate} stores its result in {@link #itemCache}.
193          *     When itemCache[key] exists, its value is returned, and
194          *     {@link #generate} is not called again.
195          */
196         this.key = "";
197         /**
198          * @property {string} A message to be shown before any results.
199          */
200         this.message = null;
201         this.name = name || "";
202         /** @private */
203         this._completions = []; // FIXME
204         /**
205          * Returns a key, as detailed in {@link #keys}.
206          * @function
207          */
208         this.getKey = function getKey(item, key) (typeof self.keys[key] == "function") ? self.keys[key].call(this, item.item) :
209                 key in self.keys ? item.item[self.keys[key]]
210                                  : item.item[key];
211         return this;
212     },
213
214     __title: Class.Memoize(function __title() this._title.map(s =>
215                 typeof s == "string" ? messages.get("completion.title." + s, s)
216                                      : s)),
217
218     set title(val) {
219         delete this.__title;
220         return this._title = val;
221     },
222     get title() this.__title,
223
224     get activeContexts() this.contextList.filter(function f(c) c.items.length),
225
226     // Temporary
227     /**
228      * @property {Object}
229      *
230      * An object describing the results from all sub-contexts. Results are
231      * adjusted so that all have the same starting offset.
232      *
233      * @deprecated
234      */
235     get allItems() {
236         let self = this;
237
238         try {
239             let allItems = this.contextList.map(function m(context) context.hasItems && context.items.length);
240             if (this.cache.allItems && array.equals(this.cache.allItems, allItems))
241                 return this.cache.allItemsResult;
242             this.cache.allItems = allItems;
243
244             let minStart = Math.min.apply(Math, this.activeContexts.map(function m(c) c.offset));
245             if (minStart == Infinity)
246                 minStart = 0;
247
248             this.cache.allItemsResult = memoize({
249                 start: minStart,
250
251                 get longestSubstring() self.longestAllSubstring,
252
253                 get items() array.flatten(self.activeContexts.map(function m(context) {
254                     let prefix = self.value.substring(minStart, context.offset);
255
256                     return context.items.map(function m(item) ({
257                         text: prefix + item.text,
258                         result: prefix + item.result,
259                         __proto__: item
260                     }));
261                 }))
262             });
263
264             return this.cache.allItemsResult;
265         }
266         catch (e) {
267             util.reportError(e);
268             return { start: 0, items: [], longestAllSubstring: "" };
269         }
270     },
271     // Temporary
272     get allSubstrings() {
273         let contexts = this.activeContexts;
274         let minStart = Math.min.apply(Math, contexts.map(function m(c) c.offset));
275         let lists = contexts.map(function m(context) {
276             let prefix = context.value.substring(minStart, context.offset);
277             return context.substrings.map(function m(s) prefix + s);
278         });
279
280         /* TODO: Deal with sub-substrings for multiple contexts again.
281          * Possibly.
282          */
283         let substrings = lists.reduce(
284                 function r(res, list) res.filter(function f(str) list.some(function s_(s) s.substr(0, str.length) == str)),
285                 lists.pop());
286         if (!substrings) // FIXME: How is this undefined?
287             return [];
288         return array.uniq(Array.slice(substrings));
289     },
290     // Temporary
291     get longestAllSubstring() {
292         return this.allSubstrings.reduce(function r(a, b) a.length > b.length ? a : b, "");
293     },
294
295     get caret() this._caret - this.offset,
296     set caret(val) this._caret = val + this.offset,
297
298     get compare() this._compare || function compare() 0,
299     set compare(val) this._compare = val,
300
301     get completions() this._completions || [],
302     set completions(items) {
303         if (items && isArray(items.array))
304             items = items.array;
305         // Accept a generator
306         if (!isArray(items))
307             items = [x for (x in Iterator(items || []))];
308         if (this._completions !== items) {
309             delete this.cache.filtered;
310             delete this.cache.filter;
311             this.cache.rows = [];
312             this._completions = items;
313             this.itemCache[this.key] = items;
314         }
315
316         if (this._completions)
317             this.hasItems = this._completions.length > 0;
318
319         if (this.updateAsync && !this.noUpdate)
320             util.trapErrors("onUpdate", this);
321     },
322
323     get createRow() this._createRow || template.completionRow, // XXX
324     set createRow(createRow) this._createRow = createRow,
325
326     get filterFunc() this._filterFunc || util.identity,
327     set filterFunc(val) this._filterFunc = val,
328
329     get filter() this._filter != null ? this._filter : this.value.substr(this.offset, this.caret),
330     set filter(val) {
331         delete this.ignoreCase;
332         return this._filter = val;
333     },
334
335     get format() ({
336         anchored: this.anchored,
337         title: this.title,
338         keys: this.keys,
339         process: this.process
340     }),
341     set format(format) {
342         this.anchored = format.anchored,
343         this.title = format.title || this.title;
344         this.keys = format.keys || this.keys;
345         this.process = format.process || this.process;
346     },
347
348     /**
349      * @property {string | xml | null}
350      * The message displayed at the head of the completions for the
351      * current context.
352      */
353     get message() this._message || (this.waitingForTab && this.hasItems !== false ? _("completion.waitingFor", "<Tab>") : null),
354     set message(val) this._message = val,
355
356     /**
357      * The prototype object for items returned by {@link items}.
358      */
359     get itemPrototype() {
360         let self = this;
361         let res = { highlight: "" };
362
363         function result(quote) {
364             yield ["context", function p_context() self];
365             yield ["result", quote ? function p_result() quote[0] + util.trapErrors(1, quote, this.text) + quote[2]
366                                    : function p_result() this.text];
367             yield ["texts", function p_texts() Array.concat(this.text)];
368         };
369
370         for (let i in iter(this.keys, result(this.quote))) {
371             let [k, v] = i;
372             if (typeof v == "string" && /^[.[]/.test(v))
373                 // This is only allowed to be a simple accessor, and shouldn't
374                 // reference any variables. Don't bother with eval context.
375                 v = Function("i", "return i" + v);
376             if (typeof v == "function")
377                 res.__defineGetter__(k, function p_gf() Class.replaceProperty(this, k, v.call(this, this.item, self)));
378             else
379                 res.__defineGetter__(k, function p_gp() Class.replaceProperty(this, k, this.item[v]));
380             res.__defineSetter__(k, function p_s(val) Class.replaceProperty(this, k, val));
381         }
382         return res;
383     },
384
385     /**
386      * Returns true when the completions generated by {@link #generate}
387      * must be regenerated. May be set to true to invalidate the current
388      * completions.
389      */
390     get regenerate() this._generate && (!this.completions || !this.itemCache[this.key] || this._cache.offset != this.offset),
391     set regenerate(val) { if (val) delete this.itemCache[this.key]; },
392
393     /**
394      * A property which may be set to a function to generate the value
395      * of {@link completions} only when necessary. The generated
396      * completions are linked to the value in {@link #key} and may be
397      * invalidated by setting the {@link #regenerate} property.
398      */
399     get generate() this._generate || null,
400     set generate(arg) {
401         this.hasItems = true;
402         this._generate = arg;
403     },
404     /**
405      * Generates the item list in {@link #completions} via the
406      * {@link #generate} method if the previously generated value is no
407      * longer valid.
408      */
409     generateCompletions: function generateCompletions() {
410         if (this.offset != this._cache.offset || this.lastActivated != this.top.runCount) {
411             this.itemCache = {};
412             this._cache.offset = this.offset;
413             this.lastActivated = this.top.runCount;
414         }
415         if (!this.itemCache[this.key] && !this.waitingForTab) {
416             try {
417                 let res = this._generate();
418                 if (res != null)
419                     this.itemCache[this.key] = res;
420             }
421             catch (e) {
422                 util.reportError(e);
423                 this.message = _("error.error", e);
424             }
425         }
426         // XXX
427         this.noUpdate = true;
428         this.completions = this.itemCache[this.key];
429         this.noUpdate = false;
430     },
431
432     ignoreCase: Class.Memoize(function M() {
433         let mode = this.wildcase;
434         if (mode == "match")
435             return false;
436         else if (mode == "ignore")
437             return true;
438         else
439             return !/[A-Z]/.test(this.filter);
440     }),
441
442     /**
443      * Returns a list of all completion items which match the current
444      * filter. The items returned are objects containing one property
445      * for each corresponding property in {@link keys}. The returned
446      * list is generated on-demand from the item list in {@link completions}
447      * or generated by {@link generate}, and is cached as long as no
448      * properties which would invalidate the result are changed.
449      */
450     get items() {
451         // Don't return any items if completions or generator haven't
452         // been set during this completion cycle.
453         if (!this.hasItems)
454             return [];
455
456         // Regenerate completions if we must
457         if (this.generate)
458             this.generateCompletions();
459         let items = this.completions;
460
461         // Check for cache miss
462         if (this._cache.completions !== this.completions) {
463             this._cache.completions = this.completions;
464             this._cache.constructed = null;
465             this.cache.filtered = null;
466         }
467
468         if (this.cache.filtered && this.cache.filter == this.filter)
469             return this.cache.filtered;
470
471         this.cache.rows = [];
472         this.cache.filter = this.filter;
473         if (items == null)
474             return null;
475
476         let self = this;
477         delete this._substrings;
478
479         if (!this.forceAnchored && this.options)
480             this.anchored = this.options.get("wildanchor").getKey(this.name, this.anchored);
481
482         // Item matchers
483         if (this.ignoreCase)
484             this.matchString = this.anchored ?
485                 function matchString(filter, str) String.toLowerCase(str).indexOf(filter.toLowerCase()) == 0 :
486                 function matchString(filter, str) String.toLowerCase(str).indexOf(filter.toLowerCase()) >= 0;
487         else
488             this.matchString = this.anchored ?
489                 function matchString(filter, str) String.indexOf(str, filter) == 0 :
490                 function matchString(filter, str) String.indexOf(str, filter) >= 0;
491
492         // Item formatters
493         this.processor = Array.slice(this.process);
494         if (!this.anchored)
495             this.processor[0] = function processor_0(item, text) self.process[0].call(self, item,
496                     template.highlightFilter(item.text, self.filter, null, item.isURI));
497
498         try {
499             // Item prototypes
500             if (!this._cache.constructed) {
501                 let proto = this.itemPrototype;
502                 this._cache.constructed = items.map(function m(item) ({ __proto__: proto, item: item }));
503             }
504
505             // Filters
506             let filtered = this.filterFunc(this._cache.constructed);
507             if (this.maxItems)
508                 filtered = filtered.slice(0, this.maxItems);
509
510             // Sorting
511             if (this.sortResults && this.compare) {
512                 filtered.sort(this.compare);
513                 if (!this.anchored) {
514                     let filter = this.filter;
515                     filtered.sort(function s(a, b) b.text.startsWith(filter) - a.text.startsWith(filter));
516                 }
517             }
518
519             return this.cache.filtered = filtered;
520         }
521         catch (e) {
522             this.message = _("error.error", e);
523             util.reportError(e);
524             return [];
525         }
526     },
527
528     /**
529      * Returns a list of all substrings common to all items which
530      * include the current filter.
531      */
532     get substrings() {
533         let items = this.items;
534         if (items.length == 0 || !this.hasItems)
535             return [];
536         if (this._substrings)
537             return this._substrings;
538
539         let fixCase = this.ignoreCase ? String.toLowerCase : util.identity;
540         let text   = fixCase(items[0].text);
541         let filter = fixCase(this.filter);
542
543         // Exceedingly long substrings cause Gecko to go into convulsions
544         if (text.length > 100)
545             text = text.substr(0, 100);
546
547         if (this.anchored) {
548             var compare = function compare(text, s) text.substr(0, s.length) == s;
549             var substrings = [text];
550         }
551         else {
552             var compare = function compare(text, s) text.contains(s);
553             var substrings = [];
554             let start = 0;
555             let idx;
556             let length = filter.length;
557             while ((idx = text.indexOf(filter, start)) > -1 && idx < text.length) {
558                 substrings.push(text.substring(idx));
559                 start = idx + 1;
560             }
561         }
562
563         substrings = items.reduce(function r(res, item)
564             res.map(function m(substring) {
565                 // A simple binary search to find the longest substring
566                 // of the given string which also matches the current
567                 // item's text.
568                 let len = substring.length;
569                 let i = 0, n = len + 1;
570                 let result = n && fixCase(item.result);
571                 while (n) {
572                     let m = Math.floor(n / 2);
573                     let keep = compare(result, substring.substring(0, i + m));
574                     if (!keep)
575                         len = i + m - 1;
576                     if (!keep || m == 0)
577                         n = m;
578                     else {
579                         i += m;
580                         n = n - m;
581                     }
582                 }
583                 return len == substring.length ? substring : substring.substr(0, Math.max(len, 0));
584             }),
585             substrings);
586
587         let quote = this.quote;
588         if (quote)
589             substrings = substrings.map(function m(str) quote[0] + quote[1](str));
590         return this._substrings = substrings;
591     },
592
593     /**
594      * Advances the context *count* characters. {@link #filter} is advanced to
595      * match. If {@link #quote} is non-null, its prefix and suffix are set to
596      * the null-string.
597      *
598      * This function is still imperfect for quoted strings. When
599      * {@link #quote} is non-null, it adjusts the count based on the quoted
600      * size of the *count*-character substring of the filter, which is accurate
601      * so long as unquoting and quoting a string will always map to the
602      * original quoted string, which is often not the case.
603      *
604      * @param {number} count The number of characters to advance the context.
605      */
606     advance: function advance(count) {
607         delete this.ignoreCase;
608         let advance = count;
609         if (this.quote && count) {
610             advance = this.quote[1](this.filter.substr(0, count)).length;
611             count = this.quote[0].length + advance;
612             this.quote[0] = "";
613             this.quote[2] = "";
614         }
615         this.offset += count;
616         if (this._filter)
617             this._filter = this._filter.substr(arguments[0] || 0);
618     },
619
620     /**
621      * Calls the {@link #cancel} method of all currently active
622      * sub-contexts.
623      */
624     cancelAll: function cancelAll() {
625         for (let [, context] in Iterator(this.contextList)) {
626             if (context.cancel)
627                 context.cancel();
628         }
629     },
630
631     /**
632      * Gets a key from {@link #cache}, setting it to *defVal* if it doesn't
633      * already exists.
634      *
635      * @param {string} key
636      * @param defVal
637      */
638     getCache: function getCache(key, defVal) {
639         if (!(key in this.cache))
640             this.cache[key] = defVal();
641         return this.cache[key];
642     },
643
644     getItems: function getItems(start, end) {
645         let items = this.items;
646         let step = start > end ? -1 : 1;
647         start = Math.max(0, start || 0);
648         end = Math.min(items.length, end ? end : items.length);
649         return iter.map(util.range(start, end, step), function m(i) items[i]);
650     },
651
652     getRow: function getRow(idx, doc) {
653         let cache = this.cache.rows;
654         if (cache) {
655             if (idx in this.items && !(idx in this.cache.rows))
656                 try {
657                     cache[idx] = DOM.fromJSON(this.createRow(this.items[idx]),
658                                               doc || this.doc);
659                 }
660                 catch (e) {
661                     util.reportError(e);
662                     util.dump(util.prettifyJSON(this.createRow(this.items[idx]), null, true));
663                     cache[idx] = DOM.fromJSON(
664                         ["div", { highlight: "CompItem", style: "white-space: nowrap" },
665                             ["li", { highlight: "CompResult" }, this.text + "\u00a0"],
666                             ["li", { highlight: "CompDesc ErrorMsg" }, e + "\u00a0"]],
667                         doc || this.doc);
668                 }
669             return cache[idx];
670         }
671     },
672
673     getRows: function getRows(start, end, doc) {
674         let items = this.items;
675         let cache = this.cache.rows;
676         let step = start > end ? -1 : 1;
677
678         start = Math.max(0, start || 0);
679         end = Math.min(items.length, end != null ? end : items.length);
680
681         this.doc = doc;
682         for (let i in util.range(start, end, step))
683             yield [i, this.getRow(i)];
684     },
685
686     /**
687      * Forks this completion context to create a new sub-context named
688      * as {this.name}/{name}. The new context is automatically advanced
689      * *offset* characters. If *completer* is provided, it is called
690      * with *self* as its 'this' object, the new context as its first
691      * argument, and any subsequent arguments after *completer* as its
692      * following arguments.
693      *
694      * If *completer* is provided, this function returns its return
695      * value, otherwise it returns the new completion context.
696      *
697      * @param {string} name The name of the new context.
698      * @param {number} offset The offset of the new context relative to
699      *      the current context's offset.
700      * @param {object} self *completer*'s 'this' object. @optional
701      * @param {function|string} completer A completer function to call
702      *      for the new context. If a string is provided, it is
703      *      interpreted as a method to access on *self*.
704      */
705     fork: function fork(name, offset, self, completer, ...args) {
706         return this.forkapply(name, offset, self, completer, args);
707     },
708
709     forkapply: function forkapply(name, offset, self, completer, args) {
710         if (isString(completer))
711             completer = self[completer];
712         let context = this.constructor(this, name, offset);
713         if (this.contextList.indexOf(context) < 0)
714             this.contextList.push(context);
715
716         if (!context.autoComplete && !context.tabPressed && context.editor)
717             context.waitingForTab = true;
718         else if (completer) {
719             let res = completer.apply(self || this, [context].concat(args));
720             if (res && !isArray(res) && !isArray(res.__proto__))
721                 res = [k for (k in res)];
722             if (res)
723                 context.completions = res;
724             return res;
725         }
726         if (completer)
727             return null;
728         return context;
729     },
730
731     split: function split(name, obj, fn, ...args) {
732         let context = this.fork(name);
733         let alias = (prop) => {
734             context.__defineGetter__(prop, () => this[prop]);
735             context.__defineSetter__(prop, (val) => this[prop] = val);
736         };
737         alias("_cache");
738         alias("_completions");
739         alias("_generate");
740         alias("_regenerate");
741         alias("itemCache");
742         alias("lastActivated");
743         context.hasItems = true;
744         this.hasItems = false;
745         if (fn)
746             return fn.apply(obj || this, [context].concat(args));
747         return context;
748     },
749
750     /**
751      * Highlights text in the nsIEditor associated with this completion
752      * context. *length* characters are highlighted from the position
753      * *start*, relative to the current context's offset, with the
754      * selection type *type* as defined in nsISelectionController.
755      *
756      * When called with no arguments, all highlights are removed. When
757      * called with a 0 length, all highlights of type *type* are
758      * removed.
759      *
760      * @param {number} start The position at which to start
761      *      highlighting.
762      * @param {number} length The length of the substring to highlight.
763      * @param {string} type The selection type to highlight with.
764      */
765     highlight: function highlight(start, length, type) {
766         if (arguments.length == 0) {
767             for (let type in this.selectionTypes)
768                 this.highlight(0, 0, type);
769             this.selectionTypes = {};
770         }
771         try {
772             // Requires Gecko >= 1.9.1
773             this.selectionTypes[type] = true;
774             const selType = Ci.nsISelectionController["SELECTION_" + type];
775             let sel = this.editor.selectionController.getSelection(selType);
776             if (length == 0)
777                 sel.removeAllRanges();
778             else {
779                 let range = this.editor.selection.getRangeAt(0).cloneRange();
780                 range.setStart(range.startContainer, this.offset + start);
781                 range.setEnd(range.startContainer, this.offset + start + length);
782                 sel.addRange(range);
783             }
784         }
785         catch (e) {}
786     },
787
788     /**
789      * Tests the given string for a match against the current filter,
790      * taking into account anchoring and case sensitivity rules.
791      *
792      * @param {string} str The string to match.
793      * @returns {boolean} True if the string matches, false otherwise.
794      */
795     match: function match(str) this.matchString(this.filter, str),
796
797     /**
798      * Pushes a new output processor onto the processor chain of
799      * {@link #process}. The provided function is called with the item
800      * and text to process along with a reference to the processor
801      * previously installed in the given *index* of {@link #process}.
802      *
803      * @param {number} index The index into {@link #process}.
804      * @param {function(object, string, function)} func The new
805      *      processor.
806      */
807     pushProcessor: function pushProcess(index, func) {
808         let next = this.process[index];
809         this.process[index] = function process_(item, text) func(item, text, next);
810     },
811
812     /**
813      * Resets this completion context and all sub-contexts for use in a
814      * new completion cycle. May only be called on the top-level
815      * context.
816      */
817     reset: function reset() {
818         if (this.parent)
819             throw Error();
820
821         this.offset = 0;
822         this.process = [template.icon, function process_1(item, k) k];
823         this.filters = [CompletionContext.Filter.text];
824         this.tabPressed = false;
825         this.title = ["Completions"];
826         this.updateAsync = false;
827
828         this.cancelAll();
829
830         if (this.editor) {
831             this.value = this.editor.selection.focusNode.textContent;
832             this._caret = this.editor.selection.focusOffset;
833         }
834         else {
835             this.value = this._value;
836             this._caret = this.value.length;
837         }
838         //for (let key in (k for ([k, v] in Iterator(this.contexts)) if (v.offset > this.caret)))
839         //    delete this.contexts[key];
840         for each (let context in this.contexts) {
841             context.hasItems = false;
842             context.incomplete = false;
843         }
844         this.waitingForTab = false;
845         this.runCount++;
846         for (let context of this.contextList)
847             context.lastActivated = this.runCount;
848         this.contextList = [];
849     },
850
851     /**
852      * Wait for all subcontexts to complete.
853      *
854      * @param {number} timeout The maximum time, in milliseconds, to wait.
855      *    If 0 or null, wait indefinitely.
856      * @param {boolean} interruptible When true, the call may be interrupted
857      *    via <C-c>, in which case, "Interrupted" may be thrown.
858      */
859     wait: function wait(timeout, interruptable) {
860         this.allItems;
861         return util.waitFor(function wf() !this.incomplete, this, timeout, interruptable);
862     }
863 }, {
864     Sort: {
865         number: function S_number(a, b) parseInt(a.text) - parseInt(b.text)
866                     || String.localeCompare(a.text, b.text),
867         unsorted: null
868     },
869
870     Filter: {
871         text: function F_text(item) {
872             let text = item.texts;
873             for (let [i, str] in Iterator(text)) {
874                 if (this.match(String(str))) {
875                     item.text = String(text[i]);
876                     return true;
877                 }
878             }
879             return false;
880         },
881         textDescription: function F_textDescription(item) {
882             return CompletionContext.Filter.text.call(this, item) || this.match(item.description);
883         }
884     }
885 });
886
887 /**
888  * @instance completion
889  */
890 var Completion = Module("completion", {
891     init: function init() {
892     },
893
894     get setFunctionCompleter() JavaScript.setCompleter, // Backward compatibility
895
896     Local: function Local(dactyl, modules, window) ({
897         urlCompleters: {},
898
899         get modules() modules,
900         get options() modules.options,
901
902         // FIXME
903         _runCompleter: function _runCompleter(name, filter, maxItems, ...args) {
904             let context = modules.CompletionContext(filter);
905             context.maxItems = maxItems;
906             let res = context.fork.apply(context, ["run", 0, this, name].concat(args));
907             if (res) {
908                 if (Components.stack.caller.name === "runCompleter") // FIXME
909                     return { items: res.map(function m(i) ({ item: i })) };
910                 context.contexts["/run"].completions = res;
911             }
912             context.wait(null, true);
913             return context.allItems;
914         },
915
916         runCompleter: function runCompleter(name, filter, maxItems) {
917             return this._runCompleter.apply(this, arguments)
918                        .items.map(function m(i) i.item);
919         },
920
921         listCompleter: function listCompleter(name, filter, maxItems, ...args) {
922             let context = modules.CompletionContext(filter || "");
923             context.maxItems = maxItems;
924             context.fork.apply(context, ["list", 0, this, name].concat(args));
925             context = context.contexts["/list"];
926             context.wait(null, true);
927
928             let contexts = context.activeContexts;
929             if (!contexts.length)
930                 contexts = context.contextList.filter(function f(c) c.hasItems).slice(0, 1);
931             if (!contexts.length)
932                 contexts = context.contextList.slice(-1);
933
934             modules.commandline.commandOutput(
935                 ["div", { highlight: "Completions" },
936                     template.map(contexts, function m(context)
937                         [template.completionRow(context.title, "CompTitle"),
938                          template.map(context.items, function m(item) context.createRow(item), null, 100)])]);
939         }
940     }),
941
942     ////////////////////////////////////////////////////////////////////////////////
943     ////////////////////// COMPLETION TYPES ////////////////////////////////////////
944     /////////////////////////////////////////////////////////////////////////////{{{
945
946     // filter a list of urls
947     //
948     // may consist of search engines, filenames, bookmarks and history,
949     // depending on the 'complete' option
950     // if the 'complete' argument is passed like "h", it temporarily overrides the complete option
951     url: function url(context, complete) {
952         if (/^jar:[^!]*$/.test(context.filter)) {
953             context.advance(4);
954
955             context.quote = context.quote || ["", util.identity, ""];
956             let quote = context.quote[1];
957             context.quote[1] = function quote_1(str) quote(str.replace(/!/g, escape));
958         }
959
960         if (this.options["urlseparator"])
961             var skip = util.regexp("^.*" + this.options["urlseparator"] + "\\s*")
962                            .exec(context.filter);
963
964         if (skip)
965             context.advance(skip[0].length);
966
967         if (/^about:/.test(context.filter))
968             context.fork("about", 6, this, function fork_(context) {
969                 context.title = ["about:"];
970                 context.generate = function generate_() {
971                     return [[k.substr(services.ABOUT.length), ""]
972                             for (k in Cc)
973                             if (k.startsWith(services.ABOUT))];
974                 };
975             });
976
977         if (complete == null)
978             complete = this.options["complete"];
979
980         // Will, and should, throw an error if !(c in opts)
981         Array.forEach(complete, function fe(c) {
982             let completer = this.urlCompleters[c] || { args: [], completer: this.autocomplete(c.replace(/^native:/, "")) };
983             context.forkapply(c, 0, this, completer.completer, completer.args);
984         }, this);
985     },
986
987     addUrlCompleter: function addUrlCompleter(name, description, completer, ...args) {
988         let completer = Completion.UrlCompleter(name, description, completer);
989         completer.args = args;
990         this.urlCompleters[name] = completer;
991     },
992
993     autocomplete: curry(function autocomplete(provider, context) {
994         let running = context.getCache("autocomplete-search-running", Object);
995
996         let name = "autocomplete:" + provider;
997         if (!services.has(name))
998             services.add(name, services.AUTOCOMPLETE + provider, "nsIAutoCompleteSearch");
999         let service = services[name];
1000
1001         util.assert(service, _("autocomplete.noSuchProvider", provider), false);
1002
1003         if (running[provider]) {
1004             this.completions = this.completions;
1005             this.cancel();
1006         }
1007
1008         context.anchored = false;
1009         context.compare = CompletionContext.Sort.unsorted;
1010         context.filterFunc = null;
1011
1012         let words = context.filter.toLowerCase().split(/\s+/g);
1013         context.hasItems = true;
1014         context.completions = context.completions.filter(function f({ url, title })
1015             words.every(function e(w) (url + " " + title).toLowerCase().indexOf(w) >= 0));
1016
1017         context.format = this.modules.bookmarks.format;
1018         context.keys.extra = function k_extra(item) {
1019             try {
1020                 return bookmarkcache.get(item.url).extra;
1021             }
1022             catch (e) {}
1023             return null;
1024         };
1025         context.title = [_("autocomplete.title", provider)];
1026
1027         context.cancel = function cancel_() {
1028             this.incomplete = false;
1029             if (running[provider])
1030                 service.stopSearch();
1031             running[provider] = false;
1032         };
1033
1034         if (!context.waitingForTab) {
1035             context.incomplete = true;
1036
1037             service.startSearch(context.filter, "", context.result, {
1038                 onSearchResult: util.wrapCallback(function onSearchResult(search, result) {
1039                     if (result.searchResult <= result.RESULT_SUCCESS)
1040                         running[provider] = null;
1041
1042                     context.incomplete = result.searchResult >= result.RESULT_NOMATCH_ONGOING;
1043                     context.completions = [
1044                         { url: result.getValueAt(i), title: result.getCommentAt(i), icon: result.getImageAt(i) }
1045                         for (i in util.range(0, result.matchCount))
1046                     ];
1047                 }),
1048                 get onUpdateSearchResult() this.onSearchResult
1049             });
1050             running[provider] = true;
1051         }
1052     }),
1053
1054     urls: function urls(context, tags) {
1055         let compare = String.localeCompare;
1056         let contains = String.indexOf;
1057         if (context.ignoreCase) {
1058             compare = util.compareIgnoreCase;
1059             contains = function contains_(a, b) a && a.toLowerCase().contains(b.toLowerCase());
1060         }
1061
1062         if (tags)
1063             context.filters.push(function filter_(item) tags.
1064                 every(function e(tag) (item.tags || []).
1065                     some(function s(t) !compare(tag, t))));
1066
1067         context.anchored = false;
1068         if (!context.title)
1069             context.title = ["URL", "Title"];
1070
1071         context.fork("additional", 0, this, function fork_(context) {
1072             context.title[0] += " " + _("completion.additional");
1073             context.filter = context.parent.filter; // FIXME
1074             context.completions = context.parent.completions;
1075
1076             // For items whose URL doesn't exactly match the filter,
1077             // accept them if all tokens match either the URL or the title.
1078             // Filter out all directly matching strings.
1079             let match = context.filters[0];
1080             context.filters[0] = function filters_0(item) !match.call(this, item);
1081
1082             // and all that don't match the tokens.
1083             let tokens = context.filter.split(/\s+/);
1084             context.filters.push(function filter_(item) tokens.every(
1085                     function e(tok) contains(item.url, tok) ||
1086                                    contains(item.title, tok)));
1087
1088             let re = RegExp(tokens.filter(util.identity).map(util.regexp.escape).join("|"), "g");
1089             function highlight(item, text, i) process[i].call(this, item, template.highlightRegexp(text, re));
1090             let process = context.process;
1091             context.process = [
1092                 function process_0(item, text) highlight.call(this, item, item.text, 0),
1093                 function process_1(item, text) highlight.call(this, item, text, 1)
1094             ];
1095         });
1096     }
1097     //}}}
1098 }, {
1099     UrlCompleter: Struct("name", "description", "completer")
1100 }, {
1101     init: function init(dactyl, modules, window) {
1102         init.superapply(this, arguments);
1103
1104         modules.CompletionContext = Class("CompletionContext", CompletionContext, {
1105             init: function init() {
1106                 this.modules = modules;
1107                 return init.superapply(this, arguments);
1108             },
1109
1110             get options() this.modules.options
1111         });
1112     },
1113     commands: function initCommands(dactyl, modules, window) {
1114         const { commands, completion } = modules;
1115         commands.add(["contexts"],
1116             "List the completion contexts used during completion of an Ex command",
1117             function (args) {
1118                 modules.commandline.commandOutput(
1119                     ["div", { highlight: "Completions" },
1120                         template.completionRow(["Context", "Title"], "CompTitle"),
1121                         template.map(completion.contextList || [],
1122                                      function m(item) template.completionRow(item, "CompItem"))]);
1123             },
1124             {
1125                 argCount: "*",
1126                 completer: function (context) {
1127                     let PREFIX = "/ex/contexts";
1128                     context.fork("ex", 0, completion, "ex");
1129                     completion.contextList = [[k.substr(PREFIX.length), v.title[0]] for ([k, v] in iter(context.contexts)) if (k.substr(0, PREFIX.length) == PREFIX)];
1130                 },
1131                 literal: 0
1132             });
1133     },
1134     options: function initOptions(dactyl, modules, window) {
1135         const { completion, options } = modules;
1136         let wildmode = {
1137             values: {
1138                 // Why do we need ""?
1139                 // Because its description is useful during completion. --Kris
1140                 "":              "Complete only the first match",
1141                 "full":          "Complete the next full match",
1142                 "longest":       "Complete the longest common string",
1143                 "list":          "If more than one match, list all matches",
1144                 "list:full":     "List all and complete first match",
1145                 "list:longest":  "List all and complete the longest common string"
1146             },
1147             checkHas: function (value, val) {
1148                 let [first, second] = value.split(":", 2);
1149                 return first == val || second == val;
1150             },
1151             has: function () {
1152                 let test = function test(val) this.value.some(function s(value) this.checkHas(value, val), this);
1153                 return Array.some(arguments, test, this);
1154             }
1155         };
1156
1157         options.add(["altwildmode", "awim"],
1158             "Define the behavior of the c_<A-Tab> key in command-line completion",
1159             "stringlist", "list:full",
1160             wildmode);
1161
1162         options.add(["autocomplete", "au"],
1163             "Automatically update the completion list on any key press",
1164             "regexplist", ".*");
1165
1166         options.add(["complete", "cpt"],
1167             "Items which are completed at the :open prompts",
1168             "stringlist", "slf",
1169             {
1170                 valueMap: {
1171                     S: "suggestion",
1172                     b: "bookmark",
1173                     f: "file",
1174                     h: "history",
1175                     l: "location",
1176                     s: "search"
1177                 },
1178
1179                 get values() values(completion.urlCompleters).toArray()
1180                                 .concat([let (name = k.substr(services.AUTOCOMPLETE.length))
1181                                             ["native:" + name, _("autocomplete.description", name)]
1182                                          for (k in Cc)
1183                                          if (k.startsWith(services.AUTOCOMPLETE))]),
1184
1185                 setter: function setter(values) {
1186                     if (values.length == 1 && !hasOwnProperty(values[0], this.values)
1187                             && Array.every(values[0], v => hasOwnProperty(this.valueMap, v)))
1188                         return Array.map(values[0], v => this.valueMap[v]);
1189
1190                     return values;
1191                 },
1192
1193                 validator: function validator(values) validator.supercall(this, this.setter(values))
1194             });
1195
1196         options.add(["wildanchor", "wia"],
1197             "Define which completion groups only match at the beginning of their text",
1198             "regexplist", "!/ex/(back|buffer|ext|forward|help|undo)");
1199
1200         options.add(["wildcase", "wic"],
1201             "Completion case matching mode",
1202             "regexpmap", ".?:smart",
1203             {
1204                 values: {
1205                     "smart": "Case is significant when capital letters are typed",
1206                     "match": "Case is always significant",
1207                     "ignore": "Case is never significant"
1208                 }
1209             });
1210
1211         options.add(["wildmode", "wim"],
1212             "Define the behavior of the c_<Tab> key in command-line completion",
1213             "stringlist", "list:full",
1214             wildmode);
1215
1216         options.add(["wildsort", "wis"],
1217             "Define which completion groups are sorted",
1218             "regexplist", ".*");
1219     }
1220 });
1221
1222 endModule();
1223
1224 // catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1225
1226 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: