1 /* See license.txt for terms of usage */
  2 
  3 FBL.ns(function() { with (FBL) {
  4 
  5 
  6 
  7 /* Defines the API for SourceBoxDecorator and provides the default implementation.
  8  * Decorators are passed the source box on construction, called to create the HTML,
  9  * and called whenever the user scrolls the view.
 10  */
 11 Firebug.SourceBoxDecorator = function(sourceBox){}
 12 
 13 Firebug.SourceBoxDecorator.sourceBoxCounter = 0;
 14 
 15 Firebug.SourceBoxDecorator.prototype =
 16 {
 17     onSourceBoxCreation: function(sourceBox)
 18     {
 19         // allow panel-document unique ids to be generated for lines.
 20         sourceBox.uniqueId = ++Firebug.SourceBoxDecorator.sourceBoxCounter;
 21     },
 22     /* called on a delay after the view port is updated, eg vertical scroll
 23      * The sourceBox will contain lines from firstRenderedLine to lastRenderedLine
 24      * The user will be able to see sourceBox.firstViewableLine to sourceBox.lastViewableLine
 25      */
 26     decorate: function(sourceBox, sourceFile)
 27     {
 28         return;
 29     },
 30 
 31     /* called once as each line is being rendered.
 32     * @param lineNo integer 1-maxLineNumbers
 33     */
 34     getUserVisibleLineNumber: function(sourceBox, lineNo)
 35     {
 36         return lineNo;
 37     },
 38 
 39     /* call once as each line is being rendered.
 40     * @param lineNo integer 1-maxLineNumbers
 41     */
 42     getLineHTML: function(sourceBox, lineNo)
 43     {
 44         var html = escapeForSourceLine(sourceBox.lines[lineNo-1]);
 45 
 46         // If the pref says so, replace tabs by corresponding number of spaces.
 47         if (Firebug.replaceTabs > 0)
 48         {
 49             var space = new Array(Firebug.replaceTabs + 1).join(" ");
 50             html = html.replace(/\t/g, space);
 51         }
 52 
 53         return html;
 54     },
 55 
 56     /*
 57      * @return a string unique to the sourcebox and line number, valid in getElementById()
 58      */
 59     getLineId: function(sourceBox, lineNo)
 60     {
 61         return 'sb' + sourceBox.uniqueId + '-L' + lineNo;
 62     },
 63 }
 64 
 65 
 66 /*
 67  * @panel Firebug.SourceBoxPanel: Intermediate level class for showing lines of source, eg Script Panel
 68  * Implements a 'viewport' to render only the lines the user is viewing or has recently viewed.
 69  * Scroll events or scrollToLine calls are converted to viewableRange line number range.
 70  * The range of lines is rendered, skipping any that have already been rendered. Then if the
 71  * new line range overlaps the old line range, done; else delete the old range.
 72  * That way the lines kept contiguous.
 73  * The rendering details are delegated to SourceBoxDecorator; each source line may be expanded into
 74  * more rendered lines.
 75  */
 76 
 77 Firebug.SourceBoxPanel = function() {};
 78 /* @lends */
 79 Firebug.SourceBoxPanel = extend( extend(Firebug.MeasureBox, Firebug.ActivablePanel),
 80 {
 81 
 82     initialize: function(context, doc)
 83     {
 84         this.onResize =  bind(this.resizer, this);
 85 
 86         this.sourceBoxes = {};
 87         this.decorator = this.getDecorator();
 88         Firebug.Panel.initialize.apply(this, arguments);
 89     },
 90 
 91     initializeNode: function(panelNode)
 92     {
 93         this.resizeEventTarget = Firebug.chrome.$('fbContentBox');
 94         this.resizeEventTarget.addEventListener("resize", this.onResize, true);
 95     },
 96 
 97     reattach: function(doc)
 98     {
 99         var oldEventTarget = this.resizeEventTarget;
100         oldEventTarget.removeEventListener("resize", this.onResize, true);
101         Firebug.Panel.reattach.apply(this, arguments);
102         this.resizeEventTarget = Firebug.chrome.$('fbContentBox');
103         this.resizeEventTarget.addEventListener("resize", this.onResize, true);
104     },
105 
106     destroyNode: function()
107     {
108         Firebug.Panel.destroyNode.apply(this, arguments);
109         this.resizeEventTarget.removeEventListener("resize", this.onResize, true);
110     },
111 
112     // **************************************
113     /*  Panel extension point.
114      *  Called just before box is shown
115      */
116     updateSourceBox: function(sourceBox)
117     {
118 
119     },
120 
121     /* Panel extension point. Called on panel initialization
122      * @return Must implement SourceBoxDecorator API.
123      */
124     getDecorator: function()
125     {
126         return new Firebug.SourceBoxDecorator();
127      },
128 
129      /* Panel extension point
130       * @return string eg "js" or "css"
131       */
132     getSourceType: function()
133     {
134         throw "Need to override in extender";
135     },
136 
137     // **************************************
138     disablePanel: function(module)
139     {
140         this.sourceBoxes = {};  // clear so we start fresh if enabled
141         Firebug.ActivablePanel.disablePanel.apply(this, arguments);
142     },
143 
144     getSourceLinesFrom: function(selection)
145     {
146         // https://developer.mozilla.org/en/DOM/Selection
147         if (selection.isCollapsed)
148             return "";
149 
150         var anchorSourceRow = getAncestorByClass(selection.anchorNode, "sourceRow");
151         var focusSourceRow = getAncestorByClass(selection.focusNode, "sourceRow");
152         if (anchorSourceRow == focusSourceRow)
153         {
154             return selection.toString();// trivial case
155         }
156         var buf = this.getSourceLine(anchorSourceRow, selection.anchorOffset);
157 
158         var currentSourceRow = anchorSourceRow.nextSibling;
159         while(currentSourceRow && (currentSourceRow != focusSourceRow) && hasClass(currentSourceRow, "sourceRow"))
160         {
161             buf += this.getSourceLine(currentSourceRow);
162             currentSourceRow = currentSourceRow.nextSibling;
163         }
164         buf += this.getSourceLine(focusSourceRow, 0, selection.focusOffset);
165         return buf;
166     },
167 
168     getSourceLine: function(sourceRow, beginOffset, endOffset)
169     {
170         var source = getChildByClass(sourceRow, "sourceRowText").textContent;
171         if (endOffset)
172             source = source.substring(beginOffset, endOffset);
173         else if (beginOffset)
174             source = source.substring(beginOffset);
175         else
176             source = source;
177 
178         return source;
179     },
180 
181     // ****************************************************************************************
182 
183     getSourceBoxBySourceFile: function(sourceFile)
184     {
185         if (sourceFile.href)
186         {
187             var sourceBox = this.getSourceBoxByURL(sourceFile.href);
188             if (sourceBox && sourceBox.repObject == sourceFile)
189                 return sourceBox;
190             else
191                 return null;  // cause a new one to be created
192         }
193     },
194 
195     getSourceBoxByURL: function(url)
196     {
197         return url ? this.sourceBoxes[url] : null;
198     },
199 
200     renameSourceBox: function(oldURL, newURL)
201     {
202         var sourceBox = this.sourceBoxes[oldURL];
203         if (sourceBox)
204         {
205             delete this.sourceBoxes[oldURL];
206             this.sourceBoxes[newURL] = sourceBox;
207         }
208     },
209 
210     showSourceFile: function(sourceFile)
211     {
212         var sourceBox = this.getSourceBoxBySourceFile(sourceFile);
213         if (FBTrace.DBG_SOURCEFILES)
214             FBTrace.sysout("firebug.showSourceFile: "+sourceFile, sourceBox);
215         if (!sourceBox)
216             sourceBox = this.createSourceBox(sourceFile);
217 
218         this.showSourceBox(sourceBox);
219     },
220 
221     showSourceBox: function(sourceBox)
222     {
223         if (this.selectedSourceBox)
224             collapse(this.selectedSourceBox, true);
225 
226         this.selectedSourceBox = sourceBox;
227         delete this.currentSearch;
228 
229         if (sourceBox)
230         {
231             this.reView(sourceBox);
232             this.updateSourceBox(sourceBox);
233             collapse(sourceBox, false);
234         }
235     },
236 
237     /* Private, do not call outside of this object
238     * A sourceBox is a div with additional operations and state.
239     * @param sourceFile there is at most one sourceBox for each sourceFile
240     */
241     createSourceBox: function(sourceFile)  // decorator(sourceFile, sourceBox)
242     {
243         var sourceBox = this.initializeSourceBox(sourceFile);
244 
245         sourceBox.decorator = this.decorator;
246 
247         // Framework connection
248         sourceBox.decorator.onSourceBoxCreation(sourceBox);
249 
250         this.sourceBoxes[sourceFile.href] = sourceBox;
251 
252         if (FBTrace.DBG_SOURCEFILES)
253             FBTrace.sysout("firebug.createSourceBox with "+sourceBox.maximumLineNumber+" lines for "+sourceFile+(sourceFile.href?" sourceBoxes":" anon "), sourceBox);
254 
255         this.panelNode.appendChild(sourceBox);
256         this.setSourceBoxLineSizes(sourceBox);
257 
258         return sourceBox;
259     },
260 
261     initializeSourceBox: function(sourceFile)
262     {
263         var sourceBox = this.document.createElement("div");
264         setClass(sourceBox, "sourceBox");
265         collapse(sourceBox, true);
266 
267         var lines = sourceFile.loadScriptLines(this.context);
268         if (!lines)
269         {
270             lines = ["Failed to load source for sourceFile "+sourceFile];
271         }
272 
273         sourceBox.lines = lines;
274         sourceBox.repObject = sourceFile;
275 
276         sourceBox.maximumLineNumber = lines.length;
277         sourceBox.maxLineNoChars = (sourceBox.maximumLineNumber + "").length;
278 
279         sourceBox.getLineNode =  function(lineNo)
280         {
281             // XXXjjb this method is supposed to return null if the lineNo is not in the viewport
282             return $(this.decorator.getLineId(this, lineNo), this.ownerDocument);
283         };
284 
285         var paddedSource =
286             "<div class='topSourcePadding'>" +
287                 "<div class='sourceRow'><div class='sourceLine'></div><div class='sourceRowText'></div></div>"+
288             "</div>"+
289             "<div class='sourceViewport'></div>"+
290             "<div class='bottomSourcePadding'>"+
291                 "<div class='sourceRow'><div class='sourceLine'></div><div class='sourceRowText'></div></div>"+
292             "</div>";
293 
294         appendInnerHTML(sourceBox, paddedSource);
295 
296         sourceBox.viewport = getChildByClass(sourceBox, 'sourceViewport');
297         return sourceBox;
298     },
299 
300     setSourceBoxLineSizes: function(sourceBox)
301     {
302         var view = sourceBox.viewport;
303 
304         var lineNoCharsSpacer = "";
305         for (var i = 0; i < sourceBox.maxLineNoChars; i++)
306               lineNoCharsSpacer += "0";
307 
308         this.startMeasuring(view);
309         var size = this.measureText(lineNoCharsSpacer);
310         this.stopMeasuring();
311 
312         sourceBox.lineHeight = size.height + 1;
313         sourceBox.lineNoWidth = size.width;
314 
315         var view = sourceBox.viewport; // TODO some cleaner way
316         view.previousSibling.firstChild.firstChild.style.width = sourceBox.lineNoWidth + "px";
317         view.nextSibling.firstChild.firstChild.style.width = sourceBox.lineNoWidth +"px";
318 
319         if (FBTrace.DBG_SOURCEFILES)
320         {
321             FBTrace.sysout("setSourceBoxLineSizes size", size);
322             FBTrace.sysout("firebug.setSourceBoxLineSizes, sourceBox.scrollTop "+sourceBox.scrollTop+ " sourceBox.lineHeight: "+sourceBox.lineHeight+" sourceBox.lineNoWidth:"+sourceBox.lineNoWidth+"\n");
323         }
324     },
325 
326     /*
327      * @return SourceLink to currently selected source file
328      */
329     getSourceLink: function(lineNo)
330     {
331         if (!this.selectedSourceBox)
332             return;
333         if (!lineNo)
334             lineNo = this.getCentralLine(this.selectedSourceBox);
335         return new SourceLink(this.selectedSourceBox.repObject.href, lineNo, this.getSourceType());
336     },
337 
338     /* Select sourcebox with href, scroll lineNo into center, highlight lineNo with highlighter given
339      * @param href a URL, null means the selected sourcefile
340      * @param lineNo integer 1-maximumLineNumber
341      * @param highlighter callback, a function(sourceBox). sourceBox.centralLine will be lineNo
342      */
343     scrollToLine: function(href, lineNo, highlighter)
344     {
345         if (FBTrace.DBG_SOURCEFILES) FBTrace.sysout("SourceBoxPanel.scrollToLine: "+lineNo+"@"+href+"\n");
346 
347         if (this.context.scrollTimeout)
348         {
349             this.context.clearTimeout(this.contextscrollTimeout);
350             delete this.context.scrollTimeout
351         }
352 
353         if (href)
354         {
355             if (!this.selectedSourceBox || this.selectedSourceBox.repObject.href != href)
356             {
357                 var sourceFile = this.context.sourceFileMap[href];
358                 if (!sourceFile)
359                 {
360                     if(FBTrace.DBG_SOURCEFILES)
361                         FBTrace.sysout("scrollToLine FAILS, no sourceFile for href "+href, this.context.sourceFileMap);
362                     return;
363                 }
364                 this.navigate(sourceFile);
365             }
366         }
367 
368         this.context.scrollTimeout = this.context.setTimeout(bindFixed(function()
369         {
370             if (!this.selectedSourceBox)
371             {
372                 if (FBTrace.DBG_SOURCEFILES)
373                     FBTrace.sysout("SourceBoxPanel.scrollTimeout no selectedSourceBox");
374                 return;
375             }
376 
377             this.selectedSourceBox.targetedLine = lineNo;
378 
379             // At this time we know which sourcebox is selected but the viewport is not selected.
380             // We need to scroll, let the scroll handler set the viewport, then highlight any lines visible.
381             var skipScrolling = false;
382             if (this.selectedSourceBox.firstViewableLine && this.selectedSourceBox.lastViewableLine)
383             {
384                 var linesFromTop = lineNo - this.selectedSourceBox.firstViewableLine;
385                 var linesFromBot = this.selectedSourceBox.lastViewableLine - lineNo;
386                 skipScrolling = (linesFromTop > 3 && linesFromBot > 3);
387                 if (FBTrace.DBG_SOURCEFILES) FBTrace.sysout("SourceBoxPanel.scrollTimeout: skipScrolling: "+skipScrolling+" fromTop:"+linesFromTop+" fromBot:"+linesFromBot);
388             }
389             else  // the selectedSourceBox has not been built
390             {
391                 if (FBTrace.DBG_SOURCEFILES)
392                     FBTrace.sysout("SourceBoxPanel.scrollTimeout, no viewable lines", this.selectedSourceBox);
393             }
394 
395             if (highlighter)
396                  this.selectedSourceBox.highlighter = highlighter;
397 
398             if (!skipScrolling)
399             {
400                 var viewRange = this.getViewRangeFromTargetLine(this.selectedSourceBox, lineNo);
401                 this.selectedSourceBox.newScrollTop = this.getScrollTopFromViewRange(this.selectedSourceBox, viewRange);
402                 if (FBTrace.DBG_SOURCEFILES) FBTrace.sysout("SourceBoxPanel.scrollTimeout: newScrollTop "+this.selectedSourceBox.newScrollTop+" for "+this.selectedSourceBox.repObject.href);
403                 this.selectedSourceBox.scrollTop = this.selectedSourceBox.newScrollTop; // *may* cause scrolling
404                 if (FBTrace.DBG_SOURCEFILES) FBTrace.sysout("SourceBoxPanel.scrollTimeout: scrollTo "+lineNo+" scrollTop:"+this.selectedSourceBox.scrollTop+ " lineHeight: "+this.selectedSourceBox.lineHeight);
405             }
406 
407             if (this.selectedSourceBox.highlighter)
408                 this.applyDecorator(this.selectedSourceBox); // may need to highlight even if we don't scroll
409 
410         }, this));
411     },
412 
413     /*
414      * @return a highlighter function(sourceBox) that puts a class on the line for a time slice
415      */
416     jumpHighlightFactory: function(lineNo, context)
417     {
418         return function jumpHighlightIfInView(sourceBox)
419         {
420             var  lineNode = sourceBox.getLineNode(lineNo);
421             if (lineNode)
422             {
423                 setClassTimed(lineNode, "jumpHighlight", context);
424                 if (FBTrace.DBG_SOURCEFILES)
425                     FBTrace.sysout("jumpHighlightFactory on line "+lineNo+" lineNode:"+lineNode.innerHTML+"\n");
426             }
427             else
428             {
429                 if (FBTrace.DBG_SOURCEFILES)
430                     FBTrace.sysout("jumpHighlightFactory no node at line "+lineNo, sourceBox);
431             }
432 
433             return false; // not sticky
434         }
435     },
436 
437     /*
438      * resize and scroll event handler
439      */
440     resizer: function(event)
441     {
442         // The resize target is Firebug as a whole. But most of the UI needs no special code for resize.
443         // But our SourceBoxPanel has viewport that will change size.
444         if (this.selectedSourceBox && this.visible)
445         {
446             if (FBTrace.DBG_SOURCEFILES)
447                 FBTrace.sysout("resizer event: "+event.type, event);
448 
449             this.reView(this.selectedSourceBox);
450         }
451     },
452 
453     reView: function(sourceBox, clearCache)  // called for all scroll events, including any time sourcebox.scrollTop is set
454     {
455         if (sourceBox.targetedLine)
456         {
457             sourceBox.targetLineNumber = sourceBox.targetedLine;
458             var viewRange = this.getViewRangeFromTargetLine(sourceBox, sourceBox.targetedLine);
459             delete sourceBox.targetedLine;
460         }
461         else
462         {
463             var viewRange = this.getViewRangeFromScrollTop(sourceBox, sourceBox.scrollTop);
464         }
465 
466         if (clearCache)
467         {
468             this.clearSourceBox(sourceBox);
469         }
470         else if (sourceBox.scrollTop === sourceBox.lastScrollTop && sourceBox.clientHeight === sourceBox.lastClientHeight)
471         {
472             if (sourceBox.firstRenderedLine <= viewRange.firstLine && sourceBox.lastRenderedLine >= viewRange.lastLine)
473             {
474                 if (FBTrace.DBG_SOURCEFILES)
475                     FBTrace.sysout("reView skipping sourceBox "+sourceBox.scrollTop+"=scrollTop="+sourceBox.lastScrollTop+", "+ sourceBox.clientHeight+"=clientHeight="+sourceBox.lastClientHeight, sourceBox);
476                 // skip work if nothing changes.
477                 return;
478             }
479         }
480 
481         dispatch([Firebug.A11yModel], "onBeforeViewportChange", [this]);  // XXXjjb TODO where should this be?
482         this.buildViewAround(sourceBox, viewRange);
483 
484         if (Firebug.uiListeners.length > 0)
485         {
486             var link = new SourceLink(sourceBox.repObject.href, sourceBox.centralLine, this.getSourceType());
487             dispatch(Firebug.uiListeners, "onViewportChange", [link]);
488         }
489 
490         sourceBox.lastScrollTop = sourceBox.scrollTop;
491         sourceBox.lastClientHeight = sourceBox.clientHeight;
492     },
493 
494     buildViewAround: function(sourceBox, viewRange)
495     {
496         try
497         {
498             this.updateViewportCache(sourceBox, viewRange);
499         }
500         catch(exc)
501         {
502             if(FBTrace.DBG_ERRORS)
503                 FBTrace.sysout("buildViewAround updateViewportCache FAILS "+exc, exc);
504         }
505 
506         this.setViewportPadding(sourceBox, viewRange);
507 
508         sourceBox.centralLine = Math.floor( (viewRange.lastLine - viewRange.firstLine)/2 );
509 
510         this.applyDecorator(sourceBox);
511 
512         return;
513     },
514 
515     updateViewportCache: function(sourceBox, viewRange)
516     {
517         var cacheHit = this.insertedLinesOverlapCache(sourceBox, viewRange);
518 
519         if (!cacheHit)
520         {
521             this.clearSourceBox(sourceBox);  // no overlap, remove old range
522             sourceBox.firstRenderedLine = viewRange.firstLine; // reset cached range
523             sourceBox.lastRenderedLine = viewRange.lastLine;
524         }
525         else  // cache overlap, expand range of cache
526         {
527             sourceBox.firstRenderedLine = Math.min(viewRange.firstLine, sourceBox.firstRenderedLine);
528             sourceBox.lastRenderedLine = Math.max(viewRange.lastLine, sourceBox.lastRenderedLine);
529         }
530         sourceBox.firstViewableLine = viewRange.firstLine;  // todo actually check that these are viewable
531         sourceBox.lastViewableLine = viewRange.lastLine;
532         sourceBox.numberOfRenderedLines = sourceBox.lastRenderedLine - sourceBox.firstRenderedLine + 1;
533 
534         if (FBTrace.DBG_SOURCEFILES)
535             FBTrace.sysout("buildViewAround viewRange: "+viewRange.firstLine+"-"+viewRange.lastLine+" rendered: "+sourceBox.firstRenderedLine+"-"+sourceBox.lastRenderedLine, sourceBox);
536     },
537 
538     /*
539      * Add lines from viewRange, but do not adjust first/lastRenderedLine.
540      * @return true if viewRange overlaps first/lastRenderedLine
541      */
542     insertedLinesOverlapCache: function(sourceBox, viewRange)
543     {
544         var topCacheLine = null;
545         var cacheHit = false;
546         for (var line = viewRange.firstLine; line <= viewRange.lastLine; line++)
547         {
548             if (line >= sourceBox.firstRenderedLine && line <= sourceBox.lastRenderedLine )
549             {
550                 cacheHit = true;
551                 continue;
552             }
553 
554             var lineHTML = this.getSourceLineHTML(sourceBox, line);
555 
556             var ref = null;
557             if (line < sourceBox.firstRenderedLine)   // prepend if we are above the cache
558             {
559                 if (!topCacheLine)
560                     topCacheLine = sourceBox.getLineNode(sourceBox.firstRenderedLine);
561                 ref = topCacheLine;
562             }
563 
564             var newElement = appendInnerHTML(sourceBox.viewport, lineHTML, ref);
565         }
566         return cacheHit;
567     },
568 
569     clearSourceBox: function(sourceBox)
570     {
571         if (sourceBox.firstRenderedLine)
572         {
573             var topMostCachedElement = sourceBox.getLineNode(sourceBox.firstRenderedLine);  // eg 1
574             var totalCached = sourceBox.lastRenderedLine - sourceBox.firstRenderedLine + 1;   // eg 20 - 1 + 1 = 19
575             if (topMostCachedElement && totalCached)
576                 this.removeLines(sourceBox, topMostCachedElement, totalCached);
577         }
578         sourceBox.lastRenderedLine = 0;
579         sourceBox.firstRenderedLine = 0;
580         sourceBox.numberOfRenderedLines = 0;
581     },
582 
583     getSourceLineHTML: function(sourceBox, i)
584     {
585         var lineNo = sourceBox.decorator.getUserVisibleLineNumber(sourceBox, i);
586         var lineHTML = sourceBox.decorator.getLineHTML(sourceBox, i);
587         var lineId = sourceBox.decorator.getLineId(sourceBox, i);    // decorator lines may not have ids
588 
589         var lineNoText = this.getTextForLineNo(lineNo, sourceBox.maxLineNoChars);
590 
591         var theHTML =
592             '<div '
593                + (lineId ? ('id="' + lineId + '"') : "")
594                + ' class="sourceRow" role="presentation"><a class="'
595                +  'sourceLine' + '" role="presentation">'
596                + lineNoText
597                + '</a><span class="sourceRowText" role="presentation">'
598                + lineHTML
599                + '</span></div>';
600 
601         return theHTML;
602     },
603 
604     getTextForLineNo: function(lineNo, maxLineNoChars)
605     {
606         // Make sure all line numbers are the same width (with a fixed-width font)
607         var lineNoText = lineNo + "";
608         while (lineNoText.length < maxLineNoChars)
609             lineNoText = " " + lineNoText;
610 
611         return lineNoText;
612     },
613 
614     removeLines: function(sourceBox, firstRemoval, totalRemovals)
615     {
616         for(var i = 1; i <= totalRemovals; i++)
617         {
618             var nextSourceLine = firstRemoval;
619             firstRemoval = firstRemoval.nextSibling;
620             sourceBox.viewport.removeChild(nextSourceLine);
621         }
622     },
623 
624     getCentralLine: function(sourceBox)
625     {
626         return sourceBox.centralLine;
627     },
628 
629     getViewRangeFromTargetLine: function(sourceBox, targetLineNumber)
630     {
631         var viewRange = {firstLine: 1, centralLine: targetLineNumber, lastLine: 1};
632 
633         var averageLineHeight = this.getAverageLineHeight(sourceBox);
634         var panelHeight = this.panelNode.clientHeight;
635         var linesPerViewport = Math.round((panelHeight / averageLineHeight) + 1);
636 
637         viewRange.firstLine = Math.round(targetLineNumber - linesPerViewport / 2);
638 
639         if (viewRange.firstLine <= 0)
640             viewRange.firstLine = 1;
641 
642         viewRange.lastLine = viewRange.firstLine + linesPerViewport;
643 
644         if (viewRange.lastLine > sourceBox.maximumLineNumber)
645             viewRange.lastLine = sourceBox.maximumLineNumber;
646 
647         return viewRange;
648     },
649 
650     /*
651      * Use the average height of source lines in the cache to estimate where the scroll bar points based on scrollTop
652      */
653     getViewRangeFromScrollTop: function(sourceBox, scrollTop)
654     {
655         var viewRange = {};
656         var averageLineHeight = this.getAverageLineHeight(sourceBox);
657         viewRange.firstLine = Math.floor(scrollTop / averageLineHeight + 1);
658 
659         var panelHeight = this.panelNode.clientHeight;
660         var viewableLines = Math.ceil((panelHeight / averageLineHeight) + 1);
661         viewRange.lastLine = viewRange.firstLine + viewableLines;
662         if (viewRange.lastLine > sourceBox.maximumLineNumber)
663             viewRange.lastLine = sourceBox.maximumLineNumber;
664 
665         viewRange.centralLine = Math.floor((viewRange.lastLine - viewRange.firstLine)/2);
666 
667         if (FBTrace.DBG_SOURCEFILES)
668         {
669             FBTrace.sysout("getViewRangeFromScrollTop scrollTop:"+scrollTop+" viewRange: "+viewRange.firstLine+"-"+viewRange.lastLine);
670             if (!this.noRecurse)
671             {
672                 this.noRecurse = true;
673                 var testScrollTop = this.getScrollTopFromViewRange(sourceBox, viewRange);
674                 delete this.noRecurse;
675                 FBTrace.sysout("getViewRangeFromScrollTop "+((scrollTop==testScrollTop)?"checks":(scrollTop+"=!scrollTop!="+testScrollTop)));
676             }
677         }
678 
679         return viewRange;
680     },
681 
682     /*
683      * inverse of the getViewRangeFromScrollTop.
684      * If the viewRange was set by targetLineNumber, then this value become the new scroll top
685      *    else the value will be the same as the scrollbar's given value of scrollTop.
686      */
687     getScrollTopFromViewRange: function(sourceBox, viewRange)
688     {
689         var averageLineHeight = this.getAverageLineHeight(sourceBox);
690         var scrollTop = Math.floor(averageLineHeight * (viewRange.firstLine - 1));
691 
692         if (FBTrace.DBG_SOURCEFILES)
693         {
694             FBTrace.sysout("getScrollTopFromViewRange viewRange:"+viewRange.firstLine+"-"+viewRange.lastLine+" averageLineHeight: "+averageLineHeight+" scrollTop "+scrollTop);
695             if (!this.noRecurse)
696             {
697                 this.noRecurse = true;
698                 var testViewRange = this.getViewRangeFromScrollTop(sourceBox, scrollTop);
699                 delete this.noRecurse;
700                 var vrStr = viewRange.firstLine+"-"+viewRange.lastLine;
701                 var tvrStr = testViewRange.firstLine+"-"+testViewRange.lastLine;
702                 FBTrace.sysout("getScrollTopFromCenterLine "+((viewRange==testViewRange)? "checks" : vrStr+"=!viewRange!="+tvrStr));
703             }
704         }
705 
706         return scrollTop;
707     },
708 
709     /*
710      * The virtual sourceBox height is the averageLineHeight * max lines
711      * @return float
712      */
713     getAverageLineHeight: function(sourceBox)
714     {
715         var averageLineHeight = sourceBox.lineHeight;  // fall back to single line height
716 
717         var renderedViewportHeight = sourceBox.viewport.clientHeight;
718         var numberOfRenderedLines = sourceBox.numberOfRenderedLines;
719         if (renderedViewportHeight && numberOfRenderedLines)
720             averageLineHeight = renderedViewportHeight / numberOfRenderedLines;
721 
722         return averageLineHeight;
723     },
724 
725     /*
726      * The virtual sourceBox = topPadding + sourceBox.viewport + bottomPadding
727      * The viewport grows as more lines are added to the cache
728      * The virtual sourceBox height is estimated from the average height lines in the viewport cache
729      */
730     getTotalPadding: function(sourceBox)
731     {
732         var numberOfRenderedLines = sourceBox.numberOfRenderedLines;
733         if (!numberOfRenderedLines)
734             return 0;
735 
736         var max = sourceBox.maximumLineNumber;
737         var averageLineHeight = this.getAverageLineHeight(sourceBox);
738         // total box will be the average line height times total lines
739         var virtualSourceBoxHeight = Math.floor(max * averageLineHeight);
740         if (virtualSourceBoxHeight < sourceBox.clientHeight)
741         {
742             var scrollBarHeight = sourceBox.offsetHeight - sourceBox.clientHeight;
743             // the total - view-taken-up - scrollbar
744             var totalPadding = sourceBox.clientHeight - sourceBox.viewport.clientHeight - 1;
745         }
746         else
747             var totalPadding = virtualSourceBoxHeight - sourceBox.viewport.clientHeight;
748 
749         if (FBTrace.DBG_SOURCEFILES)
750             FBTrace.sysout("getTotalPadding clientHeight:"+sourceBox.viewport.clientHeight+"  max: "+max+" gives total padding "+totalPadding);
751 
752         return totalPadding;
753     },
754 
755     setViewportPadding: function(sourceBox, viewRange)
756     {
757         var firstRenderedLineElement = sourceBox.getLineNode(sourceBox.firstRenderedLine);
758         if (!firstRenderedLineElement)
759         {
760             if (FBTrace.DBG_ERRORS)
761                 FBTrace.sysout("setViewportPadding FAILS, no line at "+sourceBox.firstRenderedLine, sourceBox);
762             return;
763         }
764 
765         var firstRenderedLineOffset = firstRenderedLineElement.offsetTop;
766         var firstViewRangeElement = sourceBox.getLineNode(viewRange.firstLine);
767         var firstViewRangeOffset = firstViewRangeElement.offsetTop;
768         var topPadding = sourceBox.scrollTop - (firstViewRangeOffset - firstRenderedLineOffset);
769         // Because of rounding when converting from pixels to lines, topPadding can be +/- lineHeight/2, round up
770         var averageLineHeight = this.getAverageLineHeight(sourceBox);
771         var linesOfPadding = Math.floor( (topPadding + averageLineHeight)/ averageLineHeight);
772         var topPadding = (linesOfPadding - 1)* averageLineHeight;
773 
774         if (FBTrace.DBG_SOURCEFILES)
775             FBTrace.sysout("setViewportPadding sourceBox.scrollTop - (firstViewRangeOffset - firstRenderedLineOffset): "+sourceBox.scrollTop+"-"+"("+firstViewRangeOffset+"-"+firstRenderedLineOffset+")="+topPadding);
776         // we want the bottomPadding to take up the rest
777         var totalPadding = this.getTotalPadding(sourceBox);
778         if (totalPadding < 0)
779             var bottomPadding = Math.abs(totalPadding);
780         else
781             var bottomPadding = Math.floor(totalPadding - topPadding);
782 
783         if (bottomPadding < 0)
784             bottomPadding = 0;
785 
786         if(FBTrace.DBG_SOURCEFILES)
787         {
788             FBTrace.sysout("setViewportPadding viewport.offsetHeight: "+sourceBox.viewport.offsetHeight+" viewport.clientHeight "+sourceBox.viewport.clientHeight);
789             FBTrace.sysout("setViewportPadding sourceBox.offsetHeight: "+sourceBox.offsetHeight+" sourceBox.clientHeight "+sourceBox.clientHeight);
790             FBTrace.sysout("setViewportPadding scrollTop: "+sourceBox.scrollTop+" firstRenderedLine "+sourceBox.firstRenderedLine+" bottom: "+bottomPadding+" top: "+topPadding);
791         }
792         var view = sourceBox.viewport;
793 
794         // Set the size on the line number field so the padding is filled with same style as source lines.
795         view.previousSibling.style.height = topPadding + "px";
796         view.nextSibling.style.height = bottomPadding + "px";
797 
798         //sourceRow
799         view.previousSibling.firstChild.style.height = topPadding + "px";
800         view.nextSibling.firstChild.style.height = bottomPadding + "px";
801 
802         //sourceLine
803         view.previousSibling.firstChild.firstChild.style.height = topPadding + "px";
804         view.nextSibling.firstChild.firstChild.style.height = bottomPadding + "px";
805     },
806 
807     applyDecorator: function(sourceBox)
808     {
809         if (this.context.sourceBoxDecoratorTimeout)
810         {
811             this.context.clearTimeout(this.context.sourceBoxDecoratorTimeout);
812             delete this.context.sourceBoxDecoratorTimeout;
813         }
814 
815         this.context.sourceBoxDecoratorTimeout = this.context.setTimeout(bindFixed(function delaySourceBoxDecorator()
816         {
817             try
818             {
819                 if (sourceBox.highlighter)
820                 {
821                     var sticky = sourceBox.highlighter(sourceBox);
822                     if (FBTrace.DBG_SOURCEFILES)
823                         FBTrace.sysout("sourceBoxDecoratorTimeout highlighter sticky:"+sticky, sourceBox.highlighter);
824                     if (!sticky)
825                         delete sourceBox.highlighter;
826                 }
827                 sourceBox.decorator.decorate(sourceBox, sourceBox.repObject);
828 
829                 if (Firebug.uiListeners.length > 0) dispatch(Firebug.uiListeners, "onApplyDecorator", [sourceBox]);
830                 if (FBTrace.DBG_SOURCEFILES)
831                     FBTrace.sysout("sourceBoxDecoratorTimeout "+sourceBox.repObject, sourceBox);
832             }
833             catch (exc)
834             {
835                 if (FBTrace.DBG_ERRORS)
836                     FBTrace.sysout("sourcebox applyDecorator FAILS "+exc, exc);
837             }
838         }, this));
839     },
840 });
841 
842 
843 
844 
845     // ************************************************************************************************
846 }});
847