]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/modules/completion.jsm
Import 1.0rc1 supporting Firefox up to 11.*
[dactyl.git] / common / modules / completion.jsm
index 9671e95aad543f8eadf3a5f6acaaf8498bef26c0..ff9c0917dade8a5403118350df32baa0b1485682 100644 (file)
@@ -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(
+                        <div highlight="CompItem" style="white-space: nowrap">
+                            <li highlight="CompResult">{this.text}&#xa0;</li>
+                            <li highlight="CompDesc ErrorMsg">{e}&#xa0;</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)];
     },
 
     /**
@@ -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: