UNPKG

chrome-devtools-frontend

Version:
1,156 lines (1,037 loc) • 62.2 kB
// Copyright 2021 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* eslint-disable rulesdir/no-imperative-dom-api */ /* * Copyright (C) 2008 Apple Inc. All Rights Reserved. * 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: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. 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. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``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 APPLE INC. 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 '../../ui/legacy/legacy.js'; 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 Protocol from '../../generated/protocol.js'; import * as Bindings from '../../models/bindings/bindings.js'; import * as Breakpoints from '../../models/breakpoints/breakpoints.js'; import * as Extensions from '../../models/extensions/extensions.js'; import * as Workspace from '../../models/workspace/workspace.js'; import * as ObjectUI from '../../ui/legacy/components/object_ui/object_ui.js'; import type * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import * as Snippets from '../snippets/snippets.js'; import {CallStackSidebarPane} from './CallStackSidebarPane.js'; import {DebuggerPausedMessage} from './DebuggerPausedMessage.js'; import {NavigatorView} from './NavigatorView.js'; import sourcesPanelStyles from './sourcesPanel.css.js'; import {Events, SourcesView} from './SourcesView.js'; import {ThreadsSidebarPane} from './ThreadsSidebarPane.js'; import {UISourceCodeFrame} from './UISourceCodeFrame.js'; const UIStrings = { /** *@description Text that appears when user drag and drop something (for example, a file) in Sources Panel of the Sources panel */ dropWorkspaceFolderHere: 'Drop workspace folder here', /** *@description Text to show more options */ moreOptions: 'More options', /** * @description Tooltip for the the navigator toggle in the Sources panel. Command to open/show the * sidebar containing the navigator tool. */ showNavigator: 'Show navigator', /** * @description Tooltip for the the navigator toggle in the Sources panel. Command to close/hide * the sidebar containing the navigator tool. */ hideNavigator: 'Hide navigator', /** * @description Screen reader announcement when the navigator sidebar is shown in the Sources panel. */ navigatorShown: 'Navigator sidebar shown', /** * @description Screen reader announcement when the navigator sidebar is hidden in the Sources panel. */ navigatorHidden: 'Navigator sidebar hidden', /** * @description Screen reader announcement when the navigator sidebar is shown in the Sources panel. */ debuggerShown: 'Debugger sidebar shown', /** * @description Screen reader announcement when the navigator sidebar is hidden in the Sources panel. */ debuggerHidden: 'Debugger sidebar hidden', /** * @description Tooltip for the the debugger toggle in the Sources panel. Command to open/show the * sidebar containing the debugger tool. */ showDebugger: 'Show debugger', /** * @description Tooltip for the the debugger toggle in the Sources panel. Command to close/hide the * sidebar containing the debugger tool. */ hideDebugger: 'Hide debugger', /** *@description Text in Sources Panel of the Sources panel */ groupByFolder: 'Group by folder', /** *@description Text in Sources Panel of the Sources panel */ groupByAuthored: 'Group by Authored/Deployed', /** *@description Text in Sources Panel of the Sources panel */ hideIgnoreListed: 'Hide ignore-listed sources', /** *@description Tooltip text that appears when hovering over the largeicon play button in the Sources Panel of the Sources panel */ resumeWithAllPausesBlockedForMs: 'Resume with all pauses blocked for 500 ms', /** *@description Tooltip text that appears when hovering over the largeicon terminate execution button in the Sources Panel of the Sources panel */ terminateCurrentJavascriptCall: 'Terminate current JavaScript call', /** *@description Text in Sources Panel of the Sources panel */ pauseOnCaughtExceptions: 'Pause on caught exceptions', /** *@description A context menu item in the Sources Panel of the Sources panel */ revealInSidebar: 'Reveal in navigator sidebar', /** *@description A context menu item in the Sources Panel of the Sources panel when debugging JS code. * When clicked, the execution is resumed until it reaches the line specified by the right-click that * opened the context menu. */ continueToHere: 'Continue to here', /** *@description A context menu item in the Console that stores selection as a temporary global variable */ storeAsGlobalVariable: 'Store as global variable', /** *@description A context menu item in the Console, Sources, and Network panel *@example {string} PH1 */ copyS: 'Copy {PH1}', /** *@description A context menu item for strings in the Console, Sources, and Network panel. * When clicked, the raw contents of the string is copied to the clipboard. */ copyStringContents: 'Copy string contents', /** *@description A context menu item for strings in the Console, Sources, and Network panel. * When clicked, the string is copied to the clipboard as a valid JavaScript literal. */ copyStringAsJSLiteral: 'Copy string as JavaScript literal', /** *@description A context menu item for strings in the Console, Sources, and Network panel. * When clicked, the string is copied to the clipboard as a valid JSON literal. */ copyStringAsJSONLiteral: 'Copy string as JSON literal', /** *@description A context menu item in the Sources Panel of the Sources panel */ showFunctionDefinition: 'Show function definition', /** *@description Text in Sources Panel of the Sources panel */ openInSourcesPanel: 'Open in Sources panel', } as const; const str_ = i18n.i18n.registerUIStrings('panels/sources/SourcesPanel.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const primitiveRemoteObjectTypes = new Set(['number', 'boolean', 'bigint', 'undefined']); let sourcesPanelInstance: SourcesPanel; export class SourcesPanel extends UI.Panel.Panel implements UI.ContextMenu.Provider<Workspace.UISourceCode.UISourceCode|Workspace.UISourceCode.UILocation| SDK.RemoteObject.RemoteObject|SDK.NetworkRequest.NetworkRequest|UISourceCodeFrame>, SDK.TargetManager.Observer, UI.View.ViewLocationResolver { private readonly workspace: Workspace.Workspace.WorkspaceImpl; private readonly togglePauseAction: UI.ActionRegistration.Action; private readonly stepOverAction: UI.ActionRegistration.Action; private readonly stepIntoAction: UI.ActionRegistration.Action; private readonly stepOutAction: UI.ActionRegistration.Action; private readonly stepAction: UI.ActionRegistration.Action; private readonly toggleBreakpointsActiveAction: UI.ActionRegistration.Action; private readonly debugToolbar: UI.Toolbar.Toolbar; private readonly debugToolbarDrawer: HTMLDivElement; private readonly debuggerPausedMessage: DebuggerPausedMessage; private overlayLoggables?: {debuggerPausedMessage: object, resumeButton: object, stepOverButton: object}; private splitWidget: UI.SplitWidget.SplitWidget; editorView: UI.SplitWidget.SplitWidget; private navigatorTabbedLocation: UI.View.TabbedViewLocation; sourcesViewInternal: SourcesView; private readonly toggleNavigatorSidebarButton: UI.Toolbar.ToolbarButton; private readonly toggleDebuggerSidebarButton: UI.Toolbar.ToolbarButton; private threadsSidebarPane: UI.View.View|null; private readonly watchSidebarPane: UI.View.View; private readonly callstackPane: CallStackSidebarPane; private liveLocationPool: Bindings.LiveLocation.LiveLocationPool; private lastModificationTime: number; private pausedInternal?: boolean; private switchToPausedTargetTimeout?: number; private executionLineLocation?: Bindings.DebuggerWorkspaceBinding.Location|null; private sidebarPaneStack?: UI.View.ViewLocation; private tabbedLocationHeader?: Element|null; private extensionSidebarPanesContainer?: UI.View.ViewLocation; sidebarPaneView?: UI.Widget.VBox|UI.SplitWidget.SplitWidget; #lastPausedTarget: WeakRef<SDK.Target.Target>|null = null; constructor() { super('sources'); this.registerRequiredCSS(sourcesPanelStyles); new UI.DropTarget.DropTarget( this.element, [UI.DropTarget.Type.Folder], i18nString(UIStrings.dropWorkspaceFolderHere), this.handleDrop.bind(this)); this.workspace = Workspace.Workspace.WorkspaceImpl.instance(); this.togglePauseAction = UI.ActionRegistry.ActionRegistry.instance().getAction('debugger.toggle-pause'); this.stepOverAction = UI.ActionRegistry.ActionRegistry.instance().getAction('debugger.step-over'); this.stepIntoAction = UI.ActionRegistry.ActionRegistry.instance().getAction('debugger.step-into'); this.stepOutAction = UI.ActionRegistry.ActionRegistry.instance().getAction('debugger.step-out'); this.stepAction = UI.ActionRegistry.ActionRegistry.instance().getAction('debugger.step'); this.toggleBreakpointsActiveAction = UI.ActionRegistry.ActionRegistry.instance().getAction('debugger.toggle-breakpoints-active'); this.debugToolbar = this.createDebugToolbar(); this.debugToolbarDrawer = this.createDebugToolbarDrawer(); this.debuggerPausedMessage = new DebuggerPausedMessage(); const initialDebugSidebarWidth = 225; this.splitWidget = new UI.SplitWidget.SplitWidget(true, true, 'sources-panel-split-view-state', initialDebugSidebarWidth); this.splitWidget.enableShowModeSaving(); this.splitWidget.show(this.element); // Create scripts navigator const initialNavigatorWidth = 225; this.editorView = new UI.SplitWidget.SplitWidget(true, false, 'sources-panel-navigator-split-view-state', initialNavigatorWidth); this.editorView.enableShowModeSaving(); this.splitWidget.setMainWidget(this.editorView); // Create navigator tabbed pane with toolbar. this.navigatorTabbedLocation = UI.ViewManager.ViewManager.instance().createTabbedLocation( this.revealNavigatorSidebar.bind(this), 'navigator-view', true, true); const tabbedPane = this.navigatorTabbedLocation.tabbedPane(); tabbedPane.setMinimumSize(100, 25); tabbedPane.element.classList.add('navigator-tabbed-pane'); tabbedPane.headerElement().setAttribute( 'jslog', `${VisualLogging.toolbar('navigator').track({keydown: 'ArrowUp|ArrowLeft|ArrowDown|ArrowRight|Enter|Space'})}`); const navigatorMenuButton = new UI.ContextMenu.MenuButton(); navigatorMenuButton.populateMenuCall = this.populateNavigatorMenu.bind(this); navigatorMenuButton.jslogContext = 'more-options'; navigatorMenuButton.iconName = 'dots-vertical'; navigatorMenuButton.title = i18nString(UIStrings.moreOptions); tabbedPane.rightToolbar().appendToolbarItem(new UI.Toolbar.ToolbarItem(navigatorMenuButton)); if (UI.ViewManager.ViewManager.instance().hasViewsForLocation('run-view-sidebar')) { const navigatorSplitWidget = new UI.SplitWidget.SplitWidget(false, true, 'source-panel-navigator-sidebar-split-view-state'); navigatorSplitWidget.setMainWidget(tabbedPane); const runViewTabbedPane = UI.ViewManager.ViewManager.instance() .createTabbedLocation(this.revealNavigatorSidebar.bind(this), 'run-view-sidebar') .tabbedPane(); navigatorSplitWidget.setSidebarWidget(runViewTabbedPane); navigatorSplitWidget.installResizer(runViewTabbedPane.headerElement()); this.editorView.setSidebarWidget(navigatorSplitWidget); } else { this.editorView.setSidebarWidget(tabbedPane); } this.sourcesViewInternal = new SourcesView(); this.sourcesViewInternal.addEventListener(Events.EDITOR_SELECTED, this.editorSelected.bind(this)); this.toggleNavigatorSidebarButton = this.editorView.createShowHideSidebarButton( i18nString(UIStrings.showNavigator), i18nString(UIStrings.hideNavigator), i18nString(UIStrings.navigatorShown), i18nString(UIStrings.navigatorHidden), 'navigator'); this.toggleDebuggerSidebarButton = this.splitWidget.createShowHideSidebarButton( i18nString(UIStrings.showDebugger), i18nString(UIStrings.hideDebugger), i18nString(UIStrings.debuggerShown), i18nString(UIStrings.debuggerHidden), 'debugger'); this.editorView.setMainWidget(this.sourcesViewInternal); this.threadsSidebarPane = null; this.watchSidebarPane = UI.ViewManager.ViewManager.instance().view('sources.watch'); this.callstackPane = CallStackSidebarPane.instance(); Common.Settings.Settings.instance() .moduleSetting('sidebar-position') .addChangeListener(this.updateSidebarPosition.bind(this)); this.updateSidebarPosition(); void this.updateDebuggerButtonsAndStatus(); this.liveLocationPool = new Bindings.LiveLocation.LiveLocationPool(); this.setTarget(UI.Context.Context.instance().flavor(SDK.Target.Target)); Common.Settings.Settings.instance() .moduleSetting('breakpoints-active') .addChangeListener(this.breakpointsActiveStateChanged, this); UI.Context.Context.instance().addFlavorChangeListener(SDK.Target.Target, this.onCurrentTargetChanged, this); UI.Context.Context.instance().addFlavorChangeListener(SDK.DebuggerModel.CallFrame, this.callFrameChanged, this); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.DebuggerWasEnabled, this.debuggerWasEnabled, this); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.DebuggerPaused, this.debuggerPaused, this); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.DebugInfoAttached, this.debugInfoAttached, this); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.DebuggerResumed, event => this.debuggerResumed(event.data)); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.GlobalObjectCleared, event => this.debuggerResumed(event.data)); Extensions.ExtensionServer.ExtensionServer.instance().addEventListener( Extensions.ExtensionServer.Events.SidebarPaneAdded, this.extensionSidebarPaneAdded, this); SDK.TargetManager.TargetManager.instance().observeTargets(this); this.lastModificationTime = -Infinity; } static instance(opts: { forceNew: boolean|null, }|undefined = {forceNew: null}): SourcesPanel { const {forceNew} = opts; if (!sourcesPanelInstance || forceNew) { sourcesPanelInstance = new SourcesPanel(); } return sourcesPanelInstance; } static updateResizerAndSidebarButtons(panel: SourcesPanel): void { panel.sourcesViewInternal.leftToolbar().removeToolbarItems(); panel.sourcesViewInternal.rightToolbar().removeToolbarItems(); panel.sourcesViewInternal.bottomToolbar().removeToolbarItems(); const isInWrapper = UI.Context.Context.instance().flavor(QuickSourceView) && !UI.InspectorView.InspectorView.instance().isDrawerMinimized(); if (panel.splitWidget.isVertical() || isInWrapper) { panel.splitWidget.uninstallResizer(panel.sourcesViewInternal.scriptViewToolbar()); } else { panel.splitWidget.installResizer(panel.sourcesViewInternal.scriptViewToolbar()); } if (!isInWrapper) { panel.sourcesViewInternal.leftToolbar().appendToolbarItem(panel.toggleNavigatorSidebarButton); if (panel.splitWidget.isVertical()) { panel.sourcesViewInternal.rightToolbar().appendToolbarItem(panel.toggleDebuggerSidebarButton); } else { panel.sourcesViewInternal.bottomToolbar().appendToolbarItem(panel.toggleDebuggerSidebarButton); } } } targetAdded(_target: SDK.Target.Target): void { this.showThreadsIfNeeded(); } targetRemoved(_target: SDK.Target.Target): void { } private showThreadsIfNeeded(): void { if (ThreadsSidebarPane.shouldBeShown() && !this.threadsSidebarPane) { this.threadsSidebarPane = UI.ViewManager.ViewManager.instance().view('sources.threads'); if (this.sidebarPaneStack && this.threadsSidebarPane) { this.sidebarPaneStack.appendView( this.threadsSidebarPane, this.splitWidget.isVertical() ? this.watchSidebarPane : this.callstackPane); } } } private setTarget(target: SDK.Target.Target|null): void { if (!target) { return; } const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); if (!debuggerModel) { return; } if (debuggerModel.isPaused()) { this.showDebuggerPausedDetails( (debuggerModel.debuggerPausedDetails() as SDK.DebuggerModel.DebuggerPausedDetails)); } else { this.pausedInternal = false; this.clearInterface(); this.toggleDebuggerSidebarButton.setEnabled(true); } } private onCurrentTargetChanged({data: target}: Common.EventTarget.EventTargetEvent<SDK.Target.Target|null>): void { this.setTarget(target); } paused(): boolean { return this.pausedInternal || false; } override wasShown(): void { UI.Context.Context.instance().setFlavor(SourcesPanel, this); super.wasShown(); if (UI.Context.Context.instance().flavor(QuickSourceView)) { UI.InspectorView.InspectorView.instance().setDrawerMinimized(true); SourcesPanel.updateResizerAndSidebarButtons(this); } this.editorView.setMainWidget(this.sourcesViewInternal); } override willHide(): void { super.willHide(); UI.Context.Context.instance().setFlavor(SourcesPanel, null); const wrapperView = UI.Context.Context.instance().flavor(QuickSourceView); if (wrapperView) { wrapperView.showViewInWrapper(); UI.InspectorView.InspectorView.instance().setDrawerMinimized(false); SourcesPanel.updateResizerAndSidebarButtons(this); } } resolveLocation(locationName: string): UI.View.ViewLocation|null { if (locationName === 'sources.sidebar-top' || locationName === 'sources.sidebar-bottom' || locationName === 'sources.sidebar-tabs') { return this.sidebarPaneStack || null; } return this.navigatorTabbedLocation; } ensureSourcesViewVisible(): boolean { if (UI.Context.Context.instance().flavor(QuickSourceView)) { return true; } if (!UI.InspectorView.InspectorView.instance().canSelectPanel('sources')) { return false; } void UI.ViewManager.ViewManager.instance().showView('sources'); return true; } override onResize(): void { if (Common.Settings.Settings.instance().moduleSetting('sidebar-position').get() === 'auto') { this.element.window().requestAnimationFrame(this.updateSidebarPosition.bind(this)); } // Do not force layout. } override searchableView(): UI.SearchableView.SearchableView { return this.sourcesViewInternal.searchableView(); } toggleNavigatorSidebar(): void { this.editorView.toggleSidebar(); } toggleDebuggerSidebar(): void { this.splitWidget.toggleSidebar(); } private debuggerPaused(event: Common.EventTarget.EventTargetEvent<SDK.DebuggerModel.DebuggerModel>): void { const debuggerModel = event.data; const details = debuggerModel.debuggerPausedDetails(); if (!this.pausedInternal && Common.Settings.Settings.instance().moduleSetting('auto-focus-on-debugger-paused-enabled').get()) { void this.setAsCurrentPanel(); } if (UI.Context.Context.instance().flavor(SDK.Target.Target) === debuggerModel.target()) { this.showDebuggerPausedDetails((details as SDK.DebuggerModel.DebuggerPausedDetails)); } else if (!this.pausedInternal) { UI.Context.Context.instance().setFlavor(SDK.Target.Target, debuggerModel.target()); } } private debugInfoAttached(event: Common.EventTarget.EventTargetEvent<SDK.Script.Script>): void { const {debuggerModel} = event.data; if (!debuggerModel.isPaused()) { return; } const details = debuggerModel.debuggerPausedDetails(); if (details && UI.Context.Context.instance().flavor(SDK.Target.Target) === debuggerModel.target()) { this.showDebuggerPausedDetails(details); } } private showDebuggerPausedDetails(details: SDK.DebuggerModel.DebuggerPausedDetails): void { this.pausedInternal = true; void this.updateDebuggerButtonsAndStatus(); UI.Context.Context.instance().setFlavor(SDK.DebuggerModel.DebuggerPausedDetails, details); this.toggleDebuggerSidebarButton.setEnabled(false); this.revealDebuggerSidebar(); const pausedTarget = details.debuggerModel.target(); if (this.threadsSidebarPane && this.#lastPausedTarget?.deref() !== pausedTarget && pausedTarget !== SDK.TargetManager.TargetManager.instance().primaryPageTarget()) { // If we pause in something other than the main frame (e.g. worker), we should expand the // "Threads" list to make it more clear which target is paused. We do this only if the target of // the previous pause is different from the new pause to prevent annoying the user by re-opening // the "Threads" list while stepping or hitting the same breakpoint multiple points. void this.sidebarPaneStack?.showView(this.threadsSidebarPane); } window.focus(); Host.InspectorFrontendHost.InspectorFrontendHostInstance.bringToFront(); const withOverlay = UI.Context.Context.instance().flavor(SDK.Target.Target)?.model(SDK.OverlayModel.OverlayModel) && !Common.Settings.Settings.instance().moduleSetting('disable-paused-state-overlay').get(); if (withOverlay && !this.overlayLoggables) { this.overlayLoggables = {debuggerPausedMessage: {}, resumeButton: {}, stepOverButton: {}}; VisualLogging.registerLoggable( this.overlayLoggables.debuggerPausedMessage, `${VisualLogging.dialog('debugger-paused')}`, null, new DOMRect(0, 0, 200, 20)); VisualLogging.registerLoggable( this.overlayLoggables.resumeButton, `${VisualLogging.action('debugger.toggle-pause')}`, this.overlayLoggables.debuggerPausedMessage, new DOMRect(0, 0, 20, 20)); VisualLogging.registerLoggable( this.overlayLoggables.stepOverButton, `${VisualLogging.action('debugger.step-over')}`, this.overlayLoggables.debuggerPausedMessage, new DOMRect(0, 0, 20, 20)); } this.#lastPausedTarget = new WeakRef(details.debuggerModel.target()); } private maybeLogOverlayAction(): void { if (!this.overlayLoggables) { return; } const byOverlayButton = !document.hasFocus(); // In the overlary we show two buttons: resume and step over. Both trigger // the Debugger.resumed event. The latter however will trigger // Debugger.paused shortly after, while the former won't. Here we guess // which one was clicked by checking if we are paused again after 0.5s. window.setTimeout(() => { if (!this.overlayLoggables) { return; } if (byOverlayButton) { const details = UI.Context.Context.instance().flavor(SDK.DebuggerModel.DebuggerPausedDetails); VisualLogging.logClick( this.pausedInternal && details?.reason === Protocol.Debugger.PausedEventReason.Step ? this.overlayLoggables.stepOverButton : this.overlayLoggables.resumeButton, new MouseEvent('click')); } if (!this.pausedInternal) { VisualLogging.logResize(this.overlayLoggables.debuggerPausedMessage, new DOMRect(0, 0, 0, 0)); this.overlayLoggables = undefined; } }, 500); } private debuggerResumed(debuggerModel: SDK.DebuggerModel.DebuggerModel): void { this.maybeLogOverlayAction(); const target = debuggerModel.target(); if (UI.Context.Context.instance().flavor(SDK.Target.Target) !== target) { return; } this.pausedInternal = false; this.clearInterface(); this.toggleDebuggerSidebarButton.setEnabled(true); this.switchToPausedTargetTimeout = window.setTimeout(this.switchToPausedTarget.bind(this, debuggerModel), 500); } private debuggerWasEnabled(event: Common.EventTarget.EventTargetEvent<SDK.DebuggerModel.DebuggerModel>): void { const debuggerModel = event.data; if (UI.Context.Context.instance().flavor(SDK.Target.Target) !== debuggerModel.target()) { return; } void this.updateDebuggerButtonsAndStatus(); } get visibleView(): UI.Widget.Widget|null { return this.sourcesViewInternal.visibleView(); } showUISourceCode( uiSourceCode: Workspace.UISourceCode.UISourceCode, location?: SourceFrame.SourceFrame.RevealPosition, omitFocus?: boolean): void { if (omitFocus) { if (!this.isShowing() && !UI.Context.Context.instance().flavor(QuickSourceView)) { return; } } else { this.showEditor(); } this.sourcesViewInternal.showSourceLocation(uiSourceCode, location, omitFocus); } private showEditor(): void { if (UI.Context.Context.instance().flavor(QuickSourceView)) { return; } void this.setAsCurrentPanel(); } showUILocation(uiLocation: Workspace.UISourceCode.UILocation, omitFocus?: boolean): void { const {uiSourceCode, lineNumber, columnNumber} = uiLocation; this.showUISourceCode(uiSourceCode, {lineNumber, columnNumber}, omitFocus); } async revealInNavigator(uiSourceCode: Workspace.UISourceCode.UISourceCode, skipReveal?: boolean): Promise<void> { const viewManager = UI.ViewManager.ViewManager.instance(); for (const view of viewManager.viewsForLocation(UI.ViewManager.ViewLocationValues.NAVIGATOR_VIEW)) { const navigatorView = await view.widget(); if (navigatorView instanceof NavigatorView && navigatorView.acceptProject(uiSourceCode.project())) { navigatorView.revealUISourceCode(uiSourceCode, true); this.navigatorTabbedLocation.tabbedPane().selectTab(view.viewId(), true); if (!skipReveal) { this.editorView.showBoth(true); navigatorView.focus(); } break; } } } private addExperimentMenuItem( menuSection: UI.ContextMenu.Section, experiment: string, menuItem: Common.UIString.LocalizedString): void { // menu handler function toggleExperiment(): void { const checked = Root.Runtime.experiments.isEnabled(experiment); Root.Runtime.experiments.setEnabled(experiment, !checked); Host.userMetrics.experimentChanged(experiment, checked); // Need to signal to the NavigatorView that grouping has changed. Unfortunately, // it can't listen to an experiment, and this class doesn't directly interact // with it, so we will convince it a different grouping setting changed. When we switch // from using an experiment to a setting, it will listen to that setting and we // won't need to do this. const groupByFolderSetting = Common.Settings.Settings.instance().moduleSetting('navigator-group-by-folder'); groupByFolderSetting.set(groupByFolderSetting.get()); } menuSection.appendCheckboxItem(menuItem, toggleExperiment, { checked: Root.Runtime.experiments.isEnabled(experiment), experimental: true, jslogContext: Platform.StringUtilities.toKebabCase(experiment), }); } private populateNavigatorMenu(contextMenu: UI.ContextMenu.ContextMenu): void { const groupByFolderSetting = Common.Settings.Settings.instance().moduleSetting('navigator-group-by-folder'); contextMenu.appendItemsAtLocation('navigatorMenu'); contextMenu.viewSection().appendCheckboxItem( i18nString(UIStrings.groupByFolder), () => groupByFolderSetting.set(!groupByFolderSetting.get()), {checked: groupByFolderSetting.get(), jslogContext: groupByFolderSetting.name}); this.addExperimentMenuItem( contextMenu.viewSection(), Root.Runtime.ExperimentName.AUTHORED_DEPLOYED_GROUPING, i18nString(UIStrings.groupByAuthored)); this.addExperimentMenuItem( contextMenu.viewSection(), Root.Runtime.ExperimentName.JUST_MY_CODE, i18nString(UIStrings.hideIgnoreListed)); } updateLastModificationTime(): void { this.lastModificationTime = window.performance.now(); } private async executionLineChanged(liveLocation: Bindings.LiveLocation.LiveLocation): Promise<void> { const uiLocation = await liveLocation.uiLocation(); if (liveLocation.isDisposed()) { return; } if (!uiLocation) { return; } if (window.performance.now() - this.lastModificationTime < lastModificationTimeout) { return; } this.sourcesViewInternal.showSourceLocation(uiLocation.uiSourceCode, uiLocation, undefined, true); } private async callFrameChanged(): Promise<void> { const callFrame = UI.Context.Context.instance().flavor(SDK.DebuggerModel.CallFrame); if (!callFrame) { return; } if (this.executionLineLocation) { this.executionLineLocation.dispose(); } this.executionLineLocation = await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().createCallFrameLiveLocation( callFrame.location(), this.executionLineChanged.bind(this), this.liveLocationPool); } private async updateDebuggerButtonsAndStatus(): Promise<void> { const currentTarget = UI.Context.Context.instance().flavor(SDK.Target.Target); const currentDebuggerModel = currentTarget ? currentTarget.model(SDK.DebuggerModel.DebuggerModel) : null; if (!currentDebuggerModel) { this.togglePauseAction.setEnabled(false); this.stepOverAction.setEnabled(false); this.stepIntoAction.setEnabled(false); this.stepOutAction.setEnabled(false); this.stepAction.setEnabled(false); } else if (this.pausedInternal) { this.togglePauseAction.setToggled(true); this.togglePauseAction.setEnabled(true); this.stepOverAction.setEnabled(true); this.stepIntoAction.setEnabled(true); this.stepOutAction.setEnabled(true); this.stepAction.setEnabled(true); } else { this.togglePauseAction.setToggled(false); this.togglePauseAction.setEnabled(!currentDebuggerModel.isPausing()); this.stepOverAction.setEnabled(false); this.stepIntoAction.setEnabled(false); this.stepOutAction.setEnabled(false); this.stepAction.setEnabled(false); } const details = currentDebuggerModel ? currentDebuggerModel.debuggerPausedDetails() : null; await this.debuggerPausedMessage.render( details, Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance(), Breakpoints.BreakpointManager.BreakpointManager.instance()); if (details) { this.updateDebuggerButtonsAndStatusForTest(); } } private updateDebuggerButtonsAndStatusForTest(): void { } private clearInterface(): void { void this.updateDebuggerButtonsAndStatus(); UI.Context.Context.instance().setFlavor(SDK.DebuggerModel.DebuggerPausedDetails, null); if (this.switchToPausedTargetTimeout) { clearTimeout(this.switchToPausedTargetTimeout); } this.liveLocationPool.disposeAll(); } private switchToPausedTarget(debuggerModel: SDK.DebuggerModel.DebuggerModel): void { delete this.switchToPausedTargetTimeout; if (this.pausedInternal || debuggerModel.isPaused()) { return; } for (const debuggerModel of SDK.TargetManager.TargetManager.instance().models(SDK.DebuggerModel.DebuggerModel)) { if (debuggerModel.isPaused()) { UI.Context.Context.instance().setFlavor(SDK.Target.Target, debuggerModel.target()); break; } } } runSnippet(): void { const uiSourceCode = this.sourcesViewInternal.currentUISourceCode(); if (uiSourceCode) { void Snippets.ScriptSnippetFileSystem.evaluateScriptSnippet(uiSourceCode); } } private editorSelected(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void { const uiSourceCode = event.data; UI.Context.Context.instance().setFlavor(Workspace.UISourceCode.UISourceCode, uiSourceCode); if (this.editorView.mainWidget() && Common.Settings.Settings.instance().moduleSetting('auto-reveal-in-navigator').get()) { void this.revealInNavigator(uiSourceCode, true); } } togglePause(): boolean { const target = UI.Context.Context.instance().flavor(SDK.Target.Target); if (!target) { return true; } const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); if (!debuggerModel) { return true; } if (this.pausedInternal) { this.pausedInternal = false; debuggerModel.resume(); } else { // Make sure pauses didn't stick skipped. debuggerModel.pause(); } this.clearInterface(); return true; } private prepareToResume(): SDK.DebuggerModel.DebuggerModel|null { if (!this.pausedInternal) { return null; } this.pausedInternal = false; this.clearInterface(); const target = UI.Context.Context.instance().flavor(SDK.Target.Target); return target ? target.model(SDK.DebuggerModel.DebuggerModel) : null; } private longResume(): void { const debuggerModel = this.prepareToResume(); if (debuggerModel) { debuggerModel.skipAllPausesUntilReloadOrTimeout(500); debuggerModel.resume(); } } private terminateExecution(): void { const debuggerModel = this.prepareToResume(); if (debuggerModel) { void debuggerModel.runtimeModel().terminateExecution(); debuggerModel.resume(); } } stepOver(): boolean { const debuggerModel = this.prepareToResume(); if (debuggerModel) { void debuggerModel.stepOver(); } return true; } stepInto(): boolean { const debuggerModel = this.prepareToResume(); if (debuggerModel) { void debuggerModel.stepInto(); } return true; } stepIntoAsync(): boolean { const debuggerModel = this.prepareToResume(); if (debuggerModel) { debuggerModel.scheduleStepIntoAsync(); } return true; } stepOut(): boolean { const debuggerModel = this.prepareToResume(); if (debuggerModel) { void debuggerModel.stepOut(); } return true; } private async continueToLocation(uiLocation: Workspace.UISourceCode.UILocation): Promise<void> { const executionContext = UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext); if (!executionContext) { return; } // Always use 0 column. const rawLocations = await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().uiLocationToRawLocations( uiLocation.uiSourceCode, uiLocation.lineNumber, 0); const rawLocation = rawLocations.find(location => location.debuggerModel === executionContext.debuggerModel); if (rawLocation && this.prepareToResume()) { rawLocation.continueToLocation(); } } toggleBreakpointsActive(): void { Common.Settings.Settings.instance() .moduleSetting('breakpoints-active') .set(!Common.Settings.Settings.instance().moduleSetting('breakpoints-active').get()); } private breakpointsActiveStateChanged(): void { const active = Common.Settings.Settings.instance().moduleSetting('breakpoints-active').get(); this.toggleBreakpointsActiveAction.setToggled(!active); this.sourcesViewInternal.toggleBreakpointsActiveState(active); } private createDebugToolbar(): UI.Toolbar.Toolbar { const debugToolbar = document.createElement('devtools-toolbar'); debugToolbar.classList.add('scripts-debug-toolbar'); debugToolbar.setAttribute( 'jslog', `${VisualLogging.toolbar('debug').track({keydown: 'ArrowUp|ArrowLeft|ArrowDown|ArrowRight|Enter|Space'})}`); const longResumeButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.resumeWithAllPausesBlockedForMs), 'play'); longResumeButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, this.longResume, this); const terminateExecutionButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.terminateCurrentJavascriptCall), 'stop'); terminateExecutionButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, this.terminateExecution, this); const pauseActionButton = UI.Toolbar.Toolbar.createLongPressActionButton( this.togglePauseAction, [terminateExecutionButton, longResumeButton], []); pauseActionButton.toggleOnClick(false); debugToolbar.appendToolbarItem(pauseActionButton); debugToolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButton(this.stepOverAction)); debugToolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButton(this.stepIntoAction)); debugToolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButton(this.stepOutAction)); debugToolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButton(this.stepAction)); debugToolbar.appendSeparator(); debugToolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButton(this.toggleBreakpointsActiveAction)); return debugToolbar; } private createDebugToolbarDrawer(): HTMLDivElement { const debugToolbarDrawer = document.createElement('div'); debugToolbarDrawer.classList.add('scripts-debug-toolbar-drawer'); const label = i18nString(UIStrings.pauseOnCaughtExceptions); const setting = Common.Settings.Settings.instance().moduleSetting('pause-on-caught-exception'); debugToolbarDrawer.appendChild(UI.SettingsUI.createSettingCheckbox(label, setting)); return debugToolbarDrawer; } appendApplicableItems( event: Event, contextMenu: UI.ContextMenu.ContextMenu, target: Workspace.UISourceCode.UISourceCode|Workspace.UISourceCode.UILocation|SDK.RemoteObject.RemoteObject| SDK.NetworkRequest.NetworkRequest|UISourceCodeFrame): void { if (target instanceof Workspace.UISourceCode.UISourceCode) { this.appendUISourceCodeItems(event, contextMenu, target); return; } if (target instanceof UISourceCodeFrame) { this.appendUISourceCodeFrameItems(contextMenu, target); return; } if (target instanceof Workspace.UISourceCode.UILocation) { this.appendUILocationItems(contextMenu, target); return; } if (target instanceof SDK.RemoteObject.RemoteObject) { this.appendRemoteObjectItems(contextMenu, target); return; } this.appendNetworkRequestItems(contextMenu, target); } private appendUISourceCodeItems( event: Event, contextMenu: UI.ContextMenu.ContextMenu, uiSourceCode: Workspace.UISourceCode.UISourceCode): void { if (!event.target) { return; } const eventTarget = (event.target as Node); if (!uiSourceCode.project().isServiceProject() && !eventTarget.isSelfOrDescendant(this.navigatorTabbedLocation.widget().element) && !(Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.JUST_MY_CODE) && Bindings.IgnoreListManager.IgnoreListManager.instance().isUserOrSourceMapIgnoreListedUISourceCode( uiSourceCode))) { contextMenu.revealSection().appendItem( i18nString(UIStrings.revealInSidebar), this.revealInNavigator.bind(this, uiSourceCode), { jslogContext: 'sources.reveal-in-navigator-sidebar', }); } if (UI.ActionRegistry.ActionRegistry.instance().hasAction('drjones.sources-panel-context')) { const editorElement = this.element.querySelector('devtools-text-editor'); if (!eventTarget.isSelfOrDescendant(editorElement) && uiSourceCode.contentType().isTextType()) { UI.Context.Context.instance().setFlavor(Workspace.UISourceCode.UISourceCode, uiSourceCode); contextMenu.footerSection().appendAction( 'drjones.sources-panel-context', ); } } // Ignore list only works for JavaScript debugging. if (uiSourceCode.contentType().hasScripts() && Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance() .scriptsForUISourceCode(uiSourceCode) .every(script => script.isJavaScript())) { this.callstackPane.appendIgnoreListURLContextMenuItems(contextMenu, uiSourceCode); } } private appendUISourceCodeFrameItems(contextMenu: UI.ContextMenu.ContextMenu, target: UISourceCodeFrame): void { if (target.uiSourceCode().contentType().isFromSourceMap() || target.textEditor.state.selection.main.empty) { return; } contextMenu.debugSection().appendAction('debugger.evaluate-selection'); } appendUILocationItems(contextMenu: UI.ContextMenu.ContextMenu, uiLocation: Workspace.UISourceCode.UILocation): void { const uiSourceCode = uiLocation.uiSourceCode; if (!Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance() .scriptsForUISourceCode(uiSourceCode) .every(script => script.isJavaScript())) { // Ignore List and 'Continue to here' currently only works for JavaScript debugging. return; } const contentType = uiSourceCode.contentType(); if (contentType.hasScripts()) { const target = UI.Context.Context.instance().flavor(SDK.Target.Target); const debuggerModel = target ? target.model(SDK.DebuggerModel.DebuggerModel) : null; if (debuggerModel?.isPaused()) { contextMenu.debugSection().appendItem( i18nString(UIStrings.continueToHere), this.continueToLocation.bind(this, uiLocation), {jslogContext: 'continue-to-here'}); } this.callstackPane.appendIgnoreListURLContextMenuItems(contextMenu, uiSourceCode); } } private appendRemoteObjectItems(contextMenu: UI.ContextMenu.ContextMenu, remoteObject: SDK.RemoteObject.RemoteObject): void { const indent = Common.Settings.Settings.instance().moduleSetting('text-editor-indent').get(); const executionContext = UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext); function getObjectTitle(): string|undefined { if (remoteObject.type === 'wasm') { return remoteObject.subtype; } if (remoteObject.subtype === 'node') { return 'outerHTML'; } return remoteObject.type; } const copyContextMenuTitle = getObjectTitle(); contextMenu.debugSection().appendItem( i18nString(UIStrings.storeAsGlobalVariable), () => executionContext?.target() .model(SDK.ConsoleModel.ConsoleModel) ?.saveToTempVariable(executionContext, remoteObject), {jslogContext: 'store-as-global-variable'}); const ctxMenuClipboardSection = contextMenu.clipboardSection(); const inspectorFrontendHost = Host.InspectorFrontendHost.InspectorFrontendHostInstance; if (remoteObject.type === 'string') { ctxMenuClipboardSection.appendItem(i18nString(UIStrings.copyStringContents), () => { inspectorFrontendHost.copyText(remoteObject.value); }, {jslogContext: 'copy-string-contents'}); ctxMenuClipboardSection.appendItem(i18nString(UIStrings.copyStringAsJSLiteral), () => { inspectorFrontendHost.copyText(Platform.StringUtilities.formatAsJSLiteral(remoteObject.value)); }, {jslogContext: 'copy-string-as-js-literal'}); ctxMenuClipboardSection.appendItem(i18nString(UIStrings.copyStringAsJSONLiteral), () => { inspectorFrontendHost.copyText(JSON.stringify(remoteObject.value)); }, {jslogContext: 'copy-string-as-json-literal'}); } // We are trying to copy a primitive value. else if (primitiveRemoteObjectTypes.has(remoteObject.type)) { ctxMenuClipboardSection.appendItem(i18nString(UIStrings.copyS, {PH1: String(copyContextMenuTitle)}), () => { inspectorFrontendHost.copyText(remoteObject.description); }, {jslogContext: 'copy-primitive'}); } // We are trying to copy a remote object. else if (remoteObject.type === 'object') { const copyDecodedValueHandler = async(): Promise<void> => { const result = await remoteObject.callFunctionJSON(toStringForClipboard, [{ value: { subtype: remoteObject.subtype, indent, }, }]); inspectorFrontendHost.copyText(result); }; ctxMenuClipboardSection.appendItem( i18nString(UIStrings.copyS, {PH1: String(copyContextMenuTitle)}), copyDecodedValueHandler, {jslogContext: 'copy-object'}); } else if (remoteObject.type === 'function') { contextMenu.debugSection().appendItem( i18nString(UIStrings.showFunctionDefinition), this.showFunctionDefinition.bind(this, remoteObject), {jslogContext: 'show-function-definition'}); } function toStringForClipboard(this: Object, data: {subtype: string, indent: string}): string|undefined { const subtype = data.subtype; const indent = data.indent; if (subtype === 'map') { if (this instanceof Map) { const elements = Array.from(this.entries()); const literal = elements.length === 0 ? '' : JSON.stringify(elements, null, indent); return `new Map(${literal})`; } return undefined; } if (subtype === 'set') { if (this instanceof Set) { const values = Array.from(this.values()); const literal = values.length === 0 ? '' : JSON.stringify(values, null, indent); return `new Set(${literal})`; } return undefined; } if (subtype === 'node') { return this instanceof Element ? this.outerHTML : undefined; } if (subtype && typeof this === 'undefined') { return String(subtype); } try { return JSON.stringify(this, null, indent); } catch { return String(this); } } } private appendNetworkRequestItems( contextMenu: UI.ContextMenu.ContextMenu, request: SDK.NetworkRequest.NetworkRequest): void { const uiSourceCode = this.workspace.uiSourceCodeForURL(request.url()); if (!uiSourceCode) { return; } const openText = i18nString(UIStrings.openInSourcesPanel); const callback: () => void = this.showUILocation.bind(this, uiSourceCode.uiLocation(0, 0)); contextMenu.revealSection().appendItem(openText, callback, {jslogContext: 'reveal-in-sources'}); } private showFunctionDefinition(remoteObject: SDK.RemoteObject.RemoteObject): void { void SDK.RemoteObject.RemoteFunction.objectAsFunction(remoteObject) .targetFunction() .then( targetFunction => targetFunction.debuggerModel() .functionDetailsPromise(targetFunction) .then(this.didGetFunctionDetails.bind(this))); } private async didGetFunctionDetails(response: { location: SDK.DebuggerModel.Location|null, }|null): Promise<void> { if (!response?.location) { return; } const uiLocation = await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().rawLocationToUILocation( response.location); if (uiLocation) { this.showUILocation(uiLocation); } } private revealNavigatorSidebar(): void { void this.setAsCurrentPanel(); this.editorView.showBoth(true); } private revealDebuggerSidebar(): void { if (!Common.Settings.Settings.instance().moduleSetting('auto-focus-on-debugger-paused-enabled').get()) { return; } void this.setAsCurrentPanel(); this.splitWidget.showBoth(true); } private updateSidebarPosition(): void { let vertically; const position = Common.Settings.Settings.instance().moduleSetting('sidebar-position').get(); if (position === 'right') { vertically = false; } else if (position === 'bottom') { vertically = true; } else { vertically = UI.InspectorView.InspectorView.instance().element.offsetWidth < 680; } if (this.sidebarPane