blob: a891398bce14d10e488d0060001cf0701b0e6654 [file] [log] [blame]
/*
* Copyright (C) 2011 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @extends {WebInspector.View}
* @constructor
* @param {!WebInspector.ContentProvider} contentProvider
*/
WebInspector.SourceFrame = function(contentProvider)
{
WebInspector.View.call(this);
this.element.classList.add("script-view");
this.element.classList.add("fill");
this._url = contentProvider.contentURL();
this._contentProvider = contentProvider;
var textEditorDelegate = new WebInspector.TextEditorDelegateForSourceFrame(this);
loadScript("CodeMirrorTextEditor.js");
this._textEditor = new WebInspector.CodeMirrorTextEditor(this._url, textEditorDelegate);
this._currentSearchResultIndex = -1;
this._searchResults = [];
this._messages = [];
this._rowMessages = {};
this._messageBubbles = {};
this._textEditor.setReadOnly(!this.canEditSource());
this._shortcuts = {};
this.addShortcut(WebInspector.KeyboardShortcut.makeKey("s", WebInspector.KeyboardShortcut.Modifiers.CtrlOrMeta), this._commitEditing.bind(this));
this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false);
this._sourcePosition = new WebInspector.StatusBarText("", "source-frame-cursor-position");
}
/**
* @param {string} query
* @param {string=} modifiers
* @return {!RegExp}
*/
WebInspector.SourceFrame.createSearchRegex = function(query, modifiers)
{
var regex;
modifiers = modifiers || "";
// First try creating regex if user knows the / / hint.
try {
if (/^\/.+\/$/.test(query)) {
regex = new RegExp(query.substring(1, query.length - 1), modifiers);
regex.__fromRegExpQuery = true;
}
} catch (e) {
// Silent catch.
}
// Otherwise just do case-insensitive search.
if (!regex)
regex = createPlainTextSearchRegex(query, "i" + modifiers);
return regex;
}
WebInspector.SourceFrame.Events = {
ScrollChanged: "ScrollChanged",
SelectionChanged: "SelectionChanged"
}
WebInspector.SourceFrame.prototype = {
/**
* @param {number} key
* @param {function()} handler
*/
addShortcut: function(key, handler)
{
this._shortcuts[key] = handler;
},
wasShown: function()
{
this._ensureContentLoaded();
this._textEditor.show(this.element);
this._editorAttached = true;
this._wasShownOrLoaded();
},
/**
* @return {boolean}
*/
_isEditorShowing: function()
{
return this.isShowing() && this._editorAttached;
},
willHide: function()
{
WebInspector.View.prototype.willHide.call(this);
this._clearPositionHighlight();
this._clearLineToReveal();
},
/**
* @return {?Element}
*/
statusBarText: function()
{
return this._sourcePosition.element;
},
/**
* @return {!Array.<!Element>}
*/
statusBarItems: function()
{
return [];
},
defaultFocusedElement: function()
{
return this._textEditor.defaultFocusedElement();
},
get loaded()
{
return this._loaded;
},
hasContent: function()
{
return true;
},
get textEditor()
{
return this._textEditor;
},
_ensureContentLoaded: function()
{
if (!this._contentRequested) {
this._contentRequested = true;
this._contentProvider.requestContent(this.setContent.bind(this));
}
},
addMessage: function(msg)
{
this._messages.push(msg);
if (this.loaded)
this.addMessageToSource(msg.line - 1, msg);
},
clearMessages: function()
{
for (var line in this._messageBubbles) {
var bubble = this._messageBubbles[line];
var lineNumber = parseInt(line, 10);
this._textEditor.removeDecoration(lineNumber, bubble);
}
this._messages = [];
this._rowMessages = {};
this._messageBubbles = {};
},
/**
* @override
*/
canHighlightPosition: function()
{
return true;
},
/**
* @override
*/
highlightPosition: function(line, column)
{
this._clearLineToReveal();
this._clearLineToScrollTo();
this._clearSelectionToSet();
this._positionToHighlight = { line: line, column: column };
this._innerHighlightPositionIfNeeded();
},
_innerHighlightPositionIfNeeded: function()
{
if (!this._positionToHighlight)
return;
if (!this.loaded || !this._isEditorShowing())
return;
this._textEditor.highlightPosition(this._positionToHighlight.line, this._positionToHighlight.column);
delete this._positionToHighlight;
},
_clearPositionHighlight: function()
{
this._textEditor.clearPositionHighlight();
delete this._positionToHighlight;
},
/**
* @param {number} line
*/
revealLine: function(line)
{
this._clearPositionHighlight();
this._clearLineToScrollTo();
this._clearSelectionToSet();
this._lineToReveal = line;
this._innerRevealLineIfNeeded();
},
_innerRevealLineIfNeeded: function()
{
if (typeof this._lineToReveal === "number") {
if (this.loaded && this._isEditorShowing()) {
this._textEditor.revealLine(this._lineToReveal);
delete this._lineToReveal;
}
}
},
_clearLineToReveal: function()
{
delete this._lineToReveal;
},
/**
* @param {number} line
*/
scrollToLine: function(line)
{
this._clearPositionHighlight();
this._clearLineToReveal();
this._lineToScrollTo = line;
this._innerScrollToLineIfNeeded();
},
_innerScrollToLineIfNeeded: function()
{
if (typeof this._lineToScrollTo === "number") {
if (this.loaded && this._isEditorShowing()) {
this._textEditor.scrollToLine(this._lineToScrollTo);
delete this._lineToScrollTo;
}
}
},
_clearLineToScrollTo: function()
{
delete this._lineToScrollTo;
},
/**
* @param {!WebInspector.TextRange} textRange
*/
setSelection: function(textRange)
{
this._selectionToSet = textRange;
this._innerSetSelectionIfNeeded();
},
_innerSetSelectionIfNeeded: function()
{
if (this._selectionToSet && this.loaded && this._isEditorShowing()) {
this._textEditor.setSelection(this._selectionToSet);
delete this._selectionToSet;
}
},
_clearSelectionToSet: function()
{
delete this._selectionToSet;
},
_wasShownOrLoaded: function()
{
this._innerHighlightPositionIfNeeded();
this._innerRevealLineIfNeeded();
this._innerSetSelectionIfNeeded();
this._innerScrollToLineIfNeeded();
},
onTextChanged: function(oldRange, newRange)
{
if (this._searchResultsChangedCallback && !this._isReplacing)
this._searchResultsChangedCallback();
this.clearMessages();
},
_simplifyMimeType: function(content, mimeType)
{
if (!mimeType)
return "";
if (mimeType.indexOf("javascript") >= 0 ||
mimeType.indexOf("jscript") >= 0 ||
mimeType.indexOf("ecmascript") >= 0)
return "text/javascript";
// A hack around the fact that files with "php" extension might be either standalone or html embedded php scripts.
if (mimeType === "text/x-php" && content.match(/\<\?.*\?\>/g))
return "application/x-httpd-php";
return mimeType;
},
/**
* @param {string} highlighterType
*/
setHighlighterType: function(highlighterType)
{
this._highlighterType = highlighterType;
this._updateHighlighterType("");
},
/**
* @param {string} content
*/
_updateHighlighterType: function(content)
{
this._textEditor.setMimeType(this._simplifyMimeType(content, this._highlighterType));
},
/**
* @param {?string} content
*/
setContent: function(content)
{
if (!this._loaded) {
this._loaded = true;
this._textEditor.setText(content || "");
this._textEditor.markClean();
} else {
var firstLine = this._textEditor.firstVisibleLine();
var selection = this._textEditor.selection();
this._textEditor.setText(content || "");
this._textEditor.scrollToLine(firstLine);
this._textEditor.setSelection(selection);
}
this._updateHighlighterType(content || "");
this._textEditor.beginUpdates();
this._setTextEditorDecorations();
this._wasShownOrLoaded();
if (this._delayedFindSearchMatches) {
this._delayedFindSearchMatches();
delete this._delayedFindSearchMatches;
}
this.onTextEditorContentLoaded();
this._textEditor.endUpdates();
},
onTextEditorContentLoaded: function() {},
_setTextEditorDecorations: function()
{
this._rowMessages = {};
this._messageBubbles = {};
this._textEditor.beginUpdates();
this._addExistingMessagesToSource();
this._textEditor.endUpdates();
},
/**
* @param {string} query
* @param {boolean} shouldJump
* @param {function(!WebInspector.View, number)} callback
* @param {function(number)} currentMatchChangedCallback
* @param {function()} searchResultsChangedCallback
*/
performSearch: function(query, shouldJump, callback, currentMatchChangedCallback, searchResultsChangedCallback)
{
function doFindSearchMatches(query)
{
this._currentSearchResultIndex = -1;
this._searchResults = [];
var regex = WebInspector.SourceFrame.createSearchRegex(query);
this._searchRegex = regex;
this._searchResults = this._collectRegexMatches(regex);
if (!this._searchResults.length)
this._textEditor.cancelSearchResultsHighlight();
else if (shouldJump)
this.jumpToNextSearchResult();
else
this._textEditor.highlightSearchResults(regex, null);
callback(this, this._searchResults.length);
}
this._resetSearch();
this._currentSearchMatchChangedCallback = currentMatchChangedCallback;
this._searchResultsChangedCallback = searchResultsChangedCallback;
if (this.loaded)
doFindSearchMatches.call(this, query);
else
this._delayedFindSearchMatches = doFindSearchMatches.bind(this, query);
this._ensureContentLoaded();
},
_editorFocused: function()
{
if (!this._searchResults.length)
return;
this._currentSearchResultIndex = -1;
if (this._currentSearchMatchChangedCallback)
this._currentSearchMatchChangedCallback(this._currentSearchResultIndex);
this._textEditor.highlightSearchResults(this._searchRegex, null);
},
_searchResultAfterSelectionIndex: function(selection)
{
if (!selection)
return 0;
for (var i = 0; i < this._searchResults.length; ++i) {
if (this._searchResults[i].compareTo(selection) >= 0)
return i;
}
return 0;
},
_resetSearch: function()
{
delete this._delayedFindSearchMatches;
delete this._currentSearchMatchChangedCallback;
delete this._searchResultsChangedCallback;
this._currentSearchResultIndex = -1;
this._searchResults = [];
delete this._searchRegex;
},
searchCanceled: function()
{
var range = this._currentSearchResultIndex !== -1 ? this._searchResults[this._currentSearchResultIndex] : null;
this._resetSearch();
if (!this.loaded)
return;
this._textEditor.cancelSearchResultsHighlight();
if (range)
this._textEditor.setSelection(range);
},
hasSearchResults: function()
{
return this._searchResults.length > 0;
},
jumpToFirstSearchResult: function()
{
this.jumpToSearchResult(0);
},
jumpToLastSearchResult: function()
{
this.jumpToSearchResult(this._searchResults.length - 1);
},
jumpToNextSearchResult: function()
{
var currentIndex = this._searchResultAfterSelectionIndex(this._textEditor.selection());
var nextIndex = this._currentSearchResultIndex === -1 ? currentIndex : currentIndex + 1;
this.jumpToSearchResult(nextIndex);
},
jumpToPreviousSearchResult: function()
{
var currentIndex = this._searchResultAfterSelectionIndex(this._textEditor.selection());
this.jumpToSearchResult(currentIndex - 1);
},
showingFirstSearchResult: function()
{
return this._searchResults.length && this._currentSearchResultIndex === 0;
},
showingLastSearchResult: function()
{
return this._searchResults.length && this._currentSearchResultIndex === (this._searchResults.length - 1);
},
get currentSearchResultIndex()
{
return this._currentSearchResultIndex;
},
jumpToSearchResult: function(index)
{
if (!this.loaded || !this._searchResults.length)
return;
this._currentSearchResultIndex = (index + this._searchResults.length) % this._searchResults.length;
if (this._currentSearchMatchChangedCallback)
this._currentSearchMatchChangedCallback(this._currentSearchResultIndex);
this._textEditor.highlightSearchResults(this._searchRegex, this._searchResults[this._currentSearchResultIndex]);
},
/**
* @param {string} text
*/
replaceSearchMatchWith: function(text)
{
var range = this._searchResults[this._currentSearchResultIndex];
if (!range)
return;
this._textEditor.highlightSearchResults(this._searchRegex, null);
this._isReplacing = true;
var newRange = this._textEditor.editRange(range, text);
delete this._isReplacing;
this._textEditor.setSelection(newRange.collapseToEnd());
},
/**
* @param {string} query
* @param {string} replacement
*/
replaceAllWith: function(query, replacement)
{
this._textEditor.highlightSearchResults(this._searchRegex, null);
var text = this._textEditor.text();
var range = this._textEditor.range();
var regex = WebInspector.SourceFrame.createSearchRegex(query, "g");
if (regex.__fromRegExpQuery)
text = text.replace(regex, replacement);
else
text = text.replace(regex, function() { return replacement; });
this._isReplacing = true;
this._textEditor.editRange(range, text);
delete this._isReplacing;
},
_collectRegexMatches: function(regexObject)
{
var ranges = [];
for (var i = 0; i < this._textEditor.linesCount; ++i) {
var line = this._textEditor.line(i);
var offset = 0;
do {
var match = regexObject.exec(line);
if (match) {
if (match[0].length)
ranges.push(new WebInspector.TextRange(i, offset + match.index, i, offset + match.index + match[0].length));
offset += match.index + 1;
line = line.substring(match.index + 1);
}
} while (match && line);
}
return ranges;
},
_addExistingMessagesToSource: function()
{
var length = this._messages.length;
for (var i = 0; i < length; ++i)
this.addMessageToSource(this._messages[i].line - 1, this._messages[i]);
},
/**
* @param {number} lineNumber
* @param {!WebInspector.ConsoleMessage} msg
*/
addMessageToSource: function(lineNumber, msg)
{
if (lineNumber >= this._textEditor.linesCount)
lineNumber = this._textEditor.linesCount - 1;
if (lineNumber < 0)
lineNumber = 0;
var rowMessages = this._rowMessages[lineNumber];
if (!rowMessages) {
rowMessages = [];
this._rowMessages[lineNumber] = rowMessages;
}
for (var i = 0; i < rowMessages.length; ++i) {
if (rowMessages[i].consoleMessage.isEqual(msg)) {
rowMessages[i].repeatCount = msg.totalRepeatCount;
this._updateMessageRepeatCount(rowMessages[i]);
return;
}
}
var rowMessage = { consoleMessage: msg };
rowMessages.push(rowMessage);
this._textEditor.beginUpdates();
var messageBubbleElement = this._messageBubbles[lineNumber];
if (!messageBubbleElement) {
messageBubbleElement = document.createElement("div");
messageBubbleElement.className = "webkit-html-message-bubble";
this._messageBubbles[lineNumber] = messageBubbleElement;
this._textEditor.addDecoration(lineNumber, messageBubbleElement);
}
var imageElement = document.createElement("div");
switch (msg.level) {
case WebInspector.ConsoleMessage.MessageLevel.Error:
messageBubbleElement.classList.add("webkit-html-error-message");
imageElement.className = "error-icon-small";
break;
case WebInspector.ConsoleMessage.MessageLevel.Warning:
messageBubbleElement.classList.add("webkit-html-warning-message");
imageElement.className = "warning-icon-small";
break;
}
var messageLineElement = document.createElement("div");
messageLineElement.className = "webkit-html-message-line";
messageBubbleElement.appendChild(messageLineElement);
// Create the image element in the Inspector's document so we can use relative image URLs.
messageLineElement.appendChild(imageElement);
messageLineElement.appendChild(document.createTextNode(msg.message));
rowMessage.element = messageLineElement;
rowMessage.repeatCount = msg.totalRepeatCount;
this._updateMessageRepeatCount(rowMessage);
this._textEditor.endUpdates();
},
_updateMessageRepeatCount: function(rowMessage)
{
if (rowMessage.repeatCount < 2)
return;
if (!rowMessage.repeatCountElement) {
var repeatCountElement = document.createElement("span");
rowMessage.element.appendChild(repeatCountElement);
rowMessage.repeatCountElement = repeatCountElement;
}
rowMessage.repeatCountElement.textContent = WebInspector.UIString(" (repeated %d times)", rowMessage.repeatCount);
},
/**
* @param {number} lineNumber
* @param {!WebInspector.ConsoleMessage} msg
*/
removeMessageFromSource: function(lineNumber, msg)
{
if (lineNumber >= this._textEditor.linesCount)
lineNumber = this._textEditor.linesCount - 1;
if (lineNumber < 0)
lineNumber = 0;
var rowMessages = this._rowMessages[lineNumber];
for (var i = 0; rowMessages && i < rowMessages.length; ++i) {
var rowMessage = rowMessages[i];
if (rowMessage.consoleMessage !== msg)
continue;
var messageLineElement = rowMessage.element;
var messageBubbleElement = messageLineElement.parentElement;
messageBubbleElement.removeChild(messageLineElement);
rowMessages.remove(rowMessage);
if (!rowMessages.length)
delete this._rowMessages[lineNumber];
if (!messageBubbleElement.childElementCount) {
this._textEditor.removeDecoration(lineNumber, messageBubbleElement);
delete this._messageBubbles[lineNumber];
}
break;
}
},
populateLineGutterContextMenu: function(contextMenu, lineNumber)
{
},
populateTextAreaContextMenu: function(contextMenu, lineNumber)
{
},
inheritScrollPositions: function(sourceFrame)
{
this._textEditor.inheritScrollPositions(sourceFrame._textEditor);
},
/**
* @return {boolean}
*/
canEditSource: function()
{
return false;
},
/**
* @param {string} text
*/
commitEditing: function(text)
{
},
/**
* @param {!WebInspector.TextRange} textRange
*/
selectionChanged: function(textRange)
{
this._updateSourcePosition(textRange);
this.dispatchEventToListeners(WebInspector.SourceFrame.Events.SelectionChanged, textRange);
WebInspector.notifications.dispatchEventToListeners(WebInspector.SourceFrame.Events.SelectionChanged, textRange);
},
/**
* @param {!WebInspector.TextRange} textRange
*/
_updateSourcePosition: function(textRange)
{
if (!textRange)
return;
if (textRange.isEmpty()) {
this._sourcePosition.setText(WebInspector.UIString("Line %d, Column %d", textRange.endLine + 1, textRange.endColumn + 1));
return;
}
textRange = textRange.normalize();
var selectedText = this._textEditor.copyRange(textRange);
if (textRange.startLine === textRange.endLine)
this._sourcePosition.setText(WebInspector.UIString("%d characters selected", selectedText.length));
else
this._sourcePosition.setText(WebInspector.UIString("%d lines, %d characters selected", textRange.endLine - textRange.startLine + 1, selectedText.length));
},
/**
* @param {number} lineNumber
*/
scrollChanged: function(lineNumber)
{
this.dispatchEventToListeners(WebInspector.SourceFrame.Events.ScrollChanged, lineNumber);
},
_handleKeyDown: function(e)
{
var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e);
var handler = this._shortcuts[shortcutKey];
if (handler && handler())
e.consume(true);
},
_commitEditing: function()
{
if (this._textEditor.readOnly())
return false;
var content = this._textEditor.text();
this.commitEditing(content);
return true;
},
__proto__: WebInspector.View.prototype
}
/**
* @implements {WebInspector.TextEditorDelegate}
* @constructor
*/
WebInspector.TextEditorDelegateForSourceFrame = function(sourceFrame)
{
this._sourceFrame = sourceFrame;
}
WebInspector.TextEditorDelegateForSourceFrame.prototype = {
onTextChanged: function(oldRange, newRange)
{
this._sourceFrame.onTextChanged(oldRange, newRange);
},
/**
* @param {!WebInspector.TextRange} textRange
*/
selectionChanged: function(textRange)
{
this._sourceFrame.selectionChanged(textRange);
},
/**
* @param {number} lineNumber
*/
scrollChanged: function(lineNumber)
{
this._sourceFrame.scrollChanged(lineNumber);
},
editorFocused: function()
{
this._sourceFrame._editorFocused();
},
populateLineGutterContextMenu: function(contextMenu, lineNumber)
{
this._sourceFrame.populateLineGutterContextMenu(contextMenu, lineNumber);
},
populateTextAreaContextMenu: function(contextMenu, lineNumber)
{
this._sourceFrame.populateTextAreaContextMenu(contextMenu, lineNumber);
},
/**
* @param {string} hrefValue
* @param {boolean} isExternal
* @return {!Element}
*/
createLink: function(hrefValue, isExternal)
{
var targetLocation = WebInspector.ParsedURL.completeURL(this._sourceFrame._url, hrefValue);
return WebInspector.linkifyURLAsNode(targetLocation || hrefValue, hrefValue, undefined, isExternal);
},
__proto__: WebInspector.TextEditorDelegate.prototype
}