mirror of
https://github.com/gryf/pentadactyl-pm.git
synced 2025-12-20 10:08:00 +01:00
Add 'javascript' module. Misc fixes along the way.
This commit is contained in:
@@ -250,7 +250,7 @@ const AutoCommands = Module("autocommands", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
completion: function () {
|
completion: function () {
|
||||||
completion.setFunctionCompleter(autocommands.get, [function () config.autocommands]);
|
JavaScript.setCompleter(this.get, [function () config.autocommands]);
|
||||||
|
|
||||||
completion.autocmdEvent = function autocmdEvent(context) {
|
completion.autocmdEvent = function autocmdEvent(context) {
|
||||||
context.completions = config.autocommands;
|
context.completions = config.autocommands;
|
||||||
|
|||||||
@@ -872,7 +872,7 @@ const Commands = Module("commands", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
completion: function () {
|
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) {
|
completion.command = function command(context) {
|
||||||
context.title = ["Command"];
|
context.title = ["Command"];
|
||||||
|
|||||||
@@ -627,20 +627,9 @@ const CompletionContext = Class("CompletionContext", {
|
|||||||
*/
|
*/
|
||||||
const Completion = Module("completion", {
|
const Completion = Module("completion", {
|
||||||
init: function () {
|
init: function () {
|
||||||
this.javascriptCompleter = Completion.Javascript();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setFunctionCompleter: function setFunctionCompleter(funcs, completers) {
|
get setFunctionCompleter() JavaScript.setCompleter, // Backward compatibility
|
||||||
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);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// FIXME
|
// FIXME
|
||||||
_runCompleter: function _runCompleter(name, filter, maxItems) {
|
_runCompleter: function _runCompleter(name, filter, maxItems) {
|
||||||
@@ -677,8 +666,6 @@ const Completion = Module("completion", {
|
|||||||
////////////////////// COMPLETION TYPES ////////////////////////////////////////
|
////////////////////// COMPLETION TYPES ////////////////////////////////////////
|
||||||
/////////////////////////////////////////////////////////////////////////////{{{
|
/////////////////////////////////////////////////////////////////////////////{{{
|
||||||
|
|
||||||
javascript: function (context) this.javascriptCompleter.complete(context),
|
|
||||||
|
|
||||||
// filter a list of urls
|
// filter a list of urls
|
||||||
//
|
//
|
||||||
// may consist of search engines, filenames, bookmarks and history,
|
// may consist of search engines, filenames, bookmarks and history,
|
||||||
@@ -754,556 +741,6 @@ const Completion = Module("completion", {
|
|||||||
//}}}
|
//}}}
|
||||||
}, {
|
}, {
|
||||||
UrlCompleter: Struct("name", "description", "completer"),
|
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:
|
// vim: set fdm=marker sw=4 ts=4 et:
|
||||||
|
|||||||
@@ -975,7 +975,7 @@ lookup:
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
completion: function () {
|
completion: function () {
|
||||||
completion.setFunctionCompleter([this.File, File.expandPath],
|
JavaScript.setCompleter([this.File, File.expandPath],
|
||||||
[function (context, obj, args) {
|
[function (context, obj, args) {
|
||||||
context.quote[2] = "";
|
context.quote[2] = "";
|
||||||
completion.file(context, true);
|
completion.file(context, true);
|
||||||
|
|||||||
613
common/content/javascript.js
Normal file
613
common/content/javascript.js
Normal file
@@ -0,0 +1,613 @@
|
|||||||
|
// Copyright (c) 2008-2009 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.
|
||||||
|
|
||||||
|
// 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.<Tab>
|
||||||
|
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
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -45,6 +45,7 @@
|
|||||||
"finder.js",
|
"finder.js",
|
||||||
"hints.js",
|
"hints.js",
|
||||||
"io.js",
|
"io.js",
|
||||||
|
"javascript.js",
|
||||||
"mappings.js",
|
"mappings.js",
|
||||||
"marks.js",
|
"marks.js",
|
||||||
"modes.js",
|
"modes.js",
|
||||||
|
|||||||
@@ -1702,9 +1702,6 @@ const Liberator = Module("liberator", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
completion: function () {
|
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) {
|
completion.dialog = function dialog(context) {
|
||||||
context.title = ["Dialog"];
|
context.title = ["Dialog"];
|
||||||
context.completions = config.dialogs;
|
context.completions = config.dialogs;
|
||||||
|
|||||||
@@ -470,18 +470,18 @@ const Mappings = Module("mappings", {
|
|||||||
[mode.disp.toLowerCase()]);
|
[mode.disp.toLowerCase()]);
|
||||||
},
|
},
|
||||||
completion: function () {
|
completion: function () {
|
||||||
completion.setFunctionCompleter(mappings.get,
|
JavaScript.setCompleter(this.get,
|
||||||
[
|
[
|
||||||
null,
|
null,
|
||||||
function (context, obj, args) {
|
function (context, obj, args) {
|
||||||
let mode = args[0];
|
let mode = args[0];
|
||||||
return util.Array.flatten(
|
return util.Array.flatten(
|
||||||
[
|
[
|
||||||
[[name, map.description] for ([i, name] in Iterator(map.names))]
|
[[name, map.description] for ([i, name] in Iterator(map.names))]
|
||||||
for ([i, map] in Iterator(mappings._user[mode].concat(mappings._main[mode])))
|
for ([i, map] in Iterator(mappings._user[mode].concat(mappings._main[mode])))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
completion.userMapping = function userMapping(context, args, modes) {
|
completion.userMapping = function userMapping(context, args, modes) {
|
||||||
// FIXME: have we decided on a 'standard' way to handle this clash? --djk
|
// FIXME: have we decided on a 'standard' way to handle this clash? --djk
|
||||||
|
|||||||
@@ -1227,8 +1227,8 @@ const Options = Module("options", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
completion: function () {
|
completion: function () {
|
||||||
completion.setFunctionCompleter(options.get, [function () ([o.name, o.description] for (o in options))]);
|
JavaScript.setCompleter(this.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.getPref, this.safeSetPref, this.setPref, this.resetPref, this.invertPref],
|
||||||
[function () options.allPrefs().map(function (pref) [pref, ""])]);
|
[function () options.allPrefs().map(function (pref) [pref, ""])]);
|
||||||
|
|
||||||
completion.option = function option(context, scope) {
|
completion.option = function option(context, scope) {
|
||||||
|
|||||||
@@ -105,6 +105,13 @@ const Services = Module("services", {
|
|||||||
* @param {string} name The class's cache key.
|
* @param {string} name The class's cache key.
|
||||||
*/
|
*/
|
||||||
create: function (name) this.classes[name]()
|
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:
|
// vim: set fdm=marker sw=4 ts=4 et:
|
||||||
|
|||||||
@@ -696,7 +696,7 @@ Module("styles", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
completion: function () {
|
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)
|
[ // Prototype: (system, name, filter, css, index)
|
||||||
null,
|
null,
|
||||||
function (context, obj, args) args[0] ? styles.systemNames : styles.userNames,
|
function (context, obj, args) args[0] ? styles.systemNames : styles.userNames,
|
||||||
|
|||||||
@@ -767,18 +767,20 @@ const Util = Module("util", {
|
|||||||
/**
|
/**
|
||||||
* Array utility methods.
|
* Array utility methods.
|
||||||
*/
|
*/
|
||||||
Array: Class("Array", {
|
Array: Class("Array", Array, {
|
||||||
init: function (ary) {
|
init: function (ary) {
|
||||||
return {
|
return {
|
||||||
__proto__: ary,
|
__proto__: ary,
|
||||||
__iterator__: function () this.iteritems(),
|
__iterator__: function () this.iteritems(),
|
||||||
__noSuchMethod__: function (meth, args) {
|
__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))
|
if (util.Array.isinstance(res))
|
||||||
return util.Array(res);
|
return util.Array(res);
|
||||||
return 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))
|
map: function () this.__noSuchMethod__("map", Array.slice(arguments))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -787,8 +789,6 @@ const Util = Module("util", {
|
|||||||
return Object.prototype.toString.call(obj) == "[object Array]";
|
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
|
* Converts an array to an object. As in lisp, an assoc is an
|
||||||
* array of key-value pairs, which maps directly to an object,
|
* array of key-value pairs, which maps directly to an object,
|
||||||
@@ -864,7 +864,7 @@ const Util = Module("util", {
|
|||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// vim: set fdm=marker sw=4 ts=4 et:
|
// vim: set fdm=marker sw=4 ts=4 et:
|
||||||
|
|||||||
Reference in New Issue
Block a user