1 /* See license.txt for terms of usage */ 2 3 define([ 4 "firebug/lib/object", 5 "firebug/firebug", 6 "arch/compilationunit", 7 "firebug/lib/events", 8 "firebug/js/sourceLink", 9 "firebug/lib/css", 10 "firebug/lib/dom", 11 "firebug/lib/string", 12 ], 13 function(Obj, Firebug, CompilationUnit, Events, SourceLink, Css, Dom, Str) { 14 15 // ********************************************************************************************* // 16 17 /** 18 * @class Defines the API for SourceBoxDecorator and provides the default implementation. 19 * Decorators are passed the source box on construction, called to create the HTML, 20 * and called whenever the user scrolls the view. 21 */ 22 Firebug.SourceBoxDecorator = function(sourceBox) 23 { 24 } 25 26 Firebug.SourceBoxDecorator.sourceBoxCounter = 0; 27 28 Firebug.SourceBoxDecorator.prototype = 29 /** @lends Firebug.SourceBoxDecorator */ 30 { 31 onSourceBoxCreation: function(sourceBox) 32 { 33 // allow panel-document unique ids to be generated for lines. 34 sourceBox.uniqueId = ++Firebug.SourceBoxDecorator.sourceBoxCounter; 35 }, 36 37 /** 38 * called on a delay after the view port is updated, eg vertical scroll 39 * The sourceBox will contain lines from firstRenderedLine to lastRenderedLine 40 * The user will be able to see sourceBox.firstViewableLine to sourceBox.lastViewableLine 41 */ 42 decorate: function(sourceBox, compilationUnit) 43 { 44 return; 45 }, 46 47 /** 48 * called once as each line is being rendered. 49 * @param lineNo integer 1-maxLineNumbers 50 */ 51 getUserVisibleLineNumber: function(sourceBox, lineNo) 52 { 53 return lineNo; 54 }, 55 56 /** 57 * call once as each line is being rendered. 58 * @param lineNo integer 1-maxLineNumbers 59 */ 60 getLineHTML: function(sourceBox, lineNo) 61 { 62 var line = sourceBox.lines[lineNo-1]; 63 64 // Crop huge lines. 65 if (Firebug.maxScriptLineLength > 0) 66 { 67 if (line.length > Firebug.maxScriptLineLength) 68 line = Str.cropString(line, Firebug.maxScriptLineLength); 69 } 70 71 var html = Str.escapeForSourceLine(line); 72 73 // If the pref says so, replace tabs by corresponding number of spaces. 74 if (Firebug.replaceTabs > 0) 75 { 76 var space = new Array(Firebug.replaceTabs + 1).join(" "); 77 html = html.replace(/\t/g, space); 78 } 79 80 return html; 81 }, 82 83 /** 84 * @return a string unique to the sourcebox and line number, valid in getElementById() 85 */ 86 getLineId: function(sourceBox, lineNo) 87 { 88 return 'sb' + sourceBox.uniqueId + '-L' + lineNo; 89 }, 90 } 91 92 // ********************************************************************************************* // 93 94 /** 95 * @panel Firebug.SourceBoxPanel: Intermediate level class for showing lines of source, eg Script Panel 96 * Implements a 'viewport' to render only the lines the user is viewing or has recently viewed. 97 * Scroll events or scrollToLine calls are converted to viewableRange line number range. 98 * The range of lines is rendered, skipping any that have already been rendered. Then if the 99 * new line range overlaps the old line range, done; else delete the old range. 100 * That way the lines kept contiguous. 101 * The rendering details are delegated to SourceBoxDecorator; each source line may be expanded into 102 * more rendered lines. 103 */ 104 Firebug.SourceBoxPanel = function() {}; 105 106 var SourceBoxPanelBase = Obj.extend(Firebug.MeasureBox, Firebug.ActivablePanel); 107 Firebug.SourceBoxPanel = Obj.extend(SourceBoxPanelBase, 108 /** @lends Firebug.SourceBoxPanel */ 109 { 110 initialize: function(context, doc) 111 { 112 this.onResize = Obj.bind(this.resizer, this); 113 this.sourceBoxes = {}; 114 this.decorator = this.getDecorator(); 115 116 Firebug.ActivablePanel.initialize.apply(this, arguments); 117 }, 118 119 initializeNode: function(panelNode) 120 { 121 this.resizeEventTarget = Firebug.chrome.$('fbContentBox'); 122 Events.addEventListener(this.resizeEventTarget, "resize", this.onResize, true); 123 this.attachToCache(); 124 125 Firebug.ActivablePanel.initializeNode.apply(this, arguments); 126 }, 127 128 destroyNode: function() 129 { 130 if (this.resizeEventTarget) 131 { 132 Events.removeEventListener(this.resizeEventTarget, "resize", this.onResize, true); 133 } 134 else 135 { 136 if (FBTrace.DBG_ERRORS) 137 FBTrace.sysout("sourceBox.destroyNode; ERROR this.resizeEventTarget is NULL " + 138 this, this); 139 } 140 141 this.detachFromCache(); 142 143 Firebug.ActivablePanel.destroyNode.apply(this, arguments); 144 }, 145 146 attachToCache: function() 147 { 148 this.context.sourceCache.addListener(this); 149 }, 150 151 detachFromCache: function() 152 { 153 this.context.sourceCache.removeListener(this); 154 }, 155 156 onTextSizeChange: function(zoom) 157 { 158 this.refresh(); 159 }, 160 161 removeAllSourceBoxes: function() 162 { 163 for (var url in this.sourceBoxes) 164 { 165 var sourceBox = this.sourceBoxes[url]; 166 if (sourceBox) 167 { 168 try 169 { 170 this.panelNode.removeChild(sourceBox); 171 } 172 catch (err) 173 { 174 if (FBTrace.DBG_ERRORS) 175 FBTrace.sysout("sourceBox.removeAllSourceBoxes; EXCEPTION " + err, err); 176 } 177 } 178 else if (FBTrace.DBG_ERRORS) 179 { 180 FBTrace.sysout("sourceBoxPanel ERROR no sourceBox at "+url+" in context "+ 181 this.context.getName()); 182 } 183 } 184 185 this.sourceBoxes = {}; 186 187 delete this.selectedSourceBox; 188 delete this.location; 189 }, 190 191 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 192 // TabCache listener implementation 193 194 onStartRequest: function(context, request) 195 { 196 197 }, 198 199 onStopRequest: function(context, request, responseText) 200 { 201 if (context === this.context) 202 { 203 var url = request.URI.spec; 204 var compilationUnit = context.getCompilationUnit(url); 205 206 // The compilation unit is created when JSD is compiling the script 207 // (e.g. onTopLevelScriptCreated), but onStopRequest can be triggered 208 // before (by a response channel listener) and so, the compilation 209 // unit doesn't have to exist at this moment 210 // However it should be ok, since the UI shouldn't exist before compilation 211 // and so, there shouldn't be what to update. 212 if (compilationUnit) 213 this.removeSourceBoxByCompilationUnit(compilationUnit); 214 } 215 }, 216 217 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 218 219 /** 220 * Panel extension point. 221 * Called just before box is shown 222 */ 223 updateSourceBox: function(sourceBox) 224 { 225 226 }, 227 228 /* Panel extension point. Called on panel initialization 229 * @return Must implement SourceBoxDecorator API. 230 */ 231 getDecorator: function() 232 { 233 return new Firebug.SourceBoxDecorator(); 234 }, 235 236 /* Panel extension point 237 * @return string eg "js" or "css" 238 */ 239 getSourceType: function() 240 { 241 throw "SourceBox.getSourceType: Need to override in extender "; 242 }, 243 244 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 245 246 disablePanel: function(module) 247 { 248 this.sourceBoxes = {}; // clear so we start fresh if enabled 249 Firebug.ActivablePanel.disablePanel.apply(this, arguments); 250 }, 251 252 getSourceLinesFrom: function(selection) 253 { 254 // https://developer.mozilla.org/en/DOM/Selection 255 if (selection.isCollapsed) 256 return ""; 257 258 var anchorSourceRow = Dom.getAncestorByClass(selection.anchorNode, "sourceRow"); 259 var focusSourceRow = Dom.getAncestorByClass(selection.focusNode, "sourceRow"); 260 if (anchorSourceRow == focusSourceRow) 261 { 262 return selection.toString();// trivial case 263 } 264 265 var buf = this.getSourceLine(anchorSourceRow, selection.anchorOffset); 266 var currentSourceRow = anchorSourceRow.nextSibling; 267 268 while(currentSourceRow && (currentSourceRow != focusSourceRow) && 269 Css.hasClass(currentSourceRow, "sourceRow")) 270 { 271 buf += this.getSourceLine(currentSourceRow); 272 currentSourceRow = currentSourceRow.nextSibling; 273 } 274 275 buf += this.getSourceLine(focusSourceRow, 0, selection.focusOffset); 276 return buf; 277 }, 278 279 getSourceLine: function(sourceRow, beginOffset, endOffset) 280 { 281 var source = Dom.getChildByClass(sourceRow, "sourceRowText").textContent; 282 if (endOffset) 283 source = source.substring(beginOffset, endOffset); 284 else if (beginOffset) 285 source = source.substring(beginOffset); 286 else 287 source = source; 288 289 return source; 290 }, 291 292 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 293 294 getSourceBoxByCompilationUnit: function(compilationUnit) 295 { 296 if (compilationUnit.getURL()) 297 { 298 var sourceBox = this.getSourceBoxByURL(compilationUnit.getURL()); 299 if (sourceBox && sourceBox.repObject == compilationUnit) 300 return sourceBox; 301 else 302 return null; // cause a new one to be created 303 } 304 }, 305 306 getCompilationUnit: function() 307 { 308 if (this.selectedSourceBox) 309 return this.seletedSourceBox.repObject; 310 }, 311 312 getSourceBoxByURL: function(url) 313 { 314 if (!this.sourceBoxes) 315 return null; 316 317 return url ? this.sourceBoxes[url] : null; 318 }, 319 320 removeSourceBoxByCompilationUnit: function(compilationUnit) 321 { 322 var sourceBox = this.getSourceBoxByCompilationUnit(compilationUnit); 323 if (sourceBox) // else we did not create one for this compilationUnit 324 { 325 delete this.sourceBoxes[compilationUnit.getURL()]; 326 327 if (sourceBox.parentNode === this.panelNode) 328 this.panelNode.removeChild(sourceBox); 329 330 if (this.selectedSourceBox === sourceBox) // need to update the view 331 { 332 delete this.selectedSourceBox; 333 delete this.location; 334 this.showSource(compilationUnit.getURL()); 335 } 336 } 337 }, 338 339 renameSourceBox: function(oldURL, newURL) 340 { 341 var sourceBox = this.sourceBoxes[oldURL]; 342 if (sourceBox) 343 { 344 delete this.sourceBoxes[oldURL]; 345 this.sourceBoxes[newURL] = sourceBox; 346 } 347 }, 348 349 showSource: function(url) 350 { 351 var sourceBox = this.getOrCreateSourceBox(url); 352 this.showSourceBox(sourceBox); 353 }, 354 355 getOrCreateSourceBox: function(url) 356 { 357 var compilationUnit = this.context.getCompilationUnit(url); 358 359 if (FBTrace.DBG_COMPILATION_UNITS) 360 FBTrace.sysout("firebug.showSource: "+url, compilationUnit); 361 362 if (!compilationUnit) 363 return; 364 365 var sourceBox = this.getSourceBoxByCompilationUnit(compilationUnit); 366 if (!sourceBox) 367 sourceBox = this.createSourceBox(compilationUnit); 368 369 return sourceBox; 370 }, 371 372 /** 373 * Assumes that locations are compilationUnits, TODO lower class 374 */ 375 showSourceLink: function(sourceLink) 376 { 377 var sourceBox = this.getOrCreateSourceBox(sourceLink.href); 378 379 if (sourceBox) 380 { 381 if (sourceLink.line) 382 { 383 this.showSourceBox(sourceBox, sourceLink.line); 384 this.scrollToLine(sourceLink.href, sourceLink.line, 385 this.jumpHighlightFactory(sourceLink.line, this.context)); 386 } 387 else 388 { 389 this.showSourceBox(sourceBox); 390 } 391 392 Events.dispatch(this.fbListeners, "onShowSourceLink", [this, sourceLink.line]); 393 } 394 395 // then clear it so the next link will scroll and highlight. 396 if (sourceLink == this.selection) 397 delete this.selection; 398 }, 399 400 showSourceBox: function(sourceBox, lineNo) 401 { 402 if (this.selectedSourceBox) 403 Dom.collapse(this.selectedSourceBox, true); 404 405 if (this.selectedSourceBox !== sourceBox) 406 delete this.currentSearch; 407 408 this.selectedSourceBox = sourceBox; 409 410 if (sourceBox) 411 { 412 sourceBox.targetedLineNumber = lineNo; // signal reView to put this line in the center 413 Dom.collapse(sourceBox, false); 414 this.reView(sourceBox); 415 this.updateSourceBox(sourceBox); 416 } 417 }, 418 419 /** 420 * Private, do not call outside of this object 421 * A sourceBox is a div with additional operations and state. 422 * @param compilationUnit there is at most one sourceBox for each compilationUnit 423 */ 424 createSourceBox: function(compilationUnit) // decorator(compilationUnit, sourceBox) 425 { 426 var sourceBox = this.initializeSourceBox(compilationUnit); 427 428 sourceBox.decorator = this.decorator; 429 430 // Framework connection 431 sourceBox.decorator.onSourceBoxCreation(sourceBox); 432 433 this.sourceBoxes[compilationUnit.getURL()] = sourceBox; 434 435 if (FBTrace.DBG_COMPILATION_UNITS) 436 FBTrace.sysout("firebug.createSourceBox with " + sourceBox.maximumLineNumber + 437 " lines for "+compilationUnit+(compilationUnit.getURL()?" sourceBoxes":" anon "), 438 sourceBox); 439 440 this.panelNode.appendChild(sourceBox); 441 this.setSourceBoxLineSizes(sourceBox); 442 443 return sourceBox; 444 }, 445 446 getSourceBoxURL: function(sourceBox) 447 { 448 return sourceBox.repObject.getURL(); 449 }, 450 451 initializeSourceBox: function(compilationUnit) 452 { 453 var sourceBox = this.document.createElement("div"); 454 Css.setClass(sourceBox, "sourceBox"); 455 Dom.collapse(sourceBox, true); 456 sourceBox.repObject = compilationUnit; 457 compilationUnit.sourceBox = sourceBox; 458 459 sourceBox.getLineNode = function(lineNo) 460 { 461 // XXXjjb this method is supposed to return null if the lineNo is not in the viewport 462 return this.ownerDocument.getElementById(this.decorator.getLineId(this, lineNo)); 463 }; 464 465 var paddedSource = 466 "<div class='topSourcePadding'>" + 467 "<div class='sourceRow'><div class='sourceLine'></div><div class='sourceRowText'></div></div>"+ 468 "</div>"+ 469 "<div class='sourceViewport'></div>"+ 470 "<div class='bottomSourcePadding'>"+ 471 "<div class='sourceRow'><div class='sourceLine'></div><div class='sourceRowText'></div></div>"+ 472 "</div>"; 473 474 Dom.appendInnerHTML(sourceBox, paddedSource); 475 476 sourceBox.viewport = Dom.getChildByClass(sourceBox, 'sourceViewport'); 477 return sourceBox; 478 }, 479 480 onSourceLinesAvailable: function(compilationUnit, firstLineAvailable, lastLineAvailable, lines) 481 { 482 var sourceBox = compilationUnit.sourceBox; 483 var requestedLines = compilationUnit.pendingViewRange; 484 delete compilationUnit.pendingViewRange; 485 486 if (requestedLines) // then are viewing a range 487 { 488 if (firstLineAvailable > requestedLines.firstLine) 489 requestedLines.firstLine = firstLineAvailable; 490 491 if (lastLineAvailable < requestedLines.lastLine) 492 requestedLines.lastLine = lastLineAvailable; 493 } 494 else // then no range was given, render all. 495 { 496 requestedLines = {firstLine: firstLineAvailable, lastLine: lastLineAvailable}; 497 } 498 499 sourceBox.lines = lines; // an array indexed from firstLineAvailable to lastLineAvailable 500 501 sourceBox.maximumLineNumber = compilationUnit.getNumberOfLines(); 502 sourceBox.maxLineNoChars = (sourceBox.maximumLineNumber + "").length; 503 504 this.setSourceBoxLineSizes(sourceBox); 505 506 this.reViewOnSourceLinesAvailable(sourceBox, requestedLines); 507 }, 508 509 setSourceBoxLineSizes: function(sourceBox) 510 { 511 var view = sourceBox.viewport; 512 513 var lineNoCharsSpacer = ""; 514 for (var i = 0; i < sourceBox.maxLineNoChars; i++) 515 lineNoCharsSpacer += "0"; 516 517 this.startMeasuring(view); 518 var size = this.measureText(lineNoCharsSpacer); 519 this.stopMeasuring(); 520 521 sourceBox.lineHeight = size.height + 1; 522 sourceBox.lineNoWidth = size.width; 523 524 var view = sourceBox.viewport; // TODO some cleaner way 525 view.previousSibling.firstChild.firstChild.style.width = sourceBox.lineNoWidth + "px"; 526 view.nextSibling.firstChild.firstChild.style.width = sourceBox.lineNoWidth + "px"; 527 528 if (FBTrace.DBG_COMPILATION_UNITS) 529 { 530 FBTrace.sysout("setSourceBoxLineSizes size for lineNoCharsSpacer " + 531 lineNoCharsSpacer, size); 532 FBTrace.sysout("firebug.setSourceBoxLineSizes, sourceBox.scrollTop " + 533 sourceBox.scrollTop+ " sourceBox.lineHeight: "+sourceBox.lineHeight+ 534 " sourceBox.lineNoWidth:"+sourceBox.lineNoWidth); 535 } 536 }, 537 538 /** 539 * @return SourceLink to currently selected source file 540 */ 541 getSourceLink: function(lineNo) 542 { 543 if (!this.selectedSourceBox) 544 return; 545 546 if (!lineNo) 547 lineNo = this.getCentralLine(this.selectedSourceBox); 548 549 return new SourceLink.SourceLink(this.selectedSourceBox.repObject.href, lineNo, 550 this.getSourceType()); 551 }, 552 553 /** 554 * Select sourcebox with href, scroll lineNo into center, highlight lineNo with 555 * highlighter given 556 * 557 * @param href a Url, null means the selected compilationUnit 558 * @param lineNo integer 1 - maximumLineNumber 559 * @param highlighter callback, a function(sourceBox). sourceBox.centralLine will be lineNo 560 */ 561 scrollToLine: function(href, lineNo, highlighter) 562 { 563 if (FBTrace.DBG_COMPILATION_UNITS) 564 FBTrace.sysout("SourceBoxPanel.scrollToLine: "+lineNo+"@"+href+" with highlighter "+ 565 highlighter, highlighter); 566 567 if (this.context.scrollTimeout) 568 { 569 this.context.clearTimeout(this.context.scrollTimeout); 570 delete this.context.scrollTimeout 571 } 572 573 if (href) 574 { 575 var sourceBox = this.getOrCreateSourceBox(href); 576 this.showSourceBox(sourceBox, lineNo); 577 } 578 579 if (!this.skipScrolling(lineNo)) 580 { 581 var viewRange = this.getViewRangeFromTargetLine(this.selectedSourceBox, lineNo); 582 this.selectedSourceBox.newScrollTop = this.getScrollTopFromViewRange( 583 this.selectedSourceBox, viewRange); 584 585 if (FBTrace.DBG_COMPILATION_UNITS) 586 FBTrace.sysout("SourceBoxPanel.scrollTimeout: newScrollTop "+ 587 this.selectedSourceBox.newScrollTop+" vs old "+ 588 this.selectedSourceBox.scrollTop+" for "+this.selectedSourceBox.repObject.href); 589 590 // *may* cause scrolling 591 this.selectedSourceBox.scrollTop = this.selectedSourceBox.newScrollTop; 592 } 593 594 this.context.scrollTimeout = this.context.setTimeout(Obj.bindFixed(function() 595 { 596 if (!this.selectedSourceBox) 597 { 598 if (FBTrace.DBG_COMPILATION_UNITS) 599 FBTrace.sysout("SourceBoxPanel.scrollTimeout no selectedSourceBox"); 600 return; 601 } 602 603 if (this.selectedSourceBox.highlighter) 604 this.applyDecorator(this.selectedSourceBox); // may need to highlight even if we don't scroll 605 606 if (FBTrace.DBG_COMPILATION_UNITS) 607 FBTrace.sysout("SourceBoxPanel.scrollTimeout: scrollTo "+lineNo+ 608 " this.selectedSourceBox.highlighter: "+this.selectedSourceBox.highlighter); 609 }, this)); 610 611 this.selectedSourceBox.highlighter = highlighter; // clears if null 612 }, 613 614 skipScrolling: function(lineNo) 615 { 616 var skipScrolling = false; 617 var firstViewRangeElement = this.selectedSourceBox.getLineNode( 618 this.selectedSourceBox.firstViewableLine); 619 var scrollTopOffset = this.selectedSourceBox.scrollTop - firstViewRangeElement.offsetTop; 620 621 if (FBTrace.DBG_COMPILATION_UNITS) 622 FBTrace.sysout("SourceBoxPanel.skipScrolling scrollTopOffset "+ 623 Math.abs(scrollTopOffset) + " > " + firstViewRangeElement.offsetHeight); 624 625 if (Math.abs(scrollTopOffset) > firstViewRangeElement.offsetHeight) 626 return skipScrolling; 627 628 if (this.selectedSourceBox.firstViewableLine && this.selectedSourceBox.lastViewableLine) 629 { 630 var linesFromTop = lineNo - this.selectedSourceBox.firstViewableLine; 631 var linesFromBot = this.selectedSourceBox.lastViewableLine - lineNo; 632 skipScrolling = (linesFromTop > 3 && linesFromBot > 3); 633 if (FBTrace.DBG_COMPILATION_UNITS) 634 FBTrace.sysout("SourceBoxPanel.skipScrolling: skipScrolling: "+skipScrolling+ 635 " fromTop:"+linesFromTop+" fromBot:"+linesFromBot); 636 } 637 else // the selectedSourceBox has not been built 638 { 639 if (FBTrace.DBG_COMPILATION_UNITS) 640 FBTrace.sysout("SourceBoxPanel.skipScrolling, no viewable lines", 641 this.selectedSourceBox); 642 } 643 644 return skipScrolling; 645 }, 646 647 /** 648 * @return a highlighter function(sourceBox) that puts a class on the line for a time slice 649 */ 650 jumpHighlightFactory: function(lineNo, context) 651 { 652 if (FBTrace.DBG_COMPILATION_UNITS) 653 FBTrace.sysout("sourceBox.jumpHighlightFactory; highlighter created for " + lineNo); 654 655 return function jumpHighlightIfInView(sourceBox) 656 { 657 var lineNode = sourceBox.getLineNode(lineNo); 658 659 if (context.highlightedRow) 660 Css.cancelClassTimed(context.highlightedRow, "jumpHighlight", context); 661 662 if (lineNode) 663 { 664 Css.setClassTimed(lineNode, "jumpHighlight", context); 665 666 context.highlightedRow = lineNode; 667 668 if (FBTrace.DBG_COMPILATION_UNITS) 669 FBTrace.sysout("jumpHighlightFactory on line "+lineNo+" lineNode:"+ 670 lineNode.innerHTML); 671 } 672 else 673 { 674 if (FBTrace.DBG_COMPILATION_UNITS) 675 FBTrace.sysout("jumpHighlightFactory no node at line "+lineNo, sourceBox); 676 } 677 678 return false; // not sticky 679 } 680 }, 681 682 /* 683 * resize and scroll event handler 684 */ 685 resizer: function(event) 686 { 687 // The resize target is Firebug as a whole. But most of the UI needs no special 688 // code for resize. 689 // But our SourceBoxPanel has viewport that will change size. 690 if (this.selectedSourceBox && this.visible) 691 { 692 if (FBTrace.DBG_COMPILATION_UNITS) 693 FBTrace.sysout("resizer event: "+event.type+" in panel "+this.name+" for "+ 694 this.context.getName(), event); 695 696 this.reView(this.selectedSourceBox); 697 } 698 }, 699 700 // called for all scroll events, including any time sourcebox.scrollTop is set 701 reView: function(sourceBox, clearCache) 702 { 703 if (sourceBox.targetedLineNumber) // then we requested a certain line 704 { 705 var viewRange = this.getViewRangeFromTargetLine(sourceBox, sourceBox.targetedLineNumber); 706 if (FBTrace.DBG_COMPILATION_UNITS) 707 FBTrace.sysout("reView got viewRange from target line: "+ 708 sourceBox.targetedLineNumber, viewRange); 709 710 // We've positioned on the targeted line. Now the user may scroll 711 delete sourceBox.targetedLineNumber; 712 713 // our current scrolltop is not useful, so clear the saved value to avoid comparing below. 714 delete sourceBox.lastScrollTop; 715 } 716 else // no special line, assume scrolling 717 { 718 var viewRange = this.getViewRangeFromScrollTop(sourceBox, sourceBox.scrollTop); 719 if (FBTrace.DBG_COMPILATION_UNITS) 720 FBTrace.sysout("reView got viewRange from scrollTop: "+sourceBox.scrollTop, viewRange); 721 } 722 723 if (clearCache) 724 { 725 this.clearSourceBox(sourceBox); 726 } 727 else if (sourceBox.scrollTop === sourceBox.lastScrollTop && sourceBox.clientHeight && 728 sourceBox.clientHeight === sourceBox.lastClientHeight) 729 { 730 if (sourceBox.firstRenderedLine <= viewRange.firstLine && 731 sourceBox.lastRenderedLine >= viewRange.lastLine) 732 { 733 if (FBTrace.DBG_COMPILATION_UNITS) 734 FBTrace.sysout("reView skipping sourceBox "+sourceBox.scrollTop+ 735 "=scrollTop="+sourceBox.lastScrollTop+", "+ sourceBox.clientHeight+ 736 "=clientHeight="+sourceBox.lastClientHeight, sourceBox); 737 738 // skip work if nothing changes. 739 return; 740 } 741 } 742 743 var compilationUnit = sourceBox.repObject; 744 compilationUnit.pendingViewRange = viewRange; 745 compilationUnit.getSourceLines(viewRange.firstLine, viewRange.lastLine, 746 Obj.bind(this.onSourceLinesAvailable, this)); 747 }, 748 749 reViewOnSourceLinesAvailable: function(sourceBox, viewRange) 750 { 751 // XXXjjb TODO where should this be? 752 Events.dispatch(this.fbListeners, "onBeforeViewportChange", [this]); 753 754 this.buildViewAround(sourceBox, viewRange); 755 756 if (Firebug.uiListeners.length > 0) 757 { 758 var link = new SourceLink.SourceLink(sourceBox.repObject.href, sourceBox.centralLine, 759 this.getSourceType()); 760 761 Events.dispatch(Firebug.uiListeners, "onViewportChange", [link]); 762 } 763 764 sourceBox.lastScrollTop = sourceBox.scrollTop; 765 sourceBox.lastClientHeight = sourceBox.clientHeight; 766 767 if (FBTrace.DBG_COMPILATION_UNITS) 768 FBTrace.sysout("sourceBox.reViewOnSourceLinesAvailable sourceBox.lastScrollTop "+ 769 sourceBox.lastScrollTop+" sourceBox.lastClientHeight "+sourceBox.lastClientHeight); 770 }, 771 772 buildViewAround: function(sourceBox, viewRange) 773 { 774 try 775 { 776 this.updateViewportCache(sourceBox, viewRange); 777 } 778 catch(exc) 779 { 780 if(FBTrace.DBG_ERRORS) 781 FBTrace.sysout("buildViewAround updateViewportCache FAILS "+exc, exc); 782 } 783 784 Dom.collapse(sourceBox, false); // the elements must be visible for the offset values 785 this.setViewportPadding(sourceBox, viewRange); 786 787 sourceBox.centralLine = Math.ceil((viewRange.lastLine + viewRange.firstLine)/2); 788 789 this.applyDecorator(sourceBox); 790 791 return; 792 }, 793 794 updateViewportCache: function(sourceBox, viewRange) 795 { 796 var cacheHit = this.insertedLinesOverlapCache(sourceBox, viewRange); 797 798 if (!cacheHit) 799 { 800 this.clearSourceBox(sourceBox); // no overlap, remove old range 801 sourceBox.firstRenderedLine = viewRange.firstLine; // reset cached range 802 sourceBox.lastRenderedLine = viewRange.lastLine; 803 } 804 else // cache overlap, expand range of cache 805 { 806 sourceBox.firstRenderedLine = Math.min(viewRange.firstLine, sourceBox.firstRenderedLine); 807 sourceBox.lastRenderedLine = Math.max(viewRange.lastLine, sourceBox.lastRenderedLine); 808 } 809 810 // todo actually check that these are viewable 811 sourceBox.firstViewableLine = viewRange.firstLine; 812 sourceBox.lastViewableLine = viewRange.lastLine; 813 sourceBox.numberOfRenderedLines = sourceBox.lastRenderedLine - sourceBox.firstRenderedLine + 1; 814 815 if (FBTrace.DBG_COMPILATION_UNITS) 816 FBTrace.sysout("buildViewAround viewRange: "+viewRange.firstLine+"-"+ 817 viewRange.lastLine+" rendered: "+sourceBox.firstRenderedLine+"-"+ 818 sourceBox.lastRenderedLine, sourceBox); 819 }, 820 821 /* 822 * Add lines from viewRange, but do not adjust first/lastRenderedLine. 823 * @return true if viewRange overlaps first/lastRenderedLine 824 */ 825 insertedLinesOverlapCache: function(sourceBox, viewRange) 826 { 827 var topCacheLine = null; 828 var cacheHit = false; 829 for (var line = viewRange.firstLine; line <= viewRange.lastLine; line++) 830 { 831 if (line >= sourceBox.firstRenderedLine && line <= sourceBox.lastRenderedLine ) 832 { 833 cacheHit = true; 834 continue; 835 } 836 837 var lineHTML = this.getSourceLineHTML(sourceBox, line); 838 839 var ref = null; 840 if (line < sourceBox.firstRenderedLine) // prepend if we are above the cache 841 { 842 if (!topCacheLine) 843 topCacheLine = sourceBox.getLineNode(sourceBox.firstRenderedLine); 844 ref = topCacheLine; 845 } 846 847 var newElement = Dom.appendInnerHTML(sourceBox.viewport, lineHTML, ref); 848 } 849 return cacheHit; 850 }, 851 852 clearSourceBox: function(sourceBox) 853 { 854 if (sourceBox.firstRenderedLine) 855 { 856 var topMostCachedElement = sourceBox.getLineNode(sourceBox.firstRenderedLine); // eg 1 857 var totalCached = sourceBox.lastRenderedLine - sourceBox.firstRenderedLine + 1; // eg 20 - 1 + 1 = 19 858 if (topMostCachedElement && totalCached) 859 this.removeLines(sourceBox, topMostCachedElement, totalCached); 860 } 861 sourceBox.lastRenderedLine = 0; 862 sourceBox.firstRenderedLine = 0; 863 sourceBox.numberOfRenderedLines = 0; 864 }, 865 866 getSourceLineHTML: function(sourceBox, i) 867 { 868 var lineNo = sourceBox.decorator.getUserVisibleLineNumber(sourceBox, i); 869 var lineHTML = sourceBox.decorator.getLineHTML(sourceBox, i); 870 871 // decorator lines may not have ids 872 var lineId = sourceBox.decorator.getLineId(sourceBox, i); 873 874 var lineNoText = this.getTextForLineNo(lineNo, sourceBox.maxLineNoChars); 875 876 var theHTML = 877 '<div ' 878 + (lineId ? ('id="' + lineId + '"') : "") 879 + ' class="sourceRow" role="presentation"><a class="' 880 + 'sourceLine' + '" role="presentation">' 881 + lineNoText 882 + '</a><span class="sourceRowText" role="presentation">' 883 + lineHTML 884 + '</span></div>'; 885 886 return theHTML; 887 }, 888 889 getTextForLineNo: function(lineNo, maxLineNoChars) 890 { 891 // Make sure all line numbers are the same width (with a fixed-width font) 892 var lineNoText = lineNo + ""; 893 while (lineNoText.length < maxLineNoChars) 894 lineNoText = " " + lineNoText; 895 896 return lineNoText; 897 }, 898 899 removeLines: function(sourceBox, firstRemoval, totalRemovals) 900 { 901 for(var i = 1; i <= totalRemovals; i++) 902 { 903 var nextSourceLine = firstRemoval; 904 firstRemoval = firstRemoval.nextSibling; 905 sourceBox.viewport.removeChild(nextSourceLine); 906 } 907 }, 908 909 getCentralLine: function(sourceBox) 910 { 911 return sourceBox.centralLine; 912 }, 913 914 getViewRangeFromTargetLine: function(sourceBox, targetLineNumber) 915 { 916 var viewRange = {firstLine: 1, centralLine: targetLineNumber, lastLine: 1}; 917 918 var averageLineHeight = this.getAverageLineHeight(sourceBox); 919 var panelHeight = this.panelNode.clientHeight; 920 // We never want viewableLines * lineHeight > clientHeight 921 // So viewableLines <= clientHeight / lineHeight 922 // 923 // In some cases when Script panel is restored flooring the result can cause 924 // loosing one line and so, the Script panel is not properly restored 925 // (the top line is less by one) 926 // Math.floor changed to Math.round 927 // So, 'viewableLines * lineHeight' can be a bit higher than 'clientHeight'. 928 var linesPerViewport = Math.round((panelHeight / averageLineHeight)); 929 930 viewRange.firstLine = Math.round(targetLineNumber - linesPerViewport / 2); 931 932 if (viewRange.firstLine <= 0) 933 viewRange.firstLine = 1; 934 935 viewRange.lastLine = viewRange.firstLine + linesPerViewport; 936 937 if (viewRange.lastLine > sourceBox.maximumLineNumber) 938 viewRange.lastLine = sourceBox.maximumLineNumber; 939 940 return viewRange; 941 }, 942 943 /** 944 * Use the average height of source lines in the cache to estimate where the scroll bar 945 * points based on scrollTop 946 */ 947 getViewRangeFromScrollTop: function(sourceBox, scrollTop) 948 { 949 var viewRange = {}; 950 var averageLineHeight = this.getAverageLineHeight(sourceBox); 951 // If the scrollTop comes in zero, then we better pick line 1. (0 / 14) + 1 = 1 952 // If the scrollTop is in the middle of line 2, pick line 2. (20 / 14) + 1 = 2.43 ==> 2 953 viewRange.firstLine = Math.floor((scrollTop / averageLineHeight) + 1); 954 955 var panelHeight = this.panelNode.clientHeight; 956 957 // then we probably have not inserted the elements yet and the clientHeight is bogus 958 if (panelHeight === 0) 959 panelHeight = this.panelNode.ownerDocument.documentElement.clientHeight; 960 961 // Set minimum height of the panel (in case Firebug UI is set to minimum using 962 // the splitter) such that the source box can be properly created (issue 4417). 963 panelHeight = (panelHeight < 100) ? 100 : panelHeight; 964 965 // see getViewRangeFromTargetLine 966 var viewableLines = Math.round((panelHeight / averageLineHeight)); 967 viewRange.lastLine = viewRange.firstLine + viewableLines - 1; // 15 = 1 + 15 - 1; 968 969 if (viewRange.lastLine > sourceBox.maximumLineNumber) 970 viewRange.lastLine = sourceBox.maximumLineNumber; 971 972 viewRange.centralLine = Math.ceil((viewRange.lastLine - viewRange.firstLine)/2); 973 974 if (FBTrace.DBG_COMPILATION_UNITS) 975 { 976 FBTrace.sysout("getViewRangeFromScrollTop scrollTop:"+scrollTop+" viewRange: "+ 977 viewRange.firstLine+"-"+viewRange.lastLine+" max: "+sourceBox.maximumLineNumber+ 978 " panelHeight "+panelHeight); 979 980 if (!this.noRecurse) 981 { 982 this.noRecurse = true; 983 var testScrollTop = this.getScrollTopFromViewRange(sourceBox, viewRange); 984 delete this.noRecurse; 985 986 FBTrace.sysout("getViewRangeFromScrollTop "+((scrollTop==testScrollTop)? 987 "checks":(scrollTop+"=!scrollTop!="+testScrollTop))); 988 } 989 } 990 991 return viewRange; 992 }, 993 994 /** 995 * inverse of the getViewRangeFromScrollTop. 996 * If the viewRange was set by targetLineNumber, then this value become the new scroll top 997 * else the value will be the same as the scrollbar's given value of scrollTop. 998 */ 999 getScrollTopFromViewRange: function(sourceBox, viewRange) 1000 { 1001 var averageLineHeight = this.getAverageLineHeight(sourceBox); 1002 // If the fist line is 1, scrollTop should be 0 14 * (1 - 1) = 0 1003 // If the first line is 2, scrollTop would be lineHeight 14 * (2 - 1) = 14 1004 1005 var scrollTop = averageLineHeight * (viewRange.firstLine - 1); 1006 1007 if (FBTrace.DBG_COMPILATION_UNITS) 1008 { 1009 FBTrace.sysout("getScrollTopFromViewRange viewRange:"+viewRange.firstLine+"-"+ 1010 viewRange.lastLine+" averageLineHeight: "+averageLineHeight+" scrollTop "+scrollTop); 1011 1012 if (!this.noRecurse) 1013 { 1014 this.noRecurse = true; 1015 var testViewRange = this.getViewRangeFromScrollTop(sourceBox, scrollTop); 1016 delete this.noRecurse; 1017 var vrStr = viewRange.firstLine+"-"+viewRange.lastLine; 1018 var tvrStr = testViewRange.firstLine+"-"+testViewRange.lastLine; 1019 1020 FBTrace.sysout("getScrollTopFromViewRange "+ 1021 ((vrStr==tvrStr)? "checks" : vrStr+"=!viewRange!="+tvrStr)); 1022 } 1023 } 1024 1025 return scrollTop; 1026 }, 1027 1028 /** 1029 * The virtual sourceBox height is the averageLineHeight * max lines 1030 * @return float 1031 */ 1032 getAverageLineHeight: function(sourceBox) 1033 { 1034 var averageLineHeight = sourceBox.lineHeight; // fall back to single line height 1035 1036 var renderedViewportHeight = sourceBox.viewport.clientHeight; 1037 var numberOfRenderedLines = sourceBox.numberOfRenderedLines; 1038 if (renderedViewportHeight && numberOfRenderedLines) 1039 averageLineHeight = renderedViewportHeight / numberOfRenderedLines; 1040 1041 return averageLineHeight; 1042 }, 1043 1044 /** 1045 * The virtual sourceBox = topPadding + sourceBox.viewport + bottomPadding 1046 * The viewport grows as more lines are added to the cache 1047 * The virtual sourceBox height is estimated from the average height lines in the 1048 * viewport cache 1049 */ 1050 getTotalPadding: function(sourceBox) 1051 { 1052 var numberOfRenderedLines = sourceBox.numberOfRenderedLines; 1053 if (!numberOfRenderedLines) 1054 return 0; 1055 1056 var max = sourceBox.maximumLineNumber; 1057 var averageLineHeight = this.getAverageLineHeight(sourceBox); 1058 // total box will be the average line height times total lines 1059 var virtualSourceBoxHeight = Math.floor(max * averageLineHeight); 1060 if (virtualSourceBoxHeight < sourceBox.clientHeight) 1061 { 1062 // the total - view-taken-up - scrollbar 1063 // clientHeight excludes scrollbar 1064 var totalPadding = sourceBox.clientHeight - sourceBox.viewport.clientHeight - 1; 1065 } 1066 else 1067 var totalPadding = virtualSourceBoxHeight - sourceBox.viewport.clientHeight; 1068 1069 if (FBTrace.DBG_COMPILATION_UNITS) 1070 FBTrace.sysout("getTotalPadding clientHeight:"+sourceBox.viewport.clientHeight+ 1071 " max: "+max+" gives total padding "+totalPadding); 1072 1073 return totalPadding; 1074 }, 1075 1076 setViewportPadding: function(sourceBox, viewRange) 1077 { 1078 var firstRenderedLineElement = sourceBox.getLineNode(sourceBox.firstRenderedLine); 1079 if (!firstRenderedLineElement) 1080 { 1081 // It's not an error if the panel is disabled. 1082 if (FBTrace.DBG_ERRORS && this.isEnabled()) 1083 FBTrace.sysout("setViewportPadding FAILS, no line at "+ 1084 sourceBox.firstRenderedLine, sourceBox); 1085 return; 1086 } 1087 1088 var averageLineHeight = this.getAverageLineHeight(sourceBox); 1089 // At this point our rendered range should surround our viewRange 1090 var linesOfPadding = sourceBox.firstRenderedLine; // above our viewRange.firstLine might be some rendered lines in the buffer. 1091 var topPadding = (linesOfPadding - 1) * averageLineHeight; // pixels 1092 // Because of rounding when converting from pixels to lines, topPadding can be +/- lineHeight/2, round up 1093 linesOfPadding = Math.floor( (topPadding + averageLineHeight)/ averageLineHeight); 1094 var topPadding = (linesOfPadding - 1)* averageLineHeight; 1095 1096 if (FBTrace.DBG_COMPILATION_UNITS) 1097 FBTrace.sysout("setViewportPadding topPadding = "+topPadding+ 1098 " = (linesOfPadding - 1)* averageLineHeight = ("+linesOfPadding+" - 1)* "+ 1099 averageLineHeight); 1100 1101 // we want the bottomPadding to take up the rest 1102 1103 var totalPadding = this.getTotalPadding(sourceBox); 1104 if (totalPadding < 0) 1105 var bottomPadding = Math.abs(totalPadding); 1106 else 1107 var bottomPadding = Math.floor(totalPadding - topPadding); 1108 1109 if (bottomPadding < 0) 1110 bottomPadding = 0; 1111 1112 var view = sourceBox.viewport; 1113 1114 // Set the size on the line number field so the padding is filled with 1115 // same style as source lines. 1116 view.previousSibling.style.height = topPadding + "px"; 1117 view.nextSibling.style.height = bottomPadding + "px"; 1118 1119 //sourceRow 1120 view.previousSibling.firstChild.style.height = topPadding + "px"; 1121 view.nextSibling.firstChild.style.height = bottomPadding + "px"; 1122 1123 //sourceLine 1124 view.previousSibling.firstChild.firstChild.style.height = topPadding + "px"; 1125 view.nextSibling.firstChild.firstChild.style.height = bottomPadding + "px"; 1126 1127 1128 if(FBTrace.DBG_COMPILATION_UNITS) 1129 { 1130 var firstViewRangeElement = sourceBox.getLineNode(viewRange.firstLine); 1131 var scrollTopOffset = sourceBox.scrollTop - firstViewRangeElement.offsetTop; 1132 FBTrace.sysout("setViewportPadding viewport offsetHeight: "+ 1133 sourceBox.viewport.offsetHeight+", clientHeight "+sourceBox.viewport.clientHeight); 1134 FBTrace.sysout("setViewportPadding sourceBox, offsetHeight: "+ 1135 sourceBox.offsetHeight+", clientHeight "+sourceBox.clientHeight+ 1136 ", scrollHeight: "+sourceBox.scrollHeight); 1137 FBTrace.sysout("setViewportPadding scrollTopOffset: "+scrollTopOffset+ 1138 " firstLine "+viewRange.firstLine+" bottom: "+bottomPadding+" top: "+topPadding); 1139 } 1140 1141 }, 1142 1143 applyDecorator: function(sourceBox) 1144 { 1145 if (this.context.sourceBoxDecoratorTimeout) 1146 { 1147 this.context.clearTimeout(this.context.sourceBoxDecoratorTimeout); 1148 delete this.context.sourceBoxDecoratorTimeout; 1149 } 1150 1151 // Run source code decorating on 150ms timeout, which is bigger than 1152 // the period in which scroll events are fired. So, if the user is moving 1153 // scroll-bar thumb (or quickly clicking on scroll-arrows), the source code is 1154 // not decorated (the timeout cleared by the code above) and the scrolling is fast. 1155 this.context.sourceBoxDecoratorTimeout = this.context.setTimeout( 1156 Obj.bindFixed(this.asyncDecorating, this, sourceBox), 150); 1157 1158 if (this.context.sourceBoxHighlighterTimeout) 1159 { 1160 this.context.clearTimeout(this.context.sourceBoxHighlighterTimeout); 1161 delete this.context.sourceBoxHighlighterTimeout; 1162 } 1163 1164 // Source code highlighting is using different timeout: 0ms. When searching 1165 // within the Script panel, the user expects immediate response. 1166 this.context.sourceBoxHighlighterTimeout = this.context.setTimeout( 1167 Obj.bindFixed(this.asyncHighlighting, this, sourceBox)); 1168 1169 if (FBTrace.DBG_COMPILATION_UNITS) 1170 FBTrace.sysout("applyDecorator "+sourceBox.repObject.url+" sourceBox.highlighter "+ 1171 sourceBox.highlighter, sourceBox); 1172 }, 1173 1174 asyncDecorating: function(sourceBox) 1175 { 1176 try 1177 { 1178 sourceBox.decorator.decorate(sourceBox, sourceBox.repObject); 1179 1180 if (Firebug.uiListeners.length > 0) 1181 Events.dispatch(Firebug.uiListeners, "onApplyDecorator", [sourceBox]); 1182 1183 if (FBTrace.DBG_COMPILATION_UNITS) 1184 FBTrace.sysout("sourceBoxDecoratorTimeout "+sourceBox.repObject, sourceBox); 1185 } 1186 catch (exc) 1187 { 1188 if (FBTrace.DBG_ERRORS) 1189 FBTrace.sysout("sourcebox applyDecorator FAILS "+exc, exc); 1190 } 1191 }, 1192 1193 asyncHighlighting: function(sourceBox) 1194 { 1195 try 1196 { 1197 if (FBTrace.DBG_COMPILATION_UNITS) 1198 FBTrace.sysout("asyncHighlighting "+sourceBox.repObject.url+ 1199 " sourceBox.highlighter "+sourceBox.highlighter, sourceBox); 1200 1201 if (sourceBox.highlighter) 1202 { 1203 // If the sticky flag is false, the highlight is removed, eg the search 1204 // and sourcelink highlights. 1205 // else the highlight must be removed by the caller, eg breakpoint 1206 // hit executable line. 1207 var sticky = sourceBox.highlighter(sourceBox); 1208 if (FBTrace.DBG_COMPILATION_UNITS) 1209 FBTrace.sysout("asyncHighlighting highlighter sticky:"+sticky, 1210 sourceBox.highlighter); 1211 1212 if (!sticky) 1213 delete sourceBox.highlighter; 1214 // else we delete these when we get highlighting call with invalid line (eg -1) 1215 } 1216 } 1217 catch (exc) 1218 { 1219 if (FBTrace.DBG_ERRORS) 1220 FBTrace.sysout("sourcebox highlighter FAILS "+exc, exc); 1221 } 1222 } 1223 }); 1224 1225 // ********************************************************************************************* // 1226 // Registration 1227 1228 return Firebug.SourceBoxPanel; 1229 1230 // ********************************************************************************************* // 1231 }); 1232