mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-01 17:57:36 +08:00
The unrestricted annotation is Closure-specific and can be removed. This patch removes all comments with only the unrestricted annotation in them. R=szuend@chromium.org No-Presubmit: True Bug: 1011811 Change-Id: Iba0723644f53f5307c3077d341271af86becc32a Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2569758 Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org> Reviewed-by: Simon Zünd <szuend@chromium.org> Auto-Submit: Tim van der Lippe <tvanderlippe@chromium.org>
2037 lines
78 KiB
JavaScript
2037 lines
78 KiB
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 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';
|
|
|
|
// 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 => !!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._showBlackboxInfobarIfNeeded, this);
|
|
Common.Settings.Settings.instance()
|
|
.moduleSetting('skipContentScripts')
|
|
.addChangeListener(this._showBlackboxInfobarIfNeeded, this);
|
|
|
|
/** @type {!Map.<number, !DecoratorWidget>} */
|
|
this._valueWidgets = new Map();
|
|
/** @type {?Map<!CodeMirror.TextMarker, !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._blackboxInfobar = null;
|
|
this._showBlackboxInfobarIfNeeded();
|
|
|
|
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();
|
|
}
|
|
|
|
_showBlackboxInfobarIfNeeded() {
|
|
const uiSourceCode = this._uiSourceCode;
|
|
if (!uiSourceCode.contentType().hasScripts()) {
|
|
return;
|
|
}
|
|
const projectType = uiSourceCode.project().type();
|
|
if (!Bindings.BlackboxManager.BlackboxManager.instance().isBlackboxedUISourceCode(uiSourceCode)) {
|
|
this._hideBlackboxInfobar();
|
|
return;
|
|
}
|
|
|
|
if (this._blackboxInfobar) {
|
|
this._blackboxInfobar.dispose();
|
|
}
|
|
|
|
function unblackbox() {
|
|
Bindings.BlackboxManager.BlackboxManager.instance().unblackboxUISourceCode(uiSourceCode);
|
|
if (projectType === Workspace.Workspace.projectTypes.ContentScripts) {
|
|
Bindings.BlackboxManager.BlackboxManager.instance().unblackboxContentScripts();
|
|
}
|
|
}
|
|
|
|
const infobar = new UI.Infobar.Infobar(
|
|
UI.Infobar.Type.Warning, Common.UIString.UIString('This script is blackboxed in the debugger'), [
|
|
{text: ls`Unblackbox`, highlight: false, delegate: unblackbox, dismiss: true}, {
|
|
text: ls`Configure`,
|
|
highlight: false,
|
|
delegate:
|
|
UI.ViewManager.ViewManager.instance().showView.bind(UI.ViewManager.ViewManager.instance(), 'blackbox'),
|
|
dismiss: false
|
|
}
|
|
]);
|
|
this._blackboxInfobar = infobar;
|
|
|
|
infobar.createDetailsRowMessage(Common.UIString.UIString(
|
|
'The debugger will skip stepping through this script, and will not stop on exceptions.'));
|
|
|
|
const scriptFile =
|
|
this._scriptFileForDebuggerModel.size ? this._scriptFileForDebuggerModel.values().next().value : null;
|
|
if (scriptFile && scriptFile.hasSourceMapURL()) {
|
|
infobar.createDetailsRowMessage(Common.UIString.UIString('Source map found, but ignored for blackboxed file.'));
|
|
}
|
|
this._textEditor.attachInfobar(this._blackboxInfobar);
|
|
}
|
|
|
|
_hideBlackboxInfobar() {
|
|
if (!this._blackboxInfobar) {
|
|
return;
|
|
}
|
|
this._blackboxInfobar.dispose();
|
|
this._blackboxInfobar = 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 => !!breakpoint));
|
|
const hasOnlyJavaScript = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance()
|
|
.scriptsForUISourceCode(this._uiSourceCode)
|
|
.every(script => script.isJavaScript());
|
|
if (!breakpoints.length) {
|
|
contextMenu.debugSection().appendItem(
|
|
Common.UIString.UIString('Add breakpoint'), this._createNewBreakpoint.bind(this, editorLineNumber, '', true));
|
|
// Conditional breakpoints, logpoints and 'Never pause here'
|
|
// are currently only available for JavaScript debugging.
|
|
if (hasOnlyJavaScript) {
|
|
contextMenu.debugSection().appendItem(
|
|
Common.UIString.UIString('Add conditional breakpoint…'),
|
|
this._editBreakpointCondition.bind(this, editorLineNumber, null, null, false /* preferLogpoint */));
|
|
contextMenu.debugSection().appendItem(
|
|
ls`Add logpoint…`,
|
|
this._editBreakpointCondition.bind(this, editorLineNumber, null, null, true /* preferLogpoint */));
|
|
contextMenu.debugSection().appendItem(
|
|
Common.UIString.UIString('Never pause here'),
|
|
this._createNewBreakpoint.bind(this, editorLineNumber, 'false', true));
|
|
}
|
|
} else {
|
|
const hasOneBreakpoint = breakpoints.length === 1;
|
|
const removeTitle = hasOneBreakpoint ? Common.UIString.UIString('Remove breakpoint') :
|
|
Common.UIString.UIString('Remove all breakpoints in line');
|
|
contextMenu.debugSection().appendItem(removeTitle, () => breakpoints.map(breakpoint => breakpoint.remove(false)));
|
|
if (hasOneBreakpoint && hasOnlyJavaScript) {
|
|
// Editing breakpoints only make sense for conditional breakpoints
|
|
// and logpoints and both are currently only available for JavaScript
|
|
// debugging.
|
|
contextMenu.debugSection().appendItem(
|
|
Common.UIString.UIString('Edit breakpoint…'),
|
|
this._editBreakpointCondition.bind(
|
|
this, editorLineNumber, breakpoints[0], null, false /* preferLogpoint */));
|
|
}
|
|
const hasEnabled = breakpoints.some(breakpoint => breakpoint.enabled());
|
|
if (hasEnabled) {
|
|
const title = hasOneBreakpoint ? Common.UIString.UIString('Disable breakpoint') :
|
|
Common.UIString.UIString('Disable all breakpoints in line');
|
|
contextMenu.debugSection().appendItem(title, () => breakpoints.map(breakpoint => breakpoint.setEnabled(false)));
|
|
}
|
|
const hasDisabled = breakpoints.some(breakpoint => !breakpoint.enabled());
|
|
if (hasDisabled) {
|
|
const title = hasOneBreakpoint ? Common.UIString.UIString('Enable breakpoint') :
|
|
Common.UIString.UIString('Enable all breakpoints in line');
|
|
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.BlackboxManager.BlackboxManager.instance().isBlackboxedUISourceCode(this._uiSourceCode)) {
|
|
if (this._scriptFileForDebuggerModel.size) {
|
|
const scriptFile = this._scriptFileForDebuggerModel.values().next().value;
|
|
const addSourceMapURLLabel = Common.UIString.UIString('Add source map…');
|
|
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 = !!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.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, !!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, !!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) {
|
|
break;
|
|
}
|
|
const names = namesPerLine.get(i - 1);
|
|
if (names && names.has(name)) {
|
|
continue;
|
|
} // Only render name once in the given continuous block.
|
|
if (renderedNameCount) {
|
|
UI.UIUtils.createTextChild(widget, ', ');
|
|
}
|
|
const nameValuePair = /** @type {!HTMLElement} */ (widget.createChild('span'));
|
|
widget.__nameToToken.set(name, nameValuePair);
|
|
UI.UIUtils.createTextChild(nameValuePair, name + ' = ');
|
|
const value = valuesMap.get(name);
|
|
if (!value) {
|
|
throw new Error('value is expected to be null');
|
|
}
|
|
const propertyCount = value.preview ? value.preview.properties.length : 0;
|
|
const entryCount = value.preview && value.preview.entries ? value.preview.entries.length : 0;
|
|
if (value.preview && propertyCount + entryCount < 10) {
|
|
formatter.appendObjectPreview(nameValuePair, value.preview, false /* isEntry */);
|
|
} else {
|
|
const propertyValue = ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection.createPropertyValue(
|
|
value, /* wasThrown */ false, /* showPreview */ false);
|
|
nameValuePair.appendChild(propertyValue.element);
|
|
}
|
|
++renderedNameCount;
|
|
}
|
|
|
|
let widgetChanged = true;
|
|
if (oldWidget) {
|
|
widgetChanged = false;
|
|
for (const name of widget.__nameToToken.keys()) {
|
|
const oldTextElement = oldWidget.__nameToToken.get(name);
|
|
const newTextElement = widget.__nameToToken.get(name);
|
|
const oldText = oldTextElement ? oldTextElement.textContent : '';
|
|
const newText = newTextElement ? newTextElement.textContent : '';
|
|
if (newText !== oldText) {
|
|
widgetChanged = true;
|
|
// value has changed, update it.
|
|
UI.UIUtils.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() {
|
|
this._textEditor.operation(() => {
|
|
if (this._executionLocation) {
|
|
this._textEditor.clearExecutionLine();
|
|
}
|
|
this._executionLocation = null;
|
|
if (this._clearValueWidgetsTimer) {
|
|
clearTimeout(this._clearValueWidgetsTimer);
|
|
this._clearValueWidgetsTimer = null;
|
|
}
|
|
this._clearValueWidgetsTimer = window.setTimeout(this._clearValueWidgets.bind(this), 1000);
|
|
this._clearContinueToLocationsNoRestore();
|
|
});
|
|
}
|
|
|
|
_clearValueWidgets() {
|
|
if (this._clearValueWidgetsTimer) {
|
|
clearTimeout(this._clearValueWidgetsTimer);
|
|
}
|
|
this._clearValueWidgetsTimer = null;
|
|
this._textEditor.operation(() => {
|
|
for (const line of this._valueWidgets.keys()) {
|
|
const valueWidget = this._valueWidgets.get(line);
|
|
if (valueWidget) {
|
|
this._textEditor.removeDecoration(valueWidget, line);
|
|
}
|
|
}
|
|
this._valueWidgets.clear();
|
|
});
|
|
}
|
|
|
|
_clearContinueToLocationsNoRestore() {
|
|
const continueToLocationDecorations = this._continueToLocationDecorations;
|
|
if (!continueToLocationDecorations) {
|
|
return;
|
|
}
|
|
this._textEditor.operation(() => {
|
|
for (const decoration of continueToLocationDecorations.keys()) {
|
|
this._textEditor.removeHighlight(decoration);
|
|
}
|
|
this._continueToLocationDecorations = null;
|
|
this._setAsyncStepInHoveredLine(null, false);
|
|
});
|
|
}
|
|
|
|
_clearContinueToLocations() {
|
|
if (!this._continueToLocationDecorations) {
|
|
return;
|
|
}
|
|
this._textEditor.operation(() => {
|
|
this._textEditor.showExecutionLineBackground();
|
|
this._generateValuesInSource();
|
|
this._clearContinueToLocationsNoRestore();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {number} lineNumber
|
|
* @return {!Array<!BreakpointDecoration>}
|
|
*/
|
|
_lineBreakpointDecorations(lineNumber) {
|
|
return Array.from(this._breakpointDecorations)
|
|
.filter(decoration => (decoration.handle.resolve() || {}).lineNumber === lineNumber);
|
|
}
|
|
|
|
/**
|
|
* @param {number} editorLineNumber
|
|
* @param {number} editorColumnNumber
|
|
* @return {?BreakpointDecoration}
|
|
*/
|
|
_breakpointDecoration(editorLineNumber, editorColumnNumber) {
|
|
for (const decoration of this._breakpointDecorations) {
|
|
const location = decoration.handle.resolve();
|
|
if (!location) {
|
|
continue;
|
|
}
|
|
if (location.lineNumber === editorLineNumber && location.columnNumber === editorColumnNumber) {
|
|
return decoration;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param {!BreakpointDecoration} decoration
|
|
*/
|
|
_updateBreakpointDecoration(decoration) {
|
|
if (!this._scheduledBreakpointDecorationUpdates) {
|
|
/** @type {?Set<!BreakpointDecoration>} */
|
|
this._scheduledBreakpointDecorationUpdates = new Set();
|
|
queueMicrotask(() => {
|
|
this._textEditor.operation(update.bind(this));
|
|
});
|
|
}
|
|
this._scheduledBreakpointDecorationUpdates.add(decoration);
|
|
|
|
/**
|
|
* @this {DebuggerPlugin}
|
|
*/
|
|
function update() {
|
|
if (!this._scheduledBreakpointDecorationUpdates) {
|
|
return;
|
|
}
|
|
const editorLineNumbers = new Set();
|
|
for (const decoration of this._scheduledBreakpointDecorationUpdates) {
|
|
const location = decoration.handle.resolve();
|
|
if (!location) {
|
|
continue;
|
|
}
|
|
editorLineNumbers.add(location.lineNumber);
|
|
}
|
|
this._scheduledBreakpointDecorationUpdates = null;
|
|
let waitingForInlineDecorations = false;
|
|
for (const lineNumber of editorLineNumbers) {
|
|
const decorations = this._lineBreakpointDecorations(lineNumber);
|
|
updateGutter.call(this, lineNumber, decorations);
|
|
if (this._possibleBreakpointsRequested.has(lineNumber)) {
|
|
waitingForInlineDecorations = true;
|
|
continue;
|
|
}
|
|
updateInlineDecorations.call(this, lineNumber, decorations);
|
|
}
|
|
if (!waitingForInlineDecorations) {
|
|
this._breakpointDecorationsUpdatedForTest();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {number} editorLineNumber
|
|
* @param {!Array<!BreakpointDecoration>} decorations
|
|
* @this {DebuggerPlugin}
|
|
*/
|
|
function updateGutter(editorLineNumber, decorations) {
|
|
this._textEditor.toggleLineClass(editorLineNumber, 'cm-breakpoint', false);
|
|
this._textEditor.toggleLineClass(editorLineNumber, 'cm-breakpoint-disabled', false);
|
|
this._textEditor.toggleLineClass(editorLineNumber, 'cm-breakpoint-unbound', false);
|
|
this._textEditor.toggleLineClass(editorLineNumber, 'cm-breakpoint-conditional', false);
|
|
this._textEditor.toggleLineClass(editorLineNumber, 'cm-breakpoint-logpoint', false);
|
|
|
|
if (decorations.length) {
|
|
decorations.sort(BreakpointDecoration.mostSpecificFirst);
|
|
const isDisabled = !decorations[0].enabled || this._muted;
|
|
const isLogpoint = decorations[0].condition.includes(LogpointPrefix);
|
|
const isUnbound = !decorations[0].bound;
|
|
const isConditionalBreakpoint = !!decorations[0].condition && !isLogpoint;
|
|
|
|
this._textEditor.toggleLineClass(editorLineNumber, 'cm-breakpoint', true);
|
|
this._textEditor.toggleLineClass(editorLineNumber, 'cm-breakpoint-disabled', isDisabled);
|
|
this._textEditor.toggleLineClass(editorLineNumber, 'cm-breakpoint-unbound', isUnbound && !isDisabled);
|
|
this._textEditor.toggleLineClass(editorLineNumber, 'cm-breakpoint-logpoint', isLogpoint);
|
|
this._textEditor.toggleLineClass(editorLineNumber, 'cm-breakpoint-conditional', isConditionalBreakpoint);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {number} editorLineNumber
|
|
* @param {!Array<!BreakpointDecoration>} decorations
|
|
* @this {DebuggerPlugin}
|
|
*/
|
|
function updateInlineDecorations(editorLineNumber, decorations) {
|
|
const actualBookmarks =
|
|
new Set(decorations.map(decoration => decoration.bookmark).filter(bookmark => !!bookmark));
|
|
const lineEnd = this._textEditor.line(editorLineNumber).length;
|
|
const bookmarks = this._textEditor.bookmarks(
|
|
new TextUtils.TextRange.TextRange(editorLineNumber, 0, editorLineNumber, lineEnd),
|
|
BreakpointDecoration.bookmarkSymbol);
|
|
for (const bookmark of bookmarks) {
|
|
if (!actualBookmarks.has(bookmark)) {
|
|
bookmark.clear();
|
|
}
|
|
}
|
|
if (!decorations.length) {
|
|
return;
|
|
}
|
|
if (decorations.length > 1) {
|
|
for (const decoration of decorations) {
|
|
decoration.update();
|
|
if (!this._muted) {
|
|
decoration.show();
|
|
} else {
|
|
decoration.hide();
|
|
}
|
|
}
|
|
} else {
|
|
decorations[0].update();
|
|
decorations[0].hide();
|
|
}
|
|
}
|
|
}
|
|
|
|
_breakpointDecorationsUpdatedForTest() {
|
|
}
|
|
|
|
/**
|
|
* @param {!BreakpointDecoration} decoration
|
|
* @param {!MouseEvent} event
|
|
*/
|
|
async _inlineBreakpointClick(decoration, event) {
|
|
event.consume(true);
|
|
if (decoration.breakpoint) {
|
|
if (event.shiftKey) {
|
|
decoration.breakpoint.setEnabled(!decoration.breakpoint.enabled());
|
|
} else {
|
|
decoration.breakpoint.remove(false);
|
|
}
|
|
} else {
|
|
const editorLocation = decoration.handle.resolve();
|
|
if (!editorLocation) {
|
|
return;
|
|
}
|
|
const location =
|
|
this._transformer.editorLocationToUILocation(editorLocation.lineNumber, editorLocation.columnNumber);
|
|
await this._setBreakpoint(location.lineNumber, location.columnNumber, decoration.condition, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!BreakpointDecoration} decoration
|
|
* @param {!Event} event
|
|
*/
|
|
_inlineBreakpointContextMenu(decoration, event) {
|
|
event.consume(true);
|
|
const editorLocation = decoration.handle.resolve();
|
|
if (!editorLocation) {
|
|
return;
|
|
}
|
|
if (this._textEditor.hasLineClass(editorLocation.lineNumber, 'cm-non-breakable-line')) {
|
|
return;
|
|
}
|
|
if (!Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance()
|
|
.scriptsForUISourceCode(this._uiSourceCode)
|
|
.every(script => script.isJavaScript())) {
|
|
// Editing breakpoints only make sense for conditional breakpoints
|
|
// and logpoints and both are currently only available for JavaScript
|
|
// debugging.
|
|
return;
|
|
}
|
|
const location =
|
|
this._transformer.editorLocationToUILocation(editorLocation.lineNumber, editorLocation.columnNumber);
|
|
const contextMenu = new UI.ContextMenu.ContextMenu(event);
|
|
if (decoration.breakpoint) {
|
|
contextMenu.debugSection().appendItem(
|
|
Common.UIString.UIString('Edit breakpoint…'),
|
|
this._editBreakpointCondition.bind(
|
|
this, editorLocation.lineNumber, decoration.breakpoint, null, false /* preferLogpoint */));
|
|
} else {
|
|
contextMenu.debugSection().appendItem(
|
|
Common.UIString.UIString('Add conditional breakpoint…'),
|
|
this._editBreakpointCondition.bind(
|
|
this, editorLocation.lineNumber, null, editorLocation, false /* preferLogpoint */));
|
|
contextMenu.debugSection().appendItem(
|
|
ls`Add logpoint…`,
|
|
this._editBreakpointCondition.bind(
|
|
this, editorLocation.lineNumber, null, editorLocation, true /* preferLogpoint */));
|
|
contextMenu.debugSection().appendItem(
|
|
Common.UIString.UIString('Never pause here'),
|
|
this._setBreakpoint.bind(this, location.lineNumber, location.columnNumber, 'false', true));
|
|
}
|
|
contextMenu.show();
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.EventTarget.EventTargetEvent} event
|
|
* @return {boolean}
|
|
*/
|
|
_shouldIgnoreExternalBreakpointEvents(event) {
|
|
const uiLocation = /** @type {!Workspace.UISourceCode.UILocation} */ (event.data.uiLocation);
|
|
if (uiLocation.uiSourceCode !== this._uiSourceCode) {
|
|
return true;
|
|
}
|
|
if (this._muted) {
|
|
return true;
|
|
}
|
|
for (const scriptFile of this._scriptFileForDebuggerModel.values()) {
|
|
if (scriptFile.isDivergingFromVM() || scriptFile.isMergingToVM()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.EventTarget.EventTargetEvent} event
|
|
*/
|
|
_breakpointAdded(event) {
|
|
if (this._shouldIgnoreExternalBreakpointEvents(event)) {
|
|
return;
|
|
}
|
|
const uiLocation = /** @type {!Workspace.UISourceCode.UILocation} */ (event.data.uiLocation);
|
|
const breakpoint = /** @type {!Bindings.BreakpointManager.Breakpoint} */ (event.data.breakpoint);
|
|
this._addBreakpoint(uiLocation, breakpoint);
|
|
}
|
|
|
|
/**
|
|
* @param {!Workspace.UISourceCode.UILocation} uiLocation
|
|
* @param {!Bindings.BreakpointManager.Breakpoint} breakpoint
|
|
*/
|
|
_addBreakpoint(uiLocation, breakpoint) {
|
|
const editorLocation = this._transformer.uiLocationToEditorLocation(uiLocation.lineNumber, uiLocation.columnNumber);
|
|
const lineDecorations = this._lineBreakpointDecorations(uiLocation.lineNumber);
|
|
let decoration = this._breakpointDecoration(editorLocation.lineNumber, editorLocation.columnNumber);
|
|
if (decoration) {
|
|
decoration.breakpoint = breakpoint;
|
|
decoration.condition = breakpoint.condition();
|
|
decoration.enabled = breakpoint.enabled();
|
|
} else {
|
|
const handle = this._textEditor.textEditorPositionHandle(editorLocation.lineNumber, editorLocation.columnNumber);
|
|
decoration = new BreakpointDecoration(
|
|
this._textEditor, handle, breakpoint.condition(), breakpoint.enabled(),
|
|
breakpoint.bound() || !breakpoint.hasBoundScript(), breakpoint);
|
|
decoration.element.addEventListener('click', this._inlineBreakpointClick.bind(this, decoration), true);
|
|
decoration.element.addEventListener(
|
|
'contextmenu', this._inlineBreakpointContextMenu.bind(this, decoration), true);
|
|
this._breakpointDecorations.add(decoration);
|
|
}
|
|
this._decorationByBreakpoint.set(breakpoint, decoration);
|
|
this._updateBreakpointDecoration(decoration);
|
|
if (breakpoint.enabled() && !lineDecorations.length) {
|
|
this._possibleBreakpointsRequested.add(editorLocation.lineNumber);
|
|
const start = this._transformer.editorLocationToUILocation(editorLocation.lineNumber, 0);
|
|
const end = this._transformer.editorLocationToUILocation(editorLocation.lineNumber + 1, 0);
|
|
this._breakpointManager
|
|
.possibleBreakpoints(
|
|
this._uiSourceCode,
|
|
new TextUtils.TextRange.TextRange(
|
|
start.lineNumber, start.columnNumber || 0, end.lineNumber, end.columnNumber || 0))
|
|
.then(addInlineDecorations.bind(this, editorLocation.lineNumber));
|
|
}
|
|
|
|
/**
|
|
* @this {DebuggerPlugin}
|
|
* @param {number} editorLineNumber
|
|
* @param {!Array<!Workspace.UISourceCode.UILocation>} possibleLocations
|
|
*/
|
|
function addInlineDecorations(editorLineNumber, possibleLocations) {
|
|
this._possibleBreakpointsRequested.delete(editorLineNumber);
|
|
const decorations = this._lineBreakpointDecorations(editorLineNumber);
|
|
for (const decoration of decorations) {
|
|
this._updateBreakpointDecoration(decoration);
|
|
}
|
|
if (!decorations.some(decoration => !!decoration.breakpoint)) {
|
|
return;
|
|
}
|
|
/** @type {!Set<number>} */
|
|
const columns = new Set();
|
|
for (const decoration of decorations) {
|
|
const editorLocation = decoration.handle.resolve();
|
|
if (!editorLocation) {
|
|
continue;
|
|
}
|
|
columns.add(editorLocation.columnNumber);
|
|
}
|
|
// Only consider the first 100 inline breakpoints, as DevTools might appear to hang while CodeMirror is updating
|
|
// the inline breakpoints. See crbug.com/1060105.
|
|
for (const location of possibleLocations.slice(0, 100)) {
|
|
const editorLocation = this._transformer.uiLocationToEditorLocation(location.lineNumber, location.columnNumber);
|
|
if (editorLocation.lineNumber !== editorLineNumber) {
|
|
continue;
|
|
}
|
|
if (columns.has(editorLocation.columnNumber)) {
|
|
continue;
|
|
}
|
|
const handle =
|
|
this._textEditor.textEditorPositionHandle(editorLocation.lineNumber, editorLocation.columnNumber);
|
|
const decoration = new BreakpointDecoration(
|
|
this._textEditor, handle, '', /** enabled */ false, /** bound */ false, /** breakpoint */ null);
|
|
decoration.element.addEventListener('click', this._inlineBreakpointClick.bind(this, decoration), true);
|
|
decoration.element.addEventListener(
|
|
'contextmenu', this._inlineBreakpointContextMenu.bind(this, decoration), true);
|
|
this._breakpointDecorations.add(decoration);
|
|
this._updateBreakpointDecoration(decoration);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.EventTarget.EventTargetEvent} event
|
|
*/
|
|
_breakpointRemoved(event) {
|
|
if (this._shouldIgnoreExternalBreakpointEvents(event)) {
|
|
return;
|
|
}
|
|
const uiLocation = /** @type {!Workspace.UISourceCode.UILocation} */ (event.data.uiLocation);
|
|
const breakpoint = /** @type {!Bindings.BreakpointManager.Breakpoint} */ (event.data.breakpoint);
|
|
const decoration = this._decorationByBreakpoint.get(breakpoint);
|
|
if (!decoration) {
|
|
return;
|
|
}
|
|
this._decorationByBreakpoint.delete(breakpoint);
|
|
|
|
const editorLocation = this._transformer.uiLocationToEditorLocation(uiLocation.lineNumber, uiLocation.columnNumber);
|
|
decoration.breakpoint = null;
|
|
decoration.enabled = false;
|
|
|
|
const lineDecorations = this._lineBreakpointDecorations(editorLocation.lineNumber);
|
|
if (!lineDecorations.some(decoration => !!decoration.breakpoint)) {
|
|
for (const lineDecoration of lineDecorations) {
|
|
this._breakpointDecorations.delete(lineDecoration);
|
|
this._updateBreakpointDecoration(lineDecoration);
|
|
}
|
|
} else {
|
|
this._updateBreakpointDecoration(decoration);
|
|
}
|
|
}
|
|
|
|
_initializeBreakpoints() {
|
|
const breakpointLocations = this._breakpointManager.breakpointLocationsForUISourceCode(this._uiSourceCode);
|
|
for (const breakpointLocation of breakpointLocations) {
|
|
this._addBreakpoint(breakpointLocation.uiLocation, breakpointLocation.breakpoint);
|
|
}
|
|
}
|
|
|
|
_updateLinesWithoutMappingHighlight() {
|
|
const isSourceMapSource =
|
|
!!Bindings.CompilerScriptMapping.CompilerScriptMapping.uiSourceCodeOrigin(this._uiSourceCode);
|
|
if (isSourceMapSource) {
|
|
const linesCount = this._textEditor.linesCount;
|
|
for (let i = 0; i < linesCount; ++i) {
|
|
const lineHasMapping =
|
|
Bindings.CompilerScriptMapping.CompilerScriptMapping.uiLineHasMapping(this._uiSourceCode, i);
|
|
if (!lineHasMapping) {
|
|
this._hasLineWithoutMapping = true;
|
|
}
|
|
if (this._hasLineWithoutMapping) {
|
|
this._textEditor.toggleLineClass(i, 'cm-non-breakable-line', !lineHasMapping);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
_updateScriptFiles() {
|
|
for (const debuggerModel of SDK.SDKModel.TargetManager.instance().models(SDK.DebuggerModel.DebuggerModel)) {
|
|
const scriptFile = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().scriptFile(
|
|
this._uiSourceCode, debuggerModel);
|
|
if (scriptFile) {
|
|
this._updateScriptFile(debuggerModel);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.DebuggerModel.DebuggerModel} debuggerModel
|
|
*/
|
|
_updateScriptFile(debuggerModel) {
|
|
const oldScriptFile = this._scriptFileForDebuggerModel.get(debuggerModel);
|
|
const newScriptFile = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().scriptFile(
|
|
this._uiSourceCode, debuggerModel);
|
|
this._scriptFileForDebuggerModel.delete(debuggerModel);
|
|
if (oldScriptFile) {
|
|
oldScriptFile.removeEventListener(
|
|
Bindings.ResourceScriptMapping.ResourceScriptFile.Events.DidMergeToVM, this._didMergeToVM, this);
|
|
oldScriptFile.removeEventListener(
|
|
Bindings.ResourceScriptMapping.ResourceScriptFile.Events.DidDivergeFromVM, this._didDivergeFromVM, this);
|
|
if (this._muted && !this._uiSourceCode.isDirty()) {
|
|
this._restoreBreakpointsIfConsistentScripts();
|
|
}
|
|
}
|
|
if (!newScriptFile) {
|
|
return;
|
|
}
|
|
this._scriptFileForDebuggerModel.set(debuggerModel, newScriptFile);
|
|
newScriptFile.addEventListener(
|
|
Bindings.ResourceScriptMapping.ResourceScriptFile.Events.DidMergeToVM, this._didMergeToVM, this);
|
|
newScriptFile.addEventListener(
|
|
Bindings.ResourceScriptMapping.ResourceScriptFile.Events.DidDivergeFromVM, this._didDivergeFromVM, this);
|
|
newScriptFile.checkMapping();
|
|
if (newScriptFile.hasSourceMapURL()) {
|
|
this._showSourceMapInfobar();
|
|
}
|
|
}
|
|
|
|
_showSourceMapInfobar() {
|
|
if (this._sourceMapInfobar) {
|
|
return;
|
|
}
|
|
this._sourceMapInfobar = UI.Infobar.Infobar.create(
|
|
UI.Infobar.Type.Info, Common.UIString.UIString('Source Map detected.'), [],
|
|
Common.Settings.Settings.instance().createSetting('sourceMapInfobarDisabled', false));
|
|
if (!this._sourceMapInfobar) {
|
|
return;
|
|
}
|
|
this._sourceMapInfobar.createDetailsRowMessage(Common.UIString.UIString(
|
|
'Associated files should be added to the file tree. You can debug these resolved source files as regular JavaScript files.'));
|
|
this._sourceMapInfobar.createDetailsRowMessage(Common.UIString.UIString(
|
|
'Associated files are available via file tree or %s.',
|
|
UI.ShortcutRegistry.ShortcutRegistry.instance().shortcutTitleForAction('quickOpen.show')));
|
|
this._sourceMapInfobar.setCloseCallback(() => {
|
|
this._sourceMapInfobar = null;
|
|
});
|
|
this._textEditor.attachInfobar(this._sourceMapInfobar);
|
|
}
|
|
|
|
async _detectMinified() {
|
|
const content = this._uiSourceCode.content();
|
|
if (!content || !TextUtils.TextUtils.isMinified(content)) {
|
|
return;
|
|
}
|
|
|
|
const editorActions = await Root.Runtime.Runtime.instance().allInstances(EditorAction);
|
|
/** @type {(function(): void) | null} */
|
|
let formatterCallback = null;
|
|
for (const editorAction of editorActions) {
|
|
if (editorAction instanceof ScriptFormatterEditorAction) {
|
|
// Check if the source code is formattable the same way the pretty print button does
|
|
if (!editorAction.isCurrentUISourceCodeFormatable()) {
|
|
return;
|
|
}
|
|
formatterCallback = () => editorAction.toggleFormatScriptSource.bind(editorAction);
|
|
break;
|
|
}
|
|
}
|
|
|
|
this._prettyPrintInfobar = UI.Infobar.Infobar.create(
|
|
UI.Infobar.Type.Info, Common.UIString.UIString('Pretty-print this minified file?'),
|
|
[{text: ls`Pretty-print`, delegate: formatterCallback, highlight: true, dismiss: true}],
|
|
Common.Settings.Settings.instance().createSetting('prettyPrintInfobarDisabled', false));
|
|
if (!this._prettyPrintInfobar) {
|
|
return;
|
|
}
|
|
|
|
this._prettyPrintInfobar.setCloseCallback(() => {
|
|
this._prettyPrintInfobar = null;
|
|
});
|
|
const toolbar = new UI.Toolbar.Toolbar('');
|
|
const button = new UI.Toolbar.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';
|
|
toolbar.element.tabIndex = -1;
|
|
const element = this._prettyPrintInfobar.createDetailsRowMessage();
|
|
element.appendChild(UI.UIUtils.formatLocalized(
|
|
'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 %s button on the bottom status bar.',
|
|
[toolbar.element]));
|
|
this._textEditor.attachInfobar(this._prettyPrintInfobar);
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.EventTarget.EventTargetEvent} event
|
|
*/
|
|
async _handleGutterClick(event) {
|
|
if (this._muted) {
|
|
return;
|
|
}
|
|
|
|
const eventData = /** @type {!SourceFrame.SourcesTextEditor.GutterClickEventData} */ (event.data);
|
|
if (eventData.gutterType !== SourceFrame.SourcesTextEditor.lineNumbersGutterType) {
|
|
return;
|
|
}
|
|
const editorLineNumber = eventData.lineNumber;
|
|
const eventObject = eventData.event;
|
|
|
|
if (eventObject.button !== 0 || eventObject.altKey || eventObject.ctrlKey || eventObject.metaKey) {
|
|
return;
|
|
}
|
|
|
|
await this._toggleBreakpoint(editorLineNumber, eventObject.shiftKey);
|
|
eventObject.consume(true);
|
|
}
|
|
|
|
/**
|
|
* @param {number} editorLineNumber
|
|
* @param {boolean} onlyDisable
|
|
*/
|
|
async _toggleBreakpoint(editorLineNumber, onlyDisable) {
|
|
const decorations = this._lineBreakpointDecorations(editorLineNumber);
|
|
if (!decorations.length) {
|
|
await this._createNewBreakpoint(editorLineNumber, '', true);
|
|
return;
|
|
}
|
|
const hasDisabled = this._textEditor.hasLineClass(editorLineNumber, 'cm-breakpoint-disabled');
|
|
const breakpoints = /** @type {!Array<!Bindings.BreakpointManager.Breakpoint>} */ (
|
|
decorations.map(decoration => decoration.breakpoint).filter(breakpoint => !!breakpoint));
|
|
for (const breakpoint of breakpoints) {
|
|
if (onlyDisable) {
|
|
breakpoint.setEnabled(hasDisabled);
|
|
} else {
|
|
breakpoint.remove(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {number} editorLineNumber
|
|
* @param {string} condition
|
|
* @param {boolean} enabled
|
|
*/
|
|
async _createNewBreakpoint(editorLineNumber, condition, enabled) {
|
|
if (this._textEditor.hasLineClass(editorLineNumber, 'cm-non-breakable-line')) {
|
|
return;
|
|
}
|
|
Host.userMetrics.actionTaken(Host.UserMetrics.Action.ScriptsBreakpointSet);
|
|
const origin = this._transformer.editorLocationToUILocation(editorLineNumber);
|
|
await this._setBreakpoint(origin.lineNumber, origin.columnNumber, condition, enabled);
|
|
}
|
|
|
|
/**
|
|
* @param {number} lineNumber
|
|
* @param {number|undefined} columnNumber
|
|
* @param {string} condition
|
|
* @param {boolean} enabled
|
|
*/
|
|
async _setBreakpoint(lineNumber, columnNumber, condition, enabled) {
|
|
Common.Settings.Settings.instance().moduleSetting('breakpointsActive').set(true);
|
|
await this._breakpointManager.setBreakpoint(this._uiSourceCode, lineNumber, columnNumber, condition, enabled);
|
|
this._breakpointWasSetForTest(lineNumber, columnNumber, condition, enabled);
|
|
}
|
|
|
|
/**
|
|
* @param {number} lineNumber
|
|
* @param {number|undefined} columnNumber
|
|
* @param {string} condition
|
|
* @param {boolean} enabled
|
|
*/
|
|
_breakpointWasSetForTest(lineNumber, columnNumber, condition, enabled) {
|
|
}
|
|
|
|
|
|
async _callFrameChanged() {
|
|
this._liveLocationPool.disposeAll();
|
|
const callFrame = UI.Context.Context.instance().flavor(SDK.DebuggerModel.CallFrame);
|
|
if (!callFrame) {
|
|
this._clearExecutionLine();
|
|
return;
|
|
}
|
|
await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().createCallFrameLiveLocation(
|
|
callFrame.location(), this._executionLineChanged.bind(this), this._liveLocationPool);
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
dispose() {
|
|
for (const decoration of this._breakpointDecorations) {
|
|
decoration.dispose();
|
|
}
|
|
this._breakpointDecorations.clear();
|
|
if (this._scheduledBreakpointDecorationUpdates) {
|
|
for (const decoration of this._scheduledBreakpointDecorationUpdates) {
|
|
decoration.dispose();
|
|
}
|
|
this._scheduledBreakpointDecorationUpdates.clear();
|
|
}
|
|
|
|
this._hideBlackboxInfobar();
|
|
if (this._sourceMapInfobar) {
|
|
this._sourceMapInfobar.dispose();
|
|
}
|
|
if (this._prettyPrintInfobar) {
|
|
this._prettyPrintInfobar.dispose();
|
|
}
|
|
this._scriptsPanel.element.removeEventListener('scroll', this._boundPopoverHelperHide, true);
|
|
for (const script of this._scriptFileForDebuggerModel.values()) {
|
|
script.removeEventListener(
|
|
Bindings.ResourceScriptMapping.ResourceScriptFile.Events.DidMergeToVM, this._didMergeToVM, this);
|
|
script.removeEventListener(
|
|
Bindings.ResourceScriptMapping.ResourceScriptFile.Events.DidDivergeFromVM, this._didDivergeFromVM, this);
|
|
}
|
|
this._scriptFileForDebuggerModel.clear();
|
|
|
|
|
|
this._textEditor.element.removeEventListener('keydown', this._boundKeyDown, true);
|
|
this._textEditor.element.removeEventListener('keyup', this._boundKeyUp, true);
|
|
this._textEditor.element.removeEventListener('mousemove', this._boundMouseMove, false);
|
|
this._textEditor.element.removeEventListener('mousedown', this._boundMouseDown, true);
|
|
this._textEditor.element.removeEventListener('focusout', this._boundBlur, false);
|
|
this._textEditor.element.removeEventListener('wheel', this._boundWheel, true);
|
|
|
|
this._textEditor.removeEventListener(
|
|
SourceFrame.SourcesTextEditor.Events.GutterClick, this._boundGutterClick, this);
|
|
this._popoverHelper.hidePopover();
|
|
this._popoverHelper.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.WorkingCopyChanged, this._workingCopyChanged, this);
|
|
this._uiSourceCode.removeEventListener(
|
|
Workspace.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this);
|
|
|
|
Common.Settings.Settings.instance()
|
|
.moduleSetting('skipStackFramesPattern')
|
|
.removeChangeListener(this._showBlackboxInfobarIfNeeded, this);
|
|
Common.Settings.Settings.instance()
|
|
.moduleSetting('skipContentScripts')
|
|
.removeChangeListener(this._showBlackboxInfobarIfNeeded, this);
|
|
super.dispose();
|
|
|
|
this._clearExecutionLine();
|
|
UI.Context.Context.instance().removeFlavorChangeListener(SDK.DebuggerModel.CallFrame, this._callFrameChanged, this);
|
|
this._liveLocationPool.disposeAll();
|
|
}
|
|
}
|
|
|
|
export class BreakpointDecoration {
|
|
/**
|
|
* @param {!SourceFrame.SourcesTextEditor.SourcesTextEditor} textEditor
|
|
* @param {!TextEditor.CodeMirrorTextEditor.TextEditorPositionHandle} handle
|
|
* @param {string} condition
|
|
* @param {boolean} enabled
|
|
* @param {boolean} bound
|
|
* @param {?Bindings.BreakpointManager.Breakpoint} breakpoint
|
|
*/
|
|
constructor(textEditor, handle, condition, enabled, bound, breakpoint) {
|
|
this._textEditor = textEditor;
|
|
this.handle = handle;
|
|
this.condition = condition;
|
|
this.enabled = enabled;
|
|
this.bound = bound;
|
|
this.breakpoint = breakpoint;
|
|
this.element = document.createElement('span');
|
|
this.element.classList.toggle('cm-inline-breakpoint', true);
|
|
|
|
/** @type {?TextEditor.CodeMirrorTextEditor.TextEditorBookMark} */
|
|
this.bookmark = null;
|
|
}
|
|
|
|
/**
|
|
* @param {!BreakpointDecoration} decoration1
|
|
* @param {!BreakpointDecoration} decoration2
|
|
* @return {number}
|
|
*/
|
|
static mostSpecificFirst(decoration1, decoration2) {
|
|
if (decoration1.enabled !== decoration2.enabled) {
|
|
return decoration1.enabled ? -1 : 1;
|
|
}
|
|
if (decoration1.bound !== decoration2.bound) {
|
|
return decoration1.bound ? -1 : 1;
|
|
}
|
|
if (!!decoration1.condition !== !!decoration2.condition) {
|
|
return !!decoration1.condition ? -1 : 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
update() {
|
|
const isLogpoint = !!this.condition && this.condition.includes(LogpointPrefix);
|
|
const isConditionalBreakpoint = !!this.condition && !isLogpoint;
|
|
this.element.classList.toggle('cm-inline-logpoint', isLogpoint);
|
|
this.element.classList.toggle('cm-inline-breakpoint-conditional', isConditionalBreakpoint);
|
|
this.element.classList.toggle('cm-inline-disabled', !this.enabled);
|
|
}
|
|
|
|
show() {
|
|
if (this.bookmark) {
|
|
return;
|
|
}
|
|
const editorLocation = this.handle.resolve();
|
|
if (!editorLocation) {
|
|
return;
|
|
}
|
|
this.bookmark = this._textEditor.addBookmark(
|
|
editorLocation.lineNumber, editorLocation.columnNumber, this.element, BreakpointDecoration.bookmarkSymbol);
|
|
// @ts-ignore Only used for layout tests
|
|
this.bookmark[BreakpointDecoration._elementSymbolForTest] = this.element;
|
|
}
|
|
|
|
hide() {
|
|
if (!this.bookmark) {
|
|
return;
|
|
}
|
|
this.bookmark.clear();
|
|
this.bookmark = null;
|
|
}
|
|
|
|
dispose() {
|
|
const location = this.handle.resolve();
|
|
if (location) {
|
|
this._textEditor.toggleLineClass(location.lineNumber, 'cm-breakpoint', false);
|
|
this._textEditor.toggleLineClass(location.lineNumber, 'cm-breakpoint-disabled', false);
|
|
this._textEditor.toggleLineClass(location.lineNumber, 'cm-breakpoint-unbound', false);
|
|
this._textEditor.toggleLineClass(location.lineNumber, 'cm-breakpoint-conditional', false);
|
|
this._textEditor.toggleLineClass(location.lineNumber, 'cm-breakpoint-logpoint', false);
|
|
}
|
|
this.hide();
|
|
}
|
|
}
|
|
|
|
BreakpointDecoration.bookmarkSymbol = Symbol('bookmark');
|
|
BreakpointDecoration._elementSymbolForTest = Symbol('element');
|
|
|
|
export const continueToLocationDecorationSymbol = Symbol('bookmark');
|