Web Inspector: move sources panel out of experimental.
[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     addMessage: function(msg)
124     {
125         this._messages.push(msg);
126         if (this.loaded)
127             this.addMessageToSource(msg.line - 1, msg);
128     },
129
130     clearMessages: function()
131     {
132         for (var line in this._messageBubbles) {
133             var bubble = this._messageBubbles[line];
134             bubble.parentNode.removeChild(bubble);
135         }
136
137         this._messages = [];
138         this._rowMessages = {};
139         this._messageBubbles = {};
140
141         this._textViewer.doResize();
142     },
143
144     get textModel()
145     {
146         return this._textModel;
147     },
148
149     canHighlightLine: function(line)
150     {
151         return true;
152     },
153
154     highlightLine: function(line)
155     {
156         this._clearLineToReveal();
157         if (this.loaded)
158             this._textViewer.highlightLine(line);
159         else
160             this._lineToHighlight = line;
161     },
162
163     _clearLineHighlight: function()
164     {
165         if (this.loaded)
166             this._textViewer.clearLineHighlight();
167         else
168             delete this._lineToHighlight;
169     },
170
171     revealLine: function(line)
172     {
173         this._clearLineHighlight();
174         if (this.loaded)
175             this._textViewer.revealLine(line);
176         else
177             this._lineToReveal = line;
178     },
179
180     _clearLineToReveal: function()
181     {
182         delete this._lineToReveal;
183     },
184
185     beforeTextChanged: function()
186     {
187         WebInspector.searchController.cancelSearch();
188         this.clearMessages();
189     },
190
191     afterTextChanged: function(oldRange, newRange)
192     {
193     },
194
195     /**
196      * @param {?string} content
197      * @param {boolean} contentEncoded
198      * @param {string} mimeType
199      */
200     setContent: function(content, contentEncoded, mimeType)
201     {
202         this._textViewer.mimeType = mimeType;
203
204         this._loaded = true;
205         this._textModel.setText(content || "");
206
207         this._textViewer.beginUpdates();
208
209         this._setTextViewerDecorations();
210
211         if (typeof this._lineToHighlight === "number") {
212             this.highlightLine(this._lineToHighlight);
213             delete this._lineToHighlight;
214         }
215
216         if (typeof this._lineToReveal === "number") {
217             this.revealLine(this._lineToReveal);
218             delete this._lineToReveal;
219         }
220
221         if (this._delayedFindSearchMatches) {
222             this._delayedFindSearchMatches();
223             delete this._delayedFindSearchMatches;
224         }
225
226         this.onTextViewerContentLoaded();
227
228         this._textViewer.endUpdates();
229     },
230
231     onTextViewerContentLoaded: function() {},
232
233     _setTextViewerDecorations: function()
234     {
235         this._rowMessages = {};
236         this._messageBubbles = {};
237
238         this._textViewer.beginUpdates();
239
240         this._addExistingMessagesToSource();
241
242         this._textViewer.doResize();
243
244         this._textViewer.endUpdates();
245     },
246
247     performSearch: function(query, callback)
248     {
249         // Call searchCanceled since it will reset everything we need before doing a new search.
250         this.searchCanceled();
251
252         function doFindSearchMatches(query)
253         {
254             this._currentSearchResultIndex = -1;
255             this._searchResults = [];
256
257             var regex = WebInspector.SourceFrame.createSearchRegex(query);
258             this._searchResults = this._collectRegexMatches(regex);
259
260             callback(this, this._searchResults.length);
261         }
262
263         if (this.loaded)
264             doFindSearchMatches.call(this, query);
265         else
266             this._delayedFindSearchMatches = doFindSearchMatches.bind(this, query);
267
268         this._ensureContentLoaded();
269     },
270
271     searchCanceled: function()
272     {
273         delete this._delayedFindSearchMatches;
274         if (!this.loaded)
275             return;
276
277         this._currentSearchResultIndex = -1;
278         this._searchResults = [];
279         this._textViewer.markAndRevealRange(null);
280     },
281
282     hasSearchResults: function()
283     {
284         return this._searchResults.length > 0;
285     },
286
287     jumpToFirstSearchResult: function()
288     {
289         this.jumpToSearchResult(0);
290     },
291
292     jumpToLastSearchResult: function()
293     {
294         this.jumpToSearchResult(this._searchResults.length - 1);
295     },
296
297     jumpToNextSearchResult: function()
298     {
299         this.jumpToSearchResult(this._currentSearchResultIndex + 1);
300     },
301
302     jumpToPreviousSearchResult: function()
303     {
304         this.jumpToSearchResult(this._currentSearchResultIndex - 1);
305     },
306
307     showingFirstSearchResult: function()
308     {
309         return this._searchResults.length &&  this._currentSearchResultIndex === 0;
310     },
311
312     showingLastSearchResult: function()
313     {
314         return this._searchResults.length && this._currentSearchResultIndex === (this._searchResults.length - 1);
315     },
316
317     get currentSearchResultIndex()
318     {
319         return this._currentSearchResultIndex;
320     },
321
322     jumpToSearchResult: function(index)
323     {
324         if (!this.loaded || !this._searchResults.length)
325             return;
326         this._currentSearchResultIndex = (index + this._searchResults.length) % this._searchResults.length;
327         this._textViewer.markAndRevealRange(this._searchResults[this._currentSearchResultIndex]);
328     },
329
330     _collectRegexMatches: function(regexObject)
331     {
332         var ranges = [];
333         for (var i = 0; i < this._textModel.linesCount; ++i) {
334             var line = this._textModel.line(i);
335             var offset = 0;
336             do {
337                 var match = regexObject.exec(line);
338                 if (match) {
339                     if (match[0].length)
340                         ranges.push(new WebInspector.TextRange(i, offset + match.index, i, offset + match.index + match[0].length));
341                     offset += match.index + 1;
342                     line = line.substring(match.index + 1);
343                 }
344             } while (match && line);
345         }
346         return ranges;
347     },
348
349     _addExistingMessagesToSource: function()
350     {
351         var length = this._messages.length;
352         for (var i = 0; i < length; ++i)
353             this.addMessageToSource(this._messages[i].line - 1, this._messages[i]);
354     },
355
356     addMessageToSource: function(lineNumber, msg)
357     {
358         if (lineNumber >= this._textModel.linesCount)
359             lineNumber = this._textModel.linesCount - 1;
360         if (lineNumber < 0)
361             lineNumber = 0;
362
363         var messageBubbleElement = this._messageBubbles[lineNumber];
364         if (!messageBubbleElement || messageBubbleElement.nodeType !== Node.ELEMENT_NODE || !messageBubbleElement.hasStyleClass("webkit-html-message-bubble")) {
365             messageBubbleElement = document.createElement("div");
366             messageBubbleElement.className = "webkit-html-message-bubble";
367             this._messageBubbles[lineNumber] = messageBubbleElement;
368             this._textViewer.addDecoration(lineNumber, messageBubbleElement);
369         }
370
371         var rowMessages = this._rowMessages[lineNumber];
372         if (!rowMessages) {
373             rowMessages = [];
374             this._rowMessages[lineNumber] = rowMessages;
375         }
376
377         for (var i = 0; i < rowMessages.length; ++i) {
378             if (rowMessages[i].consoleMessage.isEqual(msg)) {
379                 rowMessages[i].repeatCount = msg.totalRepeatCount;
380                 this._updateMessageRepeatCount(rowMessages[i]);
381                 return;
382             }
383         }
384
385         var rowMessage = { consoleMessage: msg };
386         rowMessages.push(rowMessage);
387
388         var imageURL;
389         switch (msg.level) {
390             case WebInspector.ConsoleMessage.MessageLevel.Error:
391                 messageBubbleElement.addStyleClass("webkit-html-error-message");
392                 imageURL = "Images/errorIcon.png";
393                 break;
394             case WebInspector.ConsoleMessage.MessageLevel.Warning:
395                 messageBubbleElement.addStyleClass("webkit-html-warning-message");
396                 imageURL = "Images/warningIcon.png";
397                 break;
398         }
399
400         var messageLineElement = document.createElement("div");
401         messageLineElement.className = "webkit-html-message-line";
402         messageBubbleElement.appendChild(messageLineElement);
403
404         // Create the image element in the Inspector's document so we can use relative image URLs.
405         var image = document.createElement("img");
406         image.src = imageURL;
407         image.className = "webkit-html-message-icon";
408         messageLineElement.appendChild(image);
409         messageLineElement.appendChild(document.createTextNode(msg.message));
410
411         rowMessage.element = messageLineElement;
412         rowMessage.repeatCount = msg.totalRepeatCount;
413         this._updateMessageRepeatCount(rowMessage);
414     },
415
416     _updateMessageRepeatCount: function(rowMessage)
417     {
418         if (rowMessage.repeatCount < 2)
419             return;
420
421         if (!rowMessage.repeatCountElement) {
422             var repeatCountElement = document.createElement("span");
423             rowMessage.element.appendChild(repeatCountElement);
424             rowMessage.repeatCountElement = repeatCountElement;
425         }
426
427         rowMessage.repeatCountElement.textContent = WebInspector.UIString(" (repeated %d times)", rowMessage.repeatCount);
428     },
429
430     removeMessageFromSource: function(lineNumber, msg)
431     {
432         if (lineNumber >= this._textModel.linesCount)
433             lineNumber = this._textModel.linesCount - 1;
434         if (lineNumber < 0)
435             lineNumber = 0;
436
437         var rowMessages = this._rowMessages[lineNumber];
438         for (var i = 0; rowMessages && i < rowMessages.length; ++i) {
439             var rowMessage = rowMessages[i];
440             if (rowMessage.consoleMessage !== msg)
441                 continue;
442
443             var messageLineElement = rowMessage.element;
444             var messageBubbleElement = messageLineElement.parentElement;
445             messageBubbleElement.removeChild(messageLineElement);
446             rowMessages.remove(rowMessage);
447             if (!rowMessages.length)
448                 delete this._rowMessages[lineNumber];
449             if (!messageBubbleElement.childElementCount) {
450                 this._textViewer.removeDecoration(lineNumber, messageBubbleElement);
451                 delete this._messageBubbles[lineNumber];
452             }
453             break;
454         }
455     },
456
457     populateLineGutterContextMenu: function(contextMenu, lineNumber)
458     {
459     },
460
461     populateTextAreaContextMenu: function(contextMenu, lineNumber)
462     {
463         if (!window.getSelection().isCollapsed)
464             return;
465         WebInspector.populateResourceContextMenu(contextMenu, this._url, lineNumber);
466     },
467
468     inheritScrollPositions: function(sourceFrame)
469     {
470         this._textViewer.inheritScrollPositions(sourceFrame._textViewer);
471     },
472
473     /**
474      * @return {boolean}
475      */
476     canEditSource: function()
477     {
478         return false;
479     },
480
481     /**
482      * @param {string} text 
483      */
484     commitEditing: function(text)
485     {
486     }
487 }
488
489 WebInspector.SourceFrame.prototype.__proto__ = WebInspector.View.prototype;
490
491
492 /**
493  * @implements {WebInspector.TextViewerDelegate}
494  * @constructor
495  */
496 WebInspector.TextViewerDelegateForSourceFrame = function(sourceFrame)
497 {
498     this._sourceFrame = sourceFrame;
499 }
500
501 WebInspector.TextViewerDelegateForSourceFrame.prototype = {
502     beforeTextChanged: function()
503     {
504         this._sourceFrame.beforeTextChanged();
505     },
506
507     afterTextChanged: function(oldRange, newRange)
508     {
509         this._sourceFrame.afterTextChanged(oldRange, newRange);
510     },
511
512     commitEditing: function()
513     {
514         this._sourceFrame.commitEditing(this._sourceFrame._textModel.text);
515     },
516
517     populateLineGutterContextMenu: function(contextMenu, lineNumber)
518     {
519         this._sourceFrame.populateLineGutterContextMenu(contextMenu, lineNumber);
520     },
521
522     populateTextAreaContextMenu: function(contextMenu, lineNumber)
523     {
524         this._sourceFrame.populateTextAreaContextMenu(contextMenu, lineNumber);
525     }
526 }
527
528 WebInspector.TextViewerDelegateForSourceFrame.prototype.__proto__ = WebInspector.TextViewerDelegate.prototype;