Changeset: Migrate from OpIter to deserializeOps()

This commit is contained in:
Richard Hansen 2021-10-25 05:48:58 -04:00
parent 0eca0251f2
commit 89fe40e080
16 changed files with 147 additions and 179 deletions

View file

@ -21,6 +21,8 @@
* `eachAttribNumber()` * `eachAttribNumber()`
* `makeAttribsString()` * `makeAttribsString()`
* `opAttributeValue()` * `opAttributeValue()`
* `opIterator()`: Deprecated in favor of the new `deserializeOps()` generator
function.
* `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

@ -670,9 +670,8 @@ const Changeset = require('ep_etherpad-lite/static/js/Changeset');
exports.getLineHTMLForExport = async (hookName, context) => { exports.getLineHTMLForExport = async (hookName, context) => {
if (!context.attribLine) return; if (!context.attribLine) return;
const opIter = Changeset.opIterator(context.attribLine); const [op] = Changeset.deserializeOps(context.attribLine);
if (!opIter.hasNext()) return; if (op == null) return;
const op = opIter.next();
const heading = AttributeMap.fromString(op.attribs, context.apool).get('heading'); const heading = AttributeMap.fromString(op.attribs, context.apool).get('heading');
if (!heading) return; if (!heading) return;
context.lineContent = `<${heading}>${context.lineContent}</${heading}>`; context.lineContent = `<${heading}>${context.lineContent}</${heading}>`;

View file

@ -527,12 +527,10 @@ exports.restoreRevision = async (padID, rev) => {
atext.text += '\n'; atext.text += '\n';
const eachAttribRun = (attribs, func) => { const eachAttribRun = (attribs, func) => {
const attribsIter = Changeset.opIterator(attribs);
let textIndex = 0; let textIndex = 0;
const newTextStart = 0; const newTextStart = 0;
const newTextEnd = atext.text.length; const newTextEnd = atext.text.length;
while (attribsIter.hasNext()) { for (const op of Changeset.deserializeOps(attribs)) {
const op = attribsIter.next();
const nextIndex = textIndex + op.chars; const nextIndex = textIndex + op.chars;
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);

View file

@ -586,12 +586,7 @@ const handleUserChanges = async (socket, message) => {
Changeset.checkRep(changeset); Changeset.checkRep(changeset);
// Validate all added 'author' attribs to be the same value as the current user // Validate all added 'author' attribs to be the same value as the current user
const iterator = Changeset.opIterator(Changeset.unpack(changeset).ops); for (const op of Changeset.deserializeOps(Changeset.unpack(changeset).ops)) {
let op;
while (iterator.hasNext()) {
op = iterator.next();
// + can add text with attribs // + can add text with attribs
// = can change or add attribs // = can change or add attribs
// - can have attribs, but they are discarded and don't show up in the attribs - // - can have attribs, but they are discarded and don't show up in the attribs -
@ -741,10 +736,8 @@ const _correctMarkersInPad = (atext, apool) => {
// collect char positions of line markers (e.g. bullets) in new atext // collect char positions of line markers (e.g. bullets) in new atext
// that aren't at the start of a line // that aren't at the start of a line
const badMarkers = []; const badMarkers = [];
const iter = Changeset.opIterator(atext.attribs);
let offset = 0; let offset = 0;
while (iter.hasNext()) { for (const op of Changeset.deserializeOps(atext.attribs)) {
const op = iter.next();
const attribs = AttributeMap.fromString(op.attribs, apool); const attribs = AttributeMap.fromString(op.attribs, apool);
const hasMarker = AttributeManager.lineAttributes.some((a) => attribs.has(a)); const hasMarker = AttributeManager.lineAttributes.some((a) => attribs.has(a));
if (hasMarker) { if (hasMarker) {

View file

@ -52,9 +52,8 @@ exports._analyzeLine = (text, aline, apool) => {
let lineMarker = 0; let lineMarker = 0;
line.listLevel = 0; line.listLevel = 0;
if (aline) { if (aline) {
const opIter = Changeset.opIterator(aline); const [op] = Changeset.deserializeOps(aline);
if (opIter.hasNext()) { if (op != null) {
const op = opIter.next();
const attribs = AttributeMap.fromString(op.attribs, apool); const attribs = AttributeMap.fromString(op.attribs, apool);
let listType = attribs.get('list'); let listType = attribs.get('list');
if (listType) { if (listType) {

View file

@ -197,13 +197,12 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
return; return;
} }
const iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars)); const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars));
idx += numChars; idx += numChars;
// this iterates over every op string and decides which tags to open or to close // this iterates over every op string and decides which tags to open or to close
// based on the attribs used // based on the attribs used
while (iter.hasNext()) { for (const o of ops) {
const o = iter.next();
const usedAttribs = []; const usedAttribs = [];
// mark all attribs as used // mark all attribs as used

View file

@ -76,11 +76,10 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
return; return;
} }
const iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars)); const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars));
idx += numChars; idx += numChars;
while (iter.hasNext()) { for (const o of ops) {
const o = iter.next();
let propChanged = false; let propChanged = false;
for (const a of attributes.decodeAttribString(o.attribs)) { for (const a of attributes.decodeAttribString(o.attribs)) {

View file

@ -67,12 +67,10 @@ exports.setPadHTML = async (pad, html) => {
const builder = Changeset.builder(1); const builder = Changeset.builder(1);
// assemble each line into the builder // assemble each line into the builder
const attribsIter = Changeset.opIterator(newAttribs);
let textIndex = 0; let textIndex = 0;
const newTextStart = 0; const newTextStart = 0;
const newTextEnd = newText.length; const newTextEnd = newText.length;
while (attribsIter.hasNext()) { for (const op of Changeset.deserializeOps(newAttribs)) {
const op = attribsIter.next();
const nextIndex = textIndex + op.chars; const nextIndex = textIndex + op.chars;
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
const start = Math.max(newTextStart, textIndex); const start = Math.max(newTextStart, textIndex);

View file

@ -35,16 +35,10 @@ PadDiff.prototype._isClearAuthorship = function (changeset) {
return false; return false;
} }
// lets iterator over the operators const [clearOperator, anotherOp] = Changeset.deserializeOps(unpacked.ops);
const iterator = Changeset.opIterator(unpacked.ops);
// get the first operator, this should be a clear operator
const clearOperator = iterator.next();
// check if there is only one operator // check if there is only one operator
if (iterator.hasNext() === true) { if (anotherOp != null) return false;
return false;
}
// check if this operator doesn't change text // check if this operator doesn't change text
if (clearOperator.opcode !== '=') { if (clearOperator.opcode !== '=') {
@ -212,7 +206,6 @@ PadDiff.prototype._extendChangesetWithAuthor = (changeset, author, apool) => {
// unpack // unpack
const unpacked = Changeset.unpack(changeset); const unpacked = Changeset.unpack(changeset);
const iterator = Changeset.opIterator(unpacked.ops);
const assem = Changeset.opAssembler(); const assem = Changeset.opAssembler();
// create deleted attribs // create deleted attribs
@ -220,10 +213,7 @@ PadDiff.prototype._extendChangesetWithAuthor = (changeset, author, apool) => {
const deletedAttrib = apool.putAttrib(['removed', true]); const deletedAttrib = apool.putAttrib(['removed', true]);
const attribs = `*${Changeset.numToString(authorAttrib)}*${Changeset.numToString(deletedAttrib)}`; const attribs = `*${Changeset.numToString(authorAttrib)}*${Changeset.numToString(deletedAttrib)}`;
// iteratore over the operators of the changeset for (const operator of Changeset.deserializeOps(unpacked.ops)) {
while (iterator.hasNext()) {
const operator = iterator.next();
if (operator.opcode === '-') { if (operator.opcode === '-') {
// this is a delete operator, extend it with the author // this is a delete operator, extend it with the author
operator.attribs = attribs; operator.attribs = attribs;
@ -268,22 +258,23 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
let curLine = 0; let curLine = 0;
let curChar = 0; let curChar = 0;
let curLineOpIter = null; let curLineOps = null;
let curLineOpIterLine; let curLineOpsNext = null;
let curLineOpsLine;
let curLineNextOp = new Changeset.Op('+'); let curLineNextOp = new Changeset.Op('+');
const unpacked = Changeset.unpack(cs); const unpacked = Changeset.unpack(cs);
const csIter = Changeset.opIterator(unpacked.ops);
const builder = Changeset.builder(unpacked.newLen); const builder = Changeset.builder(unpacked.newLen);
const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => { const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => {
if ((!curLineOpIter) || (curLineOpIterLine !== curLine)) { if (!curLineOps || curLineOpsLine !== curLine) {
// create curLineOpIter and advance it to curChar curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
curLineOpIter = Changeset.opIterator(aLinesGet(curLine)); curLineOpsNext = curLineOps.next();
curLineOpIterLine = curLine; curLineOpsLine = curLine;
let indexIntoLine = 0; let indexIntoLine = 0;
while (curLineOpIter.hasNext()) { while (!curLineOpsNext.done) {
curLineNextOp = curLineOpIter.next(); curLineNextOp = curLineOpsNext.value;
curLineOpsNext = curLineOps.next();
if (indexIntoLine + curLineNextOp.chars >= curChar) { if (indexIntoLine + curLineNextOp.chars >= curChar) {
curLineNextOp.chars -= (curChar - indexIntoLine); curLineNextOp.chars -= (curChar - indexIntoLine);
break; break;
@ -293,16 +284,22 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
} }
while (numChars > 0) { while (numChars > 0) {
if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) { if (!curLineNextOp.chars && curLineOpsNext.done) {
curLine++; curLine++;
curChar = 0; curChar = 0;
curLineOpIterLine = curLine; curLineOpsLine = curLine;
curLineNextOp.chars = 0; curLineNextOp.chars = 0;
curLineOpIter = Changeset.opIterator(aLinesGet(curLine)); curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
curLineOpsNext = curLineOps.next();
} }
if (!curLineNextOp.chars) { if (!curLineNextOp.chars) {
curLineNextOp = curLineOpIter.hasNext() ? curLineOpIter.next() : new Changeset.Op(); if (curLineOpsNext.done) {
curLineNextOp = new Changeset.Op();
} else {
curLineNextOp = curLineOpsNext.value;
curLineOpsNext = curLineOps.next();
}
} }
const charsToUse = Math.min(numChars, curLineNextOp.chars); const charsToUse = Math.min(numChars, curLineNextOp.chars);
@ -314,7 +311,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
curChar += charsToUse; curChar += charsToUse;
} }
if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) { if (!curLineNextOp.chars && curLineOpsNext.done) {
curLine++; curLine++;
curChar = 0; curChar = 0;
} }
@ -324,7 +321,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
if (L) { if (L) {
curLine += L; curLine += L;
curChar = 0; curChar = 0;
} else if (curLineOpIter && curLineOpIterLine === curLine) { } else if (curLineOps && curLineOpsLine === curLine) {
consumeAttribRuns(N, () => {}); consumeAttribRuns(N, () => {});
} else { } else {
curChar += N; curChar += N;
@ -361,10 +358,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
}; };
}; };
// iterate over all operators of this changeset for (const csOp of Changeset.deserializeOps(unpacked.ops)) {
while (csIter.hasNext()) {
const csOp = csIter.next();
if (csOp.opcode === '=') { if (csOp.opcode === '=') {
const textBank = nextText(csOp.chars); const textBank = nextText(csOp.chars);

View file

@ -150,9 +150,9 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
// get `attributeName` attribute of first char of line // get `attributeName` attribute of first char of line
const aline = this.rep.alines[lineNum]; const aline = this.rep.alines[lineNum];
if (!aline) return ''; if (!aline) return '';
const opIter = Changeset.opIterator(aline); const [op] = Changeset.deserializeOps(aline);
if (!opIter.hasNext()) return ''; if (op == null) return '';
return AttributeMap.fromString(opIter.next().attribs, this.rep.apool).get(attributeName) || ''; return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || '';
}, },
/* /*
@ -163,9 +163,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
// get attributes of first char of line // get attributes of first char of line
const aline = this.rep.alines[lineNum]; const aline = this.rep.alines[lineNum];
if (!aline) return []; if (!aline) return [];
const opIter = Changeset.opIterator(aline); const [op] = Changeset.deserializeOps(aline);
if (!opIter.hasNext()) return []; if (op == null) return [];
const op = opIter.next();
return [...attributes.attribsFromString(op.attribs, this.rep.apool)]; return [...attributes.attribsFromString(op.attribs, this.rep.apool)];
}, },
@ -221,13 +220,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
const end = selEnd[1]; const end = selEnd[1];
let hasAttrib = true; let hasAttrib = true;
// Iterate over attribs on this line
const opIter = Changeset.opIterator(rep.alines[lineNum]);
let indexIntoLine = 0; let indexIntoLine = 0;
for (const op of Changeset.deserializeOps(rep.alines[lineNum])) {
while (opIter.hasNext()) {
const op = opIter.next();
const opStartInLine = indexIntoLine; const opStartInLine = indexIntoLine;
const opEndInLine = opStartInLine + op.chars; const opEndInLine = opStartInLine + op.chars;
if (!hasIt(op.attribs)) { if (!hasIt(op.attribs)) {
@ -260,15 +254,11 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
if (!aline) { if (!aline) {
return []; return [];
} }
// iterate through all operations of a line
const opIter = Changeset.opIterator(aline);
// we need to sum up how much characters each operations take until the wanted position // we need to sum up how much characters each operations take until the wanted position
let currentPointer = 0; let currentPointer = 0;
let currentOperation;
while (opIter.hasNext()) { for (const currentOperation of Changeset.deserializeOps(aline)) {
currentOperation = opIter.next();
currentPointer += currentOperation.chars; currentPointer += currentOperation.chars;
if (currentPointer <= column) continue; if (currentPointer <= column) continue;
return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)]; return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)];

View file

@ -187,7 +187,7 @@ exports.newLen = (cs) => exports.unpack(cs).newLen;
* @yields {Op} * @yields {Op}
* @returns {Generator<Op>} * @returns {Generator<Op>}
*/ */
const deserializeOps = function* (ops) { exports.deserializeOps = function* (ops) {
// TODO: Migrate to String.prototype.matchAll() once there is enough browser support. // TODO: Migrate to String.prototype.matchAll() once there is enough browser support.
const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|(.)/g; const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|(.)/g;
let match; let match;
@ -206,13 +206,15 @@ const deserializeOps = function* (ops) {
* Iterator over a changeset's operations. * Iterator over a changeset's operations.
* *
* Note: This class does NOT implement the ECMAScript iterable or iterator protocols. * Note: This class does NOT implement the ECMAScript iterable or iterator protocols.
*
* @deprecated Use `deserializeOps` instead.
*/ */
class OpIter { class OpIter {
/** /**
* @param {string} ops - String encoding the change operations to iterate over. * @param {string} ops - String encoding the change operations to iterate over.
*/ */
constructor(ops) { constructor(ops) {
this._gen = deserializeOps(ops); this._gen = exports.deserializeOps(ops);
this._next = this._gen.next(); this._next = this._gen.next();
} }
@ -246,10 +248,15 @@ class OpIter {
/** /**
* Creates an iterator which decodes string changeset operations. * Creates an iterator which decodes string changeset operations.
* *
* @deprecated Use `deserializeOps` instead.
* @param {string} opsStr - String encoding of the change operations to perform. * @param {string} opsStr - String encoding of the change operations to perform.
* @returns {OpIter} Operator iterator object. * @returns {OpIter} Operator iterator object.
*/ */
exports.opIterator = (opsStr) => new OpIter(opsStr); exports.opIterator = (opsStr) => {
padutils.warnWithStack(
'Changeset.opIterator() is deprecated; use Changeset.deserializeOps() instead');
return new OpIter(opsStr);
};
/** /**
* Cleans an Op object. * Cleans an Op object.
@ -374,9 +381,7 @@ exports.checkRep = (cs) => {
let oldPos = 0; let oldPos = 0;
let calcNewLen = 0; let calcNewLen = 0;
let numInserted = 0; let numInserted = 0;
const iter = new OpIter(ops); for (const o of exports.deserializeOps(ops)) {
while (iter.hasNext()) {
const o = iter.next();
switch (o.opcode) { switch (o.opcode) {
case '=': case '=':
oldPos += o.chars; oldPos += o.chars;
@ -1027,15 +1032,18 @@ class TextLinesMutator {
* @returns {string} the integrated changeset * @returns {string} the integrated changeset
*/ */
const applyZip = (in1, in2, func) => { const applyZip = (in1, in2, func) => {
const iter1 = new OpIter(in1); const ops1 = exports.deserializeOps(in1);
const iter2 = new OpIter(in2); const ops2 = exports.deserializeOps(in2);
let next1 = ops1.next();
let next2 = ops2.next();
const assem = exports.smartOpAssembler(); const assem = exports.smartOpAssembler();
const op1 = new Op(); while (!next1.done || !next2.done) {
const op2 = new Op(); if (!next1.done && !next1.value.opcode) next1 = ops1.next();
while (op1.opcode || iter1.hasNext() || op2.opcode || iter2.hasNext()) { if (!next2.done && !next2.value.opcode) next2 = ops2.next();
if ((!op1.opcode) && iter1.hasNext()) iter1.next(op1); if (next1.value == null) next1.value = new Op();
if ((!op2.opcode) && iter2.hasNext()) iter2.next(op2); if (next2.value == null) next2.value = new Op();
const opOut = func(op1, op2); if (!next1.value.opcode && !next2.value.opcode) break;
const opOut = func(next1.value, next2.value);
if (opOut && opOut.opcode) assem.append(opOut); if (opOut && opOut.opcode) assem.append(opOut);
} }
assem.endDocument(); assem.endDocument();
@ -1097,12 +1105,10 @@ exports.pack = (oldLen, newLen, opsStr, bank) => {
exports.applyToText = (cs, str) => { exports.applyToText = (cs, str) => {
const unpacked = exports.unpack(cs); const unpacked = exports.unpack(cs);
assert(str.length === unpacked.oldLen, `mismatched apply: ${str.length} / ${unpacked.oldLen}`); assert(str.length === unpacked.oldLen, `mismatched apply: ${str.length} / ${unpacked.oldLen}`);
const csIter = new OpIter(unpacked.ops);
const bankIter = exports.stringIterator(unpacked.charBank); const bankIter = exports.stringIterator(unpacked.charBank);
const strIter = exports.stringIterator(str); const strIter = exports.stringIterator(str);
const assem = exports.stringAssembler(); const assem = exports.stringAssembler();
while (csIter.hasNext()) { for (const op of exports.deserializeOps(unpacked.ops)) {
const op = csIter.next();
switch (op.opcode) { switch (op.opcode) {
case '+': case '+':
// op is + and op.lines 0: no newlines must be in op.chars // op is + and op.lines 0: no newlines must be in op.chars
@ -1142,11 +1148,9 @@ exports.applyToText = (cs, str) => {
*/ */
exports.mutateTextLines = (cs, lines) => { exports.mutateTextLines = (cs, lines) => {
const unpacked = exports.unpack(cs); const unpacked = exports.unpack(cs);
const csIter = new OpIter(unpacked.ops);
const bankIter = exports.stringIterator(unpacked.charBank); const bankIter = exports.stringIterator(unpacked.charBank);
const mut = new TextLinesMutator(lines); const mut = new TextLinesMutator(lines);
while (csIter.hasNext()) { for (const op of exports.deserializeOps(unpacked.ops)) {
const op = csIter.next();
switch (op.opcode) { switch (op.opcode) {
case '+': case '+':
mut.insert(bankIter.take(op.chars), op.lines); mut.insert(bankIter.take(op.chars), op.lines);
@ -1273,24 +1277,30 @@ exports.applyToAttribution = (cs, astr, pool) => {
exports.mutateAttributionLines = (cs, lines, pool) => { exports.mutateAttributionLines = (cs, lines, pool) => {
const unpacked = exports.unpack(cs); const unpacked = exports.unpack(cs);
const csIter = new OpIter(unpacked.ops); const csOps = exports.deserializeOps(unpacked.ops);
let csOpsNext = csOps.next();
const csBank = unpacked.charBank; const csBank = unpacked.charBank;
let csBankIndex = 0; let csBankIndex = 0;
// treat the attribution lines as text lines, mutating a line at a time // treat the attribution lines as text lines, mutating a line at a time
const mut = new TextLinesMutator(lines); const mut = new TextLinesMutator(lines);
/** @type {?OpIter} */ /** @type {?Generator<Op>} */
let lineIter = null; let lineOps = null;
let lineOpsNext = null;
const isNextMutOp = () => (lineIter && lineIter.hasNext()) || mut.hasMore(); const lineOpsHasNext = () => lineOpsNext && !lineOpsNext.done;
const isNextMutOp = () => lineOpsHasNext() || mut.hasMore();
const nextMutOp = () => { const nextMutOp = () => {
if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) { if (!lineOpsHasNext() && mut.hasMore()) {
const line = mut.removeLines(1); const line = mut.removeLines(1);
lineIter = new OpIter(line); lineOps = exports.deserializeOps(line);
lineOpsNext = lineOps.next();
} }
if (!lineIter || !lineIter.hasNext()) return new Op(); if (!lineOpsHasNext()) return new Op();
return lineIter.next(); const op = lineOpsNext.value;
lineOpsNext = lineOps.next();
return op;
}; };
let lineAssem = null; let lineAssem = null;
@ -1308,12 +1318,15 @@ exports.mutateAttributionLines = (cs, lines, pool) => {
let csOp = new Op(); let csOp = new Op();
let attOp = new Op(); let attOp = new Op();
while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) { while (csOp.opcode || !csOpsNext.done || attOp.opcode || isNextMutOp()) {
if (!csOp.opcode && csIter.hasNext()) csOp = csIter.next(); if (!csOp.opcode && !csOpsNext.done) {
if ((!csOp.opcode) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) { csOp = csOpsNext.value;
csOpsNext = csOps.next();
}
if (!csOp.opcode && !attOp.opcode && !lineAssem && !lineOpsHasNext()) {
break; // done break; // done
} else if (csOp.opcode === '=' && csOp.lines > 0 && (!csOp.attribs) && } else if (csOp.opcode === '=' && csOp.lines > 0 && !csOp.attribs && !attOp.opcode &&
(!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) { !lineAssem && !lineOpsHasNext()) {
// skip multiple lines; this is what makes small changes not order of the document size // skip multiple lines; this is what makes small changes not order of the document size
mut.skipLines(csOp.lines); mut.skipLines(csOp.lines);
csOp.opcode = ''; csOp.opcode = '';
@ -1350,16 +1363,12 @@ exports.mutateAttributionLines = (cs, lines, pool) => {
exports.joinAttributionLines = (theAlines) => { exports.joinAttributionLines = (theAlines) => {
const assem = exports.mergingOpAssembler(); const assem = exports.mergingOpAssembler();
for (const aline of theAlines) { for (const aline of theAlines) {
const iter = new OpIter(aline); for (const op of exports.deserializeOps(aline)) assem.append(op);
while (iter.hasNext()) {
assem.append(iter.next());
}
} }
return assem.toString(); return assem.toString();
}; };
exports.splitAttributionLines = (attrOps, text) => { exports.splitAttributionLines = (attrOps, text) => {
const iter = new OpIter(attrOps);
const assem = exports.mergingOpAssembler(); const assem = exports.mergingOpAssembler();
const lines = []; const lines = [];
let pos = 0; let pos = 0;
@ -1373,8 +1382,7 @@ exports.splitAttributionLines = (attrOps, text) => {
pos += op.chars; pos += op.chars;
}; };
while (iter.hasNext()) { for (const op of exports.deserializeOps(attrOps)) {
const op = iter.next();
let numChars = op.chars; let numChars = op.chars;
let numLines = op.lines; let numLines = op.lines;
while (numLines > 1) { while (numLines > 1) {
@ -1517,11 +1525,9 @@ const toSplices = (cs) => {
const splices = []; const splices = [];
let oldPos = 0; let oldPos = 0;
const iter = new OpIter(unpacked.ops);
const charIter = exports.stringIterator(unpacked.charBank); const charIter = exports.stringIterator(unpacked.charBank);
let inSplice = false; let inSplice = false;
while (iter.hasNext()) { for (const op of exports.deserializeOps(unpacked.ops)) {
const op = iter.next();
if (op.opcode === '=') { if (op.opcode === '=') {
oldPos += op.chars; oldPos += op.chars;
inSplice = false; inSplice = false;
@ -1764,11 +1770,10 @@ exports.copyAText = (atext1, atext2) => {
*/ */
exports.opsFromAText = function* (atext) { exports.opsFromAText = function* (atext) {
// intentionally skips last newline char of atext // intentionally skips last newline char of atext
const iter = new OpIter(atext.attribs);
let lastOp = null; let lastOp = null;
while (iter.hasNext()) { for (const op of exports.deserializeOps(atext.attribs)) {
if (lastOp != null) yield lastOp; if (lastOp != null) yield lastOp;
lastOp = iter.next(); lastOp = op;
} }
if (lastOp == null) return; if (lastOp == null) return;
// exclude final newline // exclude final newline
@ -1986,15 +1991,19 @@ exports.makeAttribsString = (opcode, attribs, pool) => {
* Like "substring" but on a single-line attribution string. * Like "substring" but on a single-line attribution string.
*/ */
exports.subattribution = (astr, start, optEnd) => { exports.subattribution = (astr, start, optEnd) => {
const iter = new OpIter(astr); const attOps = exports.deserializeOps(astr);
let attOpsNext = attOps.next();
const assem = exports.smartOpAssembler(); const assem = exports.smartOpAssembler();
let attOp = new Op(); let attOp = new Op();
const csOp = new Op(); const csOp = new Op();
const doCsOp = () => { const doCsOp = () => {
if (!csOp.chars) return; if (!csOp.chars) return;
while (csOp.opcode && (attOp.opcode || iter.hasNext())) { while (csOp.opcode && (attOp.opcode || !attOpsNext.done)) {
if (!attOp.opcode) attOp = iter.next(); if (!attOp.opcode) {
attOp = attOpsNext.value;
attOpsNext = attOps.next();
}
if (csOp.opcode && attOp.opcode && csOp.chars >= attOp.chars && if (csOp.opcode && attOp.opcode && csOp.chars >= attOp.chars &&
attOp.lines > 0 && csOp.lines <= 0) { attOp.lines > 0 && csOp.lines <= 0) {
csOp.lines++; csOp.lines++;
@ -2013,7 +2022,10 @@ exports.subattribution = (astr, start, optEnd) => {
if (attOp.opcode) { if (attOp.opcode) {
assem.append(attOp); assem.append(attOp);
} }
while (iter.hasNext()) assem.append(iter.next()); while (!attOpsNext.done) {
assem.append(attOpsNext.value);
attOpsNext = attOps.next();
}
} else { } else {
csOp.opcode = '='; csOp.opcode = '=';
csOp.chars = optEnd - start; csOp.chars = optEnd - start;
@ -2050,22 +2062,23 @@ exports.inverse = (cs, lines, alines, pool) => {
let curLine = 0; let curLine = 0;
let curChar = 0; let curChar = 0;
let curLineOpIter = null; let curLineOps = null;
let curLineOpIterLine; let curLineOpsNext = null;
let curLineOpsLine;
let curLineNextOp = new Op('+'); let curLineNextOp = new Op('+');
const unpacked = exports.unpack(cs); const unpacked = exports.unpack(cs);
const csIter = new OpIter(unpacked.ops);
const builder = exports.builder(unpacked.newLen); const builder = exports.builder(unpacked.newLen);
const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => { const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => {
if ((!curLineOpIter) || (curLineOpIterLine !== curLine)) { if (!curLineOps || curLineOpsLine !== curLine) {
// create curLineOpIter and advance it to curChar curLineOps = exports.deserializeOps(alinesGet(curLine));
curLineOpIter = new OpIter(alinesGet(curLine)); curLineOpsNext = curLineOps.next();
curLineOpIterLine = curLine; curLineOpsLine = curLine;
let indexIntoLine = 0; let indexIntoLine = 0;
while (curLineOpIter.hasNext()) { while (!curLineOpsNext.done) {
curLineNextOp = curLineOpIter.next(); curLineNextOp = curLineOpsNext.value;
curLineOpsNext = curLineOps.next();
if (indexIntoLine + curLineNextOp.chars >= curChar) { if (indexIntoLine + curLineNextOp.chars >= curChar) {
curLineNextOp.chars -= (curChar - indexIntoLine); curLineNextOp.chars -= (curChar - indexIntoLine);
break; break;
@ -2075,15 +2088,21 @@ exports.inverse = (cs, lines, alines, pool) => {
} }
while (numChars > 0) { while (numChars > 0) {
if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) { if (!curLineNextOp.chars && curLineOpsNext.done) {
curLine++; curLine++;
curChar = 0; curChar = 0;
curLineOpIterLine = curLine; curLineOpsLine = curLine;
curLineNextOp.chars = 0; curLineNextOp.chars = 0;
curLineOpIter = new OpIter(alinesGet(curLine)); curLineOps = exports.deserializeOps(alinesGet(curLine));
curLineOpsNext = curLineOps.next();
} }
if (!curLineNextOp.chars) { if (!curLineNextOp.chars) {
curLineNextOp = curLineOpIter.hasNext() ? curLineOpIter.next() : new Op(); if (curLineOpsNext.done) {
curLineNextOp = new Op();
} else {
curLineNextOp = curLineOpsNext.value;
curLineOpsNext = curLineOps.next();
}
} }
const charsToUse = Math.min(numChars, curLineNextOp.chars); const charsToUse = Math.min(numChars, curLineNextOp.chars);
func(charsToUse, curLineNextOp.attribs, charsToUse === curLineNextOp.chars && func(charsToUse, curLineNextOp.attribs, charsToUse === curLineNextOp.chars &&
@ -2093,7 +2112,7 @@ exports.inverse = (cs, lines, alines, pool) => {
curChar += charsToUse; curChar += charsToUse;
} }
if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) { if (!curLineNextOp.chars && curLineOpsNext.done) {
curLine++; curLine++;
curChar = 0; curChar = 0;
} }
@ -2103,7 +2122,7 @@ exports.inverse = (cs, lines, alines, pool) => {
if (L) { if (L) {
curLine += L; curLine += L;
curChar = 0; curChar = 0;
} else if (curLineOpIter && curLineOpIterLine === curLine) { } else if (curLineOps && curLineOpsLine === curLine) {
consumeAttribRuns(N, () => {}); consumeAttribRuns(N, () => {});
} else { } else {
curChar += N; curChar += N;
@ -2138,8 +2157,7 @@ exports.inverse = (cs, lines, alines, pool) => {
}; };
}; };
while (csIter.hasNext()) { for (const csOp of exports.deserializeOps(unpacked.ops)) {
const csOp = csIter.next();
if (csOp.opcode === '=') { if (csOp.opcode === '=') {
if (csOp.attribs) { if (csOp.attribs) {
const attribs = AttributeMap.fromString(csOp.attribs, pool); const attribs = AttributeMap.fromString(csOp.attribs, pool);

View file

@ -1576,13 +1576,8 @@ function Ace2Inner(editorInfo, cssManagers) {
const end = selEnd[1]; const end = selEnd[1];
let hasAttrib = true; let hasAttrib = true;
// Iterate over attribs on this line
const opIter = Changeset.opIterator(rep.alines[lineNum]);
let indexIntoLine = 0; let indexIntoLine = 0;
for (const op of Changeset.deserializeOps(rep.alines[lineNum])) {
while (opIter.hasNext()) {
const op = opIter.next();
const opStartInLine = indexIntoLine; const opStartInLine = indexIntoLine;
const opEndInLine = opStartInLine + op.chars; const opEndInLine = opStartInLine + op.chars;
if (!hasIt(op.attribs)) { if (!hasIt(op.attribs)) {
@ -1615,7 +1610,6 @@ function Ace2Inner(editorInfo, cssManagers) {
const selStartLine = rep.selStart[0]; const selStartLine = rep.selStart[0];
const selEndLine = rep.selEnd[0]; const selEndLine = rep.selEnd[0];
for (let n = selStartLine; n <= selEndLine; n++) { for (let n = selStartLine; n <= selEndLine; n++) {
const opIter = Changeset.opIterator(rep.alines[n]);
let indexIntoLine = 0; let indexIntoLine = 0;
let selectionStartInLine = 0; let selectionStartInLine = 0;
if (documentAttributeManager.lineHasMarker(n)) { if (documentAttributeManager.lineHasMarker(n)) {
@ -1628,8 +1622,7 @@ function Ace2Inner(editorInfo, cssManagers) {
if (n === selEndLine) { if (n === selEndLine) {
selectionEndInLine = rep.selEnd[1]; selectionEndInLine = rep.selEnd[1];
} }
while (opIter.hasNext()) { for (const op of Changeset.deserializeOps(rep.alines[n])) {
const op = opIter.next();
const opStartInLine = indexIntoLine; const opStartInLine = indexIntoLine;
const opEndInLine = opStartInLine + op.chars; const opEndInLine = opStartInLine + op.chars;
if (!hasIt(op.attribs)) { if (!hasIt(op.attribs)) {
@ -1754,12 +1747,10 @@ function Ace2Inner(editorInfo, cssManagers) {
}; };
const eachAttribRun = (attribs, func /* (startInNewText, endInNewText, attribs)*/) => { const eachAttribRun = (attribs, func /* (startInNewText, endInNewText, attribs)*/) => {
const attribsIter = Changeset.opIterator(attribs);
let textIndex = 0; let textIndex = 0;
const newTextStart = commonStart; const newTextStart = commonStart;
const newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0); const newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0);
while (attribsIter.hasNext()) { for (const op of Changeset.deserializeOps(attribs)) {
const op = attribsIter.next();
const nextIndex = textIndex + op.chars; const nextIndex = textIndex + op.chars;
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
@ -1873,9 +1864,7 @@ function Ace2Inner(editorInfo, cssManagers) {
const attribRuns = (attribs) => { const attribRuns = (attribs) => {
const lengs = []; const lengs = [];
const atts = []; const atts = [];
const iter = Changeset.opIterator(attribs); for (const op of Changeset.deserializeOps(attribs)) {
while (iter.hasNext()) {
const op = iter.next();
lengs.push(op.chars); lengs.push(op.chars);
atts.push(op.attribs); atts.push(op.attribs);
} }
@ -2619,9 +2608,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// TODO: There appears to be a race condition or so. // TODO: There appears to be a race condition or so.
const authorIds = new Set(); const authorIds = new Set();
if (alineAttrs) { if (alineAttrs) {
const opIter = Changeset.opIterator(alineAttrs); for (const op of Changeset.deserializeOps(alineAttrs)) {
while (opIter.hasNext()) {
const op = opIter.next();
const authorId = AttributeMap.fromString(op.attribs, apool).get('author'); const authorId = AttributeMap.fromString(op.attribs, apool).get('author');
if (authorId) authorIds.add(authorId); if (authorId) authorIds.add(authorId);
} }

View file

@ -162,13 +162,8 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
// some chars are replaced (no attributes change and no length change) // some chars are replaced (no attributes change and no length change)
// test if there are keep ops at the start of the cs // test if there are keep ops at the start of the cs
if (lineChanged === undefined) { if (lineChanged === undefined) {
lineChanged = 0; const [op] = Changeset.deserializeOps(Changeset.unpack(changeset).ops);
const opIter = Changeset.opIterator(Changeset.unpack(changeset).ops); lineChanged = op != null && op.opcode === '=' ? op.lines : 0;
if (opIter.hasNext()) {
const op = opIter.next();
if (op.opcode === '=') lineChanged += op.lines;
}
} }
const goToLineNumber = (lineNumber) => { const goToLineNumber = (lineNumber) => {

View file

@ -143,12 +143,9 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
// Sanitize authorship: Replace all author attributes with this user's author ID in case the // Sanitize authorship: Replace all author attributes with this user's author ID in case the
// text was copied from another author. // text was copied from another author.
const cs = Changeset.unpack(userChangeset); const cs = Changeset.unpack(userChangeset);
const iterator = Changeset.opIterator(cs.ops);
let op;
const assem = Changeset.mergingOpAssembler(); const assem = Changeset.mergingOpAssembler();
while (iterator.hasNext()) { for (const op of Changeset.deserializeOps(cs.ops)) {
op = iterator.next();
if (op.opcode === '+') { if (op.opcode === '+') {
const attribs = AttributeMap.fromString(op.attribs, apool); const attribs = AttributeMap.fromString(op.attribs, apool);
const oldAuthorId = attribs.get('author'); const oldAuthorId = attribs.get('author');

View file

@ -98,11 +98,13 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
return classes.substring(1); return classes.substring(1);
}; };
const attributionIter = Changeset.opIterator(aline); const attrOps = Changeset.deserializeOps(aline);
let attrOpsNext = attrOps.next();
let nextOp, nextOpClasses; let nextOp, nextOpClasses;
const goNextOp = () => { const goNextOp = () => {
nextOp = attributionIter.hasNext() ? attributionIter.next() : new Changeset.Op(); nextOp = attrOpsNext.done ? new Changeset.Op() : attrOpsNext.value;
if (!attrOpsNext.done) attrOpsNext = attrOps.next();
nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs)); nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs));
}; };
goNextOp(); goNextOp();

View file

@ -31,17 +31,15 @@ const randInt = (maxValue) => Math.floor(Math.random() * maxValue);
describe('easysync', function () { describe('easysync', function () {
it('throughIterator', async function () { it('throughIterator', 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 iter = Changeset.opIterator(x);
const assem = Changeset.opAssembler(); const assem = Changeset.opAssembler();
while (iter.hasNext()) assem.append(iter.next()); for (const op of Changeset.deserializeOps(x)) assem.append(op);
expect(assem.toString()).to.equal(x); expect(assem.toString()).to.equal(x);
}); });
it('throughSmartAssembler', async function () { it('throughSmartAssembler', 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 iter = Changeset.opIterator(x);
const assem = Changeset.smartOpAssembler(); const assem = Changeset.smartOpAssembler();
while (iter.hasNext()) assem.append(iter.next()); for (const op of Changeset.deserializeOps(x)) assem.append(op);
assem.endDocument(); assem.endDocument();
expect(assem.toString()).to.equal(x); expect(assem.toString()).to.equal(x);
}); });
@ -730,7 +728,7 @@ describe('easysync', function () {
p.putAttrib(['name', 'david']); p.putAttrib(['name', 'david']);
p.putAttrib(['color', 'green']); p.putAttrib(['color', 'green']);
const stringOp = (str) => Changeset.opIterator(str).next(); const stringOp = (str) => Changeset.deserializeOps(str).next().value;
expect(Changeset.opAttributeValue(stringOp('*0*1+1'), 'name', p)).to.equal('david'); expect(Changeset.opAttributeValue(stringOp('*0*1+1'), 'name', p)).to.equal('david');
expect(Changeset.opAttributeValue(stringOp('*0+1'), 'name', p)).to.equal('david'); expect(Changeset.opAttributeValue(stringOp('*0+1'), 'name', p)).to.equal('david');