Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/widget/codeBlockModelCollection.ts
5221 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 { encodeBase64, VSBuffer } from '../../../../../base/common/buffer.js';
7
import { Iterable } from '../../../../../base/common/iterator.js';
8
import { Disposable, IReference } from '../../../../../base/common/lifecycle.js';
9
import { Schemas } from '../../../../../base/common/network.js';
10
import { URI } from '../../../../../base/common/uri.js';
11
import { Range } from '../../../../../editor/common/core/range.js';
12
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
13
import { PLAINTEXT_LANGUAGE_ID } from '../../../../../editor/common/languages/modesRegistry.js';
14
import { EndOfLinePreference, ITextModel } from '../../../../../editor/common/model.js';
15
import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js';
16
import { extractCodeblockUrisFromText, extractVulnerabilitiesFromText, IMarkdownVulnerability } from './annotations.js';
17
import { isChatContentVariableReference } from '../chatService/chatService.js';
18
import { IChatRequestViewModel, IChatResponseViewModel, isResponseVM } from '../model/chatViewModel.js';
19
20
21
interface CodeBlockContent {
22
readonly text: string;
23
readonly languageId?: string;
24
readonly isComplete: boolean;
25
}
26
27
export interface CodeBlockEntry {
28
readonly model: Promise<ITextModel>;
29
readonly vulns: readonly IMarkdownVulnerability[];
30
readonly codemapperUri?: URI;
31
readonly isEdit?: boolean;
32
readonly subAgentInvocationId?: string;
33
}
34
35
export class CodeBlockModelCollection extends Disposable {
36
37
private readonly _models = new Map<string, {
38
model: Promise<IReference<IResolvedTextEditorModel>>;
39
vulns: readonly IMarkdownVulnerability[];
40
inLanguageId: string | undefined;
41
codemapperUri?: URI;
42
isEdit?: boolean;
43
subAgentInvocationId?: string;
44
}>();
45
46
/**
47
* Max number of models to keep in memory.
48
*
49
* Currently always maintains the most recently created models.
50
*/
51
private readonly maxModelCount = 100;
52
53
constructor(
54
private readonly tag: string | undefined,
55
@ILanguageService private readonly languageService: ILanguageService,
56
@ITextModelService private readonly textModelService: ITextModelService,
57
) {
58
super();
59
60
this._register(this.languageService.onDidChange(async () => {
61
for (const entry of this._models.values()) {
62
if (!entry.inLanguageId) {
63
continue;
64
}
65
66
const model = (await entry.model).object;
67
const existingLanguageId = model.getLanguageId();
68
if (!existingLanguageId || existingLanguageId === PLAINTEXT_LANGUAGE_ID) {
69
this.trySetTextModelLanguage(entry.inLanguageId, model.textEditorModel);
70
}
71
}
72
}));
73
}
74
75
public override dispose(): void {
76
super.dispose();
77
this.clear();
78
}
79
80
get(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): CodeBlockEntry | undefined {
81
const entry = this._models.get(this.getKey(sessionResource, chat, codeBlockIndex));
82
if (!entry) {
83
return;
84
}
85
return {
86
model: entry.model.then(ref => ref.object.textEditorModel),
87
vulns: entry.vulns,
88
codemapperUri: entry.codemapperUri,
89
isEdit: entry.isEdit,
90
subAgentInvocationId: entry.subAgentInvocationId,
91
};
92
}
93
94
getOrCreate(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): CodeBlockEntry {
95
const existing = this.get(sessionResource, chat, codeBlockIndex);
96
if (existing) {
97
return existing;
98
}
99
100
const uri = this.getCodeBlockUri(sessionResource, chat, codeBlockIndex);
101
const model = this.textModelService.createModelReference(uri);
102
this._models.set(this.getKey(sessionResource, chat, codeBlockIndex), {
103
model: model,
104
vulns: [],
105
inLanguageId: undefined,
106
codemapperUri: undefined,
107
});
108
109
while (this._models.size > this.maxModelCount) {
110
const first = Iterable.first(this._models.keys());
111
if (!first) {
112
break;
113
}
114
this.delete(first);
115
}
116
117
return { model: model.then(x => x.object.textEditorModel), vulns: [], codemapperUri: undefined };
118
}
119
120
private delete(key: string) {
121
const entry = this._models.get(key);
122
if (!entry) {
123
return;
124
}
125
126
entry.model.then(ref => ref.dispose());
127
this._models.delete(key);
128
}
129
130
clear(): void {
131
this._models.forEach(async entry => await entry.model.then(ref => ref.dispose()));
132
this._models.clear();
133
}
134
135
updateSync(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, content: CodeBlockContent): CodeBlockEntry {
136
const entry = this.getOrCreate(sessionResource, chat, codeBlockIndex);
137
138
this.updateInternalCodeBlockEntry(content, sessionResource, chat, codeBlockIndex);
139
140
return this.get(sessionResource, chat, codeBlockIndex) ?? entry;
141
}
142
143
markCodeBlockCompleted(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): void {
144
const entry = this._models.get(this.getKey(sessionResource, chat, codeBlockIndex));
145
if (!entry) {
146
return;
147
}
148
// TODO: fill this in once we've implemented https://github.com/microsoft/vscode/issues/232538
149
}
150
151
async update(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, content: CodeBlockContent): Promise<CodeBlockEntry> {
152
const entry = this.getOrCreate(sessionResource, chat, codeBlockIndex);
153
154
const newText = this.updateInternalCodeBlockEntry(content, sessionResource, chat, codeBlockIndex);
155
156
const textModel = await entry.model;
157
if (!textModel || textModel.isDisposed()) {
158
// Somehow we get an undefined textModel sometimes - #237782
159
return entry;
160
}
161
162
if (content.languageId) {
163
this.trySetTextModelLanguage(content.languageId, textModel);
164
}
165
166
const currentText = textModel.getValue(EndOfLinePreference.LF);
167
if (newText === currentText) {
168
return entry;
169
}
170
171
if (newText.startsWith(currentText)) {
172
const text = newText.slice(currentText.length);
173
const lastLine = textModel.getLineCount();
174
const lastCol = textModel.getLineMaxColumn(lastLine);
175
textModel.applyEdits([{ range: new Range(lastLine, lastCol, lastLine, lastCol), text }]);
176
} else {
177
// console.log(`Failed to optimize setText`);
178
textModel.setValue(newText);
179
}
180
181
return entry;
182
}
183
184
private updateInternalCodeBlockEntry(content: CodeBlockContent, sessionResource: URI, chat: IChatResponseViewModel | IChatRequestViewModel, codeBlockIndex: number) {
185
const entry = this._models.get(this.getKey(sessionResource, chat, codeBlockIndex));
186
if (entry) {
187
entry.inLanguageId = content.languageId;
188
}
189
190
const extractedVulns = extractVulnerabilitiesFromText(content.text);
191
let newText = fixCodeText(extractedVulns.newText, content.languageId);
192
if (entry) {
193
entry.vulns = extractedVulns.vulnerabilities;
194
}
195
196
const codeblockUri = extractCodeblockUrisFromText(newText);
197
if (codeblockUri) {
198
if (entry) {
199
entry.codemapperUri = codeblockUri.uri;
200
entry.isEdit = codeblockUri.isEdit;
201
entry.subAgentInvocationId = codeblockUri.subAgentInvocationId;
202
}
203
204
newText = codeblockUri.textWithoutResult;
205
}
206
207
if (content.isComplete) {
208
this.markCodeBlockCompleted(sessionResource, chat, codeBlockIndex);
209
}
210
211
return newText;
212
}
213
214
private trySetTextModelLanguage(inLanguageId: string, textModel: ITextModel) {
215
const vscodeLanguageId = this.languageService.getLanguageIdByLanguageName(inLanguageId);
216
if (vscodeLanguageId && vscodeLanguageId !== textModel.getLanguageId()) {
217
textModel.setLanguage(vscodeLanguageId);
218
}
219
}
220
221
private getKey(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, index: number): string {
222
return `${sessionResource.toString()}/${chat.id}/${index}`;
223
}
224
225
private getCodeBlockUri(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, index: number): URI {
226
const metadata = this.getUriMetaData(chat);
227
const indexPart = this.tag ? `${this.tag}-${index}` : `${index}`;
228
const encodedSessionId = encodeBase64(VSBuffer.wrap(new TextEncoder().encode(sessionResource.toString())), false, true);
229
return URI.from({
230
scheme: Schemas.vscodeChatCodeBlock,
231
authority: encodedSessionId,
232
path: `/${chat.id}/${indexPart}`,
233
fragment: metadata ? JSON.stringify(metadata) : undefined,
234
});
235
}
236
237
private getUriMetaData(chat: IChatRequestViewModel | IChatResponseViewModel) {
238
if (!isResponseVM(chat)) {
239
return undefined;
240
}
241
242
return {
243
references: chat.contentReferences.map(ref => {
244
if (typeof ref.reference === 'string') {
245
return;
246
}
247
248
const uriOrLocation = isChatContentVariableReference(ref.reference) ?
249
ref.reference.value :
250
ref.reference;
251
if (!uriOrLocation) {
252
return;
253
}
254
255
if (URI.isUri(uriOrLocation)) {
256
return {
257
uri: uriOrLocation.toJSON()
258
};
259
}
260
261
return {
262
uri: uriOrLocation.uri.toJSON(),
263
range: uriOrLocation.range,
264
};
265
})
266
};
267
}
268
}
269
270
function fixCodeText(text: string, languageId: string | undefined): string {
271
if (languageId === 'php') {
272
// <?php or short tag version <?
273
if (!text.trim().startsWith('<?')) {
274
return `<?php\n${text}`;
275
}
276
}
277
278
return text;
279
}
280
281