1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2014 Kris Maglione <maglione.k@gmail.com>
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
9 defineModule("completion", {
10 exports: ["CompletionContext", "Completion", "completion"]
13 lazyRequire("dom", ["DOM"]);
14 lazyRequire("messages", ["_", "messages"]);
15 lazyRequire("template", ["template"]);
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 cc_init(editor, name="", offset=0) {
38 if (editor instanceof this.constructor) {
40 name = parent.name + "/" + name;
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);
48 this.contexts = parent.contexts;
49 if (name in this.contexts)
50 self = this.contexts[name];
52 self.contexts[name] = this;
55 * @property {CompletionContext} This context's parent. {null} when
56 * this is a top-level context.
60 ["filters", "keys", "process", "title", "quote"]
61 .forEach(key => self[key] = parent[key] && util.cloneObject(parent[key]));
62 ["anchored", "compare", "editor", "_filter", "filterFunc", "forceAnchored", "top"]
63 .forEach(key => self[key] = parent[key]);
65 self.__defineGetter__("value", function get_value() this.top.value);
67 self.offset = parent.offset;
71 * @property {boolean} Specifies that this context is not finished
75 self.incomplete = false;
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.
82 self.waitingForTab = false;
86 delete self._generate;
87 delete self.ignoreCase;
90 ["_caret", "contextList", "maxItems", "onUpdate", "selectionTypes", "tabPressed", "updateAsync", "value"].forEach(function fe(key) {
91 self.__defineGetter__(key, function () this.top[key]);
92 self.__defineSetter__(key, function (val) this.top[key] = val);
96 if (typeof editor == "string")
101 * @property {boolean} Specifies whether this context results must
102 * match the filter at the beginning of the string.
105 this.anchored = true;
106 this.forceAnchored = null;
108 this.compare = function compare(a, b) String.localeCompare(a.text, b.text);
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
116 * @property {[CompletionContext]} A list of active
117 * completion contexts, in the order in which they were
120 this.contextList = [];
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}.
128 this.contexts = { "": this };
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.
134 this.filterFunc = function filterFunc(items) {
136 .reduce((res, filter)
137 => res.filter((item) => filter.call(this, item)),
141 * @property {Array} An array of predicates on which to filter the
144 this.filters = [CompletionContext.Filter.text];
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).
151 this.keys = { text: 0, description: 1, icon: "icon" };
153 * @property {number} This context's offset from the beginning of
154 * {@link #editor}'s value.
156 this.offset = offset;
158 * @property {function} A function which is called when any subcontext
159 * changes its completion list. Only called when
160 * {@link #updateAsync} is true.
162 this.onUpdate = function onUpdate() true;
167 * @property {CompletionContext} The top-level completion context.
170 this.__defineGetter__("incomplete", function get_incomplete() this._incomplete
171 || this.contextList.some(c => c.parent && c.incomplete));
172 this.__defineGetter__("waitingForTab", function get_waitingForTab() this._waitingForTab
173 || this.contextList.some(c => c.parent && c.waitingForTab));
174 this.__defineSetter__("incomplete", function get_incomplete(val) { this._incomplete = val; });
175 this.__defineSetter__("waitingForTab", function get_waitingForTab(val) { this._waitingForTab = val; });
179 * @property {Object} A general-purpose store for functions which need to
180 * cache data between calls.
186 * @property {Object} A cache for return values of {@link #generate}.
190 * @property {string} A key detailing when the cached value of
191 * {@link #generate} may be used. Every call to
192 * {@link #generate} stores its result in {@link #itemCache}.
193 * When itemCache[key] exists, its value is returned, and
194 * {@link #generate} is not called again.
198 * @property {string} A message to be shown before any results.
201 this.name = name || "";
203 this._completions = []; // FIXME
205 * Returns a key, as detailed in {@link #keys}.
208 this.getKey = function getKey(item, key) (typeof self.keys[key] == "function") ? self.keys[key].call(this, item.item) :
209 key in self.keys ? item.item[self.keys[key]]
214 __title: Class.Memoize(function __title() this._title.map(s =>
215 typeof s == "string" ? messages.get("completion.title." + s, s)
220 return this._title = val;
222 get title() this.__title,
224 get activeContexts() this.contextList.filter(function f(c) c.items.length),
230 * An object describing the results from all sub-contexts. Results are
231 * adjusted so that all have the same starting offset.
239 let allItems = this.contextList.map(function m(context) context.hasItems && context.items.length);
240 if (this.cache.allItems && array.equals(this.cache.allItems, allItems))
241 return this.cache.allItemsResult;
242 this.cache.allItems = allItems;
244 let minStart = Math.min.apply(Math, this.activeContexts.map(function m(c) c.offset));
245 if (minStart == Infinity)
248 this.cache.allItemsResult = memoize({
251 get longestSubstring() self.longestAllSubstring,
253 get items() array.flatten(self.activeContexts.map(function m(context) {
254 let prefix = self.value.substring(minStart, context.offset);
256 return context.items.map(function m(item) ({
257 text: prefix + item.text,
258 result: prefix + item.result,
264 return this.cache.allItemsResult;
268 return { start: 0, items: [], longestAllSubstring: "" };
272 get allSubstrings() {
273 let contexts = this.activeContexts;
274 let minStart = Math.min.apply(Math, contexts.map(function m(c) c.offset));
275 let lists = contexts.map(function m(context) {
276 let prefix = context.value.substring(minStart, context.offset);
277 return context.substrings.map(function m(s) prefix + s);
280 /* TODO: Deal with sub-substrings for multiple contexts again.
283 let substrings = lists.reduce(
284 function r(res, list) res.filter(function f(str) list.some(function s_(s) s.substr(0, str.length) == str)),
286 if (!substrings) // FIXME: How is this undefined?
288 return array.uniq(Array.slice(substrings));
291 get longestAllSubstring() {
292 return this.allSubstrings.reduce(function r(a, b) a.length > b.length ? a : b, "");
295 get caret() this._caret - this.offset,
296 set caret(val) this._caret = val + this.offset,
298 get compare() this._compare || function compare() 0,
299 set compare(val) this._compare = val,
301 get completions() this._completions || [],
302 set completions(items) {
303 if (items && isArray(items.array))
305 // Accept a generator
307 items = [x for (x in Iterator(items || []))];
308 if (this._completions !== items) {
309 delete this.cache.filtered;
310 delete this.cache.filter;
311 this.cache.rows = [];
312 this._completions = items;
313 this.itemCache[this.key] = items;
316 if (this._completions)
317 this.hasItems = this._completions.length > 0;
319 if (this.updateAsync && !this.noUpdate)
320 util.trapErrors("onUpdate", this);
323 get createRow() this._createRow || template.completionRow, // XXX
324 set createRow(createRow) this._createRow = createRow,
326 get filterFunc() this._filterFunc || util.identity,
327 set filterFunc(val) this._filterFunc = val,
329 get filter() this._filter != null ? this._filter : this.value.substr(this.offset, this.caret),
331 delete this.ignoreCase;
332 return this._filter = val;
336 anchored: this.anchored,
339 process: this.process
342 this.anchored = format.anchored,
343 this.title = format.title || this.title;
344 this.keys = format.keys || this.keys;
345 this.process = format.process || this.process;
349 * @property {string | xml | null}
350 * The message displayed at the head of the completions for the
353 get message() this._message || (this.waitingForTab && this.hasItems !== false ? _("completion.waitingFor", "<Tab>") : null),
354 set message(val) this._message = val,
357 * The prototype object for items returned by {@link items}.
359 get itemPrototype() {
361 let res = { highlight: "" };
363 function result(quote) {
364 yield ["context", function p_context() self];
365 yield ["result", quote ? function p_result() quote[0] + util.trapErrors(1, quote, this.text) + quote[2]
366 : function p_result() this.text];
367 yield ["texts", function p_texts() Array.concat(this.text)];
370 for (let i in iter(this.keys, result(this.quote))) {
372 if (typeof v == "string" && /^[.[]/.test(v))
373 // This is only allowed to be a simple accessor, and shouldn't
374 // reference any variables. Don't bother with eval context.
375 v = Function("i", "return i" + v);
376 if (typeof v == "function")
377 res.__defineGetter__(k, function p_gf() Class.replaceProperty(this, k, v.call(this, this.item, self)));
379 res.__defineGetter__(k, function p_gp() Class.replaceProperty(this, k, this.item[v]));
380 res.__defineSetter__(k, function p_s(val) Class.replaceProperty(this, k, val));
386 * Returns true when the completions generated by {@link #generate}
387 * must be regenerated. May be set to true to invalidate the current
390 get regenerate() this._generate && (!this.completions || !this.itemCache[this.key] || this._cache.offset != this.offset),
391 set regenerate(val) { if (val) delete this.itemCache[this.key]; },
394 * A property which may be set to a function to generate the value
395 * of {@link completions} only when necessary. The generated
396 * completions are linked to the value in {@link #key} and may be
397 * invalidated by setting the {@link #regenerate} property.
399 get generate() this._generate || null,
401 this.hasItems = true;
402 this._generate = arg;
405 * Generates the item list in {@link #completions} via the
406 * {@link #generate} method if the previously generated value is no
409 generateCompletions: function generateCompletions() {
410 if (this.offset != this._cache.offset || this.lastActivated != this.top.runCount) {
412 this._cache.offset = this.offset;
413 this.lastActivated = this.top.runCount;
415 if (!this.itemCache[this.key] && !this.waitingForTab) {
417 let res = this._generate();
419 this.itemCache[this.key] = res;
423 this.message = _("error.error", e);
427 this.noUpdate = true;
428 this.completions = this.itemCache[this.key];
429 this.noUpdate = false;
432 ignoreCase: Class.Memoize(function M() {
433 let mode = this.wildcase;
436 else if (mode == "ignore")
439 return !/[A-Z]/.test(this.filter);
443 * Returns a list of all completion items which match the current
444 * filter. The items returned are objects containing one property
445 * for each corresponding property in {@link keys}. The returned
446 * list is generated on-demand from the item list in {@link completions}
447 * or generated by {@link generate}, and is cached as long as no
448 * properties which would invalidate the result are changed.
451 // Don't return any items if completions or generator haven't
452 // been set during this completion cycle.
456 // Regenerate completions if we must
458 this.generateCompletions();
459 let items = this.completions;
461 // Check for cache miss
462 if (this._cache.completions !== this.completions) {
463 this._cache.completions = this.completions;
464 this._cache.constructed = null;
465 this.cache.filtered = null;
468 if (this.cache.filtered && this.cache.filter == this.filter)
469 return this.cache.filtered;
471 this.cache.rows = [];
472 this.cache.filter = this.filter;
477 delete this._substrings;
479 if (!this.forceAnchored && this.options)
480 this.anchored = this.options.get("wildanchor").getKey(this.name, this.anchored);
484 this.matchString = this.anchored ?
485 function matchString(filter, str) String.toLowerCase(str).indexOf(filter.toLowerCase()) == 0 :
486 function matchString(filter, str) String.toLowerCase(str).indexOf(filter.toLowerCase()) >= 0;
488 this.matchString = this.anchored ?
489 function matchString(filter, str) String.indexOf(str, filter) == 0 :
490 function matchString(filter, str) String.indexOf(str, filter) >= 0;
493 this.processor = Array.slice(this.process);
495 this.processor[0] = function processor_0(item, text) self.process[0].call(self, item,
496 template.highlightFilter(item.text, self.filter, null, item.isURI));
500 if (!this._cache.constructed) {
501 let proto = this.itemPrototype;
502 this._cache.constructed = items.map(function m(item) ({ __proto__: proto, item: item }));
506 let filtered = this.filterFunc(this._cache.constructed);
508 filtered = filtered.slice(0, this.maxItems);
511 if (this.sortResults && this.compare) {
512 filtered.sort(this.compare);
513 if (!this.anchored) {
514 let filter = this.filter;
515 filtered.sort(function s(a, b) b.text.startsWith(filter) - a.text.startsWith(filter));
519 return this.cache.filtered = filtered;
522 this.message = _("error.error", e);
529 * Returns a list of all substrings common to all items which
530 * include the current filter.
533 let items = this.items;
534 if (items.length == 0 || !this.hasItems)
536 if (this._substrings)
537 return this._substrings;
539 let fixCase = this.ignoreCase ? String.toLowerCase : util.identity;
540 let text = fixCase(items[0].text);
541 let filter = fixCase(this.filter);
543 // Exceedingly long substrings cause Gecko to go into convulsions
544 if (text.length > 100)
545 text = text.substr(0, 100);
548 var compare = function compare(text, s) text.substr(0, s.length) == s;
549 var substrings = [text];
552 var compare = function compare(text, s) text.contains(s);
556 let length = filter.length;
557 while ((idx = text.indexOf(filter, start)) > -1 && idx < text.length) {
558 substrings.push(text.substring(idx));
563 substrings = items.reduce(function r(res, item)
564 res.map(function m(substring) {
565 // A simple binary search to find the longest substring
566 // of the given string which also matches the current
568 let len = substring.length;
569 let i = 0, n = len + 1;
570 let result = n && fixCase(item.result);
572 let m = Math.floor(n / 2);
573 let keep = compare(result, substring.substring(0, i + m));
583 return len == substring.length ? substring : substring.substr(0, Math.max(len, 0));
587 let quote = this.quote;
589 substrings = substrings.map(function m(str) quote[0] + quote[1](str));
590 return this._substrings = substrings;
594 * Advances the context *count* characters. {@link #filter} is advanced to
595 * match. If {@link #quote} is non-null, its prefix and suffix are set to
598 * This function is still imperfect for quoted strings. When
599 * {@link #quote} is non-null, it adjusts the count based on the quoted
600 * size of the *count*-character substring of the filter, which is accurate
601 * so long as unquoting and quoting a string will always map to the
602 * original quoted string, which is often not the case.
604 * @param {number} count The number of characters to advance the context.
606 advance: function advance(count) {
607 delete this.ignoreCase;
609 if (this.quote && count) {
610 advance = this.quote[1](this.filter.substr(0, count)).length;
611 count = this.quote[0].length + advance;
615 this.offset += count;
617 this._filter = this._filter.substr(arguments[0] || 0);
621 * Calls the {@link #cancel} method of all currently active
624 cancelAll: function cancelAll() {
625 for (let [, context] in Iterator(this.contextList)) {
632 * Gets a key from {@link #cache}, setting it to *defVal* if it doesn't
635 * @param {string} key
638 getCache: function getCache(key, defVal) {
639 if (!(key in this.cache))
640 this.cache[key] = defVal();
641 return this.cache[key];
644 getItems: function getItems(start, end) {
645 let items = this.items;
646 let step = start > end ? -1 : 1;
647 start = Math.max(0, start || 0);
648 end = Math.min(items.length, end ? end : items.length);
649 return iter.map(util.range(start, end, step), function m(i) items[i]);
652 getRow: function getRow(idx, doc) {
653 let cache = this.cache.rows;
655 if (idx in this.items && !(idx in this.cache.rows))
657 cache[idx] = DOM.fromJSON(this.createRow(this.items[idx]),
662 util.dump(util.prettifyJSON(this.createRow(this.items[idx]), null, true));
663 cache[idx] = DOM.fromJSON(
664 ["div", { highlight: "CompItem", style: "white-space: nowrap" },
665 ["li", { highlight: "CompResult" }, this.text + "\u00a0"],
666 ["li", { highlight: "CompDesc ErrorMsg" }, e + "\u00a0"]],
673 getRows: function getRows(start, end, doc) {
674 let items = this.items;
675 let cache = this.cache.rows;
676 let step = start > end ? -1 : 1;
678 start = Math.max(0, start || 0);
679 end = Math.min(items.length, end != null ? end : items.length);
682 for (let i in util.range(start, end, step))
683 yield [i, this.getRow(i)];
687 * Forks this completion context to create a new sub-context named
688 * as {this.name}/{name}. The new context is automatically advanced
689 * *offset* characters. If *completer* is provided, it is called
690 * with *self* as its 'this' object, the new context as its first
691 * argument, and any subsequent arguments after *completer* as its
692 * following arguments.
694 * If *completer* is provided, this function returns its return
695 * value, otherwise it returns the new completion context.
697 * @param {string} name The name of the new context.
698 * @param {number} offset The offset of the new context relative to
699 * the current context's offset.
700 * @param {object} self *completer*'s 'this' object. @optional
701 * @param {function|string} completer A completer function to call
702 * for the new context. If a string is provided, it is
703 * interpreted as a method to access on *self*.
705 fork: function fork(name, offset, self, completer, ...args) {
706 return this.forkapply(name, offset, self, completer, args);
709 forkapply: function forkapply(name, offset, self, completer, args) {
710 if (isString(completer))
711 completer = self[completer];
712 let context = this.constructor(this, name, offset);
713 if (this.contextList.indexOf(context) < 0)
714 this.contextList.push(context);
716 if (!context.autoComplete && !context.tabPressed && context.editor)
717 context.waitingForTab = true;
718 else if (completer) {
719 let res = completer.apply(self || this, [context].concat(args));
720 if (res && !isArray(res) && !isArray(res.__proto__))
721 res = [k for (k in res)];
723 context.completions = res;
731 split: function split(name, obj, fn, ...args) {
732 let context = this.fork(name);
733 let alias = (prop) => {
734 context.__defineGetter__(prop, () => this[prop]);
735 context.__defineSetter__(prop, (val) => this[prop] = val);
738 alias("_completions");
740 alias("_regenerate");
742 alias("lastActivated");
743 context.hasItems = true;
744 this.hasItems = false;
746 return fn.apply(obj || this, [context].concat(args));
751 * Highlights text in the nsIEditor associated with this completion
752 * context. *length* characters are highlighted from the position
753 * *start*, relative to the current context's offset, with the
754 * selection type *type* as defined in nsISelectionController.
756 * When called with no arguments, all highlights are removed. When
757 * called with a 0 length, all highlights of type *type* are
760 * @param {number} start The position at which to start
762 * @param {number} length The length of the substring to highlight.
763 * @param {string} type The selection type to highlight with.
765 highlight: function highlight(start, length, type) {
766 if (arguments.length == 0) {
767 for (let type in this.selectionTypes)
768 this.highlight(0, 0, type);
769 this.selectionTypes = {};
772 // Requires Gecko >= 1.9.1
773 this.selectionTypes[type] = true;
774 const selType = Ci.nsISelectionController["SELECTION_" + type];
775 let sel = this.editor.selectionController.getSelection(selType);
777 sel.removeAllRanges();
779 let range = this.editor.selection.getRangeAt(0).cloneRange();
780 range.setStart(range.startContainer, this.offset + start);
781 range.setEnd(range.startContainer, this.offset + start + length);
789 * Tests the given string for a match against the current filter,
790 * taking into account anchoring and case sensitivity rules.
792 * @param {string} str The string to match.
793 * @returns {boolean} True if the string matches, false otherwise.
795 match: function match(str) this.matchString(this.filter, str),
798 * Pushes a new output processor onto the processor chain of
799 * {@link #process}. The provided function is called with the item
800 * and text to process along with a reference to the processor
801 * previously installed in the given *index* of {@link #process}.
803 * @param {number} index The index into {@link #process}.
804 * @param {function(object, string, function)} func The new
807 pushProcessor: function pushProcess(index, func) {
808 let next = this.process[index];
809 this.process[index] = function process_(item, text) func(item, text, next);
813 * Resets this completion context and all sub-contexts for use in a
814 * new completion cycle. May only be called on the top-level
817 reset: function reset() {
822 this.process = [template.icon, function process_1(item, k) k];
823 this.filters = [CompletionContext.Filter.text];
824 this.tabPressed = false;
825 this.title = ["Completions"];
826 this.updateAsync = false;
831 this.value = this.editor.selection.focusNode.textContent;
832 this._caret = this.editor.selection.focusOffset;
835 this.value = this._value;
836 this._caret = this.value.length;
838 //for (let key in (k for ([k, v] in Iterator(this.contexts)) if (v.offset > this.caret)))
839 // delete this.contexts[key];
840 for each (let context in this.contexts) {
841 context.hasItems = false;
842 context.incomplete = false;
844 this.waitingForTab = false;
846 for (let context of this.contextList)
847 context.lastActivated = this.runCount;
848 this.contextList = [];
852 * Wait for all subcontexts to complete.
854 * @param {number} timeout The maximum time, in milliseconds, to wait.
855 * If 0 or null, wait indefinitely.
856 * @param {boolean} interruptible When true, the call may be interrupted
857 * via <C-c>, in which case, "Interrupted" may be thrown.
859 wait: function wait(timeout, interruptable) {
861 return util.waitFor(function wf() !this.incomplete, this, timeout, interruptable);
865 number: function S_number(a, b) parseInt(a.text) - parseInt(b.text)
866 || String.localeCompare(a.text, b.text),
871 text: function F_text(item) {
872 let text = item.texts;
873 for (let [i, str] in Iterator(text)) {
874 if (this.match(String(str))) {
875 item.text = String(text[i]);
881 textDescription: function F_textDescription(item) {
882 return CompletionContext.Filter.text.call(this, item) || this.match(item.description);
888 * @instance completion
890 var Completion = Module("completion", {
891 init: function init() {
894 get setFunctionCompleter() JavaScript.setCompleter, // Backward compatibility
896 Local: function Local(dactyl, modules, window) ({
899 get modules() modules,
900 get options() modules.options,
903 _runCompleter: function _runCompleter(name, filter, maxItems, ...args) {
904 let context = modules.CompletionContext(filter);
905 context.maxItems = maxItems;
906 let res = context.fork.apply(context, ["run", 0, this, name].concat(args));
908 if (Components.stack.caller.name === "runCompleter") // FIXME
909 return { items: res.map(function m(i) ({ item: i })) };
910 context.contexts["/run"].completions = res;
912 context.wait(null, true);
913 return context.allItems;
916 runCompleter: function runCompleter(name, filter, maxItems) {
917 return this._runCompleter.apply(this, arguments)
918 .items.map(function m(i) i.item);
921 listCompleter: function listCompleter(name, filter, maxItems, ...args) {
922 let context = modules.CompletionContext(filter || "");
923 context.maxItems = maxItems;
924 context.fork.apply(context, ["list", 0, this, name].concat(args));
925 context = context.contexts["/list"];
926 context.wait(null, true);
928 let contexts = context.activeContexts;
929 if (!contexts.length)
930 contexts = context.contextList.filter(function f(c) c.hasItems).slice(0, 1);
931 if (!contexts.length)
932 contexts = context.contextList.slice(-1);
934 modules.commandline.commandOutput(
935 ["div", { highlight: "Completions" },
936 template.map(contexts, function m(context)
937 [template.completionRow(context.title, "CompTitle"),
938 template.map(context.items, function m(item) context.createRow(item), null, 100)])]);
942 ////////////////////////////////////////////////////////////////////////////////
943 ////////////////////// COMPLETION TYPES ////////////////////////////////////////
944 /////////////////////////////////////////////////////////////////////////////{{{
946 // filter a list of urls
948 // may consist of search engines, filenames, bookmarks and history,
949 // depending on the 'complete' option
950 // if the 'complete' argument is passed like "h", it temporarily overrides the complete option
951 url: function url(context, complete) {
952 if (/^jar:[^!]*$/.test(context.filter)) {
955 context.quote = context.quote || ["", util.identity, ""];
956 let quote = context.quote[1];
957 context.quote[1] = function quote_1(str) quote(str.replace(/!/g, escape));
960 if (this.options["urlseparator"])
961 var skip = util.regexp("^.*" + this.options["urlseparator"] + "\\s*")
962 .exec(context.filter);
965 context.advance(skip[0].length);
967 if (/^about:/.test(context.filter))
968 context.fork("about", 6, this, function fork_(context) {
969 context.title = ["about:"];
970 context.generate = function generate_() {
971 return [[k.substr(services.ABOUT.length), ""]
973 if (k.startsWith(services.ABOUT))];
977 if (complete == null)
978 complete = this.options["complete"];
980 // Will, and should, throw an error if !(c in opts)
981 Array.forEach(complete, function fe(c) {
982 let completer = this.urlCompleters[c] || { args: [], completer: this.autocomplete(c.replace(/^native:/, "")) };
983 context.forkapply(c, 0, this, completer.completer, completer.args);
987 addUrlCompleter: function addUrlCompleter(name, description, completer, ...args) {
988 let completer = Completion.UrlCompleter(name, description, completer);
989 completer.args = args;
990 this.urlCompleters[name] = completer;
993 autocomplete: curry(function autocomplete(provider, context) {
994 let running = context.getCache("autocomplete-search-running", Object);
996 let name = "autocomplete:" + provider;
997 if (!services.has(name))
998 services.add(name, services.AUTOCOMPLETE + provider, "nsIAutoCompleteSearch");
999 let service = services[name];
1001 util.assert(service, _("autocomplete.noSuchProvider", provider), false);
1003 if (running[provider]) {
1004 this.completions = this.completions;
1008 context.anchored = false;
1009 context.compare = CompletionContext.Sort.unsorted;
1010 context.filterFunc = null;
1012 let words = context.filter.toLowerCase().split(/\s+/g);
1013 context.hasItems = true;
1014 context.completions = context.completions.filter(function f({ url, title })
1015 words.every(function e(w) (url + " " + title).toLowerCase().indexOf(w) >= 0));
1017 context.format = this.modules.bookmarks.format;
1018 context.keys.extra = function k_extra(item) {
1020 return bookmarkcache.get(item.url).extra;
1025 context.title = [_("autocomplete.title", provider)];
1027 context.cancel = function cancel_() {
1028 this.incomplete = false;
1029 if (running[provider])
1030 service.stopSearch();
1031 running[provider] = false;
1034 if (!context.waitingForTab) {
1035 context.incomplete = true;
1037 service.startSearch(context.filter, "", context.result, {
1038 onSearchResult: util.wrapCallback(function onSearchResult(search, result) {
1039 if (result.searchResult <= result.RESULT_SUCCESS)
1040 running[provider] = null;
1042 context.incomplete = result.searchResult >= result.RESULT_NOMATCH_ONGOING;
1043 context.completions = [
1044 { url: result.getValueAt(i), title: result.getCommentAt(i), icon: result.getImageAt(i) }
1045 for (i in util.range(0, result.matchCount))
1048 get onUpdateSearchResult() this.onSearchResult
1050 running[provider] = true;
1054 urls: function urls(context, tags) {
1055 let compare = String.localeCompare;
1056 let contains = String.indexOf;
1057 if (context.ignoreCase) {
1058 compare = util.compareIgnoreCase;
1059 contains = function contains_(a, b) a && a.toLowerCase().contains(b.toLowerCase());
1063 context.filters.push(function filter_(item) tags.
1064 every(function e(tag) (item.tags || []).
1065 some(function s(t) !compare(tag, t))));
1067 context.anchored = false;
1069 context.title = ["URL", "Title"];
1071 context.fork("additional", 0, this, function fork_(context) {
1072 context.title[0] += " " + _("completion.additional");
1073 context.filter = context.parent.filter; // FIXME
1074 context.completions = context.parent.completions;
1076 // For items whose URL doesn't exactly match the filter,
1077 // accept them if all tokens match either the URL or the title.
1078 // Filter out all directly matching strings.
1079 let match = context.filters[0];
1080 context.filters[0] = function filters_0(item) !match.call(this, item);
1082 // and all that don't match the tokens.
1083 let tokens = context.filter.split(/\s+/);
1084 context.filters.push(function filter_(item) tokens.every(
1085 function e(tok) contains(item.url, tok) ||
1086 contains(item.title, tok)));
1088 let re = RegExp(tokens.filter(util.identity).map(util.regexp.escape).join("|"), "g");
1089 function highlight(item, text, i) process[i].call(this, item, template.highlightRegexp(text, re));
1090 let process = context.process;
1092 function process_0(item, text) highlight.call(this, item, item.text, 0),
1093 function process_1(item, text) highlight.call(this, item, text, 1)
1099 UrlCompleter: Struct("name", "description", "completer")
1101 init: function init(dactyl, modules, window) {
1102 init.superapply(this, arguments);
1104 modules.CompletionContext = Class("CompletionContext", CompletionContext, {
1105 init: function init() {
1106 this.modules = modules;
1107 return init.superapply(this, arguments);
1110 get options() this.modules.options
1113 commands: function initCommands(dactyl, modules, window) {
1114 const { commands, completion } = modules;
1115 commands.add(["contexts"],
1116 "List the completion contexts used during completion of an Ex command",
1118 modules.commandline.commandOutput(
1119 ["div", { highlight: "Completions" },
1120 template.completionRow(["Context", "Title"], "CompTitle"),
1121 template.map(completion.contextList || [],
1122 function m(item) template.completionRow(item, "CompItem"))]);
1126 completer: function (context) {
1127 let PREFIX = "/ex/contexts";
1128 context.fork("ex", 0, completion, "ex");
1129 completion.contextList = [[k.substr(PREFIX.length), v.title[0]] for ([k, v] in iter(context.contexts)) if (k.substr(0, PREFIX.length) == PREFIX)];
1134 options: function initOptions(dactyl, modules, window) {
1135 const { completion, options } = modules;
1138 // Why do we need ""?
1139 // Because its description is useful during completion. --Kris
1140 "": "Complete only the first match",
1141 "full": "Complete the next full match",
1142 "longest": "Complete the longest common string",
1143 "list": "If more than one match, list all matches",
1144 "list:full": "List all and complete first match",
1145 "list:longest": "List all and complete the longest common string"
1147 checkHas: function (value, val) {
1148 let [first, second] = value.split(":", 2);
1149 return first == val || second == val;
1152 let test = function test(val) this.value.some(function s(value) this.checkHas(value, val), this);
1153 return Array.some(arguments, test, this);
1157 options.add(["altwildmode", "awim"],
1158 "Define the behavior of the c_<A-Tab> key in command-line completion",
1159 "stringlist", "list:full",
1162 options.add(["autocomplete", "au"],
1163 "Automatically update the completion list on any key press",
1164 "regexplist", ".*");
1166 options.add(["complete", "cpt"],
1167 "Items which are completed at the :open prompts",
1168 "stringlist", "slf",
1179 get values() values(completion.urlCompleters).toArray()
1180 .concat([let (name = k.substr(services.AUTOCOMPLETE.length))
1181 ["native:" + name, _("autocomplete.description", name)]
1183 if (k.startsWith(services.AUTOCOMPLETE))]),
1185 setter: function setter(values) {
1186 if (values.length == 1 && !hasOwnProperty(values[0], this.values)
1187 && Array.every(values[0], v => hasOwnProperty(this.valueMap, v)))
1188 return Array.map(values[0], v => this.valueMap[v]);
1193 validator: function validator(values) validator.supercall(this, this.setter(values))
1196 options.add(["wildanchor", "wia"],
1197 "Define which completion groups only match at the beginning of their text",
1198 "regexplist", "!/ex/(back|buffer|ext|forward|help|undo)");
1200 options.add(["wildcase", "wic"],
1201 "Completion case matching mode",
1202 "regexpmap", ".?:smart",
1205 "smart": "Case is significant when capital letters are typed",
1206 "match": "Case is always significant",
1207 "ignore": "Case is never significant"
1211 options.add(["wildmode", "wim"],
1212 "Define the behavior of the c_<Tab> key in command-line completion",
1213 "stringlist", "list:full",
1216 options.add(["wildsort", "wis"],
1217 "Define which completion groups are sorted",
1218 "regexplist", ".*");
1224 // catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1226 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: