Skip to content

Commit caaa7bc

Browse files
authored
add tests for toInlineSuggestion prefix stripping logic and add advanced argument with default value (#4231)
1 parent a05ab9d commit caaa7bc

4 files changed

Lines changed: 153 additions & 2 deletions

File tree

src/extension/inlineEdits/test/vscode-node/isInlineSuggestion.spec.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,20 @@ function createDocumentSymbol(
275275
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
276276
});
277277

278+
test('next-line: non-empty replace range covering only whitespace on next line', () => {
279+
const document = createMockDocument([
280+
' for item in items:',
281+
' ',
282+
'other_code',
283+
], 'python');
284+
const cursorPosition = new Position(1, 4);
285+
const replaceRange = new Range(0, 22, 1, 8);
286+
const replaceText = '\n process(item)\n return result';
287+
288+
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
289+
assert.isDefined(result);
290+
});
291+
278292
test('next-line: range 2 lines ahead is rejected', () => {
279293
const document = createMockDocument(['line 0', 'line 1', '', 'line 3']);
280294
const cursorPosition = new Position(0, 6);
@@ -419,4 +433,118 @@ function createDocumentSymbol(
419433
assert.isDefined(result);
420434
assert.strictEqual(result!.newText, 'console.log');
421435
});
436+
437+
// --- Prefix-stripping: multi-line range reduction ---
438+
439+
test('prefix-strip: multi-line range reduced to single-line edit on cursor line', () => {
440+
// Range spans lines 0-1, replaced text = "abc\ndef", newText = "abc\ndefghi"
441+
// Common prefix up to newline = "abc\n", strip it → range becomes (1,0)-(1,3), newText = "defghi"
442+
const document = createMockDocument(['abc', 'def', 'other']);
443+
const cursorPosition = new Position(1, 0);
444+
const replaceRange = new Range(0, 0, 1, 3);
445+
const replaceText = 'abc\ndefghi';
446+
447+
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
448+
assert.isDefined(result);
449+
assert.deepStrictEqual(result!.range, new Range(1, 0, 1, 3));
450+
assert.strictEqual(result!.newText, 'defghi');
451+
});
452+
453+
test('prefix-strip: no newline in common prefix, multi-line range still rejected', () => {
454+
// Range spans lines 0-1, replaced = "ab\ncd", newText = "abXY"
455+
// Common prefix = "ab" but no newline → no stripping → multi-line range rejected
456+
const document = createMockDocument(['ab', 'cd', 'other']);
457+
const cursorPosition = new Position(0, 0);
458+
const replaceRange = new Range(0, 0, 1, 2);
459+
const replaceText = 'abXY';
460+
461+
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
462+
});
463+
464+
test('prefix-strip: strips multiple newlines to last boundary', () => {
465+
// Range spans 3 lines: "line0\nline1\nxy", newText = "line0\nline1\nxyz"
466+
// Common prefix includes two newlines, stripping to last → range becomes (2,0)-(2,2)
467+
const document = createMockDocument(['line0', 'line1', 'xy', 'other']);
468+
const cursorPosition = new Position(2, 0);
469+
const replaceRange = new Range(0, 0, 2, 2);
470+
const replaceText = 'line0\nline1\nxyz';
471+
472+
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
473+
assert.isDefined(result);
474+
assert.deepStrictEqual(result!.range, new Range(2, 0, 2, 2));
475+
assert.strictEqual(result!.newText, 'xyz');
476+
});
477+
478+
test('prefix-strip: after stripping still multi-line, rejected', () => {
479+
// Range spans 3 lines: "a\nb\nc", newText = "a\nB\nC"
480+
// Common prefix up to newline = "a\n", strip → range becomes (1,0)-(2,1) which is still multi-line
481+
const document = createMockDocument(['a', 'b', 'c', 'other']);
482+
const cursorPosition = new Position(1, 0);
483+
const replaceRange = new Range(0, 0, 2, 1);
484+
const replaceText = 'a\nB\nC';
485+
486+
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
487+
});
488+
489+
test('prefix-strip: reduced to single line but cursor on different line, rejected', () => {
490+
// Strip reduces range to line 1, but cursor is on line 0
491+
const document = createMockDocument(['abc', 'def', 'other']);
492+
const cursorPosition = new Position(0, 2);
493+
const replaceRange = new Range(0, 0, 1, 3);
494+
const replaceText = 'abc\ndefghi';
495+
496+
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
497+
});
498+
499+
test('prefix-strip: reduced to single line, subword check fails', () => {
500+
// After stripping "abc\n", range = (1,0)-(1,3) with replaced "def", newText = "xy"
501+
// "def" is not a subword of "xy"
502+
const document = createMockDocument(['abc', 'def', 'other']);
503+
const cursorPosition = new Position(1, 0);
504+
const replaceRange = new Range(0, 0, 1, 3);
505+
const replaceText = 'abc\nxy';
506+
507+
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
508+
});
509+
510+
test('prefix-strip: diverges before first newline, no stripping', () => {
511+
// replaced = "ax\nyz", newText = "ab\nyz" → common prefix "a" has no newline → no strip
512+
const document = createMockDocument(['ax', 'yz']);
513+
const cursorPosition = new Position(0, 0);
514+
const replaceRange = new Range(0, 0, 1, 2);
515+
const replaceText = 'ab\nyz';
516+
517+
assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText));
518+
});
519+
520+
test('prefix-strip: range starts mid-line, strips prefix through newline', () => {
521+
// Document: "hello world", " ns", "other"
522+
// Range (0,6)-(1,4) → replaced text = "world\n ns", newText = "world\n new_stuff"
523+
// Common prefix = "world\n " → last newline at index 5, strip "world\n"
524+
// Reduced range: (1,0)-(1,4), newText = " new_stuff"
525+
// isSubword(" ns", " new_stuff") → true
526+
const document = createMockDocument(['hello world', ' ns', 'other']);
527+
const cursorPosition = new Position(1, 0);
528+
const replaceRange = new Range(0, 6, 1, 4);
529+
const replaceText = 'world\n new_stuff';
530+
531+
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
532+
assert.isDefined(result);
533+
assert.deepStrictEqual(result!.range, new Range(1, 0, 1, 4));
534+
assert.strictEqual(result!.newText, ' new_stuff');
535+
});
536+
537+
test('prefix-strip: empty newText after stripping prefix', () => {
538+
// replaced = "abc\n", newText = "abc\n" → after stripping "abc\n", replaced="" and newText=""
539+
// This is a no-op on the second line, succeeds as empty edit
540+
const document = createMockDocument(['abc', '', 'other']);
541+
const cursorPosition = new Position(1, 0);
542+
const replaceRange = new Range(0, 0, 1, 0);
543+
const replaceText = 'abc\n';
544+
545+
const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText);
546+
assert.isDefined(result);
547+
assert.deepStrictEqual(result!.range, new Range(1, 0, 1, 0));
548+
assert.strictEqual(result!.newText, '');
549+
});
422550
});

src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export class InlineCompletionProviderImpl extends Disposable implements InlineCo
146146

147147
private readonly _displayNextEditorNES: boolean;
148148
private readonly _renameSymbolSuggestions: IObservable<boolean>;
149+
private readonly _inlineCompletionsAdvanced: IObservable<boolean>;
149150

150151
constructor(
151152
private readonly model: InlineEditModel,
@@ -170,6 +171,7 @@ export class InlineCompletionProviderImpl extends Disposable implements InlineCo
170171
this._logger = this._logService.createSubLogger(['NES', 'Provider']);
171172
this._displayNextEditorNES = this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.UseAlternativeNESNotebookFormat, this._expService);
172173
this._renameSymbolSuggestions = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.Advanced.InlineEditsRenameSymbolSuggestions, this._expService);
174+
this._inlineCompletionsAdvanced = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsInlineCompletionsAdvanced, this._expService);
173175

174176
this.setCurrentModelId = (modelId: string) => this._modelService.setCurrentModelId(modelId);
175177

@@ -386,7 +388,7 @@ export class InlineCompletionProviderImpl extends Disposable implements InlineCo
386388
} else if (targetDocument === document) {
387389
// nes is for this same document.
388390
const allowInlineCompletions = this.model.inlineEditsInlineCompletionsEnabled.get();
389-
const inlineSuggestion = allowInlineCompletions ? toInlineSuggestion(position, document, range, result.edit.newText) : undefined;
391+
const inlineSuggestion = allowInlineCompletions ? toInlineSuggestion(position, document, range, result.edit.newText, this._inlineCompletionsAdvanced.get()) : undefined;
390392
isInlineCompletion = !!inlineSuggestion;
391393
completionItem = serveAsCompletionsProvider && !isInlineCompletion ?
392394
undefined :

src/extension/inlineEdits/vscode-node/isInlineSuggestion.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface InlineSuggestionEdit {
1616
* If so, returns the (possibly adjusted) range and text that touches the cursor position,
1717
* which is required for VS Code to render ghost text.
1818
*/
19-
export function toInlineSuggestion(cursorPos: Position, doc: TextDocument, range: Range, newText: string): InlineSuggestionEdit | undefined {
19+
export function toInlineSuggestion(cursorPos: Position, doc: TextDocument, range: Range, newText: string, advanced: boolean = true): InlineSuggestionEdit | undefined {
2020
// If multi line insertion starts on the next line
2121
// All new lines have to be newly created lines
2222
if (range.isEmpty && cursorPos.line + 1 === range.start.line && range.start.character === 0
@@ -29,6 +29,26 @@ export function toInlineSuggestion(cursorPos: Position, doc: TextDocument, range
2929
return { range: adjustedRange, newText: textBetweenCursorAndRange + newText };
3030
}
3131

32+
if (advanced) {
33+
// If the range spans multiple lines, try to reduce it by stripping a common
34+
// prefix (up to a newline boundary) from the replaced text and newText.
35+
if (range.start.line !== range.end.line) {
36+
const fullReplacedText = doc.getText(range);
37+
let commonLen = 0;
38+
const maxLen = Math.min(fullReplacedText.length, newText.length);
39+
while (commonLen < maxLen && fullReplacedText[commonLen] === newText[commonLen]) {
40+
commonLen++;
41+
}
42+
const lastNewline = fullReplacedText.substring(0, commonLen).lastIndexOf('\n');
43+
if (lastNewline >= 0) {
44+
const strippedLen = lastNewline + 1;
45+
newText = newText.substring(strippedLen);
46+
const newStart = doc.positionAt(doc.offsetAt(range.start) + strippedLen);
47+
range = new Range(newStart, range.end);
48+
}
49+
}
50+
}
51+
3252
if (range.start.line !== range.end.line || range.start.line !== cursorPos.line) {
3353
return undefined;
3454
}

src/platform/configuration/common/configurationService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,7 @@ export namespace ConfigKey {
785785
export const InlineEditsXtabProviderModelConfiguration = defineTeamInternalSetting<xtabPromptOptions.ModelConfiguration | undefined>('chat.advanced.inlineEdits.xtabProvider.modelConfiguration', ConfigType.Simple, undefined, xtabPromptOptions.MODEL_CONFIGURATION_VALIDATOR);
786786
export const InlineEditsNextCursorPredictionLintOptions = defineTeamInternalSetting<xtabPromptOptions.LintOptions | undefined>('chat.advanced.inlineEdits.nextCursorPrediction.lintOptions', ConfigType.Simple, undefined, xtabPromptOptions.LINT_OPTIONS_VALIDATOR);
787787
export const InlineEditsInlineCompletionsEnabled = defineTeamInternalSetting<boolean>('chat.advanced.inlineEdits.inlineCompletions.enabled', ConfigType.Simple, true, vBoolean());
788+
export const InlineEditsInlineCompletionsAdvanced = defineTeamInternalSetting<boolean>('chat.advanced.inlineEdits.inlineCompletions.advancedDetection', ConfigType.ExperimentBased, true, vBoolean());
788789
export const InlineEditsXtabProviderUsePrediction = defineTeamInternalSetting<boolean>('chat.advanced.inlineEdits.xtabProvider.usePrediction', ConfigType.ExperimentBased, true, vBoolean());
789790
export const InlineEditsXtabLanguageContextEnabledLanguages = defineTeamInternalSetting<LanguageContextLanguages>('chat.advanced.inlineEdits.xtabProvider.languageContext.enabledLanguages', ConfigType.Simple, LANGUAGE_CONTEXT_ENABLED_LANGUAGES);
790791
export const InlineEditsXtabLanguageContextTraitsPosition = defineTeamInternalSetting<'before' | 'after'>('chat.advanced.inlineEdits.xtabProvider.languageContext.traitsPosition', ConfigType.ExperimentBased, 'before');

0 commit comments

Comments
 (0)