Home | History | Annotate | Download | only in source_frame
      1 /*
      2  * Copyright (C) 2011 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 importScript("../cm/codemirror.js");
     32 importScript("../cm/css.js");
     33 importScript("../cm/javascript.js");
     34 importScript("../cm/xml.js");
     35 importScript("../cm/htmlmixed.js");
     36 
     37 importScript("../cm/matchbrackets.js");
     38 importScript("../cm/closebrackets.js");
     39 importScript("../cm/markselection.js");
     40 importScript("../cm/comment.js");
     41 importScript("../cm/overlay.js");
     42 
     43 importScript("../cm/htmlembedded.js");
     44 importScript("../cm/clike.js");
     45 importScript("../cm/coffeescript.js");
     46 importScript("../cm/php.js");
     47 importScript("../cm/python.js");
     48 importScript("../cm/shell.js");
     49 importScript("CodeMirrorUtils.js");
     50 importScript("CodeMirrorTextEditor.js");
     51 
     52 /**
     53  * @extends {WebInspector.VBox}
     54  * @constructor
     55  * @implements {WebInspector.Replaceable}
     56  * @param {!WebInspector.ContentProvider} contentProvider
     57  */
     58 WebInspector.SourceFrame = function(contentProvider)
     59 {
     60     WebInspector.VBox.call(this);
     61     this.element.classList.add("script-view");
     62 
     63     this._url = contentProvider.contentURL();
     64     this._contentProvider = contentProvider;
     65 
     66     var textEditorDelegate = new WebInspector.TextEditorDelegateForSourceFrame(this);
     67 
     68     this._textEditor = new WebInspector.CodeMirrorTextEditor(this._url, textEditorDelegate);
     69 
     70     this._currentSearchResultIndex = -1;
     71     this._searchResults = [];
     72 
     73     this._messages = [];
     74     this._rowMessages = {};
     75     this._messageBubbles = {};
     76 
     77     this._textEditor.setReadOnly(!this.canEditSource());
     78 
     79     this._shortcuts = {};
     80     this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false);
     81 
     82     this._sourcePosition = new WebInspector.StatusBarText("", "source-frame-cursor-position");
     83 }
     84 
     85 /**
     86  * @param {string} query
     87  * @param {string=} modifiers
     88  * @return {!RegExp}
     89  */
     90 WebInspector.SourceFrame.createSearchRegex = function(query, modifiers)
     91 {
     92     var regex;
     93     modifiers = modifiers || "";
     94 
     95     // First try creating regex if user knows the / / hint.
     96     try {
     97         if (/^\/.+\/$/.test(query)) {
     98             regex = new RegExp(query.substring(1, query.length - 1), modifiers);
     99             regex.__fromRegExpQuery = true;
    100         }
    101     } catch (e) {
    102         // Silent catch.
    103     }
    104 
    105     // Otherwise just do case-insensitive search.
    106     if (!regex)
    107         regex = createPlainTextSearchRegex(query, "i" + modifiers);
    108 
    109     return regex;
    110 }
    111 
    112 WebInspector.SourceFrame.Events = {
    113     ScrollChanged: "ScrollChanged",
    114     SelectionChanged: "SelectionChanged",
    115     JumpHappened: "JumpHappened"
    116 }
    117 
    118 WebInspector.SourceFrame.prototype = {
    119     /**
    120      * @param {number} key
    121      * @param {function()} handler
    122      */
    123     addShortcut: function(key, handler)
    124     {
    125         this._shortcuts[key] = handler;
    126     },
    127 
    128     wasShown: function()
    129     {
    130         this._ensureContentLoaded();
    131         this._textEditor.show(this.element);
    132         this._editorAttached = true;
    133         this._wasShownOrLoaded();
    134     },
    135 
    136     /**
    137      * @return {boolean}
    138      */
    139     _isEditorShowing: function()
    140     {
    141         return this.isShowing() && this._editorAttached;
    142     },
    143 
    144     willHide: function()
    145     {
    146         WebInspector.View.prototype.willHide.call(this);
    147 
    148         this._clearPositionToReveal();
    149     },
    150 
    151     /**
    152      * @return {?Element}
    153      */
    154     statusBarText: function()
    155     {
    156         return this._sourcePosition.element;
    157     },
    158 
    159     /**
    160      * @return {!Array.<!Element>}
    161      */
    162     statusBarItems: function()
    163     {
    164         return [];
    165     },
    166 
    167     /**
    168      * @return {!Element}
    169      */
    170     defaultFocusedElement: function()
    171     {
    172         return this._textEditor.defaultFocusedElement();
    173     },
    174 
    175     get loaded()
    176     {
    177         return this._loaded;
    178     },
    179 
    180     /**
    181      * @return {boolean}
    182      */
    183     hasContent: function()
    184     {
    185         return true;
    186     },
    187 
    188     get textEditor()
    189     {
    190         return this._textEditor;
    191     },
    192 
    193     _ensureContentLoaded: function()
    194     {
    195         if (!this._contentRequested) {
    196             this._contentRequested = true;
    197             this._contentProvider.requestContent(this.setContent.bind(this));
    198         }
    199     },
    200 
    201     addMessage: function(msg)
    202     {
    203         this._messages.push(msg);
    204         if (this.loaded)
    205             this.addMessageToSource(msg.line - 1, msg);
    206     },
    207 
    208     clearMessages: function()
    209     {
    210         for (var line in this._messageBubbles) {
    211             var bubble = this._messageBubbles[line];
    212             var lineNumber = parseInt(line, 10);
    213             this._textEditor.removeDecoration(lineNumber, bubble);
    214         }
    215 
    216         this._messages = [];
    217         this._rowMessages = {};
    218         this._messageBubbles = {};
    219     },
    220 
    221     /**
    222      * @param {number} line
    223      * @param {number=} column
    224      * @param {boolean=} shouldHighlight
    225      */
    226     revealPosition: function(line, column, shouldHighlight)
    227     {
    228         this._clearLineToScrollTo();
    229         this._clearSelectionToSet();
    230         this._positionToReveal = { line: line, column: column, shouldHighlight: shouldHighlight };
    231         this._innerRevealPositionIfNeeded();
    232     },
    233 
    234     _innerRevealPositionIfNeeded: function()
    235     {
    236         if (!this._positionToReveal)
    237             return;
    238 
    239         if (!this.loaded || !this._isEditorShowing())
    240             return;
    241 
    242         this._textEditor.revealPosition(this._positionToReveal.line, this._positionToReveal.column, this._positionToReveal.shouldHighlight);
    243         delete this._positionToReveal;
    244     },
    245 
    246     _clearPositionToReveal: function()
    247     {
    248         this._textEditor.clearPositionHighlight();
    249         delete this._positionToReveal;
    250     },
    251 
    252     /**
    253      * @param {number} line
    254      */
    255     scrollToLine: function(line)
    256     {
    257         this._clearPositionToReveal();
    258         this._lineToScrollTo = line;
    259         this._innerScrollToLineIfNeeded();
    260     },
    261 
    262     _innerScrollToLineIfNeeded: function()
    263     {
    264         if (typeof this._lineToScrollTo === "number") {
    265             if (this.loaded && this._isEditorShowing()) {
    266                 this._textEditor.scrollToLine(this._lineToScrollTo);
    267                 delete this._lineToScrollTo;
    268             }
    269         }
    270     },
    271 
    272     _clearLineToScrollTo: function()
    273     {
    274         delete this._lineToScrollTo;
    275     },
    276 
    277     /**
    278      * @return {!WebInspector.TextRange}
    279      */
    280     selection: function()
    281     {
    282         return this.textEditor.selection();
    283     },
    284 
    285     /**
    286      * @param {!WebInspector.TextRange} textRange
    287      */
    288     setSelection: function(textRange)
    289     {
    290         this._selectionToSet = textRange;
    291         this._innerSetSelectionIfNeeded();
    292     },
    293 
    294     _innerSetSelectionIfNeeded: function()
    295     {
    296         if (this._selectionToSet && this.loaded && this._isEditorShowing()) {
    297             this._textEditor.setSelection(this._selectionToSet);
    298             delete this._selectionToSet;
    299         }
    300     },
    301 
    302     _clearSelectionToSet: function()
    303     {
    304         delete this._selectionToSet;
    305     },
    306 
    307     _wasShownOrLoaded: function()
    308     {
    309         this._innerRevealPositionIfNeeded();
    310         this._innerSetSelectionIfNeeded();
    311         this._innerScrollToLineIfNeeded();
    312     },
    313 
    314     onTextChanged: function(oldRange, newRange)
    315     {
    316         if (this._searchResultsChangedCallback && !this._isReplacing)
    317             this._searchResultsChangedCallback();
    318         this.clearMessages();
    319     },
    320 
    321     _simplifyMimeType: function(content, mimeType)
    322     {
    323         if (!mimeType)
    324             return "";
    325         if (mimeType.indexOf("javascript") >= 0 ||
    326             mimeType.indexOf("jscript") >= 0 ||
    327             mimeType.indexOf("ecmascript") >= 0)
    328             return "text/javascript";
    329         // A hack around the fact that files with "php" extension might be either standalone or html embedded php scripts.
    330         if (mimeType === "text/x-php" && content.match(/\<\?.*\?\>/g))
    331             return "application/x-httpd-php";
    332         return mimeType;
    333     },
    334 
    335     /**
    336      * @param {string} highlighterType
    337      */
    338     setHighlighterType: function(highlighterType)
    339     {
    340         this._highlighterType = highlighterType;
    341         this._updateHighlighterType("");
    342     },
    343 
    344     /**
    345      * @param {string} content
    346      */
    347     _updateHighlighterType: function(content)
    348     {
    349         this._textEditor.setMimeType(this._simplifyMimeType(content, this._highlighterType));
    350     },
    351 
    352     /**
    353      * @param {?string} content
    354      */
    355     setContent: function(content)
    356     {
    357         if (!this._loaded) {
    358             this._loaded = true;
    359             this._textEditor.setText(content || "");
    360             this._textEditor.markClean();
    361         } else {
    362             var firstLine = this._textEditor.firstVisibleLine();
    363             var selection = this._textEditor.selection();
    364             this._textEditor.setText(content || "");
    365             this._textEditor.scrollToLine(firstLine);
    366             this._textEditor.setSelection(selection);
    367         }
    368 
    369         this._updateHighlighterType(content || "");
    370 
    371         this._textEditor.beginUpdates();
    372 
    373         this._setTextEditorDecorations();
    374 
    375         this._wasShownOrLoaded();
    376 
    377         if (this._delayedFindSearchMatches) {
    378             this._delayedFindSearchMatches();
    379             delete this._delayedFindSearchMatches;
    380         }
    381 
    382         this.onTextEditorContentLoaded();
    383 
    384         this._textEditor.endUpdates();
    385     },
    386 
    387     onTextEditorContentLoaded: function() {},
    388 
    389     _setTextEditorDecorations: function()
    390     {
    391         this._rowMessages = {};
    392         this._messageBubbles = {};
    393 
    394         this._textEditor.beginUpdates();
    395 
    396         this._addExistingMessagesToSource();
    397 
    398         this._textEditor.endUpdates();
    399     },
    400 
    401     /**
    402      * @param {string} query
    403      * @param {boolean} shouldJump
    404      * @param {boolean} jumpBackwards
    405      * @param {function(!WebInspector.View, number)} callback
    406      * @param {function(number)} currentMatchChangedCallback
    407      * @param {function()} searchResultsChangedCallback
    408      */
    409     performSearch: function(query, shouldJump, jumpBackwards, callback, currentMatchChangedCallback, searchResultsChangedCallback)
    410     {
    411         /**
    412          * @param {string} query
    413          * @this {WebInspector.SourceFrame}
    414          */
    415         function doFindSearchMatches(query)
    416         {
    417             this._currentSearchResultIndex = -1;
    418             this._searchResults = [];
    419 
    420             var regex = WebInspector.SourceFrame.createSearchRegex(query);
    421             this._searchRegex = regex;
    422             this._searchResults = this._collectRegexMatches(regex);
    423             if (!this._searchResults.length)
    424                 this._textEditor.cancelSearchResultsHighlight();
    425             else if (shouldJump && jumpBackwards)
    426                 this.jumpToPreviousSearchResult();
    427             else if (shouldJump)
    428                 this.jumpToNextSearchResult();
    429             else
    430                 this._textEditor.highlightSearchResults(regex, null);
    431             callback(this, this._searchResults.length);
    432         }
    433 
    434         this._resetSearch();
    435         this._currentSearchMatchChangedCallback = currentMatchChangedCallback;
    436         this._searchResultsChangedCallback = searchResultsChangedCallback;
    437         if (this.loaded)
    438             doFindSearchMatches.call(this, query);
    439         else
    440             this._delayedFindSearchMatches = doFindSearchMatches.bind(this, query);
    441 
    442         this._ensureContentLoaded();
    443     },
    444 
    445     _editorFocused: function()
    446     {
    447         this._resetCurrentSearchResultIndex();
    448     },
    449 
    450     _resetCurrentSearchResultIndex: function()
    451     {
    452         if (!this._searchResults.length)
    453             return;
    454         this._currentSearchResultIndex = -1;
    455         if (this._currentSearchMatchChangedCallback)
    456             this._currentSearchMatchChangedCallback(this._currentSearchResultIndex);
    457         this._textEditor.highlightSearchResults(this._searchRegex, null);
    458     },
    459 
    460     _resetSearch: function()
    461     {
    462         delete this._delayedFindSearchMatches;
    463         delete this._currentSearchMatchChangedCallback;
    464         delete this._searchResultsChangedCallback;
    465         this._currentSearchResultIndex = -1;
    466         this._searchResults = [];
    467         delete this._searchRegex;
    468     },
    469 
    470     searchCanceled: function()
    471     {
    472         var range = this._currentSearchResultIndex !== -1 ? this._searchResults[this._currentSearchResultIndex] : null;
    473         this._resetSearch();
    474         if (!this.loaded)
    475             return;
    476         this._textEditor.cancelSearchResultsHighlight();
    477         if (range)
    478             this._textEditor.setSelection(range);
    479     },
    480 
    481     /**
    482      * @return {boolean}
    483      */
    484     hasSearchResults: function()
    485     {
    486         return this._searchResults.length > 0;
    487     },
    488 
    489     jumpToFirstSearchResult: function()
    490     {
    491         this.jumpToSearchResult(0);
    492     },
    493 
    494     jumpToLastSearchResult: function()
    495     {
    496         this.jumpToSearchResult(this._searchResults.length - 1);
    497     },
    498 
    499     /**
    500      * @return {number}
    501      */
    502     _searchResultIndexForCurrentSelection: function()
    503     {
    504         return insertionIndexForObjectInListSortedByFunction(this._textEditor.selection(), this._searchResults, WebInspector.TextRange.comparator);
    505     },
    506 
    507     jumpToNextSearchResult: function()
    508     {
    509         var currentIndex = this._searchResultIndexForCurrentSelection();
    510         var nextIndex = this._currentSearchResultIndex === -1 ? currentIndex : currentIndex + 1;
    511         this.jumpToSearchResult(nextIndex);
    512     },
    513 
    514     jumpToPreviousSearchResult: function()
    515     {
    516         var currentIndex = this._searchResultIndexForCurrentSelection();
    517         this.jumpToSearchResult(currentIndex - 1);
    518     },
    519 
    520     /**
    521      * @return {boolean}
    522      */
    523     showingFirstSearchResult: function()
    524     {
    525         return this._searchResults.length &&  this._currentSearchResultIndex === 0;
    526     },
    527 
    528     /**
    529      * @return {boolean}
    530      */
    531     showingLastSearchResult: function()
    532     {
    533         return this._searchResults.length && this._currentSearchResultIndex === (this._searchResults.length - 1);
    534     },
    535 
    536     get currentSearchResultIndex()
    537     {
    538         return this._currentSearchResultIndex;
    539     },
    540 
    541     jumpToSearchResult: function(index)
    542     {
    543         if (!this.loaded || !this._searchResults.length)
    544             return;
    545         this._currentSearchResultIndex = (index + this._searchResults.length) % this._searchResults.length;
    546         if (this._currentSearchMatchChangedCallback)
    547             this._currentSearchMatchChangedCallback(this._currentSearchResultIndex);
    548         this._textEditor.highlightSearchResults(this._searchRegex, this._searchResults[this._currentSearchResultIndex]);
    549     },
    550 
    551     /**
    552      * @param {string} text
    553      */
    554     replaceSelectionWith: function(text)
    555     {
    556         var range = this._searchResults[this._currentSearchResultIndex];
    557         if (!range)
    558             return;
    559         this._textEditor.highlightSearchResults(this._searchRegex, null);
    560 
    561         this._isReplacing = true;
    562         var newRange = this._textEditor.editRange(range, text);
    563         delete this._isReplacing;
    564 
    565         this._textEditor.setSelection(newRange.collapseToEnd());
    566     },
    567 
    568     /**
    569      * @param {string} query
    570      * @param {string} replacement
    571      */
    572     replaceAllWith: function(query, replacement)
    573     {
    574         this._resetCurrentSearchResultIndex();
    575 
    576         var text = this._textEditor.text();
    577         var range = this._textEditor.range();
    578         var regex = WebInspector.SourceFrame.createSearchRegex(query, "g");
    579         if (regex.__fromRegExpQuery)
    580             text = text.replace(regex, replacement);
    581         else
    582             text = text.replace(regex, function() { return replacement; });
    583 
    584         var ranges = this._collectRegexMatches(regex);
    585         if (!ranges.length)
    586             return;
    587 
    588         // Calculate the position of the end of the last range to be edited.
    589         var currentRangeIndex = insertionIndexForObjectInListSortedByFunction(this._textEditor.selection(), ranges, WebInspector.TextRange.comparator);
    590         var lastRangeIndex = mod(currentRangeIndex - 1, ranges.length);
    591         var lastRange = ranges[lastRangeIndex];
    592         var replacementLineEndings = replacement.lineEndings();
    593         var replacementLineCount = replacementLineEndings.length;
    594         var lastLineNumber = lastRange.startLine + replacementLineEndings.length - 1;
    595         var lastColumnNumber = lastRange.startColumn;
    596         if (replacementLineEndings.length > 1)
    597             lastColumnNumber = replacementLineEndings[replacementLineCount - 1] - replacementLineEndings[replacementLineCount - 2] - 1;
    598 
    599         this._isReplacing = true;
    600         this._textEditor.editRange(range, text);
    601         this._textEditor.revealPosition(lastLineNumber, lastColumnNumber);
    602         this._textEditor.setSelection(WebInspector.TextRange.createFromLocation(lastLineNumber, lastColumnNumber));
    603         delete this._isReplacing;
    604     },
    605 
    606     _collectRegexMatches: function(regexObject)
    607     {
    608         var ranges = [];
    609         for (var i = 0; i < this._textEditor.linesCount; ++i) {
    610             var line = this._textEditor.line(i);
    611             var offset = 0;
    612             do {
    613                 var match = regexObject.exec(line);
    614                 if (match) {
    615                     if (match[0].length)
    616                         ranges.push(new WebInspector.TextRange(i, offset + match.index, i, offset + match.index + match[0].length));
    617                     offset += match.index + 1;
    618                     line = line.substring(match.index + 1);
    619                 }
    620             } while (match && line);
    621         }
    622         return ranges;
    623     },
    624 
    625     _addExistingMessagesToSource: function()
    626     {
    627         var length = this._messages.length;
    628         for (var i = 0; i < length; ++i)
    629             this.addMessageToSource(this._messages[i].line - 1, this._messages[i]);
    630     },
    631 
    632     /**
    633      * @param {number} lineNumber
    634      * @param {!WebInspector.ConsoleMessage} msg
    635      */
    636     addMessageToSource: function(lineNumber, msg)
    637     {
    638         if (lineNumber >= this._textEditor.linesCount)
    639             lineNumber = this._textEditor.linesCount - 1;
    640         if (lineNumber < 0)
    641             lineNumber = 0;
    642 
    643         var rowMessages = this._rowMessages[lineNumber];
    644         if (!rowMessages) {
    645             rowMessages = [];
    646             this._rowMessages[lineNumber] = rowMessages;
    647         }
    648 
    649         for (var i = 0; i < rowMessages.length; ++i) {
    650             if (rowMessages[i].consoleMessage.isEqual(msg)) {
    651                 rowMessages[i].repeatCount++;
    652                 this._updateMessageRepeatCount(rowMessages[i]);
    653                 return;
    654             }
    655         }
    656 
    657         var rowMessage = { consoleMessage: msg };
    658         rowMessages.push(rowMessage);
    659 
    660         this._textEditor.beginUpdates();
    661         var messageBubbleElement = this._messageBubbles[lineNumber];
    662         if (!messageBubbleElement) {
    663             messageBubbleElement = document.createElement("div");
    664             messageBubbleElement.className = "webkit-html-message-bubble";
    665             this._messageBubbles[lineNumber] = messageBubbleElement;
    666             this._textEditor.addDecoration(lineNumber, messageBubbleElement);
    667         }
    668 
    669         var imageElement = document.createElement("div");
    670         switch (msg.level) {
    671             case WebInspector.ConsoleMessage.MessageLevel.Error:
    672                 messageBubbleElement.classList.add("webkit-html-error-message");
    673                 imageElement.className = "error-icon-small";
    674                 break;
    675             case WebInspector.ConsoleMessage.MessageLevel.Warning:
    676                 messageBubbleElement.classList.add("webkit-html-warning-message");
    677                 imageElement.className = "warning-icon-small";
    678                 break;
    679         }
    680 
    681         var messageLineElement = document.createElement("div");
    682         messageLineElement.className = "webkit-html-message-line";
    683         messageBubbleElement.appendChild(messageLineElement);
    684 
    685         // Create the image element in the Inspector's document so we can use relative image URLs.
    686         messageLineElement.appendChild(imageElement);
    687         messageLineElement.appendChild(document.createTextNode(msg.messageText));
    688 
    689         rowMessage.element = messageLineElement;
    690         rowMessage.repeatCount = 1;
    691         this._updateMessageRepeatCount(rowMessage);
    692         this._textEditor.endUpdates();
    693     },
    694 
    695     _updateMessageRepeatCount: function(rowMessage)
    696     {
    697         if (rowMessage.repeatCount < 2)
    698             return;
    699 
    700         if (!rowMessage.repeatCountElement) {
    701             var repeatCountElement = document.createElement("span");
    702             rowMessage.element.appendChild(repeatCountElement);
    703             rowMessage.repeatCountElement = repeatCountElement;
    704         }
    705 
    706         rowMessage.repeatCountElement.textContent = WebInspector.UIString(" (repeated %d times)", rowMessage.repeatCount);
    707     },
    708 
    709     /**
    710      * @param {number} lineNumber
    711      * @param {!WebInspector.ConsoleMessage} msg
    712      */
    713     removeMessageFromSource: function(lineNumber, msg)
    714     {
    715         if (lineNumber >= this._textEditor.linesCount)
    716             lineNumber = this._textEditor.linesCount - 1;
    717         if (lineNumber < 0)
    718             lineNumber = 0;
    719 
    720         var rowMessages = this._rowMessages[lineNumber];
    721         for (var i = 0; rowMessages && i < rowMessages.length; ++i) {
    722             var rowMessage = rowMessages[i];
    723             if (rowMessage.consoleMessage !== msg)
    724                 continue;
    725 
    726             var messageLineElement = rowMessage.element;
    727             var messageBubbleElement = messageLineElement.parentElement;
    728             messageBubbleElement.removeChild(messageLineElement);
    729             rowMessages.remove(rowMessage);
    730             if (!rowMessages.length)
    731                 delete this._rowMessages[lineNumber];
    732             if (!messageBubbleElement.childElementCount) {
    733                 this._textEditor.removeDecoration(lineNumber, messageBubbleElement);
    734                 delete this._messageBubbles[lineNumber];
    735             }
    736             break;
    737         }
    738     },
    739 
    740     populateLineGutterContextMenu: function(contextMenu, lineNumber)
    741     {
    742     },
    743 
    744     populateTextAreaContextMenu: function(contextMenu, lineNumber)
    745     {
    746     },
    747 
    748     /**
    749      * @param {?WebInspector.TextRange} from
    750      * @param {?WebInspector.TextRange} to
    751      */
    752     onJumpToPosition: function(from, to)
    753     {
    754         this.dispatchEventToListeners(WebInspector.SourceFrame.Events.JumpHappened, {
    755             from: from,
    756             to: to
    757         });
    758     },
    759 
    760     inheritScrollPositions: function(sourceFrame)
    761     {
    762         this._textEditor.inheritScrollPositions(sourceFrame._textEditor);
    763     },
    764 
    765     /**
    766      * @return {boolean}
    767      */
    768     canEditSource: function()
    769     {
    770         return false;
    771     },
    772 
    773     /**
    774      * @param {!WebInspector.TextRange} textRange
    775      */
    776     selectionChanged: function(textRange)
    777     {
    778         this._updateSourcePosition();
    779         this.dispatchEventToListeners(WebInspector.SourceFrame.Events.SelectionChanged, textRange);
    780         WebInspector.notifications.dispatchEventToListeners(WebInspector.SourceFrame.Events.SelectionChanged, textRange);
    781     },
    782 
    783     _updateSourcePosition: function()
    784     {
    785         var selections = this._textEditor.selections();
    786         if (!selections.length)
    787             return;
    788         if (selections.length > 1) {
    789             this._sourcePosition.setText(WebInspector.UIString("%d selection regions", selections.length));
    790             return;
    791         }
    792         var textRange = selections[0];
    793         if (textRange.isEmpty()) {
    794             this._sourcePosition.setText(WebInspector.UIString("Line %d, Column %d", textRange.endLine + 1, textRange.endColumn + 1));
    795             return;
    796         }
    797         textRange = textRange.normalize();
    798 
    799         var selectedText = this._textEditor.copyRange(textRange);
    800         if (textRange.startLine === textRange.endLine)
    801             this._sourcePosition.setText(WebInspector.UIString("%d characters selected", selectedText.length));
    802         else
    803             this._sourcePosition.setText(WebInspector.UIString("%d lines, %d characters selected", textRange.endLine - textRange.startLine + 1, selectedText.length));
    804     },
    805 
    806     /**
    807      * @param {number} lineNumber
    808      */
    809     scrollChanged: function(lineNumber)
    810     {
    811         this.dispatchEventToListeners(WebInspector.SourceFrame.Events.ScrollChanged, lineNumber);
    812     },
    813 
    814     _handleKeyDown: function(e)
    815     {
    816         var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e);
    817         var handler = this._shortcuts[shortcutKey];
    818         if (handler && handler())
    819             e.consume(true);
    820     },
    821 
    822     __proto__: WebInspector.VBox.prototype
    823 }
    824 
    825 
    826 /**
    827  * @implements {WebInspector.TextEditorDelegate}
    828  * @constructor
    829  */
    830 WebInspector.TextEditorDelegateForSourceFrame = function(sourceFrame)
    831 {
    832     this._sourceFrame = sourceFrame;
    833 }
    834 
    835 WebInspector.TextEditorDelegateForSourceFrame.prototype = {
    836     onTextChanged: function(oldRange, newRange)
    837     {
    838         this._sourceFrame.onTextChanged(oldRange, newRange);
    839     },
    840 
    841     /**
    842      * @param {!WebInspector.TextRange} textRange
    843      */
    844     selectionChanged: function(textRange)
    845     {
    846         this._sourceFrame.selectionChanged(textRange);
    847     },
    848 
    849     /**
    850      * @param {number} lineNumber
    851      */
    852     scrollChanged: function(lineNumber)
    853     {
    854         this._sourceFrame.scrollChanged(lineNumber);
    855     },
    856 
    857     editorFocused: function()
    858     {
    859         this._sourceFrame._editorFocused();
    860     },
    861 
    862     populateLineGutterContextMenu: function(contextMenu, lineNumber)
    863     {
    864         this._sourceFrame.populateLineGutterContextMenu(contextMenu, lineNumber);
    865     },
    866 
    867     populateTextAreaContextMenu: function(contextMenu, lineNumber)
    868     {
    869         this._sourceFrame.populateTextAreaContextMenu(contextMenu, lineNumber);
    870     },
    871 
    872     /**
    873      * @param {string} hrefValue
    874      * @param {boolean} isExternal
    875      * @return {!Element}
    876      */
    877     createLink: function(hrefValue, isExternal)
    878     {
    879         var targetLocation = WebInspector.ParsedURL.completeURL(this._sourceFrame._url, hrefValue);
    880         return WebInspector.linkifyURLAsNode(targetLocation || hrefValue, hrefValue, undefined, isExternal);
    881     },
    882 
    883     /**
    884      * @param {?WebInspector.TextRange} from
    885      * @param {?WebInspector.TextRange} to
    886      */
    887     onJumpToPosition: function(from, to)
    888     {
    889         this._sourceFrame.onJumpToPosition(from, to);
    890     }
    891 }
    892 
    893 importScript("GoToLineDialog.js");
    894 importScript("ResourceView.js");
    895 importScript("FontView.js");
    896 importScript("ImageView.js");
    897