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