Path: blob/main/src/vs/workbench/contrib/chat/common/widget/codeBlockModelCollection.ts
5221 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;31readonly subAgentInvocationId?: string;32}3334export class CodeBlockModelCollection extends Disposable {3536private readonly _models = new Map<string, {37model: Promise<IReference<IResolvedTextEditorModel>>;38vulns: readonly IMarkdownVulnerability[];39inLanguageId: string | undefined;40codemapperUri?: URI;41isEdit?: boolean;42subAgentInvocationId?: string;43}>();4445/**46* Max number of models to keep in memory.47*48* Currently always maintains the most recently created models.49*/50private readonly maxModelCount = 100;5152constructor(53private readonly tag: string | undefined,54@ILanguageService private readonly languageService: ILanguageService,55@ITextModelService private readonly textModelService: ITextModelService,56) {57super();5859this._register(this.languageService.onDidChange(async () => {60for (const entry of this._models.values()) {61if (!entry.inLanguageId) {62continue;63}6465const model = (await entry.model).object;66const existingLanguageId = model.getLanguageId();67if (!existingLanguageId || existingLanguageId === PLAINTEXT_LANGUAGE_ID) {68this.trySetTextModelLanguage(entry.inLanguageId, model.textEditorModel);69}70}71}));72}7374public override dispose(): void {75super.dispose();76this.clear();77}7879get(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): CodeBlockEntry | undefined {80const entry = this._models.get(this.getKey(sessionResource, chat, codeBlockIndex));81if (!entry) {82return;83}84return {85model: entry.model.then(ref => ref.object.textEditorModel),86vulns: entry.vulns,87codemapperUri: entry.codemapperUri,88isEdit: entry.isEdit,89subAgentInvocationId: entry.subAgentInvocationId,90};91}9293getOrCreate(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): CodeBlockEntry {94const existing = this.get(sessionResource, chat, codeBlockIndex);95if (existing) {96return existing;97}9899const uri = this.getCodeBlockUri(sessionResource, chat, codeBlockIndex);100const model = this.textModelService.createModelReference(uri);101this._models.set(this.getKey(sessionResource, chat, codeBlockIndex), {102model: model,103vulns: [],104inLanguageId: undefined,105codemapperUri: undefined,106});107108while (this._models.size > this.maxModelCount) {109const first = Iterable.first(this._models.keys());110if (!first) {111break;112}113this.delete(first);114}115116return { model: model.then(x => x.object.textEditorModel), vulns: [], codemapperUri: undefined };117}118119private delete(key: string) {120const entry = this._models.get(key);121if (!entry) {122return;123}124125entry.model.then(ref => ref.dispose());126this._models.delete(key);127}128129clear(): void {130this._models.forEach(async entry => await entry.model.then(ref => ref.dispose()));131this._models.clear();132}133134updateSync(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, content: CodeBlockContent): CodeBlockEntry {135const entry = this.getOrCreate(sessionResource, chat, codeBlockIndex);136137this.updateInternalCodeBlockEntry(content, sessionResource, chat, codeBlockIndex);138139return this.get(sessionResource, chat, codeBlockIndex) ?? entry;140}141142markCodeBlockCompleted(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): void {143const entry = this._models.get(this.getKey(sessionResource, chat, codeBlockIndex));144if (!entry) {145return;146}147// TODO: fill this in once we've implemented https://github.com/microsoft/vscode/issues/232538148}149150async update(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, content: CodeBlockContent): Promise<CodeBlockEntry> {151const entry = this.getOrCreate(sessionResource, chat, codeBlockIndex);152153const newText = this.updateInternalCodeBlockEntry(content, sessionResource, chat, codeBlockIndex);154155const textModel = await entry.model;156if (!textModel || textModel.isDisposed()) {157// Somehow we get an undefined textModel sometimes - #237782158return entry;159}160161if (content.languageId) {162this.trySetTextModelLanguage(content.languageId, textModel);163}164165const currentText = textModel.getValue(EndOfLinePreference.LF);166if (newText === currentText) {167return entry;168}169170if (newText.startsWith(currentText)) {171const text = newText.slice(currentText.length);172const lastLine = textModel.getLineCount();173const lastCol = textModel.getLineMaxColumn(lastLine);174textModel.applyEdits([{ range: new Range(lastLine, lastCol, lastLine, lastCol), text }]);175} else {176// console.log(`Failed to optimize setText`);177textModel.setValue(newText);178}179180return entry;181}182183private updateInternalCodeBlockEntry(content: CodeBlockContent, sessionResource: URI, chat: IChatResponseViewModel | IChatRequestViewModel, codeBlockIndex: number) {184const entry = this._models.get(this.getKey(sessionResource, chat, codeBlockIndex));185if (entry) {186entry.inLanguageId = content.languageId;187}188189const extractedVulns = extractVulnerabilitiesFromText(content.text);190let newText = fixCodeText(extractedVulns.newText, content.languageId);191if (entry) {192entry.vulns = extractedVulns.vulnerabilities;193}194195const codeblockUri = extractCodeblockUrisFromText(newText);196if (codeblockUri) {197if (entry) {198entry.codemapperUri = codeblockUri.uri;199entry.isEdit = codeblockUri.isEdit;200entry.subAgentInvocationId = codeblockUri.subAgentInvocationId;201}202203newText = codeblockUri.textWithoutResult;204}205206if (content.isComplete) {207this.markCodeBlockCompleted(sessionResource, chat, codeBlockIndex);208}209210return newText;211}212213private trySetTextModelLanguage(inLanguageId: string, textModel: ITextModel) {214const vscodeLanguageId = this.languageService.getLanguageIdByLanguageName(inLanguageId);215if (vscodeLanguageId && vscodeLanguageId !== textModel.getLanguageId()) {216textModel.setLanguage(vscodeLanguageId);217}218}219220private getKey(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, index: number): string {221return `${sessionResource.toString()}/${chat.id}/${index}`;222}223224private getCodeBlockUri(sessionResource: URI, chat: IChatRequestViewModel | IChatResponseViewModel, index: number): URI {225const metadata = this.getUriMetaData(chat);226const indexPart = this.tag ? `${this.tag}-${index}` : `${index}`;227const encodedSessionId = encodeBase64(VSBuffer.wrap(new TextEncoder().encode(sessionResource.toString())), false, true);228return URI.from({229scheme: Schemas.vscodeChatCodeBlock,230authority: encodedSessionId,231path: `/${chat.id}/${indexPart}`,232fragment: metadata ? JSON.stringify(metadata) : undefined,233});234}235236private getUriMetaData(chat: IChatRequestViewModel | IChatResponseViewModel) {237if (!isResponseVM(chat)) {238return undefined;239}240241return {242references: chat.contentReferences.map(ref => {243if (typeof ref.reference === 'string') {244return;245}246247const uriOrLocation = isChatContentVariableReference(ref.reference) ?248ref.reference.value :249ref.reference;250if (!uriOrLocation) {251return;252}253254if (URI.isUri(uriOrLocation)) {255return {256uri: uriOrLocation.toJSON()257};258}259260return {261uri: uriOrLocation.uri.toJSON(),262range: uriOrLocation.range,263};264})265};266}267}268269function fixCodeText(text: string, languageId: string | undefined): string {270if (languageId === 'php') {271// <?php or short tag version <?272if (!text.trim().startsWith('<?')) {273return `<?php\n${text}`;274}275}276277return text;278}279280281