UNPKG

chrome-devtools-frontend

Version:
285 lines (257 loc) • 11.6 kB
// Copyright 2021 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* eslint-disable @devtools/no-imperative-dom-api */ /* * Copyright (C) 2011 Google Inc. All rights reserved. * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com> * Copyright (C) 2009 Joseph Pecoraro * * 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. * 3. Neither the name of Apple Computer, Inc. ("Apple") 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 APPLE AND ITS 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 APPLE OR ITS 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 i18n from '../../../../core/i18n/i18n.js'; import * as SDK from '../../../../core/sdk/sdk.js'; import * as StackTrace from '../../../../models/stack_trace/stack_trace.js'; import * as Workspace from '../../../../models/workspace/workspace.js'; import * as VisualLogging from '../../../visual_logging/visual_logging.js'; import * as UI from '../../legacy.js'; import jsUtilsStyles from './jsUtils.css.js'; import {Linkifier} from './Linkifier.js'; const UIStrings = { /** * @description Text to stop preventing the debugger from stepping into library code */ removeFromIgnore: 'Remove from ignore list', /** * @description Text for scripts that should not be stepped into when debugging */ addToIgnore: 'Add script to ignore list', /** * @description A link to show more frames when they are available. */ showMoreFrames: 'Show ignore-listed frames', /** * @description A link to rehide frames that are by default hidden. */ showLess: 'Show less', } as const; const str_ = i18n.i18n.registerUIStrings('ui/legacy/components/utils/JSPresentationUtils.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); function populateContextMenu(link: Element, event: Event): void { const contextMenu = new UI.ContextMenu.ContextMenu(event); event.consume(true); const uiLocation = Linkifier.uiLocation(link); if (uiLocation && Workspace.IgnoreListManager.IgnoreListManager.instance().canIgnoreListUISourceCode(uiLocation.uiSourceCode)) { if (Workspace.IgnoreListManager.IgnoreListManager.instance().isUserIgnoreListedURL(uiLocation.uiSourceCode.url())) { contextMenu.debugSection().appendItem( i18nString(UIStrings.removeFromIgnore), () => Workspace.IgnoreListManager.IgnoreListManager.instance().unIgnoreListUISourceCode( uiLocation.uiSourceCode), {jslogContext: 'remove-from-ignore-list'}); } else { contextMenu.debugSection().appendItem( i18nString(UIStrings.addToIgnore), () => Workspace.IgnoreListManager.IgnoreListManager.instance().ignoreListUISourceCode(uiLocation.uiSourceCode), {jslogContext: 'add-to-ignore-list'}); } } contextMenu.appendApplicableItems(event); void contextMenu.show(); } function buildStackTraceRows( stackTrace: StackTrace.StackTrace.StackTrace, tabStops: boolean|undefined, showColumnNumber?: boolean, ): Array<StackTraceRegularRow|StackTraceAsyncRow> { const stackTraceRows: Array<StackTraceRegularRow|StackTraceAsyncRow> = []; function buildStackTraceRowsHelper( fragment: StackTrace.StackTrace.Fragment|StackTrace.StackTrace.AsyncFragment, previousFragment: StackTrace.StackTrace.Fragment|undefined = undefined): void { let asyncRow: StackTraceAsyncRow|null = null; const isAsync = 'description' in fragment; if (previousFragment && isAsync) { asyncRow = { asyncDescription: UI.UIUtils.asyncStackTraceLabel( fragment.description, previousFragment.frames.map(f => ({functionName: f.name ?? ''}))), }; stackTraceRows.push(asyncRow); } let previousStackFrameWasBreakpointCondition = false; for (const frame of fragment.frames) { const functionName = UI.UIUtils.beautifyFunctionName(frame.name ?? ''); const link = Linkifier.linkifyStackTraceFrame(frame, { showColumnNumber, tabStop: Boolean(tabStops), inlineFrameIndex: 0, revealBreakpoint: previousStackFrameWasBreakpointCondition, maxLength: UI.UIUtils.MaxLengthForDisplayedURLsInConsole, }); link.setAttribute('jslog', `${VisualLogging.link('stack-trace').track({click: true})}`); link.addEventListener('contextmenu', populateContextMenu.bind(null, link)); stackTraceRows.push({functionName, link}); previousStackFrameWasBreakpointCondition = [ SDK.DebuggerModel.COND_BREAKPOINT_SOURCE_URL, SDK.DebuggerModel.LOGPOINT_SOURCE_URL, ].includes(frame.url ?? ''); } } buildStackTraceRowsHelper(stackTrace.syncFragment); let previousFragment = stackTrace.syncFragment; for (const asyncFragment of stackTrace.asyncFragments) { if (asyncFragment.frames.length) { buildStackTraceRowsHelper(asyncFragment, previousFragment); } previousFragment = asyncFragment; } return stackTraceRows; } function renderStackTraceTable( container: Element, parent: Element, expandable: boolean, stackTraceRows: Array<StackTraceRegularRow|StackTraceAsyncRow>): void { container.removeChildren(); // The tableSection groups one or more synchronous call frames together. // Wherever there is an asynchronous call, a new section is created. let tableSection: Element|null = null; let firstRow = true; for (const item of stackTraceRows) { if (!tableSection || 'asyncDescription' in item) { tableSection = container.createChild('tbody'); } const row = tableSection.createChild('tr'); if (firstRow && expandable) { const button = row.createChild('td').createChild('button', 'arrow-icon-button'); button.createChild('span', 'arrow-icon'); parent.classList.add('expandable'); container.classList.add('expandable'); button.addEventListener('click', () => { button.setAttribute('jslog', `${VisualLogging.expand().track({click: true})}`); parent.classList.toggle('expanded'); container.classList.toggle('expanded'); }); firstRow = false; } else { row.createChild('td').textContent = '\n'; } if ('asyncDescription' in item) { row.createChild('td', 'stack-preview-async-description').textContent = item.asyncDescription; row.createChild('td'); row.createChild('td'); row.classList.add('stack-preview-async-row'); } else { row.createChild('td', 'function-name').textContent = item.functionName; row.createChild('td').textContent = ' @ '; row.createChild('td', 'link').appendChild(item.link); } } tableSection = container.createChild('tfoot'); const showAllRow = tableSection.createChild('tr', 'show-all-link'); showAllRow.createChild('td'); const cell = showAllRow.createChild('td'); cell.colSpan = 4; const showAllLink = cell.createChild('span', 'link'); // Don't directly put the text of the link in the DOM, as it will likely be // invisible and it may be confusing if it is copied to the clipboard. showAllLink.createChild('span', 'css-inserted-text') .setAttribute('data-inserted-text', i18nString(UIStrings.showMoreFrames)); showAllLink.addEventListener('click', () => { container.classList.add('show-hidden-rows'); parent.classList.add('show-hidden-rows'); // If we are in a popup, this will trigger a re-layout UI.GlassPane.GlassPane.containerMoved(container); }, false); const showLessRow = tableSection.createChild('tr', 'show-less-link'); showLessRow.createChild('td'); const showLesscell = showLessRow.createChild('td'); showLesscell.colSpan = 4; const showLessLink = showLesscell.createChild('span', 'link'); showLessLink.createChild('span', 'css-inserted-text') .setAttribute('data-inserted-text', i18nString(UIStrings.showLess)); showLessLink.addEventListener('click', () => { container.classList.remove('show-hidden-rows'); parent.classList.remove('show-hidden-rows'); // If we are in a popup, this will trigger a re-layout UI.GlassPane.GlassPane.containerMoved(container); }, false); } export interface Options { tabStops?: boolean; // Whether the width of stack trace preview // is constrained to its container or whether // it can grow the container. widthConstrained?: boolean; showColumnNumber?: boolean; expandable?: boolean; } interface StackTraceRegularRow { functionName: string; link: HTMLElement; } interface StackTraceAsyncRow { asyncDescription: string; } export class StackTracePreviewContent extends UI.Widget.Widget { #stackTrace?: StackTrace.StackTrace.StackTrace; #options: Options = {}; readonly #table: HTMLElement; constructor(element?: HTMLElement) { super(element, {useShadowDom: true, classes: ['monospace', 'stack-preview-container']}); UI.DOMUtilities.appendStyle(this.element.shadowRoot as ShadowRoot, jsUtilsStyles); this.#table = this.contentElement.createChild('table', 'stack-preview-container'); } hasContent(): boolean { if (!this.#stackTrace) { return false; } const {syncFragment, asyncFragments} = this.#stackTrace; return syncFragment.frames.length > 0 || asyncFragments.some(f => f.frames.length > 0); } override performUpdate(): void { if (!this.#stackTrace) { return; } const stackTraceRows = buildStackTraceRows(this.#stackTrace, this.#options.tabStops, this.#options.showColumnNumber); renderStackTraceTable(this.#table, this.element, this.#options.expandable ?? false, stackTraceRows); } get linkElements(): readonly HTMLElement[] { return [...this.contentElement.querySelectorAll<HTMLElement>('td.link > .devtools-link')]; } set options(options: Options) { this.#options = options; this.#table.classList.toggle('width-constrained', this.#options.widthConstrained ?? false); this.requestUpdate(); } set stackTrace(stackTrace: StackTrace.StackTrace.StackTrace) { if (this.#stackTrace) { this.#stackTrace.removeEventListener(StackTrace.StackTrace.Events.UPDATED, this.requestUpdate, this); } this.#stackTrace = stackTrace; this.#stackTrace.addEventListener(StackTrace.StackTrace.Events.UPDATED, this.requestUpdate, this); this.requestUpdate(); } }