Skip to content

Commit

Permalink
Add splitLevels to Tree.Edit
Browse files Browse the repository at this point in the history
  • Loading branch information
hackerwins committed Nov 8, 2023
1 parent db5634d commit b4c0e1d
Show file tree
Hide file tree
Showing 6 changed files with 328 additions and 283 deletions.
14 changes: 8 additions & 6 deletions public/prosemirror.html
Original file line number Diff line number Diff line change
Expand Up @@ -323,32 +323,34 @@ <h2>Yorkie.Tree</h2>
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()));
}
Expand Down
189 changes: 87 additions & 102 deletions src/document/crdt/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,9 +550,8 @@ export class CRDTTree extends CRDTGCElement {
/**
* `findFloorNode` finds node of given id.
*/
private findFloorNode(id: CRDTTreeNodeID) {
private findFloorNode(id: CRDTTreeNodeID): CRDTTreeNode | undefined {
const entry = this.nodeMapByID.floorEntry(id);

if (!entry || !entry.key.getCreatedAt().equals(id.getCreatedAt())) {
return;
}
Expand All @@ -561,66 +560,60 @@ export class CRDTTree extends CRDTGCElement {
}

/**
* `findNodesAndSplitText` finds `TreePos` of the given `CRDTTreeNodeID` and
* splits the text node if necessary.
* `findNodesAndSplit` finds `TreePos` of the given `CRDTTreeNodeID` and
* splits nodes for the given split level.
*
* `CRDTTreeNodeID` is a position in the CRDT perspective. This is
* different from `TreePos` which is a position of the tree in the local
* perspective.
* The ids of the given `pos` are the ids of the node in the CRDT perspective.
* This is different from `TreePos` which is a position of the tree in the
* physical perspective.
*/
public findNodesAndSplitText(
public findNodesAndSplit(
pos: CRDTTreePos,
editedAt: TimeTicket,
/* eslint-disable @typescript-eslint/no-unused-vars */
splitLevel: number,
): [CRDTTreeNode, CRDTTreeNode] {
const treeNodes = this.toTreeNodes(pos);

if (!treeNodes) {
throw new Error(`cannot find node at ${pos}`);
}
const [parentNode] = treeNodes;
let [, leftSiblingNode] = treeNodes;

// Find the appropriate position. This logic is similar to the logical to
// handle the same position insertion of RGA.

if (leftSiblingNode.isText) {
const absOffset = leftSiblingNode.id.getOffset();
const split = leftSiblingNode.split(
// 01. Find the parent and left sibling node of the given position.
const [parent, leftSibling] = this.toTreeNodes(pos);
let leftNode = leftSibling;

// 02. Split nodes for the given split level.
if (leftNode.isText) {
const absOffset = leftNode.id.getOffset();
const split = leftNode.split(
pos.getLeftSiblingID().getOffset() - absOffset,
absOffset,
);

if (split) {
split.insPrevID = leftSiblingNode.id;
this.nodeMapByID.put(split.id, split);

if (leftSiblingNode.insNextID) {
const insNext = this.findFloorNode(leftSiblingNode.insNextID)!;

split.insPrevID = leftNode.id;
if (leftNode.insNextID) {
const insNext = this.findFloorNode(leftNode.insNextID)!;
insNext.insPrevID = split.id;
split.insNextID = leftSiblingNode.insNextID;
split.insNextID = leftNode.insNextID;
}
leftSiblingNode.insNextID = split.id;
leftNode.insNextID = split.id;

this.nodeMapByID.put(split.id, split);
}
}

const allChildren = parentNode.allChildren;
const index =
parentNode === leftSiblingNode
? 0
: allChildren.indexOf(leftSiblingNode) + 1;
// 03. Find the appropriate left node. If some nodes are inserted at the
// same position concurrently, then we need to find the appropriate left
// node. This is similar to RGA.
const allChildren = parent.allChildren;
const index = parent === leftNode ? 0 : allChildren.indexOf(leftNode) + 1;

for (let i = index; i < parentNode.allChildren.length; i++) {
for (let i = index; i < parent.allChildren.length; i++) {
const next = allChildren[i];

if (next.id.getCreatedAt().after(editedAt)) {
leftSiblingNode = next;
} else {
if (!next.id.getCreatedAt().after(editedAt)) {
break;
}

leftNode = next;
}

return [parentNode, leftSiblingNode];
return [parent, leftNode];
}

/**
Expand All @@ -631,13 +624,13 @@ export class CRDTTree extends CRDTGCElement {
attributes: { [key: string]: string } | undefined,
editedAt: TimeTicket,
) {
const [fromParent, fromLeft] = this.findNodesAndSplitText(
const [fromParent, fromLeft] = this.findNodesAndSplit(
range[0],
editedAt,
0,
);
const [toParent, toLeft] = this.findNodesAndSplitText(range[1], editedAt);
const [toParent, toLeft] = this.findNodesAndSplit(range[1], editedAt, 0);
const changes: Array<TreeChange> = [];

changes.push({
type: TreeChangeType.Style,
from: this.toIndex(fromParent, fromLeft),
Expand Down Expand Up @@ -670,15 +663,21 @@ export class CRDTTree extends CRDTGCElement {
public edit(
range: [CRDTTreePos, CRDTTreePos],
contents: Array<CRDTTreeNode> | undefined,
splitLevels: [number, number],
editedAt: TimeTicket,
latestCreatedAtMapByActor?: Map<string, TimeTicket>,
): [Array<TreeChange>, Map<string, TimeTicket>] {
// 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.
Expand Down Expand Up @@ -803,7 +802,6 @@ export class CRDTTree extends CRDTGCElement {
): void {
const fromIdx = this.toIndex(fromParent, fromLeft);
const toIdx = this.toIndex(toParent, toLeft);

return this.indexTree.nodesBetween(fromIdx, toIdx, callback);
}

Expand All @@ -814,11 +812,12 @@ export class CRDTTree extends CRDTGCElement {
public editT(
range: [number, number],
contents: Array<CRDTTreeNode> | 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);
}

/**
Expand Down Expand Up @@ -926,17 +925,9 @@ export class CRDTTree extends CRDTGCElement {
*/
public pathToPosRange(path: Array<number>): [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<number>): TreePos<CRDTTreeNode> {
return this.indexTree.pathToTreePos(path);
}

/**
* `pathToPos` finds the position of the given index in the tree by path.
*/
Expand Down Expand Up @@ -1007,8 +998,7 @@ export class CRDTTree extends CRDTGCElement {
*/
public deepcopy(): CRDTTree {
const root = this.getRoot();
const tree = new CRDTTree(root.deepcopy(), this.getCreatedAt());
return tree;
return new CRDTTree(root.deepcopy(), this.getCreatedAt());
}

/**
Expand All @@ -1019,7 +1009,6 @@ export class CRDTTree extends CRDTGCElement {
leftSiblingNode: CRDTTreeNode,
): Array<number> {
const treePos = this.toTreePos(parentNode, leftSiblingNode);

if (!treePos) {
return [];
}
Expand All @@ -1035,34 +1024,34 @@ export class CRDTTree extends CRDTGCElement {
leftSiblingNode: CRDTTreeNode,
): number {
const treePos = this.toTreePos(parentNode, leftSiblingNode);

if (!treePos) {
return -1;
}

return this.indexTree.indexOf(treePos);
}

private toTreeNodes(pos: CRDTTreePos) {
/**
* `toTreeNodes` converts the given pos to parent and left sibling nodes.
*/
private toTreeNodes(pos: CRDTTreePos): [CRDTTreeNode, CRDTTreeNode] {
const parentID = pos.getParentID();
const leftSiblingID = pos.getLeftSiblingID();
const parentNode = this.findFloorNode(parentID);
let leftSiblingNode = this.findFloorNode(leftSiblingID);

if (!parentNode || !leftSiblingNode) {
return [];
let leftNode = this.findFloorNode(leftSiblingID);
if (!parentNode || !leftNode) {
throw new Error(`cannot find node at ${pos}`);
}

if (
leftSiblingID.getOffset() > 0 &&
leftSiblingID.getOffset() === leftSiblingNode.id.getOffset() &&
leftSiblingNode.insPrevID
leftSiblingID.getOffset() === leftNode.id.getOffset() &&
leftNode.insPrevID
) {
leftSiblingNode =
this.findFloorNode(leftSiblingNode.insPrevID) || leftSiblingNode;
leftNode = this.findFloorNode(leftNode.insPrevID) || leftNode;
}

return [parentNode, leftSiblingNode!];
return [parentNode, leftNode!];
}

/**
Expand All @@ -1076,49 +1065,43 @@ export class CRDTTree extends CRDTGCElement {
return;
}

let treePos;

if (parentNode.isRemoved) {
let childNode: CRDTTreeNode;
while (parentNode.isRemoved) {
childNode = parentNode;
parentNode = childNode.parent!;
}

const childOffset = parentNode.findOffset(childNode!);
const offset = parentNode.findOffset(childNode!);
return {
node: parentNode,
offset,
};
}

treePos = {
if (parentNode === leftSiblingNode) {
return {
node: parentNode,
offset: childOffset,
offset: 0,
};
} else {
if (parentNode === leftSiblingNode) {
treePos = {
node: parentNode,
offset: 0,
};
} else {
let offset = parentNode.findOffset(leftSiblingNode);

if (!leftSiblingNode.isRemoved) {
if (leftSiblingNode.isText) {
return {
node: leftSiblingNode,
offset: leftSiblingNode.paddedSize,
};
} else {
offset++;
}
}
}

treePos = {
node: parentNode,
offset,
let offset = parentNode.findOffset(leftSiblingNode);
if (!leftSiblingNode.isRemoved) {
if (leftSiblingNode.isText) {
return {
node: leftSiblingNode,
offset: leftSiblingNode.paddedSize,
};
}

offset++;
}

return treePos;
return {
node: parentNode,
offset,
};
}

/**
Expand Down Expand Up @@ -1168,11 +1151,12 @@ export class CRDTTree extends CRDTGCElement {
range: TreePosRange,
timeTicket: TimeTicket,
): [Array<number>, Array<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.toPath(fromParent, fromLeft), this.toPath(toParent, toLeft)];
}
Expand All @@ -1184,11 +1168,12 @@ export class CRDTTree extends CRDTGCElement {
range: TreePosRange,
timeTicket: TimeTicket,
): [number, number] {
const [fromParent, fromLeft] = this.findNodesAndSplitText(
const [fromParent, fromLeft] = this.findNodesAndSplit(
range[0],
timeTicket,
0,
);
const [toParent, toLeft] = this.findNodesAndSplitText(range[1], timeTicket);
const [toParent, toLeft] = this.findNodesAndSplit(range[1], timeTicket, 0);

return [this.toIndex(fromParent, fromLeft), this.toIndex(toParent, toLeft)];
}
Expand Down
Loading

0 comments on commit b4c0e1d

Please sign in to comment.