| // 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, |
| }); |