Path: blob/main/src/vs/editor/common/textModelEditSource.ts
5240 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 { sumBy } from '../../base/common/arrays.js';6import { prefixedUuid } from '../../base/common/uuid.js';7import { LineEdit } from './core/edits/lineEdit.js';8import { BaseStringEdit } from './core/edits/stringEdit.js';9import { StringText } from './core/text/abstractText.js';10import { TextLength } from './core/text/textLength.js';11import { ProviderId, VersionedExtensionId } from './languages.js';1213const privateSymbol = Symbol('TextModelEditSource');1415export class TextModelEditSource {16constructor(17public readonly metadata: ITextModelEditSourceMetadata,18_privateCtorGuard: typeof privateSymbol,19) { }2021public toString(): string {22return `${this.metadata.source}`;23}2425public getType(): string {26const metadata = this.metadata;27switch (metadata.source) {28case 'cursor':29return metadata.kind;30case 'inlineCompletionAccept':31return metadata.source + (metadata.$nes ? ':nes' : '');32case 'unknown':33return metadata.name || 'unknown';34default:35return metadata.source;36}37}3839/**40* Converts the metadata to a key string.41* Only includes properties/values that have `level` many `$` prefixes or less.42*/43public toKey(level: number, filter: { [TKey in ITextModelEditSourceMetadataKeys]?: boolean } = {}): string {44const metadata = this.metadata;45const keys = Object.entries(metadata).filter(([key, value]) => {46const filterVal = (filter as Record<string, boolean>)[key];47if (filterVal !== undefined) {48return filterVal;49}5051const prefixCount = (key.match(/\$/g) || []).length;52return prefixCount <= level && value !== undefined && value !== null && value !== '';53}).map(([key, value]) => `${key}:${value}`);54return keys.join('-');55}5657public get props(): Record<ITextModelEditSourceMetadataKeys, string | undefined> {58// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any59return this.metadata as any;60}61}6263type TextModelEditSourceT<T> = TextModelEditSource & {64metadataT: T;65};6667// eslint-disable-next-line @typescript-eslint/no-explicit-any68function createEditSource<T extends Record<string, any>>(metadata: T): TextModelEditSourceT<T> {69// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any70return new TextModelEditSource(metadata as any, privateSymbol) as any;71}7273export function isAiEdit(source: TextModelEditSource): boolean {74switch (source.metadata.source) {75case 'inlineCompletionAccept':76case 'inlineCompletionPartialAccept':77case 'inlineChat.applyEdits':78case 'Chat.applyEdits':79return true;80}81return false;82}8384export function isUserEdit(source: TextModelEditSource): boolean {85switch (source.metadata.source) {86case 'cursor':87return source.metadata.kind === 'type';88}89return false;90}9192export const EditSources = {93unknown(data: { name?: string | null }) {94return createEditSource({95source: 'unknown',96name: data.name,97} as const);98},99100rename: (oldName: string | undefined, newName: string) => createEditSource({ source: 'rename', $$$oldName: oldName, $$$newName: newName } as const),101102chatApplyEdits(data: {103modelId: string | undefined;104sessionId: string | undefined;105requestId: string | undefined;106languageId: string;107mode: string | undefined;108extensionId: VersionedExtensionId | undefined;109codeBlockSuggestionId: EditSuggestionId | undefined;110}) {111return createEditSource({112source: 'Chat.applyEdits',113$modelId: avoidPathRedaction(data.modelId),114$extensionId: data.extensionId?.extensionId,115$extensionVersion: data.extensionId?.version,116$$languageId: data.languageId,117$$sessionId: data.sessionId,118$$requestId: data.requestId,119$$mode: data.mode,120$$codeBlockSuggestionId: data.codeBlockSuggestionId,121} as const);122},123124chatUndoEdits: () => createEditSource({ source: 'Chat.undoEdits' } as const),125chatReset: () => createEditSource({ source: 'Chat.reset' } as const),126127inlineCompletionAccept(data: { nes: boolean; requestUuid: string; languageId: string; providerId?: ProviderId; correlationId: string | undefined }) {128return createEditSource({129source: 'inlineCompletionAccept',130$nes: data.nes,131...toProperties(data.providerId),132$$correlationId: data.correlationId,133$$requestUuid: data.requestUuid,134$$languageId: data.languageId,135} as const);136},137138inlineCompletionPartialAccept(data: { nes: boolean; requestUuid: string; languageId: string; providerId?: ProviderId; correlationId: string | undefined; type: 'word' | 'line' }) {139return createEditSource({140source: 'inlineCompletionPartialAccept',141type: data.type,142$nes: data.nes,143...toProperties(data.providerId),144$$correlationId: data.correlationId,145$$requestUuid: data.requestUuid,146$$languageId: data.languageId,147} as const);148},149150inlineChatApplyEdit(data: { modelId: string | undefined; requestId: string | undefined; sessionId: string | undefined; languageId: string; extensionId: VersionedExtensionId | undefined }) {151return createEditSource({152source: 'inlineChat.applyEdits',153$modelId: avoidPathRedaction(data.modelId),154$extensionId: data.extensionId?.extensionId,155$extensionVersion: data.extensionId?.version,156$$sessionId: data.sessionId,157$$requestId: data.requestId,158$$languageId: data.languageId,159} as const);160},161162reloadFromDisk: () => createEditSource({ source: 'reloadFromDisk' } as const),163164cursor(data: { kind: 'compositionType' | 'compositionEnd' | 'type' | 'paste' | 'cut' | 'executeCommands' | 'executeCommand'; detailedSource?: string | null }) {165return createEditSource({166source: 'cursor',167kind: data.kind,168detailedSource: data.detailedSource,169} as const);170},171172setValue: () => createEditSource({ source: 'setValue' } as const),173eolChange: () => createEditSource({ source: 'eolChange' } as const),174applyEdits: () => createEditSource({ source: 'applyEdits' } as const),175snippet: () => createEditSource({ source: 'snippet' } as const),176suggest: (data: { providerId: ProviderId | undefined }) => createEditSource({ source: 'suggest', ...toProperties(data.providerId) } as const),177178codeAction: (data: { kind: string | undefined; providerId: ProviderId | undefined }) => createEditSource({ source: 'codeAction', $kind: data.kind, ...toProperties(data.providerId) } as const)179};180181function toProperties(version: ProviderId | undefined) {182if (!version) {183return {};184}185return {186$extensionId: version.extensionId,187$extensionVersion: version.extensionVersion,188$providerId: version.providerId,189};190}191192type Values<T> = T[keyof T];193export type ITextModelEditSourceMetadata = Values<{ [TKey in keyof typeof EditSources]: ReturnType<typeof EditSources[TKey]>['metadataT'] }>;194type ITextModelEditSourceMetadataKeys = Values<{ [TKey in keyof typeof EditSources]: keyof ReturnType<typeof EditSources[TKey]>['metadataT'] }>;195196197function avoidPathRedaction(str: string | undefined): string | undefined {198if (str === undefined) {199return undefined;200}201// To avoid false-positive file path redaction.202return str.replaceAll('/', '|');203}204205206export class EditDeltaInfo {207public static fromText(text: string): EditDeltaInfo {208const linesAdded = TextLength.ofText(text).lineCount;209const charsAdded = text.length;210return new EditDeltaInfo(linesAdded, 0, charsAdded, 0);211}212213/** @internal */214public static fromEdit(edit: BaseStringEdit, originalString: StringText): EditDeltaInfo {215const lineEdit = LineEdit.fromStringEdit(edit, originalString);216const linesAdded = sumBy(lineEdit.replacements, r => r.newLines.length);217const linesRemoved = sumBy(lineEdit.replacements, r => r.lineRange.length);218const charsAdded = sumBy(edit.replacements, r => r.getNewLength());219const charsRemoved = sumBy(edit.replacements, r => r.replaceRange.length);220return new EditDeltaInfo(linesAdded, linesRemoved, charsAdded, charsRemoved);221}222223public static tryCreate(224linesAdded: number | undefined,225linesRemoved: number | undefined,226charsAdded: number | undefined,227charsRemoved: number | undefined228): EditDeltaInfo | undefined {229if (linesAdded === undefined || linesRemoved === undefined || charsAdded === undefined || charsRemoved === undefined) {230return undefined;231}232return new EditDeltaInfo(linesAdded, linesRemoved, charsAdded, charsRemoved);233}234235constructor(236public readonly linesAdded: number,237public readonly linesRemoved: number,238public readonly charsAdded: number,239public readonly charsRemoved: number240) { }241}242243244/**245* This is an opaque serializable type that represents a unique identity for an edit.246*/247export interface EditSuggestionId {248readonly _brand: 'EditIdentity';249}250251export namespace EditSuggestionId {252/**253* Use AiEditTelemetryServiceImpl to create a new id!254*/255export function newId(genPrefixedUuid?: (ns: string) => string): EditSuggestionId {256const id = genPrefixedUuid ? genPrefixedUuid('sgt') : prefixedUuid('sgt');257return toEditIdentity(id);258}259}260261function toEditIdentity(id: string): EditSuggestionId {262return id as unknown as EditSuggestionId;263}264265266