Upstream version 10.38.220.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / front_end / source_frame / SourceFrame.js
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
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 && !this._isReplacing)
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
568         this._isReplacing = true;
569         var newRange = this._textEditor.editRange(range, text);
570         delete this._isReplacing;
571
572         this._textEditor.setSelection(newRange.collapseToEnd());
573     },
574
575     /**
576      * @param {string} query
577      * @param {string} replacement
578      */
579     replaceAllWith: function(query, replacement)
580     {
581         this._resetCurrentSearchResultIndex();
582
583         var text = this._textEditor.text();
584         var range = this._textEditor.range();
585         var regex = WebInspector.SourceFrame.createSearchRegex(query, "g");
586         if (regex.__fromRegExpQuery)
587             text = text.replace(regex, replacement);
588         else
589             text = text.replace(regex, function() { return replacement; });
590
591         var ranges = this._collectRegexMatches(regex);
592         if (!ranges.length)
593             return;
594
595         // Calculate the position of the end of the last range to be edited.
596         var currentRangeIndex = insertionIndexForObjectInListSortedByFunction(this._textEditor.selection(), ranges, WebInspector.TextRange.comparator);
597         var lastRangeIndex = mod(currentRangeIndex - 1, ranges.length);
598         var lastRange = ranges[lastRangeIndex];
599         var replacementLineEndings = replacement.lineEndings();
600         var replacementLineCount = replacementLineEndings.length;
601         var lastLineNumber = lastRange.startLine + replacementLineEndings.length - 1;
602         var lastColumnNumber = lastRange.startColumn;
603         if (replacementLineEndings.length > 1)
604             lastColumnNumber = replacementLineEndings[replacementLineCount - 1] - replacementLineEndings[replacementLineCount - 2] - 1;
605
606         this._isReplacing = true;
607         this._textEditor.editRange(range, text);
608         this._textEditor.revealPosition(lastLineNumber, lastColumnNumber);
609         this._textEditor.setSelection(WebInspector.TextRange.createFromLocation(lastLineNumber, lastColumnNumber));
610         delete this._isReplacing;
611     },
612
613     _collectRegexMatches: function(regexObject)
614     {
615         var ranges = [];
616         for (var i = 0; i < this._textEditor.linesCount; ++i) {
617             var line = this._textEditor.line(i);
618             var offset = 0;
619             do {
620                 var match = regexObject.exec(line);
621                 if (match) {
622                     if (match[0].length)
623                         ranges.push(new WebInspector.TextRange(i, offset + match.index, i, offset + match.index + match[0].length));
624                     offset += match.index + 1;
625                     line = line.substring(match.index + 1);
626                 }
627             } while (match && line);
628         }
629         return ranges;
630     },
631
632     _addExistingMessagesToSource: function()
633     {
634         var length = this._messages.length;
635         for (var i = 0; i < length; ++i)
636             this.addMessageToSource(this._messages[i].line - 1, this._messages[i]);
637     },
638
639     /**
640      * @param {number} lineNumber
641      * @param {!WebInspector.ConsoleMessage} consoleMessage
642      */
643     addMessageToSource: function(lineNumber, consoleMessage)
644     {
645         if (lineNumber >= this._textEditor.linesCount)
646             lineNumber = this._textEditor.linesCount - 1;
647         if (lineNumber < 0)
648             lineNumber = 0;
649
650         if (!this._rowMessageBuckets[lineNumber])
651             this._rowMessageBuckets[lineNumber] = new WebInspector.SourceFrame.RowMessageBucket(this, this._textEditor, lineNumber);
652         var messageBucket = this._rowMessageBuckets[lineNumber];
653         messageBucket.addMessage(consoleMessage);
654     },
655
656     /**
657      * @param {number} lineNumber
658      * @param {!WebInspector.ConsoleMessage} msg
659      */
660     removeMessageFromSource: function(lineNumber, msg)
661     {
662         if (lineNumber >= this._textEditor.linesCount)
663             lineNumber = this._textEditor.linesCount - 1;
664         if (lineNumber < 0)
665             lineNumber = 0;
666
667         var messageBucket = this._rowMessageBuckets[lineNumber];
668         if (!messageBucket)
669             return;
670         messageBucket.removeMessage(msg);
671         if (!messageBucket.uniqueMessagesCount()) {
672             messageBucket.detachFromEditor();
673             delete this._rowMessageBuckets[lineNumber];
674         }
675     },
676
677     populateLineGutterContextMenu: function(contextMenu, lineNumber)
678     {
679     },
680
681     populateTextAreaContextMenu: function(contextMenu, lineNumber)
682     {
683     },
684
685     /**
686      * @param {?WebInspector.TextRange} from
687      * @param {?WebInspector.TextRange} to
688      */
689     onJumpToPosition: function(from, to)
690     {
691         this.dispatchEventToListeners(WebInspector.SourceFrame.Events.JumpHappened, {
692             from: from,
693             to: to
694         });
695     },
696
697     inheritScrollPositions: function(sourceFrame)
698     {
699         this._textEditor.inheritScrollPositions(sourceFrame._textEditor);
700     },
701
702     /**
703      * @return {boolean}
704      */
705     canEditSource: function()
706     {
707         return false;
708     },
709
710     /**
711      * @param {!WebInspector.TextRange} textRange
712      */
713     selectionChanged: function(textRange)
714     {
715         this._updateSourcePosition();
716         this.dispatchEventToListeners(WebInspector.SourceFrame.Events.SelectionChanged, textRange);
717         WebInspector.notifications.dispatchEventToListeners(WebInspector.SourceFrame.Events.SelectionChanged, textRange);
718     },
719
720     _updateSourcePosition: function()
721     {
722         var selections = this._textEditor.selections();
723         if (!selections.length)
724             return;
725         if (selections.length > 1) {
726             this._sourcePosition.setText(WebInspector.UIString("%d selection regions", selections.length));
727             return;
728         }
729         var textRange = selections[0];
730         if (textRange.isEmpty()) {
731             this._sourcePosition.setText(WebInspector.UIString("Line %d, Column %d", textRange.endLine + 1, textRange.endColumn + 1));
732             return;
733         }
734         textRange = textRange.normalize();
735
736         var selectedText = this._textEditor.copyRange(textRange);
737         if (textRange.startLine === textRange.endLine)
738             this._sourcePosition.setText(WebInspector.UIString("%d characters selected", selectedText.length));
739         else
740             this._sourcePosition.setText(WebInspector.UIString("%d lines, %d characters selected", textRange.endLine - textRange.startLine + 1, selectedText.length));
741     },
742
743     /**
744      * @param {number} lineNumber
745      */
746     scrollChanged: function(lineNumber)
747     {
748         this.dispatchEventToListeners(WebInspector.SourceFrame.Events.ScrollChanged, lineNumber);
749     },
750
751     _handleKeyDown: function(e)
752     {
753         var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e);
754         var handler = this._shortcuts[shortcutKey];
755         if (handler && handler())
756             e.consume(true);
757     },
758
759     __proto__: WebInspector.VBox.prototype
760 }
761
762 WebInspector.SourceFrame._iconClassPerLevel = {};
763 WebInspector.SourceFrame._iconClassPerLevel[WebInspector.ConsoleMessage.MessageLevel.Error] = "error-icon-small";
764 WebInspector.SourceFrame._iconClassPerLevel[WebInspector.ConsoleMessage.MessageLevel.Warning] = "warning-icon-small";
765
766 WebInspector.SourceFrame._lineClassPerLevel = {};
767 WebInspector.SourceFrame._lineClassPerLevel[WebInspector.ConsoleMessage.MessageLevel.Error] = "text-editor-line-with-error";
768 WebInspector.SourceFrame._lineClassPerLevel[WebInspector.ConsoleMessage.MessageLevel.Warning] = "text-editor-line-with-warning";
769
770 /**
771  * @constructor
772  * @param {!WebInspector.ConsoleMessage} consoleMessage
773  */
774 WebInspector.SourceFrame.RowMessage = function(consoleMessage)
775 {
776     this._consoleMessage = consoleMessage;
777     this._repeatCount = 1;
778     this.element = document.createElementWithClass("div", "text-editor-row-message");
779     this._icon = this.element.createChild("span", "text-editor-row-message-icon");
780     this._icon.classList.add(WebInspector.SourceFrame._iconClassPerLevel[consoleMessage.level]);
781     this._repeatCountElement = this.element.createChild("span", "bubble-repeat-count hidden error");
782     var linesContainer = this.element.createChild("div", "text-editor-row-message-lines");
783     var lines = this._consoleMessage.messageText.split("\n");
784     for (var i = 0; i < lines.length; ++i) {
785         var messageLine = linesContainer.createChild("div");
786         messageLine.textContent = lines[i];
787     }
788 }
789
790 WebInspector.SourceFrame.RowMessage.prototype = {
791     /**
792      * @return {!WebInspector.ConsoleMessage}
793      */
794     consoleMessage: function()
795     {
796         return this._consoleMessage;
797     },
798
799     /**
800      * @return {number}
801      */
802     repeatCount: function()
803     {
804         return this._repeatCount;
805     },
806
807     setRepeatCount: function(repeatCount)
808     {
809         if (this._repeatCount === repeatCount)
810             return;
811         this._repeatCount = repeatCount;
812         this._updateMessageRepeatCount();
813     },
814
815     _updateMessageRepeatCount: function()
816     {
817         this._repeatCountElement.textContent = this._repeatCount;
818         var showRepeatCount = this._repeatCount > 1;
819         this._repeatCountElement.classList.toggle("hidden", !showRepeatCount);
820         this._icon.classList.toggle("hidden", showRepeatCount);
821     }
822 }
823
824 /**
825  * @constructor
826  * @param {!WebInspector.SourceFrame} sourceFrame
827  * @param {!WebInspector.TextEditor} textEditor
828  * @param {number} lineNumber
829  */
830 WebInspector.SourceFrame.RowMessageBucket = function(sourceFrame, textEditor, lineNumber)
831 {
832     this._sourceFrame = sourceFrame;
833     this._textEditor = textEditor;
834     this._lineHandle = textEditor.textEditorPositionHandle(lineNumber, 0);
835     this._decoration = document.createElementWithClass("div", "text-editor-line-decoration");
836     this._decoration._messageBucket = this;
837     this._wave = this._decoration.createChild("div", "text-editor-line-decoration-wave");
838     this._icon = this._wave.createChild("div", "text-editor-line-decoration-icon");
839
840     this._textEditor.addDecoration(lineNumber, this._decoration);
841
842     this._messagesDescriptionElement = document.createElementWithClass("div", "text-editor-messages-description-container");
843     /** @type {!Array.<!WebInspector.SourceFrame.RowMessage>} */
844     this._messages = [];
845
846     this._updateDecorationPosition();
847
848     this._level = null;
849 }
850
851 WebInspector.SourceFrame.RowMessageBucket.prototype = {
852     _updateDecorationPosition: function()
853     {
854         if (!this._sourceFrame._isEditorShowing())
855             return;
856         var position = this._lineHandle.resolve();
857         if (!position)
858             return;
859         var lineNumber = position.lineNumber;
860         var lineText = this._textEditor.line(lineNumber);
861         var lineIndent = WebInspector.TextUtils.lineIndent(lineText).length;
862         var base = this._textEditor.cursorPositionToCoordinates(lineNumber, 0);
863         var start = this._textEditor.cursorPositionToCoordinates(lineNumber, lineIndent);
864         var end = this._textEditor.cursorPositionToCoordinates(lineNumber, lineText.length);
865         /** @const */
866         var codeMirrorLinesLeftPadding = 4;
867         this._wave.style.left = (start.x - base.x + codeMirrorLinesLeftPadding) + "px";
868         this._wave.style.width = (end.x - start.x) + "px";
869     },
870
871     /**
872      * @return {!Element}
873      */
874     messagesDescription: function()
875     {
876         this._messagesDescriptionElement.removeChildren();
877         for (var i = 0; i < this._messages.length; ++i) {
878             this._messagesDescriptionElement.appendChild(this._messages[i].element);
879         }
880         return this._messagesDescriptionElement;
881     },
882
883     detachFromEditor: function()
884     {
885         var position = this._lineHandle.resolve();
886         if (!position)
887             return;
888         var lineNumber = position.lineNumber;
889         if (this._level)
890             this._textEditor.toggleLineClass(lineNumber, WebInspector.SourceFrame._lineClassPerLevel[this._level], false);
891         this._textEditor.removeDecoration(lineNumber, this._decoration);
892     },
893
894     /**
895      * @return {number}
896      */
897     uniqueMessagesCount: function()
898     {
899         return this._messages.length;
900     },
901
902     /**
903      * @param {!WebInspector.ConsoleMessage} consoleMessage
904      */
905     addMessage: function(consoleMessage)
906     {
907         for (var i = 0; i < this._messages.length; ++i) {
908             var message = this._messages[i];
909             if (message.consoleMessage().isEqual(consoleMessage)) {
910                 message.setRepeatCount(message.repeatCount() + 1);
911                 return;
912             }
913         }
914
915         var rowMessage = new WebInspector.SourceFrame.RowMessage(consoleMessage);
916         this._messages.push(rowMessage);
917         this._updateBucketLevel();
918     },
919
920     /**
921      * @param {!WebInspector.ConsoleMessage} consoleMessage
922      */
923     removeMessage: function(consoleMessage)
924     {
925         for (var i = 0; i < this._messages.length; ++i) {
926             var rowMessage = this._messages[i];
927             if (!rowMessage.consoleMessage().isEqual(consoleMessage))
928                 continue;
929             rowMessage.setRepeatCount(rowMessage.repeatCount() - 1);
930             if (!rowMessage.repeatCount())
931                 this._messages.splice(i, 1);
932             this._updateBucketLevel();
933             return;
934         }
935     },
936
937     _updateBucketLevel: function()
938     {
939         if (!this._messages.length)
940             return;
941         var position = this._lineHandle.resolve();
942         if (!position)
943             return;
944
945         var lineNumber = position.lineNumber;
946         var maxMessage = null;
947         for (var i = 0; i < this._messages.length; ++i) {
948             var message = this._messages[i].consoleMessage();;
949             if (!maxMessage || WebInspector.ConsoleMessage.messageLevelComparator(maxMessage, message) < 0)
950                 maxMessage = message;
951         }
952
953         if (this._level) {
954             this._textEditor.toggleLineClass(lineNumber, WebInspector.SourceFrame._lineClassPerLevel[this._level], false);
955             this._icon.classList.toggle(WebInspector.SourceFrame._iconClassPerLevel[this._level], false);
956         }
957         this._level = maxMessage.level;
958         if (!this._level)
959             return;
960         this._textEditor.toggleLineClass(lineNumber, WebInspector.SourceFrame._lineClassPerLevel[this._level], true);
961         this._icon.classList.toggle(WebInspector.SourceFrame._iconClassPerLevel[this._level], true);
962     }
963 }
964
965 /**
966  * @implements {WebInspector.TextEditorDelegate}
967  * @constructor
968  */
969 WebInspector.TextEditorDelegateForSourceFrame = function(sourceFrame)
970 {
971     this._sourceFrame = sourceFrame;
972 }
973
974 WebInspector.TextEditorDelegateForSourceFrame.prototype = {
975     onTextChanged: function(oldRange, newRange)
976     {
977         this._sourceFrame.onTextChanged(oldRange, newRange);
978     },
979
980     /**
981      * @param {!WebInspector.TextRange} textRange
982      */
983     selectionChanged: function(textRange)
984     {
985         this._sourceFrame.selectionChanged(textRange);
986     },
987
988     /**
989      * @param {number} lineNumber
990      */
991     scrollChanged: function(lineNumber)
992     {
993         this._sourceFrame.scrollChanged(lineNumber);
994     },
995
996     editorFocused: function()
997     {
998         this._sourceFrame._editorFocused();
999     },
1000
1001     populateLineGutterContextMenu: function(contextMenu, lineNumber)
1002     {
1003         this._sourceFrame.populateLineGutterContextMenu(contextMenu, lineNumber);
1004     },
1005
1006     populateTextAreaContextMenu: function(contextMenu, lineNumber)
1007     {
1008         this._sourceFrame.populateTextAreaContextMenu(contextMenu, lineNumber);
1009     },
1010
1011     /**
1012      * @param {string} hrefValue
1013      * @param {boolean} isExternal
1014      * @return {!Element}
1015      */
1016     createLink: function(hrefValue, isExternal)
1017     {
1018         var targetLocation = WebInspector.ParsedURL.completeURL(this._sourceFrame._url, hrefValue);
1019         return WebInspector.linkifyURLAsNode(targetLocation || hrefValue, hrefValue, undefined, isExternal);
1020     },
1021
1022     /**
1023      * @param {?WebInspector.TextRange} from
1024      * @param {?WebInspector.TextRange} to
1025      */
1026     onJumpToPosition: function(from, to)
1027     {
1028         this._sourceFrame.onJumpToPosition(from, to);
1029     }
1030 }