Skip to content

Commit e299edd

Browse files
committed
Changeset: Migrate from smartOpAssembler() to canonicalizeOps()
1 parent 74bed6a commit e299edd

File tree

8 files changed

+162
-164
lines changed

8 files changed

+162
-164
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
* `opAssembler()`: Deprecated in favor of the new `serializeOps()` function.
2929
* `mergingOpAssembler()`: Deprecated in favor of the new `squashOps()`
3030
generator function (combined with `serializeOps()`).
31+
* `smartOpAssembler()`: Deprecated in favor of the new `canonicalizeOps()`
32+
generator function (combined with `serializeOps()`).
3133
* `appendATextToAssembler()`: Deprecated in favor of the new `opsFromAText()`
3234
generator function.
3335
* `newOp()`: Deprecated in favor of the new `Op` class.

src/node/db/Pad.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -501,21 +501,20 @@ Pad.prototype.copyPadWithoutHistory = async function (destinationID, force) {
501501

502502
const oldAText = this.atext;
503503

504-
// based on Changeset.makeSplice
505-
const assem = Changeset.smartOpAssembler();
506-
for (const op of Changeset.opsFromAText(oldAText)) assem.append(op);
507-
assem.endDocument();
504+
let newLength;
505+
const serializedOps = Changeset.serializeOps((function* () {
506+
newLength = yield* Changeset.canonicalizeOps(Changeset.opsFromAText(oldAText), true);
507+
})());
508508

509509
// although we have instantiated the newPad with '\n', an additional '\n' is
510510
// added internally, so the pad text on the revision 0 is "\n\n"
511511
const oldLength = 2;
512512

513-
const newLength = assem.getLengthChange();
514513
const newText = oldAText.text;
515514

516515
// create a changeset that removes the previous text and add the newText with
517516
// all atributes present on the source pad
518-
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
517+
const changeset = Changeset.pack(oldLength, newLength, serializedOps, newText);
519518
newPad.appendRevision(changeset);
520519

521520
await hooks.aCallAll('padCopy', {originalPad: this, destinationID});

src/static/js/Changeset.js

Lines changed: 92 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ class MergingOpAssembler {
432432
* @returns {Generator<Op, number>} The done value indicates how much the sequence of operations
433433
* changes the length of the document (in characters).
434434
*/
435-
const canonicalizeOps = function* (ops, finalize) {
435+
exports.canonicalizeOps = function* (ops, finalize) {
436436
let minusOps = [];
437437
let plusOps = [];
438438
let keepOps = [];
@@ -519,6 +519,8 @@ const opsFromText = function* (opcode, text, attribs = '', pool = null) {
519519
* - strips final "="
520520
* - ignores 0-length changes
521521
* - reorders consecutive + and - (which MergingOpAssembler doesn't do)
522+
*
523+
* @deprecated Use `canonicalizeOps` with `serializeOps` instead.
522524
*/
523525
class SmartOpAssembler {
524526
constructor() {
@@ -533,7 +535,7 @@ class SmartOpAssembler {
533535

534536
_serialize(finalize) {
535537
this._serialized = exports.serializeOps((function* () {
536-
this._lengthChange = yield* canonicalizeOps(this._ops, finalize);
538+
this._lengthChange = yield* exports.canonicalizeOps(this._ops, finalize);
537539
}).call(this));
538540
}
539541

@@ -586,54 +588,58 @@ exports.checkRep = (cs) => {
586588
const unpacked = exports.unpack(cs);
587589
const oldLen = unpacked.oldLen;
588590
const newLen = unpacked.newLen;
589-
const ops = unpacked.ops;
590591
let charBank = unpacked.charBank;
591592

592-
const assem = new SmartOpAssembler();
593593
let oldPos = 0;
594594
let calcNewLen = 0;
595-
for (const o of exports.deserializeOps(ops)) {
596-
switch (o.opcode) {
597-
case '=':
598-
oldPos += o.chars;
599-
calcNewLen += o.chars;
600-
break;
601-
case '-':
602-
oldPos += o.chars;
603-
assert(oldPos <= oldLen, `${oldPos} > ${oldLen} in ${cs}`);
604-
break;
605-
case '+':
606-
{
607-
assert(charBank.length >= o.chars, 'Invalid changeset: not enough chars in charBank');
608-
const chars = charBank.slice(0, o.chars);
609-
const nlines = (chars.match(/\n/g) || []).length;
610-
assert(nlines === o.lines,
611-
'Invalid changeset: number of newlines in insert op does not match the charBank');
612-
assert(o.lines === 0 || chars.endsWith('\n'),
613-
'Invalid changeset: multiline insert op does not end with a newline');
614-
charBank = charBank.slice(o.chars);
615-
calcNewLen += o.chars;
616-
assert(calcNewLen <= newLen, `${calcNewLen} > ${newLen} in ${cs}`);
617-
break;
595+
const ops = (function* () {
596+
for (const o of exports.deserializeOps(unpacked.ops)) {
597+
switch (o.opcode) {
598+
case '=':
599+
oldPos += o.chars;
600+
calcNewLen += o.chars;
601+
break;
602+
case '-':
603+
oldPos += o.chars;
604+
assert(oldPos <= oldLen, `${oldPos} > ${oldLen} in ${cs}`);
605+
break;
606+
case '+': {
607+
assert(charBank.length >= o.chars, 'Invalid changeset: not enough chars in charBank');
608+
const chars = charBank.slice(0, o.chars);
609+
const nlines = (chars.match(/\n/g) || []).length;
610+
assert(nlines === o.lines,
611+
'Invalid changeset: number of newlines in insert op does not match the charBank');
612+
assert(o.lines === 0 || chars.endsWith('\n'),
613+
'Invalid changeset: multiline insert op does not end with a newline');
614+
charBank = charBank.slice(o.chars);
615+
calcNewLen += o.chars;
616+
assert(calcNewLen <= newLen, `${calcNewLen} > ${newLen} in ${cs}`);
617+
break;
618+
}
619+
default:
620+
assert(false, `Invalid changeset: Unknown opcode: ${JSON.stringify(o.opcode)}`);
618621
}
619-
default:
620-
assert(false, `Invalid changeset: Unknown opcode: ${JSON.stringify(o.opcode)}`);
622+
yield o;
621623
}
622-
assem.append(o);
623-
}
624+
})();
625+
const serializedOps = exports.serializeOps(exports.canonicalizeOps(ops, true));
624626
calcNewLen += oldLen - oldPos;
625627
assert(calcNewLen === newLen, 'Invalid changeset: claimed length does not match actual length');
626628
assert(charBank === '', 'Invalid changeset: excess characters in the charBank');
627-
assem.endDocument();
628-
const normalized = exports.pack(oldLen, calcNewLen, assem.toString(), unpacked.charBank);
629+
const normalized = exports.pack(oldLen, calcNewLen, serializedOps, unpacked.charBank);
629630
assert(normalized === cs, 'Invalid changeset: not in canonical form');
630631
return cs;
631632
};
632633

633634
/**
635+
* @deprecated Use `canonicalizeOps` with `serializeOps` instead.
634636
* @returns {SmartOpAssembler}
635637
*/
636-
exports.smartOpAssembler = () => new SmartOpAssembler();
638+
exports.smartOpAssembler = () => {
639+
padutils.warnWithStack(
640+
'Changeset.smartOpAssembler() is deprecated; use Changeset.canonicalizeOps() instead');
641+
return new SmartOpAssembler();
642+
};
637643

638644
/**
639645
* @deprecated Use `squashOps` with `serializeOps` instead.
@@ -1082,22 +1088,22 @@ class TextLinesMutator {
10821088
* @returns {string} the integrated changeset
10831089
*/
10841090
const applyZip = (in1, in2, func) => {
1085-
const ops1 = exports.deserializeOps(in1);
1086-
const ops2 = exports.deserializeOps(in2);
1087-
let next1 = ops1.next();
1088-
let next2 = ops2.next();
1089-
const assem = new SmartOpAssembler();
1090-
while (!next1.done || !next2.done) {
1091-
if (!next1.done && !next1.value.opcode) next1 = ops1.next();
1092-
if (!next2.done && !next2.value.opcode) next2 = ops2.next();
1093-
if (next1.value == null) next1.value = new Op();
1094-
if (next2.value == null) next2.value = new Op();
1095-
if (!next1.value.opcode && !next2.value.opcode) break;
1096-
const opOut = func(next1.value, next2.value);
1097-
if (opOut && opOut.opcode) assem.append(opOut);
1098-
}
1099-
assem.endDocument();
1100-
return assem.toString();
1091+
const ops = (function* () {
1092+
const ops1 = exports.deserializeOps(in1);
1093+
const ops2 = exports.deserializeOps(in2);
1094+
let next1 = ops1.next();
1095+
let next2 = ops2.next();
1096+
while (!next1.done || !next2.done) {
1097+
if (!next1.done && !next1.value.opcode) next1 = ops1.next();
1098+
if (!next2.done && !next2.value.opcode) next2 = ops2.next();
1099+
if (next1.value == null) next1.value = new Op();
1100+
if (next2.value == null) next2.value = new Op();
1101+
if (!next1.value.opcode && !next2.value.opcode) break;
1102+
const opOut = func(next1.value, next2.value);
1103+
if (opOut && opOut.opcode) yield opOut;
1104+
}
1105+
})();
1106+
return exports.serializeOps(exports.canonicalizeOps(ops, true));
11011107
};
11021108

11031109
/**
@@ -1546,15 +1552,13 @@ exports.makeSplice = (oldFullText, spliceStart, numRemoved, newText, optNewTextA
15461552
const oldText = oldFullText.substring(spliceStart, spliceStart + numRemoved);
15471553
const newLen = oldLen + newText.length - oldText.length;
15481554

1549-
const assem = new SmartOpAssembler();
15501555
const ops = (function* () {
15511556
yield* opsFromText('=', oldFullText.substring(0, spliceStart));
15521557
yield* opsFromText('-', oldText);
15531558
yield* opsFromText('+', newText, optNewTextAPairs, pool);
15541559
})();
1555-
for (const op of ops) assem.append(op);
1556-
assem.endDocument();
1557-
return exports.pack(oldLen, newLen, assem.toString(), newText);
1560+
const serializedOps = exports.serializeOps(exports.canonicalizeOps(ops, true));
1561+
return exports.pack(oldLen, newLen, serializedOps, newText);
15581562
};
15591563

15601564
/**
@@ -1676,11 +1680,8 @@ exports.moveOpsToNewPool = (cs, oldPool, newPool) => {
16761680
* @param {string} text - text to insert
16771681
* @returns {string}
16781682
*/
1679-
exports.makeAttribution = (text) => {
1680-
const assem = new SmartOpAssembler();
1681-
for (const op of opsFromText('+', text)) assem.append(op);
1682-
return assem.toString();
1683-
};
1683+
exports.makeAttribution =
1684+
(text) => exports.serializeOps(exports.canonicalizeOps(opsFromText('+', text), false));
16841685

16851686
/**
16861687
* Iterates over attributes in exports, attribution string, or attribs property of an op and runs
@@ -1933,8 +1934,7 @@ exports.attribsAttributeValue = (attribs, key, pool) => {
19331934
* @returns {Builder}
19341935
*/
19351936
exports.builder = (oldLen) => {
1936-
const assem = new SmartOpAssembler();
1937-
const o = new Op();
1937+
const ops = [];
19381938
const charBank = exports.stringAssembler();
19391939

19401940
const self = {
@@ -1949,12 +1949,12 @@ exports.builder = (oldLen) => {
19491949
* @returns {Builder} this
19501950
*/
19511951
keep: (N, L, attribs, pool) => {
1952-
o.opcode = '=';
1952+
const o = new Op('=');
19531953
o.attribs = typeof attribs === 'string'
19541954
? attribs : new AttributeMap(pool).update(attribs || []).toString();
19551955
o.chars = N;
19561956
o.lines = (L || 0);
1957-
assem.append(o);
1957+
ops.push(o);
19581958
return self;
19591959
},
19601960

@@ -1967,7 +1967,7 @@ exports.builder = (oldLen) => {
19671967
* @returns {Builder} this
19681968
*/
19691969
keepText: (text, attribs, pool) => {
1970-
for (const op of opsFromText('=', text, attribs, pool)) assem.append(op);
1970+
ops.push(...opsFromText('=', text, attribs, pool));
19711971
return self;
19721972
},
19731973

@@ -1980,7 +1980,7 @@ exports.builder = (oldLen) => {
19801980
* @returns {Builder} this
19811981
*/
19821982
insert: (text, attribs, pool) => {
1983-
for (const op of opsFromText('+', text, attribs, pool)) assem.append(op);
1983+
ops.push(...opsFromText('+', text, attribs, pool));
19841984
charBank.append(text);
19851985
return self;
19861986
},
@@ -1992,18 +1992,22 @@ exports.builder = (oldLen) => {
19921992
* @returns {Builder} this
19931993
*/
19941994
remove: (N, L) => {
1995-
o.opcode = '-';
1995+
const o = new Op('-');
19961996
o.attribs = '';
19971997
o.chars = N;
19981998
o.lines = (L || 0);
1999-
assem.append(o);
1999+
ops.push(o);
20002000
return self;
20012001
},
20022002

20032003
toString: () => {
2004-
assem.endDocument();
2005-
const newLen = oldLen + assem.getLengthChange();
2006-
return exports.pack(oldLen, newLen, assem.toString(), charBank.toString());
2004+
/** @type {number} */
2005+
let lengthChange;
2006+
const serializedOps = exports.serializeOps((function* () {
2007+
lengthChange = yield* exports.canonicalizeOps(ops, true);
2008+
})());
2009+
const newLen = oldLen + lengthChange;
2010+
return exports.pack(oldLen, newLen, serializedOps, charBank.toString());
20072011
},
20082012
};
20092013

@@ -2038,11 +2042,11 @@ exports.makeAttribsString = (opcode, attribs, pool) => {
20382042
exports.subattribution = (astr, start, optEnd) => {
20392043
const attOps = exports.deserializeOps(astr);
20402044
let attOpsNext = attOps.next();
2041-
const assem = new SmartOpAssembler();
20422045
let attOp = new Op();
2043-
const csOp = new Op();
2046+
const csOp = new Op('-');
2047+
csOp.chars = start;
20442048

2045-
const doCsOp = () => {
2049+
const doCsOp = function* () {
20462050
if (!csOp.chars) return;
20472051
while (csOp.opcode && (attOp.opcode || !attOpsNext.done)) {
20482052
if (!attOp.opcode) {
@@ -2054,30 +2058,25 @@ exports.subattribution = (astr, start, optEnd) => {
20542058
csOp.lines++;
20552059
}
20562060
const opOut = slicerZipperFunc(attOp, csOp, null);
2057-
if (opOut.opcode) assem.append(opOut);
2061+
if (opOut.opcode) yield opOut;
20582062
}
20592063
};
20602064

2061-
csOp.opcode = '-';
2062-
csOp.chars = start;
2063-
2064-
doCsOp();
2065-
2066-
if (optEnd === undefined) {
2067-
if (attOp.opcode) {
2068-
assem.append(attOp);
2069-
}
2070-
while (!attOpsNext.done) {
2071-
assem.append(attOpsNext.value);
2072-
attOpsNext = attOps.next();
2065+
const ops = (function* () {
2066+
yield* doCsOp();
2067+
if (optEnd === undefined) {
2068+
if (attOp.opcode) yield attOp;
2069+
while (!attOpsNext.done) {
2070+
yield attOpsNext.value;
2071+
attOpsNext = attOps.next();
2072+
}
2073+
} else {
2074+
csOp.opcode = '=';
2075+
csOp.chars = optEnd - start;
2076+
yield* doCsOp();
20732077
}
2074-
} else {
2075-
csOp.opcode = '=';
2076-
csOp.chars = optEnd - start;
2077-
doCsOp();
2078-
}
2079-
2080-
return assem.toString();
2078+
})();
2079+
return exports.serializeOps(exports.canonicalizeOps(ops, false));
20812080
};
20822081

20832082
exports.inverse = (cs, lines, alines, pool) => {

src/static/js/ace2_inner.js

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -522,18 +522,24 @@ function Ace2Inner(editorInfo, cssManagers) {
522522
const numLines = rep.lines.length();
523523
const upToLastLine = rep.lines.offsetOfIndex(numLines - 1);
524524
const lastLineLength = rep.lines.atIndex(numLines - 1).text.length;
525-
const assem = Changeset.smartOpAssembler();
526-
const o = new Changeset.Op('-');
527-
o.chars = upToLastLine;
528-
o.lines = numLines - 1;
529-
assem.append(o);
530-
o.chars = lastLineLength;
531-
o.lines = 0;
532-
assem.append(o);
533-
for (const op of Changeset.opsFromAText(atext)) assem.append(op);
534-
const newLen = oldLen + assem.getLengthChange();
535-
const changeset = Changeset.checkRep(
536-
Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1)));
525+
const ops = (function* () {
526+
const op1 = new Changeset.Op('-');
527+
op1.chars = upToLastLine;
528+
op1.lines = numLines - 1;
529+
yield op1;
530+
const op2 = new Changeset.Op('-');
531+
op2.chars = lastLineLength;
532+
op2.lines = 0;
533+
yield op2;
534+
yield* Changeset.opsFromAText(atext);
535+
})();
536+
let lengthChange;
537+
const serializedOps = Changeset.serializeOps((function* () {
538+
lengthChange = yield* Changeset.canonicalizeOps(ops, false);
539+
})());
540+
const newLen = oldLen + lengthChange;
541+
const changeset =
542+
Changeset.checkRep(Changeset.pack(oldLen, newLen, serializedOps, atext.text.slice(0, -1)));
537543
performDocumentApplyChangeset(changeset);
538544

539545
performSelectionChange(

0 commit comments

Comments
 (0)