Skip to content

Commit 06df190

Browse files
committed
Changeset: Use a generator to implement MergingOpAssembler
Eventually all uses of the class will be switched to the generator.
1 parent f48d897 commit 06df190

File tree

1 file changed

+79
-43
lines changed

1 file changed

+79
-43
lines changed

src/static/js/Changeset.js

Lines changed: 79 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,18 @@ exports.newOp = (optOpcode) => {
309309
*/
310310
const copyOp = (op1, op2 = new Op()) => Object.assign(op2, op1);
311311

312+
/**
313+
* Converts an iterable of operation objects to wire format.
314+
*
315+
* @param {Iterable<Op>} ops - Iterable of operations to serialize.
316+
* @returns {string} A string containing the encoded op data (example: '|5=2p=v*4*5+1').
317+
*/
318+
const serializeOps = (ops) => {
319+
let res = '';
320+
for (const op of ops) res += op.toString();
321+
return res;
322+
};
323+
312324
/**
313325
* Serializes a sequence of Ops.
314326
*/
@@ -335,68 +347,92 @@ class OpAssembler {
335347
}
336348

337349
/**
338-
* Efficiently merges consecutive operations that are mergeable, ignores no-ops, and drops final
339-
* pure "keeps". It does not re-order operations.
350+
* Combines consecutive operations when possible. Also skips no-op changes.
351+
*
352+
* @param {Iterable<Op>} ops - Iterable of operations to combine.
353+
* @param {boolean} finalize - If truthy, omits the final op if it is an attributeless keep op.
354+
* @yields {Op} The squashed operations.
355+
* @returns {Generator<Op>}
340356
*/
341-
class MergingOpAssembler {
342-
constructor() {
343-
this._assem = new OpAssembler();
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;
349-
}
357+
const squashOps = function* (ops, finalize) {
358+
let prevOp = new Op();
359+
// If we get, for example, insertions [xxx\n,yyy], those don't merge, but if we get
360+
// [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n]. This variable stores the length of yyy and
361+
// any other newline-less ops immediately after it.
362+
let prevOpAdditionalCharsAfterNewline = 0;
350363

351-
_flush(isEndDocument) {
352-
if (!this._bufOp.opcode) return;
353-
if (isEndDocument && this._bufOp.opcode === '=' && !this._bufOp.attribs) {
364+
const flush = function* (finalize) {
365+
if (!prevOp.opcode) return;
366+
if (finalize && prevOp.opcode === '=' && !prevOp.attribs) {
354367
// final merged keep, leave it implicit
355368
} else {
356-
this._assem.append(this._bufOp);
357-
if (this._bufOpAdditionalCharsAfterNewline) {
358-
this._bufOp.chars = this._bufOpAdditionalCharsAfterNewline;
359-
this._bufOp.lines = 0;
360-
this._assem.append(this._bufOp);
361-
this._bufOpAdditionalCharsAfterNewline = 0;
369+
yield prevOp;
370+
if (prevOpAdditionalCharsAfterNewline) {
371+
const op = new Op(prevOp.opcode);
372+
op.chars = prevOpAdditionalCharsAfterNewline;
373+
op.lines = 0;
374+
op.attribs = prevOp.attribs;
375+
yield op;
376+
prevOpAdditionalCharsAfterNewline = 0;
362377
}
363378
}
364-
this._bufOp.opcode = '';
365-
}
379+
prevOp = new Op();
380+
};
366381

367-
append(op) {
368-
if (op.chars <= 0) return;
369-
if (this._bufOp.opcode === op.opcode && this._bufOp.attribs === op.attribs) {
382+
for (const op of ops) {
383+
if (!op.opcode || op.chars <= 0) continue;
384+
if (prevOp.opcode === op.opcode && prevOp.attribs === op.attribs) {
370385
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;
386+
// bufOp and additional chars are all mergeable into a multi-line op
387+
prevOp.chars += prevOpAdditionalCharsAfterNewline + op.chars;
388+
prevOp.lines += op.lines;
389+
prevOpAdditionalCharsAfterNewline = 0;
390+
} else if (prevOp.lines === 0) {
391+
// both prevOp and op are in-line
392+
prevOp.chars += op.chars;
378393
} else {
379-
// append in-line text to multi-line this._bufOp
380-
this._bufOpAdditionalCharsAfterNewline += op.chars;
394+
// append in-line text to multi-line prevOp
395+
prevOpAdditionalCharsAfterNewline += op.chars;
381396
}
382397
} else {
383-
this._flush();
384-
copyOp(op, this._bufOp);
398+
yield* flush(false);
399+
prevOp = copyOp(op); // prevOp is mutated, so make a copy to protect op.
385400
}
386401
}
387402

388-
endDocument() {
389-
this._flush(true);
403+
yield* flush(finalize);
404+
};
405+
406+
/**
407+
* Efficiently merges consecutive operations that are mergeable, ignores no-ops, and drops final
408+
* pure "keeps". It does not re-order operations.
409+
*/
410+
class MergingOpAssembler {
411+
constructor() {
412+
this.clear();
390413
}
391414

392-
toString() {
393-
this._flush();
394-
return this._assem.toString();
415+
_serialize(finalize) {
416+
this._serialized = serializeOps(squashOps(this._ops, finalize));
395417
}
396418

397419
clear() {
398-
this._assem.clear();
399-
clearOp(this._bufOp);
420+
this._ops = [];
421+
this._serialized = null;
422+
}
423+
424+
append(op) {
425+
this._serialized = null;
426+
this._ops.push(copyOp(op));
427+
}
428+
429+
endDocument() {
430+
this._serialize(true);
431+
}
432+
433+
toString() {
434+
if (this._serialized == null) this._serialize(false);
435+
return this._serialized;
400436
}
401437
}
402438

0 commit comments

Comments
 (0)