debug-server-next
Version:
Dev server for hippy-core.
1,041 lines (1,040 loc) • 86.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.
*/
/* eslint-disable rulesdir/no_underscored_properties */
import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import * as Workspace from '../../models/workspace/workspace.js';
import * as ObjectUI from '../../ui/legacy/components/object_ui/object_ui.js';
import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js';
import * as UI from '../../ui/legacy/legacy.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 { getRegisteredEditorActions } from './SourcesView.js';
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 Context menu command to delete/remove a breakpoint that the user
*has set. One line of code can have multiple breakpoints. Always >= 1 breakpoint.
*/
removeBreakpoint: '{n, plural, =1 {Remove breakpoint} other {Remove all breakpoints in line}}',
/**
*@description A context menu item in the Debugger Plugin of the Sources panel
*/
editBreakpoint: 'Edit breakpoint…',
/**
*@description Context menu command to disable (but not delete) a breakpoint
*that the user has set. One line of code can have multiple breakpoints. Always
*>= 1 breakpoint.
*/
disableBreakpoint: '{n, plural, =1 {Disable breakpoint} other {Disable all breakpoints in line}}',
/**
*@description Context menu command to enable a breakpoint that the user has
*set. One line of code can have multiple breakpoints. Always >= 1 breakpoint.
*/
enableBreakpoint: '{n, plural, =1 {Enable breakpoint} other {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('panels/sources/DebuggerPlugin.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
// eslint-disable-next-line no-unused-vars
class DecoratorWidget extends HTMLDivElement {
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/naming-convention
__nameToToken;
constructor() {
super();
}
}
export class DebuggerPlugin extends Plugin {
_textEditor;
_uiSourceCode;
_transformer;
_executionLocation;
_controlDown;
_asyncStepInHoveredLine;
_asyncStepInHovered;
_clearValueWidgetsTimer;
_sourceMapInfobar;
_controlTimeout;
_scriptsPanel;
_breakpointManager;
_popoverHelper;
_boundPopoverHelperHide;
_boundKeyDown;
_boundKeyUp;
_boundMouseMove;
_boundMouseDown;
_boundBlur;
_boundWheel;
_boundGutterClick;
_breakpointDecorations;
_decorationByBreakpoint;
_possibleBreakpointsRequested;
_scriptFileForDebuggerModel;
_valueWidgets;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_continueToLocationDecorations;
_liveLocationPool;
_muted;
_mutedFromStart;
_ignoreListInfobar;
_hasLineWithoutMapping;
_prettyPrintInfobar;
_scheduledBreakpointDecorationUpdates;
constructor(textEditor, uiSourceCode, transformer) {
super();
this._textEditor = textEditor;
this._uiSourceCode = uiSourceCode;
this._transformer = transformer;
this._executionLocation = null;
this._controlDown = false;
this._asyncStepInHoveredLine = 0;
this._asyncStepInHovered = false;
this._clearValueWidgetsTimer = null;
this._sourceMapInfobar = null;
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 = this._onKeyDown.bind(this);
this._textEditor.element.addEventListener('keydown', this._boundKeyDown, true);
this._boundKeyUp = this._onKeyUp.bind(this);
this._textEditor.element.addEventListener('keyup', this._boundKeyUp, true);
this._boundMouseMove = this._onMouseMove.bind(this);
this._textEditor.element.addEventListener('mousemove', this._boundMouseMove, false);
this._boundMouseDown = this._onMouseDown.bind(this);
this._textEditor.element.addEventListener('mousedown', this._boundMouseDown, true);
this._boundBlur = this._onBlur.bind(this);
this._textEditor.element.addEventListener('focusout', this._boundBlur, false);
this._boundWheel = this._onWheel.bind(this);
this._textEditor.element.addEventListener('wheel', this._boundWheel, true);
this._boundGutterClick = ((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);
this._breakpointDecorations = new Set();
this._decorationByBreakpoint = new Map();
this._possibleBreakpointsRequested = new Set();
this._scriptFileForDebuggerModel = new Map();
Common.Settings.Settings.instance()
.moduleSetting('skipStackFramesPattern')
.addChangeListener(this._showIgnoreListInfobarIfNeeded, this);
Common.Settings.Settings.instance()
.moduleSetting('skipContentScripts')
.addChangeListener(this._showIgnoreListInfobarIfNeeded, this);
this._valueWidgets = new Map();
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();
}
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')) {
this._prettyPrintInfobar = null;
this._detectMinified();
}
}
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;
}
wasShown() {
if (this._executionLocation) {
// We need SourcesTextEditor to be initialized prior to this call. @see crbug.com/499889
queueMicrotask(() => {
this._generateValuesInSource();
});
}
}
willHide() {
this._popoverHelper.hidePopover();
}
async populateLineGutterContextMenu(contextMenu, editorLineNumber) {
const uiLocation = new Workspace.UISourceCode.UILocation(this._uiSourceCode, editorLineNumber, 0);
this._scriptsPanel.appendUILocationItems(contextMenu, uiLocation);
const breakpoints = 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 removeTitle = i18nString(UIStrings.removeBreakpoint, { n: breakpoints.length });
contextMenu.debugSection().appendItem(removeTitle, () => breakpoints.map(breakpoint => breakpoint.remove(false)));
if (breakpoints.length === 1 && 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 = i18nString(UIStrings.disableBreakpoint, { n: breakpoints.length });
contextMenu.debugSection().appendItem(title, () => breakpoints.map(breakpoint => breakpoint.setEnabled(false)));
}
const hasDisabled = breakpoints.some(breakpoint => !breakpoint.enabled());
if (hasDisabled) {
const title = i18nString(UIStrings.enableBreakpoint, { n: breakpoints.length });
contextMenu.debugSection().appendItem(title, () => breakpoints.map(breakpoint => breakpoint.setEnabled(true)));
}
}
}
populateTextAreaContextMenu(contextMenu, editorLineNumber, editorColumnNumber) {
function addSourceMapURL(scriptFile) {
const dialog = new AddSourceMapURLDialog(addSourceMapURLDialogCallback.bind(null, scriptFile));
dialog.show();
}
function addSourceMapURLDialogCallback(scriptFile, url) {
if (!url) {
return;
}
scriptFile.addSourceMapURL(url);
}
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();
}
}
_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);
}
}
}
_isIdentifier(tokenType) {
return tokenType.startsWith('js-variable') || tokenType.startsWith('js-property') || tokenType === 'js-def' ||
tokenType === 'variable';
}
_getPopoverRequest(event) {
if (UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(event)) {
return null;
}
const target = UI.Context.Context.instance().flavor(SDK.Target.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 editorLineNumber = -1;
let startHighlight = -1;
let endHighlight = -1;
const selectedCallFrame = 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;
}
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;
}
editorLineNumber = textPosition.startLine;
startHighlight = token.startColumn;
endHighlight = token.endColumn - 1;
// For $label identifiers we can't show a meaningful preview (https://crbug.com/1155548),
// so we suppress them for now. Label identifiers can only appear as operands to control
// instructions[1], so we just check the first token on the line and filter them out.
//
// [1]: https://webassembly.github.io/spec/core/text/instructions.html#control-instructions
for (let firstColumn = 0; firstColumn < startHighlight; ++firstColumn) {
const firstToken = this._textEditor.tokenAtTextPosition(editorLineNumber, firstColumn);
if (firstToken && firstToken.type === 'keyword') {
const line = this._textEditor.line(editorLineNumber);
switch (line.substring(firstToken.startColumn, firstToken.endColumn)) {
case 'block':
case 'loop':
case 'if':
case 'else':
case 'end':
case 'br':
case 'br_if':
case 'br_table':
return null;
default:
break;
}
break;
}
}
}
else {
let token = this._textEditor.tokenAtTextPosition(textPosition.startLine, textPosition.startColumn);
if (!token) {
return null;
}
editorLineNumber = textPosition.startLine;
const line = this._textEditor.line(editorLineNumber);
let tokenContent = line.substring(token.startColumn, token.endColumn);
// When the user hovers an opening bracket, we look for the closing bracket
// and kick off the matching from that below.
if (tokenContent === '[') {
const closingColumn = line.indexOf(']', token.startColumn);
if (closingColumn < 0) {
return null;
}
token = this._textEditor.tokenAtTextPosition(editorLineNumber, closingColumn);
if (!token) {
return null;
}
tokenContent = line.substring(token.startColumn, token.endColumn);
}
startHighlight = token.startColumn;
endHighlight = token.endColumn - 1;
// Consume multiple `[index][0]...[f(1)]` at the end of the expression.
while (tokenContent === ']') {
startHighlight = line.lastIndexOf('[', startHighlight) - 1;
if (startHighlight < 0) {
return null;
}
token = this._textEditor.tokenAtTextPosition(editorLineNumber, startHighlight);
if (!token) {
return null;
}
tokenContent = line.substring(token.startColumn, token.endColumn);
startHighlight = token.startColumn;
}
if (!token.type) {
return null;
}
const isIdentifier = this._isIdentifier(token.type);
if (!isIdentifier && (token.type !== 'js-keyword' || tokenContent !== 'this')) {
return null;
}
while (startHighlight > 1 && line.charAt(startHighlight - 1) === '.') {
// Consume multiple `[index][0]...[f(1)]` preceeding a dot.
while (line.charAt(startHighlight - 2) === ']') {
startHighlight = line.lastIndexOf('[', startHighlight - 2) - 1;
if (startHighlight < 0) {
return null;
}
}
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;
}
}
const leftCorner = this._textEditor.cursorPositionToCoordinates(editorLineNumber, startHighlight);
const rightCorner = this._textEditor.cursorPositionToCoordinates(editorLineNumber, endHighlight);
const box = new AnchorBox(leftCorner.x, leftCorner.y, rightCorner.x - leftCorner.x, leftCorner.height);
let objectPopoverHelper = null;
let highlightDescriptor = null;
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,
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);
}
},
};
}
_onWheel(event) {
if (this._executionLocation && UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(event)) {
event.preventDefault();
}
}
_onKeyDown(event) {
if (!event.ctrlKey || (!event.metaKey && Host.Platform.isMac())) {
this._clearControlDown();
}
if (event.key === Platform.KeyboardUtilities.ESCAPE_KEY) {
if (this._popoverHelper.isPopoverVisible()) {
this._popoverHelper.hidePopover();
event.consume();
}
return;
}
if (UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(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);
}
}
}
_onMouseMove(event) {
if (this._executionLocation && this._controlDown &&
UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(event)) {
if (!this._continueToLocationDecorations) {
this._showContinueToLocations();
}
}
if (this._continueToLocationDecorations) {
const target = 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);
}
}
_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);
}
}
_onMouseDown(event) {
if (!this._executionLocation || !UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(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;
}
}
}
_onBlur(_event) {
this._clearControlDown();
}
_onKeyUp(_event) {
this._clearControlDown();
}
_clearControlDown() {
this._controlDown = false;
this._clearContinueToLocations();
if (this._controlTimeout) {
clearTimeout(this._controlTimeout);
}
}
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);
dialog.focusEditor();
}
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)));
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 = 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 !== "call" /* 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 === "call" /* 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 === "call" /* 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() {
}
_findAsyncStepInRange(textEditor, editorLineNumber, line, column) {
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;
}
}
}
_asyncStepIn(location, isCurrentPosition) {
if (!isCurrentPosition) {
location.continueToLocation(asyncStepIn);
}
else {
asyncStepIn();
}
function asyncStepIn() {
location.debuggerModel.scheduleStepIntoAsync();
}
}
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(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) {