b6df75f289d9d271f717497d65fc562cac8d0276
[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     this._textModel = new WebInspector.TextEditorModel();
45
46     var textViewerDelegate = new WebInspector.TextViewerDelegateForSourceFrame(this);
47     this._textViewer = new WebInspector.TextViewer(this._textModel, WebInspector.platform(), this._url, textViewerDelegate);
48
49     this._currentSearchResultIndex = -1;
50     this._searchResults = [];
51
52     this._messages = [];
53     this._rowMessages = {};
54     this._messageBubbles = {};
55
56     this._textViewer.readOnly = !this.canEditSource();
57 }
58
59 WebInspector.SourceFrame.createSearchRegex = function(query)
60 {
61     var regex;
62
63     // First try creating regex if user knows the / / hint.
64     try {
65         if (/^\/.*\/$/.test(query))
66             regex = new RegExp(query.substring(1, query.length - 1));
67     } catch (e) {
68         // Silent catch.
69     }
70
71     // Otherwise just do case-insensitive search.
72     if (!regex)
73         regex = createPlainTextSearchRegex(query, "i");
74
75     return regex;
76 }
77
78 WebInspector.SourceFrame.prototype = {
79     wasShown: function()
80     {
81         this._ensureContentLoaded();
82         this._textViewer.show(this.element);
83     },
84
85     willHide: function()
86     {
87         WebInspector.View.prototype.willHide.call(this);
88         if (this.loaded)
89             this._textViewer.freeCachedElements();
90
91         this._clearLineHighlight();
92         this._clearLineToReveal();
93     },
94
95     defaultFocusedElement: function()
96     {
97         return this._textViewer.defaultFocusedElement();
98     },
99
100     get loaded()
101     {
102         return this._loaded;
103     },
104
105     hasContent: function()
106     {
107         return true;
108     },
109
110     get textViewer()
111     {
112         return this._textViewer;
113     },
114
115     _ensureContentLoaded: function()
116     {
117         if (!this._contentRequested) {
118             this._contentRequested = true;
119             this._contentProvider.requestContent(this.setContent.bind(this));
120         }
121     },
122
123     /**
124      * @param {TextDiff} diffData
125      */
126     markDiff: function(diffData)
127     {
128         if (this._diffLines && this.loaded)
129             this._removeDiffDecorations();
130
131         this._diffLines = diffData;
132         if (this.loaded)
133             this._updateDiffDecorations();
134     },
135
136     addMessage: function(msg)
137     {
138         this._messages.push(msg);
139         if (this.loaded)
140             this.addMessageToSource(msg.line - 1, msg);
141     },
142
143     clearMessages: function()
144     {
145         for (var line in this._messageBubbles) {
146             var bubble = this._messageBubbles[line];
147             bubble.parentNode.removeChild(bubble);
148         }
149
150         this._messages = [];
151         this._rowMessages = {};
152         this._messageBubbles = {};
153
154         this._textViewer.doResize();
155     },
156
157     get textModel()
158     {
159         return this._textModel;
160     },
161
162     canHighlightLine: function(line)
163     {
164         return true;
165     },
166
167     highlightLine: function(line)
168     {
169         this._clearLineToReveal();
170         if (this.loaded)
171             this._textViewer.highlightLine(line);
172         else
173             this._lineToHighlight = line;
174     },
175
176     _clearLineHighlight: function()
177     {
178         if (this.loaded)
179             this._textViewer.clearLineHighlight();
180         else
181             delete this._lineToHighlight;
182     },
183
184     revealLine: function(line)
185     {
186         this._clearLineHighlight();
187         if (this.loaded)
188             this._textViewer.revealLine(line);
189         else
190             this._lineToReveal = line;
191     },
192
193     _clearLineToReveal: function()
194     {
195         delete this._lineToReveal;
196     },
197
198     beforeTextChanged: function()
199     {
200         WebInspector.searchController.cancelSearch();
201         this.clearMessages();
202     },
203
204     afterTextChanged: function(oldRange, newRange)
205     {
206     },
207
208     /**
209      * @param {?string} content
210      * @param {boolean} contentEncoded
211      * @param {string} mimeType
212      */
213     setContent: function(content, contentEncoded, mimeType)
214     {
215         this._textViewer.mimeType = mimeType;
216
217         this._loaded = true;
218         this._textModel.setText(content || "");
219
220         this._textViewer.beginUpdates();
221
222         this._setTextViewerDecorations();
223
224         if (typeof this._lineToHighlight === "number") {
225             this.highlightLine(this._lineToHighlight);
226             delete this._lineToHighlight;
227         }
228
229         if (typeof this._lineToReveal === "number") {
230             this.revealLine(this._lineToReveal);
231             delete this._lineToReveal;
232         }
233
234         if (this._delayedFindSearchMatches) {
235             this._delayedFindSearchMatches();
236             delete this._delayedFindSearchMatches;
237         }
238
239         this.onTextViewerContentLoaded();
240
241         this._textViewer.endUpdates();
242     },
243
244     onTextViewerContentLoaded: function() {},
245
246     _setTextViewerDecorations: function()
247     {
248         this._rowMessages = {};
249         this._messageBubbles = {};
250
251         this._textViewer.beginUpdates();
252
253         this._addExistingMessagesToSource();
254         this._updateDiffDecorations();
255
256         this._textViewer.doResize();
257
258         this._textViewer.endUpdates();
259     },
260
261     performSearch: function(query, callback)
262     {
263         // Call searchCanceled since it will reset everything we need before doing a new search.
264         this.searchCanceled();
265
266         function doFindSearchMatches(query)
267         {
268             this._currentSearchResultIndex = -1;
269             this._searchResults = [];
270
271             var regex = WebInspector.SourceFrame.createSearchRegex(query);
272             this._searchResults = this._collectRegexMatches(regex);
273
274             callback(this, this._searchResults.length);
275         }
276
277         if (this.loaded)
278             doFindSearchMatches.call(this, query);
279         else
280             this._delayedFindSearchMatches = doFindSearchMatches.bind(this, query);
281
282         this._ensureContentLoaded();
283     },
284
285     searchCanceled: function()
286     {
287         delete this._delayedFindSearchMatches;
288         if (!this.loaded)
289             return;
290
291         this._currentSearchResultIndex = -1;
292         this._searchResults = [];
293         this._textViewer.markAndRevealRange(null);
294     },
295
296     hasSearchResults: function()
297     {
298         return this._searchResults.length > 0;
299     },
300
301     jumpToFirstSearchResult: function()
302     {
303         this.jumpToSearchResult(0);
304     },
305
306     jumpToLastSearchResult: function()
307     {
308         this.jumpToSearchResult(this._searchResults.length - 1);
309     },
310
311     jumpToNextSearchResult: function()
312     {
313         this.jumpToSearchResult(this._currentSearchResultIndex + 1);
314     },
315
316     jumpToPreviousSearchResult: function()
317     {
318         this.jumpToSearchResult(this._currentSearchResultIndex - 1);
319     },
320
321     showingFirstSearchResult: function()
322     {
323         return this._searchResults.length &&  this._currentSearchResultIndex === 0;
324     },
325
326     showingLastSearchResult: function()
327     {
328         return this._searchResults.length && this._currentSearchResultIndex === (this._searchResults.length - 1);
329     },
330
331     get currentSearchResultIndex()
332     {
333         return this._currentSearchResultIndex;
334     },
335
336     jumpToSearchResult: function(index)
337     {
338         if (!this.loaded || !this._searchResults.length)
339             return;
340         this._currentSearchResultIndex = (index + this._searchResults.length) % this._searchResults.length;
341         this._textViewer.markAndRevealRange(this._searchResults[this._currentSearchResultIndex]);
342     },
343
344     _collectRegexMatches: function(regexObject)
345     {
346         var ranges = [];
347         for (var i = 0; i < this._textModel.linesCount; ++i) {
348             var line = this._textModel.line(i);
349             var offset = 0;
350             do {
351                 var match = regexObject.exec(line);
352                 if (match) {
353                     if (match[0].length)
354                         ranges.push(new WebInspector.TextRange(i, offset + match.index, i, offset + match.index + match[0].length));
355                     offset += match.index + 1;
356                     line = line.substring(match.index + 1);
357                 }
358             } while (match && line);
359         }
360         return ranges;
361     },
362
363     _updateDiffDecorations: function()
364     {
365         if (!this._diffLines)
366             return;
367
368         function addDecorations(textViewer, lines, className)
369         {
370             for (var i = 0; i < lines.length; ++i)
371                 textViewer.addDecoration(lines[i], className);
372         }
373         addDecorations(this._textViewer, this._diffLines.added, "webkit-added-line");
374         addDecorations(this._textViewer, this._diffLines.removed, "webkit-removed-line");
375         addDecorations(this._textViewer, this._diffLines.changed, "webkit-changed-line");
376     },
377
378     _removeDiffDecorations: function()
379     {
380         function removeDecorations(textViewer, lines, className)
381         {
382             for (var i = 0; i < lines.length; ++i)
383                 textViewer.removeDecoration(lines[i], className);
384         }
385         removeDecorations(this._textViewer, this._diffLines.added, "webkit-added-line");
386         removeDecorations(this._textViewer, this._diffLines.removed, "webkit-removed-line");
387         removeDecorations(this._textViewer, this._diffLines.changed, "webkit-changed-line");
388     },
389
390     _addExistingMessagesToSource: function()
391     {
392         var length = this._messages.length;
393         for (var i = 0; i < length; ++i)
394             this.addMessageToSource(this._messages[i].line - 1, this._messages[i]);
395     },
396
397     addMessageToSource: function(lineNumber, msg)
398     {
399         if (lineNumber >= this._textModel.linesCount)
400             lineNumber = this._textModel.linesCount - 1;
401         if (lineNumber < 0)
402             lineNumber = 0;
403
404         var messageBubbleElement = this._messageBubbles[lineNumber];
405         if (!messageBubbleElement || messageBubbleElement.nodeType !== Node.ELEMENT_NODE || !messageBubbleElement.hasStyleClass("webkit-html-message-bubble")) {
406             messageBubbleElement = document.createElement("div");
407             messageBubbleElement.className = "webkit-html-message-bubble";
408             this._messageBubbles[lineNumber] = messageBubbleElement;
409             this._textViewer.addDecoration(lineNumber, messageBubbleElement);
410         }
411
412         var rowMessages = this._rowMessages[lineNumber];
413         if (!rowMessages) {
414             rowMessages = [];
415             this._rowMessages[lineNumber] = rowMessages;
416         }
417
418         for (var i = 0; i < rowMessages.length; ++i) {
419             if (rowMessages[i].consoleMessage.isEqual(msg)) {
420                 rowMessages[i].repeatCount = msg.totalRepeatCount;
421                 this._updateMessageRepeatCount(rowMessages[i]);
422                 return;
423             }
424         }
425
426         var rowMessage = { consoleMessage: msg };
427         rowMessages.push(rowMessage);
428
429         var imageURL;
430         switch (msg.level) {
431             case WebInspector.ConsoleMessage.MessageLevel.Error:
432                 messageBubbleElement.addStyleClass("webkit-html-error-message");
433                 imageURL = "Images/errorIcon.png";
434                 break;
435             case WebInspector.ConsoleMessage.MessageLevel.Warning:
436                 messageBubbleElement.addStyleClass("webkit-html-warning-message");
437                 imageURL = "Images/warningIcon.png";
438                 break;
439         }
440
441         var messageLineElement = document.createElement("div");
442         messageLineElement.className = "webkit-html-message-line";
443         messageBubbleElement.appendChild(messageLineElement);
444
445         // Create the image element in the Inspector's document so we can use relative image URLs.
446         var image = document.createElement("img");
447         image.src = imageURL;
448         image.className = "webkit-html-message-icon";
449         messageLineElement.appendChild(image);
450         messageLineElement.appendChild(document.createTextNode(msg.message));
451
452         rowMessage.element = messageLineElement;
453         rowMessage.repeatCount = msg.totalRepeatCount;
454         this._updateMessageRepeatCount(rowMessage);
455     },
456
457     _updateMessageRepeatCount: function(rowMessage)
458     {
459         if (rowMessage.repeatCount < 2)
460             return;
461
462         if (!rowMessage.repeatCountElement) {
463             var repeatCountElement = document.createElement("span");
464             rowMessage.element.appendChild(repeatCountElement);
465             rowMessage.repeatCountElement = repeatCountElement;
466         }
467
468         rowMessage.repeatCountElement.textContent = WebInspector.UIString(" (repeated %d times)", rowMessage.repeatCount);
469     },
470
471     removeMessageFromSource: function(lineNumber, msg)
472     {
473         if (lineNumber >= this._textModel.linesCount)
474             lineNumber = this._textModel.linesCount - 1;
475         if (lineNumber < 0)
476             lineNumber = 0;
477
478         var rowMessages = this._rowMessages[lineNumber];
479         for (var i = 0; rowMessages && i < rowMessages.length; ++i) {
480             var rowMessage = rowMessages[i];
481             if (rowMessage.consoleMessage !== msg)
482                 continue;
483
484             var messageLineElement = rowMessage.element;
485             var messageBubbleElement = messageLineElement.parentElement;
486             messageBubbleElement.removeChild(messageLineElement);
487             rowMessages.remove(rowMessage);
488             if (!rowMessages.length)
489                 delete this._rowMessages[lineNumber];
490             if (!messageBubbleElement.childElementCount) {
491                 this._textViewer.removeDecoration(lineNumber, messageBubbleElement);
492                 delete this._messageBubbles[lineNumber];
493             }
494             break;
495         }
496     },
497
498     populateLineGutterContextMenu: function(contextMenu, lineNumber)
499     {
500     },
501
502     populateTextAreaContextMenu: function(contextMenu, lineNumber)
503     {
504         if (!window.getSelection().isCollapsed)
505             return;
506         WebInspector.populateResourceContextMenu(contextMenu, this._url, lineNumber);
507     },
508
509     inheritScrollPositions: function(sourceFrame)
510     {
511         this._textViewer.inheritScrollPositions(sourceFrame._textViewer);
512     },
513
514     /**
515      * @return {boolean}
516      */
517     canEditSource: function()
518     {
519         return false;
520     },
521
522     /**
523      * @param {string} text 
524      */
525     commitEditing: function(text)
526     {
527     }
528 }
529
530 WebInspector.SourceFrame.prototype.__proto__ = WebInspector.View.prototype;
531
532
533 /**
534  * @implements {WebInspector.TextViewerDelegate}
535  * @constructor
536  */
537 WebInspector.TextViewerDelegateForSourceFrame = function(sourceFrame)
538 {
539     this._sourceFrame = sourceFrame;
540 }
541
542 WebInspector.TextViewerDelegateForSourceFrame.prototype = {
543     beforeTextChanged: function()
544     {
545         this._sourceFrame.beforeTextChanged();
546     },
547
548     afterTextChanged: function(oldRange, newRange)
549     {
550         this._sourceFrame.afterTextChanged(oldRange, newRange);
551     },
552
553     commitEditing: function()
554     {
555         this._sourceFrame.commitEditing(this._sourceFrame._textModel.text);
556     },
557
558     populateLineGutterContextMenu: function(contextMenu, lineNumber)
559     {
560         this._sourceFrame.populateLineGutterContextMenu(contextMenu, lineNumber);
561     },
562
563     populateTextAreaContextMenu: function(contextMenu, lineNumber)
564     {
565         this._sourceFrame.populateTextAreaContextMenu(contextMenu, lineNumber);
566     }
567 }
568
569 WebInspector.TextViewerDelegateForSourceFrame.prototype.__proto__ = WebInspector.TextViewerDelegate.prototype;