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