mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-14 01:34:20 +01:00
Merge pull request #204846 from microsoft/aiday/bugInGetEditsGeneration
Fixing incorrect edit generation in `_getEdits`
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user