import { ArrayExt , StringExt } from '@lumino/algorithm' ; import { Field } from './field' ; import { createTriplexIds } from './utilities' ; /**
 * A field which represents collaborative text.
 */ export class TextField extends Field < TextField . Value , TextField . Update , TextField . Metadata , TextField . Change , TextField . Patch > { param options constructor ( options = { } ) { super ( options ) ; } /**
     * The discriminated type of the field.
     */ get type ( ) : 'text' { return 'text' ; } returns createValue ( ) { return '' ; } returns createMetadata ( ) { return { ids : [ ] , cemetery : { } } ; } param args returns applyUpdate ( args ) { let { previous , update , metadata , version , storeId } = args ; let value = previous ; let change = [ ] ; let patch = [ ] ; if ( Private . isSplice ( update ) ) { update = [ update ] ; } for ( let splice of update ) { let obj = Private . applySplice ( value , splice , metadata , version , storeId ) ; change . push ( obj . change ) ; patch . push ( obj . patch ) ; value = obj . value ; } return { value , change , patch } ; } param args returns applyPatch ( args ) { let { previous , patch , metadata } = args ; let value = previous ; let change = [ ] ; for ( let part of patch ) { let obj = Private . applyPatch ( value , part , metadata ) ; change . push ( ... obj . change ) ; value = obj . value ; } return { value , change } ; } param args returns unapplyPatch ( args ) { let { previous , patch , metadata } = args ; let value = previous ; let change = [ ] ; for ( let part of patch ) { let reversed = { removedIds : part . insertedIds , insertedIds : part . removedIds , removedText : part . insertedText , insertedText : part . removedText } ; let obj = Private . applyPatch ( value , reversed , metadata ) ; change . push ( ... obj . change ) ; value = obj . value ; } return { value , change } ; } param first param second returns mergeChange ( first , second ) { return [ ... first , ... second ] ; } param first param second returns mergePatch ( first , second ) { return [ ... first , ... second ] ; } } /**
 * The namespace for the `TextField` class statics.
 */ export namespace TextField { /**
     * An options object for initializing a text field.
     */ export interface IOptions extends Field . IOptions { } /**
     * A type alias for the value type of a text field.
     */ export type Value = string ; /**
     * A type alias for a text field splice.
     */ export type Splice = { /**
         * The index of the splice.
         */ readonly index : number ; /**
         * The number of characters to remove.
         */ readonly remove : number ; /**
         * The text to insert.
         */ readonly text : string ; } ; /**
     * A type alias for the text field update type.
     */ export type Update = Splice | ReadonlyArray < Splice > ; /**
     * A type alias for the text field metadata type.
     */ export type Metadata = { /**
         * An array of ids corresponding to the text characters.
         */ readonly ids : Array < string > ; /**
         * The cemetery for concurrently deleted characters.
         */ readonly cemetery : { [ id ] : number ; } ; } ; /**
     * A type alias for a text field change part.
     */ export type ChangePart = { /**
         * The index of the modification.
         */ readonly index : number ; /**
         * The text that was removed.
         */ readonly removed : string ; /**
         * The text that was inserted.
         */ readonly inserted : string ; } ; /**
     * A type alias for the text field change type.
     */ export type Change = ReadonlyArray < ChangePart > ; /**
     * A type alias for the text field patch part.
     */ export type PatchPart = { /**
         * The ids that were removed.
         */ readonly removedIds : ReadonlyArray < string > ; /**
         * The text that was removed.
         */ readonly removedText : string ; /**
         * The ids that were inserted.
         */ readonly insertedIds : ReadonlyArray < string > ; /**
         * The text that was inserted.
         */ readonly insertedText : string ; } ; /**
     * A type alias for the text field patch type.
     */ export type Patch = ReadonlyArray < PatchPart > ; } /**
 * The namespace for the module implementation details.
 */ namespace Private { /**
     * A type-guard function for a text field update type.
     */ export function isSplice ( value ) { return ! Array . isArray ( value ) ; } /**
     * A type alias for the result of a splice operation.
     */ export type SpliceResult = { /**
         * The user-facing change part for the splice.
         */ readonly change : TextField . ChangePart ; /**
         * The system-facing patch part for the splice.
         */ readonly patch : TextField . PatchPart ; /**
         * The new value of the text.
         */ readonly value : string ; } ; param value param splice param metadata param version param storeId returns export function applySplice ( value , splice , metadata , version , storeId ) { let { index , remove , text } = splice ; if ( index < 0 ) { index = Math . max ( 0 , index + value . length ) ; } else { index = Math . min ( index , value . length ) ; } let count = Math . min ( remove , value . length - index ) ; let lower = index === 0 ? '' : metadata . ids [ index - 1 ] ; let upper = index === value . length ? '' : metadata . ids [ index ] ; let ids = createTriplexIds ( text . length , version , storeId , lower , upper ) ; let removedIds = spliceArray ( metadata . ids , index , count , ids ) ; let removedText = value . slice ( index , index + count ) ; let change = { index , removed : removedText , inserted : text } ; let patch = { removedIds , removedText , insertedIds : ids , insertedText : text } ; value = value . slice ( 0 , index ) + text + value . slice ( index + count ) ; return { change , patch , value } ; } /**
     * A type alias for the result of a patch operation.
     */ export type PatchResult = { /**
         * The user-facing change for the patch.
         */ readonly change : TextField . Change ; /**
         * The new value of the text.
         */ readonly value : string ; } ; param value param patch param metadata returns export function applyPatch ( value , patch , metadata ) { let { removedIds , insertedIds , insertedText } = patch ; let change = [ ] ; if ( removedIds . length > 0 ) { let chunks = findRemovedChunks ( removedIds , metadata ) ; while ( chunks . length > 0 ) { let { index , count } = chunks . pop ( ) ! ; metadata . ids . splice ( index , count ) ; let removed = value . slice ( index , index + count ) ; value = value . slice ( 0 , index ) + value . slice ( index + count ) ; change . push ( { index , removed , inserted : '' } ) ; } } if ( insertedIds . length > 0 ) { let chunks = findInsertedChunks ( insertedIds , insertedText , metadata ) ; while ( chunks . length > 0 ) { let { index , ids , text } = chunks . pop ( ) ! ; spliceArray ( metadata . ids , index , 0 , ids ) ; value = value . slice ( 0 , index ) + text + value . slice ( index ) ; change . push ( { index , removed : '' , inserted : text } ) ; } } return { change , value } ; } /**
     * A type alias for a remove chunk.
     */ type RemoveChunk = { index : number ; count : number ; } ; param ids param metadata returns function findRemovedChunks ( ids , metadata ) { let chunks = [ ] ; let i = 0 ; let n = ids . length ; while ( i < n ) { let j = ArrayExt . lowerBound ( metadata . ids , ids [ i ] , StringExt . cmp ) ; if ( j === metadata . ids . length || metadata . ids [ j ] !== ids [ i ] ) { let count = metadata . cemetery [ ids [ i ] ] || 0 ; metadata . cemetery [ ids [ i ] ] = count + 1 ; i ++ ; continue ; } let index = j ; let count = 0 ; while ( i < n && StringExt . cmp ( ids [ i ] , metadata . ids [ j ] ) === 0 ) { count ++ ; i ++ ; j ++ ; } if ( count > 0 ) { chunks . push ( { index , count } ) ; } else { i ++ ; } } return chunks ; } /**
     * A type alias for an insert chunk.
     */ type InsertChunk = { index : number ; ids : string [ ] ; text : string ; } ; param ids param text param metadata returns function findInsertedChunks ( ids , text , metadata ) { let indices = [ ] ; let insertIds = [ ] ; let insertText = '' ; for ( let i = 0 ; i < ids . length ; i ++ ) { if ( checkCemeteryForInsert ( ids [ i ] , metadata . cemetery ) ) { continue ; } insertIds . push ( ids [ i ] ) ; indices . push ( ArrayExt . lowerBound ( metadata . ids , ids [ i ] , StringExt . cmp ) ) ; insertText += text [ i ] ; } return chunkifyInsertions ( insertIds , insertText , indices ) ; } param ids param text param indices returns function chunkifyInsertions ( ids , text , indices ) { let chunks = [ ] ; let insertIndex ; let i = 0 ; while ( i < ids . length ) { let chunkIds = [ ] ; let chunkText = '' ; insertIndex = indices [ i ] ; while ( indices [ i ] === insertIndex && i < ids . length ) { chunkIds . push ( ids [ i ] ) ; chunkText += text [ i ] ; i ++ ; } if ( chunkText ) { chunks . push ( { index : insertIndex , ids : chunkIds , text : chunkText } ) ; } } return chunks ; } param id param cemetery returns function checkCemeteryForInsert ( id , cemetery ) { let count = cemetery [ id ] || 0 ; if ( count === 1 ) { delete cemetery [ id ] ; return true ; } if ( count > 1 ) { cemetery [ id ] = count - 1 ; return true ; } return false ; } param arr param start param deleteCount param items returns function spliceArray < T > ( arr , start , deleteCount ? , items ? ) { if ( ! items ) { return arr . splice ( start , deleteCount ) ; } let size = 100000 ; if ( items . length < size ) { return arr . splice ( start , deleteCount || 0 , ... items ) ; } let deleted = arr . splice ( start , deleteCount ) ; let n = Math . floor ( items . length / size ) ; let idx = 0 ; for ( let i = 0 ; i < n ; i ++ , idx += size ) { arr . splice ( idx , 0 , ... items . slice ( idx , size ) ) ; } arr . splice ( idx , 0 , ... items . slice ( idx ) ) ; return deleted ; } }