Skip to content

Commit

Permalink
Modify tree.edit to allow multiple nodes at a time (#576)
Browse files Browse the repository at this point in the history
Co-authored-by: JOOHOJANG <[email protected]>
  • Loading branch information
ehuas and JOOHOJANG authored Jul 19, 2023
1 parent e0b5136 commit 9e0c431
Show file tree
Hide file tree
Showing 3 changed files with 338 additions and 85 deletions.
32 changes: 18 additions & 14 deletions src/document/crdt/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,26 +639,30 @@ export class CRDTTree extends CRDTGCElement {
}
}

// TODO(ehuas): Fix here
// 03. insert the given node at the given position.
if (contents?.length) {
// 03-1. insert the content nodes to the list.
let previous = fromRight!.prev!;
traverse(contents[0], (node) => {
this.insertAfter(previous, node);
previous = node;
});

// 03-2. insert the content nodes to the tree.
if (fromPos.node.isText) {
if (fromPos.offset === 0) {
fromPos.node.parent!.insertBefore(contents[0], fromPos.node);
const node = fromPos.node;
let offset = fromPos.offset;

for (const content of contents!) {
traverse(content, (node) => {
this.insertAfter(previous, node);
previous = node;
});

// 03-2. insert the content nodes to the tree.
if (node.isText) {
if (fromPos.offset === 0) {
node.parent!.insertBefore(content, node);
} else {
node.parent!.insertAfter(content, node);
}
} else {
fromPos.node.parent!.insertAfter(contents[0], fromPos.node);
node.insertAt(content, offset);
offset++;
}
} else {
const target = fromPos.node;
target.insertAt(contents[0], fromPos.offset);
}
}

Expand Down
164 changes: 107 additions & 57 deletions src/document/json/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ function buildDescendants(
const { type } = treeNode;
const ticket = context.issueTimeTicket();

if (type === 'text') {
if (type === DefaultTextType) {
validateTextNode(treeNode as TextNode);
const { value } = treeNode as TextNode;
const textNode = CRDTTreeNode.create(
CRDTTreePos.of(ticket, 0),
Expand Down Expand Up @@ -101,7 +102,7 @@ function createCRDTTreeNode(context: ChangeContext, content: TreeNode) {
const ticket = context.issueTimeTicket();

let root;
if (content.type === 'text') {
if (content.type === DefaultTextType) {
const { value } = content as TextNode;
root = CRDTTreeNode.create(CRDTTreePos.of(ticket, 0), type, value);
} else if (content) {
Expand Down Expand Up @@ -133,6 +134,47 @@ function createCRDTTreeNode(context: ChangeContext, content: TreeNode) {
return root;
}

/**
* `validateTextNode` ensures that a text node has a non-empty string value.
*/
function validateTextNode(textNode: TextNode): boolean {
if (!textNode.value.length) {
throw new Error('text node cannot have empty value');
} else {
return true;
}
}

/**
* `validateTreeNodes` ensures that treeNodes consists of only one type.
*/
function validateTreeNodes(treeNodes: Array<TreeNode>): 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);
}
} 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;
}

/**
* `Tree` is a CRDT-based tree structure that is used to represent the document
* tree of text-based editor such as ProseMirror.
Expand Down Expand Up @@ -272,42 +314,54 @@ export class Tree {
);
}

/**
* `editByPath` edits this tree with the given node and path.
*/
public editByPath(
fromPath: Array<number>,
toPath: Array<number>,
...contents: Array<TreeNode>
private editInternal(
fromPos: CRDTTreePos,
toPos: CRDTTreePos,
contents: Array<TreeNode>,
): boolean {
if (!this.context || !this.tree) {
throw new Error('it is not initialized yet');
}
if (fromPath.length !== toPath.length) {
throw new Error('path length should be equal');
}
if (!fromPath.length || !toPath.length) {
throw new Error('path should not be empty');
if (contents.length !== 0 && contents[0]) {
validateTreeNodes(contents);
if (contents[0].type !== DefaultTextType) {
for (const content of contents) {
const { children = [] } = content as ElementNode;
validateTreeNodes(children);
}
}
}

const crdtNodes: Array<CRDTTreeNode> = contents
.map((content) => content && createCRDTTreeNode(this.context!, content))
.filter((a) => a) as Array<CRDTTreeNode>;
const ticket = this.context!.getLastTimeTicket();
let crdtNodes = new Array<CRDTTreeNode>();

const fromPos = this.tree.pathToPos(fromPath);
const toPos = this.tree.pathToPos(toPath);
const ticket = this.context.getLastTimeTicket();
this.tree.edit(
if (contents[0]?.type === DefaultTextType) {
let compVal = '';
for (const content of contents) {
const { value } = content as TextNode;
compVal += value;
}
crdtNodes.push(
CRDTTreeNode.create(
CRDTTreePos.of(this.context!.issueTimeTicket(), 0),
DefaultTextType,
compVal,
),
);
} else {
crdtNodes = contents
.map((content) => content && createCRDTTreeNode(this.context!, content))
.filter((a) => a) as Array<CRDTTreeNode>;
}

this.tree!.edit(
[fromPos, toPos],
crdtNodes.length
? crdtNodes.map((crdtNode) => crdtNode?.deepcopy())
: undefined,
ticket,
);

this.context.push(
this.context!.push(
TreeEditOperation.create(
this.tree.getCreatedAt(),
this.tree!.getCreatedAt(),
fromPos,
toPos,
crdtNodes.length ? crdtNodes : undefined,
Expand All @@ -319,14 +373,38 @@ export class Tree {
!fromPos.getCreatedAt().equals(toPos.getCreatedAt()) ||
fromPos.getOffset() !== toPos.getOffset()
) {
this.context.registerElementHasRemovedNodes(this.tree!);
this.context!.registerElementHasRemovedNodes(this.tree!);
}

return true;
}

/**
* `edit` edits this tree with the given node.
* `editByPath` edits this tree with the given node and path.
*/
public editByPath(
fromPath: Array<number>,
toPath: Array<number>,
...contents: Array<TreeNode>
): boolean {
if (!this.context || !this.tree) {
throw new Error('it is not initialized yet');
}
if (fromPath.length !== toPath.length) {
throw new Error('path length should be equal');
}
if (!fromPath.length || !toPath.length) {
throw new Error('path should not be empty');
}

const fromPos = this.tree.pathToPos(fromPath);
const toPos = this.tree.pathToPos(toPath);

return this.editInternal(fromPos, toPos, contents);
}

/**
* `edit` edits this tree with the given nodes.
*/
public edit(
fromIdx: number,
Expand All @@ -340,38 +418,10 @@ export class Tree {
throw new Error('from should be less than or equal to to');
}

const crdtNodes: Array<CRDTTreeNode> = contents
.map((content) => content && createCRDTTreeNode(this.context!, content))
.filter((a) => a) as Array<CRDTTreeNode>;
const fromPos = this.tree.findPos(fromIdx);
const toPos = this.tree.findPos(toIdx);
const ticket = this.context.getLastTimeTicket();
this.tree.edit(
[fromPos, toPos],
crdtNodes.length
? crdtNodes.map((crdtNode) => crdtNode?.deepcopy())
: undefined,
ticket,
);

this.context.push(
TreeEditOperation.create(
this.tree.getCreatedAt(),
fromPos,
toPos,
crdtNodes.length ? crdtNodes : undefined,
ticket,
),
);

if (
!fromPos.getCreatedAt().equals(toPos.getCreatedAt()) ||
fromPos.getOffset() !== toPos.getOffset()
) {
this.context.registerElementHasRemovedNodes(this.tree!);
}

return true;
return this.editInternal(fromPos, toPos, contents);
}

/**
Expand Down
Loading

0 comments on commit 9e0c431

Please sign in to comment.