UNPKG

@quick-game/cli

Version:

Command line interface for rapid qg development

1,026 lines (1,025 loc) 105 kB
/* * 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 &&