Skip to content

Commit

Permalink
Implement merge elements in Tree.Edit
Browse files Browse the repository at this point in the history
  • Loading branch information
hackerwins committed Nov 3, 2023
1 parent dd9be29 commit e571547
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 32 deletions.
32 changes: 28 additions & 4 deletions src/document/crdt/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,7 @@ export class CRDTTree extends CRDTGCElement {
});

const toBeRemoveds: Array<CRDTTreeNode> = [];
const toBeMovedToFromParents: Array<CRDTTreeNode> = [];
const latestCreatedAtMap = new Map<string, TimeTicket>();

this.traverseInPosRange(
Expand All @@ -704,12 +705,32 @@ export class CRDTTree extends CRDTGCElement {
toParent,
toLeft,
(node, contain) => {
// If node is a element node and half-contained in the range,
// it should not be removed.
if (!node.isText && contain != TagContained.All) {
// NOTE(hackerwins): If the node overlaps as a closing tag with the
// range then we need to keep the node.
if (!node.isText && contain == TagContained.Closing) {
return;
}

// NOTE(hackerwins): If the node overlaps as an opening tag with the
// range then we need to move the remaining children to fromParent.
if (!node.isText && contain == TagContained.Opening) {
// TODO(hackerwins): Define more clearly merge-able rules
// between two parents. For now, we only merge two parents are
// both element nodes having text children.
// e.g. <p>a|b</p><p>c|d</p> -> <p>a|d</p>
if (!fromParent.hasTextChild() || !toParent.hasTextChild()) {
return;
}

for (const child of node.children) {
if (toBeRemoveds.includes(child)) {
continue;
}

toBeMovedToFromParents.push(child);
}
}

const actorID = node.getCreatedAt().getActorID()!;
const latestCreatedAt = latestCreatedAtMapByActor
? latestCreatedAtMapByActor!.has(actorID!)
Expand All @@ -732,12 +753,15 @@ export class CRDTTree extends CRDTGCElement {

for (const node of toBeRemoveds) {
node.remove(editedAt);

if (node.isRemoved) {
this.removedNodeMap.set(node.id.toIDString(), node);
}
}

for (const node of toBeMovedToFromParents) {
fromParent.append(node);
}

// 03. insert the given node at the given position.
if (contents?.length) {
let leftInChildren = fromLeft; // tree
Expand Down
8 changes: 1 addition & 7 deletions test/integration/tree_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -963,13 +963,7 @@ describe('Tree.edit', function () {
);

doc.update((root) => root.t.edit(2, 18));
assert.equal(
doc.getRoot().t.toXML(),
/*html*/ `<doc><p>a</p><p>f</p></doc>`,
);

// TODO(sejongk): Use the below assertion after implementing Tree.Move.
// assert.equal(doc.getRoot().t.toXML(), /*html*/ `<doc><p>af</p></doc>`);
assert.equal(doc.getRoot().t.toXML(), /*html*/ `<doc><p>af</p></doc>`);
});
});

Expand Down
73 changes: 52 additions & 21 deletions test/unit/document/crdt/tree_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ describe('CRDTTreeNode', function () {
});

// NOTE: To see the XML string as highlighted, install es6-string-html plugin in VSCode.
describe('CRDTTree', function () {
describe('CRDTTree.Edit', function () {
it('Can inserts nodes with edit', function () {
// 0
// <root> </root>
Expand Down Expand Up @@ -250,7 +250,7 @@ describe('CRDTTree', function () {
});
});

describe.skip('Tree.split', function () {
describe.skip('CRDTTree.Split', function () {
it('Can split text nodes', function () {
// 00. Create a tree with 2 paragraphs.
// 0 1 6 11
Expand Down Expand Up @@ -417,7 +417,7 @@ describe.skip('Tree.split', function () {
});
});

describe('Tree.move', function () {
describe('CRDTTree.Merge', function () {
it('Can delete nodes between element nodes with edit', function () {
// 01. Create a tree with 2 paragraphs.
// 0 1 2 3 4 5 6 7 8
Expand All @@ -440,27 +440,58 @@ describe('Tree.move', function () {
);
assert.deepEqual(tree.toXML(), /*html*/ `<root><p>ab</p><p>cd</p></root>`);

// 02. delete b, c and first paragraph.
// 02. delete b, c and the second paragraph.
// 0 1 2 3 4
// <root> <p> a d </p> </root>
tree.editByIndex([2, 6], undefined, issueTime());
assert.deepEqual(tree.toXML(), /*html*/ `<root><p>a</p><p>d</p></root>`);
// TODO(sejongk): Use the below assertion after implementing Tree.Move.
// assert.deepEqual(tree.toXML(), /*html*/ `<root><p>ad</p></root>`);

// const treeNode = tree.toTestTreeNode();
// assert.equal(treeNode.size, 4); // root
// assert.equal(treeNode.children![0].size, 2); // p
// assert.equal(treeNode.children![0].children![0].size, 1); // a
// assert.equal(treeNode.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*/ `<root><p>@ad</p></root>`);
assert.deepEqual(tree.toXML(), /*html*/ `<root><p>ad</p></root>`);

const node = tree.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*/ `<root><p>@ad</p></root>`);
});

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
// <root> <p> <b> a b </b> </p> <p> c d </p> </root>
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(),
);
assert.deepEqual(
tree.toXML(),
/*html*/ `<root><p><b>ab</b></p><p>cd</p></root>`,
);

// 02. delete b, c and second paragraph.
// 0 1 2 3 4 5
// <root> <p> <b> a d </b> </root>
tree.editByIndex([3, 8], undefined, issueTime());
assert.deepEqual(tree.toXML(), /*html*/ `<root><p><b>ad</b></p></root>`);
});

it.skip('Can merge different levels with edit', function () {
Expand Down

0 comments on commit e571547

Please sign in to comment.