UNPKG

chrome-devtools-frontend

Version:
656 lines (593 loc) • 26 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. * * 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 * 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 SDK from '../../core/sdk/sdk.js'; import type * as Protocol from '../../generated/protocol.js'; import * as Bindings from '../../models/bindings/bindings.js'; import * as Persistence from '../../models/persistence/persistence.js'; import * as SourceMapScopes from '../../models/source_map_scopes/source_map_scopes.js'; import type * as Workspace from '../../models/workspace/workspace.js'; import * as IconButton from '../../ui/components/icon_button/icon_button.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import callStackSidebarPaneStyles from './callStackSidebarPane.css.js'; const UIStrings = { /** *@description Text in Call Stack Sidebar Pane of the Sources panel */ callStack: 'Call Stack', /** *@description Not paused message element text content in Call Stack Sidebar Pane of the Sources panel */ notPaused: 'Not paused', /** *@description Text exposed to screen reader when navigating through a ignore-listed call frame in the sources panel */ onIgnoreList: 'on ignore list', /** *@description Show all link text content in Call Stack Sidebar Pane of the Sources panel */ showIgnorelistedFrames: 'Show ignore-listed frames', /** *@description Text to show more content */ showMore: 'Show more', /** *@description A context menu item in the Call Stack Sidebar Pane of the Sources panel */ copyStackTrace: 'Copy stack trace', /** *@description Text in Call Stack Sidebar Pane of the Sources panel when some call frames have warnings */ callFrameWarnings: 'Some call frames have warnings', /** *@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 A context menu item in the Call Stack Sidebar Pane. "Restart" is a verb and * "frame" is a noun. "Frame" refers to an individual item in the call stack, i.e. a call frame. * The user opens this context menu by selecting a specific call frame in the call stack sidebar pane. */ restartFrame: 'Restart frame', } as const; const str_ = i18n.i18n.registerUIStrings('panels/sources/CallStackSidebarPane.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); let callstackSidebarPaneInstance: CallStackSidebarPane; export class CallStackSidebarPane extends UI.View.SimpleView implements UI.ContextFlavorListener.ContextFlavorListener, UI.ListControl.ListDelegate<Item> { private readonly ignoreListMessageElement: Element; private readonly ignoreListCheckboxElement: HTMLInputElement; private readonly notPausedMessageElement: HTMLElement; private readonly callFrameWarningsElement: HTMLElement; private readonly items: UI.ListModel.ListModel<Item>; private list: UI.ListControl.ListControl<Item>; private readonly showMoreMessageElement: Element; private showIgnoreListed: boolean; private readonly locationPool: Bindings.LiveLocation.LiveLocationPool; private readonly updateThrottler: Common.Throttler.Throttler; private maxAsyncStackChainDepth: number; private readonly updateItemThrottler: Common.Throttler.Throttler; private readonly scheduledForUpdateItems: Set<Item>; private muteActivateItem?: boolean; private lastDebuggerModel: SDK.DebuggerModel.DebuggerModel|null = null; private constructor() { super(i18nString(UIStrings.callStack), true, 'sources.callstack'); this.registerRequiredCSS(callStackSidebarPaneStyles); this.contentElement.setAttribute('jslog', `${VisualLogging.section('sources.callstack')}`); ({element: this.ignoreListMessageElement, checkbox: this.ignoreListCheckboxElement} = this.createIgnoreListMessageElementAndCheckbox()); this.contentElement.appendChild(this.ignoreListMessageElement); this.notPausedMessageElement = this.contentElement.createChild('div', 'gray-info-message'); this.notPausedMessageElement.textContent = i18nString(UIStrings.notPaused); this.notPausedMessageElement.tabIndex = -1; this.callFrameWarningsElement = this.contentElement.createChild('div', 'call-frame-warnings-message'); const icon = new IconButton.Icon.Icon(); icon.data = { iconName: 'warning-filled', color: 'var(--icon-warning)', width: '14px', height: '14px', }; icon.classList.add('call-frame-warning-icon'); this.callFrameWarningsElement.appendChild(icon); this.callFrameWarningsElement.appendChild(document.createTextNode(i18nString(UIStrings.callFrameWarnings))); this.callFrameWarningsElement.tabIndex = -1; this.items = new UI.ListModel.ListModel(); this.list = new UI.ListControl.ListControl(this.items, this, UI.ListControl.ListMode.NonViewport); this.contentElement.appendChild(this.list.element); this.list.element.addEventListener('contextmenu', this.onContextMenu.bind(this), false); self.onInvokeElement(this.list.element, event => { const item = this.list.itemForNode((event.target as Node | null)); if (item) { this.activateItem(item); event.consume(true); } }); this.showMoreMessageElement = this.createShowMoreMessageElement(); this.showMoreMessageElement.classList.add('hidden'); this.contentElement.appendChild(this.showMoreMessageElement); this.showIgnoreListed = false; this.locationPool = new Bindings.LiveLocation.LiveLocationPool(); this.updateThrottler = new Common.Throttler.Throttler(100); this.maxAsyncStackChainDepth = defaultMaxAsyncStackChainDepth; this.update(); this.updateItemThrottler = new Common.Throttler.Throttler(100); this.scheduledForUpdateItems = new Set(); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.DebugInfoAttached, this.debugInfoAttached, this); } static instance(opts: { forceNew: boolean|null, } = {forceNew: null}): CallStackSidebarPane { const {forceNew} = opts; if (!callstackSidebarPaneInstance || forceNew) { callstackSidebarPaneInstance = new CallStackSidebarPane(); } return callstackSidebarPaneInstance; } flavorChanged(_object: Object|null): void { this.showIgnoreListed = false; this.ignoreListCheckboxElement.checked = false; this.maxAsyncStackChainDepth = defaultMaxAsyncStackChainDepth; this.update(); } private debugInfoAttached(): void { this.update(); } private setSourceMapSubscription(debuggerModel: SDK.DebuggerModel.DebuggerModel|null): void { // Shortcut for the case when we are listening to the same model. if (this.lastDebuggerModel === debuggerModel) { return; } if (this.lastDebuggerModel) { this.lastDebuggerModel.sourceMapManager().removeEventListener( SDK.SourceMapManager.Events.SourceMapAttached, this.debugInfoAttached, this); } this.lastDebuggerModel = debuggerModel; if (this.lastDebuggerModel) { this.lastDebuggerModel.sourceMapManager().addEventListener( SDK.SourceMapManager.Events.SourceMapAttached, this.debugInfoAttached, this); } } update(): void { void this.updateThrottler.schedule(() => this.doUpdate()); } async doUpdate(): Promise<void> { this.locationPool.disposeAll(); this.callFrameWarningsElement.classList.add('hidden'); const details = UI.Context.Context.instance().flavor(SDK.DebuggerModel.DebuggerPausedDetails); this.setSourceMapSubscription(details?.debuggerModel ?? null); if (!details) { this.notPausedMessageElement.classList.remove('hidden'); this.ignoreListMessageElement.classList.add('hidden'); this.showMoreMessageElement.classList.add('hidden'); this.items.replaceAll([]); UI.Context.Context.instance().setFlavor(SDK.DebuggerModel.CallFrame, null); return; } this.notPausedMessageElement.classList.add('hidden'); const itemPromises = []; const uniqueWarnings = new Set<string>(); for (const frame of details.callFrames) { const itemPromise = Item.createForDebuggerCallFrame(frame, this.locationPool, this.refreshItem.bind(this)); itemPromises.push(itemPromise); if (frame.missingDebugInfoDetails) { uniqueWarnings.add(frame.missingDebugInfoDetails.details); } } const items = await Promise.all(itemPromises); if (uniqueWarnings.size) { this.callFrameWarningsElement.classList.remove('hidden'); UI.Tooltip.Tooltip.install(this.callFrameWarningsElement, Array.from(uniqueWarnings).join('\n')); } let debuggerModel = details.debuggerModel; let asyncStackTraceId = details.asyncStackTraceId; let asyncStackTrace: Protocol.Runtime.StackTrace|undefined|null = details.asyncStackTrace; let previousStackTrace: Protocol.Runtime.CallFrame[]|SDK.DebuggerModel.CallFrame[] = details.callFrames; for (let {maxAsyncStackChainDepth} = this; maxAsyncStackChainDepth > 0; --maxAsyncStackChainDepth) { if (!asyncStackTrace) { if (!asyncStackTraceId) { break; } if (asyncStackTraceId.debuggerId) { const dm = await SDK.DebuggerModel.DebuggerModel.modelForDebuggerId(asyncStackTraceId.debuggerId); if (!dm) { break; } debuggerModel = dm; } asyncStackTrace = await debuggerModel.fetchAsyncStackTrace(asyncStackTraceId); if (!asyncStackTrace) { break; } } const title = UI.UIUtils.asyncStackTraceLabel(asyncStackTrace.description, previousStackTrace); items.push(...await Item.createItemsForAsyncStack( title, debuggerModel, asyncStackTrace.callFrames, this.locationPool, this.refreshItem.bind(this))); previousStackTrace = asyncStackTrace.callFrames; asyncStackTraceId = asyncStackTrace.parentId; asyncStackTrace = asyncStackTrace.parent; } this.showMoreMessageElement.classList.toggle('hidden', !asyncStackTrace); this.items.replaceAll(items); for (const item of this.items) { this.refreshItem(item); } if (this.maxAsyncStackChainDepth === defaultMaxAsyncStackChainDepth) { this.list.selectNextItem(true /* canWrap */, false /* center */); const selectedItem = this.list.selectedItem(); if (selectedItem) { this.activateItem(selectedItem); } } this.updatedForTest(); } private updatedForTest(): void { } private refreshItem(item: Item): void { this.scheduledForUpdateItems.add(item); void this.updateItemThrottler.schedule(async () => { const items = Array.from(this.scheduledForUpdateItems); this.scheduledForUpdateItems.clear(); this.muteActivateItem = true; if (!this.showIgnoreListed && this.items.every(item => item.isIgnoreListed)) { this.showIgnoreListed = true; for (let i = 0; i < this.items.length; ++i) { this.list.refreshItemByIndex(i); } this.ignoreListMessageElement.classList.toggle('hidden', true); } else { this.showIgnoreListed = this.ignoreListCheckboxElement.checked; const itemsSet = new Set<Item>(items); let hasIgnoreListed = false; for (let i = 0; i < this.items.length; ++i) { const item = this.items.at(i); if (itemsSet.has(item)) { this.list.refreshItemByIndex(i); } hasIgnoreListed = hasIgnoreListed || item.isIgnoreListed; } this.ignoreListMessageElement.classList.toggle('hidden', !hasIgnoreListed); } delete this.muteActivateItem; }); } createElementForItem(item: Item): Element { const element = document.createElement('div'); element.classList.add('call-frame-item'); const title = element.createChild('div', 'call-frame-item-title'); const titleElement = title.createChild('div', 'call-frame-title-text'); titleElement.textContent = item.title; if (item.isAsyncHeader) { element.classList.add('async-header'); } else { UI.Tooltip.Tooltip.install(titleElement, item.title); const linkElement = element.createChild('div', 'call-frame-location'); linkElement.textContent = Platform.StringUtilities.trimMiddle(item.linkText, 30); UI.Tooltip.Tooltip.install(linkElement, item.linkText); element.classList.toggle('ignore-listed-call-frame', item.isIgnoreListed); if (item.isIgnoreListed) { UI.ARIAUtils.setDescription(element, i18nString(UIStrings.onIgnoreList)); } if (!item.frame) { UI.ARIAUtils.setDisabled(element, true); } } const callframe = item.frame; const isSelected = callframe === UI.Context.Context.instance().flavor(SDK.DebuggerModel.CallFrame); element.classList.toggle('selected', isSelected); UI.ARIAUtils.setSelected(element, isSelected); element.classList.toggle('hidden', !this.showIgnoreListed && item.isIgnoreListed); const icon = new IconButton.Icon.Icon(); icon.data = { iconName: 'large-arrow-right-filled', color: 'var(--icon-arrow-main-thread)', width: '14px', height: '14px', }; icon.classList.add('selected-call-frame-icon'); element.appendChild(icon); element.tabIndex = item === this.list.selectedItem() ? 0 : -1; if (callframe?.missingDebugInfoDetails) { const icon = new IconButton.Icon.Icon(); icon.data = { iconName: 'warning-filled', color: 'var(--icon-warning)', width: '14px', height: '14px', }; icon.classList.add('call-frame-warning-icon'); const messages = callframe.missingDebugInfoDetails.resources.map( r => i18nString(UIStrings.debugFileNotFound, {PH1: Common.ParsedURL.ParsedURL.extractName(r.resourceUrl)})); UI.Tooltip.Tooltip.install(icon, [callframe.missingDebugInfoDetails.details, ...messages].join('\n')); element.appendChild(icon); } return element; } heightForItem(_item: Item): number { console.assert(false); // Should not be called. return 0; } isItemSelectable(_item: Item): boolean { return true; } selectedItemChanged(_from: Item|null, _to: Item|null, fromElement: HTMLElement|null, toElement: HTMLElement|null): void { if (fromElement) { fromElement.tabIndex = -1; } if (toElement) { this.setDefaultFocusedElement(toElement); toElement.tabIndex = 0; if (this.hasFocus()) { toElement.focus(); } } } updateSelectedItemARIA(_fromElement: Element|null, _toElement: Element|null): boolean { return true; } private createIgnoreListMessageElementAndCheckbox(): {element: Element, checkbox: HTMLInputElement} { const element = document.createElement('div'); element.classList.add('ignore-listed-message'); const label = element.createChild('label'); label.classList.add('ignore-listed-message-label'); const checkbox = label.createChild('input'); checkbox.tabIndex = 0; checkbox.type = 'checkbox'; checkbox.classList.add('ignore-listed-checkbox'); label.append(i18nString(UIStrings.showIgnorelistedFrames)); const showAll = (): void => { this.showIgnoreListed = checkbox.checked; for (const item of this.items) { this.refreshItem(item); } }; checkbox.addEventListener('click', showAll); return {element, checkbox}; } private createShowMoreMessageElement(): Element { const element = document.createElement('div'); element.classList.add('show-more-message'); element.createChild('span'); const showAllLink = element.createChild('span', 'link'); showAllLink.textContent = i18nString(UIStrings.showMore); showAllLink.addEventListener('click', () => { this.maxAsyncStackChainDepth += defaultMaxAsyncStackChainDepth; this.update(); }, false); return element; } private onContextMenu(event: Event): void { const item = this.list.itemForNode((event.target as Node | null)); if (!item) { return; } const contextMenu = new UI.ContextMenu.ContextMenu(event); const debuggerCallFrame = item.frame; if (debuggerCallFrame) { contextMenu.defaultSection().appendItem(i18nString(UIStrings.restartFrame), () => { Host.userMetrics.actionTaken(Host.UserMetrics.Action.StackFrameRestarted); void debuggerCallFrame.restart(); }, {disabled: !debuggerCallFrame.canBeRestarted, jslogContext: 'restart-frame'}); } contextMenu.defaultSection().appendItem( i18nString(UIStrings.copyStackTrace), this.copyStackTrace.bind(this), {jslogContext: 'copy-stack-trace'}); if (item.uiLocation) { this.appendIgnoreListURLContextMenuItems(contextMenu, item.uiLocation.uiSourceCode); } void contextMenu.show(); } private activateItem(item: Item): void { const uiLocation = item.uiLocation; if (this.muteActivateItem || !uiLocation) { return; } this.list.selectItem(item); const debuggerCallFrame = item.frame; const oldItem = this.activeCallFrameItem(); if (debuggerCallFrame && oldItem !== item) { debuggerCallFrame.debuggerModel.setSelectedCallFrame(debuggerCallFrame); UI.Context.Context.instance().setFlavor(SDK.DebuggerModel.CallFrame, debuggerCallFrame); if (oldItem) { this.refreshItem(oldItem); } this.refreshItem(item); } else { void Common.Revealer.reveal(uiLocation); } } activeCallFrameItem(): Item|null { const callFrame = UI.Context.Context.instance().flavor(SDK.DebuggerModel.CallFrame); if (callFrame) { return this.items.find(callFrameItem => callFrameItem.frame === callFrame) || null; } return null; } appendIgnoreListURLContextMenuItems( contextMenu: UI.ContextMenu.ContextMenu, uiSourceCode: Workspace.UISourceCode.UISourceCode): void { const binding = Persistence.Persistence.PersistenceImpl.instance().binding(uiSourceCode); if (binding) { uiSourceCode = binding.network; } const menuSection = contextMenu.section('ignoreList'); if (menuSection.items.length > 0) { // Already added menu items. return; } for (const {text, callback, jslogContext} of Bindings.IgnoreListManager.IgnoreListManager.instance() .getIgnoreListURLContextMenuItems(uiSourceCode)) { menuSection.appendItem(text, callback, {jslogContext}); } } selectNextCallFrameOnStack(): void { const oldItem = this.activeCallFrameItem(); const startIndex = oldItem ? this.items.indexOf(oldItem) + 1 : 0; for (let i = startIndex; i < this.items.length; i++) { const newItem = this.items.at(i); if (newItem.frame) { this.activateItem(newItem); break; } } } selectPreviousCallFrameOnStack(): void { const oldItem = this.activeCallFrameItem(); const startIndex = oldItem ? this.items.indexOf(oldItem) - 1 : this.items.length - 1; for (let i = startIndex; i >= 0; i--) { const newItem = this.items.at(i); if (newItem.frame) { this.activateItem(newItem); break; } } } private copyStackTrace(): void { const text = []; for (const item of this.items) { let itemText = item.title; if (item.uiLocation) { itemText += ' (' + item.uiLocation.linkText(true /* skipTrim */) + ')'; } text.push(itemText); } Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(text.join('\n')); } } export const elementSymbol = Symbol('element'); export const defaultMaxAsyncStackChainDepth = 32; export class ActionDelegate implements UI.ActionRegistration.ActionDelegate { handleAction(_context: UI.Context.Context, actionId: string): boolean { switch (actionId) { case 'debugger.next-call-frame': CallStackSidebarPane.instance().selectNextCallFrameOnStack(); return true; case 'debugger.previous-call-frame': CallStackSidebarPane.instance().selectPreviousCallFrameOnStack(); return true; } return false; } } export class Item { isIgnoreListed: boolean; title: string; linkText: string; uiLocation: Workspace.UISourceCode.UILocation|null; isAsyncHeader: boolean; updateDelegate: (arg0: Item) => void; /** Only set for synchronous frames */ readonly frame?: SDK.DebuggerModel.CallFrame; static async createForDebuggerCallFrame( frame: SDK.DebuggerModel.CallFrame, locationPool: Bindings.LiveLocation.LiveLocationPool, updateDelegate: (arg0: Item) => void): Promise<Item> { const name = frame.functionName; const item = new Item(UI.UIUtils.beautifyFunctionName(name), updateDelegate, frame); await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().createCallFrameLiveLocation( frame.location(), item.update.bind(item), locationPool); void SourceMapScopes.NamesResolver.resolveDebuggerFrameFunctionName(frame).then(functionName => { if (functionName && functionName !== name) { // Just update the item's title and call the update delegate directly, // instead of going through the update method below, since location // didn't change. item.title = functionName; item.updateDelegate(item); } }); return item; } static async createItemsForAsyncStack( title: string, debuggerModel: SDK.DebuggerModel.DebuggerModel, frames: Protocol.Runtime.CallFrame[], locationPool: Bindings.LiveLocation.LiveLocationPool, updateDelegate: (arg0: Item) => void): Promise<Item[]> { const headerItemToItemsSet = new WeakMap<Item, Set<Item>>(); const asyncHeaderItem = new Item(title, updateDelegate); headerItemToItemsSet.set(asyncHeaderItem, new Set()); asyncHeaderItem.isAsyncHeader = true; const asyncFrameItems = []; const liveLocationPromises = []; for (const frame of frames) { const item = new Item(UI.UIUtils.beautifyFunctionName(frame.functionName), update); const rawLocation = debuggerModel.createRawLocationByScriptId(frame.scriptId, frame.lineNumber, frame.columnNumber); liveLocationPromises.push( Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().createCallFrameLiveLocation( rawLocation, item.update.bind(item), locationPool)); void SourceMapScopes.NamesResolver.resolveProfileFrameFunctionName(frame, debuggerModel.target()) .then(functionName => { if (functionName && functionName !== frame.functionName) { item.title = functionName; item.updateDelegate(item); } }); asyncFrameItems.push(item); } await Promise.all(liveLocationPromises); updateDelegate(asyncHeaderItem); return [asyncHeaderItem, ...asyncFrameItems]; function update(item: Item): void { updateDelegate(item); let shouldUpdate = false; const items = headerItemToItemsSet.get(asyncHeaderItem); if (items) { if (item.isIgnoreListed) { items.delete(item); shouldUpdate = items.size === 0; } else { shouldUpdate = items.size === 0; items.add(item); } asyncHeaderItem.isIgnoreListed = items.size === 0; } if (shouldUpdate) { updateDelegate(asyncHeaderItem); } } } constructor(title: string, updateDelegate: (arg0: Item) => void, frame?: SDK.DebuggerModel.CallFrame) { this.isIgnoreListed = false; this.title = title; this.linkText = ''; this.uiLocation = null; this.isAsyncHeader = false; this.updateDelegate = updateDelegate; this.frame = frame; } private async update(liveLocation: Bindings.LiveLocation.LiveLocation): Promise<void> { const uiLocation = await liveLocation.uiLocation(); this.isIgnoreListed = await liveLocation.isIgnoreListed(); this.linkText = uiLocation ? uiLocation.linkText() : ''; this.uiLocation = uiLocation; this.updateDelegate(this); } }