diff --git a/src/helpers.ts b/src/helpers.ts index 7e50290..492eb12 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -228,11 +228,15 @@ export const concatChanges = (baseChanges: Change[], changesToAdd: Change[]) => // Search for overlap between the two changes for simplification if (isAdd(prevChange) && isRemove(change)) { + const minLength = Math.min(getChangeLength(prevChange), getChangeLength(change)) const beginningOverlap = getBeginningOverlap(getChangeText(prevChange), getChangeText(change)) - const endingOverlap = getEndingOverlap(getChangeText(prevChange), getChangeText(change)) + // Clamp so a repeated boundary char (' ' vs ' ') isn't counted as both beginning and ending overlap + const endingOverlap = Math.min( + getEndingOverlap(getChangeText(prevChange), getChangeText(change)), + minLength - beginningOverlap + ) if (beginningOverlap > 0 || endingOverlap > 0) { - const minLength = Math.min(getChangeLength(prevChange), getChangeLength(change)) const getNonOverlap = (text: string) => text.slice(beginningOverlap, endingOverlap > 0 ? -endingOverlap : Infinity) const newChanges = [ @@ -240,9 +244,7 @@ export const concatChanges = (baseChanges: Change[], changesToAdd: Change[]) => replaceChangeText(prevChange, getNonOverlap(getChangeText(prevChange))), replaceChangeText(change, getNonOverlap(getChangeText(change))), ] - if (endingOverlap > 0 && minLength !== beginningOverlap) { - newChanges.push(getChangeText(change).slice(-Math.min(endingOverlap, minLength - beginningOverlap))) - } + if (endingOverlap > 0) newChanges.push(getChangeText(change).slice(-endingOverlap)) changes.splice(index - 1, 2, ...newChanges.filter(getChangeLength)) index-- diff --git a/tests/index.test.ts b/tests/index.test.ts index e6af460..d4a9f95 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,4 +1,5 @@ -import { createStringTracker, StringOp, StringTracker } from '../src' +import { Change, createStringTracker, StringOp, StringTracker } from '../src' +import { assertValidTracker } from './helpers' const str1 = 'this is my word' @@ -180,6 +181,24 @@ it('should remove subsequent adds/strings with a remove in between in changes ar expect(tracker.getChanges()).toStrictEqual(['asd', [1, 'a '], 'pog']) }) +it.each<[string, () => StringTracker, Change[]]>([ + [ + 'add shorter than remove', + () => createStringTracker(' 0').remove(2, 3).add(2, 'x ').remove(0, 4), + [' ', [StringOp.Remove, ' 0']], + ], + ['add longer than remove', () => createStringTracker('aa').add(0, 'aaa').remove(3, 5), ['aa', [StringOp.Add, 'a']]], + [ + 'overlaps cross mid-text', + () => createStringTracker('aab').add(0, 'ab').remove(2, 5), + ['a', [StringOp.Remove, 'a'], 'b'], + ], +])('should not drop repeated chars when folding an overlapping add and remove (%s)', (_, build, changes) => { + const tracker = build() + expect(tracker.getChanges()).toStrictEqual(changes) + assertValidTracker(tracker) +}) + // getIndexOnOriginal it('getIndexOnOriginal should throw a RangeError when called with a negative number', () => { expect(() => createStringTracker('asd').getIndexOnOriginal(-1))