Path: blob/main/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts
3296 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 { Iterable } from '../../../../base/common/iterator.js';6import { Disposable, IReference } from '../../../../base/common/lifecycle.js';7import { Schemas } from '../../../../base/common/network.js';8import { URI } from '../../../../base/common/uri.js';9import { Range } from '../../../../editor/common/core/range.js';10import { ILanguageService } from '../../../../editor/common/languages/language.js';11import { EndOfLinePreference, ITextModel } from '../../../../editor/common/model.js';12import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js';13import { extractCodeblockUrisFromText, extractVulnerabilitiesFromText, IMarkdownVulnerability } from './annotations.js';14import { IChatRequestViewModel, IChatResponseViewModel, isResponseVM } from './chatViewModel.js';151617interface CodeBlockContent {18readonly text: string;19readonly languageId?: string;20readonly isComplete: boolean;21}2223export interface CodeBlockEntry {24readonly model: Promise<ITextModel>;25readonly vulns: readonly IMarkdownVulnerability[];26readonly codemapperUri?: URI;27readonly isEdit?: boolean;28}2930export class CodeBlockModelCollection extends Disposable {3132private readonly _models = new Map<string, {33model: Promise<IReference<IResolvedTextEditorModel>>;34vulns: readonly IMarkdownVulnerability[];35codemapperUri?: URI;36isEdit?: boolean;37}>();3839/**40* Max number of models to keep in memory.41*42* Currently always maintains the most recently created models.43*/44private readonly maxModelCount = 100;4546constructor(47private readonly tag: string | undefined,48@ILanguageService private readonly languageService: ILanguageService,49@ITextModelService private readonly textModelService: ITextModelService,50) {51super();52}5354public override dispose(): void {55super.dispose();56this.clear();57}5859get(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): CodeBlockEntry | undefined {60const entry = this._models.get(this.getKey(sessionId, chat, codeBlockIndex));61if (!entry) {62return;63}64return {65model: entry.model.then(ref => ref.object.textEditorModel),66vulns: entry.vulns,67codemapperUri: entry.codemapperUri,68isEdit: entry.isEdit,69};70}7172getOrCreate(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): CodeBlockEntry {73const existing = this.get(sessionId, chat, codeBlockIndex);74if (existing) {75return existing;76}7778const uri = this.getCodeBlockUri(sessionId, chat, codeBlockIndex);79const model = this.textModelService.createModelReference(uri);80this._models.set(this.getKey(sessionId, chat, codeBlockIndex), {81model: model,82vulns: [],83codemapperUri: undefined,84});8586while (this._models.size > this.maxModelCount) {87const first = Iterable.first(this._models.keys());88if (!first) {89break;90}91this.delete(first);92}9394return { model: model.then(x => x.object.textEditorModel), vulns: [], codemapperUri: undefined };95}9697private delete(key: string) {98const entry = this._models.get(key);99if (!entry) {100return;101}102103entry.model.then(ref => ref.object.dispose());104105this._models.delete(key);106}107108clear(): void {109this._models.forEach(async entry => (await entry.model).dispose());110this._models.clear();111}112113updateSync(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, content: CodeBlockContent): CodeBlockEntry {114const entry = this.getOrCreate(sessionId, chat, codeBlockIndex);115116const extractedVulns = extractVulnerabilitiesFromText(content.text);117const newText = fixCodeText(extractedVulns.newText, content.languageId);118this.setVulns(sessionId, chat, codeBlockIndex, extractedVulns.vulnerabilities);119120const codeblockUri = extractCodeblockUrisFromText(newText);121if (codeblockUri) {122this.setCodemapperUri(sessionId, chat, codeBlockIndex, codeblockUri.uri, codeblockUri.isEdit);123}124125if (content.isComplete) {126this.markCodeBlockCompleted(sessionId, chat, codeBlockIndex);127}128129return this.get(sessionId, chat, codeBlockIndex) ?? entry;130}131132markCodeBlockCompleted(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): void {133const entry = this._models.get(this.getKey(sessionId, chat, codeBlockIndex));134if (!entry) {135return;136}137// TODO: fill this in once we've implemented https://github.com/microsoft/vscode/issues/232538138}139140async update(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, content: CodeBlockContent): Promise<CodeBlockEntry> {141const entry = this.getOrCreate(sessionId, chat, codeBlockIndex);142143const extractedVulns = extractVulnerabilitiesFromText(content.text);144let newText = fixCodeText(extractedVulns.newText, content.languageId);145this.setVulns(sessionId, chat, codeBlockIndex, extractedVulns.vulnerabilities);146147const codeblockUri = extractCodeblockUrisFromText(newText);148if (codeblockUri) {149this.setCodemapperUri(sessionId, chat, codeBlockIndex, codeblockUri.uri, codeblockUri.isEdit);150newText = codeblockUri.textWithoutResult;151}152153if (content.isComplete) {154this.markCodeBlockCompleted(sessionId, chat, codeBlockIndex);155}156157const textModel = await entry.model;158if (!textModel || textModel.isDisposed()) {159// Somehow we get an undefined textModel sometimes - #237782160return entry;161}162163if (content.languageId) {164const vscodeLanguageId = this.languageService.getLanguageIdByLanguageName(content.languageId);165if (vscodeLanguageId && vscodeLanguageId !== textModel.getLanguageId()) {166textModel.setLanguage(vscodeLanguageId);167}168}169170const currentText = textModel.getValue(EndOfLinePreference.LF);171if (newText === currentText) {172return entry;173}174175if (newText.startsWith(currentText)) {176const text = newText.slice(currentText.length);177const lastLine = textModel.getLineCount();178const lastCol = textModel.getLineMaxColumn(lastLine);179textModel.applyEdits([{ range: new Range(lastLine, lastCol, lastLine, lastCol), text }]);180} else {181// console.log(`Failed to optimize setText`);182textModel.setValue(newText);183}184185return entry;186}187188private setCodemapperUri(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, codemapperUri: URI, isEdit?: boolean) {189const entry = this._models.get(this.getKey(sessionId, chat, codeBlockIndex));190if (entry) {191entry.codemapperUri = codemapperUri;192entry.isEdit = isEdit;193}194}195196private setVulns(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, vulnerabilities: IMarkdownVulnerability[]) {197const entry = this._models.get(this.getKey(sessionId, chat, codeBlockIndex));198if (entry) {199entry.vulns = vulnerabilities;200}201}202203private getKey(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, index: number): string {204return `${sessionId}/${chat.id}/${index}`;205}206207private getCodeBlockUri(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, index: number): URI {208const metadata = this.getUriMetaData(chat);209const indexPart = this.tag ? `${this.tag}-${index}` : `${index}`;210return URI.from({211scheme: Schemas.vscodeChatCodeBlock,212authority: sessionId,213path: `/${chat.id}/${indexPart}`,214fragment: metadata ? JSON.stringify(metadata) : undefined,215});216}217218private getUriMetaData(chat: IChatRequestViewModel | IChatResponseViewModel) {219if (!isResponseVM(chat)) {220return undefined;221}222223return {224references: chat.contentReferences.map(ref => {225if (typeof ref.reference === 'string') {226return;227}228229const uriOrLocation = 'variableName' in ref.reference ?230ref.reference.value :231ref.reference;232if (!uriOrLocation) {233return;234}235236if (URI.isUri(uriOrLocation)) {237return {238uri: uriOrLocation.toJSON()239};240}241242return {243uri: uriOrLocation.uri.toJSON(),244range: uriOrLocation.range,245};246})247};248}249}250251function fixCodeText(text: string, languageId: string | undefined): string {252if (languageId === 'php') {253// <?php or short tag version <?254if (!text.trim().startsWith('<?')) {255return `<?php\n${text}`;256}257}258259return text;260}261262263