Path: blob/main/src/vs/editor/common/textModelEditSource.ts
3292 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> {58return this.metadata as any;59}60}6162type TextModelEditSourceT<T> = TextModelEditSource & {63metadataT: T;64};6566function createEditSource<T extends Record<string, any>>(metadata: T): TextModelEditSourceT<T> {67return new TextModelEditSource(metadata as any, privateSymbol) as any;68}6970export function isAiEdit(source: TextModelEditSource): boolean {71switch (source.metadata.source) {72case 'inlineCompletionAccept':73case 'inlineCompletionPartialAccept':74case 'inlineChat.applyEdits':75case 'Chat.applyEdits':76return true;77}78return false;79}8081export function isUserEdit(source: TextModelEditSource): boolean {82switch (source.metadata.source) {83case 'cursor':84return source.metadata.kind === 'type';85}86return false;87}8889export const EditSources = {90unknown(data: { name?: string | null }) {91return createEditSource({92source: 'unknown',93name: data.name,94} as const);95},9697rename: () => createEditSource({ source: 'rename' } as const),9899chatApplyEdits(data: {100modelId: string | undefined;101sessionId: string | undefined;102requestId: string | undefined;103languageId: string;104mode: string | undefined;105extensionId: VersionedExtensionId | undefined;106codeBlockSuggestionId: EditSuggestionId | undefined;107}) {108return createEditSource({109source: 'Chat.applyEdits',110$modelId: avoidPathRedaction(data.modelId),111$extensionId: data.extensionId?.extensionId,112$extensionVersion: data.extensionId?.version,113$$languageId: data.languageId,114$$sessionId: data.sessionId,115$$requestId: data.requestId,116$$mode: data.mode,117$$codeBlockSuggestionId: data.codeBlockSuggestionId,118} as const);119},120121chatUndoEdits: () => createEditSource({ source: 'Chat.undoEdits' } as const),122chatReset: () => createEditSource({ source: 'Chat.reset' } as const),123124inlineCompletionAccept(data: { nes: boolean; requestUuid: string; languageId: string; providerId?: ProviderId }) {125return createEditSource({126source: 'inlineCompletionAccept',127$nes: data.nes,128...toProperties(data.providerId),129$$requestUuid: data.requestUuid,130$$languageId: data.languageId,131} as const);132},133134inlineCompletionPartialAccept(data: { nes: boolean; requestUuid: string; languageId: string; providerId?: ProviderId; type: 'word' | 'line' }) {135return createEditSource({136source: 'inlineCompletionPartialAccept',137type: data.type,138$nes: data.nes,139...toProperties(data.providerId),140$$requestUuid: data.requestUuid,141$$languageId: data.languageId,142} as const);143},144145inlineChatApplyEdit(data: { modelId: string | undefined; requestId: string | undefined; languageId: string; extensionId: VersionedExtensionId | undefined }) {146return createEditSource({147source: 'inlineChat.applyEdits',148$modelId: avoidPathRedaction(data.modelId),149$extensionId: data.extensionId?.extensionId,150$extensionVersion: data.extensionId?.version,151$$requestId: data.requestId,152$$languageId: data.languageId,153} as const);154},155156reloadFromDisk: () => createEditSource({ source: 'reloadFromDisk' } as const),157158cursor(data: { kind: 'compositionType' | 'compositionEnd' | 'type' | 'paste' | 'cut' | 'executeCommands' | 'executeCommand'; detailedSource?: string | null }) {159return createEditSource({160source: 'cursor',161kind: data.kind,162detailedSource: data.detailedSource,163} as const);164},165166setValue: () => createEditSource({ source: 'setValue' } as const),167eolChange: () => createEditSource({ source: 'eolChange' } as const),168applyEdits: () => createEditSource({ source: 'applyEdits' } as const),169snippet: () => createEditSource({ source: 'snippet' } as const),170suggest: (data: { providerId: ProviderId | undefined }) => createEditSource({ source: 'suggest', ...toProperties(data.providerId) } as const),171172codeAction: (data: { kind: string | undefined; providerId: ProviderId | undefined }) => createEditSource({ source: 'codeAction', $kind: data.kind, ...toProperties(data.providerId) } as const)173};174175function toProperties(version: ProviderId | undefined) {176if (!version) {177return {};178}179return {180$extensionId: version.extensionId,181$extensionVersion: version.extensionVersion,182$providerId: version.providerId,183};184}185186type Values<T> = T[keyof T];187export type ITextModelEditSourceMetadata = Values<{ [TKey in keyof typeof EditSources]: ReturnType<typeof EditSources[TKey]>['metadataT'] }>;188type ITextModelEditSourceMetadataKeys = Values<{ [TKey in keyof typeof EditSources]: keyof ReturnType<typeof EditSources[TKey]>['metadataT'] }>;189190191function avoidPathRedaction(str: string | undefined): string | undefined {192if (str === undefined) {193return undefined;194}195// To avoid false-positive file path redaction.196return str.replaceAll('/', '|');197}198199200export class EditDeltaInfo {201public static fromText(text: string): EditDeltaInfo {202const linesAdded = TextLength.ofText(text).lineCount;203const charsAdded = text.length;204return new EditDeltaInfo(linesAdded, 0, charsAdded, 0);205}206207public static fromEdit(edit: BaseStringEdit, originalString: StringText): EditDeltaInfo {208const lineEdit = LineEdit.fromStringEdit(edit, originalString);209const linesAdded = sumBy(lineEdit.replacements, r => r.newLines.length);210const linesRemoved = sumBy(lineEdit.replacements, r => r.lineRange.length);211const charsAdded = sumBy(edit.replacements, r => r.getNewLength());212const charsRemoved = sumBy(edit.replacements, r => r.replaceRange.length);213return new EditDeltaInfo(linesAdded, linesRemoved, charsAdded, charsRemoved);214}215216public static tryCreate(217linesAdded: number | undefined,218linesRemoved: number | undefined,219charsAdded: number | undefined,220charsRemoved: number | undefined221): EditDeltaInfo | undefined {222if (linesAdded === undefined || linesRemoved === undefined || charsAdded === undefined || charsRemoved === undefined) {223return undefined;224}225return new EditDeltaInfo(linesAdded, linesRemoved, charsAdded, charsRemoved);226}227228constructor(229public readonly linesAdded: number,230public readonly linesRemoved: number,231public readonly charsAdded: number,232public readonly charsRemoved: number233) { }234}235236237/**238* This is an opaque serializable type that represents a unique identity for an edit.239*/240export interface EditSuggestionId {241readonly _brand: 'EditIdentity';242}243244export namespace EditSuggestionId {245/**246* Use AiEditTelemetryServiceImpl to create a new id!247*/248export function newId(): EditSuggestionId {249const id = prefixedUuid('sgt');250return toEditIdentity(id);251}252}253254function toEditIdentity(id: string): EditSuggestionId {255return id as unknown as EditSuggestionId;256}257258259