blob: 435ae218531f1ae8822e9ec25b5cb014f184b268 [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.
*/
/**
* @constructor
* @extends {WebInspector.SourceFrame}
* @param {WebInspector.ScriptsPanel} scriptsPanel
* @param {WebInspector.JavaScriptSource} javaScriptSource
*/
WebInspector.JavaScriptSourceFrame = function(scriptsPanel, javaScriptSource)
{
this._scriptsPanel = scriptsPanel;
this._breakpointManager = WebInspector.breakpointManager;
this._javaScriptSource = javaScriptSource;
var locations = this._breakpointManager.breakpointLocationsForUISourceCode(this._javaScriptSource);
for (var i = 0; i < locations.length; ++i)
this._breakpointAdded({data:locations[i]});
WebInspector.SourceFrame.call(this, javaScriptSource);
this._popoverHelper = new WebInspector.ObjectPopoverHelper(this.textEditor.element,
this._getPopoverAnchor.bind(this), this._resolveObjectForPopover.bind(this), this._onHidePopover.bind(this), true);
this.textEditor.element.addEventListener("mousedown", this._onMouseDown.bind(this), true);
this.textEditor.element.addEventListener("keydown", this._onKeyDown.bind(this), true);
this._breakpointManager.addEventListener(WebInspector.BreakpointManager.Events.BreakpointAdded, this._breakpointAdded, this);
this._breakpointManager.addEventListener(WebInspector.BreakpointManager.Events.BreakpointRemoved, this._breakpointRemoved, this);
this._javaScriptSource.addEventListener(WebInspector.UISourceCode.Events.ContentChanged, this._onContentChanged, this);
this._javaScriptSource.addEventListener(WebInspector.UISourceCode.Events.ConsoleMessageAdded, this._consoleMessageAdded, this);
this._javaScriptSource.addEventListener(WebInspector.UISourceCode.Events.ConsoleMessageRemoved, this._consoleMessageRemoved, this);
this._javaScriptSource.addEventListener(WebInspector.UISourceCode.Events.ConsoleMessagesCleared, this._consoleMessagesCleared, this);
}
WebInspector.JavaScriptSourceFrame.prototype = {
// View events
wasShown: function()
{
WebInspector.SourceFrame.prototype.wasShown.call(this);
},
willHide: function()
{
WebInspector.SourceFrame.prototype.willHide.call(this);
this._popoverHelper.hidePopover();
},
/**
* @return {boolean}
*/
canEditSource: function()
{
return this._javaScriptSource.isEditable();
},
/**
* @param {string} text
*/
commitEditing: function(text)
{
if (!this._javaScriptSource.isDirty())
return;
this._isCommittingEditing = true;
this._javaScriptSource.commitWorkingCopy(this._didEditContent.bind(this));
},
/**
* @param {WebInspector.Event} event
*/
_onContentChanged: function(event)
{
if (this._isCommittingEditing)
return;
var content = /** @type {string} */ event.data.content;
if (this._javaScriptSource.togglingFormatter())
this.setContent(content, false, this._javaScriptSource.mimeType());
else {
var breakpointLocations = this._breakpointManager.breakpointLocationsForUISourceCode(this._javaScriptSource);
for (var i = 0; i < breakpointLocations.length; ++i)
breakpointLocations[i].breakpoint.remove();
this.setContent(content, false, this._javaScriptSource.mimeType());
}
},
populateLineGutterContextMenu: function(contextMenu, lineNumber)
{
contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Continue to here" : "Continue to Here"), this._continueToLine.bind(this, lineNumber));
var breakpoint = this._breakpointManager.findBreakpoint(this._javaScriptSource, lineNumber);
if (!breakpoint) {
// This row doesn't have a breakpoint: We want to show Add Breakpoint and Add and Edit Breakpoint.
contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add breakpoint" : "Add Breakpoint"), this._setBreakpoint.bind(this, lineNumber, "", true));
contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add conditional breakpoint…" : "Add Conditional Breakpoint…"), this._editBreakpointCondition.bind(this, lineNumber));
} else {
// This row has a breakpoint, we want to show edit and remove breakpoint, and either disable or enable.
contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Remove breakpoint" : "Remove Breakpoint"), breakpoint.remove.bind(breakpoint));
contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Edit breakpoint…" : "Edit Breakpoint…"), this._editBreakpointCondition.bind(this, lineNumber, breakpoint));
if (breakpoint.enabled())
contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Disable breakpoint" : "Disable Breakpoint"), breakpoint.setEnabled.bind(breakpoint, false));
else
contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Enable breakpoint" : "Enable Breakpoint"), breakpoint.setEnabled.bind(breakpoint, true));
}
},
populateTextAreaContextMenu: function(contextMenu, lineNumber)
{
WebInspector.SourceFrame.prototype.populateTextAreaContextMenu.call(this, contextMenu, lineNumber);
var selection = window.getSelection();
if (selection.type === "Range" && !selection.isCollapsed) {
var addToWatchLabel = WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add to watch" : "Add to Watch");
contextMenu.appendItem(addToWatchLabel, this._scriptsPanel.addToWatch.bind(this._scriptsPanel, selection.toString()));
var evaluateLabel = WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Evaluate in console" : "Evaluate in Console");
contextMenu.appendItem(evaluateLabel, WebInspector.evaluateInConsole.bind(WebInspector, selection.toString()));
contextMenu.appendSeparator();
}
contextMenu.appendApplicableItems(this._javaScriptSource);
},
afterTextChanged: function(oldRange, newRange)
{
WebInspector.SourceFrame.prototype.afterTextChanged.call(this, oldRange, newRange);
this._javaScriptSource.setWorkingCopy(this.textModel.text);
this._restoreBreakpointsAfterEditing();
},
beforeTextChanged: function()
{
WebInspector.SourceFrame.prototype.beforeTextChanged.call(this);
this._removeBreakpointsBeforeEditing();
},
_didEditContent: function(error)
{
delete this._isCommittingEditing;
if (error) {
WebInspector.log(error, WebInspector.ConsoleMessage.MessageLevel.Error, true);
return;
}
if (!this._javaScriptSource.supportsEnabledBreakpointsWhileEditing())
this._restoreBreakpointsAfterEditing();
},
_removeBreakpointsBeforeEditing: function()
{
if (!this._javaScriptSource.isDirty() || this._javaScriptSource.supportsEnabledBreakpointsWhileEditing()) {
// Disable all breakpoints in the model, store them as muted breakpoints.
var breakpointLocations = this._breakpointManager.breakpointLocationsForUISourceCode(this._javaScriptSource);
var lineNumbers = {};
for (var i = 0; i < breakpointLocations.length; ++i) {
var breakpoint = breakpointLocations[i].breakpoint;
breakpointLocations[i].breakpoint.remove();
// Re-adding decoration only.
this._addBreakpointDecoration(breakpointLocations[i].uiLocation.lineNumber, breakpoint.condition(), breakpoint.enabled(), true);
}
this.clearExecutionLine();
}
},
_restoreBreakpointsAfterEditing: function()
{
if (!this._javaScriptSource.isDirty() || this._javaScriptSource.supportsEnabledBreakpointsWhileEditing()) {
// Restore all muted breakpoints.
for (var lineNumber = 0; lineNumber < this.textModel.linesCount; ++lineNumber) {
var breakpointDecoration = this.textModel.getAttribute(lineNumber, "breakpoint");
if (breakpointDecoration) {
// Remove fake decoration
this._removeBreakpointDecoration(lineNumber);
// Set new breakpoint
this._setBreakpoint(lineNumber, breakpointDecoration.condition, breakpointDecoration.enabled);
}
}
}
},
_getPopoverAnchor: function(element, event)
{
if (!WebInspector.debuggerModel.isPaused())
return null;
if (window.getSelection().type === "Range")
return null;
var lineElement = element.enclosingNodeOrSelfWithClass("webkit-line-content");
if (!lineElement)
return null;
if (element.hasStyleClass("webkit-javascript-ident"))
return element;
if (element.hasStyleClass("source-frame-token"))
return element;
// We are interested in identifiers and "this" keyword.
if (element.hasStyleClass("webkit-javascript-keyword"))
return element.textContent === "this" ? element : null;
if (element !== lineElement || lineElement.childElementCount)
return null;
// Handle non-highlighted case
// 1. Collect ranges of identifier suspects
var lineContent = lineElement.textContent;
var ranges = [];
var regex = new RegExp("[a-zA-Z_\$0-9]+", "g");
var match;
while (regex.lastIndex < lineContent.length && (match = regex.exec(lineContent)))
ranges.push({offset: match.index, length: regex.lastIndex - match.index});
// 2. 'highlight' them with artificial style to detect word boundaries
var changes = [];
WebInspector.highlightRangesWithStyleClass(lineElement, ranges, "source-frame-token", changes);
var lineOffsetLeft = lineElement.totalOffsetLeft();
for (var child = lineElement.firstChild; child; child = child.nextSibling) {
if (child.nodeType !== Node.ELEMENT_NODE || !child.hasStyleClass("source-frame-token"))
continue;
if (event.x > lineOffsetLeft + child.offsetLeft && event.x < lineOffsetLeft + child.offsetLeft + child.offsetWidth) {
var text = child.textContent;
return (text === "this" || !WebInspector.SourceJavaScriptTokenizer.Keywords[text]) ? child : null;
}
}
return null;
},
_resolveObjectForPopover: function(element, showCallback, objectGroupName)
{
this._highlightElement = this._highlightExpression(element);
/**
* @param {?RuntimeAgent.RemoteObject} result
* @param {boolean=} wasThrown
*/
function showObjectPopover(result, wasThrown)
{
if (!WebInspector.debuggerModel.isPaused()) {
this._popoverHelper.hidePopover();
return;
}
showCallback(WebInspector.RemoteObject.fromPayload(result), wasThrown, this._highlightElement);
// Popover may have been removed by showCallback().
if (this._highlightElement)
this._highlightElement.addStyleClass("source-frame-eval-expression");
}
if (!WebInspector.debuggerModel.isPaused()) {
this._popoverHelper.hidePopover();
return;
}
var selectedCallFrame = WebInspector.debuggerModel.selectedCallFrame();
selectedCallFrame.evaluate(this._highlightElement.textContent, objectGroupName, false, true, false, showObjectPopover.bind(this));
},
_onHidePopover: function()
{
// Replace higlight element with its contents inplace.
var highlightElement = this._highlightElement;
if (!highlightElement)
return;
// FIXME: the text editor should maintain highlight on its own. The check below is a workaround for
// the case when highlight element is detached from DOM by the TextEditor when re-building the DOM.
var parentElement = highlightElement.parentElement;
if (parentElement) {
var child = highlightElement.firstChild;
while (child) {
var nextSibling = child.nextSibling;
parentElement.insertBefore(child, highlightElement);
child = nextSibling;
}
parentElement.removeChild(highlightElement);
}
delete this._highlightElement;
},
_highlightExpression: function(element)
{
// Collect tokens belonging to evaluated expression.
var tokens = [ element ];
var token = element.previousSibling;
while (token && (token.className === "webkit-javascript-ident" || token.className === "source-frame-token" || token.className === "webkit-javascript-keyword" || token.textContent.trim() === ".")) {
tokens.push(token);
token = token.previousSibling;
}
tokens.reverse();
// Wrap them with highlight element.
var parentElement = element.parentElement;
var nextElement = element.nextSibling;
var container = document.createElement("span");
for (var i = 0; i < tokens.length; ++i)
container.appendChild(tokens[i]);
parentElement.insertBefore(container, nextElement);
return container;
},
/**
* @param {number} lineNumber
* @param {string} condition
* @param {boolean} enabled
* @param {boolean} mutedWhileEditing
*/
_addBreakpointDecoration: function(lineNumber, condition, enabled, mutedWhileEditing)
{
var breakpoint = {
condition: condition,
enabled: enabled
};
this.textModel.setAttribute(lineNumber, "breakpoint", breakpoint);
this.textEditor.beginUpdates();
this.textEditor.addDecoration(lineNumber, "webkit-breakpoint");
if (!enabled || (mutedWhileEditing && !this._javaScriptSource.supportsEnabledBreakpointsWhileEditing()))
this.textEditor.addDecoration(lineNumber, "webkit-breakpoint-disabled");
else
this.textEditor.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
if (!!condition)
this.textEditor.addDecoration(lineNumber, "webkit-breakpoint-conditional");
else
this.textEditor.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
this.textEditor.endUpdates();
},
_removeBreakpointDecoration: function(lineNumber)
{
this.textModel.removeAttribute(lineNumber, "breakpoint");
this.textEditor.beginUpdates();
this.textEditor.removeDecoration(lineNumber, "webkit-breakpoint");
this.textEditor.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
this.textEditor.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
this.textEditor.endUpdates();
},
_onMouseDown: function(event)
{
if (this._javaScriptSource.isDirty() && !this._javaScriptSource.supportsEnabledBreakpointsWhileEditing())
return;
if (event.button != 0 || event.altKey || event.ctrlKey || event.metaKey)
return;
var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
if (!target)
return;
var lineNumber = target.lineNumber;
this._toggleBreakpoint(lineNumber, event.shiftKey);
event.preventDefault();
},
_onKeyDown: function(event)
{
if (event.keyIdentifier === "U+001B") { // Escape key
if (this._popoverHelper.isPopoverVisible()) {
this._popoverHelper.hidePopover();
event.consume();
}
}
},
/**
* @param {number} lineNumber
* @param {WebInspector.BreakpointManager.Breakpoint=} breakpoint
*/
_editBreakpointCondition: function(lineNumber, breakpoint)
{
this._conditionElement = this._createConditionElement(lineNumber);
this.textEditor.addDecoration(lineNumber, this._conditionElement);
function finishEditing(committed, element, newText)
{
this.textEditor.removeDecoration(lineNumber, this._conditionElement);
delete this._conditionEditorElement;
delete this._conditionElement;
if (breakpoint)
breakpoint.setCondition(newText);
else
this._setBreakpoint(lineNumber, newText, true);
}
var config = new WebInspector.EditingConfig(finishEditing.bind(this, true), finishEditing.bind(this, false));
WebInspector.startEditing(this._conditionEditorElement, config);
this._conditionEditorElement.value = breakpoint ? breakpoint.condition() : "";
this._conditionEditorElement.select();
},
_createConditionElement: function(lineNumber)
{
var conditionElement = document.createElement("div");
conditionElement.className = "source-frame-breakpoint-condition";
var labelElement = document.createElement("label");
labelElement.className = "source-frame-breakpoint-message";
labelElement.htmlFor = "source-frame-breakpoint-condition";
labelElement.appendChild(document.createTextNode(WebInspector.UIString("The breakpoint on line %d will stop only if this expression is true:", lineNumber)));
conditionElement.appendChild(labelElement);
var editorElement = document.createElement("input");
editorElement.id = "source-frame-breakpoint-condition";
editorElement.className = "monospace";
editorElement.type = "text";
conditionElement.appendChild(editorElement);
this._conditionEditorElement = editorElement;
return conditionElement;
},
/**
* @param {number} lineNumber
*/
setExecutionLine: function(lineNumber)
{
this._executionLineNumber = lineNumber;
if (this.loaded) {
this.textEditor.addDecoration(lineNumber, "webkit-execution-line");
this.revealLine(this._executionLineNumber);
if (this.canEditSource())
this.setSelection(WebInspector.TextRange.createFromLocation(lineNumber, 0));
}
},
clearExecutionLine: function()
{
if (this.loaded && typeof this._executionLineNumber === "number")
this.textEditor.removeDecoration(this._executionLineNumber, "webkit-execution-line");
delete this._executionLineNumber;
},
_lineNumberAfterEditing: function(lineNumber, oldRange, newRange)
{
var shiftOffset = lineNumber <= oldRange.startLine ? 0 : newRange.linesCount - oldRange.linesCount;
// Special case of editing the line itself. We should decide whether the line number should move below or not.
if (lineNumber === oldRange.startLine) {
var whiteSpacesRegex = /^[\s\xA0]*$/;
for (var i = 0; lineNumber + i <= newRange.endLine; ++i) {
if (!whiteSpacesRegex.test(this.textModel.line(lineNumber + i))) {
shiftOffset = i;
break;
}
}
}
var newLineNumber = Math.max(0, lineNumber + shiftOffset);
if (oldRange.startLine < lineNumber && lineNumber < oldRange.endLine)
newLineNumber = oldRange.startLine;
return newLineNumber;
},
_breakpointAdded: function(event)
{
var uiLocation = /** @type {WebInspector.UILocation} */ event.data.uiLocation;
if (uiLocation.uiSourceCode !== this._javaScriptSource)
return;
var breakpoint = /** @type {WebInspector.BreakpointManager.Breakpoint} */ event.data.breakpoint;
if (this.loaded)
this._addBreakpointDecoration(uiLocation.lineNumber, breakpoint.condition(), breakpoint.enabled(), false);
},
_breakpointRemoved: function(event)
{
var uiLocation = /** @type {WebInspector.UILocation} */ event.data.uiLocation;
if (uiLocation.uiSourceCode !== this._javaScriptSource)
return;
var breakpoint = /** @type {WebInspector.BreakpointManager.Breakpoint} */ event.data.breakpoint;
var remainingBreakpoint = this._breakpointManager.findBreakpoint(this._javaScriptSource, uiLocation.lineNumber);
if (!remainingBreakpoint && this.loaded)
this._removeBreakpointDecoration(uiLocation.lineNumber);
},
_consoleMessageAdded: function(event)
{
var message = /** @type {WebInspector.PresentationConsoleMessage} */ event.data;
if (this.loaded)
this.addMessageToSource(message.lineNumber, message.originalMessage);
},
_consoleMessageRemoved: function(event)
{
var message = /** @type {WebInspector.PresentationConsoleMessage} */ event.data;
if (this.loaded)
this.removeMessageFromSource(message.lineNumber, message.originalMessage);
},
_consoleMessagesCleared: function(event)
{
this.clearMessages();
},
onTextEditorContentLoaded: function()
{
if (typeof this._executionLineNumber === "number")
this.setExecutionLine(this._executionLineNumber);
var breakpointLocations = this._breakpointManager.breakpointLocationsForUISourceCode(this._javaScriptSource);
for (var i = 0; i < breakpointLocations.length; ++i) {
var breakpoint = breakpointLocations[i].breakpoint;
this._addBreakpointDecoration(breakpointLocations[i].uiLocation.lineNumber, breakpoint.condition(), breakpoint.enabled(), false);
}
var messages = this._javaScriptSource.consoleMessages();
for (var i = 0; i < messages.length; ++i) {
var message = messages[i];
this.addMessageToSource(message.lineNumber, message.originalMessage);
}
},
/**
* @param {number} lineNumber
* @param {boolean} onlyDisable
*/
_toggleBreakpoint: function(lineNumber, onlyDisable)
{
var breakpoint = this._breakpointManager.findBreakpoint(this._javaScriptSource, lineNumber);
if (breakpoint) {
if (onlyDisable)
breakpoint.setEnabled(!breakpoint.enabled());
else
breakpoint.remove();
} else
this._setBreakpoint(lineNumber, "", true);
},
toggleBreakpointOnCurrentLine: function()
{
var selection = this.textEditor.selection();
if (!selection)
return;
this._toggleBreakpoint(selection.startLine, false);
},
/**
* @param {number} lineNumber
* @param {string} condition
* @param {boolean} enabled
*/
_setBreakpoint: function(lineNumber, condition, enabled)
{
this._breakpointManager.setBreakpoint(this._javaScriptSource, lineNumber, condition, enabled);
},
/**
* @param {number} lineNumber
*/
_continueToLine: function(lineNumber)
{
var rawLocation = this._javaScriptSource.uiLocationToRawLocation(lineNumber, 0);
WebInspector.debuggerModel.continueToLocation(rawLocation);
}
}
WebInspector.JavaScriptSourceFrame.prototype.__proto__ = WebInspector.SourceFrame.prototype;