2011-04-11 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(delegate, url)
32 {
33     WebInspector.TextViewerDelegate.call(this);
34
35     this._delegate = delegate;
36     this._url = url;
37
38     this._textModel = new WebInspector.TextEditorModel();
39     this._textModel.replaceTabsWithSpaces = true;
40
41     this._textViewer = new WebInspector.TextViewer(this._textModel, WebInspector.platform, this._url, this);
42     this._visible = false;
43
44     this._currentSearchResultIndex = -1;
45     this._searchResults = [];
46
47     this._messages = [];
48     this._rowMessages = {};
49     this._messageBubbles = {};
50
51     this._breakpoints = {};
52 }
53
54 WebInspector.SourceFrame.Events = {
55     Loaded: "loaded"
56 }
57
58 WebInspector.SourceFrame.prototype = {
59     get visible()
60     {
61         return this._textViewer.visible;
62     },
63
64     set visible(x)
65     {
66         this._textViewer.visible = x;
67     },
68
69     show: function(parentElement)
70     {
71         this._ensureContentLoaded();
72
73         this._textViewer.show(parentElement);
74         this._textViewer.resize();
75
76         if (this.loaded) {
77             if (this._scrollTop)
78                 this._textViewer.scrollTop = this._scrollTop;
79             if (this._scrollLeft)
80                 this._textViewer.scrollLeft = this._scrollLeft;
81         }
82     },
83
84     hide: function()
85     {
86         if (this.loaded) {
87             this._scrollTop = this._textViewer.scrollTop;
88             this._scrollLeft = this._textViewer.scrollLeft;
89             this._textViewer.freeCachedElements();
90         }
91
92         this._textViewer.hide();
93         this._hidePopup();
94         this._clearLineHighlight();
95     },
96
97     detach: function()
98     {
99         this._textViewer.detach();
100     },
101
102     get element()
103     {
104         return this._textViewer.element;
105     },
106
107     get loaded()
108     {
109         return !!this._content;
110     },
111
112     hasContent: function()
113     {
114         return true;
115     },
116
117     _ensureContentLoaded: function()
118     {
119         if (!this._contentRequested) {
120             this._contentRequested = true;
121             this._requestContent(this._initializeTextViewer.bind(this));
122         }
123     },
124
125     _requestContent: function(callback)
126     {
127         this._delegate.requestContent(callback);
128     },
129
130     markDiff: function(diffData)
131     {
132         if (this._diffLines && this.loaded)
133             this._removeDiffDecorations();
134
135         this._diffLines = diffData;
136         if (this.loaded)
137             this._updateDiffDecorations();
138     },
139
140     addMessage: function(msg)
141     {
142         this._messages.push(msg);
143         if (this.loaded)
144             this.addMessageToSource(msg.line - 1, msg);
145     },
146
147     clearMessages: function()
148     {
149         for (var line in this._messageBubbles) {
150             var bubble = this._messageBubbles[line];
151             bubble.parentNode.removeChild(bubble);
152         }
153
154         this._messages = [];
155         this._rowMessages = {};
156         this._messageBubbles = {};
157
158         this._textViewer.resize();
159     },
160
161     get textModel()
162     {
163         return this._textModel;
164     },
165
166     get scrollTop()
167     {
168         return this.loaded ? this._textViewer.scrollTop : this._scrollTop;
169     },
170
171     set scrollTop(scrollTop)
172     {
173         this._scrollTop = scrollTop;
174         if (this.loaded)
175             this._textViewer.scrollTop = scrollTop;
176     },
177
178     highlightLine: function(line)
179     {
180         if (this.loaded)
181             this._textViewer.highlightLine(line);
182         else
183             this._lineToHighlight = line;
184     },
185
186     _clearLineHighlight: function()
187     {
188         if (this.loaded)
189             this._textViewer.clearLineHighlight();
190         else
191             delete this._lineToHighlight;
192     },
193
194     _saveViewerState: function()
195     {
196         this._viewerState = {
197             textModelContent: this._textModel.text,
198             executionLineNumber: this._executionLineNumber,
199             messages: this._messages,
200             diffLines: this._diffLines,
201             breakpoints: this._breakpoints
202         };
203     },
204
205     _restoreViewerState: function()
206     {
207         if (!this._viewerState)
208             return;
209         this._textModel.setText(null, this._viewerState.textModelContent);
210
211         this._messages = this._viewerState.messages;
212         this._diffLines = this._viewerState.diffLines;
213         this._setTextViewerDecorations();
214
215         if (typeof this._viewerState.executionLineNumber === "number") {
216             this.clearExecutionLine();
217             this.setExecutionLine(this._viewerState.executionLineNumber);
218         }
219
220         var oldBreakpoints = this._breakpoints;
221         this._breakpoints = {};
222         for (var lineNumber in oldBreakpoints)
223             this.removeBreakpoint(Number(lineNumber));
224
225         var newBreakpoints = this._viewerState.breakpoints;
226         for (var lineNumber in newBreakpoints) {
227             lineNumber = Number(lineNumber);
228             var breakpoint = newBreakpoints[lineNumber];
229             this.addBreakpoint(lineNumber, breakpoint.resolved, breakpoint.conditional, breakpoint.enabled);
230         }
231
232         delete this._viewerState;
233     },
234
235     isContentEditable: function()
236     {
237         return this._delegate.canEditScriptSource();
238     },
239
240     startEditing: function()
241     {
242         if (!this._viewerState) {
243             this._saveViewerState();
244             this._delegate.setScriptSourceIsBeingEdited(true);
245         }
246
247         WebInspector.searchController.cancelSearch();
248         this.clearMessages();
249     },
250
251     endEditing: function(oldRange, newRange)
252     {
253         // Adjust execution line number.
254         if (typeof this._executionLineNumber === "number") {
255             var newExecutionLineNumber = this._lineNumberAfterEditing(this._executionLineNumber, oldRange, newRange);
256             this.clearExecutionLine();
257             this.setExecutionLine(newExecutionLineNumber, true);
258         }
259
260         // Adjust breakpoints.
261         var oldBreakpoints = this._breakpoints;
262         this._breakpoints = {};
263         for (var lineNumber in oldBreakpoints) {
264             lineNumber = Number(lineNumber);
265             var breakpoint = oldBreakpoints[lineNumber];
266             var newLineNumber = this._lineNumberAfterEditing(lineNumber, oldRange, newRange);
267             if (lineNumber === newLineNumber)
268                 this._breakpoints[lineNumber] = breakpoint;
269             else {
270                 this.removeBreakpoint(lineNumber);
271                 this.addBreakpoint(newLineNumber, breakpoint.resolved, breakpoint.conditional, breakpoint.enabled);
272             }
273         }
274     },
275
276     _lineNumberAfterEditing: function(lineNumber, oldRange, newRange)
277     {
278         var shiftOffset = lineNumber <= oldRange.startLine ? 0 : newRange.linesCount - oldRange.linesCount;
279
280         // Special case of editing the line itself. We should decide whether the line number should move below or not.
281         if (lineNumber === oldRange.startLine && lineNumber + 1 <= newRange.endLine && this._textModel.lineLength(lineNumber) < this._textModel.lineLength(lineNumber + 1))
282             shiftOffset = 1;
283
284         var newLineNumber = Math.max(0, lineNumber + shiftOffset);
285         if (oldRange.startLine < lineNumber && lineNumber < oldRange.endLine)
286             newLineNumber = oldRange.startLine;
287         return newLineNumber;
288     },
289
290     _initializeTextViewer: function(mimeType, content)
291     {
292         this._textViewer.mimeType = mimeType;
293
294         this._content = content;
295         this._textModel.setText(null, content);
296
297         var element = this._textViewer.element;
298         element.addStyleClass("script-view");
299         if (this._delegate.debuggingSupported()) {
300             element.addEventListener("contextmenu", this._contextMenu.bind(this), true);
301             element.addEventListener("mousedown", this._mouseDown.bind(this), true);
302             element.addEventListener("mousemove", this._mouseMove.bind(this), true);
303             element.addEventListener("scroll", this._scroll.bind(this), true);
304         }
305
306         this._textViewer.beginUpdates();
307
308         this._setTextViewerDecorations();
309
310         if (typeof this._executionLineNumber === "number")
311             this.setExecutionLine(this._executionLineNumber);
312
313         if (this._lineToHighlight) {
314             this.highlightLine(this._lineToHighlight);
315             delete this._lineToHighlight;
316         }
317
318         if (this._delayedFindSearchMatches) {
319             this._delayedFindSearchMatches();
320             delete this._delayedFindSearchMatches;
321         }
322
323         this.dispatchEventToListeners(WebInspector.SourceFrame.Events.Loaded);
324
325         this._textViewer.endUpdates();
326
327         if (this._parentElement)
328             this.show(this._parentElement)
329     },
330
331     _setTextViewerDecorations: function()
332     {
333         this._rowMessages = {};
334         this._messageBubbles = {};
335
336         this._textViewer.beginUpdates();
337
338         this._addExistingMessagesToSource();
339         this._updateDiffDecorations();
340
341         this._textViewer.resize();
342
343         this._textViewer.endUpdates();
344     },
345
346     performSearch: function(query, callback)
347     {
348         // Call searchCanceled since it will reset everything we need before doing a new search.
349         this.searchCanceled();
350
351         function doFindSearchMatches(query)
352         {
353             this._currentSearchResultIndex = -1;
354             this._searchResults = [];
355
356             // First do case-insensitive search.
357             var regexObject = createSearchRegex(query);
358             this._collectRegexMatches(regexObject, this._searchResults);
359
360             // Then try regex search if user knows the / / hint.
361             try {
362                 if (/^\/.*\/$/.test(query))
363                     this._collectRegexMatches(new RegExp(query.substring(1, query.length - 1)), this._searchResults);
364             } catch (e) {
365                 // Silent catch.
366             }
367
368             callback(this, this._searchResults.length);
369         }
370
371         if (this.loaded)
372             doFindSearchMatches.call(this, query);
373         else
374             this._delayedFindSearchMatches = doFindSearchMatches.bind(this, query);
375
376         this._ensureContentLoaded();
377     },
378
379     searchCanceled: function()
380     {
381         delete this._delayedFindSearchMatches;
382         if (!this.loaded)
383             return;
384
385         this._currentSearchResultIndex = -1;
386         this._searchResults = [];
387         this._textViewer.markAndRevealRange(null);
388     },
389
390     jumpToFirstSearchResult: function()
391     {
392         this._jumpToSearchResult(0);
393     },
394
395     jumpToLastSearchResult: function()
396     {
397         this._jumpToSearchResult(this._searchResults.length - 1);
398     },
399
400     jumpToNextSearchResult: function()
401     {
402         this._jumpToSearchResult(this._currentSearchResultIndex + 1);
403     },
404
405     jumpToPreviousSearchResult: function()
406     {
407         this._jumpToSearchResult(this._currentSearchResultIndex - 1);
408     },
409
410     showingFirstSearchResult: function()
411     {
412         return this._searchResults.length &&  this._currentSearchResultIndex === 0;
413     },
414
415     showingLastSearchResult: function()
416     {
417         return this._searchResults.length && this._currentSearchResultIndex === (this._searchResults.length - 1);
418     },
419
420     _jumpToSearchResult: function(index)
421     {
422         if (!this.loaded || !this._searchResults.length)
423             return;
424         this._currentSearchResultIndex = (index + this._searchResults.length) % this._searchResults.length;
425         this._textViewer.markAndRevealRange(this._searchResults[this._currentSearchResultIndex]);
426     },
427
428     _collectRegexMatches: function(regexObject, ranges)
429     {
430         for (var i = 0; i < this._textModel.linesCount; ++i) {
431             var line = this._textModel.line(i);
432             var offset = 0;
433             do {
434                 var match = regexObject.exec(line);
435                 if (match) {
436                     ranges.push(new WebInspector.TextRange(i, offset + match.index, i, offset + match.index + match[0].length));
437                     offset += match.index + 1;
438                     line = line.substring(match.index + 1);
439                 }
440             } while (match)
441         }
442         return ranges;
443     },
444
445     _incrementMessageRepeatCount: function(msg, repeatDelta)
446     {
447         if (!msg._resourceMessageLineElement)
448             return;
449
450         if (!msg._resourceMessageRepeatCountElement) {
451             var repeatedElement = document.createElement("span");
452             msg._resourceMessageLineElement.appendChild(repeatedElement);
453             msg._resourceMessageRepeatCountElement = repeatedElement;
454         }
455
456         msg.repeatCount += repeatDelta;
457         msg._resourceMessageRepeatCountElement.textContent = WebInspector.UIString(" (repeated %d times)", msg.repeatCount);
458     },
459
460     setExecutionLine: function(lineNumber, skipRevealLine)
461     {
462         this._executionLineNumber = lineNumber;
463         if (this.loaded) {
464             this._textViewer.addDecoration(lineNumber, "webkit-execution-line");
465             if (!skipRevealLine)
466                 this._textViewer.revealLine(lineNumber);
467         }
468     },
469
470     clearExecutionLine: function()
471     {
472         if (this.loaded)
473             this._textViewer.removeDecoration(this._executionLineNumber, "webkit-execution-line");
474         delete this._executionLineNumber;
475     },
476
477     _updateDiffDecorations: function()
478     {
479         if (!this._diffLines)
480             return;
481
482         function addDecorations(textViewer, lines, className)
483         {
484             for (var i = 0; i < lines.length; ++i)
485                 textViewer.addDecoration(lines[i], className);
486         }
487         addDecorations(this._textViewer, this._diffLines.added, "webkit-added-line");
488         addDecorations(this._textViewer, this._diffLines.removed, "webkit-removed-line");
489         addDecorations(this._textViewer, this._diffLines.changed, "webkit-changed-line");
490     },
491
492     _removeDiffDecorations: function()
493     {
494         function removeDecorations(textViewer, lines, className)
495         {
496             for (var i = 0; i < lines.length; ++i)
497                 textViewer.removeDecoration(lines[i], className);
498         }
499         removeDecorations(this._textViewer, this._diffLines.added, "webkit-added-line");
500         removeDecorations(this._textViewer, this._diffLines.removed, "webkit-removed-line");
501         removeDecorations(this._textViewer, this._diffLines.changed, "webkit-changed-line");
502     },
503
504     _addExistingMessagesToSource: function()
505     {
506         var length = this._messages.length;
507         for (var i = 0; i < length; ++i)
508             this.addMessageToSource(this._messages[i].line - 1, this._messages[i]);
509     },
510
511     addMessageToSource: function(lineNumber, msg)
512     {
513         if (lineNumber >= this._textModel.linesCount)
514             lineNumber = this._textModel.linesCount - 1;
515         if (lineNumber < 0)
516             lineNumber = 0;
517
518         var messageBubbleElement = this._messageBubbles[lineNumber];
519         if (!messageBubbleElement || messageBubbleElement.nodeType !== Node.ELEMENT_NODE || !messageBubbleElement.hasStyleClass("webkit-html-message-bubble")) {
520             messageBubbleElement = document.createElement("div");
521             messageBubbleElement.className = "webkit-html-message-bubble";
522             this._messageBubbles[lineNumber] = messageBubbleElement;
523             this._textViewer.addDecoration(lineNumber, messageBubbleElement);
524         }
525
526         var rowMessages = this._rowMessages[lineNumber];
527         if (!rowMessages) {
528             rowMessages = [];
529             this._rowMessages[lineNumber] = rowMessages;
530         }
531
532         for (var i = 0; i < rowMessages.length; ++i) {
533             if (rowMessages[i].isEqual(msg)) {
534                 this._incrementMessageRepeatCount(rowMessages[i], msg.repeatDelta);
535                 return;
536             }
537         }
538
539         rowMessages.push(msg);
540
541         var imageURL;
542         switch (msg.level) {
543             case WebInspector.ConsoleMessage.MessageLevel.Error:
544                 messageBubbleElement.addStyleClass("webkit-html-error-message");
545                 imageURL = "Images/errorIcon.png";
546                 break;
547             case WebInspector.ConsoleMessage.MessageLevel.Warning:
548                 messageBubbleElement.addStyleClass("webkit-html-warning-message");
549                 imageURL = "Images/warningIcon.png";
550                 break;
551         }
552
553         var messageLineElement = document.createElement("div");
554         messageLineElement.className = "webkit-html-message-line";
555         messageBubbleElement.appendChild(messageLineElement);
556
557         // Create the image element in the Inspector's document so we can use relative image URLs.
558         var image = document.createElement("img");
559         image.src = imageURL;
560         image.className = "webkit-html-message-icon";
561         messageLineElement.appendChild(image);
562         messageLineElement.appendChild(document.createTextNode(msg.message));
563
564         msg._resourceMessageLineElement = messageLineElement;
565     },
566
567     addBreakpoint: function(lineNumber, resolved, conditional, enabled)
568     {
569         this._breakpoints[lineNumber] = {
570             resolved: resolved,
571             conditional: conditional,
572             enabled: enabled
573         };
574         this._textViewer.beginUpdates();
575         this._textViewer.addDecoration(lineNumber, "webkit-breakpoint");
576         if (!enabled)
577             this._textViewer.addDecoration(lineNumber, "webkit-breakpoint-disabled");
578         if (conditional)
579             this._textViewer.addDecoration(lineNumber, "webkit-breakpoint-conditional");
580         this._textViewer.endUpdates();
581     },
582
583     removeBreakpoint: function(lineNumber)
584     {
585         delete this._breakpoints[lineNumber];
586         this._textViewer.beginUpdates();
587         this._textViewer.removeDecoration(lineNumber, "webkit-breakpoint");
588         this._textViewer.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
589         this._textViewer.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
590         this._textViewer.endUpdates();
591     },
592
593     _contextMenu: function(event)
594     {
595         var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
596         if (!target)
597             return;
598         var lineNumber = target.lineNumber;
599
600         var contextMenu = new WebInspector.ContextMenu();
601
602         contextMenu.appendItem(WebInspector.UIString("Continue to Here"), this._delegate.continueToLine.bind(this._delegate, lineNumber));
603
604         var breakpoint = this._delegate.findBreakpoint(lineNumber);
605         if (!breakpoint) {
606             // This row doesn't have a breakpoint: We want to show Add Breakpoint and Add and Edit Breakpoint.
607             contextMenu.appendItem(WebInspector.UIString("Add Breakpoint"), this._delegate.setBreakpoint.bind(this._delegate, lineNumber, "", true));
608
609             function addConditionalBreakpoint()
610             {
611                 this.addBreakpoint(lineNumber, true, true, true);
612                 function didEditBreakpointCondition(committed, condition)
613                 {
614                     this.removeBreakpoint(lineNumber);
615                     if (committed)
616                         this._delegate.setBreakpoint(lineNumber, condition, true);
617                 }
618                 this._editBreakpointCondition(lineNumber, "", didEditBreakpointCondition.bind(this));
619             }
620             contextMenu.appendItem(WebInspector.UIString("Add Conditional Breakpoint…"), addConditionalBreakpoint.bind(this));
621         } else {
622             // This row has a breakpoint, we want to show edit and remove breakpoint, and either disable or enable.
623             contextMenu.appendItem(WebInspector.UIString("Remove Breakpoint"), this._delegate.removeBreakpoint.bind(this._delegate, lineNumber));
624             function editBreakpointCondition()
625             {
626                 function didEditBreakpointCondition(committed, condition)
627                 {
628                     if (committed)
629                         this._delegate.updateBreakpoint(lineNumber, condition, breakpoint.enabled);
630                 }
631                 this._editBreakpointCondition(lineNumber, breakpoint.condition, didEditBreakpointCondition.bind(this));
632             }
633             contextMenu.appendItem(WebInspector.UIString("Edit Breakpoint…"), editBreakpointCondition.bind(this));
634             function setBreakpointEnabled(enabled)
635             {
636                 this._delegate.updateBreakpoint(lineNumber, breakpoint.condition, enabled);
637             }
638             if (breakpoint.enabled)
639                 contextMenu.appendItem(WebInspector.UIString("Disable Breakpoint"), setBreakpointEnabled.bind(this, false));
640             else
641                 contextMenu.appendItem(WebInspector.UIString("Enable Breakpoint"), setBreakpointEnabled.bind(this, true));
642         }
643         contextMenu.show(event);
644     },
645
646     _scroll: function(event)
647     {
648         this._hidePopup();
649     },
650
651     _mouseDown: function(event)
652     {
653         this._resetHoverTimer();
654         this._hidePopup();
655         if (event.button != 0 || event.altKey || event.ctrlKey || event.metaKey)
656             return;
657         var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
658         if (!target)
659             return;
660         var lineNumber = target.lineNumber;
661
662         var breakpoint = this._delegate.findBreakpoint(lineNumber);
663         if (breakpoint) {
664             if (event.shiftKey)
665                 this._delegate.updateBreakpoint(lineNumber, breakpoint.condition, !breakpoint.enabled);
666             else
667                 this._delegate.removeBreakpoint(lineNumber);
668         } else
669             this._delegate.setBreakpoint(lineNumber, "", true);
670         event.preventDefault();
671     },
672
673     _mouseMove: function(event)
674     {
675         // Pretend that nothing has happened.
676         if (this._hoverElement === event.target || event.target.hasStyleClass("source-frame-eval-expression"))
677             return;
678
679         this._resetHoverTimer();
680         // User has 500ms to reach the popup.
681         if (this._popup) {
682             var self = this;
683             function doHide()
684             {
685                 self._hidePopup();
686                 delete self._hidePopupTimer;
687             }
688             if (!("_hidePopupTimer" in this))
689                 this._hidePopupTimer = setTimeout(doHide, 500);
690         }
691
692         this._hoverElement = event.target;
693
694         // Now that cleanup routines are set up above, leave this in case we are not on a break.
695         if (!this._delegate.debuggerPaused())
696             return;
697
698         // We are interested in identifiers and "this" keyword.
699         if (this._hoverElement.hasStyleClass("webkit-javascript-keyword")) {
700             if (this._hoverElement.textContent !== "this")
701                 return;
702         } else if (!this._hoverElement.hasStyleClass("webkit-javascript-ident"))
703             return;
704
705         const toolTipDelay = this._popup ? 600 : 1000;
706         this._hoverTimer = setTimeout(this._mouseHover.bind(this, this._hoverElement), toolTipDelay);
707     },
708
709     _resetHoverTimer: function()
710     {
711         if (this._hoverTimer) {
712             clearTimeout(this._hoverTimer);
713             delete this._hoverTimer;
714         }
715     },
716
717     _hidePopup: function()
718     {
719         if (!this._popup)
720             return;
721
722         // Replace higlight element with its contents inplace.
723         var parentElement = this._popup.highlightElement.parentElement;
724         var child = this._popup.highlightElement.firstChild;
725         while (child) {
726             var nextSibling = child.nextSibling;
727             parentElement.insertBefore(child, this._popup.highlightElement);
728             child = nextSibling;
729         }
730         parentElement.removeChild(this._popup.highlightElement);
731
732         this._popup.hide();
733         delete this._popup;
734         this._delegate.releaseEvaluationResult();
735     },
736
737     _mouseHover: function(element)
738     {
739         delete this._hoverTimer;
740
741         var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content");
742         if (!lineRow)
743             return;
744
745         // Collect tokens belonging to evaluated exression.
746         var tokens = [ element ];
747         var token = element.previousSibling;
748         while (token && (token.className === "webkit-javascript-ident" || token.className === "webkit-javascript-keyword" || token.textContent.trim() === ".")) {
749             tokens.push(token);
750             token = token.previousSibling;
751         }
752         tokens.reverse();
753
754         // Wrap them with highlight element.
755         var parentElement = element.parentElement;
756         var nextElement = element.nextSibling;
757         var container = document.createElement("span");
758         for (var i = 0; i < tokens.length; ++i)
759             container.appendChild(tokens[i]);
760         parentElement.insertBefore(container, nextElement);
761         this._showPopup(container);
762     },
763
764     _showPopup: function(element)
765     {
766         if (!this._delegate.debuggerPaused())
767             return;
768
769         function killHidePopupTimer()
770         {
771             if (this._hidePopupTimer) {
772                 clearTimeout(this._hidePopupTimer);
773                 delete this._hidePopupTimer;
774
775                 // We know that we reached the popup, but we might have moved over other elements.
776                 // Discard pending command.
777                 this._resetHoverTimer();
778             }
779         }
780
781         function showObjectPopup(result)
782         {
783             if (result.isError() || !this._delegate.debuggerPaused())
784                 return;
785
786             var popupContentElement = null;
787             if (result.type !== "object" && result.type !== "node" && result.type !== "array") {
788                 popupContentElement = document.createElement("span");
789                 popupContentElement.className = "monospace console-formatted-" + result.type;
790                 popupContentElement.style.whiteSpace = "pre";
791                 popupContentElement.textContent = result.description;
792                 if (result.type === "string")
793                     popupContentElement.textContent = "\"" + popupContentElement.textContent + "\"";
794                 this._popup = new WebInspector.Popover(popupContentElement);
795                 this._popup.show(element);
796             } else {
797                 var popupContentElement = document.createElement("div");
798
799                 var titleElement = document.createElement("div");
800                 titleElement.className = "source-frame-popover-title monospace";
801                 titleElement.textContent = result.description;
802                 popupContentElement.appendChild(titleElement);
803
804                 var section = new WebInspector.ObjectPropertiesSection(result, "", null, false);
805                 section.expanded = true;
806                 section.element.addStyleClass("source-frame-popover-tree");
807                 section.headerElement.addStyleClass("hidden");
808                 popupContentElement.appendChild(section.element);
809
810                 this._popup = new WebInspector.Popover(popupContentElement);
811                 const popupWidth = 300;
812                 const popupHeight = 250;
813                 this._popup.show(element, popupWidth, popupHeight);
814             }
815             this._popup.highlightElement = element;
816             this._popup.highlightElement.addStyleClass("source-frame-eval-expression");
817             popupContentElement.addEventListener("mousemove", killHidePopupTimer.bind(this), true);
818         }
819
820         this._delegate.evaluateInSelectedCallFrame(element.textContent, showObjectPopup.bind(this));
821     },
822
823     _editBreakpointCondition: function(lineNumber, condition, callback)
824     {
825         this._conditionElement = this._createConditionElement(lineNumber);
826         this._textViewer.addDecoration(lineNumber, this._conditionElement);
827
828         function finishEditing(committed, element, newText)
829         {
830             this._textViewer.removeDecoration(lineNumber, this._conditionElement);
831             delete this._conditionEditorElement;
832             delete this._conditionElement;
833             callback(committed, newText);
834         }
835
836         WebInspector.startEditing(this._conditionEditorElement, {
837             context: null,
838             commitHandler: finishEditing.bind(this, true),
839             cancelHandler: finishEditing.bind(this, false)
840         });
841         this._conditionEditorElement.value = condition;
842         this._conditionEditorElement.select();
843     },
844
845     _createConditionElement: function(lineNumber)
846     {
847         var conditionElement = document.createElement("div");
848         conditionElement.className = "source-frame-breakpoint-condition";
849
850         var labelElement = document.createElement("label");
851         labelElement.className = "source-frame-breakpoint-message";
852         labelElement.htmlFor = "source-frame-breakpoint-condition";
853         labelElement.appendChild(document.createTextNode(WebInspector.UIString("The breakpoint on line %d will stop only if this expression is true:", lineNumber)));
854         conditionElement.appendChild(labelElement);
855
856         var editorElement = document.createElement("input");
857         editorElement.id = "source-frame-breakpoint-condition";
858         editorElement.className = "monospace";
859         editorElement.type = "text";
860         conditionElement.appendChild(editorElement);
861         this._conditionEditorElement = editorElement;
862
863         return conditionElement;
864     },
865
866     resize: function()
867     {
868         this._textViewer.resize();
869     },
870
871     commitEditing: function(callback)
872     {
873         if (!this._viewerState) {
874             // No editing was actually done.
875             this._delegate.setScriptSourceIsBeingEdited(false);
876             callback();
877             return;
878         }
879
880         function didEditContent(error)
881         {
882             if (error) {
883                 WebInspector.log(error.data[0], WebInspector.ConsoleMessage.MessageLevel.Error);
884                 WebInspector.showConsole();
885                 callback(error);
886                 return;
887             }
888
889             var newBreakpoints = {};
890             for (var lineNumber in this._breakpoints) {
891                 newBreakpoints[lineNumber] = this._breakpoints[lineNumber];
892                 this.removeBreakpoint(Number(lineNumber));
893             }
894
895             for (var lineNumber in this._viewerState.breakpoints)
896                 this._delegate.removeBreakpoint(Number(lineNumber));
897
898             for (var lineNumber in newBreakpoints) {
899                 var breakpoint = newBreakpoints[lineNumber];
900                 this._delegate.setBreakpoint(Number(lineNumber), breakpoint.condition, breakpoint.enabled);
901             }
902
903             delete this._viewerState;
904             this._delegate.setScriptSourceIsBeingEdited(false);
905
906             callback();
907         }
908         this._editContent(this._textModel.text, didEditContent.bind(this));
909     },
910
911     _editContent: function(newContent, callback)
912     {
913         this._delegate.editScriptSource(newContent, callback);
914     },
915
916     cancelEditing: function()
917     {
918         this._restoreViewerState();
919         this._delegate.setScriptSourceIsBeingEdited(false);
920     }
921 }
922
923 WebInspector.SourceFrame.prototype.__proto__ = WebInspector.TextViewerDelegate.prototype;
924
925
926 WebInspector.SourceFrameDelegate = function()
927 {
928 }
929
930 WebInspector.SourceFrameDelegate.prototype = {
931     requestContent: function(callback)
932     {
933         // Should be implemented by subclasses.
934     },
935
936     debuggingSupported: function()
937     {
938         return false;
939     },
940
941     setBreakpoint: function(lineNumber, condition, enabled)
942     {
943         // Should be implemented by subclasses.
944     },
945
946     removeBreakpoint: function(lineNumber)
947     {
948         // Should be implemented by subclasses.
949     },
950
951     updateBreakpoint: function(lineNumber, condition, enabled)
952     {
953         // Should be implemented by subclasses.
954     },
955
956     findBreakpoint: function(lineNumber)
957     {
958         // Should be implemented by subclasses.
959     },
960
961     continueToLine: function(lineNumber)
962     {
963         // Should be implemented by subclasses.
964     },
965
966     canEditScriptSource: function()
967     {
968         return false;
969     },
970
971     editScriptSource: function(text, callback)
972     {
973         // Should be implemented by subclasses.
974     },
975
976     setScriptSourceIsBeingEdited: function(inEditMode)
977     {
978         // Should be implemented by subclasses.
979     },
980
981     debuggerPaused: function()
982     {
983         // Should be implemented by subclasses.
984     },
985
986     evaluateInSelectedCallFrame: function(string)
987     {
988         // Should be implemented by subclasses.
989     },
990
991     releaseEvaluationResult: function()
992     {
993         // Should be implemented by subclasses.
994     }
995 }