Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompts/node/inline/diagnosticsContext.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
import { BasePromptElementProps, PromptElement, PromptReference, PromptSizing } from '@vscode/prompt-tsx';
6
import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';
7
import { IIgnoreService } from '../../../../platform/ignore/common/ignoreService';
8
import { ILogService } from '../../../../platform/log/common/logService';
9
import { IParserService, treeSitterOffsetRangeToVSCodeRange, treeSitterToVSCodeRange, vscodeToTreeSitterOffsetRange, vscodeToTreeSitterRange } from '../../../../platform/parser/node/parserService';
10
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';
11
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
12
import { Diagnostic, Location, Range, Uri } from '../../../../vscodeTypes';
13
import { asyncComputeWithTimeBudget } from '../../../context/node/resolvers/selectionContextHelpers';
14
import { IDocumentContext } from '../../../prompt/node/documentContext';
15
import { Tag } from '../base/tag';
16
import { ReferencesAtPosition } from '../panel/referencesAtPosition';
17
import { CodeBlock } from '../panel/safeElements';
18
import { ContextLocation, Cookbook, IFixCookbookService } from './fixCookbookService';
19
20
// #region Diagnostics
21
22
export type DiagnosticContext = Pick<IDocumentContext, 'document' | 'language'>;
23
24
interface DiagnosticsProps extends BasePromptElementProps {
25
readonly documentContext: DiagnosticContext;
26
readonly diagnostics: Diagnostic[];
27
readonly includeRelatedInfos?: boolean;
28
}
29
30
const LINE_CONTEXT_MAX_SIZE = 200;
31
const RELATED_INFO_MAX_SIZE = 300;
32
33
export class Diagnostics extends PromptElement<DiagnosticsProps> {
34
35
constructor(
36
props: DiagnosticsProps,
37
@IIgnoreService private readonly ignoreService: IIgnoreService,
38
@IFixCookbookService private readonly fixCookbookService: IFixCookbookService,
39
) {
40
super(props);
41
}
42
43
async render(state: void, sizing: PromptSizing) {
44
const { diagnostics, documentContext } = this.props;
45
const isIgnored = await this.ignoreService.isCopilotIgnored(documentContext.document.uri);
46
if (isIgnored) {
47
return <ignoredFiles value={[documentContext.document.uri]} />;
48
}
49
50
return (
51
diagnostics.length > 0 &&
52
<>
53
{
54
diagnostics.map((d, idx) => {
55
const cookbook = this.fixCookbookService.getCookbook(documentContext.language.languageId, d);
56
return <>
57
<DiagnosticDescription diagnostic={d} cookbook={cookbook} maxLength={LINE_CONTEXT_MAX_SIZE} documentContext={documentContext} />
58
{this.props.includeRelatedInfos !== false && <DiagnosticRelatedInfo diagnostic={d} cookbook={cookbook} document={documentContext.document} />}
59
<DiagnosticSuggestedFix cookbook={cookbook} />
60
</>;
61
})
62
}
63
</>
64
);
65
}
66
}
67
// #endregion Diagnostics
68
69
// #region DiagnosticDescription
70
71
interface DiagnosticDescriptionProps extends BasePromptElementProps {
72
readonly documentContext: DiagnosticContext;
73
readonly diagnostic: Diagnostic;
74
readonly cookbook?: Cookbook;
75
readonly maxLength: number;
76
}
77
78
79
class DiagnosticDescription extends PromptElement<DiagnosticDescriptionProps> {
80
render(state: void, sizing: PromptSizing) {
81
const d = this.props.diagnostic;
82
const document = this.props.documentContext.document;
83
const range = d.range;
84
const content = document.getText(new Range(range.start.line, 0, range.end.line + 1, 0)).trimEnd();
85
const code = (content.length > this.props.maxLength) ?
86
content.slice(0, this.props.maxLength) + ' (truncated…)' :
87
content;
88
return <>
89
{code
90
? <>This code at line {range.start.line + 1}<br />
91
<CodeBlock code={code} uri={document.uri} shouldTrim={false} /><br /></>
92
: <>At line {range.start.line + 1}<br /></>}
93
has the problem reported:<br />
94
<Tag name='compileError'>
95
{d.message}
96
</Tag>
97
</>;
98
}
99
}
100
101
// #endregion DiagnosticDescription
102
103
// #region DiagnosticRelatedInfo
104
105
interface DiagnosticRelatedInfoProps extends BasePromptElementProps {
106
readonly diagnostic: Diagnostic;
107
readonly cookbook: Cookbook;
108
readonly document: TextDocumentSnapshot;
109
}
110
111
type RelatedInfo = {
112
readonly content: string;
113
readonly uri: Uri;
114
readonly range: Range;
115
};
116
117
interface DiagnosticRelatedInfoState {
118
readonly infos: RelatedInfo[];
119
readonly definitionRanges: Range[];
120
readonly ignoredFiles: Uri[];
121
}
122
123
export class DiagnosticRelatedInfo extends PromptElement<DiagnosticRelatedInfoProps> {
124
125
126
constructor(
127
props: DiagnosticRelatedInfoProps,
128
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
129
@IParserService private readonly parserService: IParserService,
130
@IIgnoreService private readonly ignoreService: IIgnoreService,
131
@ILogService private readonly logService: ILogService,
132
@ITelemetryService private readonly telemetryService: ITelemetryService,
133
134
) {
135
super(props);
136
}
137
138
async render(_state: void, sizing: PromptSizing) {
139
const { infos, ignoredFiles, definitionRanges } = await this.getRelatedInfos();
140
if (!infos.length && !definitionRanges.length) {
141
return <ignoredFiles value={ignoredFiles} />;
142
}
143
return <>
144
This diagnostic has some related code:<br />
145
{
146
infos.map(info => <CodeBlock code={info.content} uri={info.uri} references={[new PromptReference(new Location(info.uri, info.range))]} includeFilepath={true} />)
147
}
148
{
149
definitionRanges.map(range => <ReferencesAtPosition document={this.props.document} position={range.start} />)
150
}
151
<ignoredFiles value={ignoredFiles} />
152
</>;
153
}
154
155
private async getRelatedInfos(): Promise<DiagnosticRelatedInfoState> {
156
const infos: RelatedInfo[] = [];
157
const definitionRanges: Range[] = [];
158
const ignoredFiles: Uri[] = [];
159
const diagnostic = this.props.diagnostic;
160
161
if (diagnostic.relatedInformation) {
162
for (const relatedInformation of diagnostic.relatedInformation) {
163
try {
164
const location = relatedInformation.location;
165
if (await this.ignoreService.isCopilotIgnored(location.uri)) {
166
ignoredFiles.push(location.uri);
167
continue;
168
}
169
const document = await this.workspaceService.openTextDocument(location.uri);
170
const locationRange = location.range;
171
const treeSitterAST = this.parserService.getTreeSitterAST(document);
172
let relatedCodeText: string | undefined;
173
if (treeSitterAST) {
174
const treeSitterLocationRange = vscodeToTreeSitterRange(locationRange);
175
const rangeOfInterest = await treeSitterAST.getCoarseParentScope(treeSitterLocationRange);
176
relatedCodeText = document.getText(treeSitterToVSCodeRange(rangeOfInterest));
177
}
178
if (!relatedCodeText || relatedCodeText.length > RELATED_INFO_MAX_SIZE) {
179
relatedCodeText = document.getText(locationRange);
180
}
181
if (relatedCodeText.length <= RELATED_INFO_MAX_SIZE) {
182
infos.push({ content: relatedCodeText, uri: location.uri, range: location.range });
183
}
184
} catch (e) {
185
// ignore
186
}
187
}
188
}
189
const definitionLocations = this.props.cookbook.additionalContext();
190
for (const location of definitionLocations) {
191
switch (location) {
192
case ContextLocation.ParentCallDefinition: {
193
const treeSitterAST = this.parserService.getTreeSitterAST(this.props.document);
194
if (treeSitterAST) {
195
const diagnosticOffsetRange = vscodeToTreeSitterOffsetRange(this.props.diagnostic.range, this.props.document);
196
const expressionInfos = await asyncComputeWithTimeBudget(this.logService, this.telemetryService, this.props.document, 500, () => treeSitterAST.getCallExpressions(diagnosticOffsetRange), []);
197
for (const expressionInfo of expressionInfos) {
198
const expressionRange = treeSitterOffsetRangeToVSCodeRange(this.props.document, expressionInfo);
199
definitionRanges.push(expressionRange);
200
}
201
}
202
break;
203
}
204
case ContextLocation.DefinitionAtLocation: {
205
definitionRanges.push(this.props.diagnostic.range);
206
break;
207
}
208
}
209
}
210
return { infos, definitionRanges, ignoredFiles };
211
}
212
}
213
214
// #endregion DiagnosticRelatedInfo
215
216
// #region DiagnosticSuggestedFix
217
218
export interface DiagnosticSuggestedFixProps extends BasePromptElementProps {
219
readonly cookbook: Cookbook;
220
}
221
222
export class DiagnosticSuggestedFix extends PromptElement<DiagnosticSuggestedFixProps> {
223
224
render(state: void, sizing: PromptSizing) {
225
const suggestedFixes = this.props.cookbook.fixes;
226
if (suggestedFixes.length) {
227
const prompt = suggestedFixes[0];
228
return <Tag name='suggestedFix'>{prompt.title + prompt.message}</Tag>;
229
}
230
return null;
231
}
232
}
233
234
// #endregion DiagnosticSuggestedFix
235
236