1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/object",
  5     "firebug/firebug",
  6     "firebug/lib/domplate",
  7     "firebug/lib/locale",
  8     "firebug/lib/events",
  9     "firebug/lib/wrapper",
 10     "firebug/lib/dom",
 11     "firebug/lib/string",
 12     "firebug/lib/array",
 13     "firebug/editor/editor"
 14 ],
 15 function(Obj, Firebug, Domplate, Locale, Events, Wrapper, Dom, Str, Arr, Editor) {
 16 
 17 // ********************************************************************************************* //
 18 // Constants
 19 
 20 const kwActions = ["throw", "return", "in", "instanceof", "delete", "new",
 21                    "typeof", "void", "yield"];
 22 const reOpenBracket = /[\[\(\{]/;
 23 const reCloseBracket = /[\]\)\}]/;
 24 const reJSChar = /[a-zA-Z0-9$_]/;
 25 const reLiteralExpr = /^[ "0-9,]*$/;
 26 
 27 var measureCache = {};
 28 
 29 // ********************************************************************************************* //
 30 // JavaScript auto-completion
 31 
 32 Firebug.JSAutoCompleter = function(textBox, completionBox, options)
 33 {
 34     var popupSize = 40;
 35 
 36     this.textBox = textBox;
 37     this.options = options;
 38 
 39     this.completionBox = completionBox;
 40     this.popupTop = this.popupBottom = null;
 41 
 42     this.completionBase = {
 43         pre: null,
 44         expr: null,
 45         forceShowPopup: false,
 46         candidates: [],
 47         hiddenCandidates: []
 48     };
 49     this.completions = null;
 50 
 51     this.revertValue = null;
 52 
 53     this.showCompletionPopup = options.showCompletionPopup;
 54     this.completionPopup = options.completionPopup;
 55     this.selectedPopupElement = null;
 56 
 57     /**
 58      * If a completion was just performed, revert it. Otherwise do nothing.
 59      * Returns true iff the completion was reverted.
 60      */
 61     this.revert = function(context)
 62     {
 63         if (this.revertValue === null)
 64             return false;
 65 
 66         this.textBox.value = this.revertValue;
 67         var len = this.textBox.value.length;
 68         setCursorToEOL(this.textBox);
 69 
 70         this.complete(context);
 71         return true;
 72     };
 73 
 74     /**
 75      * Hide completions temporarily, so they show up again on the next key press.
 76      */
 77     this.hide = function()
 78     {
 79         this.completionBase = {
 80             pre: null,
 81             expr: null,
 82             forceShowPopup: false,
 83             candidates: [],
 84             hiddenCandidates: []
 85         };
 86         this.completions = null;
 87 
 88         this.showCompletions(false);
 89     };
 90 
 91     /**
 92      * Hide completions for this expression (/completion base). Appending further
 93      * characters to the variable name will not make completions appear, but
 94      * adding, say, a semicolon and typing something else will.
 95      */
 96     this.hideForExpression = function()
 97     {
 98         this.completionBase.candidates = [];
 99         this.completionBase.hiddenCandidates = [];
100         this.completions = null;
101 
102         this.showCompletions(false);
103     };
104 
105     /**
106      * Check whether it would be acceptable for the return key to evaluate the
107      * expression instead of completing things.
108      */
109     this.acceptReturn = function()
110     {
111         if (!this.completions)
112             return true;
113 
114         if (this.getCompletionValue() === this.textBox.value)
115         {
116             // The user wouldn't see a difference if we completed. This can
117             // happen for example if you type 'alert' and press enter,
118             // regardless of whether or not there exist other completions.
119             return true;
120         }
121 
122         return false;
123     };
124 
125     /**
126      * Show completions for the current contents of the text box. Either this or
127      * hide() must be called when the contents change.
128      */
129     this.complete = function(context)
130     {
131         this.revertValue = null;
132         this.createCandidates(context);
133         this.showCompletions(false);
134     };
135 
136     /**
137      * Update the completion base and create completion candidates for the
138      * current value of the text box.
139      */
140     this.createCandidates = function(context)
141     {
142         var offset = this.textBox.selectionStart;
143         if (offset !== this.textBox.value.length)
144         {
145             this.hide();
146             return;
147         }
148 
149         var value = this.textBox.value;
150 
151         // Create a simplified expression by redacting contents/normalizing
152         // delimiters of strings and regexes, to make parsing easier.
153         // Give up if the syntax is too weird.
154         var svalue = simplifyExpr(value);
155         if (svalue === null)
156         {
157             this.hide();
158             return;
159         }
160 
161         if (killCompletions(svalue, value))
162         {
163             this.hide();
164             return;
165         }
166 
167         // Find the expression to be completed.
168         var parseStart = getExpressionOffset(svalue);
169         var parsed = value.substr(parseStart);
170         var sparsed = svalue.substr(parseStart);
171 
172         // Find which part of it represents the property access.
173         var propertyStart = getPropertyOffset(sparsed);
174         var prop = parsed.substring(propertyStart);
175         var spreExpr = sparsed.substr(0, propertyStart);
176         var preExpr = parsed.substr(0, propertyStart);
177 
178         this.completionBase.pre = value.substr(0, parseStart);
179 
180         if (FBTrace.DBG_COMMANDLINE)
181         {
182             var sep = (parsed.indexOf("|") > -1) ? "^" : "|";
183             FBTrace.sysout("Completing: " + this.completionBase.pre + sep + preExpr + sep + prop);
184         }
185 
186         var prevCompletions = this.completions;
187 
188         // We only need to calculate a new candidate list if the expression has
189         // changed (we can ignore completionBase.pre since completions do not
190         // depend upon that).
191         if (preExpr !== this.completionBase.expr)
192         {
193             this.completionBase.expr = preExpr;
194             var ev = autoCompleteEval(context, preExpr, spreExpr,
195                 this.options.includeCurrentScope);
196             prevCompletions = null;
197             this.completionBase.candidates = ev.completions;
198             this.completionBase.hiddenCandidates = ev.hiddenCompletions;
199             this.completionBase.forceShowPopup = false;
200         }
201 
202         this.createCompletions(prop, prevCompletions);
203     };
204 
205     /**
206      * From a valid completion base, create a list of completions (containing
207      * those completion candidates that share a (sometimes case-insensitive)
208      * prefix with the user's input) and a default completion. The completions
209      * for the previous expression (null if none) are used to help with the
210      * latter.
211      */
212     this.createCompletions = function(prefix, prevCompletions)
213     {
214         if (!this.completionBase.expr && !prefix)
215         {
216             // Don't complete "".
217             this.completions = null;
218             return;
219         }
220         if (!this.completionBase.candidates.length && !prefix)
221         {
222             // Don't complete empty objects -> toString.
223             this.completions = null;
224             return;
225         }
226 
227         var mustMatchFirstLetter = (this.completionBase.expr === "");
228         var clist = [
229             this.completionBase.candidates,
230             this.completionBase.hiddenCandidates
231         ], cind = 0;
232         var valid = [], ciValid = [];
233         var lowPrefix = prefix.toLowerCase();
234         while (ciValid.length === 0 && cind < 2)
235         {
236             var candidates = clist[cind];
237             for (var i = 0; i < candidates.length; ++i)
238             {
239                 // Mark a candidate as matching if it matches the prefix case-
240                 // insensitively, and shares its upper-case characters. The
241                 // exception to this is that for global completions, the first
242                 // character must match exactly (see issue 6030).
243                 var name = candidates[i];
244                 if (!Str.hasPrefix(name.toLowerCase(), lowPrefix))
245                     continue;
246 
247                 if (mustMatchFirstLetter && name.charAt(0) !== prefix.charAt(0))
248                     continue;
249 
250                 var fail = false;
251                 for (var j = 0; j < prefix.length; ++j)
252                 {
253                     var ch = prefix.charAt(j);
254                     if (ch !== ch.toLowerCase() && ch !== name.charAt(j))
255                     {
256                         fail = true;
257                         break;
258                     }
259                 }
260                 if (!fail)
261                 {
262                     ciValid.push(name);
263                     if (Str.hasPrefix(name, prefix))
264                         valid.push(name);
265                 }
266             }
267             ++cind;
268         }
269 
270         if (ciValid.length > 0)
271         {
272             // If possible, default to a candidate matching the case by picking
273             // a default from 'valid' and correcting its index.
274             var hasMatchingCase = (valid.length > 0);
275 
276             this.completions = {
277                 list: (hasMatchingCase ? valid : ciValid),
278                 prefix: prefix,
279                 hidePopup: (cind === 2)
280             };
281             this.completions.index = this.pickDefaultCandidate(prevCompletions);
282 
283             if (hasMatchingCase)
284             {
285                 var find = valid[this.completions.index];
286                 this.completions.list = ciValid;
287                 this.completions.index = ciValid.indexOf(find);
288             }
289         }
290         else
291         {
292             this.completions = null;
293         }
294     };
295 
296     /**
297      * Choose a default candidate from the list of completions. The first of all
298      * shortest completions is current used for this, except in some very hacky,
299      * but useful, special cases (issue 5593).
300      */
301     this.pickDefaultCandidate = function(prevCompletions)
302     {
303         var list = this.completions.list, ind;
304 
305         // If the typed expression is an extension of the previous completion, keep it.
306         if (prevCompletions && Str.hasPrefix(this.completions.prefix, prevCompletions.prefix))
307         {
308             var lastCompletion = prevCompletions.list[prevCompletions.index];
309             ind = list.indexOf(lastCompletion);
310             if (ind !== -1)
311                 return ind;
312         }
313 
314         // Special-case certain expressions.
315         var special = {
316             "": ["document", "console", "frames", "window", "parseInt", "undefined",
317                 "Array", "Math", "Object", "String", "XMLHttpRequest", "Window"],
318             "window.": ["console"],
319             "location.": ["href"],
320             "document.": ["getElementById", "addEventListener", "createElement",
321                 "documentElement"],
322             "Object.prototype.toString": ["call"]
323         };
324         if (special.hasOwnProperty(this.completionBase.expr))
325         {
326             var ar = special[this.completionBase.expr];
327             for (var i = 0; i < ar.length; ++i)
328             {
329                 var prop = ar[i];
330                 if (Str.hasPrefix(prop, this.completions.prefix))
331                 {
332                     // Use 'prop' as a completion, if it exists.
333                     ind = list.indexOf(prop);
334                     if (ind !== -1)
335                         return ind;
336                 }
337             }
338         }
339 
340         // 'prototype' is a good default if it exists.
341         ind = list.indexOf("prototype");
342         if (ind !== -1)
343             return ind;
344 
345         ind = 0;
346         for (var i = 1; i < list.length; ++i)
347         {
348             if (list[i].length < list[ind].length)
349                 ind = i;
350         }
351 
352         // Avoid some completions in favor of others.
353         var replacements = {
354             "toSource": "toString",
355             "toFixed": "toString",
356             "watch": "toString",
357             "pattern": "parentNode"
358         };
359         if (replacements.hasOwnProperty(list[ind]))
360         {
361             var ind2 = list.indexOf(replacements[list[ind]]);
362             if (ind2 !== -1)
363                 return ind2;
364         }
365 
366         return ind;
367     };
368 
369     /**
370      * Go backward or forward by some number of steps in the list of completions.
371      * dir is the relative movement in the list (negative for backwards movement).
372      */
373     this.cycle = function(dir, clamp)
374     {
375         var ind = this.completions.index + dir;
376         if (clamp)
377             ind = Math.max(Math.min(ind, this.completions.list.length - 1), 0);
378         else if (ind >= this.completions.list.length)
379             ind = 0;
380         else if (ind < 0)
381             ind = this.completions.list.length - 1;
382         this.completions.index = ind;
383         this.showCompletions(true);
384     };
385 
386     /**
387      * Get the property name that is currently selected as a completion (or
388      * null if there is none).
389      */
390     this.getCurrentCompletion = function()
391     {
392         return (this.completions ? this.completions.list[this.completions.index] : null);
393     };
394 
395     /**
396      * See if we have any completions.
397      */
398     this.hasCompletions = function()
399     {
400         return !!this.completions;
401     };
402 
403     /**
404      * Get the value the completion box should have for some value of the
405      * text box and a selected completion.
406      */
407     this.getCompletionBoxValue = function()
408     {
409         var completion = this.getCurrentCompletion();
410         if (completion === null)
411             return "";
412         var userTyped = this.textBox.value;
413         var value = this.completionBase.pre + this.completionBase.expr + completion;
414         return userTyped + value.substr(userTyped.length);
415     };
416 
417     /**
418      * Update the completion box and popup to be consistent with the current
419      * state of the auto-completer. If just cycling, the old scolling state
420      * for the popup is preserved.
421      */
422     this.showCompletions = function(cycling)
423     {
424         this.completionBox.value = this.getCompletionBoxValue();
425 
426         if (this.completions && (this.completionBase.forceShowPopup ||
427             (this.completions.list.length > 1 && this.showCompletionPopup &&
428              !this.completions.hidePopup)))
429         {
430             this.popupCandidates(cycling);
431         }
432         else
433         {
434             this.closePopup();
435         }
436     };
437 
438     /**
439      * Handle a keypress event. Returns true if the auto-completer used up
440      * the event and does not want it to propagate further.
441      */
442     this.handleKeyPress = function(event, context)
443     {
444         var clearedTabWarning = this.clearTabWarning();
445 
446         if (Events.isAlt(event))
447             return false;
448 
449         if (event.keyCode === KeyEvent.DOM_VK_TAB &&
450             !Events.isControl(event) && !Events.isControlShift(event) &&
451             this.textBox.value !== "")
452         {
453             if (this.completions)
454             {
455                 this.acceptCompletion();
456                 Events.cancelEvent(event);
457                 return true;
458             }
459             else if (this.options.tabWarnings)
460             {
461                 if (clearedTabWarning)
462                 {
463                     // Send tab along if the user was warned.
464                     return false;
465                 }
466 
467                 this.setTabWarning();
468                 Events.cancelEvent(event);
469                 return true;
470             }
471         }
472         else if (event.keyCode === KeyEvent.DOM_VK_RETURN && !this.acceptReturn())
473         {
474             // Completion on return, when one is user-visible.
475             this.acceptCompletion();
476             Events.cancelEvent(event);
477             return true;
478         }
479         else if (event.keyCode === KeyEvent.DOM_VK_RIGHT && this.completions &&
480             this.textBox.selectionStart === this.textBox.value.length)
481         {
482             // Complete on right arrow at end of line.
483             this.acceptCompletion();
484             Events.cancelEvent(event);
485             return true;
486         }
487         else if (event.keyCode === KeyEvent.DOM_VK_ESCAPE)
488         {
489             if (this.completions)
490             {
491                 this.hideForExpression();
492                 Events.cancelEvent(event);
493                 return true;
494             }
495             else
496             {
497                 // There are no visible completions, but we might still be able to
498                 // revert a recently performed completion.
499                 if (this.revert(context))
500                 {
501                     Events.cancelEvent(event);
502                     return true;
503                 }
504             }
505         }
506         else if (event.keyCode === KeyEvent.DOM_VK_UP ||
507             event.keyCode === KeyEvent.DOM_VK_DOWN)
508         {
509             if (this.completions)
510             {
511                 this.cycle(event.keyCode === KeyEvent.DOM_VK_UP ? -1 : 1, false);
512                 Events.cancelEvent(event);
513                 return true;
514             }
515         }
516         else if (event.keyCode === KeyEvent.DOM_VK_PAGE_UP ||
517             event.keyCode === KeyEvent.DOM_VK_PAGE_DOWN)
518         {
519             if (this.completions)
520             {
521                 this.pageCycle(event.keyCode === KeyEvent.DOM_VK_PAGE_UP ? -1 : 1);
522                 Events.cancelEvent(event);
523                 return true;
524             }
525         }
526         else if (event.keyCode === KeyEvent.DOM_VK_HOME ||
527             event.keyCode === KeyEvent.DOM_VK_END)
528         {
529             if (this.isPopupOpen())
530             {
531                 this.topCycle(event.keyCode === KeyEvent.DOM_VK_HOME ? -1 : 1);
532                 Events.cancelEvent(event);
533                 return true;
534             }
535         }
536         return false;
537     };
538 
539     /**
540      * Handle a keydown event.
541      */
542     this.handleKeyDown = function(event, context)
543     {
544         if (event.keyCode === KeyEvent.DOM_VK_ESCAPE && this.completions)
545         {
546             // Close the completion popup on escape in keydown, so that the popup
547             // does not close itself and prevent event propagation on keypress.
548             // (Unless the popup is only open due to Ctrl+Space, in which case
549             // that's precisely what we want.)
550             if (!this.forceShowPopup)
551                 this.closePopup();
552         }
553         else if (event.keyCode === KeyEvent.DOM_VK_SPACE && Events.isControl(event))
554         {
555             if (!this.completions)
556             {
557                 // If completions have been hidden, show them again.
558                 this.hide();
559                 this.complete(context);
560             }
561 
562             if (this.completions && !this.isPopupOpen())
563             {
564                 // Force-show the completion popup.
565                 this.completionBase.forceShowPopup = true;
566                 this.popupCandidates(false);
567             }
568         }
569     };
570 
571     this.clearTabWarning = function()
572     {
573         if (this.tabWarning)
574         {
575             this.completionBox.value = "";
576             delete this.tabWarning;
577             return true;
578         }
579         return false;
580     };
581 
582     this.setTabWarning = function()
583     {
584         this.completionBox.value = this.textBox.value + "    " +
585             Locale.$STR("firebug.completion.empty");
586 
587         this.tabWarning = true;
588     };
589 
590     /**
591      * Get what should be completed to; this is only vaguely related to what is
592      * shown in the completion box.
593      */
594     this.getCompletionValue = function()
595     {
596         var property = this.getCurrentCompletion();
597         var preParsed = this.completionBase.pre, preExpr = this.completionBase.expr;
598         var res = preParsed + preExpr + property;
599 
600         // Don't adjust index completions.
601         if (/^\[['"]$/.test(preExpr.slice(-2)))
602             return res;
603 
604         if (!isValidProperty(property))
605         {
606             // The property name is actually invalid in free form, so replace
607             // it with array syntax.
608 
609             if (preExpr)
610             {
611                 res = preParsed + preExpr.slice(0, -1);
612             }
613             else
614             {
615                 // Global variable access - assume the variable is a member of 'window'.
616                 res = preParsed + "window";
617             }
618             res += '["' + Str.escapeJS(property) + '"]';
619         }
620         return res;
621     };
622 
623     /**
624      * Accept the current completion into the text box.
625      */
626     this.acceptCompletion = function()
627     {
628         var completion = this.getCompletionValue();
629         var originalValue = this.textBox.value;
630         this.textBox.value = completion;
631         setCursorToEOL(this.textBox);
632 
633         this.hide();
634         this.revertValue = originalValue;
635     };
636 
637     this.pageCycle = function(dir)
638     {
639         var list = this.completions.list, selIndex = this.completions.index;
640 
641         if (!this.isPopupOpen())
642         {
643             // When no popup is open, cycle by a fixed amount and stop at edges.
644             this.cycle(dir * 15, true);
645             return;
646         }
647 
648         var top = this.popupTop, bottom = this.popupBottom;
649         if (top === 0 && bottom === list.length)
650         {
651             // For a single scroll page, act like home/end.
652             this.topCycle(dir);
653             return;
654         }
655 
656         var immediateTarget;
657         if (dir === -1)
658             immediateTarget = (top === 0 ? top : top + 2);
659         else
660             immediateTarget = (bottom === list.length ? bottom: bottom - 2) - 1;
661         if ((selIndex - immediateTarget) * dir < 0)
662         {
663             // The selection has not yet reached the edge target, so jump to it.
664             selIndex = immediateTarget;
665         }
666         else
667         {
668             // Show the next page.
669             if (dir === -1 && top - popupSize <= 0)
670                 selIndex = 0;
671             else if (dir === 1 && bottom + popupSize >= list.length)
672                 selIndex = list.length - 1;
673             else
674                 selIndex = immediateTarget + dir*popupSize;
675         }
676 
677         this.completions.index = selIndex;
678         this.showCompletions(true);
679     };
680 
681     this.topCycle = function(dir)
682     {
683         if (dir === -1)
684             this.completions.index = 0;
685         else
686             this.completions.index = this.completions.list.length - 1;
687         this.showCompletions(true);
688     };
689 
690     this.popupCandidates = function(cycling)
691     {
692         Dom.eraseNode(this.completionPopup);
693         this.selectedPopupElement = null;
694 
695         var vbox = this.completionPopup.ownerDocument.createElement("vbox");
696         vbox.classList.add("fbCommandLineCompletions");
697         this.completionPopup.appendChild(vbox);
698 
699         var title = this.completionPopup.ownerDocument.
700             createElementNS("http://www.w3.org/1999/xhtml", "div");
701         title.textContent = Locale.$STR("console.Use Arrow keys, Tab or Enter");
702         title.classList.add("fbPopupTitle");
703         vbox.appendChild(title);
704 
705         var list = this.completions.list, selIndex = this.completions.index;
706 
707         if (this.completions.list.length <= popupSize)
708         {
709             this.popupTop = 0;
710             this.popupBottom = list.length;
711         }
712         else
713         {
714             var self = this;
715             var setTop = function(val)
716             {
717                 if (val < 0)
718                     val = 0;
719                 self.popupTop = val;
720                 self.popupBottom = val + popupSize;
721                 if (self.popupBottom > list.length)
722                     setBottom(list.length);
723             };
724             var setBottom = function(val)
725             {
726                 if (val > list.length)
727                     val = list.length;
728                 self.popupBottom = val;
729                 self.popupTop = val - popupSize;
730                 if (self.popupTop < 0)
731                     setTop(0);
732             };
733 
734             if (!cycling)
735             {
736                 // Show the selection at nearly the bottom of the popup, where
737                 // it is more local.
738                 setBottom(selIndex + 3);
739             }
740             else
741             {
742                 // Scroll the popup such that selIndex fits.
743                 if (selIndex - 2 < this.popupTop)
744                     setTop(selIndex - 2);
745                 else if (selIndex + 3 > this.popupBottom)
746                     setBottom(selIndex + 3);
747             }
748         }
749 
750         for (var i = this.popupTop; i < this.popupBottom; i++)
751         {
752             var prefixLen = this.completions.prefix.length;
753             var completion = list[i];
754 
755             var hbox = this.completionPopup.ownerDocument.
756                 createElementNS("http://www.w3.org/1999/xhtml", "div");
757             hbox.completionIndex = i;
758 
759             var pre = this.completionPopup.ownerDocument.
760                 createElementNS("http://www.w3.org/1999/xhtml", "span");
761             var preText = this.completionBase.expr + completion.substr(0, prefixLen);
762             pre.textContent = preText;
763             pre.classList.add("userTypedText");
764 
765             var post = this.completionPopup.ownerDocument.
766                 createElementNS("http://www.w3.org/1999/xhtml", "span");
767             var postText = completion.substr(prefixLen);
768             post.textContent = postText;
769             post.classList.add("completionText");
770 
771             if (i === selIndex)
772                 this.selectedPopupElement = hbox;
773 
774             hbox.appendChild(pre);
775             hbox.appendChild(post);
776             vbox.appendChild(hbox);
777         }
778 
779         if (this.selectedPopupElement)
780             this.selectedPopupElement.setAttribute("selected", "true");
781 
782         // Open the popup at the pixel position of the start of the completed
783         // expression. The text length times the width of a single character,
784         // plus apparent padding, is a good enough approximation of this.
785         var chWidth = this.getCharWidth(this.completionBase.pre);
786         var offsetX = Math.round(this.completionBase.pre.length * chWidth) + 2;
787         this.completionPopup.openPopup(this.textBox, "before_start", offsetX, 0, false, false);
788     };
789 
790     this.getCharWidth = function(text)
791     {
792         var size = Firebug.textSize;
793         if (!measureCache[size])
794         {
795             var measurer = this.options.popupMeasurer;
796             measurer.style.fontSizeAdjust = this.textBox.style.fontSizeAdjust;
797             measureCache[size] = measurer.offsetWidth / 60;
798         }
799         return measureCache[size];
800     };
801 
802     this.isPopupOpen = function()
803     {
804         return (this.completionPopup && this.completionPopup.state !== "closed");
805     };
806 
807     this.closePopup = function()
808     {
809         if (!this.isPopupOpen())
810             return;
811 
812         try
813         {
814             this.completionPopup.hidePopup();
815         }
816         catch (err)
817         {
818             if (FBTrace.DBG_ERRORS)
819                 FBTrace.sysout("Firebug.JSAutoCompleter.closePopup; EXCEPTION " + err, err);
820         }
821     };
822 
823     this.getCompletionPopupElementFromEvent = function(event)
824     {
825         var selected = event.target;
826         while (selected && selected.localName !== "div")
827             selected = selected.parentNode;
828 
829         return (selected && typeof selected.completionIndex !== "undefined" ? selected : null);
830     };
831 
832     this.popupMousedown = function(event)
833     {
834         var el = this.getCompletionPopupElementFromEvent(event);
835         if (!el)
836             return;
837 
838         if (this.selectedPopupElement)
839             this.selectedPopupElement.removeAttribute("selected");
840 
841         this.selectedPopupElement = el;
842         this.selectedPopupElement.setAttribute("selected", "true");
843         this.completions.index = el.completionIndex;
844         this.completionBox.value = this.getCompletionBoxValue();
845     };
846 
847     this.popupScroll = function(event)
848     {
849         if (event.axis !== event.VERTICAL_AXIS)
850             return;
851         if (!this.getCompletionPopupElementFromEvent(event))
852             return;
853         this.cycle(event.detail, true);
854     };
855 
856     this.popupClick = function(event)
857     {
858         var el = this.getCompletionPopupElementFromEvent(event);
859         if (!el)
860             return;
861 
862         this.completions.index = el.completionIndex;
863         this.acceptCompletion();
864     };
865 
866     this.popupMousedown = Obj.bind(this.popupMousedown, this);
867     this.popupScroll = Obj.bind(this.popupScroll, this);
868     this.popupClick = Obj.bind(this.popupClick, this);
869 
870     /**
871      * A destructor function, to be called when the auto-completer is destroyed.
872      */
873     this.shutdown = function()
874     {
875         this.completionBox.value = "";
876 
877         if (this.completionPopup)
878         {
879             Events.removeEventListener(this.completionPopup, "mousedown", this.popupMousedown, true);
880             Events.removeEventListener(this.completionPopup, "DOMMouseScroll", this.popupScroll, true);
881             Events.removeEventListener(this.completionPopup, "click", this.popupClick, true);
882         }
883     };
884 
885     if (this.completionPopup)
886     {
887         Events.addEventListener(this.completionPopup, "mousedown", this.popupMousedown, true);
888         Events.addEventListener(this.completionPopup, "DOMMouseScroll", this.popupScroll, true);
889         Events.addEventListener(this.completionPopup, "click", this.popupClick, true);
890     }
891 };
892 
893 // ********************************************************************************************* //
894 
895 /**
896  * An (abstract) editor with simple JavaScript auto-completion.
897  */
898 Firebug.JSEditor = function()
899 {
900 };
901 
902 with (Domplate) {
903 Firebug.JSEditor.prototype = domplate(Firebug.InlineEditor.prototype,
904 {
905     setupCompleter: function(completionBox, options)
906     {
907         this.tabNavigation = false;
908         this.arrowCompletion = false;
909         this.fixedWidth = true;
910         this.completionBox = completionBox;
911 
912         this.autoCompleter = new EditorJSAutoCompleter(this.input, this.completionBox, options);
913     },
914 
915     updateLayout: function()
916     {
917         // Make sure the completion box stays in sync with the input box.
918         Firebug.InlineEditor.prototype.updateLayout.apply(this, arguments);
919         this.completionBox.style.width = this.input.style.width;
920         this.completionBox.style.height = this.input.style.height;
921     },
922 
923     destroy: function()
924     {
925         this.autoCompleter.destroy();
926         Firebug.InlineEditor.prototype.destroy.call(this);
927     },
928 
929     onKeyPress: function(event)
930     {
931         var context = this.panel.context;
932 
933         if (this.getAutoCompleter().handleKeyPress(event, context))
934             return;
935 
936         if (event.keyCode === KeyEvent.DOM_VK_TAB ||
937             event.keyCode === KeyEvent.DOM_VK_RETURN)
938         {
939             Firebug.Editor.stopEditing();
940             Events.cancelEvent(event);
941         }
942     },
943 
944     onInput: function()
945     {
946         var context = this.panel.context;
947         this.getAutoCompleter().complete(context);
948         Firebug.Editor.update();
949     }
950 });
951 }
952 
953 function EditorJSAutoCompleter(box, completionBox, options)
954 {
955     var ac = new Firebug.JSAutoCompleter(box, completionBox, options);
956 
957     this.destroy = Obj.bindFixed(ac.shutdown, ac);
958     this.reset = Obj.bindFixed(ac.hide, ac);
959     this.complete = Obj.bind(ac.complete, ac);
960     this.handleKeyPress = Obj.bind(ac.handleKeyPress, ac);
961 }
962 
963 // ********************************************************************************************* //
964 // Auto-completion helpers
965 
966 /**
967  * Try to find the position at which the expression to be completed starts.
968  */
969 function getExpressionOffset(command)
970 {
971     var bracketCount = 0;
972 
973     var start = command.length, instr = false;
974 
975     // When completing []-accessed properties, start instead from the last [.
976     var lastBr = command.lastIndexOf("[");
977     if (lastBr !== -1 && /^" *$/.test(command.substr(lastBr+1)))
978         start = lastBr;
979 
980     for (var i = start-1; i >= 0; --i)
981     {
982         var c = command[i];
983         if (reOpenBracket.test(c))
984         {
985             if (bracketCount)
986                 --bracketCount;
987             else
988                 break;
989         }
990         else if (reCloseBracket.test(c))
991         {
992             var next = command[i + 1];
993             if (bracketCount === 0 && next !== "." && next !== "[")
994                 break;
995             else
996                 ++bracketCount;
997         }
998         else if (bracketCount === 0)
999         {
1000             if (c === '"') instr = !instr;
1001             else if (!instr && !reJSChar.test(c) && c !== ".")
1002                 break;
1003         }
1004     }
1005     ++i;
1006 
1007     // The 'new' operator has higher precedence than function calls, so, if
1008     // present, it should be included if the expression contains a parenthesis.
1009     if (i-4 >= 0 && command.indexOf("(", i) !== -1 && command.substr(i-4, 4) === "new ")
1010     {
1011         i -= 4;
1012     }
1013 
1014     return i;
1015 }
1016 
1017 /**
1018  * Try to find the position at which the property name of the final property
1019  * access in an expression starts (for example, 2 in 'a.b').
1020  */
1021 function getPropertyOffset(expr)
1022 {
1023     var lastBr = expr.lastIndexOf("[");
1024     if (lastBr !== -1 && /^" *$/.test(expr.substr(lastBr+1)))
1025         return lastBr+2;
1026 
1027     var lastDot = expr.lastIndexOf(".");
1028     if (lastDot !== -1)
1029         return lastDot+1;
1030 
1031     return 0;
1032 }
1033 
1034 /**
1035  * Get the index of the last non-whitespace character in the range [0, from)
1036  * in str, or -1 if there is none.
1037  */
1038 function prevNonWs(str, from)
1039 {
1040     for (var i = from-1; i >= 0; --i)
1041     {
1042         if (str.charAt(i) !== " ")
1043             return i;
1044     }
1045     return -1;
1046 }
1047 
1048 /**
1049  * Find the start of a word consisting of characters matching reJSChar, if
1050  * str[from] is the last character in the word. (This can be used together
1051  * with prevNonWs to traverse words backwards from a position.)
1052  */
1053 function prevWord(str, from)
1054 {
1055     for (var i = from-1; i >= 0; --i)
1056     {
1057         if (!reJSChar.test(str.charAt(i)))
1058             return i+1;
1059     }
1060     return 0;
1061 }
1062 
1063 /**
1064  * Check if a position 'pos', marking the start of a property name, is
1065  * preceded by a function-declaring keyword.
1066  */
1067 function isFunctionName(expr, pos)
1068 {
1069     var ind = prevNonWs(expr, pos);
1070     if (ind === -1 || !reJSChar.test(expr.charAt(ind)))
1071         return false;
1072     var word = expr.substring(prevWord(expr, ind), ind+1);
1073     return (word === "function" || word === "get" || word === "set");
1074 }
1075 
1076 function bwFindMatchingParen(expr, from)
1077 {
1078     var bcount = 1;
1079     for (var i = from-1; i >= 0; --i)
1080     {
1081         if (reCloseBracket.test(expr.charAt(i)))
1082             ++bcount;
1083         else if (reOpenBracket.test(expr.charAt(i)))
1084             if (--bcount === 0)
1085                 return i;
1086     }
1087     return -1;
1088 }
1089 
1090 /**
1091  * Check if a '/' at the end of 'expr' would be a regex or a division.
1092  * May also return null if the expression seems invalid.
1093  */
1094 function endingDivIsRegex(expr)
1095 {
1096     var kwCont = ["function", "if", "while", "for", "switch", "catch", "with"];
1097 
1098     var ind = prevNonWs(expr, expr.length), ch = (ind === -1 ? "{" : expr.charAt(ind));
1099     if (reJSChar.test(ch))
1100     {
1101         // Test if the previous word is a keyword usable like 'kw <expr>'.
1102         // If so, we have a regex, otherwise, we have a division (a variable
1103         // or literal being divided by something).
1104         var w = expr.substring(prevWord(expr, ind), ind+1);
1105         return (kwActions.indexOf(w) !== -1 || w === "do" || w === "else");
1106     }
1107     else if (ch === ")")
1108     {
1109         // We have a regex in the cases 'if (...) /blah/' and 'function name(...) /blah/'.
1110         ind = bwFindMatchingParen(expr, ind);
1111         if (ind === -1)
1112             return null;
1113         ind = prevNonWs(expr, ind);
1114         if (ind === -1)
1115             return false;
1116         if (!reJSChar.test(expr.charAt(ind)))
1117             return false;
1118         var wind = prevWord(expr, ind);
1119         if (kwCont.indexOf(expr.substring(wind, ind+1)) !== -1)
1120             return true;
1121         return isFunctionName(expr, wind);
1122     }
1123     else if (ch === "]")
1124     {
1125         return false;
1126     }
1127     return true;
1128 }
1129 
1130 // Check if a "{" in an expression is an object declaration.
1131 function isObjectDecl(expr, pos)
1132 {
1133     var ind = prevNonWs(expr, pos);
1134     if (ind === -1)
1135         return false;
1136     var ch = expr.charAt(ind);
1137     if (ch === ")" || ch === "{" || ch === "}" || ch === ";")
1138         return false;
1139     if (!reJSChar.test(ch))
1140         return true;
1141     var w = expr.substring(prevWord(expr, ind), ind+1);
1142     return (kwActions.indexOf(w) !== -1);
1143 }
1144 
1145 function isCommaProp(expr, start)
1146 {
1147     var beg = expr.lastIndexOf(",")+1;
1148     if (beg < start)
1149         beg = start;
1150     while (expr.charAt(beg) === " ")
1151         ++beg;
1152     var prop = expr.substr(beg);
1153     return isValidProperty(prop);
1154 }
1155 
1156 function simplifyExpr(expr)
1157 {
1158     var ret = "", len = expr.length, instr = false, strend, inreg = false, inclass, brackets = [];
1159 
1160     for (var i = 0; i < len; ++i)
1161     {
1162         var ch = expr.charAt(i);
1163         if (instr)
1164         {
1165             if (ch === strend)
1166             {
1167                 ret += '"';
1168                 instr = false;
1169             }
1170             else
1171             {
1172                 if (ch === "\\" && i+1 !== len)
1173                 {
1174                     ret += " ";
1175                     ++i;
1176                 }
1177                 ret += " ";
1178             }
1179         }
1180         else if (inreg)
1181         {
1182             if (inclass && ch === "]")
1183                 inclass = false;
1184             else if (!inclass && ch === "[")
1185                 inclass = true;
1186             else if (!inclass && ch === "/")
1187             {
1188                 // End of regex, eat regex flags
1189                 inreg = false;
1190                 while (i+1 !== len && reJSChar.test(expr.charAt(i+1)))
1191                 {
1192                     ret += " ";
1193                     ++i;
1194                 }
1195                 ret += '"';
1196             }
1197             if (inreg)
1198             {
1199                 if (ch === "\\" && i+1 !== len)
1200                 {
1201                     ret += " ";
1202                     ++i;
1203                 }
1204                 ret += " ";
1205             }
1206         }
1207         else
1208         {
1209             if (ch === "'" || ch === '"')
1210             {
1211                 instr = true;
1212                 strend = ch;
1213                 ret += '"';
1214             }
1215             else if (ch === "/")
1216             {
1217                 var re = endingDivIsRegex(ret);
1218                 if (re === null)
1219                     return null;
1220                 if (re)
1221                 {
1222                     inreg = true;
1223                     ret += '"';
1224                 }
1225                 else
1226                     ret += "/";
1227             }
1228             else
1229             {
1230                 if (reOpenBracket.test(ch))
1231                     brackets.push(ch);
1232                 else if (reCloseBracket.test(ch))
1233                 {
1234                     // Check for mismatched brackets
1235                     if (!brackets.length)
1236                         return null;
1237                     var br = brackets.pop();
1238                     if (br === "(" && ch !== ")")
1239                         return null;
1240                     if (br === "[" && ch !== "]")
1241                         return null;
1242                     if (br === "{" && ch !== "}")
1243                         return null;
1244                 }
1245                 ret += ch;
1246             }
1247         }
1248     }
1249 
1250     return ret;
1251 }
1252 
1253 // Check if auto-completion should be killed.
1254 function killCompletions(expr, origExpr)
1255 {
1256     // Make sure there is actually something to complete at the end.
1257     if (expr.length === 0)
1258         return true;
1259 
1260     if (reJSChar.test(expr[expr.length-1]) ||
1261             expr[expr.length-1] === ".")
1262     {
1263         // An expression at the end - we're fine.
1264     }
1265     else
1266     {
1267         var lastBr = expr.lastIndexOf("[");
1268         if (lastBr !== -1 && /^" *$/.test(expr.substr(lastBr+1)) &&
1269             origExpr.charAt(lastBr+1) !== "/")
1270         {
1271             // Array completions - we're fine.
1272         }
1273         else {
1274             return true;
1275         }
1276     }
1277 
1278     // Check for 'function i'.
1279     var ind = expr.lastIndexOf(" ");
1280     if (isValidProperty(expr.substr(ind+1)) && isFunctionName(expr, ind+1))
1281         return true;
1282 
1283     // Check for '{prop: ..., i'.
1284     var bwp = bwFindMatchingParen(expr, expr.length);
1285     if (bwp !== -1 && expr.charAt(bwp) === "{" &&
1286             isObjectDecl(expr, bwp) && isCommaProp(expr, bwp+1))
1287     {
1288         return true;
1289     }
1290 
1291     // Check for 'var prop..., i'.
1292     var vind = expr.lastIndexOf("var ");
1293     if (bwp < vind && isCommaProp(expr, vind+4))
1294     {
1295         // Note: This doesn't strictly work, because it kills completions even
1296         // when we have started a new expression and used the comma operator
1297         // in it (ie. 'var a; a, i'). This happens very seldom though, so it's
1298         // not really a problem.
1299         return true;
1300     }
1301 
1302     // Check for 'function f(i'.
1303     while (bwp !== -1 && expr.charAt(bwp) !== "(")
1304     {
1305         bwp = bwFindMatchingParen(expr, bwp);
1306     }
1307     if (bwp !== -1)
1308     {
1309         var ind = prevNonWs(expr, bwp);
1310         if (ind !== -1 && reJSChar.test(expr.charAt(ind)))
1311         {
1312             var stw = prevWord(expr, ind);
1313             if (expr.substring(stw, ind+1) === "function")
1314                 return true;
1315             if (isFunctionName(expr, stw))
1316                 return true;
1317         }
1318     }
1319     return false;
1320 }
1321 
1322 // Types the autocompletion knows about, some of their non-enumerable properties,
1323 // and the return types of some member functions.
1324 
1325 var AutoCompletionKnownTypes = {
1326     "void": {
1327         "_fb_ignorePrototype": true
1328     },
1329     "Array": {
1330         "pop": "|void",
1331         "push": "|void",
1332         "shift": "|void",
1333         "unshift": "|void",
1334         "reverse": "|Array",
1335         "sort": "|Array",
1336         "splice": "|Array",
1337         "concat": "|Array",
1338         "slice": "|Array",
1339         "join": "|String",
1340         "indexOf": "|Number",
1341         "lastIndexOf": "|Number",
1342         "filter": "|Array",
1343         "map": "|Array",
1344         "reduce": "|void",
1345         "reduceRight": "|void",
1346         "every": "|void",
1347         "forEach": "|void",
1348         "some": "|void",
1349         "length": "Number"
1350     },
1351     "String": {
1352         "_fb_contType": "String",
1353         "split": "|Array",
1354         "substr": "|String",
1355         "substring": "|String",
1356         "charAt": "|String",
1357         "charCodeAt": "|String",
1358         "concat": "|String",
1359         "indexOf": "|Number",
1360         "lastIndexOf": "|Number",
1361         "localeCompare": "|Number",
1362         "match": "|Array",
1363         "search": "|Number",
1364         "slice": "|String",
1365         "replace": "|String",
1366         "toLowerCase": "|String",
1367         "toLocaleLowerCase": "|String",
1368         "toUpperCase": "|String",
1369         "toLocaleUpperCase": "|String",
1370         "trim": "|String",
1371         "length": "Number"
1372     },
1373     "RegExp": {
1374         "test": "|void",
1375         "exec": "|Array",
1376         "lastIndex": "Number",
1377         "ignoreCase": "void",
1378         "global": "void",
1379         "multiline": "void",
1380         "source": "String"
1381     },
1382     "Date": {
1383         "getTime": "|Number",
1384         "getYear": "|Number",
1385         "getFullYear": "|Number",
1386         "getMonth": "|Number",
1387         "getDate": "|Number",
1388         "getDay": "|Number",
1389         "getHours": "|Number",
1390         "getMinutes": "|Number",
1391         "getSeconds": "|Number",
1392         "getMilliseconds": "|Number",
1393         "getUTCFullYear": "|Number",
1394         "getUTCMonth": "|Number",
1395         "getUTCDate": "|Number",
1396         "getUTCDay": "|Number",
1397         "getUTCHours": "|Number",
1398         "getUTCMinutes": "|Number",
1399         "getUTCSeconds": "|Number",
1400         "getUTCMilliseconds": "|Number",
1401         "setTime": "|void",
1402         "setYear": "|void",
1403         "setFullYear": "|void",
1404         "setMonth": "|void",
1405         "setDate": "|void",
1406         "setHours": "|void",
1407         "setMinutes": "|void",
1408         "setSeconds": "|void",
1409         "setMilliseconds": "|void",
1410         "setUTCFullYear": "|void",
1411         "setUTCMonth": "|void",
1412         "setUTCDate": "|void",
1413         "setUTCHours": "|void",
1414         "setUTCMinutes": "|void",
1415         "setUTCSeconds": "|void",
1416         "setUTCMilliseconds": "|void",
1417         "toUTCString": "|String",
1418         "toLocaleDateString": "|String",
1419         "toLocaleTimeString": "|String",
1420         "toLocaleFormat": "|String",
1421         "toDateString": "|String",
1422         "toTimeString": "|String",
1423         "toISOString": "|String",
1424         "toGMTString": "|String",
1425         "toJSON": "|String",
1426         "toString": "|String",
1427         "toLocaleString": "|String",
1428         "getTimezoneOffset": "|Number"
1429     },
1430     "Function": {
1431         "call": "|void",
1432         "apply": "|void",
1433         "length": "Number",
1434         "prototype": "void"
1435     },
1436     "HTMLElement": {
1437         "getElementsByClassName": "|NodeList",
1438         "getElementsByTagName": "|NodeList",
1439         "getElementsByTagNameNS": "|NodeList",
1440         "querySelector": "|HTMLElement",
1441         "querySelectorAll": "|NodeList",
1442         "firstChild": "HTMLElement",
1443         "lastChild": "HTMLElement",
1444         "firstElementChild": "HTMLElement",
1445         "lastElementChild": "HTMLElement",
1446         "parentNode": "HTMLElement",
1447         "previousSibling": "HTMLElement",
1448         "nextSibling": "HTMLElement",
1449         "previousElementSibling": "HTMLElement",
1450         "nextElementSibling": "HTMLElement",
1451         "children": "NodeList",
1452         "childNodes": "NodeList"
1453     },
1454     "NodeList": {
1455         "_fb_contType": "HTMLElement",
1456         "length": "Number",
1457         "item": "|HTMLElement",
1458         "namedItem": "|HTMLElement"
1459     },
1460     "Window": {
1461         "encodeURI": "|String",
1462         "encodeURIComponent": "|String",
1463         "decodeURI": "|String",
1464         "decodeURIComponent": "|String",
1465         "eval": "|void",
1466         "parseInt": "|Number",
1467         "parseFloat": "|Number",
1468         "isNaN": "|void",
1469         "isFinite": "|void",
1470         "NaN": "Number",
1471         "Math": "Math",
1472         "undefined": "void",
1473         "Infinity": "Number"
1474     },
1475     "HTMLDocument": {
1476         "querySelector": "|HTMLElement",
1477         "querySelectorAll": "|NodeList"
1478     },
1479     "Math": {
1480         "E": "Number",
1481         "LN2": "Number",
1482         "LN10": "Number",
1483         "LOG2E": "Number",
1484         "LOG10E": "Number",
1485         "PI": "Number",
1486         "SQRT1_2": "Number",
1487         "SQRT2": "Number",
1488         "abs": "|Number",
1489         "acos": "|Number",
1490         "asin": "|Number",
1491         "atan": "|Number",
1492         "atan2": "|Number",
1493         "ceil": "|Number",
1494         "cos": "|Number",
1495         "exp": "|Number",
1496         "floor": "|Number",
1497         "log": "|Number",
1498         "max": "|Number",
1499         "min": "|Number",
1500         "pow": "|Number",
1501         "random": "|Number",
1502         "round": "|Number",
1503         "sin": "|Number",
1504         "sqrt": "|Number",
1505         "tan": "|Number"
1506     },
1507     "Number": {
1508         "valueOf": "|Number",
1509         "toFixed": "|String",
1510         "toExponential": "|String",
1511         "toPrecision": "|String",
1512         "toLocaleString": "|String",
1513         "toString": "|String"
1514     }
1515 };
1516 
1517 var LinkType = {
1518     "PROPERTY": 0,
1519     "INDEX": 1,
1520     "CALL": 2,
1521     "RETVAL_HEURISTIC": 3
1522 };
1523 
1524 function getKnownType(t)
1525 {
1526     var known = AutoCompletionKnownTypes;
1527     if (known.hasOwnProperty(t))
1528         return known[t];
1529     return null;
1530 }
1531 
1532 function getKnownTypeInfo(r)
1533 {
1534     if (r.charAt(0) === "|")
1535         return {"val": "Function", "ret": r.substr(1)};
1536     return {"val": r};
1537 }
1538 
1539 function getFakeCompleteKeys(name)
1540 {
1541     var ret = [], type = getKnownType(name);
1542     if (!type)
1543         return ret;
1544     for (var prop in type) {
1545         if (prop.substr(0, 4) !== "_fb_")
1546             ret.push(prop);
1547     }
1548     return ret;
1549 }
1550 
1551 function eatProp(expr, start)
1552 {
1553     for (var i = start; i < expr.length; ++i)
1554         if (!reJSChar.test(expr.charAt(i)))
1555             break;
1556     return i;
1557 }
1558 
1559 function matchingBracket(expr, start)
1560 {
1561     var count = 1;
1562     for (var i = start + 1; i < expr.length; ++i) {
1563         var ch = expr.charAt(i);
1564         if (reOpenBracket.test(ch))
1565             ++count;
1566         else if (reCloseBracket.test(ch))
1567             if (!--count)
1568                 return i;
1569     }
1570     return -1;
1571 }
1572 
1573 function getTypeExtractionExpression(command)
1574 {
1575     // Return a JavaScript expression for determining the type / [[Class]] of
1576     // an object given by another JavaScript expression. For DOM nodes, return
1577     // HTMLElement instead of HTML[node type]Element, for simplicity.
1578     var ret = "(function() { var v = " + command + "; ";
1579     ret += "if (window.HTMLElement && v instanceof HTMLElement) return 'HTMLElement'; ";
1580     ret += "return Object.prototype.toString.call(v).slice(8, -1);})()";
1581     return ret;
1582 }
1583 
1584 /**
1585  * Compare two property names a and b with a custom sort order. The comparison
1586  * is lexicographical, but treats _ as higher than other letters in the
1587  * beginning of the word, so that:
1588  *  $ < AutoCompleter < add_widget < additive < _ < _priv < __proto__
1589  * @return -1, 0 or 1 depending on whether (a < b), (a == b) or (a > b).
1590  */
1591 function comparePropertyNames(lhs, rhs)
1592 {
1593     var len = Math.min(lhs.length, rhs.length);
1594     for (var i = 0; i < len; ++i)
1595     {
1596         var u1 = (lhs.charAt(i) === "_");
1597         var u2 = (rhs.charAt(i) === "_");
1598         if (!u1 && !u2)
1599             break;
1600         if (!u1 || !u2)
1601             return (u1 ? 1 : -1);
1602     }
1603 
1604     if (lhs < rhs)
1605         return -1;
1606     return (lhs === rhs ? 0 : 1);
1607 }
1608 
1609 function propertiesToHide(expr, obj)
1610 {
1611     var ret = [];
1612 
1613     // __{define,lookup}[SG]etter__ appear as own properties on lots of DOM objects.
1614     ret.push("__defineGetter__", "__defineSetter__",
1615         "__lookupGetter__", "__lookupSetter__");
1616 
1617     // function.caller/argument are deprecated and ugly.
1618     if (typeof obj === "function")
1619         ret.push("caller", "arguments");
1620 
1621     if (Object.prototype.toString.call(obj) === "[object String]")
1622     {
1623         // Unused, cluttery.
1624         ret.push("toLocaleLowerCase", "toLocaleUpperCase", "quote", "bold",
1625             "italics", "fixed", "fontsize", "fontcolor", "link", "anchor",
1626             "strike", "small", "big", "blink", "sup", "sub");
1627     }
1628 
1629     if (expr === "" || expr === "window.")
1630     {
1631         // Internal Firefox things.
1632         ret.push("getInterface", "Components", "XPCNativeWrapper",
1633             "InstallTrigger", "WindowInternal", "DocumentXBL",
1634             "startProfiling", "stopProfiling", "pauseProfilers",
1635             "resumeProfilers", "dumpProfile", "netscape",
1636             "BoxObject", "BarProp", "BrowserFeedWriter", "ChromeWindow",
1637             "ElementCSSInlineStyle", "JSWindow", "NSEditableElement",
1638             "NSRGBAColor", "NSEvent", "NSXPathExpression", "ToString",
1639             "OpenWindowEventDetail", "Parser", "ParserJS", "Rect",
1640             "RGBColor", "ROCSSPrimitiveValue", "RequestService",
1641             "PaintRequest", "PaintRequestList", "WindowUtils",
1642             "GlobalPropertyInitializer", "GlobalObjectConstructor"
1643         );
1644 
1645         // Hide ourselves.
1646         ret.push("_FirebugCommandLine", "_firebug");
1647     }
1648 
1649     // Old and ugly.
1650     if (expr === "document.")
1651         ret.push("fgColor", "vlinkColor", "linkColor");
1652     if (expr === "document.body.")
1653         ret.push("link", "aLink", "vLink");
1654 
1655     // Rather universal and feel like built-ins.
1656     ret.push("valueOf", "toSource", "constructor", "QueryInterface");
1657 
1658     return ret;
1659 }
1660 
1661 
1662 function setCompletionsFromObject(out, object, context)
1663 {
1664     // 'object' is a user-level, non-null object.
1665     try
1666     {
1667         var isObjectPrototype = function(obj)
1668         {
1669             // Check if an object is "Object.prototype". This isn't as simple
1670             // as 'obj === context.window.wrappedJSObject.Object.prototype' due
1671             // to cross-window properties, nor just '!Object.getPrototypeOf(obj)'
1672             // because of Object.create.
1673             return !Object.getPrototypeOf(obj) && "hasOwnProperty" in obj;
1674         }
1675 
1676         var obj = object;
1677         while (obj !== null)
1678         {
1679             var target = (isObjectPrototype(obj) ?
1680                     out.hiddenCompletions : out.completions);
1681             target.push.apply(target, Object.getOwnPropertyNames(obj));
1682             obj = Object.getPrototypeOf(obj);
1683         }
1684 
1685         // As a special case, when completing "Object.prototype." no properties
1686         // should be hidden.
1687         if (isObjectPrototype(object))
1688         {
1689             out.completions = out.hiddenCompletions;
1690             out.hiddenCompletions = [];
1691         }
1692         else
1693         {
1694             // Hide a list of well-chosen annoying properties.
1695             var hide = propertiesToHide(out.spreExpr, object);
1696             var hideMap = Object.create(null);
1697             for (var i = 0; i < hide.length; ++i)
1698                 hideMap[hide[i]] = 1;
1699             var hideRegex = /^XUL[A-Za-z]+$/;
1700 
1701             var newCompletions = [];
1702             out.completions.forEach(function(prop)
1703             {
1704                 if (prop in hideMap || hideRegex.test(prop))
1705                     out.hiddenCompletions.push(prop);
1706                 else
1707                     newCompletions.push(prop);
1708             });
1709             out.completions = newCompletions;
1710         }
1711 
1712         // Firefox hides __proto__ - add it back.
1713         if ("__proto__" in object)
1714             out.hiddenCompletions.push("__proto__");
1715     }
1716     catch (exc)
1717     {
1718         if (FBTrace.DBG_COMMANDLINE)
1719             FBTrace.sysout("autoCompleter.getCompletionsFromPrototypeChain failed", exc);
1720     }
1721 }
1722 
1723 function propChainBuildComplete(out, context, tempExpr, result)
1724 {
1725     var done = function(result)
1726     {
1727         if (result !== undefined && result !== null)
1728         {
1729             if (typeof result !== "object" && typeof result !== "function")
1730             {
1731                 // Convert the primitive into its scope's matching object type.
1732                 result = Wrapper.getContentView(out.window).Object(result);
1733             }
1734             setCompletionsFromObject(out, result, context);
1735         }
1736     };
1737 
1738     if (tempExpr.fake)
1739     {
1740         var name = tempExpr.value.val;
1741         if (getKnownType(name)._fb_ignorePrototype)
1742             return;
1743         var command = name + ".prototype";
1744         Firebug.CommandLine.evaluate(name + ".prototype", context, context.thisValue, null,
1745             function found(result, context)
1746             {
1747                 done(result);
1748             },
1749             function failed(result, context) { }
1750         );
1751     }
1752     else
1753     {
1754         done(result);
1755     }
1756 }
1757 
1758 function evalPropChainStep(step, tempExpr, evalChain, out, context)
1759 {
1760     if (tempExpr.fake)
1761     {
1762         if (step === evalChain.length)
1763         {
1764             propChainBuildComplete(out, context, tempExpr);
1765             return;
1766         }
1767 
1768         var link = evalChain[step], type = link.type;
1769         if (type === LinkType.PROPERTY || type === LinkType.INDEX)
1770         {
1771             // Use the accessed property if it exists, otherwise abort. It
1772             // would be possible to continue with a 'real' expression of
1773             // `tempExpr.value.val`.prototype, but since prototypes seldom
1774             // contain actual values of things this doesn't work very well.
1775             var mem = (type === LinkType.INDEX ? "_fb_contType" : link.name);
1776             var t = getKnownType(tempExpr.value.val);
1777             if (t.hasOwnProperty(mem))
1778                 tempExpr.value = getKnownTypeInfo(t[mem]);
1779             else
1780                 return;
1781         }
1782         else if (type === LinkType.CALL)
1783         {
1784             if (tempExpr.value.ret)
1785                 tempExpr.value = getKnownTypeInfo(tempExpr.value.ret);
1786             else
1787                 return;
1788         }
1789         evalPropChainStep(step+1, tempExpr, evalChain, out, context);
1790     }
1791     else
1792     {
1793         var funcCommand = null, link, type;
1794         while (step !== evalChain.length)
1795         {
1796             link = evalChain[step];
1797             type = link.type;
1798             if (type === LinkType.PROPERTY)
1799             {
1800                 tempExpr.thisCommand = tempExpr.command;
1801                 tempExpr.command += "." + link.name;
1802             }
1803             else if (type === LinkType.INDEX)
1804             {
1805                 tempExpr.thisCommand = "window";
1806                 tempExpr.command += "[" + link.cont + "]";
1807             }
1808             else if (type === LinkType.CALL)
1809             {
1810                 if (link.origCont !== null &&
1811                      (link.name.substr(0, 3) === "get" ||
1812                       (link.name.charAt(0) === "$" && link.cont.indexOf(",") === -1)))
1813                 {
1814                     // Names beginning with get or $ are almost always getters, so
1815                     // assume we can safely just call it.
1816                     tempExpr.thisCommand = "window";
1817                     tempExpr.command += "(" + link.origCont + ")";
1818                 }
1819                 else if (!link.name)
1820                 {
1821                     // We cannot know about functions without name; try the
1822                     // heuristic directly.
1823                     link.type = LinkType.RETVAL_HEURISTIC;
1824                     evalPropChainStep(step, tempExpr, evalChain, out, context);
1825                     return;
1826                 }
1827                 else
1828                 {
1829                     funcCommand = getTypeExtractionExpression(tempExpr.thisCommand);
1830                     break;
1831                 }
1832             }
1833             else if (type === LinkType.RETVAL_HEURISTIC)
1834             {
1835                 funcCommand = "Function.prototype.toString.call(" + tempExpr.command + ")";
1836                 break;
1837             }
1838             ++step;
1839         }
1840 
1841         var func = (funcCommand !== null), command = (func ? funcCommand : tempExpr.command);
1842         Firebug.CommandLine.evaluate(command, context, context.thisValue, null,
1843             function found(result, context)
1844             {
1845                 if (func)
1846                 {
1847                     if (type === LinkType.CALL)
1848                     {
1849                         if (typeof result !== "string")
1850                             return;
1851 
1852                         var t = getKnownType(result);
1853                         if (t && t.hasOwnProperty(link.name))
1854                         {
1855                             var propVal = getKnownTypeInfo(t[link.name]);
1856 
1857                             // Make sure the property is a callable function
1858                             if (!propVal.ret)
1859                                 return;
1860 
1861                             tempExpr.fake = true;
1862                             tempExpr.value = getKnownTypeInfo(propVal.ret);
1863                             evalPropChainStep(step+1, tempExpr, evalChain, out, context);
1864                         }
1865                         else
1866                         {
1867                             // Unknown 'this' type or function name, use
1868                             // heuristics on the function instead.
1869                             link.type = LinkType.RETVAL_HEURISTIC;
1870                             evalPropChainStep(step, tempExpr, evalChain, out, context);
1871                         }
1872                     }
1873                     else if (type === LinkType.RETVAL_HEURISTIC)
1874                     {
1875                         if (typeof result !== "string")
1876                             return;
1877 
1878                         // Perform some crude heuristics for figuring out the
1879                         // return value of a function based on its contents.
1880                         // It's certainly not perfect, and it's easily fooled
1881                         // into giving wrong results,  but it might work in
1882                         // some common cases.
1883 
1884                         // Check for chaining functions. This is done before
1885                         // checking for nested functions, because completing
1886                         // results of member functions containing nested
1887                         // functions that use 'return this' seems uncommon,
1888                         // and being wrong is not a huge problem.
1889                         if (result.indexOf("return this;") !== -1)
1890                         {
1891                             tempExpr.command = tempExpr.thisCommand;
1892                             tempExpr.thisCommand = "window";
1893                             evalPropChainStep(step+1, tempExpr, evalChain, out, context);
1894                             return;
1895                         }
1896 
1897                         // Don't support nested functions.
1898                         if (result.lastIndexOf("function") !== 0)
1899                             return;
1900 
1901                         // Check for arrays.
1902                         if (result.indexOf("return [") !== -1)
1903                         {
1904                             tempExpr.fake = true;
1905                             tempExpr.value = getKnownTypeInfo("Array");
1906                             evalPropChainStep(step+1, tempExpr, evalChain, out, context);
1907                             return;
1908                         }
1909 
1910                         // Check for 'return new Type(...);', and use the
1911                         // prototype as a pseudo-object for those (since it
1912                         // is probably not a known type that we can fake).
1913                         var newPos = result.indexOf("return new ");
1914                         if (newPos !== -1)
1915                         {
1916                             var rest = result.substr(newPos + 11),
1917                                 epos = rest.search(/[^a-zA-Z0-9_$.]/);
1918                             if (epos !== -1)
1919                             {
1920                                 rest = rest.substring(0, epos);
1921                                 tempExpr.command = rest + ".prototype";
1922                                 evalPropChainStep(step+1, tempExpr, evalChain, out, context);
1923                                 return;
1924                             }
1925                         }
1926                     }
1927                 }
1928                 else
1929                 {
1930                     propChainBuildComplete(out, context, tempExpr, result);
1931                 }
1932             },
1933             function failed(result, context) { }
1934         );
1935     }
1936 }
1937 
1938 function evalPropChain(out, preExpr, origExpr, context)
1939 {
1940     var evalChain = [], linkStart = 0, len = preExpr.length, lastProp = "";
1941     var tempExpr = {"fake": false, "command": "window", "thisCommand": "window"};
1942     while (linkStart !== len)
1943     {
1944         var ch = preExpr.charAt(linkStart);
1945         if (linkStart === 0)
1946         {
1947             if (preExpr.substr(0, 4) === "new ")
1948             {
1949                 var parInd = preExpr.indexOf("(");
1950                 tempExpr.command = preExpr.substring(4, parInd) + ".prototype";
1951                 linkStart = matchingBracket(preExpr, parInd) + 1;
1952             }
1953             else if (ch === "[")
1954             {
1955                 tempExpr.fake = true;
1956                 tempExpr.value = getKnownTypeInfo("Array");
1957                 linkStart = matchingBracket(preExpr, linkStart) + 1;
1958             }
1959             else if (ch === '"')
1960             {
1961                 var isRegex = (origExpr.charAt(0) === "/");
1962                 tempExpr.fake = true;
1963                 tempExpr.value = getKnownTypeInfo(isRegex ? "RegExp" : "String");
1964                 linkStart = preExpr.indexOf('"', 1) + 1;
1965             }
1966             else if (!isNaN(ch))
1967             {
1968                 // The expression is really a decimal number.
1969                 return false;
1970             }
1971             else if (reJSChar.test(ch))
1972             {
1973                 // The expression begins with a regular property name
1974                 var nextLink = eatProp(preExpr, linkStart);
1975                 lastProp = preExpr.substring(linkStart, nextLink);
1976                 linkStart = nextLink;
1977                 tempExpr.command = lastProp;
1978             }
1979 
1980             // Syntax error (like '.') or a too complicated expression.
1981             if (linkStart === 0)
1982                 return false;
1983         }
1984         else
1985         {
1986             if (ch === ".")
1987             {
1988                 // Property access
1989                 var nextLink = eatProp(preExpr, linkStart+1);
1990                 lastProp = preExpr.substring(linkStart+1, nextLink);
1991                 linkStart = nextLink;
1992                 evalChain.push({"type": LinkType.PROPERTY, "name": lastProp});
1993             }
1994             else if (ch === "(")
1995             {
1996                 // Function call. Save the function name and the arguments if
1997                 // they are safe to evaluate.
1998                 var endCont = matchingBracket(preExpr, linkStart);
1999                 var cont = preExpr.substring(linkStart+1, endCont), origCont = null;
2000                 if (reLiteralExpr.test(cont))
2001                     origCont = origExpr.substring(linkStart+1, endCont);
2002                 linkStart = endCont + 1;
2003                 evalChain.push({
2004                     "type": LinkType.CALL,
2005                     "name": lastProp,
2006                     "origCont": origCont,
2007                     "cont": cont
2008                 });
2009 
2010                 lastProp = "";
2011             }
2012             else if (ch === "[")
2013             {
2014                 // Index. Use the supplied index if it is a literal; otherwise
2015                 // it is probably a loop index with a variable not yet defined
2016                 // (like 'for(var i = 0; i < ar.length; ++i) ar[i].prop'), and
2017                 // '0' seems like a reasonably good guess at a valid index.
2018                 var endInd = matchingBracket(preExpr, linkStart);
2019                 var ind = preExpr.substring(linkStart+1, endInd);
2020                 if (reLiteralExpr.test(ind))
2021                     ind = origExpr.substring(linkStart+1, endInd);
2022                 else
2023                     ind = "0";
2024                 linkStart = endInd+1;
2025                 evalChain.push({"type": LinkType.INDEX, "cont": ind});
2026                 lastProp = "";
2027             }
2028             else
2029             {
2030                 // Syntax error
2031                 return false;
2032             }
2033         }
2034     }
2035 
2036     evalPropChainStep(0, tempExpr, evalChain, out, context);
2037     return true;
2038 }
2039 
2040 function autoCompleteEval(context, preExpr, spreExpr, includeCurrentScope)
2041 {
2042     var out = {
2043         spreExpr: spreExpr,
2044         completions: [],
2045         hiddenCompletions: [],
2046         window: context.baseWindow || context.window
2047     };
2048     var indexCompletion = false;
2049 
2050     try
2051     {
2052         if (spreExpr)
2053         {
2054             // Complete member variables of some .-chained expression
2055 
2056             // In case of array indexing, remove the bracket and set a flag to
2057             // escape completions.
2058             var len = spreExpr.length;
2059             if (len >= 2 && spreExpr[len-2] === "[" && spreExpr[len-1] === '"')
2060             {
2061                 indexCompletion = true;
2062                 out.indexQuoteType = preExpr[len-1];
2063                 spreExpr = spreExpr.substr(0, len-2);
2064                 preExpr = preExpr.substr(0, len-2);
2065             }
2066             else
2067             {
2068                 // Remove the trailing dot (if there is one)
2069                 var lastDot = spreExpr.lastIndexOf(".");
2070                 if (lastDot !== -1)
2071                 {
2072                     spreExpr = spreExpr.substr(0, lastDot);
2073                     preExpr = preExpr.substr(0, lastDot);
2074                 }
2075             }
2076 
2077             if (FBTrace.DBG_COMMANDLINE)
2078                 FBTrace.sysout("commandLine.autoCompleteEval pre:'" + preExpr +
2079                     "' spre:'" + spreExpr + "'.");
2080 
2081             // Don't auto-complete '.'.
2082             if (spreExpr === "")
2083                 return out;
2084 
2085             evalPropChain(out, spreExpr, preExpr, context);
2086         }
2087         else
2088         {
2089             // Complete variables from the local scope
2090 
2091             var contentView = Wrapper.getContentView(out.window);
2092             if (context.stopped && includeCurrentScope)
2093             {
2094                 out.completions = Firebug.Debugger.getCurrentFrameKeys(context);
2095             }
2096             else if (contentView && contentView.Window &&
2097                 contentView.constructor.toString() === contentView.Window.toString())
2098                 // Cross window type pseudo-comparison
2099             {
2100                 setCompletionsFromObject(out, contentView, context);
2101             }
2102             else  // hopefully sandbox in Chromebug
2103             {
2104                 setCompletionsFromObject(out, context.global, context);
2105             }
2106         }
2107 
2108         if (indexCompletion)
2109         {
2110             // If we are doing index-completions, add "] to everything.
2111             function convertQuotes(x)
2112             {
2113                 x = (out.indexQuoteType === '"') ? Str.escapeJS(x): Str.escapeSingleQuoteJS(x);
2114                 return x + out.indexQuoteType + "]";
2115             }
2116             out.completions = out.completions.map(convertQuotes);
2117             out.hiddenCompletions = out.hiddenCompletions.map(convertQuotes);
2118         }
2119         else if (out.completions.indexOf("length") !== -1 && out.completions.indexOf("0") !== -1)
2120         {
2121             // ... otherwise remove numeric keys from array-like things.
2122             var rePositiveNumber = /^[1-9][0-9]*$/;
2123             out.completions = out.completions.filter(function(x)
2124             {
2125                 return !rePositiveNumber.test(x) && x !== "0";
2126             });
2127         }
2128 
2129         // Sort the completions, and avoid duplicates.
2130         // XXX: If we make it possible to show both regular and hidden completions
2131         // at the same time, completions must shadow hiddenCompletions.
2132         out.completions = Arr.sortUnique(out.completions, comparePropertyNames);
2133         out.hiddenCompletions = Arr.sortUnique(out.hiddenCompletions, comparePropertyNames);
2134     }
2135     catch (exc)
2136     {
2137         if (FBTrace.DBG_ERRORS && FBTrace.DBG_COMMANDLINE)
2138             FBTrace.sysout("commandLine.autoCompleteEval FAILED", exc);
2139     }
2140     return out;
2141 }
2142 
2143 var reValidJSToken = /^[A-Za-z_$][A-Za-z_$0-9]*$/;
2144 function isValidProperty(value)
2145 {
2146     // Use only string props
2147     if (typeof(value) != "string")
2148         return false;
2149 
2150     // Use only those props that don't contain unsafe charactes and so need
2151     // quotation (e.g. object["my prop"] notice the space character).
2152     // Following expression checks that the name starts with a letter or $_,
2153     // and there are only letters, numbers or $_ character in the string (no spaces).
2154 
2155     return reValidJSToken.test(value);
2156 }
2157 
2158 function setCursorToEOL(input)
2159 {
2160     // textbox version, https://developer.mozilla.org/en/XUL/Property/inputField
2161     // input.inputField.setSelectionRange(len, len);
2162     input.setSelectionRange(input.value.length, input.value.length);
2163 }
2164 
2165 // ********************************************************************************************* //
2166 // Registration
2167 
2168 return Firebug.JSAutoCompleter;
2169 
2170 // ********************************************************************************************* //
2171 });
2172