]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/javascript.jsm
Import 1.0b7.1 supporting Firefox up to 8.*
[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     use: ["messages", "services", "template", "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),
58
59     get completers() JavaScript.completers, // For backward compatibility
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 = 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 = JavaScript.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             this.context.waitingForTab = true;
597             this.context.message = _("completion.waitingForKeyPress");
598             return null;
599         }
600
601         if (!/^(?:[a-zA-Z_$][\w$]*)?$/.test(key))
602             return null; // Not a word. Forget it. Can this even happen?
603
604         try { // FIXME
605             var o = this._top.offset;
606             this._top.offset = offset;
607             return this._complete(obj, key);
608         }
609         finally {
610             this._top.offset = o;
611         }
612         return null;
613     },
614
615     magicalNames: Class.memoize(function () Object.getOwnPropertyNames(Cu.Sandbox(this.window), true).sort()),
616
617     /**
618      * A list of properties of the global object which are not
619      * enumerable by any standard method.
620      */
621     globalNames: Class.memoize(function () let (self = this) array.uniq([
622         "Array", "ArrayBuffer", "AttributeName", "Boolean", "Components",
623         "CSSFontFaceStyleDecl", "CSSGroupRuleRuleList", "CSSNameSpaceRule",
624         "CSSRGBColor", "CSSRect", "ComputedCSSStyleDeclaration", "Date",
625         "Error", "EvalError", "Float32Array", "Float64Array", "Function",
626         "HTMLDelElement", "HTMLInsElement", "HTMLSpanElement", "Infinity",
627         "InnerModalContentWindow", "InnerWindow", "Int16Array", "Int32Array",
628         "Int8Array", "InternalError", "Iterator", "JSON", "KeyboardEvent",
629         "Math", "NaN", "Namespace", "Number", "Object", "Proxy", "QName",
630         "ROCSSPrimitiveValue", "RangeError", "ReferenceError", "RegExp",
631         "StopIteration", "String", "SyntaxError", "TypeError", "URIError",
632         "Uint16Array", "Uint32Array", "Uint8Array", "XML", "XMLHttpProgressEvent",
633         "XMLList", "XMLSerializer", "XPCNativeWrapper", "XPCSafeJSWrapper",
634         "XULControllers", "constructor", "decodeURI", "decodeURIComponent",
635         "encodeURI", "encodeURIComponent", "escape", "eval", "isFinite", "isNaN",
636         "isXMLName", "parseFloat", "parseInt", "undefined", "unescape", "uneval"
637     ].concat([k.substr(6) for (k in keys(Ci)) if (/^nsIDOM/.test(k))])
638      .concat([k.substr(3) for (k in keys(Ci)) if (/^nsI/.test(k))])
639      .concat(this.magicalNames)
640      .filter(function (k) k in self.window))),
641
642 }, {
643     EVAL_TMP: "__dactyl_eval_tmp",
644
645     /**
646      * A map of argument completion functions for named methods. The
647      * signature and specification of the completion function
648      * are fairly complex and yet undocumented.
649      *
650      * @see JavaScript.setCompleter
651      */
652     completers: {},
653
654     /**
655      * Installs argument string completers for a set of functions.
656      * The second argument is an array of functions (or null
657      * values), each corresponding the argument of the same index.
658      * Each provided completion function receives as arguments a
659      * CompletionContext, the 'this' object of the method, and an
660      * array of values for the preceding arguments.
661      *
662      * It is important to note that values in the arguments array
663      * provided to the completers are lazily evaluated the first
664      * time they are accessed, so they should be accessed
665      * judiciously.
666      *
667      * @param {function|[function]} funcs The functions for which to
668      *      install the completers.
669      * @param {[function]} completers An array of completer
670      *      functions.
671      */
672     setCompleter: function (funcs, completers) {
673         funcs = Array.concat(funcs);
674         for (let [, func] in Iterator(funcs)) {
675             func.dactylCompleter = function (context, func, obj, args) {
676                 let completer = completers[args.length - 1];
677                 if (!completer)
678                     return [];
679                 return completer.call(obj, context, obj, args);
680             };
681         }
682         return arguments[0];
683     }
684 }, {
685     init: function init(dactyl, modules, window) {
686         init.superapply(this, arguments);
687         modules.JavaScript = Class("JavaScript", JavaScript, { modules: modules, window: window });
688     },
689     completion: function (dactyl, modules, window) {
690         const { completion } = modules;
691         update(modules.completion, {
692             get javascript() modules.javascript.closure.complete,
693             javascriptCompleter: JavaScript // Backwards compatibility
694         });
695     },
696     modes: function initModes(dactyl, modules, window) {
697         initModes.require("commandline");
698         const { modes } = modules;
699
700         modes.addMode("REPL", {
701             description: "JavaScript Read Eval Print Loop",
702             bases: [modes.COMMAND_LINE],
703             displayName: Class.memoize(function () this.name)
704         });
705     },
706     commandline: function initCommandLine(dactyl, modules, window) {
707         const { Buffer, modes } = modules;
708
709         var REPL = Class("REPL", {
710             init: function init(context) {
711                 this.context = context;
712                 this.results = [];
713             },
714
715             addOutput: function addOutput(js) {
716                 default xml namespace = XHTML;
717                 this.count++;
718
719                 try {
720                     var result = dactyl.userEval(js, this.context);
721                     var xml = util.objectToString(result, true);
722                 }
723                 catch (e) {
724                     util.reportError(e);
725                     result = e;
726
727                     if (e.fileName)
728                         e = util.fixURI(e.fileName) + ":" + e.lineNumber + ": " + e;
729                     xml = <span highlight="ErrorMsg">{e}</span>;
730                 }
731
732                 let prompt = "js" + this.count;
733                 Class.replaceProperty(this.context, prompt, result);
734
735                 XML.ignoreWhitespace = XML.prettyPrinting = false;
736                 let nodes = {};
737                 this.rootNode.appendChild(
738                     util.xmlToDom(<e4x>
739                         <div highlight="REPL-E" key="e"><span highlight="REPL-R">{prompt}></span> {js}</div>
740                         <div highlight="REPL-P" key="p">{xml}</div>
741                     </e4x>.elements(), this.document, nodes));
742
743                 this.rootNode.scrollTop += nodes.e.getBoundingClientRect().top
744                                          - this.rootNode.getBoundingClientRect().top;
745             },
746
747             count: 0,
748
749             message: Class.memoize(function () {
750                 default xml namespace = XHTML;
751                 util.xmlToDom(<div highlight="REPL" key="rootNode"/>,
752                               this.document, this);
753
754                 return this.rootNode;
755             }),
756
757             __noSuchMethod__: function (meth, args) Buffer[meth].apply(Buffer, [this.rootNode].concat(args))
758         });
759
760         modules.CommandREPLMode = Class("CommandREPLMode", modules.CommandMode, {
761             init: function init(context) {
762                 init.supercall(this);
763
764                 let self = this;
765                 let sandbox = isinstance(context, ["Sandbox"]);
766
767                 this.context = modules.newContext(context, !sandbox);
768                 this.js = modules.JavaScript();
769                 this.js.replContext = this.context;
770                 this.js.newContext = function newContext() modules.newContext(self.context, !sandbox);
771
772                 this.js.globals = [
773                    [this.context, /*L*/"REPL Variables"],
774                    [context, /*L*/"REPL Global"]
775                 ].concat(this.js.globals.filter(function ([global]) isPrototypeOf.call(global, context)));
776
777                 if (!isPrototypeOf.call(modules.jsmodules, context))
778                     this.js.toplevel = context;
779
780                 if (!isPrototypeOf.call(window, context))
781                     this.js.window = context;
782
783                 if (this.js.globals.slice(2).some(function ([global]) global === context))
784                     this.js.globals.splice(1);
785
786                 this.repl = REPL(this.context);
787             },
788
789             open: function open(context) {
790
791                 modules.mow.echo(this.repl);
792                 this.widgets.message = null;
793
794                 open.superapply(this, arguments);
795                 this.updatePrompt();
796             },
797
798             complete: function complete(context) {
799                 context.fork("js", 0, this.js, "complete");
800             },
801
802             historyKey: "javascript",
803
804             mode: modes.REPL,
805
806             get completionList() this.widgets.statusbar.commandline.id,
807
808             accept: function accept() {
809                 dactyl.trapErrors(function () { this.repl.addOutput(this.command); }, this);
810
811                 this.completions.cleanup();
812                 this.history.save();
813                 this.history.reset();
814                 this.updatePrompt();
815
816                 modules.mow.resize();
817             },
818
819             leave: function leave(params) {
820                 leave.superapply(this, arguments);
821                 if (!params.push)
822                     modes.delay(function () { modes.pop(); });
823             },
824
825             updatePrompt: function updatePrompt() {
826                 this.command = "";
827                 this.prompt = ["REPL-R", "js" + (this.repl.count + 1) + "> "];
828             }
829         });
830     },
831     commands: function initCommands(dactyl, modules, window) {
832         const { commands } = modules;
833
834         commands.add(["javas[cript]", "js"],
835             "Evaluate a JavaScript string",
836             function (args) {
837                 if (args[0] && !args.bang)
838                     dactyl.userEval(args[0]);
839                 else {
840                     modules.commandline;
841                     modules.CommandREPLMode(args[0] ? dactyl.userEval(args[0]) : modules.userContext)
842                            .open();
843                 }
844             }, {
845                 argCount: "?",
846                 bang: true,
847                 completer: function (context) modules.completion.javascript(context),
848                 hereDoc: true,
849                 literal: 0
850             });
851     },
852     mappings: function initMappings(dactyl, modules, window) {
853         const { mappings, modes } = modules;
854
855         function bind() mappings.add.apply(mappings,
856                                            [[modes.REPL]].concat(Array.slice(arguments)))
857
858         bind(["<Return>"], "Accept the current input",
859              function ({ self }) { self.accept(); });
860
861         bind(["<C-e>"], "Scroll down one line",
862              function ({ self }) { self.repl.scrollVertical("lines", 1); });
863
864         bind(["<C-y>"], "Scroll up one line",
865              function ({ self }) { self.repl.scrollVertical("lines", -1); });
866
867         bind(["<C-d>"], "Scroll down half a page",
868              function ({ self }) { self.repl.scrollVertical("pages", .5); });
869
870         bind(["<C-f>", "<PageDown>"], "Scroll down one page",
871              function ({ self }) { self.repl.scrollVertical("pages", 1); });
872
873         bind(["<C-u>"], "Scroll up half a page",
874              function ({ self }) { self.repl.scrollVertical("pages", -.5); });
875
876         bind(["<C-b>", "<PageUp>"], "Scroll up half a page",
877              function ({ self }) { self.repl.scrollVertical("pages", -1); });
878     },
879     options: function (dactyl, modules, window) {
880         modules.options.add(["jsdebugger", "jsd"],
881             "Enable the JavaScript debugger service for use in JavaScript completion",
882             "boolean", false, {
883                 setter: function (value) {
884                     if (services.debugger.isOn != value)
885                         if (value)
886                             (services.debugger.asyncOn || services.debugger.on)(null);
887                         else
888                             services.debugger.off();
889                 },
890                 getter: function () services.debugger.isOn
891             });
892     }
893 });
894
895 endModule();
896
897 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
898
899 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: