chrome-devtools-frontend
Version:
Chrome DevTools UI
312 lines (285 loc) • 12.3 kB
text/typescript
// 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.
/*
* 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 Root from '../../../../core/root/root.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 {Directives, html, nothing, render, type TemplateResult} from '../../../lit/lit.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 {classMap} = Directives;
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();
}
export interface ViewInput {
stackTrace?: StackTrace.StackTrace.StackTrace;
tabStops?: boolean;
widthConstrained?: boolean;
showColumnNumber?: boolean;
expandable?: boolean;
expanded?: boolean;
showIgnoreListed?: boolean;
ignoreListManager?: Workspace.IgnoreListManager.IgnoreListManager;
onExpand: () => void;
onShowMore: () => void;
onShowLess: () => void;
}
export type View = (input: ViewInput, output: object, target: HTMLElement|DocumentFragment) => void;
export const DEFAULT_VIEW: View = (input, output, target) => {
let renderExpandButton = Boolean(input.expandable);
const maybeRenderExpandButton = (): TemplateResult => {
// clang-format off
const result = html`
${renderExpandButton ? html`
<button class="arrow-icon-button" jslog=${VisualLogging.expand().track({click: true})} @click=${input.onExpand}>
<span class="arrow-icon"></span>
</button>
` : '\n'}`;
// clang-format on
renderExpandButton = false;
return result;
};
const classes = {
'stack-preview-container': true,
'width-constrained': Boolean(input.widthConstrained),
expandable: Boolean(input.expandable),
expanded: Boolean(input.expanded),
'show-hidden-rows': Boolean(input.showIgnoreListed),
};
const {stackTrace} = input;
// clang-format off
render(html`
<style>${jsUtilsStyles}</style>
<table class=${classMap(classes)}>
${stackTrace ? html`
${[stackTrace.syncFragment, ...stackTrace.asyncFragments].map(fragment => html`
<tbody>
${'description' in fragment ? html`
<tr class="stack-preview-async-row">
<td>${maybeRenderExpandButton()}</td>
<td class="stack-preview-async-description">
${UI.UIUtils.asyncFragmentLabel(stackTrace, fragment as StackTrace.StackTrace.AsyncFragment)}
</td>
<td></td>
<td></td>
</tr>
` : nothing}
${fragment.frames.map((frame, i) => {
const previousStackFrameWasBreakpointCondition = i > 0 && [
SDK.DebuggerModel.COND_BREAKPOINT_SOURCE_URL,
SDK.DebuggerModel.LOGPOINT_SOURCE_URL,
].includes(fragment.frames[i - 1].url ?? '');
const link = Linkifier.linkifyStackTraceFrame(frame, {
showColumnNumber: Boolean(input.showColumnNumber),
tabStop: Boolean(input.tabStops),
inlineFrameIndex: 0,
revealBreakpoint: previousStackFrameWasBreakpointCondition,
maxLength: UI.UIUtils.MaxLengthForDisplayedURLsInConsole,
ignoreListManager: input.ignoreListManager,
});
link.setAttribute('jslog', `${VisualLogging.link('stack-trace').track({click: true})}`);
link.addEventListener('contextmenu', populateContextMenu.bind(null, link));
return html`
<tr>
<td>${maybeRenderExpandButton()}</td>
<td class="function-name">
${UI.UIUtils.beautifyFunctionName(frame.name ?? '')}
</td>
<td> @ </td>
<td class="link">${link}</td>
</tr>
`;})}
</tbody>
`)}
<tfoot>
<tr class="show-all-link">
<td></td>
<td colspan="3">
<span class="link" @click=${input.onShowMore}>
<span class="css-inserted-text" data-inserted-text=${i18nString(UIStrings.showMoreFrames)}></span>
</span>
</td>
</tr>
<tr class="show-less-link">
<td></td>
<td colspan="3">
<span class="link" @click=${input.onShowLess}>
<span class="css-inserted-text" data-inserted-text=${i18nString(UIStrings.showLess)}></span>
</span>
</td>
</tr>
</tfoot>
` : nothing}
</table>
`, target, {container: {classes: ['monospace', 'stack-preview-container']}});
// clang-format on
};
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;
ignoreListManager?: Workspace.IgnoreListManager.IgnoreListManager;
}
export class StackTracePreviewContent extends UI.Widget.Widget<ShadowRoot> {
readonly #view: View;
#stackTrace?: StackTrace.StackTrace.StackTrace;
#options: Options = {};
#expanded = false;
#showIgnoreListed = false;
constructor(element?: HTMLElement, view = DEFAULT_VIEW) {
super(element, {useShadowDom: 'pure'});
this.#view = view;
}
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 {
this.element.classList.toggle('expandable', this.#options.expandable);
this.element.classList.toggle('expanded', this.#expanded);
this.element.classList.toggle('show-hidden-rows', this.#showIgnoreListed);
const input: ViewInput = {
stackTrace: this.#stackTrace,
...this.#options,
expanded: this.#expanded,
showIgnoreListed: this.#showIgnoreListed,
onExpand: this.#onExpand.bind(this),
onShowMore: this.#onShowMoreLess.bind(this, true),
onShowLess: this.#onShowMoreLess.bind(this, false),
};
this.#view(input, {}, this.contentElement);
this.#updateHasNonIgnoredLinks();
}
// Propagate ignore-list state to the host element so that CSS outside the
// shadow DOM can coordinate ignore-list toggling across multiple stack
// traces (e.g. Error inline stack + console.error call stack).
// See crbug.com/379788109.
#updateHasNonIgnoredLinks = (): void => {
const hasNonIgnoredLinks = this.linkElements.some(link => {
const uiLocation = Linkifier.uiLocation(link);
if (uiLocation) {
return !uiLocation.isIgnoreListed(this.#options.ignoreListManager);
}
return !link.classList.contains('ignore-list-link');
});
this.element.classList.toggle('has-non-ignored-links', hasNonIgnoredLinks);
};
override wasShown(): void {
super.wasShown();
if (Root.DevToolsContext.globalInstance().has(Workspace.IgnoreListManager.IgnoreListManager)) {
Workspace.IgnoreListManager.IgnoreListManager.instance().addChangeListener(this.#updateHasNonIgnoredLinks);
}
}
override willHide(): void {
if (Root.DevToolsContext.globalInstance().has(Workspace.IgnoreListManager.IgnoreListManager)) {
Workspace.IgnoreListManager.IgnoreListManager.instance().removeChangeListener(this.#updateHasNonIgnoredLinks);
}
super.willHide();
}
get linkElements(): readonly HTMLElement[] {
return [...this.contentElement.querySelectorAll<HTMLElement>('td.link > .devtools-link')];
}
set options(options: Options) {
this.#options = options;
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();
}
#onShowMoreLess(more: boolean): void {
this.#showIgnoreListed = more;
this.requestUpdate();
// If we are in a popup, this will trigger a re-layout
void this.updateComplete.then(() => UI.GlassPane.GlassPane.containerMoved(this.element));
}
#onExpand(): void {
this.#expanded = !this.#expanded;
this.requestUpdate();
}
}