1 // Copyright (c) 2008-2012 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(function () 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(function (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(function (item) /^[a-zA-Z_$][\w$]*$/.test(item.text));
350 base.quote = [last, function (text) util.escapeString(text, ""), last];
352 base.filters.push(function (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 ? function (text) text.substr(prefix.length) : util.identity,
375 description: function (item) self.getKey(this.obj, item),
376 key: function (item) {
378 return parseInt(key);
379 if (/^[A-Z_][A-Z0-9_]*$/.test(key))
386 // We've already listed anchored matches, so don't list them again here.
387 function unanchored(item) util.compareIgnoreCase(item.text.substr(0, this.filter.length), this.filter);
389 objects.forEach(function (obj) {
390 let context = base.fork(obj[1]);
391 context.title = [obj[1]];
392 context.keys.obj = function () obj[0];
393 context.key = obj[1] + last;
394 if (obj[0] == this.cache.evalContext)
395 context.regenerate = true;
397 obj.ctxt_t = context.fork("toplevel");
399 obj.ctxt_p = context.fork("prototypes");
400 obj.ctxt_t.generate = function () self.objectKeys(obj[0], true);
401 obj.ctxt_p.generate = function () self.objectKeys(obj[0], false);
405 // TODO: Make this a generic completion helper function.
406 objects.forEach(function (obj) {
407 obj.ctxt_t.split(obj[1] + "/anchored", this, function (context) {
408 context.anchored = true;
410 compl(context, obj[0]);
417 objects.forEach(function (obj) {
418 obj.ctxt_p.split(obj[1] + "/anchored", this, function (context) {
419 context.anchored = true;
420 context.title[0] += /*L*/" (prototypes)";
424 objects.forEach(function (obj) {
425 obj.ctxt_t.split(obj[1] + "/unanchored", this, function (context) {
426 context.anchored = false;
427 context.title[0] += /*L*/" (substrings)";
428 context.filters.push(unanchored);
432 objects.forEach(function (obj) {
433 obj.ctxt_p.split(obj[1] + "/unanchored", this, function (context) {
434 context.anchored = false;
435 context.title[0] += /*L*/" (prototype substrings)";
436 context.filters.push(unanchored);
441 _getKey: function () {
442 if (this._last == "")
444 // After the opening [ upto the opening ", plus '' to take care of any operators before it
445 let key = this._str.substring(this._get(-2, null, "offset") + 1, this._get(-1, null, "offset")) + "''";
446 // Now eval the key, to process any referenced variables.
447 return this.evalled(key);
450 get cache() this.context.cache,
452 complete: function _complete(context) {
454 this.context = context;
457 this._buildStack.call(this, context.filter);
461 util.assert(!e.message, e.message);
465 this.context.getCache("evalled", Object);
466 this.context.getCache("evalContext", this.closure.newContext);
468 // Okay, have parse stack. Figure out what we're completing.
470 // Find any complete statements that we can eval before we eval our object.
471 // This allows for things like:
472 // let doc = content.document; let elem = doc.createEle<Tab> ...
474 for (let [, v] in Iterator(this._get(0).fullStatements)) {
475 let key = this._str.substring(prev, v + 1);
476 if (this._checkFunction(prev, v, key))
482 // If this is a function argument, try to get the function's
483 // prototype and show it.
485 let i = (this._get(-2) && this._get(-2).char == "(") ? -2 : -1;
486 if (this._get(i).char == "(") {
487 let [offset, obj, funcName] = this._getObjKey(i - 1);
489 let func = obj[0][0][funcName];
490 if (callable(func)) {
491 let [, prefix, args] = /^(function .*?)\((.*?)\)/.exec(Function.prototype.toString.call(func));
492 let n = this._get(i).comma.length;
493 args = template.map(Iterator(args.split(", ")),
494 function ([i, arg]) ["span", { highlight: i == n ? "Filter" : "" }, arg],
496 this.context.message = ["", prefix + "(", args, ")"];
503 // In a string. Check if we're dereferencing an object or
504 // completing a function argument. Otherwise, do nothing.
505 if (this._last == "'" || this._last == '"') {
507 // str = "foo[bar + 'baz"
511 // The top of the stack is the sting we're completing.
512 // Wrap it in its delimiters and eval it to process escape sequences.
513 let string = this._str.substring(this._get(-1).offset + 1, this._lastIdx).replace(/((?:\\\\)*)\\/, "$1");
514 string = Cu.evalInSandbox(this._last + string + this._last, this._nullSandbox);
516 // Is this an object accessor?
517 if (this._get(-2).char == "[") { // Are we inside of []?
521 // [-3]: base statement
523 // Yes. If the [ starts at the beginning of a logical
524 // statement, we're in an array literal, and we're done.
525 if (this._get(-3, 0, "statements") == this._get(-2).offset)
528 // Beginning of the statement upto the opening [
529 let obj = this._getObj(-3, this._get(-2).offset);
531 return this._complete(obj, this._getKey(), null, string, this._last);
534 // Is this a function call?
535 if (this._get(-2).char == "(") {
539 // [-3]: base statement
541 // Does the opening "(" mark a function call?
542 if (this._get(-3, 0, "functions") != this._get(-2).offset)
543 return null; // No. We're done.
545 let [offset, obj, funcName] = this._getObjKey(-3);
548 obj = obj.slice(0, 1);
551 let func = obj[0][0][funcName];
552 var completer = func.dactylCompleter;
556 completer = this.completers[funcName];
560 // Split up the arguments
561 let prev = this._get(-2).offset;
563 for (let [i, idx] in Iterator(this._get(-2).comma)) {
564 let arg = this._str.substring(prev + 1, idx);
566 memoize(args, i, function () self.evalled(arg));
568 let key = this._getKey();
569 args.push(key + string);
571 let compl = function (context, obj) {
572 let res = completer.call(self, context, funcName, obj, args);
574 context.completions = res;
577 obj[0][1] += "." + funcName + "(... [" + args.length + "]";
578 return this._complete(obj, key, compl, string, this._last);
581 // In a string that's not an obj key or a function arg.
586 // str = "foo.bar.baz"
591 // obj = [modules, window]
594 let [offset, obj, key] = this._getObjKey(-1);
596 // Wait for a keypress before completing when there's no key
597 if (!this.context.tabPressed && key == "" && obj.length > 1) {
598 let message = this.context.message || "";
599 this.context.waitingForTab = true;
600 this.context.message = ["", message, "\n",
601 _("completion.waitingForKeyPress")];
605 if (!/^(?:[a-zA-Z_$][\w$]*)?$/.test(key))
606 return null; // Not a word. Forget it. Can this even happen?
609 var o = this._top.offset;
610 this._top.offset = offset;
611 return this._complete(obj, key);
614 this._top.offset = o;
619 magicalNames: Class.Memoize(function () Object.getOwnPropertyNames(Cu.Sandbox(this.window), true).sort()),
622 * A list of properties of the global object which are not
623 * enumerable by any standard method.
625 globalNames: Class.Memoize(function () let (self = this) array.uniq([
626 "Array", "ArrayBuffer", "AttributeName", "Audio", "Boolean", "Components",
627 "CSSFontFaceStyleDecl", "CSSGroupRuleRuleList", "CSSNameSpaceRule",
628 "CSSRGBColor", "CSSRect", "ComputedCSSStyleDeclaration", "Date", "Error",
629 "EvalError", "File", "Float32Array", "Float64Array", "Function",
630 "HTMLDelElement", "HTMLInsElement", "HTMLSpanElement", "Infinity",
631 "InnerModalContentWindow", "InnerWindow", "Int16Array", "Int32Array",
632 "Int8Array", "InternalError", "Iterator", "JSON", "KeyboardEvent",
633 "Math", "NaN", "Namespace", "Number", "Object", "Proxy", "QName",
634 "ROCSSPrimitiveValue", "RangeError", "ReferenceError", "RegExp",
635 "StopIteration", "String", "SyntaxError", "TypeError", "URIError",
636 "Uint16Array", "Uint32Array", "Uint8Array", "XML", "XMLHttpProgressEvent",
637 "XMLList", "XMLSerializer", "XPCNativeWrapper", "XPCSafeJSWrapper",
638 "XULControllers", "constructor", "decodeURI", "decodeURIComponent",
639 "encodeURI", "encodeURIComponent", "escape", "eval", "isFinite", "isNaN",
640 "isXMLName", "parseFloat", "parseInt", "undefined", "unescape", "uneval"
641 ].concat([k.substr(6) for (k in keys(Ci)) if (/^nsIDOM/.test(k))])
642 .concat([k.substr(3) for (k in keys(Ci)) if (/^nsI/.test(k))])
643 .concat(this.magicalNames)
644 .filter(function (k) k in self.window))),
647 EVAL_TMP: "__dactyl_eval_tmp",
650 * A map of argument completion functions for named methods. The
651 * signature and specification of the completion function
652 * are fairly complex and yet undocumented.
654 * @see JavaScript.setCompleter
659 * Installs argument string completers for a set of functions.
660 * The second argument is an array of functions (or null
661 * values), each corresponding the argument of the same index.
662 * Each provided completion function receives as arguments a
663 * CompletionContext, the 'this' object of the method, and an
664 * array of values for the preceding arguments.
666 * It is important to note that values in the arguments array
667 * provided to the completers are lazily evaluated the first
668 * time they are accessed, so they should be accessed
671 * @param {function|[function]} funcs The functions for which to
672 * install the completers.
673 * @param {[function]} completers An array of completer
676 setCompleter: function (funcs, completers) {
677 funcs = Array.concat(funcs);
678 for (let [, func] in Iterator(funcs)) {
679 func.dactylCompleter = function (context, func, obj, args) {
680 let completer = completers[args.length - 1];
683 return completer.call(obj, context, obj, args);
689 init: function init(dactyl, modules, window) {
690 init.superapply(this, arguments);
691 modules.JavaScript = Class("JavaScript", JavaScript, { modules: modules, window: window });
693 completion: function (dactyl, modules, window) {
694 const { completion } = modules;
695 update(modules.completion, {
696 get javascript() modules.javascript.closure.complete,
697 javascriptCompleter: JavaScript // Backwards compatibility
700 modes: function initModes(dactyl, modules, window) {
701 initModes.require("commandline");
702 const { modes } = modules;
704 modes.addMode("REPL", {
705 description: "JavaScript Read Eval Print Loop",
706 bases: [modes.COMMAND_LINE],
707 displayName: Class.Memoize(function () this.name)
710 commandline: function initCommandLine(dactyl, modules, window) {
711 const { Buffer, modes } = modules;
713 var REPL = Class("REPL", {
714 init: function init(context) {
715 this.context = context;
719 addOutput: function addOutput(js) {
723 var result = dactyl.userEval(js, this.context);
724 var xml = result === undefined ? "" : util.objectToString(result, true);
731 e = util.fixURI(e.fileName) + ":" + e.lineNumber + ": " + e;
732 xml = ["span", { highlight: "ErrorMsg" }, e];
735 let prompt = "js" + this.count;
736 Class.replaceProperty(this.context, prompt, result);
739 this.rootNode.appendChild(
741 [["div", { highlight: "REPL-E", key: "e" },
742 ["span", { highlight: "REPL-R" },
743 prompt, ">"], " ", js],
744 ["div", { highlight: "REPL-P", key: "p" },
746 this.document, nodes));
748 this.rootNode.scrollTop += nodes.e.getBoundingClientRect().top
749 - this.rootNode.getBoundingClientRect().top;
754 message: Class.Memoize(function () {
755 DOM.fromJSON(["div", { highlight: "REPL", key: "rootNode" }],
756 this.document, this);
758 return this.rootNode;
761 __noSuchMethod__: function (meth, args) Buffer[meth].apply(Buffer, [this.rootNode].concat(args))
764 modules.CommandREPLMode = Class("CommandREPLMode", modules.CommandMode, {
765 init: function init(context) {
766 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 = function newContext() modules.newContext(self.context, !sandbox, "Dactyl REPL Temp Context");
777 [this.context, /*L*/"REPL Variables"],
778 [context, /*L*/"REPL Global"]
779 ].concat(this.js.globals.filter(function ([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(function ([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() mappings.add.apply(mappings,
860 [[modes.REPL]].concat(Array.slice(arguments)))
862 bind(["<Return>"], "Accept the current input",
863 function ({ self }) { self.accept(); });
865 bind(["<C-e>"], "Scroll down one line",
866 function ({ self }) { self.repl.scrollVertical("lines", 1); });
868 bind(["<C-y>"], "Scroll up one line",
869 function ({ self }) { self.repl.scrollVertical("lines", -1); });
871 bind(["<C-d>"], "Scroll down half a page",
872 function ({ self }) { self.repl.scrollVertical("pages", .5); });
874 bind(["<C-f>", "<PageDown>"], "Scroll down one page",
875 function ({ self }) { self.repl.scrollVertical("pages", 1); });
877 bind(["<C-u>"], "Scroll up half a page",
878 function ({ self }) { self.repl.scrollVertical("pages", -.5); });
880 bind(["<C-b>", "<PageUp>"], "Scroll up half a page",
881 function ({ self }) { self.repl.scrollVertical("pages", -1); });
883 options: function initOptions(dactyl, modules, window) {
884 modules.options.add(["jsdebugger", "jsd"],
885 "Enable the JavaScript debugger service for use in JavaScript completion",
887 setter: function (value) {
888 if (services.debugger.isOn != value)
890 (services.debugger.asyncOn || services.debugger.on)(null);
892 services.debugger.off();
894 getter: function () services.debugger.isOn
901 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
903 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: