1 // Copyright (c) 2008-2013 Kris Maglione <maglione.k at Gmail>
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
7 let { getOwnPropertyNames } = Object;
11 defineModule("javascript", {
12 exports: ["JavaScript", "javascript"],
16 lazyRequire("template", ["template"]);
18 let isPrototypeOf = Object.prototype.isPrototypeOf;
20 // TODO: Clean this up.
22 var JavaScript = Module("javascript", {
26 this._top = {}; // The element on the top of the stack.
27 this._last = ""; // The last opening char pushed onto the stack.
28 this._lastNonwhite = ""; // Last non-whitespace character we saw.
29 this._lastChar = ""; // Last character we saw, used for \ escaping quotes.
34 this._cacheKey = null;
36 this._nullSandbox = Cu.Sandbox("about:blank");
39 Local: function (dactyl, modules, window) ({
40 init: function init() {
41 this.modules = modules;
48 globals: Class.Memoize(function () [
49 [this.modules.userContext, /*L*/"Global Variables"],
50 [this.modules, "modules"],
51 [this.window, "window"]
54 toplevel: Class.Memoize(function () this.modules.jsmodules),
58 newContext: function () this.modules.newContext(this.modules.userContext, true, "Dactyl JS Temp Context"),
60 completers: Class.Memoize(() => Object.create(JavaScript.completers)),
62 // Some object members are only accessible as function calls
63 getKey: function (obj, key) {
71 iter: function iter_(obj, toplevel) {
75 let seen = isinstance(obj, ["Sandbox"]) ? Set(JavaScript.magicalNames) : {};
76 let globals = values(toplevel && this.window === obj ? this.globalNames : []);
78 if (toplevel && isObject(obj) && "wrappedJSObject" in obj)
79 if (!Set.add(seen, "wrappedJSObject"))
80 yield "wrappedJSObject";
82 for (let key in iter(globals, properties(obj, !toplevel, true)))
83 if (!Set.add(seen, key))
86 // Properties aren't visible in an XPCNativeWrapper until
88 for (let key in properties(this.getKey(obj, "wrappedJSObject"), !toplevel, true))
90 if (key in obj && !Set.has(seen, key))
96 objectKeys: function objectKeys(obj, toplevel) {
97 // Things we can dereference
98 if (!obj || ["object", "string", "function"].indexOf(typeof obj) === -1)
100 if (isinstance(obj, ["Sandbox"]) && !toplevel) // Temporary hack.
102 if (isPrototypeOf.call(this.toplevel, obj) && !toplevel)
105 let completions = [k for (k in this.iter(obj, toplevel))];
106 if (obj === this.modules) // Hack.
107 completions = array.uniq(completions.concat([k for (k in this.iter(this.modules.jsmodules, toplevel))]));
111 evalled: function evalled(arg, key, tmp) {
112 let cache = this.context.cache.evalled;
113 let context = this.context.cache.evalContext;
120 context[JavaScript.EVAL_TMP] = tmp;
122 cache[key] = this.modules.dactyl.userEval(arg, context,
123 /*L*/"[Command Line Completion]", 1);
129 this.context.message = _("error.error", e);
133 delete context[JavaScript.EVAL_TMP];
137 // Get an element from the stack. If @frame is negative,
138 // count from the top of the stack, otherwise, the bottom.
139 // If @nth is provided, return the @mth value of element @type
140 // of the stack entry at @frame.
141 _get: function (frame, nth, type) {
142 let a = this._stack[frame >= 0 ? frame : this._stack.length + frame];
147 return a[a.length - nth - 1];
150 // Push and pop the stack, maintaining references to 'top' and 'last'.
151 _push: function push(arg) {
155 statements: [this._i],
161 this._last = this._top.char;
162 this._stack.push(this._top);
165 _pop: function pop(arg) {
166 if (this._i == this.context.caret - 1)
167 this.context.highlight(this._top.offset, 1, "FIND");
169 if (this._top.char != arg) {
170 this.context.highlight(this._top.offset, this._i - this._top.offset, "SPELLCHECK");
171 throw Error(/*L*/"Invalid JS");
174 // The closing character of this stack frame will have pushed a new
175 // statement, leaving us with an empty statement. This doesn't matter,
176 // now, as we simply throw away the frame when we pop it, but it may later.
177 if (this._top.statements[this._top.statements.length - 1] == this._i)
178 this._top.statements.pop();
179 this._top = this._get(-2);
180 this._last = this._top.char;
181 return this._stack.pop();
184 _buildStack: function (filter) {
185 // Todo: Fix these one-letter variable names.
187 this._c = ""; // Current index and character, respectively.
189 // Reuse the old stack.
190 if (this._str && filter.substr(0, this._str.length) == this._str) {
191 this.context.highlight(0, 0, "FIND");
192 this._i = this._str.length;
193 if (this.popStatement)
194 this._top.statements.pop();
197 this.context.highlight();
199 this._functions = [];
203 // Build a parse stack, discarding entries as opening characters
204 // match closing characters. The stack is walked from the top entry
205 // and down as many levels as it takes us to figure out what it is
206 // that we're completing.
208 let length = this._str.length;
209 for (; this._i < length; this._lastChar = this._c, this._i++) {
210 this._c = this._str[this._i];
211 if (/['"\/]/.test(this._last)) {
212 if (this._lastChar == "\\") { // Escape. Skip the next char, whatever it may be.
216 else if (this._c == this._last)
220 // A word character following a non-word character, or simply a non-word
221 // character. Start a new statement.
222 if (/[a-zA-Z_$]/.test(this._c) && !/[\w$]/.test(this._lastChar) || !/[\w\s$]/.test(this._c))
223 this._top.statements.push(this._i);
225 // A "." or a "[" dereferences the last "statement" and effectively
226 // joins it to this logical statement.
227 if ((this._c == "." || this._c == "[") && /[\w$\])"']/.test(this._lastNonwhite)
228 || this._lastNonwhite == "." && /[a-zA-Z_$]/.test(this._c))
229 this._top.statements.pop();
233 // Function call, or if/while/for/...
234 if (/[\w$]/.test(this._lastNonwhite)) {
235 this._functions.push(this._i);
236 this._top.functions.push(this._i);
237 this._top.statements.pop();
247 this._top.dots.push(this._i);
249 case ")": this._pop("("); break;
250 case "]": this._pop("["); break;
251 case "}": this._pop("{"); // Fallthrough
253 this._top.fullStatements.push(this._i);
256 this._top.comma.push(this._i);
260 if (/\S/.test(this._c))
261 this._lastNonwhite = this._c;
265 this.popStatement = false;
266 if (!/[\w$]/.test(this._lastChar) && this._lastNonwhite != ".") {
267 this.popStatement = true;
268 this._top.statements.push(this._i);
271 this._lastIdx = this._i;
274 // Don't eval any function calls unless the user presses tab.
275 _checkFunction: function (start, end, key) {
276 let res = this._functions.some(idx => (idx >= start && idx < end));
277 if (!res || this.context.tabPressed || key in this.cache.evalled)
279 this.context.waitingForTab = true;
283 // For each DOT in a statement, prefix it with TMP, eval it,
284 // and save the result back to TMP. The point of this is to
285 // cache the entire path through an object chain, mainly in
286 // the presence of function calls. There are drawbacks. For
287 // instance, if the value of a variable changes in the course
288 // of inputting a command (let foo=bar; frob(foo); foo=foo.bar; ...),
289 // we'll still use the old value. But, it's worth it.
290 _getObj: function (frame, stop) {
291 let statement = this._get(frame, 0, "statements") || 0; // Current statement.
292 let prev = statement;
293 let obj = this.window;
295 for (let [, dot] in Iterator(this._get(frame).dots.concat(stop))) {
298 if (dot > stop || dot <= prev)
301 let s = this._str.substring(prev, dot);
302 if (prev != statement)
303 s = JavaScript.EVAL_TMP + "." + s;
304 cacheKey = this._str.substring(statement, dot);
306 if (this._checkFunction(prev, dot, cacheKey))
308 if (prev != statement && obj == null) {
309 this.context.message = /*L*/"Error: " + cacheKey.quote() + " is " + String(obj);
314 obj = this.evalled(s, cacheKey, obj);
316 return [[obj, cacheKey]];
319 _getObjKey: function (frame) {
320 let dot = this._get(frame, 0, "dots") || -1; // Last dot in frame.
321 let statement = this._get(frame, 0, "statements") || 0; // Current statement.
322 let end = (frame == -1 ? this._lastIdx : this._get(frame + 1).offset);
324 this._cacheKey = null;
325 let obj = [[this.cache.evalContext, /*L*/"Local Variables"]].concat(this.globals);
326 // Is this an object dereference?
327 if (dot < statement) // No.
329 else // Yes. Set the object to the string before the dot.
330 obj = this._getObj(frame, dot);
332 let [, space, key] = this._str.substring(dot + 1, end).match(/^(\s*)(.*)/);
333 return [dot + 1 + space.length, obj, key];
336 _complete: function (objects, key, compl, string, last) {
339 if (!getOwnPropertyNames && !services.debugger.isOn && !this.context.message)
340 this.context.message = /*L*/"For better completion data, please enable the JavaScript debugger (:set jsdebugger)";
342 let base = this.context.fork("js", this._top.offset);
343 base.forceAnchored = true;
344 base.filter = last == null ? key : string;
345 let prefix = last != null ? key : "";
347 if (last == null) // We're not looking for a quoted string, so filter out anything that's not a valid identifier
348 base.filters.push(item => /^[a-zA-Z_$][\w$]*$/.test(item.text));
350 base.quote = [last, text => util.escapeString(text, ""), last];
352 base.filters.push(item => item.item.indexOf(prefix) === 0);
356 base.process[1] = function highlight(item, v)
357 template.highlight(typeof v == "xml" ? new String(v.toXMLString()) : v, true, 200);
359 // Sort in a logical fashion for object keys:
360 // Numbers are sorted as numbers, rather than strings, and appear first.
361 // Constants are unsorted, and appear before other non-null strings.
362 // Other strings are sorted in the default manner.
364 let isnan = function isnan(item) item != '' && isNaN(item);
365 let compare = base.compare;
367 base.compare = function (a, b) {
368 if (!isnan(a.key) && !isnan(b.key))
369 return a.key - b.key;
370 return isnan(b.key) - isnan(a.key) || compare(a, b);
374 text: prefix ? text => text.substr(prefix.length)
376 description: function (item) self.getKey(this.obj, item),
377 key: function (item) {
379 return parseInt(key);
380 if (/^[A-Z_][A-Z0-9_]*$/.test(key))
387 // We've already listed anchored matches, so don't list them again here.
388 function unanchored(item) util.compareIgnoreCase(item.text.substr(0, this.filter.length), this.filter);
390 objects.forEach(function (obj) {
391 let context = base.fork(obj[1]);
392 context.title = [obj[1]];
393 context.keys.obj = () => obj[0];
394 context.key = obj[1] + last;
395 if (obj[0] == this.cache.evalContext)
396 context.regenerate = true;
398 obj.ctxt_t = context.fork("toplevel");
400 obj.ctxt_p = context.fork("prototypes");
401 obj.ctxt_t.generate = () => self.objectKeys(obj[0], true);
402 obj.ctxt_p.generate = () => self.objectKeys(obj[0], false);
406 // TODO: Make this a generic completion helper function.
407 objects.forEach(function (obj) {
408 obj.ctxt_t.split(obj[1] + "/anchored", this, function (context) {
409 context.anchored = true;
411 compl(context, obj[0]);
418 objects.forEach(function (obj) {
419 obj.ctxt_p.split(obj[1] + "/anchored", this, function (context) {
420 context.anchored = true;
421 context.title[0] += /*L*/" (prototypes)";
425 objects.forEach(function (obj) {
426 obj.ctxt_t.split(obj[1] + "/unanchored", this, function (context) {
427 context.anchored = false;
428 context.title[0] += /*L*/" (substrings)";
429 context.filters.push(unanchored);
433 objects.forEach(function (obj) {
434 obj.ctxt_p.split(obj[1] + "/unanchored", this, function (context) {
435 context.anchored = false;
436 context.title[0] += /*L*/" (prototype substrings)";
437 context.filters.push(unanchored);
442 _getKey: function () {
443 if (this._last == "")
445 // After the opening [ upto the opening ", plus '' to take care of any operators before it
446 let key = this._str.substring(this._get(-2, null, "offset") + 1, this._get(-1, null, "offset")) + "''";
447 // Now eval the key, to process any referenced variables.
448 return this.evalled(key);
451 get cache() this.context.cache,
453 complete: function _complete(context) {
455 this.context = context;
458 this._buildStack.call(this, context.filter);
462 util.assert(!e.message, e.message);
466 this.context.getCache("evalled", Object);
467 this.context.getCache("evalContext", this.closure.newContext);
469 // Okay, have parse stack. Figure out what we're completing.
471 // Find any complete statements that we can eval before we eval our object.
472 // This allows for things like:
473 // let doc = content.document; let elem = doc.createEle<Tab> ...
475 for (let [, v] in Iterator(this._get(0).fullStatements)) {
476 let key = this._str.substring(prev, v + 1);
477 if (this._checkFunction(prev, v, key))
483 // If this is a function argument, try to get the function's
484 // prototype and show it.
486 let i = (this._get(-2) && this._get(-2).char == "(") ? -2 : -1;
487 if (this._get(i).char == "(") {
488 let [offset, obj, funcName] = this._getObjKey(i - 1);
490 let func = obj[0][0][funcName];
491 if (callable(func)) {
492 let [, prefix, args] = /^(function .*?)\((.*?)\)/.exec(Function.prototype.toString.call(func));
493 let n = this._get(i).comma.length;
494 args = template.map(Iterator(args.split(", ")),
495 ([i, arg]) => ["span", { highlight: i == n ? "Filter" : "" }, arg],
497 this.context.message = ["", prefix + "(", args, ")"];
504 // In a string. Check if we're dereferencing an object or
505 // completing a function argument. Otherwise, do nothing.
506 if (this._last == "'" || this._last == '"') {
508 // str = "foo[bar + 'baz"
512 // The top of the stack is the sting we're completing.
513 // Wrap it in its delimiters and eval it to process escape sequences.
514 let string = this._str.substring(this._get(-1).offset + 1, this._lastIdx).replace(/((?:\\\\)*)\\/, "$1");
515 string = Cu.evalInSandbox(this._last + string + this._last, this._nullSandbox);
517 // Is this an object accessor?
518 if (this._get(-2).char == "[") { // Are we inside of []?
522 // [-3]: base statement
524 // Yes. If the [ starts at the beginning of a logical
525 // statement, we're in an array literal, and we're done.
526 if (this._get(-3, 0, "statements") == this._get(-2).offset)
529 // Beginning of the statement upto the opening [
530 let obj = this._getObj(-3, this._get(-2).offset);
532 return this._complete(obj, this._getKey(), null, string, this._last);
535 // Is this a function call?
536 if (this._get(-2).char == "(") {
540 // [-3]: base statement
542 // Does the opening "(" mark a function call?
543 if (this._get(-3, 0, "functions") != this._get(-2).offset)
544 return null; // No. We're done.
546 let [offset, obj, funcName] = this._getObjKey(-3);
549 obj = obj.slice(0, 1);
552 let func = obj[0][0][funcName];
553 var completer = func.dactylCompleter;
557 completer = this.completers[funcName];
561 // Split up the arguments
562 let prev = this._get(-2).offset;
564 for (let [i, idx] in Iterator(this._get(-2).comma)) {
565 let arg = this._str.substring(prev + 1, idx);
567 memoize(args, i, () => self.evalled(arg));
569 let key = this._getKey();
570 args.push(key + string);
572 let compl = function (context, obj) {
573 let res = completer.call(self, context, funcName, obj, args);
575 context.completions = res;
578 obj[0][1] += "." + funcName + "(... [" + args.length + "]";
579 return this._complete(obj, key, compl, string, this._last);
582 // In a string that's not an obj key or a function arg.
587 // str = "foo.bar.baz"
592 // obj = [modules, window]
595 let [offset, obj, key] = this._getObjKey(-1);
597 // Wait for a keypress before completing when there's no key
598 if (!this.context.tabPressed && key == "" && obj.length > 1) {
599 let message = this.context.message || "";
600 this.context.waitingForTab = true;
601 this.context.message = ["", message, "\n",
602 _("completion.waitingForKeyPress")];
606 if (!/^(?:[a-zA-Z_$][\w$]*)?$/.test(key))
607 return null; // Not a word. Forget it. Can this even happen?
610 var o = this._top.offset;
611 this._top.offset = offset;
612 return this._complete(obj, key);
615 this._top.offset = o;
620 magicalNames: Class.Memoize(function () Object.getOwnPropertyNames(Cu.Sandbox(this.window), true).sort()),
623 * A list of properties of the global object which are not
624 * enumerable by any standard method.
626 globalNames: Class.Memoize(function () let (self = this) array.uniq([
627 "Array", "ArrayBuffer", "AttributeName", "Audio", "Boolean", "Components",
628 "CSSFontFaceStyleDecl", "CSSGroupRuleRuleList", "CSSNameSpaceRule",
629 "CSSRGBColor", "CSSRect", "ComputedCSSStyleDeclaration", "Date", "Error",
630 "EvalError", "File", "Float32Array", "Float64Array", "Function",
631 "HTMLDelElement", "HTMLInsElement", "HTMLSpanElement", "Infinity",
632 "InnerModalContentWindow", "InnerWindow", "Int16Array", "Int32Array",
633 "Int8Array", "InternalError", "Iterator", "JSON", "KeyboardEvent",
634 "Math", "NaN", "Namespace", "Number", "Object", "Proxy", "QName",
635 "ROCSSPrimitiveValue", "RangeError", "ReferenceError", "RegExp",
636 "StopIteration", "String", "SyntaxError", "TypeError", "URIError",
637 "Uint16Array", "Uint32Array", "Uint8Array", "XML", "XMLHttpProgressEvent",
638 "XMLList", "XMLSerializer", "XPCNativeWrapper", "XPCSafeJSWrapper",
639 "XULControllers", "constructor", "decodeURI", "decodeURIComponent",
640 "encodeURI", "encodeURIComponent", "escape", "eval", "isFinite", "isNaN",
641 "isXMLName", "parseFloat", "parseInt", "undefined", "unescape", "uneval"
642 ].concat([k.substr(6) for (k in keys(Ci)) if (/^nsIDOM/.test(k))])
643 .concat([k.substr(3) for (k in keys(Ci)) if (/^nsI/.test(k))])
644 .concat(this.magicalNames)
645 .filter(k => k in self.window))),
648 EVAL_TMP: "__dactyl_eval_tmp",
651 * A map of argument completion functions for named methods. The
652 * signature and specification of the completion function
653 * are fairly complex and yet undocumented.
655 * @see JavaScript.setCompleter
660 * Installs argument string completers for a set of functions.
661 * The second argument is an array of functions (or null
662 * values), each corresponding the argument of the same index.
663 * Each provided completion function receives as arguments a
664 * CompletionContext, the 'this' object of the method, and an
665 * array of values for the preceding arguments.
667 * It is important to note that values in the arguments array
668 * provided to the completers are lazily evaluated the first
669 * time they are accessed, so they should be accessed
672 * @param {function|[function]} funcs The functions for which to
673 * install the completers.
674 * @param {[function]} completers An array of completer
677 setCompleter: function (funcs, completers) {
678 funcs = Array.concat(funcs);
679 for (let [, func] in Iterator(funcs)) {
680 func.dactylCompleter = function (context, func, obj, args) {
681 let completer = completers[args.length - 1];
684 return completer.call(obj, context, obj, args);
690 init: function init(dactyl, modules, window) {
691 init.superapply(this, arguments);
692 modules.JavaScript = Class("JavaScript", JavaScript, { modules: modules, window: window });
694 completion: function (dactyl, modules, window) {
695 const { completion } = modules;
696 update(modules.completion, {
697 get javascript() modules.javascript.closure.complete,
698 javascriptCompleter: JavaScript // Backwards compatibility
701 modes: function initModes(dactyl, modules, window) {
702 initModes.require("commandline");
703 const { modes } = modules;
705 modes.addMode("REPL", {
706 description: "JavaScript Read Eval Print Loop",
707 bases: [modes.COMMAND_LINE],
708 displayName: Class.Memoize(function () this.name)
711 commandline: function initCommandLine(dactyl, modules, window) {
712 const { Buffer, modes } = modules;
714 var REPL = Class("REPL", {
715 init: function init(context) {
716 this.context = context;
720 addOutput: function addOutput(js) {
724 var result = dactyl.userEval(js, this.context);
725 var xml = result === undefined ? "" : util.objectToString(result, true);
732 e = util.fixURI(e.fileName) + ":" + e.lineNumber + ": " + e;
733 xml = ["span", { highlight: "ErrorMsg" }, e];
736 let prompt = "js" + this.count;
737 Class.replaceProperty(this.context, prompt, result);
740 this.rootNode.appendChild(
742 [["div", { highlight: "REPL-E", key: "e" },
743 ["span", { highlight: "REPL-R" },
744 prompt, ">"], " ", js],
745 ["div", { highlight: "REPL-P", key: "p" },
747 this.document, nodes));
749 this.rootNode.scrollTop += nodes.e.getBoundingClientRect().top
750 - this.rootNode.getBoundingClientRect().top;
755 message: Class.Memoize(function () {
756 DOM.fromJSON(["div", { highlight: "REPL", key: "rootNode" }],
757 this.document, this);
759 return this.rootNode;
762 __noSuchMethod__: function (meth, args) Buffer[meth].apply(Buffer, [this.rootNode].concat(args))
765 modules.CommandREPLMode = Class("CommandREPLMode", modules.CommandMode, {
766 init: function init(context) {
767 init.supercall(this);
769 let sandbox = true || isinstance(context, ["Sandbox"]);
771 this.context = modules.newContext(context, !sandbox, "Dactyl REPL Context");
772 this.js = modules.JavaScript();
773 this.js.replContext = this.context;
774 this.js.newContext = () => modules.newContext(this.context, !sandbox, "Dactyl REPL Temp Context");
777 [this.context, /*L*/"REPL Variables"],
778 [context, /*L*/"REPL Global"]
779 ].concat(this.js.globals.filter(([global]) => isPrototypeOf.call(global, context)));
781 if (!isPrototypeOf.call(modules.jsmodules, context))
782 this.js.toplevel = context;
784 if (!isPrototypeOf.call(window, context))
785 this.js.window = context;
787 if (this.js.globals.slice(2).some(([global]) => global === context))
788 this.js.globals.splice(1);
790 this.repl = REPL(this.context);
793 open: function open(context) {
795 modules.mow.echo(this.repl);
796 this.widgets.message = null;
798 open.superapply(this, arguments);
802 complete: function complete(context) {
803 context.fork("js", 0, this.js, "complete");
806 historyKey: "javascript",
810 get completionList() this.widgets.statusbar.commandline.id,
812 accept: function accept() {
813 dactyl.trapErrors(function () { this.repl.addOutput(this.command); }, this);
815 this.completions.cleanup();
817 this.history.reset();
820 modules.mow.resize();
823 leave: function leave(params) {
824 leave.superapply(this, arguments);
826 modes.delay(function () { modes.pop(); });
829 updatePrompt: function updatePrompt() {
831 this.prompt = ["REPL-R", "js" + (this.repl.count + 1) + "> "];
835 commands: function initCommands(dactyl, modules, window) {
836 const { commands } = modules;
838 commands.add(["javas[cript]", "js"],
839 "Evaluate a JavaScript string",
841 if (args[0] && !args.bang)
842 dactyl.userEval(args[0]);
845 modules.CommandREPLMode(args[0] ? dactyl.userEval(args[0]) : modules.userContext)
851 completer: function (context) modules.completion.javascript(context),
856 mappings: function initMappings(dactyl, modules, window) {
857 const { mappings, modes } = modules;
859 function bind(...args) mappings.add.apply(mappings, [[modes.REPL]].concat(args))
861 bind(["<Return>"], "Accept the current input",
862 function ({ self }) { self.accept(); });
864 bind(["<C-e>"], "Scroll down one line",
865 function ({ self }) { self.repl.scrollVertical("lines", 1); });
867 bind(["<C-y>"], "Scroll up one line",
868 function ({ self }) { self.repl.scrollVertical("lines", -1); });
870 bind(["<C-d>"], "Scroll down half a page",
871 function ({ self }) { self.repl.scrollVertical("pages", .5); });
873 bind(["<C-f>", "<PageDown>"], "Scroll down one page",
874 function ({ self }) { self.repl.scrollVertical("pages", 1); });
876 bind(["<C-u>"], "Scroll up half a page",
877 function ({ self }) { self.repl.scrollVertical("pages", -.5); });
879 bind(["<C-b>", "<PageUp>"], "Scroll up half a page",
880 function ({ self }) { self.repl.scrollVertical("pages", -1); });
882 options: function initOptions(dactyl, modules, window) {
883 modules.options.add(["jsdebugger", "jsd"],
884 "Enable the JavaScript debugger service for use in JavaScript completion",
886 setter: function (value) {
887 if (services.debugger.isOn != value)
889 (services.debugger.asyncOn || services.debugger.on)(null);
891 services.debugger.off();
893 getter: function () services.debugger.isOn
900 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
902 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: