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