Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompts/node/feedback/currentChange.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, PromptPiece, PromptReference, PromptSizing } from '@vscode/prompt-tsx';
6
import type { Position, Selection, TextDocument } from 'vscode';
7
import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';
8
import { IGitExtensionService } from '../../../../platform/git/common/gitExtensionService';
9
import { Repository } from '../../../../platform/git/vscode/git';
10
import { IIgnoreService } from '../../../../platform/ignore/common/ignoreService';
11
import { ILogService } from '../../../../platform/log/common/logService';
12
import { IParserService } from '../../../../platform/parser/node/parserService';
13
import { ServicesAccessor } from '../../../../util/vs/platform/instantiation/common/instantiation';
14
import { Location, Range, Uri } from '../../../../vscodeTypes';
15
import { Tag } from '../base/tag';
16
import { CodeBlock } from '../panel/safeElements';
17
import { SymbolAtCursor } from '../panel/symbolAtCursor';
18
19
export interface CurrentChangeProps extends BasePromptElementProps {
20
input: CurrentChangeInput[];
21
logService: ILogService;
22
}
23
24
export interface CurrentChangeInput {
25
document: TextDocumentSnapshot;
26
relativeDocumentPath: string;
27
change?: Change;
28
selection?: Selection | Range;
29
}
30
31
interface CurrentChangeState extends BasePromptElementProps {
32
input: {
33
input: CurrentChangeInput;
34
hunks: Hunk[];
35
}[];
36
}
37
38
export interface Change {
39
repository: Repository;
40
uri: Uri;
41
hunks: Hunk[];
42
}
43
44
export interface Hunk {
45
range: Range;
46
text: string;
47
}
48
49
interface GitHunk {
50
startDeletedLine: number; // 1-based
51
deletedLines: number;
52
startAddedLine: number; // 1-based
53
addedLines: number;
54
additions: { start: number; length: number }[];
55
diffText: string;
56
}
57
58
interface Text {
59
input: CurrentChangeInput;
60
hunks: Hunk[];
61
tokens: number;
62
}
63
64
export class CurrentChange extends PromptElement<CurrentChangeProps, CurrentChangeState> {
65
constructor(
66
props: CurrentChangeProps,
67
@IParserService private readonly parserService: IParserService,
68
@IIgnoreService private readonly ignoreService: IIgnoreService
69
) {
70
super(props);
71
}
72
73
override async prepare(sizing: PromptSizing): Promise<CurrentChangeState> {
74
const allowed = [];
75
for (const input of this.props.input) {
76
if (!await this.ignoreService.isCopilotIgnored(input.document.uri)) {
77
allowed.push(input);
78
}
79
}
80
81
const texts: Text[] = await Promise.all(allowed.map(async input => {
82
const { document, change, selection } = input;
83
let textAll: string;
84
if (change?.hunks.length) {
85
const first = change.hunks[0];
86
textAll = [
87
first.range.start.line > 0 ? CurrentChange.enumeratedLines(document, 0, first.range.start.line) : '',
88
...change.hunks.map((hunk, i, a) => {
89
const nextHunkLine = i + 1 < a.length ? a[i + 1].range.start.line : document.lineCount;
90
return [
91
CurrentChange.enumeratedChangeLines(hunk.text, hunk.range.start.line), '\n',
92
hunk.range.end.line < nextHunkLine ? CurrentChange.enumeratedLines(document, hunk.range.end.line, nextHunkLine) : '',
93
];
94
}).flat(),
95
].join('');
96
} else if (selection) {
97
const selectionEndLine = selection.end.line + (selection.end.character > 0 ? 1 : 0); // Being line-based.
98
textAll = CurrentChange.enumeratedSelectedLines(document, 0, document.lineCount, selection.start.line, selectionEndLine);
99
} else {
100
textAll = CurrentChange.enumeratedLines(document, 0, document.lineCount);
101
}
102
return {
103
input,
104
hunks: [{
105
range: new Range(0, 0, input.document.lineCount, 0),
106
text: textAll,
107
}],
108
tokens: await sizing.countTokens(textAll),
109
};
110
}));
111
112
let currentTokens = texts.reduce((acc, { tokens }) => acc + tokens, 0);
113
114
this.props.logService.info(`[CurrentChange] Full documents: ${currentTokens} tokens, ${sizing.tokenBudget} budget`);
115
if (currentTokens <= sizing.tokenBudget) {
116
return {
117
input: texts.map(({ input, hunks }) => ({
118
input,
119
hunks,
120
}))
121
};
122
}
123
124
const sorted = texts.slice().sort((a, b) => b.tokens - a.tokens);
125
for (const text of sorted) {
126
const { input, tokens } = text;
127
const { document, change, selection } = input;
128
if (change?.hunks.length) {
129
const definitionHunks = [];
130
let definitionTokens = 0;
131
for (const hunk of change.hunks) {
132
const definition = await SymbolAtCursor.getDefinitionAtRange(this.ignoreService, this.parserService, document, hunk.range, false);
133
if (definition) {
134
const definitionEndLine = definition.range.end.line + (definition.range.end.character > 0 ? 1 : 0); // Being line-based.
135
const hunkEndLine = hunk.range.end.line + (hunk.range.end.character > 0 ? 1 : 0); // Being line-based.
136
const textDefinition = [
137
hunk.range.start.line > definition.range.start.line ? CurrentChange.enumeratedLines(document, definition.range.start.line, hunk.range.start.line) : '',
138
CurrentChange.enumeratedChangeLines(hunk.text, hunk.range.start.line), '\n',
139
definitionEndLine > hunkEndLine ? CurrentChange.enumeratedLines(document, hunkEndLine, definitionEndLine) : '',
140
].join('');
141
definitionHunks.push({
142
range: new Range(Math.min(hunk.range.start.line, definition.range.start.line), 0, Math.max(definitionEndLine, hunkEndLine), 0),
143
text: textDefinition,
144
});
145
definitionTokens += await sizing.countTokens(textDefinition);
146
} else {
147
const hunkText = CurrentChange.enumeratedChangeLines(hunk.text, hunk.range.start.line);
148
const hunkEndLine = hunk.range.end.line + (hunk.range.end.character > 0 ? 1 : 0); // Being line-based.
149
definitionHunks.push({
150
range: new Range(hunk.range.start.line, 0, hunkEndLine, 0),
151
text: hunkText,
152
});
153
definitionTokens += await sizing.countTokens(hunkText);
154
}
155
}
156
text.hunks = definitionHunks;
157
text.tokens = definitionTokens;
158
currentTokens += text.tokens - tokens;
159
} else if (selection) {
160
const definition = await SymbolAtCursor.getDefinitionAtRange(this.ignoreService, this.parserService, document, selection, false);
161
if (definition) {
162
const definitionEndLine = definition.range.end.line + (definition.range.end.character > 0 ? 1 : 0); // Being line-based.
163
const selectionEndLine = selection.end.line + (selection.end.character > 0 ? 1 : 0); // Being line-based.
164
const textDefinition = CurrentChange.enumeratedSelectedLines(document, definition.range.start.line, definitionEndLine, selection.start.line, selectionEndLine);
165
const textDefinitionTokens = await sizing.countTokens(textDefinition);
166
text.hunks = [{
167
range: definition.range,
168
text: textDefinition,
169
}];
170
text.tokens = textDefinitionTokens;
171
currentTokens += text.tokens - tokens;
172
} else {
173
const selectionEndLine = selection.end.line + (selection.end.character > 0 ? 1 : 0); // Being line-based.
174
const hunkText = CurrentChange.enumeratedSelectedLines(document, selection.start.line, selectionEndLine, selection.start.line, selectionEndLine);
175
text.hunks = [{
176
range: new Range(selection.start.line, 0, selectionEndLine, 0),
177
text: hunkText,
178
}];
179
text.tokens = await sizing.countTokens(hunkText);
180
currentTokens += text.tokens - tokens;
181
}
182
} else {
183
text.hunks = [];
184
text.tokens = 0;
185
currentTokens += text.tokens - tokens;
186
}
187
188
this.props.logService.info(`[CurrentChange] Reduced ${input.relativeDocumentPath} to defintions: ${currentTokens} tokens, ${sizing.tokenBudget} budget`);
189
if (currentTokens <= sizing.tokenBudget) {
190
return {
191
input: texts.map(({ input, hunks }) => ({
192
input,
193
hunks,
194
}))
195
};
196
}
197
}
198
199
this.props.logService.info(`[CurrentChange] Still too large: ${currentTokens} tokens, ${sizing.tokenBudget} budget, ${texts.length} inputs`);
200
if (texts.length > 1) {
201
const err = new Error('Split prompt.');
202
(err as any).code = 'split_input';
203
throw err;
204
}
205
return {
206
input: texts.map(({ input, hunks }) => ({
207
input,
208
hunks,
209
}))
210
};
211
}
212
213
override render(state: CurrentChangeState, sizing: PromptSizing): PromptPiece<any, any> | undefined {
214
const input = state.input.filter(i => i.hunks.length > 0);
215
if (!input.length) {
216
return;
217
}
218
219
return (<>
220
<Tag name='currentChange' priority={this.props.priority}>
221
{
222
input.map(input => (<>
223
{input.input.change ? <>
224
Change at cursor:<br />
225
<br />
226
Each line is annotated with the line number in the file.<br />
227
</> : <>
228
Current selection with the selected lines labeled as such:<br />
229
</>}
230
<br />
231
From the file: {input.input.relativeDocumentPath}<br />
232
{
233
input.hunks.map(hunk => (
234
<CodeBlock references={[new PromptReference(new Location(input.input.document.uri, hunk.range))]} uri={input.input.document.uri} code={hunk.text} languageId={`${input.input.document.languageId}/${input.input.relativeDocumentPath}: FROM_LINE: ${hunk.range.start.line + 1} - TO_LINE: ${hunk.range.end.line}`} />
235
))
236
}
237
<br />
238
<br />
239
</>))
240
}
241
</Tag >
242
</>);
243
}
244
245
static async getCurrentChanges(gitExtensionService: IGitExtensionService, group: 'index' | 'workingTree' | 'all'): Promise<Change[]> {
246
const git = gitExtensionService.getExtensionApi();
247
if (!git) {
248
return [];
249
}
250
const changes = await Promise.all(git.repositories.map(async repository => {
251
const stats = await (
252
group === 'index' ? repository.diffIndexWithHEAD() :
253
group === 'workingTree' ? repository.diffWithHEAD() :
254
repository.diffWith('HEAD')
255
);
256
const changes = await Promise.all(stats.map(async change => {
257
const text = await (group === 'index' ? repository.diffIndexWithHEAD(change.uri.fsPath) : repository.diffWithHEAD(change.uri.fsPath));
258
return {
259
repository,
260
uri: change.uri,
261
hunks: CurrentChange.parseDiff(text)
262
.map(hunk => CurrentChange.gitHunkToHunk(hunk))
263
} satisfies Change;
264
}));
265
return changes;
266
}));
267
return changes.flat();
268
}
269
270
static async getCurrentChange(accessor: ServicesAccessor, _document: TextDocument, cursor: Position): Promise<Change | undefined> {
271
const document = TextDocumentSnapshot.create(_document);
272
const gitExtensionService = accessor.get(IGitExtensionService);
273
const git = gitExtensionService.getExtensionApi();
274
if (!git) {
275
return;
276
}
277
278
const repository = git.getRepository(document.uri);
279
if (!repository) {
280
return;
281
}
282
283
const diff = await repository.diffWithHEAD(document.uri.fsPath);
284
if (!diff) {
285
return;
286
}
287
288
const hunks = CurrentChange.parseDiff(diff);
289
const overlappingHunk = hunks.find(hunk => {
290
return hunk.additions.some(addition => {
291
const start = addition.start - 1;
292
const end = start + addition.length - 1;
293
return cursor.line >= start && cursor.line <= end;
294
});
295
});
296
297
if (!overlappingHunk) {
298
return;
299
}
300
301
return {
302
repository,
303
uri: document.uri,
304
hunks: [CurrentChange.gitHunkToHunk(overlappingHunk)]
305
} satisfies Change;
306
}
307
308
static async getChanges(gitExtensionService: IGitExtensionService, repositoryUri: Uri, uri: Uri, diff: string): Promise<Change | undefined> {
309
const git = gitExtensionService.getExtensionApi();
310
if (!git) {
311
return;
312
}
313
314
const hunks = CurrentChange.parseDiff(diff);
315
316
const repository = git.repositories.find(r => r.rootUri.toString().toLowerCase() === repositoryUri.toString().toLowerCase());
317
if (!repository) {
318
return;
319
}
320
321
return {
322
repository,
323
uri,
324
hunks: hunks.map(hunk => CurrentChange.gitHunkToHunk(hunk))
325
} satisfies Change;
326
}
327
328
private static gitHunkToHunk(hunk: GitHunk): Hunk {
329
const range = new Range(hunk.startAddedLine - 1, 0, hunk.startAddedLine - 1 + hunk.addedLines, 0);
330
return {
331
range,
332
text: hunk.diffText,
333
};
334
}
335
336
private static parseDiff(diff: string): GitHunk[] {
337
const hunkTexts = diff.split('\n@@');
338
if (hunkTexts.length && hunkTexts[hunkTexts.length - 1].endsWith('\n')) {
339
hunkTexts[hunkTexts.length - 1] = hunkTexts[hunkTexts.length - 1].slice(0, -1);
340
}
341
const hunks = hunkTexts.map(chunk => {
342
const rangeMatch = chunk.match(/-(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))?/);
343
if (rangeMatch) {
344
let startDeletedLine = parseInt(rangeMatch[1]);
345
const deletedLines = rangeMatch[2] ? parseInt(rangeMatch[2]) : 1;
346
let startAddedLine = parseInt(rangeMatch[3]);
347
const addedLines = rangeMatch[4] ? parseInt(rangeMatch[4]) : 1;
348
349
const additions: { start: number; length: number }[] = [];
350
const lines = chunk.split('\n')
351
.slice(1);
352
let d = 0;
353
let addStart: number | undefined;
354
for (const line of lines) {
355
const ch = line.charAt(0);
356
if (ch === '+') {
357
if (addStart === undefined) {
358
addStart = startAddedLine + d;
359
}
360
d++;
361
} else {
362
if (addStart !== undefined) {
363
additions.push({ start: addStart, length: startAddedLine + d - addStart });
364
addStart = undefined;
365
}
366
if (ch === ' ') {
367
d++;
368
}
369
}
370
}
371
if (addStart !== undefined) {
372
additions.push({ start: addStart, length: startAddedLine + d - addStart });
373
addStart = undefined;
374
}
375
if (startDeletedLine === 0) {
376
startDeletedLine = 1; // when deletedLines is 0?
377
}
378
if (startAddedLine === 0) {
379
startAddedLine = 1; // when addedLines is 0?
380
}
381
return {
382
startDeletedLine, // 1-based
383
deletedLines,
384
startAddedLine, // 1-based
385
addedLines,
386
additions,
387
diffText: lines.join('\n'),
388
};
389
}
390
return null;
391
}).filter(Boolean as unknown as (<C>(x: C) => x is NonNullable<C>));
392
return hunks;
393
}
394
395
private static enumeratedLines(document: TextDocumentSnapshot, startLine: number, endLine: number) {
396
const text = document.getText(new Range(startLine, 0, endLine, 0));
397
const lines = text.split('\n');
398
const code = lines
399
.map((line, i) => i === endLine - startLine ? line : `/* Line ${startLine + i + 1} */${line}`)
400
.join('\n');
401
return code;
402
}
403
404
private static enumeratedSelectedLines(document: TextDocumentSnapshot, startLine: number, endLine: number, startSelectionLine: number, endSelectionLine: number) {
405
const text = document.getText(new Range(startLine, 0, endLine, 0));
406
const lines = text.split('\n');
407
const code = lines
408
.map((line, i) => {
409
if (i === endLine - startLine) {
410
return line;
411
}
412
const currentLine = startLine + i;
413
return `/* ${startSelectionLine <= currentLine && currentLine < endSelectionLine ? 'Selected ' : ''}Line ${currentLine + 1} */${line}`;
414
})
415
.join('\n');
416
return code;
417
}
418
419
private static enumeratedChangeLines(text: string, startLine: number) {
420
let removedLines = 0;
421
const code = text.split('\n')
422
.filter(line => line[0] !== '-') // TODO: Try with removed lines included.
423
.map((line, i) => {
424
const changeChar = line[0];
425
const removal = changeChar === '-';
426
if (removal) {
427
removedLines++;
428
}
429
const addition = changeChar === '+';
430
return `/* ${removal ? 'Removed Line' : `${addition ? 'Changed ' : ''}Line ${startLine + i - removedLines + 1}`} */${line.substring(1)}`;
431
})
432
.join('\n');
433
return code;
434
}
435
}
436
437