Skip to content

Commit fe9048f

Browse files
authored
nes: expand edit window lines below to capture whole merge conflict (#1213)
this's behind a setting; should help with proposing nicer NES for merge conflicts
1 parent 37baeb6 commit fe9048f

File tree

3 files changed

+332
-3
lines changed

3 files changed

+332
-3
lines changed

src/extension/xtab/node/xtabProvider.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,9 @@ export class XtabProvider implements IStatelessNextEditProvider {
245245

246246
const areaAroundEditWindowLinesRange = this.computeAreaAroundEditWindowLinesRange(currentFileContentLines, cursorLineIdx);
247247

248-
const editWindowLinesRange = this.computeEditWindowLinesRange(currentFileContentLines, cursorLineIdx, request, retryState);
248+
const maxMergeConflictLines = this.configService.getExperimentBasedConfig(ConfigKey.Internal.InlineEditsXtabMaxMergeConflictLines, this.expService);
249+
250+
const editWindowLinesRange = this.computeEditWindowLinesRange(currentFileContentLines, cursorLineIdx, request, maxMergeConflictLines, retryState);
249251

250252
const cursorOriginalLinesOffset = Math.max(0, cursorLineIdx - editWindowLinesRange.start);
251253
const editWindowLastLineLength = activeDocument.documentAfterEdits.getTransformer().getLineLength(editWindowLinesRange.endExclusive);
@@ -784,7 +786,7 @@ export class XtabProvider implements IStatelessNextEditProvider {
784786
return new OffsetRange(areaAroundStart, areaAroundEndExcl);
785787
}
786788

787-
private computeEditWindowLinesRange(currentDocLines: string[], cursorLine: number, request: StatelessNextEditRequest, retryState: RetryState): OffsetRange {
789+
private computeEditWindowLinesRange(currentDocLines: string[], cursorLine: number, request: StatelessNextEditRequest, maxMergeConflictLines: number | undefined, retryState: RetryState): OffsetRange {
788790
let nLinesAbove: number;
789791
{
790792
const useVaryingLinesAbove = this.configService.getExperimentBasedConfig(ConfigKey.Internal.InlineEditsXtabProviderUseVaryingLinesAbove, this.expService);
@@ -829,7 +831,16 @@ export class XtabProvider implements IStatelessNextEditProvider {
829831
}
830832

831833
const codeToEditStart = Math.max(0, cursorLine - nLinesAbove);
832-
const codeToEditEndExcl = Math.min(currentDocLines.length, cursorLine + nLinesBelow + 1);
834+
let codeToEditEndExcl = Math.min(currentDocLines.length, cursorLine + nLinesBelow + 1);
835+
836+
if (maxMergeConflictLines) {
837+
const tentativeEditWindow = new OffsetRange(codeToEditStart, codeToEditEndExcl);
838+
const mergeConflictRange = findMergeConflictMarkersRange(currentDocLines, tentativeEditWindow, maxMergeConflictLines);
839+
if (mergeConflictRange) {
840+
this.tracer.trace(`Expanding edit window to include merge conflict markers: ${mergeConflictRange.toString()}`);
841+
codeToEditEndExcl = Math.max(codeToEditEndExcl, mergeConflictRange.endExclusive);
842+
}
843+
}
833844

834845
return new OffsetRange(codeToEditStart, codeToEditEndExcl);
835846
}
@@ -1024,3 +1035,27 @@ export class XtabProvider implements IStatelessNextEditProvider {
10241035
logContext.addLog(msg);
10251036
}
10261037
}
1038+
1039+
/**
1040+
* Finds the range of lines containing merge conflict markers within a specified edit window.
1041+
*
1042+
* @param lines - Array of strings representing the lines of text to search through
1043+
* @param editWindowRange - The range within which to search for merge conflict markers
1044+
* @param maxMergeConflictLines - Maximum number of lines to search for conflict markers
1045+
* @returns An OffsetRange object representing the start and end of the conflict markers, or undefined if not found
1046+
*/
1047+
export function findMergeConflictMarkersRange(lines: string[], editWindowRange: OffsetRange, maxMergeConflictLines: number): OffsetRange | undefined {
1048+
for (let i = editWindowRange.start; i < Math.min(lines.length, editWindowRange.endExclusive); ++i) {
1049+
if (!lines[i].startsWith('<<<<<<<')) {
1050+
continue;
1051+
}
1052+
1053+
// found start of merge conflict markers -- now find the end
1054+
for (let j = i + 1; j < lines.length && (j - i) < maxMergeConflictLines; ++j) {
1055+
if (lines[j].startsWith('>>>>>>>')) {
1056+
return new OffsetRange(i, j + 1 /* because endExclusive */);
1057+
}
1058+
}
1059+
}
1060+
return undefined;
1061+
}
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
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 { expect, suite, test } from 'vitest';
7+
import { findMergeConflictMarkersRange } from '../../node/xtabProvider';
8+
import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';
9+
10+
suite('findMergeConflictMarkersRange', () => {
11+
12+
test('should find merge conflict markers within edit window', () => {
13+
const lines = [
14+
'function foo() {',
15+
'<<<<<<< HEAD',
16+
' return 1;',
17+
'=======',
18+
' return 2;',
19+
'>>>>>>> branch',
20+
'}',
21+
];
22+
const editWindowRange = new OffsetRange(0, 7);
23+
const maxMergeConflictLines = 10;
24+
25+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
26+
27+
expect(result).toBeDefined();
28+
expect(result?.start).toBe(1);
29+
expect(result?.endExclusive).toBe(6);
30+
});
31+
32+
test('should return undefined when no merge conflict markers present', () => {
33+
const lines = [
34+
'function foo() {',
35+
' return 1;',
36+
'}',
37+
];
38+
const editWindowRange = new OffsetRange(0, 3);
39+
const maxMergeConflictLines = 10;
40+
41+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
42+
43+
expect(result).toBeUndefined();
44+
});
45+
46+
test('should return undefined when start marker exists but no end marker', () => {
47+
const lines = [
48+
'function foo() {',
49+
'<<<<<<< HEAD',
50+
' return 1;',
51+
'=======',
52+
' return 2;',
53+
'}',
54+
];
55+
const editWindowRange = new OffsetRange(0, 6);
56+
const maxMergeConflictLines = 10;
57+
58+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
59+
60+
expect(result).toBeUndefined();
61+
});
62+
63+
test('should return undefined when conflict exceeds maxMergeConflictLines', () => {
64+
const lines = [
65+
'<<<<<<< HEAD',
66+
'line 1',
67+
'line 2',
68+
'line 3',
69+
'line 4',
70+
'>>>>>>> branch',
71+
];
72+
const editWindowRange = new OffsetRange(0, 6);
73+
const maxMergeConflictLines = 3; // Too small to reach end marker
74+
75+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
76+
77+
expect(result).toBeUndefined();
78+
});
79+
80+
test('should find conflict when exactly at maxMergeConflictLines boundary', () => {
81+
const lines = [
82+
'<<<<<<< HEAD',
83+
'line 1',
84+
'line 2',
85+
'>>>>>>> branch',
86+
];
87+
const editWindowRange = new OffsetRange(0, 4);
88+
const maxMergeConflictLines = 4;
89+
90+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
91+
92+
expect(result).toBeDefined();
93+
expect(result?.start).toBe(0);
94+
expect(result?.endExclusive).toBe(4);
95+
});
96+
97+
test('should only search within edit window range', () => {
98+
const lines = [
99+
'function foo() {',
100+
' return 1;',
101+
'<<<<<<< HEAD',
102+
' return 2;',
103+
'>>>>>>> branch',
104+
'}',
105+
];
106+
const editWindowRange = new OffsetRange(0, 2); // Excludes the conflict
107+
const maxMergeConflictLines = 10;
108+
109+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
110+
111+
expect(result).toBeUndefined();
112+
});
113+
114+
test('should find first conflict when multiple conflicts exist', () => {
115+
const lines = [
116+
'<<<<<<< HEAD',
117+
'first conflict',
118+
'>>>>>>> branch',
119+
'some code',
120+
'<<<<<<< HEAD',
121+
'second conflict',
122+
'>>>>>>> branch',
123+
];
124+
const editWindowRange = new OffsetRange(0, 7);
125+
const maxMergeConflictLines = 10;
126+
127+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
128+
129+
expect(result).toBeDefined();
130+
expect(result?.start).toBe(0);
131+
expect(result?.endExclusive).toBe(3);
132+
});
133+
134+
test('should handle conflict at start of edit window', () => {
135+
const lines = [
136+
'<<<<<<< HEAD',
137+
'content',
138+
'>>>>>>> branch',
139+
];
140+
const editWindowRange = new OffsetRange(0, 3);
141+
const maxMergeConflictLines = 10;
142+
143+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
144+
145+
expect(result).toBeDefined();
146+
expect(result?.start).toBe(0);
147+
expect(result?.endExclusive).toBe(3);
148+
});
149+
150+
test('should handle conflict at end of edit window', () => {
151+
const lines = [
152+
'some code',
153+
'<<<<<<< HEAD',
154+
'content',
155+
'>>>>>>> branch',
156+
];
157+
const editWindowRange = new OffsetRange(0, 4);
158+
const maxMergeConflictLines = 10;
159+
160+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
161+
162+
expect(result).toBeDefined();
163+
expect(result?.start).toBe(1);
164+
expect(result?.endExclusive).toBe(4);
165+
});
166+
167+
test('should handle empty lines array', () => {
168+
const lines: string[] = [];
169+
const editWindowRange = new OffsetRange(0, 0);
170+
const maxMergeConflictLines = 10;
171+
172+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
173+
174+
expect(result).toBeUndefined();
175+
});
176+
177+
test('should handle single line with start marker only', () => {
178+
const lines = ['<<<<<<< HEAD'];
179+
const editWindowRange = new OffsetRange(0, 1);
180+
const maxMergeConflictLines = 10;
181+
182+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
183+
184+
expect(result).toBeUndefined();
185+
});
186+
187+
test('should handle lines with merge markers that do not start at beginning', () => {
188+
const lines = [
189+
'function foo() {',
190+
' <<<<<<< HEAD',
191+
' return 1;',
192+
' >>>>>>> branch',
193+
'}',
194+
];
195+
const editWindowRange = new OffsetRange(0, 5);
196+
const maxMergeConflictLines = 10;
197+
198+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
199+
200+
expect(result).toBeUndefined(); // Should not match as markers don't start at line beginning
201+
});
202+
203+
test('should handle conflict that extends beyond lines array', () => {
204+
const lines = [
205+
'<<<<<<< HEAD',
206+
'content',
207+
];
208+
const editWindowRange = new OffsetRange(0, 2);
209+
const maxMergeConflictLines = 10;
210+
211+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
212+
213+
expect(result).toBeUndefined();
214+
});
215+
216+
test('should handle edit window extending beyond lines array', () => {
217+
const lines = [
218+
'<<<<<<< HEAD',
219+
'content',
220+
'>>>>>>> branch',
221+
];
222+
const editWindowRange = new OffsetRange(0, 100); // Beyond array length
223+
const maxMergeConflictLines = 10;
224+
225+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
226+
227+
expect(result).toBeDefined();
228+
expect(result?.start).toBe(0);
229+
expect(result?.endExclusive).toBe(3);
230+
});
231+
232+
test('should handle minimal conflict (start and end markers only)', () => {
233+
const lines = [
234+
'<<<<<<< HEAD',
235+
'>>>>>>> branch',
236+
];
237+
const editWindowRange = new OffsetRange(0, 2);
238+
const maxMergeConflictLines = 10;
239+
240+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
241+
242+
expect(result).toBeDefined();
243+
expect(result?.start).toBe(0);
244+
expect(result?.endExclusive).toBe(2);
245+
});
246+
247+
test('should handle maxMergeConflictLines of 1', () => {
248+
const lines = [
249+
'<<<<<<< HEAD',
250+
'>>>>>>> branch',
251+
];
252+
const editWindowRange = new OffsetRange(0, 2);
253+
const maxMergeConflictLines = 1;
254+
255+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
256+
257+
expect(result).toBeUndefined(); // Cannot find end marker within limit
258+
});
259+
260+
test('should handle maxMergeConflictLines of 2', () => {
261+
const lines = [
262+
'<<<<<<< HEAD',
263+
'>>>>>>> branch',
264+
];
265+
const editWindowRange = new OffsetRange(0, 2);
266+
const maxMergeConflictLines = 2;
267+
268+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
269+
270+
expect(result).toBeDefined();
271+
expect(result?.start).toBe(0);
272+
expect(result?.endExclusive).toBe(2);
273+
});
274+
275+
test('should find conflict starting in middle of edit window', () => {
276+
const lines = [
277+
'line 1',
278+
'line 2',
279+
'<<<<<<< HEAD',
280+
'conflict',
281+
'>>>>>>> branch',
282+
'line 5',
283+
];
284+
const editWindowRange = new OffsetRange(0, 6);
285+
const maxMergeConflictLines = 10;
286+
287+
const result = findMergeConflictMarkersRange(lines, editWindowRange, maxMergeConflictLines);
288+
289+
expect(result).toBeDefined();
290+
expect(result?.start).toBe(2);
291+
expect(result?.endExclusive).toBe(5);
292+
});
293+
});

src/platform/configuration/common/configurationService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,7 @@ export namespace ConfigKey {
697697
export const InlineEditsXtabProviderUseXtab275Prompting = defineExpSetting<boolean>('chat.advanced.inlineEdits.xtabProvider.xtab275Prompting', false, INTERNAL_RESTRICTED);
698698
export const InlineEditsXtabUseNes41Miniv3Prompting = defineExpSetting<boolean>('chat.advanced.inlineEdits.xtabProvider.useNes41Miniv3Prompting', false, INTERNAL_RESTRICTED);
699699
export const InlineEditsXtabCodexV21NesUnified = defineExpSetting<boolean>('chat.advanced.inlineEdits.xtabProvider.codexv21nesUnified', false, INTERNAL_RESTRICTED);
700+
export const InlineEditsXtabMaxMergeConflictLines = defineExpSetting<number | undefined>('chat.advanced.inlineEdits.xtabProvider.maxMergeConflictLines', undefined, INTERNAL_RESTRICTED);
700701
export const InlineEditsUndoInsertionFilteringEnabled = defineExpSetting<boolean>('chat.advanced.inlineEdits.undoInsertionFilteringEnabled', true, INTERNAL_RESTRICTED);
701702
export const InlineEditsDiagnosticsExplorationEnabled = defineSetting<boolean | undefined>('chat.advanced.inlineEdits.inlineEditsDiagnosticsExplorationEnabled', false, INTERNAL_RESTRICTED);
702703
export const EditSourceTrackingShowDecorations = defineSetting('chat.advanced.editSourceTracking.showDecorations', false, INTERNAL);

0 commit comments

Comments
 (0)