UNPKG

chrome-devtools-frontend

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