Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingFeature.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
7
import { CachedFunction } from '../../../../../base/common/cache.js';
8
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
9
import { Disposable } from '../../../../../base/common/lifecycle.js';
10
import { autorun, mapObservableArrayCached, derived, IObservable, ISettableObservable, observableValue, derivedWithSetter, observableFromEvent } from '../../../../../base/common/observable.js';
11
import { DynamicCssRules } from '../../../../../editor/browser/editorDom.js';
12
import { observableCodeEditor } from '../../../../../editor/browser/observableCodeEditor.js';
13
import { CodeEditorWidget } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';
14
import { IModelDeltaDecoration } from '../../../../../editor/common/model.js';
15
import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js';
16
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
17
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
18
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
19
import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js';
20
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
21
import { IEditorService } from '../../../../services/editor/common/editorService.js';
22
import { IStatusbarService, StatusbarAlignment } from '../../../../services/statusbar/browser/statusbar.js';
23
import { EditSource } from '../helpers/documentWithAnnotatedEdits.js';
24
import { EditSourceTrackingImpl } from './editSourceTrackingImpl.js';
25
import { AnnotatedDocuments } from '../helpers/annotatedDocuments.js';
26
import { DataChannelForwardingTelemetryService } from './forwardingTelemetryService.js';
27
import { EDIT_TELEMETRY_DETAILS_SETTING_ID, EDIT_TELEMETRY_SHOW_DECORATIONS, EDIT_TELEMETRY_SHOW_STATUS_BAR } from '../settings.js';
28
import { VSCodeWorkspace } from '../helpers/vscodeObservableWorkspace.js';
29
import { IExtensionService } from '../../../../services/extensions/common/extensions.js';
30
31
export class EditTrackingFeature extends Disposable {
32
33
private readonly _editSourceTrackingShowDecorations;
34
private readonly _editSourceTrackingShowStatusBar;
35
private readonly _showStateInMarkdownDoc = 'editTelemetry.showDebugDetails';
36
private readonly _toggleDecorations = 'editTelemetry.toggleDebugDecorations';
37
38
constructor(
39
private readonly _workspace: VSCodeWorkspace,
40
private readonly _annotatedDocuments: AnnotatedDocuments,
41
@IConfigurationService private readonly _configurationService: IConfigurationService,
42
@IInstantiationService private readonly _instantiationService: IInstantiationService,
43
@IStatusbarService private readonly _statusbarService: IStatusbarService,
44
45
@IEditorService private readonly _editorService: IEditorService,
46
@IExtensionService private readonly _extensionService: IExtensionService,
47
) {
48
super();
49
50
this._editSourceTrackingShowDecorations = makeSettable(observableConfigValue(EDIT_TELEMETRY_SHOW_DECORATIONS, false, this._configurationService));
51
this._editSourceTrackingShowStatusBar = observableConfigValue(EDIT_TELEMETRY_SHOW_STATUS_BAR, false, this._configurationService);
52
const editSourceDetailsEnabled = observableConfigValue(EDIT_TELEMETRY_DETAILS_SETTING_ID, false, this._configurationService);
53
54
const extensions = observableFromEvent(this._extensionService.onDidChangeExtensions, () => {
55
return this._extensionService.extensions;
56
});
57
const extensionIds = derived(reader => new Set(extensions.read(reader).map(e => e.id?.toLowerCase())));
58
function getExtensionInfoObs(extensionId: string, extensionService: IExtensionService) {
59
const extIdLowerCase = extensionId.toLowerCase();
60
return derived(reader => extensionIds.read(reader).has(extIdLowerCase));
61
}
62
63
const copilotInstalled = getExtensionInfoObs('GitHub.copilot', this._extensionService);
64
const copilotChatInstalled = getExtensionInfoObs('GitHub.copilot-chat', this._extensionService);
65
66
const shouldSendDetails = derived(reader => editSourceDetailsEnabled.read(reader) || !!copilotInstalled.read(reader) || !!copilotChatInstalled.read(reader));
67
68
const instantiationServiceWithInterceptedTelemetry = this._instantiationService.createChild(new ServiceCollection(
69
[ITelemetryService, this._instantiationService.createInstance(DataChannelForwardingTelemetryService)]
70
));
71
const impl = this._register(instantiationServiceWithInterceptedTelemetry.createInstance(EditSourceTrackingImpl, shouldSendDetails, this._annotatedDocuments));
72
73
this._register(autorun((reader) => {
74
if (!this._editSourceTrackingShowDecorations.read(reader)) {
75
return;
76
}
77
78
const visibleEditors = observableFromEvent(this, this._editorService.onDidVisibleEditorsChange, () => this._editorService.visibleTextEditorControls);
79
80
mapObservableArrayCached(this, visibleEditors, (editor, store) => {
81
if (editor instanceof CodeEditorWidget) {
82
const obsEditor = observableCodeEditor(editor);
83
84
const cssStyles = new DynamicCssRules(editor);
85
const decorations = new CachedFunction((source: EditSource) => {
86
const r = store.add(cssStyles.createClassNameRef({
87
backgroundColor: source.getColor(),
88
}));
89
return r.className;
90
});
91
92
store.add(obsEditor.setDecorations(derived(reader => {
93
const uri = obsEditor.model.read(reader)?.uri;
94
if (!uri) { return []; }
95
const doc = this._workspace.getDocument(uri);
96
if (!doc) { return []; }
97
const docsState = impl.docsState.read(reader).get(doc);
98
if (!docsState) { return []; }
99
100
const ranges = (docsState.longtermTracker.read(reader)?.getTrackedRanges(reader)) ?? [];
101
102
return ranges.map<IModelDeltaDecoration>(r => ({
103
range: doc.value.get().getTransformer().getRange(r.range),
104
options: {
105
description: 'editSourceTracking',
106
inlineClassName: decorations.get(r.source),
107
}
108
}));
109
})));
110
}
111
}).recomputeInitiallyAndOnChange(reader.store);
112
}));
113
114
this._register(autorun(reader => {
115
if (!this._editSourceTrackingShowStatusBar.read(reader)) {
116
return;
117
}
118
119
const statusBarItem = reader.store.add(this._statusbarService.addEntry(
120
{
121
name: '',
122
text: '',
123
command: this._showStateInMarkdownDoc,
124
tooltip: 'Edit Source Tracking',
125
ariaLabel: '',
126
},
127
'editTelemetry',
128
StatusbarAlignment.RIGHT,
129
100
130
));
131
132
const sumChangedCharacters = derived(reader => {
133
const docs = impl.docsState.read(reader);
134
let sum = 0;
135
for (const state of docs.values()) {
136
const t = state.longtermTracker.read(reader);
137
if (!t) { continue; }
138
const d = state.getTelemetryData(t.getTrackedRanges(reader));
139
sum += d.totalModifiedCharactersInFinalState;
140
}
141
return sum;
142
});
143
144
const tooltipMarkdownString = derived(reader => {
145
const docs = impl.docsState.read(reader);
146
const docsDataInTooltip: string[] = [];
147
const editSources: EditSource[] = [];
148
for (const [doc, state] of docs) {
149
const tracker = state.longtermTracker.read(reader);
150
if (!tracker) {
151
continue;
152
}
153
const trackedRanges = tracker.getTrackedRanges(reader);
154
const data = state.getTelemetryData(trackedRanges);
155
if (data.totalModifiedCharactersInFinalState === 0) {
156
continue; // Don't include unmodified documents in tooltip
157
}
158
159
editSources.push(...trackedRanges.map(r => r.source));
160
161
// Filter out unmodified properties as these are not interesting to see in the hover
162
const filteredData = Object.fromEntries(
163
Object.entries(data).filter(([_, value]) => !(typeof value === 'number') || value !== 0)
164
);
165
166
docsDataInTooltip.push([
167
`### ${doc.uri.fsPath}`,
168
'```json',
169
JSON.stringify(filteredData, undefined, '\t'),
170
'```',
171
'\n'
172
].join('\n'));
173
}
174
175
let tooltipContent: string;
176
if (docsDataInTooltip.length === 0) {
177
tooltipContent = 'No modified documents';
178
} else if (docsDataInTooltip.length <= 3) {
179
tooltipContent = docsDataInTooltip.join('\n\n');
180
} else {
181
const lastThree = docsDataInTooltip.slice(-3);
182
tooltipContent = '...\n\n' + lastThree.join('\n\n');
183
}
184
185
const agenda = this._createEditSourceAgenda(editSources);
186
187
const tooltipWithCommand = new MarkdownString(tooltipContent + '\n\n[View Details](command:' + this._showStateInMarkdownDoc + ')');
188
tooltipWithCommand.appendMarkdown('\n\n' + agenda + '\n\nToggle decorations: [Click here](command:' + this._toggleDecorations + ')');
189
tooltipWithCommand.isTrusted = { enabledCommands: [this._toggleDecorations] };
190
tooltipWithCommand.supportHtml = true;
191
192
return tooltipWithCommand;
193
});
194
195
reader.store.add(autorun(reader => {
196
statusBarItem.update({
197
name: 'editTelemetry',
198
text: `$(edit) ${sumChangedCharacters.read(reader)} chars inserted`,
199
ariaLabel: `Edit Source Tracking: ${sumChangedCharacters.read(reader)} modified characters`,
200
tooltip: tooltipMarkdownString.read(reader),
201
command: this._showStateInMarkdownDoc,
202
});
203
}));
204
205
reader.store.add(CommandsRegistry.registerCommand(this._toggleDecorations, () => {
206
this._editSourceTrackingShowDecorations.set(!this._editSourceTrackingShowDecorations.get(), undefined);
207
}));
208
}));
209
}
210
211
private _createEditSourceAgenda(editSources: EditSource[]): string {
212
// Collect all edit sources from the tracked documents
213
const editSourcesSeen = new Set<string>();
214
const editSourceInfo = [];
215
for (const editSource of editSources) {
216
if (!editSourcesSeen.has(editSource.toString())) {
217
editSourcesSeen.add(editSource.toString());
218
editSourceInfo.push({ name: editSource.toString(), color: editSource.getColor() });
219
}
220
}
221
222
const agendaItems = editSourceInfo.map(info =>
223
`<span style="background-color:${info.color};border-radius:3px;">${info.name}</span>`
224
);
225
226
return agendaItems.join(' ');
227
}
228
}
229
230
export function makeSettable<T>(obs: IObservable<T>): ISettableObservable<T> {
231
const overrideObs = observableValue<T | undefined>('overrideObs', undefined);
232
return derivedWithSetter(overrideObs, (reader) => {
233
return overrideObs.read(reader) ?? obs.read(reader);
234
}, (value, tx) => {
235
overrideObs.set(value, tx);
236
});
237
}
238
239