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 */
37WebInspector.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 */
71WebInspector.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
93WebInspector.SourceFrame.Events = {
94    ScrollChanged: "ScrollChanged",
95    SelectionChanged: "SelectionChanged",
96    JumpHappened: "JumpHappened"
97}
98
99WebInspector.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
756WebInspector.SourceFrame._iconClassPerLevel = {};
757WebInspector.SourceFrame._iconClassPerLevel[WebInspector.ConsoleMessage.MessageLevel.Error] = "error-icon-small";
758WebInspector.SourceFrame._iconClassPerLevel[WebInspector.ConsoleMessage.MessageLevel.Warning] = "warning-icon-small";
759
760WebInspector.SourceFrame._lineClassPerLevel = {};
761WebInspector.SourceFrame._lineClassPerLevel[WebInspector.ConsoleMessage.MessageLevel.Error] = "text-editor-line-with-error";
762WebInspector.SourceFrame._lineClassPerLevel[WebInspector.ConsoleMessage.MessageLevel.Warning] = "text-editor-line-with-warning";
763
764/**
765 * @constructor
766 * @param {!WebInspector.ConsoleMessage} consoleMessage
767 */
768WebInspector.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
784WebInspector.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 */
824WebInspector.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
845WebInspector.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 */
963WebInspector.TextEditorDelegateForSourceFrame = function(sourceFrame)
964{
965    this._sourceFrame = sourceFrame;
966}
967
968WebInspector.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