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