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