Merge pull request #204846 from microsoft/aiday/bugInGetEditsGeneration

Fixing incorrect edit generation in `_getEdits`
This commit is contained in:
Aiday Marlen Kyzy
2024-02-12 16:00:28 +01:00
committed by GitHub
4 changed files with 138 additions and 22 deletions
+9
View File
@@ -254,6 +254,15 @@ export function splitLines(str: string): string[] {
return str.split(/\r\n|\r|\n/);
}
export function splitLinesIncludeSeparators(str: string): string[] {
const linesWithSeparators: string[] = [];
const splitLinesAndSeparators = str.split(/(\r\n|\r|\n)/);
for (let i = 0; i < Math.ceil(splitLinesAndSeparators.length / 2); i++) {
linesWithSeparators.push(splitLinesAndSeparators[2 * i] + (splitLinesAndSeparators[2 * i + 1] ?? ''));
}
return linesWithSeparators;
}
/**
* Returns first index of the string that is not whitespace.
* If string is empty or contains only whitespaces, returns -1
@@ -7,7 +7,7 @@ import { mapFindFirst } from 'vs/base/common/arraysFind';
import { BugIndicatingError, onUnexpectedExternalError } from 'vs/base/common/errors';
import { Disposable } from 'vs/base/common/lifecycle';
import { IObservable, IReader, ITransaction, autorun, derived, derivedHandleChanges, derivedOpts, recomputeInitiallyAndOnChange, observableSignal, observableValue, subtransaction, transaction } from 'vs/base/common/observable';
import { commonPrefixLength } from 'vs/base/common/strings';
import { commonPrefixLength, splitLinesIncludeSeparators } from 'vs/base/common/strings';
import { isDefined } from 'vs/base/common/types';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditOperation } from 'vs/editor/common/core/editOperation';
@@ -22,7 +22,7 @@ import { GhostText, GhostTextOrReplacement, ghostTextOrReplacementEquals, ghostT
import { InlineCompletionWithUpdatedRange, InlineCompletionsSource } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource';
import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit';
import { SuggestItemInfo } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider';
import { Permutation, addPositions, getNewRanges, lengthOfText } from 'vs/editor/contrib/inlineCompletions/browser/utils';
import { Permutation, addPositions, getNewRanges, lengthOfText, subtractPositions } from 'vs/editor/contrib/inlineCompletions/browser/utils';
import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
@@ -223,7 +223,7 @@ export class InlineCompletionsModel extends Disposable {
const mode = this._suggestPreviewMode.read(reader);
const positions = this._positions.read(reader);
const edits = [fullEdit, ...this._getSecondaryEdits(this.textModel, positions, fullEdit)];
const edits = [fullEdit, ...getSecondaryEdits(this.textModel, positions, fullEdit)];
const ghostTexts = edits
.map((edit, idx) => edit.computeGhostText(model, mode, positions[idx], fullEditPreviewLength))
.filter(isDefined);
@@ -237,7 +237,7 @@ export class InlineCompletionsModel extends Disposable {
const replacement = inlineCompletion.toSingleTextEdit(reader);
const mode = this._inlineSuggestMode.read(reader);
const positions = this._positions.read(reader);
const edits = [replacement, ...this._getSecondaryEdits(this.textModel, positions, replacement)];
const edits = [replacement, ...getSecondaryEdits(this.textModel, positions, replacement)];
const ghostTexts = edits
.map((edit, idx) => edit.computeGhostText(model, mode, positions[idx], 0))
.filter(isDefined);
@@ -432,7 +432,7 @@ export class InlineCompletionsModel extends Disposable {
const replaceRange = Range.fromPositions(cursorPosition, ghostTextPos);
const newText = editor.getModel()!.getValueInRange(replaceRange) + partialGhostTextVal;
const primaryEdit = new SingleTextEdit(replaceRange, newText);
const edits = [primaryEdit, ...this._getSecondaryEdits(this.textModel, positions, primaryEdit)];
const edits = [primaryEdit, ...getSecondaryEdits(this.textModel, positions, primaryEdit)];
const selections = getEndPositionsAfterApplying(edits).map(p => Selection.fromPositions(p));
editor.executeEdits('inlineSuggestion.accept', edits.map(edit => EditOperation.replaceMove(edit.range, edit.text)));
editor.setSelections(selections, 'inlineCompletionPartialAccept');
@@ -455,23 +455,6 @@ export class InlineCompletionsModel extends Disposable {
}
}
private _getSecondaryEdits(textModel: ITextModel, positions: readonly Position[], primaryEdit: SingleTextEdit): SingleTextEdit[] {
const primaryPosition = positions[0];
const secondaryPositions = positions.slice(1);
const replacedTextAfterPrimaryCursor = textModel
.getLineContent(primaryPosition.lineNumber)
.substring(primaryPosition.column - 1, primaryEdit.range.endColumn - 1);
const secondaryEditText = primaryEdit.text.substring(primaryPosition.column - primaryEdit.range.startColumn);
return secondaryPositions.map(pos => {
const textAfterSecondaryCursor = this.textModel
.getLineContent(pos.lineNumber)
.substring(pos.column - 1);
const l = commonPrefixLength(replacedTextAfterPrimaryCursor, textAfterSecondaryCursor);
const range = Range.fromPositions(pos, pos.delta(0, l));
return new SingleTextEdit(range, secondaryEditText);
});
}
public handleSuggestAccepted(item: SuggestItemInfo) {
const itemEdit = item.toSingleTextEdit().removeCommonPrefix(this.textModel);
const augmentedCompletion = this._computeAugmentation(itemEdit, undefined);
@@ -486,6 +469,34 @@ export class InlineCompletionsModel extends Disposable {
}
}
export function getSecondaryEdits(textModel: ITextModel, positions: readonly Position[], primaryEdit: SingleTextEdit): SingleTextEdit[] {
const primaryPosition = positions[0];
const secondaryPositions = positions.slice(1);
const primaryEditEndPosition = primaryEdit.range.getEndPosition();
const replacedTextAfterPrimaryCursor = textModel.getValueInRange(
Range.fromPositions(primaryPosition, primaryEditEndPosition)
);
const positionWithinTextEdit = subtractPositions(primaryPosition, primaryEdit.range.getStartPosition());
const secondaryEditText = getTextFromPosition(primaryEdit.text, positionWithinTextEdit);
return secondaryPositions.map(pos => {
const textAfterSecondaryCursor = textModel.getValueInRange(
Range.fromPositions(pos, primaryEditEndPosition)
);
const l = commonPrefixLength(replacedTextAfterPrimaryCursor, textAfterSecondaryCursor);
const range = Range.fromPositions(pos, pos.delta(0, l));
return new SingleTextEdit(range, secondaryEditText);
});
}
function getTextFromPosition(text: string, pos: Position): string {
let subtext = '';
const lines = splitLinesIncludeSeparators(text);
for (let i = pos.lineNumber - 1; i < lines.length; i++) {
subtext += lines[i].substring(i === pos.lineNumber - 1 ? pos.column - 1 : 0);
}
return subtext;
}
function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Position[] {
const sortPerm = Permutation.createSortPermutation(edits, (edit1, edit2) => Range.compareRangesUsingStarts(edit1.range, edit2.range));
const sortedNewRanges = getNewRanges(sortPerm.apply(edits));
@@ -96,6 +96,10 @@ export function addPositions(pos1: Position, pos2: Position): Position {
return new Position(pos1.lineNumber + pos2.lineNumber - 1, pos2.lineNumber === 1 ? pos1.column + pos2.column - 1 : pos2.column);
}
export function subtractPositions(pos1: Position, pos2: Position): Position {
return new Position(pos1.lineNumber - pos2.lineNumber + 1, pos1.lineNumber - pos2.lineNumber === 0 ? pos1.column - pos2.column + 1 : pos1.column);
}
export function lengthOfText(text: string): Position {
let line = 1;
let column = 1;
@@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Position } from 'vs/editor/common/core/position';
import { getSecondaryEdits } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel';
import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit';
import { createTextModel } from 'vs/editor/test/common/testTextModel';
import { Range } from 'vs/editor/common/core/range';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
suite('inlineCompletionModel', () => {
ensureNoDisposablesAreLeakedInTestSuite();
test('getSecondaryEdits - basic', async function () {
const textModel = createTextModel([
'function fib(',
'function fib('
].join('\n'));
const positions = [
new Position(1, 14),
new Position(2, 14)
];
const primaryEdit = new SingleTextEdit(new Range(1, 1, 1, 14), 'function fib() {');
const secondaryEdits = getSecondaryEdits(textModel, positions, primaryEdit);
assert.deepStrictEqual(secondaryEdits, [new SingleTextEdit(
new Range(2, 14, 2, 14),
') {'
)]);
textModel.dispose();
});
test('getSecondaryEdits - cursor not on same line as primary edit 1', async function () {
const textModel = createTextModel([
'function fib(',
'',
'function fib(',
''
].join('\n'));
const positions = [
new Position(2, 1),
new Position(4, 1)
];
const primaryEdit = new SingleTextEdit(new Range(1, 1, 2, 1), [
'function fib() {',
' return 0;',
'}'
].join('\n'));
const secondaryEdits = getSecondaryEdits(textModel, positions, primaryEdit);
assert.deepStrictEqual(secondaryEdits, [new SingleTextEdit(
new Range(4, 1, 4, 1), [
' return 0;',
'}'
].join('\n')
)]);
textModel.dispose();
});
test('getSecondaryEdits - cursor not on same line as primary edit 2', async function () {
const textModel = createTextModel([
'class A {',
'',
'class B {',
'',
'function f() {}'
].join('\n'));
const positions = [
new Position(2, 1),
new Position(4, 1)
];
const primaryEdit = new SingleTextEdit(new Range(1, 1, 2, 1), [
'class A {',
' public x: number = 0;',
' public y: number = 0;',
'}'
].join('\n'));
const secondaryEdits = getSecondaryEdits(textModel, positions, primaryEdit);
assert.deepStrictEqual(secondaryEdits, [new SingleTextEdit(
new Range(4, 1, 4, 1), [
' public x: number = 0;',
' public y: number = 0;',
'}'
].join('\n')
)]);
textModel.dispose();
});
});