diff --git a/src/Chunk.js b/src/Chunk.js index 3b95a7a..88b5cf2 100644 --- a/src/Chunk.js +++ b/src/Chunk.js @@ -84,6 +84,16 @@ export default class Chunk { this.intro = content + this.intro; } + reset() { + this.intro = ''; + this.outro = ''; + if (this.edited) { + this.content = this.original; + this.storeName = false; + this.edited = false; + } + } + split(index) { const sliceIndex = index - this.start; diff --git a/src/MagicString.js b/src/MagicString.js index 542513b..7e24e35 100644 --- a/src/MagicString.js +++ b/src/MagicString.js @@ -496,6 +496,32 @@ export default class MagicString { return this; } + reset(start, end) { + while (start < 0) start += this.original.length; + while (end < 0) end += this.original.length; + + if (start === end) return this; + + if (start < 0 || end > this.original.length) throw new Error('Character is out of bounds'); + if (start > end) throw new Error('end must be greater than start'); + + if (DEBUG) this.stats.time('reset'); + + this._split(start); + this._split(end); + + let chunk = this.byStart[start]; + + while (chunk) { + chunk.reset(); + + chunk = end > chunk.end ? this.byStart[chunk.end] : null; + } + + if (DEBUG) this.stats.timeEnd('reset'); + return this; + } + lastChar() { if (this.outro.length) return this.outro[this.outro.length - 1]; let chunk = this.lastChunk; diff --git a/src/index.d.ts b/src/index.d.ts index 42c0ec2..06b1a17 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -209,6 +209,10 @@ export default class MagicString { * Removing the same content twice, or making removals that partially overlap, will cause an error. */ remove(start: number, end: number): MagicString; + /** + * Reset the modified characters from `start` to `end` (of the original string, **not** the generated string). + */ + reset(start: number, end: number): MagicString; /** * Returns the content of the generated string that corresponds to the slice between `start` and `end` of the original string. * Throws error if the indices are for characters that were already removed. diff --git a/test/MagicString.js b/test/MagicString.js index 49b8880..4298ea4 100644 --- a/test/MagicString.js +++ b/test/MagicString.js @@ -1199,6 +1199,131 @@ describe('MagicString', () => { }); }); + describe('reset', () => { + it('should reset moved characters from the original string', () => { + const s = new MagicString('abcdefghijkl'); + + s.remove(1, 5); + s.reset(2, 4); + assert.equal(s.toString(), 'acdfghijkl'); + + s.reset(4, 5); + assert.equal(s.toString(), 'acdefghijkl'); + }); + + it('should reset from the start', () => { + const s = new MagicString('abcdefghijkl'); + + s.remove(0, 6); + s.reset(0, 3); + assert.equal(s.toString(), 'abcghijkl'); + }); + + it('should reset from the end', () => { + const s = new MagicString('abcdefghijkl'); + + s.remove(6, 12); + s.reset(10, 12); + assert.equal(s.toString(), 'abcdefkl'); + }); + + it('should treat zero-length resets as a no-op', () => { + const s = new MagicString('abcdefghijkl'); + + s.remove(3, 5); + s.reset(0, 0).reset(6, 6).reset(9, -3); + assert.equal(s.toString(), 'abcfghijkl'); + }); + + it('should treat not modified resets as a no-op', () => { + const s = new MagicString('abcdefghijkl'); + + s.reset(3, 5); + assert.equal(s.toString(), 'abcdefghijkl'); + }); + + it('should reset overlapping ranges', () => { + const s1 = new MagicString('abcdefghijkl'); + + s1.remove(0, 10); + s1.reset(1, 7).reset(5, 9); + assert.equal(s1.toString(), 'bcdefghikl'); + + const s2 = new MagicString('abcdefghijkl'); + + s2.remove(0, 10); + s2.reset(3, 7).reset(4, 6); + assert.equal(s2.toString(), 'defgkl'); + }); + + it('should reset overlapping ranges, redux', () => { + const s = new MagicString('abccde'); + + s.remove(0, 6); + s.reset(2, 3); // c + s.reset(1, 3); // bc + assert.equal(s.toString(), 'bc'); + }); + + it('should reset modified ranges', () => { + const s = new MagicString('abcdefghi'); + + s.overwrite(3, 6, 'DEF'); + s.remove(1, 8); // bcDEFgh + s.reset(2, 7); // cDEFg + assert.equal(s.slice(1, 8), 'cdefg'); + assert.equal(s.toString(), 'acdefgi'); + }); + + it('should reset modified ranges, redux', () => { + const s = new MagicString('abcdefghi'); + + s.remove(1, 8); + s.appendLeft(2, 'W'); + s.appendRight(2, 'X'); + s.prependLeft(3, 'Y'); + s.prependRight(5, 'Z'); + s.reset(2, 7); + assert.equal(s.toString(), 'aWcdefgi'); + }); + + it('should not reset content inserted after the end of range', () => { + const s = new MagicString('ab.c;'); + + s.prependRight(0, '('); + s.prependRight(4, ')'); + s.remove(1, 4); + s.reset(2, 4); + assert.equal(s.toString(), '(a.c);'); + }); + + it('should provide a useful error when illegal removals are attempted', () => { + const s = new MagicString('abcdefghijkl'); + + s.remove(4, 8); + + s.overwrite(5, 7, 'XX'); + + assert.throws(() => s.reset(4, 6), /Cannot split a chunk that has already been edited/); + }); + + it('should return this', () => { + const s = new MagicString('abcdefghijkl'); + s.remove(2, 5); + assert.strictEqual(s.reset(3, 4), s); + }); + + it('removes across moved content', () => { + const s = new MagicString('abcdefghijkl'); + + s.remove(5, 8); + s.move(6, 9, 3); + s.reset(7, 8); + + assert.equal(s.toString(), 'abchidejkl'); + }); + }); + describe('slice', () => { it('should return the generated content between the specified original characters', () => { const s = new MagicString('abcdefghijkl');