Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/inlineEdits/test/vscode-node/isInlineSuggestion.spec.ts
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
6
import { assert, suite, test } from 'vitest';
7
import { Position, Range, Uri } from 'vscode';
8
import { createTextDocumentData } from '../../../../util/common/test/shims/textDocument';
9
import { toInlineSuggestion } from '../../vscode-node/isInlineSuggestion';
10
11
suite('toInlineSuggestion', () => {
12
13
function createMockDocument(lines: string[], languageId: string = 'typescript') {
14
return createTextDocumentData(Uri.from({ scheme: 'test', path: '/test/file.ts' }), lines.join('\n'), languageId).document;
15
}
16
17
function getBaseCompletionScenario() {
18
const document = createMockDocument(['This is line 1,', 'This is line,', 'This is line 3,']);
19
const replaceRange = new Range(1, 0, 1, 13);
20
const completionInsertionPoint = new Position(1, 12);
21
const replaceText = 'This is line 2,';
22
return { document, completionInsertionPoint, replaceRange, replaceText };
23
}
24
25
test('line before completion', () => {
26
const { document, completionInsertionPoint, replaceRange, replaceText } = getBaseCompletionScenario();
27
28
const cursorPosition = new Position(completionInsertionPoint.line - 1, completionInsertionPoint.character);
29
30
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
31
});
32
33
test('same line before completion', () => {
34
const { document, completionInsertionPoint, replaceRange, replaceText } = getBaseCompletionScenario();
35
36
const cursorPosition = new Position(completionInsertionPoint.line, completionInsertionPoint.character - 1);
37
38
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
39
assert.isDefined(result);
40
assert.deepStrictEqual(result!.range, replaceRange);
41
assert.strictEqual(result!.newText, replaceText);
42
});
43
44
test('same line at completion', () => {
45
const { document, completionInsertionPoint, replaceRange, replaceText } = getBaseCompletionScenario();
46
47
const cursorPosition = new Position(completionInsertionPoint.line, completionInsertionPoint.character);
48
49
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
50
assert.isDefined(result);
51
assert.deepStrictEqual(result!.range, replaceRange);
52
assert.strictEqual(result!.newText, replaceText);
53
});
54
55
test('same line after completion', () => {
56
const { document, completionInsertionPoint, replaceRange, replaceText } = getBaseCompletionScenario();
57
58
const cursorPosition = new Position(completionInsertionPoint.line, completionInsertionPoint.character + 1);
59
60
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
61
});
62
63
test('line after completion', () => {
64
const { document, completionInsertionPoint, replaceRange, replaceText } = getBaseCompletionScenario();
65
66
const cursorPosition = new Position(completionInsertionPoint.line + 1, completionInsertionPoint.character);
67
68
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
69
});
70
71
test('multi-line replace range', () => {
72
const document = createMockDocument(['This is line 1,', 'This is line,', 'This is line,']);
73
const replaceRange = new Range(1, 0, 2, 13);
74
const replaceText = 'This is line 2,\nThis is line 3,';
75
76
const cursorPosition = replaceRange.start;
77
78
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
79
});
80
81
test('multi-line insertion on same line', () => {
82
const document = createMockDocument(['This is line 1,', 'This is line,', 'This is line 5,']);
83
const replaceRange = new Range(1, 12, 1, 13);
84
const replaceText = ' 2,\nThis is line 3,\nThis is line 4,';
85
86
const cursorPosition = replaceRange.start;
87
88
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
89
assert.isDefined(result);
90
assert.deepStrictEqual(result!.range, replaceRange);
91
assert.strictEqual(result!.newText, replaceText);
92
});
93
94
test('multi-line insertion on next line extends range to cursor', () => {
95
const document = createMockDocument(['This is line 1,', 'This is line 2,', 'This is line 5,']);
96
const cursorPosition = new Position(1, 15); // end of "This is line 2,"
97
const replaceRange = new Range(2, 0, 2, 0);
98
const replaceText = 'This is line 3,\nThis is line 4,\n';
99
100
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
101
assert.isDefined(result);
102
// Range is an empty range at the cursor for a pure insertion
103
assert.deepStrictEqual(result!.range, new Range(1, 15, 1, 15));
104
// Text is prepended with the newline between cursor and original range,
105
// and the trailing newline is dropped so we don't introduce a blank line.
106
assert.strictEqual(result!.newText, '\n' + replaceText.replace(/\r?\n$/, ''));
107
});
108
109
test('should not use ghost text when inserting on next line when none empty', () => {
110
const document = createMockDocument(['This is line 1,', 'This is line 2,', 'line 3,']);
111
const cursorPosition = new Position(1, 15);
112
const replaceRange = new Range(2, 0, 2, 0);
113
const replaceText = 'This is ';
114
115
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
116
});
117
118
// Even though this would be a nice way to render the suggestion, ghost text view on the core side
119
// is not able to render such suggestions
120
test('should not use ghost text when inserting on existing line below', () => {
121
const document = createMockDocument(['This is line 1,', 'This is line 2,', '', 'This is line 4,']);
122
const cursorPosition = new Position(1, 15);
123
const replaceRange = new Range(2, 0, 2, 0);
124
const replaceText = 'This is line 3,';
125
126
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
127
});
128
129
// Tests probing the behavior change: multi-line next-line insertions
130
// where newText does not end with '\n'
131
132
test('multi-line insertion on next empty line without trailing newline', () => {
133
const document = createMockDocument(['function foo(', '', 'other']);
134
const cursorPosition = new Position(0, 13); // end of "function foo("
135
const replaceRange = new Range(1, 0, 1, 0); // empty line
136
const replaceText = ' a: string,\n b: number\n)';
137
138
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
139
assert.isDefined(result);
140
assert.deepStrictEqual(result!.range, new Range(0, 13, 0, 13));
141
assert.strictEqual(result!.newText, '\n' + replaceText);
142
});
143
144
test('multi-line insertion on next non-empty line with trailing newline', () => {
145
const document = createMockDocument(['function foo(', ')', 'other']);
146
const cursorPosition = new Position(0, 13); // end of "function foo("
147
const replaceRange = new Range(1, 0, 1, 0); // non-empty line ")"
148
const replaceText = ' a: string,\n b: number\n';
149
150
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
151
assert.isDefined(result);
152
assert.deepStrictEqual(result!.range, new Range(0, 13, 0, 13));
153
// Trailing '\n' is dropped to avoid a spurious blank line.
154
assert.strictEqual(result!.newText, '\n' + replaceText.replace(/\r?\n$/, ''));
155
});
156
157
test('multi-line insertion without trailing newline rejected when target line has content', () => {
158
const document = createMockDocument(['function foo(', ')', 'other']);
159
const cursorPosition = new Position(0, 13);
160
const replaceRange = new Range(1, 0, 1, 0);
161
const replaceText = ' a: string,\n b: number';
162
163
// newText doesn't end with \n, and target line ")" is non-empty → undefined
164
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
165
});
166
167
test('single-line insertion on next empty line is not an inline suggestion', () => {
168
const document = createMockDocument(['function foo(', '', 'other']);
169
const cursorPosition = new Position(0, 13);
170
const replaceRange = new Range(1, 0, 1, 0);
171
const replaceText = ' a: string';
172
173
// Single-line text has no \n — neither endsWith nor includes matches
174
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
175
});
176
177
test('render ghost text for next line suggestion with massaged range', () => {
178
179
const document = createMockDocument([`import * as vscode from 'vscode';
180
import { NodeTypesIndex } from './nodeTypesIndex';
181
import { Result } from './util/common/result';
182
183
export class NodeTypesOutlineProvider implements vscode.DocumentSymbolProvider {
184
185
/**
186
* @remark This works only for valid tree-sitter \`node-types.json\` files.
187
*/
188
provideDocumentSymbols(
189
document: vscode.TextDocument,
190
token: vscode.CancellationToken
191
): vscode.ProviderResult<vscode.SymbolInformation[] | vscode.DocumentSymbol[]> {
192
193
const nodeTypesIndex = new NodeTypesIndex(document);
194
195
const astNodes = nodeTypesIndex.nodes;
196
197
if (Result.isErr(astNodes)) {
198
throw astNodes.err;
199
}
200
201
const symbols: vscode.DocumentSymbol[] = astNodes.val.map(astNode => {
202
const range = new vscode.Range(
203
document.positionAt(astNode.offset),
204
document.positionAt(astNode.offset + astNode.length)
205
);
206
207
const revealRange = new vscode.Range(
208
document.positionAt(astNode.type.offset),
209
document.positionAt(astNode.type.offset + astNode.type.length)
210
);
211
212
return new vscode.DocumentSymbol(
213
astNode.type.value,
214
astNode.named.value ? 'Named' : 'Anonymous',
215
vscode.SymbolKind.Object,
216
range,
217
revealRange,
218
);
219
});
220
221
return symbols;
222
}
223
}
224
function createDocumentSymbol(
225
`]);
226
const cursorPosition = new Position(45, 30);
227
const replaceRange = new Range(46, 0, 46, 0);
228
const replaceText = ` astNode: { type: { value: string; offset: number; length: number }; named: { value: boolean }; offset: number; length: number },
229
document: vscode.TextDocument
230
): vscode.DocumentSymbol {
231
const range = new vscode.Range(
232
document.positionAt(astNode.offset),
233
document.positionAt(astNode.offset + astNode.length)
234
);
235
236
const revealRange = new vscode.Range(
237
document.positionAt(astNode.type.offset),
238
document.positionAt(astNode.type.offset + astNode.type.length)
239
);
240
241
return new vscode.DocumentSymbol(
242
astNode.type.value,
243
astNode.named.value ? 'Named' : 'Anonymous',
244
vscode.SymbolKind.Object,
245
range,
246
revealRange,
247
);
248
}`;
249
250
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
251
assert.isDefined(result);
252
// Range is an empty range at cursor position
253
assert.deepStrictEqual(result!.range, new Range(45, 30, 45, 30));
254
// Text is prepended with newline
255
assert.strictEqual(result!.newText, '\n' + replaceText);
256
});
257
258
// --- Branch 1 regression: next-line insertion edge cases ---
259
260
test('next-line: cursor mid-line rejects even with valid next-line edit', () => {
261
const document = createMockDocument(['function foo(bar', '', 'other']);
262
const cursorPosition = new Position(0, 8); // middle of "function foo(bar"
263
const replaceRange = new Range(1, 0, 1, 0);
264
const replaceText = ' param1,\n param2\n';
265
266
// Cursor not at end of line → rejected
267
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
268
});
269
270
test('next-line: non-empty range on next line falls through and is rejected', () => {
271
const document = createMockDocument(['function foo(', 'old content', 'other']);
272
const cursorPosition = new Position(0, 13);
273
const replaceRange = new Range(1, 0, 1, 11); // non-empty range replacing "old content"
274
const replaceText = 'new content\n';
275
276
// range.isEmpty is false → branch 1 skipped, branch 2 rejects (different line)
277
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
278
});
279
280
test('next-line: non-empty replace range covering only whitespace on next line', () => {
281
const document = createMockDocument([
282
' for item in items:',
283
' ',
284
'other_code',
285
], 'python');
286
const cursorPosition = new Position(1, 4);
287
const replaceRange = new Range(0, 22, 1, 8);
288
const replaceText = '\n process(item)\n return result';
289
290
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
291
assert.isDefined(result);
292
});
293
294
test('next-line: range 2 lines ahead is rejected', () => {
295
const document = createMockDocument(['line 0', 'line 1', '', 'line 3']);
296
const cursorPosition = new Position(0, 6);
297
const replaceRange = new Range(2, 0, 2, 0);
298
const replaceText = 'inserted\n';
299
300
// cursorPos.line + 1 !== range.start.line → rejected
301
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
302
});
303
304
test('next-line: empty range at non-zero column on next line is rejected', () => {
305
const document = createMockDocument(['function foo(', ' ', 'other']);
306
const cursorPosition = new Position(0, 13);
307
const replaceRange = new Range(1, 4, 1, 4); // empty range at col 4
308
const replaceText = 'a: string,\n b: number\n';
309
310
// range.start.character !== 0 → rejected
311
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
312
});
313
314
test('next-line: inserting just a newline character', () => {
315
const document = createMockDocument(['line 0', '', 'line 2']);
316
const cursorPosition = new Position(0, 6);
317
const replaceRange = new Range(1, 0, 1, 0);
318
const replaceText = '\n';
319
320
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
321
assert.isDefined(result);
322
assert.deepStrictEqual(result!.range, new Range(0, 6, 0, 6));
323
// Trailing '\n' is dropped — only the prepended newline remains.
324
assert.strictEqual(result!.newText, '\n');
325
});
326
327
test('next-line: cursor at end of an empty line', () => {
328
const document = createMockDocument(['', '', 'other']);
329
const cursorPosition = new Position(0, 0); // end of empty line 0
330
const replaceRange = new Range(1, 0, 1, 0);
331
const replaceText = 'new line\n';
332
333
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
334
assert.isDefined(result);
335
assert.deepStrictEqual(result!.range, new Range(0, 0, 0, 0));
336
// Trailing '\n' is dropped to avoid a spurious blank line.
337
assert.strictEqual(result!.newText, '\nnew line');
338
});
339
340
test('next-line: range on line before cursor is rejected', () => {
341
const document = createMockDocument(['line 0', 'line 1', 'line 2']);
342
const cursorPosition = new Position(2, 6);
343
const replaceRange = new Range(1, 0, 1, 0);
344
const replaceText = 'inserted\n';
345
346
// cursorPos.line + 1 !== range.start.line (1 !== 3)
347
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
348
});
349
350
// --- Branch 2 regression: same-line edit edge cases ---
351
352
test('same-line: cursor before range start rejects', () => {
353
const document = createMockDocument(['abcdef']);
354
const cursorPosition = new Position(0, 1);
355
const replaceRange = new Range(0, 3, 0, 6); // replaces "def"
356
const replaceText = 'defgh';
357
358
// cursorOffsetInReplacedText < 0
359
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
360
});
361
362
test('same-line: text before cursor differs rejects', () => {
363
const document = createMockDocument(['abcdef']);
364
const cursorPosition = new Position(0, 4);
365
const replaceRange = new Range(0, 0, 0, 6);
366
const replaceText = 'XXXX_modified';
367
368
// "abcd" !== "XXXX" → text before cursor mismatch
369
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
370
});
371
372
test('same-line: replaced text is not subword of new text rejects', () => {
373
const document = createMockDocument(['abcxyz']);
374
const cursorPosition = new Position(0, 0);
375
const replaceRange = new Range(0, 0, 0, 6);
376
const replaceText = 'abc'; // "abcxyz" is not a subword of "abc"
377
378
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
379
});
380
381
test('same-line: deletion (empty newText) rejects', () => {
382
const document = createMockDocument(['abcdef']);
383
const cursorPosition = new Position(0, 0);
384
const replaceRange = new Range(0, 0, 0, 3);
385
const replaceText = '';
386
387
// "abc" is not a subword of "" → rejected
388
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
389
});
390
391
test('same-line: empty range and empty text at cursor (no-op) succeeds', () => {
392
const document = createMockDocument(['abcdef']);
393
const cursorPosition = new Position(0, 3);
394
const replaceRange = new Range(0, 3, 0, 3);
395
const replaceText = '';
396
397
// Empty replaced text is trivially a subword of empty new text
398
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
399
assert.isDefined(result);
400
assert.deepStrictEqual(result!.range, new Range(0, 3, 0, 3));
401
assert.strictEqual(result!.newText, '');
402
});
403
404
test('same-line: pure insertion (empty range) at cursor', () => {
405
const document = createMockDocument(['ab']);
406
const cursorPosition = new Position(0, 1);
407
const replaceRange = new Range(0, 1, 0, 1);
408
const replaceText = 'XY';
409
410
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
411
assert.isDefined(result);
412
assert.deepStrictEqual(result!.range, new Range(0, 1, 0, 1));
413
assert.strictEqual(result!.newText, 'XY');
414
});
415
416
test('same-line: cursor at col 0 with range at col 0', () => {
417
const document = createMockDocument(['hello']);
418
const cursorPosition = new Position(0, 0);
419
const replaceRange = new Range(0, 0, 0, 5);
420
const replaceText = 'hello world';
421
422
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
423
assert.isDefined(result);
424
assert.deepStrictEqual(result!.range, replaceRange);
425
assert.strictEqual(result!.newText, 'hello world');
426
});
427
428
test('same-line: subword insertion mid-word', () => {
429
const document = createMockDocument(['clog']);
430
const cursorPosition = new Position(0, 1);
431
const replaceRange = new Range(0, 0, 0, 4);
432
const replaceText = 'console.log';
433
434
// "clog" IS a subword of "console.log" (c...o...l...og)
435
// But text before cursor: replaced[0..1]="c", new[0..1]="c" → match
436
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
437
assert.isDefined(result);
438
assert.strictEqual(result!.newText, 'console.log');
439
});
440
441
// --- Prefix-stripping: multi-line range reduction ---
442
443
test('prefix-strip: multi-line range reduced to single-line edit on cursor line', () => {
444
// Range spans lines 0-1, replaced text = "abc\ndef", newText = "abc\ndefghi"
445
// Common prefix up to newline = "abc\n", strip it → range becomes (1,0)-(1,3), newText = "defghi"
446
const document = createMockDocument(['abc', 'def', 'other']);
447
const cursorPosition = new Position(1, 0);
448
const replaceRange = new Range(0, 0, 1, 3);
449
const replaceText = 'abc\ndefghi';
450
451
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
452
assert.isDefined(result);
453
assert.deepStrictEqual(result!.range, new Range(1, 0, 1, 3));
454
assert.strictEqual(result!.newText, 'defghi');
455
});
456
457
test('prefix-strip: no newline in common prefix, multi-line range still rejected', () => {
458
// Range spans lines 0-1, replaced = "ab\ncd", newText = "abXY"
459
// Common prefix = "ab" but no newline → no stripping → multi-line range rejected
460
const document = createMockDocument(['ab', 'cd', 'other']);
461
const cursorPosition = new Position(0, 0);
462
const replaceRange = new Range(0, 0, 1, 2);
463
const replaceText = 'abXY';
464
465
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
466
});
467
468
test('prefix-strip: strips multiple newlines to last boundary', () => {
469
// Range spans 3 lines: "line0\nline1\nxy", newText = "line0\nline1\nxyz"
470
// Common prefix includes two newlines, stripping to last → range becomes (2,0)-(2,2)
471
const document = createMockDocument(['line0', 'line1', 'xy', 'other']);
472
const cursorPosition = new Position(2, 0);
473
const replaceRange = new Range(0, 0, 2, 2);
474
const replaceText = 'line0\nline1\nxyz';
475
476
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
477
assert.isDefined(result);
478
assert.deepStrictEqual(result!.range, new Range(2, 0, 2, 2));
479
assert.strictEqual(result!.newText, 'xyz');
480
});
481
482
test('prefix-strip: after stripping still multi-line, rejected', () => {
483
// Range spans 3 lines: "a\nb\nc", newText = "a\nB\nC"
484
// Common prefix up to newline = "a\n", strip → range becomes (1,0)-(2,1) which is still multi-line
485
const document = createMockDocument(['a', 'b', 'c', 'other']);
486
const cursorPosition = new Position(1, 0);
487
const replaceRange = new Range(0, 0, 2, 1);
488
const replaceText = 'a\nB\nC';
489
490
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
491
});
492
493
test('prefix-strip: reduced to single line but cursor on different line, rejected', () => {
494
// Strip reduces range to line 1, but cursor is on line 0
495
const document = createMockDocument(['abc', 'def', 'other']);
496
const cursorPosition = new Position(0, 2);
497
const replaceRange = new Range(0, 0, 1, 3);
498
const replaceText = 'abc\ndefghi';
499
500
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
501
});
502
503
test('prefix-strip: reduced to single line, subword check fails', () => {
504
// After stripping "abc\n", range = (1,0)-(1,3) with replaced "def", newText = "xy"
505
// "def" is not a subword of "xy"
506
const document = createMockDocument(['abc', 'def', 'other']);
507
const cursorPosition = new Position(1, 0);
508
const replaceRange = new Range(0, 0, 1, 3);
509
const replaceText = 'abc\nxy';
510
511
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
512
});
513
514
test('prefix-strip: diverges before first newline, no stripping', () => {
515
// replaced = "ax\nyz", newText = "ab\nyz" → common prefix "a" has no newline → no strip
516
const document = createMockDocument(['ax', 'yz']);
517
const cursorPosition = new Position(0, 0);
518
const replaceRange = new Range(0, 0, 1, 2);
519
const replaceText = 'ab\nyz';
520
521
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
522
});
523
524
test('prefix-strip: range starts mid-line, strips prefix through newline', () => {
525
// Document: "hello world", " ns", "other"
526
// Range (0,6)-(1,4) → replaced text = "world\n ns", newText = "world\n new_stuff"
527
// Common prefix = "world\n " → last newline at index 5, strip "world\n"
528
// Reduced range: (1,0)-(1,4), newText = " new_stuff"
529
// isSubword(" ns", " new_stuff") → true
530
const document = createMockDocument(['hello world', ' ns', 'other']);
531
const cursorPosition = new Position(1, 0);
532
const replaceRange = new Range(0, 6, 1, 4);
533
const replaceText = 'world\n new_stuff';
534
535
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
536
assert.isDefined(result);
537
assert.deepStrictEqual(result!.range, new Range(1, 0, 1, 4));
538
assert.strictEqual(result!.newText, ' new_stuff');
539
});
540
541
test('prefix-strip: empty newText after stripping prefix', () => {
542
// replaced = "abc\n", newText = "abc\n" → after stripping "abc\n", replaced="" and newText=""
543
// This is a no-op on the second line, succeeds as empty edit
544
const document = createMockDocument(['abc', '', 'other']);
545
const cursorPosition = new Position(1, 0);
546
const replaceRange = new Range(0, 0, 1, 0);
547
const replaceText = 'abc\n';
548
549
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
550
assert.isDefined(result);
551
assert.deepStrictEqual(result!.range, new Range(1, 0, 1, 0));
552
assert.strictEqual(result!.newText, '');
553
});
554
555
test('insertion on next line in fieldLabels object', () => {
556
const doc = `import React, { useState } from "react";
557
558
interface FormData {
559
firstName: string;
560
lastName: string;
561
password: string;
562
email: string;
563
age: string;
564
city: string;
565
}
566
567
const initialFormData: FormData = {
568
firstName: "",
569
lastName: "",
570
password: "",
571
email: "",
572
age: "",
573
city: "",
574
};
575
576
const fieldLabels: Record<keyof FormData, string> = {
577
firstName: "First Name",
578
lastName: "Last Name",
579
email: "Email Address",
580
age: "Age",
581
city: "City",
582
};
583
`;
584
const document = createTextDocumentData(Uri.from({ scheme: 'test', path: '/test/file.tsx' }), doc, 'typescriptreact').document;
585
const cursorPosition = new Position(22, 26); // end of ` lastName: "Last Name",`
586
const replaceRange = new Range(23, 0, 23, 0);
587
const replaceText = ' password: "Password",\n';
588
589
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText, true);
590
assert.isDefined(result);
591
assert.deepStrictEqual(result!.range, new Range(22, 26, 22, 26));
592
// Trailing '\n' is dropped because the original line terminator after
593
// the cursor is preserved.
594
assert.strictEqual(result!.newText, '\n password: "Password",');
595
});
596
597
suite('CRLF', () => {
598
599
function createCRLFDocument(lines: string[], languageId: string = 'typescript') {
600
return createTextDocumentData(
601
Uri.from({ scheme: 'test', path: '/test/file.ts' }),
602
lines.join('\r\n'),
603
languageId,
604
'\r\n',
605
).document;
606
}
607
608
test('next-line insertion: trailing CRLF is dropped (no dangling \\r)', () => {
609
const document = createCRLFDocument(['function foo(', '', 'other']);
610
const cursorPosition = new Position(0, 13); // end of "function foo("
611
const replaceRange = new Range(1, 0, 1, 0); // empty line
612
const replaceText = ' a: string,\r\n b: number\r\n';
613
614
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
615
assert.isDefined(result);
616
assert.deepStrictEqual(result!.range, new Range(0, 13, 0, 13));
617
// The trailing CRLF must be stripped entirely; no dangling '\r'
618
// should leak into the suggestion text.
619
assert.strictEqual(result!.newText, '\r\n a: string,\r\n b: number');
620
});
621
622
test('next-line insertion: trailing CRLF on non-empty target line', () => {
623
const document = createCRLFDocument(['function foo(', ')', 'other']);
624
const cursorPosition = new Position(0, 13);
625
const replaceRange = new Range(1, 0, 1, 0);
626
const replaceText = ' a: string,\r\n b: number\r\n';
627
628
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
629
assert.isDefined(result);
630
assert.deepStrictEqual(result!.range, new Range(0, 13, 0, 13));
631
assert.strictEqual(result!.newText, '\r\n a: string,\r\n b: number');
632
});
633
634
test('next-line insertion: CRLF-only newText is fully stripped', () => {
635
const document = createCRLFDocument(['line 0', '', 'line 2']);
636
const cursorPosition = new Position(0, 6);
637
const replaceRange = new Range(1, 0, 1, 0);
638
const replaceText = '\r\n';
639
640
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
641
assert.isDefined(result);
642
assert.deepStrictEqual(result!.range, new Range(0, 6, 0, 6));
643
// Only the prepended CRLF between cursor and original range remains.
644
assert.strictEqual(result!.newText, '\r\n');
645
});
646
});
647
648
suite('multi-line range, no common prefix', () => {
649
650
// Regression: when commonLen === 0 and the replaced text starts with '\n',
651
// `lastIndexOf('\n', -1)` would (incorrectly) clamp to 0 and report a
652
// match, causing the leading newline to be stripped — which can collapse
653
// the multi-line range into a same-line "suggestion" that the function
654
// then accepts. With the original substring-based check, no strip occurs
655
// and the result is `undefined`.
656
test('does not strip leading newline when nothing is in common', () => {
657
const document = createMockDocument(['abc', 'x', 'rest']);
658
// replacedText = '\nx', newText[0]='Y' differs from '\n', commonLen=0.
659
const replaceRange = new Range(0, 3, 1, 1);
660
const cursorPosition = new Position(1, 1);
661
const replaceText = 'Yx';
662
663
// The range cannot legitimately be collapsed to a single line, so
664
// the function must not synthesize a ghost-text suggestion.
665
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
666
});
667
});
668
});
669
670