Path: blob/main/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingFeature.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/456import { CachedFunction } from '../../../../../base/common/cache.js';7import { MarkdownString } from '../../../../../base/common/htmlContent.js';8import { Disposable } from '../../../../../base/common/lifecycle.js';9import { autorun, mapObservableArrayCached, derived, IObservable, ISettableObservable, observableValue, derivedWithSetter, observableFromEvent } from '../../../../../base/common/observable.js';10import { DynamicCssRules } from '../../../../../editor/browser/editorDom.js';11import { observableCodeEditor } from '../../../../../editor/browser/observableCodeEditor.js';12import { CodeEditorWidget } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';13import { IModelDeltaDecoration } from '../../../../../editor/common/model.js';14import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js';15import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';16import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';17import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';18import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js';19import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';20import { IEditorService } from '../../../../services/editor/common/editorService.js';21import { IStatusbarService, StatusbarAlignment } from '../../../../services/statusbar/browser/statusbar.js';22import { EditSource } from '../helpers/documentWithAnnotatedEdits.js';23import { EditSourceTrackingImpl } from './editSourceTrackingImpl.js';24import { AnnotatedDocuments } from '../helpers/annotatedDocuments.js';25import { DataChannelForwardingTelemetryService } from './forwardingTelemetryService.js';26import { EDIT_TELEMETRY_DETAILS_SETTING_ID, EDIT_TELEMETRY_SHOW_DECORATIONS, EDIT_TELEMETRY_SHOW_STATUS_BAR } from '../settings.js';27import { VSCodeWorkspace } from '../helpers/vscodeObservableWorkspace.js';28import { IExtensionService } from '../../../../services/extensions/common/extensions.js';2930export class EditTrackingFeature extends Disposable {3132private readonly _editSourceTrackingShowDecorations;33private readonly _editSourceTrackingShowStatusBar;34private readonly _showStateInMarkdownDoc = 'editTelemetry.showDebugDetails';35private readonly _toggleDecorations = 'editTelemetry.toggleDebugDecorations';3637constructor(38private readonly _workspace: VSCodeWorkspace,39private readonly _annotatedDocuments: AnnotatedDocuments,40@IConfigurationService private readonly _configurationService: IConfigurationService,41@IInstantiationService private readonly _instantiationService: IInstantiationService,42@IStatusbarService private readonly _statusbarService: IStatusbarService,4344@IEditorService private readonly _editorService: IEditorService,45@IExtensionService private readonly _extensionService: IExtensionService,46) {47super();4849this._editSourceTrackingShowDecorations = makeSettable(observableConfigValue(EDIT_TELEMETRY_SHOW_DECORATIONS, false, this._configurationService));50this._editSourceTrackingShowStatusBar = observableConfigValue(EDIT_TELEMETRY_SHOW_STATUS_BAR, false, this._configurationService);51const editSourceDetailsEnabled = observableConfigValue(EDIT_TELEMETRY_DETAILS_SETTING_ID, false, this._configurationService);5253const extensions = observableFromEvent(this._extensionService.onDidChangeExtensions, () => {54return this._extensionService.extensions;55});56const extensionIds = derived(reader => new Set(extensions.read(reader).map(e => e.id?.toLowerCase())));57function getExtensionInfoObs(extensionId: string, extensionService: IExtensionService) {58const extIdLowerCase = extensionId.toLowerCase();59return derived(reader => extensionIds.read(reader).has(extIdLowerCase));60}6162const copilotInstalled = getExtensionInfoObs('GitHub.copilot', this._extensionService);63const copilotChatInstalled = getExtensionInfoObs('GitHub.copilot-chat', this._extensionService);6465const shouldSendDetails = derived(reader => editSourceDetailsEnabled.read(reader) || !!copilotInstalled.read(reader) || !!copilotChatInstalled.read(reader));6667const instantiationServiceWithInterceptedTelemetry = this._instantiationService.createChild(new ServiceCollection(68[ITelemetryService, this._instantiationService.createInstance(DataChannelForwardingTelemetryService)]69));70const impl = this._register(instantiationServiceWithInterceptedTelemetry.createInstance(EditSourceTrackingImpl, shouldSendDetails, this._annotatedDocuments));7172this._register(autorun((reader) => {73if (!this._editSourceTrackingShowDecorations.read(reader)) {74return;75}7677const visibleEditors = observableFromEvent(this, this._editorService.onDidVisibleEditorsChange, () => this._editorService.visibleTextEditorControls);7879mapObservableArrayCached(this, visibleEditors, (editor, store) => {80if (editor instanceof CodeEditorWidget) {81const obsEditor = observableCodeEditor(editor);8283const cssStyles = new DynamicCssRules(editor);84const decorations = new CachedFunction((source: EditSource) => {85const r = store.add(cssStyles.createClassNameRef({86backgroundColor: source.getColor(),87}));88return r.className;89});9091store.add(obsEditor.setDecorations(derived(reader => {92const uri = obsEditor.model.read(reader)?.uri;93if (!uri) { return []; }94const doc = this._workspace.getDocument(uri);95if (!doc) { return []; }96const docsState = impl.docsState.read(reader).get(doc);97if (!docsState) { return []; }9899const ranges = (docsState.longtermTracker.read(reader)?.getTrackedRanges(reader)) ?? [];100101return ranges.map<IModelDeltaDecoration>(r => ({102range: doc.value.get().getTransformer().getRange(r.range),103options: {104description: 'editSourceTracking',105inlineClassName: decorations.get(r.source),106}107}));108})));109}110}).recomputeInitiallyAndOnChange(reader.store);111}));112113this._register(autorun(reader => {114if (!this._editSourceTrackingShowStatusBar.read(reader)) {115return;116}117118const statusBarItem = reader.store.add(this._statusbarService.addEntry(119{120name: '',121text: '',122command: this._showStateInMarkdownDoc,123tooltip: 'Edit Source Tracking',124ariaLabel: '',125},126'editTelemetry',127StatusbarAlignment.RIGHT,128100129));130131const sumChangedCharacters = derived(reader => {132const docs = impl.docsState.read(reader);133let sum = 0;134for (const state of docs.values()) {135const t = state.longtermTracker.read(reader);136if (!t) { continue; }137const d = state.getTelemetryData(t.getTrackedRanges(reader));138sum += d.totalModifiedCharactersInFinalState;139}140return sum;141});142143const tooltipMarkdownString = derived(reader => {144const docs = impl.docsState.read(reader);145const docsDataInTooltip: string[] = [];146const editSources: EditSource[] = [];147for (const [doc, state] of docs) {148const tracker = state.longtermTracker.read(reader);149if (!tracker) {150continue;151}152const trackedRanges = tracker.getTrackedRanges(reader);153const data = state.getTelemetryData(trackedRanges);154if (data.totalModifiedCharactersInFinalState === 0) {155continue; // Don't include unmodified documents in tooltip156}157158editSources.push(...trackedRanges.map(r => r.source));159160// Filter out unmodified properties as these are not interesting to see in the hover161const filteredData = Object.fromEntries(162Object.entries(data).filter(([_, value]) => !(typeof value === 'number') || value !== 0)163);164165docsDataInTooltip.push([166`### ${doc.uri.fsPath}`,167'```json',168JSON.stringify(filteredData, undefined, '\t'),169'```',170'\n'171].join('\n'));172}173174let tooltipContent: string;175if (docsDataInTooltip.length === 0) {176tooltipContent = 'No modified documents';177} else if (docsDataInTooltip.length <= 3) {178tooltipContent = docsDataInTooltip.join('\n\n');179} else {180const lastThree = docsDataInTooltip.slice(-3);181tooltipContent = '...\n\n' + lastThree.join('\n\n');182}183184const agenda = this._createEditSourceAgenda(editSources);185186const tooltipWithCommand = new MarkdownString(tooltipContent + '\n\n[View Details](command:' + this._showStateInMarkdownDoc + ')');187tooltipWithCommand.appendMarkdown('\n\n' + agenda + '\n\nToggle decorations: [Click here](command:' + this._toggleDecorations + ')');188tooltipWithCommand.isTrusted = { enabledCommands: [this._toggleDecorations] };189tooltipWithCommand.supportHtml = true;190191return tooltipWithCommand;192});193194reader.store.add(autorun(reader => {195statusBarItem.update({196name: 'editTelemetry',197text: `$(edit) ${sumChangedCharacters.read(reader)} chars inserted`,198ariaLabel: `Edit Source Tracking: ${sumChangedCharacters.read(reader)} modified characters`,199tooltip: tooltipMarkdownString.read(reader),200command: this._showStateInMarkdownDoc,201});202}));203204reader.store.add(CommandsRegistry.registerCommand(this._toggleDecorations, () => {205this._editSourceTrackingShowDecorations.set(!this._editSourceTrackingShowDecorations.get(), undefined);206}));207}));208}209210private _createEditSourceAgenda(editSources: EditSource[]): string {211// Collect all edit sources from the tracked documents212const editSourcesSeen = new Set<string>();213const editSourceInfo = [];214for (const editSource of editSources) {215if (!editSourcesSeen.has(editSource.toString())) {216editSourcesSeen.add(editSource.toString());217editSourceInfo.push({ name: editSource.toString(), color: editSource.getColor() });218}219}220221const agendaItems = editSourceInfo.map(info =>222`<span style="background-color:${info.color};border-radius:3px;">${info.name}</span>`223);224225return agendaItems.join(' ');226}227}228229export function makeSettable<T>(obs: IObservable<T>): ISettableObservable<T> {230const overrideObs = observableValue<T | undefined>('overrideObs', undefined);231return derivedWithSetter(overrideObs, (reader) => {232return overrideObs.read(reader) ?? obs.read(reader);233}, (value, tx) => {234overrideObs.set(value, tx);235});236}237238239