mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-06 20:11:38 +08:00
Bug: 1299782 Change-Id: If35a3832e990affb4ef0a0918a290f58fcb0ca87 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3566003 Reviewed-by: Eric Leese <leese@chromium.org> Reviewed-by: Simon Zünd <szuend@chromium.org> Commit-Queue: Kateryna Prokopenko <kprokopenko@chromium.org>
233 lines
8.4 KiB
TypeScript
233 lines
8.4 KiB
TypeScript
// Copyright 2019 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.
|
|
|
|
import * as i18n from '../../core/i18n/i18n.js';
|
|
import type * as Platform from '../../core/platform/platform.js';
|
|
import * as SDK from '../../core/sdk/sdk.js';
|
|
import * as Formatter from '../../models/formatter/formatter.js';
|
|
import type * as Workspace from '../../models/workspace/workspace.js';
|
|
import * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.js';
|
|
import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js';
|
|
import * as UI from '../../ui/legacy/legacy.js';
|
|
import type * as TextEditor from '../../ui/components/text_editor/text_editor.js';
|
|
import * as Coverage from '../coverage/coverage.js';
|
|
|
|
import {Plugin} from './Plugin.js';
|
|
|
|
// Plugin that shows a gutter with coverage information when available.
|
|
|
|
const UIStrings = {
|
|
/**
|
|
*@description Text for Coverage Status Bar Item in Sources Panel
|
|
*/
|
|
clickToShowCoveragePanel: 'Click to show Coverage Panel',
|
|
/**
|
|
*@description Text for Coverage Status Bar Item in Sources Panel
|
|
*/
|
|
showDetails: 'Show Details',
|
|
/**
|
|
*@description Text to show in the status bar if coverage data is available
|
|
*@example {12.3} PH1
|
|
*/
|
|
coverageS: 'Coverage: {PH1}',
|
|
/**
|
|
*@description Text to be shown in the status bar if no coverage data is available
|
|
*/
|
|
coverageNa: 'Coverage: n/a',
|
|
};
|
|
const str_ = i18n.i18n.registerUIStrings('panels/sources/CoveragePlugin.ts', UIStrings);
|
|
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
|
|
|
export class CoveragePlugin extends Plugin {
|
|
private originalSourceCode: Workspace.UISourceCode.UISourceCode;
|
|
private infoInToolbar: UI.Toolbar.ToolbarButton;
|
|
private model: Coverage.CoverageModel.CoverageModel|null|undefined;
|
|
private coverage: Coverage.CoverageModel.URLCoverageInfo|null|undefined;
|
|
|
|
constructor(uiSourceCode: Workspace.UISourceCode.UISourceCode) {
|
|
super(uiSourceCode);
|
|
this.originalSourceCode =
|
|
Formatter.SourceFormatter.SourceFormatter.instance().getOriginalUISourceCode(this.uiSourceCode);
|
|
this.infoInToolbar = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.clickToShowCoveragePanel));
|
|
this.infoInToolbar.setSecondary();
|
|
this.infoInToolbar.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => {
|
|
void UI.ViewManager.ViewManager.instance().showView('coverage');
|
|
});
|
|
|
|
const mainTarget = SDK.TargetManager.TargetManager.instance().mainTarget();
|
|
if (mainTarget) {
|
|
this.model = mainTarget.model(Coverage.CoverageModel.CoverageModel);
|
|
if (this.model) {
|
|
this.model.addEventListener(Coverage.CoverageModel.Events.CoverageReset, this.handleReset, this);
|
|
|
|
this.coverage = this.model.getCoverageForUrl(this.originalSourceCode.url());
|
|
if (this.coverage) {
|
|
this.coverage.addEventListener(
|
|
Coverage.CoverageModel.URLCoverageInfo.Events.SizesChanged, this.handleCoverageSizesChanged, this);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.updateStats();
|
|
}
|
|
|
|
dispose(): void {
|
|
if (this.coverage) {
|
|
this.coverage.removeEventListener(
|
|
Coverage.CoverageModel.URLCoverageInfo.Events.SizesChanged, this.handleCoverageSizesChanged, this);
|
|
}
|
|
if (this.model) {
|
|
this.model.removeEventListener(Coverage.CoverageModel.Events.CoverageReset, this.handleReset, this);
|
|
}
|
|
}
|
|
|
|
static accepts(uiSourceCode: Workspace.UISourceCode.UISourceCode): boolean {
|
|
return uiSourceCode.contentType().isDocumentOrScriptOrStyleSheet();
|
|
}
|
|
|
|
private handleReset(): void {
|
|
this.coverage = null;
|
|
this.updateStats();
|
|
}
|
|
|
|
private handleCoverageSizesChanged(): void {
|
|
this.updateStats();
|
|
}
|
|
|
|
private updateStats(): void {
|
|
if (this.coverage) {
|
|
this.infoInToolbar.setTitle(i18nString(UIStrings.showDetails));
|
|
const formatter = new Intl.NumberFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale, {
|
|
style: 'percent',
|
|
maximumFractionDigits: 1,
|
|
});
|
|
this.infoInToolbar.setText(
|
|
i18nString(UIStrings.coverageS, {PH1: formatter.format(this.coverage.usedPercentage())}));
|
|
} else {
|
|
this.infoInToolbar.setTitle(i18nString(UIStrings.clickToShowCoveragePanel));
|
|
this.infoInToolbar.setText(i18nString(UIStrings.coverageNa));
|
|
}
|
|
}
|
|
|
|
async rightToolbarItems(): Promise<UI.Toolbar.ToolbarItem[]> {
|
|
return [this.infoInToolbar];
|
|
}
|
|
|
|
editorExtension(): CodeMirror.Extension {
|
|
return coverageCompartment.of([]);
|
|
}
|
|
|
|
private getCoverageManager(): Coverage.CoverageDecorationManager.CoverageDecorationManager|undefined {
|
|
return this.uiSourceCode.getDecorationData(SourceFrame.SourceFrame.DecoratorType.COVERAGE);
|
|
}
|
|
|
|
editorInitialized(editor: TextEditor.TextEditor.TextEditor): void {
|
|
if (this.getCoverageManager()) {
|
|
this.startDecoUpdate(editor);
|
|
}
|
|
}
|
|
|
|
decorationChanged(type: SourceFrame.SourceFrame.DecoratorType, editor: TextEditor.TextEditor.TextEditor): void {
|
|
if (type === SourceFrame.SourceFrame.DecoratorType.COVERAGE) {
|
|
this.startDecoUpdate(editor);
|
|
}
|
|
}
|
|
|
|
private startDecoUpdate(editor: TextEditor.TextEditor.TextEditor): void {
|
|
const manager = this.getCoverageManager();
|
|
void (manager ? manager.usageByLine(this.uiSourceCode) : Promise.resolve([])).then(usageByLine => {
|
|
const enabled = Boolean(editor.state.field(coverageState, false));
|
|
if (!usageByLine.length) {
|
|
if (enabled) {
|
|
editor.dispatch({effects: coverageCompartment.reconfigure([])});
|
|
}
|
|
} else if (!enabled) {
|
|
editor.dispatch({
|
|
effects: coverageCompartment.reconfigure([
|
|
coverageState.init(state => markersFromCoverageData(usageByLine, state)),
|
|
coverageGutter(this.uiSourceCode.url()),
|
|
theme,
|
|
]),
|
|
});
|
|
} else {
|
|
editor.dispatch({effects: setCoverageState.of(usageByLine)});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
const coveredMarker = new (class extends CodeMirror.GutterMarker {
|
|
elementClass = 'cm-coverageUsed';
|
|
})();
|
|
|
|
const notCoveredMarker = new (class extends CodeMirror.GutterMarker {
|
|
elementClass = 'cm-coverageUnused';
|
|
})();
|
|
|
|
function markersFromCoverageData(
|
|
usageByLine: (boolean|undefined)[], state: CodeMirror.EditorState): CodeMirror.RangeSet<CodeMirror.GutterMarker> {
|
|
const builder = new CodeMirror.RangeSetBuilder<CodeMirror.GutterMarker>();
|
|
for (let line = 0; line < usageByLine.length; line++) {
|
|
const usage = usageByLine[line];
|
|
if (usage !== undefined && line < state.doc.lines) {
|
|
const lineStart = state.doc.line(line + 1).from;
|
|
builder.add(lineStart, lineStart, usage ? coveredMarker : notCoveredMarker);
|
|
}
|
|
}
|
|
return builder.finish();
|
|
}
|
|
|
|
const setCoverageState = CodeMirror.StateEffect.define<(boolean | undefined)[]>();
|
|
|
|
const coverageState = CodeMirror.StateField.define<CodeMirror.RangeSet<CodeMirror.GutterMarker>>({
|
|
create(): CodeMirror.RangeSet<CodeMirror.GutterMarker> {
|
|
return CodeMirror.RangeSet.empty;
|
|
},
|
|
update(markers, tr) {
|
|
return tr.effects.reduce((markers, effect) => {
|
|
return effect.is(setCoverageState) ? markersFromCoverageData(effect.value, tr.state) : markers;
|
|
}, markers.map(tr.changes));
|
|
},
|
|
});
|
|
|
|
function coverageGutter(url: Platform.DevToolsPath.UrlString): CodeMirror.Extension {
|
|
return CodeMirror.gutter({
|
|
markers: (view): CodeMirror.RangeSet<CodeMirror.GutterMarker> => view.state.field(coverageState),
|
|
|
|
domEventHandlers: {
|
|
click() {
|
|
void UI.ViewManager.ViewManager.instance()
|
|
.showView('coverage')
|
|
.then(() => {
|
|
const view = UI.ViewManager.ViewManager.instance().view('coverage');
|
|
return view && view.widget();
|
|
})
|
|
.then(widget => {
|
|
const matchFormattedSuffix = url.match(/(.*):formatted$/);
|
|
const urlWithoutFormattedSuffix = (matchFormattedSuffix && matchFormattedSuffix[1]) || url;
|
|
(widget as Coverage.CoverageView.CoverageView).selectCoverageItemByUrl(urlWithoutFormattedSuffix);
|
|
});
|
|
return true;
|
|
},
|
|
},
|
|
|
|
class: 'cm-coverageGutter',
|
|
});
|
|
}
|
|
|
|
const coverageCompartment = new CodeMirror.Compartment();
|
|
|
|
const theme = CodeMirror.EditorView.baseTheme({
|
|
'.cm-coverageGutter': {
|
|
width: '5px',
|
|
marginLeft: '3px',
|
|
},
|
|
'.cm-coverageUnused': {
|
|
backgroundColor: 'var(--color-accent-red)',
|
|
},
|
|
'.cm-coverageUsed': {
|
|
backgroundColor: 'var(--color-coverage-used)',
|
|
},
|
|
});
|