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"],
14 use: ["services", "template", "util"]
17 let isPrototypeOf = Object.prototype.isPrototypeOf;
19 // TODO: Clean this up.
21 var JavaScript = Module("javascript", {
25 this._top = {}; // The element on the top of the stack.
26 this._last = ""; // The last opening char pushed onto the stack.
27 this._lastNonwhite = ""; // Last non-whitespace character we saw.
28 this._lastChar = ""; // Last character we saw, used for \ escaping quotes.
33 this._cacheKey = null;
35 this._nullSandbox = Cu.Sandbox("about:blank");
38 Local: function (dactyl, modules, window) ({
39 init: function init() {
40 this.modules = modules;
47 globals: Class.memoize(function () [
48 [this.modules.userContext, "Global Variables"],
49 [this.modules, "modules"],
50 [this.window, "window"]
53 toplevel: Class.memoize(function () this.modules.jsmodules),
57 newContext: function () this.modules.newContext(this.modules.userContext),
59 get completers() JavaScript.completers, // For backward compatibility
61 // Some object members are only accessible as function calls
62 getKey: function (obj, key) {
70 iter: function iter_(obj, toplevel) {
74 let seen = isinstance(obj, ["Sandbox"]) ? set(JavaScript.magicalNames) : {};
75 let globals = values(toplevel && this.window === obj ? this.globalNames : []);
77 if (toplevel && isObject(obj) && "wrappedJSObject" in obj)
78 if (!set.add(seen, "wrappedJSObject"))
79 yield "wrappedJSObject";
81 for (let key in iter(globals, properties(obj, !toplevel, true)))
82 if (!set.add(seen, key))
85 // Properties aren't visible in an XPCNativeWrapper until
87 for (let key in properties(this.getKey(obj, "wrappedJSObject"), !toplevel, true))
89 if (key in obj && !set.has(seen, key))
95 objectKeys: function objectKeys(obj, toplevel) {
96 // Things we can dereference
97 if (!obj || ["object", "string", "function"].indexOf(typeof obj) === -1)
99 if (isinstance(obj, ["Sandbox"]) && !toplevel) // Temporary hack.
101 if (isPrototypeOf.call(this.toplevel, obj) && !toplevel)
104 let completions = [k for (k in this.iter(obj, toplevel))];
105 if (obj === this.modules) // Hack.
106 completions = completions.concat([k for (k in this.iter(this.modules.jsmodules, toplevel))]);
110 evalled: function evalled(arg, key, tmp) {
111 let cache = this.context.cache.evalled;
112 let context = this.context.cache.evalContext;
119 context[JavaScript.EVAL_TMP] = tmp;
120 context[JavaScript.EVAL_EXPORT] = function export_(obj) cache[key] = obj;
122 if (tmp != null) // Temporary hack until bug 609949 is fixed.
123 this.modules.dactyl.userEval(JavaScript.EVAL_EXPORT + "(" + arg + ")", context, "[Command Line Completion]", 1);
125 cache[key] = this.modules.dactyl.userEval(arg, context, "[Command Line Completion]", 1);
131 this.context.message = "Error: " + e;
135 delete context[JavaScript.EVAL_TMP];
139 // Get an element from the stack. If @frame is negative,
140 // count from the top of the stack, otherwise, the bottom.
141 // If @nth is provided, return the @mth value of element @type
142 // of the stack entry at @frame.
143 _get: function (frame, nth, type) {
144 let a = this._stack[frame >= 0 ? frame : this._stack.length + frame];
149 return a[a.length - nth - 1];
152 // Push and pop the stack, maintaining references to 'top' and 'last'.
153 _push: function push(arg) {
157 statements: [this._i],
163 this._last = this._top.char;
164 this._stack.push(this._top);
167 _pop: function pop(arg) {
168 if (this._i == this.context.caret - 1)
169 this.context.highlight(this._top.offset, 1, "FIND");
171 if (this._top.char != arg) {
172 this.context.highlight(this._top.offset, this._i - this._top.offset, "SPELLCHECK");
173 throw Error("Invalid JS");
176 // The closing character of this stack frame will have pushed a new
177 // statement, leaving us with an empty statement. This doesn't matter,
178 // now, as we simply throw away the frame when we pop it, but it may later.
179 if (this._top.statements[this._top.statements.length - 1] == this._i)
180 this._top.statements.pop();
181 this._top = this._get(-2);
182 this._last = this._top.char;
183 return this._stack.pop();
186 _buildStack: function (filter) {
187 // Todo: Fix these one-letter variable names.
189 this._c = ""; // Current index and character, respectively.
191 // Reuse the old stack.
192 if (this._str && filter.substr(0, this._str.length) == this._str) {
193 this.context.highlight(0, 0, "FIND");
194 this._i = this._str.length;
195 if (this.popStatement)
196 this._top.statements.pop();
199 this.context.highlight();
201 this._functions = [];
205 // Build a parse stack, discarding entries as opening characters
206 // match closing characters. The stack is walked from the top entry
207 // and down as many levels as it takes us to figure out what it is
208 // that we're completing.
210 let length = this._str.length;
211 for (; this._i < length; this._lastChar = this._c, this._i++) {
212 this._c = this._str[this._i];
213 if (/['"\/]/.test(this._last)) {
214 if (this._lastChar == "\\") { // Escape. Skip the next char, whatever it may be.
218 else if (this._c == this._last)
222 // A word character following a non-word character, or simply a non-word
223 // character. Start a new statement.
224 if (/[a-zA-Z_$]/.test(this._c) && !/[\w$]/.test(this._lastChar) || !/[\w\s$]/.test(this._c))
225 this._top.statements.push(this._i);
227 // A "." or a "[" dereferences the last "statement" and effectively
228 // joins it to this logical statement.
229 if ((this._c == "." || this._c == "[") && /[\w$\])"']/.test(this._lastNonwhite)
230 || this._lastNonwhite == "." && /[a-zA-Z_$]/.test(this._c))
231 this._top.statements.pop();
235 // Function call, or if/while/for/...
236 if (/[\w$]/.test(this._lastNonwhite)) {
237 this._functions.push(this._i);
238 this._top.functions.push(this._i);
239 this._top.statements.pop();
249 this._top.dots.push(this._i);
251 case ")": this._pop("("); break;
252 case "]": this._pop("["); break;
253 case "}": this._pop("{"); // Fallthrough
255 this._top.fullStatements.push(this._i);
258 this._top.comma.push(this._i);
262 if (/\S/.test(this._c))
263 this._lastNonwhite = this._c;
267 this.popStatement = false;
268 if (!/[\w$]/.test(this._lastChar) && this._lastNonwhite != ".") {
269 this.popStatement = true;
270 this._top.statements.push(this._i);
273 this._lastIdx = this._i;
276 // Don't eval any function calls unless the user presses tab.
277 _checkFunction: function (start, end, key) {
278 let res = this._functions.some(function (idx) idx >= start && idx < end);
279 if (!res || this.context.tabPressed || key in this.cache.evalled)
281 this.context.waitingForTab = true;
285 // For each DOT in a statement, prefix it with TMP, eval it,
286 // and save the result back to TMP. The point of this is to
287 // cache the entire path through an object chain, mainly in
288 // the presence of function calls. There are drawbacks. For
289 // instance, if the value of a variable changes in the course
290 // of inputting a command (let foo=bar; frob(foo); foo=foo.bar; ...),
291 // we'll still use the old value. But, it's worth it.
292 _getObj: function (frame, stop) {
293 let statement = this._get(frame, 0, "statements") || 0; // Current statement.
294 let prev = statement;
295 let obj = this.window;
297 for (let [, dot] in Iterator(this._get(frame).dots.concat(stop))) {
300 if (dot > stop || dot <= prev)
303 let s = this._str.substring(prev, dot);
304 if (prev != statement)
305 s = JavaScript.EVAL_TMP + "." + s;
306 cacheKey = this._str.substring(statement, dot);
308 if (this._checkFunction(prev, dot, cacheKey))
310 if (prev != statement && obj == null) {
311 this.context.message = "Error: " + cacheKey.quote() + " is " + String(obj);
316 obj = this.evalled(s, cacheKey, obj);
318 return [[obj, cacheKey]];
321 _getObjKey: function (frame) {
322 let dot = this._get(frame, 0, "dots") || -1; // Last dot in frame.
323 let statement = this._get(frame, 0, "statements") || 0; // Current statement.
324 let end = (frame == -1 ? this._lastIdx : this._get(frame + 1).offset);
326 this._cacheKey = null;
327 let obj = [[this.cache.evalContext, "Local Variables"]].concat(this.globals);
328 // Is this an object dereference?
329 if (dot < statement) // No.
331 else // Yes. Set the object to the string before the dot.
332 obj = this._getObj(frame, dot);
334 let [, space, key] = this._str.substring(dot + 1, end).match(/^(\s*)(.*)/);
335 return [dot + 1 + space.length, obj, key];
338 _complete: function (objects, key, compl, string, last) {
341 if (!getOwnPropertyNames && !services.debugger.isOn && !this.context.message)
342 this.context.message = "For better completion data, please enable the JavaScript debugger (:set jsdebugger)";
344 let base = this.context.fork("js", this._top.offset);
345 base.forceAnchored = true;
346 base.filter = last == null ? key : string;
347 let prefix = last != null ? key : "";
349 if (last == null) // We're not looking for a quoted string, so filter out anything that's not a valid identifier
350 base.filters.push(function (item) /^[a-zA-Z_$][\w$]*$/.test(item.text));
352 base.quote = [last, function (text) util.escapeString(text, ""), last];
354 base.filters.push(function (item) item.item.indexOf(prefix) === 0);
358 base.process[1] = function highlight(item, v)
359 template.highlight(typeof v == "xml" ? new String(v.toXMLString()) : v, true, 200);
361 // Sort in a logical fashion for object keys:
362 // Numbers are sorted as numbers, rather than strings, and appear first.
363 // Constants are unsorted, and appear before other non-null strings.
364 // Other strings are sorted in the default manner.
366 let isnan = function isnan(item) item != '' && isNaN(item);
367 let compare = base.compare;
369 base.compare = function (a, b) {
370 if (!isnan(a.key) && !isnan(b.key))
371 return a.key - b.key;
372 return isnan(b.key) - isnan(a.key) || compare(a, b);
376 text: prefix ? function (text) text.substr(prefix.length) : util.identity,
377 description: function (item) self.getKey(this.obj, item),
378 key: function (item) {
380 return parseInt(key);
381 if (/^[A-Z_][A-Z0-9_]*$/.test(key))
388 // We've already listed anchored matches, so don't list them again here.
389 function unanchored(item) util.compareIgnoreCase(item.text.substr(0, this.filter.length), this.filter);
391 objects.forEach(function (obj) {
392 let context = base.fork(obj[1]);
393 context.title = [obj[1]];
394 context.keys.obj = function () obj[0];
395 context.key = obj[1] + last;
396 if (obj[0] == this.cache.evalContext)
397 context.regenerate = true;
399 obj.ctxt_t = context.fork("toplevel");
401 obj.ctxt_p = context.fork("prototypes");
402 obj.ctxt_t.generate = function () self.objectKeys(obj[0], true);
403 obj.ctxt_p.generate = function () self.objectKeys(obj[0], false);
407 // TODO: Make this a generic completion helper function.
408 objects.forEach(function (obj) {
409 obj.ctxt_t.split(obj[1] + "/anchored", this, function (context) {
410 context.anchored = true;
412 compl(context, obj[0]);
419 objects.forEach(function (obj) {
420 obj.ctxt_p.split(obj[1] + "/anchored", this, function (context) {
421 context.anchored = true;
422 context.title[0] += " (prototypes)";
426 objects.forEach(function (obj) {
427 obj.ctxt_t.split(obj[1] + "/unanchored", this, function (context) {
428 context.anchored = false;
429 context.title[0] += " (substrings)";
430 context.filters.push(unanchored);
434 objects.forEach(function (obj) {
435 obj.ctxt_p.split(obj[1] + "/unanchored", this, function (context) {
436 context.anchored = false;
437 context.title[0] += " (prototype substrings)";
438 context.filters.push(unanchored);
443 _getKey: function () {
444 if (this._last == "")
446 // After the opening [ upto the opening ", plus '' to take care of any operators before it
447 let key = this._str.substring(this._get(-2, null, "offset") + 1, this._get(-1, null, "offset")) + "''";
448 // Now eval the key, to process any referenced variables.
449 return this.evalled(key);
452 get cache() this.context.cache,
454 complete: function _complete(context) {
456 this.context = context;
459 this._buildStack.call(this, context.filter);
463 util.assert(!e.message, e.message);
467 this.context.getCache("evalled", Object);
468 this.context.getCache("evalContext", this.closure.newContext);
470 // Okay, have parse stack. Figure out what we're completing.
472 // Find any complete statements that we can eval before we eval our object.
473 // This allows for things like:
474 // let doc = content.document; let elem = doc.createEle<Tab> ...
476 for (let [, v] in Iterator(this._get(0).fullStatements)) {
477 let key = this._str.substring(prev, v + 1);
478 if (this._checkFunction(prev, v, key))
484 // If this is a function argument, try to get the function's
485 // prototype and show it.
487 let i = (this._get(-2) && this._get(-2).char == "(") ? -2 : -1;
488 if (this._get(i).char == "(") {
489 let [offset, obj, funcName] = this._getObjKey(i - 1);
491 let func = obj[0][0][funcName];
492 if (callable(func)) {
493 let [, prefix, args] = /^(function .*?)\((.*?)\)/.exec(Function.prototype.toString.call(func));
494 let n = this._get(i).comma.length;
495 args = template.map(Iterator(args.split(", ")),
496 function ([i, arg]) <span highlight={i == n ? "Filter" : ""}>{arg}</span>,
498 this.context.message = <>{prefix}({args})</>;
505 // In a string. Check if we're dereferencing an object or
506 // completing a function argument. Otherwise, do nothing.
507 if (this._last == "'" || this._last == '"') {
509 // str = "foo[bar + 'baz"
513 // The top of the stack is the sting we're completing.
514 // Wrap it in its delimiters and eval it to process escape sequences.
515 let string = this._str.substring(this._get(-1).offset + 1, this._lastIdx).replace(/((?:\\\\)*)\\/, "$1");
516 string = Cu.evalInSandbox(this._last + string + this._last, this._nullSandbox);
518 // Is this an object accessor?
519 if (this._get(-2).char == "[") { // Are we inside of []?
523 // [-3]: base statement
525 // Yes. If the [ starts at the beginning of a logical
526 // statement, we're in an array literal, and we're done.
527 if (this._get(-3, 0, "statements") == this._get(-2).offset)
530 // Beginning of the statement upto the opening [
531 let obj = this._getObj(-3, this._get(-2).offset);
533 return this._complete(obj, this._getKey(), null, string, this._last);
536 // Is this a function call?
537 if (this._get(-2).char == "(") {
541 // [-3]: base statement
543 // Does the opening "(" mark a function call?
544 if (this._get(-3, 0, "functions") != this._get(-2).offset)
545 return null; // No. We're done.
547 let [offset, obj, funcName] = this._getObjKey(-3);
550 obj = obj.slice(0, 1);
553 let func = obj[0][0][funcName];
554 var completer = func.dactylCompleter;
558 completer = JavaScript.completers[funcName];
562 // Split up the arguments
563 let prev = this._get(-2).offset;
565 for (let [i, idx] in Iterator(this._get(-2).comma)) {
566 let arg = this._str.substring(prev + 1, idx);
568 memoize(args, i, function () self.evalled(arg));
570 let key = this._getKey();
571 args.push(key + string);
573 let compl = function (context, obj) {
574 let res = completer.call(self, context, funcName, obj, args);
576 context.completions = res;
579 obj[0][1] += "." + funcName + "(... [" + args.length + "]";
580 return this._complete(obj, key, compl, string, this._last);
583 // In a string that's not an obj key or a function arg.
588 // str = "foo.bar.baz"
593 // obj = [modules, window]
596 let [offset, obj, key] = this._getObjKey(-1);
598 // Wait for a keypress before completing when there's no key
599 if (!this.context.tabPressed && key == "" && obj.length > 1) {
600 this.context.waitingForTab = true;
601 this.context.message = "Waiting for key press";
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", "Boolean", "Components",
627 "CSSFontFaceStyleDecl", "CSSGroupRuleRuleList", "CSSNameSpaceRule",
628 "CSSRGBColor", "CSSRect", "ComputedCSSStyleDeclaration", "Date",
629 "Error", "EvalError", "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",
637 "XMLHttpProgressEvent", "XMLList", "XMLSerializer", "XPCNativeWrapper",
638 "XPCSafeJSWrapper", "XULControllers", "constructor", "decodeURI",
639 "decodeURIComponent", "encodeURI", "encodeURIComponent", "escape",
640 "eval", "isFinite", "isNaN", "isXMLName", "parseFloat", "parseInt",
641 "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(function (k) k in self.window))),
648 EVAL_TMP: "__dactyl_eval_tmp",
649 EVAL_EXPORT: "__dactyl_eval_export",
652 * A map of argument completion functions for named methods. The
653 * signature and specification of the completion function
654 * are fairly complex and yet undocumented.
656 * @see JavaScript.setCompleter
661 * Installs argument string completers for a set of functions.
662 * The second argument is an array of functions (or null
663 * values), each corresponding the argument of the same index.
664 * Each provided completion function receives as arguments a
665 * CompletionContext, the 'this' object of the method, and an
666 * array of values for the preceding arguments.
668 * It is important to note that values in the arguments array
669 * provided to the completers are lazily evaluated the first
670 * time they are accessed, so they should be accessed
673 * @param {function|function[]} funcs The functions for which to
674 * install the completers.
675 * @param {function[]} completers An array of completer
678 setCompleter: function (funcs, completers) {
679 funcs = Array.concat(funcs);
680 for (let [, func] in Iterator(funcs)) {
681 func.dactylCompleter = function (context, func, obj, args) {
682 let completer = completers[args.length - 1];
685 return completer.call(obj, context, obj, args);
691 init: function init(dactyl, modules, window) {
692 init.superapply(this, arguments);
693 modules.JavaScript = Class("JavaScript", JavaScript, { modules: modules, window: window });
695 completion: function (dactyl, modules, window) {
696 const { completion } = modules;
697 update(modules.completion, {
698 get javascript() modules.javascript.closure.complete,
699 javascriptCompleter: JavaScript // Backwards compatibility
702 modes: function initModes(dactyl, modules, window) {
703 initModes.require("commandline");
704 const { modes } = modules;
706 modes.addMode("REPL", {
707 description: "JavaScript Read Eval Print Loop",
708 bases: [modes.COMMAND_LINE]
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) {
721 default xml namespace = XHTML;
725 var result = dactyl.userEval(js, this.context);
726 var xml = util.objectToString(result, true);
733 e = util.fixURI(e.fileName) + ":" + e.lineNumber + ": " + e;
734 xml = <span highlight="ErrorMsg">{e}</span>;
737 let prompt = "js" + this.count;
738 Class.replaceProperty(this.context, prompt, result);
740 XML.ignoreWhitespace = XML.prettyPrinting = false;
742 this.rootNode.appendChild(
744 <div highlight="REPL-E" key="e"><span highlight="REPL-R">{prompt}></span> {js}</div>
745 <div highlight="REPL-P" key="p">{xml}</div>
746 </e4x>.elements(), this.document, nodes));
748 this.rootNode.scrollTop += nodes.e.getBoundingClientRect().top
749 - this.rootNode.getBoundingClientRect().top;
754 message: Class.memoize(function () {
755 default xml namespace = XHTML;
756 util.xmlToDom(<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);
770 let sandbox = isinstance(context, ["Sandbox"]);
772 this.context = modules.newContext(context, !sandbox);
773 this.js = modules.JavaScript();
774 this.js.replContext = this.context;
775 this.js.newContext = function newContext() modules.newContext(self.context, !sandbox);
778 [this.context, "REPL Variables"],
779 [context, "REPL Global"]
780 ].concat(this.js.globals.filter(function ([global]) isPrototypeOf.call(global, context)));
782 if (!isPrototypeOf.call(modules.jsmodules, context))
783 this.js.toplevel = context;
785 if (!isPrototypeOf.call(window, context))
786 this.js.window = context;
788 if (this.js.globals.slice(2).some(function ([global]) global === context))
789 this.js.globals.splice(1);
791 this.repl = REPL(this.context);
793 open: function open(context) {
796 modules.mow.echo(this.repl);
797 this.widgets.message = null;
799 open.superapply(this, arguments);
802 complete: function complete(context) {
803 context.fork("js", 0, this.js, "complete");
806 historyKey: "javascript",
810 accept: function accept() {
811 dactyl.trapErrors(function () { this.repl.addOutput(this.command) }, this);
813 this.completions.cleanup();
815 this.history.reset();
818 modules.mow.resize();
821 leave: function leave(params) {
822 leave.superapply(this, arguments);
824 modes.delay(function () { modes.pop(); });
827 updatePrompt: function updatePrompt() {
829 this.prompt = ["REPL-R", "js" + (this.repl.count + 1) + "> "];
833 commands: function initCommands(dactyl, modules, window) {
834 const { commands } = modules;
836 commands.add(["javas[cript]", "js"],
837 "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)
850 completer: function (context) modules.completion.javascript(context),
855 mappings: function initMappings(dactyl, modules, window) {
856 const { mappings, modes } = modules;
858 function bind() mappings.add.apply(mappings,
859 [[modes.REPL]].concat(Array.slice(arguments)))
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 (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 ts=4 et ft=javascript: