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