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