f96f2773527b54d7acf2fa9d2b7fc16cd7e63d7b
[WebKit-https.git] / Source / WebCore / inspector / front-end / 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.View}
33  * @constructor
34  * @param {WebInspector.ContentProvider} contentProvider
35  */
36 WebInspector.SourceFrame = function(contentProvider)
37 {
38     WebInspector.View.call(this);
39     this.element.addStyleClass("script-view");
40
41     this._url = contentProvider.contentURL();
42     this._contentProvider = contentProvider;
43
44     var textEditorDelegate = new WebInspector.TextEditorDelegateForSourceFrame(this);
45
46     if (WebInspector.experimentsSettings.codemirror.isEnabled()) {
47         loadScript("CodeMirrorTextEditor.js");
48         this._textEditor = new WebInspector.CodeMirrorTextEditor(this._url, textEditorDelegate);
49     } else
50         this._textEditor = new WebInspector.DefaultTextEditor(this._url, textEditorDelegate);
51
52     this._currentSearchResultIndex = -1;
53     this._searchResults = [];
54
55     this._messages = [];
56     this._rowMessages = {};
57     this._messageBubbles = {};
58
59     this._textEditor.setReadOnly(!this.canEditSource());
60
61     this._shortcuts = {};
62     this._shortcuts[WebInspector.KeyboardShortcut.makeKey("s", WebInspector.KeyboardShortcut.Modifiers.CtrlOrMeta)] = this._commitEditing.bind(this);
63     this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false);
64
65     this._sourcePositionElement = document.createElement("div");
66     this._sourcePositionElement.className = "source-frame-cursor-position";
67 }
68
69 /**
70  * @param {string} query
71  * @param {string=} modifiers
72  */
73 WebInspector.SourceFrame.createSearchRegex = function(query, modifiers)
74 {
75     var regex;
76     modifiers = modifiers || "";
77
78     // First try creating regex if user knows the / / hint.
79     try {
80         if (/^\/.*\/$/.test(query))
81             regex = new RegExp(query.substring(1, query.length - 1), modifiers);
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 }
97
98 WebInspector.SourceFrame.prototype = {
99     wasShown: function()
100     {
101         this._ensureContentLoaded();
102         this._textEditor.show(this.element);
103         this._wasShownOrLoaded();
104     },
105
106     willHide: function()
107     {
108         WebInspector.View.prototype.willHide.call(this);
109
110         this._clearLineHighlight();
111         this._clearLineToReveal();
112     },
113
114     /**
115      * @return {?Element}
116      */
117     statusBarText: function()
118     {
119         return this._sourcePositionElement;
120     },
121
122     /**
123      * @return {Array.<Element>}
124      */
125     statusBarItems: function()
126     {
127         return [];
128     },
129
130     defaultFocusedElement: function()
131     {
132         return this._textEditor.defaultFocusedElement();
133     },
134
135     get loaded()
136     {
137         return this._loaded;
138     },
139
140     hasContent: function()
141     {
142         return true;
143     },
144
145     get textEditor()
146     {
147         return this._textEditor;
148     },
149
150     _ensureContentLoaded: function()
151     {
152         if (!this._contentRequested) {
153             this._contentRequested = true;
154             this._contentProvider.requestContent(this.setContent.bind(this));
155         }
156     },
157
158     addMessage: function(msg)
159     {
160         this._messages.push(msg);
161         if (this.loaded)
162             this.addMessageToSource(msg.line - 1, msg);
163     },
164
165     clearMessages: function()
166     {
167         for (var line in this._messageBubbles) {
168             var bubble = this._messageBubbles[line];
169             bubble.parentNode.removeChild(bubble);
170         }
171
172         this._messages = [];
173         this._rowMessages = {};
174         this._messageBubbles = {};
175
176         this._textEditor.doResize();
177     },
178
179     /**
180      * @param {number} line
181      */
182     canHighlightLine: function(line)
183     {
184         return true;
185     },
186
187     /**
188      * @param {number} line
189      */
190     highlightLine: function(line)
191     {
192         this._clearLineToReveal();
193         this._clearLineToScrollTo();
194         this._lineToHighlight = line;
195         this._innerHighlightLineIfNeeded();
196         this._textEditor.setSelection(WebInspector.TextRange.createFromLocation(line, 0));
197     },
198
199     _innerHighlightLineIfNeeded: function()
200     {
201         if (typeof this._lineToHighlight === "number") {
202             if (this.loaded && this._textEditor.isShowing()) {
203                 this._textEditor.highlightLine(this._lineToHighlight);
204                 delete this._lineToHighlight
205             }
206         }
207     },
208
209     _clearLineHighlight: function()
210     {
211         this._textEditor.clearLineHighlight();
212         delete this._lineToHighlight;
213     },
214
215     /**
216      * @param {number} line
217      */
218     revealLine: function(line)
219     {
220         this._clearLineHighlight();
221         this._clearLineToScrollTo();
222         this._lineToReveal = line;
223         this._innerRevealLineIfNeeded();
224     },
225
226     _innerRevealLineIfNeeded: function()
227     {
228         if (typeof this._lineToReveal === "number") {
229             if (this.loaded && this._textEditor.isShowing()) {
230                 this._textEditor.revealLine(this._lineToReveal);
231                 delete this._lineToReveal
232             }
233         }
234     },
235
236     _clearLineToReveal: function()
237     {
238         delete this._lineToReveal;
239     },
240
241     /**
242      * @param {number} line
243      */
244     scrollToLine: function(line)
245     {
246         this._clearLineHighlight();
247         this._clearLineToReveal();
248         this._lineToScrollTo = line;
249         this._innerScrollToLineIfNeeded();
250     },
251
252     _innerScrollToLineIfNeeded: function()
253     {
254         if (typeof this._lineToScrollTo === "number") {
255             if (this.loaded && this._textEditor.isShowing()) {
256                 this._textEditor.scrollToLine(this._lineToScrollTo);
257                 delete this._lineToScrollTo
258             }
259         }
260     },
261
262     _clearLineToScrollTo: function()
263     {
264         delete this._lineToScrollTo;
265     },
266
267     /**
268      * @param {WebInspector.TextRange} textRange
269      */
270     setSelection: function(textRange)
271     {
272         this._selectionToSet = textRange;
273         this._innerSetSelectionIfNeeded();
274     },
275
276     _innerSetSelectionIfNeeded: function()
277     {
278         if (this._selectionToSet && this.loaded && this._textEditor.isShowing()) {
279             this._textEditor.setSelection(this._selectionToSet);
280             delete this._selectionToSet;
281         }
282     },
283
284     _wasShownOrLoaded: function()
285     {
286         this._innerHighlightLineIfNeeded();
287         this._innerRevealLineIfNeeded();
288         this._innerScrollToLineIfNeeded();
289         this._innerSetSelectionIfNeeded();
290     },
291
292     onTextChanged: function(oldRange, newRange)
293     {
294         if (!this._isReplacing)
295             WebInspector.searchController.cancelSearch();
296         this.clearMessages();
297     },
298
299     /**
300      * @param {?string} content
301      * @param {boolean} contentEncoded
302      * @param {string} mimeType
303      */
304     setContent: function(content, contentEncoded, mimeType)
305     {
306         this._textEditor.mimeType = mimeType;
307
308         if (!this._loaded) {
309             this._loaded = true;
310             this._textEditor.setText(content || "");
311         } else
312             this._textEditor.editRange(this._textEditor.range(), content || "");
313
314         this._textEditor.beginUpdates();
315
316         this._setTextEditorDecorations();
317
318         this._wasShownOrLoaded();
319
320         if (this._delayedFindSearchMatches) {
321             this._delayedFindSearchMatches();
322             delete this._delayedFindSearchMatches;
323         }
324
325         this.onTextEditorContentLoaded();
326
327         this._textEditor.endUpdates();
328     },
329
330     onTextEditorContentLoaded: function() {},
331
332     _setTextEditorDecorations: function()
333     {
334         this._rowMessages = {};
335         this._messageBubbles = {};
336
337         this._textEditor.beginUpdates();
338
339         this._addExistingMessagesToSource();
340
341         this._textEditor.doResize();
342
343         this._textEditor.endUpdates();
344     },
345
346     /**
347      * @param {string} query
348      * @param {function(WebInspector.View, number)} callback
349      */
350     performSearch: function(query, callback)
351     {
352         // Call searchCanceled since it will reset everything we need before doing a new search.
353         this.searchCanceled();
354
355         function doFindSearchMatches(query)
356         {
357             this._currentSearchResultIndex = -1;
358             this._searchResults = [];
359
360             var regex = WebInspector.SourceFrame.createSearchRegex(query);
361             this._searchResults = this._collectRegexMatches(regex);
362             var shiftToIndex = 0;
363             var selection = this._textEditor.lastSelection();
364             for (var i = 0; selection && i < this._searchResults.length; ++i) {
365                 if (this._searchResults[i].compareTo(selection) >= 0) {
366                     shiftToIndex = i;
367                     break;
368                 }
369             }
370
371             if (shiftToIndex)
372                 this._searchResults = this._searchResults.rotate(shiftToIndex);
373
374             callback(this, this._searchResults.length);
375         }
376
377         if (this.loaded)
378             doFindSearchMatches.call(this, query);
379         else
380             this._delayedFindSearchMatches = doFindSearchMatches.bind(this, query);
381
382         this._ensureContentLoaded();
383     },
384
385     searchCanceled: function()
386     {
387         delete this._delayedFindSearchMatches;
388         if (!this.loaded)
389             return;
390
391         this._currentSearchResultIndex = -1;
392         this._searchResults = [];
393         this._textEditor.markAndRevealRange(null);
394     },
395
396     hasSearchResults: function()
397     {
398         return this._searchResults.length > 0;
399     },
400
401     jumpToFirstSearchResult: function()
402     {
403         this.jumpToSearchResult(0);
404     },
405
406     jumpToLastSearchResult: function()
407     {
408         this.jumpToSearchResult(this._searchResults.length - 1);
409     },
410
411     jumpToNextSearchResult: function()
412     {
413         this.jumpToSearchResult(this._currentSearchResultIndex + 1);
414     },
415
416     jumpToPreviousSearchResult: function()
417     {
418         this.jumpToSearchResult(this._currentSearchResultIndex - 1);
419     },
420
421     showingFirstSearchResult: function()
422     {
423         return this._searchResults.length &&  this._currentSearchResultIndex === 0;
424     },
425
426     showingLastSearchResult: function()
427     {
428         return this._searchResults.length && this._currentSearchResultIndex === (this._searchResults.length - 1);
429     },
430
431     get currentSearchResultIndex()
432     {
433         return this._currentSearchResultIndex;
434     },
435
436     jumpToSearchResult: function(index)
437     {
438         if (!this.loaded || !this._searchResults.length)
439             return;
440         this._currentSearchResultIndex = (index + this._searchResults.length) % this._searchResults.length;
441         this._textEditor.markAndRevealRange(this._searchResults[this._currentSearchResultIndex]);
442     },
443
444     /**
445      * @param {string} text
446      */
447     replaceSearchMatchWith: function(text)
448     {
449         var range = this._searchResults[this._currentSearchResultIndex];
450         if (!range)
451             return;
452         this._textEditor.markAndRevealRange(null);
453
454         this._isReplacing = true;
455         var newRange = this._textEditor.editRange(range, text);
456         delete this._isReplacing;
457
458         this._textEditor.setSelection(newRange.collapseToEnd());
459     },
460
461     /**
462      * @param {string} query
463      * @param {string} replacement
464      */
465     replaceAllWith: function(query, replacement)
466     {
467         this._textEditor.markAndRevealRange(null);
468
469         var text = this._textEditor.text();
470         var range = this._textEditor.range();
471         text = text.replace(WebInspector.SourceFrame.createSearchRegex(query, "g"), replacement);
472
473         this._isReplacing = true;
474         this._textEditor.editRange(range, text);
475         delete this._isReplacing;
476     },
477
478     _collectRegexMatches: function(regexObject)
479     {
480         var ranges = [];
481         for (var i = 0; i < this._textEditor.linesCount; ++i) {
482             var line = this._textEditor.line(i);
483             var offset = 0;
484             do {
485                 var match = regexObject.exec(line);
486                 if (match) {
487                     if (match[0].length)
488                         ranges.push(new WebInspector.TextRange(i, offset + match.index, i, offset + match.index + match[0].length));
489                     offset += match.index + 1;
490                     line = line.substring(match.index + 1);
491                 }
492             } while (match && line);
493         }
494         return ranges;
495     },
496
497     _addExistingMessagesToSource: function()
498     {
499         var length = this._messages.length;
500         for (var i = 0; i < length; ++i)
501             this.addMessageToSource(this._messages[i].line - 1, this._messages[i]);
502     },
503
504     addMessageToSource: function(lineNumber, msg)
505     {
506         if (lineNumber >= this._textEditor.linesCount)
507             lineNumber = this._textEditor.linesCount - 1;
508         if (lineNumber < 0)
509             lineNumber = 0;
510
511         var messageBubbleElement = this._messageBubbles[lineNumber];
512         if (!messageBubbleElement || messageBubbleElement.nodeType !== Node.ELEMENT_NODE || !messageBubbleElement.hasStyleClass("webkit-html-message-bubble")) {
513             messageBubbleElement = document.createElement("div");
514             messageBubbleElement.className = "webkit-html-message-bubble";
515             this._messageBubbles[lineNumber] = messageBubbleElement;
516             this._textEditor.addDecoration(lineNumber, messageBubbleElement);
517         }
518
519         var rowMessages = this._rowMessages[lineNumber];
520         if (!rowMessages) {
521             rowMessages = [];
522             this._rowMessages[lineNumber] = rowMessages;
523         }
524
525         for (var i = 0; i < rowMessages.length; ++i) {
526             if (rowMessages[i].consoleMessage.isEqual(msg)) {
527                 rowMessages[i].repeatCount = msg.totalRepeatCount;
528                 this._updateMessageRepeatCount(rowMessages[i]);
529                 return;
530             }
531         }
532
533         var rowMessage = { consoleMessage: msg };
534         rowMessages.push(rowMessage);
535
536         var imageURL;
537         switch (msg.level) {
538             case WebInspector.ConsoleMessage.MessageLevel.Error:
539                 messageBubbleElement.addStyleClass("webkit-html-error-message");
540                 imageURL = "Images/errorIcon.png";
541                 break;
542             case WebInspector.ConsoleMessage.MessageLevel.Warning:
543                 messageBubbleElement.addStyleClass("webkit-html-warning-message");
544                 imageURL = "Images/warningIcon.png";
545                 break;
546         }
547
548         var messageLineElement = document.createElement("div");
549         messageLineElement.className = "webkit-html-message-line";
550         messageBubbleElement.appendChild(messageLineElement);
551
552         // Create the image element in the Inspector's document so we can use relative image URLs.
553         var image = document.createElement("img");
554         image.src = imageURL;
555         image.className = "webkit-html-message-icon";
556         messageLineElement.appendChild(image);
557         messageLineElement.appendChild(document.createTextNode(msg.message));
558
559         rowMessage.element = messageLineElement;
560         rowMessage.repeatCount = msg.totalRepeatCount;
561         this._updateMessageRepeatCount(rowMessage);
562     },
563
564     _updateMessageRepeatCount: function(rowMessage)
565     {
566         if (rowMessage.repeatCount < 2)
567             return;
568
569         if (!rowMessage.repeatCountElement) {
570             var repeatCountElement = document.createElement("span");
571             rowMessage.element.appendChild(repeatCountElement);
572             rowMessage.repeatCountElement = repeatCountElement;
573         }
574
575         rowMessage.repeatCountElement.textContent = WebInspector.UIString(" (repeated %d times)", rowMessage.repeatCount);
576     },
577
578     removeMessageFromSource: function(lineNumber, msg)
579     {
580         if (lineNumber >= this._textEditor.linesCount)
581             lineNumber = this._textEditor.linesCount - 1;
582         if (lineNumber < 0)
583             lineNumber = 0;
584
585         var rowMessages = this._rowMessages[lineNumber];
586         for (var i = 0; rowMessages && i < rowMessages.length; ++i) {
587             var rowMessage = rowMessages[i];
588             if (rowMessage.consoleMessage !== msg)
589                 continue;
590
591             var messageLineElement = rowMessage.element;
592             var messageBubbleElement = messageLineElement.parentElement;
593             messageBubbleElement.removeChild(messageLineElement);
594             rowMessages.remove(rowMessage);
595             if (!rowMessages.length)
596                 delete this._rowMessages[lineNumber];
597             if (!messageBubbleElement.childElementCount) {
598                 this._textEditor.removeDecoration(lineNumber, messageBubbleElement);
599                 delete this._messageBubbles[lineNumber];
600             }
601             break;
602         }
603     },
604
605     populateLineGutterContextMenu: function(contextMenu, lineNumber)
606     {
607     },
608
609     populateTextAreaContextMenu: function(contextMenu, lineNumber)
610     {
611     },
612
613     inheritScrollPositions: function(sourceFrame)
614     {
615         this._textEditor.inheritScrollPositions(sourceFrame._textEditor);
616     },
617
618     /**
619      * @return {boolean}
620      */
621     canEditSource: function()
622     {
623         return false;
624     },
625
626     /**
627      * @param {string} text 
628      */
629     commitEditing: function(text)
630     {
631     },
632
633     /**
634      * @param {WebInspector.TextRange} textRange
635      */
636     selectionChanged: function(textRange)
637     {
638         this._updateSourcePosition(textRange);
639         this.dispatchEventToListeners(WebInspector.SourceFrame.Events.SelectionChanged, textRange);
640     },
641
642     /**
643      * @param {WebInspector.TextRange} textRange
644      */
645     _updateSourcePosition: function(textRange)
646     {
647         if (!textRange)
648             return;
649
650         if (textRange.isEmpty()) {
651             this._sourcePositionElement.textContent = WebInspector.UIString("Line %d, Column %d", textRange.endLine + 1, textRange.endColumn + 1);
652             return;
653         }
654         textRange = textRange.normalize();
655
656         var selectedText = this._textEditor.copyRange(textRange);
657         if (textRange.startLine === textRange.endLine)
658             this._sourcePositionElement.textContent = WebInspector.UIString("%d characters selected", selectedText.length);
659         else
660             this._sourcePositionElement.textContent = WebInspector.UIString("%d lines, %d characters selected", textRange.endLine - textRange.startLine + 1, selectedText.length);
661     },
662
663     /**
664      * @param {number} lineNumber
665      */
666     scrollChanged: function(lineNumber)
667     {
668         this.dispatchEventToListeners(WebInspector.SourceFrame.Events.ScrollChanged, lineNumber);
669     },
670
671     _handleKeyDown: function(e)
672     {
673         var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e);
674         var handler = this._shortcuts[shortcutKey];
675         if (handler && handler())
676             e.consume(true);
677     },
678
679     _commitEditing: function()
680     {
681         if (this._textEditor.readOnly())
682             return false;
683
684         var content = this._textEditor.text();
685         this.commitEditing(content);
686         return true;
687     },
688
689     __proto__: WebInspector.View.prototype
690 }
691
692
693 /**
694  * @implements {WebInspector.TextEditorDelegate}
695  * @constructor
696  */
697 WebInspector.TextEditorDelegateForSourceFrame = function(sourceFrame)
698 {
699     this._sourceFrame = sourceFrame;
700 }
701
702 WebInspector.TextEditorDelegateForSourceFrame.prototype = {
703     onTextChanged: function(oldRange, newRange)
704     {
705         this._sourceFrame.onTextChanged(oldRange, newRange);
706     },
707
708     /**
709      * @param {WebInspector.TextRange} textRange
710      */
711     selectionChanged: function(textRange)
712     {
713         this._sourceFrame.selectionChanged(textRange);
714     },
715
716     /**
717      * @param {number} lineNumber
718      */
719     scrollChanged: function(lineNumber)
720     {
721         this._sourceFrame.scrollChanged(lineNumber);
722     },
723
724     populateLineGutterContextMenu: function(contextMenu, lineNumber)
725     {
726         this._sourceFrame.populateLineGutterContextMenu(contextMenu, lineNumber);
727     },
728
729     populateTextAreaContextMenu: function(contextMenu, lineNumber)
730     {
731         this._sourceFrame.populateTextAreaContextMenu(contextMenu, lineNumber);
732     },
733
734     /**
735      * @param {string} hrefValue
736      * @param {boolean} isExternal
737      * @return {Element}
738      */
739     createLink: function(hrefValue, isExternal)
740     {
741         var targetLocation = WebInspector.ParsedURL.completeURL(this._sourceFrame._url, hrefValue);
742         return WebInspector.linkifyURLAsNode(targetLocation || hrefValue, hrefValue, undefined, isExternal);
743     },
744
745     __proto__: WebInspector.TextEditorDelegate.prototype
746 }