]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/javascript.jsm
Import r6976 from upstream hg supporting Firefox up to 25.*
[dactyl.git] / common / modules / javascript.jsm
1 // Copyright (c) 2008-2013 Kris Maglione <maglione.k at Gmail>
2 //
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
5 "use strict";
6
7 let { getOwnPropertyNames } = Object;
8
9 try {
10
11 defineModule("javascript", {
12     exports: ["JavaScript", "javascript"],
13     require: ["util"]
14 });
15
16 lazyRequire("template", ["template"]);
17
18 let isPrototypeOf = Object.prototype.isPrototypeOf;
19
20 // TODO: Clean this up.
21
22 var JavaScript = Module("javascript", {
23     init: function () {
24         this._stack = [];
25         this._functions = [];
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.
30         this._str = "";
31
32         this._lastIdx = 0;
33
34         this._cacheKey = null;
35
36         this._nullSandbox = Cu.Sandbox("about:blank");
37     },
38
39     Local: function (dactyl, modules, window) ({
40         init: function init() {
41             this.modules = modules;
42             this.window = window;
43
44             init.supercall(this);
45         }
46     }),
47
48     globals: Class.Memoize(function () [
49        [this.modules.userContext, /*L*/"Global Variables"],
50        [this.modules, "modules"],
51        [this.window, "window"]
52     ]),
53
54     toplevel: Class.Memoize(function () this.modules.jsmodules),
55
56     lazyInit: true,
57
58     newContext: function () this.modules.newContext(this.modules.userContext, true, "Dactyl JS Temp Context"),
59
60     completers: Class.Memoize(() => Object.create(JavaScript.completers)),
61
62     // Some object members are only accessible as function calls
63     getKey: function (obj, key) {
64         try {
65             return obj[key];
66         }
67         catch (e) {}
68         return undefined;
69     },
70
71     iter: function iter_(obj, toplevel) {
72         if (obj == null)
73             return;
74
75         let seen = isinstance(obj, ["Sandbox"]) ? Set(JavaScript.magicalNames) : {};
76         let globals = values(toplevel && this.window === obj ? this.globalNames : []);
77
78         if (toplevel && isObject(obj) && "wrappedJSObject" in obj)
79             if (!Set.add(seen, "wrappedJSObject"))
80                 yield "wrappedJSObject";
81
82         for (let key in iter(globals, properties(obj, !toplevel, true)))
83             if (!Set.add(seen, key))
84                 yield key;
85
86         // Properties aren't visible in an XPCNativeWrapper until
87         // they're accessed.
88         for (let key in properties(this.getKey(obj, "wrappedJSObject"), !toplevel, true))
89             try {
90                 if (key in obj && !Set.has(seen, key))
91                     yield key;
92             }
93             catch (e) {}
94     },
95
96     objectKeys: function objectKeys(obj, toplevel) {
97         // Things we can dereference
98         if (!obj || ["object", "string", "function"].indexOf(typeof obj) === -1)
99             return [];
100         if (isinstance(obj, ["Sandbox"]) && !toplevel) // Temporary hack.
101             return [];
102         if (isPrototypeOf.call(this.toplevel, obj) && !toplevel)
103             return [];
104
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))]));
108         return completions;
109     },
110
111     evalled: function evalled(arg, key, tmp) {
112         let cache = this.context.cache.evalled;
113         let context = this.context.cache.evalContext;
114
115         if (!key)
116             key = arg;
117         if (key in cache)
118             return cache[key];
119
120         context[JavaScript.EVAL_TMP] = tmp;
121         try {
122             cache[key] = this.modules.dactyl.userEval(arg, context,
123                                                       /*L*/"[Command Line Completion]", 1);
124
125             return cache[key];
126         }
127         catch (e) {
128             util.reportError(e);
129             this.context.message = _("error.error", e);
130             return null;
131         }
132         finally {
133             delete context[JavaScript.EVAL_TMP];
134         }
135     },
136
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];
143         if (type != null)
144             a = a[type];
145         if (nth == null)
146             return a;
147         return a[a.length - nth - 1];
148     },
149
150     // Push and pop the stack, maintaining references to 'top' and 'last'.
151     _push: function push(arg) {
152         this._top = {
153             offset:     this._i,
154             char:       arg,
155             statements: [this._i],
156             dots:       [],
157             fullStatements: [],
158             comma:      [],
159             functions:  []
160         };
161         this._last = this._top.char;
162         this._stack.push(this._top);
163     },
164
165     _pop: function pop(arg) {
166         if (this._i == this.context.caret - 1)
167             this.context.highlight(this._top.offset, 1, "FIND");
168
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");
172         }
173
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();
182     },
183
184     _buildStack: function (filter) {
185         // Todo: Fix these one-letter variable names.
186         this._i = 0;
187         this._c = ""; // Current index and character, respectively.
188
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();
195         }
196         else {
197             this.context.highlight();
198             this._stack = [];
199             this._functions = [];
200             this._push("#root");
201         }
202
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.
207         this._str = filter;
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.
213                     this._c = "";
214                     this._i++;
215                 }
216                 else if (this._c == this._last)
217                     this._pop(this._c);
218             }
219             else {
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);
224
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();
230
231                 switch (this._c) {
232                 case "(":
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();
238                     }
239                 case '"':
240                 case "'":
241                 case "/":
242                 case "{":
243                 case "[":
244                     this._push(this._c);
245                     break;
246                 case ".":
247                     this._top.dots.push(this._i);
248                     break;
249                 case ")": this._pop("("); break;
250                 case "]": this._pop("["); break;
251                 case "}": this._pop("{"); // Fallthrough
252                 case ";":
253                     this._top.fullStatements.push(this._i);
254                     break;
255                 case ",":
256                     this._top.comma.push(this._i);
257                     break;
258                 }
259
260                 if (/\S/.test(this._c))
261                     this._lastNonwhite = this._c;
262             }
263         }
264
265         this.popStatement = false;
266         if (!/[\w$]/.test(this._lastChar) && this._lastNonwhite != ".") {
267             this.popStatement = true;
268             this._top.statements.push(this._i);
269         }
270
271         this._lastIdx = this._i;
272     },
273
274     // Don't eval any function calls unless the user presses tab.
275     _checkFunction: function (start, end, key) {
276         let res = this._functions.some(idx => (idx >= start && idx < end));
277         if (!res || this.context.tabPressed || key in this.cache.evalled)
278             return false;
279         this.context.waitingForTab = true;
280         return true;
281     },
282
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;
294         let cacheKey;
295         for (let [, dot] in Iterator(this._get(frame).dots.concat(stop))) {
296             if (dot < statement)
297                 continue;
298             if (dot > stop || dot <= prev)
299                 break;
300
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);
305
306             if (this._checkFunction(prev, dot, cacheKey))
307                 return [];
308             if (prev != statement && obj == null) {
309                 this.context.message = /*L*/"Error: " + cacheKey.quote() + " is " + String(obj);
310                 return [];
311             }
312
313             prev = dot + 1;
314             obj = this.evalled(s, cacheKey, obj);
315         }
316         return [[obj, cacheKey]];
317     },
318
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);
323
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.
328             dot = statement - 1;
329         else // Yes. Set the object to the string before the dot.
330             obj = this._getObj(frame, dot);
331
332         let [, space, key] = this._str.substring(dot + 1, end).match(/^(\s*)(.*)/);
333         return [dot + 1 + space.length, obj, key];
334     },
335
336     _complete: function (objects, key, compl, string, last) {
337         const self = this;
338
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)";
341
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 : "";
346
347         if (last == null) // We're not looking for a quoted string, so filter out anything that's not a valid identifier
348             base.filters.push(item => /^[a-zA-Z_$][\w$]*$/.test(item.text));
349         else {
350             base.quote = [last, text => util.escapeString(text, ""), last];
351             if (prefix)
352                 base.filters.push(item => item.item.indexOf(prefix) === 0);
353         }
354
355         if (!compl) {
356             base.process[1] = function highlight(item, v)
357                 template.highlight(typeof v == "xml" ? new String(v.toXMLString()) : v, true, 200);
358
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.
363
364             let isnan = function isnan(item) item != '' && isNaN(item);
365             let compare = base.compare;
366
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);
371             };
372
373             base.keys = {
374                 text: prefix ? text => text.substr(prefix.length)
375                              : text => text,
376                 description: function (item) self.getKey(this.obj, item),
377                 key: function (item) {
378                     if (!isNaN(key))
379                         return parseInt(key);
380                      if (/^[A-Z_][A-Z0-9_]*$/.test(key))
381                         return "";
382                     return item;
383                 }
384             };
385         }
386
387         // We've already listed anchored matches, so don't list them again here.
388         function unanchored(item) util.compareIgnoreCase(item.text.substr(0, this.filter.length), this.filter);
389
390         objects.forEach(function (obj) {
391             let context = base.fork(obj[1]);
392             context.title = [obj[1]];
393             context.keys.obj = () => obj[0];
394             context.key = obj[1] + last;
395             if (obj[0] == this.cache.evalContext)
396                 context.regenerate = true;
397
398             obj.ctxt_t = context.fork("toplevel");
399             if (!compl) {
400                 obj.ctxt_p = context.fork("prototypes");
401                 obj.ctxt_t.generate = () => self.objectKeys(obj[0], true);
402                 obj.ctxt_p.generate = () => self.objectKeys(obj[0], false);
403             }
404         }, this);
405
406         // TODO: Make this a generic completion helper function.
407         objects.forEach(function (obj) {
408             obj.ctxt_t.split(obj[1] + "/anchored", this, function (context) {
409                 context.anchored = true;
410                 if (compl)
411                     compl(context, obj[0]);
412             });
413         });
414
415         if (compl)
416             return;
417
418         objects.forEach(function (obj) {
419             obj.ctxt_p.split(obj[1] + "/anchored", this, function (context) {
420                 context.anchored = true;
421                 context.title[0] += /*L*/" (prototypes)";
422             });
423         });
424
425         objects.forEach(function (obj) {
426             obj.ctxt_t.split(obj[1] + "/unanchored", this, function (context) {
427                 context.anchored = false;
428                 context.title[0] += /*L*/" (substrings)";
429                 context.filters.push(unanchored);
430             });
431         });
432
433         objects.forEach(function (obj) {
434             obj.ctxt_p.split(obj[1] + "/unanchored", this, function (context) {
435                 context.anchored = false;
436                 context.title[0] += /*L*/" (prototype substrings)";
437                 context.filters.push(unanchored);
438             });
439         });
440     },
441
442     _getKey: function () {
443         if (this._last == "")
444             return "";
445         // After the opening [ upto the opening ", plus '' to take care of any operators before it
446         let key = this._str.substring(this._get(-2, null, "offset") + 1, this._get(-1, null, "offset")) + "''";
447         // Now eval the key, to process any referenced variables.
448         return this.evalled(key);
449     },
450
451     get cache() this.context.cache,
452
453     complete: function _complete(context) {
454         const self = this;
455         this.context = context;
456
457         try {
458             this._buildStack.call(this, context.filter);
459         }
460         catch (e) {
461             this._lastIdx = 0;
462             util.assert(!e.message, e.message);
463             return null;
464         }
465
466         this.context.getCache("evalled", Object);
467         this.context.getCache("evalContext", this.closure.newContext);
468
469         // Okay, have parse stack. Figure out what we're completing.
470
471         // Find any complete statements that we can eval before we eval our object.
472         // This allows for things like:
473         //   let doc = content.document; let elem = doc.createEle<Tab> ...
474         let prev = 0;
475         for (let [, v] in Iterator(this._get(0).fullStatements)) {
476             let key = this._str.substring(prev, v + 1);
477             if (this._checkFunction(prev, v, key))
478                 return null;
479             this.evalled(key);
480             prev = v + 1;
481         }
482
483         // If this is a function argument, try to get the function's
484         // prototype and show it.
485         try {
486             let i = (this._get(-2) && this._get(-2).char == "(") ? -2 : -1;
487             if (this._get(i).char == "(") {
488                 let [offset, obj, funcName] = this._getObjKey(i - 1);
489                 if (obj.length) {
490                     let func = obj[0][0][funcName];
491                     if (callable(func)) {
492                         let [, prefix, args] = /^(function .*?)\((.*?)\)/.exec(Function.prototype.toString.call(func));
493                         let n = this._get(i).comma.length;
494                         args = template.map(Iterator(args.split(", ")),
495                             ([i, arg]) => ["span", { highlight: i == n ? "Filter" : "" }, arg],
496                             ",\u00a0");
497                         this.context.message = ["", prefix + "(", args, ")"];
498                     }
499                 }
500             }
501         }
502         catch (e) {}
503
504         // In a string. Check if we're dereferencing an object or
505         // completing a function argument. Otherwise, do nothing.
506         if (this._last == "'" || this._last == '"') {
507
508             // str = "foo[bar + 'baz"
509             // obj = "foo"
510             // key = "bar + ''"
511
512             // The top of the stack is the sting we're completing.
513             // Wrap it in its delimiters and eval it to process escape sequences.
514             let string = this._str.substring(this._get(-1).offset + 1, this._lastIdx).replace(/((?:\\\\)*)\\/, "$1");
515             string = Cu.evalInSandbox(this._last + string + this._last, this._nullSandbox);
516
517             // Is this an object accessor?
518             if (this._get(-2).char == "[") { // Are we inside of []?
519                 // Stack:
520                 //  [-1]: "...
521                 //  [-2]: [...
522                 //  [-3]: base statement
523
524                 // Yes. If the [ starts at the beginning of a logical
525                 // statement, we're in an array literal, and we're done.
526                 if (this._get(-3, 0, "statements") == this._get(-2).offset)
527                     return null;
528
529                 // Beginning of the statement upto the opening [
530                 let obj = this._getObj(-3, this._get(-2).offset);
531
532                 return this._complete(obj, this._getKey(), null, string, this._last);
533             }
534
535             // Is this a function call?
536             if (this._get(-2).char == "(") {
537                 // Stack:
538                 //  [-1]: "...
539                 //  [-2]: (...
540                 //  [-3]: base statement
541
542                 // Does the opening "(" mark a function call?
543                 if (this._get(-3, 0, "functions") != this._get(-2).offset)
544                     return null; // No. We're done.
545
546                 let [offset, obj, funcName] = this._getObjKey(-3);
547                 if (!obj.length)
548                     return null;
549                 obj = obj.slice(0, 1);
550
551                 try {
552                     let func = obj[0][0][funcName];
553                     var completer = func.dactylCompleter;
554                 }
555                 catch (e) {}
556                 if (!completer)
557                     completer = this.completers[funcName];
558                 if (!completer)
559                     return null;
560
561                 // Split up the arguments
562                 let prev = this._get(-2).offset;
563                 let args = [];
564                 for (let [i, idx] in Iterator(this._get(-2).comma)) {
565                     let arg = this._str.substring(prev + 1, idx);
566                     prev = idx;
567                     memoize(args, i, () => self.evalled(arg));
568                 }
569                 let key = this._getKey();
570                 args.push(key + string);
571
572                 let compl = function (context, obj) {
573                     let res = completer.call(self, context, funcName, obj, args);
574                     if (res)
575                         context.completions = res;
576                 };
577
578                 obj[0][1] += "." + funcName + "(... [" + args.length + "]";
579                 return this._complete(obj, key, compl, string, this._last);
580             }
581
582             // In a string that's not an obj key or a function arg.
583             // Nothing to do.
584             return null;
585         }
586
587         // str = "foo.bar.baz"
588         // obj = "foo.bar"
589         // key = "baz"
590         //
591         // str = "foo"
592         // obj = [modules, window]
593         // key = "foo"
594
595         let [offset, obj, key] = this._getObjKey(-1);
596
597         // Wait for a keypress before completing when there's no key
598         if (!this.context.tabPressed && key == "" && obj.length > 1) {
599             let message = this.context.message || "";
600             this.context.waitingForTab = true;
601             this.context.message = ["", message, "\n",
602                                     _("completion.waitingForKeyPress")];
603             return null;
604         }
605
606         if (!/^(?:[a-zA-Z_$][\w$]*)?$/.test(key))
607             return null; // Not a word. Forget it. Can this even happen?
608
609         try { // FIXME
610             var o = this._top.offset;
611             this._top.offset = offset;
612             return this._complete(obj, key);
613         }
614         finally {
615             this._top.offset = o;
616         }
617         return null;
618     },
619
620     magicalNames: Class.Memoize(function () Object.getOwnPropertyNames(Cu.Sandbox(this.window), true).sort()),
621
622     /**
623      * A list of properties of the global object which are not
624      * enumerable by any standard method.
625      */
626     globalNames: Class.Memoize(function () let (self = this) array.uniq([
627         "Array", "ArrayBuffer", "AttributeName", "Audio", "Boolean", "Components",
628         "CSSFontFaceStyleDecl", "CSSGroupRuleRuleList", "CSSNameSpaceRule",
629         "CSSRGBColor", "CSSRect", "ComputedCSSStyleDeclaration", "Date", "Error",
630         "EvalError", "File", "Float32Array", "Float64Array", "Function",
631         "HTMLDelElement", "HTMLInsElement", "HTMLSpanElement", "Infinity",
632         "InnerModalContentWindow", "InnerWindow", "Int16Array", "Int32Array",
633         "Int8Array", "InternalError", "Iterator", "JSON", "KeyboardEvent",
634         "Math", "NaN", "Namespace", "Number", "Object", "Proxy", "QName",
635         "ROCSSPrimitiveValue", "RangeError", "ReferenceError", "RegExp",
636         "StopIteration", "String", "SyntaxError", "TypeError", "URIError",
637         "Uint16Array", "Uint32Array", "Uint8Array", "XML", "XMLHttpProgressEvent",
638         "XMLList", "XMLSerializer", "XPCNativeWrapper", "XPCSafeJSWrapper",
639         "XULControllers", "constructor", "decodeURI", "decodeURIComponent",
640         "encodeURI", "encodeURIComponent", "escape", "eval", "isFinite", "isNaN",
641         "isXMLName", "parseFloat", "parseInt", "undefined", "unescape", "uneval"
642     ].concat([k.substr(6) for (k in keys(Ci)) if (/^nsIDOM/.test(k))])
643      .concat([k.substr(3) for (k in keys(Ci)) if (/^nsI/.test(k))])
644      .concat(this.magicalNames)
645      .filter(k => k in self.window))),
646
647 }, {
648     EVAL_TMP: "__dactyl_eval_tmp",
649
650     /**
651      * A map of argument completion functions for named methods. The
652      * signature and specification of the completion function
653      * are fairly complex and yet undocumented.
654      *
655      * @see JavaScript.setCompleter
656      */
657     completers: {},
658
659     /**
660      * Installs argument string completers for a set of functions.
661      * The second argument is an array of functions (or null
662      * values), each corresponding the argument of the same index.
663      * Each provided completion function receives as arguments a
664      * CompletionContext, the 'this' object of the method, and an
665      * array of values for the preceding arguments.
666      *
667      * It is important to note that values in the arguments array
668      * provided to the completers are lazily evaluated the first
669      * time they are accessed, so they should be accessed
670      * judiciously.
671      *
672      * @param {function|[function]} funcs The functions for which to
673      *      install the completers.
674      * @param {[function]} completers An array of completer
675      *      functions.
676      */
677     setCompleter: function (funcs, completers) {
678         funcs = Array.concat(funcs);
679         for (let [, func] in Iterator(funcs)) {
680             func.dactylCompleter = function (context, func, obj, args) {
681                 let completer = completers[args.length - 1];
682                 if (!completer)
683                     return [];
684                 return completer.call(obj, context, obj, args);
685             };
686         }
687         return arguments[0];
688     }
689 }, {
690     init: function init(dactyl, modules, window) {
691         init.superapply(this, arguments);
692         modules.JavaScript = Class("JavaScript", JavaScript, { modules: modules, window: window });
693     },
694     completion: function (dactyl, modules, window) {
695         const { completion } = modules;
696         update(modules.completion, {
697             get javascript() modules.javascript.closure.complete,
698             javascriptCompleter: JavaScript // Backwards compatibility
699         });
700     },
701     modes: function initModes(dactyl, modules, window) {
702         initModes.require("commandline");
703         const { modes } = modules;
704
705         modes.addMode("REPL", {
706             description: "JavaScript Read Eval Print Loop",
707             bases: [modes.COMMAND_LINE],
708             displayName: Class.Memoize(function () this.name)
709         });
710     },
711     commandline: function initCommandLine(dactyl, modules, window) {
712         const { Buffer, modes } = modules;
713
714         var REPL = Class("REPL", {
715             init: function init(context) {
716                 this.context = context;
717                 this.results = [];
718             },
719
720             addOutput: function addOutput(js) {
721                 this.count++;
722
723                 try {
724                     var result = dactyl.userEval(js, this.context);
725                     var xml = result === undefined ? "" : util.objectToString(result, true);
726                 }
727                 catch (e) {
728                     util.reportError(e);
729                     result = e;
730
731                     if (e.fileName)
732                         e = util.fixURI(e.fileName) + ":" + e.lineNumber + ": " + e;
733                     xml = ["span", { highlight: "ErrorMsg" }, e];
734                 }
735
736                 let prompt = "js" + this.count;
737                 Class.replaceProperty(this.context, prompt, result);
738
739                 let nodes = {};
740                 this.rootNode.appendChild(
741                     DOM.fromJSON(
742                         [["div", { highlight: "REPL-E", key: "e" },
743                             ["span", { highlight: "REPL-R" },
744                                 prompt, ">"], " ", js],
745                          ["div", { highlight: "REPL-P", key: "p" },
746                             xml]],
747                         this.document, nodes));
748
749                 this.rootNode.scrollTop += nodes.e.getBoundingClientRect().top
750                                          - this.rootNode.getBoundingClientRect().top;
751             },
752
753             count: 0,
754
755             message: Class.Memoize(function () {
756                 DOM.fromJSON(["div", { highlight: "REPL", key: "rootNode" }],
757                               this.document, this);
758
759                 return this.rootNode;
760             }),
761
762             __noSuchMethod__: function (meth, args) Buffer[meth].apply(Buffer, [this.rootNode].concat(args))
763         });
764
765         modules.CommandREPLMode = Class("CommandREPLMode", modules.CommandMode, {
766             init: function init(context) {
767                 init.supercall(this);
768
769                 let sandbox = true || isinstance(context, ["Sandbox"]);
770
771                 this.context = modules.newContext(context, !sandbox, "Dactyl REPL Context");
772                 this.js = modules.JavaScript();
773                 this.js.replContext = this.context;
774                 this.js.newContext = () => modules.newContext(this.context, !sandbox, "Dactyl REPL Temp Context");
775
776                 this.js.globals = [
777                    [this.context, /*L*/"REPL Variables"],
778                    [context, /*L*/"REPL Global"]
779                 ].concat(this.js.globals.filter(([global]) => isPrototypeOf.call(global, context)));
780
781                 if (!isPrototypeOf.call(modules.jsmodules, context))
782                     this.js.toplevel = context;
783
784                 if (!isPrototypeOf.call(window, context))
785                     this.js.window = context;
786
787                 if (this.js.globals.slice(2).some(([global]) => global === context))
788                     this.js.globals.splice(1);
789
790                 this.repl = REPL(this.context);
791             },
792
793             open: function open(context) {
794
795                 modules.mow.echo(this.repl);
796                 this.widgets.message = null;
797
798                 open.superapply(this, arguments);
799                 this.updatePrompt();
800             },
801
802             complete: function complete(context) {
803                 context.fork("js", 0, this.js, "complete");
804             },
805
806             historyKey: "javascript",
807
808             mode: modes.REPL,
809
810             get completionList() this.widgets.statusbar.commandline.id,
811
812             accept: function accept() {
813                 dactyl.trapErrors(function () { this.repl.addOutput(this.command); }, this);
814
815                 this.completions.cleanup();
816                 this.history.save();
817                 this.history.reset();
818                 this.updatePrompt();
819
820                 modules.mow.resize();
821             },
822
823             leave: function leave(params) {
824                 leave.superapply(this, arguments);
825                 if (!params.push)
826                     modes.delay(function () { modes.pop(); });
827             },
828
829             updatePrompt: function updatePrompt() {
830                 this.command = "";
831                 this.prompt = ["REPL-R", "js" + (this.repl.count + 1) + "> "];
832             }
833         });
834     },
835     commands: function initCommands(dactyl, modules, window) {
836         const { commands } = modules;
837
838         commands.add(["javas[cript]", "js"],
839             "Evaluate a JavaScript string",
840             function (args) {
841                 if (args[0] && !args.bang)
842                     dactyl.userEval(args[0]);
843                 else {
844                     modules.commandline;
845                     modules.CommandREPLMode(args[0] ? dactyl.userEval(args[0]) : modules.userContext)
846                            .open();
847                 }
848             }, {
849                 argCount: "?",
850                 bang: true,
851                 completer: function (context) modules.completion.javascript(context),
852                 hereDoc: true,
853                 literal: 0
854             });
855     },
856     mappings: function initMappings(dactyl, modules, window) {
857         const { mappings, modes } = modules;
858
859         function bind(...args) mappings.add.apply(mappings, [[modes.REPL]].concat(args))
860
861         bind(["<Return>"], "Accept the current input",
862              function ({ self }) { self.accept(); });
863
864         bind(["<C-e>"], "Scroll down one line",
865              function ({ self }) { self.repl.scrollVertical("lines", 1); });
866
867         bind(["<C-y>"], "Scroll up one line",
868              function ({ self }) { self.repl.scrollVertical("lines", -1); });
869
870         bind(["<C-d>"], "Scroll down half a page",
871              function ({ self }) { self.repl.scrollVertical("pages", .5); });
872
873         bind(["<C-f>", "<PageDown>"], "Scroll down one page",
874              function ({ self }) { self.repl.scrollVertical("pages", 1); });
875
876         bind(["<C-u>"], "Scroll up half a page",
877              function ({ self }) { self.repl.scrollVertical("pages", -.5); });
878
879         bind(["<C-b>", "<PageUp>"], "Scroll up half a page",
880              function ({ self }) { self.repl.scrollVertical("pages", -1); });
881     },
882     options: function initOptions(dactyl, modules, window) {
883         modules.options.add(["jsdebugger", "jsd"],
884             "Enable the JavaScript debugger service for use in JavaScript completion",
885             "boolean", false, {
886                 setter: function (value) {
887                     if (services.debugger.isOn != value)
888                         if (value)
889                             (services.debugger.asyncOn || services.debugger.on)(null);
890                         else
891                             services.debugger.off();
892                 },
893                 getter: function () services.debugger.isOn
894             });
895     }
896 });
897
898 endModule();
899
900 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
901
902 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: