blob: 13de635127018497c6d13123c4da6abc652c2562 [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.
*/
/**
* @unrestricted
*/
Sources.JavaScriptSourceFrame = class extends Sources.UISourceCodeFrame {
/**
* @param {!Workspace.UISourceCode} uiSourceCode
*/
constructor(uiSourceCode) {
super(uiSourceCode);
this._scriptsPanel = Sources.SourcesPanel.instance();
this._breakpointManager = Bindings.breakpointManager;
if (uiSourceCode.project().type() === Workspace.projectTypes.Debugger)
this.element.classList.add('source-frame-debugger-script');
this._popoverHelper = new Components.ObjectPopoverHelper(
this._scriptsPanel.element, this._getPopoverAnchor.bind(this), this._resolveObjectForPopover.bind(this),
this._onHidePopover.bind(this), true);
this._popoverHelper.setTimeout(250, 250);
this.textEditor.element.addEventListener('keydown', this._onKeyDown.bind(this), true);
this.textEditor.addEventListener(
SourceFrame.SourcesTextEditor.Events.GutterClick, this._handleGutterClick.bind(this), this);
this._breakpointManager.addEventListener(
Bindings.BreakpointManager.Events.BreakpointAdded, this._breakpointAdded, this);
this._breakpointManager.addEventListener(
Bindings.BreakpointManager.Events.BreakpointRemoved, this._breakpointRemoved, this);
this.uiSourceCode().addEventListener(
Workspace.UISourceCode.Events.SourceMappingChanged, this._onSourceMappingChanged, this);
this.uiSourceCode().addEventListener(
Workspace.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this);
this.uiSourceCode().addEventListener(
Workspace.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this);
this.uiSourceCode().addEventListener(
Workspace.UISourceCode.Events.TitleChanged, this._showBlackboxInfobarIfNeeded, this);
/** @type {!Map.<!SDK.Target, !Bindings.ResourceScriptFile>}*/
this._scriptFileForTarget = new Map();
var targets = SDK.targetManager.targets();
for (var i = 0; i < targets.length; ++i) {
var scriptFile = Bindings.debuggerWorkspaceBinding.scriptFile(uiSourceCode, targets[i]);
if (scriptFile)
this._updateScriptFile(targets[i]);
}
if (this._scriptFileForTarget.size || uiSourceCode.extension() === 'js' ||
uiSourceCode.project().type() === Workspace.projectTypes.Snippets)
this._compiler = new Sources.JavaScriptCompiler(this);
Common.moduleSetting('skipStackFramesPattern').addChangeListener(this._showBlackboxInfobarIfNeeded, this);
Common.moduleSetting('skipContentScripts').addChangeListener(this._showBlackboxInfobarIfNeeded, this);
this._showBlackboxInfobarIfNeeded();
/** @type {!Map.<number, !Element>} */
this._valueWidgets = new Map();
}
/**
* @override
* @return {!Array<!UI.ToolbarItem>}
*/
syncToolbarItems() {
var result = super.syncToolbarItems();
var originURL = Bindings.CompilerScriptMapping.uiSourceCodeOrigin(this.uiSourceCode());
if (originURL) {
var parsedURL = originURL.asParsedURL();
if (parsedURL)
result.push(new UI.ToolbarText(Common.UIString('(source mapped from %s)', parsedURL.displayName)));
}
if (this.uiSourceCode().project().type() === Workspace.projectTypes.Snippets) {
result.push(new UI.ToolbarSeparator(true));
var runSnippet = UI.Toolbar.createActionButtonForId('debugger.run-snippet');
runSnippet.setText(Host.isMac() ? Common.UIString('\u2318+Enter') : Common.UIString('Ctrl+Enter'));
result.push(runSnippet);
}
return result;
}
_updateInfobars() {
this.attachInfobars([this._blackboxInfobar, this._divergedInfobar]);
}
_showDivergedInfobar() {
if (!this.uiSourceCode().contentType().isScript())
return;
if (this._divergedInfobar)
this._divergedInfobar.dispose();
var infobar = new UI.Infobar(UI.Infobar.Type.Warning, Common.UIString('Workspace mapping mismatch'));
this._divergedInfobar = infobar;
var fileURL = this.uiSourceCode().url();
infobar.createDetailsRowMessage(Common.UIString('The content of this file on the file system:\u00a0'))
.appendChild(UI.linkifyURLAsNode(fileURL, fileURL, 'source-frame-infobar-details-url', true));
var scriptURL = this.uiSourceCode().url();
infobar.createDetailsRowMessage(Common.UIString('does not match the loaded script:\u00a0'))
.appendChild(UI.linkifyURLAsNode(scriptURL, scriptURL, 'source-frame-infobar-details-url', true));
infobar.createDetailsRowMessage();
infobar.createDetailsRowMessage(Common.UIString('Possible solutions are:'));
if (Common.moduleSetting('cacheDisabled').get()) {
infobar.createDetailsRowMessage(' - ').createTextChild(Common.UIString('Reload inspected page'));
} else {
infobar.createDetailsRowMessage(' - ').createTextChild(Common.UIString(
'Check "Disable cache" in settings and reload inspected page (recommended setup for authoring and debugging)'));
}
infobar.createDetailsRowMessage(' - ').createTextChild(Common.UIString(
'Check that your file and script are both loaded from the correct source and their contents match'));
this._updateInfobars();
}
_hideDivergedInfobar() {
if (!this._divergedInfobar)
return;
this._divergedInfobar.dispose();
delete this._divergedInfobar;
}
_showBlackboxInfobarIfNeeded() {
var uiSourceCode = this.uiSourceCode();
if (!uiSourceCode.contentType().hasScripts())
return;
var projectType = uiSourceCode.project().type();
if (projectType === Workspace.projectTypes.Snippets)
return;
if (!Bindings.blackboxManager.isBlackboxedUISourceCode(uiSourceCode)) {
this._hideBlackboxInfobar();
return;
}
if (this._blackboxInfobar)
this._blackboxInfobar.dispose();
var infobar = new UI.Infobar(UI.Infobar.Type.Warning, Common.UIString('This script is blackboxed in debugger'));
this._blackboxInfobar = infobar;
infobar.createDetailsRowMessage(
Common.UIString('Debugger will skip stepping through this script, and will not stop on exceptions'));
var scriptFile = this._scriptFileForTarget.size ? this._scriptFileForTarget.valuesArray()[0] : null;
if (scriptFile && scriptFile.hasSourceMapURL())
infobar.createDetailsRowMessage(Common.UIString('Source map found, but ignored for blackboxed file.'));
infobar.createDetailsRowMessage();
infobar.createDetailsRowMessage(Common.UIString('Possible ways to cancel this behavior are:'));
infobar.createDetailsRowMessage(' - ').createTextChild(
Common.UIString('Go to "%s" tab in settings', Common.UIString('Blackboxing')));
var unblackboxLink = infobar.createDetailsRowMessage(' - ').createChild('span', 'link');
unblackboxLink.textContent = Common.UIString('Unblackbox this script');
unblackboxLink.addEventListener('click', unblackbox, false);
function unblackbox() {
Bindings.blackboxManager.unblackboxUISourceCode(uiSourceCode);
if (projectType === Workspace.projectTypes.ContentScripts)
Bindings.blackboxManager.unblackboxContentScripts();
}
this._updateInfobars();
}
_hideBlackboxInfobar() {
if (!this._blackboxInfobar)
return;
this._blackboxInfobar.dispose();
delete this._blackboxInfobar;
}
/**
* @override
*/
wasShown() {
super.wasShown();
if (this._executionLocation && this.loaded) {
// We need SourcesTextEditor to be initialized prior to this call. @see crbug.com/499889
setImmediate(this._generateValuesInSource.bind(this));
}
}
/**
* @override
*/
willHide() {
super.willHide();
this._popoverHelper.hidePopover();
}
/**
* @override
*/
onUISourceCodeContentChanged() {
this._removeAllBreakpoints();
super.onUISourceCodeContentChanged();
}
/**
* @override
*/
onTextChanged(oldRange, newRange) {
this._scriptsPanel.updateLastModificationTime();
super.onTextChanged(oldRange, newRange);
if (this._compiler)
this._compiler.scheduleCompile();
}
/**
* @override
* @return {!Promise}
*/
populateLineGutterContextMenu(contextMenu, lineNumber) {
/**
* @this {Sources.JavaScriptSourceFrame}
*/
function populate(resolve, reject) {
var uiLocation = new Workspace.UILocation(this.uiSourceCode(), lineNumber, 0);
this._scriptsPanel.appendUILocationItems(contextMenu, uiLocation);
var breakpoints = this._breakpointManager.findBreakpoints(this.uiSourceCode(), lineNumber);
if (!breakpoints.length) {
// This row doesn't have a breakpoint: We want to show Add Breakpoint and Add and Edit Breakpoint.
contextMenu.appendItem(
Common.UIString('Add breakpoint'), this._createNewBreakpoint.bind(this, lineNumber, '', true));
contextMenu.appendItem(
Common.UIString('Add conditional breakpoint…'), this._editBreakpointCondition.bind(this, lineNumber));
contextMenu.appendItem(
Common.UIString('Never pause here'), this._createNewBreakpoint.bind(this, lineNumber, 'false', true));
} else {
var breakpoint = breakpoints[0];
// This row has a breakpoint, we want to show edit and remove breakpoint, and either disable or enable.
contextMenu.appendItem(Common.UIString('Remove breakpoint'), breakpoint.remove.bind(breakpoint));
contextMenu.appendItem(
Common.UIString('Edit breakpoint…'), this._editBreakpointCondition.bind(this, lineNumber, breakpoint));
if (breakpoint.enabled())
contextMenu.appendItem(Common.UIString('Disable breakpoint'), breakpoint.setEnabled.bind(breakpoint, false));
else
contextMenu.appendItem(Common.UIString('Enable breakpoint'), breakpoint.setEnabled.bind(breakpoint, true));
}
resolve();
}
return new Promise(populate.bind(this));
}
/**
* @override
* @return {!Promise}
*/
populateTextAreaContextMenu(contextMenu, lineNumber, columnNumber) {
/**
* @param {!Bindings.ResourceScriptFile} scriptFile
*/
function addSourceMapURL(scriptFile) {
Sources.AddSourceMapURLDialog.show(addSourceMapURLDialogCallback.bind(null, scriptFile));
}
/**
* @param {!Bindings.ResourceScriptFile} scriptFile
* @param {string} url
*/
function addSourceMapURLDialogCallback(scriptFile, url) {
if (!url)
return;
scriptFile.addSourceMapURL(url);
}
/**
* @this {Sources.JavaScriptSourceFrame}
*/
function populateSourceMapMembers() {
if (this.uiSourceCode().project().type() === Workspace.projectTypes.Network &&
Common.moduleSetting('jsSourceMapsEnabled').get() &&
!Bindings.blackboxManager.isBlackboxedUISourceCode(this.uiSourceCode())) {
if (this._scriptFileForTarget.size) {
var scriptFile = this._scriptFileForTarget.valuesArray()[0];
var addSourceMapURLLabel = Common.UIString.capitalize('Add ^source ^map\u2026');
contextMenu.appendItem(addSourceMapURLLabel, addSourceMapURL.bind(null, scriptFile));
contextMenu.appendSeparator();
}
}
}
return super.populateTextAreaContextMenu(contextMenu, lineNumber, columnNumber)
.then(populateSourceMapMembers.bind(this));
}
_workingCopyChanged(event) {
if (this._supportsEnabledBreakpointsWhileEditing() || this._scriptFileForTarget.size)
return;
if (this.uiSourceCode().isDirty())
this._muteBreakpointsWhileEditing();
else
this._restoreBreakpointsAfterEditing();
}
_workingCopyCommitted(event) {
this._scriptsPanel.updateLastModificationTime();
if (this._supportsEnabledBreakpointsWhileEditing())
return;
if (!this._scriptFileForTarget.size)
this._restoreBreakpointsAfterEditing();
}
_didMergeToVM() {
if (this._supportsEnabledBreakpointsWhileEditing())
return;
this._updateDivergedInfobar();
this._restoreBreakpointsIfConsistentScripts();
}
_didDivergeFromVM() {
if (this._supportsEnabledBreakpointsWhileEditing())
return;
this._updateDivergedInfobar();
this._muteBreakpointsWhileEditing();
}
_muteBreakpointsWhileEditing() {
if (this._muted)
return;
for (var lineNumber = 0; lineNumber < this._textEditor.linesCount; ++lineNumber) {
var breakpointDecoration = this._textEditor.getAttribute(lineNumber, 'breakpoint');
if (!breakpointDecoration)
continue;
this._removeBreakpointDecoration(lineNumber);
this._addBreakpointDecoration(
lineNumber, breakpointDecoration.columnNumber, breakpointDecoration.condition, breakpointDecoration.enabled,
true);
}
this._muted = true;
}
_updateDivergedInfobar() {
if (this.uiSourceCode().project().type() !== Workspace.projectTypes.FileSystem) {
this._hideDivergedInfobar();
return;
}
var scriptFiles = this._scriptFileForTarget.valuesArray();
var hasDivergedScript = false;
for (var i = 0; i < scriptFiles.length; ++i)
hasDivergedScript = hasDivergedScript || scriptFiles[i].hasDivergedFromVM();
if (this._divergedInfobar) {
if (!hasDivergedScript)
this._hideDivergedInfobar();
} else {
if (hasDivergedScript && !this.uiSourceCode().isDirty())
this._showDivergedInfobar();
}
}
_supportsEnabledBreakpointsWhileEditing() {
return this.uiSourceCode().project().type() === Workspace.projectTypes.Snippets;
}
_restoreBreakpointsIfConsistentScripts() {
var scriptFiles = this._scriptFileForTarget.valuesArray();
for (var i = 0; i < scriptFiles.length; ++i) {
if (scriptFiles[i].hasDivergedFromVM() || scriptFiles[i].isMergingToVM())
return;
}
this._restoreBreakpointsAfterEditing();
}
_restoreBreakpointsAfterEditing() {
delete this._muted;
var breakpoints = {};
// Save and remove muted breakpoint decorations.
for (var lineNumber = 0; lineNumber < this._textEditor.linesCount; ++lineNumber) {
var breakpointDecoration = this._textEditor.getAttribute(lineNumber, 'breakpoint');
if (breakpointDecoration) {
breakpoints[lineNumber] = breakpointDecoration;
this._removeBreakpointDecoration(lineNumber);
}
}
// Remove all breakpoints.
this._removeAllBreakpoints();
// Restore all breakpoints from saved decorations.
for (var lineNumberString in breakpoints) {
var lineNumber = parseInt(lineNumberString, 10);
if (isNaN(lineNumber))
continue;
var breakpointDecoration = breakpoints[lineNumberString];
this._setBreakpoint(
lineNumber, breakpointDecoration.columnNumber, breakpointDecoration.condition, breakpointDecoration.enabled);
}
}
_removeAllBreakpoints() {
var breakpoints = this._breakpointManager.breakpointsForUISourceCode(this.uiSourceCode());
for (var i = 0; i < breakpoints.length; ++i)
breakpoints[i].remove();
}
/**
* @param {string} tokenType
* @return {boolean}
*/
_isIdentifier(tokenType) {
return tokenType.startsWith('js-variable') || tokenType.startsWith('js-property') || tokenType === 'js-def';
}
_getPopoverAnchor(element, event) {
var target = UI.context.flavor(SDK.Target);
var debuggerModel = SDK.DebuggerModel.fromTarget(target);
if (!debuggerModel || !debuggerModel.isPaused())
return;
var textPosition = this.textEditor.coordinatesToCursorPosition(event.x, event.y);
if (!textPosition)
return;
var mouseLine = textPosition.startLine;
var mouseColumn = textPosition.startColumn;
var textSelection = this.textEditor.selection().normalize();
if (textSelection && !textSelection.isEmpty()) {
if (textSelection.startLine !== textSelection.endLine || textSelection.startLine !== mouseLine ||
mouseColumn < textSelection.startColumn || mouseColumn > textSelection.endColumn)
return;
var leftCorner = this.textEditor.cursorPositionToCoordinates(textSelection.startLine, textSelection.startColumn);
var rightCorner = this.textEditor.cursorPositionToCoordinates(textSelection.endLine, textSelection.endColumn);
var anchorBox = new AnchorBox(leftCorner.x, leftCorner.y, rightCorner.x - leftCorner.x, leftCorner.height);
anchorBox.highlight = {
lineNumber: textSelection.startLine,
startColumn: textSelection.startColumn,
endColumn: textSelection.endColumn - 1
};
anchorBox.forSelection = true;
return anchorBox;
}
var token = this.textEditor.tokenAtTextPosition(textPosition.startLine, textPosition.startColumn);
if (!token || !token.type)
return;
var lineNumber = textPosition.startLine;
var line = this.textEditor.line(lineNumber);
var tokenContent = line.substring(token.startColumn, token.endColumn);
var isIdentifier = this._isIdentifier(token.type);
if (!isIdentifier && (token.type !== 'js-keyword' || tokenContent !== 'this'))
return;
var leftCorner = this.textEditor.cursorPositionToCoordinates(lineNumber, token.startColumn);
var rightCorner = this.textEditor.cursorPositionToCoordinates(lineNumber, token.endColumn - 1);
var anchorBox = new AnchorBox(leftCorner.x, leftCorner.y, rightCorner.x - leftCorner.x, leftCorner.height);
anchorBox.highlight = {lineNumber: lineNumber, startColumn: token.startColumn, endColumn: token.endColumn - 1};
return anchorBox;
}
_resolveObjectForPopover(anchorBox, showCallback, objectGroupName) {
var target = UI.context.flavor(SDK.Target);
var selectedCallFrame = UI.context.flavor(SDK.DebuggerModel.CallFrame);
if (!selectedCallFrame) {
this._popoverHelper.hidePopover();
return;
}
var lineNumber = anchorBox.highlight.lineNumber;
var startHighlight = anchorBox.highlight.startColumn;
var endHighlight = anchorBox.highlight.endColumn;
var line = this.textEditor.line(lineNumber);
if (!anchorBox.forSelection) {
while (startHighlight > 1 && line.charAt(startHighlight - 1) === '.') {
var token = this.textEditor.tokenAtTextPosition(lineNumber, startHighlight - 2);
if (!token || !token.type) {
this._popoverHelper.hidePopover();
return;
}
startHighlight = token.startColumn;
}
}
var evaluationText = line.substring(startHighlight, endHighlight + 1);
Sources.SourceMapNamesResolver
.resolveExpression(
selectedCallFrame, evaluationText, this.uiSourceCode(), lineNumber, startHighlight, endHighlight)
.then(onResolve.bind(this));
/**
* @param {?string=} text
* @this {Sources.JavaScriptSourceFrame}
*/
function onResolve(text) {
selectedCallFrame.evaluate(
text || evaluationText, objectGroupName, false, true, false, false, showObjectPopover.bind(this));
}
/**
* @param {?Protocol.Runtime.RemoteObject} result
* @param {!Protocol.Runtime.ExceptionDetails=} exceptionDetails
* @this {Sources.JavaScriptSourceFrame}
*/
function showObjectPopover(result, exceptionDetails) {
var target = UI.context.flavor(SDK.Target);
var potentiallyUpdatedCallFrame = UI.context.flavor(SDK.DebuggerModel.CallFrame);
if (selectedCallFrame !== potentiallyUpdatedCallFrame || !result) {
this._popoverHelper.hidePopover();
return;
}
this._popoverAnchorBox = anchorBox;
showCallback(target.runtimeModel.createRemoteObject(result), !!exceptionDetails, this._popoverAnchorBox);
// Popover may have been removed by showCallback().
if (this._popoverAnchorBox) {
var highlightRange = new Common.TextRange(lineNumber, startHighlight, lineNumber, endHighlight);
this._popoverAnchorBox._highlightDescriptor =
this.textEditor.highlightRange(highlightRange, 'source-frame-eval-expression');
}
}
}
_onHidePopover() {
if (!this._popoverAnchorBox)
return;
if (this._popoverAnchorBox._highlightDescriptor)
this.textEditor.removeHighlight(this._popoverAnchorBox._highlightDescriptor);
delete this._popoverAnchorBox;
}
/**
* @param {number} lineNumber
* @param {number} columnNumber
* @param {string} condition
* @param {boolean} enabled
* @param {boolean} mutedWhileEditing
*/
_addBreakpointDecoration(lineNumber, columnNumber, condition, enabled, mutedWhileEditing) {
var breakpoint = {condition: condition, enabled: enabled, columnNumber: columnNumber};
this.textEditor.setAttribute(lineNumber, 'breakpoint', breakpoint);
var disabled = !enabled || mutedWhileEditing;
this.textEditor.addBreakpoint(lineNumber, disabled, !!condition);
}
_removeBreakpointDecoration(lineNumber) {
this.textEditor.removeAttribute(lineNumber, 'breakpoint');
this.textEditor.removeBreakpoint(lineNumber);
}
_onKeyDown(event) {
if (event.key === 'Escape') {
if (this._popoverHelper.isPopoverVisible()) {
this._popoverHelper.hidePopover();
event.consume();
}
}
}
/**
* @param {number} lineNumber
* @param {!Bindings.BreakpointManager.Breakpoint=} breakpoint
*/
_editBreakpointCondition(lineNumber, breakpoint) {
this._conditionElement = this._createConditionElement(lineNumber);
this.textEditor.addDecoration(this._conditionElement, lineNumber);
/**
* @this {Sources.JavaScriptSourceFrame}
*/
function finishEditing(committed, element, newText) {
this.textEditor.removeDecoration(this._conditionElement, lineNumber);
delete this._conditionEditorElement;
delete this._conditionElement;
if (!committed)
return;
if (breakpoint)
breakpoint.setCondition(newText);
else
this._createNewBreakpoint(lineNumber, newText, true);
}
var config = new UI.InplaceEditor.Config(finishEditing.bind(this, true), finishEditing.bind(this, false));
UI.InplaceEditor.startEditing(this._conditionEditorElement, config);
this._conditionEditorElement.value = breakpoint ? breakpoint.condition() : '';
this._conditionEditorElement.select();
}
_createConditionElement(lineNumber) {
var conditionElement = createElementWithClass('div', 'source-frame-breakpoint-condition');
var labelElement = conditionElement.createChild('label', 'source-frame-breakpoint-message');
labelElement.htmlFor = 'source-frame-breakpoint-condition';
labelElement.createTextChild(
Common.UIString('The breakpoint on line %d will stop only if this expression is true:', lineNumber + 1));
var editorElement = conditionElement.createChild('input', 'monospace');
editorElement.id = 'source-frame-breakpoint-condition';
editorElement.type = 'text';
this._conditionEditorElement = editorElement;
return conditionElement;
}
/**
* @param {!Workspace.UILocation} uiLocation
*/
setExecutionLocation(uiLocation) {
this._executionLocation = uiLocation;
if (!this.loaded)
return;
this.textEditor.setExecutionLocation(uiLocation.lineNumber, uiLocation.columnNumber);
if (this.isShowing()) {
// We need SourcesTextEditor to be initialized prior to this call. @see crbug.com/506566
setImmediate(this._generateValuesInSource.bind(this));
}
}
_generateValuesInSource() {
if (!Common.moduleSetting('inlineVariableValues').get())
return;
var executionContext = UI.context.flavor(SDK.ExecutionContext);
if (!executionContext)
return;
var callFrame = UI.context.flavor(SDK.DebuggerModel.CallFrame);
if (!callFrame)
return;
var localScope = callFrame.localScope();
var functionLocation = callFrame.functionLocation();
if (localScope && functionLocation) {
Sources.SourceMapNamesResolver.resolveScopeInObject(localScope)
.getAllProperties(false, this._prepareScopeVariables.bind(this, callFrame));
}
if (this._clearValueWidgetsTimer) {
clearTimeout(this._clearValueWidgetsTimer);
delete this._clearValueWidgetsTimer;
}
}
/**
* @param {!SDK.DebuggerModel.CallFrame} callFrame
* @param {?Array.<!SDK.RemoteObjectProperty>} properties
* @param {?Array.<!SDK.RemoteObjectProperty>} internalProperties
*/
_prepareScopeVariables(callFrame, properties, internalProperties) {
if (!properties || !properties.length || properties.length > 500) {
this._clearValueWidgets();
return;
}
var functionUILocation = Bindings.debuggerWorkspaceBinding.rawLocationToUILocation(
/** @type {!SDK.DebuggerModel.Location} */ (callFrame.functionLocation()));
var executionUILocation = Bindings.debuggerWorkspaceBinding.rawLocationToUILocation(callFrame.location());
if (functionUILocation.uiSourceCode !== this.uiSourceCode() ||
executionUILocation.uiSourceCode !== this.uiSourceCode()) {
this._clearValueWidgets();
return;
}
var fromLine = functionUILocation.lineNumber;
var fromColumn = functionUILocation.columnNumber;
var toLine = executionUILocation.lineNumber;
// Make sure we have a chance to update all existing widgets.
if (this._valueWidgets) {
for (var line of this._valueWidgets.keys())
toLine = Math.max(toLine, line + 1);
}
if (fromLine >= toLine || toLine - fromLine > 500 || fromLine < 0 || toLine >= this.textEditor.linesCount) {
this._clearValueWidgets();
return;
}
var valuesMap = new Map();
for (var property of properties)
valuesMap.set(property.name, property.value);
/** @type {!Map.<number, !Set<string>>} */
var namesPerLine = new Map();
var skipObjectProperty = false;
var tokenizer = new TextEditor.CodeMirrorUtils.TokenizerFactory().createTokenizer('text/javascript');
tokenizer(this.textEditor.line(fromLine).substring(fromColumn), processToken.bind(this, fromLine));
for (var i = fromLine + 1; i < toLine; ++i)
tokenizer(this.textEditor.line(i), processToken.bind(this, i));
/**
* @param {number} lineNumber
* @param {string} tokenValue
* @param {?string} tokenType
* @param {number} column
* @param {number} newColumn
* @this {Sources.JavaScriptSourceFrame}
*/
function processToken(lineNumber, tokenValue, tokenType, column, newColumn) {
if (!skipObjectProperty && tokenType && this._isIdentifier(tokenType) && valuesMap.get(tokenValue)) {
var names = namesPerLine.get(lineNumber);
if (!names) {
names = new Set();
namesPerLine.set(lineNumber, names);
}
names.add(tokenValue);
}
skipObjectProperty = tokenValue === '.';
}
this.textEditor.operation(this._renderDecorations.bind(this, valuesMap, namesPerLine, fromLine, toLine));
}
/**
* @param {!Map.<string,!SDK.RemoteObject>} valuesMap
* @param {!Map.<number, !Set<string>>} namesPerLine
* @param {number} fromLine
* @param {number} toLine
*/
_renderDecorations(valuesMap, namesPerLine, fromLine, toLine) {
var formatter = new Components.RemoteObjectPreviewFormatter();
for (var i = fromLine; i < toLine; ++i) {
var names = namesPerLine.get(i);
var oldWidget = this._valueWidgets.get(i);
if (!names) {
if (oldWidget) {
this._valueWidgets.delete(i);
this.textEditor.removeDecoration(oldWidget, i);
}
continue;
}
var widget = createElementWithClass('div', 'text-editor-value-decoration');
var base = this.textEditor.cursorPositionToCoordinates(i, 0);
var offset = this.textEditor.cursorPositionToCoordinates(i, this.textEditor.line(i).length);
var codeMirrorLinesLeftPadding = 4;
var left = offset.x - base.x + codeMirrorLinesLeftPadding;
widget.style.left = left + 'px';
widget.__nameToToken = new Map();
var renderedNameCount = 0;
for (var name of names) {
if (renderedNameCount > 10)
break;
if (namesPerLine.get(i - 1) && namesPerLine.get(i - 1).has(name))
continue; // Only render name once in the given continuous block.
if (renderedNameCount)
widget.createTextChild(', ');
var nameValuePair = widget.createChild('span');
widget.__nameToToken.set(name, nameValuePair);
nameValuePair.createTextChild(name + ' = ');
var value = valuesMap.get(name);
var propertyCount = value.preview ? value.preview.properties.length : 0;
var entryCount = value.preview && value.preview.entries ? value.preview.entries.length : 0;
if (value.preview && propertyCount + entryCount < 10)
formatter.appendObjectPreview(nameValuePair, value.preview);
else
nameValuePair.appendChild(Components.ObjectPropertiesSection.createValueElement(value, false));
++renderedNameCount;
}
var widgetChanged = true;
if (oldWidget) {
widgetChanged = false;
for (var name of widget.__nameToToken.keys()) {
var oldText = oldWidget.__nameToToken.get(name) ? oldWidget.__nameToToken.get(name).textContent : '';
var newText = widget.__nameToToken.get(name) ? widget.__nameToToken.get(name).textContent : '';
if (newText !== oldText) {
widgetChanged = true;
// value has changed, update it.
UI.runCSSAnimationOnce(
/** @type {!Element} */ (widget.__nameToToken.get(name)), 'source-frame-value-update-highlight');
}
}
if (widgetChanged) {
this._valueWidgets.delete(i);
this.textEditor.removeDecoration(oldWidget, i);
}
}
if (widgetChanged) {
this._valueWidgets.set(i, widget);
this.textEditor.addDecoration(widget, i);
}
}
}
clearExecutionLine() {
if (this.loaded && this._executionLocation)
this.textEditor.clearExecutionLine();
delete this._executionLocation;
this._clearValueWidgetsTimer = setTimeout(this._clearValueWidgets.bind(this), 1000);
}
_clearValueWidgets() {
delete this._clearValueWidgetsTimer;
for (var line of this._valueWidgets.keys())
this.textEditor.removeDecoration(this._valueWidgets.get(line), line);
this._valueWidgets.clear();
}
/**
* @return {boolean}
*/
_shouldIgnoreExternalBreakpointEvents() {
if (this._supportsEnabledBreakpointsWhileEditing())
return false;
if (this._muted)
return true;
var scriptFiles = this._scriptFileForTarget.valuesArray();
for (var i = 0; i < scriptFiles.length; ++i) {
if (scriptFiles[i].isDivergingFromVM() || scriptFiles[i].isMergingToVM())
return true;
}
return false;
}
_breakpointAdded(event) {
var uiLocation = /** @type {!Workspace.UILocation} */ (event.data.uiLocation);
if (uiLocation.uiSourceCode !== this.uiSourceCode())
return;
if (this._shouldIgnoreExternalBreakpointEvents())
return;
var breakpoint = /** @type {!Bindings.BreakpointManager.Breakpoint} */ (event.data.breakpoint);
if (this.loaded) {
this._addBreakpointDecoration(
uiLocation.lineNumber, uiLocation.columnNumber, breakpoint.condition(), breakpoint.enabled(), false);
}
}
_breakpointRemoved(event) {
var uiLocation = /** @type {!Workspace.UILocation} */ (event.data.uiLocation);
if (uiLocation.uiSourceCode !== this.uiSourceCode())
return;
if (this._shouldIgnoreExternalBreakpointEvents())
return;
var remainingBreakpoints = this._breakpointManager.findBreakpoints(this.uiSourceCode(), uiLocation.lineNumber);
if (!remainingBreakpoints.length && this.loaded)
this._removeBreakpointDecoration(uiLocation.lineNumber);
}
/**
* @param {!Common.Event} event
*/
_onSourceMappingChanged(event) {
var data = /** @type {{target: !SDK.Target}} */ (event.data);
this._updateScriptFile(data.target);
this._updateLinesWithoutMappingHighlight();
}
_updateLinesWithoutMappingHighlight() {
var linesCount = this.textEditor.linesCount;
for (var i = 0; i < linesCount; ++i) {
var lineHasMapping = Bindings.debuggerWorkspaceBinding.uiLineHasMapping(this.uiSourceCode(), i);
if (!lineHasMapping)
this._hasLineWithoutMapping = true;
if (this._hasLineWithoutMapping)
this.textEditor.toggleLineClass(i, 'cm-line-without-source-mapping', !lineHasMapping);
}
}
/**
* @param {!SDK.Target} target
*/
_updateScriptFile(target) {
var oldScriptFile = this._scriptFileForTarget.get(target);
var newScriptFile = Bindings.debuggerWorkspaceBinding.scriptFile(this.uiSourceCode(), target);
this._scriptFileForTarget.remove(target);
if (oldScriptFile) {
oldScriptFile.removeEventListener(Bindings.ResourceScriptFile.Events.DidMergeToVM, this._didMergeToVM, this);
oldScriptFile.removeEventListener(
Bindings.ResourceScriptFile.Events.DidDivergeFromVM, this._didDivergeFromVM, this);
if (this._muted && !this.uiSourceCode().isDirty())
this._restoreBreakpointsIfConsistentScripts();
}
if (newScriptFile)
this._scriptFileForTarget.set(target, newScriptFile);
this._updateDivergedInfobar();
if (newScriptFile) {
newScriptFile.addEventListener(Bindings.ResourceScriptFile.Events.DidMergeToVM, this._didMergeToVM, this);
newScriptFile.addEventListener(Bindings.ResourceScriptFile.Events.DidDivergeFromVM, this._didDivergeFromVM, this);
if (this.loaded)
newScriptFile.checkMapping();
if (newScriptFile.hasSourceMapURL()) {
var sourceMapInfobar = UI.Infobar.create(
UI.Infobar.Type.Info, Common.UIString('Source Map detected.'),
Common.settings.createSetting('sourceMapInfobarDisabled', false));
if (sourceMapInfobar) {
sourceMapInfobar.createDetailsRowMessage(Common.UIString(
'Associated files should be added to the file tree. You can debug these resolved source files as regular JavaScript files.'));
sourceMapInfobar.createDetailsRowMessage(Common.UIString(
'Associated files are available via file tree or %s.',
UI.shortcutRegistry.shortcutTitleForAction('sources.go-to-source')));
this.attachInfobars([sourceMapInfobar]);
}
}
}
}
/**
* @override
*/
onTextEditorContentSet() {
super.onTextEditorContentSet();
if (this._executionLocation)
this.setExecutionLocation(this._executionLocation);
var breakpointLocations = this._breakpointManager.breakpointLocationsForUISourceCode(this.uiSourceCode());
for (var i = 0; i < breakpointLocations.length; ++i)
this._breakpointAdded({data: breakpointLocations[i]});
var scriptFiles = this._scriptFileForTarget.valuesArray();
for (var i = 0; i < scriptFiles.length; ++i)
scriptFiles[i].checkMapping();
this._updateLinesWithoutMappingHighlight();
this._detectMinified();
}
_detectMinified() {
if (this._prettyPrintInfobar)
return;
var minified = false;
for (var i = 0; i < 10 && i < this.textEditor.linesCount; ++i) {
var line = this.textEditor.line(i);
if (line.startsWith('//#')) // mind source map.
continue;
if (line.length > 500) {
minified = true;
break;
}
}
if (!minified)
return;
this._prettyPrintInfobar = UI.Infobar.create(
UI.Infobar.Type.Info, Common.UIString('Pretty-print this minified file?'),
Common.settings.createSetting('prettyPrintInfobarDisabled', false));
if (!this._prettyPrintInfobar)
return;
this._prettyPrintInfobar.setCloseCallback(() => delete this._prettyPrintInfobar);
var toolbar = new UI.Toolbar('');
var button = new UI.ToolbarButton('', 'largeicon-pretty-print');
toolbar.appendToolbarItem(button);
toolbar.element.style.display = 'inline-block';
toolbar.element.style.verticalAlign = 'middle';
toolbar.element.style.marginBottom = '3px';
toolbar.element.style.pointerEvents = 'none';
var element = this._prettyPrintInfobar.createDetailsRowMessage();
element.appendChild(UI.formatLocalized(
'You can click the %s button on the bottom status bar, and continue debugging with the new formatted source.',
[toolbar.element]));
this.attachInfobars([this._prettyPrintInfobar]);
}
/**
* @param {!Common.Event} event
*/
_handleGutterClick(event) {
if (this._muted)
return;
var eventData = /** @type {!SourceFrame.SourcesTextEditor.GutterClickEventData} */ (event.data);
var lineNumber = eventData.lineNumber;
var eventObject = eventData.event;
if (eventObject.button !== 0 || eventObject.altKey || eventObject.ctrlKey || eventObject.metaKey)
return;
this._toggleBreakpoint(lineNumber, eventObject.shiftKey);
eventObject.consume(true);
}
/**
* @param {number} lineNumber
* @param {boolean} onlyDisable
*/
_toggleBreakpoint(lineNumber, onlyDisable) {
var breakpoints = this._breakpointManager.findBreakpoints(this.uiSourceCode(), lineNumber);
if (breakpoints.length) {
if (onlyDisable)
breakpoints[0].setEnabled(!breakpoints[0].enabled());
else
breakpoints[0].remove();
} else {
this._createNewBreakpoint(lineNumber, '', true);
}
}
/**
* @param {number} lineNumber
* @param {string} condition
* @param {boolean} enabled
*/
_createNewBreakpoint(lineNumber, condition, enabled) {
findPossibleBreakpoints.call(this, lineNumber)
.then(checkNextLineIfNeeded.bind(this, lineNumber, 4))
.then(setBreakpoint.bind(this));
/**
* @this {!Sources.JavaScriptSourceFrame}
* @param {number} lineNumber
* @return {!Promise<?Array<!Workspace.UILocation>>}
*/
function findPossibleBreakpoints(lineNumber) {
const maxLengthToCheck = 1024;
if (lineNumber >= this._textEditor.linesCount)
return Promise.resolve(/** @type {?Array<!Workspace.UILocation>} */ ([]));
if (this._textEditor.line(lineNumber).length >= maxLengthToCheck)
return Promise.resolve(/** @type {?Array<!Workspace.UILocation>} */ ([]));
return this._breakpointManager
.possibleBreakpoints(this.uiSourceCode(), new Common.TextRange(lineNumber, 0, lineNumber + 1, 0))
.then(locations => locations.length ? locations : null);
}
/**
* @this {!Sources.JavaScriptSourceFrame}
* @param {number} currentLineNumber
* @param {number} linesToCheck
* @param {?Array<!Workspace.UILocation>} locations
* @return {!Promise<?Array<!Workspace.UILocation>>}
*/
function checkNextLineIfNeeded(currentLineNumber, linesToCheck, locations) {
if (locations || linesToCheck <= 0)
return Promise.resolve(locations);
return findPossibleBreakpoints.call(this, currentLineNumber + 1)
.then(checkNextLineIfNeeded.bind(this, currentLineNumber + 1, linesToCheck - 1));
}
/**
* @this {!Sources.JavaScriptSourceFrame}
* @param {?Array<!Workspace.UILocation>} locations
*/
function setBreakpoint(locations) {
if (!locations || !locations.length)
this._setBreakpoint(lineNumber, 0, condition, enabled);
else
this._setBreakpoint(locations[0].lineNumber, locations[0].columnNumber, condition, enabled);
Host.userMetrics.actionTaken(Host.UserMetrics.Action.ScriptsBreakpointSet);
}
}
toggleBreakpointOnCurrentLine() {
if (this._muted)
return;
var selection = this.textEditor.selection();
if (!selection)
return;
this._toggleBreakpoint(selection.startLine, false);
}
/**
* @param {number} lineNumber
* @param {number} columnNumber
* @param {string} condition
* @param {boolean} enabled
*/
_setBreakpoint(lineNumber, columnNumber, condition, enabled) {
if (!Bindings.debuggerWorkspaceBinding.uiLineHasMapping(this.uiSourceCode(), lineNumber))
return;
this._breakpointManager.setBreakpoint(this.uiSourceCode(), lineNumber, columnNumber, condition, enabled);
this._breakpointWasSetForTest(lineNumber, columnNumber, condition, enabled);
}
/**
* @param {number} lineNumber
* @param {number} columnNumber
* @param {string} condition
* @param {boolean} enabled
*/
_breakpointWasSetForTest(lineNumber, columnNumber, condition, enabled) {
}
/**
* @override
*/
dispose() {
this._breakpointManager.removeEventListener(
Bindings.BreakpointManager.Events.BreakpointAdded, this._breakpointAdded, this);
this._breakpointManager.removeEventListener(
Bindings.BreakpointManager.Events.BreakpointRemoved, this._breakpointRemoved, this);
this.uiSourceCode().removeEventListener(
Workspace.UISourceCode.Events.SourceMappingChanged, this._onSourceMappingChanged, this);
this.uiSourceCode().removeEventListener(
Workspace.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this);
this.uiSourceCode().removeEventListener(
Workspace.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this);
this.uiSourceCode().removeEventListener(
Workspace.UISourceCode.Events.TitleChanged, this._showBlackboxInfobarIfNeeded, this);
Common.moduleSetting('skipStackFramesPattern').removeChangeListener(this._showBlackboxInfobarIfNeeded, this);
Common.moduleSetting('skipContentScripts').removeChangeListener(this._showBlackboxInfobarIfNeeded, this);
super.dispose();
}
};