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