Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/widget/annotations.ts
4780 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
import { findLastIdx } from '../../../../../base/common/arraysFind.js';
6
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
7
import { basename } from '../../../../../base/common/resources.js';
8
import { URI } from '../../../../../base/common/uri.js';
9
import { IRange } from '../../../../../editor/common/core/range.js';
10
import { isLocation } from '../../../../../editor/common/languages.js';
11
import { IChatProgressRenderableResponseContent, IChatProgressResponseContent, appendMarkdownString, canMergeMarkdownStrings } from '../model/chatModel.js';
12
import { IChatAgentVulnerabilityDetails, IChatMarkdownContent } from '../chatService/chatService.js';
13
14
export const contentRefUrl = 'http://_vscodecontentref_'; // must be lowercase for URI
15
16
export function annotateSpecialMarkdownContent(response: Iterable<IChatProgressResponseContent>): IChatProgressRenderableResponseContent[] {
17
let refIdPool = 0;
18
19
const result: IChatProgressRenderableResponseContent[] = [];
20
for (const item of response) {
21
const previousItemIndex = findLastIdx(result, p => p.kind !== 'textEditGroup' && p.kind !== 'undoStop');
22
const previousItem = result[previousItemIndex];
23
if (item.kind === 'inlineReference') {
24
let label: string | undefined = item.name;
25
if (!label) {
26
if (URI.isUri(item.inlineReference)) {
27
label = basename(item.inlineReference);
28
} else if (isLocation(item.inlineReference)) {
29
label = basename(item.inlineReference.uri);
30
} else {
31
label = item.inlineReference.name;
32
}
33
}
34
35
const refId = refIdPool++;
36
const printUri = URI.parse(contentRefUrl).with({ path: String(refId) });
37
const markdownText = `[${label}](${printUri.toString()})`;
38
39
const annotationMetadata = { [refId]: item };
40
41
if (previousItem?.kind === 'markdownContent') {
42
const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText));
43
result[previousItemIndex] = { ...previousItem, content: merged, inlineReferences: { ...annotationMetadata, ...(previousItem.inlineReferences || {}) } };
44
} else {
45
result.push({ content: new MarkdownString(markdownText), inlineReferences: annotationMetadata, kind: 'markdownContent' });
46
}
47
} else if (item.kind === 'markdownContent' && previousItem?.kind === 'markdownContent' && canMergeMarkdownStrings(previousItem.content, item.content)) {
48
const merged = appendMarkdownString(previousItem.content, item.content);
49
result[previousItemIndex] = { ...previousItem, content: merged };
50
} else if (item.kind === 'markdownVuln') {
51
const vulnText = encodeURIComponent(JSON.stringify(item.vulnerabilities));
52
const markdownText = `<vscode_annotation details='${vulnText}'>${item.content.value}</vscode_annotation>`;
53
if (previousItem?.kind === 'markdownContent') {
54
// Since this is inside a codeblock, it needs to be merged into the previous markdown content.
55
const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText));
56
result[previousItemIndex] = { ...previousItem, content: merged };
57
} else {
58
result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' });
59
}
60
} else if (item.kind === 'codeblockUri') {
61
if (previousItem?.kind === 'markdownContent') {
62
const isEditText = item.isEdit ? ` isEdit` : '';
63
const markdownText = `<vscode_codeblock_uri${isEditText}>${item.uri.toString()}</vscode_codeblock_uri>`;
64
const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText));
65
// delete the previous and append to ensure that we don't reorder the edit before the undo stop containing it
66
result.splice(previousItemIndex, 1);
67
result.push({ ...previousItem, content: merged });
68
}
69
} else {
70
result.push(item);
71
}
72
}
73
74
return result;
75
}
76
77
export interface IMarkdownVulnerability {
78
readonly title: string;
79
readonly description: string;
80
readonly range: IRange;
81
}
82
83
export function annotateVulnerabilitiesInText(response: ReadonlyArray<IChatProgressResponseContent>): readonly IChatMarkdownContent[] {
84
const result: IChatMarkdownContent[] = [];
85
for (const item of response) {
86
const previousItem = result[result.length - 1];
87
if (item.kind === 'markdownContent') {
88
if (previousItem?.kind === 'markdownContent') {
89
result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + item.content.value, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' };
90
} else {
91
result.push(item);
92
}
93
} else if (item.kind === 'markdownVuln') {
94
const vulnText = encodeURIComponent(JSON.stringify(item.vulnerabilities));
95
const markdownText = `<vscode_annotation details='${vulnText}'>${item.content.value}</vscode_annotation>`;
96
if (previousItem?.kind === 'markdownContent') {
97
result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + markdownText, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' };
98
} else {
99
result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' });
100
}
101
}
102
}
103
104
return result;
105
}
106
107
export function extractCodeblockUrisFromText(text: string): { uri: URI; isEdit?: boolean; textWithoutResult: string } | undefined {
108
const match = /<vscode_codeblock_uri( isEdit)?>(.*?)<\/vscode_codeblock_uri>/ms.exec(text);
109
if (match) {
110
const [all, isEdit, uriString] = match;
111
if (uriString) {
112
const result = URI.parse(uriString);
113
const textWithoutResult = text.substring(0, match.index) + text.substring(match.index + all.length);
114
return { uri: result, textWithoutResult, isEdit: !!isEdit };
115
}
116
}
117
return undefined;
118
}
119
120
export function hasCodeblockUriTag(text: string): boolean {
121
return text.includes('<vscode_codeblock_uri');
122
}
123
124
export function extractVulnerabilitiesFromText(text: string): { newText: string; vulnerabilities: IMarkdownVulnerability[] } {
125
const vulnerabilities: IMarkdownVulnerability[] = [];
126
let newText = text;
127
let match: RegExpExecArray | null;
128
while ((match = /<vscode_annotation details='(.*?)'>(.*?)<\/vscode_annotation>/ms.exec(newText)) !== null) {
129
const [full, details, content] = match;
130
const start = match.index;
131
const textBefore = newText.substring(0, start);
132
const linesBefore = textBefore.split('\n').length - 1;
133
const linesInside = content.split('\n').length - 1;
134
135
const previousNewlineIdx = textBefore.lastIndexOf('\n');
136
const startColumn = start - (previousNewlineIdx + 1) + 1;
137
const endPreviousNewlineIdx = (textBefore + content).lastIndexOf('\n');
138
const endColumn = start + content.length - (endPreviousNewlineIdx + 1) + 1;
139
140
try {
141
const vulnDetails: IChatAgentVulnerabilityDetails[] = JSON.parse(decodeURIComponent(details));
142
vulnDetails.forEach(({ title, description }) => vulnerabilities.push({
143
title, description, range: { startLineNumber: linesBefore + 1, startColumn, endLineNumber: linesBefore + linesInside + 1, endColumn }
144
}));
145
} catch (err) {
146
// Something went wrong with encoding this text, just ignore it
147
}
148
newText = newText.substring(0, start) + content + newText.substring(start + full.length);
149
}
150
151
return { newText, vulnerabilities };
152
}
153
154