]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/javascript.jsm
0f78615cc5b204b924716042538f1bae4c41101b
[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: ["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, "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),
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         context[JavaScript.EVAL_EXPORT] = function export_(obj) cache[key] = obj;
121         try {
122             if (tmp != null) // Temporary hack until bug 609949 is fixed.
123                 this.modules.dactyl.userEval(JavaScript.EVAL_EXPORT + "(" + arg + ")", context, "[Command Line Completion]", 1);
124             else
125                 cache[key] = this.modules.dactyl.userEval(arg, context, "[Command Line Completion]", 1);
126
127             return cache[key];
128         }
129         catch (e) {
130             util.reportError(e);
131             this.context.message = "Error: " + e;
132             return null;
133         }
134         finally {
135             delete context[JavaScript.EVAL_TMP];
136         }
137     },
138
139     // Get an element from the stack. If @frame is negative,
140     // count from the top of the stack, otherwise, the bottom.
141     // If @nth is provided, return the @mth value of element @type
142     // of the stack entry at @frame.
143     _get: function (frame, nth, type) {
144         let a = this._stack[frame >= 0 ? frame : this._stack.length + frame];
145         if (type != null)
146             a = a[type];
147         if (nth == null)
148             return a;
149         return a[a.length - nth - 1];
150     },
151
152     // Push and pop the stack, maintaining references to 'top' and 'last'.
153     _push: function push(arg) {
154         this._top = {
155             offset:     this._i,
156             char:       arg,
157             statements: [this._i],
158             dots:       [],
159             fullStatements: [],
160             comma:      [],
161             functions:  []
162         };
163         this._last = this._top.char;
164         this._stack.push(this._top);
165     },
166
167     _pop: function pop(arg) {
168         if (this._i == this.context.caret - 1)
169             this.context.highlight(this._top.offset, 1, "FIND");
170
171         if (this._top.char != arg) {
172             this.context.highlight(this._top.offset, this._i - this._top.offset, "SPELLCHECK");
173             throw Error("Invalid JS");
174         }
175
176         // The closing character of this stack frame will have pushed a new
177         // statement, leaving us with an empty statement. This doesn't matter,
178         // now, as we simply throw away the frame when we pop it, but it may later.
179         if (this._top.statements[this._top.statements.length - 1] == this._i)
180             this._top.statements.pop();
181         this._top = this._get(-2);
182         this._last = this._top.char;
183         return this._stack.pop();
184     },
185
186     _buildStack: function (filter) {
187         // Todo: Fix these one-letter variable names.
188         this._i = 0;
189         this._c = ""; // Current index and character, respectively.
190
191         // Reuse the old stack.
192         if (this._str && filter.substr(0, this._str.length) == this._str) {
193             this.context.highlight(0, 0, "FIND");
194             this._i = this._str.length;
195             if (this.popStatement)
196                 this._top.statements.pop();
197         }
198         else {
199             this.context.highlight();
200             this._stack = [];
201             this._functions = [];
202             this._push("#root");
203         }
204
205         // Build a parse stack, discarding entries as opening characters
206         // match closing characters. The stack is walked from the top entry
207         // and down as many levels as it takes us to figure out what it is
208         // that we're completing.
209         this._str = filter;
210         let length = this._str.length;
211         for (; this._i < length; this._lastChar = this._c, this._i++) {
212             this._c = this._str[this._i];
213             if (/['"\/]/.test(this._last)) {
214                 if (this._lastChar == "\\") { // Escape. Skip the next char, whatever it may be.
215                     this._c = "";
216                     this._i++;
217                 }
218                 else if (this._c == this._last)
219                     this._pop(this._c);
220             }
221             else {
222                 // A word character following a non-word character, or simply a non-word
223                 // character. Start a new statement.
224                 if (/[a-zA-Z_$]/.test(this._c) && !/[\w$]/.test(this._lastChar) || !/[\w\s$]/.test(this._c))
225                     this._top.statements.push(this._i);
226
227                 // A "." or a "[" dereferences the last "statement" and effectively
228                 // joins it to this logical statement.
229                 if ((this._c == "." || this._c == "[") && /[\w$\])"']/.test(this._lastNonwhite)
230                 || this._lastNonwhite == "." && /[a-zA-Z_$]/.test(this._c))
231                         this._top.statements.pop();
232
233                 switch (this._c) {
234                 case "(":
235                     // Function call, or if/while/for/...
236                     if (/[\w$]/.test(this._lastNonwhite)) {
237                         this._functions.push(this._i);
238                         this._top.functions.push(this._i);
239                         this._top.statements.pop();
240                     }
241                 case '"':
242                 case "'":
243                 case "/":
244                 case "{":
245                 case "[":
246                     this._push(this._c);
247                     break;
248                 case ".":
249                     this._top.dots.push(this._i);
250                     break;
251                 case ")": this._pop("("); break;
252                 case "]": this._pop("["); break;
253                 case "}": this._pop("{"); // Fallthrough
254                 case ";":
255                     this._top.fullStatements.push(this._i);
256                     break;
257                 case ",":
258                     this._top.comma.push(this._i);
259                     break;
260                 }
261
262                 if (/\S/.test(this._c))
263                     this._lastNonwhite = this._c;
264             }
265         }
266
267         this.popStatement = false;
268         if (!/[\w$]/.test(this._lastChar) && this._lastNonwhite != ".") {
269             this.popStatement = true;
270             this._top.statements.push(this._i);
271         }
272
273         this._lastIdx = this._i;
274     },
275
276     // Don't eval any function calls unless the user presses tab.
277     _checkFunction: function (start, end, key) {
278         let res = this._functions.some(function (idx) idx >= start && idx < end);
279         if (!res || this.context.tabPressed || key in this.cache.evalled)
280             return false;
281         this.context.waitingForTab = true;
282         return true;
283     },
284
285     // For each DOT in a statement, prefix it with TMP, eval it,
286     // and save the result back to TMP. The point of this is to
287     // cache the entire path through an object chain, mainly in
288     // the presence of function calls. There are drawbacks. For
289     // instance, if the value of a variable changes in the course
290     // of inputting a command (let foo=bar; frob(foo); foo=foo.bar; ...),
291     // we'll still use the old value. But, it's worth it.
292     _getObj: function (frame, stop) {
293         let statement = this._get(frame, 0, "statements") || 0; // Current statement.
294         let prev = statement;
295         let obj = this.window;
296         let cacheKey;
297         for (let [, dot] in Iterator(this._get(frame).dots.concat(stop))) {
298             if (dot < statement)
299                 continue;
300             if (dot > stop || dot <= prev)
301                 break;
302
303             let s = this._str.substring(prev, dot);
304             if (prev != statement)
305                 s = JavaScript.EVAL_TMP + "." + s;
306             cacheKey = this._str.substring(statement, dot);
307
308             if (this._checkFunction(prev, dot, cacheKey))
309                 return [];
310             if (prev != statement && obj == null) {
311                 this.context.message = "Error: " + cacheKey.quote() + " is " + String(obj);
312                 return [];
313             }
314
315             prev = dot + 1;
316             obj = this.evalled(s, cacheKey, obj);
317         }
318         return [[obj, cacheKey]];
319     },
320
321     _getObjKey: function (frame) {
322         let dot = this._get(frame, 0, "dots") || -1; // Last dot in frame.
323         let statement = this._get(frame, 0, "statements") || 0; // Current statement.
324         let end = (frame == -1 ? this._lastIdx : this._get(frame + 1).offset);
325
326         this._cacheKey = null;
327         let obj = [[this.cache.evalContext, "Local Variables"]].concat(this.globals);
328         // Is this an object dereference?
329         if (dot < statement) // No.
330             dot = statement - 1;
331         else // Yes. Set the object to the string before the dot.
332             obj = this._getObj(frame, dot);
333
334         let [, space, key] = this._str.substring(dot + 1, end).match(/^(\s*)(.*)/);
335         return [dot + 1 + space.length, obj, key];
336     },
337
338     _complete: function (objects, key, compl, string, last) {
339         const self = this;
340
341         if (!getOwnPropertyNames && !services.debugger.isOn && !this.context.message)
342             this.context.message = "For better completion data, please enable the JavaScript debugger (:set jsdebugger)";
343
344         let base = this.context.fork("js", this._top.offset);
345         base.forceAnchored = true;
346         base.filter = last == null ? key : string;
347         let prefix  = last != null ? key : "";
348
349         if (last == null) // We're not looking for a quoted string, so filter out anything that's not a valid identifier
350             base.filters.push(function (item) /^[a-zA-Z_$][\w$]*$/.test(item.text));
351         else {
352             base.quote = [last, function (text) util.escapeString(text, ""), last];
353             if (prefix)
354                 base.filters.push(function (item) item.item.indexOf(prefix) === 0);
355         }
356
357         if (!compl) {
358             base.process[1] = function highlight(item, v)
359                 template.highlight(typeof v == "xml" ? new String(v.toXMLString()) : v, true, 200);
360
361             // Sort in a logical fashion for object keys:
362             //  Numbers are sorted as numbers, rather than strings, and appear first.
363             //  Constants are unsorted, and appear before other non-null strings.
364             //  Other strings are sorted in the default manner.
365
366             let isnan = function isnan(item) item != '' && isNaN(item);
367             let compare = base.compare;
368
369             base.compare = function (a, b) {
370                 if (!isnan(a.key) && !isnan(b.key))
371                     return a.key - b.key;
372                 return isnan(b.key) - isnan(a.key) || compare(a, b);
373             };
374
375             base.keys = {
376                 text: prefix ? function (text) text.substr(prefix.length) : util.identity,
377                 description: function (item) self.getKey(this.obj, item),
378                 key: function (item) {
379                     if (!isNaN(key))
380                         return parseInt(key);
381                      if (/^[A-Z_][A-Z0-9_]*$/.test(key))
382                         return "";
383                     return item;
384                 }
385             };
386         }
387
388         // We've already listed anchored matches, so don't list them again here.
389         function unanchored(item) util.compareIgnoreCase(item.text.substr(0, this.filter.length), this.filter);
390
391         objects.forEach(function (obj) {
392             let context = base.fork(obj[1]);
393             context.title = [obj[1]];
394             context.keys.obj = function () obj[0];
395             context.key = obj[1] + last;
396             if (obj[0] == this.cache.evalContext)
397                 context.regenerate = true;
398
399             obj.ctxt_t = context.fork("toplevel");
400             if (!compl) {
401                 obj.ctxt_p = context.fork("prototypes");
402                 obj.ctxt_t.generate = function () self.objectKeys(obj[0], true);
403                 obj.ctxt_p.generate = function () self.objectKeys(obj[0], false);
404             }
405         }, this);
406
407         // TODO: Make this a generic completion helper function.
408         objects.forEach(function (obj) {
409             obj.ctxt_t.split(obj[1] + "/anchored", this, function (context) {
410                 context.anchored = true;
411                 if (compl)
412                     compl(context, obj[0]);
413             });
414         });
415
416         if (compl)
417             return;
418
419         objects.forEach(function (obj) {
420             obj.ctxt_p.split(obj[1] + "/anchored", this, function (context) {
421                 context.anchored = true;
422                 context.title[0] += " (prototypes)";
423             });
424         });
425
426         objects.forEach(function (obj) {
427             obj.ctxt_t.split(obj[1] + "/unanchored", this, function (context) {
428                 context.anchored = false;
429                 context.title[0] += " (substrings)";
430                 context.filters.push(unanchored);
431             });
432         });
433
434         objects.forEach(function (obj) {
435             obj.ctxt_p.split(obj[1] + "/unanchored", this, function (context) {
436                 context.anchored = false;
437                 context.title[0] += " (prototype substrings)";
438                 context.filters.push(unanchored);
439             });
440         });
441     },
442
443     _getKey: function () {
444         if (this._last == "")
445             return "";
446         // After the opening [ upto the opening ", plus '' to take care of any operators before it
447         let key = this._str.substring(this._get(-2, null, "offset") + 1, this._get(-1, null, "offset")) + "''";
448         // Now eval the key, to process any referenced variables.
449         return this.evalled(key);
450     },
451
452     get cache() this.context.cache,
453
454     complete: function _complete(context) {
455         const self = this;
456         this.context = context;
457
458         try {
459             this._buildStack.call(this, context.filter);
460         }
461         catch (e) {
462             this._lastIdx = 0;
463             util.assert(!e.message, e.message);
464             return null;
465         }
466
467         this.context.getCache("evalled", Object);
468         this.context.getCache("evalContext", this.closure.newContext);
469
470         // Okay, have parse stack. Figure out what we're completing.
471
472         // Find any complete statements that we can eval before we eval our object.
473         // This allows for things like:
474         //   let doc = content.document; let elem = doc.createEle<Tab> ...
475         let prev = 0;
476         for (let [, v] in Iterator(this._get(0).fullStatements)) {
477             let key = this._str.substring(prev, v + 1);
478             if (this._checkFunction(prev, v, key))
479                 return null;
480             this.evalled(key);
481             prev = v + 1;
482         }
483
484         // If this is a function argument, try to get the function's
485         // prototype and show it.
486         try {
487             let i = (this._get(-2) && this._get(-2).char == "(") ? -2 : -1;
488             if (this._get(i).char == "(") {
489                 let [offset, obj, funcName] = this._getObjKey(i - 1);
490                 if (obj.length) {
491                     let func = obj[0][0][funcName];
492                     if (callable(func)) {
493                         let [, prefix, args] = /^(function .*?)\((.*?)\)/.exec(Function.prototype.toString.call(func));
494                         let n = this._get(i).comma.length;
495                         args = template.map(Iterator(args.split(", ")),
496                             function ([i, arg]) <span highlight={i == n ? "Filter" : ""}>{arg}</span>,
497                             <>,&#xa0;</>);
498                         this.context.message = <>{prefix}({args})</>;
499                     }
500                 }
501             }
502         }
503         catch (e) {}
504
505         // In a string. Check if we're dereferencing an object or
506         // completing a function argument. Otherwise, do nothing.
507         if (this._last == "'" || this._last == '"') {
508
509             // str = "foo[bar + 'baz"
510             // obj = "foo"
511             // key = "bar + ''"
512
513             // The top of the stack is the sting we're completing.
514             // Wrap it in its delimiters and eval it to process escape sequences.
515             let string = this._str.substring(this._get(-1).offset + 1, this._lastIdx).replace(/((?:\\\\)*)\\/, "$1");
516             string = Cu.evalInSandbox(this._last + string + this._last, this._nullSandbox);
517
518             // Is this an object accessor?
519             if (this._get(-2).char == "[") { // Are we inside of []?
520                 // Stack:
521                 //  [-1]: "...
522                 //  [-2]: [...
523                 //  [-3]: base statement
524
525                 // Yes. If the [ starts at the beginning of a logical
526                 // statement, we're in an array literal, and we're done.
527                 if (this._get(-3, 0, "statements") == this._get(-2).offset)
528                     return null;
529
530                 // Beginning of the statement upto the opening [
531                 let obj = this._getObj(-3, this._get(-2).offset);
532
533                 return this._complete(obj, this._getKey(), null, string, this._last);
534             }
535
536             // Is this a function call?
537             if (this._get(-2).char == "(") {
538                 // Stack:
539                 //  [-1]: "...
540                 //  [-2]: (...
541                 //  [-3]: base statement
542
543                 // Does the opening "(" mark a function call?
544                 if (this._get(-3, 0, "functions") != this._get(-2).offset)
545                     return null; // No. We're done.
546
547                 let [offset, obj, funcName] = this._getObjKey(-3);
548                 if (!obj.length)
549                     return null;
550                 obj = obj.slice(0, 1);
551
552                 try {
553                     let func = obj[0][0][funcName];
554                     var completer = func.dactylCompleter;
555                 }
556                 catch (e) {}
557                 if (!completer)
558                     completer = JavaScript.completers[funcName];
559                 if (!completer)
560                     return null;
561
562                 // Split up the arguments
563                 let prev = this._get(-2).offset;
564                 let args = [];
565                 for (let [i, idx] in Iterator(this._get(-2).comma)) {
566                     let arg = this._str.substring(prev + 1, idx);
567                     prev = idx;
568                     memoize(args, i, function () self.evalled(arg));
569                 }
570                 let key = this._getKey();
571                 args.push(key + string);
572
573                 let compl = function (context, obj) {
574                     let res = completer.call(self, context, funcName, obj, args);
575                     if (res)
576                         context.completions = res;
577                 };
578
579                 obj[0][1] += "." + funcName + "(... [" + args.length + "]";
580                 return this._complete(obj, key, compl, string, this._last);
581             }
582
583             // In a string that's not an obj key or a function arg.
584             // Nothing to do.
585             return null;
586         }
587
588         // str = "foo.bar.baz"
589         // obj = "foo.bar"
590         // key = "baz"
591         //
592         // str = "foo"
593         // obj = [modules, window]
594         // key = "foo"
595
596         let [offset, obj, key] = this._getObjKey(-1);
597
598         // Wait for a keypress before completing when there's no key
599         if (!this.context.tabPressed && key == "" && obj.length > 1) {
600             this.context.waitingForTab = true;
601             this.context.message = "Waiting for key press";
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", "Boolean", "Components",
627         "CSSFontFaceStyleDecl", "CSSGroupRuleRuleList", "CSSNameSpaceRule",
628         "CSSRGBColor", "CSSRect", "ComputedCSSStyleDeclaration", "Date",
629         "Error", "EvalError", "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",
637         "XMLHttpProgressEvent", "XMLList", "XMLSerializer", "XPCNativeWrapper",
638         "XPCSafeJSWrapper", "XULControllers", "constructor", "decodeURI",
639         "decodeURIComponent", "encodeURI", "encodeURIComponent", "escape",
640         "eval", "isFinite", "isNaN", "isXMLName", "parseFloat", "parseInt",
641         "undefined", "unescape", "uneval"
642     ].concat([k.substr(6) for (k in keys(Ci)) if (/^nsIDOM/.test(k))])
643      .concat([k.substr(3) for (k in keys(Ci)) if (/^nsI/.test(k))])
644      .concat(this.magicalNames)
645      .filter(function (k) k in self.window))),
646
647 }, {
648     EVAL_TMP: "__dactyl_eval_tmp",
649     EVAL_EXPORT: "__dactyl_eval_export",
650
651     /**
652      * A map of argument completion functions for named methods. The
653      * signature and specification of the completion function
654      * are fairly complex and yet undocumented.
655      *
656      * @see JavaScript.setCompleter
657      */
658     completers: {},
659
660     /**
661      * Installs argument string completers for a set of functions.
662      * The second argument is an array of functions (or null
663      * values), each corresponding the argument of the same index.
664      * Each provided completion function receives as arguments a
665      * CompletionContext, the 'this' object of the method, and an
666      * array of values for the preceding arguments.
667      *
668      * It is important to note that values in the arguments array
669      * provided to the completers are lazily evaluated the first
670      * time they are accessed, so they should be accessed
671      * judiciously.
672      *
673      * @param {function|function[]} funcs The functions for which to
674      *      install the completers.
675      * @param {function[]} completers An array of completer
676      *      functions.
677      */
678     setCompleter: function (funcs, completers) {
679         funcs = Array.concat(funcs);
680         for (let [, func] in Iterator(funcs)) {
681             func.dactylCompleter = function (context, func, obj, args) {
682                 let completer = completers[args.length - 1];
683                 if (!completer)
684                     return [];
685                 return completer.call(obj, context, obj, args);
686             };
687         }
688         return arguments[0];
689     }
690 }, {
691     init: function init(dactyl, modules, window) {
692         init.superapply(this, arguments);
693         modules.JavaScript = Class("JavaScript", JavaScript, { modules: modules, window: window });
694     },
695     completion: function (dactyl, modules, window) {
696         const { completion } = modules;
697         update(modules.completion, {
698             get javascript() modules.javascript.closure.complete,
699             javascriptCompleter: JavaScript // Backwards compatibility
700         });
701     },
702     modes: function initModes(dactyl, modules, window) {
703         initModes.require("commandline");
704         const { modes } = modules;
705
706         modes.addMode("REPL", {
707             description: "JavaScript Read Eval Print Loop",
708             bases: [modes.COMMAND_LINE]
709         });
710     },
711     commandline: function initCommandLine(dactyl, modules, window) {
712         const { Buffer, modes } = modules;
713
714         var REPL = Class("REPL", {
715             init: function init(context) {
716                 this.context = context;
717                 this.results = [];
718             },
719
720             addOutput: function addOutput(js) {
721                 default xml namespace = XHTML;
722                 this.count++;
723
724                 try {
725                     var result = dactyl.userEval(js, this.context);
726                     var xml = util.objectToString(result, true);
727                 }
728                 catch (e) {
729                     util.reportError(e);
730                     result = e;
731
732                     if (e.fileName)
733                         e = util.fixURI(e.fileName) + ":" + e.lineNumber + ": " + e;
734                     xml = <span highlight="ErrorMsg">{e}</span>;
735                 }
736
737                 let prompt = "js" + this.count;
738                 Class.replaceProperty(this.context, prompt, result);
739
740                 XML.ignoreWhitespace = XML.prettyPrinting = false;
741                 let nodes = {};
742                 this.rootNode.appendChild(
743                     util.xmlToDom(<e4x>
744                         <div highlight="REPL-E" key="e"><span highlight="REPL-R">{prompt}></span> {js}</div>
745                         <div highlight="REPL-P" key="p">{xml}</div>
746                     </e4x>.elements(), 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                 default xml namespace = XHTML;
756                 util.xmlToDom(<div highlight="REPL" key="rootNode"/>,
757                               this.document, this);
758
759                 return this.rootNode;
760             }),
761
762             __noSuchMethod__: function (meth, args) Buffer[meth].apply(Buffer, [this.rootNode].concat(args))
763         });
764
765         modules.CommandREPLMode = Class("CommandREPLMode", modules.CommandMode, {
766             init: function init(context) {
767                 init.supercall(this);
768
769                 let self = this;
770                 let sandbox = isinstance(context, ["Sandbox"]);
771
772                 this.context = modules.newContext(context, !sandbox);
773                 this.js = modules.JavaScript();
774                 this.js.replContext = this.context;
775                 this.js.newContext = function newContext() modules.newContext(self.context, !sandbox);
776
777                 this.js.globals = [
778                    [this.context, "REPL Variables"],
779                    [context, "REPL Global"]
780                 ].concat(this.js.globals.filter(function ([global]) isPrototypeOf.call(global, context)));
781
782                 if (!isPrototypeOf.call(modules.jsmodules, context))
783                     this.js.toplevel = context;
784
785                 if (!isPrototypeOf.call(window, context))
786                     this.js.window = context;
787
788                 if (this.js.globals.slice(2).some(function ([global]) global === context))
789                     this.js.globals.splice(1);
790
791                 this.repl = REPL(this.context);
792             },
793             open: function open(context) {
794                 this.updatePrompt();
795
796                 modules.mow.echo(this.repl);
797                 this.widgets.message = null;
798
799                 open.superapply(this, arguments);
800             },
801
802             complete: function complete(context) {
803                 context.fork("js", 0, this.js, "complete");
804             },
805
806             historyKey: "javascript",
807
808             mode: modes.REPL,
809
810             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                 modules.commandline;
840
841                 if (args[0] && !args.bang)
842                     dactyl.userEval(args[0]);
843                 else {
844                     modules.commandline;
845                     modules.CommandREPLMode(args[0] ? dactyl.userEval(args[0]) : modules.userContext)
846                            .open();
847                 }
848             }, {
849                 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() mappings.add.apply(mappings,
859                                            [[modes.REPL]].concat(Array.slice(arguments)))
860
861         bind(["<Return>"], "Accept the current input",
862              function ({ self }) { self.accept(); });
863
864         bind(["<C-e>"], "Scroll down one line",
865              function ({ self }) { self.repl.scrollVertical("lines", 1); });
866
867         bind(["<C-y>"], "Scroll up one line",
868              function ({ self }) { self.repl.scrollVertical("lines", -1); });
869
870         bind(["<C-d>"], "Scroll down half a page",
871              function ({ self }) { self.repl.scrollVertical("pages", .5); });
872
873         bind(["<C-f>", "<PageDown>"], "Scroll down one page",
874              function ({ self }) { self.repl.scrollVertical("pages", 1); });
875
876         bind(["<C-u>"], "Scroll up half a page",
877              function ({ self }) { self.repl.scrollVertical("pages", -.5); });
878
879         bind(["<C-b>", "<PageUp>"], "Scroll up half a page",
880              function ({ self }) { self.repl.scrollVertical("pages", -1); });
881     },
882     options: function (dactyl, modules, window) {
883         modules.options.add(["jsdebugger", "jsd"],
884             "Enable the JavaScript debugger service for use in JavaScript completion",
885             "boolean", false, {
886                 setter: function (value) {
887                     if (services.debugger.isOn != value)
888                         if (value)
889                             (services.debugger.asyncOn || services.debugger.on)(null);
890                         else
891                             services.debugger.off();
892                 },
893                 getter: function () services.debugger.isOn
894             });
895     }
896 });
897
898 endModule();
899
900 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
901
902 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: