diff --git a/public/prosemirror.html b/public/prosemirror.html index 33250b371..aca1d553d 100644 --- a/public/prosemirror.html +++ b/public/prosemirror.html @@ -323,32 +323,34 @@

Yorkie.Tree

slice: { content, openStart, openEnd }, } = step; - // 02-1. Backspace: Level delete + // 01. Move: level up/down, move left/right. // TODO(hackerwins): replaceAround replaces the given range with given gap. if (stepType === 'replaceAround') { // root.tree.move(from, to, gapFrom, gapTo); continue; } - // 02-2. Enter Key: Insert Paragraph + // 02. Split: split the given range. if ( stepType === 'replace' && openStart && openEnd && structure ) { - // TODO(hackerwins): Figure out how many depth to split. - // root.tree.split(from, 2); + root.tree.edit(from, to, docToTreeNode(node.toJSON()), [ + openStart, + openEnd, + ]); continue; } - // 02-1. Delete the given range. + // 03. Edit: Delete the given range. if (!content.content.length) { root.tree.edit(from, to); continue; } - // 03-4. Edit: Insert the given content. + // 04. Edit: Replace the given range with the given content. for (const node of content.content) { root.tree.edit(from, to, docToTreeNode(node.toJSON())); } diff --git a/src/document/crdt/tree.ts b/src/document/crdt/tree.ts index ad8e7eb56..cd110d098 100644 --- a/src/document/crdt/tree.ts +++ b/src/document/crdt/tree.ts @@ -550,9 +550,8 @@ export class CRDTTree extends CRDTGCElement { /** * `findFloorNode` finds node of given id. */ - private findFloorNode(id: CRDTTreeNodeID) { + private findFloorNode(id: CRDTTreeNodeID): CRDTTreeNode | undefined { const entry = this.nodeMapByID.floorEntry(id); - if (!entry || !entry.key.getCreatedAt().equals(id.getCreatedAt())) { return; } @@ -561,66 +560,60 @@ export class CRDTTree extends CRDTGCElement { } /** - * `findNodesAndSplitText` finds `TreePos` of the given `CRDTTreeNodeID` and - * splits the text node if necessary. + * `findNodesAndSplit` finds `TreePos` of the given `CRDTTreeNodeID` and + * splits nodes for the given split level. * - * `CRDTTreeNodeID` is a position in the CRDT perspective. This is - * different from `TreePos` which is a position of the tree in the local - * perspective. + * The ids of the given `pos` are the ids of the node in the CRDT perspective. + * This is different from `TreePos` which is a position of the tree in the + * physical perspective. */ - public findNodesAndSplitText( + public findNodesAndSplit( pos: CRDTTreePos, editedAt: TimeTicket, + /* eslint-disable @typescript-eslint/no-unused-vars */ + splitLevel: number, ): [CRDTTreeNode, CRDTTreeNode] { - const treeNodes = this.toTreeNodes(pos); - - if (!treeNodes) { - throw new Error(`cannot find node at ${pos}`); - } - const [parentNode] = treeNodes; - let [, leftSiblingNode] = treeNodes; - - // Find the appropriate position. This logic is similar to the logical to - // handle the same position insertion of RGA. - - if (leftSiblingNode.isText) { - const absOffset = leftSiblingNode.id.getOffset(); - const split = leftSiblingNode.split( + // 01. Find the parent and left sibling node of the given position. + const [parent, leftSibling] = this.toTreeNodes(pos); + let leftNode = leftSibling; + + // 02. Split nodes for the given split level. + if (leftNode.isText) { + const absOffset = leftNode.id.getOffset(); + const split = leftNode.split( pos.getLeftSiblingID().getOffset() - absOffset, absOffset, ); if (split) { - split.insPrevID = leftSiblingNode.id; - this.nodeMapByID.put(split.id, split); - - if (leftSiblingNode.insNextID) { - const insNext = this.findFloorNode(leftSiblingNode.insNextID)!; - + split.insPrevID = leftNode.id; + if (leftNode.insNextID) { + const insNext = this.findFloorNode(leftNode.insNextID)!; insNext.insPrevID = split.id; - split.insNextID = leftSiblingNode.insNextID; + split.insNextID = leftNode.insNextID; } - leftSiblingNode.insNextID = split.id; + leftNode.insNextID = split.id; + + this.nodeMapByID.put(split.id, split); } } - const allChildren = parentNode.allChildren; - const index = - parentNode === leftSiblingNode - ? 0 - : allChildren.indexOf(leftSiblingNode) + 1; + // 03. Find the appropriate left node. If some nodes are inserted at the + // same position concurrently, then we need to find the appropriate left + // node. This is similar to RGA. + const allChildren = parent.allChildren; + const index = parent === leftNode ? 0 : allChildren.indexOf(leftNode) + 1; - for (let i = index; i < parentNode.allChildren.length; i++) { + for (let i = index; i < parent.allChildren.length; i++) { const next = allChildren[i]; - - if (next.id.getCreatedAt().after(editedAt)) { - leftSiblingNode = next; - } else { + if (!next.id.getCreatedAt().after(editedAt)) { break; } + + leftNode = next; } - return [parentNode, leftSiblingNode]; + return [parent, leftNode]; } /** @@ -631,13 +624,13 @@ export class CRDTTree extends CRDTGCElement { attributes: { [key: string]: string } | undefined, editedAt: TimeTicket, ) { - const [fromParent, fromLeft] = this.findNodesAndSplitText( + const [fromParent, fromLeft] = this.findNodesAndSplit( range[0], editedAt, + 0, ); - const [toParent, toLeft] = this.findNodesAndSplitText(range[1], editedAt); + const [toParent, toLeft] = this.findNodesAndSplit(range[1], editedAt, 0); const changes: Array = []; - changes.push({ type: TreeChangeType.Style, from: this.toIndex(fromParent, fromLeft), @@ -670,15 +663,21 @@ export class CRDTTree extends CRDTGCElement { public edit( range: [CRDTTreePos, CRDTTreePos], contents: Array | undefined, + splitLevels: [number, number], editedAt: TimeTicket, latestCreatedAtMapByActor?: Map, ): [Array, Map] { // 01. split text nodes at the given range if needed. - const [fromParent, fromLeft] = this.findNodesAndSplitText( + const [fromParent, fromLeft] = this.findNodesAndSplit( range[0], editedAt, + splitLevels[0], + ); + const [toParent, toLeft] = this.findNodesAndSplit( + range[1], + editedAt, + splitLevels[1], ); - const [toParent, toLeft] = this.findNodesAndSplitText(range[1], editedAt); // TODO(hackerwins): If concurrent deletion happens, we need to seperate the // range(from, to) into multiple ranges. @@ -803,7 +802,6 @@ export class CRDTTree extends CRDTGCElement { ): void { const fromIdx = this.toIndex(fromParent, fromLeft); const toIdx = this.toIndex(toParent, toLeft); - return this.indexTree.nodesBetween(fromIdx, toIdx, callback); } @@ -814,11 +812,12 @@ export class CRDTTree extends CRDTGCElement { public editT( range: [number, number], contents: Array | undefined, + splitLevels: [number, number], editedAt: TimeTicket, ): void { const fromPos = this.findPos(range[0]); const toPos = this.findPos(range[1]); - this.edit([fromPos, toPos], contents, editedAt); + this.edit([fromPos, toPos], contents, splitLevels, editedAt); } /** @@ -926,17 +925,9 @@ export class CRDTTree extends CRDTGCElement { */ public pathToPosRange(path: Array): [CRDTTreePos, CRDTTreePos] { const fromIdx = this.pathToIndex(path); - return [this.findPos(fromIdx), this.findPos(fromIdx + 1)]; } - /** - * `pathToTreePos` finds the tree position path. - */ - public pathToTreePos(path: Array): TreePos { - return this.indexTree.pathToTreePos(path); - } - /** * `pathToPos` finds the position of the given index in the tree by path. */ @@ -1007,8 +998,7 @@ export class CRDTTree extends CRDTGCElement { */ public deepcopy(): CRDTTree { const root = this.getRoot(); - const tree = new CRDTTree(root.deepcopy(), this.getCreatedAt()); - return tree; + return new CRDTTree(root.deepcopy(), this.getCreatedAt()); } /** @@ -1019,7 +1009,6 @@ export class CRDTTree extends CRDTGCElement { leftSiblingNode: CRDTTreeNode, ): Array { const treePos = this.toTreePos(parentNode, leftSiblingNode); - if (!treePos) { return []; } @@ -1035,7 +1024,6 @@ export class CRDTTree extends CRDTGCElement { leftSiblingNode: CRDTTreeNode, ): number { const treePos = this.toTreePos(parentNode, leftSiblingNode); - if (!treePos) { return -1; } @@ -1043,26 +1031,27 @@ export class CRDTTree extends CRDTGCElement { return this.indexTree.indexOf(treePos); } - private toTreeNodes(pos: CRDTTreePos) { + /** + * `toTreeNodes` converts the given pos to parent and left sibling nodes. + */ + private toTreeNodes(pos: CRDTTreePos): [CRDTTreeNode, CRDTTreeNode] { const parentID = pos.getParentID(); const leftSiblingID = pos.getLeftSiblingID(); const parentNode = this.findFloorNode(parentID); - let leftSiblingNode = this.findFloorNode(leftSiblingID); - - if (!parentNode || !leftSiblingNode) { - return []; + let leftNode = this.findFloorNode(leftSiblingID); + if (!parentNode || !leftNode) { + throw new Error(`cannot find node at ${pos}`); } if ( leftSiblingID.getOffset() > 0 && - leftSiblingID.getOffset() === leftSiblingNode.id.getOffset() && - leftSiblingNode.insPrevID + leftSiblingID.getOffset() === leftNode.id.getOffset() && + leftNode.insPrevID ) { - leftSiblingNode = - this.findFloorNode(leftSiblingNode.insPrevID) || leftSiblingNode; + leftNode = this.findFloorNode(leftNode.insPrevID) || leftNode; } - return [parentNode, leftSiblingNode!]; + return [parentNode, leftNode!]; } /** @@ -1076,8 +1065,6 @@ export class CRDTTree extends CRDTGCElement { return; } - let treePos; - if (parentNode.isRemoved) { let childNode: CRDTTreeNode; while (parentNode.isRemoved) { @@ -1085,40 +1072,36 @@ export class CRDTTree extends CRDTGCElement { parentNode = childNode.parent!; } - const childOffset = parentNode.findOffset(childNode!); + const offset = parentNode.findOffset(childNode!); + return { + node: parentNode, + offset, + }; + } - treePos = { + if (parentNode === leftSiblingNode) { + return { node: parentNode, - offset: childOffset, + offset: 0, }; - } else { - if (parentNode === leftSiblingNode) { - treePos = { - node: parentNode, - offset: 0, - }; - } else { - let offset = parentNode.findOffset(leftSiblingNode); - - if (!leftSiblingNode.isRemoved) { - if (leftSiblingNode.isText) { - return { - node: leftSiblingNode, - offset: leftSiblingNode.paddedSize, - }; - } else { - offset++; - } - } + } - treePos = { - node: parentNode, - offset, + let offset = parentNode.findOffset(leftSiblingNode); + if (!leftSiblingNode.isRemoved) { + if (leftSiblingNode.isText) { + return { + node: leftSiblingNode, + offset: leftSiblingNode.paddedSize, }; } + + offset++; } - return treePos; + return { + node: parentNode, + offset, + }; } /** @@ -1168,11 +1151,12 @@ export class CRDTTree extends CRDTGCElement { range: TreePosRange, timeTicket: TimeTicket, ): [Array, Array] { - const [fromParent, fromLeft] = this.findNodesAndSplitText( + const [fromParent, fromLeft] = this.findNodesAndSplit( range[0], timeTicket, + 0, ); - const [toParent, toLeft] = this.findNodesAndSplitText(range[1], timeTicket); + const [toParent, toLeft] = this.findNodesAndSplit(range[1], timeTicket, 0); return [this.toPath(fromParent, fromLeft), this.toPath(toParent, toLeft)]; } @@ -1184,11 +1168,12 @@ export class CRDTTree extends CRDTGCElement { range: TreePosRange, timeTicket: TimeTicket, ): [number, number] { - const [fromParent, fromLeft] = this.findNodesAndSplitText( + const [fromParent, fromLeft] = this.findNodesAndSplit( range[0], timeTicket, + 0, ); - const [toParent, toLeft] = this.findNodesAndSplitText(range[1], timeTicket); + const [toParent, toLeft] = this.findNodesAndSplit(range[1], timeTicket, 0); return [this.toIndex(fromParent, fromLeft), this.toIndex(toParent, toLeft)]; } diff --git a/src/document/json/tree.ts b/src/document/json/tree.ts index 2c7204f44..195111570 100644 --- a/src/document/json/tree.ts +++ b/src/document/json/tree.ts @@ -153,38 +153,37 @@ function createCRDTTreeNode(context: ChangeContext, content: TreeNode) { function validateTextNode(textNode: TextNode): boolean { if (!textNode.value.length) { throw new Error('text node cannot have empty value'); - } else { - return true; } + + return true; } /** * `validateTreeNodes` ensures that treeNodes consists of only one type. */ function validateTreeNodes(treeNodes: Array): boolean { - if (treeNodes.length) { - const firstTreeNodeType = treeNodes[0].type; - if (firstTreeNodeType === DefaultTextType) { - for (const treeNode of treeNodes) { - const { type } = treeNode; - if (type !== DefaultTextType) { - throw new Error( - 'element node and text node cannot be passed together', - ); - } - validateTextNode(treeNode as TextNode); + if (!treeNodes.length) { + return true; + } + + const firstTreeNodeType = treeNodes[0].type; + if (firstTreeNodeType === DefaultTextType) { + for (const treeNode of treeNodes) { + const { type } = treeNode; + if (type !== DefaultTextType) { + throw new Error('element node and text node cannot be passed together'); } - } else { - for (const treeNode of treeNodes) { - const { type } = treeNode; - if (type === DefaultTextType) { - throw new Error( - 'element node and text node cannot be passed together', - ); - } + validateTextNode(treeNode as TextNode); + } + } else { + for (const treeNode of treeNodes) { + const { type } = treeNode; + if (type === DefaultTextType) { + throw new Error('element node and text node cannot be passed together'); } } } + return true; } @@ -363,11 +362,13 @@ export class Tree { .filter((a) => a) as Array; } + // TODO(hackerwins): Implement splitLevels. const [, maxCreatedAtMapByActor] = this.tree!.edit( [fromPos, toPos], crdtNodes.length ? crdtNodes.map((crdtNode) => crdtNode?.deepcopy()) : undefined, + [0, 0], ticket, ); diff --git a/src/document/operation/tree_edit_operation.ts b/src/document/operation/tree_edit_operation.ts index 086faa955..c9e21f19c 100644 --- a/src/document/operation/tree_edit_operation.ts +++ b/src/document/operation/tree_edit_operation.ts @@ -85,9 +85,11 @@ export class TreeEditOperation extends Operation { logger.fatal(`fail to execute, only Tree can execute edit`); } const tree = parentObject as CRDTTree; + // TODO(hackerwins): Implement splitLevels. const [changes] = tree.edit( [this.fromPos, this.toPos], this.contents?.map((content) => content.deepcopy()), + [0, 0], this.getExecutedAt(), this.maxCreatedAtMapByActor, ); diff --git a/test/integration/tree_test.ts b/test/integration/tree_test.ts index caa2119be..e34db0f50 100644 --- a/test/integration/tree_test.ts +++ b/test/integration/tree_test.ts @@ -263,12 +263,12 @@ describe('Tree', () => { ); }); - const actualOperations: Array = []; + const actualOps: Array = []; doc.subscribe('$.t', (event) => { if (event.type === 'local-change') { const { operations } = event.value; - actualOperations.push( + actualOps.push( ...(operations.filter( (op) => op.type === 'tree-edit', ) as Array), @@ -289,7 +289,7 @@ describe('Tree', () => { }); assert.deepEqual( - actualOperations.map((it) => { + actualOps.map((it) => { return { type: it.type, fromPath: it.fromPath, diff --git a/test/unit/document/crdt/tree_test.ts b/test/unit/document/crdt/tree_test.ts index cd10c5535..8bd1a390c 100644 --- a/test/unit/document/crdt/tree_test.ts +++ b/test/unit/document/crdt/tree_test.ts @@ -30,6 +30,7 @@ import { CRDTTreeNodeID, CRDTTreePos, toXML, + TreeNodeForTest, } from '@yorkie-js-sdk/src/document/crdt/tree'; /** @@ -101,13 +102,18 @@ describe('CRDTTree.Edit', function () { // 1 //

- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); assert.equal(t.toXML(), /*html*/ `

`); assert.equal(t.getRoot().size, 2); // 1 //

h e l l o

- t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'hello')], timeT()); + t.editT( + [1, 1], + [new CRDTTreeNode(posT(), 'text', 'hello')], + [0, 0], + timeT(), + ); assert.equal(t.toXML(), /*html*/ `

hello

`); assert.equal(t.getRoot().size, 7); @@ -115,46 +121,43 @@ describe('CRDTTree.Edit', function () { //

h e l l o

w o r l d

const p = new CRDTTreeNode(posT(), 'p', []); p.insertAt(new CRDTTreeNode(posT(), 'text', 'world'), 0); - t.editT([7, 7], [p], timeT()); + t.editT([7, 7], [p], [0, 0], timeT()); assert.equal(t.toXML(), /*html*/ `

hello

world

`); assert.equal(t.getRoot().size, 14); // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 //

h e l l o !

w o r l d

- t.editT([6, 6], [new CRDTTreeNode(posT(), 'text', '!')], timeT()); + t.editT([6, 6], [new CRDTTreeNode(posT(), 'text', '!')], [0, 0], timeT()); assert.equal(t.toXML(), /*html*/ `

hello!

world

`); - assert.deepEqual( - JSON.stringify(t.toTestTreeNode()), - JSON.stringify({ - type: 'r', - children: [ - { - type: 'p', - children: [ - { type: 'text', value: 'hello', size: 5, isRemoved: false }, - { type: 'text', value: '!', size: 1, isRemoved: false }, - ], - size: 6, - isRemoved: false, - }, - { - type: 'p', - children: [ - { type: 'text', value: 'world', size: 5, isRemoved: false }, - ], - size: 5, - isRemoved: false, - }, - ], - size: 15, - isRemoved: false, - }), - ); + assert.deepEqual(t.toTestTreeNode(), { + type: 'r', + children: [ + { + type: 'p', + children: [ + { type: 'text', value: 'hello', size: 5, isRemoved: false }, + { type: 'text', value: '!', size: 1, isRemoved: false }, + ], + size: 6, + isRemoved: false, + } as TreeNodeForTest, + { + type: 'p', + children: [ + { type: 'text', value: 'world', size: 5, isRemoved: false }, + ], + size: 5, + isRemoved: false, + } as TreeNodeForTest, + ], + size: 15, + isRemoved: false, + }); // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 //

h e l l o ~ !

w o r l d

- t.editT([6, 6], [new CRDTTreeNode(posT(), 'text', '~')], timeT()); + t.editT([6, 6], [new CRDTTreeNode(posT(), 'text', '~')], [0, 0], timeT()); assert.equal(t.toXML(), /*html*/ `

hello~!

world

`); }); @@ -163,10 +166,20 @@ describe('CRDTTree.Edit', function () { // 0 1 2 3 4 5 6 7 8 //

a b

c d

const tree = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - tree.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - tree.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); - tree.editT([4, 4], [new CRDTTreeNode(posT(), 'p')], timeT()); - tree.editT([5, 5], [new CRDTTreeNode(posT(), 'text', 'cd')], timeT()); + tree.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + tree.editT( + [1, 1], + [new CRDTTreeNode(posT(), 'text', 'ab')], + [0, 0], + timeT(), + ); + tree.editT([4, 4], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + tree.editT( + [5, 5], + [new CRDTTreeNode(posT(), 'text', 'cd')], + [0, 0], + timeT(), + ); assert.deepEqual(tree.toXML(), /*html*/ `

ab

cd

`); let treeNode = tree.toTestTreeNode(); @@ -177,7 +190,7 @@ describe('CRDTTree.Edit', function () { // 02. delete b from first paragraph // 0 1 2 3 4 5 6 7 //

a

c d

- tree.editT([2, 3], undefined, timeT()); + tree.editT([2, 3], undefined, [0, 0], timeT()); assert.deepEqual(tree.toXML(), /*html*/ `

a

cd

`); treeNode = tree.toTestTreeNode(); @@ -194,142 +207,178 @@ describe('CRDTTree.Edit', function () { // 0 1 2 3 4 //

a b

- t.editT([0, 0], [pNode], timeT()); - t.editT([1, 1], [textNode], timeT()); + t.editT([0, 0], [pNode], [0, 0], timeT()); + t.editT([1, 1], [textNode], [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ab

`); // Find the closest index.TreePos when leftSiblingNode in crdt.TreePos is removed. // 0 1 2 //

- t.editT([1, 3], undefined, timeT()); + t.editT([1, 3], undefined, [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

`); - let [parent, left] = t.findNodesAndSplitText( + let [parent, left] = t.findNodesAndSplit( new CRDTTreePos(pNode.id, textNode.id), timeT(), + 0, ); assert.equal(t.toIndex(parent, left), 1); // Find the closest index.TreePos when parentNode in crdt.TreePos is removed. // 0 // - t.editT([0, 2], undefined, timeT()); + t.editT([0, 2], undefined, [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ ``); - [parent, left] = t.findNodesAndSplitText( + [parent, left] = t.findNodesAndSplit( new CRDTTreePos(pNode.id, textNode.id), timeT(), + 0, ); assert.equal(t.toIndex(parent, left), 0); }); }); -describe.skip('CRDTTree.Split', function () { +describe('CRDTTree.Split', function () { it('Can split text nodes', function () { // 00. Create a tree with 2 paragraphs. // 0 1 6 11 //

hello world

const t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'helloworld')], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT( + [1, 1], + [new CRDTTreeNode(posT(), 'text', 'helloworld')], + [0, 0], + timeT(), + ); + const expectedIntial = { + type: 'root', + children: [ + { + type: 'p', + children: [ + { type: 'text', value: 'helloworld', size: 10, isRemoved: false }, + ], + size: 10, + isRemoved: false, + } as TreeNodeForTest, + ], + size: 12, + isRemoved: false, + }; + assert.deepEqual(t.toTestTreeNode(), expectedIntial); // 01. Split left side of 'helloworld'. - // tree.split(1, 1); - // TODO(JOOHOJANG): make new helper function when implement Tree.split - //betweenEqual(tree, 1, 11, ['text.helloworld']); + t.editT([1, 1], undefined, [0, 0], timeT()); + assert.deepEqual(t.toTestTreeNode(), expectedIntial); // 02. Split right side of 'helloworld'. - // tree.split(11, 1); - // TODO(JOOHOJANG): make new helper function when implement Tree.split - //betweenEqual(tree, 1, 11, ['text.helloworld']); + t.editT([11, 11], undefined, [0, 0], timeT()); + assert.deepEqual(t.toTestTreeNode(), expectedIntial); // 03. Split 'helloworld' into 'hello' and 'world'. - // tree.split(6, 1); - // TODO(JOOHOJANG): make new helper function when implement Tree.split - //betweenEqual(tree, 1, 11, ['text.hello', 'text.world']); + t.editT([6, 6], undefined, [0, 0], timeT()); + assert.deepEqual(t.toTestTreeNode(), { + type: 'root', + children: [ + { + type: 'p', + children: [ + { type: 'text', value: 'hello', size: 5, isRemoved: false }, + { type: 'text', value: 'world', size: 5, isRemoved: false }, + ], + size: 10, + isRemoved: false, + } as TreeNodeForTest, + ], + size: 12, + isRemoved: false, + }); }); - it('Can split element nodes', function () { + it.skip('Can split element nodes level 1', function () { + // 0 1 2 3 4 + //

a b

+ // 01. Split position 1. let t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ab

`); - // tree.split(1, 2); + t.editT([1, 1], undefined, [1, 1], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ab

`); assert.equal(t.getSize(), 6); // 02. Split position 2. - // 0 1 2 3 4 - //

a b

t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ab

`); - // tree.split(2, 2); + t.editT([2, 2], undefined, [1, 1], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

a

b

`); assert.equal(t.getSize(), 6); // 03. Split position 3. t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ab

`); - // tree.split(3, 2); + t.editT([3, 3], undefined, [1, 1], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ab

`); assert.equal(t.getSize(), 6); + }); - // 04. Split position 3. - t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); - t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'cd')], timeT()); - assert.deepEqual(t.toXML(), /*html*/ `

abcd

`); - // tree.split(3, 2); - assert.deepEqual(t.toXML(), /*html*/ `

ab

cd

`); - assert.equal(t.getSize(), 8); + it.skip('Can split element nodes multi-level', function () { + // 0 1 2 3 4 5 6 + //

a b

- // 05. Split multiple nodes level 1. - t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT()); - t.editT([2, 2], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); - assert.deepEqual(t.toXML(), /*html*/ `

ab

`); - // tree.split(3, 1); + // 01. Split nodes level 1. + let t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT()); + t.editT([2, 2], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ab

`); - assert.equal(t.getSize(), 6); + t.editT([3, 3], undefined, [1, 1], timeT()); + assert.deepEqual( + t.toXML(), + /*html*/ `

ab

`, + ); - // Split multiple nodes level 2. + // 02. Split nodes level 2. t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT()); - t.editT([2, 2], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT()); + t.editT([2, 2], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ab

`); - // tree.split(3, 2); + t.editT([3, 3], undefined, [2, 2], timeT()); assert.deepEqual( t.toXML(), - /*html*/ `

ab

`, + /*html*/ `

a

b

`, ); - assert.equal(t.getSize(), 8); - // Split multiple nodes level 3. + // Split multiple nodes level 3. But, it is allowed to split only level 2. t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT()); - t.editT([2, 2], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT()); + t.editT([2, 2], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ab

`); - // tree.split(3, 3); + t.editT([3, 3], undefined, [3, 3], timeT()); assert.deepEqual( t.toXML(), /*html*/ `

a

b

`, ); - assert.equal(t.getSize(), 10); }); - it('Can split and merge element nodes by edit', function () { + it.skip('Can split and merge element nodes by edit', function () { const t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'abcd')], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT( + [1, 1], + [new CRDTTreeNode(posT(), 'text', 'abcd')], + [0, 0], + timeT(), + ); assert.deepEqual(t.toXML(), /*html*/ `

abcd

`); assert.equal(t.getSize(), 6); @@ -339,7 +388,7 @@ describe.skip('CRDTTree.Split', function () { assert.deepEqual(t.toXML(), /*html*/ `

ab

cd

`); assert.equal(t.getSize(), 8); - t.editT([3, 5], undefined, timeT()); + t.editT([3, 5], undefined, [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

abcd

`); assert.equal(t.getSize(), 6); }); @@ -351,16 +400,16 @@ describe('CRDTTree.Merge', function () { // 0 1 2 3 4 5 6 7 8 //

a b

c d

const t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); - t.editT([4, 4], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([5, 5], [new CRDTTreeNode(posT(), 'text', 'cd')], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT()); + t.editT([4, 4], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([5, 5], [new CRDTTreeNode(posT(), 'text', 'cd')], [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ab

cd

`); // 02. delete b, c and the second paragraph. // 0 1 2 3 4 //

a d

- t.editT([2, 6], undefined, timeT()); + t.editT([2, 6], undefined, [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ad

`); const node = t.toTestTreeNode(); @@ -370,7 +419,7 @@ describe('CRDTTree.Merge', function () { assert.equal(node.children![0].children![1].size, 1); // d // 03. insert a new text node at the start of the first paragraph. - t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', '@')], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', '@')], [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

@ad

`); }); @@ -379,11 +428,11 @@ describe('CRDTTree.Merge', function () { // 0 1 2 3 4 5 6 7 8 9 10 //

a b

c d

const t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT()); - t.editT([2, 2], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); - t.editT([6, 6], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([7, 7], [new CRDTTreeNode(posT(), 'text', 'cd')], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT()); + t.editT([2, 2], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT()); + t.editT([6, 6], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([7, 7], [new CRDTTreeNode(posT(), 'text', 'cd')], [0, 0], timeT()); assert.deepEqual( t.toXML(), /*html*/ `

ab

cd

`, @@ -392,105 +441,111 @@ describe('CRDTTree.Merge', function () { // 02. delete b, c and second paragraph. // 0 1 2 3 4 5 //

a d - t.editT([3, 8], undefined, timeT()); + t.editT([3, 8], undefined, [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ad

`); }); it.skip('Can merge different levels with edit', function () { + // TODO(hackerwins): Fix this test. // 01. edit between two element nodes in the same hierarchy. // 0 1 2 3 4 5 6 7 8 //

a b

let t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT()); - t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], timeT()); - t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT()); + t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], [0, 0], timeT()); + t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT()); assert.deepEqual( t.toXML(), /*html*/ `

ab

`, ); - t.editT([5, 6], undefined, timeT()); + t.editT([5, 6], undefined, [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ab

`); // 02. edit between two element nodes in same hierarchy. t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT()); - t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], timeT()); - t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT()); + t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], [0, 0], timeT()); + t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT()); assert.deepEqual( t.toXML(), /*html*/ `

ab

`, ); - t.editT([6, 7], undefined, timeT()); + t.editT([6, 7], undefined, [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ab

`); // 03. edit between text and element node in same hierarchy. t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT()); - t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], timeT()); - t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT()); + t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], [0, 0], timeT()); + t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT()); assert.deepEqual( t.toXML(), /*html*/ `

ab

`, ); - t.editT([4, 6], undefined, timeT()); + t.editT([4, 6], undefined, [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

a

`); // 04. edit between text and element node in same hierarchy. t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT()); - t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], timeT()); - t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT()); + t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], [0, 0], timeT()); + t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT()); assert.deepEqual( t.toXML(), /*html*/ `

ab

`, ); - t.editT([5, 7], undefined, timeT()); + t.editT([5, 7], undefined, [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ab

`); // 05. edit between text and element node in same hierarchy. t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT()); - t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], timeT()); - t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT()); + t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], [0, 0], timeT()); + t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT()); assert.deepEqual( t.toXML(), /*html*/ `

ab

`, ); - t.editT([4, 7], undefined, timeT()); + t.editT([4, 7], undefined, [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

a

`); // 06. edit between text and element node in same hierarchy. t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], timeT()); - t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], timeT()); - t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT()); + t.editT([2, 2], [new CRDTTreeNode(posT(), 'i')], [0, 0], timeT()); + t.editT([3, 3], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT()); assert.deepEqual( t.toXML(), /*html*/ `

ab

`, ); - t.editT([3, 7], undefined, timeT()); + t.editT([3, 7], undefined, [0, 0], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

`); // 07. edit between text and element node in same hierarchy. t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); - t.editT([4, 4], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([5, 5], [new CRDTTreeNode(posT(), 'b')], timeT()); - t.editT([6, 6], [new CRDTTreeNode(posT(), 'text', 'cd')], timeT()); - t.editT([10, 10], [new CRDTTreeNode(posT(), 'p')], timeT()); - t.editT([11, 11], [new CRDTTreeNode(posT(), 'text', 'ef')], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT()); + t.editT([4, 4], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([5, 5], [new CRDTTreeNode(posT(), 'b')], [0, 0], timeT()); + t.editT([6, 6], [new CRDTTreeNode(posT(), 'text', 'cd')], [0, 0], timeT()); + t.editT([10, 10], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT( + [11, 11], + [new CRDTTreeNode(posT(), 'text', 'ef')], + [0, 0], + timeT(), + ); assert.deepEqual( t.toXML(), /*html*/ `

ab

cd

ef

`, ); - t.editT([9, 10], undefined, timeT()); + t.editT([9, 10], undefined, [0, 0], timeT()); assert.deepEqual( t.toXML(), /*html*/ `

ab

cd

ef

`,