Skip to content

Commit 8c4a0a1

Browse files
committed
Changeset: Use a generator to implement MergingOpAssembler
Eventually all uses of the class will be switched to the generator.
1 parent 3b4752b commit 8c4a0a1

File tree

1 file changed

+66
-41
lines changed

1 file changed

+66
-41
lines changed

src/static/js/Changeset.js

Lines changed: 66 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,63 @@ class OpAssembler {
330330
}
331331
}
332332

333+
/**
334+
* Combines consecutive operations when possible. Also skips no-op changes.
335+
*
336+
* @param {Iterable<Op>} ops - Iterable of operations to combine.
337+
* @param {boolean} finalize - If truthy, omits the final op if it is an attributeless keep op.
338+
* @yields {Op} The squashed operations.
339+
* @returns {Generator<Op>}
340+
*/
341+
const squashOps = function* (ops, finalize) {
342+
let prevOp = new Op();
343+
// If we get, for example, insertions [xxx\n,yyy], those don't merge, but if we get
344+
// [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n]. This variable stores the length of yyy and
345+
// any other newline-less ops immediately after it.
346+
let prevOpAdditionalCharsAfterNewline = 0;
347+
348+
const flush = function* (finalize) {
349+
if (!prevOp.opcode) return;
350+
if (finalize && prevOp.opcode === '=' && !prevOp.attribs) {
351+
// final merged keep, leave it implicit
352+
} else {
353+
yield prevOp;
354+
if (prevOpAdditionalCharsAfterNewline) {
355+
const op = new Op(prevOp.opcode);
356+
op.chars = prevOpAdditionalCharsAfterNewline;
357+
op.lines = 0;
358+
op.attribs = prevOp.attribs;
359+
yield op;
360+
prevOpAdditionalCharsAfterNewline = 0;
361+
}
362+
}
363+
prevOp = new Op();
364+
};
365+
366+
for (const op of ops) {
367+
if (!op.opcode || op.chars <= 0) continue;
368+
if (prevOp.opcode === op.opcode && prevOp.attribs === op.attribs) {
369+
if (op.lines > 0) {
370+
// bufOp and additional chars are all mergeable into a multi-line op
371+
prevOp.chars += prevOpAdditionalCharsAfterNewline + op.chars;
372+
prevOp.lines += op.lines;
373+
prevOpAdditionalCharsAfterNewline = 0;
374+
} else if (prevOp.lines === 0) {
375+
// both prevOp and op are in-line
376+
prevOp.chars += op.chars;
377+
} else {
378+
// append in-line text to multi-line prevOp
379+
prevOpAdditionalCharsAfterNewline += op.chars;
380+
}
381+
} else {
382+
yield* flush(false);
383+
prevOp = copyOp(op); // prevOp is mutated, so make a copy to protect op.
384+
}
385+
}
386+
387+
yield* flush(finalize);
388+
};
389+
333390
/**
334391
* Efficiently merges consecutive operations that are mergeable, ignores no-ops, and drops final
335392
* pure "keeps". It does not re-order operations.
@@ -340,58 +397,26 @@ class MergingOpAssembler {
340397
}
341398

342399
clear() {
343-
this._assem = [];
344-
this._bufOp = new Op();
345-
// If we get, for example, insertions [xxx\n,yyy], those don't merge, but if we get
346-
// [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n]. This variable stores the length of yyy and
347-
// any other newline-less ops immediately after it.
348-
this._bufOpAdditionalCharsAfterNewline = 0;
400+
this._ops = [];
401+
this._serialized = null;
349402
}
350403

351-
_flush(isEndDocument) {
352-
if (!this._bufOp.opcode) return;
353-
if (isEndDocument && this._bufOp.opcode === '=' && !this._bufOp.attribs) {
354-
// final merged keep, leave it implicit
355-
} else {
356-
this._assem.push(copyOp(this._bufOp));
357-
if (this._bufOpAdditionalCharsAfterNewline) {
358-
this._bufOp.chars = this._bufOpAdditionalCharsAfterNewline;
359-
this._bufOp.lines = 0;
360-
this._assem.push(copyOp(this._bufOp));
361-
this._bufOpAdditionalCharsAfterNewline = 0;
362-
}
363-
}
364-
this._bufOp.opcode = '';
404+
_serialize(finalize) {
405+
this._serialized = exports.serializeOps(squashOps(this._ops, finalize));
365406
}
366407

367408
append(op) {
368-
if (op.chars <= 0) return;
369-
if (this._bufOp.opcode === op.opcode && this._bufOp.attribs === op.attribs) {
370-
if (op.lines > 0) {
371-
// this._bufOp and additional chars are all mergeable into a multi-line op
372-
this._bufOp.chars += this._bufOpAdditionalCharsAfterNewline + op.chars;
373-
this._bufOp.lines += op.lines;
374-
this._bufOpAdditionalCharsAfterNewline = 0;
375-
} else if (this._bufOp.lines === 0) {
376-
// both this._bufOp and op are in-line
377-
this._bufOp.chars += op.chars;
378-
} else {
379-
// append in-line text to multi-line this._bufOp
380-
this._bufOpAdditionalCharsAfterNewline += op.chars;
381-
}
382-
} else {
383-
this._flush();
384-
copyOp(op, this._bufOp);
385-
}
409+
this._serialized = null;
410+
this._ops.push(copyOp(op));
386411
}
387412

388413
endDocument() {
389-
this._flush(true);
414+
this._serialize(true);
390415
}
391416

392417
toString() {
393-
this._flush();
394-
return exports.serializeOps(this._assem);
418+
if (this._serialized == null) this._serialize(false);
419+
return this._serialized;
395420
}
396421
}
397422

0 commit comments

Comments
 (0)