diff --git a/common/content/autocommands.js b/common/content/autocommands.js index 18141a1e..52ceecb6 100644 --- a/common/content/autocommands.js +++ b/common/content/autocommands.js @@ -250,7 +250,7 @@ const AutoCommands = Module("autocommands", { }); }, completion: function () { - completion.setFunctionCompleter(autocommands.get, [function () config.autocommands]); + JavaScript.setCompleter(this.get, [function () config.autocommands]); completion.autocmdEvent = function autocmdEvent(context) { context.completions = config.autocommands; diff --git a/common/content/commands.js b/common/content/commands.js index a8eaa669..2baa56f0 100644 --- a/common/content/commands.js +++ b/common/content/commands.js @@ -872,7 +872,7 @@ const Commands = Module("commands", { }, completion: function () { - completion.setFunctionCompleter(commands.get, [function () ([c.name, c.description] for (c in commands))]); + JavaScript.setCompleter(this.get, [function () ([c.name, c.description] for (c in commands))]); completion.command = function command(context) { context.title = ["Command"]; diff --git a/common/content/completion.js b/common/content/completion.js index 8c9d1260..7b18b287 100644 --- a/common/content/completion.js +++ b/common/content/completion.js @@ -627,20 +627,9 @@ const CompletionContext = Class("CompletionContext", { */ const Completion = Module("completion", { init: function () { - this.javascriptCompleter = Completion.Javascript(); }, - setFunctionCompleter: function setFunctionCompleter(funcs, completers) { - funcs = Array.concat(funcs); - for (let [, func] in Iterator(funcs)) { - func.liberatorCompleter = function liberatorCompleter(context, func, obj, args) { - let completer = completers[args.length - 1]; - if (!completer) - return []; - return completer.call(this, context, obj, args); - }; - } - }, + get setFunctionCompleter() JavaScript.setCompleter, // Backward compatibility // FIXME _runCompleter: function _runCompleter(name, filter, maxItems) { @@ -677,8 +666,6 @@ const Completion = Module("completion", { ////////////////////// COMPLETION TYPES //////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ - javascript: function (context) this.javascriptCompleter.complete(context), - // filter a list of urls // // may consist of search engines, filenames, bookmarks and history, @@ -754,556 +741,6 @@ const Completion = Module("completion", { //}}} }, { UrlCompleter: Struct("name", "description", "completer"), - - Javascript: Class("Javascript", { - init: function () { - const OFFSET = 0, CHAR = 1, STATEMENTS = 2, DOTS = 3, FULL_STATEMENTS = 4, COMMA = 5, FUNCTIONS = 6; - let stack = []; - let functions = []; - let top = []; // The element on the top of the stack. - let last = ""; // The last opening char pushed onto the stack. - let lastNonwhite = ""; // Last non-whitespace character we saw. - let lastChar = ""; // Last character we saw, used for \ escaping quotes. - let compl = []; - let str = ""; - - let lastIdx = 0; - - let cacheKey = null; - - this.completers = {}; - - // Some object members are only accessible as function calls - function getKey(obj, key) { - try { - return obj[key]; - } - catch (e) { - return undefined; - } - } - - this.iter = function iter(obj, toplevel) { - toplevel = !!toplevel; - let seen = {}; - let ret = {}; - - try { - let orig = obj; - let top = services.get("debugger").wrapValue(obj); - - if (!toplevel) - obj = obj.__proto__; - - for (; obj; obj = !toplevel && obj.__proto__) { - services.get("debugger").wrapValue(obj).getProperties(ret, {}); - for (let prop in values(ret.value)) { - let name = '|' + prop.name.stringValue; - if (name in seen) - continue; - seen[name] = 1; - yield [prop.name.stringValue, top.getProperty(prop.name.stringValue).value.getWrappedValue()] - } - } - // The debugger doesn't list some properties. I can't guess why. - for (let k in orig) - if (k in orig && !('|' + k in seen) && obj.hasOwnProperty(k) == toplevel) - yield [k, getKey(orig, k)] - } - catch(e) { - for (k in allkeys(obj)) - if (obj.hasOwnProperty(k) == toplevel) - yield [k, getKey(obj, k)]; - } - }; - - // Search the object for strings starting with @key. - // If @last is defined, key is a quoted string, it's - // wrapped in @last after @offset characters are sliced - // off of it and it's quoted. - this.objectKeys = function objectKeys(obj, toplevel) { - // Things we can dereference - if (["object", "string", "function"].indexOf(typeof obj) == -1) - return []; - if (!obj) - return []; - - if (modules.isPrototypeOf(obj)) - compl = [v for (v in Iterator(obj))]; - else { - compl = [k for (k in this.iter(obj, toplevel))]; - if (!toplevel) - compl = util.Array.uniq(compl, true); - } - - // Add keys for sorting later. - // Numbers are parsed to ints. - // Constants, which should be unsorted, are found and marked null. - compl.forEach(function (item) { - let key = item[0]; - if (!isNaN(key)) - key = parseInt(key); - else if (/^[A-Z_][A-Z0-9_]*$/.test(key)) - key = ""; - item.key = key; - }); - - return compl; - }; - - this.eval = function eval(arg, key, tmp) { - let cache = this.context.cache.eval; - let context = this.context.cache.evalContext; - - if (!key) - key = arg; - if (key in cache) - return cache[key]; - - context[Completion.Javascript.EVAL_TMP] = tmp; - try { - return cache[key] = liberator.eval(arg, context); - } - catch (e) { - return null; - } - finally { - delete context[Completion.Javascript.EVAL_TMP]; - } - }; - - // Get an element from the stack. If @n is negative, - // count from the top of the stack, otherwise, the bottom. - // If @m is provided, return the @mth value of element @o - // of the stack entry at @n. - let get = function get(n, m, o) { - let a = stack[n >= 0 ? n : stack.length + n]; - if (o != null) - a = a[o]; - if (m == null) - return a; - return a[a.length - m - 1]; - }; - - function buildStack(filter) { - let self = this; - // Push and pop the stack, maintaining references to 'top' and 'last'. - let push = function push(arg) { - top = [i, arg, [i], [], [], [], []]; - last = top[CHAR]; - stack.push(top); - }; - let pop = function pop(arg) { - if (top[CHAR] != arg) { - self.context.highlight(top[OFFSET], i - top[OFFSET], "SPELLCHECK"); - self.context.highlight(top[OFFSET], 1, "FIND"); - throw new Error("Invalid JS"); - } - if (i == self.context.caret - 1) - self.context.highlight(top[OFFSET], 1, "FIND"); - // The closing character of this stack frame will have pushed a new - // statement, leaving us with an empty statement. This doesn't matter, - // now, as we simply throw away the frame when we pop it, but it may later. - if (top[STATEMENTS][top[STATEMENTS].length - 1] == i) - top[STATEMENTS].pop(); - top = get(-2); - last = top[CHAR]; - let ret = stack.pop(); - return ret; - }; - - let i = 0, c = ""; // Current index and character, respectively. - - // Reuse the old stack. - if (str && filter.substr(0, str.length) == str) { - i = str.length; - if (this.popStatement) - top[STATEMENTS].pop(); - } - else { - stack = []; - functions = []; - push("#root"); - } - - // Build a parse stack, discarding entries as opening characters - // match closing characters. The stack is walked from the top entry - // and down as many levels as it takes us to figure out what it is - // that we're completing. - str = filter; - let length = str.length; - for (; i < length; lastChar = c, i++) { - c = str[i]; - if (last == '"' || last == "'" || last == "/") { - if (lastChar == "\\") { // Escape. Skip the next char, whatever it may be. - c = ""; - i++; - } - else if (c == last) - pop(c); - } - else { - // A word character following a non-word character, or simply a non-word - // character. Start a new statement. - if (/[a-zA-Z_$]/.test(c) && !/[\w$]/.test(lastChar) || !/[\w\s$]/.test(c)) - top[STATEMENTS].push(i); - - // A "." or a "[" dereferences the last "statement" and effectively - // joins it to this logical statement. - if ((c == "." || c == "[") && /[\w$\])"']/.test(lastNonwhite) - || lastNonwhite == "." && /[a-zA-Z_$]/.test(c)) - top[STATEMENTS].pop(); - - switch (c) { - case "(": - // Function call, or if/while/for/... - if (/[\w$]/.test(lastNonwhite)) { - functions.push(i); - top[FUNCTIONS].push(i); - top[STATEMENTS].pop(); - } - case '"': - case "'": - case "/": - case "{": - push(c); - break; - case "[": - push(c); - break; - case ".": - top[DOTS].push(i); - break; - case ")": pop("("); break; - case "]": pop("["); break; - case "}": pop("{"); // Fallthrough - case ";": - top[FULL_STATEMENTS].push(i); - break; - case ",": - top[COMMA].push(i); - break; - } - - if (/\S/.test(c)) - lastNonwhite = c; - } - } - - this.popStatement = false; - if (!/[\w$]/.test(lastChar) && lastNonwhite != ".") { - this.popStatement = true; - top[STATEMENTS].push(i); - } - - lastIdx = i; - } - - this.complete = function _complete(context) { - this.context = context; - - let self = this; - try { - buildStack.call(this, context.filter); - } - catch (e) { - if (e.message != "Invalid JS") - liberator.reportError(e); - lastIdx = 0; - return null; - } - - let cache = this.context.cache; - this.context.getCache("eval", Object); - this.context.getCache("evalContext", function () ({ __proto__: userContext })); - - // Okay, have parse stack. Figure out what we're completing. - - // Find any complete statements that we can eval before we eval our object. - // This allows for things like: let doc = window.content.document; let elem = doc.createElement...; elem. - let prev = 0; - for (let [, v] in Iterator(get(0)[FULL_STATEMENTS])) { - let key = str.substring(prev, v + 1); - if (checkFunction(prev, v, key)) - return null; - this.eval(key); - prev = v + 1; - } - - // Don't eval any function calls unless the user presses tab. - function checkFunction(start, end, key) { - let res = functions.some(function (idx) idx >= start && idx < end); - if (!res || self.context.tabPressed || key in cache.eval) - return false; - self.context.waitingForTab = true; - return true; - } - - // For each DOT in a statement, prefix it with TMP, eval it, - // and save the result back to TMP. The point of this is to - // cache the entire path through an object chain, mainly in - // the presence of function calls. There are drawbacks. For - // instance, if the value of a variable changes in the course - // of inputting a command (let foo=bar; frob(foo); foo=foo.bar; ...), - // we'll still use the old value. But, it's worth it. - function getObj(frame, stop) { - let statement = get(frame, 0, STATEMENTS) || 0; // Current statement. - let prev = statement; - let obj; - let cacheKey; - for (let [i, dot] in Iterator(get(frame)[DOTS].concat(stop))) { - if (dot < statement) - continue; - if (dot > stop || dot <= prev) - break; - let s = str.substring(prev, dot); - - if (prev != statement) - s = Completion.Javascript.EVAL_TMP + "." + s; - cacheKey = str.substring(statement, dot); - - if (checkFunction(prev, dot, cacheKey)) - return []; - - prev = dot + 1; - obj = self.eval(s, cacheKey, obj); - } - return [[obj, cacheKey]]; - } - - function getObjKey(frame) { - let dot = get(frame, 0, DOTS) || -1; // Last dot in frame. - let statement = get(frame, 0, STATEMENTS) || 0; // Current statement. - let end = (frame == -1 ? lastIdx : get(frame + 1)[OFFSET]); - - cacheKey = null; - let obj = [[cache.evalContext, "Local Variables"], - [userContext, "Global Variables"], - [modules, "modules"], - [window, "window"]]; // Default objects; - // Is this an object dereference? - if (dot < statement) // No. - dot = statement - 1; - else // Yes. Set the object to the string before the dot. - obj = getObj(frame, dot); - - let [, space, key] = str.substring(dot + 1, end).match(/^(\s*)(.*)/); - return [dot + 1 + space.length, obj, key]; - } - - function fill(context, obj, name, compl, anchored, key, last, offset) { - context.title = [name]; - context.anchored = anchored; - context.filter = key; - context.itemCache = context.parent.itemCache; - context.key = name; - - if (last != null) - context.quote = [last, function (text) util.escapeString(text.substr(offset), ""), last]; - else // We're not looking for a quoted string, so filter out anything that's not a valid identifier - context.filters.push(function (item) /^[a-zA-Z_$][\w$]*$/.test(item.text)); - - compl.call(self, context, obj); - } - - function complete(objects, key, compl, string, last) { - let orig = compl; - if (!compl) { - compl = function (context, obj, recurse) { - context.process = [null, function highlight(item, v) template.highlight(v, true)]; - // Sort in a logical fashion for object keys: - // Numbers are sorted as numbers, rather than strings, and appear first. - // Constants are unsorted, and appear before other non-null strings. - // Other strings are sorted in the default manner. - let compare = context.compare; - function isnan(item) item != '' && isNaN(item); - context.compare = function (a, b) { - if (!isnan(a.item.key) && !isnan(b.item.key)) - return a.item.key - b.item.key; - return isnan(b.item.key) - isnan(a.item.key) || compare(a, b); - }; - if (!context.anchored) // We've already listed anchored matches, so don't list them again here. - context.filters.push(function (item) util.compareIgnoreCase(item.text.substr(0, this.filter.length), this.filter)); - if (obj == cache.evalContext) - context.regenerate = true; - context.generate = function () self.objectKeys(obj, !recurse); - }; - } - // TODO: Make this a generic completion helper function. - let filter = key + (string || ""); - for (let [, obj] in Iterator(objects)) { - this.context.fork(obj[1], top[OFFSET], this, fill, - obj[0], obj[1], compl, - true, filter, last, key.length); - } - - if (orig) - return; - - for (let [, obj] in Iterator(objects)) { - let name = obj[1] + " (prototypes)"; - this.context.fork(name, top[OFFSET], this, fill, - obj[0], name, function (a, b) compl(a, b, true), - true, filter, last, key.length); - } - - for (let [, obj] in Iterator(objects)) { - let name = obj[1] + " (substrings)"; - this.context.fork(name, top[OFFSET], this, fill, - obj[0], name, compl, - false, filter, last, key.length); - } - - for (let [, obj] in Iterator(objects)) { - let name = obj[1] + " (prototype substrings)"; - this.context.fork(name, top[OFFSET], this, fill, - obj[0], name, function (a, b) compl(a, b, true), - false, filter, last, key.length); - } - } - - // In a string. Check if we're dereferencing an object. - // Otherwise, do nothing. - if (last == "'" || last == '"') { - // - // str = "foo[bar + 'baz" - // obj = "foo" - // key = "bar + ''" - // - - // The top of the stack is the sting we're completing. - // Wrap it in its delimiters and eval it to process escape sequences. - let string = str.substring(get(-1)[OFFSET] + 1, lastIdx); - string = eval(last + string + last); - - function getKey() { - if (last == "") - return ""; - // After the opening [ upto the opening ", plus '' to take care of any operators before it - let key = str.substring(get(-2, 0, STATEMENTS), get(-1, null, OFFSET)) + "''"; - // Now eval the key, to process any referenced variables. - return this.eval(key); - } - - // Is this an object accessor? - if (get(-2)[CHAR] == "[") { // Are we inside of []? - // Stack: - // [-1]: "... - // [-2]: [... - // [-3]: base statement - - // Yes. If the [ starts at the beginning of a logical - // statement, we're in an array literal, and we're done. - if (get(-3, 0, STATEMENTS) == get(-2)[OFFSET]) - return null; - - // Beginning of the statement upto the opening [ - let obj = getObj(-3, get(-2)[OFFSET]); - - return void complete.call(this, obj, getKey(), null, string, last); - } - - // Is this a function call? - if (get(-2)[CHAR] == "(") { - // Stack: - // [-1]: "... - // [-2]: (... - // [-3]: base statement - - // Does the opening "(" mark a function call? - if (get(-3, 0, FUNCTIONS) != get(-2)[OFFSET]) - return null; // No. We're done. - - let [offset, obj, func] = getObjKey(-3); - if (!obj.length) - return null; - obj = obj.slice(0, 1); - - try { - var completer = obj[0][0][func].liberatorCompleter; - } - catch (e) {} - if (!completer) - completer = this.completers[func]; - if (!completer) - return null; - - // Split up the arguments - let prev = get(-2)[OFFSET]; - let args = []; - for (let [i, idx] in Iterator(get(-2)[COMMA])) { - let arg = str.substring(prev + 1, idx); - prev = idx; - util.memoize(args, i, function () self.eval(arg)); - } - let key = getKey(); - args.push(key + string); - - compl = function (context, obj) { - let res = completer.call(self, context, func, obj, args); - if (res) - context.completions = res; - }; - - obj[0][1] += "." + func + "(... [" + args.length + "]"; - return void complete.call(this, obj, key, compl, string, last); - } - - // In a string that's not an obj key or a function arg. - // Nothing to do. - return null; - } - - // - // str = "foo.bar.baz" - // obj = "foo.bar" - // key = "baz" - // - // str = "foo" - // obj = [modules, window] - // key = "foo" - // - - let [offset, obj, key] = getObjKey(-1); - - // Wait for a keypress before completing the default objects. - if (!this.context.tabPressed && key == "" && obj.length > 1) { - this.context.waitingForTab = true; - this.context.message = "Waiting for key press"; - return null; - } - - if (!/^(?:[a-zA-Z_$][\w$]*)?$/.test(key)) - return null; // Not a word. Forget it. Can this even happen? - - try { // FIXME - var o = top[OFFSET]; - top[OFFSET] = offset; - return void complete.call(this, obj, key); - } - finally { - top[OFFSET] = o; - } - return null; - } - } - }, { - EVAL_TMP: "__liberator_eval_tmp" - }) -}, { - options: function () { - options.add(["jsdebugger", "jsd"], - "Switch on/off jsdebugger", - "boolean", false, { - setter: function(value) { - if (value) - services.get("debugger").on(); - else - services.get("debugger").off(); - }, - getter: function () services.get("debugger").isOn - }); - }, }); // vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/io.js b/common/content/io.js index 8e79ada6..3e5bd21a 100644 --- a/common/content/io.js +++ b/common/content/io.js @@ -975,7 +975,7 @@ lookup: }); }, completion: function () { - completion.setFunctionCompleter([this.File, File.expandPath], + JavaScript.setCompleter([this.File, File.expandPath], [function (context, obj, args) { context.quote[2] = ""; completion.file(context, true); diff --git a/common/content/javascript.js b/common/content/javascript.js new file mode 100644 index 00000000..4a18e0fe --- /dev/null +++ b/common/content/javascript.js @@ -0,0 +1,613 @@ +// Copyright (c) 2008-2009 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. + +// TODO: Clean this up. + +const JavaScript = Module("javascript", { + init: function () { + this._stack = []; + this._functions = []; + this._top = []; // The element on the top of the stack. + this._last = ""; // The last opening char pushed onto the stack. + this._lastNonwhite = ""; // Last non-whitespace character we saw. + this._lastChar = ""; // Last character we saw, used for \ escaping quotes. + this._str = ""; + + this._lastIdx = 0; + + this._cacheKey = null; + }, + + get completers() JavaScript.completers, // For backward compatibility + + // Some object members are only accessible as function calls + getKey: function (obj, key) { + try { + return obj[key]; + } + catch (e) { + return undefined; + } + }, + + iter: function iter(obj, toplevel) { + toplevel = !!toplevel; + let seen = {}; + let ret = {}; + + try { + let orig = obj; + let top = services.get("debugger").wrapValue(obj); + + if (!toplevel) + obj = obj.__proto__; + + for (; obj; obj = !toplevel && obj.__proto__) { + services.get("debugger").wrapValue(obj).getProperties(ret, {}); + for (let prop in values(ret.value)) { + let name = '|' + prop.name.stringValue; + if (name in seen) + continue; + seen[name] = 1; + yield [prop.name.stringValue, top.getProperty(prop.name.stringValue).value.getWrappedValue()] + } + } + // The debugger doesn't list some properties. I can't guess why. + for (let k in orig) + if (k in orig && !('|' + k in seen) && obj.hasOwnProperty(k) == toplevel) + yield [k, this.getKey(orig, k)] + } + catch(e) { + for (k in allkeys(obj)) + if (obj.hasOwnProperty(k) == toplevel) + yield [k, this.getKey(obj, k)]; + } + }, + + // Search the object for strings starting with @key. + // If @last is defined, key is a quoted string, it's + // wrapped in @last after @offset characters are sliced + // off of it and it's quoted. + objectKeys: function objectKeys(obj, toplevel) { + // Things we can dereference + if (["object", "string", "function"].indexOf(typeof obj) == -1) + return []; + if (!obj) + return []; + + let completions; + if (modules.isPrototypeOf(obj)) + completions = [v for (v in Iterator(obj))]; + else { + completions = [k for (k in this.iter(obj, toplevel))]; + if (!toplevel) + completions = util.Array.uniq(completions, true); + } + + // Add keys for sorting later. + // Numbers are parsed to ints. + // Constants, which should be unsorted, are found and marked null. + completions.forEach(function (item) { + let key = item[0]; + if (!isNaN(key)) + key = parseInt(key); + else if (/^[A-Z_][A-Z0-9_]*$/.test(key)) + key = ""; + item.key = key; + }); + + return completions; + }, + + eval: function eval(arg, key, tmp) { + let cache = this.context.cache.eval; + let context = this.context.cache.evalContext; + + if (!key) + key = arg; + if (key in cache) + return cache[key]; + + context[JavaScript.EVAL_TMP] = tmp; + try { + return cache[key] = liberator.eval(arg, context); + } + catch (e) { + return null; + } + finally { + delete context[JavaScript.EVAL_TMP]; + } + }, + + // Get an element from the stack. If @n is negative, + // count from the top of the stack, otherwise, the bottom. + // If @m is provided, return the @mth value of element @o + // of the stack entry at @n. + _get: function (n, m, o) { + let a = this._stack[n >= 0 ? n : this._stack.length + n]; + if (o != null) + a = a[o]; + if (m == null) + return a; + return a[a.length - m - 1]; + }, + + // Push and pop the stack, maintaining references to 'top' and 'last'. + _push: function push(arg) { + this._top = [this._i, arg, [this._i], [], [], [], []]; + this._last = this._top[JavaScript.CHAR]; + this._stack.push(this._top); + }, + + _pop: function pop(arg) { + if (this._top[JavaScript.CHAR] != arg) { + this.context.highlight(this._top[JavaScript.OFFSET], this._i - this._top[JavaScript.OFFSET], "SPELLCHECK"); + this.context.highlight(this._top[JavaScript.OFFSET], 1, "FIND"); + throw new Error("Invalid JS"); + } + if (this._i == this.context.caret - 1) + this.context.highlight(this._top[JavaScript.OFFSET], 1, "FIND"); + // The closing character of this stack frame will have pushed a new + // statement, leaving us with an empty statement. This doesn't matter, + // now, as we simply throw away the frame when we pop it, but it may later. + if (this._top[JavaScript.STATEMENTS][this._top[JavaScript.STATEMENTS].length - 1] == this._i) + this._top[JavaScript.STATEMENTS].pop(); + this._top = this._get(-2); + this._last = this._top[JavaScript.CHAR]; + let ret = this._stack.pop(); + return ret; + }, + + _buildStack: function (filter) { + let self = this; + + // Todo: Fix these one-letter variable names. + this._i = 0; + this._c = ""; // Current index and character, respectively. + + // Reuse the old stack. + if (this._str && filter.substr(0, this._str.length) == this._str) { + this._i = this._str.length; + if (this.popStatement) + this._top[JavaScript.STATEMENTS].pop(); + } + else { + this._stack = []; + this._functions = []; + this._push("#root"); + } + + // Build a parse stack, discarding entries as opening characters + // match closing characters. The stack is walked from the top entry + // and down as many levels as it takes us to figure out what it is + // that we're completing. + this._str = filter; + let length = this._str.length; + for (; this._i < length; this._lastChar = this._c, this._i++) { + this._c = this._str[this._i]; + if (this._last == '"' || this._last == "'" || this._last == "/") { + if (this._lastChar == "\\") { // Escape. Skip the next char, whatever it may be. + this._c = ""; + this._i++; + } + else if (this._c == this._last) + this._pop(this._c); + } + else { + // A word character following a non-word character, or simply a non-word + // character. Start a new statement. + if (/[a-zA-Z_$]/.test(this._c) && !/[\w$]/.test(this._lastChar) || !/[\w\s$]/.test(this._c)) + this._top[JavaScript.STATEMENTS].push(this._i); + + // A "." or a "[" dereferences the last "statement" and effectively + // joins it to this logical statement. + if ((this._c == "." || this._c == "[") && /[\w$\])"']/.test(this._lastNonwhite) + || this._lastNonwhite == "." && /[a-zA-Z_$]/.test(this._c)) + this._top[JavaScript.STATEMENTS].pop(); + + switch (this._c) { + case "(": + // Function call, or if/while/for/... + if (/[\w$]/.test(this._lastNonwhite)) { + this._functions.push(this._i); + this._top[JavaScript.FUNCTIONS].push(this._i); + this._top[JavaScript.STATEMENTS].pop(); + } + case '"': + case "'": + case "/": + case "{": + this._push(this._c); + break; + case "[": + this._push(this._c); + break; + case ".": + this._top[JavaScript.DOTS].push(this._i); + break; + case ")": this._pop("("); break; + case "]": this._pop("["); break; + case "}": this._pop("{"); // Fallthrough + case ";": + this._top[JavaScript.FULL_STATEMENTS].push(this._i); + break; + case ",": + this._top[JavaScript.COMMA].push(this._i); + break; + } + + if (/\S/.test(this._c)) + this._lastNonwhite = this._c; + } + } + + this.popStatement = false; + if (!/[\w$]/.test(this._lastChar) && this._lastNonwhite != ".") { + this.popStatement = true; + this._top[JavaScript.STATEMENTS].push(this._i); + } + + this._lastIdx = this._i; + }, + + // Don't eval any function calls unless the user presses tab. + _checkFunction: function (start, end, key) { + let res = this._functions.some(function (idx) idx >= start && idx < end); + if (!res || this.context.tabPressed || key in this.cache.eval) + return false; + this.context.waitingForTab = true; + return true; + }, + + // For each DOT in a statement, prefix it with TMP, eval it, + // and save the result back to TMP. The point of this is to + // cache the entire path through an object chain, mainly in + // the presence of function calls. There are drawbacks. For + // instance, if the value of a variable changes in the course + // of inputting a command (let foo=bar; frob(foo); foo=foo.bar; ...), + // we'll still use the old value. But, it's worth it. + _getObj: function (frame, stop) { + let statement = this._get(frame, 0, JavaScript.STATEMENTS) || 0; // Current statement. + let prev = statement; + let obj; + let cacheKey; + for (let [, dot] in Iterator(this._get(frame)[JavaScript.DOTS].concat(stop))) { + if (dot < statement) + continue; + if (dot > stop || dot <= prev) + break; + let s = this._str.substring(prev, dot); + + if (prev != statement) + s = JavaScript.EVAL_TMP + "." + s; + cacheKey = this._str.substring(statement, dot); + + if (this._checkFunction(prev, dot, cacheKey)) + return []; + + prev = dot + 1; + obj = this.eval(s, cacheKey, obj); + } + return [[obj, cacheKey]]; + }, + + _getObjKey: function (frame) { + let dot = this._get(frame, 0, JavaScript.DOTS) || -1; // Last dot in frame. + let statement = this._get(frame, 0, JavaScript.STATEMENTS) || 0; // Current statement. + let end = (frame == -1 ? this._lastIdx : this._get(frame + 1)[JavaScript.OFFSET]); + + this._cacheKey = null; + let obj = [[this.cache.evalContext, "Local Variables"], + [userContext, "Global Variables"], + [modules, "modules"], + [window, "window"]]; // Default objects; + // Is this an object dereference? + if (dot < statement) // No. + dot = statement - 1; + else // Yes. Set the object to the string before the dot. + obj = this._getObj(frame, dot); + + let [, space, key] = this._str.substring(dot + 1, end).match(/^(\s*)(.*)/); + return [dot + 1 + space.length, obj, key]; + }, + + _fill: function (context, obj, name, compl, anchored, key, last, offset) { + context.title = [name]; + context.anchored = anchored; + context.filter = key; + context.itemCache = context.parent.itemCache; + context.key = name; + + if (last != null) + context.quote = [last, function (text) util.escapeString(text.substr(offset), ""), last]; + else // We're not looking for a quoted string, so filter out anything that's not a valid identifier + context.filters.push(function (item) /^[a-zA-Z_$][\w$]*$/.test(item.text)); + + compl.call(self, context, obj); + }, + + _complete: function (objects, key, compl, string, last) { + const self = this; + let orig = compl; + if (!compl) { + compl = function (context, obj, recurse) { + context.process = [null, function highlight(item, v) template.highlight(v, true)]; + // Sort in a logical fashion for object keys: + // Numbers are sorted as numbers, rather than strings, and appear first. + // Constants are unsorted, and appear before other non-null strings. + // Other strings are sorted in the default manner. + let compare = context.compare; + function isnan(item) item != '' && isNaN(item); + context.compare = function (a, b) { + if (!isnan(a.item.key) && !isnan(b.item.key)) + return a.item.key - b.item.key; + return isnan(b.item.key) - isnan(a.item.key) || compare(a, b); + }; + if (!context.anchored) // We've already listed anchored matches, so don't list them again here. + context.filters.push(function (item) util.compareIgnoreCase(item.text.substr(0, this.filter.length), this.filter)); + if (obj == self.cache.evalContext) + context.regenerate = true; + context.generate = function () self.objectKeys(obj, !recurse); + }; + } + // TODO: Make this a generic completion helper function. + let filter = key + (string || ""); + for (let [, obj] in Iterator(objects)) { + this.context.fork(obj[1], this._top[JavaScript.OFFSET], this, this._fill, + obj[0], obj[1], compl, + true, filter, last, key.length); + } + + if (orig) + return; + + for (let [, obj] in Iterator(objects)) { + let name = obj[1] + " (prototypes)"; + this.context.fork(name, this._top[JavaScript.OFFSET], this, this._fill, + obj[0], name, function (a, b) compl(a, b, true), + true, filter, last, key.length); + } + + for (let [, obj] in Iterator(objects)) { + let name = obj[1] + " (substrings)"; + this.context.fork(name, this._top[JavaScript.OFFSET], this, this._fill, + obj[0], name, compl, + false, filter, last, key.length); + } + + for (let [, obj] in Iterator(objects)) { + let name = obj[1] + " (prototype substrings)"; + this.context.fork(name, this._top[JavaScript.OFFSET], this, this._fill, + obj[0], name, function (a, b) compl(a, b, true), + false, filter, last, key.length); + } + }, + + _getKey: function () { + if (this._last == "") + return ""; + // After the opening [ upto the opening ", plus '' to take care of any operators before it + let key = this._str.substring(this._get(-2, 0, JavaScript.STATEMENTS), this._get(-1, null, JavaScript.OFFSET)) + "''"; + // Now eval the key, to process any referenced variables. + return this.eval(key); + }, + + get cache() this.context.cache, + + complete: function _complete(context) { + const self = this; + this.context = context; + + try { + this._buildStack.call(this, context.filter); + } + catch (e) { + if (e.message != "Invalid JS") + liberator.reportError(e); + this._lastIdx = 0; + return null; + } + + this.context.getCache("eval", Object); + this.context.getCache("evalContext", function () ({ __proto__: userContext })); + + // Okay, have parse stack. Figure out what we're completing. + + // Find any complete statements that we can eval before we eval our object. + // This allows for things like: let doc = window.content.document; let elem = doc.createElement...; elem. + let prev = 0; + for (let [, v] in Iterator(this._get(0)[JavaScript.FULL_STATEMENTS])) { + let key = this._str.substring(prev, v + 1); + if (this._checkFunction(prev, v, key)) + return null; + this.eval(key); + prev = v + 1; + } + + // In a string. Check if we're dereferencing an object. + // Otherwise, do nothing. + if (this._last == "'" || this._last == '"') { + // + // str = "foo[bar + 'baz" + // obj = "foo" + // key = "bar + ''" + // + + // The top of the stack is the sting we're completing. + // Wrap it in its delimiters and eval it to process escape sequences. + let string = this._str.substring(this._get(-1)[JavaScript.OFFSET] + 1, this._lastIdx); + string = eval(this._last + string + this._last); + + // Is this an object accessor? + if (this._get(-2)[JavaScript.CHAR] == "[") { // Are we inside of []? + // Stack: + // [-1]: "... + // [-2]: [... + // [-3]: base statement + + // Yes. If the [ starts at the beginning of a logical + // statement, we're in an array literal, and we're done. + if (this._get(-3, 0, JavaScript.STATEMENTS) == this._get(-2)[JavaScript.OFFSET]) + return null; + + // Beginning of the statement upto the opening [ + let obj = this._getObj(-3, this._get(-2)[JavaScript.OFFSET]); + + return this._complete(obj, this._getKey(), null, string, this._last); + } + + // Is this a function call? + if (this._get(-2)[JavaScript.CHAR] == "(") { + // Stack: + // [-1]: "... + // [-2]: (... + // [-3]: base statement + + // Does the opening "(" mark a function call? + if (this._get(-3, 0, JavaScript.FUNCTIONS) != this._get(-2)[JavaScript.OFFSET]) + return null; // No. We're done. + + let [offset, obj, func] = this._getObjKey(-3); + if (!obj.length) + return null; + obj = obj.slice(0, 1); + + try { + var completer = obj[0][0][func].liberatorCompleter; + } + catch (e) {} + if (!completer) + completer = JavaScript.completers[func]; + if (!completer) + return null; + + // Split up the arguments + let prev = this._get(-2)[JavaScript.OFFSET]; + let args = []; + for (let [, idx] in Iterator(this._get(-2)[JavaScript.COMMA])) { + let arg = this._str.substring(prev + 1, idx); + prev = idx; + util.memoize(args, this._i, function () self.eval(arg)); + } + let key = this._getKey(); + args.push(key + string); + + compl = function (context, obj) { + let res = completer.call(self, context, func, obj, args); + if (res) + context.completions = res; + }; + + obj[0][1] += "." + func + "(... [" + args.length + "]"; + return this._complete(obj, key, compl, string, this._last); + } + + // In a string that's not an obj key or a function arg. + // Nothing to do. + return null; + } + + // + // str = "foo.bar.baz" + // obj = "foo.bar" + // key = "baz" + // + // str = "foo" + // obj = [modules, window] + // key = "foo" + // + + let [offset, obj, key] = this._getObjKey(-1); + + // Wait for a keypress before completing the default objects. + if (!this.context.tabPressed && key == "" && obj.length > 1) { + this.context.waitingForTab = true; + this.context.message = "Waiting for key press"; + return null; + } + + if (!/^(?:[a-zA-Z_$][\w$]*)?$/.test(key)) + return null; // Not a word. Forget it. Can this even happen? + + try { // FIXME + var o = this._top[JavaScript.OFFSET]; + this._top[JavaScript.OFFSET] = offset; + return this._complete(obj, key); + } + finally { + this._top[JavaScript.OFFSET] = o; + } + return null; + } +}, { + EVAL_TMP: "__liberator_eval_tmp", + + // Internal use only + OFFSET: 0, + CHAR: 1, + STATEMENTS: 2, + DOTS: 3, + FULL_STATEMENTS: 4, + COMMA: 5, + FUNCTIONS: 6, + + /** + * A map of argument completion functions for named methods. The + * signature and specification of the completion function + * are fairly complex and yet undocumented. + * + * @see JavaScript.setCompleter + */ + completers: {}, + + /** + * Installs argument string completers for a set of functions. + * The second argument is an array of functions (or null + * values), each corresponding the argument of the same index. + * Each provided completion function receives as arguments a + * CompletionContext, the 'this' object of the method, and an + * array of values for the preceding arguments. + * + * It is important to note that values in the arguments array + * provided to the completers are lazily evaluated the first + * time they are accessed, so they should be accessed + * judiciously. + * + * @param {function|function[]} funcs The functions for which to + * install the completers. + * @param {function[]} completers An array of completer + * functions. + */ + setCompleter: function (funcs, completers) { + funcs = Array.concat(funcs); + for (let [, func] in Iterator(funcs)) { + func.liberatorCompleter = function (context, func, obj, args) { + let completer = completers[args.length - 1]; + if (!completer) + return []; + return completer.call(this, context, obj, args); + }; + } + } +}, { + completion: function () { + completion.javascript = this.closure.complete; + completion.javascriptCompleter = JavaScript; // Backwards compatibility. + }, + options: function () { + options.add(["jsdebugger", "jsd"], + "Switch on/off jsdebugger", + "boolean", false, { + setter: function(value) { + if (value) + services.get("debugger").on(); + else + services.get("debugger").off(); + }, + getter: function () services.get("debugger").isOn + }); + }, +}) diff --git a/common/content/liberator-overlay.js b/common/content/liberator-overlay.js index e974b2e8..478ecd9e 100644 --- a/common/content/liberator-overlay.js +++ b/common/content/liberator-overlay.js @@ -45,6 +45,7 @@ "finder.js", "hints.js", "io.js", + "javascript.js", "mappings.js", "marks.js", "modes.js", diff --git a/common/content/liberator.js b/common/content/liberator.js index 4efc132d..5a600ae1 100644 --- a/common/content/liberator.js +++ b/common/content/liberator.js @@ -1702,9 +1702,6 @@ const Liberator = Module("liberator", { }, completion: function () { - completion.setFunctionCompleter(services.get, [function () services.services]); - completion.setFunctionCompleter(services.create, [function () [[c, ""] for (c in services.classes)]]); - completion.dialog = function dialog(context) { context.title = ["Dialog"]; context.completions = config.dialogs; diff --git a/common/content/mappings.js b/common/content/mappings.js index 91f502fe..e604a26a 100644 --- a/common/content/mappings.js +++ b/common/content/mappings.js @@ -470,18 +470,18 @@ const Mappings = Module("mappings", { [mode.disp.toLowerCase()]); }, completion: function () { - completion.setFunctionCompleter(mappings.get, - [ - null, - function (context, obj, args) { - let mode = args[0]; - return util.Array.flatten( - [ - [[name, map.description] for ([i, name] in Iterator(map.names))] - for ([i, map] in Iterator(mappings._user[mode].concat(mappings._main[mode]))) - ]); - } - ]); + JavaScript.setCompleter(this.get, + [ + null, + function (context, obj, args) { + let mode = args[0]; + return util.Array.flatten( + [ + [[name, map.description] for ([i, name] in Iterator(map.names))] + for ([i, map] in Iterator(mappings._user[mode].concat(mappings._main[mode]))) + ]); + } + ]); completion.userMapping = function userMapping(context, args, modes) { // FIXME: have we decided on a 'standard' way to handle this clash? --djk diff --git a/common/content/options.js b/common/content/options.js index 07e5c613..f0d985ff 100644 --- a/common/content/options.js +++ b/common/content/options.js @@ -1227,8 +1227,8 @@ const Options = Module("options", { }); }, completion: function () { - completion.setFunctionCompleter(options.get, [function () ([o.name, o.description] for (o in options))]); - completion.setFunctionCompleter([options.getPref, options.safeSetPref, options.setPref, options.resetPref, options.invertPref], + JavaScript.setCompleter(this.get, [function () ([o.name, o.description] for (o in options))]); + JavaScript.setCompleter([this.getPref, this.safeSetPref, this.setPref, this.resetPref, this.invertPref], [function () options.allPrefs().map(function (pref) [pref, ""])]); completion.option = function option(context, scope) { diff --git a/common/content/services.js b/common/content/services.js index 61d86959..20e73b48 100644 --- a/common/content/services.js +++ b/common/content/services.js @@ -105,6 +105,13 @@ const Services = Module("services", { * @param {string} name The class's cache key. */ create: function (name) this.classes[name]() +}, { +}, { + completion: function () { + JavaScript.setCompleter(this.get, [function () services.services]); + JavaScript.setCompleter(this.create, [function () [[c, ""] for (c in services.classes)]]); + + } }); // vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/style.js b/common/content/style.js index 286dbf36..39c1f136 100644 --- a/common/content/style.js +++ b/common/content/style.js @@ -696,7 +696,7 @@ Module("styles", { }); }, completion: function () { - completion.setFunctionCompleter(["get", "addSheet", "removeSheet", "findSheets"].map(function (m) styles[m]), + JavaScript.setCompleter(["get", "addSheet", "removeSheet", "findSheets"].map(function (m) styles[m]), [ // Prototype: (system, name, filter, css, index) null, function (context, obj, args) args[0] ? styles.systemNames : styles.userNames, diff --git a/common/content/util.js b/common/content/util.js index c2638cda..57640d7d 100644 --- a/common/content/util.js +++ b/common/content/util.js @@ -767,18 +767,20 @@ const Util = Module("util", { /** * Array utility methods. */ - Array: Class("Array", { + Array: Class("Array", Array, { init: function (ary) { return { __proto__: ary, __iterator__: function () this.iteritems(), __noSuchMethod__: function (meth, args) { - let res = (util.Array[meth] || Array[meth]).apply(null, [this.__proto__].concat(args)); + var res = util.Array[meth].apply(null, [this.__proto__].concat(args)); + if (util.Array.isinstance(res)) return util.Array(res); return res; }, - concat: function () [].concat.apply(this.__proto__, arguments), + toString: function () this.__proto__.toString(), + concat: function () this.__proto__.concat.apply(this.__proto__, arguments), map: function () this.__noSuchMethod__("map", Array.slice(arguments)) }; } @@ -787,8 +789,6 @@ const Util = Module("util", { return Object.prototype.toString.call(obj) == "[object Array]"; }, - toString: function () this.__proto__.toString(), - /** * Converts an array to an object. As in lisp, an assoc is an * array of key-value pairs, which maps directly to an object, @@ -864,7 +864,7 @@ const Util = Module("util", { } return ret; } - }) + }), }); // vim: set fdm=marker sw=4 ts=4 et: