Skip to content

Commit fef53e1

Browse files
authored
edit tacking for import nes (#233)
1 parent b7251b1 commit fef53e1

File tree

9 files changed

+710
-46
lines changed

9 files changed

+710
-46
lines changed
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
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 * as assert from 'assert';
7+
import { suite, test } from 'vitest';
8+
import { StringEdit } from '../../../../util/vs/editor/common/core/edits/stringEdit';
9+
import { Range } from '../../../../util/vs/editor/common/core/range';
10+
import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';
11+
import { StringText } from '../../../../util/vs/editor/common/core/text/abstractText';
12+
import { Diagnostic, DiagnosticSeverity } from '../../vscode-node/features/diagnosticsBasedCompletions/diagnosticsCompletions';
13+
import { DiagnosticsCollection } from '../../vscode-node/features/diagnosticsCompletionProcessor';
14+
15+
// Helper function to create a mock VS Code diagnostic
16+
function createMockVSCodeDiagnostic(
17+
message: string,
18+
range: Range,
19+
severity: number = 0,
20+
source?: string,
21+
code?: string | number
22+
): any {
23+
return {
24+
message,
25+
range: {
26+
start: { line: range.startLineNumber - 1, character: range.startColumn - 1 },
27+
end: { line: range.endLineNumber - 1, character: range.endColumn - 1 }
28+
},
29+
severity,
30+
source,
31+
code
32+
};
33+
}
34+
35+
// Helper function to create a Diagnostic from a mock VS Code diagnostic
36+
function createDiagnostic(
37+
message: string,
38+
range: Range,
39+
severity: DiagnosticSeverity = DiagnosticSeverity.Error,
40+
source?: string,
41+
code?: string | number
42+
): Diagnostic {
43+
const mockVSCodeDiagnostic = createMockVSCodeDiagnostic(message, range, severity, source, code);
44+
return Diagnostic.fromVSCodeDiagnostic(mockVSCodeDiagnostic);
45+
}
46+
47+
suite('DiagnosticsCollection', () => {
48+
test('isEqualAndUpdate should return true for empty arrays', () => {
49+
const collection = new DiagnosticsCollection();
50+
const result = collection.isEqualAndUpdate([]);
51+
assert.strictEqual(result, true);
52+
});
53+
test('isEqualAndUpdate should update diagnostics and return false when different', () => {
54+
const collection = new DiagnosticsCollection();
55+
const diagnostic = createDiagnostic(
56+
'Test error',
57+
new Range(1, 1, 1, 5)
58+
);
59+
60+
const result = collection.isEqualAndUpdate([diagnostic]);
61+
62+
assert.strictEqual(result, false);
63+
});
64+
test('isEqualAndUpdate should return true when diagnostics are equal', () => {
65+
const collection = new DiagnosticsCollection();
66+
const diagnostic1 = createDiagnostic('Test error', new Range(1, 1, 1, 5));
67+
const diagnostic2 = createDiagnostic('Test error', new Range(1, 1, 1, 5));
68+
69+
collection.isEqualAndUpdate([diagnostic1]);
70+
const result = collection.isEqualAndUpdate([diagnostic2]);
71+
72+
assert.strictEqual(result, true);
73+
});
74+
test('isEqualAndUpdate should return false when a diagnostics is invalidated', () => {
75+
const collection = new DiagnosticsCollection();
76+
const diagnostic1 = createDiagnostic('Test error', new Range(1, 1, 1, 5));
77+
const diagnostic2 = createDiagnostic('Test error', new Range(1, 1, 1, 5));
78+
79+
collection.isEqualAndUpdate([diagnostic1]);
80+
81+
diagnostic1.invalidate();
82+
83+
const result = collection.isEqualAndUpdate([diagnostic2]);
84+
85+
assert.strictEqual(result, false);
86+
});
87+
88+
suite('applyEdit', () => {
89+
test('should invalidate when typing numbers at the end of a diagnostic range', () => {
90+
const collection = new DiagnosticsCollection();
91+
const diagnostic = createDiagnostic('Test error', new Range(1, 13, 1, 17)); // "test" = positions 12-15 (1-based: 13-17)
92+
collection.isEqualAndUpdate([diagnostic]);
93+
94+
// Replace "test" with "test123"
95+
const before = new StringText('hello world test');
96+
const edit = StringEdit.replace(new OffsetRange(12, 16), 'test123'); // 0-based: 12-15
97+
const after = edit.applyOnText(before);
98+
99+
const hasInvalidated = collection.applyEdit(before, edit, after);
100+
assert.strictEqual(hasInvalidated, true);
101+
assert.strictEqual(diagnostic.isValid(), false);
102+
});
103+
104+
test('should invalidate diagnostic when range shrinks', () => {
105+
const collection = new DiagnosticsCollection();
106+
const diagnostic = createDiagnostic('Test error', new Range(1, 7, 1, 12)); // "world"
107+
collection.isEqualAndUpdate([diagnostic]);
108+
109+
// Create an edit that removes "w"
110+
const before = new StringText('hello world test');
111+
const edit = StringEdit.replace(new OffsetRange(6, 7), ''); // Remove "w"
112+
const after = edit.applyOnText(before);
113+
114+
const hasInvalidated = collection.applyEdit(before, edit, after);
115+
116+
assert.strictEqual(hasInvalidated, true);
117+
assert.strictEqual(diagnostic.isValid(), false);
118+
});
119+
120+
test('should update range when content stays the same and range length unchanged', () => {
121+
const collection = new DiagnosticsCollection();
122+
const diagnostic = createDiagnostic('Test error', new Range(1, 13, 1, 17));
123+
collection.isEqualAndUpdate([diagnostic]);
124+
125+
// Insert " big" without touching the diagnostic range
126+
const before = new StringText('hello world test');
127+
const edit = StringEdit.replace(new OffsetRange(6, 6), ' big');
128+
const after = edit.applyOnText(before);
129+
130+
const hasInvalidated = collection.applyEdit(before, edit, after);
131+
132+
assert.strictEqual(hasInvalidated, false);
133+
assert.strictEqual(diagnostic.isValid(), true);
134+
});
135+
136+
test('should invalidate diagnostic when content at range changes with same length', () => {
137+
const collection = new DiagnosticsCollection();
138+
const diagnostic = createDiagnostic('Test error', new Range(1, 13, 1, 17)); // "test"
139+
collection.isEqualAndUpdate([diagnostic]);
140+
141+
// Replace "test" with "best"
142+
const before = new StringText('hello world test');
143+
const edit = StringEdit.replace(new OffsetRange(12, 16), 'best');
144+
const after = edit.applyOnText(before);
145+
146+
const hasInvalidated = collection.applyEdit(before, edit, after);
147+
148+
assert.strictEqual(hasInvalidated, true);
149+
assert.strictEqual(diagnostic.isValid(), false);
150+
});
151+
test('should handle range growth with same prefix content', () => {
152+
const collection = new DiagnosticsCollection();
153+
const diagnostic = createDiagnostic('Test error', new Range(1, 13, 1, 17));
154+
collection.isEqualAndUpdate([diagnostic]);
155+
156+
// "test" becomes "test!" (non-alphanumeric edge)
157+
const before = new StringText('hello world test');
158+
const edit = StringEdit.replace(new OffsetRange(12, 16), 'test!');
159+
const after = edit.applyOnText(before);
160+
161+
const hasInvalidated = collection.applyEdit(before, edit, after);
162+
163+
assert.strictEqual(hasInvalidated, false);
164+
assert.strictEqual(diagnostic.isValid(), true);
165+
166+
// Range should still point to the original "test" part
167+
assert.strictEqual(diagnostic.range.startColumn, 13);
168+
assert.strictEqual(diagnostic.range.endColumn, 17);
169+
});
170+
171+
test('should handle range growth with same suffix content', () => {
172+
const collection = new DiagnosticsCollection();
173+
const diagnostic = createDiagnostic('Test error', new Range(1, 13, 1, 17)); // "test"
174+
collection.isEqualAndUpdate([diagnostic]);
175+
176+
const before = new StringText('hello world test');
177+
const edit = StringEdit.replace(new OffsetRange(12, 12), 'ab');
178+
const after = edit.applyOnText(before);
179+
180+
const hasInvalidated = collection.applyEdit(before, edit, after);
181+
182+
assert.strictEqual(hasInvalidated, false);
183+
assert.strictEqual(diagnostic.isValid(), true);
184+
// Range should point to the suffix "test" part
185+
assert.strictEqual(diagnostic.range.startColumn, 15); // 13 + 2 ("ab")
186+
assert.strictEqual(diagnostic.range.endColumn, 19); // 17 + 2 ("ab")
187+
});
188+
189+
test('should invalidate when edge character is alphanumeric with prefix match', () => {
190+
const collection = new DiagnosticsCollection();
191+
const diagnostic = createDiagnostic('Test error', new Range(1, 13, 1, 17)); // "test"
192+
collection.isEqualAndUpdate([diagnostic]);
193+
194+
const before = new StringText('hello world test');
195+
const edit = StringEdit.replace(new OffsetRange(16, 16), 'A');
196+
const after = edit.applyOnText(before);
197+
198+
// Add A after "test"
199+
200+
const hasInvalidated = collection.applyEdit(before, edit, after);
201+
202+
assert.strictEqual(hasInvalidated, true);
203+
assert.strictEqual(diagnostic.isValid(), false);
204+
});
205+
206+
test('should not invalidate when edge character is non-alphanumeric with prefix match', () => {
207+
const collection = new DiagnosticsCollection();
208+
const diagnostic = createDiagnostic('Test error', new Range(1, 13, 1, 17)); // "test" = positions 12-15 (1-based: 13-17)
209+
collection.isEqualAndUpdate([diagnostic]);
210+
211+
const before = new StringText('hello world test');
212+
const after = new StringText('hello world test!'); // "test" becomes "test!" (non-alphanumeric edge)
213+
214+
// Replace "test" with "test!"
215+
const edit = StringEdit.replace(new OffsetRange(12, 16), 'test!'); // 0-based: 12-15
216+
217+
const hasInvalidated = collection.applyEdit(before, edit, after);
218+
219+
assert.strictEqual(hasInvalidated, false);
220+
assert.strictEqual(diagnostic.isValid(), true);
221+
});
222+
223+
test('should handle multiple diagnostics correctly', () => {
224+
const collection = new DiagnosticsCollection();
225+
const diagnostic1 = createDiagnostic('Error 1', new Range(1, 1, 1, 6)); // "hello" = positions 0-4 (1-based: 1-5), but using 6 for end
226+
const diagnostic2 = createDiagnostic('Error 2', new Range(1, 13, 1, 17)); // "test" = positions 12-15 (1-based: 13-17)
227+
collection.isEqualAndUpdate([diagnostic1, diagnostic2]);
228+
229+
const before = new StringText('hello world test');
230+
const after = new StringText('hello big world test');
231+
232+
// Insert "big " at position 6 (0-based)
233+
const edit = StringEdit.replace(new OffsetRange(6, 6), 'big ');
234+
235+
const hasInvalidated = collection.applyEdit(before, edit, after);
236+
237+
assert.strictEqual(hasInvalidated, false);
238+
assert.strictEqual(diagnostic1.isValid(), true);
239+
assert.strictEqual(diagnostic2.isValid(), true);
240+
241+
// First diagnostic range should be unchanged
242+
assert.strictEqual(diagnostic1.range.startColumn, 1);
243+
assert.strictEqual(diagnostic1.range.endColumn, 6);
244+
245+
// Second diagnostic range should be shifted by 4 positions ("big ")
246+
assert.strictEqual(diagnostic2.range.startColumn, 17); // 13 + 4
247+
assert.strictEqual(diagnostic2.range.endColumn, 21); // 17 + 4
248+
});
249+
250+
test('should handle edge case with empty edge character', () => {
251+
const collection = new DiagnosticsCollection();
252+
const diagnostic = createDiagnostic('Test error', new Range(1, 13, 1, 17)); // "test" = positions 12-15 (1-based: 13-17)
253+
collection.isEqualAndUpdate([diagnostic]);
254+
255+
const before = new StringText('hello world test');
256+
const after = new StringText('hello world testx'); // Add 'x' at end
257+
258+
// Replace "test" with "testx"
259+
const edit = StringEdit.replace(new OffsetRange(12, 16), 'testx'); // 0-based: 12-15
260+
261+
const hasInvalidated = collection.applyEdit(before, edit, after);
262+
263+
// Since 'x' is alphanumeric, should invalidate
264+
assert.strictEqual(hasInvalidated, true);
265+
assert.strictEqual(diagnostic.isValid(), false);
266+
});
267+
268+
test('should handle suffix match with non-alphanumeric edge character', () => {
269+
const collection = new DiagnosticsCollection();
270+
const diagnostic = createDiagnostic('Test error', new Range(1, 13, 1, 17)); // "test" = positions 12-15 (1-based: 13-17)
271+
collection.isEqualAndUpdate([diagnostic]);
272+
273+
const before = new StringText('hello world test');
274+
const after = new StringText('hello world .test'); // "test" becomes ".test"
275+
276+
// Replace "test" with ".test"
277+
const edit = StringEdit.replace(new OffsetRange(12, 16), '.test'); // 0-based: 12-15
278+
279+
const hasInvalidated = collection.applyEdit(before, edit, after);
280+
281+
assert.strictEqual(hasInvalidated, false);
282+
assert.strictEqual(diagnostic.isValid(), true);
283+
// Range should point to the suffix "test" part
284+
assert.strictEqual(diagnostic.range.startColumn, 14); // 13 + 1 (".")
285+
assert.strictEqual(diagnostic.range.endColumn, 18); // 17 + 1 (".")
286+
});
287+
288+
test('should handle case where newOffsetRange is null', () => {
289+
const collection = new DiagnosticsCollection();
290+
const diagnostic = createDiagnostic('Test error', new Range(1, 13, 1, 17)); // "test" = positions 12-15 (1-based: 13-17)
291+
collection.isEqualAndUpdate([diagnostic]);
292+
293+
// Mock applyEditsToRanges to return null (would happen if range is completely removed)
294+
const before = new StringText('hello world test');
295+
const after = new StringText('hello world'); // "test" completely removed
296+
297+
// Remove " test" completely (0-based: positions 11-15)
298+
const edit = StringEdit.replace(new OffsetRange(11, 16), '');
299+
300+
const hasInvalidated = collection.applyEdit(before, edit, after);
301+
302+
assert.strictEqual(hasInvalidated, true);
303+
assert.strictEqual(diagnostic.isValid(), false);
304+
});
305+
});
306+
});

src/extension/inlineEdits/vscode-node/features/diagnosticsBasedCompletions/anyDiagnosticsCompletionProvider.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ export class AnyDiagnosticCompletionItem extends DiagnosticCompletionItem {
2323
constructor(
2424
codeAction: IAnyCodeAction,
2525
diagnostic: Diagnostic,
26-
nextEditDisplayLocation: INextEditDisplayLocation | undefined,
26+
private readonly _nextEditDisplayLocation: INextEditDisplayLocation | undefined,
2727
workspaceDocument: IVSCodeObservableDocument,
2828
) {
29-
super(codeAction.type, diagnostic, codeAction.edit, nextEditDisplayLocation, workspaceDocument);
29+
super(codeAction.type, diagnostic, codeAction.edit, workspaceDocument);
30+
}
31+
32+
protected override _getDisplayLocation(): INextEditDisplayLocation | undefined {
33+
return this._nextEditDisplayLocation;
3034
}
3135
}
3236

src/extension/inlineEdits/vscode-node/features/diagnosticsBasedCompletions/asyncDiagnosticsCompletionProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class AsyncDiagnosticCompletionItem extends DiagnosticCompletionItem {
2121
edit: TextReplacement,
2222
workspaceDocument: IVSCodeObservableDocument,
2323
) {
24-
super(AsyncDiagnosticCompletionItem.type, diagnostic, edit, undefined, workspaceDocument);
24+
super(AsyncDiagnosticCompletionItem.type, diagnostic, edit, workspaceDocument);
2525
}
2626
}
2727
export class AsyncDiagnosticCompletionProvider implements IDiagnosticCompletionProvider<AsyncDiagnosticCompletionItem> {

0 commit comments

Comments
 (0)