2 * Copyright (C) 2011 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
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
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.
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.
32 * @extends {WebInspector.View}
35 WebInspector.SourceFrame = function(url)
37 WebInspector.View.call(this);
38 this.element.addStyleClass("script-view");
42 this._textModel = new WebInspector.TextEditorModel();
44 var textViewerDelegate = new WebInspector.TextViewerDelegateForSourceFrame(this);
45 this._textViewer = new WebInspector.TextViewer(this._textModel, WebInspector.platform(), this._url, textViewerDelegate);
47 this._editButton = new WebInspector.StatusBarButton(WebInspector.UIString("Edit"), "edit-source-status-bar-item");
48 this._editButton.addEventListener("click", this._editButtonClicked.bind(this), this);
50 this._currentSearchResultIndex = -1;
51 this._searchResults = [];
54 this._rowMessages = {};
55 this._messageBubbles = {};
58 WebInspector.SourceFrame.Events = {
62 WebInspector.SourceFrame.createSearchRegex = function(query)
66 // First try creating regex if user knows the / / hint.
68 if (/^\/.*\/$/.test(query))
69 regex = new RegExp(query.substring(1, query.length - 1));
74 // Otherwise just do case-insensitive search.
76 regex = createPlainTextSearchRegex(query, "i");
81 WebInspector.SourceFrame.prototype = {
84 this._ensureContentLoaded();
85 this._textViewer.show(this.element);
86 if (this._wasHiddenWhileEditing)
87 this.setReadOnly(false);
92 WebInspector.View.prototype.willHide.call(this);
94 this._textViewer.freeCachedElements();
96 this._clearLineHighlight();
97 if (!this._textViewer.readOnly)
98 this._wasHiddenWhileEditing = true;
99 this.setReadOnly(true);
104 this._textViewer.focus();
109 return [this._editButton.element];
117 hasContent: function()
124 return this._textViewer;
127 _ensureContentLoaded: function()
129 if (!this._contentRequested) {
130 this._contentRequested = true;
131 this.requestContent(this.setContent.bind(this));
135 requestContent: function(callback)
140 * @param {TextDiff} diffData
142 markDiff: function(diffData)
144 if (this._diffLines && this.loaded)
145 this._removeDiffDecorations();
147 this._diffLines = diffData;
149 this._updateDiffDecorations();
152 addMessage: function(msg)
154 this._messages.push(msg);
156 this.addMessageToSource(msg.line - 1, msg);
159 clearMessages: function()
161 for (var line in this._messageBubbles) {
162 var bubble = this._messageBubbles[line];
163 bubble.parentNode.removeChild(bubble);
167 this._rowMessages = {};
168 this._messageBubbles = {};
170 this._textViewer.doResize();
175 return this._textModel;
178 canHighlightLine: function(line)
183 highlightLine: function(line)
186 this._textViewer.highlightLine(line);
188 this._lineToHighlight = line;
191 _clearLineHighlight: function()
194 this._textViewer.clearLineHighlight();
196 delete this._lineToHighlight;
199 _saveViewerState: function()
201 this._viewerState = {
202 textModelContent: this._textModel.text,
203 messages: this._messages,
204 diffLines: this._diffLines,
208 _restoreViewerState: function()
210 if (!this._viewerState)
212 this._textModel.setText(null, this._viewerState.textModelContent);
214 this._messages = this._viewerState.messages;
215 this._diffLines = this._viewerState.diffLines;
216 this._setTextViewerDecorations();
218 delete this._viewerState;
221 beforeTextChanged: function()
223 if (!this._viewerState)
224 this._saveViewerState();
226 WebInspector.searchController.cancelSearch();
227 this.clearMessages();
230 afterTextChanged: function(oldRange, newRange)
234 setContent: function(mimeType, content)
236 this._textViewer.mimeType = mimeType;
239 this._textModel.setText(null, content);
241 this._textViewer.beginUpdates();
243 this._setTextViewerDecorations();
245 if (typeof this._lineToHighlight === "number") {
246 this.highlightLine(this._lineToHighlight);
247 delete this._lineToHighlight;
250 if (this._delayedFindSearchMatches) {
251 this._delayedFindSearchMatches();
252 delete this._delayedFindSearchMatches;
255 this.dispatchEventToListeners(WebInspector.SourceFrame.Events.Loaded);
257 this._textViewer.endUpdates();
259 if (!this.canEditSource())
260 this._editButton.disabled = true;
263 _setTextViewerDecorations: function()
265 this._rowMessages = {};
266 this._messageBubbles = {};
268 this._textViewer.beginUpdates();
270 this._addExistingMessagesToSource();
271 this._updateDiffDecorations();
273 this._textViewer.doResize();
275 this._textViewer.endUpdates();
278 performSearch: function(query, callback)
280 // Call searchCanceled since it will reset everything we need before doing a new search.
281 this.searchCanceled();
283 function doFindSearchMatches(query)
285 this._currentSearchResultIndex = -1;
286 this._searchResults = [];
288 var regex = WebInspector.SourceFrame.createSearchRegex(query);
289 this._searchResults = this._collectRegexMatches(regex);
291 callback(this, this._searchResults.length);
295 doFindSearchMatches.call(this, query);
297 this._delayedFindSearchMatches = doFindSearchMatches.bind(this, query);
299 this._ensureContentLoaded();
302 searchCanceled: function()
304 delete this._delayedFindSearchMatches;
308 this._currentSearchResultIndex = -1;
309 this._searchResults = [];
310 this._textViewer.markAndRevealRange(null);
313 hasSearchResults: function()
315 return this._searchResults.length > 0;
318 jumpToFirstSearchResult: function()
320 this.jumpToSearchResult(0);
323 jumpToLastSearchResult: function()
325 this.jumpToSearchResult(this._searchResults.length - 1);
328 jumpToNextSearchResult: function()
330 this.jumpToSearchResult(this._currentSearchResultIndex + 1);
333 jumpToPreviousSearchResult: function()
335 this.jumpToSearchResult(this._currentSearchResultIndex - 1);
338 showingFirstSearchResult: function()
340 return this._searchResults.length && this._currentSearchResultIndex === 0;
343 showingLastSearchResult: function()
345 return this._searchResults.length && this._currentSearchResultIndex === (this._searchResults.length - 1);
348 get currentSearchResultIndex()
350 return this._currentSearchResultIndex;
353 jumpToSearchResult: function(index)
355 if (!this.loaded || !this._searchResults.length)
357 this._currentSearchResultIndex = (index + this._searchResults.length) % this._searchResults.length;
358 this._textViewer.markAndRevealRange(this._searchResults[this._currentSearchResultIndex]);
361 _collectRegexMatches: function(regexObject)
364 for (var i = 0; i < this._textModel.linesCount; ++i) {
365 var line = this._textModel.line(i);
368 var match = regexObject.exec(line);
371 ranges.push(new WebInspector.TextRange(i, offset + match.index, i, offset + match.index + match[0].length));
372 offset += match.index + 1;
373 line = line.substring(match.index + 1);
375 } while (match && line);
380 _updateDiffDecorations: function()
382 if (!this._diffLines)
385 function addDecorations(textViewer, lines, className)
387 for (var i = 0; i < lines.length; ++i)
388 textViewer.addDecoration(lines[i], className);
390 addDecorations(this._textViewer, this._diffLines.added, "webkit-added-line");
391 addDecorations(this._textViewer, this._diffLines.removed, "webkit-removed-line");
392 addDecorations(this._textViewer, this._diffLines.changed, "webkit-changed-line");
395 _removeDiffDecorations: function()
397 function removeDecorations(textViewer, lines, className)
399 for (var i = 0; i < lines.length; ++i)
400 textViewer.removeDecoration(lines[i], className);
402 removeDecorations(this._textViewer, this._diffLines.added, "webkit-added-line");
403 removeDecorations(this._textViewer, this._diffLines.removed, "webkit-removed-line");
404 removeDecorations(this._textViewer, this._diffLines.changed, "webkit-changed-line");
407 _addExistingMessagesToSource: function()
409 var length = this._messages.length;
410 for (var i = 0; i < length; ++i)
411 this.addMessageToSource(this._messages[i].line - 1, this._messages[i]);
414 addMessageToSource: function(lineNumber, msg)
416 if (lineNumber >= this._textModel.linesCount)
417 lineNumber = this._textModel.linesCount - 1;
421 var messageBubbleElement = this._messageBubbles[lineNumber];
422 if (!messageBubbleElement || messageBubbleElement.nodeType !== Node.ELEMENT_NODE || !messageBubbleElement.hasStyleClass("webkit-html-message-bubble")) {
423 messageBubbleElement = document.createElement("div");
424 messageBubbleElement.className = "webkit-html-message-bubble";
425 this._messageBubbles[lineNumber] = messageBubbleElement;
426 this._textViewer.addDecoration(lineNumber, messageBubbleElement);
429 var rowMessages = this._rowMessages[lineNumber];
432 this._rowMessages[lineNumber] = rowMessages;
435 for (var i = 0; i < rowMessages.length; ++i) {
436 if (rowMessages[i].consoleMessage.isEqual(msg)) {
437 rowMessages[i].repeatCount = msg.totalRepeatCount;
438 this._updateMessageRepeatCount(rowMessages[i]);
443 var rowMessage = { consoleMessage: msg };
444 rowMessages.push(rowMessage);
448 case WebInspector.ConsoleMessage.MessageLevel.Error:
449 messageBubbleElement.addStyleClass("webkit-html-error-message");
450 imageURL = "Images/errorIcon.png";
452 case WebInspector.ConsoleMessage.MessageLevel.Warning:
453 messageBubbleElement.addStyleClass("webkit-html-warning-message");
454 imageURL = "Images/warningIcon.png";
458 var messageLineElement = document.createElement("div");
459 messageLineElement.className = "webkit-html-message-line";
460 messageBubbleElement.appendChild(messageLineElement);
462 // Create the image element in the Inspector's document so we can use relative image URLs.
463 var image = document.createElement("img");
464 image.src = imageURL;
465 image.className = "webkit-html-message-icon";
466 messageLineElement.appendChild(image);
467 messageLineElement.appendChild(document.createTextNode(msg.message));
469 rowMessage.element = messageLineElement;
470 rowMessage.repeatCount = msg.totalRepeatCount;
471 this._updateMessageRepeatCount(rowMessage);
474 _updateMessageRepeatCount: function(rowMessage)
476 if (rowMessage.repeatCount < 2)
479 if (!rowMessage.repeatCountElement) {
480 var repeatCountElement = document.createElement("span");
481 rowMessage.element.appendChild(repeatCountElement);
482 rowMessage.repeatCountElement = repeatCountElement;
485 rowMessage.repeatCountElement.textContent = WebInspector.UIString(" (repeated %d times)", rowMessage.repeatCount);
488 populateLineGutterContextMenu: function(lineNumber, contextMenu)
492 populateTextAreaContextMenu: function(contextMenu)
494 if (!window.getSelection().isCollapsed)
496 WebInspector.populateResourceContextMenu(contextMenu, this._url);
499 suggestedFileName: function()
503 inheritScrollPositions: function(sourceFrame)
505 this._textViewer.inheritScrollPositions(sourceFrame._textViewer);
508 _editButtonClicked: function()
510 if (!this.canEditSource())
513 const shouldStartEditing = !this._editButton.toggled;
514 if (shouldStartEditing)
517 this.commitEditing();
520 canEditSource: function()
525 startEditing: function()
527 if (!this.canEditSource())
530 if (this._commitEditingInProgress)
533 this.setReadOnly(false);
537 commitEditing: function()
539 if (!this._viewerState) {
540 // No editing was actually done.
541 this.setReadOnly(true);
545 this._commitEditingInProgress = true;
546 this._textViewer.readOnly = true;
547 this._editButton.toggled = false;
548 this.editContent(this._textModel.text, this.didEditContent.bind(this));
551 didEditContent: function(error)
553 this._commitEditingInProgress = false;
554 this._textViewer.readOnly = false;
558 WebInspector.log(error.message, WebInspector.ConsoleMessage.MessageLevel.Error, true);
562 delete this._viewerState;
565 editContent: function(newContent, callback)
569 cancelEditing: function()
571 this._restoreViewerState();
572 this.setReadOnly(true);
577 return this._textViewer.readOnly;
580 setReadOnly: function(readOnly)
582 this._textViewer.readOnly = readOnly;
583 this._editButton.toggled = !readOnly;
584 WebInspector.markBeingEdited(this._textViewer.element, !readOnly);
588 WebInspector.SourceFrame.prototype.__proto__ = WebInspector.View.prototype;
592 * @implements {WebInspector.TextViewerDelegate}
595 WebInspector.TextViewerDelegateForSourceFrame = function(sourceFrame)
597 this._sourceFrame = sourceFrame;
600 WebInspector.TextViewerDelegateForSourceFrame.prototype = {
601 doubleClick: function(lineNumber)
603 this._sourceFrame.startEditing(lineNumber);
606 beforeTextChanged: function()
608 this._sourceFrame.beforeTextChanged();
611 afterTextChanged: function(oldRange, newRange)
613 this._sourceFrame.afterTextChanged(oldRange, newRange);
616 commitEditing: function()
618 this._sourceFrame.commitEditing();
621 cancelEditing: function()
623 this._sourceFrame.cancelEditing();
626 populateLineGutterContextMenu: function(lineNumber, contextMenu)
628 this._sourceFrame.populateLineGutterContextMenu(lineNumber, contextMenu);
631 populateTextAreaContextMenu: function(contextMenu)
633 this._sourceFrame.populateTextAreaContextMenu(contextMenu);
636 suggestedFileName: function()
638 return this._sourceFrame.suggestedFileName();
642 WebInspector.TextViewerDelegateForSourceFrame.prototype.__proto__ = WebInspector.TextViewerDelegate.prototype;