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

Add feature: copy, like move #193

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
7 changes: 6 additions & 1 deletion CHANGELOG.md
@@ -1,8 +1,13 @@
# magic-string changelog

## 0.26.0 (unreleased)

* Add a new method `MagicString.copy(start, end, index)`, which works like `.move()`, but
also keeps the original code in place. See Readme for caveats ([#193](https://github.com/Rich-Harris/magic-string/pull/193))

## 0.25.7

* fix bundle mappings after remove and move in multiple sources ([#172](https://github.com/Rich-Harris/magic-string/issues/172))
* Fix bundle mappings after remove and move in multiple sources ([#172](https://github.com/Rich-Harris/magic-string/issues/172))

## 0.25.6

Expand Down
10 changes: 10 additions & 0 deletions README.md
Expand Up @@ -93,6 +93,16 @@ Appends the specified `content` at the `index` in the original string. If a rang

Does what you'd expect.

### s.copy( start, end, index )

Copies the characters from `start` to `end` to `index`, keeping the original characters in place.

Note a caveat: if you make any changes to the area you'll copy later (appendLeft/Right, overwrite), the changes are carried over to the new region as well. However, any changes you make afterwards are only made to the original area.

Returns `this`.

_Implementation detail: the created duplicate segments aren't added to the `byStart`/`byEnd` indexes - the original chunks stay there, so there's pretty much no way to address the created (copied) characters for appends etc. They are only reachable by the `.next`/`.previous` chains, and eventually as first/last chunk. Please don't do insane things and everything will keep working as you can expect._

### s.generateDecodedMap( options )

Generates a sourcemap object with raw mappings in array form, rather than encoded as a string. See `generateMap` documentation below for options details. Useful if you need to manipulate the sourcemap further, but most of the time you will use `generateMap` instead.
Expand Down
1 change: 1 addition & 0 deletions index.d.ts
Expand Up @@ -81,6 +81,7 @@ export default class MagicString {
appendLeft(index: number, content: string): MagicString;
appendRight(index: number, content: string): MagicString;
clone(): MagicString;
copy(start: number, end: number, index: number): MagicString;
generateMap(options?: Partial<SourceMapOptions>): SourceMap;
generateDecodedMap(options?: Partial<SourceMapOptions>): DecodedSourceMap;
getIndentString(): string;
Expand Down
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -32,13 +32,13 @@
],
"scripts": {
"test": "mocha",
"pretest": "npm run lint && npm run build",
"pretest": "npm run lint && npm run build -- --environment DEBUG",
"format": "prettier --single-quote --print-width 100 --use-tabs --write src/*.js src/**/*.js",
"build": "rollup -c",
"prepare": "npm run build",
"prepublishOnly": "rm -rf dist && npm test",
"lint": "eslint src test",
"watch": "rollup -cw"
"watch": "rollup -cw --environment DEBUG"
},
"files": [
"dist/*",
Expand Down
2 changes: 1 addition & 1 deletion rollup.config.js
Expand Up @@ -5,7 +5,7 @@ import replace from 'rollup-plugin-replace';
const plugins = [
buble({ exclude: 'node_modules/**' }),
nodeResolve(),
replace({ DEBUG: false })
replace({ DEBUG: !!process.env.DEBUG })
];

export default [
Expand Down
55 changes: 53 additions & 2 deletions src/MagicString.js
Expand Up @@ -311,9 +311,9 @@ export default class MagicString {
if (newLeft) newLeft.next = first;
if (newRight) newRight.previous = last;

if (!first.previous) this.firstChunk = last.next;
if (!first.previous) this.firstChunk = oldRight;
if (!last.next) {
this.lastChunk = first.previous;
this.lastChunk = oldLeft;
this.lastChunk.next = null;
}

Expand All @@ -327,6 +327,57 @@ export default class MagicString {
return this;
}

copy(start, end, index) {
if (DEBUG) this.stats.time('copy');

this._split(start);
this._split(end);
this._split(index);

const first = this.byStart[start];
const last = this.byEnd[end];

const newRight = this.byStart[index];
if (!newRight && last === this.lastChunk) return this;
const newLeft = newRight ? newRight.previous : this.lastChunk;

const duplicates = [first.clone()];
if (first !== last) {
let lastOld = first;
let lastDuped = duplicates[duplicates.length - 1];
while (true) {
const nextOld = lastOld.next;
const nextDuped = nextOld.clone();

lastDuped.next = nextDuped;
nextDuped.previous = lastDuped;

duplicates.push(nextDuped);

if (nextOld === last) break;
lastOld = nextOld;
lastDuped = nextDuped;
}
}
if (DEBUG) {
duplicates.forEach(dupe => dupe.isCopy = true);
}
const newFirst = duplicates[0];
const newLast = duplicates[duplicates.length - 1];

if (newLeft) newLeft.next = newFirst;
newFirst.previous = newLeft;

if (newRight) newRight.previous = newLast;
newLast.next = newRight || null;

if (!newLeft) this.firstChunk = newFirst;
if (!newRight) this.lastChunk = newLast;

if (DEBUG) this.stats.timeEnd('copy');
return this;
}

overwrite(start, end, content, options) {
if (typeof content !== 'string') throw new TypeError('replacement content must be a string');

Expand Down
124 changes: 124 additions & 0 deletions test/MagicString.js
Expand Up @@ -721,6 +721,130 @@ describe('MagicString', () => {
});
});

describe('copy', () => {
it('copies characters', () => {
const s = new MagicString('abcDEFghijkl');

s.copy(3, 6, 9);
assert.equal(s.toString(), 'abcDEFghiDEFjkl');
});

it('copies to the beginning', () => {
const s = new MagicString('abcDEFghijkl');

s.copy(3, 6, 0);
assert.equal(s.toString(), 'DEFabcDEFghijkl');
});

it('copies to the end', () => {
const s = new MagicString('abcDEFghijkl');

s.copy(3, 6, 12);
assert.equal(s.toString(), 'abcDEFghijklDEF');
});

it('allows pasting selection into itself', () => {
const s = new MagicString('abcDEFghijkl');

s.copy(3, 6, 4);
assert.equal(s.toString(), 'abcDDEFEFghijkl');
});

it('puts multiple insertions in the same place in the order they were inserted', () => {
const s = new MagicString('ABcDEfghijkl');

s.copy(3, 5, 9);
s.copy(0, 2, 9);
assert.equal(s.toString(), 'ABcDEfghiDEABjkl');
});

it('carries over append made beforehand at beginning of selection', () => {
const s = new MagicString('abcDEFghijkl');

s.appendRight(3, 'x');
s.copy(3, 6, 9);
assert.equal(s.toString(), 'abcxDEFghixDEFjkl');
});

it('carries over append made beforehand at end of selection', () => {
const s = new MagicString('abcDEFghijkl');

s.appendLeft(6, 'x');
s.copy(3, 6, 9);
assert.equal(s.toString(), 'abcDEFxghiDEFxjkl');
});

it('carries over overwrite made beforehand in middle of selection', () => {
const s = new MagicString('abcDEFghijkl');

s.overwrite(4, 5, 'xy');
s.copy(3, 6, 9);
assert.equal(s.toString(), 'abcDxyFghiDxyFjkl');
});

it('does not carry over changes made after copy', () => {
const s = new MagicString('abcDEFghijkl');

s.copy(3, 6, 9);
s.overwrite(4, 5, 'xy');
s.appendRight(4, 'a');
s.appendLeft(5, 'b');
assert.equal(s.toString(), 'abcDaxybFghiDEFjkl');
});

it('does not carry over changes next to the selection', () => {
const s = new MagicString('abcDEFghijkl');

s.appendRight(6, 'x');
s.overwrite(2, 3, 'foo');
s.appendLeft(3, 'y');
s.copy(3, 6, 9);
assert.equal(s.toString(), 'abfooyDEFxghiDEFjkl');
});

it('cannot insert into an overwritten area', () => {
const s = new MagicString('abcDEFghijkl');

s.overwrite(8, 10, 'foo');
assert.throws(() => s.copy(3, 6, 9), /Cannot split a chunk that has already been edited/);
});

it('cannot copy part of overwritten area', () => {
const s = new MagicString('abcDEFghijkl');

s.overwrite(2, 5, 'foo');
assert.throws(() => s.copy(3, 6, 9), /Cannot split a chunk that has already been edited/);
});

it('cannot overwrite area where something was inserted', () => {
const s = new MagicString('abcDEFghijkl');

s.copy(3, 6, 9);
assert.throws(() => s.overwrite(8, 10, 'foo'), /Cannot overwrite across a split point/);
});

it('can overwrite area from where something was copied', () => {
const s = new MagicString('abcDEFghijkl');

s.copy(3, 6, 9);
s.overwrite(2, 5, 'foo');
assert.equal(s.toString(), 'abfooFghiDEFjkl');
});

it('can surround inserted area by copy region', () => {
const s = new MagicString('abcDEFghijkl');

s.copy(3, 6, 9);
s.copy(8, 10, 11);
assert.equal(s.toString(), 'abcDEFghiDEFjkiDEFjl');
});

it('returns this', () => {
const s = new MagicString('abcdefghijkl');
assert.strictEqual(s.copy(3, 6, 9), s);
});
});

describe('overwrite', () => {
it('should replace characters', () => {
const s = new MagicString('abcdefghijkl');
Expand Down
14 changes: 8 additions & 6 deletions test/utils/IntegrityCheckingMagicString.js
Expand Up @@ -7,15 +7,17 @@ class IntegrityCheckingMagicString extends MagicString {
let chunk = this.firstChunk;
let numNodes = 0;
while (chunk) {
assert.strictEqual(this.byStart[chunk.start], chunk);
assert.strictEqual(this.byEnd[chunk.end], chunk);
assert.strictEqual(chunk.previous, prevChunk);
if (prevChunk) {
assert.strictEqual(prevChunk.next, chunk);
if (!chunk.isCopy) {
assert.strictEqual(this.byStart[chunk.start], chunk);
assert.strictEqual(this.byEnd[chunk.end], chunk);
assert.strictEqual(chunk.previous, prevChunk);
if (prevChunk) {
assert.strictEqual(prevChunk.next, chunk);
}
numNodes++;
}
prevChunk = chunk;
chunk = chunk.next;
numNodes++;
}
assert.strictEqual(prevChunk, this.lastChunk);
assert.strictEqual(this.lastChunk.next, null);
Expand Down