Changeset: Migrate from smartOpAssembler() to canonicalizeOps()

This commit is contained in:
Richard Hansen 2021-11-23 21:31:38 -05:00
parent 23e7809b4a
commit d3d2090ca5
8 changed files with 162 additions and 164 deletions

View file

@ -39,6 +39,8 @@
* `opAssembler()`: Deprecated in favor of the new `serializeOps()` function. * `opAssembler()`: Deprecated in favor of the new `serializeOps()` function.
* `mergingOpAssembler()`: Deprecated in favor of the new `squashOps()` * `mergingOpAssembler()`: Deprecated in favor of the new `squashOps()`
generator function (combined with `serializeOps()`). generator function (combined with `serializeOps()`).
* `smartOpAssembler()`: Deprecated in favor of the new `canonicalizeOps()`
generator function (combined with `serializeOps()`).
* `appendATextToAssembler()`: Deprecated in favor of the new `opsFromAText()` * `appendATextToAssembler()`: Deprecated in favor of the new `opsFromAText()`
generator function. generator function.
* `newOp()`: Deprecated in favor of the new `Op` class. * `newOp()`: Deprecated in favor of the new `Op` class.

View file

@ -494,21 +494,20 @@ Pad.prototype.copyPadWithoutHistory = async function (destinationID, force) {
const oldAText = this.atext; const oldAText = this.atext;
// based on Changeset.makeSplice let newLength;
const assem = Changeset.smartOpAssembler(); const serializedOps = Changeset.serializeOps((function* () {
for (const op of Changeset.opsFromAText(oldAText)) assem.append(op); newLength = yield* Changeset.canonicalizeOps(Changeset.opsFromAText(oldAText), true);
assem.endDocument(); })());
// although we have instantiated the newPad with '\n', an additional '\n' is // although we have instantiated the newPad with '\n', an additional '\n' is
// added internally, so the pad text on the revision 0 is "\n\n" // added internally, so the pad text on the revision 0 is "\n\n"
const oldLength = 2; const oldLength = 2;
const newLength = assem.getLengthChange();
const newText = oldAText.text; const newText = oldAText.text;
// create a changeset that removes the previous text and add the newText with // create a changeset that removes the previous text and add the newText with
// all atributes present on the source pad // all atributes present on the source pad
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText); const changeset = Changeset.pack(oldLength, newLength, serializedOps, newText);
newPad.appendRevision(changeset); newPad.appendRevision(changeset);
await hooks.aCallAll('padCopy', {originalPad: this, destinationID}); await hooks.aCallAll('padCopy', {originalPad: this, destinationID});

View file

@ -432,7 +432,7 @@ class MergingOpAssembler {
* @returns {Generator<Op, number>} The done value indicates how much the sequence of operations * @returns {Generator<Op, number>} The done value indicates how much the sequence of operations
* changes the length of the document (in characters). * changes the length of the document (in characters).
*/ */
const canonicalizeOps = function* (ops, finalize) { exports.canonicalizeOps = function* (ops, finalize) {
let minusOps = []; let minusOps = [];
let plusOps = []; let plusOps = [];
let keepOps = []; let keepOps = [];
@ -519,6 +519,8 @@ const opsFromText = function* (opcode, text, attribs = '', pool = null) {
* - strips final "=" * - strips final "="
* - ignores 0-length changes * - ignores 0-length changes
* - reorders consecutive + and - (which MergingOpAssembler doesn't do) * - reorders consecutive + and - (which MergingOpAssembler doesn't do)
*
* @deprecated Use `canonicalizeOps` with `serializeOps` instead.
*/ */
class SmartOpAssembler { class SmartOpAssembler {
constructor() { constructor() {
@ -533,7 +535,7 @@ class SmartOpAssembler {
_serialize(finalize) { _serialize(finalize) {
this._serialized = exports.serializeOps((function* () { this._serialized = exports.serializeOps((function* () {
this._lengthChange = yield* canonicalizeOps(this._ops, finalize); this._lengthChange = yield* exports.canonicalizeOps(this._ops, finalize);
}).call(this)); }).call(this));
} }
@ -586,54 +588,58 @@ exports.checkRep = (cs) => {
const unpacked = exports.unpack(cs); const unpacked = exports.unpack(cs);
const oldLen = unpacked.oldLen; const oldLen = unpacked.oldLen;
const newLen = unpacked.newLen; const newLen = unpacked.newLen;
const ops = unpacked.ops;
let charBank = unpacked.charBank; let charBank = unpacked.charBank;
const assem = new SmartOpAssembler();
let oldPos = 0; let oldPos = 0;
let calcNewLen = 0; let calcNewLen = 0;
for (const o of exports.deserializeOps(ops)) { const ops = (function* () {
switch (o.opcode) { for (const o of exports.deserializeOps(unpacked.ops)) {
case '=': switch (o.opcode) {
oldPos += o.chars; case '=':
calcNewLen += o.chars; oldPos += o.chars;
break; calcNewLen += o.chars;
case '-': break;
oldPos += o.chars; case '-':
assert(oldPos <= oldLen, `${oldPos} > ${oldLen} in ${cs}`); oldPos += o.chars;
break; assert(oldPos <= oldLen, `${oldPos} > ${oldLen} in ${cs}`);
case '+': break;
{ case '+': {
assert(charBank.length >= o.chars, 'Invalid changeset: not enough chars in charBank'); assert(charBank.length >= o.chars, 'Invalid changeset: not enough chars in charBank');
const chars = charBank.slice(0, o.chars); const chars = charBank.slice(0, o.chars);
const nlines = (chars.match(/\n/g) || []).length; const nlines = (chars.match(/\n/g) || []).length;
assert(nlines === o.lines, assert(nlines === o.lines,
'Invalid changeset: number of newlines in insert op does not match the charBank'); 'Invalid changeset: number of newlines in insert op does not match the charBank');
assert(o.lines === 0 || chars.endsWith('\n'), assert(o.lines === 0 || chars.endsWith('\n'),
'Invalid changeset: multiline insert op does not end with a newline'); 'Invalid changeset: multiline insert op does not end with a newline');
charBank = charBank.slice(o.chars); charBank = charBank.slice(o.chars);
calcNewLen += o.chars; calcNewLen += o.chars;
assert(calcNewLen <= newLen, `${calcNewLen} > ${newLen} in ${cs}`); assert(calcNewLen <= newLen, `${calcNewLen} > ${newLen} in ${cs}`);
break; break;
}
default:
assert(false, `Invalid changeset: Unknown opcode: ${JSON.stringify(o.opcode)}`);
} }
default: yield o;
assert(false, `Invalid changeset: Unknown opcode: ${JSON.stringify(o.opcode)}`);
} }
assem.append(o); })();
} const serializedOps = exports.serializeOps(exports.canonicalizeOps(ops, true));
calcNewLen += oldLen - oldPos; calcNewLen += oldLen - oldPos;
assert(calcNewLen === newLen, 'Invalid changeset: claimed length does not match actual length'); assert(calcNewLen === newLen, 'Invalid changeset: claimed length does not match actual length');
assert(charBank === '', 'Invalid changeset: excess characters in the charBank'); assert(charBank === '', 'Invalid changeset: excess characters in the charBank');
assem.endDocument(); const normalized = exports.pack(oldLen, calcNewLen, serializedOps, unpacked.charBank);
const normalized = exports.pack(oldLen, calcNewLen, assem.toString(), unpacked.charBank);
assert(normalized === cs, 'Invalid changeset: not in canonical form'); assert(normalized === cs, 'Invalid changeset: not in canonical form');
return cs; return cs;
}; };
/** /**
* @deprecated Use `canonicalizeOps` with `serializeOps` instead.
* @returns {SmartOpAssembler} * @returns {SmartOpAssembler}
*/ */
exports.smartOpAssembler = () => new SmartOpAssembler(); exports.smartOpAssembler = () => {
padutils.warnDeprecated(
'Changeset.smartOpAssembler() is deprecated; use Changeset.canonicalizeOps() instead');
return new SmartOpAssembler();
};
/** /**
* @deprecated Use `squashOps` with `serializeOps` instead. * @deprecated Use `squashOps` with `serializeOps` instead.
@ -1082,22 +1088,22 @@ class TextLinesMutator {
* @returns {string} the integrated changeset * @returns {string} the integrated changeset
*/ */
const applyZip = (in1, in2, func) => { const applyZip = (in1, in2, func) => {
const ops1 = exports.deserializeOps(in1); const ops = (function* () {
const ops2 = exports.deserializeOps(in2); const ops1 = exports.deserializeOps(in1);
let next1 = ops1.next(); const ops2 = exports.deserializeOps(in2);
let next2 = ops2.next(); let next1 = ops1.next();
const assem = new SmartOpAssembler(); let next2 = ops2.next();
while (!next1.done || !next2.done) { while (!next1.done || !next2.done) {
if (!next1.done && !next1.value.opcode) next1 = ops1.next(); if (!next1.done && !next1.value.opcode) next1 = ops1.next();
if (!next2.done && !next2.value.opcode) next2 = ops2.next(); if (!next2.done && !next2.value.opcode) next2 = ops2.next();
if (next1.value == null) next1.value = new Op(); if (next1.value == null) next1.value = new Op();
if (next2.value == null) next2.value = new Op(); if (next2.value == null) next2.value = new Op();
if (!next1.value.opcode && !next2.value.opcode) break; if (!next1.value.opcode && !next2.value.opcode) break;
const opOut = func(next1.value, next2.value); const opOut = func(next1.value, next2.value);
if (opOut && opOut.opcode) assem.append(opOut); if (opOut && opOut.opcode) yield opOut;
} }
assem.endDocument(); })();
return assem.toString(); return exports.serializeOps(exports.canonicalizeOps(ops, true));
}; };
/** /**
@ -1540,15 +1546,13 @@ exports.makeSplice = (orig, start, ndel, ins, attribs, pool) => {
if (start > orig.length) start = orig.length; if (start > orig.length) start = orig.length;
if (ndel > orig.length - start) ndel = orig.length - start; if (ndel > orig.length - start) ndel = orig.length - start;
const deleted = orig.substring(start, start + ndel); const deleted = orig.substring(start, start + ndel);
const assem = new SmartOpAssembler();
const ops = (function* () { const ops = (function* () {
yield* opsFromText('=', orig.substring(0, start)); yield* opsFromText('=', orig.substring(0, start));
yield* opsFromText('-', deleted); yield* opsFromText('-', deleted);
yield* opsFromText('+', ins, attribs, pool); yield* opsFromText('+', ins, attribs, pool);
})(); })();
for (const op of ops) assem.append(op); const serializedOps = exports.serializeOps(exports.canonicalizeOps(ops, true));
assem.endDocument(); return exports.pack(orig.length, orig.length + ins.length - ndel, serializedOps, ins);
return exports.pack(orig.length, orig.length + ins.length - ndel, assem.toString(), ins);
}; };
/** /**
@ -1670,11 +1674,8 @@ exports.moveOpsToNewPool = (cs, oldPool, newPool) => {
* @param {string} text - text to insert * @param {string} text - text to insert
* @returns {string} * @returns {string}
*/ */
exports.makeAttribution = (text) => { exports.makeAttribution =
const assem = new SmartOpAssembler(); (text) => exports.serializeOps(exports.canonicalizeOps(opsFromText('+', text), false));
for (const op of opsFromText('+', text)) assem.append(op);
return assem.toString();
};
/** /**
* Iterates over attributes in exports, attribution string, or attribs property of an op and runs * Iterates over attributes in exports, attribution string, or attribs property of an op and runs
@ -1928,8 +1929,7 @@ exports.attribsAttributeValue = (attribs, key, pool) => {
* @returns {Builder} * @returns {Builder}
*/ */
exports.builder = (oldLen) => { exports.builder = (oldLen) => {
const assem = new SmartOpAssembler(); const ops = [];
const o = new Op();
const charBank = exports.stringAssembler(); const charBank = exports.stringAssembler();
const self = { const self = {
@ -1944,12 +1944,12 @@ exports.builder = (oldLen) => {
* @returns {Builder} this * @returns {Builder} this
*/ */
keep: (N, L, attribs, pool) => { keep: (N, L, attribs, pool) => {
o.opcode = '='; const o = new Op('=');
o.attribs = typeof attribs === 'string' o.attribs = typeof attribs === 'string'
? attribs : new AttributeMap(pool).update(attribs || []).toString(); ? attribs : new AttributeMap(pool).update(attribs || []).toString();
o.chars = N; o.chars = N;
o.lines = (L || 0); o.lines = (L || 0);
assem.append(o); ops.push(o);
return self; return self;
}, },
@ -1962,7 +1962,7 @@ exports.builder = (oldLen) => {
* @returns {Builder} this * @returns {Builder} this
*/ */
keepText: (text, attribs, pool) => { keepText: (text, attribs, pool) => {
for (const op of opsFromText('=', text, attribs, pool)) assem.append(op); ops.push(...opsFromText('=', text, attribs, pool));
return self; return self;
}, },
@ -1975,7 +1975,7 @@ exports.builder = (oldLen) => {
* @returns {Builder} this * @returns {Builder} this
*/ */
insert: (text, attribs, pool) => { insert: (text, attribs, pool) => {
for (const op of opsFromText('+', text, attribs, pool)) assem.append(op); ops.push(...opsFromText('+', text, attribs, pool));
charBank.append(text); charBank.append(text);
return self; return self;
}, },
@ -1987,18 +1987,22 @@ exports.builder = (oldLen) => {
* @returns {Builder} this * @returns {Builder} this
*/ */
remove: (N, L) => { remove: (N, L) => {
o.opcode = '-'; const o = new Op('-');
o.attribs = ''; o.attribs = '';
o.chars = N; o.chars = N;
o.lines = (L || 0); o.lines = (L || 0);
assem.append(o); ops.push(o);
return self; return self;
}, },
toString: () => { toString: () => {
assem.endDocument(); /** @type {number} */
const newLen = oldLen + assem.getLengthChange(); let lengthChange;
return exports.pack(oldLen, newLen, assem.toString(), charBank.toString()); const serializedOps = exports.serializeOps((function* () {
lengthChange = yield* exports.canonicalizeOps(ops, true);
})());
const newLen = oldLen + lengthChange;
return exports.pack(oldLen, newLen, serializedOps, charBank.toString());
}, },
}; };
@ -2033,11 +2037,11 @@ exports.makeAttribsString = (opcode, attribs, pool) => {
exports.subattribution = (astr, start, optEnd) => { exports.subattribution = (astr, start, optEnd) => {
const attOps = exports.deserializeOps(astr); const attOps = exports.deserializeOps(astr);
let attOpsNext = attOps.next(); let attOpsNext = attOps.next();
const assem = new SmartOpAssembler();
let attOp = new Op(); let attOp = new Op();
const csOp = new Op(); const csOp = new Op('-');
csOp.chars = start;
const doCsOp = () => { const doCsOp = function* () {
if (!csOp.chars) return; if (!csOp.chars) return;
while (csOp.opcode && (attOp.opcode || !attOpsNext.done)) { while (csOp.opcode && (attOp.opcode || !attOpsNext.done)) {
if (!attOp.opcode) { if (!attOp.opcode) {
@ -2049,30 +2053,25 @@ exports.subattribution = (astr, start, optEnd) => {
csOp.lines++; csOp.lines++;
} }
const opOut = slicerZipperFunc(attOp, csOp, null); const opOut = slicerZipperFunc(attOp, csOp, null);
if (opOut.opcode) assem.append(opOut); if (opOut.opcode) yield opOut;
} }
}; };
csOp.opcode = '-'; const ops = (function* () {
csOp.chars = start; yield* doCsOp();
if (optEnd === undefined) {
doCsOp(); if (attOp.opcode) yield attOp;
while (!attOpsNext.done) {
if (optEnd === undefined) { yield attOpsNext.value;
if (attOp.opcode) { attOpsNext = attOps.next();
assem.append(attOp); }
} else {
csOp.opcode = '=';
csOp.chars = optEnd - start;
yield* doCsOp();
} }
while (!attOpsNext.done) { })();
assem.append(attOpsNext.value); return exports.serializeOps(exports.canonicalizeOps(ops, false));
attOpsNext = attOps.next();
}
} else {
csOp.opcode = '=';
csOp.chars = optEnd - start;
doCsOp();
}
return assem.toString();
}; };
exports.inverse = (cs, lines, alines, pool) => { exports.inverse = (cs, lines, alines, pool) => {

View file

@ -522,18 +522,24 @@ function Ace2Inner(editorInfo, cssManagers) {
const numLines = rep.lines.length(); const numLines = rep.lines.length();
const upToLastLine = rep.lines.offsetOfIndex(numLines - 1); const upToLastLine = rep.lines.offsetOfIndex(numLines - 1);
const lastLineLength = rep.lines.atIndex(numLines - 1).text.length; const lastLineLength = rep.lines.atIndex(numLines - 1).text.length;
const assem = Changeset.smartOpAssembler(); const ops = (function* () {
const o = new Changeset.Op('-'); const op1 = new Changeset.Op('-');
o.chars = upToLastLine; op1.chars = upToLastLine;
o.lines = numLines - 1; op1.lines = numLines - 1;
assem.append(o); yield op1;
o.chars = lastLineLength; const op2 = new Changeset.Op('-');
o.lines = 0; op2.chars = lastLineLength;
assem.append(o); op2.lines = 0;
for (const op of Changeset.opsFromAText(atext)) assem.append(op); yield op2;
const newLen = oldLen + assem.getLengthChange(); yield* Changeset.opsFromAText(atext);
const changeset = Changeset.checkRep( })();
Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1))); let lengthChange;
const serializedOps = Changeset.serializeOps((function* () {
lengthChange = yield* Changeset.canonicalizeOps(ops, false);
})());
const newLen = oldLen + lengthChange;
const changeset =
Changeset.checkRep(Changeset.pack(oldLen, newLen, serializedOps, atext.text.slice(0, -1)));
performDocumentApplyChangeset(changeset); performDocumentApplyChangeset(changeset);
performSelectionChange( performSelectionChange(

View file

@ -82,31 +82,30 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
const lines = (() => { const lines = (() => {
const textArray = []; const textArray = [];
const attribsArray = []; const attribsArray = [];
let attribsBuilder = null; let ops = null;
const op = new Changeset.Op('+');
const self = { const self = {
length: () => textArray.length, length: () => textArray.length,
atColumnZero: () => textArray[textArray.length - 1] === '', atColumnZero: () => textArray[textArray.length - 1] === '',
startNew: () => { startNew: () => {
textArray.push(''); textArray.push('');
self.flush(true); self.flush(true);
attribsBuilder = Changeset.smartOpAssembler(); ops = [];
}, },
textOfLine: (i) => textArray[i], textOfLine: (i) => textArray[i],
appendText: (txt, attrString = '') => { appendText: (txt, attrString = '') => {
textArray[textArray.length - 1] += txt; textArray[textArray.length - 1] += txt;
const op = new Changeset.Op('+');
op.attribs = attrString; op.attribs = attrString;
op.chars = txt.length; op.chars = txt.length;
attribsBuilder.append(op); ops.push(op);
}, },
textLines: () => textArray.slice(), textLines: () => textArray.slice(),
attribLines: () => attribsArray, attribLines: () => attribsArray,
// call flush only when you're done // call flush only when you're done
flush: (withNewline) => { flush: (withNewline) => {
if (attribsBuilder) { if (ops == null) return;
attribsArray.push(attribsBuilder.toString()); attribsArray.push(Changeset.serializeOps(Changeset.canonicalizeOps(ops, false)));
attribsBuilder = null; ops = null;
}
}, },
}; };
self.startNew(); self.startNew();

View file

@ -168,26 +168,22 @@ const randomTestChangeset = (origText, withAttribs) => {
const charBank = Changeset.stringAssembler(); const charBank = Changeset.stringAssembler();
let textLeft = origText; // always keep final newline let textLeft = origText; // always keep final newline
const outTextAssem = Changeset.stringAssembler(); const outTextAssem = Changeset.stringAssembler();
const opAssem = Changeset.smartOpAssembler(); const ops = [];
const oldLen = origText.length; const oldLen = origText.length;
const nextOp = new Changeset.Op();
const appendMultilineOp = (opcode, txt) => { const appendMultilineOp = (opcode, txt) => {
nextOp.opcode = opcode; const attribs = withAttribs ? randomTwoPropAttribs(opcode) : '';
if (withAttribs) {
nextOp.attribs = randomTwoPropAttribs(opcode);
}
txt.replace(/\n|[^\n]+/g, (t) => { txt.replace(/\n|[^\n]+/g, (t) => {
const nextOp = new Changeset.Op(opcode);
nextOp.attribs = attribs;
if (t === '\n') { if (t === '\n') {
nextOp.chars = 1; nextOp.chars = 1;
nextOp.lines = 1; nextOp.lines = 1;
opAssem.append(nextOp);
} else { } else {
nextOp.chars = t.length; nextOp.chars = t.length;
nextOp.lines = 0; nextOp.lines = 0;
opAssem.append(nextOp);
} }
ops.push(nextOp);
return ''; return '';
}); });
}; };
@ -214,8 +210,8 @@ const randomTestChangeset = (origText, withAttribs) => {
while (textLeft.length > 1) doOp(); while (textLeft.length > 1) doOp();
for (let i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen) for (let i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen)
const outText = `${outTextAssem.toString()}\n`; const outText = `${outTextAssem.toString()}\n`;
opAssem.endDocument(); const serializedOps = Changeset.serializeOps(Changeset.canonicalizeOps(ops, true));
const cs = Changeset.pack(oldLen, outText.length, opAssem.toString(), charBank.toString()); const cs = Changeset.pack(oldLen, outText.length, serializedOps, charBank.toString());
Changeset.checkRep(cs); Changeset.checkRep(cs);
return [cs, outText]; return [cs, outText];
}; };

View file

@ -8,20 +8,18 @@ describe('easysync-assembler', function () {
expect(Changeset.serializeOps(Changeset.deserializeOps(x))).to.equal(x); expect(Changeset.serializeOps(Changeset.deserializeOps(x))).to.equal(x);
}); });
it('smartOpAssembler', async function () { it('canonicalizeOps', async function () {
const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1';
const assem = Changeset.smartOpAssembler(); expect(Changeset.serializeOps(Changeset.canonicalizeOps(Changeset.deserializeOps(x), true)))
for (const op of Changeset.deserializeOps(x)) assem.append(op); .to.equal(x);
assem.endDocument();
expect(assem.toString()).to.equal(x);
}); });
describe('append atext to assembler', function () { describe('append atext to assembler', function () {
const testAppendATextToAssembler = (testId, atext, correctOps) => { const testAppendATextToAssembler = (testId, atext, correctOps) => {
it(`testAppendATextToAssembler#${testId}`, async function () { it(`testAppendATextToAssembler#${testId}`, async function () {
const assem = Changeset.smartOpAssembler(); const serializedOps =
for (const op of Changeset.opsFromAText(atext)) assem.append(op); Changeset.serializeOps(Changeset.canonicalizeOps(Changeset.opsFromAText(atext), false));
expect(assem.toString()).to.equal(correctOps); expect(serializedOps).to.equal(correctOps);
}); });
}; };

View file

@ -15,37 +15,36 @@ describe('easysync-mutations', function () {
}; };
const mutationsToChangeset = (oldLen, arrayOfArrays) => { const mutationsToChangeset = (oldLen, arrayOfArrays) => {
const assem = Changeset.smartOpAssembler();
const op = new Changeset.Op();
const bank = Changeset.stringAssembler(); const bank = Changeset.stringAssembler();
let oldPos = 0; let oldPos = 0;
let newLen = 0; let newLen = 0;
arrayOfArrays.forEach((a) => { const ops = (function* () {
if (a[0] === 'skip') { for (const a of arrayOfArrays) {
op.opcode = '='; const op = new Changeset.Op();
op.chars = a[1]; if (a[0] === 'skip') {
op.lines = (a[2] || 0); op.opcode = '=';
assem.append(op); op.chars = a[1];
oldPos += op.chars; op.lines = (a[2] || 0);
newLen += op.chars; oldPos += op.chars;
} else if (a[0] === 'remove') { newLen += op.chars;
op.opcode = '-'; } else if (a[0] === 'remove') {
op.chars = a[1]; op.opcode = '-';
op.lines = (a[2] || 0); op.chars = a[1];
assem.append(op); op.lines = (a[2] || 0);
oldPos += op.chars; oldPos += op.chars;
} else if (a[0] === 'insert') { } else if (a[0] === 'insert') {
op.opcode = '+'; op.opcode = '+';
bank.append(a[1]); bank.append(a[1]);
op.chars = a[1].length; op.chars = a[1].length;
op.lines = (a[2] || 0); op.lines = (a[2] || 0);
assem.append(op); newLen += op.chars;
newLen += op.chars; }
yield op;
} }
}); })();
const serializedOps = Changeset.serializeOps(Changeset.canonicalizeOps(ops, true));
newLen += oldLen - oldPos; newLen += oldLen - oldPos;
assem.endDocument(); return Changeset.pack(oldLen, newLen, serializedOps, bank.toString());
return Changeset.pack(oldLen, newLen, assem.toString(), bank.toString());
}; };
const runMutationTest = (testId, origLines, muts, correct) => { const runMutationTest = (testId, origLines, muts, correct) => {