Path: blob/main/src/vs/workbench/contrib/chat/common/annotations.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*--------------------------------------------------------------------------------------------*/4import { findLastIdx } from '../../../../base/common/arraysFind.js';5import { MarkdownString } from '../../../../base/common/htmlContent.js';6import { basename } from '../../../../base/common/resources.js';7import { URI } from '../../../../base/common/uri.js';8import { IRange } from '../../../../editor/common/core/range.js';9import { IChatProgressRenderableResponseContent, IChatProgressResponseContent, appendMarkdownString, canMergeMarkdownStrings } from './chatModel.js';10import { IChatAgentVulnerabilityDetails, IChatMarkdownContent } from './chatService.js';1112export const contentRefUrl = 'http://_vscodecontentref_'; // must be lowercase for URI1314export function annotateSpecialMarkdownContent(response: Iterable<IChatProgressResponseContent>): IChatProgressRenderableResponseContent[] {15let refIdPool = 0;1617const result: IChatProgressRenderableResponseContent[] = [];18for (const item of response) {19const previousItemIndex = findLastIdx(result, p => p.kind !== 'textEditGroup' && p.kind !== 'undoStop');20const previousItem = result[previousItemIndex];21if (item.kind === 'inlineReference') {22let label: string | undefined = item.name;23if (!label) {24if (URI.isUri(item.inlineReference)) {25label = basename(item.inlineReference);26} else if ('name' in item.inlineReference) {27label = item.inlineReference.name;28} else {29label = basename(item.inlineReference.uri);30}31}3233const refId = refIdPool++;34const printUri = URI.parse(contentRefUrl).with({ path: String(refId) });35const markdownText = `[${label}](${printUri.toString()})`;3637const annotationMetadata = { [refId]: item };3839if (previousItem?.kind === 'markdownContent') {40const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText));41result[previousItemIndex] = { ...previousItem, content: merged, inlineReferences: { ...annotationMetadata, ...(previousItem.inlineReferences || {}) } };42} else {43result.push({ content: new MarkdownString(markdownText), inlineReferences: annotationMetadata, kind: 'markdownContent' });44}45} else if (item.kind === 'markdownContent' && previousItem?.kind === 'markdownContent' && canMergeMarkdownStrings(previousItem.content, item.content)) {46const merged = appendMarkdownString(previousItem.content, item.content);47result[previousItemIndex] = { ...previousItem, content: merged };48} else if (item.kind === 'markdownVuln') {49const vulnText = encodeURIComponent(JSON.stringify(item.vulnerabilities));50const markdownText = `<vscode_annotation details='${vulnText}'>${item.content.value}</vscode_annotation>`;51if (previousItem?.kind === 'markdownContent') {52// Since this is inside a codeblock, it needs to be merged into the previous markdown content.53const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText));54result[previousItemIndex] = { ...previousItem, content: merged };55} else {56result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' });57}58} else if (item.kind === 'codeblockUri') {59if (previousItem?.kind === 'markdownContent') {60const isEditText = item.isEdit ? ` isEdit` : '';61const markdownText = `<vscode_codeblock_uri${isEditText}>${item.uri.toString()}</vscode_codeblock_uri>`;62const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText));63// delete the previous and append to ensure that we don't reorder the edit before the undo stop containing it64result.splice(previousItemIndex, 1);65result.push({ ...previousItem, content: merged });66}67} else {68result.push(item);69}70}7172return result;73}7475export interface IMarkdownVulnerability {76readonly title: string;77readonly description: string;78readonly range: IRange;79}8081export function annotateVulnerabilitiesInText(response: ReadonlyArray<IChatProgressResponseContent>): readonly IChatMarkdownContent[] {82const result: IChatMarkdownContent[] = [];83for (const item of response) {84const previousItem = result[result.length - 1];85if (item.kind === 'markdownContent') {86if (previousItem?.kind === 'markdownContent') {87result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + item.content.value, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' };88} else {89result.push(item);90}91} else if (item.kind === 'markdownVuln') {92const vulnText = encodeURIComponent(JSON.stringify(item.vulnerabilities));93const markdownText = `<vscode_annotation details='${vulnText}'>${item.content.value}</vscode_annotation>`;94if (previousItem?.kind === 'markdownContent') {95result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + markdownText, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' };96} else {97result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' });98}99}100}101102return result;103}104105export function extractCodeblockUrisFromText(text: string): { uri: URI; isEdit?: boolean; textWithoutResult: string } | undefined {106const match = /<vscode_codeblock_uri( isEdit)?>(.*?)<\/vscode_codeblock_uri>/ms.exec(text);107if (match) {108const [all, isEdit, uriString] = match;109if (uriString) {110const result = URI.parse(uriString);111const textWithoutResult = text.substring(0, match.index) + text.substring(match.index + all.length);112return { uri: result, textWithoutResult, isEdit: !!isEdit };113}114}115return undefined;116}117118export function extractVulnerabilitiesFromText(text: string): { newText: string; vulnerabilities: IMarkdownVulnerability[] } {119const vulnerabilities: IMarkdownVulnerability[] = [];120let newText = text;121let match: RegExpExecArray | null;122while ((match = /<vscode_annotation details='(.*?)'>(.*?)<\/vscode_annotation>/ms.exec(newText)) !== null) {123const [full, details, content] = match;124const start = match.index;125const textBefore = newText.substring(0, start);126const linesBefore = textBefore.split('\n').length - 1;127const linesInside = content.split('\n').length - 1;128129const previousNewlineIdx = textBefore.lastIndexOf('\n');130const startColumn = start - (previousNewlineIdx + 1) + 1;131const endPreviousNewlineIdx = (textBefore + content).lastIndexOf('\n');132const endColumn = start + content.length - (endPreviousNewlineIdx + 1) + 1;133134try {135const vulnDetails: IChatAgentVulnerabilityDetails[] = JSON.parse(decodeURIComponent(details));136vulnDetails.forEach(({ title, description }) => vulnerabilities.push({137title, description, range: { startLineNumber: linesBefore + 1, startColumn, endLineNumber: linesBefore + linesInside + 1, endColumn }138}));139} catch (err) {140// Something went wrong with encoding this text, just ignore it141}142newText = newText.substring(0, start) + content + newText.substring(start + full.length);143}144145return { newText, vulnerabilities };146}147148149