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