]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/javascript.jsm
b74c86f48f53ced9f1affd7cceb9cc2ad65c33c3
[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 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(function ([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(function ([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             },
827
828             updatePrompt: function updatePrompt() {
829                 this.command = "";
830                 this.prompt = ["REPL-R", "js" + (this.repl.count + 1) + "> "];
831             }
832         });
833     },
834     commands: function initCommands(dactyl, modules, window) {
835         const { commands } = modules;
836
837         commands.add(["javas[cript]", "js"],
838             "Evaluate a JavaScript string",
839             function (args) {
840                 if (args[0] && !args.bang)
841                     dactyl.userEval(args[0]);
842                 else {
843                     modules.commandline;
844                     modules.CommandREPLMode(args[0] ? dactyl.userEval(args[0]) : modules.userContext)
845                            .open();
846                 }
847             }, {
848                 argCount: "?",
849                 bang: true,
850                 completer: function (context) modules.completion.javascript(context),
851                 hereDoc: true,
852                 literal: 0
853             });
854     },
855     mappings: function initMappings(dactyl, modules, window) {
856         const { mappings, modes } = modules;
857
858         function bind(...args) mappings.add.apply(mappings, [[modes.REPL]].concat(args))
859
860         bind(["<Return>"], "Accept the current input",
861              function ({ self }) { self.accept(); });
862
863         bind(["<C-e>"], "Scroll down one line",
864              function ({ self }) { self.repl.scrollVertical("lines", 1); });
865
866         bind(["<C-y>"], "Scroll up one line",
867              function ({ self }) { self.repl.scrollVertical("lines", -1); });
868
869         bind(["<C-d>"], "Scroll down half a page",
870              function ({ self }) { self.repl.scrollVertical("pages", .5); });
871
872         bind(["<C-f>", "<PageDown>"], "Scroll down one page",
873              function ({ self }) { self.repl.scrollVertical("pages", 1); });
874
875         bind(["<C-u>"], "Scroll up half a page",
876              function ({ self }) { self.repl.scrollVertical("pages", -.5); });
877
878         bind(["<C-b>", "<PageUp>"], "Scroll up half a page",
879              function ({ self }) { self.repl.scrollVertical("pages", -1); });
880     },
881     options: function initOptions(dactyl, modules, window) {
882         modules.options.add(["jsdebugger", "jsd"],
883             "Enable the JavaScript debugger service for use in JavaScript completion",
884             "boolean", false, {
885                 setter: function (value) {
886                     if (services.debugger.isOn != value)
887                         if (value)
888                             (services.debugger.asyncOn || services.debugger.on)(null);
889                         else
890                             services.debugger.off();
891                 },
892                 getter: function () services.debugger.isOn
893             });
894     }
895 });
896
897 endModule();
898
899 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
900
901 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: