From cef7c98441864d02e2e2504aafca35e545a284d1 Mon Sep 17 00:00:00 2001 From: Wahid Rizka Fathurrohman Date: Fri, 11 Sep 2026 13:59:47 +0700 Subject: [PATCH] fix: treat moving a range to where it already sits as a no-op move() already returns early when the range is the last chunk and the target is the end of the string, but it had no such check when an earlier move had already put the range right before the target index. The splice then made the range its own neighbour: the range vanished from toString() unless it was at the very start, and the chunk list got a backwards loop that could make lastChar(), lastLine() and trimEnd() run forever. It now returns early in that case too. --- src/MagicString.ts | 5 +++++ test/MagicString.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/MagicString.ts b/src/MagicString.ts index be55b24..a6afc15 100644 --- a/src/MagicString.ts +++ b/src/MagicString.ts @@ -647,6 +647,11 @@ export default class MagicString { const newRight = this.byStart.get(index) if (!newRight && last === this.lastChunk) return this + // Nothing to do if an earlier move already put the range right before + // `index`. Splicing it in next to itself would link the chunk list back on + // itself, and drop the range from the output unless it comes first. + if (newRight && newRight.previous === last) + return this const newLeft = newRight ? newRight.previous : this.lastChunk if (oldLeft) diff --git a/test/MagicString.test.ts b/test/MagicString.test.ts index 4395f6b..f72e986 100644 --- a/test/MagicString.test.ts +++ b/test/MagicString.test.ts @@ -1452,6 +1452,31 @@ describe('magicString', () => { assert.equal(s.toString(), 'abcdefghijkl') }) + it('does nothing when moving a range to where it already is', () => { + // The first move puts "d" right before "b", so the second has nothing to + // do. It used to splice "d" in next to itself, which dropped it from the + // output and left the chunk list pointing back at itself. + const s = new MagicString('abcd') + s.move(3, 4, 1) + assert.equal(s.toString(), 'adbc') + + s.move(3, 4, 1) + s.checkIntegrity() + assert.equal(s.toString(), 'adbc') + }) + + it('does nothing when moving a range to the front where it already is', () => { + // The same no-op at the very start. The text survived here, but the first + // chunk became its own previous chunk, so lastLine() looped forever. + const s = new MagicString('xb') + s.move(1, 2, 0) + s.move(1, 2, 0) + s.checkIntegrity() + + assert.equal(s.toString(), 'bx') + assert.equal(s.lastLine(), 'bx') + }) + it('allows edits of moved content', () => { const s1 = new MagicString('abcdefghijkl')