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