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>
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
11 Components.utils.import("resource://dactyl/bootstrap.jsm");
12 defineModule("completion", {
13 exports: ["CompletionContext", "Completion", "completion"],
14 use: ["config", "template", "util"]
18 * Creates a new completion context.
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
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
31 * @param {number} offset The offset from the parent context.
32 * @author Kris Maglione <maglione.k@gmail.com>
35 var CompletionContext = Class("CompletionContext", {
36 init: function (editor, name, offset) {
41 if (editor instanceof this.constructor) {
43 name = parent.name + "/" + name;
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);
51 this.contexts = parent.contexts;
52 if (name in this.contexts)
53 self = this.contexts[name];
55 self.contexts[name] = this;
58 * @property {CompletionContext} This context's parent. {null} when
59 * this is a top-level context.
63 ["filters", "keys", "process", "title", "quote"].forEach(function (key)
64 self[key] = parent[key] && util.cloneObject(parent[key]));
65 ["anchored", "compare", "editor", "_filter", "filterFunc", "forceAnchored", "top"].forEach(function (key)
66 self[key] = parent[key]);
68 self.__defineGetter__("value", function () this.top.value);
70 self.offset = parent.offset;
71 self.advance(offset || 0);
74 * @property {boolean} Specifies that this context is not finished
78 self.incomplete = false;
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.
85 self.waitingForTab = false;
89 delete self._generate;
90 delete self.ignoreCase;
93 ["_caret", "contextList", "maxItems", "onUpdate", "selectionTypes", "tabPressed", "updateAsync", "value"].forEach(function (key) {
94 self.__defineGetter__(key, function () this.top[key]);
95 self.__defineSetter__(key, function (val) this.top[key] = val);
99 if (typeof editor == "string")
100 this._value = editor;
102 this.editor = editor;
104 * @property {boolean} Specifies whether this context results must
105 * match the filter at the beginning of the string.
108 this.anchored = true;
109 this.forceAnchored = null;
111 this.compare = function (a, b) String.localeCompare(a.text, b.text);
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
119 * @property {[CompletionContext]} A list of active
120 * completion contexts, in the order in which they were
123 this.contextList = [];
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}.
131 this.contexts = { "": this };
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.
137 this.filterFunc = function (items) {
140 reduce(function (res, filter) res.filter(function (item) filter.call(self, item)),
144 * @property {Array} An array of predicates on which to filter the
147 this.filters = [CompletionContext.Filter.text];
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).
154 this.keys = { text: 0, description: 1, icon: "icon" };
156 * @property {number} This context's offset from the beginning of
157 * {@link #editor}'s value.
159 this.offset = offset || 0;
161 * @property {function} A function which is called when any subcontext
162 * changes its completion list. Only called when
163 * {@link #updateAsync} is true.
165 this.onUpdate = function () true;
170 * @property {CompletionContext} The top-level completion context.
173 this.__defineGetter__("incomplete", function () this._incomplete || this.contextList.some(function (c) c.parent && c.incomplete));
174 this.__defineGetter__("waitingForTab", function () this._waitingForTab || this.contextList.some(function (c) c.parent && c.waitingForTab));
175 this.__defineSetter__("incomplete", function (val) { this._incomplete = val; });
176 this.__defineSetter__("waitingForTab", function (val) { this._waitingForTab = val; });
180 * @property {Object} A general-purpose store for functions which need to
181 * cache data between calls.
187 * @property {Object} A cache for return values of {@link #generate}.
191 * @property {string} A key detailing when the cached value of
192 * {@link #generate} may be used. Every call to
193 * {@link #generate} stores its result in {@link #itemCache}.
194 * When itemCache[key] exists, its value is returned, and
195 * {@link #generate} is not called again.
199 * @property {string} A message to be shown before any results.
202 this.name = name || "";
204 this._completions = []; // FIXME
206 * Returns a key, as detailed in {@link #keys}.
209 this.getKey = function (item, key) (typeof self.keys[key] == "function") ? self.keys[key].call(this, item.item) :
210 key in self.keys ? item.item[self.keys[key]]
219 * An object describing the results from all sub-contexts. Results are
220 * adjusted so that all have the same starting offset.
227 let allItems = this.contextList.map(function (context) context.hasItems && context.items);
228 if (this.cache.allItems && array.equals(this.cache.allItems, allItems))
229 return this.cache.allItemsResult;
230 this.cache.allItems = allItems;
232 let minStart = Math.min.apply(Math, [context.offset for ([k, context] in Iterator(this.contexts)) if (context.hasItems && context.items.length)]);
233 if (minStart == Infinity)
235 let items = this.contextList.map(function (context) {
236 if (!context.hasItems)
238 let prefix = self.value.substring(minStart, context.offset);
239 return context.items.map(function (item) ({
240 text: prefix + item.text,
241 result: prefix + item.result,
245 this.cache.allItemsResult = { start: minStart, items: array.flatten(items) };
246 memoize(this.cache.allItemsResult, "longestSubstring", function () self.longestAllSubstring);
247 return this.cache.allItemsResult;
251 return { start: 0, items: [], longestAllSubstring: "" };
255 get allSubstrings() {
256 let contexts = this.contextList.filter(function (c) c.hasItems && c.items.length);
257 let minStart = Math.min.apply(Math, contexts.map(function (c) c.offset));
258 let lists = contexts.map(function (context) {
259 let prefix = context.value.substring(minStart, context.offset);
260 return context.substrings.map(function (s) prefix + s);
263 /* TODO: Deal with sub-substrings for multiple contexts again.
266 let substrings = lists.reduce(
267 function (res, list) res.filter(function (str) list.some(function (s) s.substr(0, str.length) == str)),
269 if (!substrings) // FIXME: How is this undefined?
271 return array.uniq(Array.slice(substrings));
274 get longestAllSubstring() {
275 return this.allSubstrings.reduce(function (a, b) a.length > b.length ? a : b, "");
278 get caret() this._caret - this.offset,
279 set caret(val) this._caret = val + this.offset,
281 get compare() this._compare || function () 0,
282 set compare(val) this._compare = val,
284 get completions() this._completions || [],
285 set completions(items) {
286 if (items && isArray(items.array))
288 // Accept a generator
290 items = [x for (x in Iterator(items || []))];
291 if (this._completions !== items) {
292 delete this.cache.filtered;
293 delete this.cache.filter;
294 this.cache.rows = [];
295 this._completions = items;
296 this.itemCache[this.key] = items;
298 if (this._completions)
299 this.hasItems = this._completions.length > 0;
300 if (this.updateAsync && !this.noUpdate)
304 get createRow() this._createRow || template.completionRow, // XXX
305 set createRow(createRow) this._createRow = createRow,
307 get filterFunc() this._filterFunc || util.identity,
308 set filterFunc(val) this._filterFunc = val,
310 get filter() this._filter != null ? this._filter : this.value.substr(this.offset, this.caret),
312 delete this.ignoreCase;
313 return this._filter = val;
317 anchored: this.anchored,
320 process: this.process
323 this.anchored = format.anchored,
324 this.title = format.title || this.title;
325 this.keys = format.keys || this.keys;
326 this.process = format.process || this.process;
330 * @property {string | xml | null}
331 * The message displayed at the head of the completions for the
334 get message() this._message || (this.waitingForTab && this.hasItems !== false ? "Waiting for <Tab>" : null),
335 set message(val) this._message = val,
338 * The prototype object for items returned by {@link items}.
340 get itemPrototype() {
342 function result(quote) {
343 yield ["result", quote ? function () quote[0] + quote[1](this.text) + quote[2]
344 : function () this.text];
346 for (let i in iter(this.keys, result(this.quote))) {
348 if (typeof v == "string" && /^[.[]/.test(v))
349 // This is only allowed to be a simple accessor, and shouldn't
350 // reference any variables. Don't bother with eval context.
351 v = Function("i", "return i" + v);
352 if (typeof v == "function")
353 res.__defineGetter__(k, function () Class.replaceProperty(this, k, v.call(this, this.item)));
355 res.__defineGetter__(k, function () Class.replaceProperty(this, k, this.item[v]));
356 res.__defineSetter__(k, function (val) Class.replaceProperty(this, k, val));
362 * Returns true when the completions generated by {@link #generate}
363 * must be regenerated. May be set to true to invalidate the current
366 get regenerate() this._generate && (!this.completions || !this.itemCache[this.key] || this._cache.offset != this.offset),
367 set regenerate(val) { if (val) delete this.itemCache[this.key]; },
370 * A property which may be set to a function to generate the value
371 * of {@link completions} only when necessary. The generated
372 * completions are linked to the value in {@link #key} and may be
373 * invalidated by setting the {@link #regenerate} property.
375 get generate() this._generate || null,
377 this.hasItems = true;
378 this._generate = arg;
381 * Generates the item list in {@link #completions} via the
382 * {@link #generate} method if the previously generated value is no
385 generateCompletions: function generateCompletions() {
386 if (this.offset != this._cache.offset || this.lastActivated != this.top.runCount) {
388 this._cache.offset = this.offset;
389 this.lastActivated = this.top.runCount;
391 if (!this.itemCache[this.key]) {
393 let res = this._generate();
395 this.itemCache[this.key] = res;
399 this.message = "Error: " + e;
403 this.noUpdate = true;
404 this.completions = this.itemCache[this.key];
405 this.noUpdate = false;
408 ignoreCase: Class.memoize(function () {
409 let mode = this.wildcase;
412 else if (mode == "ignore")
415 return !/[A-Z]/.test(this.filter);
419 * Returns a list of all completion items which match the current
420 * filter. The items returned are objects containing one property
421 * for each corresponding property in {@link keys}. The returned
422 * list is generated on-demand from the item list in {@link completions}
423 * or generated by {@link generate}, and is cached as long as no
424 * properties which would invalidate the result are changed.
427 // Don't return any items if completions or generator haven't
428 // been set during this completion cycle.
432 // Regenerate completions if we must
434 this.generateCompletions();
435 let items = this.completions;
437 // Check for cache miss
438 if (this._cache.completions !== this.completions) {
439 this._cache.completions = this.completions;
440 this._cache.constructed = null;
441 this.cache.filtered = null;
444 if (this.cache.filtered && this.cache.filter == this.filter)
445 return this.cache.filtered;
447 this.cache.rows = [];
448 this.cache.filter = this.filter;
453 delete this._substrings;
455 if (!this.forceAnchored && this.options)
456 this.anchored = this.options.get("wildanchor").getKey(this.name, this.anchored);
460 this.matchString = this.anchored ?
461 function (filter, str) String.toLowerCase(str).indexOf(filter.toLowerCase()) == 0 :
462 function (filter, str) String.toLowerCase(str).indexOf(filter.toLowerCase()) >= 0;
464 this.matchString = this.anchored ?
465 function (filter, str) String.indexOf(str, filter) == 0 :
466 function (filter, str) String.indexOf(str, filter) >= 0;
469 this.processor = Array.slice(this.process);
471 this.processor[0] = function (item, text) self.process[0].call(self, item,
472 template.highlightFilter(item.text, self.filter));
476 if (!this._cache.constructed) {
477 let proto = this.itemPrototype;
478 this._cache.constructed = items.map(function (item) ({ __proto__: proto, item: item }));
482 let filtered = this.filterFunc(this._cache.constructed);
484 filtered = filtered.slice(0, this.maxItems);
487 if (this.sortResults && this.compare) {
488 filtered.sort(this.compare);
489 if (!this.anchored) {
490 let filter = this.filter;
491 filtered.sort(function (a, b) (b.text.indexOf(filter) == 0) - (a.text.indexOf(filter) == 0));
495 return this.cache.filtered = filtered;
498 this.message = "Error: " + e;
505 * Returns a list of all substrings common to all items which
506 * include the current filter.
509 let items = this.items;
510 if (items.length == 0 || !this.hasItems)
512 if (this._substrings)
513 return this._substrings;
515 let fixCase = this.ignoreCase ? String.toLowerCase : util.identity;
516 let text = fixCase(items[0].text);
517 let filter = fixCase(this.filter);
519 // Exceedingly long substrings cause Gecko to go into convulsions
520 if (text.length > 100)
521 text = text.substr(0, 100);
524 var compare = function compare(text, s) text.substr(0, s.length) == s;
525 var substrings = [text];
528 var compare = function compare(text, s) text.indexOf(s) >= 0;
532 let length = filter.length;
533 while ((idx = text.indexOf(filter, start)) > -1 && idx < text.length) {
534 substrings.push(text.substring(idx));
539 substrings = items.reduce(function (res, item)
540 res.map(function (substring) {
541 // A simple binary search to find the longest substring
542 // of the given string which also matches the current
544 let len = substring.length;
547 let m = Math.floor(n / 2);
548 let keep = compare(fixCase(item.text), substring.substring(0, i + m));
558 return len == substring.length ? substring : substring.substr(0, Math.max(len, 0));
562 let quote = this.quote;
564 substrings = substrings.map(function (str) quote[0] + quote[1](str));
565 return this._substrings = substrings;
569 * Advances the context *count* characters. {@link #filter} is advanced to
570 * match. If {@link #quote} is non-null, its prefix and suffix are set to
573 * This function is still imperfect for quoted strings. When
574 * {@link #quote} is non-null, it adjusts the count based on the quoted
575 * size of the *count*-character substring of the filter, which is accurate
576 * so long as unquoting and quoting a string will always map to the
577 * original quoted string, which is often not the case.
579 * @param {number} count The number of characters to advance the context.
581 advance: function advance(count) {
582 delete this.ignoreCase;
584 if (this.quote && count) {
585 advance = this.quote[1](this.filter.substr(0, count)).length;
586 count = this.quote[0].length + advance;
590 this.offset += count;
592 this._filter = this._filter.substr(advance);
596 * Calls the {@link #cancel} method of all currently active
599 cancelAll: function () {
600 for (let [, context] in Iterator(this.contextList)) {
607 * Gets a key from {@link #cache}, setting it to *defVal* if it doesn't
610 * @param {string} key
613 getCache: function (key, defVal) {
614 if (!(key in this.cache))
615 this.cache[key] = defVal();
616 return this.cache[key];
619 getItems: function getItems(start, end) {
620 let items = this.items;
621 let step = start > end ? -1 : 1;
622 start = Math.max(0, start || 0);
623 end = Math.min(items.length, end ? end : items.length);
624 return iter.map(util.range(start, end, step), function (i) items[i]);
627 getRows: function getRows(start, end, doc) {
629 let items = this.items;
630 let cache = this.cache.rows;
631 let step = start > end ? -1 : 1;
632 start = Math.max(0, start || 0);
633 end = Math.min(items.length, end != null ? end : items.length);
634 for (let i in util.range(start, end, step))
635 yield [i, cache[i] = cache[i] || util.xmlToDom(self.createRow(items[i]), doc)];
639 * Forks this completion context to create a new sub-context named
640 * as {this.name}/{name}. The new context is automatically advanced
641 * *offset* characters. If *completer* is provided, it is called
642 * with *self* as its 'this' object, the new context as its first
643 * argument, and any subsequent arguments after *completer* as its
644 * following arguments.
646 * If *completer* is provided, this function returns its return
647 * value, otherwise it returns the new completion context.
649 * @param {string} name The name of the new context.
650 * @param {number} offset The offset of the new context relative to
651 * the current context's offset.
652 * @param {object} self *completer*'s 'this' object. @optional
653 * @param {function|string} completer A completer function to call
654 * for the new context. If a string is provided, it is
655 * interpreted as a method to access on *self*.
657 fork: function fork(name, offset, self, completer) {
658 return this.forkapply(name, offset, self, completer, Array.slice(arguments, fork.length));
661 forkapply: function forkapply(name, offset, self, completer, args) {
662 if (isString(completer))
663 completer = self[completer];
664 let context = this.constructor(this, name, offset);
665 if (this.contextList.indexOf(context) < 0)
666 this.contextList.push(context);
668 if (!context.autoComplete && !context.tabPressed && context.editor)
669 context.waitingForTab = true;
670 else if (completer) {
671 let res = completer.apply(self || this, [context].concat(args));
672 if (res && !isArray(res) && !isArray(res.__proto__))
673 res = [k for (k in res)];
675 context.completions = res;
683 split: function split(name, obj, fn) {
686 let context = this.fork(name);
687 function alias(prop) {
688 context.__defineGetter__(prop, function () self[prop]);
689 context.__defineSetter__(prop, function (val) self[prop] = val);
692 alias("_completions");
694 alias("_regenerate");
696 alias("lastActivated");
697 context.hasItems = true;
698 this.hasItems = false;
700 return fn.apply(obj || this, [context].concat(Array.slice(arguments, split.length)));
705 * Highlights text in the nsIEditor associated with this completion
706 * context. *length* characters are highlighted from the position
707 * *start*, relative to the current context's offset, with the
708 * selection type *type* as defined in nsISelectionController.
710 * When called with no arguments, all highlights are removed. When
711 * called with a 0 length, all highlights of type *type* are
714 * @param {number} start The position at which to start
716 * @param {number} length The length of the substring to highlight.
717 * @param {string} type The selection type to highlight with.
719 highlight: function highlight(start, length, type) {
720 if (arguments.length == 0) {
721 for (let type in this.selectionTypes)
722 this.highlight(0, 0, type);
723 this.selectionTypes = {};
726 // Requires Gecko >= 1.9.1
727 this.selectionTypes[type] = true;
728 const selType = Ci.nsISelectionController["SELECTION_" + type];
729 let sel = this.editor.selectionController.getSelection(selType);
731 sel.removeAllRanges();
733 let range = this.editor.selection.getRangeAt(0).cloneRange();
734 range.setStart(range.startContainer, this.offset + start);
735 range.setEnd(range.startContainer, this.offset + start + length);
743 * Tests the given string for a match against the current filter,
744 * taking into account anchoring and case sensitivity rules.
746 * @param {string} str The string to match.
747 * @returns {boolean} True if the string matches, false otherwise.
749 match: function match(str) this.matchString(this.filter, str),
752 * Pushes a new output processor onto the processor chain of
753 * {@link #process}. The provided function is called with the item
754 * and text to process along with a reference to the processor
755 * previously installed in the given *index* of {@link #process}.
757 * @param {number} index The index into {@link #process}.
758 * @param {function(object, string, function)} func The new
761 pushProcessor: function pushProcess(index, func) {
762 let next = this.process[index];
763 this.process[index] = function (item, text) func(item, text, next);
767 * Resets this completion context and all sub-contexts for use in a
768 * new completion cycle. May only be called on the top-level
771 reset: function reset() {
777 this.process = [template.icon, function (item, k) k];
778 this.filters = [CompletionContext.Filter.text];
779 this.tabPressed = false;
780 this.title = ["Completions"];
781 this.updateAsync = false;
786 this.value = this.editor.selection.focusNode.textContent;
787 this._caret = this.editor.selection.focusOffset;
790 this.value = this._value;
791 this._caret = this.value.length;
793 //for (let key in (k for ([k, v] in Iterator(self.contexts)) if (v.offset > this.caret)))
794 // delete this.contexts[key];
795 for each (let context in this.contexts) {
796 context.hasItems = false;
797 context.incomplete = false;
799 this.waitingForTab = false;
801 for each (let context in this.contextList)
802 context.lastActivated = this.runCount;
803 this.contextList = [];
807 * Wait for all subcontexts to complete.
809 * @param {number} timeout The maximum time, in milliseconds, to wait.
810 * If 0 or null, wait indefinitely.
811 * @param {boolean} interruptible When true, the call may be interrupted
812 * via <C-c>, in which case, "Interrupted" may be thrown.
814 wait: function wait(timeout, interruptable) {
816 return util.waitFor(function () !this.incomplete, this, timeout, interruptable);
820 number: function (a, b) parseInt(a.text) - parseInt(b.text) || String.localeCompare(a.text, b.text),
825 text: function (item) {
826 let text = Array.concat(item.text);
827 for (let [i, str] in Iterator(text)) {
828 if (this.match(String(str))) {
829 item.text = String(text[i]);
835 textDescription: function (item) {
836 return CompletionContext.Filter.text.call(this, item) || this.match(item.description);
842 * @instance completion
844 var Completion = Module("completion", {
848 get setFunctionCompleter() JavaScript.setCompleter, // Backward compatibility
850 Local: function (dactyl, modules, window) ({
853 get options() modules.options,
856 _runCompleter: function _runCompleter(name, filter, maxItems) {
857 let context = modules.CompletionContext(filter);
858 context.maxItems = maxItems;
859 let res = context.fork.apply(context, ["run", 0, this, name].concat(Array.slice(arguments, 3)));
861 if (Components.stack.caller.name === "runCompleter") // FIXME
862 return { items: res.map(function (i) ({ item: i })) };
863 context.contexts["/run"].completions = res;
865 context.wait(null, true);
866 return context.allItems;
869 runCompleter: function runCompleter(name, filter, maxItems) {
870 return this._runCompleter.apply(this, Array.slice(arguments))
871 .items.map(function (i) i.item);
874 listCompleter: function listCompleter(name, filter, maxItems) {
875 let context = modules.CompletionContext(filter || "");
876 context.maxItems = maxItems;
877 context.fork.apply(context, ["list", 0, this, name].concat(Array.slice(arguments, 3)));
878 context = context.contexts["/list"];
879 context.wait(null, true);
881 let contexts = context.contextList.filter(function (c) c.hasItems && c.items.length);
882 if (!contexts.length)
883 contexts = context.contextList.filter(function (c) c.hasItems).slice(0, 1);
884 if (!contexts.length)
885 contexts = context.contextList.slice(-1);
887 modules.commandline.commandOutput(
888 <div highlight="Completions">
889 { template.map(contexts, function (context)
890 template.completionRow(context.title, "CompTitle") +
891 template.map(context.items, function (item) context.createRow(item), null, 100)) }
896 ////////////////////////////////////////////////////////////////////////////////
897 ////////////////////// COMPLETION TYPES ////////////////////////////////////////
898 /////////////////////////////////////////////////////////////////////////////{{{
900 // filter a list of urls
902 // may consist of search engines, filenames, bookmarks and history,
903 // depending on the 'complete' option
904 // if the 'complete' argument is passed like "h", it temporarily overrides the complete option
905 url: function url(context, complete) {
907 if (this.options["urlseparator"])
908 var skip = util.regexp("^.*" + this.options["urlseparator"] + "\\s*")
909 .exec(context.filter);
912 context.advance(skip[0].length);
914 if (/^about:/.test(context.filter))
915 context.fork("about", 6, this, function (context) {
916 context.generate = function () {
917 const PREFIX = "@mozilla.org/network/protocol/about;1?what=";
918 return [[k.substr(PREFIX.length), ""] for (k in Cc) if (k.indexOf(PREFIX) == 0)];
922 if (complete == null)
923 complete = this.options["complete"];
925 // Will, and should, throw an error if !(c in opts)
926 Array.forEach(complete, function (c) {
927 let completer = this.urlCompleters[c];
928 context.forkapply(c, 0, this, completer.completer, completer.args);
932 addUrlCompleter: function addUrlCompleter(opt) {
933 let completer = Completion.UrlCompleter.apply(null, Array.slice(arguments));
934 completer.args = Array.slice(arguments, completer.length);
935 this.urlCompleters[opt] = completer;
938 urls: function (context, tags) {
939 let compare = String.localeCompare;
940 let contains = String.indexOf;
941 if (context.ignoreCase) {
942 compare = util.compareIgnoreCase;
943 contains = function (a, b) a && a.toLowerCase().indexOf(b.toLowerCase()) > -1;
947 context.filters.push(function (item) tags.
948 every(function (tag) (item.tags || []).
949 some(function (t) !compare(tag, t))));
951 context.anchored = false;
953 context.title = ["URL", "Title"];
955 context.fork("additional", 0, this, function (context) {
956 context.title[0] += " (additional)";
957 context.filter = context.parent.filter; // FIXME
958 context.completions = context.parent.completions;
959 // For items whose URL doesn't exactly match the filter,
960 // accept them if all tokens match either the URL or the title.
961 // Filter out all directly matching strings.
962 let match = context.filters[0];
963 context.filters[0] = function (item) !match.call(this, item);
964 // and all that don't match the tokens.
965 let tokens = context.filter.split(/\s+/);
966 context.filters.push(function (item) tokens.every(
967 function (tok) contains(item.url, tok) ||
968 contains(item.title, tok)));
970 let re = RegExp(tokens.filter(util.identity).map(util.regexp.escape).join("|"), "g");
971 function highlight(item, text, i) process[i].call(this, item, template.highlightRegexp(text, re));
972 let process = context.process;
974 function (item, text) highlight.call(this, item, item.text, 0),
975 function (item, text) highlight.call(this, item, text, 1)
981 UrlCompleter: Struct("name", "description", "completer")
983 init: function init(dactyl, modules, window) {
984 init.superapply(this, arguments);
986 modules.CompletionContext = Class("CompletionContext", CompletionContext, {
987 init: function init() {
988 this.modules = modules;
989 return init.superapply(this, arguments);
992 get options() this.modules.options
995 commands: function (dactyl, modules, window) {
996 const { commands, completion } = modules;
997 commands.add(["contexts"],
998 "List the completion contexts used during completion of an Ex command",
1000 modules.commandline.commandOutput(
1001 <div highlight="Completions">
1002 { template.completionRow(["Context", "Title"], "CompTitle") }
1003 { template.map(completion.contextList || [], function (item) template.completionRow(item, "CompItem")) }
1008 completer: function (context) {
1009 let PREFIX = "/ex/contexts";
1010 context.fork("ex", 0, completion, "ex");
1011 completion.contextList = [[k.substr(PREFIX.length), v.title[0]] for ([k, v] in iter(context.contexts)) if (k.substr(0, PREFIX.length) == PREFIX)];
1016 options: function (dactyl, modules, window) {
1017 const { completion, options } = modules;
1020 // Why do we need ""?
1021 // Because its description is useful during completion. --Kris
1022 "": "Complete only the first match",
1023 "full": "Complete the next full match",
1024 "longest": "Complete the longest common string",
1025 "list": "If more than one match, list all matches",
1026 "list:full": "List all and complete first match",
1027 "list:longest": "List all and complete the longest common string"
1029 checkHas: function (value, val) {
1030 let [first, second] = value.split(":", 2);
1031 return first == val || second == val;
1034 test = function (val) this.value.some(function (value) this.checkHas(value, val), this);
1035 return Array.some(arguments, test, this);
1039 options.add(["altwildmode", "awim"],
1040 "Define the behavior of the <A-Tab> key in command-line completion",
1041 "stringlist", "list:full",
1044 options.add(["autocomplete", "au"],
1045 "Automatically update the completion list on any key press",
1046 "regexplist", ".*");
1048 options.add(["complete", "cpt"],
1049 "Items which are completed at the :open prompts",
1050 "charlist", config.defaults.complete == null ? "slf" : config.defaults.complete,
1051 { get values() values(completion.urlCompleters).toArray() });
1053 options.add(["wildanchor", "wia"],
1054 "Define which completion groups only match at the beginning of their text",
1055 "regexplist", "!/ex/(back|buffer|ext|forward|help|undo)");
1057 options.add(["wildcase", "wic"],
1058 "Completion case matching mode",
1059 "regexpmap", ".?:smart",
1062 "smart": "Case is significant when capital letters are typed",
1063 "match": "Case is always significant",
1064 "ignore": "Case is never significant"
1068 options.add(["wildmode", "wim"],
1069 "Define the behavior of the <Tab> key in command-line completion",
1070 "stringlist", "list:full",
1073 options.add(["wildsort", "wis"],
1074 "Define which completion groups are sorted",
1075 "regexplist", ".*");
1081 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1083 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: