Path: blob/main/extensions/copilot/src/extension/prompts/node/inline/diagnosticsContext.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*--------------------------------------------------------------------------------------------*/4import { BasePromptElementProps, PromptElement, PromptReference, PromptSizing } from '@vscode/prompt-tsx';5import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';6import { IIgnoreService } from '../../../../platform/ignore/common/ignoreService';7import { ILogService } from '../../../../platform/log/common/logService';8import { IParserService, treeSitterOffsetRangeToVSCodeRange, treeSitterToVSCodeRange, vscodeToTreeSitterOffsetRange, vscodeToTreeSitterRange } from '../../../../platform/parser/node/parserService';9import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';10import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';11import { Diagnostic, Location, Range, Uri } from '../../../../vscodeTypes';12import { asyncComputeWithTimeBudget } from '../../../context/node/resolvers/selectionContextHelpers';13import { IDocumentContext } from '../../../prompt/node/documentContext';14import { Tag } from '../base/tag';15import { ReferencesAtPosition } from '../panel/referencesAtPosition';16import { CodeBlock } from '../panel/safeElements';17import { ContextLocation, Cookbook, IFixCookbookService } from './fixCookbookService';1819// #region Diagnostics2021export type DiagnosticContext = Pick<IDocumentContext, 'document' | 'language'>;2223interface DiagnosticsProps extends BasePromptElementProps {24readonly documentContext: DiagnosticContext;25readonly diagnostics: Diagnostic[];26readonly includeRelatedInfos?: boolean;27}2829const LINE_CONTEXT_MAX_SIZE = 200;30const RELATED_INFO_MAX_SIZE = 300;3132export class Diagnostics extends PromptElement<DiagnosticsProps> {3334constructor(35props: DiagnosticsProps,36@IIgnoreService private readonly ignoreService: IIgnoreService,37@IFixCookbookService private readonly fixCookbookService: IFixCookbookService,38) {39super(props);40}4142async render(state: void, sizing: PromptSizing) {43const { diagnostics, documentContext } = this.props;44const isIgnored = await this.ignoreService.isCopilotIgnored(documentContext.document.uri);45if (isIgnored) {46return <ignoredFiles value={[documentContext.document.uri]} />;47}4849return (50diagnostics.length > 0 &&51<>52{53diagnostics.map((d, idx) => {54const cookbook = this.fixCookbookService.getCookbook(documentContext.language.languageId, d);55return <>56<DiagnosticDescription diagnostic={d} cookbook={cookbook} maxLength={LINE_CONTEXT_MAX_SIZE} documentContext={documentContext} />57{this.props.includeRelatedInfos !== false && <DiagnosticRelatedInfo diagnostic={d} cookbook={cookbook} document={documentContext.document} />}58<DiagnosticSuggestedFix cookbook={cookbook} />59</>;60})61}62</>63);64}65}66// #endregion Diagnostics6768// #region DiagnosticDescription6970interface DiagnosticDescriptionProps extends BasePromptElementProps {71readonly documentContext: DiagnosticContext;72readonly diagnostic: Diagnostic;73readonly cookbook?: Cookbook;74readonly maxLength: number;75}767778class DiagnosticDescription extends PromptElement<DiagnosticDescriptionProps> {79render(state: void, sizing: PromptSizing) {80const d = this.props.diagnostic;81const document = this.props.documentContext.document;82const range = d.range;83const content = document.getText(new Range(range.start.line, 0, range.end.line + 1, 0)).trimEnd();84const code = (content.length > this.props.maxLength) ?85content.slice(0, this.props.maxLength) + ' (truncated…)' :86content;87return <>88{code89? <>This code at line {range.start.line + 1}<br />90<CodeBlock code={code} uri={document.uri} shouldTrim={false} /><br /></>91: <>At line {range.start.line + 1}<br /></>}92has the problem reported:<br />93<Tag name='compileError'>94{d.message}95</Tag>96</>;97}98}99100// #endregion DiagnosticDescription101102// #region DiagnosticRelatedInfo103104interface DiagnosticRelatedInfoProps extends BasePromptElementProps {105readonly diagnostic: Diagnostic;106readonly cookbook: Cookbook;107readonly document: TextDocumentSnapshot;108}109110type RelatedInfo = {111readonly content: string;112readonly uri: Uri;113readonly range: Range;114};115116interface DiagnosticRelatedInfoState {117readonly infos: RelatedInfo[];118readonly definitionRanges: Range[];119readonly ignoredFiles: Uri[];120}121122export class DiagnosticRelatedInfo extends PromptElement<DiagnosticRelatedInfoProps> {123124125constructor(126props: DiagnosticRelatedInfoProps,127@IWorkspaceService private readonly workspaceService: IWorkspaceService,128@IParserService private readonly parserService: IParserService,129@IIgnoreService private readonly ignoreService: IIgnoreService,130@ILogService private readonly logService: ILogService,131@ITelemetryService private readonly telemetryService: ITelemetryService,132133) {134super(props);135}136137async render(_state: void, sizing: PromptSizing) {138const { infos, ignoredFiles, definitionRanges } = await this.getRelatedInfos();139if (!infos.length && !definitionRanges.length) {140return <ignoredFiles value={ignoredFiles} />;141}142return <>143This diagnostic has some related code:<br />144{145infos.map(info => <CodeBlock code={info.content} uri={info.uri} references={[new PromptReference(new Location(info.uri, info.range))]} includeFilepath={true} />)146}147{148definitionRanges.map(range => <ReferencesAtPosition document={this.props.document} position={range.start} />)149}150<ignoredFiles value={ignoredFiles} />151</>;152}153154private async getRelatedInfos(): Promise<DiagnosticRelatedInfoState> {155const infos: RelatedInfo[] = [];156const definitionRanges: Range[] = [];157const ignoredFiles: Uri[] = [];158const diagnostic = this.props.diagnostic;159160if (diagnostic.relatedInformation) {161for (const relatedInformation of diagnostic.relatedInformation) {162try {163const location = relatedInformation.location;164if (await this.ignoreService.isCopilotIgnored(location.uri)) {165ignoredFiles.push(location.uri);166continue;167}168const document = await this.workspaceService.openTextDocument(location.uri);169const locationRange = location.range;170const treeSitterAST = this.parserService.getTreeSitterAST(document);171let relatedCodeText: string | undefined;172if (treeSitterAST) {173const treeSitterLocationRange = vscodeToTreeSitterRange(locationRange);174const rangeOfInterest = await treeSitterAST.getCoarseParentScope(treeSitterLocationRange);175relatedCodeText = document.getText(treeSitterToVSCodeRange(rangeOfInterest));176}177if (!relatedCodeText || relatedCodeText.length > RELATED_INFO_MAX_SIZE) {178relatedCodeText = document.getText(locationRange);179}180if (relatedCodeText.length <= RELATED_INFO_MAX_SIZE) {181infos.push({ content: relatedCodeText, uri: location.uri, range: location.range });182}183} catch (e) {184// ignore185}186}187}188const definitionLocations = this.props.cookbook.additionalContext();189for (const location of definitionLocations) {190switch (location) {191case ContextLocation.ParentCallDefinition: {192const treeSitterAST = this.parserService.getTreeSitterAST(this.props.document);193if (treeSitterAST) {194const diagnosticOffsetRange = vscodeToTreeSitterOffsetRange(this.props.diagnostic.range, this.props.document);195const expressionInfos = await asyncComputeWithTimeBudget(this.logService, this.telemetryService, this.props.document, 500, () => treeSitterAST.getCallExpressions(diagnosticOffsetRange), []);196for (const expressionInfo of expressionInfos) {197const expressionRange = treeSitterOffsetRangeToVSCodeRange(this.props.document, expressionInfo);198definitionRanges.push(expressionRange);199}200}201break;202}203case ContextLocation.DefinitionAtLocation: {204definitionRanges.push(this.props.diagnostic.range);205break;206}207}208}209return { infos, definitionRanges, ignoredFiles };210}211}212213// #endregion DiagnosticRelatedInfo214215// #region DiagnosticSuggestedFix216217export interface DiagnosticSuggestedFixProps extends BasePromptElementProps {218readonly cookbook: Cookbook;219}220221export class DiagnosticSuggestedFix extends PromptElement<DiagnosticSuggestedFixProps> {222223render(state: void, sizing: PromptSizing) {224const suggestedFixes = this.props.cookbook.fixes;225if (suggestedFixes.length) {226const prompt = suggestedFixes[0];227return <Tag name='suggestedFix'>{prompt.title + prompt.message}</Tag>;228}229return null;230}231}232233// #endregion DiagnosticSuggestedFix234235236