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"],
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, /*L*/"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, true, "Dactyl JS Temp Context"),
59 completers: Class.Memoize(function () Object.create(JavaScript.completers)),
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 = array.uniq(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;
121 cache[key] = this.modules.dactyl.userEval(arg, context, /*L*/"[Command Line Completion]", 1);
127 this.context.message = _("error.error", e);
131 delete context[JavaScript.EVAL_TMP];
135 // Get an element from the stack. If @frame is negative,
136 // count from the top of the stack, otherwise, the bottom.
137 // If @nth is provided, return the @mth value of element @type
138 // of the stack entry at @frame.
139 _get: function (frame, nth, type) {
140 let a = this._stack[frame >= 0 ? frame : this._stack.length + frame];
145 return a[a.length - nth - 1];
148 // Push and pop the stack, maintaining references to 'top' and 'last'.
149 _push: function push(arg) {
153 statements: [this._i],
159 this._last = this._top.char;
160 this._stack.push(this._top);
163 _pop: function pop(arg) {
164 if (this._i == this.context.caret - 1)
165 this.context.highlight(this._top.offset, 1, "FIND");
167 if (this._top.char != arg) {
168 this.context.highlight(this._top.offset, this._i - this._top.offset, "SPELLCHECK");
169 throw Error(/*L*/"Invalid JS");
172 // The closing character of this stack frame will have pushed a new
173 // statement, leaving us with an empty statement. This doesn't matter,
174 // now, as we simply throw away the frame when we pop it, but it may later.
175 if (this._top.statements[this._top.statements.length - 1] == this._i)
176 this._top.statements.pop();
177 this._top = this._get(-2);
178 this._last = this._top.char;
179 return this._stack.pop();
182 _buildStack: function (filter) {
183 // Todo: Fix these one-letter variable names.
185 this._c = ""; // Current index and character, respectively.
187 // Reuse the old stack.
188 if (this._str && filter.substr(0, this._str.length) == this._str) {
189 this.context.highlight(0, 0, "FIND");
190 this._i = this._str.length;
191 if (this.popStatement)
192 this._top.statements.pop();
195 this.context.highlight();
197 this._functions = [];
201 // Build a parse stack, discarding entries as opening characters
202 // match closing characters. The stack is walked from the top entry
203 // and down as many levels as it takes us to figure out what it is
204 // that we're completing.
206 let length = this._str.length;
207 for (; this._i < length; this._lastChar = this._c, this._i++) {
208 this._c = this._str[this._i];
209 if (/['"\/]/.test(this._last)) {
210 if (this._lastChar == "\\") { // Escape. Skip the next char, whatever it may be.
214 else if (this._c == this._last)
218 // A word character following a non-word character, or simply a non-word
219 // character. Start a new statement.
220 if (/[a-zA-Z_$]/.test(this._c) && !/[\w$]/.test(this._lastChar) || !/[\w\s$]/.test(this._c))
221 this._top.statements.push(this._i);
223 // A "." or a "[" dereferences the last "statement" and effectively
224 // joins it to this logical statement.
225 if ((this._c == "." || this._c == "[") && /[\w$\])"']/.test(this._lastNonwhite)
226 || this._lastNonwhite == "." && /[a-zA-Z_$]/.test(this._c))
227 this._top.statements.pop();
231 // Function call, or if/while/for/...
232 if (/[\w$]/.test(this._lastNonwhite)) {
233 this._functions.push(this._i);
234 this._top.functions.push(this._i);
235 this._top.statements.pop();
245 this._top.dots.push(this._i);
247 case ")": this._pop("("); break;
248 case "]": this._pop("["); break;
249 case "}": this._pop("{"); // Fallthrough
251 this._top.fullStatements.push(this._i);
254 this._top.comma.push(this._i);
258 if (/\S/.test(this._c))
259 this._lastNonwhite = this._c;
263 this.popStatement = false;
264 if (!/[\w$]/.test(this._lastChar) && this._lastNonwhite != ".") {
265 this.popStatement = true;
266 this._top.statements.push(this._i);
269 this._lastIdx = this._i;
272 // Don't eval any function calls unless the user presses tab.
273 _checkFunction: function (start, end, key) {
274 let res = this._functions.some(function (idx) idx >= start && idx < end);
275 if (!res || this.context.tabPressed || key in this.cache.evalled)
277 this.context.waitingForTab = true;
281 // For each DOT in a statement, prefix it with TMP, eval it,
282 // and save the result back to TMP. The point of this is to
283 // cache the entire path through an object chain, mainly in
284 // the presence of function calls. There are drawbacks. For
285 // instance, if the value of a variable changes in the course
286 // of inputting a command (let foo=bar; frob(foo); foo=foo.bar; ...),
287 // we'll still use the old value. But, it's worth it.
288 _getObj: function (frame, stop) {
289 let statement = this._get(frame, 0, "statements") || 0; // Current statement.
290 let prev = statement;
291 let obj = this.window;
293 for (let [, dot] in Iterator(this._get(frame).dots.concat(stop))) {
296 if (dot > stop || dot <= prev)
299 let s = this._str.substring(prev, dot);
300 if (prev != statement)
301 s = JavaScript.EVAL_TMP + "." + s;
302 cacheKey = this._str.substring(statement, dot);
304 if (this._checkFunction(prev, dot, cacheKey))
306 if (prev != statement && obj == null) {
307 this.context.message = /*L*/"Error: " + cacheKey.quote() + " is " + String(obj);
312 obj = this.evalled(s, cacheKey, obj);
314 return [[obj, cacheKey]];
317 _getObjKey: function (frame) {
318 let dot = this._get(frame, 0, "dots") || -1; // Last dot in frame.
319 let statement = this._get(frame, 0, "statements") || 0; // Current statement.
320 let end = (frame == -1 ? this._lastIdx : this._get(frame + 1).offset);
322 this._cacheKey = null;
323 let obj = [[this.cache.evalContext, /*L*/"Local Variables"]].concat(this.globals);
324 // Is this an object dereference?
325 if (dot < statement) // No.
327 else // Yes. Set the object to the string before the dot.
328 obj = this._getObj(frame, dot);
330 let [, space, key] = this._str.substring(dot + 1, end).match(/^(\s*)(.*)/);
331 return [dot + 1 + space.length, obj, key];
334 _complete: function (objects, key, compl, string, last) {
337 if (!getOwnPropertyNames && !services.debugger.isOn && !this.context.message)
338 this.context.message = /*L*/"For better completion data, please enable the JavaScript debugger (:set jsdebugger)";
340 let base = this.context.fork("js", this._top.offset);
341 base.forceAnchored = true;
342 base.filter = last == null ? key : string;
343 let prefix = last != null ? key : "";
345 if (last == null) // We're not looking for a quoted string, so filter out anything that's not a valid identifier
346 base.filters.push(function (item) /^[a-zA-Z_$][\w$]*$/.test(item.text));
348 base.quote = [last, function (text) util.escapeString(text, ""), last];
350 base.filters.push(function (item) item.item.indexOf(prefix) === 0);
354 base.process[1] = function highlight(item, v)
355 template.highlight(typeof v == "xml" ? new String(v.toXMLString()) : v, true, 200);
357 // Sort in a logical fashion for object keys:
358 // Numbers are sorted as numbers, rather than strings, and appear first.
359 // Constants are unsorted, and appear before other non-null strings.
360 // Other strings are sorted in the default manner.
362 let isnan = function isnan(item) item != '' && isNaN(item);
363 let compare = base.compare;
365 base.compare = function (a, b) {
366 if (!isnan(a.key) && !isnan(b.key))
367 return a.key - b.key;
368 return isnan(b.key) - isnan(a.key) || compare(a, b);
372 text: prefix ? function (text) text.substr(prefix.length) : util.identity,
373 description: function (item) self.getKey(this.obj, item),
374 key: function (item) {
376 return parseInt(key);
377 if (/^[A-Z_][A-Z0-9_]*$/.test(key))
384 // We've already listed anchored matches, so don't list them again here.
385 function unanchored(item) util.compareIgnoreCase(item.text.substr(0, this.filter.length), this.filter);
387 objects.forEach(function (obj) {
388 let context = base.fork(obj[1]);
389 context.title = [obj[1]];
390 context.keys.obj = function () obj[0];
391 context.key = obj[1] + last;
392 if (obj[0] == this.cache.evalContext)
393 context.regenerate = true;
395 obj.ctxt_t = context.fork("toplevel");
397 obj.ctxt_p = context.fork("prototypes");
398 obj.ctxt_t.generate = function () self.objectKeys(obj[0], true);
399 obj.ctxt_p.generate = function () self.objectKeys(obj[0], false);
403 // TODO: Make this a generic completion helper function.
404 objects.forEach(function (obj) {
405 obj.ctxt_t.split(obj[1] + "/anchored", this, function (context) {
406 context.anchored = true;
408 compl(context, obj[0]);
415 objects.forEach(function (obj) {
416 obj.ctxt_p.split(obj[1] + "/anchored", this, function (context) {
417 context.anchored = true;
418 context.title[0] += /*L*/" (prototypes)";
422 objects.forEach(function (obj) {
423 obj.ctxt_t.split(obj[1] + "/unanchored", this, function (context) {
424 context.anchored = false;
425 context.title[0] += /*L*/" (substrings)";
426 context.filters.push(unanchored);
430 objects.forEach(function (obj) {
431 obj.ctxt_p.split(obj[1] + "/unanchored", this, function (context) {
432 context.anchored = false;
433 context.title[0] += /*L*/" (prototype substrings)";
434 context.filters.push(unanchored);
439 _getKey: function () {
440 if (this._last == "")
442 // After the opening [ upto the opening ", plus '' to take care of any operators before it
443 let key = this._str.substring(this._get(-2, null, "offset") + 1, this._get(-1, null, "offset")) + "''";
444 // Now eval the key, to process any referenced variables.
445 return this.evalled(key);
448 get cache() this.context.cache,
450 complete: function _complete(context) {
452 this.context = context;
455 this._buildStack.call(this, context.filter);
459 util.assert(!e.message, e.message);
463 this.context.getCache("evalled", Object);
464 this.context.getCache("evalContext", this.closure.newContext);
466 // Okay, have parse stack. Figure out what we're completing.
468 // Find any complete statements that we can eval before we eval our object.
469 // This allows for things like:
470 // let doc = content.document; let elem = doc.createEle<Tab> ...
472 for (let [, v] in Iterator(this._get(0).fullStatements)) {
473 let key = this._str.substring(prev, v + 1);
474 if (this._checkFunction(prev, v, key))
480 // If this is a function argument, try to get the function's
481 // prototype and show it.
483 let i = (this._get(-2) && this._get(-2).char == "(") ? -2 : -1;
484 if (this._get(i).char == "(") {
485 let [offset, obj, funcName] = this._getObjKey(i - 1);
487 let func = obj[0][0][funcName];
488 if (callable(func)) {
489 let [, prefix, args] = /^(function .*?)\((.*?)\)/.exec(Function.prototype.toString.call(func));
490 let n = this._get(i).comma.length;
491 args = template.map(Iterator(args.split(", ")),
492 function ([i, arg]) <span highlight={i == n ? "Filter" : ""}>{arg}</span>,
494 this.context.message = <>{prefix}({args})</>;
501 // In a string. Check if we're dereferencing an object or
502 // completing a function argument. Otherwise, do nothing.
503 if (this._last == "'" || this._last == '"') {
505 // str = "foo[bar + 'baz"
509 // The top of the stack is the sting we're completing.
510 // Wrap it in its delimiters and eval it to process escape sequences.
511 let string = this._str.substring(this._get(-1).offset + 1, this._lastIdx).replace(/((?:\\\\)*)\\/, "$1");
512 string = Cu.evalInSandbox(this._last + string + this._last, this._nullSandbox);
514 // Is this an object accessor?
515 if (this._get(-2).char == "[") { // Are we inside of []?
519 // [-3]: base statement
521 // Yes. If the [ starts at the beginning of a logical
522 // statement, we're in an array literal, and we're done.
523 if (this._get(-3, 0, "statements") == this._get(-2).offset)
526 // Beginning of the statement upto the opening [
527 let obj = this._getObj(-3, this._get(-2).offset);
529 return this._complete(obj, this._getKey(), null, string, this._last);
532 // Is this a function call?
533 if (this._get(-2).char == "(") {
537 // [-3]: base statement
539 // Does the opening "(" mark a function call?
540 if (this._get(-3, 0, "functions") != this._get(-2).offset)
541 return null; // No. We're done.
543 let [offset, obj, funcName] = this._getObjKey(-3);
546 obj = obj.slice(0, 1);
549 let func = obj[0][0][funcName];
550 var completer = func.dactylCompleter;
554 completer = this.completers[funcName];
558 // Split up the arguments
559 let prev = this._get(-2).offset;
561 for (let [i, idx] in Iterator(this._get(-2).comma)) {
562 let arg = this._str.substring(prev + 1, idx);
564 memoize(args, i, function () self.evalled(arg));
566 let key = this._getKey();
567 args.push(key + string);
569 let compl = function (context, obj) {
570 let res = completer.call(self, context, funcName, obj, args);
572 context.completions = res;
575 obj[0][1] += "." + funcName + "(... [" + args.length + "]";
576 return this._complete(obj, key, compl, string, this._last);
579 // In a string that's not an obj key or a function arg.
584 // str = "foo.bar.baz"
589 // obj = [modules, window]
592 let [offset, obj, key] = this._getObjKey(-1);
594 // Wait for a keypress before completing when there's no key
595 if (!this.context.tabPressed && key == "" && obj.length > 1) {
596 let message = this.context.message || "";
597 this.context.waitingForTab = true;
598 this.context.message = <>{message}
599 {_("completion.waitingForKeyPress")}</>;
603 if (!/^(?:[a-zA-Z_$][\w$]*)?$/.test(key))
604 return null; // Not a word. Forget it. Can this even happen?
607 var o = this._top.offset;
608 this._top.offset = offset;
609 return this._complete(obj, key);
612 this._top.offset = o;
617 magicalNames: Class.Memoize(function () Object.getOwnPropertyNames(Cu.Sandbox(this.window), true).sort()),
620 * A list of properties of the global object which are not
621 * enumerable by any standard method.
623 globalNames: Class.Memoize(function () let (self = this) array.uniq([
624 "Array", "ArrayBuffer", "AttributeName", "Audio", "Boolean", "Components",
625 "CSSFontFaceStyleDecl", "CSSGroupRuleRuleList", "CSSNameSpaceRule",
626 "CSSRGBColor", "CSSRect", "ComputedCSSStyleDeclaration", "Date", "Error",
627 "EvalError", "File", "Float32Array", "Float64Array", "Function",
628 "HTMLDelElement", "HTMLInsElement", "HTMLSpanElement", "Infinity",
629 "InnerModalContentWindow", "InnerWindow", "Int16Array", "Int32Array",
630 "Int8Array", "InternalError", "Iterator", "JSON", "KeyboardEvent",
631 "Math", "NaN", "Namespace", "Number", "Object", "Proxy", "QName",
632 "ROCSSPrimitiveValue", "RangeError", "ReferenceError", "RegExp",
633 "StopIteration", "String", "SyntaxError", "TypeError", "URIError",
634 "Uint16Array", "Uint32Array", "Uint8Array", "XML", "XMLHttpProgressEvent",
635 "XMLList", "XMLSerializer", "XPCNativeWrapper", "XPCSafeJSWrapper",
636 "XULControllers", "constructor", "decodeURI", "decodeURIComponent",
637 "encodeURI", "encodeURIComponent", "escape", "eval", "isFinite", "isNaN",
638 "isXMLName", "parseFloat", "parseInt", "undefined", "unescape", "uneval"
639 ].concat([k.substr(6) for (k in keys(Ci)) if (/^nsIDOM/.test(k))])
640 .concat([k.substr(3) for (k in keys(Ci)) if (/^nsI/.test(k))])
641 .concat(this.magicalNames)
642 .filter(function (k) k in self.window))),
645 EVAL_TMP: "__dactyl_eval_tmp",
648 * A map of argument completion functions for named methods. The
649 * signature and specification of the completion function
650 * are fairly complex and yet undocumented.
652 * @see JavaScript.setCompleter
657 * Installs argument string completers for a set of functions.
658 * The second argument is an array of functions (or null
659 * values), each corresponding the argument of the same index.
660 * Each provided completion function receives as arguments a
661 * CompletionContext, the 'this' object of the method, and an
662 * array of values for the preceding arguments.
664 * It is important to note that values in the arguments array
665 * provided to the completers are lazily evaluated the first
666 * time they are accessed, so they should be accessed
669 * @param {function|[function]} funcs The functions for which to
670 * install the completers.
671 * @param {[function]} completers An array of completer
674 setCompleter: function (funcs, completers) {
675 funcs = Array.concat(funcs);
676 for (let [, func] in Iterator(funcs)) {
677 func.dactylCompleter = function (context, func, obj, args) {
678 let completer = completers[args.length - 1];
681 return completer.call(obj, context, obj, args);
687 init: function init(dactyl, modules, window) {
688 init.superapply(this, arguments);
689 modules.JavaScript = Class("JavaScript", JavaScript, { modules: modules, window: window });
691 completion: function (dactyl, modules, window) {
692 const { completion } = modules;
693 update(modules.completion, {
694 get javascript() modules.javascript.closure.complete,
695 javascriptCompleter: JavaScript // Backwards compatibility
698 modes: function initModes(dactyl, modules, window) {
699 initModes.require("commandline");
700 const { modes } = modules;
702 modes.addMode("REPL", {
703 description: "JavaScript Read Eval Print Loop",
704 bases: [modes.COMMAND_LINE],
705 displayName: Class.Memoize(function () this.name)
708 commandline: function initCommandLine(dactyl, modules, window) {
709 const { Buffer, modes } = modules;
711 var REPL = Class("REPL", {
712 init: function init(context) {
713 this.context = context;
717 addOutput: function addOutput(js) {
718 default xml namespace = XHTML;
722 var result = dactyl.userEval(js, this.context);
723 var xml = result === undefined ? "" : util.objectToString(result, true);
730 e = util.fixURI(e.fileName) + ":" + e.lineNumber + ": " + e;
731 xml = <span highlight="ErrorMsg">{e}</span>;
734 let prompt = "js" + this.count;
735 Class.replaceProperty(this.context, prompt, result);
737 XML.ignoreWhitespace = XML.prettyPrinting = false;
739 this.rootNode.appendChild(
741 <div highlight="REPL-E" key="e"><span highlight="REPL-R">{prompt}></span> {js}</div>
742 <div highlight="REPL-P" key="p">{xml}</div>
743 </e4x>.elements(), this.document, nodes));
745 this.rootNode.scrollTop += nodes.e.getBoundingClientRect().top
746 - this.rootNode.getBoundingClientRect().top;
751 message: Class.Memoize(function () {
752 default xml namespace = XHTML;
753 util.xmlToDom(<div highlight="REPL" key="rootNode"/>,
754 this.document, this);
756 return this.rootNode;
759 __noSuchMethod__: function (meth, args) Buffer[meth].apply(Buffer, [this.rootNode].concat(args))
762 modules.CommandREPLMode = Class("CommandREPLMode", modules.CommandMode, {
763 init: function init(context) {
764 init.supercall(this);
767 let sandbox = true || isinstance(context, ["Sandbox"]);
769 this.context = modules.newContext(context, !sandbox, "Dactyl REPL Context");
770 this.js = modules.JavaScript();
771 this.js.replContext = this.context;
772 this.js.newContext = function newContext() modules.newContext(self.context, !sandbox, "Dactyl REPL Temp Context");
775 [this.context, /*L*/"REPL Variables"],
776 [context, /*L*/"REPL Global"]
777 ].concat(this.js.globals.filter(function ([global]) isPrototypeOf.call(global, context)));
779 if (!isPrototypeOf.call(modules.jsmodules, context))
780 this.js.toplevel = context;
782 if (!isPrototypeOf.call(window, context))
783 this.js.window = context;
785 if (this.js.globals.slice(2).some(function ([global]) global === context))
786 this.js.globals.splice(1);
788 this.repl = REPL(this.context);
791 open: function open(context) {
793 modules.mow.echo(this.repl);
794 this.widgets.message = null;
796 open.superapply(this, arguments);
800 complete: function complete(context) {
801 context.fork("js", 0, this.js, "complete");
804 historyKey: "javascript",
808 get completionList() this.widgets.statusbar.commandline.id,
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",
839 if (args[0] && !args.bang)
840 dactyl.userEval(args[0]);
843 modules.CommandREPLMode(args[0] ? dactyl.userEval(args[0]) : modules.userContext)
849 completer: function (context) modules.completion.javascript(context),
854 mappings: function initMappings(dactyl, modules, window) {
855 const { mappings, modes } = modules;
857 function bind() mappings.add.apply(mappings,
858 [[modes.REPL]].concat(Array.slice(arguments)))
860 bind(["<Return>"], "Accept the current input",
861 function ({ self }) { self.accept(); });
863 bind(["<C-e>"], "Scroll down one line",
864 function ({ self }) { self.repl.scrollVertical("lines", 1); });
866 bind(["<C-y>"], "Scroll up one line",
867 function ({ self }) { self.repl.scrollVertical("lines", -1); });
869 bind(["<C-d>"], "Scroll down half a page",
870 function ({ self }) { self.repl.scrollVertical("pages", .5); });
872 bind(["<C-f>", "<PageDown>"], "Scroll down one page",
873 function ({ self }) { self.repl.scrollVertical("pages", 1); });
875 bind(["<C-u>"], "Scroll up half a page",
876 function ({ self }) { self.repl.scrollVertical("pages", -.5); });
878 bind(["<C-b>", "<PageUp>"], "Scroll up half a page",
879 function ({ self }) { self.repl.scrollVertical("pages", -1); });
881 options: function (dactyl, modules, window) {
882 modules.options.add(["jsdebugger", "jsd"],
883 "Enable the JavaScript debugger service for use in JavaScript completion",
885 setter: function (value) {
886 if (services.debugger.isOn != value)
888 (services.debugger.asyncOn || services.debugger.on)(null);
890 services.debugger.off();
892 getter: function () services.debugger.isOn
899 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
901 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: