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