]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/javascript.jsm
Import r6923 from upstream hg supporting Firefox up to 22.0a1
[dactyl.git] / common / modules / javascript.jsm
1 // Copyright (c) 2008-2012 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(function () 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(function (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(function (item) /^[a-zA-Z_$][\w$]*$/.test(item.text));
349         else {
350             base.quote = [last, function (text) util.escapeString(text, ""), last];
351             if (prefix)
352                 base.filters.push(function (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 ? function (text) text.substr(prefix.length) : util.identity,
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 = function () 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 = function () self.objectKeys(obj[0], true);
401                 obj.ctxt_p.generate = function () 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.closure.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                             function ([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, function () 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", "XPCSafeJSWrapper",
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(function (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.closure.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 self = this;
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 = function newContext() modules.newContext(self.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(function ([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(function ([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() mappings.add.apply(mappings,
860                                            [[modes.REPL]].concat(Array.slice(arguments)))
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 ts=4 et ft=javascript: