Home | History | Annotate | Download | only in front-end
      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._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 
     55 WebInspector.SourceFrame.Events = {
     56     Loaded: "loaded"
     57 }
     58 
     59 WebInspector.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 
    948 WebInspector.SourceFrame.prototype.__proto__ = WebInspector.TextViewerDelegate.prototype;
    949 
    950 
    951 WebInspector.SourceFrameDelegate = function()
    952 {
    953 }
    954 
    955 WebInspector.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