mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-11-06 21:29:31 +08:00
The option was only allowed to be set to `false`, so it's not been used at all, so now we can remove it, given that we've had the dark mode legacy patching disabled without any major bugs in the past few weeks. Bug: chromium:1152736 Change-Id: I1e794d75f9c354a0d16b1b3975f7ebdf82b3448b Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2988303 Commit-Queue: Jack Franklin <jacktfranklin@chromium.org> Reviewed-by: Paul Lewis <aerotwist@chromium.org>
664 lines
25 KiB
TypeScript
664 lines
25 KiB
TypeScript
// Copyright (c) 2016 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_underscored_properties */
|
|
|
|
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 * as Bindings from '../../models/bindings/bindings.js';
|
|
import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js';
|
|
import * as UI from '../../ui/legacy/legacy.js';
|
|
|
|
import type * as Workspace from '../../models/workspace/workspace.js';
|
|
|
|
import {CoverageDecorationManager, decoratorType} from './CoverageDecorationManager.js';
|
|
import {CoverageListView} from './CoverageListView.js';
|
|
import type {CoverageInfo, URLCoverageInfo} from './CoverageModel.js';
|
|
import {CoverageModel, Events, CoverageType} from './CoverageModel.js';
|
|
|
|
const UIStrings = {
|
|
/**
|
|
*@description Tooltip in Coverage List View of the Coverage tab for selecting JavaScript coverage mode
|
|
*/
|
|
chooseCoverageGranularityPer:
|
|
'Choose coverage granularity: Per function has low overhead, per block has significant overhead.',
|
|
/**
|
|
*@description Text in Coverage List View of the Coverage tab
|
|
*/
|
|
perFunction: 'Per function',
|
|
/**
|
|
*@description Text in Coverage List View of the Coverage tab
|
|
*/
|
|
perBlock: 'Per block',
|
|
/**
|
|
*@description Text to clear everything
|
|
*/
|
|
clearAll: 'Clear all',
|
|
/**
|
|
*@description Tooltip text that appears when hovering over the largeicon download button in the Coverage View of the Coverage tab
|
|
*/
|
|
export: 'Export...',
|
|
/**
|
|
*@description Text in Coverage View of the Coverage tab
|
|
*/
|
|
urlFilter: 'URL filter',
|
|
/**
|
|
*@description Label for the type filter in the Converage Panel
|
|
*/
|
|
filterCoverageByType: 'Filter coverage by type',
|
|
/**
|
|
*@description Text for everything
|
|
*/
|
|
all: 'All',
|
|
/**
|
|
*@description Text that appears on a button for the css resource type filter.
|
|
*/
|
|
css: 'CSS',
|
|
/**
|
|
*@description Text in Timeline Tree View of the Performance panel
|
|
*/
|
|
javascript: 'JavaScript',
|
|
/**
|
|
*@description Tooltip text that appears on the setting when hovering over it in Coverage View of the Coverage tab
|
|
*/
|
|
includeExtensionContentScripts: 'Include extension content scripts',
|
|
/**
|
|
*@description Title for a type of source files
|
|
*/
|
|
contentScripts: 'Content scripts',
|
|
/**
|
|
*@description Message in Coverage View of the Coverage tab
|
|
*@example {record button icon} PH1
|
|
*/
|
|
clickTheReloadButtonSToReloadAnd: 'Click the reload button {PH1} to reload and start capturing coverage.',
|
|
/**
|
|
*@description Message in Coverage View of the Coverage tab
|
|
*@example {record button icon} PH1
|
|
*/
|
|
clickTheRecordButtonSToStart: 'Click the record button {PH1} to start capturing coverage.',
|
|
/**
|
|
*@description Footer message in Coverage View of the Coverage tab
|
|
*@example {300k used, 600k unused} PH1
|
|
*@example {500k used, 800k unused} PH2
|
|
*/
|
|
filteredSTotalS: 'Filtered: {PH1} Total: {PH2}',
|
|
/**
|
|
*@description Footer message in Coverage View of the Coverage tab
|
|
*@example {1.5 MB} PH1
|
|
*@example {2.1 MB} PH2
|
|
*@example {71%} PH3
|
|
*@example {29%} PH4
|
|
*/
|
|
sOfSSUsedSoFarSUnused: '{PH1} of {PH2} ({PH3}%) used so far, {PH4} unused.',
|
|
};
|
|
const str_ = i18n.i18n.registerUIStrings('panels/coverage/CoverageView.ts', UIStrings);
|
|
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
|
|
|
let coverageViewInstance: CoverageView;
|
|
|
|
export class CoverageView extends UI.Widget.VBox {
|
|
_model: CoverageModel|null;
|
|
_decorationManager: CoverageDecorationManager|null;
|
|
_resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel|null;
|
|
_coverageTypeComboBox: UI.Toolbar.ToolbarComboBox;
|
|
_coverageTypeComboBoxSetting: Common.Settings.Setting<number>;
|
|
_toggleRecordAction: UI.ActionRegistration.Action;
|
|
_toggleRecordButton: UI.Toolbar.ToolbarButton;
|
|
_inlineReloadButton: Element|null;
|
|
_startWithReloadButton: UI.Toolbar.ToolbarButton|undefined;
|
|
_clearButton: UI.Toolbar.ToolbarButton;
|
|
_saveButton: UI.Toolbar.ToolbarButton;
|
|
_textFilterRegExp: RegExp|null;
|
|
_filterInput: UI.Toolbar.ToolbarInput;
|
|
_typeFilterValue: number|null;
|
|
_filterByTypeComboBox: UI.Toolbar.ToolbarComboBox;
|
|
_showContentScriptsSetting: Common.Settings.Setting<boolean>;
|
|
_contentScriptsCheckbox: UI.Toolbar.ToolbarSettingCheckbox;
|
|
_coverageResultsElement: HTMLElement;
|
|
_landingPage: UI.Widget.VBox;
|
|
_listView: CoverageListView;
|
|
_statusToolbarElement: HTMLElement;
|
|
_statusMessageElement: HTMLElement;
|
|
|
|
private constructor() {
|
|
super(true);
|
|
|
|
this._model = null;
|
|
this._decorationManager = null;
|
|
this._resourceTreeModel = null;
|
|
|
|
this.registerRequiredCSS('panels/coverage/coverageView.css');
|
|
|
|
const toolbarContainer = this.contentElement.createChild('div', 'coverage-toolbar-container');
|
|
const toolbar = new UI.Toolbar.Toolbar('coverage-toolbar', toolbarContainer);
|
|
toolbar.makeWrappable(true);
|
|
|
|
this._coverageTypeComboBox = new UI.Toolbar.ToolbarComboBox(
|
|
this._onCoverageTypeComboBoxSelectionChanged.bind(this), i18nString(UIStrings.chooseCoverageGranularityPer));
|
|
const coverageTypes = [
|
|
{
|
|
label: i18nString(UIStrings.perFunction),
|
|
value: CoverageType.JavaScript | CoverageType.JavaScriptPerFunction,
|
|
},
|
|
{
|
|
label: i18nString(UIStrings.perBlock),
|
|
value: CoverageType.JavaScript,
|
|
},
|
|
];
|
|
for (const type of coverageTypes) {
|
|
this._coverageTypeComboBox.addOption(this._coverageTypeComboBox.createOption(type.label, `${type.value}`));
|
|
}
|
|
this._coverageTypeComboBoxSetting =
|
|
Common.Settings.Settings.instance().createSetting('coverageViewCoverageType', 0);
|
|
this._coverageTypeComboBox.setSelectedIndex(this._coverageTypeComboBoxSetting.get());
|
|
this._coverageTypeComboBox.setEnabled(true);
|
|
toolbar.appendToolbarItem(this._coverageTypeComboBox);
|
|
this._toggleRecordAction =
|
|
UI.ActionRegistry.ActionRegistry.instance().action('coverage.toggle-recording') as UI.ActionRegistration.Action;
|
|
this._toggleRecordButton = UI.Toolbar.Toolbar.createActionButton(this._toggleRecordAction);
|
|
toolbar.appendToolbarItem(this._toggleRecordButton);
|
|
|
|
const mainTarget = SDK.TargetManager.TargetManager.instance().mainTarget();
|
|
const mainTargetSupportsRecordOnReload = mainTarget && mainTarget.model(SDK.ResourceTreeModel.ResourceTreeModel);
|
|
this._inlineReloadButton = null;
|
|
if (mainTargetSupportsRecordOnReload) {
|
|
const startWithReloadAction = UI.ActionRegistry.ActionRegistry.instance().action('coverage.start-with-reload') as
|
|
UI.ActionRegistration.Action;
|
|
this._startWithReloadButton = UI.Toolbar.Toolbar.createActionButton(startWithReloadAction);
|
|
toolbar.appendToolbarItem(this._startWithReloadButton);
|
|
this._toggleRecordButton.setEnabled(false);
|
|
this._toggleRecordButton.setVisible(false);
|
|
}
|
|
this._clearButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.clearAll), 'largeicon-clear');
|
|
this._clearButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._clear.bind(this));
|
|
toolbar.appendToolbarItem(this._clearButton);
|
|
|
|
toolbar.appendSeparator();
|
|
this._saveButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.export), 'largeicon-download');
|
|
this._saveButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, _event => {
|
|
this._exportReport();
|
|
});
|
|
toolbar.appendToolbarItem(this._saveButton);
|
|
this._saveButton.setEnabled(false);
|
|
|
|
this._textFilterRegExp = null;
|
|
toolbar.appendSeparator();
|
|
this._filterInput = new UI.Toolbar.ToolbarInput(i18nString(UIStrings.urlFilter), '', 0.4, 1);
|
|
this._filterInput.setEnabled(false);
|
|
this._filterInput.addEventListener(UI.Toolbar.ToolbarInput.Event.TextChanged, this._onFilterChanged, this);
|
|
toolbar.appendToolbarItem(this._filterInput);
|
|
|
|
toolbar.appendSeparator();
|
|
|
|
this._typeFilterValue = null;
|
|
this._filterByTypeComboBox = new UI.Toolbar.ToolbarComboBox(
|
|
this._onFilterByTypeChanged.bind(this), i18nString(UIStrings.filterCoverageByType));
|
|
const options = [
|
|
{
|
|
label: i18nString(UIStrings.all),
|
|
value: '',
|
|
},
|
|
{
|
|
label: i18nString(UIStrings.css),
|
|
value: CoverageType.CSS,
|
|
},
|
|
{
|
|
label: i18nString(UIStrings.javascript),
|
|
value: CoverageType.JavaScript | CoverageType.JavaScriptPerFunction,
|
|
},
|
|
];
|
|
for (const option of options) {
|
|
this._filterByTypeComboBox.addOption(this._filterByTypeComboBox.createOption(option.label, `${option.value}`));
|
|
}
|
|
|
|
this._filterByTypeComboBox.setSelectedIndex(0);
|
|
this._filterByTypeComboBox.setEnabled(false);
|
|
toolbar.appendToolbarItem(this._filterByTypeComboBox);
|
|
|
|
toolbar.appendSeparator();
|
|
this._showContentScriptsSetting = Common.Settings.Settings.instance().createSetting('showContentScripts', false);
|
|
this._showContentScriptsSetting.addChangeListener(this._onFilterChanged, this);
|
|
this._contentScriptsCheckbox = new UI.Toolbar.ToolbarSettingCheckbox(
|
|
this._showContentScriptsSetting, i18nString(UIStrings.includeExtensionContentScripts),
|
|
i18nString(UIStrings.contentScripts));
|
|
this._contentScriptsCheckbox.setEnabled(false);
|
|
toolbar.appendToolbarItem(this._contentScriptsCheckbox);
|
|
|
|
this._coverageResultsElement = this.contentElement.createChild('div', 'coverage-results');
|
|
this._landingPage = this._buildLandingPage();
|
|
this._listView = new CoverageListView(this._isVisible.bind(this, false));
|
|
|
|
this._statusToolbarElement = this.contentElement.createChild('div', 'coverage-toolbar-summary');
|
|
this._statusMessageElement = this._statusToolbarElement.createChild('div', 'coverage-message');
|
|
this._landingPage.show(this._coverageResultsElement);
|
|
}
|
|
|
|
static instance(): CoverageView {
|
|
if (!coverageViewInstance) {
|
|
coverageViewInstance = new CoverageView();
|
|
}
|
|
return coverageViewInstance;
|
|
}
|
|
|
|
_buildLandingPage(): UI.Widget.VBox {
|
|
const widget = new UI.Widget.VBox();
|
|
let message;
|
|
if (this._startWithReloadButton) {
|
|
this._inlineReloadButton =
|
|
UI.UIUtils.createInlineButton(UI.Toolbar.Toolbar.createActionButtonForId('coverage.start-with-reload'));
|
|
message = i18n.i18n.getFormatLocalizedString(
|
|
str_, UIStrings.clickTheReloadButtonSToReloadAnd, {PH1: this._inlineReloadButton});
|
|
} else {
|
|
const recordButton =
|
|
UI.UIUtils.createInlineButton(UI.Toolbar.Toolbar.createActionButton(this._toggleRecordAction));
|
|
message = i18n.i18n.getFormatLocalizedString(str_, UIStrings.clickTheRecordButtonSToStart, {PH1: recordButton});
|
|
}
|
|
message.classList.add('message');
|
|
widget.contentElement.appendChild(message);
|
|
widget.element.classList.add('landing-page');
|
|
return widget;
|
|
}
|
|
|
|
_clear(): void {
|
|
if (this._model) {
|
|
this._model.reset();
|
|
}
|
|
this._reset();
|
|
}
|
|
|
|
_reset(): void {
|
|
if (this._decorationManager) {
|
|
this._decorationManager.dispose();
|
|
this._decorationManager = null;
|
|
}
|
|
this._listView.reset();
|
|
this._listView.detach();
|
|
this._landingPage.show(this._coverageResultsElement);
|
|
this._statusMessageElement.textContent = '';
|
|
this._filterInput.setEnabled(false);
|
|
this._filterByTypeComboBox.setEnabled(false);
|
|
this._contentScriptsCheckbox.setEnabled(false);
|
|
this._saveButton.setEnabled(false);
|
|
}
|
|
|
|
_toggleRecording(): void {
|
|
const enable = !this._toggleRecordAction.toggled();
|
|
|
|
if (enable) {
|
|
this._startRecording({reload: false, jsCoveragePerBlock: this.isBlockCoverageSelected()});
|
|
} else {
|
|
this.stopRecording();
|
|
}
|
|
}
|
|
|
|
isBlockCoverageSelected(): boolean {
|
|
const option = this._coverageTypeComboBox.selectedOption();
|
|
const coverageType = Number(option ? option.value : Number.NaN);
|
|
// Check that Coverage.CoverageType.JavaScriptPerFunction is not present.
|
|
return coverageType === CoverageType.JavaScript;
|
|
}
|
|
|
|
_selectCoverageType(jsCoveragePerBlock: boolean): void {
|
|
const selectedIndex = jsCoveragePerBlock ? 1 : 0;
|
|
this._coverageTypeComboBox.setSelectedIndex(selectedIndex);
|
|
}
|
|
|
|
_onCoverageTypeComboBoxSelectionChanged(): void {
|
|
this._coverageTypeComboBoxSetting.set(this._coverageTypeComboBox.selectedIndex());
|
|
}
|
|
|
|
async ensureRecordingStarted(): Promise<void> {
|
|
const enabled = this._toggleRecordAction.toggled();
|
|
|
|
if (enabled) {
|
|
await this.stopRecording();
|
|
}
|
|
await this._startRecording({reload: false, jsCoveragePerBlock: false});
|
|
}
|
|
|
|
async _startRecording(options: {reload: (boolean|undefined), jsCoveragePerBlock: (boolean|undefined)}|
|
|
null): Promise<void> {
|
|
let hadFocus, reloadButtonFocused;
|
|
if ((this._startWithReloadButton && this._startWithReloadButton.element.hasFocus()) ||
|
|
(this._inlineReloadButton && this._inlineReloadButton.hasFocus())) {
|
|
reloadButtonFocused = true;
|
|
} else if (this.hasFocus()) {
|
|
hadFocus = true;
|
|
}
|
|
|
|
this._reset();
|
|
const mainTarget = SDK.TargetManager.TargetManager.instance().mainTarget();
|
|
if (!mainTarget) {
|
|
return;
|
|
}
|
|
|
|
const {reload, jsCoveragePerBlock} = {reload: false, jsCoveragePerBlock: false, ...options};
|
|
|
|
if (!this._model || reload) {
|
|
this._model = mainTarget.model(CoverageModel);
|
|
}
|
|
if (!this._model) {
|
|
return;
|
|
}
|
|
Host.userMetrics.actionTaken(Host.UserMetrics.Action.CoverageStarted);
|
|
if (jsCoveragePerBlock) {
|
|
Host.userMetrics.actionTaken(Host.UserMetrics.Action.CoverageStartedPerBlock);
|
|
}
|
|
const success = await this._model.start(Boolean(jsCoveragePerBlock));
|
|
if (!success) {
|
|
return;
|
|
}
|
|
this._selectCoverageType(Boolean(jsCoveragePerBlock));
|
|
|
|
this._model.addEventListener(Events.CoverageUpdated, this._onCoverageDataReceived, this);
|
|
this._resourceTreeModel =
|
|
mainTarget.model(SDK.ResourceTreeModel.ResourceTreeModel) as SDK.ResourceTreeModel.ResourceTreeModel | null;
|
|
if (this._resourceTreeModel) {
|
|
this._resourceTreeModel.addEventListener(
|
|
SDK.ResourceTreeModel.Events.MainFrameNavigated, this._onMainFrameNavigated, this);
|
|
}
|
|
this._decorationManager = new CoverageDecorationManager(this._model as CoverageModel);
|
|
this._toggleRecordAction.setToggled(true);
|
|
this._clearButton.setEnabled(false);
|
|
if (this._startWithReloadButton) {
|
|
this._startWithReloadButton.setEnabled(false);
|
|
this._startWithReloadButton.setVisible(false);
|
|
this._toggleRecordButton.setEnabled(true);
|
|
this._toggleRecordButton.setVisible(true);
|
|
if (reloadButtonFocused) {
|
|
this._toggleRecordButton.focus();
|
|
}
|
|
}
|
|
this._coverageTypeComboBox.setEnabled(false);
|
|
this._filterInput.setEnabled(true);
|
|
this._filterByTypeComboBox.setEnabled(true);
|
|
this._contentScriptsCheckbox.setEnabled(true);
|
|
if (this._landingPage.isShowing()) {
|
|
this._landingPage.detach();
|
|
}
|
|
this._listView.show(this._coverageResultsElement);
|
|
if (hadFocus && !reloadButtonFocused) {
|
|
this._listView.focus();
|
|
}
|
|
if (reload && this._resourceTreeModel) {
|
|
this._resourceTreeModel.reloadPage();
|
|
} else {
|
|
this._model.startPolling();
|
|
}
|
|
}
|
|
|
|
_onCoverageDataReceived(event: Common.EventTarget.EventTargetEvent): void {
|
|
const data = event.data as CoverageInfo[];
|
|
this._updateViews(data);
|
|
}
|
|
|
|
async stopRecording(): Promise<void> {
|
|
if (this._resourceTreeModel) {
|
|
this._resourceTreeModel.removeEventListener(
|
|
SDK.ResourceTreeModel.Events.MainFrameNavigated, this._onMainFrameNavigated, this);
|
|
this._resourceTreeModel = null;
|
|
}
|
|
if (this.hasFocus()) {
|
|
this._listView.focus();
|
|
}
|
|
// Stopping the model triggers one last poll to get the final data.
|
|
if (this._model) {
|
|
await this._model.stop();
|
|
this._model.removeEventListener(Events.CoverageUpdated, this._onCoverageDataReceived, this);
|
|
}
|
|
this._toggleRecordAction.setToggled(false);
|
|
this._coverageTypeComboBox.setEnabled(true);
|
|
if (this._startWithReloadButton) {
|
|
this._startWithReloadButton.setEnabled(true);
|
|
this._startWithReloadButton.setVisible(true);
|
|
this._toggleRecordButton.setEnabled(false);
|
|
this._toggleRecordButton.setVisible(false);
|
|
}
|
|
this._clearButton.setEnabled(true);
|
|
}
|
|
|
|
processBacklog(): void {
|
|
this._model && this._model.processJSBacklog();
|
|
}
|
|
|
|
_onMainFrameNavigated(): void {
|
|
this._model && this._model.reset();
|
|
this._decorationManager && this._decorationManager.reset();
|
|
this._listView.reset();
|
|
this._model && this._model.startPolling();
|
|
}
|
|
|
|
_updateViews(updatedEntries: CoverageInfo[]): void {
|
|
this._updateStats();
|
|
this._listView.update(this._model && this._model.entries() || []);
|
|
this._saveButton.setEnabled(this._model !== null && this._model.entries().length > 0);
|
|
this._decorationManager && this._decorationManager.update(updatedEntries);
|
|
}
|
|
|
|
_updateStats(): void {
|
|
const all = {total: 0, unused: 0};
|
|
const filtered = {total: 0, unused: 0};
|
|
let filterApplied = false;
|
|
if (this._model) {
|
|
for (const info of this._model.entries()) {
|
|
all.total += info.size();
|
|
all.unused += info.unusedSize();
|
|
if (this._isVisible(false, info)) {
|
|
filtered.total += info.size();
|
|
filtered.unused += info.unusedSize();
|
|
} else {
|
|
filterApplied = true;
|
|
}
|
|
}
|
|
}
|
|
this._statusMessageElement.textContent = filterApplied ?
|
|
i18nString(UIStrings.filteredSTotalS, {PH1: formatStat(filtered), PH2: formatStat(all)}) :
|
|
formatStat(all);
|
|
|
|
function formatStat({total, unused}: {total: number, unused: number}): string {
|
|
const used = total - unused;
|
|
const percentUsed = total ? Math.round(100 * used / total) : 0;
|
|
return i18nString(UIStrings.sOfSSUsedSoFarSUnused, {
|
|
PH1: Platform.NumberUtilities.bytesToString(used),
|
|
PH2: Platform.NumberUtilities.bytesToString(total),
|
|
PH3: percentUsed,
|
|
PH4: Platform.NumberUtilities.bytesToString(unused),
|
|
});
|
|
}
|
|
}
|
|
|
|
_onFilterChanged(): void {
|
|
if (!this._listView) {
|
|
return;
|
|
}
|
|
const text = this._filterInput.value();
|
|
this._textFilterRegExp = text ? createPlainTextSearchRegex(text, 'i') : null;
|
|
this._listView.updateFilterAndHighlight(this._textFilterRegExp);
|
|
this._updateStats();
|
|
}
|
|
|
|
_onFilterByTypeChanged(): void {
|
|
if (!this._listView) {
|
|
return;
|
|
}
|
|
|
|
Host.userMetrics.actionTaken(Host.UserMetrics.Action.CoverageReportFiltered);
|
|
|
|
const option = this._filterByTypeComboBox.selectedOption();
|
|
const type = option && option.value;
|
|
this._typeFilterValue = parseInt(type || '', 10) || null;
|
|
this._listView.updateFilterAndHighlight(this._textFilterRegExp);
|
|
this._updateStats();
|
|
}
|
|
|
|
_isVisible(ignoreTextFilter: boolean, coverageInfo: URLCoverageInfo): boolean {
|
|
const url = coverageInfo.url();
|
|
if (url.startsWith(CoverageView.EXTENSION_BINDINGS_URL_PREFIX)) {
|
|
return false;
|
|
}
|
|
if (coverageInfo.isContentScript() && !this._showContentScriptsSetting.get()) {
|
|
return false;
|
|
}
|
|
if (this._typeFilterValue && !(coverageInfo.type() & this._typeFilterValue)) {
|
|
return false;
|
|
}
|
|
|
|
return ignoreTextFilter || !this._textFilterRegExp || this._textFilterRegExp.test(url);
|
|
}
|
|
|
|
async _exportReport(): Promise<void> {
|
|
const fos = new Bindings.FileUtils.FileOutputStream();
|
|
const fileName = `Coverage-${Platform.DateUtilities.toISO8601Compact(new Date())}.json`;
|
|
const accepted = await fos.open(fileName);
|
|
if (!accepted) {
|
|
return;
|
|
}
|
|
this._model && this._model.exportReport(fos);
|
|
}
|
|
|
|
selectCoverageItemByUrl(url: string): void {
|
|
this._listView.selectByUrl(url);
|
|
}
|
|
|
|
static readonly EXTENSION_BINDINGS_URL_PREFIX = 'extensions::';
|
|
}
|
|
|
|
let actionDelegateInstance: ActionDelegate;
|
|
|
|
export class ActionDelegate implements UI.ActionRegistration.ActionDelegate {
|
|
handleAction(context: UI.Context.Context, actionId: string): boolean {
|
|
const coverageViewId = 'coverage';
|
|
UI.ViewManager.ViewManager.instance()
|
|
.showView(coverageViewId, /** userGesture= */ false, /** omitFocus= */ true)
|
|
.then(() => {
|
|
const view = UI.ViewManager.ViewManager.instance().view(coverageViewId);
|
|
return view && view.widget();
|
|
})
|
|
.then(widget => this._innerHandleAction(widget as CoverageView, actionId));
|
|
|
|
return true;
|
|
}
|
|
static instance(opts: {forceNew: boolean|null} = {forceNew: null}): ActionDelegate {
|
|
const {forceNew} = opts;
|
|
if (!actionDelegateInstance || forceNew) {
|
|
actionDelegateInstance = new ActionDelegate();
|
|
}
|
|
return actionDelegateInstance;
|
|
}
|
|
|
|
_innerHandleAction(coverageView: CoverageView, actionId: string): void {
|
|
switch (actionId) {
|
|
case 'coverage.toggle-recording':
|
|
coverageView._toggleRecording();
|
|
break;
|
|
case 'coverage.start-with-reload':
|
|
coverageView._startRecording({reload: true, jsCoveragePerBlock: coverageView.isBlockCoverageSelected()});
|
|
break;
|
|
default:
|
|
console.assert(false, `Unknown action: ${actionId}`);
|
|
}
|
|
}
|
|
}
|
|
let lineDecoratorInstance: LineDecorator;
|
|
export class LineDecorator implements SourceFrame.SourceFrame.LineDecorator {
|
|
static instance({forceNew}: {forceNew: boolean} = {forceNew: false}): LineDecorator {
|
|
if (!lineDecoratorInstance || forceNew) {
|
|
lineDecoratorInstance = new LineDecorator();
|
|
}
|
|
|
|
return lineDecoratorInstance;
|
|
}
|
|
|
|
_listeners:
|
|
WeakMap<SourceFrame.SourcesTextEditor.SourcesTextEditor, (arg0: Common.EventTarget.EventTargetEvent) => void>;
|
|
constructor() {
|
|
this._listeners = new WeakMap();
|
|
}
|
|
|
|
decorate(
|
|
uiSourceCode: Workspace.UISourceCode.UISourceCode,
|
|
textEditor: SourceFrame.SourcesTextEditor.SourcesTextEditor): void {
|
|
const decorations = uiSourceCode.decorationsForType(decoratorType);
|
|
if (!decorations || !decorations.size) {
|
|
this._uninstallGutter(textEditor);
|
|
return;
|
|
}
|
|
const decorationManager = decorations.values().next().value.data() as CoverageDecorationManager;
|
|
decorationManager.usageByLine(uiSourceCode).then(lineUsage => {
|
|
textEditor.operation(() => this._innerDecorate(uiSourceCode, textEditor, lineUsage));
|
|
});
|
|
}
|
|
|
|
_innerDecorate(
|
|
uiSourceCode: Workspace.UISourceCode.UISourceCode, textEditor: SourceFrame.SourcesTextEditor.SourcesTextEditor,
|
|
lineUsage: (boolean|undefined)[]): void {
|
|
const gutterType = LineDecorator.GUTTER_TYPE;
|
|
this._uninstallGutter(textEditor);
|
|
if (lineUsage.length) {
|
|
this._installGutter(textEditor, uiSourceCode.url());
|
|
}
|
|
for (let line = 0; line < lineUsage.length; ++line) {
|
|
// Do not decorate the line if we don't have data.
|
|
if (typeof lineUsage[line] !== 'boolean') {
|
|
continue;
|
|
}
|
|
const className = lineUsage[line] ? 'text-editor-coverage-used-marker' : 'text-editor-coverage-unused-marker';
|
|
const gutterElement = document.createElement('div');
|
|
gutterElement.classList.add(className);
|
|
textEditor.setGutterDecoration(line, gutterType, gutterElement);
|
|
}
|
|
}
|
|
|
|
makeGutterClickHandler(url: string): (arg0: Common.EventTarget.EventTargetEvent) => void {
|
|
function handleGutterClick(event: Common.EventTarget.EventTargetEvent): void {
|
|
const eventData = event.data as SourceFrame.SourcesTextEditor.GutterClickEventData;
|
|
if (eventData.gutterType !== LineDecorator.GUTTER_TYPE) {
|
|
return;
|
|
}
|
|
const coverageViewId = 'coverage';
|
|
UI.ViewManager.ViewManager.instance()
|
|
.showView(coverageViewId)
|
|
.then(() => {
|
|
const view = UI.ViewManager.ViewManager.instance().view(coverageViewId);
|
|
return view && view.widget();
|
|
})
|
|
.then(widget => {
|
|
const matchFormattedSuffix = url.match(/(.*):formatted$/);
|
|
const urlWithoutFormattedSuffix = (matchFormattedSuffix && matchFormattedSuffix[1]) || url;
|
|
(widget as CoverageView).selectCoverageItemByUrl(urlWithoutFormattedSuffix);
|
|
});
|
|
}
|
|
return handleGutterClick;
|
|
}
|
|
|
|
_installGutter(textEditor: SourceFrame.SourcesTextEditor.SourcesTextEditor, url: string): void {
|
|
let listener = this._listeners.get(textEditor);
|
|
if (!listener) {
|
|
listener = this.makeGutterClickHandler(url);
|
|
this._listeners.set(textEditor, listener);
|
|
}
|
|
textEditor.installGutter(LineDecorator.GUTTER_TYPE, false);
|
|
textEditor.addEventListener(SourceFrame.SourcesTextEditor.Events.GutterClick, listener, this);
|
|
}
|
|
|
|
_uninstallGutter(textEditor: SourceFrame.SourcesTextEditor.SourcesTextEditor): void {
|
|
textEditor.uninstallGutter(LineDecorator.GUTTER_TYPE);
|
|
const listener = this._listeners.get(textEditor);
|
|
if (listener) {
|
|
textEditor.removeEventListener(SourceFrame.SourcesTextEditor.Events.GutterClick, listener, this);
|
|
this._listeners.delete(textEditor);
|
|
}
|
|
}
|
|
|
|
static readonly GUTTER_TYPE = 'CodeMirror-gutter-coverage';
|
|
}
|
|
|
|
SourceFrame.SourceFrame.registerLineDecorator({
|
|
lineDecorator: LineDecorator.instance,
|
|
decoratorType: SourceFrame.SourceFrame.DecoratorType.COVERAGE,
|
|
});
|