blob: 9f841e0abf983f4db003469dfc87427f53a62219 [file] [log] [blame]
// 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 '../../source_frame/source_frame.js';
import * as UI from '../../ui/legacy/legacy.js';
import type * as Workspace from '../../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', {enableLegacyPatching: false});
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.SDKModel.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.SDKModel.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,
});