Path: blob/main/src/vs/workbench/contrib/editTelemetry/browser/helpers/documentWithAnnotatedEdits.ts
5263 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*--------------------------------------------------------------------------------------------*/45import { AsyncReader, AsyncReaderEndOfStream } from '../../../../../base/common/async.js';6import { CachedFunction } from '../../../../../base/common/cache.js';7import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';8import { IObservableWithChange, ISettableObservable, observableValue, runOnChange } from '../../../../../base/common/observable.js';9import { AnnotatedStringEdit, IEditData, StringEdit } from '../../../../../editor/common/core/edits/stringEdit.js';10import { StringText } from '../../../../../editor/common/core/text/abstractText.js';11import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js';12import { TextModelEditSource } from '../../../../../editor/common/textModelEditSource.js';13import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';14import { IObservableDocument } from './observableWorkspace.js';15import { iterateObservableChanges, mapObservableDelta } from './utils.js';1617export interface IDocumentWithAnnotatedEdits<TEditData extends IEditData<TEditData> = EditKeySourceData> {18readonly value: IObservableWithChange<StringText, { edit: AnnotatedStringEdit<TEditData> }>;19waitForQueue(): Promise<void>;20}2122/**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*/26export class DocumentWithSourceAnnotatedEdits extends Disposable implements IDocumentWithAnnotatedEdits<EditSourceData> {27public readonly value: IObservableWithChange<StringText, { edit: AnnotatedStringEdit<EditSourceData> }>;2829constructor(private readonly _originalDoc: IObservableDocument) {30super();3132const v = this.value = observableValue(this, _originalDoc.value.get());3334this._register(runOnChange(this._originalDoc.value, (val, _prevVal, edits) => {35const eComposed = AnnotatedStringEdit.compose(edits.map(e => {36const editSourceData = new EditSourceData(e.reason);37return e.mapData(() => editSourceData);38}));3940v.set(val, undefined, { edit: eComposed });41}));42}4344public waitForQueue(): Promise<void> {45return Promise.resolve();46}47}4849/**50* Only joins touching edits if the source and the metadata is the same (e.g. requestUuids must be equal).51*/52export class EditSourceData implements IEditData<EditSourceData> {53public readonly source;54public readonly key;5556constructor(57public readonly editSource: TextModelEditSource58) {59this.key = this.editSource.toKey(1);60this.source = EditSourceBase.create(this.editSource);61}6263join(data: EditSourceData): EditSourceData | undefined {64if (this.editSource !== data.editSource) {65return undefined;66}67return this;68}6970toEditSourceData(): EditKeySourceData {71return new EditKeySourceData(this.key, this.source, this.editSource);72}73}7475export class EditKeySourceData implements IEditData<EditKeySourceData> {76constructor(77public readonly key: string,78public readonly source: EditSource,79public readonly representative: TextModelEditSource,80) { }8182join(data: EditKeySourceData): EditKeySourceData | undefined {83if (this.key !== data.key) {84return undefined;85}86if (this.source !== data.source) {87return undefined;88}89// The representatives could be different! (But equal modulo key)90return this;91}92}9394export abstract class EditSourceBase {95private static _cache = new CachedFunction({ getCacheKey: v => v.toString() }, (arg: EditSource) => arg);9697public static create(reason: TextModelEditSource): EditSource {98const data = reason.metadata;99switch (data.source) {100case 'reloadFromDisk':101return this._cache.get(new ExternalEditSource());102case 'inlineCompletionPartialAccept':103case 'inlineCompletionAccept': {104const type = 'type' in data ? data.type : undefined;105if ('$nes' in data && data.$nes) {106return this._cache.get(new InlineSuggestEditSource('nes', data.$extensionId ?? '', data.$providerId ?? '', type));107}108return this._cache.get(new InlineSuggestEditSource('completion', data.$extensionId ?? '', data.$providerId ?? '', type));109}110case 'snippet':111return this._cache.get(new IdeEditSource('suggest'));112case 'unknown':113if (!data.name) {114return this._cache.get(new UnknownEditSource());115}116switch (data.name) {117case 'formatEditsCommand':118return this._cache.get(new IdeEditSource('format'));119}120return this._cache.get(new UnknownEditSource());121122case 'Chat.applyEdits':123return this._cache.get(new ChatEditSource('sidebar'));124case 'inlineChat.applyEdits':125return this._cache.get(new ChatEditSource('inline'));126case 'cursor':127return this._cache.get(new UserEditSource());128default:129return this._cache.get(new UnknownEditSource());130}131}132133public abstract getColor(): string;134}135136export type EditSource = InlineSuggestEditSource | ChatEditSource | IdeEditSource | UserEditSource | UnknownEditSource | ExternalEditSource;137138export class InlineSuggestEditSource extends EditSourceBase {139public readonly category = 'ai';140public readonly feature = 'inlineSuggest';141constructor(142public readonly kind: 'completion' | 'nes',143public readonly extensionId: string,144public readonly providerId: string,145public readonly type: 'word' | 'line' | undefined,146) { super(); }147148override toString() { return `${this.category}/${this.feature}/${this.kind}/${this.extensionId}/${this.type}`; }149150public getColor(): string { return '#00ff0033'; }151}152153class ChatEditSource extends EditSourceBase {154public readonly category = 'ai';155public readonly feature = 'chat';156constructor(157public readonly kind: 'sidebar' | 'inline',158) { super(); }159160override toString() { return `${this.category}/${this.feature}/${this.kind}`; }161162public getColor(): string { return '#00ff0066'; }163}164165class IdeEditSource extends EditSourceBase {166public readonly category = 'ide';167constructor(168public readonly feature: 'suggest' | 'format' | string,169) { super(); }170171override toString() { return `${this.category}/${this.feature}`; }172173public getColor(): string { return this.feature === 'format' ? '#0000ff33' : '#80808033'; }174}175176class UserEditSource extends EditSourceBase {177public readonly category = 'user';178constructor() { super(); }179180override toString() { return this.category; }181182public getColor(): string { return '#d3d3d333'; }183}184185/** Caused by external tools that trigger a reload from disk */186class ExternalEditSource extends EditSourceBase {187public readonly category = 'external';188constructor() { super(); }189190override toString() { return this.category; }191192public getColor(): string { return '#009ab254'; }193}194195class UnknownEditSource extends EditSourceBase {196public readonly category = 'unknown';197constructor() { super(); }198199override toString() { return this.category; }200201public getColor(): string { return '#ff000033'; }202}203204export class CombineStreamedChanges<TEditData extends (EditKeySourceData | EditSourceData) & IEditData<TEditData>> extends Disposable implements IDocumentWithAnnotatedEdits<TEditData> {205private readonly _value: ISettableObservable<StringText, { edit: AnnotatedStringEdit<TEditData> }>;206readonly value: IObservableWithChange<StringText, { edit: AnnotatedStringEdit<TEditData> }>;207private readonly _runStore = this._register(new DisposableStore());208private _runQueue: Promise<void> = Promise.resolve();209210private readonly _diffService: DiffService;211212constructor(213private readonly _originalDoc: IDocumentWithAnnotatedEdits<TEditData>,214@IInstantiationService private readonly _instantiationService: IInstantiationService,215) {216super();217218this._diffService = this._instantiationService.createInstance(DiffService);219this.value = this._value = observableValue(this, _originalDoc.value.get());220this._restart();221222}223224async _restart(): Promise<void> {225this._runStore.clear();226const iterator = iterateObservableChanges(this._originalDoc.value, this._runStore)[Symbol.asyncIterator]();227const p = this._runQueue;228this._runQueue = this._runQueue.then(() => this._run(iterator));229await p;230}231232private async _run(iterator: AsyncIterator<{ value: StringText; prevValue: StringText; change: { edit: AnnotatedStringEdit<TEditData> }[] }, any, any>) {233const reader = new AsyncReader(iterator);234while (true) {235let peeked = await reader.peek();236if (peeked === AsyncReaderEndOfStream) {237return;238} else if (isChatEdit(peeked)) {239const first = peeked;240241let last = first;242let chatEdit = AnnotatedStringEdit.empty as AnnotatedStringEdit<TEditData>;243244do {245reader.readBufferedOrThrow();246last = peeked;247chatEdit = chatEdit.compose(AnnotatedStringEdit.compose(peeked.change.map(c => c.edit)));248const peekedOrUndefined = await reader.peekTimeout(1000);249if (!peekedOrUndefined) {250break;251}252peeked = peekedOrUndefined;253} while (peeked !== AsyncReaderEndOfStream && isChatEdit(peeked));254255if (!chatEdit.isEmpty()) {256const data = chatEdit.replacements[0].data;257const diffEdit = await this._diffService.computeDiff(first.prevValue.value, last.value.value);258const edit = diffEdit.mapData(_e => data);259this._value.set(last.value, undefined, { edit });260}261} else {262reader.readBufferedOrThrow();263const e = AnnotatedStringEdit.compose(peeked.change.map(c => c.edit));264this._value.set(peeked.value, undefined, { edit: e });265}266}267}268269async waitForQueue(): Promise<void> {270await this._originalDoc.waitForQueue();271await this._restart();272}273}274275export class DiffService {276constructor(277@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,278) {279}280281public async computeDiff(original: string, modified: string): Promise<StringEdit> {282const diffEdit = await this._editorWorkerService.computeStringEditFromDiff(original, modified, { maxComputationTimeMs: 500 }, 'advanced');283return diffEdit;284}285}286287function isChatEdit(next: { value: StringText; change: { edit: AnnotatedStringEdit<EditKeySourceData | EditSourceData> }[] }) {288return next.change.every(c => c.edit.replacements.every(e => {289if (e.data.source.category === 'ai' && e.data.source.feature === 'chat') {290return true;291}292return false;293}));294}295296export class MinimizeEditsProcessor<TEditData extends IEditData<TEditData>> extends Disposable implements IDocumentWithAnnotatedEdits<TEditData> {297readonly value: IObservableWithChange<StringText, { edit: AnnotatedStringEdit<TEditData> }>;298299constructor(300private readonly _originalDoc: IDocumentWithAnnotatedEdits<TEditData>,301) {302super();303304const v = this.value = observableValue(this, _originalDoc.value.get());305306let prevValue: string = this._originalDoc.value.get().value;307this._register(runOnChange(this._originalDoc.value, (val, _prevVal, edits) => {308const eComposed = AnnotatedStringEdit.compose(edits.map(e => e.edit));309310const e = eComposed.removeCommonSuffixAndPrefix(prevValue);311prevValue = val.value;312313v.set(val, undefined, { edit: e });314}));315}316317async waitForQueue(): Promise<void> {318await this._originalDoc.waitForQueue();319}320}321322/**323* 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).324*/325export function createDocWithJustReason(docWithAnnotatedEdits: IDocumentWithAnnotatedEdits<EditSourceData>, store: DisposableStore): IDocumentWithAnnotatedEdits<EditKeySourceData> {326const docWithJustReason: IDocumentWithAnnotatedEdits<EditKeySourceData> = {327value: mapObservableDelta(docWithAnnotatedEdits.value, edit => ({ edit: edit.edit.mapData(d => d.data.toEditSourceData()) }), store),328waitForQueue: () => docWithAnnotatedEdits.waitForQueue(),329};330return docWithJustReason;331}332333334335