import assert from "assert" ; import { EventEmitter } from "events" ; import { Dom , hasTagName , isTextNode , Scheduler , TagName } from "@fluid-example/flow-util-lib" ; import { FluidObject } from "@fluidframework/core-interfaces" ; import { ISegment , LocalReference , MergeTreeMaintenanceType } from "@fluidframework/merge-tree" ; import { SequenceEvent } from "@fluidframework/sequence" ; import { FlowDocument } from "../document" ; import { IFluidHTMLViewFactory } from "../editor" ; import { clamp , done , emptyObject , getSegmentRange } from "../util" ; import { extractRef , updateRef } from "../util/localref" ; import { debug } from "./debug" ; import { BootstrapFormatter , Formatter , IFormatterState , RootFormatter } from "./formatter" ; interface ILayoutCursor { parent : Node ; previous : Node ; } interface IFormatInfo { readonly formatter : Readonly < Formatter < IFormatterState > > ; readonly state : IFormatterState ; } class LayoutCheckpoint { public readonly formatStack : readonly Readonly < IFormatInfo > [ ] ; public readonly cursor : Readonly < ILayoutCursor > ; constructor ( formatStack , cursor ) { this . formatStack = Object . freeze ( formatStack . slice ( 0 ) ) ; this . cursor = Object . freeze ( { ... cursor } ) ; } } export const eotSegment = Object . freeze ( { cachedLength : 0 } ) as ISegment ; export class Layout extends EventEmitter { private get format ( ) { const stack = this . formatStack ; return stack . length > 0 ? stack [ stack . length - 1 ] : this . rootFormatInfo ; } private get slot ( ) { return this . root ; } private get next ( ) { const cursor = this . cursor ; const { previous } = cursor ; return previous ? previous . nextSibling : cursor . parent . lastChild ; } public get cursor ( ) : Readonly < ILayoutCursor > { return this . _cursor ; } public get position ( ) { return this . _position ; } public get segment ( ) { return this . _segment ; } public get startOffset ( ) { return this . _startOffset ; } public get endOffset ( ) { return this . _endOffset ; } public get segmentStart ( ) { return this . _segmentStart ; } public get segmentEnd ( ) { return this . _segmentEnd ; } public get rendered ( ) { return this . renderPromise ; } public renderCallback ? : ( start , end ) => void ; public invalidatedCallback ? : ( start , end ) => void ; private readonly rootFormatInfo : IFormatInfo ; private formatStack : Readonly < IFormatInfo > [ ] ; private emitted : Set < Node > ; private pending : Set < Node > = new Set ( ) ; private readonly initialCheckpoint : LayoutCheckpoint ; private readonly segmentToCheckpoint = new WeakMap < ISegment , LayoutCheckpoint > ( ) ; private readonly nodeToSegmentMap = new WeakMap < Node , ISegment > ( ) ; private readonly segmentToEmitted = new WeakMap < ISegment , Set < Node > > ( ) ; private _cursor : ILayoutCursor ; private _position = NaN ; private _segment : ISegment ; private _startOffset = NaN ; private _endOffset = NaN ; private _segmentStart = NaN ; private _segmentEnd = NaN ; private startInvalid : LocalReference ; private endInvalid : LocalReference ; private readonly scheduleRender : ( ) => void ; private renderPromise = done ; private renderResolver : ( ) => void ; constructor ( public readonly doc , public readonly root , formatter , scheduler = new Scheduler ( ) , public readonly viewFactoryRegistry = new Map ( ) , public readonly scope ? ) { super ( ) ; this . scheduleRender = scheduler . coalesce ( scheduler . onTurnEnd , ( ) => { this . render ( ) ; } ) ; this . initialCheckpoint = new LayoutCheckpoint ( [ ] , { parent : this . slot , previous : null } ) ; this . rootFormatInfo = Object . freeze ( { formatter : new BootstrapFormatter ( formatter ) , state : emptyObject } ) ; doc . on ( "sequenceDelta" , this . onChange ) ; doc . on ( "maintenance" , this . onChange ) ; debug ( "begin: initial sync" ) ; this . sync ( 0 , doc . length ) ; debug ( "end: initial sync" ) ; } public remove ( ) { this . doc . removeListener ( "sequenceDelta" , this . onChange ) ; this . doc . removeListener ( "maintenance" , this . onChange ) ; Dom . removeAllChildren ( this . root ) ; } public sync ( start = 0 , end = this . doc . length ) { let _start = start ; let _end = end ; const doc = this . doc ; const length = doc . length ; console . time ( "Layout.sync()" ) ; const oldStart = _start ; const oldEnd = _end ; { const startEndInfo = ( this . rootFormatInfo . formatter as RootFormatter < IFormatterState > ) . prepare ( this , clamp ( 0 , _start , length ) , clamp ( _start , _end , length ) ) ; _start = startEndInfo . start ; _end = startEndInfo . end ; let checkpoint = this . initialCheckpoint ; while ( _start > 0 ) { const position = _start - 1 ; const { segment , offset } = doc . getSegmentAndOffset ( position ) ; const range = getSegmentRange ( position , segment , offset ) ; if ( range . end === _start ) { checkpoint = this . segmentToCheckpoint . get ( segment ) ; break ; } _start = range . start ; } if ( _start === 0 ) { checkpoint = this . initialCheckpoint ; } this . restoreCheckpoint ( checkpoint ) ; debug ( "Begin: sync([%d..%d)) -> [%d..%d) len: %d -> %d" , oldStart , oldEnd , _start , _end , oldEnd - oldStart , _end - _start ) ; this . emit ( "render" , { _start , _end } ) ; } try { doc . visitRange ( ( position , segment , startOffset , endOffset ) => { this . beginSegment ( position , segment , startOffset , endOffset ) ; while ( true ) { const index = this . formatStack . length - 1 ; const formatInfo = this . format ; const { formatter , state } = formatInfo ; const { consumed , state : newState } = formatter . visit ( this , state ) ; if ( newState !== state && this . formatStack [ index ] === formatInfo ) { assert . deepStrictEqual ( this . formatStack [ index ] . state , state ) ; assert . deepStrictEqual ( this . formatStack [ index ] . formatter , formatter ) ; this . formatStack [ index ] = Object . freeze ( { formatter , state : Object . freeze ( newState ) } ) ; } if ( consumed ) { return this . endSegment ( end ) ; } } } , start ) ; assert ( start === end || this . segmentEnd >= end ) ; } finally { if ( end >= length || this . segmentEnd >= length ) { debug ( "Begin EOT: %o@%d (length=%d)" , this . segment , this . segmentEnd , doc . length ) ; this . beginSegment ( length , eotSegment , 0 , 0 ) ; this . popFormat ( this . formatStack . length ) ; this . endSegment ( end ) ; debug ( "End EOT" ) ; } debug ( "End: sync([%d..%d)) -> [%d..%d) len: %d -> %d" , oldStart , oldEnd , start , this . position , oldEnd - oldStart , this . position - start ) ; this . _cursor = undefined ; this . _segment = undefined ; this . _position = NaN ; this . _endOffset = NaN ; this . _startOffset = NaN ; this . _segmentStart = NaN ; this . _segmentEnd = NaN ; console . timeEnd ( "Layout.sync()" ) ; } } public pushFormat < TState extends IFormatterState > ( formatter , init ) { const depth = this . formatStack . length ; const segment = this . segment ; debug ( "  pushFormat(%o,pos=%d,%s,start=%d,end=%d,depth=%d)" , formatter , this . position , segment . toString ( ) , this . startOffset , this . endOffset , depth ) ; assert . strictEqual ( segment . removedSeq , undefined ) ; const checkpoint = this . segmentToCheckpoint . get ( segment ) ; const stack = checkpoint && checkpoint . formatStack ; const candidate = stack && stack [ this . formatStack . length ] ; const prevOut = ( candidate && candidate . formatter === formatter ? candidate . state : undefined ) as TState ; const state = formatter . begin ( this , init , prevOut ) ; this . formatStack . push ( Object . freeze ( { formatter , state : Object . freeze ( state ) } ) ) ; } public popFormat ( count = 1 ) { let _count = count ; while ( _count -- > 0 ) { const { formatter , state } = this . formatStack . pop ( ) ; debug ( "  popFormat(%o@%d):" , formatter , this . position ) ; formatter . end ( this , state ) ; } } public pushTag < T extends { } > ( tag , props ? ) { const element = this . elementForTag ( tag ) ; if ( props ) { Object . assign ( element , props ) ; } this . pushNode ( element ) ; return element ; } public popTag ( count = 1 ) { this . popNode ( count ) ; } public emitTag < T extends { } > ( tag , props ? ) { const element = this . elementForTag ( tag ) ; if ( props ) { Object . assign ( element , props ) ; } this . emitNode ( element ) ; return element ; } public emitText ( text ) { let existing = this . next ; if ( ! ! existing && isTextNode ( existing ) && this . nodeToSegment ( existing ) === this . segment ) { this . removeNode ( existing ) ; } existing = document . createTextNode ( text ) ; this . emitNode ( existing ) ; return existing ; } public pushNode ( node ) { debug ( "    pushNode(%o@%d)" , node , this . position ) ; this . emitNode ( node ) ; { const cursor = this . _cursor ; cursor . parent = node ; cursor . previous = null ; } } public emitNode ( node ) { debug ( "    emitNode(%o@%d)" , node , this . position ) ; const top = this . _cursor ; const { parent , previous } = top ; if ( node . parentNode !== parent || node . previousSibling !== previous ) { Dom . insertAfter ( parent , node , previous ) ; } this . emitted . add ( node ) ; this . pending . delete ( node ) ; top . previous = node ; this . nodeToSegmentMap . set ( node , this . segment ) ; } public popNode ( count = 1 ) { let _count = count ; while ( _count -- > 0 ) { const cursor = this . _cursor ; const parent = cursor . parent ; debug ( "    popNode(%o@%d):" , parent , this . position ) ; cursor . previous = parent ; cursor . parent = parent . parentNode ; } assert ( this . root . contains ( this . cursor . parent ) ) ; } public nodeToSegment ( node ) { const seg = this . nodeToSegmentMap . get ( node ) ; return seg && ( seg . removedSeq === undefined ? seg : undefined ) ; } public segmentAndOffsetToNodeAndOffset ( segment , offset ) { const checkpoint = this . segmentToCheckpoint . get ( segment ) ; if ( ! checkpoint ) { return { node : null , nodeOffset : NaN } ; } const result = this . segmentAndOffsetToNodeAndOffsetHelper ( checkpoint . cursor , offset ) ; if ( result ) { debug ( "@%d %o:%d -> %o:%d" , segment === eotSegment ? this . doc . length : this . doc . getPosition ( segment ) + offset , segment , offset , result . node , result . nodeOffset ) ; return result ; } debug ( "@%d %o:%d -> null:NaN" , segment === eotSegment ? this . doc . length : this . doc . getPosition ( segment ) + offset , segment , offset ) ; return { node : null , nodeOffset : NaN } ; } private segmentAndOffsetToNodeAndOffsetHelper ( cursor , offset ) { let _offset = offset ; let { previous : node } = cursor ; if ( ! node ) { const { parent } = cursor ; return { node : parent , nodeOffset : 0 } ; } while ( node . nodeType !== Node . TEXT_NODE ) { const { childNodes } = node ; const { length } = childNodes ; if ( length === 0 ) { return { node , nodeOffset : 0 } ; } node = node . childNodes [ childNodes . length - 1 ] ; if ( node . nodeType === Node . TEXT_NODE ) { _offset = node . textContent . length ; } } { const { length } = node . textContent ; return { node , nodeOffset : _offset < length ? _offset : length } ; } } private elementForTag ( tag ) { const existing = this . next ; return ! ! existing && hasTagName ( existing , tag ) && this . nodeToSegment ( existing ) === this . segment ? existing : document . createElement ( tag ) ; } private beginSegment ( position , segment , startOffset , endOffset ) { assert . strictEqual ( this . pending . size , 0 ) ; this . _position = position ; this . _segment = segment ; this . _startOffset = startOffset ; this . _endOffset = endOffset ; ( { start : this . _segmentStart , end : this . _segmentEnd } = getSegmentRange ( position , segment , startOffset ) ) ; debug ( "beginSegment(%o@%d,+%d,-%d): [%d..%d)" , segment , this . position , this . startOffset , this . endOffset , this . segmentStart , this . segmentEnd ) ; this . emitted = this . pending ; this . pending = this . segmentToEmitted . get ( this . _segment ) || new Set ( ) ; this . segmentToEmitted . set ( this . _segment , this . emitted ) ; assert . strictEqual ( this . emitted . size , 0 ) ; assert . notStrictEqual ( this . emitted , this . pending ) ; } private removePending ( ) { for ( const node of this . pending ) { this . removeNode ( node ) ; } this . pending . clear ( ) ; } private endSegment ( lastInvalidated ) { this . removePending ( ) ; const previous = this . segmentToCheckpoint . get ( this . segment ) ; this . segmentToCheckpoint . set ( this . segment , new LayoutCheckpoint ( this . formatStack , this . cursor ) ) ; if ( ! previous || this . segmentEnd < lastInvalidated ) { return true ; } const shouldContinue = this . cursor . parent !== previous . cursor . parent || this . cursor . previous !== previous . cursor . previous ; assert ( shouldContinue || this . root . contains ( previous . cursor . parent ) ) ; return shouldContinue ; } private restoreCheckpoint ( checkpoint ) { const { formatStack , cursor } = checkpoint ; this . formatStack = formatStack . map ( ( formatInfo ) => ( { ... formatInfo } ) ) ; this . _cursor = { ... cursor } ; assert ( this . root . contains ( cursor . parent ) ) ; } private removeNode ( node ) { debug ( "        removed %o" , node ) ; this . nodeToSegmentMap . delete ( node ) ; if ( node . parentNode ) { node . parentNode . removeChild ( node ) ; } } private removeSegment ( segment ) { const emitted = this . segmentToEmitted . get ( segment ) ; if ( emitted ) { for ( const node of emitted ) { this . removeNode ( node ) ; } this . segmentToEmitted . delete ( segment ) ; } this . segmentToCheckpoint . delete ( segment ) ; } private readonly onChange = ( e ) => { debug ( "onChange(%o)" , e ) ; ( this . rootFormatInfo . formatter as RootFormatter < IFormatterState > ) . onChange ( this , e ) ; for ( const { segment } of e . ranges ) { if ( segment . removedSeq ) { this . removeSegment ( segment ) ; } } if ( e . deltaOperation === MergeTreeMaintenanceType . APPEND ) { this . removeSegment ( e . deltaArgs . deltaSegments [ 1 ] . segment ) ; } this . invalidate ( e . first . position , e . last . position + e . last . segment . cachedLength ) ; } ; private unionRef ( doc , position , ref , fn , limit ) { return fn ( position === undefined ? limit : position , ref === undefined ? limit : doc . localRefToPosition ( ref ) ) ; } private invalidate ( start , end ) { let _start = start ; let _end = end ; const doc = this . doc ; _start = this . unionRef ( doc , _start , this . startInvalid , Math . min , + Infinity ) ; _end = this . unionRef ( doc , _end , this . endInvalid , Math . max , - Infinity ) ; this . startInvalid = updateRef ( doc , this . startInvalid , _start ) ; this . endInvalid = updateRef ( doc , this . endInvalid , _end ) ; this . scheduleRender ( ) ; this . renderPromise = new Promise ( ( resolve ) => { this . renderResolver = resolve ; } ) ; } private render ( ) { const doc = this . doc ; const start = extractRef ( doc , this . startInvalid ) ; this . startInvalid = undefined ; const end = extractRef ( doc , this . endInvalid ) ; this . endInvalid = undefined ; this . sync ( start , end ) ; this . renderResolver ( ) ; } }