Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new reset method (#218) #271

Merged
merged 4 commits into from Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/Chunk.js
Expand Up @@ -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;

Expand Down
26 changes: 26 additions & 0 deletions src/MagicString.js
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/index.d.ts
Expand Up @@ -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.
Expand Down
125 changes: 125 additions & 0 deletions test/MagicString.js
Expand Up @@ -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');
Expand Down