//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
-
-try {
+/* use strict */
Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("completion", {
- exports: ["CompletionContext", "Completion", "completion"],
- use: ["config", "messages", "template", "util"]
+ exports: ["CompletionContext", "Completion", "completion"]
}, this);
/**
return this;
},
+ __title: Class.Memoize(function () this._title.map(function (s)
+ typeof s == "string" ? messages.get("completion.title." + s, s)
+ : s)),
+
+ set title(val) {
+ delete this.__title;
+ return this._title = val;
+ },
+ get title() this.__title,
+
+ get activeContexts() this.contextList.filter(function (c) c.items.length),
+
// Temporary
/**
* @property {Object}
* @deprecated
*/
get allItems() {
+ let self = this;
+
try {
- let self = this;
- let allItems = this.contextList.map(function (context) context.hasItems && context.items);
+ let allItems = this.contextList.map(function (context) context.hasItems && context.items.length);
if (this.cache.allItems && array.equals(this.cache.allItems, allItems))
return this.cache.allItemsResult;
this.cache.allItems = allItems;
- let minStart = Math.min.apply(Math, [context.offset for ([k, context] in Iterator(this.contexts)) if (context.hasItems && context.items.length)]);
+ let minStart = Math.min.apply(Math, this.activeContexts.map(function (c) c.offset));
if (minStart == Infinity)
minStart = 0;
- let items = this.contextList.map(function (context) {
- if (!context.hasItems)
- return [];
- let prefix = self.value.substring(minStart, context.offset);
- return context.items.map(function (item) ({
- text: prefix + item.text,
- result: prefix + item.result,
- __proto__: item
- }));
+
+ this.cache.allItemsResult = memoize({
+ start: minStart,
+
+ get longestSubstring() self.longestAllSubstring,
+
+ get items() array.flatten(self.activeContexts.map(function (context) {
+ let prefix = self.value.substring(minStart, context.offset);
+
+ return context.items.map(function (item) ({
+ text: prefix + item.text,
+ result: prefix + item.result,
+ __proto__: item
+ }));
+ }))
});
- this.cache.allItemsResult = { start: minStart, items: array.flatten(items) };
- memoize(this.cache.allItemsResult, "longestSubstring", function () self.longestAllSubstring);
+
return this.cache.allItemsResult;
}
catch (e) {
},
// Temporary
get allSubstrings() {
- let contexts = this.contextList.filter(function (c) c.hasItems && c.items.length);
+ let contexts = this.activeContexts;
let minStart = Math.min.apply(Math, contexts.map(function (c) c.offset));
let lists = contexts.map(function (context) {
let prefix = context.value.substring(minStart, context.offset);
this._completions = items;
this.itemCache[this.key] = items;
}
+
if (this._completions)
this.hasItems = this._completions.length > 0;
+
if (this.updateAsync && !this.noUpdate)
- this.onUpdate();
+ util.trapErrors("onUpdate", this);
},
get createRow() this._createRow || template.completionRow, // XXX
* The prototype object for items returned by {@link items}.
*/
get itemPrototype() {
+ let self = this;
let res = { highlight: "" };
+
function result(quote) {
+ yield ["context", function () self];
yield ["result", quote ? function () quote[0] + util.trapErrors(1, quote, this.text) + quote[2]
: function () this.text];
+ yield ["texts", function () Array.concat(this.text)];
};
+
for (let i in iter(this.keys, result(this.quote))) {
let [k, v] = i;
if (typeof v == "string" && /^[.[]/.test(v))
// reference any variables. Don't bother with eval context.
v = Function("i", "return i" + v);
if (typeof v == "function")
- res.__defineGetter__(k, function () Class.replaceProperty(this, k, v.call(this, this.item)));
+ res.__defineGetter__(k, function () Class.replaceProperty(this, k, v.call(this, this.item, self)));
else
res.__defineGetter__(k, function () Class.replaceProperty(this, k, this.item[v]));
res.__defineSetter__(k, function (val) Class.replaceProperty(this, k, val));
this.noUpdate = false;
},
- ignoreCase: Class.memoize(function () {
+ ignoreCase: Class.Memoize(function () {
let mode = this.wildcase;
if (mode == "match")
return false;
this.processor = Array.slice(this.process);
if (!this.anchored)
this.processor[0] = function (item, text) self.process[0].call(self, item,
- template.highlightFilter(item.text, self.filter));
+ template.highlightFilter(item.text, self.filter, null, item.isURI));
try {
// Item prototypes
// of the given string which also matches the current
// item's text.
let len = substring.length;
- let i = 0, n = len;
+ let i = 0, n = len + 1;
+ let result = n && fixCase(item.result);
while (n) {
let m = Math.floor(n / 2);
- let keep = compare(fixCase(item.text), substring.substring(0, i + m));
+ let keep = compare(result, substring.substring(0, i + m));
if (!keep)
len = i + m - 1;
if (!keep || m == 0)
}
this.offset += count;
if (this._filter)
- this._filter = this._filter.substr(advance);
+ this._filter = this._filter.substr(arguments[0] || 0);
},
/**
return iter.map(util.range(start, end, step), function (i) items[i]);
},
+ getRow: function getRow(idx, doc) {
+ let cache = this.cache.rows;
+ if (cache) {
+ if (idx in this.items && !(idx in this.cache.rows))
+ try {
+ cache[idx] = util.xmlToDom(this.createRow(this.items[idx]),
+ doc || this.doc);
+ }
+ catch (e) {
+ util.reportError(e);
+ cache[idx] = util.xmlToDom(
+ <div highlight="CompItem" style="white-space: nowrap">
+ <li highlight="CompResult">{this.text} </li>
+ <li highlight="CompDesc ErrorMsg">{e} </li>
+ </div>, doc || this.doc);
+ }
+ return cache[idx];
+ }
+ },
+
getRows: function getRows(start, end, doc) {
let self = this;
let items = this.items;
let cache = this.cache.rows;
let step = start > end ? -1 : 1;
+
start = Math.max(0, start || 0);
end = Math.min(items.length, end != null ? end : items.length);
+
+ this.doc = doc;
for (let i in util.range(start, end, step))
- yield [i, cache[i] = cache[i] || util.xmlToDom(self.createRow(items[i]), doc)];
+ yield [i, this.getRow(i)];
},
/**
Filter: {
text: function (item) {
- let text = Array.concat(item.text);
+ let text = item.texts;
for (let [i, str] in Iterator(text)) {
if (this.match(String(str))) {
item.text = String(text[i]);
Local: function (dactyl, modules, window) ({
urlCompleters: {},
+ get modules() modules,
get options() modules.options,
// FIXME
context = context.contexts["/list"];
context.wait(null, true);
- let contexts = context.contextList.filter(function (c) c.hasItems && c.items.length);
+ let contexts = context.activeContexts;
if (!contexts.length)
contexts = context.contextList.filter(function (c) c.hasItems).slice(0, 1);
if (!contexts.length)
if (/^about:/.test(context.filter))
context.fork("about", 6, this, function (context) {
+ context.title = ["about:"];
context.generate = function () {
- const PREFIX = "@mozilla.org/network/protocol/about;1?what=";
- return [[k.substr(PREFIX.length), ""] for (k in Cc) if (k.indexOf(PREFIX) == 0)];
+ return [[k.substr(services.ABOUT.length), ""]
+ for (k in Cc)
+ if (k.indexOf(services.ABOUT) == 0)];
};
});
// Will, and should, throw an error if !(c in opts)
Array.forEach(complete, function (c) {
- let completer = this.urlCompleters[c];
+ let completer = this.urlCompleters[c] || { args: [], completer: this.autocomplete(c.replace(/^native:/, "")) };
context.forkapply(c, 0, this, completer.completer, completer.args);
}, this);
},
this.urlCompleters[opt] = completer;
},
+ autocomplete: curry(function autocomplete(provider, context) {
+ let running = context.getCache("autocomplete-search-running", Object);
+
+ let name = "autocomplete:" + provider;
+ if (!services.has(name))
+ services.add(name, services.AUTOCOMPLETE + provider, "nsIAutoCompleteSearch");
+ let service = services[name];
+
+ util.assert(service, _("autocomplete.noSuchProvider", provider), false);
+
+ if (running[provider]) {
+ this.completions = this.completions;
+ this.cancel();
+ }
+
+ context.anchored = false;
+ context.compare = CompletionContext.Sort.unsorted;
+ context.filterFunc = null;
+
+ let words = context.filter.toLowerCase().split(/\s+/g);
+ context.hasItems = true;
+ context.completions = context.completions.filter(function ({ url, title })
+ words.every(function (w) (url + " " + title).toLowerCase().indexOf(w) >= 0))
+ context.incomplete = true;
+
+ context.format = this.modules.bookmarks.format;
+ context.keys.extra = function (item) {
+ try {
+ return bookmarkcache.get(item.url).extra;
+ }
+ catch (e) {}
+ return null;
+ };
+ context.title = [_("autocomplete.title", provider)];
+
+ context.cancel = function () {
+ this.incomplete = false;
+ if (running[provider])
+ service.stopSearch();
+ running[provider] = false;
+ };
+
+ service.startSearch(context.filter, "", context.result, {
+ onSearchResult: util.wrapCallback(function onSearchResult(search, result) {
+ if (result.searchResult <= result.RESULT_SUCCESS)
+ running[provider] = null;
+
+ context.incomplete = result.searchResult >= result.RESULT_NOMATCH_ONGOING;
+ context.completions = [
+ { url: result.getValueAt(i), title: result.getCommentAt(i), icon: result.getImageAt(i) }
+ for (i in util.range(0, result.matchCount))
+ ];
+ }),
+ get onUpdateSearchResult() this.onSearchResult
+ });
+ running[provider] = true;
+ }),
+
urls: function (context, tags) {
let compare = String.localeCompare;
let contains = String.indexOf;
context.title[0] += " " + _("completion.additional");
context.filter = context.parent.filter; // FIXME
context.completions = context.parent.completions;
+
// For items whose URL doesn't exactly match the filter,
// accept them if all tokens match either the URL or the title.
// Filter out all directly matching strings.
let match = context.filters[0];
context.filters[0] = function (item) !match.call(this, item);
+
// and all that don't match the tokens.
let tokens = context.filter.split(/\s+/);
context.filters.push(function (item) tokens.every(
options.add(["complete", "cpt"],
"Items which are completed at the :open prompts",
- "charlist", config.defaults.complete == null ? "slf" : config.defaults.complete,
- { get values() values(completion.urlCompleters).toArray() });
+ "stringlist", "slf",
+ {
+ valueMap: {
+ S: "suggestion",
+ b: "bookmark",
+ f: "file",
+ h: "history",
+ l: "location",
+ s: "search"
+ },
+
+ get values() values(completion.urlCompleters).toArray()
+ .concat([let (name = k.substr(services.AUTOCOMPLETE.length))
+ ["native:" + name, _("autocomplete.description", name)]
+ for (k in Cc)
+ if (k.indexOf(services.AUTOCOMPLETE) == 0)]),
+
+ setter: function setter(values) {
+ if (values.length == 1 && !Set.has(values[0], this.values)
+ && Array.every(values[0], Set.has(this.valueMap)))
+ return Array.map(values[0], function (v) this[v], this.valueMap);
+ return values;
+ },
+
+ validator: function validator(values) validator.supercall(this, this.setter(values))
+ });
options.add(["wildanchor", "wia"],
"Define which completion groups only match at the beginning of their text",
endModule();
-} catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
+// catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
// vim: set fdm=marker sw=4 ts=4 et ft=javascript: