@quick-game/cli
Version:
Command line interface for rapid qg development
1,026 lines (1,025 loc) • 105 kB
JavaScript
/*
* Copyright (C) 2011 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import * as 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 Breakpoints from '../../models/breakpoints/breakpoints.js';
import * as Formatter from '../../models/formatter/formatter.js';
import * as SourceMapScopes from '../../models/source_map_scopes/source_map_scopes.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import * as Workspace from '../../models/workspace/workspace.js';
import * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.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 * as SourceComponents from './components/components.js';
import { AddDebugInfoURLDialog } from './AddSourceMapURLDialog.js';
import { BreakpointEditDialog } from './BreakpointEditDialog.js';
import { Plugin } from './Plugin.js';
import { SourcesPanel } from './SourcesPanel.js';
const { EMPTY_BREAKPOINT_CONDITION, NEVER_PAUSE_HERE_CONDITION } = Breakpoints.BreakpointManager;
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 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
*/
addWasmDebugInfo: 'Add DWARF debug info…',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
sourceMapLoaded: 'Source map loaded.',
/**
*@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.',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
sourceMapSkipped: 'Source map skipped for this file.',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
sourceMapFailed: 'Source map failed to load.',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
debuggingPowerReduced: 'DevTools can\'t show authored sources, but you can debug the deployed code.',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
reloadForSourceMap: 'To enable again, make sure the file isn\'t on the ignore list and reload.',
/**
*@description Text in Debugger Plugin of the Sources panel
*@example {http://site.com/lib.js.map} PH1
*@example {HTTP error: status code 404, net::ERR_UNKNOWN_URL_SCHEME} PH2
*/
errorLoading: 'Error loading url {PH1}: {PH2}',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
ignoreScript: 'Ignore this file',
/**
*@description Text in Debugger Plugin of the Sources panel
*/
ignoreContentScripts: 'Ignore extension scripts',
/**
*@description Error message that is displayed in UI when a file needed for debugging information for a call frame is missing
*@example {src/myapp.debug.wasm.dwp} PH1
*/
debugFileNotFound: 'Failed to load debug file "{PH1}".',
/**
*@description Error message that is displayed when no debug info could be loaded
*@example {app.wasm} PH1
*/
debugInfoNotFound: 'Failed to load any debug info for {PH1}.',
};
const str_ = i18n.i18n.registerUIStrings('panels/sources/DebuggerPlugin.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
// Note: Line numbers are passed around as zero-based numbers (though
// CodeMirror numbers them from 1).
// Don't scan for possible breakpoints on a line beyond this position;
const MAX_POSSIBLE_BREAKPOINT_LINE = 2500;
// Limits on inline variable view computation.
const MAX_CODE_SIZE_FOR_VALUE_DECORATIONS = 10000;
const MAX_PROPERTIES_IN_SCOPE_FOR_VALUE_DECORATIONS = 500;
const debuggerPluginForUISourceCode = new Map();
export class DebuggerPlugin extends Plugin {
transformer;
editor = undefined;
// Set if the debugger is stopped on a breakpoint in this file
executionLocation = null;
// Track state of the control key because holding it makes debugger
// target locations show up in the editor
controlDown = false;
controlTimeout = undefined;
sourceMapInfobar = null;
scriptsPanel;
breakpointManager;
// Manages pop-overs shown when the debugger is active and the user
// hovers over an expression
popoverHelper = null;
scriptFileForDebuggerModel;
// The current set of breakpoints for this file. The locations in
// here are kept in sync with their editor position. When a file's
// content is edited and later saved, these are used as a source of
// truth for re-creating the breakpoints.
breakpoints = [];
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
continueToLocations = null;
liveLocationPool;
// When the editor content is changed by the user, this becomes
// true. When the plugin is muted, breakpoints show up as disabled
// and can't be manipulated. It is cleared again when the content is
// saved.
muted;
// If the plugin is initialized in muted state, we cannot correlated
// breakpoint position in the breakpoint manager with editor
// locations, so breakpoint manipulation is permanently disabled.
initializedMuted;
ignoreListInfobar;
refreshBreakpointsTimeout = undefined;
activeBreakpointDialog = null;
#activeBreakpointEditRequest = undefined;
#scheduledFinishingActiveDialog = false;
missingDebugInfoBar = null;
#sourcesPanelDebuggedMetricsRecorded = false;
loader;
ignoreListCallback;
constructor(uiSourceCode, transformer) {
super(uiSourceCode);
this.transformer = transformer;
debuggerPluginForUISourceCode.set(uiSourceCode, this);
this.scriptsPanel = SourcesPanel.instance();
this.breakpointManager = Breakpoints.BreakpointManager.BreakpointManager.instance();
this.breakpointManager.addEventListener(Breakpoints.BreakpointManager.Events.BreakpointAdded, this.breakpointChange, this);
this.breakpointManager.addEventListener(Breakpoints.BreakpointManager.Events.BreakpointRemoved, this.breakpointChange, this);
this.uiSourceCode.addEventListener(Workspace.UISourceCode.Events.WorkingCopyChanged, this.workingCopyChanged, this);
this.uiSourceCode.addEventListener(Workspace.UISourceCode.Events.WorkingCopyCommitted, this.workingCopyCommitted, this);
this.scriptFileForDebuggerModel = new Map();
this.loader = SDK.PageResourceLoader.PageResourceLoader.instance();
this.loader.addEventListener(SDK.PageResourceLoader.Events.Update, this.showSourceMapInfobarIfNeeded.bind(this), this);
this.ignoreListCallback = this.showIgnoreListInfobarIfNeeded.bind(this);
Bindings.IgnoreListManager.IgnoreListManager.instance().addChangeListener(this.ignoreListCallback);
UI.Context.Context.instance().addFlavorChangeListener(SDK.DebuggerModel.CallFrame, this.callFrameChanged, this);
this.liveLocationPool = new Bindings.LiveLocation.LiveLocationPool();
this.updateScriptFiles();
this.muted = this.uiSourceCode.isDirty();
this.initializedMuted = this.muted;
this.ignoreListInfobar = null;
this.showIgnoreListInfobarIfNeeded();
for (const scriptFile of this.scriptFileForDebuggerModel.values()) {
scriptFile.checkMapping();
}
}
editorExtension() {
// Kludge to hook editor keyboard events into the ShortcutRegistry
// system.
const handlers = this.shortcutHandlers();
return [
CodeMirror.EditorView.updateListener.of(update => this.onEditorUpdate(update)),
CodeMirror.EditorView.domEventHandlers({
keydown: (event) => {
if (this.onKeyDown(event)) {
return true;
}
handlers(event);
return event.defaultPrevented;
},
keyup: event => this.onKeyUp(event),
mousemove: event => this.onMouseMove(event),
mousedown: event => this.onMouseDown(event),
focusout: event => this.onBlur(event),
wheel: event => this.onWheel(event),
}),
CodeMirror.lineNumbers({
domEventHandlers: {
mousedown: (view, block, event) => this.handleGutterClick(view.state.doc.lineAt(block.from), event),
},
}),
infobarState,
breakpointMarkers,
CodeMirror.Prec.highest(executionLine.field),
CodeMirror.Prec.lowest(continueToMarkers.field),
markIfContinueTo,
valueDecorations.field,
CodeMirror.Prec.lowest(evalExpression.field),
theme,
this.uiSourceCode.project().type() === Workspace.Workspace.projectTypes.Debugger ?
CodeMirror.EditorView.editorAttributes.of({ class: 'source-frame-debugger-script' }) :
[],
];
}
shortcutHandlers() {
const selectionLine = (editor) => {
return editor.state.doc.lineAt(editor.state.selection.main.head);
};
return UI.ShortcutRegistry.ShortcutRegistry.instance().getShortcutListener({
'debugger.toggle-breakpoint': async () => {
if (this.muted || !this.editor) {
return false;
}
await this.toggleBreakpoint(selectionLine(this.editor), false);
return true;
},
'debugger.toggle-breakpoint-enabled': async () => {
if (this.muted || !this.editor) {
return false;
}
await this.toggleBreakpoint(selectionLine(this.editor), true);
return true;
},
'debugger.breakpoint-input-window': async () => {
if (this.muted || !this.editor) {
return false;
}
const line = selectionLine(this.editor);
Host.userMetrics.breakpointEditDialogRevealedFrom(4 /* Host.UserMetrics.BreakpointEditDialogRevealedFrom.KeyboardShortcut */);
this.#openEditDialogForLine(line);
return true;
},
});
}
#openEditDialogForLine(line) {
if (this.muted) {
return;
}
if (this.activeBreakpointDialog) {
this.activeBreakpointDialog.finishEditing(false, '');
}
const breakpoint = this.breakpoints.find(b => b.position >= line.from && b.position <= line.to)?.breakpoint || null;
this.editBreakpointCondition({ line, breakpoint, location: null, isLogpoint: breakpoint?.isLogpoint() });
}
editorInitialized(editor) {
// Start asynchronous actions that require access to the editor
// instance
this.editor = editor;
computeNonBreakableLines(editor.state, this.transformer, this.uiSourceCode).then(linePositions => {
if (linePositions.length) {
editor.dispatch({ effects: SourceFrame.SourceFrame.addNonBreakableLines.of(linePositions) });
}
}, console.error);
if (this.ignoreListInfobar) {
this.attachInfobar(this.ignoreListInfobar);
}
if (this.missingDebugInfoBar) {
this.attachInfobar(this.missingDebugInfoBar);
}
if (this.sourceMapInfobar) {
this.attachInfobar(this.sourceMapInfobar);
}
if (!this.muted) {
void this.refreshBreakpoints();
}
void this.callFrameChanged();
this.popoverHelper?.dispose();
this.popoverHelper = new UI.PopoverHelper.PopoverHelper(editor, this.getPopoverRequest.bind(this));
this.popoverHelper.setDisableOnClick(true);
this.popoverHelper.setTimeout(250, 250);
this.popoverHelper.setHasPadding(true);
}
static accepts(uiSourceCode) {
return uiSourceCode.contentType().hasScripts();
}
showIgnoreListInfobarIfNeeded() {
const uiSourceCode = this.uiSourceCode;
if (!uiSourceCode.contentType().hasScripts()) {
return;
}
if (!Bindings.IgnoreListManager.IgnoreListManager.instance().isUserOrSourceMapIgnoreListedUISourceCode(uiSourceCode)) {
this.hideIgnoreListInfobar();
return;
}
if (this.ignoreListInfobar) {
this.ignoreListInfobar.dispose();
}
function unIgnoreList() {
Bindings.IgnoreListManager.IgnoreListManager.instance().unIgnoreListUISourceCode(uiSourceCode);
}
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.setCloseCallback(() => this.removeInfobar(this.ignoreListInfobar));
infobar.createDetailsRowMessage(i18nString(UIStrings.theDebuggerWillSkipStepping));
this.attachInfobar(this.ignoreListInfobar);
}
attachInfobar(bar) {
if (this.editor) {
this.editor.dispatch({ effects: addInfobar.of(bar) });
}
}
removeInfobar(bar) {
if (this.editor && bar) {
this.editor.dispatch({ effects: removeInfobar.of(bar) });
}
}
hideIgnoreListInfobar() {
if (!this.ignoreListInfobar) {
return;
}
this.ignoreListInfobar.dispose();
this.ignoreListInfobar = null;
}
willHide() {
this.popoverHelper?.hidePopover();
}
editBreakpointLocation({ breakpoint, uiLocation }) {
const { lineNumber } = this.transformer.uiLocationToEditorLocation(uiLocation.lineNumber, uiLocation.columnNumber);
const line = this.editor?.state.doc.line(lineNumber + 1);
if (!line) {
return;
}
this.editBreakpointCondition({ line, breakpoint, location: null, isLogpoint: breakpoint.isLogpoint() });
}
populateLineGutterContextMenu(contextMenu, editorLineNumber) {
const uiLocation = new Workspace.UISourceCode.UILocation(this.uiSourceCode, editorLineNumber, 0);
this.scriptsPanel.appendUILocationItems(contextMenu, uiLocation);
if (this.muted || !this.editor) {
return;
}
const line = this.editor.state.doc.line(editorLineNumber + 1);
const breakpoints = this.lineBreakpoints(line);
const supportsConditionalBreakpoints = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().supportsConditionalBreakpoints(this.uiSourceCode);
if (!breakpoints.length) {
if (this.editor && SourceFrame.SourceFrame.isBreakableLine(this.editor.state, line)) {
contextMenu.debugSection().appendItem(i18nString(UIStrings.addBreakpoint), this.createNewBreakpoint.bind(this, line, EMPTY_BREAKPOINT_CONDITION, /* enabled */ true, /* isLogpoint */ false));
if (supportsConditionalBreakpoints) {
contextMenu.debugSection().appendItem(i18nString(UIStrings.addConditionalBreakpoint), () => {
Host.userMetrics.breakpointEditDialogRevealedFrom(3 /* Host.UserMetrics.BreakpointEditDialogRevealedFrom.LineGutterContextMenu */);
this.editBreakpointCondition({ line, breakpoint: null, location: null, isLogpoint: false });
});
contextMenu.debugSection().appendItem(i18nString(UIStrings.addLogpoint), () => {
Host.userMetrics.breakpointEditDialogRevealedFrom(3 /* Host.UserMetrics.BreakpointEditDialogRevealedFrom.LineGutterContextMenu */);
this.editBreakpointCondition({ line, breakpoint: null, location: null, isLogpoint: true });
});
contextMenu.debugSection().appendItem(i18nString(UIStrings.neverPauseHere), this.createNewBreakpoint.bind(this, line, NEVER_PAUSE_HERE_CONDITION, /* enabled */ true, /* isLogpoint */ false));
}
}
}
else {
const removeTitle = i18nString(UIStrings.removeBreakpoint, { n: breakpoints.length });
contextMenu.debugSection().appendItem(removeTitle, () => breakpoints.forEach(breakpoint => {
Host.userMetrics.actionTaken(Host.UserMetrics.Action.BreakpointRemovedFromGutterContextMenu);
void 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), () => {
Host.userMetrics.breakpointEditDialogRevealedFrom(2 /* Host.UserMetrics.BreakpointEditDialogRevealedFrom.BreakpointMarkerContextMenu */);
this.editBreakpointCondition({ line, breakpoint: breakpoints[0], location: null });
});
}
const hasEnabled = breakpoints.some(breakpoint => breakpoint.enabled());
if (hasEnabled) {
const title = i18nString(UIStrings.disableBreakpoint, { n: breakpoints.length });
contextMenu.debugSection().appendItem(title, () => breakpoints.forEach(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.forEach(breakpoint => breakpoint.setEnabled(true)));
}
}
}
populateTextAreaContextMenu(contextMenu) {
function addSourceMapURL(scriptFile) {
const dialog = AddDebugInfoURLDialog.createAddSourceMapURLDialog(addSourceMapURLDialogCallback.bind(null, scriptFile));
dialog.show();
}
function addSourceMapURLDialogCallback(scriptFile, url) {
if (!url) {
return;
}
scriptFile.addSourceMapURL(url);
}
function addDebugInfoURL(scriptFile) {
const dialog = AddDebugInfoURLDialog.createAddDWARFSymbolsURLDialog(addDebugInfoURLDialogCallback.bind(null, scriptFile));
dialog.show();
}
function addDebugInfoURLDialogCallback(scriptFile, url) {
if (!url) {
return;
}
scriptFile.addDebugInfoURL(url);
}
if (this.uiSourceCode.project().type() === Workspace.Workspace.projectTypes.Network &&
Common.Settings.Settings.instance().moduleSetting('jsSourceMapsEnabled').get() &&
!Bindings.IgnoreListManager.IgnoreListManager.instance().isUserIgnoreListedURL(this.uiSourceCode.url())) {
if (this.scriptFileForDebuggerModel.size) {
const scriptFile = this.scriptFileForDebuggerModel.values().next().value;
const addSourceMapURLLabel = i18nString(UIStrings.addSourceMap);
contextMenu.debugSection().appendItem(addSourceMapURLLabel, addSourceMapURL.bind(null, scriptFile));
if (scriptFile.script?.isWasm() &&
!Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().pluginManager?.hasPluginForScript(scriptFile.script)) {
contextMenu.debugSection().appendItem(i18nString(UIStrings.addWasmDebugInfo), addDebugInfoURL.bind(null, scriptFile));
}
}
}
}
workingCopyChanged() {
if (!this.scriptFileForDebuggerModel.size) {
this.setMuted(this.uiSourceCode.isDirty());
}
}
workingCopyCommitted() {
this.scriptsPanel.updateLastModificationTime();
if (!this.scriptFileForDebuggerModel.size) {
this.setMuted(false);
}
}
didMergeToVM() {
if (this.consistentScripts()) {
this.setMuted(false);
}
}
didDivergeFromVM() {
this.setMuted(true);
}
setMuted(value) {
if (this.initializedMuted) {
return;
}
if (value !== this.muted) {
this.muted = value;
if (!value) {
void this.restoreBreakpointsAfterEditing();
}
else if (this.editor) {
this.editor.dispatch({ effects: muteBreakpoints.of(null) });
}
}
}
consistentScripts() {
for (const scriptFile of this.scriptFileForDebuggerModel.values()) {
if (scriptFile.hasDivergedFromVM() || scriptFile.isMergingToVM()) {
return false;
}
}
return true;
}
isVariableIdentifier(tokenType) {
return tokenType === 'VariableName' || tokenType === 'VariableDefinition';
}
isIdentifier(tokenType) {
return tokenType === 'VariableName' || tokenType === 'VariableDefinition' || tokenType === 'PropertyName' ||
tokenType === 'PropertyDefinition';
}
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;
const { editor } = this;
if (!debuggerModel || !debuggerModel.isPaused() || !editor) {
return null;
}
const selectedCallFrame = UI.Context.Context.instance().flavor(SDK.DebuggerModel.CallFrame);
if (!selectedCallFrame) {
return null;
}
let textPosition = editor.editor.posAtCoords(event);
if (!textPosition) {
return null;
}
const positionCoords = editor.editor.coordsAtPos(textPosition);
if (!positionCoords || event.clientY < positionCoords.top || event.clientY > positionCoords.bottom ||
event.clientX < positionCoords.left - 30 || event.clientX > positionCoords.right + 30) {
return null;
}
if (event.clientX < positionCoords.left && textPosition > editor.state.doc.lineAt(textPosition).from) {
textPosition -= 1;
}
const highlightRange = computePopoverHighlightRange(editor.state, this.uiSourceCode.mimeType(), textPosition);
if (!highlightRange) {
return null;
}
const highlightLine = editor.state.doc.lineAt(highlightRange.from);
if (highlightRange.to > highlightLine.to) {
return null;
}
const leftCorner = editor.editor.coordsAtPos(highlightRange.from);
const rightCorner = editor.editor.coordsAtPos(highlightRange.to);
if (!leftCorner || !rightCorner) {
return null;
}
const box = new AnchorBox(leftCorner.left, leftCorner.top - 2, rightCorner.right - leftCorner.left, rightCorner.bottom - leftCorner.top);
const evaluationText = editor.state.sliceDoc(highlightRange.from, highlightRange.to);
let objectPopoverHelper = null;
return {
box,
show: async (popover) => {
let resolvedText = '';
if (Root.Runtime.experiments.isEnabled('evaluateExpressionsWithSourceMaps')) {
const nameMap = await SourceMapScopes.NamesResolver.allVariablesInCallFrame(selectedCallFrame);
try {
resolvedText =
await Formatter.FormatterWorkerPool.formatterWorkerPool().javaScriptSubstitute(evaluationText, nameMap);
}
catch {
}
}
else {
resolvedText = await SourceMapScopes.NamesResolver.resolveExpression(selectedCallFrame, evaluationText, this.uiSourceCode, highlightLine.number - 1, highlightRange.from - highlightLine.from, highlightRange.to - highlightLine.from);
}
// We use side-effect free debug-evaluate when the highlighted expression contains a
// function/method call. Otherwise we allow side-effects. The motiviation here are
// frameworks like Vue, that heavily use proxies for caching:
//
// * We deem a simple property access of a proxy as deterministic so it should be
// successful even if V8 thinks its side-effecting.
// * Explicit function calls on the other hand must be side-effect free. The canonical
// example is hovering over {Math.random()} which would result in a different value
// each time the user hovers over it.
const throwOnSideEffect = Root.Runtime.experiments.isEnabled('evaluateExpressionsWithSourceMaps') &&
highlightRange.containsCallExpression;
const result = await selectedCallFrame.evaluate({
expression: resolvedText || evaluationText,
objectGroup: 'popover',
includeCommandLineAPI: false,
silent: true,
returnByValue: false,
generatePreview: false,
throwOnSideEffect,
timeout: undefined,
disableBreaks: undefined,
replMode: undefined,
allowUnsafeEvalBlockedByCSP: undefined,
});
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 decoration = CodeMirror.Decoration.set(evalExpressionMark.range(highlightRange.from, highlightRange.to));
editor.dispatch({ effects: evalExpression.update.of(decoration) });
return true;
},
hide: () => {
if (objectPopoverHelper) {
objectPopoverHelper.dispose();
}
debuggerModel.runtimeModel().releaseObjectGroup('popover');
editor.dispatch({ effects: evalExpression.update.of(CodeMirror.Decoration.none) });
},
};
}
onEditorUpdate(update) {
if (!update.changes.empty) {
// If the document changed, adjust known breakpoint positions
// for that change
for (const breakpointDesc of this.breakpoints) {
breakpointDesc.position = update.changes.mapPos(breakpointDesc.position);
}
}
}
onWheel(event) {
if (this.executionLocation && UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(event)) {
event.preventDefault();
}
}
onKeyDown(event) {
const ctrlDown = UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(event);
if (!ctrlDown) {
this.setControlDown(false);
}
if (event.key === Platform.KeyboardUtilities.ESCAPE_KEY) {
if (this.popoverHelper && this.popoverHelper.isPopoverVisible()) {
this.popoverHelper.hidePopover();
event.consume();
return true;
}
}
if (ctrlDown && this.executionLocation) {
this.setControlDown(true);
}
return false;
}
onMouseMove(event) {
if (this.executionLocation && this.controlDown &&
UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(event)) {
if (!this.continueToLocations) {
void this.showContinueToLocations();
}
}
}
onMouseDown(event) {
if (!this.executionLocation || !UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(event)) {
return;
}
if (!this.continueToLocations || !this.editor) {
return;
}
event.consume();
const textPosition = this.editor.editor.posAtCoords(event);
if (textPosition === null) {
return;
}
for (const { from, to, click } of this.continueToLocations) {
if (from <= textPosition && to >= textPosition) {
click();
break;
}
}
}
onBlur(_event) {
this.setControlDown(false);
}
onKeyUp(_event) {
this.setControlDown(false);
}
setControlDown(state) {
if (state !== this.controlDown) {
this.controlDown = state;
clearTimeout(this.controlTimeout);
this.controlTimeout = undefined;
if (state && this.executionLocation) {
this.controlTimeout = window.setTimeout(() => {
if (this.executionLocation && this.controlDown) {
void this.showContinueToLocations();
}
}, 150);
}
else {
this.clearContinueToLocations();
}
}
}
editBreakpointCondition(breakpointEditRequest) {
const { line, breakpoint, location, isLogpoint } = breakpointEditRequest;
if (breakpoint?.isRemoved) {
// This method can get called for stale breakpoints, e.g. via the revealer.
// In that case we don't show the edit dialog as to not resurrect the breakpoint
// unintentionally.
return;
}
this.#scheduledFinishingActiveDialog = false;
const isRepeatedEditRequest = this.#activeBreakpointEditRequest &&
isSameEditRequest(this.#activeBreakpointEditRequest, breakpointEditRequest);
if (isRepeatedEditRequest) {
// Do not re-show the same edit dialog, instead use the already open one.
return;
}
if (this.activeBreakpointDialog) {
// If this a request to edit a different dialog, make sure to close the current active one
// to avoid showing two dialogs at the same time.
this.activeBreakpointDialog.saveAndFinish();
}
const editor = this.editor;
const oldCondition = breakpoint ? breakpoint.condition() : '';
const isLogpointForDialog = breakpoint?.isLogpoint() ?? Boolean(isLogpoint);
const decorationElement = document.createElement('div');
const compartment = new CodeMirror.Compartment();
const dialog = new BreakpointEditDialog(line.number - 1, oldCondition, isLogpointForDialog, async (result) => {
this.activeBreakpointDialog = null;
this.#activeBreakpointEditRequest = undefined;
dialog.detach();
editor.dispatch({ effects: compartment.reconfigure([]) });
if (!result.committed) {
SourceComponents.BreakpointsView.BreakpointsSidebarController.instance().breakpointEditFinished(breakpoint, false);
return;
}
SourceComponents.BreakpointsView.BreakpointsSidebarController.instance().breakpointEditFinished(breakpoint, oldCondition !== result.condition);
recordBreakpointWithConditionAdded(result);
if (breakpoint) {
breakpoint.setCondition(result.condition, result.isLogpoint);
}
else if (location) {
await this.setBreakpoint(location.lineNumber, location.columnNumber, result.condition, /* enabled */ true, result.isLogpoint);
}
else {
await this.createNewBreakpoint(line, result.condition, /* enabled */ true, result.isLogpoint);
}
});
editor.dispatch({
effects: CodeMirror.StateEffect.appendConfig.of(compartment.of(CodeMirror.EditorView.decorations.of(CodeMirror.Decoration.set([CodeMirror.Decoration
.widget({
block: true, widget: new class extends CodeMirror.WidgetType {
toDOM() {
return decorationElement;
}
}(),
side: 1,
})
.range(line.to)])))),
});
dialog.element.addEventListener('blur', async (event) => {
if (!event.relatedTarget ||
(event.relatedTarget && !event.relatedTarget.isSelfOrDescendant(dialog.element))) {
this.#scheduledFinishingActiveDialog = true;
// Debounce repeated clicks on opening the edit dialog. Wait for a short amount of time
// in order to see whether we get a request to open the exact same dialog again.
setTimeout(() => {
if (this.activeBreakpointDialog === dialog) {
if (this.#scheduledFinishingActiveDialog) {
dialog.saveAndFinish();
this.#scheduledFinishingActiveDialog = false;
}
else {
dialog.focusEditor();
}
}
}, 200);
}
}, true);
dialog.markAsExternallyManaged();
dialog.show(decorationElement);
dialog.focusEditor();
this.activeBreakpointDialog = dialog;
this.#activeBreakpointEditRequest = breakpointEditRequest;
// This counts new conditional breakpoints or logpoints that are added.
function recordBreakpointWithConditionAdded(result) {
const { condition: newCondition, isLogpoint } = result;
const isConditionalBreakpoint = newCondition.length !== 0 && !isLogpoint;
const wasLogpoint = breakpoint?.isLogpoint();
const wasConditionalBreakpoint = oldCondition && oldCondition.length !== 0 && !wasLogpoint;
if (isLogpoint && !wasLogpoint) {
Host.userMetrics.breakpointWithConditionAdded(0 /* Host.UserMetrics.BreakpointWithConditionAdded.Logpoint */);
}
else if (isConditionalBreakpoint && !wasConditionalBreakpoint) {
Host.userMetrics.breakpointWithConditionAdded(1 /* Host.UserMetrics.BreakpointWithConditionAdded.ConditionalBreakpoint */);
}
}
function isSameEditRequest(editA, editB) {
if (editA.line.number !== editB.line.number) {
return false;
}
if (editA.line.from !== editB.line.from) {
return false;
}
if (editA.line.text !== editB.line.text) {
return false;
}
if (editA.breakpoint !== editB.breakpoint) {
return false;
}
if (editA.location !== editB.location) {
return false;
}
return editA.isLogpoint === editB.isLogpoint;
}
}
// Create decorations to indicate the current debugging position
computeExecutionDecorations(editorState, lineNumber, columnNumber) {
const { doc } = editorState;
if (lineNumber >= doc.lines) {
return CodeMirror.Decoration.none;
}
const line = doc.line(lineNumber + 1);
const decorations = [executionLineDeco.range(line.from)];
const position = Math.min(line.to, line.from + columnNumber);
let syntaxNode = CodeMirror.syntaxTree(editorState).resolveInner(position, 1);
if (syntaxNode.to === syntaxNode.from - 1 && /[(.]/.test(doc.sliceString(syntaxNode.from, syntaxNode.to))) {
syntaxNode = syntaxNode.resolve(syntaxNode.to, 1);
}
const tokenEnd = Math.min(line.to, syntaxNode.to);
if (tokenEnd > position) {
decorations.push(executionTokenDeco.range(position, tokenEnd));
}
return CodeMirror.Decoration.set(decorations);
}
// Show widgets with variable's values after lines that mention the
// variables, if the debugger is paused in this file.
async updateValueDecorations() {
if (!this.editor) {
return;
}
const decorations = this.executionLocation ? await this.computeValueDecorations() : null;
// After the `await` the DebuggerPlugin could have been disposed. Re-check `this.editor`.
if (!this.editor) {
return;
}
if (decorations || this.editor.state.field(valueDecorations.field).size) {
this.editor.dispatch({ effects: valueDecorations.update.of(decorations || CodeMirror.Decoration.none) });
}
}
async #rawLocationToEditorOffset(location, url) {
const uiLocation = location &&
await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().rawLocationToUILocation(location);
if (!uiLocation || uiLocation.uiSourceCode.url() !== url) {
return null;
}
const offset = this.editor?.toOffset(this.transformer.uiLocationToEditorLocation(uiLocation.lineNumber, uiLocation.columnNumber));
return offset ?? null;
}
async computeValueDecorations() {
if (!this.editor) {
return null;
}
if (!Common.Settings.Settings.instance().moduleSetting('inlineVariableValues').get()) {
return null;
}
const executionContext = UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext);
if (!executionContext) {
return null;
}
const callFrame = UI.Context.Context.instance().flavor(SDK.DebuggerModel.CallFrame);
if (!callFrame) {
return null;
}
const url = this.uiSourceCode.url();
const rawLocationToEditorOffset = location => this.#rawLocationToEditorOffset(location, url);
const functionOffsetPromise = this.#rawLocationToEditorOffset(callFrame.functionLocation(), url);
const executionOffsetPromise = this.#rawLocationToEditorOffset(callFrame.location(), url);
const [functionOffset, executionOffset] = await Promise.all([functionOffsetPromise, executionOffsetPromise]);
if (!functionOffset || !executionOffset || !this.editor) {
return null;
}
if (functionOffset >= executionOffset || executionOffset - functionOffset > MAX_CODE_SIZE_FOR_VALUE_DECORATIONS) {
return null;
}
const variableNames = getVariableNamesByLine(this.editor.state, functionOffset, executionOffset, executionOffset);
if (variableNames.length === 0) {
return null;
}
const scopeMappings = await computeScopeMappings(callFrame, rawLocationToEditorOffset);
if (scopeMappings.length === 0) {
return null;
}
const variablesByLine = getVariableValuesByLine(scopeMappings, variableNames);
if (!variablesByLine || !this.editor) {
return null;
}
const decorations = [];
for (const [line, names] of variablesByLine) {
const prevLine = variablesByLine.get(line - 1);
let newNames = prevLine ? Array.from(names).filter(n => prevLine.get(n[0]) !== n[1]) : Array.from(names);
if (!newNames.length) {
continue;
}
if (newNames.length > 10) {
newNames = newNames.slice(0, 10);
}
decorations.push(CodeMirror.Decoration.widget({ widget: new ValueDecoration(newNames), side: 1 })
.range(this.editor.state.doc.line(line + 1).to));
}
return CodeMirror.Decoration.set(decorations, true);
}
// Highlight the locations the debugger can continue to (when
// Control is held)
async showContinueToLocations() {
this.popoverHelper?.hidePopover();
const executionContext = UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext);
if (!executionContext || !this.editor) {
return;
}
const callFrame = UI.Context.Context.instance().flavor(SDK.DebuggerModel.CallFrame);
if (!callFrame) {
return;
}
const start = callFrame.functionLocation() || callFrame.location();
const debuggerModel = callFrame.debuggerModel;
const { state } = this.editor;
const locations = await debuggerModel.getPossibleBreakpoints(start, null, true);
this.continueToLocations = [];
let previousCallLine = -1;
for (const location of locations.reverse()) {
const editorLocation = this.transformer.uiLocationToEditorLocation(location.lineNumber, location.columnNumber);
if (previousCallLine === editorLocation.lineNumber &&
location.type !== "call" /* Protocol.Debugger.BreakLocationType.Call */ ||
editorLocation.lineNumber >= state.doc.lines) {
continue;
}
const line = state.doc.line(editorLocation.lineNumber + 1);
const position = Math.min(line.to, line.from + editorLocation.columnNumber);
let syntaxNode = CodeMirror.syntaxTree(state).resolveInner(position, 1);
if (syntaxNode.firstChild || syntaxNode.from < line.from ||
syntaxNode.to > line.to) { // Only use leaf nodes within the line
continue;
}
if (syntaxNode.name === '.') {
const nextNode = syntaxNode.resolve(syntaxNode.to, 1);
if (nextNode.firstChild || nextNode.from < line.from || nextNode.to > line.to) {
continue;
}
syntaxNode = nextNode;
}
const syntaxType = syntaxNode.name;
const validKeyword = syntaxType === 'this' || syntaxType === 'return' || syntaxType === 'new' ||
syntaxType === 'break' || syntaxType === 'continue';
if (!validKeyword && !this.isIdentifier(syntaxType)) {
continue;
}
this.continueToLocations.push({ from: syntaxNode.from, to: syntaxNode.to, async: false, click: () => location.continueToLocation() });
if (location.type === "call" /* Protocol.Debugger.BreakLocationType.Call */) {
previousCallLine = editorLocation.lineNumber;
}
const identifierName = validKeyword ? '' : line.text.slice(syntaxNode.from - line.from, syntaxNode.to - line.from);
let asyncCall = null;
if (identifierName === 'then' && syntaxNode.parent?.name === 'MemberExpression') {
asyncCall = syntaxNode.parent.parent;
}
else if (identifierName === 'setTimeout' || identifierName === 'setInterval' || identifierName === 'postMessage') {
asyncCall = syntaxNode.parent;
}
if (syntaxType === 'new') {
const callee = syntaxNode.parent?.getChild('Expression');
if (callee && callee.name === 'VariableName' && state.sliceDoc(callee.from, callee.to) === 'Worker') {
asyncCall = syntaxNode.parent;
}
}
if (asyncCall && (asyncCall.name === 'CallExpression' || asyncCall.name === 'NewExpression') &&
location.type === "call" /* Protocol.Debugger.BreakLocationType.Call */) {
const firstArg = asyncCall.getChild('ArgList')?.firstChild?.nextSibling;
let highlightNode;
if (firstArg?.name === 'VariableName') {
highlightNode = firstArg;
}
else if (firstArg?.name === 'ArrowFunction' || firstArg?.name === 'FunctionExpression') {
highlightNode = firstArg.firstChild;
if (highlightNode?.name === 'async') {
highlightNode = highlightNode.nextSibling;
}
}
if (highlightNode) {
const isCurrentPosition = this.executionLocation &&
location.lineNumber === this.executionLocation.lineNumber &&