chrome-devtools-frontend
Version:
Chrome DevTools UI
1,298 lines (1,193 loc) • 83.6 kB
JavaScript
/*
* 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.
*/
import * as Bindings from '../bindings/bindings.js';
import * as Common from '../common/common.js';
import * as Host from '../host/host.js';
import * as i18n from '../i18n/i18n.js';
import * as ObjectUI from '../object_ui/object_ui.js';
import * as Root from '../root/root.js';
import * as SDK from '../sdk/sdk.js';
import * as SourceFrame from '../source_frame/source_frame.js';
import * as TextEditor from '../text_editor/text_editor.js';
import * as TextUtils from '../text_utils/text_utils.js';
import * as UI from '../ui/ui.js';
import * as Workspace from '../workspace/workspace.js';
import {AddSourceMapURLDialog} from './AddSourceMapURLDialog.js';
import {BreakpointEditDialog, LogpointPrefix} from './BreakpointEditDialog.js';
import {Plugin} from './Plugin.js';
import {ScriptFormatterEditorAction} from './ScriptFormatterEditorAction.js';
import {resolveExpression, resolveScopeInObject} from './SourceMapNamesResolver.js';
import {SourcesPanel} from './SourcesPanel.js';
import {EditorAction} from './SourcesView.js';
export const UIStrings = {
/**
*@description Text in Debugger Plugin of the Sources panel
*/
thisScriptIsOnTheDebuggersIgnore: 'This script is on the debugger\'s ignore list',
/**
*@description Text to stop preventing the debugger from stepping into library code
*/
removeFromIgnoreList: 'Remove from ignore list',
/**
*@description Text of a button in the Sources panel Debugger Plugin to configure ignore listing in Settings
*/
configure: 'Configure',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
sourceMapFoundButIgnoredForFile: 'Source map found, but ignored for file on ignore list.',
/**
*@description Text to add a breakpoint
*/
addBreakpoint: 'Add breakpoint',
/**
*@description A context menu item in the Debugger Plugin of the Sources panel
*/
addConditionalBreakpoint: 'Add conditional breakpoint…',
/**
*@description A context menu item in the Debugger Plugin of the Sources panel
*/
addLogpoint: 'Add logpoint…',
/**
*@description A context menu item in the Debugger Plugin of the Sources panel
*/
neverPauseHere: 'Never pause here',
/**
*@description Text to remove a breakpoint
*/
removeBreakpoint: 'Remove breakpoint',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
removeAllBreakpointsInLine: 'Remove all breakpoints in line',
/**
*@description A context menu item in the Debugger Plugin of the Sources panel
*/
editBreakpoint: 'Edit breakpoint…',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
disableBreakpoint: 'Disable breakpoint',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
disableAllBreakpointsInLine: 'Disable all breakpoints in line',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
enableBreakpoint: 'Enable breakpoint',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
enableAllBreakpointsInLine: 'Enable all breakpoints in line',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
addSourceMap: 'Add source map…',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
sourceMapDetected: 'Source Map detected.',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
prettyprintThisMinifiedFile: 'Pretty-print this minified file?',
/**
*@description Label of a button in the Sources panel to pretty-print the current file
*/
prettyprint: 'Pretty-print',
/**
*@description Text in Debugger Plugin pretty-print details message of the Sources panel
*@example {Debug} PH1
*/
prettyprintingWillFormatThisFile:
'Pretty-printing will format this file in a new tab where you can continue debugging. You can also pretty-print this file by clicking the {PH1} button on the bottom status bar.',
/**
*@description Title of the Filtered List WidgetProvider of Quick Open
*@example {Ctrl+P Ctrl+O} PH1
*/
associatedFilesAreAvailable: 'Associated files are available via file tree or {PH1}.',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
associatedFilesShouldBeAdded:
'Associated files should be added to the file tree. You can debug these resolved source files as regular JavaScript files.',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
theDebuggerWillSkipStepping: 'The debugger will skip stepping through this script, and will not stop on exceptions.'
};
const str_ = i18n.i18n.registerUIStrings('sources/DebuggerPlugin.js', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
// eslint-disable-next-line no-unused-vars
class DecoratorWidget extends HTMLDivElement {
constructor() {
super();
/** @type {!Map<string,!HTMLElement>} */
this.__nameToToken;
}
}
export class DebuggerPlugin extends Plugin {
/**
* @param {!SourceFrame.SourcesTextEditor.SourcesTextEditor} textEditor
* @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode
* @param {!SourceFrame.SourceFrame.Transformer} transformer
*/
constructor(textEditor, uiSourceCode, transformer) {
super();
this._textEditor = textEditor;
this._uiSourceCode = uiSourceCode;
this._transformer = transformer;
/** @type {?Workspace.UISourceCode.UILocation} */
this._executionLocation = null;
this._controlDown = false;
/** @type {?number} */
this._asyncStepInHoveredLine = 0;
this._asyncStepInHovered = false;
/** @type {?number} */
this._clearValueWidgetsTimer = null;
/** @type {?UI.Infobar.Infobar} */
this._sourceMapInfobar = null;
/** @type {?number} */
this._controlTimeout = null;
this._scriptsPanel = SourcesPanel.instance();
this._breakpointManager = Bindings.BreakpointManager.BreakpointManager.instance();
if (uiSourceCode.project().type() === Workspace.Workspace.projectTypes.Debugger) {
this._textEditor.element.classList.add('source-frame-debugger-script');
}
this._popoverHelper =
new UI.PopoverHelper.PopoverHelper(this._scriptsPanel.element, this._getPopoverRequest.bind(this));
this._popoverHelper.setDisableOnClick(true);
this._popoverHelper.setTimeout(250, 250);
this._popoverHelper.setHasPadding(true);
this._boundPopoverHelperHide = this._popoverHelper.hidePopover.bind(this._popoverHelper);
this._scriptsPanel.element.addEventListener('scroll', this._boundPopoverHelperHide, true);
const shortcutHandlers = {
'debugger.toggle-breakpoint': async () => {
const selection = this._textEditor.selection();
if (!selection) {
return false;
}
await this._toggleBreakpoint(selection.startLine, false);
return true;
},
'debugger.toggle-breakpoint-enabled': async () => {
const selection = this._textEditor.selection();
if (!selection) {
return false;
}
await this._toggleBreakpoint(selection.startLine, true);
return true;
},
'debugger.breakpoint-input-window': async () => {
const selection = this._textEditor.selection();
if (!selection) {
return false;
}
const breakpoints = this._lineBreakpointDecorations(selection.startLine)
.map(decoration => decoration.breakpoint)
.filter(breakpoint => Boolean(breakpoint));
let breakpoint = null;
if (breakpoints.length) {
breakpoint = breakpoints[0];
}
const isLogpoint = breakpoint ? breakpoint.condition().includes(LogpointPrefix) : false;
this._editBreakpointCondition(selection.startLine, breakpoint, null, isLogpoint);
return true;
}
};
UI.ShortcutRegistry.ShortcutRegistry.instance().addShortcutListener(this._textEditor.element, shortcutHandlers);
this._boundKeyDown = /** @type {function(!Event): void} */ (this._onKeyDown.bind(this));
this._textEditor.element.addEventListener('keydown', this._boundKeyDown, true);
this._boundKeyUp = /** @type {function(!Event): void} */ (this._onKeyUp.bind(this));
this._textEditor.element.addEventListener('keyup', this._boundKeyUp, true);
this._boundMouseMove = /** @type {function(!Event): void} */ (this._onMouseMove.bind(this));
this._textEditor.element.addEventListener('mousemove', this._boundMouseMove, false);
this._boundMouseDown = /** @type {function(!Event): void} */ (this._onMouseDown.bind(this));
this._textEditor.element.addEventListener('mousedown', this._boundMouseDown, true);
this._boundBlur = /** @type {function(!Event): void} */ (this._onBlur.bind(this));
this._textEditor.element.addEventListener('focusout', this._boundBlur, false);
this._boundWheel = /** @type {function(!Event): void} */ (this._onWheel.bind(this));
this._textEditor.element.addEventListener('wheel', this._boundWheel, true);
this._boundGutterClick =
/** @type {function(!Common.EventTarget.EventTargetEvent): void} */ (e => {
this._handleGutterClick(e);
});
this._textEditor.addEventListener(SourceFrame.SourcesTextEditor.Events.GutterClick, this._boundGutterClick, 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.WorkingCopyChanged, this._workingCopyChanged, this);
this._uiSourceCode.addEventListener(
Workspace.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this);
/** @type {!Set<!BreakpointDecoration>} */
this._breakpointDecorations = new Set();
/** @type {!Map<!Bindings.BreakpointManager.Breakpoint, !BreakpointDecoration>} */
this._decorationByBreakpoint = new Map();
/** @type {!Set<number>} */
this._possibleBreakpointsRequested = new Set();
/** @type {!Map.<!SDK.DebuggerModel.DebuggerModel, !Bindings.ResourceScriptMapping.ResourceScriptFile>}*/
this._scriptFileForDebuggerModel = new Map();
Common.Settings.Settings.instance()
.moduleSetting('skipStackFramesPattern')
.addChangeListener(this._showIgnoreListInfobarIfNeeded, this);
Common.Settings.Settings.instance()
.moduleSetting('skipContentScripts')
.addChangeListener(this._showIgnoreListInfobarIfNeeded, this);
/** @type {!Map.<number, !DecoratorWidget>} */
this._valueWidgets = new Map();
/** @type {?Map<!CodeMirror.TextMarker<!CodeMirror.MarkerRange>, !Function>} */
this._continueToLocationDecorations = null;
UI.Context.Context.instance().addFlavorChangeListener(SDK.DebuggerModel.CallFrame, this._callFrameChanged, this);
this._liveLocationPool = new Bindings.LiveLocation.LiveLocationPool();
this._callFrameChanged();
this._updateScriptFiles();
if (this._uiSourceCode.isDirty()) {
this._muted = true;
this._mutedFromStart = true;
} else {
this._muted = false;
this._mutedFromStart = false;
this._initializeBreakpoints();
}
/** @type {?UI.Infobar.Infobar} */
this._ignoreListInfobar = null;
this._showIgnoreListInfobarIfNeeded();
for (const scriptFile of this._scriptFileForDebuggerModel.values()) {
scriptFile.checkMapping();
}
this._hasLineWithoutMapping = false;
this._updateLinesWithoutMappingHighlight();
if (!Root.Runtime.experiments.isEnabled('sourcesPrettyPrint')) {
/** @type {?UI.Infobar.Infobar} */
this._prettyPrintInfobar = null;
this._detectMinified();
}
}
/**
* @override
* @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode
* @return {boolean}
*/
static accepts(uiSourceCode) {
return uiSourceCode.contentType().hasScripts();
}
_showIgnoreListInfobarIfNeeded() {
const uiSourceCode = this._uiSourceCode;
if (!uiSourceCode.contentType().hasScripts()) {
return;
}
const projectType = uiSourceCode.project().type();
if (!Bindings.IgnoreListManager.IgnoreListManager.instance().isIgnoreListedUISourceCode(uiSourceCode)) {
this._hideIgnoreListInfobar();
return;
}
if (this._ignoreListInfobar) {
this._ignoreListInfobar.dispose();
}
function unIgnoreList() {
Bindings.IgnoreListManager.IgnoreListManager.instance().unIgnoreListUISourceCode(uiSourceCode);
if (projectType === Workspace.Workspace.projectTypes.ContentScripts) {
Bindings.IgnoreListManager.IgnoreListManager.instance().unIgnoreListContentScripts();
}
}
const infobar =
new UI.Infobar.Infobar(UI.Infobar.Type.Warning, i18nString(UIStrings.thisScriptIsOnTheDebuggersIgnore), [
{text: i18nString(UIStrings.removeFromIgnoreList), highlight: false, delegate: unIgnoreList, dismiss: true}, {
text: i18nString(UIStrings.configure),
highlight: false,
delegate:
UI.ViewManager.ViewManager.instance().showView.bind(UI.ViewManager.ViewManager.instance(), 'blackbox'),
dismiss: false
}
]);
this._ignoreListInfobar = infobar;
infobar.createDetailsRowMessage(i18nString(UIStrings.theDebuggerWillSkipStepping));
const scriptFile =
this._scriptFileForDebuggerModel.size ? this._scriptFileForDebuggerModel.values().next().value : null;
if (scriptFile && scriptFile.hasSourceMapURL()) {
infobar.createDetailsRowMessage(i18nString(UIStrings.sourceMapFoundButIgnoredForFile));
}
this._textEditor.attachInfobar(this._ignoreListInfobar);
}
_hideIgnoreListInfobar() {
if (!this._ignoreListInfobar) {
return;
}
this._ignoreListInfobar.dispose();
this._ignoreListInfobar = null;
}
/**
* @override
*/
wasShown() {
if (this._executionLocation) {
// We need SourcesTextEditor to be initialized prior to this call. @see crbug.com/499889
queueMicrotask(() => {
this._generateValuesInSource();
});
}
}
/**
* @override
*/
willHide() {
this._popoverHelper.hidePopover();
}
/**
* @override
* @param {!UI.ContextMenu.ContextMenu} contextMenu
* @param {number} editorLineNumber
* @return {!Promise<void>}
*/
async populateLineGutterContextMenu(contextMenu, editorLineNumber) {
const uiLocation = new Workspace.UISourceCode.UILocation(this._uiSourceCode, editorLineNumber, 0);
this._scriptsPanel.appendUILocationItems(contextMenu, uiLocation);
const breakpoints =
/** @type {!Array<!Bindings.BreakpointManager.Breakpoint>} */ (this._lineBreakpointDecorations(editorLineNumber)
.map(decoration => decoration.breakpoint)
.filter(breakpoint => Boolean(breakpoint)));
const supportsConditionalBreakpoints =
Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().supportsConditionalBreakpoints(
this._uiSourceCode);
if (!breakpoints.length) {
if (!this._textEditor.hasLineClass(editorLineNumber, 'cm-non-breakable-line')) {
contextMenu.debugSection().appendItem(
i18nString(UIStrings.addBreakpoint), this._createNewBreakpoint.bind(this, editorLineNumber, '', true));
if (supportsConditionalBreakpoints) {
contextMenu.debugSection().appendItem(
i18nString(UIStrings.addConditionalBreakpoint),
this._editBreakpointCondition.bind(this, editorLineNumber, null, null, false /* preferLogpoint */));
contextMenu.debugSection().appendItem(
i18nString(UIStrings.addLogpoint),
this._editBreakpointCondition.bind(this, editorLineNumber, null, null, true /* preferLogpoint */));
contextMenu.debugSection().appendItem(
i18nString(UIStrings.neverPauseHere),
this._createNewBreakpoint.bind(this, editorLineNumber, 'false', true));
}
}
} else {
const hasOneBreakpoint = breakpoints.length === 1;
const removeTitle =
hasOneBreakpoint ? i18nString(UIStrings.removeBreakpoint) : i18nString(UIStrings.removeAllBreakpointsInLine);
contextMenu.debugSection().appendItem(removeTitle, () => breakpoints.map(breakpoint => breakpoint.remove(false)));
if (hasOneBreakpoint && supportsConditionalBreakpoints) {
// Editing breakpoints only make sense for conditional breakpoints
// and logpoints and both are currently only available for JavaScript
// debugging.
contextMenu.debugSection().appendItem(
i18nString(UIStrings.editBreakpoint),
this._editBreakpointCondition.bind(
this, editorLineNumber, breakpoints[0], null, false /* preferLogpoint */));
}
const hasEnabled = breakpoints.some(breakpoint => breakpoint.enabled());
if (hasEnabled) {
const title = hasOneBreakpoint ? i18nString(UIStrings.disableBreakpoint) :
i18nString(UIStrings.disableAllBreakpointsInLine);
contextMenu.debugSection().appendItem(title, () => breakpoints.map(breakpoint => breakpoint.setEnabled(false)));
}
const hasDisabled = breakpoints.some(breakpoint => !breakpoint.enabled());
if (hasDisabled) {
const title = hasOneBreakpoint ? i18nString(UIStrings.enableBreakpoint) :
i18nString(UIStrings.enableAllBreakpointsInLine);
contextMenu.debugSection().appendItem(title, () => breakpoints.map(breakpoint => breakpoint.setEnabled(true)));
}
}
}
/**
* @override
* @param {!UI.ContextMenu.ContextMenu} contextMenu
* @param {number} editorLineNumber
* @param {number} editorColumnNumber
* @return {!Promise<void>}
*/
populateTextAreaContextMenu(contextMenu, editorLineNumber, editorColumnNumber) {
/**
* @param {!Bindings.ResourceScriptMapping.ResourceScriptFile} scriptFile
*/
function addSourceMapURL(scriptFile) {
const dialog = new AddSourceMapURLDialog(addSourceMapURLDialogCallback.bind(null, scriptFile));
dialog.show();
}
/**
* @param {!Bindings.ResourceScriptMapping.ResourceScriptFile} scriptFile
* @param {string} url
*/
function addSourceMapURLDialogCallback(scriptFile, url) {
if (!url) {
return;
}
scriptFile.addSourceMapURL(url);
}
/**
* @this {DebuggerPlugin}
*/
function populateSourceMapMembers() {
if (this._uiSourceCode.project().type() === Workspace.Workspace.projectTypes.Network &&
Common.Settings.Settings.instance().moduleSetting('jsSourceMapsEnabled').get() &&
!Bindings.IgnoreListManager.IgnoreListManager.instance().isIgnoreListedUISourceCode(this._uiSourceCode)) {
if (this._scriptFileForDebuggerModel.size) {
const scriptFile = this._scriptFileForDebuggerModel.values().next().value;
const addSourceMapURLLabel = i18nString(UIStrings.addSourceMap);
contextMenu.debugSection().appendItem(addSourceMapURLLabel, addSourceMapURL.bind(null, scriptFile));
}
}
}
return super.populateTextAreaContextMenu(contextMenu, editorLineNumber, editorColumnNumber)
.then(populateSourceMapMembers.bind(this));
}
_workingCopyChanged() {
if (this._scriptFileForDebuggerModel.size) {
return;
}
if (this._uiSourceCode.isDirty()) {
this._muteBreakpointsWhileEditing();
} else {
this._restoreBreakpointsAfterEditing();
}
}
/**
* @param {!Common.EventTarget.EventTargetEvent} event
*/
_workingCopyCommitted(event) {
this._scriptsPanel.updateLastModificationTime();
if (!this._scriptFileForDebuggerModel.size) {
this._restoreBreakpointsAfterEditing();
}
}
_didMergeToVM() {
this._restoreBreakpointsIfConsistentScripts();
}
_didDivergeFromVM() {
this._muteBreakpointsWhileEditing();
}
_muteBreakpointsWhileEditing() {
if (this._muted) {
return;
}
for (const decoration of this._breakpointDecorations) {
this._updateBreakpointDecoration(decoration);
}
this._muted = true;
}
async _restoreBreakpointsIfConsistentScripts() {
for (const scriptFile of this._scriptFileForDebuggerModel.values()) {
if (scriptFile.hasDivergedFromVM() || scriptFile.isMergingToVM()) {
return;
}
}
await this._restoreBreakpointsAfterEditing();
}
async _restoreBreakpointsAfterEditing() {
this._muted = false;
if (this._mutedFromStart) {
this._mutedFromStart = false;
this._initializeBreakpoints();
return;
}
const decorations = Array.from(this._breakpointDecorations);
this._breakpointDecorations.clear();
this._textEditor.operation(() => decorations.map(decoration => decoration.hide()));
for (const decoration of decorations) {
if (!decoration.breakpoint) {
continue;
}
const enabled = decoration.enabled;
decoration.breakpoint.remove(false);
const location = decoration.handle.resolve();
if (location) {
await this._setBreakpoint(location.lineNumber, location.columnNumber, decoration.condition, enabled);
}
}
}
/**
* @param {string} tokenType
* @return {boolean}
*/
_isIdentifier(tokenType) {
return tokenType.startsWith('js-variable') || tokenType.startsWith('js-property') || tokenType === 'js-def' ||
tokenType === 'variable';
}
/**
* @param {!MouseEvent} event
* @return {?UI.PopoverHelper.PopoverRequest}
*/
_getPopoverRequest(event) {
if (UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlOrMeta(event)) {
return null;
}
const target = UI.Context.Context.instance().flavor(SDK.SDKModel.Target);
const debuggerModel = target ? target.model(SDK.DebuggerModel.DebuggerModel) : null;
if (!debuggerModel || !debuggerModel.isPaused()) {
return null;
}
const textPosition = this._textEditor.coordinatesToCursorPosition(event.x, event.y);
if (!textPosition) {
return null;
}
const mouseLine = textPosition.startLine;
const mouseColumn = textPosition.startColumn;
const textSelection = this._textEditor.selection().normalize();
let anchorBox;
let editorLineNumber = -1;
let startHighlight = -1;
let endHighlight = -1;
const selectedCallFrame =
/** @type {!SDK.DebuggerModel.CallFrame} */ (UI.Context.Context.instance().flavor(SDK.DebuggerModel.CallFrame));
if (!selectedCallFrame) {
return null;
}
if (textSelection && !textSelection.isEmpty()) {
if (textSelection.startLine !== textSelection.endLine || textSelection.startLine !== mouseLine ||
mouseColumn < textSelection.startColumn || mouseColumn > textSelection.endColumn) {
return null;
}
const leftCorner =
this._textEditor.cursorPositionToCoordinates(textSelection.startLine, textSelection.startColumn);
const rightCorner = this._textEditor.cursorPositionToCoordinates(textSelection.endLine, textSelection.endColumn);
if (!leftCorner) {
throw new Error('Expected leftCorner to not be null.');
}
if (!rightCorner) {
throw new Error('Expected rightCorner to not be null.');
}
anchorBox = new AnchorBox(leftCorner.x, leftCorner.y, rightCorner.x - leftCorner.x, leftCorner.height);
editorLineNumber = textSelection.startLine;
startHighlight = textSelection.startColumn;
endHighlight = textSelection.endColumn - 1;
} else if (this._uiSourceCode.mimeType() === 'application/wasm') {
const token = this._textEditor.tokenAtTextPosition(textPosition.startLine, textPosition.startColumn);
if (!token || token.type !== 'variable-2') {
return null;
}
const leftCorner = this._textEditor.cursorPositionToCoordinates(textPosition.startLine, token.startColumn);
const rightCorner = this._textEditor.cursorPositionToCoordinates(textPosition.startLine, token.endColumn - 1);
if (!leftCorner) {
throw new Error('Expected leftCorner to not be null.');
}
if (!rightCorner) {
throw new Error('Expected rightCorner to not be null.');
}
anchorBox = new AnchorBox(leftCorner.x, leftCorner.y, rightCorner.x - leftCorner.x, leftCorner.height);
editorLineNumber = textPosition.startLine;
startHighlight = token.startColumn;
endHighlight = token.endColumn - 1;
} else {
const token = this._textEditor.tokenAtTextPosition(textPosition.startLine, textPosition.startColumn);
if (!token || !token.type) {
return null;
}
editorLineNumber = textPosition.startLine;
const line = this._textEditor.line(editorLineNumber);
const tokenContent = line.substring(token.startColumn, token.endColumn);
const isIdentifier = this._isIdentifier(token.type);
if (!isIdentifier && (token.type !== 'js-keyword' || tokenContent !== 'this')) {
return null;
}
const leftCorner = this._textEditor.cursorPositionToCoordinates(editorLineNumber, token.startColumn);
const rightCorner = this._textEditor.cursorPositionToCoordinates(editorLineNumber, token.endColumn - 1);
if (!leftCorner) {
throw new Error('Expected leftCorner to not be null.');
}
if (!rightCorner) {
throw new Error('Expected rightCorner to not be null.');
}
anchorBox = new AnchorBox(leftCorner.x, leftCorner.y, rightCorner.x - leftCorner.x, leftCorner.height);
startHighlight = token.startColumn;
endHighlight = token.endColumn - 1;
while (startHighlight > 1 && line.charAt(startHighlight - 1) === '.') {
const tokenBefore = this._textEditor.tokenAtTextPosition(editorLineNumber, startHighlight - 2);
if (!tokenBefore || !tokenBefore.type) {
return null;
}
if (tokenBefore.type === 'js-meta') {
break;
}
if (tokenBefore.type === 'js-string-2') {
// If we hit a template literal, find the opening ` in this line.
// TODO(bmeurer): We should eventually replace this tokenization
// approach with a proper soluation based on parsing, maybe reusing
// the Parser and AST inside V8 for this (or potentially relying on
// acorn to do the job).
if (tokenBefore.endColumn < 2) {
return null;
}
startHighlight = line.lastIndexOf('`', tokenBefore.endColumn - 2);
if (startHighlight < 0) {
return null;
}
break;
}
startHighlight = tokenBefore.startColumn;
}
}
/** @type {?ObjectUI.ObjectPopoverHelper.ObjectPopoverHelper} */
let objectPopoverHelper;
/** @type {?CodeMirror.TextMarker} */
let highlightDescriptor;
/**
* @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode
* @param {string} evaluationText
* @return {!Promise<?SDK.RuntimeModel.EvaluationResult>}
*/
async function evaluate(uiSourceCode, evaluationText) {
const resolvedText = await resolveExpression(
selectedCallFrame, evaluationText, uiSourceCode, editorLineNumber, startHighlight, endHighlight);
return await selectedCallFrame.evaluate({
expression: resolvedText || evaluationText,
objectGroup: 'popover',
includeCommandLineAPI: false,
silent: true,
returnByValue: false,
generatePreview: false,
throwOnSideEffect: undefined,
timeout: undefined,
disableBreaks: undefined,
replMode: undefined,
allowUnsafeEvalBlockedByCSP: undefined
});
}
return {
box: anchorBox,
show: async popover => {
const evaluationText = this._textEditor.line(editorLineNumber).substring(startHighlight, endHighlight + 1);
const result = await evaluate(this._uiSourceCode, evaluationText);
if (!result || 'error' in result || !result.object ||
(result.object.type === 'object' && result.object.subtype === 'error')) {
return false;
}
objectPopoverHelper =
await ObjectUI.ObjectPopoverHelper.ObjectPopoverHelper.buildObjectPopover(result.object, popover);
const potentiallyUpdatedCallFrame = UI.Context.Context.instance().flavor(SDK.DebuggerModel.CallFrame);
if (!objectPopoverHelper || selectedCallFrame !== potentiallyUpdatedCallFrame) {
debuggerModel.runtimeModel().releaseObjectGroup('popover');
if (objectPopoverHelper) {
objectPopoverHelper.dispose();
}
return false;
}
const highlightRange =
new TextUtils.TextRange.TextRange(editorLineNumber, startHighlight, editorLineNumber, endHighlight);
highlightDescriptor = this._textEditor.highlightRange(highlightRange, 'source-frame-eval-expression');
return true;
},
hide: () => {
if (objectPopoverHelper) {
objectPopoverHelper.dispose();
}
debuggerModel.runtimeModel().releaseObjectGroup('popover');
if (highlightDescriptor) {
this._textEditor.removeHighlight(highlightDescriptor);
}
}
};
}
/**
* @param {!WheelEvent} event
*/
_onWheel(event) {
if (this._executionLocation && UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlOrMeta(event)) {
event.preventDefault();
}
}
/**
* @param {!KeyboardEvent} event
*/
_onKeyDown(event) {
if (!event.ctrlKey || (!event.metaKey && Host.Platform.isMac())) {
this._clearControlDown();
}
if (event.key === 'Escape') {
if (this._popoverHelper.isPopoverVisible()) {
this._popoverHelper.hidePopover();
event.consume();
}
return;
}
if (UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlOrMeta(event) && this._executionLocation) {
this._controlDown = true;
if (event.key === (Host.Platform.isMac() ? 'Meta' : 'Control')) {
this._controlTimeout = window.setTimeout(() => {
if (this._executionLocation && this._controlDown) {
this._showContinueToLocations();
}
}, 150);
}
}
}
/**
* @param {!MouseEvent} event
*/
_onMouseMove(event) {
if (this._executionLocation && this._controlDown &&
UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlOrMeta(event)) {
if (!this._continueToLocationDecorations) {
this._showContinueToLocations();
}
}
if (this._continueToLocationDecorations) {
const target = /** @type {!Element} */ (event.target);
const textPosition = this._textEditor.coordinatesToCursorPosition(event.x, event.y);
const hovering = Boolean(target.enclosingNodeOrSelfWithClass('source-frame-async-step-in'));
this._setAsyncStepInHoveredLine(textPosition ? textPosition.startLine : null, hovering);
}
}
/**
* @param {?number} editorLineNumber
* @param {boolean} hovered
*/
_setAsyncStepInHoveredLine(editorLineNumber, hovered) {
if (this._asyncStepInHoveredLine === editorLineNumber && this._asyncStepInHovered === hovered) {
return;
}
if (this._asyncStepInHovered && this._asyncStepInHoveredLine) {
this._textEditor.toggleLineClass(this._asyncStepInHoveredLine, 'source-frame-async-step-in-hovered', false);
}
this._asyncStepInHoveredLine = editorLineNumber;
this._asyncStepInHovered = hovered;
if (this._asyncStepInHovered && this._asyncStepInHoveredLine) {
this._textEditor.toggleLineClass(this._asyncStepInHoveredLine, 'source-frame-async-step-in-hovered', true);
}
}
/**
* @param {!MouseEvent} event
*/
_onMouseDown(event) {
if (!this._executionLocation || !UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlOrMeta(event)) {
return;
}
if (!this._continueToLocationDecorations) {
return;
}
event.consume();
const textPosition = this._textEditor.coordinatesToCursorPosition(event.x, event.y);
if (!textPosition) {
return;
}
for (const decoration of this._continueToLocationDecorations.keys()) {
const range = decoration.find();
if (!range) {
continue;
}
if (range.from.line !== textPosition.startLine || range.to.line !== textPosition.startLine) {
continue;
}
if (range.from.ch <= textPosition.startColumn && textPosition.startColumn <= range.to.ch) {
const callback = this._continueToLocationDecorations.get(decoration);
if (!callback) {
throw new Error('Expected a function');
}
callback();
break;
}
}
}
/**
* @param {!Event} event
*/
_onBlur(event) {
this._clearControlDown();
}
/**
* @param {!KeyboardEvent} event
*/
_onKeyUp(event) {
this._clearControlDown();
}
_clearControlDown() {
this._controlDown = false;
this._clearContinueToLocations();
if (this._controlTimeout) {
clearTimeout(this._controlTimeout);
}
}
/**
* @param {number} editorLineNumber
* @param {?Bindings.BreakpointManager.Breakpoint} breakpoint
* @param {?{lineNumber: number, columnNumber: number}} location
* @param {boolean=} preferLogpoint
*/
async _editBreakpointCondition(editorLineNumber, breakpoint, location, preferLogpoint) {
const oldCondition = breakpoint ? breakpoint.condition() : '';
const decorationElement = document.createElement('div');
const dialog = new BreakpointEditDialog(editorLineNumber, oldCondition, Boolean(preferLogpoint), async result => {
dialog.detach();
this._textEditor.removeDecoration(decorationElement, editorLineNumber);
if (!result.committed) {
return;
}
if (breakpoint) {
breakpoint.setCondition(result.condition);
} else if (location) {
await this._setBreakpoint(location.lineNumber, location.columnNumber, result.condition, true);
} else {
await this._createNewBreakpoint(editorLineNumber, result.condition, true);
}
});
this._textEditor.addDecoration(decorationElement, editorLineNumber);
dialog.markAsExternallyManaged();
dialog.show(decorationElement);
}
/**
* @param {!Bindings.LiveLocation.LiveLocation} liveLocation
*/
async _executionLineChanged(liveLocation) {
this._clearExecutionLine();
const uiLocation = await liveLocation.uiLocation();
if (!uiLocation || uiLocation.uiSourceCode.url() !== this._uiSourceCode.url()) {
this._executionLocation = null;
return;
}
this._executionLocation = uiLocation;
const editorLocation = this._transformer.uiLocationToEditorLocation(uiLocation.lineNumber, uiLocation.columnNumber);
this._textEditor.setExecutionLocation(editorLocation.lineNumber, editorLocation.columnNumber);
if (this._textEditor.isShowing()) {
// We need SourcesTextEditor to be initialized prior to this call. @see crbug.com/506566
queueMicrotask(() => {
if (this._controlDown) {
this._showContinueToLocations();
} else {
this._generateValuesInSource();
}
});
}
}
_generateValuesInSource() {
if (!Common.Settings.Settings.instance().moduleSetting('inlineVariableValues').get()) {
return;
}
const executionContext = UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext);
if (!executionContext) {
return;
}
const callFrame = UI.Context.Context.instance().flavor(SDK.DebuggerModel.CallFrame);
if (!callFrame) {
return;
}
const localScope = callFrame.localScope();
const functionLocation = callFrame.functionLocation();
if (localScope && functionLocation) {
resolveScopeInObject(localScope)
.getAllProperties(false, false)
.then(this._prepareScopeVariables.bind(this, callFrame));
}
}
_showContinueToLocations() {
this._popoverHelper.hidePopover();
const executionContext = UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext);
if (!executionContext) {
return;
}
const callFrame = UI.Context.Context.instance().flavor(SDK.DebuggerModel.CallFrame);
if (!callFrame) {
return;
}
const start = callFrame.functionLocation() || callFrame.location();
const debuggerModel = callFrame.debuggerModel;
debuggerModel.getPossibleBreakpoints(start, null, true)
.then(locations => this._textEditor.operation(renderLocations.bind(this, locations)));
/**
* @param {!Array<!SDK.DebuggerModel.BreakLocation>} locations
* @this {DebuggerPlugin}
*/
function renderLocations(locations) {
this._clearContinueToLocationsNoRestore();
this._textEditor.hideExecutionLineBackground();
this._continueToLocationDecorations = new Map();
locations = locations.reverse();
let previousCallLine = -1;
for (const location of locations) {
const editorLocation = this._transformer.uiLocationToEditorLocation(location.lineNumber, location.columnNumber);
const tokenThatIsPossiblyNull =
this._textEditor.tokenAtTextPosition(editorLocation.lineNumber, editorLocation.columnNumber);
if (!tokenThatIsPossiblyNull) {
continue;
}
let token = /** @type {!TextEditor.CodeMirrorTextEditor.Token} */ (tokenThatIsPossiblyNull);
const line = this._textEditor.line(editorLocation.lineNumber);
let tokenContent = line.substring(token.startColumn, token.endColumn);
if (!token.type && tokenContent === '.') {
const nextToken = this._textEditor.tokenAtTextPosition(editorLocation.lineNumber, token.endColumn + 1);
if (!nextToken) {
throw new Error('nextToken should not be null.');
}
token = nextToken;
tokenContent = line.substring(token.startColumn, token.endColumn);
}
if (!token.type) {
continue;
}
const validKeyword = token.type === 'js-keyword' &&
(tokenContent === 'this' || tokenContent === 'return' || tokenContent === 'new' ||
tokenContent === 'continue' || tokenContent === 'break');
if (!validKeyword && !this._isIdentifier(token.type)) {
continue;
}
if (previousCallLine === editorLocation.lineNumber &&
location.type !== Protocol.Debugger.BreakLocationType.Call) {
continue;
}
let highlightRange = new TextUtils.TextRange.TextRange(
editorLocation.lineNumber, token.startColumn, editorLocation.lineNumber, token.endColumn - 1);
let decoration = this._textEditor.highlightRange(highlightRange, 'source-frame-continue-to-location');
this._continueToLocationDecorations.set(decoration, location.continueToLocation.bind(location));
if (location.type === Protocol.Debugger.BreakLocationType.Call) {
previousCallLine = editorLocation.lineNumber;
}
let isAsyncCall = (line[token.startColumn - 1] === '.' && tokenContent === 'then') ||
tokenContent === 'setTimeout' || tokenContent === 'setInterval' || tokenContent === 'postMessage';
if (tokenContent === 'new') {
const nextToken = this._textEditor.tokenAtTextPosition(editorLocation.lineNumber, token.endColumn + 1);
if (!nextToken) {
throw new Error('nextToken should not be null.');
}
token = nextToken;
tokenContent = line.substring(token.startColumn, token.endColumn);
isAsyncCall = tokenContent === 'Worker';
}
const isCurrentPosition = this._executionLocation &&
location.lineNumber === this._executionLocation.lineNumber &&
location.columnNumber === this._executionLocation.columnNumber;
if (location.type === Protocol.Debugger.BreakLocationType.Call && isAsyncCall) {
const asyncStepInRange =
this._findAsyncStepInRange(this._textEditor, editorLocation.lineNumber, line, token.endColumn);
if (asyncStepInRange) {
highlightRange = new TextUtils.TextRange.TextRange(
editorLocation.lineNumber, asyncStepInRange.from, editorLocation.lineNumber, asyncStepInRange.to - 1);
decoration = this._textEditor.highlightRange(highlightRange, 'source-frame-async-step-in');
this._continueToLocationDecorations.set(
decoration, this._asyncStepIn.bind(this, location, Boolean(isCurrentPosition)));
}
}
}
this._continueToLocationRenderedForTest();
}
}
_continueToLocationRenderedForTest() {
}
/**
* @param {!SourceFrame.SourcesTextEditor.SourcesTextEditor} textEditor
* @param {number} editorLineNumber
* @param {string} line
* @param {number} column
* @return {?{from: number, to: number}}
*/
_findAsyncStepInRange(textEditor, editorLineNumber, line, column) {
/** @type {?TextEditor.CodeMirrorTextEditor.Token} */
let token = null;
let tokenText;
let from = column;
let to = line.length;
let position = line.indexOf('(', column);
const argumentsStart = position;
if (position === -1) {
return null;
}
position++;
skipWhitespace();
if (position >= line.length) {
return null;
}
token = nextToken();
if (!token) {
return null;
}
from = token.startColumn;
if (token.type === 'js-keyword' && tokenText === 'async') {
skipWhitespace();
if (position >= line.length) {
return {from: from, to: to};
}
token = nextToken();
if (!token) {
return {from: from, to: to};
}
}
if (token.type === 'js-keyword' && tokenText === 'function') {
return {from: from, to: to};
}
if (token.type === 'js-string') {
return {from: argumentsStart, to: to};
}
if (token.type && this._isIdentifier(token.type)) {
return {from: from, to: to};
}
if (tokenText !== '(') {
return null;
}
const closeParen = line.indexOf(')', position);
if (closeParen === -1 || line.substring(position, closeParen).indexOf('(') !== -1) {
return {from: from, to: to};
}
return {from: from, to: closeParen + 1};
function nextToken() {
token = textEditor.tokenAtTextPosition(editorLineNumber, position);
if (token) {
position = token.endColumn;
to = token.endColumn;
tokenText = line.substring(token.startColumn, token.endColumn);
}
return token;
}
function skipWhitespace() {
while (position < line.length) {
if (line[position] === ' ') {
position++;
continue;
}
const token = textEditor.tokenAtTextPosition(editorLineNumber, position);
if (!token) {
throw new Error('expected token to not be null');
}
if (token.type === 'js-comment') {
position = token.endColumn;
continue;
}
break;
}
}
}
/**
* @param {!SDK.DebuggerModel.BreakLocation} location
* @param {boolean} isCurrentPosition
*/
_asyncStepIn(location, isCurrentPosition) {
if (!isCurrentPosition) {
location.continueToLocation(asyncStepIn);
} else {
asyncStepIn();
}
function asyncStepIn() {
location.debuggerModel.scheduleStepIntoAsync();
}
}
/**
* @param {!SDK.DebuggerModel.CallFrame} callFrame
* @param {!SDK.RemoteObject.GetPropertiesResult} allProperties
*/
async _prepareScopeVariables(callFrame, allProperties) {
const properties = allProperties.properties;
this._clearValueWidgets();
if (!properties || !properties.length || properties.length > 500 || !this._textEditor.isShowing()) {
return;
}
const functionUILocationPromise =
Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().rawLocationToUILocation(
/** @type {!SDK.DebuggerModel.Location} */ (callFrame.functionLocation()));
const executionUILocationPromise =
Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().rawLocationToUILocation(
callFrame.location());
const [functionUILocation, executionUILocation] =
await Promise.all([functionUILocationPromise, executionUILocationPromise]);
if (!functionUILocation || !executionUILocation ||
functionUILocation.uiSourceCode.url() !== this._uiSourceCode.url() ||
executionUILocation.uiSourceCode.url() !== this._uiSourceCode.url()) {
return;
}
const functionEditorLocation =
this._transformer.uiLocationToEditorLocation(functionUILocation.lineNumber, functionUILocation.columnNumber);
const executionEditorLocation =
this._transformer.uiLocationToEditorLocation(executionUILocation.lineNumber, executionUILocation.columnNumber);
const fromLine = functionEditorLocation.lineNumber;
const fromColumn = functionEditorLocation.columnNumber;
const toLine = executionEditorLocation.lineNumber;
if (fromLine >= toLine || toLine - fromLine > 500 || fromLine < 0 || toLine >= this._textEditor.linesCount) {
return;
}
const valuesMap = new Map();
for (const property of properties) {
valuesMap.set(property.name, property.value);
}
/** @type {!Map.<number, !Set<string>>} */
const namesPerLine = new Map();
let skipObjectProperty = false;
const tokenizer = new TextEditor.CodeMirrorUtils.TokenizerFactory().createTokenizer('text/javascript');
tokenizer(this._textEditor.line(fromLine).substring(fromColumn), processToken.bind(this, fromLine));
for (let i = fromLine + 1; i < toLine; ++i) {
tokenizer(this._textEditor.line(i), processToken.bind(this, i));
}
/**
* @param {number} editorLineNumber
* @param {string} tokenValue
* @param {?string} tokenType
* @param {number} column
* @param {number} newColumn
* @this {DebuggerPlugin}
*/
function processToken(editorLineNumber, tokenValue, tokenType, column, newColumn) {
if (!skipObjectProperty && tokenType && this._isIdentifier(tokenType) && valuesMap.get(tokenValue)) {
let names = namesPerLine.get(editorLineNumber);
if (!names) {
names = new Set();
namesPerLine.set(editorLineNumber, names);
}
names.add(tokenValue);
}
skipObjectProperty = tokenValue === '.';
}
this._textEditor.operation(this._renderDecorations.bind(this, valuesMap, namesPerLine, fromLine, toLine));
}
/**
* @param {!Map.<string,!SDK.RemoteObject.RemoteObject>} valuesMap
* @param {!Map.<number, !Set<string>>} namesPerLine
* @param {number} fromLine
* @param {number} toLine
*/
_renderDecorations(valuesMap, namesPerLine, fromLine, toLine) {
const formatter = new ObjectUI.RemoteObjectPreviewFormatter.RemoteObjectPreviewFormatter();
for (let i = fromLine; i < toLine; ++i) {
const names = namesPerLine.get(i);
const oldWidget = this._valueWidgets.get(i);
if (!names) {
if (oldWidget) {
this._valueWidgets.delete(i);
this._textEditor.removeDecoration(oldWidget, i);
}
continue;
}
const widget = /** @type {!DecoratorWidget} */ (document.createElement('div'));
widget.classList.add('text-editor-value-decoration');
const base = this._textEditor.cursorPositionToCoordinates(i, 0);
if (!base) {
throw new Error('base is expected to not be null');
}
const offset = this._textEditor.cursorPositionToCoordinates(i, this._textEditor.line(i).length);
if (!offset) {
throw new Error('offset is expected to not be null');
}
const codeMirrorLinesLeftPadding = 4;
const left = offset.x - base.x + codeMirrorLinesLeftPadding;
widget.style.left = left + 'px';
widget.__nameToToken = new Map();
let renderedNameCount = 0;
for (const name of names) {
if (renderedNameCount > 10