Skip to content

Commit 3804d13

Browse files
authored
Stricter user edit matching for cache hits (#282)
1 parent 3a3559a commit 3804d13

File tree

2 files changed

+78
-3
lines changed

2 files changed

+78
-3
lines changed

src/extension/inlineEdits/common/editRebase.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ function tryRebaseEdits<T extends IEditData<T>>(content: string, ours: Annotated
186186
let baseE = baseEdit;
187187
let previousBaseE: StringReplacement | undefined;
188188
while (baseE && ourE.replaceRange.containsRange(baseE.replaceRange)) {
189-
ourNewTextOffset = agreementIndexOf(content, ourE, baseE, previousBaseE, ourNewTextOffset, nesConfigs);
189+
ourNewTextOffset = agreementIndexOf(content, ourE, baseE, previousBaseE, ourNewTextOffset, resolution, nesConfigs);
190190
if (ourNewTextOffset === -1) {
191191
// Conflicting
192192
return undefined;
@@ -228,7 +228,10 @@ function tryRebaseEdits<T extends IEditData<T>>(content: string, ours: Annotated
228228
return AnnotatedStringEdit.create(newEdits);
229229
}
230230

231-
function agreementIndexOf<T extends IEditData<T>>(content: string, ourE: AnnotatedStringReplacement<T>, baseE: StringReplacement, previousBaseE: StringReplacement | undefined, ourNewTextOffset: number, nesConfigs: NesRebaseConfigs) {
231+
export const maxAgreementOffset = 10; // If the user's typing is more than this into the suggestion we consider it a miss.
232+
export const maxImperfectAgreementLength = 5; // If the user's typing is longer than this and the suggestion is not a perfect match we consider it a miss.
233+
234+
function agreementIndexOf<T extends IEditData<T>>(content: string, ourE: AnnotatedStringReplacement<T>, baseE: StringReplacement, previousBaseE: StringReplacement | undefined, ourNewTextOffset: number, resolution: 'strict' | 'lenient', nesConfigs: NesRebaseConfigs) {
232235
const minStart = previousBaseE ? previousBaseE.replaceRange.endExclusive : ourE.replaceRange.start;
233236
if (minStart < baseE.replaceRange.start) {
234237
baseE = new StringReplacement(
@@ -237,6 +240,12 @@ function agreementIndexOf<T extends IEditData<T>>(content: string, ourE: Annotat
237240
);
238241
}
239242
const j = ourE.newText.indexOf(baseE.newText, ourNewTextOffset);
243+
if (resolution === 'strict' && j > maxAgreementOffset) {
244+
return -1;
245+
}
246+
if (resolution === 'strict' && j > 0 && baseE.newText.length > maxImperfectAgreementLength) {
247+
return -1;
248+
}
240249
return j !== -1 ? j + baseE.newText.length : -1;
241250
}
242251

src/extension/inlineEdits/test/common/editRebase.spec.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { decomposeStringEdit } from '../../../../platform/inlineEdits/common/dat
77
import { createTracer } from '../../../../util/common/tracing';
88
import { StringEdit, StringReplacement } from '../../../../util/vs/editor/common/core/edits/stringEdit';
99
import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';
10-
import { tryRebase, tryRebaseStringEdits } from '../../common/editRebase';
10+
import { maxAgreementOffset, maxImperfectAgreementLength, tryRebase, tryRebaseStringEdits } from '../../common/editRebase';
1111

1212

1313
suite('NextEditCache', () => {
@@ -730,4 +730,70 @@ class Point3D {
730730
expect(lenient?.apply(current)).toStrictEqual('abCDEfg');
731731
expect(lenient?.removeCommonSuffixAndPrefix(current).replacements.toString()).toMatchInlineSnapshot(`"[3, 3) -> "DE""`);
732732
});
733+
734+
test('overlap: both insert in agreement with large offset', () => {
735+
const text = `abcdefg`;
736+
const userEdit = StringEdit.create([
737+
StringReplacement.replace(new OffsetRange(7, 7), 'h'),
738+
]);
739+
const current = userEdit.apply(text);
740+
expect(current).toStrictEqual(`abcdefgh`);
741+
742+
const suggestion1 = StringEdit.create([
743+
StringReplacement.replace(new OffsetRange(7, 7), 'x'.repeat(maxAgreementOffset) + 'h'),
744+
]);
745+
const applied1 = suggestion1.apply(text);
746+
expect(applied1).toStrictEqual(`abcdefg${'x'.repeat(maxAgreementOffset)}h`);
747+
748+
const strict1 = tryRebaseStringEdits(text, suggestion1, userEdit, 'strict');
749+
expect(strict1?.apply(current)).toStrictEqual(applied1);
750+
expect(strict1?.removeCommonSuffixAndPrefix(current).replacements.toString()).toMatchInlineSnapshot(`"[7, 7) -> "${'x'.repeat(maxAgreementOffset)}""`);
751+
const lenient1 = tryRebaseStringEdits(text, suggestion1, userEdit, 'lenient');
752+
expect(lenient1?.apply(current)).toStrictEqual(applied1);
753+
expect(lenient1?.removeCommonSuffixAndPrefix(current).replacements.toString()).toMatchInlineSnapshot(`"[7, 7) -> "${'x'.repeat(maxAgreementOffset)}""`);
754+
755+
const suggestion2 = StringEdit.create([
756+
StringReplacement.replace(new OffsetRange(7, 7), 'x'.repeat(maxAgreementOffset + 1) + 'h'),
757+
]);
758+
const applied2 = suggestion2.apply(text);
759+
expect(applied2).toStrictEqual(`abcdefg${'x'.repeat(maxAgreementOffset + 1)}h`);
760+
761+
expect(tryRebaseStringEdits(text, suggestion2, userEdit, 'strict')).toBeUndefined();
762+
const lenient2 = tryRebaseStringEdits(text, suggestion2, userEdit, 'lenient');
763+
expect(lenient2?.apply(current)).toStrictEqual(applied2);
764+
expect(lenient2?.removeCommonSuffixAndPrefix(current).replacements.toString()).toMatchInlineSnapshot(`"[7, 7) -> "${'x'.repeat(maxAgreementOffset + 1)}""`);
765+
});
766+
767+
test('overlap: both insert in agreement with an offset with longish user edit', () => {
768+
const text = `abcdefg`;
769+
const userEdit1 = StringEdit.create([
770+
StringReplacement.replace(new OffsetRange(7, 7), 'h'.repeat(maxImperfectAgreementLength)),
771+
]);
772+
const current1 = userEdit1.apply(text);
773+
expect(current1).toStrictEqual(`abcdefg${'h'.repeat(maxImperfectAgreementLength)}`);
774+
775+
const suggestion = StringEdit.create([
776+
StringReplacement.replace(new OffsetRange(7, 7), `x${'h'.repeat(maxImperfectAgreementLength + 2)}x`),
777+
]);
778+
const applied = suggestion.apply(text);
779+
expect(applied).toStrictEqual(`abcdefgx${'h'.repeat(maxImperfectAgreementLength + 2)}x`);
780+
781+
const strict1 = tryRebaseStringEdits(text, suggestion, userEdit1, 'strict');
782+
expect(strict1?.apply(current1)).toStrictEqual(applied);
783+
expect(strict1?.removeCommonSuffixAndPrefix(current1).replacements.toString()).toMatchInlineSnapshot(`"[7, ${7 + maxImperfectAgreementLength}) -> "x${'h'.repeat(maxImperfectAgreementLength + 2)}x""`);
784+
const lenient1 = tryRebaseStringEdits(text, suggestion, userEdit1, 'lenient');
785+
expect(lenient1?.apply(current1)).toStrictEqual(applied);
786+
expect(lenient1?.removeCommonSuffixAndPrefix(current1).replacements.toString()).toMatchInlineSnapshot(`"[7, ${7 + maxImperfectAgreementLength}) -> "x${'h'.repeat(maxImperfectAgreementLength + 2)}x""`);
787+
788+
const userEdit2 = StringEdit.create([
789+
StringReplacement.replace(new OffsetRange(7, 7), 'h'.repeat(maxImperfectAgreementLength + 1)),
790+
]);
791+
const current2 = userEdit2.apply(text);
792+
expect(current2).toStrictEqual(`abcdefg${'h'.repeat(maxImperfectAgreementLength + 1)}`);
793+
794+
expect(tryRebaseStringEdits(text, suggestion, userEdit2, 'strict')).toBeUndefined();
795+
const lenient2 = tryRebaseStringEdits(text, suggestion, userEdit2, 'lenient');
796+
expect(lenient2?.apply(current2)).toStrictEqual(applied);
797+
expect(lenient2?.removeCommonSuffixAndPrefix(current2).replacements.toString()).toMatchInlineSnapshot(`"[7, ${7 + maxImperfectAgreementLength + 1}) -> "x${'h'.repeat(maxImperfectAgreementLength + 2)}x""`);
798+
});
733799
});

0 commit comments

Comments
 (0)