Path: blob/main/src/vs/workbench/contrib/chat/common/widget/codeBlockModelCollection.ts
4780 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { encodeBase64, VSBuffer } from '../../../../../base/common/buffer.js';6import { Iterable } from '../../../../../base/common/iterator.js';7import { Disposable, IReference } from '../../../../../base/common/lifecycle.js';8import { Schemas } from '../../../../../base/common/network.js';9import { URI } from '../../../../../base/common/uri.js';10import { Range } from '../../../../../editor/common/core/range.js';11import { ILanguageService } from '../../../../../editor/common/languages/language.js';12import { PLAINTEXT_LANGUAGE_ID } from '../../../../../editor/common/languages/modesRegistry.js';13import { EndOfLinePreference, ITextModel } from '../../../../../editor/common/model.js';14import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js';15import { extractCodeblockUrisFromText, extractVulnerabilitiesFromText, IMarkdownVulnerability } from './annotations.js';16import { isChatContentVariableReference } from '../chatService/chatService.js';17import { IChatRequestViewModel, IChatResponseViewModel, isResponseVM } from '../model/chatViewModel.js';181920interface CodeBlockContent {21readonly text: string;22readonly languageId?: string;23readonly isComplete: boolean;24}2526export interface CodeBlockEntry {27readonly model: Promise<ITextModel>;28readonly vulns: readonly IMarkdownVulnerability[];29readonly codemapperUri?: URI;30readonly isEdit?: boolean;31}3233export class CodeBlockModelCollection extends Disposable {3435private readonly _models = new Map<string, {36model: Promise<IReference<IResolvedTextEditorModel>>;37vulns: readonly IMarkdownVulnerability[];38inLanguageId: string | undefined;39codemapperUri?: URI;40isEdit?: boolean;41}>();4243/**44* Max number of models to keep in memory.45*46* Currently always maintains the most recently created models.47*/48private readonly maxModelCount = 100;4950constructor(51private readonly tag: string | undefined,52@ILanguageService private readonly languageService: ILanguageService,53@ITextModelService private readonly textModelService: ITextModelService,54) {55super();5657this._register(this.languageService.onDidChange(async () => {58for (const entry of this._models.values()) {59if (!entry.inLanguageId) {60continue;61}6263const model = (await entry.model).object;64const existingLanguageId = model.getLanguageId();65if (!existingLanguageId || existingLanguageId === PLAINTEXT_LANGUAGE_ID) {66this.trySetTextModelLanguage(entry.inLanguageId, model.textEditorModel);67}68}69}));70}7172public override dispose(): void {73super.dispose();74this.clear();75}7677get(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): CodeBlockEntry | undefined {78const entry = this._models.get(this.getKey(sessionResource, chat, codeBlockIndex));79if (!entry) {80return;81}82return {83model: entry.model.then(ref => ref.object.textEditorModel),84vulns: entry.vulns,85codemapperUri: entry.codemapperUri,86isEdit: entry.isEdit,87};88}8990getOrCreate(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): CodeBlockEntry {91const existing = this.get(sessionResource, chat, codeBlockIndex);92if (existing) {93return existing;94}9596const uri = this.getCodeBlockUri(sessionResource, chat, codeBlockIndex);97const model = this.textModelService.createModelReference(uri);98this._models.set(this.getKey(sessionResource, chat, codeBlockIndex), {99model: model,100vulns: [],101inLanguageId: undefined,102codemapperUri: undefined,103});104105while (this._models.size > this.maxModelCount) {106const first = Iterable.first(this._models.keys());107if (!first) {108break;109}110this.delete(first);111}112113return { model: model.then(x => x.object.textEditorModel), vulns: [], codemapperUri: undefined };114}115116private delete(key: string) {117const entry = this._models.get(key);118if (!entry) {119return;120}121122entry.model.then(ref => ref.dispose());123this._models.delete(key);124}125126clear(): void {127this._models.forEach(async entry => await entry.model.then(ref => ref.dispose()));128this._models.clear();129}130131updateSync(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, content: CodeBlockContent): CodeBlockEntry {132const entry = this.getOrCreate(sessionResource, chat, codeBlockIndex);133134this.updateInternalCodeBlockEntry(content, sessionResource, chat, codeBlockIndex);135136return this.get(sessionResource, chat, codeBlockIndex) ?? entry;137}138139markCodeBlockCompleted(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): void {140const entry = this._models.get(this.getKey(sessionResource, chat, codeBlockIndex));141if (!entry) {142return;143}144// TODO: fill this in once we've implemented https://github.com/microsoft/vscode/issues/232538145}146147async update(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, content: CodeBlockContent): Promise<CodeBlockEntry> {148const entry = this.getOrCreate(sessionResource, chat, codeBlockIndex);149150const newText = this.updateInternalCodeBlockEntry(content, sessionResource, chat, codeBlockIndex);151152const textModel = await entry.model;153if (!textModel || textModel.isDisposed()) {154// Somehow we get an undefined textModel sometimes - #237782155return entry;156}157158if (content.languageId) {159this.trySetTextModelLanguage(content.languageId, textModel);160}161162const currentText = textModel.getValue(EndOfLinePreference.LF);163if (newText === currentText) {164return entry;165}166167if (newText.startsWith(currentText)) {168const text = newText.slice(currentText.length);169const lastLine = textModel.getLineCount();170const lastCol = textModel.getLineMaxColumn(lastLine);171textModel.applyEdits([{ range: new Range(lastLine, lastCol, lastLine, lastCol), text }]);172} else {173// console.log(`Failed to optimize setText`);174textModel.setValue(newText);175}176177return entry;178}179180private updateInternalCodeBlockEntry(content: CodeBlockContent, sessionResource: URI, chat: IChatResponseViewModel | IChatRequestViewModel, codeBlockIndex: number) {181const entry = this._models.get(this.getKey(sessionResource, chat, codeBlockIndex));182if (entry) {183entry.inLanguageId = content.languageId;184}185186const extractedVulns = extractVulnerabilitiesFromText(content.text);187let newText = fixCodeText(extractedVulns.newText, content.languageId);188if (entry) {189entry.vulns = extractedVulns.vulnerabilities;190}191192const codeblockUri = extractCodeblockUrisFromText(newText);193if (codeblockUri) {194if (entry) {195entry.codemapperUri = codeblockUri.uri;196entry.isEdit = codeblockUri.isEdit;197}198199newText = codeblockUri.textWithoutResult;200}201202if (content.isComplete) {203this.markCodeBlockCompleted(sessionResource, chat, codeBlockIndex);204}205206return newText;207}208209private trySetTextModelLanguage(inLanguageId: string, textModel: ITextModel) {210const vscodeLanguageId = this.languageService.getLanguageIdByLanguageName(inLanguageId);211if (vscodeLanguageId && vscodeLanguageId !== textModel.getLanguageId()) {212textModel.setLanguage(vscodeLanguageId);213}214}215216private getKey(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, index: number): string {217return `${sessionResource.toString()}/${chat.id}/${index}`;218}219220private getCodeBlockUri(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, index: number): URI {221const metadata = this.getUriMetaData(chat);222const indexPart = this.tag ? `${this.tag}-${index}` : `${index}`;223const encodedSessionId = encodeBase64(VSBuffer.wrap(new TextEncoder().encode(sessionResource.toString())), false, true);224return URI.from({225scheme: Schemas.vscodeChatCodeBlock,226authority: encodedSessionId,227path: `/${chat.id}/${indexPart}`,228fragment: metadata ? JSON.stringify(metadata) : undefined,229});230}231232private getUriMetaData(chat: IChatRequestViewModel | IChatResponseViewModel) {233if (!isResponseVM(chat)) {234return undefined;235}236237return {238references: chat.contentReferences.map(ref => {239if (typeof ref.reference === 'string') {240return;241}242243const uriOrLocation = isChatContentVariableReference(ref.reference) ?244ref.reference.value :245ref.reference;246if (!uriOrLocation) {247return;248}249250if (URI.isUri(uriOrLocation)) {251return {252uri: uriOrLocation.toJSON()253};254}255256return {257uri: uriOrLocation.uri.toJSON(),258range: uriOrLocation.range,259};260})261};262}263}264265function fixCodeText(text: string, languageId: string | undefined): string {266if (languageId === 'php') {267// <?php or short tag version <?268if (!text.trim().startsWith('<?')) {269return `<?php\n${text}`;270}271}272273return text;274}275276277