Path: blob/main/src/vs/workbench/contrib/chat/common/widget/annotations.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*--------------------------------------------------------------------------------------------*/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 { isLocation } from '../../../../../editor/common/languages.js';10import { IChatProgressRenderableResponseContent, IChatProgressResponseContent, appendMarkdownString, canMergeMarkdownStrings } from '../model/chatModel.js';11import { IChatAgentVulnerabilityDetails, IChatMarkdownContent } from '../chatService/chatService.js';1213export const contentRefUrl = 'http://_vscodecontentref_'; // must be lowercase for URI1415export function annotateSpecialMarkdownContent(response: Iterable<IChatProgressResponseContent>): IChatProgressRenderableResponseContent[] {16let refIdPool = 0;1718const result: IChatProgressRenderableResponseContent[] = [];19for (const item of response) {20const previousItemIndex = findLastIdx(result, p => p.kind !== 'textEditGroup' && p.kind !== 'undoStop');21const previousItem = result[previousItemIndex];22if (item.kind === 'inlineReference') {23let label: string | undefined = item.name;24if (!label) {25if (URI.isUri(item.inlineReference)) {26label = basename(item.inlineReference);27} else if (isLocation(item.inlineReference)) {28label = basename(item.inlineReference.uri);29} else {30label = item.inlineReference.name;31}32}3334const refId = refIdPool++;35const printUri = URI.parse(contentRefUrl).with({ path: String(refId) });36const markdownText = `[${label}](${printUri.toString()})`;3738const annotationMetadata = { [refId]: item };3940if (previousItem?.kind === 'markdownContent') {41const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText));42result[previousItemIndex] = { ...previousItem, content: merged, inlineReferences: { ...annotationMetadata, ...(previousItem.inlineReferences || {}) } };43} else {44result.push({ content: new MarkdownString(markdownText), inlineReferences: annotationMetadata, kind: 'markdownContent' });45}46} else if (item.kind === 'markdownContent' && previousItem?.kind === 'markdownContent' && canMergeMarkdownStrings(previousItem.content, item.content)) {47const merged = appendMarkdownString(previousItem.content, item.content);48result[previousItemIndex] = { ...previousItem, content: merged };49} else if (item.kind === 'markdownVuln') {50const vulnText = encodeURIComponent(JSON.stringify(item.vulnerabilities));51const markdownText = `<vscode_annotation details='${vulnText}'>${item.content.value}</vscode_annotation>`;52if (previousItem?.kind === 'markdownContent') {53// Since this is inside a codeblock, it needs to be merged into the previous markdown content.54const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText));55result[previousItemIndex] = { ...previousItem, content: merged };56} else {57result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' });58}59} else if (item.kind === 'codeblockUri') {60if (previousItem?.kind === 'markdownContent') {61const isEditText = item.isEdit ? ` isEdit` : '';62const markdownText = `<vscode_codeblock_uri${isEditText}>${item.uri.toString()}</vscode_codeblock_uri>`;63const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText));64// delete the previous and append to ensure that we don't reorder the edit before the undo stop containing it65result.splice(previousItemIndex, 1);66result.push({ ...previousItem, content: merged });67}68} else {69result.push(item);70}71}7273return result;74}7576export interface IMarkdownVulnerability {77readonly title: string;78readonly description: string;79readonly range: IRange;80}8182export function annotateVulnerabilitiesInText(response: ReadonlyArray<IChatProgressResponseContent>): readonly IChatMarkdownContent[] {83const result: IChatMarkdownContent[] = [];84for (const item of response) {85const previousItem = result[result.length - 1];86if (item.kind === 'markdownContent') {87if (previousItem?.kind === 'markdownContent') {88result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + item.content.value, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' };89} else {90result.push(item);91}92} else if (item.kind === 'markdownVuln') {93const vulnText = encodeURIComponent(JSON.stringify(item.vulnerabilities));94const markdownText = `<vscode_annotation details='${vulnText}'>${item.content.value}</vscode_annotation>`;95if (previousItem?.kind === 'markdownContent') {96result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + markdownText, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' };97} else {98result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' });99}100}101}102103return result;104}105106export function extractCodeblockUrisFromText(text: string): { uri: URI; isEdit?: boolean; textWithoutResult: string } | undefined {107const match = /<vscode_codeblock_uri( isEdit)?>(.*?)<\/vscode_codeblock_uri>/ms.exec(text);108if (match) {109const [all, isEdit, uriString] = match;110if (uriString) {111const result = URI.parse(uriString);112const textWithoutResult = text.substring(0, match.index) + text.substring(match.index + all.length);113return { uri: result, textWithoutResult, isEdit: !!isEdit };114}115}116return undefined;117}118119export function hasCodeblockUriTag(text: string): boolean {120return text.includes('<vscode_codeblock_uri');121}122123export function extractVulnerabilitiesFromText(text: string): { newText: string; vulnerabilities: IMarkdownVulnerability[] } {124const vulnerabilities: IMarkdownVulnerability[] = [];125let newText = text;126let match: RegExpExecArray | null;127while ((match = /<vscode_annotation details='(.*?)'>(.*?)<\/vscode_annotation>/ms.exec(newText)) !== null) {128const [full, details, content] = match;129const start = match.index;130const textBefore = newText.substring(0, start);131const linesBefore = textBefore.split('\n').length - 1;132const linesInside = content.split('\n').length - 1;133134const previousNewlineIdx = textBefore.lastIndexOf('\n');135const startColumn = start - (previousNewlineIdx + 1) + 1;136const endPreviousNewlineIdx = (textBefore + content).lastIndexOf('\n');137const endColumn = start + content.length - (endPreviousNewlineIdx + 1) + 1;138139try {140const vulnDetails: IChatAgentVulnerabilityDetails[] = JSON.parse(decodeURIComponent(details));141vulnDetails.forEach(({ title, description }) => vulnerabilities.push({142title, description, range: { startLineNumber: linesBefore + 1, startColumn, endLineNumber: linesBefore + linesInside + 1, endColumn }143}));144} catch (err) {145// Something went wrong with encoding this text, just ignore it146}147newText = newText.substring(0, start) + content + newText.substring(start + full.length);148}149150return { newText, vulnerabilities };151}152153154