1
0
mirror of https://github.com/gryf/pentadactyl-pm.git synced 2025-12-20 09:27:58 +01:00
Files
pentadactyl-pm/common/content/javascript.js

615 lines
22 KiB
JavaScript

// Copyright (c) 2008-2010 by Kris Maglione <maglione.k at Gmail>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
"use strict";
// 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) {
"use strict";
if (obj == null)
return;
let seen = {};
for (let key in properties(obj, !toplevel)) {
set.add(seen, key);
yield key;
}
// Properties aren't visible in an XPCNativeWrapper until
// they're accessed.
for (let key in properties(this.getKey(obj, "wrappedJSObject"), !toplevel))
try {
if (key in obj && !set.has(seen, key))
yield key;
}
catch (e) {}
},
objectKeys: function objectKeys(obj, toplevel) {
// Things we can dereference
if (!obj || ["object", "string", "function"].indexOf(typeof obj) == -1)
return [];
if (modules.isPrototypeOf(obj) && !toplevel)
return [];
let completions = [k for (k in this.iter(obj, toplevel))];
return completions;
},
evalled: function evalled(arg, key, tmp) {
let cache = this.context.cache.evalled;
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] = dactyl.userEval(arg, context);
}
catch (e) {
this.context.message = "Error: " + e;
return null;
}
finally {
delete context[JavaScript.EVAL_TMP];
}
},
// Get an element from the stack. If @frame is negative,
// count from the top of the stack, otherwise, the bottom.
// If @nth is provided, return the @mth value of element @type
// of the stack entry at @frame.
_get: function (frame, nth, type) {
let a = this._stack[frame >= 0 ? frame : this._stack.length + frame];
if (type != null)
a = a[type];
if (nth == null)
return a;
return a[a.length - nth - 1];
},
// Push and pop the stack, maintaining references to 'top' and 'last'.
_push: function push(arg) {
this._top = {
offset: this._i,
char: arg,
statements: [this._i],
dots: [],
fullStatements: [],
comma: [],
functions: []
};
this._last = this._top.char;
this._stack.push(this._top);
},
_pop: function pop(arg) {
if (this._i == this.context.caret - 1)
this.context.highlight(this._top.offset, 1, "FIND");
if (this._top.char != arg) {
this.context.highlight(this._top.offset, this._i - this._top.offset, "SPELLCHECK");
throw Error("Invalid JS");
}
// 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.statements[this._top.statements.length - 1] == this._i)
this._top.statements.pop();
this._top = this._get(-2);
this._last = this._top.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.context.highlight(0, 0, "FIND");
this._i = this._str.length;
if (this.popStatement)
this._top.statements.pop();
}
else {
this.context.highlight();
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 (/['"\/]/.test(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.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.statements.pop();
switch (this._c) {
case "(":
// Function call, or if/while/for/...
if (/[\w$]/.test(this._lastNonwhite)) {
this._functions.push(this._i);
this._top.functions.push(this._i);
this._top.statements.pop();
}
case '"':
case "'":
case "/":
case "{":
case "[":
this._push(this._c);
break;
case ".":
this._top.dots.push(this._i);
break;
case ")": this._pop("("); break;
case "]": this._pop("["); break;
case "}": this._pop("{"); // Fallthrough
case ";":
this._top.fullStatements.push(this._i);
break;
case ",":
this._top.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.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.evalled)
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, "statements") || 0; // Current statement.
let prev = statement;
let obj = null;
let cacheKey;
for (let [, dot] in Iterator(this._get(frame).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 [];
if (prev != statement && obj == null) {
this.context.message = "Error: " + cacheKey.quote() + " is " + String(obj);
return [];
}
prev = dot + 1;
obj = this.evalled(s, cacheKey, obj);
}
return [[obj, cacheKey]];
},
_getObjKey: function (frame) {
let dot = this._get(frame, 0, "dots") || -1; // Last dot in frame.
let statement = this._get(frame, 0, "statements") || 0; // Current statement.
let end = (frame == -1 ? this._lastIdx : this._get(frame + 1).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, args) {
context.title = [args.name];
context.anchored = args.anchored;
context.filter = args.filter;
context.itemCache = context.parent.itemCache;
context.key = args.name + args.last;
if (args.last != null)
context.quote = [args.last, function (text) util.escapeString(text.substr(args.offset), ""), args.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));
args.completer.call(self, context, args.obj);
},
_complete: function (objects, key, compl, string, last) {
const self = this;
if (!window.Object.getOwnPropertyNames && !options["jsdebugger"] && !this.context.message)
this.context.message = "For better completion data, please enable the JavaScript debugger (:set jsdebugger)";
let orig = compl;
if (!compl) {
compl = function (context, obj, recurse) {
context.process[1] = function highlight(item, v)
template.highlight(typeof v == "xml" ? new String(v.toXMLString()) : 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.key) && !isnan(b.key))
return a.key - b.key;
return isnan(b.key) - isnan(a.key) || compare(a, b);
};
context.keys = {
text: util.identity,
description: function (item) self.getKey(obj, item),
key: function (item) {
if (!isNaN(key))
return parseInt(key);
if (/^[A-Z_][A-Z0-9_]*$/.test(key))
return "";
return item;
}
};
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);
};
}
let args = {
completer: compl,
anchored: true,
filter: key + (string || ""),
last: last,
offset: key.length
};
this.context.forceAnchored = true;
// TODO: Make this a generic completion helper function.
for (let [, obj] in Iterator(objects))
this.context.fork(obj[1], this._top.offset, this, this._fill,
update(args, {
obj: obj[0],
name: obj[1]
}));
if (orig)
return;
for (let [, obj] in Iterator(objects))
this.context.fork(obj[1] + "/prototypes", this._top.offset, this, this._fill,
update(args, {
obj: obj[0],
name: obj[1] + " (prototypes)",
completer: function (a, b) compl(a, b, true)
}));
for (let [, obj] in Iterator(objects))
this.context.fork(obj[1] + "/substrings", this._top.offset, this, this._fill,
update(args, {
obj: obj[0],
name: obj[1] + " (substrings)",
anchored: false,
completer: compl
}));
for (let [, obj] in Iterator(objects))
this.context.fork(obj[1] + "/prototypes/substrings", this._top.offset, this, this._fill,
update(args, {
obj: obj[0],
name: obj[1] + " (prototype substrings)",
anchored: false,
completer: function (a, b) compl(a, b, true)
}));
},
_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, "statements"), this._get(-1, null, "offset")) + "''";
// Now eval the key, to process any referenced variables.
return this.evalled(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")
dactyl.reportError(e);
this._lastIdx = 0;
return null;
}
this.context.getCache("evalled", 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.createEle<Tab> ...
let prev = 0;
for (let [, v] in Iterator(this._get(0).fullStatements)) {
let key = this._str.substring(prev, v + 1);
if (this._checkFunction(prev, v, key))
return null;
this.evalled(key);
prev = v + 1;
}
// In a string. Check if we're dereferencing an object or
// completing a function argument. 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).offset + 1, this._lastIdx);
// This is definitely a properly quoted string.
// Just eval it normally.
string = eval(this._last + string + this._last);
// Is this an object accessor?
if (this._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 (this._get(-3, 0, "statements") == this._get(-2).offset)
return null;
// Beginning of the statement upto the opening [
let obj = this._getObj(-3, this._get(-2).offset);
return this._complete(obj, this._getKey(), null, string, this._last);
}
// Is this a function call?
if (this._get(-2).char == "(") {
// Stack:
// [-1]: "...
// [-2]: (...
// [-3]: base statement
// Does the opening "(" mark a function call?
if (this._get(-3, 0, "functions") != this._get(-2).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].dactylCompleter;
}
catch (e) {}
if (!completer)
completer = JavaScript.completers[func];
if (!completer)
return null;
// Split up the arguments
let prev = this._get(-2).offset;
let args = [];
for (let [i, idx] in Iterator(this._get(-2).comma)) {
let arg = this._str.substring(prev + 1, idx);
prev = idx;
memoize(args, i, function () self.evalled(arg));
}
let key = this._getKey();
args.push(key + string);
let 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 when there's no key
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.offset;
this._top.offset = offset;
return this._complete(obj, key);
}
finally {
this._top.offset = o;
}
return null;
}
}, {
EVAL_TMP: "__dactyl_eval_tmp",
/**
* 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.dactylCompleter = function (context, func, obj, args) {
let completer = completers[args.length - 1];
if (!completer)
return [];
return completer.call(obj, 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) {
services.get("debugger")[value ? "on" : "off"]();
},
getter: function () services.get("debugger").isOn
});
}
})