Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/editTelemetry/browser/helpers/documentWithAnnotatedEdits.ts
5263 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
import { AsyncReader, AsyncReaderEndOfStream } from '../../../../../base/common/async.js';
7
import { CachedFunction } from '../../../../../base/common/cache.js';
8
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
9
import { IObservableWithChange, ISettableObservable, observableValue, runOnChange } from '../../../../../base/common/observable.js';
10
import { AnnotatedStringEdit, IEditData, StringEdit } from '../../../../../editor/common/core/edits/stringEdit.js';
11
import { StringText } from '../../../../../editor/common/core/text/abstractText.js';
12
import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js';
13
import { TextModelEditSource } from '../../../../../editor/common/textModelEditSource.js';
14
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
15
import { IObservableDocument } from './observableWorkspace.js';
16
import { iterateObservableChanges, mapObservableDelta } from './utils.js';
17
18
export interface IDocumentWithAnnotatedEdits<TEditData extends IEditData<TEditData> = EditKeySourceData> {
19
readonly value: IObservableWithChange<StringText, { edit: AnnotatedStringEdit<TEditData> }>;
20
waitForQueue(): Promise<void>;
21
}
22
23
/**
24
* Creates a document that is a delayed copy of the original document,
25
* but with edits annotated with the source of the edit.
26
*/
27
export class DocumentWithSourceAnnotatedEdits extends Disposable implements IDocumentWithAnnotatedEdits<EditSourceData> {
28
public readonly value: IObservableWithChange<StringText, { edit: AnnotatedStringEdit<EditSourceData> }>;
29
30
constructor(private readonly _originalDoc: IObservableDocument) {
31
super();
32
33
const v = this.value = observableValue(this, _originalDoc.value.get());
34
35
this._register(runOnChange(this._originalDoc.value, (val, _prevVal, edits) => {
36
const eComposed = AnnotatedStringEdit.compose(edits.map(e => {
37
const editSourceData = new EditSourceData(e.reason);
38
return e.mapData(() => editSourceData);
39
}));
40
41
v.set(val, undefined, { edit: eComposed });
42
}));
43
}
44
45
public waitForQueue(): Promise<void> {
46
return Promise.resolve();
47
}
48
}
49
50
/**
51
* Only joins touching edits if the source and the metadata is the same (e.g. requestUuids must be equal).
52
*/
53
export class EditSourceData implements IEditData<EditSourceData> {
54
public readonly source;
55
public readonly key;
56
57
constructor(
58
public readonly editSource: TextModelEditSource
59
) {
60
this.key = this.editSource.toKey(1);
61
this.source = EditSourceBase.create(this.editSource);
62
}
63
64
join(data: EditSourceData): EditSourceData | undefined {
65
if (this.editSource !== data.editSource) {
66
return undefined;
67
}
68
return this;
69
}
70
71
toEditSourceData(): EditKeySourceData {
72
return new EditKeySourceData(this.key, this.source, this.editSource);
73
}
74
}
75
76
export class EditKeySourceData implements IEditData<EditKeySourceData> {
77
constructor(
78
public readonly key: string,
79
public readonly source: EditSource,
80
public readonly representative: TextModelEditSource,
81
) { }
82
83
join(data: EditKeySourceData): EditKeySourceData | undefined {
84
if (this.key !== data.key) {
85
return undefined;
86
}
87
if (this.source !== data.source) {
88
return undefined;
89
}
90
// The representatives could be different! (But equal modulo key)
91
return this;
92
}
93
}
94
95
export abstract class EditSourceBase {
96
private static _cache = new CachedFunction({ getCacheKey: v => v.toString() }, (arg: EditSource) => arg);
97
98
public static create(reason: TextModelEditSource): EditSource {
99
const data = reason.metadata;
100
switch (data.source) {
101
case 'reloadFromDisk':
102
return this._cache.get(new ExternalEditSource());
103
case 'inlineCompletionPartialAccept':
104
case 'inlineCompletionAccept': {
105
const type = 'type' in data ? data.type : undefined;
106
if ('$nes' in data && data.$nes) {
107
return this._cache.get(new InlineSuggestEditSource('nes', data.$extensionId ?? '', data.$providerId ?? '', type));
108
}
109
return this._cache.get(new InlineSuggestEditSource('completion', data.$extensionId ?? '', data.$providerId ?? '', type));
110
}
111
case 'snippet':
112
return this._cache.get(new IdeEditSource('suggest'));
113
case 'unknown':
114
if (!data.name) {
115
return this._cache.get(new UnknownEditSource());
116
}
117
switch (data.name) {
118
case 'formatEditsCommand':
119
return this._cache.get(new IdeEditSource('format'));
120
}
121
return this._cache.get(new UnknownEditSource());
122
123
case 'Chat.applyEdits':
124
return this._cache.get(new ChatEditSource('sidebar'));
125
case 'inlineChat.applyEdits':
126
return this._cache.get(new ChatEditSource('inline'));
127
case 'cursor':
128
return this._cache.get(new UserEditSource());
129
default:
130
return this._cache.get(new UnknownEditSource());
131
}
132
}
133
134
public abstract getColor(): string;
135
}
136
137
export type EditSource = InlineSuggestEditSource | ChatEditSource | IdeEditSource | UserEditSource | UnknownEditSource | ExternalEditSource;
138
139
export class InlineSuggestEditSource extends EditSourceBase {
140
public readonly category = 'ai';
141
public readonly feature = 'inlineSuggest';
142
constructor(
143
public readonly kind: 'completion' | 'nes',
144
public readonly extensionId: string,
145
public readonly providerId: string,
146
public readonly type: 'word' | 'line' | undefined,
147
) { super(); }
148
149
override toString() { return `${this.category}/${this.feature}/${this.kind}/${this.extensionId}/${this.type}`; }
150
151
public getColor(): string { return '#00ff0033'; }
152
}
153
154
class ChatEditSource extends EditSourceBase {
155
public readonly category = 'ai';
156
public readonly feature = 'chat';
157
constructor(
158
public readonly kind: 'sidebar' | 'inline',
159
) { super(); }
160
161
override toString() { return `${this.category}/${this.feature}/${this.kind}`; }
162
163
public getColor(): string { return '#00ff0066'; }
164
}
165
166
class IdeEditSource extends EditSourceBase {
167
public readonly category = 'ide';
168
constructor(
169
public readonly feature: 'suggest' | 'format' | string,
170
) { super(); }
171
172
override toString() { return `${this.category}/${this.feature}`; }
173
174
public getColor(): string { return this.feature === 'format' ? '#0000ff33' : '#80808033'; }
175
}
176
177
class UserEditSource extends EditSourceBase {
178
public readonly category = 'user';
179
constructor() { super(); }
180
181
override toString() { return this.category; }
182
183
public getColor(): string { return '#d3d3d333'; }
184
}
185
186
/** Caused by external tools that trigger a reload from disk */
187
class ExternalEditSource extends EditSourceBase {
188
public readonly category = 'external';
189
constructor() { super(); }
190
191
override toString() { return this.category; }
192
193
public getColor(): string { return '#009ab254'; }
194
}
195
196
class UnknownEditSource extends EditSourceBase {
197
public readonly category = 'unknown';
198
constructor() { super(); }
199
200
override toString() { return this.category; }
201
202
public getColor(): string { return '#ff000033'; }
203
}
204
205
export class CombineStreamedChanges<TEditData extends (EditKeySourceData | EditSourceData) & IEditData<TEditData>> extends Disposable implements IDocumentWithAnnotatedEdits<TEditData> {
206
private readonly _value: ISettableObservable<StringText, { edit: AnnotatedStringEdit<TEditData> }>;
207
readonly value: IObservableWithChange<StringText, { edit: AnnotatedStringEdit<TEditData> }>;
208
private readonly _runStore = this._register(new DisposableStore());
209
private _runQueue: Promise<void> = Promise.resolve();
210
211
private readonly _diffService: DiffService;
212
213
constructor(
214
private readonly _originalDoc: IDocumentWithAnnotatedEdits<TEditData>,
215
@IInstantiationService private readonly _instantiationService: IInstantiationService,
216
) {
217
super();
218
219
this._diffService = this._instantiationService.createInstance(DiffService);
220
this.value = this._value = observableValue(this, _originalDoc.value.get());
221
this._restart();
222
223
}
224
225
async _restart(): Promise<void> {
226
this._runStore.clear();
227
const iterator = iterateObservableChanges(this._originalDoc.value, this._runStore)[Symbol.asyncIterator]();
228
const p = this._runQueue;
229
this._runQueue = this._runQueue.then(() => this._run(iterator));
230
await p;
231
}
232
233
private async _run(iterator: AsyncIterator<{ value: StringText; prevValue: StringText; change: { edit: AnnotatedStringEdit<TEditData> }[] }, any, any>) {
234
const reader = new AsyncReader(iterator);
235
while (true) {
236
let peeked = await reader.peek();
237
if (peeked === AsyncReaderEndOfStream) {
238
return;
239
} else if (isChatEdit(peeked)) {
240
const first = peeked;
241
242
let last = first;
243
let chatEdit = AnnotatedStringEdit.empty as AnnotatedStringEdit<TEditData>;
244
245
do {
246
reader.readBufferedOrThrow();
247
last = peeked;
248
chatEdit = chatEdit.compose(AnnotatedStringEdit.compose(peeked.change.map(c => c.edit)));
249
const peekedOrUndefined = await reader.peekTimeout(1000);
250
if (!peekedOrUndefined) {
251
break;
252
}
253
peeked = peekedOrUndefined;
254
} while (peeked !== AsyncReaderEndOfStream && isChatEdit(peeked));
255
256
if (!chatEdit.isEmpty()) {
257
const data = chatEdit.replacements[0].data;
258
const diffEdit = await this._diffService.computeDiff(first.prevValue.value, last.value.value);
259
const edit = diffEdit.mapData(_e => data);
260
this._value.set(last.value, undefined, { edit });
261
}
262
} else {
263
reader.readBufferedOrThrow();
264
const e = AnnotatedStringEdit.compose(peeked.change.map(c => c.edit));
265
this._value.set(peeked.value, undefined, { edit: e });
266
}
267
}
268
}
269
270
async waitForQueue(): Promise<void> {
271
await this._originalDoc.waitForQueue();
272
await this._restart();
273
}
274
}
275
276
export class DiffService {
277
constructor(
278
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
279
) {
280
}
281
282
public async computeDiff(original: string, modified: string): Promise<StringEdit> {
283
const diffEdit = await this._editorWorkerService.computeStringEditFromDiff(original, modified, { maxComputationTimeMs: 500 }, 'advanced');
284
return diffEdit;
285
}
286
}
287
288
function isChatEdit(next: { value: StringText; change: { edit: AnnotatedStringEdit<EditKeySourceData | EditSourceData> }[] }) {
289
return next.change.every(c => c.edit.replacements.every(e => {
290
if (e.data.source.category === 'ai' && e.data.source.feature === 'chat') {
291
return true;
292
}
293
return false;
294
}));
295
}
296
297
export class MinimizeEditsProcessor<TEditData extends IEditData<TEditData>> extends Disposable implements IDocumentWithAnnotatedEdits<TEditData> {
298
readonly value: IObservableWithChange<StringText, { edit: AnnotatedStringEdit<TEditData> }>;
299
300
constructor(
301
private readonly _originalDoc: IDocumentWithAnnotatedEdits<TEditData>,
302
) {
303
super();
304
305
const v = this.value = observableValue(this, _originalDoc.value.get());
306
307
let prevValue: string = this._originalDoc.value.get().value;
308
this._register(runOnChange(this._originalDoc.value, (val, _prevVal, edits) => {
309
const eComposed = AnnotatedStringEdit.compose(edits.map(e => e.edit));
310
311
const e = eComposed.removeCommonSuffixAndPrefix(prevValue);
312
prevValue = val.value;
313
314
v.set(val, undefined, { edit: e });
315
}));
316
}
317
318
async waitForQueue(): Promise<void> {
319
await this._originalDoc.waitForQueue();
320
}
321
}
322
323
/**
324
* Removing the metadata allows touching edits from the same source to merged, even if they were caused by different actions (e.g. two user edits).
325
*/
326
export function createDocWithJustReason(docWithAnnotatedEdits: IDocumentWithAnnotatedEdits<EditSourceData>, store: DisposableStore): IDocumentWithAnnotatedEdits<EditKeySourceData> {
327
const docWithJustReason: IDocumentWithAnnotatedEdits<EditKeySourceData> = {
328
value: mapObservableDelta(docWithAnnotatedEdits.value, edit => ({ edit: edit.edit.mapData(d => d.data.toEditSourceData()) }), store),
329
waitForQueue: () => docWithAnnotatedEdits.waitForQueue(),
330
};
331
return docWithJustReason;
332
}
333
334
335