1 // Copyright (c) 2008-2011 by 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 Components.utils.import("resource://dactyl/bootstrap.jsm");
12 defineModule("javascript", {
13 exports: ["JavaScript", "javascript"]
16 let isPrototypeOf = Object.prototype.isPrototypeOf;
18 // TODO: Clean this up.
20 var JavaScript = Module("javascript", {
24 this._top = {}; // The element on the top of the stack.
25 this._last = ""; // The last opening char pushed onto the stack.
26 this._lastNonwhite = ""; // Last non-whitespace character we saw.
27 this._lastChar = ""; // Last character we saw, used for \ escaping quotes.
32 this._cacheKey = null;
34 this._nullSandbox = Cu.Sandbox("about:blank");
37 Local: function (dactyl, modules, window) ({
38 init: function init() {
39 this.modules = modules;
46 globals: Class.Memoize(function () [
47 [this.modules.userContext, /*L*/"Global Variables"],
48 [this.modules, "modules"],
49 [this.window, "window"]
52 toplevel: Class.Memoize(function () this.modules.jsmodules),
56 newContext: function () this.modules.newContext(this.modules.userContext, true),
58 get completers() JavaScript.completers, // For backward compatibility
60 // Some object members are only accessible as function calls
61 getKey: function (obj, key) {
69 iter: function iter_(obj, toplevel) {
73 let seen = isinstance(obj, ["Sandbox"]) ? Set(JavaScript.magicalNames) : {};
74 let globals = values(toplevel && this.window === obj ? this.globalNames : []);
76 if (toplevel && isObject(obj) && "wrappedJSObject" in obj)
77 if (!Set.add(seen, "wrappedJSObject"))
78 yield "wrappedJSObject";
80 for (let key in iter(globals, properties(obj, !toplevel, true)))
81 if (!Set.add(seen, key))
84 // Properties aren't visible in an XPCNativeWrapper until
86 for (let key in properties(this.getKey(obj, "wrappedJSObject"), !toplevel, true))
88 if (key in obj && !Set.has(seen, key))
94 objectKeys: function objectKeys(obj, toplevel) {
95 // Things we can dereference
96 if (!obj || ["object", "string", "function"].indexOf(typeof obj) === -1)
98 if (isinstance(obj, ["Sandbox"]) && !toplevel) // Temporary hack.
100 if (isPrototypeOf.call(this.toplevel, obj) && !toplevel)
103 let completions = [k for (k in this.iter(obj, toplevel))];
104 if (obj === this.modules) // Hack.
105 completions = array.uniq(completions.concat([k for (k in this.iter(this.modules.jsmodules, toplevel))]));
109 evalled: function evalled(arg, key, tmp) {
110 let cache = this.context.cache.evalled;
111 let context = this.context.cache.evalContext;
118 context[JavaScript.EVAL_TMP] = tmp;
120 cache[key] = this.modules.dactyl.userEval(arg, context, /*L*/"[Command Line Completion]", 1);
126 this.context.message = _("error.error", e);
130 delete context[JavaScript.EVAL_TMP];
134 // Get an element from the stack. If @frame is negative,
135 // count from the top of the stack, otherwise, the bottom.
136 // If @nth is provided, return the @mth value of element @type
137 // of the stack entry at @frame.
138 _get: function (frame, nth, type) {
139 let a = this._stack[frame >= 0 ? frame : this._stack.length + frame];
144 return a[a.length - nth - 1];
147 // Push and pop the stack, maintaining references to 'top' and 'last'.
148 _push: function push(arg) {
152 statements: [this._i],
158 this._last = this._top.char;
159 this._stack.push(this._top);
162 _pop: function pop(arg) {
163 if (this._i == this.context.caret - 1)
164 this.context.highlight(this._top.offset, 1, "FIND");
166 if (this._top.char != arg) {
167 this.context.highlight(this._top.offset, this._i - this._top.offset, "SPELLCHECK");
168 throw Error(/*L*/"Invalid JS");
171 // The closing character of this stack frame will have pushed a new
172 // statement, leaving us with an empty statement. This doesn't matter,
173 // now, as we simply throw away the frame when we pop it, but it may later.
174 if (this._top.statements[this._top.statements.length - 1] == this._i)
175 this._top.statements.pop();
176 this._top = this._get(-2);
177 this._last = this._top.char;
178 return this._stack.pop();
181 _buildStack: function (filter) {
182 // Todo: Fix these one-letter variable names.
184 this._c = ""; // Current index and character, respectively.
186 // Reuse the old stack.
187 if (this._str && filter.substr(0, this._str.length) == this._str) {
188 this.context.highlight(0, 0, "FIND");
189 this._i = this._str.length;
190 if (this.popStatement)
191 this._top.statements.pop();
194 this.context.highlight();
196 this._functions = [];
200 // Build a parse stack, discarding entries as opening characters
201 // match closing characters. The stack is walked from the top entry
202 // and down as many levels as it takes us to figure out what it is
203 // that we're completing.
205 let length = this._str.length;
206 for (; this._i < length; this._lastChar = this._c, this._i++) {
207 this._c = this._str[this._i];
208 if (/['"\/]/.test(this._last)) {
209 if (this._lastChar == "\\") { // Escape. Skip the next char, whatever it may be.
213 else if (this._c == this._last)
217 // A word character following a non-word character, or simply a non-word
218 // character. Start a new statement.
219 if (/[a-zA-Z_$]/.test(this._c) && !/[\w$]/.test(this._lastChar) || !/[\w\s$]/.test(this._c))
220 this._top.statements.push(this._i);
222 // A "." or a "[" dereferences the last "statement" and effectively
223 // joins it to this logical statement.
224 if ((this._c == "." || this._c == "[") && /[\w$\])"']/.test(this._lastNonwhite)
225 || this._lastNonwhite == "." && /[a-zA-Z_$]/.test(this._c))
226 this._top.statements.pop();
230 // Function call, or if/while/for/...
231 if (/[\w$]/.test(this._lastNonwhite)) {
232 this._functions.push(this._i);
233 this._top.functions.push(this._i);
234 this._top.statements.pop();
244 this._top.dots.push(this._i);
246 case ")": this._pop("("); break;
247 case "]": this._pop("["); break;
248 case "}": this._pop("{"); // Fallthrough
250 this._top.fullStatements.push(this._i);
253 this._top.comma.push(this._i);
257 if (/\S/.test(this._c))
258 this._lastNonwhite = this._c;
262 this.popStatement = false;
263 if (!/[\w$]/.test(this._lastChar) && this._lastNonwhite != ".") {
264 this.popStatement = true;
265 this._top.statements.push(this._i);
268 this._lastIdx = this._i;
271 // Don't eval any function calls unless the user presses tab.
272 _checkFunction: function (start, end, key) {
273 let res = this._functions.some(function (idx) idx >= start && idx < end);
274 if (!res || this.context.tabPressed || key in this.cache.evalled)
276 this.context.waitingForTab = true;
280 // For each DOT in a statement, prefix it with TMP, eval it,
281 // and save the result back to TMP. The point of this is to
282 // cache the entire path through an object chain, mainly in
283 // the presence of function calls. There are drawbacks. For
284 // instance, if the value of a variable changes in the course
285 // of inputting a command (let foo=bar; frob(foo); foo=foo.bar; ...),
286 // we'll still use the old value. But, it's worth it.
287 _getObj: function (frame, stop) {
288 let statement = this._get(frame, 0, "statements") || 0; // Current statement.
289 let prev = statement;
290 let obj = this.window;
292 for (let [, dot] in Iterator(this._get(frame).dots.concat(stop))) {
295 if (dot > stop || dot <= prev)
298 let s = this._str.substring(prev, dot);
299 if (prev != statement)
300 s = JavaScript.EVAL_TMP + "." + s;
301 cacheKey = this._str.substring(statement, dot);
303 if (this._checkFunction(prev, dot, cacheKey))
305 if (prev != statement && obj == null) {
306 this.context.message = /*L*/"Error: " + cacheKey.quote() + " is " + String(obj);
311 obj = this.evalled(s, cacheKey, obj);
313 return [[obj, cacheKey]];
316 _getObjKey: function (frame) {
317 let dot = this._get(frame, 0, "dots") || -1; // Last dot in frame.
318 let statement = this._get(frame, 0, "statements") || 0; // Current statement.
319 let end = (frame == -1 ? this._lastIdx : this._get(frame + 1).offset);
321 this._cacheKey = null;
322 let obj = [[this.cache.evalContext, /*L*/"Local Variables"]].concat(this.globals);
323 // Is this an object dereference?
324 if (dot < statement) // No.
326 else // Yes. Set the object to the string before the dot.
327 obj = this._getObj(frame, dot);
329 let [, space, key] = this._str.substring(dot + 1, end).match(/^(\s*)(.*)/);
330 return [dot + 1 + space.length, obj, key];
333 _complete: function (objects, key, compl, string, last) {
336 if (!getOwnPropertyNames && !services.debugger.isOn && !this.context.message)
337 this.context.message = /*L*/"For better completion data, please enable the JavaScript debugger (:set jsdebugger)";
339 let base = this.context.fork("js", this._top.offset);
340 base.forceAnchored = true;
341 base.filter = last == null ? key : string;
342 let prefix = last != null ? key : "";
344 if (last == null) // We're not looking for a quoted string, so filter out anything that's not a valid identifier
345 base.filters.push(function (item) /^[a-zA-Z_$][\w$]*$/.test(item.text));
347 base.quote = [last, function (text) util.escapeString(text, ""), last];
349 base.filters.push(function (item) item.item.indexOf(prefix) === 0);
353 base.process[1] = function highlight(item, v)
354 template.highlight(typeof v == "xml" ? new String(v.toXMLString()) : v, true, 200);
356 // Sort in a logical fashion for object keys:
357 // Numbers are sorted as numbers, rather than strings, and appear first.
358 // Constants are unsorted, and appear before other non-null strings.
359 // Other strings are sorted in the default manner.
361 let isnan = function isnan(item) item != '' && isNaN(item);
362 let compare = base.compare;
364 base.compare = function (a, b) {
365 if (!isnan(a.key) && !isnan(b.key))
366 return a.key - b.key;
367 return isnan(b.key) - isnan(a.key) || compare(a, b);
371 text: prefix ? function (text) text.substr(prefix.length) : util.identity,
372 description: function (item) self.getKey(this.obj, item),
373 key: function (item) {
375 return parseInt(key);
376 if (/^[A-Z_][A-Z0-9_]*$/.test(key))
383 // We've already listed anchored matches, so don't list them again here.
384 function unanchored(item) util.compareIgnoreCase(item.text.substr(0, this.filter.length), this.filter);
386 objects.forEach(function (obj) {
387 let context = base.fork(obj[1]);
388 context.title = [obj[1]];
389 context.keys.obj = function () obj[0];
390 context.key = obj[1] + last;
391 if (obj[0] == this.cache.evalContext)
392 context.regenerate = true;
394 obj.ctxt_t = context.fork("toplevel");
396 obj.ctxt_p = context.fork("prototypes");
397 obj.ctxt_t.generate = function () self.objectKeys(obj[0], true);
398 obj.ctxt_p.generate = function () self.objectKeys(obj[0], false);
402 // TODO: Make this a generic completion helper function.
403 objects.forEach(function (obj) {
404 obj.ctxt_t.split(obj[1] + "/anchored", this, function (context) {
405 context.anchored = true;
407 compl(context, obj[0]);
414 objects.forEach(function (obj) {
415 obj.ctxt_p.split(obj[1] + "/anchored", this, function (context) {
416 context.anchored = true;
417 context.title[0] += /*L*/" (prototypes)";
421 objects.forEach(function (obj) {
422 obj.ctxt_t.split(obj[1] + "/unanchored", this, function (context) {
423 context.anchored = false;
424 context.title[0] += /*L*/" (substrings)";
425 context.filters.push(unanchored);
429 objects.forEach(function (obj) {
430 obj.ctxt_p.split(obj[1] + "/unanchored", this, function (context) {
431 context.anchored = false;
432 context.title[0] += /*L*/" (prototype substrings)";
433 context.filters.push(unanchored);
438 _getKey: function () {
439 if (this._last == "")
441 // After the opening [ upto the opening ", plus '' to take care of any operators before it
442 let key = this._str.substring(this._get(-2, null, "offset") + 1, this._get(-1, null, "offset")) + "''";
443 // Now eval the key, to process any referenced variables.
444 return this.evalled(key);
447 get cache() this.context.cache,
449 complete: function _complete(context) {
451 this.context = context;
454 this._buildStack.call(this, context.filter);
458 util.assert(!e.message, e.message);
462 this.context.getCache("evalled", Object);
463 this.context.getCache("evalContext", this.closure.newContext);
465 // Okay, have parse stack. Figure out what we're completing.
467 // Find any complete statements that we can eval before we eval our object.
468 // This allows for things like:
469 // let doc = content.document; let elem = doc.createEle<Tab> ...
471 for (let [, v] in Iterator(this._get(0).fullStatements)) {
472 let key = this._str.substring(prev, v + 1);
473 if (this._checkFunction(prev, v, key))
479 // If this is a function argument, try to get the function's
480 // prototype and show it.
482 let i = (this._get(-2) && this._get(-2).char == "(") ? -2 : -1;
483 if (this._get(i).char == "(") {
484 let [offset, obj, funcName] = this._getObjKey(i - 1);
486 let func = obj[0][0][funcName];
487 if (callable(func)) {
488 let [, prefix, args] = /^(function .*?)\((.*?)\)/.exec(Function.prototype.toString.call(func));
489 let n = this._get(i).comma.length;
490 args = template.map(Iterator(args.split(", ")),
491 function ([i, arg]) <span highlight={i == n ? "Filter" : ""}>{arg}</span>,
493 this.context.message = <>{prefix}({args})</>;
500 // In a string. Check if we're dereferencing an object or
501 // completing a function argument. Otherwise, do nothing.
502 if (this._last == "'" || this._last == '"') {
504 // str = "foo[bar + 'baz"
508 // The top of the stack is the sting we're completing.
509 // Wrap it in its delimiters and eval it to process escape sequences.
510 let string = this._str.substring(this._get(-1).offset + 1, this._lastIdx).replace(/((?:\\\\)*)\\/, "$1");
511 string = Cu.evalInSandbox(this._last + string + this._last, this._nullSandbox);
513 // Is this an object accessor?
514 if (this._get(-2).char == "[") { // Are we inside of []?
518 // [-3]: base statement
520 // Yes. If the [ starts at the beginning of a logical
521 // statement, we're in an array literal, and we're done.
522 if (this._get(-3, 0, "statements") == this._get(-2).offset)
525 // Beginning of the statement upto the opening [
526 let obj = this._getObj(-3, this._get(-2).offset);
528 return this._complete(obj, this._getKey(), null, string, this._last);
531 // Is this a function call?
532 if (this._get(-2).char == "(") {
536 // [-3]: base statement
538 // Does the opening "(" mark a function call?
539 if (this._get(-3, 0, "functions") != this._get(-2).offset)
540 return null; // No. We're done.
542 let [offset, obj, funcName] = this._getObjKey(-3);
545 obj = obj.slice(0, 1);
548 let func = obj[0][0][funcName];
549 var completer = func.dactylCompleter;
553 completer = JavaScript.completers[funcName];
557 // Split up the arguments
558 let prev = this._get(-2).offset;
560 for (let [i, idx] in Iterator(this._get(-2).comma)) {
561 let arg = this._str.substring(prev + 1, idx);
563 memoize(args, i, function () self.evalled(arg));
565 let key = this._getKey();
566 args.push(key + string);
568 let compl = function (context, obj) {
569 let res = completer.call(self, context, funcName, obj, args);
571 context.completions = res;
574 obj[0][1] += "." + funcName + "(... [" + args.length + "]";
575 return this._complete(obj, key, compl, string, this._last);
578 // In a string that's not an obj key or a function arg.
583 // str = "foo.bar.baz"
588 // obj = [modules, window]
591 let [offset, obj, key] = this._getObjKey(-1);
593 // Wait for a keypress before completing when there's no key
594 if (!this.context.tabPressed && key == "" && obj.length > 1) {
595 let message = this.context.message || "";
596 this.context.waitingForTab = true;
597 this.context.message = <>{message}
598 {_("completion.waitingForKeyPress")}</>;
602 if (!/^(?:[a-zA-Z_$][\w$]*)?$/.test(key))
603 return null; // Not a word. Forget it. Can this even happen?
606 var o = this._top.offset;
607 this._top.offset = offset;
608 return this._complete(obj, key);
611 this._top.offset = o;
616 magicalNames: Class.Memoize(function () Object.getOwnPropertyNames(Cu.Sandbox(this.window), true).sort()),
619 * A list of properties of the global object which are not
620 * enumerable by any standard method.
622 globalNames: Class.Memoize(function () let (self = this) array.uniq([
623 "Array", "ArrayBuffer", "AttributeName", "Audio", "Boolean", "Components",
624 "CSSFontFaceStyleDecl", "CSSGroupRuleRuleList", "CSSNameSpaceRule",
625 "CSSRGBColor", "CSSRect", "ComputedCSSStyleDeclaration", "Date", "Error",
626 "EvalError", "File", "Float32Array", "Float64Array", "Function",
627 "HTMLDelElement", "HTMLInsElement", "HTMLSpanElement", "Infinity",
628 "InnerModalContentWindow", "InnerWindow", "Int16Array", "Int32Array",
629 "Int8Array", "InternalError", "Iterator", "JSON", "KeyboardEvent",
630 "Math", "NaN", "Namespace", "Number", "Object", "Proxy", "QName",
631 "ROCSSPrimitiveValue", "RangeError", "ReferenceError", "RegExp",
632 "StopIteration", "String", "SyntaxError", "TypeError", "URIError",
633 "Uint16Array", "Uint32Array", "Uint8Array", "XML", "XMLHttpProgressEvent",
634 "XMLList", "XMLSerializer", "XPCNativeWrapper", "XPCSafeJSWrapper",
635 "XULControllers", "constructor", "decodeURI", "decodeURIComponent",
636 "encodeURI", "encodeURIComponent", "escape", "eval", "isFinite", "isNaN",
637 "isXMLName", "parseFloat", "parseInt", "undefined", "unescape", "uneval"
638 ].concat([k.substr(6) for (k in keys(Ci)) if (/^nsIDOM/.test(k))])
639 .concat([k.substr(3) for (k in keys(Ci)) if (/^nsI/.test(k))])
640 .concat(this.magicalNames)
641 .filter(function (k) k in self.window))),
644 EVAL_TMP: "__dactyl_eval_tmp",
647 * A map of argument completion functions for named methods. The
648 * signature and specification of the completion function
649 * are fairly complex and yet undocumented.
651 * @see JavaScript.setCompleter
656 * Installs argument string completers for a set of functions.
657 * The second argument is an array of functions (or null
658 * values), each corresponding the argument of the same index.
659 * Each provided completion function receives as arguments a
660 * CompletionContext, the 'this' object of the method, and an
661 * array of values for the preceding arguments.
663 * It is important to note that values in the arguments array
664 * provided to the completers are lazily evaluated the first
665 * time they are accessed, so they should be accessed
668 * @param {function|[function]} funcs The functions for which to
669 * install the completers.
670 * @param {[function]} completers An array of completer
673 setCompleter: function (funcs, completers) {
674 funcs = Array.concat(funcs);
675 for (let [, func] in Iterator(funcs)) {
676 func.dactylCompleter = function (context, func, obj, args) {
677 let completer = completers[args.length - 1];
680 return completer.call(obj, context, obj, args);
686 init: function init(dactyl, modules, window) {
687 init.superapply(this, arguments);
688 modules.JavaScript = Class("JavaScript", JavaScript, { modules: modules, window: window });
690 completion: function (dactyl, modules, window) {
691 const { completion } = modules;
692 update(modules.completion, {
693 get javascript() modules.javascript.closure.complete,
694 javascriptCompleter: JavaScript // Backwards compatibility
697 modes: function initModes(dactyl, modules, window) {
698 initModes.require("commandline");
699 const { modes } = modules;
701 modes.addMode("REPL", {
702 description: "JavaScript Read Eval Print Loop",
703 bases: [modes.COMMAND_LINE],
704 displayName: Class.Memoize(function () this.name)
707 commandline: function initCommandLine(dactyl, modules, window) {
708 const { Buffer, modes } = modules;
710 var REPL = Class("REPL", {
711 init: function init(context) {
712 this.context = context;
716 addOutput: function addOutput(js) {
717 default xml namespace = XHTML;
721 var result = dactyl.userEval(js, this.context);
722 var xml = result === undefined ? "" : util.objectToString(result, true);
729 e = util.fixURI(e.fileName) + ":" + e.lineNumber + ": " + e;
730 xml = <span highlight="ErrorMsg">{e}</span>;
733 let prompt = "js" + this.count;
734 Class.replaceProperty(this.context, prompt, result);
736 XML.ignoreWhitespace = XML.prettyPrinting = false;
738 this.rootNode.appendChild(
740 <div highlight="REPL-E" key="e"><span highlight="REPL-R">{prompt}></span> {js}</div>
741 <div highlight="REPL-P" key="p">{xml}</div>
742 </e4x>.elements(), this.document, nodes));
744 this.rootNode.scrollTop += nodes.e.getBoundingClientRect().top
745 - this.rootNode.getBoundingClientRect().top;
750 message: Class.Memoize(function () {
751 default xml namespace = XHTML;
752 util.xmlToDom(<div highlight="REPL" key="rootNode"/>,
753 this.document, this);
755 return this.rootNode;
758 __noSuchMethod__: function (meth, args) Buffer[meth].apply(Buffer, [this.rootNode].concat(args))
761 modules.CommandREPLMode = Class("CommandREPLMode", modules.CommandMode, {
762 init: function init(context) {
763 init.supercall(this);
766 let sandbox = true || isinstance(context, ["Sandbox"]);
768 this.context = modules.newContext(context, !sandbox);
769 this.js = modules.JavaScript();
770 this.js.replContext = this.context;
771 this.js.newContext = function newContext() modules.newContext(self.context, !sandbox);
774 [this.context, /*L*/"REPL Variables"],
775 [context, /*L*/"REPL Global"]
776 ].concat(this.js.globals.filter(function ([global]) isPrototypeOf.call(global, context)));
778 if (!isPrototypeOf.call(modules.jsmodules, context))
779 this.js.toplevel = context;
781 if (!isPrototypeOf.call(window, context))
782 this.js.window = context;
784 if (this.js.globals.slice(2).some(function ([global]) global === context))
785 this.js.globals.splice(1);
787 this.repl = REPL(this.context);
790 open: function open(context) {
792 modules.mow.echo(this.repl);
793 this.widgets.message = null;
795 open.superapply(this, arguments);
799 complete: function complete(context) {
800 context.fork("js", 0, this.js, "complete");
803 historyKey: "javascript",
807 get completionList() this.widgets.statusbar.commandline.id,
809 accept: function accept() {
810 dactyl.trapErrors(function () { this.repl.addOutput(this.command); }, this);
812 this.completions.cleanup();
814 this.history.reset();
817 modules.mow.resize();
820 leave: function leave(params) {
821 leave.superapply(this, arguments);
823 modes.delay(function () { modes.pop(); });
826 updatePrompt: function updatePrompt() {
828 this.prompt = ["REPL-R", "js" + (this.repl.count + 1) + "> "];
832 commands: function initCommands(dactyl, modules, window) {
833 const { commands } = modules;
835 commands.add(["javas[cript]", "js"],
836 "Evaluate a JavaScript string",
838 if (args[0] && !args.bang)
839 dactyl.userEval(args[0]);
842 modules.CommandREPLMode(args[0] ? dactyl.userEval(args[0]) : modules.userContext)
848 completer: function (context) modules.completion.javascript(context),
853 mappings: function initMappings(dactyl, modules, window) {
854 const { mappings, modes } = modules;
856 function bind() mappings.add.apply(mappings,
857 [[modes.REPL]].concat(Array.slice(arguments)))
859 bind(["<Return>"], "Accept the current input",
860 function ({ self }) { self.accept(); });
862 bind(["<C-e>"], "Scroll down one line",
863 function ({ self }) { self.repl.scrollVertical("lines", 1); });
865 bind(["<C-y>"], "Scroll up one line",
866 function ({ self }) { self.repl.scrollVertical("lines", -1); });
868 bind(["<C-d>"], "Scroll down half a page",
869 function ({ self }) { self.repl.scrollVertical("pages", .5); });
871 bind(["<C-f>", "<PageDown>"], "Scroll down one page",
872 function ({ self }) { self.repl.scrollVertical("pages", 1); });
874 bind(["<C-u>"], "Scroll up half a page",
875 function ({ self }) { self.repl.scrollVertical("pages", -.5); });
877 bind(["<C-b>", "<PageUp>"], "Scroll up half a page",
878 function ({ self }) { self.repl.scrollVertical("pages", -1); });
880 options: function (dactyl, modules, window) {
881 modules.options.add(["jsdebugger", "jsd"],
882 "Enable the JavaScript debugger service for use in JavaScript completion",
884 setter: function (value) {
885 if (services.debugger.isOn != value)
887 (services.debugger.asyncOn || services.debugger.on)(null);
889 services.debugger.off();
891 getter: function () services.debugger.isOn
898 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
900 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: