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