Path: blob/main/src/vs/workbench/contrib/editTelemetry/browser/helpers/documentWithAnnotatedEdits.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*--------------------------------------------------------------------------------------------*/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 } 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 { IObservableDocument } from './observableWorkspace.js';14import { iterateObservableChanges, mapObservableDelta } from './utils.js';1516export interface IDocumentWithAnnotatedEdits<TEditData extends IEditData<TEditData> = EditKeySourceData> {17readonly value: IObservableWithChange<StringText, { edit: AnnotatedStringEdit<TEditData> }>;18waitForQueue(): Promise<void>;19}2021/**22* Creates a document that is a delayed copy of the original document,23* but with edits annotated with the source of the edit.24*/25export class DocumentWithSourceAnnotatedEdits extends Disposable implements IDocumentWithAnnotatedEdits<EditSourceData> {26public readonly value: IObservableWithChange<StringText, { edit: AnnotatedStringEdit<EditSourceData> }>;2728constructor(private readonly _originalDoc: IObservableDocument) {29super();3031const v = this.value = observableValue(this, _originalDoc.value.get());3233this._register(runOnChange(this._originalDoc.value, (val, _prevVal, edits) => {34const eComposed = AnnotatedStringEdit.compose(edits.map(e => {35const editSourceData = new EditSourceData(e.reason);36return e.mapData(() => editSourceData);37}));3839v.set(val, undefined, { edit: eComposed });40}));41}4243public waitForQueue(): Promise<void> {44return Promise.resolve();45}46}4748/**49* Only joins touching edits if the source and the metadata is the same (e.g. requestUuids must be equal).50*/51export class EditSourceData implements IEditData<EditSourceData> {52public readonly source;53public readonly key;5455constructor(56public readonly editSource: TextModelEditSource57) {58this.key = this.editSource.toKey(1);59this.source = EditSourceBase.create(this.editSource);60}6162join(data: EditSourceData): EditSourceData | undefined {63if (this.editSource !== data.editSource) {64return undefined;65}66return this;67}6869toEditSourceData(): EditKeySourceData {70return new EditKeySourceData(this.key, this.source, this.editSource);71}72}7374export class EditKeySourceData implements IEditData<EditKeySourceData> {75constructor(76public readonly key: string,77public readonly source: EditSource,78public readonly representative: TextModelEditSource,79) { }8081join(data: EditKeySourceData): EditKeySourceData | undefined {82if (this.key !== data.key) {83return undefined;84}85if (this.source !== data.source) {86return undefined;87}88// The representatives could be different! (But equal modulo key)89return this;90}91}9293export abstract class EditSourceBase {94private static _cache = new CachedFunction({ getCacheKey: v => v.toString() }, (arg: EditSource) => arg);9596public static create(reason: TextModelEditSource): EditSource {97const data = reason.metadata;98switch (data.source) {99case 'reloadFromDisk':100return this._cache.get(new ExternalEditSource());101case 'inlineCompletionPartialAccept':102case 'inlineCompletionAccept': {103const type = 'type' in data ? data.type : undefined;104if ('$nes' in data && data.$nes) {105return this._cache.get(new InlineSuggestEditSource('nes', data.$extensionId ?? '', data.$providerId ?? '', type));106}107return this._cache.get(new InlineSuggestEditSource('completion', data.$extensionId ?? '', data.$providerId ?? '', type));108}109case 'snippet':110return this._cache.get(new IdeEditSource('suggest'));111case 'unknown':112if (!data.name) {113return this._cache.get(new UnknownEditSource());114}115switch (data.name) {116case 'formatEditsCommand':117return this._cache.get(new IdeEditSource('format'));118}119return this._cache.get(new UnknownEditSource());120121case 'Chat.applyEdits':122return this._cache.get(new ChatEditSource('sidebar'));123case 'inlineChat.applyEdits':124return this._cache.get(new ChatEditSource('inline'));125case 'cursor':126return this._cache.get(new UserEditSource());127default:128return this._cache.get(new UnknownEditSource());129}130}131132public abstract getColor(): string;133}134135export type EditSource = InlineSuggestEditSource | ChatEditSource | IdeEditSource | UserEditSource | UnknownEditSource | ExternalEditSource;136137export class InlineSuggestEditSource extends EditSourceBase {138public readonly category = 'ai';139public readonly feature = 'inlineSuggest';140constructor(141public readonly kind: 'completion' | 'nes',142public readonly extensionId: string,143public readonly providerId: string,144public readonly type: 'word' | 'line' | undefined,145) { super(); }146147override toString() { return `${this.category}/${this.feature}/${this.kind}/${this.extensionId}/${this.type}`; }148149public getColor(): string { return '#00ff0033'; }150}151152class ChatEditSource extends EditSourceBase {153public readonly category = 'ai';154public readonly feature = 'chat';155constructor(156public readonly kind: 'sidebar' | 'inline',157) { super(); }158159override toString() { return `${this.category}/${this.feature}/${this.kind}`; }160161public getColor(): string { return '#00ff0066'; }162}163164class IdeEditSource extends EditSourceBase {165public readonly category = 'ide';166constructor(167public readonly feature: 'suggest' | 'format' | string,168) { super(); }169170override toString() { return `${this.category}/${this.feature}`; }171172public getColor(): string { return this.feature === 'format' ? '#0000ff33' : '#80808033'; }173}174175class UserEditSource extends EditSourceBase {176public readonly category = 'user';177constructor() { super(); }178179override toString() { return this.category; }180181public getColor(): string { return '#d3d3d333'; }182}183184/** Caused by external tools that trigger a reload from disk */185class ExternalEditSource extends EditSourceBase {186public readonly category = 'external';187constructor() { super(); }188189override toString() { return this.category; }190191public getColor(): string { return '#009ab254'; }192}193194class UnknownEditSource extends EditSourceBase {195public readonly category = 'unknown';196constructor() { super(); }197198override toString() { return this.category; }199200public getColor(): string { return '#ff000033'; }201}202203export class CombineStreamedChanges<TEditData extends (EditKeySourceData | EditSourceData) & IEditData<TEditData>> extends Disposable implements IDocumentWithAnnotatedEdits<TEditData> {204private readonly _value: ISettableObservable<StringText, { edit: AnnotatedStringEdit<TEditData> }>;205readonly value: IObservableWithChange<StringText, { edit: AnnotatedStringEdit<TEditData> }>;206private readonly _runStore = this._register(new DisposableStore());207private _runQueue: Promise<void> = Promise.resolve();208209constructor(210private readonly _originalDoc: IDocumentWithAnnotatedEdits<TEditData>,211@IEditorWorkerService private readonly _diffService: IEditorWorkerService,212) {213super();214215this.value = this._value = observableValue(this, _originalDoc.value.get());216this._restart();217218this._diffService.computeStringEditFromDiff('foo', 'last.value.value', { maxComputationTimeMs: 500 }, 'advanced');219}220221async _restart(): Promise<void> {222this._runStore.clear();223const iterator = iterateObservableChanges(this._originalDoc.value, this._runStore)[Symbol.asyncIterator]();224const p = this._runQueue;225this._runQueue = this._runQueue.then(() => this._run(iterator));226await p;227}228229private async _run(iterator: AsyncIterator<{ value: StringText; prevValue: StringText; change: { edit: AnnotatedStringEdit<TEditData> }[] }, any, any>) {230const reader = new AsyncReader(iterator);231while (true) {232let peeked = await reader.peek();233if (peeked === AsyncReaderEndOfStream) {234return;235} else if (isChatEdit(peeked)) {236const first = peeked;237238let last = first;239let chatEdit = AnnotatedStringEdit.empty as AnnotatedStringEdit<TEditData>;240241do {242reader.readBufferedOrThrow();243last = peeked;244chatEdit = chatEdit.compose(AnnotatedStringEdit.compose(peeked.change.map(c => c.edit)));245const peekedOrUndefined = await reader.peekTimeout(1000);246if (!peekedOrUndefined) {247break;248}249peeked = peekedOrUndefined;250} while (peeked !== AsyncReaderEndOfStream && isChatEdit(peeked));251252if (!chatEdit.isEmpty()) {253const data = chatEdit.replacements[0].data;254const diffEdit = await this._diffService.computeStringEditFromDiff(first.prevValue.value, last.value.value, { maxComputationTimeMs: 500 }, 'advanced');255const edit = diffEdit.mapData(_e => data);256this._value.set(last.value, undefined, { edit });257}258} else {259reader.readBufferedOrThrow();260const e = AnnotatedStringEdit.compose(peeked.change.map(c => c.edit));261this._value.set(peeked.value, undefined, { edit: e });262}263}264}265266async waitForQueue(): Promise<void> {267await this._originalDoc.waitForQueue();268await this._restart();269}270}271272function isChatEdit(next: { value: StringText; change: { edit: AnnotatedStringEdit<EditKeySourceData | EditSourceData> }[] }) {273return next.change.every(c => c.edit.replacements.every(e => {274if (e.data.source.category === 'ai' && e.data.source.feature === 'chat') {275return true;276}277return false;278}));279}280281export class MinimizeEditsProcessor<TEditData extends IEditData<TEditData>> extends Disposable implements IDocumentWithAnnotatedEdits<TEditData> {282readonly value: IObservableWithChange<StringText, { edit: AnnotatedStringEdit<TEditData> }>;283284constructor(285private readonly _originalDoc: IDocumentWithAnnotatedEdits<TEditData>,286) {287super();288289const v = this.value = observableValue(this, _originalDoc.value.get());290291let prevValue: string = this._originalDoc.value.get().value;292this._register(runOnChange(this._originalDoc.value, (val, _prevVal, edits) => {293const eComposed = AnnotatedStringEdit.compose(edits.map(e => e.edit));294295const e = eComposed.removeCommonSuffixAndPrefix(prevValue);296prevValue = val.value;297298v.set(val, undefined, { edit: e });299}));300}301302async waitForQueue(): Promise<void> {303await this._originalDoc.waitForQueue();304}305}306307/**308* 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).309*/310export function createDocWithJustReason(docWithAnnotatedEdits: IDocumentWithAnnotatedEdits<EditSourceData>, store: DisposableStore): IDocumentWithAnnotatedEdits<EditKeySourceData> {311const docWithJustReason: IDocumentWithAnnotatedEdits<EditKeySourceData> = {312value: mapObservableDelta(docWithAnnotatedEdits.value, edit => ({ edit: edit.edit.mapData(d => d.data.toEditSourceData()) }), store),313waitForQueue: () => docWithAnnotatedEdits.waitForQueue(),314};315return docWithJustReason;316}317318319320