X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=common%2Fmodules%2Fcompletion.jsm;fp=common%2Fmodules%2Fcompletion.jsm;h=ff9c0917dade8a5403118350df32baa0b1485682;hb=9044153cb63835e39b9de8ec4ade237c03e3888a;hp=9671e95aad543f8eadf3a5f6acaaf8498bef26c0;hpb=70740024f9c028c1fd63e1a1850ab062ff956054;p=dactyl.git diff --git a/common/modules/completion.jsm b/common/modules/completion.jsm index 9671e95..ff9c091 100644 --- a/common/modules/completion.jsm +++ b/common/modules/completion.jsm @@ -4,14 +4,11 @@ // // 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); /** @@ -212,6 +209,18 @@ var CompletionContext = Class("CompletionContext", { 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} @@ -222,28 +231,34 @@ var CompletionContext = Class("CompletionContext", { * @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) { @@ -253,7 +268,7 @@ var CompletionContext = Class("CompletionContext", { }, // 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); @@ -295,10 +310,12 @@ var CompletionContext = Class("CompletionContext", { 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 @@ -338,11 +355,16 @@ var CompletionContext = Class("CompletionContext", { * 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)) @@ -350,7 +372,7 @@ var CompletionContext = Class("CompletionContext", { // 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)); @@ -405,7 +427,7 @@ var CompletionContext = Class("CompletionContext", { this.noUpdate = false; }, - ignoreCase: Class.memoize(function () { + ignoreCase: Class.Memoize(function () { let mode = this.wildcase; if (mode == "match") return false; @@ -469,7 +491,7 @@ var CompletionContext = Class("CompletionContext", { 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 @@ -542,10 +564,11 @@ var CompletionContext = Class("CompletionContext", { // 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) @@ -589,7 +612,7 @@ var CompletionContext = Class("CompletionContext", { } this.offset += count; if (this._filter) - this._filter = this._filter.substr(advance); + this._filter = this._filter.substr(arguments[0] || 0); }, /** @@ -624,15 +647,38 @@ var CompletionContext = Class("CompletionContext", { 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( +
+
  • {this.text} 
  • +
  • {e} 
  • +
    , 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)]; }, /** @@ -823,7 +869,7 @@ var CompletionContext = Class("CompletionContext", { 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]); @@ -850,6 +896,7 @@ var Completion = Module("completion", { Local: function (dactyl, modules, window) ({ urlCompleters: {}, + get modules() modules, get options() modules.options, // FIXME @@ -878,7 +925,7 @@ var Completion = Module("completion", { 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) @@ -920,9 +967,11 @@ var Completion = Module("completion", { 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)]; }; }); @@ -931,7 +980,7 @@ var Completion = Module("completion", { // 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); }, @@ -942,6 +991,64 @@ var Completion = Module("completion", { 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; @@ -963,11 +1070,13 @@ var Completion = Module("completion", { 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( @@ -1054,8 +1163,32 @@ var Completion = Module("completion", { 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", @@ -1085,6 +1218,6 @@ var Completion = Module("completion", { 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: