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