1
0
mirror of https://github.com/gryf/pentadactyl-pm.git synced 2025-12-20 08:07:59 +01:00

Add 'javascript' module. Misc fixes along the way.

This commit is contained in:
Kris Maglione
2009-11-15 02:08:10 -05:00
parent 9937c2965a
commit cdaa26f968
12 changed files with 646 additions and 591 deletions

View File

@@ -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.<Tab>
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: