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