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