@@ -309,6 +309,18 @@ exports.newOp = (optOpcode) => {
309309 */
310310const 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