Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompts/node/panel/fileVariable.tsx
13405 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
6
import * as l10n from '@vscode/l10n';
7
import { BasePromptElementProps, ChatResponseReferencePartStatusKind, Document, Image, PromptElement, PromptReference, PromptSizing } from '@vscode/prompt-tsx';
8
import { UserMessage } from '@vscode/prompt-tsx/dist/base/promptElements';
9
import { AbstractDocumentWithLanguageId } from '../../../../platform/editing/common/abstractText';
10
import { NotebookDocumentSnapshot } from '../../../../platform/editing/common/notebookDocumentSnapshot';
11
import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';
12
import { modelSupportsPDFDocuments } from '../../../../platform/endpoint/common/chatModelCapabilities';
13
import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';
14
import { IIgnoreService } from '../../../../platform/ignore/common/ignoreService';
15
import { IAlternativeNotebookContentService } from '../../../../platform/notebook/common/alternativeContent';
16
import { INotebookService } from '../../../../platform/notebook/common/notebookService';
17
import { IPromptPathRepresentationService } from '../../../../platform/prompts/common/promptPathRepresentationService';
18
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
19
import { getNotebookAndCellFromUri, getNotebookCellOutput } from '../../../../util/common/notebooks';
20
import { isUri } from '../../../../util/common/types';
21
import { CachedFunction } from '../../../../util/vs/base/common/cache';
22
import { Schemas } from '../../../../util/vs/base/common/network';
23
import { basename } from '../../../../util/vs/base/common/resources';
24
import { splitLines } from '../../../../util/vs/base/common/strings';
25
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
26
import { Location, Position, Range, Uri } from '../../../../vscodeTypes';
27
import { IPromptEndpoint } from '../base/promptRenderer';
28
import { Tag } from '../base/tag';
29
import { SummarizedDocumentLineNumberStyle } from '../inline/summarizedDocument/implementation';
30
import { ICostFnFactory, ProjectedDocument, RemovableNode } from '../inline/summarizedDocument/summarizeDocument';
31
import { DocumentSummarizer, NotebookDocumentSummarizer } from '../inline/summarizedDocument/summarizeDocumentHelpers';
32
import { BinaryFileHexdump, hexdumpIfBinary } from './binaryFileHexdump';
33
import { CodeBlock } from './safeElements';
34
35
export interface FileVariableProps extends BasePromptElementProps {
36
variableName: string;
37
variableValue: Uri | Location;
38
filePathMode?: FilePathMode;
39
lineNumberStyle?: SummarizedDocumentLineNumberStyle | 'legacy';
40
alwaysIncludeSummary?: boolean;
41
omitReferences?: boolean;
42
description?: string;
43
/**
44
* If true, file contents are omitted and only the file path is included.
45
*/
46
omitContents?: boolean;
47
}
48
49
export class FileVariable extends PromptElement<FileVariableProps, unknown> {
50
constructor(
51
props: FileVariableProps,
52
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
53
@IIgnoreService private readonly ignoreService: IIgnoreService,
54
@IFileSystemService private readonly fileService: IFileSystemService,
55
@INotebookService private readonly notebookService: INotebookService,
56
@IAlternativeNotebookContentService private readonly alternativeNotebookContent: IAlternativeNotebookContentService,
57
@IPromptEndpoint private readonly promptEndpoint: IPromptEndpoint,
58
@IPromptPathRepresentationService private readonly promptPathRepresentationService: IPromptPathRepresentationService,
59
) {
60
super(props);
61
}
62
63
override async render(_state: unknown, sizing: PromptSizing) {
64
const uri = 'uri' in this.props.variableValue ? this.props.variableValue.uri : this.props.variableValue;
65
66
if (await this.ignoreService.isCopilotIgnored(uri)) {
67
return <ignoredFiles value={[uri]} />;
68
}
69
70
if (uri.scheme === 'untitled' && !this.workspaceService.textDocuments.some(doc => doc.uri.toString() === uri.toString())) {
71
// A previously open untitled document that isn't open anymore- opening it would open an empty text editor
72
return;
73
}
74
75
// When omitContents is true, just render the file path without reading the file contents
76
if (this.props.omitContents) {
77
const filePath = this.promptPathRepresentationService.getFilePath(uri);
78
const attrs: Record<string, string> = {};
79
if (this.props.variableName) {
80
attrs.id = this.props.variableName;
81
}
82
attrs.filePath = filePath;
83
return (
84
<Tag name='attachment' attrs={attrs} />
85
);
86
}
87
88
if (/\.(png|jpg|jpeg|bmp|gif|webp)$/i.test(uri.path)) {
89
const options = { status: { description: l10n.t("{0} does not support images.", this.promptEndpoint.model), kind: ChatResponseReferencePartStatusKind.Omitted } };
90
if (this.props.omitReferences) {
91
return;
92
}
93
94
if (!this.promptEndpoint.supportsVision) {
95
return (
96
<>
97
<references value={[new PromptReference(this.props.variableName ? { variableName: this.props.variableName, value: uri } : uri, undefined, options)]} />
98
</>);
99
}
100
101
try {
102
const buffer = await this.fileService.readFile(uri);
103
const base64string = Buffer.from(buffer).toString('base64');
104
return (
105
<UserMessage priority={0}>
106
<Image src={base64string} detail={'high'} />
107
<references value={[new PromptReference(this.props.variableName ? { variableName: this.props.variableName, value: uri } : uri, undefined, options)]} />
108
</UserMessage>
109
110
);
111
112
} catch (err) {
113
return (
114
<>
115
<references value={[new PromptReference(this.props.variableName ? { variableName: this.props.variableName, value: uri } : uri, undefined, options)]} />
116
</>);
117
}
118
119
}
120
121
if (/\.pdf$/i.test(uri.path)) {
122
if (!this.promptEndpoint.supportsVision || !modelSupportsPDFDocuments(this.promptEndpoint)) {
123
if (this.props.omitReferences) {
124
return;
125
}
126
const options = { status: { description: l10n.t("{0} does not support PDF documents.", this.promptEndpoint.model), kind: ChatResponseReferencePartStatusKind.Omitted } };
127
return (
128
<>
129
<references value={[new PromptReference(this.props.variableName ? { variableName: this.props.variableName, value: uri } : uri, undefined, options)]} />
130
</>);
131
}
132
133
try {
134
const buffer = await this.fileService.readFile(uri);
135
136
// Validate PDF magic bytes (%PDF = 0x25 0x50 0x44 0x46)
137
if (buffer.length < 4 || buffer[0] !== 0x25 || buffer[1] !== 0x50 || buffer[2] !== 0x44 || buffer[3] !== 0x46) {
138
if (this.props.omitReferences) {
139
return;
140
}
141
const options = { status: { description: l10n.t("File is not a valid PDF."), kind: ChatResponseReferencePartStatusKind.Omitted } };
142
return (
143
<>
144
<references value={[new PromptReference(this.props.variableName ? { variableName: this.props.variableName, value: uri } : uri, undefined, options)]} />
145
</>);
146
}
147
148
const base64string = Buffer.from(buffer).toString('base64');
149
return (
150
<UserMessage priority={0}>
151
<Document data={base64string} mediaType='application/pdf' />
152
{!this.props.omitReferences && <references value={[new PromptReference(this.props.variableName ? { variableName: this.props.variableName, value: uri } : uri)]} />}
153
</UserMessage>
154
);
155
} catch (err) {
156
if (this.props.omitReferences) {
157
return;
158
}
159
const options = { status: { description: l10n.t("Failed to read PDF file."), kind: ChatResponseReferencePartStatusKind.Omitted } };
160
return (
161
<>
162
<references value={[new PromptReference(this.props.variableName ? { variableName: this.props.variableName, value: uri } : uri, undefined, options)]} />
163
</>);
164
}
165
}
166
167
const binary = await hexdumpIfBinary(this.fileService, uri, { openTextDocuments: this.workspaceService.textDocuments });
168
if (binary) {
169
return <BinaryFileHexdump uri={uri} data={binary.data} variableName={this.props.variableName} description={this.props.description} omitReferences={this.props.omitReferences} />;
170
}
171
172
let range = isUri(this.props.variableValue) ? undefined : this.props.variableValue.range;
173
let documentSnapshot: TextDocumentSnapshot | NotebookDocumentSnapshot;
174
let fileUri: Uri = uri;
175
176
if (uri.scheme === Schemas.vscodeNotebookCellOutput) {
177
// add exception for notebook cell output with image mime type in unsupported endpoint
178
const items = getNotebookCellOutput(uri, this.workspaceService.notebookDocuments);
179
if (!items) {
180
return;
181
}
182
const outputCell = items[2];
183
if (outputCell.items.length > 0 && outputCell.items[0].mime.startsWith('image/') && !this.promptEndpoint.supportsVision) {
184
const options = { status: { description: l10n.t("{0} does not support images.", this.promptEndpoint.model), kind: ChatResponseReferencePartStatusKind.Omitted } };
185
if (this.props.omitReferences) {
186
return;
187
}
188
189
return (
190
<>
191
<references value={[new PromptReference(this.props.variableName ? { variableName: this.props.variableName, value: this.props.variableValue } : this.props.variableValue, undefined, options)]} />
192
</>
193
);
194
}
195
}
196
if (uri.scheme === Schemas.vscodeNotebookCell || uri.scheme === Schemas.vscodeNotebookCellOutput) {
197
const [notebook, cell] = getNotebookAndCellFromUri(uri, this.workspaceService.notebookDocuments);
198
if (!notebook) {
199
return;
200
}
201
fileUri = notebook.uri;
202
if (cell) {
203
const cellRange = new Range(cell.document.lineAt(0).range.start, cell.document.lineAt(cell.document.lineCount - 1).range.end);
204
range = range ?? cellRange;
205
// Ensure the range is within the cell range
206
if (range.start > cellRange.end || range.end < cellRange.start) {
207
range = cellRange;
208
}
209
const altDocument = this.alternativeNotebookContent.create(this.alternativeNotebookContent.getFormat(this.promptEndpoint)).getAlternativeDocument(notebook);
210
//Translate the range to alternative content.
211
range = new Range(altDocument.fromCellPosition(cell, range.start), altDocument.fromCellPosition(cell, range.end));
212
} else {
213
range = undefined;
214
}
215
}
216
try {
217
documentSnapshot = this.notebookService.hasSupportedNotebooks(fileUri) ?
218
await this.workspaceService.openNotebookDocumentAndSnapshot(fileUri, this.alternativeNotebookContent.getFormat(this.promptEndpoint)) :
219
await this.workspaceService.openTextDocumentAndSnapshot(fileUri);
220
} catch (err) {
221
const options = { status: { description: l10n.t('This file could not be read: {0}', err.message), kind: ChatResponseReferencePartStatusKind.Omitted } };
222
if (this.props.omitReferences) {
223
return;
224
}
225
226
return (
227
<>
228
<references value={[new PromptReference(this.props.variableName ? { variableName: this.props.variableName, value: this.props.variableValue } : this.props.variableValue, undefined, options)]} />
229
</>
230
);
231
}
232
233
if ((range && (!this.props.alwaysIncludeSummary || range.isEqual(new Range(new Position(0, 0), documentSnapshot.lineAt(documentSnapshot.lineCount - 1).range.end)))) || /\.(svg)$/i.test(uri.path)) {
234
// Don't summarize if the file is an SVG, since summarization will almost certainly not work as expected
235
return <CodeSelection variableName={this.props.variableName} document={documentSnapshot} range={range} filePathMode={this.props.filePathMode} omitReferences={this.props.omitReferences} description={this.props.description} />;
236
}
237
238
if (range) {
239
const selectionDesc = this.props.description ? this.props.description : ``;
240
const summaryDesc = `User's active file for additional context`;
241
return (
242
<>
243
<CodeSelection variableName={this.props.variableName} document={documentSnapshot} range={range} filePathMode={this.props.filePathMode} omitReferences={this.props.omitReferences} description={selectionDesc} />
244
<CodeSummary flexGrow={1} variableName={''} document={documentSnapshot} range={range} filePathMode={this.props.filePathMode} lineNumberStyle={this.props.lineNumberStyle} omitReferences={this.props.omitReferences} description={summaryDesc} />
245
</>
246
);
247
}
248
249
return <CodeSummary variableName={this.props.variableName} document={documentSnapshot} range={range} filePathMode={this.props.filePathMode} lineNumberStyle={this.props.lineNumberStyle} omitReferences={this.props.omitReferences} description={this.props.description} />;
250
}
251
}
252
253
interface CodeSelectionProps extends BasePromptElementProps {
254
variableName: string;
255
document: TextDocumentSnapshot | NotebookDocumentSnapshot;
256
range?: Range;
257
filePathMode?: FilePathMode;
258
omitReferences?: boolean;
259
description?: string;
260
}
261
262
class CodeSelection extends PromptElement<CodeSelectionProps, unknown> {
263
264
override async render(_state: unknown, sizing: PromptSizing) {
265
const { document, range } = this.props;
266
const { uri } = document;
267
const references = this.props.omitReferences ? undefined : [new PromptReference(range ? new Location(uri, range) : uri)];
268
return (
269
<Tag name='attachment' attrs={this.props.variableName ? { id: this.props.variableName } : undefined} >
270
{this.props.description ? this.props.description + ':\n' : ''}
271
Excerpt from {basename(uri)}{range ? `, lines ${range.start.line + 1} to ${range.end.line + 1}` : ''}:
272
<CodeBlock includeFilepath={this.props.filePathMode === FilePathMode.AsComment} languageId={document.languageId} uri={uri} references={references} code={document.getText(range)} />
273
</Tag >
274
);
275
}
276
}
277
278
export enum FilePathMode {
279
AsAttribute,
280
AsComment,
281
None
282
}
283
284
interface CodeSummaryProps extends BasePromptElementProps {
285
variableName: string;
286
document: TextDocumentSnapshot | NotebookDocumentSnapshot;
287
range?: Range;
288
filePathMode?: FilePathMode;
289
lineNumberStyle?: SummarizedDocumentLineNumberStyle | 'legacy';
290
omitReferences?: boolean;
291
description?: string;
292
}
293
294
class CodeSummary extends PromptElement<CodeSummaryProps, unknown> {
295
296
constructor(
297
props: CodeSummaryProps,
298
@IInstantiationService private readonly instantiationService: IInstantiationService,
299
@IPromptPathRepresentationService private readonly _promptPathRepresentationService: IPromptPathRepresentationService,
300
) {
301
super(props);
302
}
303
304
override async render(_state: unknown, sizing: PromptSizing) {
305
const { document, range } = this.props;
306
const { uri } = document;
307
const lineNumberStyle = this.props.lineNumberStyle === 'legacy' ? undefined : this.props.lineNumberStyle;
308
const summarized = document instanceof TextDocumentSnapshot ?
309
await this.instantiationService.createInstance(DocumentSummarizer).summarizeDocument(document, undefined, range, sizing.tokenBudget, {
310
costFnOverride: fileVariableCostFn,
311
lineNumberStyle,
312
}) :
313
await this.instantiationService.createInstance(NotebookDocumentSummarizer).summarizeDocument(document, undefined, range, sizing.tokenBudget, {
314
costFnOverride: fileVariableCostFn,
315
lineNumberStyle,
316
});
317
318
const code = this.props.lineNumberStyle === 'legacy' ? this.includeLineNumbers(summarized) : summarized.text;
319
const promptReferenceOptions = !summarized.isOriginal
320
? { status: { description: l10n.t('Part of this file was not sent to the model due to context window limitations. Try attaching specific selections from your file instead.'), kind: 2 } }
321
: undefined;
322
const references = this.props.omitReferences ? undefined : [new PromptReference(uri, undefined, promptReferenceOptions)];
323
const attrs: Record<string, string> = {};
324
if (this.props.variableName) {
325
attrs.id = this.props.variableName;
326
}
327
if (!summarized.isOriginal) {
328
attrs.isSummarized = 'true';
329
}
330
if (this.props.filePathMode === FilePathMode.AsAttribute) {
331
attrs.filePath = this._promptPathRepresentationService.getFilePath(uri);
332
}
333
return (
334
<Tag name='attachment' attrs={attrs} >
335
{this.props.description ? this.props.description + ':\n' : ''}
336
<CodeBlock includeFilepath={this.props.filePathMode === FilePathMode.AsComment} languageId={document.languageId} uri={uri} references={references} code={code} fence='' />
337
</Tag>
338
);
339
}
340
341
private includeLineNumbers(summarized: ProjectedDocument): string {
342
const lines = splitLines(summarized.text);
343
const lineNumberWidth = lines.length.toString().length;
344
345
return lines.map((line, index) => {
346
let lineNumber: number;
347
if (summarized.isOriginal) {
348
lineNumber = index;
349
} else {
350
const offset = summarized.positionOffsetTransformer.getOffset(new Position(index, 0));
351
const originalPosition = summarized.originalPositionOffsetTransformer.getPosition(summarized.projectBack(offset));
352
lineNumber = originalPosition.line;
353
}
354
return `${(lineNumber + 1).toString().padStart(lineNumberWidth)}: ${line}`;
355
}).join('\n');
356
}
357
}
358
359
export const fileVariableCostFn: ICostFnFactory<AbstractDocumentWithLanguageId> = {
360
createCostFn(doc) {
361
const nodeMultiplier: CachedFunction<RemovableNode, number> = new CachedFunction(node => {
362
if (doc.languageId === 'typescript') {
363
const parentCost = node.parent ? nodeMultiplier.get(node.parent) : 1;
364
const nodeText = node.text.trim();
365
if (nodeText.startsWith('private ')) { return parentCost * 1.1; }
366
if (nodeText.startsWith('export ') || nodeText.startsWith('public ')) { return parentCost * 0.9; }
367
}
368
return 1;
369
});
370
371
return (node, currentCost) => {
372
if (!node) {
373
return currentCost;
374
}
375
if (node.kind === 'import_statement') {
376
return 1000; // Include import statements last
377
}
378
const m = nodeMultiplier.get(node);
379
return currentCost * m;
380
};
381
},
382
};
383
384