Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/textModelEditSource.ts
3292 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
return this.metadata as any;
60
}
61
}
62
63
type TextModelEditSourceT<T> = TextModelEditSource & {
64
metadataT: T;
65
};
66
67
function createEditSource<T extends Record<string, any>>(metadata: T): TextModelEditSourceT<T> {
68
return new TextModelEditSource(metadata as any, privateSymbol) as any;
69
}
70
71
export function isAiEdit(source: TextModelEditSource): boolean {
72
switch (source.metadata.source) {
73
case 'inlineCompletionAccept':
74
case 'inlineCompletionPartialAccept':
75
case 'inlineChat.applyEdits':
76
case 'Chat.applyEdits':
77
return true;
78
}
79
return false;
80
}
81
82
export function isUserEdit(source: TextModelEditSource): boolean {
83
switch (source.metadata.source) {
84
case 'cursor':
85
return source.metadata.kind === 'type';
86
}
87
return false;
88
}
89
90
export const EditSources = {
91
unknown(data: { name?: string | null }) {
92
return createEditSource({
93
source: 'unknown',
94
name: data.name,
95
} as const);
96
},
97
98
rename: () => createEditSource({ source: 'rename' } as const),
99
100
chatApplyEdits(data: {
101
modelId: string | undefined;
102
sessionId: string | undefined;
103
requestId: string | undefined;
104
languageId: string;
105
mode: string | undefined;
106
extensionId: VersionedExtensionId | undefined;
107
codeBlockSuggestionId: EditSuggestionId | undefined;
108
}) {
109
return createEditSource({
110
source: 'Chat.applyEdits',
111
$modelId: avoidPathRedaction(data.modelId),
112
$extensionId: data.extensionId?.extensionId,
113
$extensionVersion: data.extensionId?.version,
114
$$languageId: data.languageId,
115
$$sessionId: data.sessionId,
116
$$requestId: data.requestId,
117
$$mode: data.mode,
118
$$codeBlockSuggestionId: data.codeBlockSuggestionId,
119
} as const);
120
},
121
122
chatUndoEdits: () => createEditSource({ source: 'Chat.undoEdits' } as const),
123
chatReset: () => createEditSource({ source: 'Chat.reset' } as const),
124
125
inlineCompletionAccept(data: { nes: boolean; requestUuid: string; languageId: string; providerId?: ProviderId }) {
126
return createEditSource({
127
source: 'inlineCompletionAccept',
128
$nes: data.nes,
129
...toProperties(data.providerId),
130
$$requestUuid: data.requestUuid,
131
$$languageId: data.languageId,
132
} as const);
133
},
134
135
inlineCompletionPartialAccept(data: { nes: boolean; requestUuid: string; languageId: string; providerId?: ProviderId; type: 'word' | 'line' }) {
136
return createEditSource({
137
source: 'inlineCompletionPartialAccept',
138
type: data.type,
139
$nes: data.nes,
140
...toProperties(data.providerId),
141
$$requestUuid: data.requestUuid,
142
$$languageId: data.languageId,
143
} as const);
144
},
145
146
inlineChatApplyEdit(data: { modelId: string | undefined; requestId: string | undefined; languageId: string; extensionId: VersionedExtensionId | undefined }) {
147
return createEditSource({
148
source: 'inlineChat.applyEdits',
149
$modelId: avoidPathRedaction(data.modelId),
150
$extensionId: data.extensionId?.extensionId,
151
$extensionVersion: data.extensionId?.version,
152
$$requestId: data.requestId,
153
$$languageId: data.languageId,
154
} as const);
155
},
156
157
reloadFromDisk: () => createEditSource({ source: 'reloadFromDisk' } as const),
158
159
cursor(data: { kind: 'compositionType' | 'compositionEnd' | 'type' | 'paste' | 'cut' | 'executeCommands' | 'executeCommand'; detailedSource?: string | null }) {
160
return createEditSource({
161
source: 'cursor',
162
kind: data.kind,
163
detailedSource: data.detailedSource,
164
} as const);
165
},
166
167
setValue: () => createEditSource({ source: 'setValue' } as const),
168
eolChange: () => createEditSource({ source: 'eolChange' } as const),
169
applyEdits: () => createEditSource({ source: 'applyEdits' } as const),
170
snippet: () => createEditSource({ source: 'snippet' } as const),
171
suggest: (data: { providerId: ProviderId | undefined }) => createEditSource({ source: 'suggest', ...toProperties(data.providerId) } as const),
172
173
codeAction: (data: { kind: string | undefined; providerId: ProviderId | undefined }) => createEditSource({ source: 'codeAction', $kind: data.kind, ...toProperties(data.providerId) } as const)
174
};
175
176
function toProperties(version: ProviderId | undefined) {
177
if (!version) {
178
return {};
179
}
180
return {
181
$extensionId: version.extensionId,
182
$extensionVersion: version.extensionVersion,
183
$providerId: version.providerId,
184
};
185
}
186
187
type Values<T> = T[keyof T];
188
export type ITextModelEditSourceMetadata = Values<{ [TKey in keyof typeof EditSources]: ReturnType<typeof EditSources[TKey]>['metadataT'] }>;
189
type ITextModelEditSourceMetadataKeys = Values<{ [TKey in keyof typeof EditSources]: keyof ReturnType<typeof EditSources[TKey]>['metadataT'] }>;
190
191
192
function avoidPathRedaction(str: string | undefined): string | undefined {
193
if (str === undefined) {
194
return undefined;
195
}
196
// To avoid false-positive file path redaction.
197
return str.replaceAll('/', '|');
198
}
199
200
201
export class EditDeltaInfo {
202
public static fromText(text: string): EditDeltaInfo {
203
const linesAdded = TextLength.ofText(text).lineCount;
204
const charsAdded = text.length;
205
return new EditDeltaInfo(linesAdded, 0, charsAdded, 0);
206
}
207
208
public static fromEdit(edit: BaseStringEdit, originalString: StringText): EditDeltaInfo {
209
const lineEdit = LineEdit.fromStringEdit(edit, originalString);
210
const linesAdded = sumBy(lineEdit.replacements, r => r.newLines.length);
211
const linesRemoved = sumBy(lineEdit.replacements, r => r.lineRange.length);
212
const charsAdded = sumBy(edit.replacements, r => r.getNewLength());
213
const charsRemoved = sumBy(edit.replacements, r => r.replaceRange.length);
214
return new EditDeltaInfo(linesAdded, linesRemoved, charsAdded, charsRemoved);
215
}
216
217
public static tryCreate(
218
linesAdded: number | undefined,
219
linesRemoved: number | undefined,
220
charsAdded: number | undefined,
221
charsRemoved: number | undefined
222
): EditDeltaInfo | undefined {
223
if (linesAdded === undefined || linesRemoved === undefined || charsAdded === undefined || charsRemoved === undefined) {
224
return undefined;
225
}
226
return new EditDeltaInfo(linesAdded, linesRemoved, charsAdded, charsRemoved);
227
}
228
229
constructor(
230
public readonly linesAdded: number,
231
public readonly linesRemoved: number,
232
public readonly charsAdded: number,
233
public readonly charsRemoved: number
234
) { }
235
}
236
237
238
/**
239
* This is an opaque serializable type that represents a unique identity for an edit.
240
*/
241
export interface EditSuggestionId {
242
readonly _brand: 'EditIdentity';
243
}
244
245
export namespace EditSuggestionId {
246
/**
247
* Use AiEditTelemetryServiceImpl to create a new id!
248
*/
249
export function newId(): EditSuggestionId {
250
const id = prefixedUuid('sgt');
251
return toEditIdentity(id);
252
}
253
}
254
255
function toEditIdentity(id: string): EditSuggestionId {
256
return id as unknown as EditSuggestionId;
257
}
258
259