248de5ab3c2ef64b81c8d0672aba4032ac02e800
[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     },
212
213     _setTextViewerDecorations: function()
214     {
215         this._rowMessages = {};
216         this._messageBubbles = {};
217
218         this._textViewer.beginUpdates();
219
220         this._addExistingMessagesToSource();
221         this._updateDiffDecorations();
222
223         if (this._executionLine)
224             this.setExecutionLine(this._executionLine);
225
226         var breakpoints = this._breakpoints();
227         for (var i = 0; i < breakpoints.length; ++i)
228             this._addBreakpoint(breakpoints[i]);
229
230         this._textViewer.resize();
231
232         this._textViewer.endUpdates();
233     },
234
235     performSearch: function(query, callback)
236     {
237         // Call searchCanceled since it will reset everything we need before doing a new search.
238         this.searchCanceled();
239
240         function doFindSearchMatches(query)
241         {
242             this._currentSearchResultIndex = -1;
243             this._searchResults = [];
244
245             // First do case-insensitive search.
246             var regexObject = createSearchRegex(query);
247             this._collectRegexMatches(regexObject, this._searchResults);
248
249             // Then try regex search if user knows the / / hint.
250             try {
251                 if (/^\/.*\/$/.test(query))
252                     this._collectRegexMatches(new RegExp(query.substring(1, query.length - 1)), this._searchResults);
253             } catch (e) {
254                 // Silent catch.
255             }
256
257             callback(this, this._searchResults.length);
258         }
259
260         if (this._textViewer)
261             doFindSearchMatches.call(this, query);
262         else
263             this._delayedFindSearchMatches = doFindSearchMatches.bind(this, query);
264
265     },
266
267     searchCanceled: function()
268     {
269         delete this._delayedFindSearchMatches;
270         if (!this._textViewer)
271             return;
272
273         this._currentSearchResultIndex = -1;
274         this._searchResults = [];
275         this._textViewer.markAndRevealRange(null);
276     },
277
278     jumpToFirstSearchResult: function()
279     {
280         this._jumpToSearchResult(0);
281     },
282
283     jumpToLastSearchResult: function()
284     {
285         this._jumpToSearchResult(this._searchResults.length - 1);
286     },
287
288     jumpToNextSearchResult: function()
289     {
290         this._jumpToSearchResult(this._currentSearchResultIndex + 1);
291     },
292
293     jumpToPreviousSearchResult: function()
294     {
295         this._jumpToSearchResult(this._currentSearchResultIndex - 1);
296     },
297
298     showingFirstSearchResult: function()
299     {
300         return this._searchResults.length &&  this._currentSearchResultIndex === 0;
301     },
302
303     showingLastSearchResult: function()
304     {
305         return this._searchResults.length && this._currentSearchResultIndex === (this._searchResults.length - 1);
306     },
307
308     _jumpToSearchResult: function(index)
309     {
310         if (!this._textViewer || !this._searchResults.length)
311             return;
312         this._currentSearchResultIndex = (index + this._searchResults.length) % this._searchResults.length;
313         this._textViewer.markAndRevealRange(this._searchResults[this._currentSearchResultIndex]);
314     },
315
316     _collectRegexMatches: function(regexObject, ranges)
317     {
318         for (var i = 0; i < this._textModel.linesCount; ++i) {
319             var line = this._textModel.line(i);
320             var offset = 0;
321             do {
322                 var match = regexObject.exec(line);
323                 if (match) {
324                     ranges.push(new WebInspector.TextRange(i, offset + match.index, i, offset + match.index + match[0].length));
325                     offset += match.index + 1;
326                     line = line.substring(match.index + 1);
327                 }
328             } while (match)
329         }
330         return ranges;
331     },
332
333     _incrementMessageRepeatCount: function(msg, repeatDelta)
334     {
335         if (!msg._resourceMessageLineElement)
336             return;
337
338         if (!msg._resourceMessageRepeatCountElement) {
339             var repeatedElement = document.createElement("span");
340             msg._resourceMessageLineElement.appendChild(repeatedElement);
341             msg._resourceMessageRepeatCountElement = repeatedElement;
342         }
343
344         msg.repeatCount += repeatDelta;
345         msg._resourceMessageRepeatCountElement.textContent = WebInspector.UIString(" (repeated %d times)", msg.repeatCount);
346     },
347
348     setExecutionLine: function(lineNumber)
349     {
350         this._executionLine = lineNumber;
351         if (!this._textViewer)
352             return;
353         var textViewerLineNumber = this._formatter.originalLineNumberToFormattedLineNumber(this._executionLine - 1);
354         this._textViewer.addDecoration(textViewerLineNumber, "webkit-execution-line");
355     },
356
357     clearExecutionLine: function()
358     {
359         if (!this._textViewer)
360             return;
361         var textViewerLineNumber = this._formatter.originalLineNumberToFormattedLineNumber(this._executionLine - 1);
362         this._textViewer.removeDecoration(textViewerLineNumber, "webkit-execution-line");
363         delete this._executionLine;
364     },
365
366     _updateDiffDecorations: function()
367     {
368         if (!this._diffLines)
369             return;
370
371         function addDecorations(textViewer, lines, className)
372         {
373             for (var i = 0; i < lines.length; ++i)
374                 textViewer.addDecoration(lines[i], className);
375         }
376         addDecorations(this._textViewer, this._diffLines.added, "webkit-added-line");
377         addDecorations(this._textViewer, this._diffLines.removed, "webkit-removed-line");
378         addDecorations(this._textViewer, this._diffLines.changed, "webkit-changed-line");
379     },
380
381     _removeDiffDecorations: function()
382     {
383         function removeDecorations(textViewer, lines, className)
384         {
385             for (var i = 0; i < lines.length; ++i)
386                 textViewer.removeDecoration(lines[i], className);
387         }
388         removeDecorations(this._textViewer, this._diffLines.added, "webkit-added-line");
389         removeDecorations(this._textViewer, this._diffLines.removed, "webkit-removed-line");
390         removeDecorations(this._textViewer, this._diffLines.changed, "webkit-changed-line");
391     },
392
393     _addExistingMessagesToSource: function()
394     {
395         var length = this._messages.length;
396         for (var i = 0; i < length; ++i)
397             this._addMessageToSource(this._messages[i]);
398     },
399
400     _addMessageToSource: function(msg)
401     {
402         if (msg.line >= this._textModel.linesCount)
403             return;
404
405         var messageBubbleElement = this._messageBubbles[msg.line];
406         if (!messageBubbleElement || messageBubbleElement.nodeType !== Node.ELEMENT_NODE || !messageBubbleElement.hasStyleClass("webkit-html-message-bubble")) {
407             messageBubbleElement = document.createElement("div");
408             messageBubbleElement.className = "webkit-html-message-bubble";
409             this._messageBubbles[msg.line] = messageBubbleElement;
410             this._textViewer.addDecoration(msg.line - 1, messageBubbleElement);
411         }
412
413         var rowMessages = this._rowMessages[msg.line];
414         if (!rowMessages) {
415             rowMessages = [];
416             this._rowMessages[msg.line] = rowMessages;
417         }
418
419         for (var i = 0; i < rowMessages.length; ++i) {
420             if (rowMessages[i].isEqual(msg)) {
421                 this._incrementMessageRepeatCount(rowMessages[i], msg.repeatDelta);
422                 return;
423             }
424         }
425
426         rowMessages.push(msg);
427
428         var imageURL;
429         switch (msg.level) {
430             case WebInspector.ConsoleMessage.MessageLevel.Error:
431                 messageBubbleElement.addStyleClass("webkit-html-error-message");
432                 imageURL = "Images/errorIcon.png";
433                 break;
434             case WebInspector.ConsoleMessage.MessageLevel.Warning:
435                 messageBubbleElement.addStyleClass("webkit-html-warning-message");
436                 imageURL = "Images/warningIcon.png";
437                 break;
438         }
439
440         var messageLineElement = document.createElement("div");
441         messageLineElement.className = "webkit-html-message-line";
442         messageBubbleElement.appendChild(messageLineElement);
443
444         // Create the image element in the Inspector's document so we can use relative image URLs.
445         var image = document.createElement("img");
446         image.src = imageURL;
447         image.className = "webkit-html-message-icon";
448         messageLineElement.appendChild(image);
449         messageLineElement.appendChild(document.createTextNode(msg.message));
450
451         msg._resourceMessageLineElement = messageLineElement;
452     },
453
454     _breakpointAdded: function(event)
455     {
456         var breakpoint = event.data;
457
458         if (breakpoint.sourceID in this._sourceIDSet())
459             this._addBreakpoint(breakpoint);
460     },
461
462     _addBreakpoint: function(breakpoint)
463     {
464         var textViewerLineNumber = this._formatter.originalLineNumberToFormattedLineNumber(breakpoint.line - 1);
465         if (textViewerLineNumber >= this._textModel.linesCount)
466             return;
467
468         breakpoint.addEventListener("enable-changed", this._breakpointChanged, this);
469         breakpoint.addEventListener("condition-changed", this._breakpointChanged, this);
470         breakpoint.addEventListener("removed", this._breakpointRemoved, this);
471
472         this._setBreakpointDecoration(textViewerLineNumber, breakpoint.enabled, !!breakpoint.condition);
473     },
474
475     _breakpointRemoved: function(event)
476     {
477         var breakpoint = event.target;
478
479         breakpoint.removeEventListener("enable-changed", null, this);
480         breakpoint.removeEventListener("condition-changed", null, this);
481         breakpoint.removeEventListener("removed", null, this);
482
483         var textViewerLineNumber = this._formatter.originalLineNumberToFormattedLineNumber(breakpoint.line - 1);
484         this._removeBreakpointDecoration(textViewerLineNumber);
485     },
486
487     _breakpointChanged: function(event)
488     {
489         var breakpoint = event.target;
490         var textViewerLineNumber = this._formatter.originalLineNumberToFormattedLineNumber(breakpoint.line - 1);
491         this._setBreakpointDecoration(textViewerLineNumber, breakpoint.enabled, !!breakpoint.condition);
492     },
493
494     _setBreakpointDecoration: function(lineNumber, enabled, hasCondition)
495     {
496         this._textViewer.beginUpdates();
497         this._textViewer.addDecoration(lineNumber, "webkit-breakpoint");
498         if (enabled)
499             this._textViewer.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
500         else
501             this._textViewer.addDecoration(lineNumber, "webkit-breakpoint-disabled");
502         if (hasCondition)
503             this._textViewer.addDecoration(lineNumber, "webkit-breakpoint-conditional");
504         else
505             this._textViewer.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
506         this._textViewer.endUpdates();
507     },
508
509     _removeBreakpointDecoration: function(lineNumber)
510     {
511         this._textViewer.beginUpdates();
512         this._textViewer.removeDecoration(lineNumber, "webkit-breakpoint");
513         this._textViewer.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
514         this._textViewer.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
515         this._textViewer.endUpdates();
516     },
517
518     _contextMenu: function(event)
519     {
520         if (!WebInspector.panels.scripts)
521             return;
522
523         var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
524         if (!target)
525             return;
526         var textViewerLineNumber = target.lineNumber;
527         var originalLineNumber = this._formatter.formattedLineNumberToOriginalLineNumber(textViewerLineNumber);
528
529         var contextMenu = new WebInspector.ContextMenu();
530
531         contextMenu.appendItem(WebInspector.UIString("Continue to Here"), this._continueToLine.bind(this, originalLineNumber));
532
533         var breakpoint = this._findBreakpoint(originalLineNumber);
534         if (!breakpoint) {
535             // This row doesn't have a breakpoint: We want to show Add Breakpoint and Add and Edit Breakpoint.
536             contextMenu.appendItem(WebInspector.UIString("Add Breakpoint"), this._setBreakpoint.bind(this, originalLineNumber, "", true));
537
538             function addConditionalBreakpoint()
539             {
540                 this._setBreakpointDecoration(textViewerLineNumber, true, true);
541                 function didEditBreakpointCondition(committed, condition)
542                 {
543                     this._removeBreakpointDecoration(textViewerLineNumber);
544                     if (committed)
545                         this._setBreakpoint(originalLineNumber, true, condition);
546                 }
547                 this._editBreakpointCondition(textViewerLineNumber, "", didEditBreakpointCondition.bind(this));
548             }
549             contextMenu.appendItem(WebInspector.UIString("Add Conditional Breakpoint…"), addConditionalBreakpoint.bind(this));
550         } else {
551             // This row has a breakpoint, we want to show edit and remove breakpoint, and either disable or enable.
552             contextMenu.appendItem(WebInspector.UIString("Remove Breakpoint"), breakpoint.remove.bind(breakpoint));
553             function editBreakpointCondition()
554             {
555                 function didEditBreakpointCondition(committed, condition)
556                 {
557                     if (committed) {
558                         breakpoint.remove();
559                         this._setBreakpoint(originalLineNumber, breakpoint.enabled, condition);
560                     }
561                 }
562                 this._editBreakpointCondition(textViewerLineNumber, breakpoint.condition, didEditBreakpointCondition.bind(this));
563             }
564             contextMenu.appendItem(WebInspector.UIString("Edit Breakpoint…"), editBreakpointCondition.bind(this));
565             function setBreakpointEnabled(enabled)
566             {
567                 breakpoint.remove();
568                 this._setBreakpoint(originalLineNumber, enabled, breakpoint.condition);
569             }
570             if (breakpoint.enabled)
571                 contextMenu.appendItem(WebInspector.UIString("Disable Breakpoint"), setBreakpointEnabled.bind(this, false));
572             else
573                 contextMenu.appendItem(WebInspector.UIString("Enable Breakpoint"), setBreakpointEnabled.bind(this, true));
574         }
575         contextMenu.show(event);
576     },
577
578     _scroll: function(event)
579     {
580         this._hidePopup();
581     },
582
583     _mouseDown: function(event)
584     {
585         this._resetHoverTimer();
586         this._hidePopup();
587         if (event.button != 0 || event.altKey || event.ctrlKey || event.metaKey)
588             return;
589         var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
590         if (!target)
591             return;
592         var originalLineNumber = this._formatter.formattedLineNumberToOriginalLineNumber(target.lineNumber);
593
594         var breakpoint = this._findBreakpoint(originalLineNumber);
595         if (breakpoint) {
596             breakpoint.remove();
597             if (event.shiftKey)
598                 this._setBreakpoint(originalLineNumber, !breakpoint.enabled, breakpoint.condition);
599         } else
600             this._setBreakpoint(originalLineNumber, true, "");
601         event.preventDefault();
602     },
603
604     _mouseMove: function(event)
605     {
606         // Pretend that nothing has happened.
607         if (this._hoverElement === event.target || event.target.hasStyleClass("source-frame-eval-expression"))
608             return;
609
610         this._resetHoverTimer();
611         // User has 500ms to reach the popup.
612         if (this._popup) {
613             var self = this;
614             function doHide()
615             {
616                 self._hidePopup();
617                 delete self._hidePopupTimer;
618             }
619             if (!("_hidePopupTimer" in this))
620                 this._hidePopupTimer = setTimeout(doHide, 500);
621         }
622
623         this._hoverElement = event.target;
624
625         // Now that cleanup routines are set up above, leave this in case we are not on a break.
626         if (!WebInspector.panels.scripts || !WebInspector.panels.scripts.paused)
627             return;
628
629         // We are interested in identifiers and "this" keyword.
630         if (this._hoverElement.hasStyleClass("webkit-javascript-keyword")) {
631             if (this._hoverElement.textContent !== "this")
632                 return;
633         } else if (!this._hoverElement.hasStyleClass("webkit-javascript-ident"))
634             return;
635
636         const toolTipDelay = this._popup ? 600 : 1000;
637         this._hoverTimer = setTimeout(this._mouseHover.bind(this, this._hoverElement), toolTipDelay);
638     },
639
640     _resetHoverTimer: function()
641     {
642         if (this._hoverTimer) {
643             clearTimeout(this._hoverTimer);
644             delete this._hoverTimer;
645         }
646     },
647
648     _hidePopup: function()
649     {
650         if (!this._popup)
651             return;
652
653         // Replace higlight element with its contents inplace.
654         var parentElement = this._popup.highlightElement.parentElement;
655         var child = this._popup.highlightElement.firstChild;
656         while (child) {
657             var nextSibling = child.nextSibling;
658             parentElement.insertBefore(child, this._popup.highlightElement);
659             child = nextSibling;
660         }
661         parentElement.removeChild(this._popup.highlightElement);
662
663         this._popup.hide();
664         delete this._popup;
665         InspectorBackend.releaseWrapperObjectGroup(0, this._popoverObjectGroup);
666     },
667
668     _mouseHover: function(element)
669     {
670         delete this._hoverTimer;
671
672         if (!WebInspector.panels.scripts || !WebInspector.panels.scripts.paused)
673             return;
674
675         var lineRow = element.enclosingNodeOrSelfWithNodeName("tr");
676         if (!lineRow)
677             return;
678
679         // Collect tokens belonging to evaluated exression.
680         var tokens = [ element ];
681         var token = element.previousSibling;
682         while (token && (token.className === "webkit-javascript-ident" || token.className === "webkit-javascript-keyword" || token.textContent.trim() === ".")) {
683             tokens.push(token);
684             token = token.previousSibling;
685         }
686         tokens.reverse();
687
688         // Wrap them with highlight element.
689         var parentElement = element.parentElement;
690         var nextElement = element.nextSibling;
691         var container = document.createElement("span");
692         for (var i = 0; i < tokens.length; ++i)
693             container.appendChild(tokens[i]);
694         parentElement.insertBefore(container, nextElement);
695         this._showPopup(container);
696     },
697
698     _showPopup: function(element)
699     {
700         function killHidePopupTimer()
701         {
702             if (this._hidePopupTimer) {
703                 clearTimeout(this._hidePopupTimer);
704                 delete this._hidePopupTimer;
705
706                 // We know that we reached the popup, but we might have moved over other elements.
707                 // Discard pending command.
708                 this._resetHoverTimer();
709             }
710         }
711
712         function showObjectPopup(result)
713         {
714             if (!WebInspector.panels.scripts.paused)
715                 return;
716
717             var popupContentElement = null;
718             if (result.type !== "object" && result.type !== "node" && result.type !== "array") {
719                 popupContentElement = document.createElement("span");
720                 popupContentElement.className = "monospace console-formatted-" + result.type;
721                 popupContentElement.style.whiteSpace = "pre";
722                 popupContentElement.textContent = result.description;
723                 if (result.type === "string")
724                     popupContentElement.textContent = "\"" + popupContentElement.textContent + "\"";
725                 this._popup = new WebInspector.Popover(popupContentElement);
726                 this._popup.show(element);
727             } else {
728                 var popupContentElement = document.createElement("div");
729
730                 var titleElement = document.createElement("div");
731                 titleElement.className = "source-frame-popover-title monospace";
732                 titleElement.textContent = result.description;
733                 popupContentElement.appendChild(titleElement);
734
735                 var section = new WebInspector.ObjectPropertiesSection(result, "", null, false);
736                 section.expanded = true;
737                 section.element.addStyleClass("source-frame-popover-tree");
738                 section.headerElement.addStyleClass("hidden");
739                 popupContentElement.appendChild(section.element);
740
741                 this._popup = new WebInspector.Popover(popupContentElement);
742                 const popupWidth = 300;
743                 const popupHeight = 250;
744                 this._popup.show(element, popupWidth, popupHeight);
745             }
746             this._popup.highlightElement = element;
747             this._popup.highlightElement.addStyleClass("source-frame-eval-expression");
748             popupContentElement.addEventListener("mousemove", killHidePopupTimer.bind(this), true);
749         }
750
751         function evaluateCallback(result)
752         {
753             if (result.isError())
754                 return;
755             if (!WebInspector.panels.scripts.paused)
756                 return;
757             showObjectPopup.call(this, result);
758         }
759         WebInspector.panels.scripts.evaluateInSelectedCallFrame(element.textContent, false, this._popoverObjectGroup, false, evaluateCallback.bind(this));
760     },
761
762     _editBreakpointCondition: function(lineNumber, condition, callback)
763     {
764         this._conditionElement = this._createConditionElement(lineNumber);
765         this._textViewer.addDecoration(lineNumber, this._conditionElement);
766
767         function finishEditing(committed, element, newText)
768         {
769             this._textViewer.removeDecoration(lineNumber, this._conditionElement);
770             delete this._conditionEditorElement;
771             delete this._conditionElement;
772             callback(committed, newText);
773         }
774
775         WebInspector.startEditing(this._conditionEditorElement, {
776             context: null,
777             commitHandler: finishEditing.bind(this, true),
778             cancelHandler: finishEditing.bind(this, false)
779         });
780         this._conditionEditorElement.value = condition;
781         this._conditionEditorElement.select();
782     },
783
784     _createConditionElement: function(lineNumber)
785     {
786         var conditionElement = document.createElement("div");
787         conditionElement.className = "source-frame-breakpoint-condition";
788
789         var labelElement = document.createElement("label");
790         labelElement.className = "source-frame-breakpoint-message";
791         labelElement.htmlFor = "source-frame-breakpoint-condition";
792         labelElement.appendChild(document.createTextNode(WebInspector.UIString("The breakpoint on line %d will stop only if this expression is true:", lineNumber)));
793         conditionElement.appendChild(labelElement);
794
795         var editorElement = document.createElement("input");
796         editorElement.id = "source-frame-breakpoint-condition";
797         editorElement.className = "monospace";
798         editorElement.type = "text"
799         conditionElement.appendChild(editorElement);
800         this._conditionEditorElement = editorElement;
801
802         return conditionElement;
803     },
804
805     _evalSelectionInCallFrame: function(event)
806     {
807         if (!WebInspector.panels.scripts || !WebInspector.panels.scripts.paused)
808             return;
809
810         var selection = this.element.contentWindow.getSelection();
811         if (!selection.rangeCount)
812             return;
813
814         var expression = selection.getRangeAt(0).toString().trim();
815         WebInspector.panels.scripts.evaluateInSelectedCallFrame(expression, false, "console", function(result) {
816             WebInspector.showConsole();
817             var commandMessage = new WebInspector.ConsoleCommand(expression);
818             WebInspector.console.addMessage(commandMessage);
819             WebInspector.console.addMessage(new WebInspector.ConsoleCommandResult(result, commandMessage));
820         });
821     },
822
823     resize: function()
824     {
825         if (this._textViewer)
826             this._textViewer.resize();
827     },
828
829     formatSource: function()
830     {
831         if (!this._formatter)
832             return;
833
834         function didFormat(source)
835         {
836             this._textModel.setText(null, source);
837             this._setTextViewerDecorations();
838         }
839         this._formatter.format(didFormat.bind(this));
840     },
841
842     _continueToLine: function(lineNumber)
843     {
844         var sourceID = this._sourceIDForLine(lineNumber);
845         if (!sourceID)
846             return;
847         WebInspector.debuggerModel.continueToLine(sourceID, lineNumber + 1);
848     },
849
850     _doubleClick: function(event)
851     {
852         if (!Preferences.canEditScriptSource || !this._isScript)
853             return;
854
855         var lineRow = event.target.enclosingNodeOrSelfWithClass("webkit-line-content");
856         if (!lineRow)
857             return;  // Do not trigger editing from line numbers.
858
859         var lineNumber = lineRow.lineNumber;
860         var sourceID = this._sourceIDForLine(lineNumber);
861         if (!sourceID)
862             return;
863
864         function didEditLine(newContent)
865         {
866             var lines = [];
867             var oldLines = this._content.split('\n');
868             for (var i = 0; i < oldLines.length; ++i) {
869                 if (i === lineNumber)
870                     lines.push(newContent);
871                 else
872                     lines.push(oldLines[i]);
873             }
874             WebInspector.debuggerModel.editScriptSource(sourceID, lines.join("\n"));
875         }
876         this._textViewer.editLine(lineRow, didEditLine.bind(this));
877     },
878
879     _setBreakpoint: function(lineNumber, enabled, condition)
880     {
881         var sourceID = this._sourceIDForLine(lineNumber);
882         if (!sourceID)
883             return;
884         WebInspector.debuggerModel.setBreakpoint(sourceID, lineNumber + 1, enabled, condition);
885         if (!WebInspector.panels.scripts.breakpointsActivated)
886             WebInspector.panels.scripts.toggleBreakpointsClicked();
887     },
888
889     _breakpoints: function()
890     {
891         var sourceIDSet = this._sourceIDSet();
892         return WebInspector.debuggerModel.queryBreakpoints(function(b) { return b.sourceID in sourceIDSet; });
893     },
894
895     _findBreakpoint: function(lineNumber)
896     {
897         var sourceID = this._sourceIDForLine(lineNumber);
898         return WebInspector.debuggerModel.findBreakpoint(sourceID, lineNumber + 1);
899     },
900
901     _sourceIDForLine: function(lineNumber)
902     {
903         var sourceIDForLine = null;
904         var closestStartingLine = 0;
905         var scripts = this._contentProvider.scripts();
906         for (var i = 0; i < scripts.length; ++i) {
907             var lineOffset = scripts[i].lineOffset;
908             if (lineOffset <= lineNumber && lineOffset >= closestStartingLine) {
909                 closestStartingLine = lineOffset;
910                 sourceIDForLine = scripts[i].sourceID;
911             }
912         }
913         return sourceIDForLine;
914     },
915
916     _sourceIDSet: function()
917     {
918         var scripts = this._contentProvider.scripts();
919         var sourceIDSet = {};
920         for (var i = 0; i < scripts.length; ++i)
921             sourceIDSet[scripts[i].sourceID] = true;
922         return sourceIDSet;
923     }
924 }
925
926 WebInspector.SourceFrame.prototype.__proto__ = WebInspector.View.prototype;
927
928
929 WebInspector.SourceFrameContentProvider = function()
930 {
931 }
932
933 WebInspector.SourceFrameContentProvider.prototype = {
934     requestContent: function(callback)
935     {
936         // Should be implemented by subclasses.
937     },
938
939     scripts: function()
940     {
941         // Should be implemented by subclasses.
942     }
943 }