Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompt/node/editFromDiffGeneration.ts
13399 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 { CharCode } from '../../../util/vs/base/common/charCode';
7
import { Lines, LinesEdit } from './editGeneration';
8
import { IGuessedIndentation, computeIndentLevel2, guessIndentation } from './indentationGuesser';
9
10
11
export interface Reporter {
12
recovery(originalLine: number, newLine: number): void;
13
warning(message: string): void;
14
}
15
16
export function createEditsFromRealDiff(code: Lines, diff: Lines, reporter?: Reporter): LinesEdit[] {
17
const edits: LinesEdit[] = [];
18
let diffLineIndex = findChuck(diff);
19
if (diffLineIndex === -1) {
20
reporter?.warning('No chunk header found in the diff.');
21
diffLineIndex = 0;
22
}
23
function handleLineContentMismatch(diffLine: string, code: Lines, originalLineIndex: number): boolean {
24
for (let i = originalLineIndex; i < code.length; i++) {
25
if (code[i] === diffLine) {
26
reporter?.recovery(originalLineIndex, i);
27
originalLineIndex = i;
28
return true;
29
}
30
}
31
for (let i = originalLineIndex - 1; i >= 0; i--) {
32
if (code[i] === diffLine) {
33
reporter?.recovery(originalLineIndex, i);
34
originalLineIndex = i;
35
return true;
36
}
37
}
38
reporter?.warning(`Diff line does not match original content: Not found,`);
39
return false;
40
}
41
42
43
let originalLineIndex = 0;
44
while (diffLineIndex < diff.length && originalLineIndex <= code.length) {
45
const diffLine = diff[diffLineIndex];
46
if (diffLine.length === 0) {
47
break;
48
}
49
const firstChar = diffLine.charCodeAt(0);
50
switch (firstChar) {
51
case CharCode.AtSign: {
52
const match = /^@@ -(\d+),?\d* \+(\d+),?\d* @@/.exec(diffLine);
53
if (match) {
54
const originalLineHint = parseInt(match[1]);
55
originalLineIndex = originalLineHint - 1;
56
} else {
57
reporter?.warning(`Invalid chunk header found in the diff: ${diffLine}`);
58
}
59
break;
60
}
61
case CharCode.Plus: {
62
const noEOL = isNextLineNoEOLMarker(diff, diffLineIndex);
63
edits.push(new LinesEdit(originalLineIndex, originalLineIndex, [diffLine.substring(1)], '', noEOL ? '' : '\n'));
64
break;
65
}
66
case CharCode.Dash:
67
if (diffLine.substring(1) !== code[originalLineIndex]) {
68
if (!handleLineContentMismatch(diffLine.substring(1), code, originalLineIndex)) {
69
break; // don't do the delete
70
}
71
}
72
edits.push(new LinesEdit(originalLineIndex, originalLineIndex + 1, []));
73
originalLineIndex++;
74
break;
75
case CharCode.Backslash: {
76
// ignore, already handled for insert, and not relevant for delete
77
break;
78
}
79
default: {
80
if (diffLine.substring(1) === code[originalLineIndex] || diffLine === code[originalLineIndex]) {
81
originalLineIndex++;
82
} else {
83
if (handleLineContentMismatch(diffLine.substring(1), code, originalLineIndex)) {
84
originalLineIndex++;
85
}
86
}
87
break;
88
}
89
}
90
diffLineIndex++;
91
}
92
return edits;
93
}
94
95
function isNextLineNoEOLMarker(diff: Lines, i: number): boolean {
96
return i + 1 < diff.length && diff[i + 1].charCodeAt(0) === CharCode.Backslash;
97
}
98
99
function findChuck(diff: Lines): number {
100
for (let i = 0; i < diff.length; i++) {
101
if (diff[i].startsWith('@@')) {
102
return i;
103
}
104
}
105
return -1;
106
}
107
108
enum Match {
109
No,
110
Yes,
111
Similar
112
}
113
114
export function createEditsFromPseudoDiff(code: Lines, diff: Lines, reporter?: Reporter): LinesEdit[] {
115
116
const diffLineInfos = getLineInfos(diff);
117
118
const codeIndentInfo = guessIndentation(code, 4, false);
119
120
const diffTabSize = getTabSize(diffLineInfos);
121
122
let indentDiff: number | undefined;
123
124
function compareLine(diffLineInfo: LineInfo, codeLine: string): Match {
125
const diffLine = diffLineInfo.content;
126
const codeIndentLength = getIndentLength(codeLine);
127
let i = diffLineInfo.indentLength, k = codeIndentLength;
128
let charactersMatched = 0;
129
130
// ignore the leading indentation
131
while (i < diffLine.length && k < codeLine.length) {
132
if (diffLine.charCodeAt(i) !== codeLine.charCodeAt(k)) {
133
break;
134
}
135
i++;
136
k++;
137
charactersMatched++;
138
}
139
if (i < diffLine.length || k < codeLine.length) {
140
return (((codeLine.length - codeIndentLength) * 3 / 4 < charactersMatched) && ((diffLine.length - diffLineInfo.indentLength) * 3 / 4 < charactersMatched)) ? Match.Similar : Match.No;
141
}
142
if (indentDiff === undefined) {
143
const diffIndent = computeIndentLevel2(diffLine, diffTabSize);
144
if (diffIndent >= 0) {
145
const codeIndent = computeIndentLevel2(codeLine, codeIndentInfo.tabSize);
146
indentDiff = codeIndent - diffIndent;
147
}
148
}
149
return Match.Yes;
150
}
151
function handleLineContentMismatch(diffLineInfo: LineInfo, code: Lines, originalLineIndex: number): number {
152
for (let i = originalLineIndex; i < code.length; i++) {
153
if (compareLine(diffLineInfo, code[i]) === Match.Yes) {
154
reporter?.recovery(originalLineIndex, i);
155
return i;
156
}
157
}
158
reporter?.warning('Unable to find a matching line for the diff line: ' + diffLineInfo.content);
159
return -1;
160
}
161
162
function findFirstOccurrenceOfLine(diffLineInfo: LineInfo, code: Lines): number {
163
for (let i = 0; i < code.length; i++) {
164
if (compareLine(diffLineInfo, code[i]) === Match.Yes) {
165
return i;
166
}
167
}
168
return 0;
169
}
170
171
const edits: LinesEdit[] = [];
172
let diffLineIndex = 0;
173
let originalLineIndex = 0;
174
if (diffLineInfos.length > 0) {
175
originalLineIndex = findFirstOccurrenceOfLine(diffLineInfos[0], code);
176
}
177
while (diffLineIndex < diffLineInfos.length && originalLineIndex < code.length) {
178
const diffLineInfo = diffLineInfos[diffLineIndex];
179
switch (diffLineInfo.op) {
180
case Op.Insert: {
181
const codeLineContent = adjustIndenation(diffLineInfo, diffTabSize, indentDiff ?? 0, codeIndentInfo);
182
edits.push(new LinesEdit(originalLineIndex, originalLineIndex, [codeLineContent]));
183
break;
184
}
185
case Op.Delete: {
186
const codeLine = code[originalLineIndex];
187
const match = compareLine(diffLineInfo, codeLine);
188
if (match === Match.No) {
189
const line = handleLineContentMismatch(diffLineInfo, code, originalLineIndex);
190
if (line !== -1) {
191
originalLineIndex = line;
192
} else {
193
break; // do not delete
194
}
195
}
196
const nextDiffLine = diffLineInfos[diffLineIndex + 1];
197
if (nextDiffLine?.op === Op.Insert) {
198
if (nextDiffLine.indentLength === diffLineInfo.indentLength) {
199
// special handling of the case where an insert follows the remove and they use the same indentation
200
const newContent = getIndent(codeLine) + nextDiffLine.content.substring(nextDiffLine.indentLength);
201
edits.push(new LinesEdit(originalLineIndex, originalLineIndex + 1, [newContent]));
202
diffLineIndex++;
203
originalLineIndex++;
204
break;
205
}
206
}
207
edits.push(new LinesEdit(originalLineIndex, originalLineIndex + 1, []));
208
originalLineIndex++;
209
break;
210
}
211
default: {
212
const match = compareLine(diffLineInfo, code[originalLineIndex]);
213
if (match === Match.No) {
214
const line = handleLineContentMismatch(diffLineInfo, code, originalLineIndex);
215
if (line !== -1) {
216
originalLineIndex = line;
217
} else {
218
break; // do not increase originalLineIndex
219
}
220
} else if (match === Match.Similar) {
221
const codeLineContent = adjustIndenation(diffLineInfo, diffTabSize, indentDiff ?? 0, codeIndentInfo);
222
edits.push(new LinesEdit(originalLineIndex, originalLineIndex + 1, [codeLineContent]));
223
}
224
originalLineIndex++;
225
break;
226
}
227
}
228
diffLineIndex++;
229
}
230
if (originalLineIndex === code.length && diffLineIndex < diffLineInfos.length) {
231
// there are still some lines to add
232
for (; diffLineIndex < diffLineInfos.length; diffLineIndex++) {
233
const diffLineInfo = diffLineInfos[diffLineIndex];
234
if (diffLineInfo.op === Op.Insert) {
235
const codeLineContent = adjustIndenation(diffLineInfo, diffTabSize, indentDiff ?? 0, codeIndentInfo);
236
edits.push(new LinesEdit(originalLineIndex, originalLineIndex, [codeLineContent], '\n', ''));
237
}
238
}
239
}
240
241
242
243
return edits;
244
}
245
246
function isWhiteSpace(charCode: number): boolean {
247
return charCode === CharCode.Space || charCode === CharCode.Tab;
248
}
249
250
function isPlusOrMinus(charCode: number): boolean {
251
return charCode === CharCode.Dash || charCode === CharCode.Plus;
252
}
253
254
function getIndentLength(line: string): number {
255
let i = 0;
256
while (i < line.length && isWhiteSpace(line.charCodeAt(i))) {
257
i++;
258
}
259
return i;
260
}
261
262
function getIndent(line: string): string {
263
let i = 0;
264
while (i < line.length && isWhiteSpace(line.charCodeAt(i))) {
265
i++;
266
}
267
return line.substring(0, i);
268
}
269
270
271
function adjustIndenation(diffLineInfo: LineInfo, diffLineTabSize: number, indentDifference: number, codeIndentInfo: IGuessedIndentation): string {
272
if (indentDifference === 0 && ((!codeIndentInfo.insertSpaces && diffLineInfo.indentKind === IndentKind.Tabs) || (codeIndentInfo.insertSpaces && diffLineInfo.indentKind === IndentKind.Spaces))) {
273
return diffLineInfo.content;
274
}
275
const diffIndent = computeIndentLevel2(diffLineInfo.content, diffLineTabSize);
276
const newIndentation = codeIndentInfo.insertSpaces ? ' '.repeat(codeIndentInfo.tabSize * (diffIndent + indentDifference)) : '\t'.repeat(diffIndent + indentDifference);
277
return newIndentation + diffLineInfo.content.substring(diffLineInfo.indentLength);
278
}
279
280
enum Op {
281
Equal = 0,
282
Insert = 1,
283
Delete = -1
284
}
285
286
enum IndentKind {
287
undefined = 0,
288
Tabs = 1,
289
Spaces = 2,
290
Mixed = 3
291
}
292
293
interface LineInfo {
294
content: string;
295
op: Op;
296
indentKind: IndentKind;
297
indentLength: number;
298
}
299
300
function udpateIndentInfo(lineInfo: LineInfo): void {
301
let indentKind = IndentKind.undefined;
302
let indentLength = 0;
303
const line = lineInfo.content;
304
if (line.length > 0) {
305
const indentChar = line.charCodeAt(0);
306
if (isWhiteSpace(indentChar)) {
307
indentLength++;
308
indentKind = indentChar === CharCode.Space ? IndentKind.Spaces : IndentKind.Tabs;
309
while (indentLength < line.length) {
310
const charCode = line.charCodeAt(indentLength);
311
if (!isWhiteSpace(charCode)) {
312
break;
313
}
314
indentLength++;
315
if (charCode !== indentChar) {
316
indentKind = IndentKind.Mixed;
317
}
318
}
319
320
}
321
}
322
lineInfo.indentKind = indentKind;
323
lineInfo.indentLength = indentLength;
324
}
325
326
function getLineInfos(diffLines: Lines): LineInfo[] {
327
const result: LineInfo[] = [];
328
329
for (let i = 0; i < diffLines.length; i++) {
330
const line = diffLines[i];
331
const lineInfo: LineInfo = {
332
content: line,
333
op: Op.Equal,
334
indentKind: IndentKind.undefined,
335
indentLength: 0
336
};
337
if (line.length > 0) {
338
if (isPlusOrMinus(line.charCodeAt(0))) {
339
lineInfo.op = line.charCodeAt(0) === CharCode.Dash ? Op.Delete : Op.Insert;
340
if (line.length > 1 && line.charCodeAt(1) === CharCode.Space) {
341
lineInfo.content = line.substring(1);
342
// replace the + or - with a space if the remaining indentation is odd
343
if (getIndentLength(lineInfo.content) % 2 === 1) {
344
lineInfo.content = ' ' + lineInfo.content;
345
}
346
} else {
347
lineInfo.content = line.substring(1);
348
}
349
}
350
udpateIndentInfo(lineInfo);
351
}
352
result.push(lineInfo);
353
}
354
sanitizeLineInfos(result);
355
return result;
356
}
357
358
359
function sanitizeLineInfos(lineInfos: LineInfo[]): void {
360
let min = Number.MAX_VALUE;
361
for (const lineInfo of lineInfos) {
362
if (lineInfo.indentKind !== IndentKind.Spaces || lineInfo.indentLength === 0) {
363
return;
364
}
365
if (lineInfo.indentLength < min) {
366
min = lineInfo.indentLength;
367
}
368
}
369
if (min > 0) {
370
for (const lineInfo of lineInfos) {
371
lineInfo.indentLength -= min;
372
lineInfo.content = lineInfo.content.substring(min);
373
}
374
}
375
}
376
377
function getTabSize(lineInfos: LineInfo[]): number {
378
return 4;
379
}
380
381