From 2c3f24e74c7f369510b9494c7c2713bcb44505ee Mon Sep 17 00:00:00 2001 From: Youngteac Hong Date: Tue, 7 Nov 2023 21:18:09 +0900 Subject: [PATCH 1/4] Revise prosemirror example --- public/prosemirror.css | 137 ++++++++------------------------ public/prosemirror.html | 172 +++++++++++----------------------------- 2 files changed, 79 insertions(+), 230 deletions(-) diff --git a/public/prosemirror.css b/public/prosemirror.css index fc6a6a005..2a5e71659 100644 --- a/public/prosemirror.css +++ b/public/prosemirror.css @@ -8,9 +8,6 @@ body { .ProseMirror { position: relative; -} - -.ProseMirror { word-wrap: break-word; white-space: pre-wrap; white-space: break-spaces; @@ -234,6 +231,26 @@ img.ProseMirror-separator { animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite; } +.username-layer::before { + content: ''; + position: absolute; + width: 2px; + left: -1px; + background-color: currentColor; + bottom: -100%; + height: 100%; +} + +.username-layer.first-top::before { + content: ''; + position: absolute; + width: 2px; + left: -1px; + background-color: currentColor; + top: -100%; + height: 100%; +} + @keyframes ProseMirror-cursor-blink { to { visibility: hidden; @@ -404,52 +421,28 @@ img.ProseMirror-separator { .layout .application > * { flex: 1 1 auto; min-width: 0; - /* width: 50%; */ - border: 1px solid black; + border: 1px solid silver; padding: 10px; box-sizing: border-box; } .layout .application > .editor-area { - display: flex; gap: 20px; position: relative; } -.sub-log { - position: absolute; - bottom: 0px; - right: 0px; - left: 0px; - height: 100px; - background-color: rgb(255, 255, 198); -} - .layout .application > .editor-area > * { - flex: 1 1 auto; min-width: 0; - /* width: 50%; */ - /* border: 1px solid black; */ -} - -.layout .application > .editor-area > *:not(.sub-log):last-child { - width: 40%; - flex: none; } #app { - border: 1px solid silver; + border: 1px solid #c0c0c0; } .layout .log { - flex: none; + display: flex; height: 50%; overflow: auto; - border-top: 1px solid silver; - box-sizing: border-box; - padding: 0 10px; - display: flex; - gap: 10px; } .layout .log > * { @@ -501,25 +494,7 @@ img.ProseMirror-separator { font-size: 0.8em; } -#tr-log { - display: block; - align-items: center; - gap: 1px; - flex-wrap: wrap; -} - -#tr-log > * { - margin-right: 2px; - margin-bottom: 2px; -} - -.pm { - flex: none !important; - min-width: 300px; -} - .tr { - padding: 10px; flex: none !important; width: 300px; } @@ -535,78 +510,34 @@ img.ProseMirror-separator { flex: 1 1 auto; } -.tr-container > *:last-child { - flex: none; +.tr-container > *:first-child { height: 300px; - background-color: yellow; + background-color: #ddd; + padding: 10px; overflow: auto; } -.selection-item { - display: inline-flex; - align-items: center; - background-color: red; - color: white; - line-height: 0px; - padding: 4px 4px; - border-radius: 3px; - box-sizing: border-box; - width: 16px; - height: 16px; - vertical-align: middle; - align-items: center; - justify-content: center; - cursor: pointer; -} - .tr-item { line-height: 1; display: inline-block; - padding: 4px 4px; border-radius: 3px; box-sizing: border-box; width: 16px; height: 16px; text-align: center; - background-color: #444; - color: white; + color: #444; + border: 1px solid #444; vertical-align: middle; cursor: pointer; } -.data-view { - display: flex; - flex-direction: column; -} - -.tree-view { - flex: none; - height: 300px; - overflow: auto; - display: flex; -} - -.yorkie { - min-width: 300px; -} - -.list-view { - flex: 1 1 auto; -} - -.log-list-view { +.data-container { display: flex; - flex-wrap: wrap; -} - -.log-list-view .block { - border: 1px solid black; + flex-direction: row; } -.log-list-view .inline::before { - content: attr(data-id); - margin-right: 4px; - /* background-color: rgba(0, 0, 0, 0.2); */ - font-size: 0.8em; - /* color: white; */ +.data-item { + flex-grow: 1; + padding: 10px; + border-left: 1px solid silver; } diff --git a/public/prosemirror.html b/public/prosemirror.html index a8fcf8432..33250b371 100644 --- a/public/prosemirror.html +++ b/public/prosemirror.html @@ -2,69 +2,37 @@ - Prosemirror Example + ProseMirror Example - -
-
-
-

Main

-
-
-
-
-

ProseMirror Doc

-
-
-
+

ProseMirror

+
-
-

Transaction

-
-
-
-
-
-
-
-
-

Yorkie.IndexTree

-
-
-
-

Yorkie.RGASplit

-
+
+
+

ProseMirror.Transaction

+
+
+
+ +
+

ProseMirror.Doc

+
+
+
+

Yorkie.Tree

+
+
@@ -334,7 +302,7 @@

Yorkie.RGASplit

]), }); }); - printLog(); + paintData(); return; } @@ -358,7 +326,7 @@

Yorkie.RGASplit

// 02-1. Backspace: Level delete // TODO(hackerwins): replaceAround replaces the given range with given gap. if (stepType === 'replaceAround') { - root.tree.move(from, to, gapFrom, gapTo); + // root.tree.move(from, to, gapFrom, gapTo); continue; } @@ -370,7 +338,7 @@

Yorkie.RGASplit

structure ) { // TODO(hackerwins): Figure out how many depth to split. - root.tree.split(from, 2); + // root.tree.split(from, 2); continue; } @@ -393,7 +361,7 @@

Yorkie.RGASplit

]), }); }); - printLog(); + paintData(); }, }); @@ -443,30 +411,30 @@

Yorkie.RGASplit

const newState = view.state.apply(transform); view.updateState(newState); } - printLog(); + paintData(); }); window.view = view; view.focus(); - printLog(); + paintData(); } document.getElementById('tr-log').onclick = (e) => { const index = e.target.getAttribute('data-index'); if (index) { - printTransaction(+index); + paintTransaction(+index); } }; - function printTransaction(index) { - const t = transactions[index || 0]; + function paintTransaction(index) { + const transaction = transactions[index || 0]; - if (t) { - if (t.type === 'selection') { + if (transaction) { + if (transaction.type === 'selection') { document.getElementById( 'tr-info', - ).innerHTML = `
selection\n${JSON.stringify(
-              t.selection.toJSON(),
+            ).innerHTML = `
selection: ${JSON.stringify(
+              transaction.selection.toJSON(),
               null,
               2,
             )}
`; @@ -474,32 +442,32 @@

Yorkie.RGASplit

document.getElementById( 'tr-info', ).innerHTML = `
selection: ${JSON.stringify(
-              t.transaction.curSelection.toJSON(),
+              transaction.transaction.curSelection.toJSON(),
+              null,
+              2,
+            )},\nsteps: ${JSON.stringify(
+              transaction.transaction.steps,
               null,
               2,
-            )}, steps: ${JSON.stringify(t.transaction.steps, null, 2)}
+            )}
           
`; } } } - function printLog() { - // how to show transaction view + function paintData() { document.getElementById('tr-log').innerHTML = transactions .slice(0, 100) .map((transaction, index) => { - if (transaction.type === 'selection') { - return `.`; - } - - return `T`; + return transaction.type === 'selection' + ? '' + : `T`; }) .join(''); - printTransaction(); - printDoc(doc.getRoot().tree.tree.getRoot(), 'yorkie-log'); - printDoc(view.state.doc, 'pm-log'); - printDocList(doc.getRoot().tree.tree, 'yorkie-list-log'); + paintTree(view.state.doc, 'pm-log'); + paintTree(doc.getRoot().tree.tree.getRoot(), 'yorkie-log'); + paintTransaction(); } function buildNodes(node, depth, nodes, index) { @@ -526,57 +494,7 @@

Yorkie.RGASplit

} } - function printDocList(doc, id) { - const head = doc.dummyHead; - - const arr = []; - let node = head; - while (node) { - const nodeType = node.type; - const pos = `${node.pos.createdAt.toTestString()}-${node.pos.offset}`; - if (nodeType === 'text') { - arr.push({ - type: nodeType, - value: node.value, - pos, - removedAt: node.removedAt, - }); - } else { - arr.push({ - type: nodeType, - size: node.size, - pos, - removedAt: node.removedAt, - }); - } - - node = node.next; - } - - const html = arr - .map((node) => { - const className = node.removedAt ? 'removed' : ''; - - if (node.type === 'text') { - return `
${ - node.value === ' ' ? '  ' : node.value - }
`; - } - return `
${ - node.type - }${ - node.size ? `(${node.size})` : '' - }
`; - }) - .join(''); - - document.getElementById(id).innerHTML = html; - } - - /** - * `printDoc` prints the content of the yorkie.Text. - */ - function printDoc(doc, id) { + function paintTree(doc, id) { const nodes = []; buildNodes(doc, 0, nodes); From db5634dc077bcd13d810d42ef07df589ad7928ca Mon Sep 17 00:00:00 2001 From: Youngteac Hong Date: Wed, 8 Nov 2023 12:04:55 +0900 Subject: [PATCH 2/4] Shorten tree interfaces --- src/document/crdt/tree.ts | 13 +- src/document/json/tree.ts | 12 - test/unit/document/crdt/tree_test.ts | 538 ++++++++++----------------- 3 files changed, 202 insertions(+), 361 deletions(-) diff --git a/src/document/crdt/tree.ts b/src/document/crdt/tree.ts index d456908ab..ad8e7eb56 100644 --- a/src/document/crdt/tree.ts +++ b/src/document/crdt/tree.ts @@ -808,10 +808,10 @@ export class CRDTTree extends CRDTGCElement { } /** - * `editByIndex` edits the given range with the given value. + * `editT` edits the given range with the given value. * This method uses indexes instead of a pair of TreePos for testing. */ - public editByIndex( + public editT( range: [number, number], contents: Array | undefined, editedAt: TimeTicket, @@ -821,15 +821,6 @@ export class CRDTTree extends CRDTGCElement { this.edit([fromPos, toPos], contents, editedAt); } - /** - * `split` splits the node at the given index. - */ - public split(index: number, depth = 1): TreePos { - // TODO(hackerwins, easylogic): Implement this with keeping references in the list. - // return this.treeByIndex.split(index, depth); - throw new Error(`not implemented, ${index} ${depth}`); - } - /** * `move` move the given source range to the given target range. */ diff --git a/src/document/json/tree.ts b/src/document/json/tree.ts index b145d3d75..2c7204f44 100644 --- a/src/document/json/tree.ts +++ b/src/document/json/tree.ts @@ -434,18 +434,6 @@ export class Tree { return this.editInternal(fromPos, toPos, contents); } - /** - * `split` splits this tree at the given index. - */ - public split(index: number, depth: number): boolean { - if (!this.context || !this.tree) { - throw new Error('it is not initialized yet'); - } - - this.tree.split(index, depth); - return true; - } - /** * `toXML` returns the XML string of this tree. */ diff --git a/test/unit/document/crdt/tree_test.ts b/test/unit/document/crdt/tree_test.ts index 3b506b9e4..cd10c5535 100644 --- a/test/unit/document/crdt/tree_test.ts +++ b/test/unit/document/crdt/tree_test.ts @@ -33,9 +33,9 @@ import { } from '@yorkie-js-sdk/src/document/crdt/tree'; /** - * `DTP` is a dummy CRDTTreeNodeID for testing. + * `idT` is a dummy CRDTTreeNodeID for testing. */ -const DTP = CRDTTreeNodeID.of(ITT, 0); +const idT = CRDTTreeNodeID.of(ITT, 0); /** * `dummyContext` is a helper context that is used for testing. @@ -47,23 +47,23 @@ const dummyContext = ChangeContext.create( ); /** - * `issuePos` is a helper function that issues a new CRDTTreeNodeID. + * `posT` is a helper function that issues a new CRDTTreeNodeID. */ -function issuePos(offset = 0): CRDTTreeNodeID { +function posT(offset = 0): CRDTTreeNodeID { return CRDTTreeNodeID.of(dummyContext.issueTimeTicket(), offset); } /** - * `issueTime` is a helper function that issues a new TimeTicket. + * `timeT` is a helper function that issues a new TimeTicket. */ -function issueTime(): TimeTicket { +function timeT(): TimeTicket { return dummyContext.issueTimeTicket(); } describe('CRDTTreeNode', function () { it('Can be created', function () { - const node = new CRDTTreeNode(DTP, 'text', 'hello'); - assert.equal(node.id, DTP); + const node = new CRDTTreeNode(idT, 'text', 'hello'); + assert.equal(node.id, idT); assert.equal(node.type, 'text'); assert.equal(node.value, 'hello'); assert.equal(node.size, 5); @@ -72,8 +72,8 @@ describe('CRDTTreeNode', function () { }); it('Can be split', function () { - const para = new CRDTTreeNode(DTP, 'p', []); - para.append(new CRDTTreeNode(DTP, 'text', 'helloyorkie')); + const para = new CRDTTreeNode(idT, 'p', []); + para.append(new CRDTTreeNode(idT, 'text', 'helloyorkie')); assert.equal(toXML(para), /*html*/ `

helloyorkie

`); assert.equal(para.size, 11); assert.equal(para.isText, false); @@ -95,45 +95,37 @@ describe('CRDTTree.Edit', function () { it('Can inserts nodes with edit', function () { // 0 // - const tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'r'), issueTime()); - assert.equal(tree.getRoot().size, 0); - assert.equal(tree.toXML(), /*html*/ ``); + const t = new CRDTTree(new CRDTTreeNode(posT(), 'r'), timeT()); + assert.equal(t.getRoot().size, 0); + assert.equal(t.toXML(), /*html*/ ``); // 1 //

- tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - assert.equal(tree.toXML(), /*html*/ `

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

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

h e l l o

- tree.editByIndex( - [1, 1], - [new CRDTTreeNode(issuePos(), 'text', 'hello')], - issueTime(), - ); - assert.equal(tree.toXML(), /*html*/ `

hello

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

hello

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

h e l l o

w o r l d

- const p = new CRDTTreeNode(issuePos(), 'p', []); - p.insertAt(new CRDTTreeNode(issuePos(), 'text', 'world'), 0); - tree.editByIndex([7, 7], [p], issueTime()); - assert.equal(tree.toXML(), /*html*/ `

hello

world

`); - assert.equal(tree.getRoot().size, 14); + const p = new CRDTTreeNode(posT(), 'p', []); + p.insertAt(new CRDTTreeNode(posT(), 'text', 'world'), 0); + t.editT([7, 7], [p], 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

- tree.editByIndex( - [6, 6], - [new CRDTTreeNode(issuePos(), 'text', '!')], - issueTime(), - ); - assert.equal(tree.toXML(), /*html*/ `

hello!

world

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

hello!

world

`); assert.deepEqual( - JSON.stringify(tree.toTestTreeNode()), + JSON.stringify(t.toTestTreeNode()), JSON.stringify({ type: 'r', children: [ @@ -162,34 +154,19 @@ describe('CRDTTree.Edit', function () { // 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

- tree.editByIndex( - [6, 6], - [new CRDTTreeNode(issuePos(), 'text', '~')], - issueTime(), - ); - assert.equal(tree.toXML(), /*html*/ `

hello~!

world

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

hello~!

world

`); }); it('Can delete text nodes with edit', function () { // 01. Create a tree with 2 paragraphs. // 0 1 2 3 4 5 6 7 8 //

a b

c d

- const tree = new CRDTTree( - new CRDTTreeNode(issuePos(), 'root'), - issueTime(), - ); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex( - [1, 1], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); - tree.editByIndex([4, 4], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex( - [5, 5], - [new CRDTTreeNode(issuePos(), 'text', 'cd')], - issueTime(), - ); + 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()); assert.deepEqual(tree.toXML(), /*html*/ `

ab

cd

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

a

c d

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

a

cd

`); treeNode = tree.toTestTreeNode(); @@ -210,43 +187,40 @@ describe('CRDTTree.Edit', function () { }); it('Can find the closest TreePos when parentNode or leftSiblingNode does not exist', function () { - const tree = new CRDTTree( - new CRDTTreeNode(issuePos(), 'root'), - issueTime(), - ); + const t = new CRDTTree(new CRDTTreeNode(posT(), 'root'), timeT()); - const pNode = new CRDTTreeNode(issuePos(), 'p'); - const textNode = new CRDTTreeNode(issuePos(), 'text', 'ab'); + const pNode = new CRDTTreeNode(posT(), 'p'); + const textNode = new CRDTTreeNode(posT(), 'text', 'ab'); // 0 1 2 3 4 //

a b

- tree.editByIndex([0, 0], [pNode], issueTime()); - tree.editByIndex([1, 1], [textNode], issueTime()); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

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

ab

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

- tree.editByIndex([1, 3], undefined, issueTime()); - assert.deepEqual(tree.toXML(), /*html*/ `

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

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

hello world

- const tree = new CRDTTree( - new CRDTTreeNode(issuePos(), 'root'), - issueTime(), - ); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex( - [1, 1], - [new CRDTTreeNode(issuePos(), 'text', 'helloworld')], - issueTime(), - ); + 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()); // 01. Split left side of 'helloworld'. - tree.split(1, 1); + // tree.split(1, 1); // TODO(JOOHOJANG): make new helper function when implement Tree.split //betweenEqual(tree, 1, 11, ['text.helloworld']); // 02. Split right side of 'helloworld'. - tree.split(11, 1); + // tree.split(11, 1); // TODO(JOOHOJANG): make new helper function when implement Tree.split //betweenEqual(tree, 1, 11, ['text.helloworld']); // 03. Split 'helloworld' into 'hello' and 'world'. - tree.split(6, 1); + // tree.split(6, 1); // TODO(JOOHOJANG): make new helper function when implement Tree.split //betweenEqual(tree, 1, 11, ['text.hello', 'text.world']); }); it('Can split element nodes', function () { // 01. Split position 1. - let tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex( - [1, 1], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

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

ab

`); - assert.equal(tree.getSize(), 6); + 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()); + assert.deepEqual(t.toXML(), /*html*/ `

ab

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

ab

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

a b

- tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex( - [1, 1], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

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

a

b

`); - assert.equal(tree.getSize(), 6); + 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()); + assert.deepEqual(t.toXML(), /*html*/ `

ab

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

a

b

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

ab

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

ab

`); - assert.equal(tree.getSize(), 6); + 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()); + assert.deepEqual(t.toXML(), /*html*/ `

ab

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

ab

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

abcd

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

ab

cd

`); - assert.equal(tree.getSize(), 8); + 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); // 05. Split multiple nodes level 1. - tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex([1, 1], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); - tree.editByIndex( - [2, 2], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

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

ab

`); - assert.equal(tree.getSize(), 6); + 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); + assert.deepEqual(t.toXML(), /*html*/ `

ab

`); + assert.equal(t.getSize(), 6); // Split multiple nodes level 2. - tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex([1, 1], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); - tree.editByIndex( - [2, 2], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); - tree.split(3, 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()); + assert.deepEqual(t.toXML(), /*html*/ `

ab

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

ab

`, ); - assert.equal(tree.getSize(), 8); + assert.equal(t.getSize(), 8); // Split multiple nodes level 3. - tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex([1, 1], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); - tree.editByIndex( - [2, 2], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

`); - tree.split(3, 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(), 'b')], timeT()); + t.editT([2, 2], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); + assert.deepEqual(t.toXML(), /*html*/ `

ab

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

a

b

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

abcd

`); - assert.equal(tree.getSize(), 6); + 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()); + assert.deepEqual(t.toXML(), /*html*/ `

abcd

`); + assert.equal(t.getSize(), 6); // 0 1 2 3 4 5 6 7 8 //

a b

c d

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

ab

cd

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

ab

cd

`); + assert.equal(t.getSize(), 8); - tree.editByIndex([3, 5], undefined, issueTime()); - assert.deepEqual(tree.toXML(), /*html*/ `

abcd

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

abcd

`); + assert.equal(t.getSize(), 6); }); }); @@ -422,215 +350,149 @@ describe('CRDTTree.Merge', function () { // 01. Create a tree with 2 paragraphs. // 0 1 2 3 4 5 6 7 8 //

a b

c d

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

ab

cd

`); + 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()); + assert.deepEqual(t.toXML(), /*html*/ `

ab

cd

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

a d

- tree.editByIndex([2, 6], undefined, issueTime()); - assert.deepEqual(tree.toXML(), /*html*/ `

ad

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

ad

`); - const node = tree.toTestTreeNode(); + const node = t.toTestTreeNode(); assert.equal(node.size, 4); // root assert.equal(node.children![0].size, 2); // p assert.equal(node.children![0].children![0].size, 1); // a assert.equal(node.children![0].children![1].size, 1); // d // 03. insert a new text node at the start of the first paragraph. - tree.editByIndex( - [1, 1], - [new CRDTTreeNode(issuePos(), 'text', '@')], - issueTime(), - ); - assert.deepEqual(tree.toXML(), /*html*/ `

@ad

`); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', '@')], timeT()); + assert.deepEqual(t.toXML(), /*html*/ `

@ad

`); }); it('Can delete nodes between elements in different level with edit', function () { // 01. Create a tree with 2 paragraphs. // 0 1 2 3 4 5 6 7 8 9 10 //

a b

c d

- const tree = new CRDTTree( - new CRDTTreeNode(issuePos(), 'root'), - issueTime(), - ); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex([1, 1], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); - tree.editByIndex( - [2, 2], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); - tree.editByIndex([6, 6], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex( - [7, 7], - [new CRDTTreeNode(issuePos(), 'text', 'cd')], - issueTime(), - ); + 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()); assert.deepEqual( - tree.toXML(), + t.toXML(), /*html*/ `

ab

cd

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

a d - tree.editByIndex([3, 8], undefined, issueTime()); - assert.deepEqual(tree.toXML(), /*html*/ `

ad

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

ad

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

a b

- let tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex([1, 1], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); - tree.editByIndex([2, 2], [new CRDTTreeNode(issuePos(), 'i')], issueTime()); - tree.editByIndex( - [3, 3], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); + 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()); assert.deepEqual( - tree.toXML(), + t.toXML(), /*html*/ `

ab

`, ); - tree.editByIndex([5, 6], undefined, issueTime()); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

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

ab

`); // 02. edit between two element nodes in same hierarchy. - tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex([1, 1], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); - tree.editByIndex([2, 2], [new CRDTTreeNode(issuePos(), 'i')], issueTime()); - tree.editByIndex( - [3, 3], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); + 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()); assert.deepEqual( - tree.toXML(), + t.toXML(), /*html*/ `

ab

`, ); - tree.editByIndex([6, 7], undefined, issueTime()); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

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

ab

`); // 03. edit between text and element node in same hierarchy. - tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex([1, 1], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); - tree.editByIndex([2, 2], [new CRDTTreeNode(issuePos(), 'i')], issueTime()); - tree.editByIndex( - [3, 3], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); + 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()); assert.deepEqual( - tree.toXML(), + t.toXML(), /*html*/ `

ab

`, ); - tree.editByIndex([4, 6], undefined, issueTime()); - assert.deepEqual(tree.toXML(), /*html*/ `

a

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

a

`); // 04. edit between text and element node in same hierarchy. - tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex([1, 1], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); - tree.editByIndex([2, 2], [new CRDTTreeNode(issuePos(), 'i')], issueTime()); - tree.editByIndex( - [3, 3], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); + 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()); assert.deepEqual( - tree.toXML(), + t.toXML(), /*html*/ `

ab

`, ); - tree.editByIndex([5, 7], undefined, issueTime()); - assert.deepEqual(tree.toXML(), /*html*/ `

ab

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

ab

`); // 05. edit between text and element node in same hierarchy. - tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex([1, 1], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); - tree.editByIndex([2, 2], [new CRDTTreeNode(issuePos(), 'i')], issueTime()); - tree.editByIndex( - [3, 3], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); + 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()); assert.deepEqual( - tree.toXML(), + t.toXML(), /*html*/ `

ab

`, ); - tree.editByIndex([4, 7], undefined, issueTime()); - assert.deepEqual(tree.toXML(), /*html*/ `

a

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

a

`); // 06. edit between text and element node in same hierarchy. - tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex([1, 1], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); - tree.editByIndex([2, 2], [new CRDTTreeNode(issuePos(), 'i')], issueTime()); - tree.editByIndex( - [3, 3], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); + 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()); assert.deepEqual( - tree.toXML(), + t.toXML(), /*html*/ `

ab

`, ); - tree.editByIndex([3, 7], undefined, issueTime()); - assert.deepEqual(tree.toXML(), /*html*/ `

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

`); // 07. edit between text and element node in same hierarchy. - tree = new CRDTTree(new CRDTTreeNode(issuePos(), 'root'), issueTime()); - tree.editByIndex([0, 0], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex( - [1, 1], - [new CRDTTreeNode(issuePos(), 'text', 'ab')], - issueTime(), - ); - tree.editByIndex([4, 4], [new CRDTTreeNode(issuePos(), 'p')], issueTime()); - tree.editByIndex([5, 5], [new CRDTTreeNode(issuePos(), 'b')], issueTime()); - tree.editByIndex( - [6, 6], - [new CRDTTreeNode(issuePos(), 'text', 'cd')], - issueTime(), - ); - tree.editByIndex( - [10, 10], - [new CRDTTreeNode(issuePos(), 'p')], - issueTime(), - ); - tree.editByIndex( - [11, 11], - [new CRDTTreeNode(issuePos(), 'text', 'ef')], - issueTime(), - ); + 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()); assert.deepEqual( - tree.toXML(), + t.toXML(), /*html*/ `

ab

cd

ef

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

ab

cd

ef

`, ); }); From d134656156c44cd1166a3a0a969b16a51ff29620 Mon Sep 17 00:00:00 2001 From: Youngteac Hong Date: Wed, 8 Nov 2023 14:07:02 +0900 Subject: [PATCH 3/4] Add splitLevels to Tree.Edit --- public/prosemirror.html | 14 +- src/document/crdt/tree.ts | 361 +++++++++--------- src/document/json/tree.ts | 43 ++- src/document/operation/tree_edit_operation.ts | 2 + test/integration/tree_test.ts | 6 +- test/unit/document/crdt/tree_test.ts | 357 +++++++++-------- 6 files changed, 417 insertions(+), 366 deletions(-) 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..0ce4fec08 100644 --- a/src/document/crdt/tree.ts +++ b/src/document/crdt/tree.ts @@ -111,6 +111,39 @@ export class CRDTTreePos { return new CRDTTreePos(parentID, leftSiblingID); } + /** + * `fromTreePos` creates a new instance of CRDTTreePos from the given TreePos. + */ + public static fromTreePos(pos: TreePos): CRDTTreePos { + const { offset } = pos; + let { node } = pos; + let leftSibling; + + if (node.isText) { + if (node.parent!.children[0] === node && offset === 0) { + leftSibling = node.parent!; + } else { + leftSibling = node; + } + + node = node.parent!; + } else { + if (offset === 0) { + leftSibling = node; + } else { + leftSibling = node.children[offset - 1]; + } + } + + return CRDTTreePos.of( + node.id, + CRDTTreeNodeID.of( + leftSibling.getCreatedAt(), + leftSibling.getOffset() + offset, + ), + ); + } + /** * `getParentID` returns the parent ID. */ @@ -150,6 +183,29 @@ export class CRDTTreePos { }; } + /** + * `toTreeNodes` converts the pos to parent and left sibling nodes. + */ + public toTreeNodes(tree: CRDTTree): [CRDTTreeNode, CRDTTreeNode] { + const parentID = this.getParentID(); + const leftSiblingID = this.getLeftSiblingID(); + const parentNode = tree.findFloorNode(parentID); + let leftNode = tree.findFloorNode(leftSiblingID); + if (!parentNode || !leftNode) { + throw new Error(`cannot find node at ${this}`); + } + + if ( + leftSiblingID.getOffset() > 0 && + leftSiblingID.getOffset() === leftNode.id.getOffset() && + leftNode.insPrevID + ) { + leftNode = tree.findFloorNode(leftNode.insPrevID) || leftNode; + } + + return [parentNode, leftNode!]; + } + /** * `getLeftSiblingID` returns the left sibling ID. */ @@ -550,9 +606,8 @@ export class CRDTTree extends CRDTGCElement { /** * `findFloorNode` finds node of given id. */ - private findFloorNode(id: CRDTTreeNodeID) { + public findFloorNode(id: CRDTTreeNodeID): CRDTTreeNode | undefined { const entry = this.nodeMapByID.floorEntry(id); - if (!entry || !entry.key.getCreatedAt().equals(id.getCreatedAt())) { return; } @@ -561,66 +616,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] = pos.toTreeNodes(this); + 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 +680,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 +719,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. @@ -794,19 +849,6 @@ export class CRDTTree extends CRDTGCElement { return [changes, latestCreatedAtMap]; } - private traverseInPosRange( - fromParent: CRDTTreeNode, - fromLeft: CRDTTreeNode, - toParent: CRDTTreeNode, - toLeft: CRDTTreeNode, - callback: (node: CRDTTreeNode, contain: TagContained) => void, - ): void { - const fromIdx = this.toIndex(fromParent, fromLeft); - const toIdx = this.toIndex(toParent, toLeft); - - return this.indexTree.nodesBetween(fromIdx, toIdx, callback); - } - /** * `editT` edits the given range with the given value. * This method uses indexes instead of a pair of TreePos for testing. @@ -814,11 +856,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); } /** @@ -884,34 +927,7 @@ export class CRDTTree extends CRDTGCElement { */ public findPos(index: number, preferText = true): CRDTTreePos { const treePos = this.indexTree.findTreePos(index, preferText); - - const { offset } = treePos; - let { node } = treePos; - let leftSibling; - - if (node.isText) { - if (node.parent!.children[0] === node && offset === 0) { - leftSibling = node.parent!; - } else { - leftSibling = node; - } - - node = node.parent!; - } else { - if (offset === 0) { - leftSibling = node; - } else { - leftSibling = node.children[offset - 1]; - } - } - - return CRDTTreePos.of( - node.id, - CRDTTreeNodeID.of( - leftSibling.getCreatedAt(), - leftSibling.getOffset() + offset, - ), - ); + return CRDTTreePos.fromTreePos(treePos); } /** @@ -926,23 +942,14 @@ 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. */ public pathToPos(path: Array): CRDTTreePos { const index = this.indexTree.pathToIndex(path); - return this.findPos(index); } @@ -1007,8 +1014,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 +1025,6 @@ export class CRDTTree extends CRDTGCElement { leftSiblingNode: CRDTTreeNode, ): Array { const treePos = this.toTreePos(parentNode, leftSiblingNode); - if (!treePos) { return []; } @@ -1035,7 +1040,6 @@ export class CRDTTree extends CRDTGCElement { leftSiblingNode: CRDTTreeNode, ): number { const treePos = this.toTreePos(parentNode, leftSiblingNode); - if (!treePos) { return -1; } @@ -1043,84 +1047,6 @@ export class CRDTTree extends CRDTGCElement { return this.indexTree.indexOf(treePos); } - private toTreeNodes(pos: CRDTTreePos) { - const parentID = pos.getParentID(); - const leftSiblingID = pos.getLeftSiblingID(); - const parentNode = this.findFloorNode(parentID); - let leftSiblingNode = this.findFloorNode(leftSiblingID); - - if (!parentNode || !leftSiblingNode) { - return []; - } - - if ( - leftSiblingID.getOffset() > 0 && - leftSiblingID.getOffset() === leftSiblingNode.id.getOffset() && - leftSiblingNode.insPrevID - ) { - leftSiblingNode = - this.findFloorNode(leftSiblingNode.insPrevID) || leftSiblingNode; - } - - return [parentNode, leftSiblingNode!]; - } - - /** - * `toTreePos` converts the given CRDTTreePos to local TreePos. - */ - private toTreePos( - parentNode: CRDTTreeNode, - leftSiblingNode: CRDTTreeNode, - ): TreePos | undefined { - if (!parentNode || !leftSiblingNode) { - return; - } - - let treePos; - - if (parentNode.isRemoved) { - let childNode: CRDTTreeNode; - while (parentNode.isRemoved) { - childNode = parentNode; - parentNode = childNode.parent!; - } - - const childOffset = parentNode.findOffset(childNode!); - - treePos = { - node: parentNode, - offset: childOffset, - }; - } 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, - }; - } - } - - return treePos; - } - /** * `indexToPath` converts the given tree index to path. */ @@ -1168,12 +1094,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,12 +1110,77 @@ 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)]; } + + /** + * `traverseInPosRange` traverses the tree in the given position range. + */ + private traverseInPosRange( + fromParent: CRDTTreeNode, + fromLeft: CRDTTreeNode, + toParent: CRDTTreeNode, + toLeft: CRDTTreeNode, + callback: (node: CRDTTreeNode, contain: TagContained) => void, + ): void { + const fromIdx = this.toIndex(fromParent, fromLeft); + const toIdx = this.toIndex(toParent, toLeft); + return this.indexTree.nodesBetween(fromIdx, toIdx, callback); + } + + /** + * `toTreePos` converts the given nodes to the position of the IndexTree. + */ + private toTreePos( + parentNode: CRDTTreeNode, + leftSiblingNode: CRDTTreeNode, + ): TreePos | undefined { + if (!parentNode || !leftSiblingNode) { + return; + } + + if (parentNode.isRemoved) { + let childNode: CRDTTreeNode; + while (parentNode.isRemoved) { + childNode = parentNode; + parentNode = childNode.parent!; + } + + const offset = parentNode.findOffset(childNode!); + return { + node: parentNode, + offset, + }; + } + + if (parentNode === leftSiblingNode) { + return { + node: parentNode, + offset: 0, + }; + } + + let offset = parentNode.findOffset(leftSiblingNode); + if (!leftSiblingNode.isRemoved) { + if (leftSiblingNode.isText) { + return { + node: leftSiblingNode, + offset: leftSiblingNode.paddedSize, + }; + } + + offset++; + } + + return { + node: parentNode, + offset, + }; + } } 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

`, From 4251c59ceacc6db2ebcadc9d9f865e4024641cbc Mon Sep 17 00:00:00 2001 From: Hackerwins Date: Mon, 20 Nov 2023 17:42:45 +0900 Subject: [PATCH 4/4] Remove splitLevels from Tree.Edit --- public/prosemirror.html | 9 +- src/document/crdt/tree.ts | 18 +- src/document/json/tree.ts | 1 - src/document/operation/tree_edit_operation.ts | 1 - test/unit/document/crdt/tree_test.ts | 218 ++++++++---------- 5 files changed, 101 insertions(+), 146 deletions(-) diff --git a/public/prosemirror.html b/public/prosemirror.html index b39855699..0295022d2 100644 --- a/public/prosemirror.html +++ b/public/prosemirror.html @@ -337,10 +337,11 @@

Yorkie.Tree

openEnd && structure ) { - root.tree.edit(from, to, docToTreeNode(node.toJSON()), [ - openStart, - openEnd, - ]); + // TODO(hackerwins): edit should support openStart and openEnd. + // root.tree.edit(from, to, docToTreeNode(node.toJSON()), [ + // openStart, + // openEnd, + // ]); continue; } diff --git a/src/document/crdt/tree.ts b/src/document/crdt/tree.ts index 149f91370..41230f3e9 100644 --- a/src/document/crdt/tree.ts +++ b/src/document/crdt/tree.ts @@ -630,8 +630,6 @@ export class CRDTTree extends CRDTGCElement { public findNodesAndSplit( pos: CRDTTreePos, editedAt: TimeTicket, - /* eslint-disable @typescript-eslint/no-unused-vars */ - splitLevel = 0, ): [CRDTTreeNode, CRDTTreeNode] { // 01. Find the parent and left sibling node of the given position. const [parent, leftSibling] = pos.toTreeNodes(this); @@ -719,21 +717,12 @@ 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.findNodesAndSplit( - range[0], - editedAt, - splitLevels[0], - ); - const [toParent, toLeft] = this.findNodesAndSplit( - range[1], - editedAt, - splitLevels[1], - ); + const [fromParent, fromLeft] = this.findNodesAndSplit(range[0], editedAt); + const [toParent, toLeft] = this.findNodesAndSplit(range[1], editedAt); // TODO(hackerwins): If concurrent deletion happens, we need to seperate the // range(from, to) into multiple ranges. @@ -856,12 +845,11 @@ 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, splitLevels, editedAt); + this.edit([fromPos, toPos], contents, editedAt); } /** diff --git a/src/document/json/tree.ts b/src/document/json/tree.ts index 195111570..22c4bc022 100644 --- a/src/document/json/tree.ts +++ b/src/document/json/tree.ts @@ -368,7 +368,6 @@ export class Tree { 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 c9e21f19c..952869457 100644 --- a/src/document/operation/tree_edit_operation.ts +++ b/src/document/operation/tree_edit_operation.ts @@ -89,7 +89,6 @@ export class TreeEditOperation extends Operation { const [changes] = tree.edit( [this.fromPos, this.toPos], this.contents?.map((content) => content.deepcopy()), - [0, 0], this.getExecutedAt(), this.maxCreatedAtMapByActor, ); diff --git a/test/unit/document/crdt/tree_test.ts b/test/unit/document/crdt/tree_test.ts index 8bd1a390c..b52bc4215 100644 --- a/test/unit/document/crdt/tree_test.ts +++ b/test/unit/document/crdt/tree_test.ts @@ -102,18 +102,13 @@ describe('CRDTTree.Edit', function () { // 1 //

- t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], [0, 0], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], 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')], - [0, 0], - timeT(), - ); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'hello')], timeT()); assert.equal(t.toXML(), /*html*/ `

hello

`); assert.equal(t.getRoot().size, 7); @@ -121,13 +116,13 @@ 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], [0, 0], timeT()); + t.editT([7, 7], [p], 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', '!')], [0, 0], timeT()); + t.editT([6, 6], [new CRDTTreeNode(posT(), 'text', '!')], timeT()); assert.equal(t.toXML(), /*html*/ `

hello!

world

`); assert.deepEqual(t.toTestTreeNode(), { @@ -157,7 +152,7 @@ describe('CRDTTree.Edit', function () { // 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', '~')], [0, 0], timeT()); + t.editT([6, 6], [new CRDTTreeNode(posT(), 'text', '~')], timeT()); assert.equal(t.toXML(), /*html*/ `

hello~!

world

`); }); @@ -166,20 +161,10 @@ 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')], [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(), - ); + 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()); assert.deepEqual(tree.toXML(), /*html*/ `

ab

cd

`); let treeNode = tree.toTestTreeNode(); @@ -190,7 +175,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, [0, 0], timeT()); + tree.editT([2, 3], undefined, timeT()); assert.deepEqual(tree.toXML(), /*html*/ `

a

cd

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

a b

- t.editT([0, 0], [pNode], [0, 0], timeT()); - t.editT([1, 1], [textNode], [0, 0], timeT()); + t.editT([0, 0], [pNode], timeT()); + t.editT([1, 1], [textNode], 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, [0, 0], timeT()); + t.editT([1, 3], undefined, timeT()); assert.deepEqual(t.toXML(), /*html*/ `

`); 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, [0, 0], timeT()); + t.editT([0, 2], undefined, timeT()); assert.deepEqual(t.toXML(), /*html*/ ``); [parent, left] = t.findNodesAndSplit( new CRDTTreePos(pNode.id, textNode.id), timeT(), - 0, ); assert.equal(t.toIndex(parent, left), 0); }); @@ -245,13 +228,8 @@ describe('CRDTTree.Split', function () { // 0 1 6 11 //

hello world

const 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(), 'text', 'helloworld')], - [0, 0], - timeT(), - ); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'helloworld')], timeT()); const expectedIntial = { type: 'root', children: [ @@ -270,15 +248,15 @@ describe('CRDTTree.Split', function () { assert.deepEqual(t.toTestTreeNode(), expectedIntial); // 01. Split left side of 'helloworld'. - t.editT([1, 1], undefined, [0, 0], timeT()); + t.editT([1, 1], undefined, timeT()); assert.deepEqual(t.toTestTreeNode(), expectedIntial); // 02. Split right side of 'helloworld'. - t.editT([11, 11], undefined, [0, 0], timeT()); + t.editT([11, 11], undefined, timeT()); assert.deepEqual(t.toTestTreeNode(), expectedIntial); // 03. Split 'helloworld' into 'hello' and 'world'. - t.editT([6, 6], undefined, [0, 0], timeT()); + t.editT([6, 6], undefined, timeT()); assert.deepEqual(t.toTestTreeNode(), { type: 'root', children: [ @@ -303,28 +281,28 @@ describe('CRDTTree.Split', function () { // 01. Split position 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(), 'text', 'ab')], [0, 0], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ab

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

ab

`); assert.equal(t.getSize(), 6); // 02. Split position 2. 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(), 'text', 'ab')], [0, 0], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ab

`); - t.editT([2, 2], undefined, [1, 1], timeT()); + // 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')], [0, 0], timeT()); - t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], [0, 0], timeT()); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'ab')], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ab

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

ab

`); assert.equal(t.getSize(), 6); }); @@ -335,11 +313,11 @@ describe('CRDTTree.Split', function () { // 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()); + 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

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

ab

`, @@ -347,11 +325,11 @@ describe('CRDTTree.Split', function () { // 02. Split nodes level 2. 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()); + 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

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

a

b

`, @@ -359,11 +337,11 @@ describe('CRDTTree.Split', function () { // 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')], [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([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

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

a

b

`, @@ -372,13 +350,8 @@ describe('CRDTTree.Split', 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')], [0, 0], timeT()); - t.editT( - [1, 1], - [new CRDTTreeNode(posT(), 'text', 'abcd')], - [0, 0], - timeT(), - ); + t.editT([0, 0], [new CRDTTreeNode(posT(), 'p')], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', 'abcd')], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

abcd

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

ab

cd

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

abcd

`); assert.equal(t.getSize(), 6); }); @@ -400,16 +373,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')], [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()); + 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()); 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, [0, 0], timeT()); + t.editT([2, 6], undefined, timeT()); assert.deepEqual(t.toXML(), /*html*/ `

ad

`); const node = t.toTestTreeNode(); @@ -419,7 +392,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', '@')], [0, 0], timeT()); + t.editT([1, 1], [new CRDTTreeNode(posT(), 'text', '@')], timeT()); assert.deepEqual(t.toXML(), /*html*/ `

@ad

`); }); @@ -428,11 +401,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')], [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()); + 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()); assert.deepEqual( t.toXML(), /*html*/ `

ab

cd

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

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

ad

`); }); @@ -451,101 +424,96 @@ describe('CRDTTree.Merge', function () { // 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')], [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()); + 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()); assert.deepEqual( t.toXML(), /*html*/ `

ab

`, ); - t.editT([5, 6], undefined, [0, 0], timeT()); + t.editT([5, 6], undefined, 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')], [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()); + 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()); assert.deepEqual( t.toXML(), /*html*/ `

ab

`, ); - t.editT([6, 7], undefined, [0, 0], timeT()); + t.editT([6, 7], undefined, 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')], [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()); + 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()); assert.deepEqual( t.toXML(), /*html*/ `

ab

`, ); - t.editT([4, 6], undefined, [0, 0], timeT()); + t.editT([4, 6], undefined, 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')], [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()); + 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()); assert.deepEqual( t.toXML(), /*html*/ `

ab

`, ); - t.editT([5, 7], undefined, [0, 0], timeT()); + t.editT([5, 7], undefined, 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')], [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()); + 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()); assert.deepEqual( t.toXML(), /*html*/ `

ab

`, ); - t.editT([4, 7], undefined, [0, 0], timeT()); + t.editT([4, 7], undefined, 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')], [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()); + 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()); assert.deepEqual( t.toXML(), /*html*/ `

ab

`, ); - t.editT([3, 7], undefined, [0, 0], timeT()); + t.editT([3, 7], undefined, 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')], [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(), - ); + 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()); assert.deepEqual( t.toXML(), /*html*/ `

ab

cd

ef

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

ab

cd

ef

`,