Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/textModelEditSource.ts
5240 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 { sumBy } from '../../base/common/arrays.js';
7
import { prefixedUuid } from '../../base/common/uuid.js';
8
import { LineEdit } from './core/edits/lineEdit.js';
9
import { BaseStringEdit } from './core/edits/stringEdit.js';
10
import { StringText } from './core/text/abstractText.js';
11
import { TextLength } from './core/text/textLength.js';
12
import { ProviderId, VersionedExtensionId } from './languages.js';
13
14
const privateSymbol = Symbol('TextModelEditSource');
15
16
export class TextModelEditSource {
17
constructor(
18
public readonly metadata: ITextModelEditSourceMetadata,
19
_privateCtorGuard: typeof privateSymbol,
20
) { }
21
22
public toString(): string {
23
return `${this.metadata.source}`;
24
}
25
26
public getType(): string {
27
const metadata = this.metadata;
28
switch (metadata.source) {
29
case 'cursor':
30
return metadata.kind;
31
case 'inlineCompletionAccept':
32
return metadata.source + (metadata.$nes ? ':nes' : '');
33
case 'unknown':
34
return metadata.name || 'unknown';
35
default:
36
return metadata.source;
37
}
38
}
39
40
/**
41
* Converts the metadata to a key string.
42
* Only includes properties/values that have `level` many `$` prefixes or less.
43
*/
44
public toKey(level: number, filter: { [TKey in ITextModelEditSourceMetadataKeys]?: boolean } = {}): string {
45
const metadata = this.metadata;
46
const keys = Object.entries(metadata).filter(([key, value]) => {
47
const filterVal = (filter as Record<string, boolean>)[key];
48
if (filterVal !== undefined) {
49
return filterVal;
50
}
51
52
const prefixCount = (key.match(/\$/g) || []).length;
53
return prefixCount <= level && value !== undefined && value !== null && value !== '';
54
}).map(([key, value]) => `${key}:${value}`);
55
return keys.join('-');
56
}
57
58
public get props(): Record<ITextModelEditSourceMetadataKeys, string | undefined> {
59
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
60
return this.metadata as any;
61
}
62
}
63
64
type TextModelEditSourceT<T> = TextModelEditSource & {
65
metadataT: T;
66
};
67
68
// eslint-disable-next-line @typescript-eslint/no-explicit-any
69
function createEditSource<T extends Record<string, any>>(metadata: T): TextModelEditSourceT<T> {
70
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
71
return new TextModelEditSource(metadata as any, privateSymbol) as any;
72
}
73
74
export function isAiEdit(source: TextModelEditSource): boolean {
75
switch (source.metadata.source) {
76
case 'inlineCompletionAccept':
77
case 'inlineCompletionPartialAccept':
78
case 'inlineChat.applyEdits':
79
case 'Chat.applyEdits':
80
return true;
81
}
82
return false;
83
}
84
85
export function isUserEdit(source: TextModelEditSource): boolean {
86
switch (source.metadata.source) {
87
case 'cursor':
88
return source.metadata.kind === 'type';
89
}
90
return false;
91
}
92
93
export const EditSources = {
94
unknown(data: { name?: string | null }) {
95
return createEditSource({
96
source: 'unknown',
97
name: data.name,
98
} as const);
99
},
100
101
rename: (oldName: string | undefined, newName: string) => createEditSource({ source: 'rename', $$$oldName: oldName, $$$newName: newName } as const),
102
103
chatApplyEdits(data: {
104
modelId: string | undefined;
105
sessionId: string | undefined;
106
requestId: string | undefined;
107
languageId: string;
108
mode: string | undefined;
109
extensionId: VersionedExtensionId | undefined;
110
codeBlockSuggestionId: EditSuggestionId | undefined;
111
}) {
112
return createEditSource({
113
source: 'Chat.applyEdits',
114
$modelId: avoidPathRedaction(data.modelId),
115
$extensionId: data.extensionId?.extensionId,
116
$extensionVersion: data.extensionId?.version,
117
$$languageId: data.languageId,
118
$$sessionId: data.sessionId,
119
$$requestId: data.requestId,
120
$$mode: data.mode,
121
$$codeBlockSuggestionId: data.codeBlockSuggestionId,
122
} as const);
123
},
124
125
chatUndoEdits: () => createEditSource({ source: 'Chat.undoEdits' } as const),
126
chatReset: () => createEditSource({ source: 'Chat.reset' } as const),
127
128
inlineCompletionAccept(data: { nes: boolean; requestUuid: string; languageId: string; providerId?: ProviderId; correlationId: string | undefined }) {
129
return createEditSource({
130
source: 'inlineCompletionAccept',
131
$nes: data.nes,
132
...toProperties(data.providerId),
133
$$correlationId: data.correlationId,
134
$$requestUuid: data.requestUuid,
135
$$languageId: data.languageId,
136
} as const);
137
},
138
139
inlineCompletionPartialAccept(data: { nes: boolean; requestUuid: string; languageId: string; providerId?: ProviderId; correlationId: string | undefined; type: 'word' | 'line' }) {
140
return createEditSource({
141
source: 'inlineCompletionPartialAccept',
142
type: data.type,
143
$nes: data.nes,
144
...toProperties(data.providerId),
145
$$correlationId: data.correlationId,
146
$$requestUuid: data.requestUuid,
147
$$languageId: data.languageId,
148
} as const);
149
},
150
151
inlineChatApplyEdit(data: { modelId: string | undefined; requestId: string | undefined; sessionId: string | undefined; languageId: string; extensionId: VersionedExtensionId | undefined }) {
152
return createEditSource({
153
source: 'inlineChat.applyEdits',
154
$modelId: avoidPathRedaction(data.modelId),
155
$extensionId: data.extensionId?.extensionId,
156
$extensionVersion: data.extensionId?.version,
157
$$sessionId: data.sessionId,
158
$$requestId: data.requestId,
159
$$languageId: data.languageId,
160
} as const);
161
},
162
163
reloadFromDisk: () => createEditSource({ source: 'reloadFromDisk' } as const),
164
165
cursor(data: { kind: 'compositionType' | 'compositionEnd' | 'type' | 'paste' | 'cut' | 'executeCommands' | 'executeCommand'; detailedSource?: string | null }) {
166
return createEditSource({
167
source: 'cursor',
168
kind: data.kind,
169
detailedSource: data.detailedSource,
170
} as const);
171
},
172
173
setValue: () => createEditSource({ source: 'setValue' } as const),
174
eolChange: () => createEditSource({ source: 'eolChange' } as const),
175
applyEdits: () => createEditSource({ source: 'applyEdits' } as const),
176
snippet: () => createEditSource({ source: 'snippet' } as const),
177
suggest: (data: { providerId: ProviderId | undefined }) => createEditSource({ source: 'suggest', ...toProperties(data.providerId) } as const),
178
179
codeAction: (data: { kind: string | undefined; providerId: ProviderId | undefined }) => createEditSource({ source: 'codeAction', $kind: data.kind, ...toProperties(data.providerId) } as const)
180
};
181
182
function toProperties(version: ProviderId | undefined) {
183
if (!version) {
184
return {};
185
}
186
return {
187
$extensionId: version.extensionId,
188
$extensionVersion: version.extensionVersion,
189
$providerId: version.providerId,
190
};
191
}
192
193
type Values<T> = T[keyof T];
194
export type ITextModelEditSourceMetadata = Values<{ [TKey in keyof typeof EditSources]: ReturnType<typeof EditSources[TKey]>['metadataT'] }>;
195
type ITextModelEditSourceMetadataKeys = Values<{ [TKey in keyof typeof EditSources]: keyof ReturnType<typeof EditSources[TKey]>['metadataT'] }>;
196
197
198
function avoidPathRedaction(str: string | undefined): string | undefined {
199
if (str === undefined) {
200
return undefined;
201
}
202
// To avoid false-positive file path redaction.
203
return str.replaceAll('/', '|');
204
}
205
206
207
export class EditDeltaInfo {
208
public static fromText(text: string): EditDeltaInfo {
209
const linesAdded = TextLength.ofText(text).lineCount;
210
const charsAdded = text.length;
211
return new EditDeltaInfo(linesAdded, 0, charsAdded, 0);
212
}
213
214
/** @internal */
215
public static fromEdit(edit: BaseStringEdit, originalString: StringText): EditDeltaInfo {
216
const lineEdit = LineEdit.fromStringEdit(edit, originalString);
217
const linesAdded = sumBy(lineEdit.replacements, r => r.newLines.length);
218
const linesRemoved = sumBy(lineEdit.replacements, r => r.lineRange.length);
219
const charsAdded = sumBy(edit.replacements, r => r.getNewLength());
220
const charsRemoved = sumBy(edit.replacements, r => r.replaceRange.length);
221
return new EditDeltaInfo(linesAdded, linesRemoved, charsAdded, charsRemoved);
222
}
223
224
public static tryCreate(
225
linesAdded: number | undefined,
226
linesRemoved: number | undefined,
227
charsAdded: number | undefined,
228
charsRemoved: number | undefined
229
): EditDeltaInfo | undefined {
230
if (linesAdded === undefined || linesRemoved === undefined || charsAdded === undefined || charsRemoved === undefined) {
231
return undefined;
232
}
233
return new EditDeltaInfo(linesAdded, linesRemoved, charsAdded, charsRemoved);
234
}
235
236
constructor(
237
public readonly linesAdded: number,
238
public readonly linesRemoved: number,
239
public readonly charsAdded: number,
240
public readonly charsRemoved: number
241
) { }
242
}
243
244
245
/**
246
* This is an opaque serializable type that represents a unique identity for an edit.
247
*/
248
export interface EditSuggestionId {
249
readonly _brand: 'EditIdentity';
250
}
251
252
export namespace EditSuggestionId {
253
/**
254
* Use AiEditTelemetryServiceImpl to create a new id!
255
*/
256
export function newId(genPrefixedUuid?: (ns: string) => string): EditSuggestionId {
257
const id = genPrefixedUuid ? genPrefixedUuid('sgt') : prefixedUuid('sgt');
258
return toEditIdentity(id);
259
}
260
}
261
262
function toEditIdentity(id: string): EditSuggestionId {
263
return id as unknown as EditSuggestionId;
264
}
265
266