2011-02-01 Pavel Podivilov <podivilov@chromium.org>
[WebKit-https.git] / Source / WebCore / inspector / front-end / SourceFrame.js
1 /*
2  * Copyright (C) 2009 Google Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *     * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *     * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *     * Neither the name of Google Inc. nor the names of its
15  * contributors may be used to endorse or promote products derived from
16  * this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 WebInspector.SourceFrame = function(contentProvider, url, isScript)
32 {
33     WebInspector.View.call(this);
34
35     this.element.addStyleClass("script-view");
36
37     this._contentProvider = contentProvider;
38     this._url = url;
39     this._isScript = isScript;
40
41     this._textModel = new WebInspector.TextEditorModel();
42     this._textModel.replaceTabsWithSpaces = true;
43
44     this._currentSearchResultIndex = -1;
45     this._searchResults = [];
46
47     this._messages = [];
48     this._rowMessages = {};
49     this._messageBubbles = {};
50
51     this._popoverObjectGroup = "popover";
52 }
53
54 WebInspector.SourceFrame.prototype = {
55
56     show: function(parentElement)
57     {
58         WebInspector.View.prototype.show.call(this, parentElement);
59
60         if (!this._contentRequested) {
61             this._contentRequested = true;
62             this._contentProvider.requestContent(this._createTextViewer.bind(this));
63         }
64
65         if (this._textViewer) {
66             if (this._scrollTop)
67                 this._textViewer.element.scrollTop = this._scrollTop;
68             if (this._scrollLeft)
69                 this._textViewer.element.scrollLeft = this._scrollLeft;
70             this._textViewer.resize();
71         }
72     },
73
74     hide: function()
75     {
76         WebInspector.View.prototype.hide.call(this);
77
78         this._hidePopup();
79         this._clearLineHighlight();
80
81         if (this._textViewer) {
82             this._scrollTop = this._textViewer.element.scrollTop;
83             this._scrollLeft = this._textViewer.element.scrollLeft;
84             this._textViewer.freeCachedElements();
85         }
86     },
87
88     hasContent: function()
89     {
90         return true;
91     },
92
93     markDiff: function(diffData)
94     {
95         if (this._diffLines && this._textViewer)
96             this._removeDiffDecorations();
97
98         this._diffLines = diffData;
99         if (this._textViewer)
100             this._updateDiffDecorations();
101     },
102
103     revealLine: function(lineNumber)
104     {
105         if (this._textViewer)
106             this._textViewer.revealLine(lineNumber - 1, 0);
107         else
108             this._lineNumberToReveal = lineNumber;
109     },
110
111     addMessage: function(msg)
112     {
113         // Don't add the message if there is no message or valid line or if the msg isn't an error or warning.
114         if (!msg.message || msg.line <= 0 || !msg.isErrorOrWarning())
115             return;
116         this._messages.push(msg)
117         if (this._textViewer)
118             this._addMessageToSource(msg);
119     },
120
121     clearMessages: function()
122     {
123         for (var line in this._messageBubbles) {
124             var bubble = this._messageBubbles[line];
125             bubble.parentNode.removeChild(bubble);
126         }
127
128         this._messages = [];
129         this._rowMessages = {};
130         this._messageBubbles = {};
131         if (this._textViewer)
132             this._textViewer.resize();
133     },
134
135     sizeToFitContentHeight: function()
136     {
137         if (this._textViewer)
138             this._textViewer.revalidateDecorationsAndPaint();
139     },
140
141     get textModel()
142     {
143         return this._textModel;
144     },
145
146     get scrollTop()
147     {
148         return this._textViewer ? this._textViewer.element.scrollTop : 0;
149     },
150
151     set scrollTop(scrollTop)
152     {
153         if (this._textViewer)
154             this._textViewer.element.scrollTop = scrollTop;
155     },
156
157     highlightLine: function(line)
158     {
159         if (this._textViewer)
160             this._textViewer.highlightLine(line - 1);
161         else
162             this._lineToHighlight = line;
163     },
164
165     _clearLineHighlight: function()
166     {
167         if (this._textViewer)
168             this._textViewer.clearLineHighlight();
169         else
170             delete this._lineToHighlight;
171     },
172
173     _createTextViewer: function(mimeType, content)
174     {
175         this._content = content;
176         this._textModel.setText(null, content);
177         this._formatter = new WebInspector.ScriptFormatter(content);
178
179         this._textViewer = new WebInspector.TextViewer(this._textModel, WebInspector.platform, this._url);
180         var element = this._textViewer.element;
181         element.addEventListener("contextmenu", this._contextMenu.bind(this), true);
182         element.addEventListener("mousedown", this._mouseDown.bind(this), true);
183         element.addEventListener("mousemove", this._mouseMove.bind(this), true);
184         element.addEventListener("scroll", this._scroll.bind(this), true);
185         element.addEventListener("dblclick", this._doubleClick.bind(this), true);
186         this.element.appendChild(element);
187
188         this._textViewer.beginUpdates();
189
190         this._textViewer.mimeType = mimeType;
191         this._setTextViewerDecorations();
192
193         if (this._lineNumberToReveal) {
194             this.revealLine(this._lineNumberToReveal);
195             delete this._lineNumberToReveal;
196         }
197
198         if (this._lineToHighlight) {
199             this.highlightLine(this._lineToHighlight);
200             delete this._lineToHighlight;
201         }
202
203         if (this._delayedFindSearchMatches) {
204             this._delayedFindSearchMatches();
205             delete this._delayedFindSearchMatches;
206         }
207
208         this._textViewer.endUpdates();
209
210         WebInspector.debuggerModel.addEventListener(WebInspector.DebuggerModel.Events.BreakpointAdded, this._breakpointAdded, this);
211         WebInspector.debuggerModel.addEventListener(WebInspector.DebuggerModel.Events.BreakpointRemoved, this._breakpointRemoved, this);
212         WebInspector.debuggerModel.addEventListener(WebInspector.DebuggerModel.Events.BreakpointResolved, this._breakpointResolved, this);
213     },
214
215     _setTextViewerDecorations: function()
216     {
217         this._rowMessages = {};
218         this._messageBubbles = {};
219
220         this._textViewer.beginUpdates();
221
222         this._addExistingMessagesToSource();
223         this._updateDiffDecorations();
224
225         if (this._executionLine)
226             this.setExecutionLine(this._executionLine);
227
228         this._breakpointIdToTextViewerLineNumber = {};
229         this._textViewerLineNumberToBreakpointId = {};
230         var breakpoints = WebInspector.debuggerModel.breakpoints;
231         for (var id in breakpoints)
232             this._breakpointAdded({ data: breakpoints[id] });
233
234         this._textViewer.resize();
235
236         this._textViewer.endUpdates();
237     },
238
239     _shouldDisplayBreakpoint: function(breakpoint)
240     {
241         if (this._url)
242             return this._url === breakpoint.url;
243         var scripts = this._contentProvider.scripts();
244         for (var i = 0; i < scripts.length; ++i) {
245             if (breakpoint.sourceID === scripts[i].sourceID)
246                 return true;
247         }
248         return false;
249     },
250
251     performSearch: function(query, callback)
252     {
253         // Call searchCanceled since it will reset everything we need before doing a new search.
254         this.searchCanceled();
255
256         function doFindSearchMatches(query)
257         {
258             this._currentSearchResultIndex = -1;
259             this._searchResults = [];
260
261             // First do case-insensitive search.
262             var regexObject = createSearchRegex(query);
263             this._collectRegexMatches(regexObject, this._searchResults);
264
265             // Then try regex search if user knows the / / hint.
266             try {
267                 if (/^\/.*\/$/.test(query))
268                     this._collectRegexMatches(new RegExp(query.substring(1, query.length - 1)), this._searchResults);
269             } catch (e) {
270                 // Silent catch.
271             }
272
273             callback(this, this._searchResults.length);
274         }
275
276         if (this._textViewer)
277             doFindSearchMatches.call(this, query);
278         else
279             this._delayedFindSearchMatches = doFindSearchMatches.bind(this, query);
280
281     },
282
283     searchCanceled: function()
284     {
285         delete this._delayedFindSearchMatches;
286         if (!this._textViewer)
287             return;
288
289         this._currentSearchResultIndex = -1;
290         this._searchResults = [];
291         this._textViewer.markAndRevealRange(null);
292     },
293
294     jumpToFirstSearchResult: function()
295     {
296         this._jumpToSearchResult(0);
297     },
298
299     jumpToLastSearchResult: function()
300     {
301         this._jumpToSearchResult(this._searchResults.length - 1);
302     },
303
304     jumpToNextSearchResult: function()
305     {
306         this._jumpToSearchResult(this._currentSearchResultIndex + 1);
307     },
308
309     jumpToPreviousSearchResult: function()
310     {
311         this._jumpToSearchResult(this._currentSearchResultIndex - 1);
312     },
313
314     showingFirstSearchResult: function()
315     {
316         return this._searchResults.length &&  this._currentSearchResultIndex === 0;
317     },
318
319     showingLastSearchResult: function()
320     {
321         return this._searchResults.length && this._currentSearchResultIndex === (this._searchResults.length - 1);
322     },
323
324     _jumpToSearchResult: function(index)
325     {
326         if (!this._textViewer || !this._searchResults.length)
327             return;
328         this._currentSearchResultIndex = (index + this._searchResults.length) % this._searchResults.length;
329         this._textViewer.markAndRevealRange(this._searchResults[this._currentSearchResultIndex]);
330     },
331
332     _collectRegexMatches: function(regexObject, ranges)
333     {
334         for (var i = 0; i < this._textModel.linesCount; ++i) {
335             var line = this._textModel.line(i);
336             var offset = 0;
337             do {
338                 var match = regexObject.exec(line);
339                 if (match) {
340                     ranges.push(new WebInspector.TextRange(i, offset + match.index, i, offset + match.index + match[0].length));
341                     offset += match.index + 1;
342                     line = line.substring(match.index + 1);
343                 }
344             } while (match)
345         }
346         return ranges;
347     },
348
349     _incrementMessageRepeatCount: function(msg, repeatDelta)
350     {
351         if (!msg._resourceMessageLineElement)
352             return;
353
354         if (!msg._resourceMessageRepeatCountElement) {
355             var repeatedElement = document.createElement("span");
356             msg._resourceMessageLineElement.appendChild(repeatedElement);
357             msg._resourceMessageRepeatCountElement = repeatedElement;
358         }
359
360         msg.repeatCount += repeatDelta;
361         msg._resourceMessageRepeatCountElement.textContent = WebInspector.UIString(" (repeated %d times)", msg.repeatCount);
362     },
363
364     setExecutionLine: function(lineNumber)
365     {
366         this._executionLine = lineNumber;
367         if (!this._textViewer)
368             return;
369         var textViewerLineNumber = this._formatter.originalLineNumberToFormattedLineNumber(this._executionLine - 1);
370         this._textViewer.addDecoration(textViewerLineNumber, "webkit-execution-line");
371     },
372
373     clearExecutionLine: function()
374     {
375         if (!this._textViewer)
376             return;
377         var textViewerLineNumber = this._formatter.originalLineNumberToFormattedLineNumber(this._executionLine - 1);
378         this._textViewer.removeDecoration(textViewerLineNumber, "webkit-execution-line");
379         delete this._executionLine;
380     },
381
382     _updateDiffDecorations: function()
383     {
384         if (!this._diffLines)
385             return;
386
387         function addDecorations(textViewer, lines, className)
388         {
389             for (var i = 0; i < lines.length; ++i)
390                 textViewer.addDecoration(lines[i], className);
391         }
392         addDecorations(this._textViewer, this._diffLines.added, "webkit-added-line");
393         addDecorations(this._textViewer, this._diffLines.removed, "webkit-removed-line");
394         addDecorations(this._textViewer, this._diffLines.changed, "webkit-changed-line");
395     },
396
397     _removeDiffDecorations: function()
398     {
399         function removeDecorations(textViewer, lines, className)
400         {
401             for (var i = 0; i < lines.length; ++i)
402                 textViewer.removeDecoration(lines[i], className);
403         }
404         removeDecorations(this._textViewer, this._diffLines.added, "webkit-added-line");
405         removeDecorations(this._textViewer, this._diffLines.removed, "webkit-removed-line");
406         removeDecorations(this._textViewer, this._diffLines.changed, "webkit-changed-line");
407     },
408
409     _addExistingMessagesToSource: function()
410     {
411         var length = this._messages.length;
412         for (var i = 0; i < length; ++i)
413             this._addMessageToSource(this._messages[i]);
414     },
415
416     _addMessageToSource: function(msg)
417     {
418         if (msg.line >= this._textModel.linesCount)
419             return;
420
421         var messageBubbleElement = this._messageBubbles[msg.line];
422         if (!messageBubbleElement || messageBubbleElement.nodeType !== Node.ELEMENT_NODE || !messageBubbleElement.hasStyleClass("webkit-html-message-bubble")) {
423             messageBubbleElement = document.createElement("div");
424             messageBubbleElement.className = "webkit-html-message-bubble";
425             this._messageBubbles[msg.line] = messageBubbleElement;
426             this._textViewer.addDecoration(msg.line - 1, messageBubbleElement);
427         }
428
429         var rowMessages = this._rowMessages[msg.line];
430         if (!rowMessages) {
431             rowMessages = [];
432             this._rowMessages[msg.line] = rowMessages;
433         }
434
435         for (var i = 0; i < rowMessages.length; ++i) {
436             if (rowMessages[i].isEqual(msg)) {
437                 this._incrementMessageRepeatCount(rowMessages[i], msg.repeatDelta);
438                 return;
439             }
440         }
441
442         rowMessages.push(msg);
443
444         var imageURL;
445         switch (msg.level) {
446             case WebInspector.ConsoleMessage.MessageLevel.Error:
447                 messageBubbleElement.addStyleClass("webkit-html-error-message");
448                 imageURL = "Images/errorIcon.png";
449                 break;
450             case WebInspector.ConsoleMessage.MessageLevel.Warning:
451                 messageBubbleElement.addStyleClass("webkit-html-warning-message");
452                 imageURL = "Images/warningIcon.png";
453                 break;
454         }
455
456         var messageLineElement = document.createElement("div");
457         messageLineElement.className = "webkit-html-message-line";
458         messageBubbleElement.appendChild(messageLineElement);
459
460         // Create the image element in the Inspector's document so we can use relative image URLs.
461         var image = document.createElement("img");
462         image.src = imageURL;
463         image.className = "webkit-html-message-icon";
464         messageLineElement.appendChild(image);
465         messageLineElement.appendChild(document.createTextNode(msg.message));
466
467         msg._resourceMessageLineElement = messageLineElement;
468     },
469
470     _breakpointAdded: function(event)
471     {
472         var breakpoint = event.data;
473
474         if (!this._shouldDisplayBreakpoint(breakpoint))
475             return;
476
477         var resolved = breakpoint.locations.length;
478         var breakpointLineNumber = breakpoint.lineNumber;
479         if (resolved)
480             breakpointLineNumber = breakpoint.locations[0].lineNumber;
481
482         var textViewerLineNumber = this._formatter.originalLineNumberToFormattedLineNumber(breakpointLineNumber);
483         if (textViewerLineNumber >= this._textModel.linesCount)
484             return;
485
486         var existingBreakpointId = this._textViewerLineNumberToBreakpointId[textViewerLineNumber];
487         if (existingBreakpointId) {
488             WebInspector.debuggerModel.removeBreakpoint(breakpoint.id);
489             return;
490         }
491
492         this._breakpointIdToTextViewerLineNumber[breakpoint.id] = textViewerLineNumber;
493         this._textViewerLineNumberToBreakpointId[textViewerLineNumber] = breakpoint.id;
494         this._setBreakpointDecoration(textViewerLineNumber, resolved, breakpoint.enabled, !!breakpoint.condition);
495     },
496
497     _breakpointRemoved: function(event)
498     {
499         var breakpointId = event.data;
500
501         var textViewerLineNumber = this._breakpointIdToTextViewerLineNumber[breakpointId];
502         if (textViewerLineNumber === undefined)
503             return;
504
505         delete this._breakpointIdToTextViewerLineNumber[breakpointId];
506         delete this._textViewerLineNumberToBreakpointId[textViewerLineNumber];
507         this._removeBreakpointDecoration(textViewerLineNumber);
508     },
509
510     _breakpointResolved: function(event)
511     {
512         var breakpoint = event.data;
513         this._breakpointRemoved({ data: breakpoint.id });
514         this._breakpointAdded({ data: breakpoint });
515     },
516
517     _setBreakpointDecoration: function(lineNumber, resolved, enabled, hasCondition)
518     {
519         this._textViewer.beginUpdates();
520         this._textViewer.addDecoration(lineNumber, "webkit-breakpoint");
521         if (!enabled)
522             this._textViewer.addDecoration(lineNumber, "webkit-breakpoint-disabled");
523         if (hasCondition)
524             this._textViewer.addDecoration(lineNumber, "webkit-breakpoint-conditional");
525         this._textViewer.endUpdates();
526     },
527
528     _removeBreakpointDecoration: function(lineNumber)
529     {
530         this._textViewer.beginUpdates();
531         this._textViewer.removeDecoration(lineNumber, "webkit-breakpoint");
532         this._textViewer.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
533         this._textViewer.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
534         this._textViewer.endUpdates();
535     },
536
537     _contextMenu: function(event)
538     {
539         if (!WebInspector.panels.scripts)
540             return;
541
542         var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
543         if (!target)
544             return;
545         var textViewerLineNumber = target.lineNumber;
546         var originalLineNumber = this._formatter.formattedLineNumberToOriginalLineNumber(textViewerLineNumber);
547
548         var contextMenu = new WebInspector.ContextMenu();
549
550         contextMenu.appendItem(WebInspector.UIString("Continue to Here"), this._continueToLine.bind(this, originalLineNumber));
551
552         var breakpoint = this._findBreakpoint(textViewerLineNumber);
553         if (!breakpoint) {
554             // This row doesn't have a breakpoint: We want to show Add Breakpoint and Add and Edit Breakpoint.
555             contextMenu.appendItem(WebInspector.UIString("Add Breakpoint"), this._setBreakpoint.bind(this, originalLineNumber, "", true));
556
557             function addConditionalBreakpoint()
558             {
559                 this._setBreakpointDecoration(textViewerLineNumber, true, true, true);
560                 function didEditBreakpointCondition(committed, condition)
561                 {
562                     this._removeBreakpointDecoration(textViewerLineNumber);
563                     if (committed)
564                         this._setBreakpoint(originalLineNumber, true, condition);
565                 }
566                 this._editBreakpointCondition(textViewerLineNumber, "", didEditBreakpointCondition.bind(this));
567             }
568             contextMenu.appendItem(WebInspector.UIString("Add Conditional Breakpoint…"), addConditionalBreakpoint.bind(this));
569         } else {
570             // This row has a breakpoint, we want to show edit and remove breakpoint, and either disable or enable.
571             function removeBreakpoint()
572             {
573                 WebInspector.debuggerModel.removeBreakpoint(breakpoint.id);
574             }
575             contextMenu.appendItem(WebInspector.UIString("Remove Breakpoint"), removeBreakpoint);
576             function editBreakpointCondition()
577             {
578                 function didEditBreakpointCondition(committed, condition)
579                 {
580                     if (committed)
581                         WebInspector.debuggerModel.updateBreakpoint(breakpoint.id, condition, breakpoint.enabled);
582                 }
583                 this._editBreakpointCondition(textViewerLineNumber, breakpoint.condition, didEditBreakpointCondition.bind(this));
584             }
585             contextMenu.appendItem(WebInspector.UIString("Edit Breakpoint…"), editBreakpointCondition.bind(this));
586             function setBreakpointEnabled(enabled)
587             {
588                 WebInspector.debuggerModel.updateBreakpoint(breakpoint.id, breakpoint.condition, enabled);
589             }
590             if (breakpoint.enabled)
591                 contextMenu.appendItem(WebInspector.UIString("Disable Breakpoint"), setBreakpointEnabled.bind(this, false));
592             else
593                 contextMenu.appendItem(WebInspector.UIString("Enable Breakpoint"), setBreakpointEnabled.bind(this, true));
594         }
595         contextMenu.show(event);
596     },
597
598     _scroll: function(event)
599     {
600         this._hidePopup();
601     },
602
603     _mouseDown: function(event)
604     {
605         this._resetHoverTimer();
606         this._hidePopup();
607         if (event.button != 0 || event.altKey || event.ctrlKey || event.metaKey)
608             return;
609         var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
610         if (!target)
611             return;
612         var textViewerLineNumber = target.lineNumber;
613         var originalLineNumber = this._formatter.formattedLineNumberToOriginalLineNumber(textViewerLineNumber);
614
615         var breakpoint = this._findBreakpoint(textViewerLineNumber);
616         if (breakpoint) {
617             if (event.shiftKey)
618                 WebInspector.debuggerModel.updateBreakpoint(breakpoint.id, breakpoint.condition, !breakpoint.enabled);
619             else
620                 WebInspector.debuggerModel.removeBreakpoint(breakpoint.id);
621         } else
622             this._setBreakpoint(originalLineNumber, true, "");
623         event.preventDefault();
624     },
625
626     _mouseMove: function(event)
627     {
628         // Pretend that nothing has happened.
629         if (this._hoverElement === event.target || event.target.hasStyleClass("source-frame-eval-expression"))
630             return;
631
632         this._resetHoverTimer();
633         // User has 500ms to reach the popup.
634         if (this._popup) {
635             var self = this;
636             function doHide()
637             {
638                 self._hidePopup();
639                 delete self._hidePopupTimer;
640             }
641             if (!("_hidePopupTimer" in this))
642                 this._hidePopupTimer = setTimeout(doHide, 500);
643         }
644
645         this._hoverElement = event.target;
646
647         // Now that cleanup routines are set up above, leave this in case we are not on a break.
648         if (!WebInspector.panels.scripts || !WebInspector.panels.scripts.paused)
649             return;
650
651         // We are interested in identifiers and "this" keyword.
652         if (this._hoverElement.hasStyleClass("webkit-javascript-keyword")) {
653             if (this._hoverElement.textContent !== "this")
654                 return;
655         } else if (!this._hoverElement.hasStyleClass("webkit-javascript-ident"))
656             return;
657
658         const toolTipDelay = this._popup ? 600 : 1000;
659         this._hoverTimer = setTimeout(this._mouseHover.bind(this, this._hoverElement), toolTipDelay);
660     },
661
662     _resetHoverTimer: function()
663     {
664         if (this._hoverTimer) {
665             clearTimeout(this._hoverTimer);
666             delete this._hoverTimer;
667         }
668     },
669
670     _hidePopup: function()
671     {
672         if (!this._popup)
673             return;
674
675         // Replace higlight element with its contents inplace.
676         var parentElement = this._popup.highlightElement.parentElement;
677         var child = this._popup.highlightElement.firstChild;
678         while (child) {
679             var nextSibling = child.nextSibling;
680             parentElement.insertBefore(child, this._popup.highlightElement);
681             child = nextSibling;
682         }
683         parentElement.removeChild(this._popup.highlightElement);
684
685         this._popup.hide();
686         delete this._popup;
687         InspectorBackend.releaseWrapperObjectGroup(0, this._popoverObjectGroup);
688     },
689
690     _mouseHover: function(element)
691     {
692         delete this._hoverTimer;
693
694         if (!WebInspector.panels.scripts || !WebInspector.panels.scripts.paused)
695             return;
696
697         var lineRow = element.enclosingNodeOrSelfWithNodeName("tr");
698         if (!lineRow)
699             return;
700
701         // Collect tokens belonging to evaluated exression.
702         var tokens = [ element ];
703         var token = element.previousSibling;
704         while (token && (token.className === "webkit-javascript-ident" || token.className === "webkit-javascript-keyword" || token.textContent.trim() === ".")) {
705             tokens.push(token);
706             token = token.previousSibling;
707         }
708         tokens.reverse();
709
710         // Wrap them with highlight element.
711         var parentElement = element.parentElement;
712         var nextElement = element.nextSibling;
713         var container = document.createElement("span");
714         for (var i = 0; i < tokens.length; ++i)
715             container.appendChild(tokens[i]);
716         parentElement.insertBefore(container, nextElement);
717         this._showPopup(container);
718     },
719
720     _showPopup: function(element)
721     {
722         function killHidePopupTimer()
723         {
724             if (this._hidePopupTimer) {
725                 clearTimeout(this._hidePopupTimer);
726                 delete this._hidePopupTimer;
727
728                 // We know that we reached the popup, but we might have moved over other elements.
729                 // Discard pending command.
730                 this._resetHoverTimer();
731             }
732         }
733
734         function showObjectPopup(result)
735         {
736             if (!WebInspector.panels.scripts.paused)
737                 return;
738
739             var popupContentElement = null;
740             if (result.type !== "object" && result.type !== "node" && result.type !== "array") {
741                 popupContentElement = document.createElement("span");
742                 popupContentElement.className = "monospace console-formatted-" + result.type;
743                 popupContentElement.style.whiteSpace = "pre";
744                 popupContentElement.textContent = result.description;
745                 if (result.type === "string")
746                     popupContentElement.textContent = "\"" + popupContentElement.textContent + "\"";
747                 this._popup = new WebInspector.Popover(popupContentElement);
748                 this._popup.show(element);
749             } else {
750                 var popupContentElement = document.createElement("div");
751
752                 var titleElement = document.createElement("div");
753                 titleElement.className = "source-frame-popover-title monospace";
754                 titleElement.textContent = result.description;
755                 popupContentElement.appendChild(titleElement);
756
757                 var section = new WebInspector.ObjectPropertiesSection(result, "", null, false);
758                 section.expanded = true;
759                 section.element.addStyleClass("source-frame-popover-tree");
760                 section.headerElement.addStyleClass("hidden");
761                 popupContentElement.appendChild(section.element);
762
763                 this._popup = new WebInspector.Popover(popupContentElement);
764                 const popupWidth = 300;
765                 const popupHeight = 250;
766                 this._popup.show(element, popupWidth, popupHeight);
767             }
768             this._popup.highlightElement = element;
769             this._popup.highlightElement.addStyleClass("source-frame-eval-expression");
770             popupContentElement.addEventListener("mousemove", killHidePopupTimer.bind(this), true);
771         }
772
773         function evaluateCallback(result)
774         {
775             if (result.isError())
776                 return;
777             if (!WebInspector.panels.scripts.paused)
778                 return;
779             showObjectPopup.call(this, result);
780         }
781         WebInspector.panels.scripts.evaluateInSelectedCallFrame(element.textContent, false, this._popoverObjectGroup, false, evaluateCallback.bind(this));
782     },
783
784     _editBreakpointCondition: function(lineNumber, condition, callback)
785     {
786         this._conditionElement = this._createConditionElement(lineNumber);
787         this._textViewer.addDecoration(lineNumber, this._conditionElement);
788
789         function finishEditing(committed, element, newText)
790         {
791             this._textViewer.removeDecoration(lineNumber, this._conditionElement);
792             delete this._conditionEditorElement;
793             delete this._conditionElement;
794             callback(committed, newText);
795         }
796
797         WebInspector.startEditing(this._conditionEditorElement, {
798             context: null,
799             commitHandler: finishEditing.bind(this, true),
800             cancelHandler: finishEditing.bind(this, false)
801         });
802         this._conditionEditorElement.value = condition;
803         this._conditionEditorElement.select();
804     },
805
806     _createConditionElement: function(lineNumber)
807     {
808         var conditionElement = document.createElement("div");
809         conditionElement.className = "source-frame-breakpoint-condition";
810
811         var labelElement = document.createElement("label");
812         labelElement.className = "source-frame-breakpoint-message";
813         labelElement.htmlFor = "source-frame-breakpoint-condition";
814         labelElement.appendChild(document.createTextNode(WebInspector.UIString("The breakpoint on line %d will stop only if this expression is true:", lineNumber)));
815         conditionElement.appendChild(labelElement);
816
817         var editorElement = document.createElement("input");
818         editorElement.id = "source-frame-breakpoint-condition";
819         editorElement.className = "monospace";
820         editorElement.type = "text"
821         conditionElement.appendChild(editorElement);
822         this._conditionEditorElement = editorElement;
823
824         return conditionElement;
825     },
826
827     _evalSelectionInCallFrame: function(event)
828     {
829         if (!WebInspector.panels.scripts || !WebInspector.panels.scripts.paused)
830             return;
831
832         var selection = this.element.contentWindow.getSelection();
833         if (!selection.rangeCount)
834             return;
835
836         var expression = selection.getRangeAt(0).toString().trim();
837         WebInspector.panels.scripts.evaluateInSelectedCallFrame(expression, false, "console", function(result) {
838             WebInspector.showConsole();
839             var commandMessage = new WebInspector.ConsoleCommand(expression);
840             WebInspector.console.addMessage(commandMessage);
841             WebInspector.console.addMessage(new WebInspector.ConsoleCommandResult(result, commandMessage));
842         });
843     },
844
845     resize: function()
846     {
847         if (this._textViewer)
848             this._textViewer.resize();
849     },
850
851     formatSource: function()
852     {
853         if (!this._formatter)
854             return;
855
856         function didFormat(source)
857         {
858             this._textModel.setText(null, source);
859             this._setTextViewerDecorations();
860         }
861         this._formatter.format(didFormat.bind(this));
862     },
863
864     _continueToLine: function(lineNumber)
865     {
866         var sourceID = this._sourceIDForLine(lineNumber);
867         if (!sourceID)
868             return;
869         WebInspector.debuggerModel.continueToLine(sourceID, lineNumber);
870     },
871
872     _doubleClick: function(event)
873     {
874         if (!Preferences.canEditScriptSource || !this._isScript)
875             return;
876
877         var lineRow = event.target.enclosingNodeOrSelfWithClass("webkit-line-content");
878         if (!lineRow)
879             return;  // Do not trigger editing from line numbers.
880
881         var lineNumber = lineRow.lineNumber;
882         var sourceID = this._sourceIDForLine(lineNumber);
883         if (!sourceID)
884             return;
885
886         function didEditLine(newContent)
887         {
888             var lines = [];
889             var oldLines = this._content.split('\n');
890             for (var i = 0; i < oldLines.length; ++i) {
891                 if (i === lineNumber)
892                     lines.push(newContent);
893                 else
894                     lines.push(oldLines[i]);
895             }
896             WebInspector.debuggerModel.editScriptSource(sourceID, lines.join("\n"));
897         }
898         this._textViewer.editLine(lineRow, didEditLine.bind(this));
899     },
900
901     _setBreakpoint: function(lineNumber, enabled, condition)
902     {
903         if (this._url)
904             WebInspector.debuggerModel.setBreakpoint(this._url, lineNumber, 0, condition, enabled);
905         else {
906             var sourceID = this._sourceIDForLine(lineNumber);
907             if (!sourceID)
908                 return;
909             WebInspector.debuggerModel.setBreakpointBySourceId(sourceID, lineNumber, 0, condition, enabled);
910         }
911         if (!WebInspector.panels.scripts.breakpointsActivated)
912             WebInspector.panels.scripts.toggleBreakpointsClicked();
913     },
914
915     _findBreakpoint: function(textViewerLineNumber)
916     {
917         var breakpointId = this._textViewerLineNumberToBreakpointId[textViewerLineNumber];
918         return WebInspector.debuggerModel.breakpointForId(breakpointId);
919     },
920
921     _sourceIDForLine: function(lineNumber)
922     {
923         var sourceIDForLine = null;
924         var closestStartingLine = 0;
925         var scripts = this._contentProvider.scripts();
926         for (var i = 0; i < scripts.length; ++i) {
927             var lineOffset = scripts[i].lineOffset;
928             if (lineOffset <= lineNumber && lineOffset >= closestStartingLine) {
929                 closestStartingLine = lineOffset;
930                 sourceIDForLine = scripts[i].sourceID;
931             }
932         }
933         return sourceIDForLine;
934     }
935 }
936
937 WebInspector.SourceFrame.prototype.__proto__ = WebInspector.View.prototype;
938
939
940 WebInspector.SourceFrameContentProvider = function()
941 {
942 }
943
944 WebInspector.SourceFrameContentProvider.prototype = {
945     requestContent: function(callback)
946     {
947         // Should be implemented by subclasses.
948     },
949
950     scripts: function()
951     {
952         // Should be implemented by subclasses.
953     }
954 }