858 lines
26 KiB
Swift
858 lines
26 KiB
Swift
/*
|
|
* Copyright 2022 The Yorkie Authors. All rights reserved.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import Foundation
|
|
|
|
class ContentChange<T: RGATreeSplitValue> {
|
|
let actor: ActorID
|
|
let from: Int
|
|
let to: Int
|
|
var content: T?
|
|
|
|
init(actor: ActorID, from: Int, to: Int, content: T? = nil) {
|
|
self.actor = actor
|
|
self.from = from
|
|
self.to = to
|
|
self.content = content
|
|
}
|
|
}
|
|
|
|
protocol RGATreeSplitValue {
|
|
init()
|
|
var count: Int { get }
|
|
func substring(from: Int, to: Int) -> Self
|
|
}
|
|
|
|
/**
|
|
* `RGATreeSplitNodeID` is an ID of RGATreeSplitNode.
|
|
*/
|
|
class RGATreeSplitNodeID: Equatable, Comparable, CustomDebugStringConvertible {
|
|
static let initial = RGATreeSplitNodeID(TimeTicket.initial, 0)
|
|
|
|
/**
|
|
* `createdAt` the creation time of this ID.
|
|
*/
|
|
public let createdAt: TimeTicket
|
|
/**
|
|
* `offset` the offset of this ID.
|
|
*/
|
|
public let offset: Int32
|
|
|
|
init(_ createdAt: TimeTicket, _ offset: Int32) {
|
|
self.createdAt = createdAt
|
|
self.offset = offset
|
|
}
|
|
|
|
/**
|
|
* `==` returns whether given ID equals to this ID or not.
|
|
*/
|
|
public static func == (lhs: RGATreeSplitNodeID, rhs: RGATreeSplitNodeID) -> Bool {
|
|
lhs.createdAt == rhs.createdAt && lhs.offset == rhs.offset
|
|
}
|
|
|
|
public static func < (lhs: RGATreeSplitNodeID, rhs: RGATreeSplitNodeID) -> Bool {
|
|
if lhs.createdAt == rhs.createdAt {
|
|
return lhs.offset < rhs.offset
|
|
} else {
|
|
return lhs.createdAt < rhs.createdAt
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `hasSameCreatedAt` returns whether given ID has same creation time with this ID.
|
|
*/
|
|
public func hasSameCreatedAt(_ other: RGATreeSplitNodeID) -> Bool {
|
|
self.createdAt == other.createdAt
|
|
}
|
|
|
|
/**
|
|
* `split` creates a new ID with an offset from this ID.
|
|
*/
|
|
public func split(_ offset: Int32) -> RGATreeSplitNodeID {
|
|
RGATreeSplitNodeID(self.createdAt, self.offset + offset)
|
|
}
|
|
|
|
/**
|
|
* `structureAsString` returns a String containing
|
|
* the meta data of the node id for debugging purpose.
|
|
*/
|
|
public var structureAsString: String {
|
|
"\(self.createdAt.structureAsString):\(self.offset)"
|
|
}
|
|
|
|
var debugDescription: String {
|
|
self.structureAsString
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `RGATreeSplitNodePos` is the position of the text inside the node.
|
|
*/
|
|
class RGATreeSplitNodePos: Equatable {
|
|
/**
|
|
* `id` returns the ID of this RGATreeSplitNodePos.
|
|
*/
|
|
let id: RGATreeSplitNodeID
|
|
|
|
/**
|
|
* `relativeOffset` returns the relative offset of this RGATreeSplitNodePos.
|
|
*/
|
|
let relativeOffset: Int32
|
|
|
|
init(_ id: RGATreeSplitNodeID, _ relativeOffset: Int32) {
|
|
self.id = id
|
|
self.relativeOffset = relativeOffset
|
|
}
|
|
|
|
/**
|
|
* `absoluteID` returns the absolute id of this RGATreeSplitNodePos.
|
|
*/
|
|
var absoluteID: RGATreeSplitNodeID {
|
|
RGATreeSplitNodeID(self.id.createdAt, self.id.offset + self.relativeOffset)
|
|
}
|
|
|
|
/**
|
|
*`structureAsString` returns a String containing
|
|
* the meta data of the position for debugging purpose.
|
|
*/
|
|
var structureAsString: String {
|
|
"\(self.id.structureAsString):\(self.relativeOffset)"
|
|
}
|
|
|
|
/**
|
|
* `==` returns whether given pos equal to this pos or not.
|
|
*/
|
|
static func == (lhs: RGATreeSplitNodePos, rhs: RGATreeSplitNodePos) -> Bool {
|
|
lhs.id == rhs.id && lhs.relativeOffset == rhs.relativeOffset
|
|
}
|
|
}
|
|
|
|
typealias RGATreeSplitNodeRange = (RGATreeSplitNodePos, RGATreeSplitNodePos)
|
|
|
|
/**
|
|
* `RGATreeSplitNode` is a node of RGATreeSplit.
|
|
*/
|
|
class RGATreeSplitNode<T: RGATreeSplitValue>: SplayNode<T> {
|
|
/**
|
|
* `id` returns the ID of this RGATreeSplitNode.
|
|
*/
|
|
public let id: RGATreeSplitNodeID
|
|
|
|
/**
|
|
* `removedAt` returns the remove time of this node.
|
|
*/
|
|
private(set) var removedAt: TimeTicket?
|
|
|
|
/**
|
|
* `prev` returns a previous node of this node.
|
|
*/
|
|
private(set) var prev: RGATreeSplitNode<T>?
|
|
|
|
/**
|
|
* `next` returns a next node of this node.
|
|
*/
|
|
private(set) var next: RGATreeSplitNode<T>? {
|
|
didSet {}
|
|
}
|
|
|
|
/**
|
|
* `insPrev` returns a previous node of this node insertion.
|
|
*/
|
|
private(set) var insPrev: RGATreeSplitNode<T>?
|
|
|
|
/**
|
|
* `insNext` returns a next node of this node insertion.
|
|
*/
|
|
private(set) var insNext: RGATreeSplitNode<T>?
|
|
|
|
init(_ id: RGATreeSplitNodeID, _ value: T? = nil, _ removedAt: TimeTicket? = nil) {
|
|
self.id = id
|
|
self.removedAt = removedAt
|
|
|
|
super.init(value ?? T())
|
|
}
|
|
|
|
/**
|
|
* `createdAt` returns creation time of the Id of RGATreeSplitNode.
|
|
*/
|
|
public var createdAt: TimeTicket {
|
|
self.id.createdAt
|
|
}
|
|
|
|
/**
|
|
* `length` returns the length of this node.
|
|
*/
|
|
override public var length: Int {
|
|
guard self.removedAt == nil else {
|
|
return 0
|
|
}
|
|
|
|
return self.contentLength
|
|
}
|
|
|
|
/**
|
|
* `contentLength` returns the length of this value.
|
|
*/
|
|
public var contentLength: Int {
|
|
self.value.count
|
|
}
|
|
|
|
/**
|
|
* `insPrevID` returns a ID of previous node insertion.
|
|
*/
|
|
public var insPrevID: RGATreeSplitNodeID? {
|
|
self.insPrev?.id
|
|
}
|
|
|
|
/**
|
|
* `setPrev` sets a previous node of this node.
|
|
*/
|
|
public func setPrev(_ node: RGATreeSplitNode<T>?) {
|
|
self.prev = node
|
|
node?.next = self
|
|
}
|
|
|
|
/**
|
|
* `setNext` sets a next node of this node.
|
|
*/
|
|
public func setNext(_ node: RGATreeSplitNode<T>?) {
|
|
self.next = node
|
|
node?.prev = self
|
|
}
|
|
|
|
/**
|
|
* `setInsPrev` sets a previous node of this node insertion.
|
|
*/
|
|
public func setInsPrev(_ node: RGATreeSplitNode<T>?) {
|
|
self.insPrev = node
|
|
node?.insNext = self
|
|
}
|
|
|
|
/**
|
|
* `setInsNext` sets a next node of this node insertion.
|
|
*/
|
|
public func setInsNext(_ node: RGATreeSplitNode<T>?) {
|
|
self.insNext = node
|
|
node?.insPrev = self
|
|
}
|
|
|
|
/**
|
|
* `hasNext` checks if next node exists.
|
|
*/
|
|
public var hasNext: Bool {
|
|
self.next != nil
|
|
}
|
|
|
|
/**
|
|
* `hasInsPrev` checks if previous insertion node exists.
|
|
*/
|
|
public var hasInsPrev: Bool {
|
|
self.insPrev != nil
|
|
}
|
|
|
|
/**
|
|
* `hasInsPrev` checks if previous insertion node exists.
|
|
*/
|
|
public var hasInsNext: Bool {
|
|
self.insNext != nil
|
|
}
|
|
|
|
/**
|
|
* `isRemoved` checks if removed time exists.
|
|
*/
|
|
public var isRemoved: Bool {
|
|
self.removedAt != nil
|
|
}
|
|
|
|
/**
|
|
* `split` creates a new split node of the given offset.
|
|
*/
|
|
public func split(_ offset: Int32) -> RGATreeSplitNode<T> {
|
|
RGATreeSplitNode(
|
|
self.id.split(offset),
|
|
self.splitValue(offset),
|
|
self.removedAt
|
|
)
|
|
}
|
|
|
|
/**
|
|
* `canDelete` checks if node is able to delete.
|
|
*/
|
|
public func canDelete(_ editedAt: TimeTicket, _ latestCreatedAt: TimeTicket) -> Bool {
|
|
!self.createdAt.after(latestCreatedAt) && (self.removedAt == nil || editedAt.after(self.removedAt!))
|
|
}
|
|
|
|
/**
|
|
* `remove` removes node of given edited time.
|
|
*/
|
|
public func remove(_ editedAt: TimeTicket?) {
|
|
self.removedAt = editedAt
|
|
}
|
|
|
|
/**
|
|
* `createRange` creates ranges of RGATreeSplitNodePos.
|
|
*/
|
|
public var createRange: RGATreeSplitNodeRange {
|
|
(RGATreeSplitNodePos(self.id, 0), RGATreeSplitNodePos(self.id, Int32(self.length)))
|
|
}
|
|
|
|
/**
|
|
* `deepcopy` returns a new instance of this RGATreeSplitNode without structural info.
|
|
*/
|
|
public func deepcopy() -> RGATreeSplitNode<T> {
|
|
RGATreeSplitNode(self.id, self.value, self.removedAt)
|
|
}
|
|
|
|
/**
|
|
* `structureAsString` returns a String containing
|
|
* the meta data of the node for debugging purpose.
|
|
*/
|
|
public var structureAsString: String {
|
|
"\(self.id.structureAsString) \(String(describing: self.value))"
|
|
}
|
|
|
|
private func splitValue(_ offset: Int32) -> T {
|
|
let value = self.value
|
|
self.value = value.substring(from: 0, to: Int(offset))
|
|
return value.substring(from: Int(offset), to: value.count)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `RGATreeSplit` is a block-based list with improved index-based lookup in RGA.
|
|
* The difference from RGATreeList is that it has data on a block basis to
|
|
* reduce the size of CRDT metadata. When an edit occurs on a block,
|
|
* the block is split.
|
|
*/
|
|
class RGATreeSplit<T: RGATreeSplitValue> {
|
|
/**
|
|
* `head` returns head of RGATreeSplitNode.
|
|
*/
|
|
private(set) var head: RGATreeSplitNode<T>
|
|
private var treeByIndex: SplayTree<T>
|
|
private var treeByID: LLRBTree<RGATreeSplitNodeID, RGATreeSplitNode<T>>
|
|
private var removedNodeMap = [String: RGATreeSplitNode<T>]()
|
|
|
|
init() {
|
|
self.head = RGATreeSplitNode(RGATreeSplitNodeID.initial)
|
|
self.treeByIndex = SplayTree()
|
|
self.treeByID = LLRBTree<RGATreeSplitNodeID, RGATreeSplitNode<T>>()
|
|
self.treeByIndex.insert(self.head)
|
|
self.treeByID.put(self.head.id, self.head)
|
|
}
|
|
|
|
/**
|
|
* `edit` does following steps
|
|
* 1. split nodes with from and to
|
|
* 2. delete between from and to
|
|
* 3. insert a new node
|
|
* 4. add removed node
|
|
* @param range - range of RGATreeSplitNode
|
|
* @param editedAt - edited time
|
|
* @param value - value
|
|
* @param latestCreatedAtMapByActor - latestCreatedAtMapByActor
|
|
* @returns `[RGATreeSplitNodePos, Map<string, TimeTicket>, Array<Change>]`
|
|
*/
|
|
@discardableResult
|
|
public func edit(_ range: RGATreeSplitNodeRange,
|
|
_ editedAt: TimeTicket,
|
|
_ value: T?,
|
|
_ latestCreatedAtMapByActor: [String: TimeTicket]? = nil) throws -> (RGATreeSplitNodePos, [String: TimeTicket], [ContentChange<T>])
|
|
{
|
|
// 01. split nodes with from and to
|
|
let (toLeft, toRight) = try self.findNodeWithSplit(range.1, editedAt)
|
|
let (fromLeft, fromRight) = try self.findNodeWithSplit(range.0, editedAt)
|
|
|
|
// 02. delete between from and to
|
|
let nodesToDelete = self.findBetween(fromRight, toRight)
|
|
var (changes, latestCreatedAtMap, removedNodeMapByNodeKey) = try self.deleteNodes(nodesToDelete, editedAt, latestCreatedAtMapByActor)
|
|
|
|
let caretID = toRight?.id ?? toLeft.id
|
|
var caretPos = RGATreeSplitNodePos(caretID, 0)
|
|
|
|
// 03. insert a new node
|
|
if let value {
|
|
let idx = try self.findIdxFromNodePos(fromLeft.createRange.1, true)
|
|
|
|
let inserted = self.insertAfter(
|
|
fromLeft,
|
|
RGATreeSplitNode(RGATreeSplitNodeID(editedAt, 0), value)
|
|
)
|
|
|
|
if !changes.isEmpty, changes[changes.count - 1].from == idx {
|
|
changes[changes.count - 1].content = value
|
|
} else {
|
|
changes.append(ContentChange<T>(actor: editedAt.actorID!, from: idx, to: idx, content: value))
|
|
}
|
|
|
|
caretPos = RGATreeSplitNodePos(inserted.id, Int32(inserted.contentLength))
|
|
}
|
|
|
|
// 04. add removed node
|
|
for (key, removedNode) in removedNodeMapByNodeKey {
|
|
self.removedNodeMap[key] = removedNode
|
|
}
|
|
|
|
return (caretPos, latestCreatedAtMap, changes)
|
|
}
|
|
|
|
/**
|
|
* `findNodePos` finds RGATreeSplitNodePos of given offset.
|
|
*/
|
|
public func findNodePos(_ idx: Int) throws -> RGATreeSplitNodePos {
|
|
let (node, offset) = self.treeByIndex.find(idx)
|
|
guard let splitNode = node as? RGATreeSplitNode<T> else {
|
|
throw YorkieError.noSuchElement(message: "no element for index \(idx)")
|
|
}
|
|
|
|
return RGATreeSplitNodePos(splitNode.id, Int32(offset))
|
|
}
|
|
|
|
/**
|
|
* `findIndexesFromRange` finds indexes based on range.
|
|
*/
|
|
public func findIndexesFromRange(_ range: RGATreeSplitNodeRange) throws -> (Int, Int) {
|
|
let (fromPos, toPos) = range
|
|
return try (self.findIdxFromNodePos(fromPos, false), self.findIdxFromNodePos(toPos, true))
|
|
}
|
|
|
|
/**
|
|
* `findIdxFromNodePos` finds index based on node position.
|
|
*/
|
|
public func findIdxFromNodePos(_ pos: RGATreeSplitNodePos, _ preferToLeft: Bool) throws -> Int {
|
|
let absoluteID = pos.absoluteID
|
|
guard let node = preferToLeft ? try? self.findFloorNodePreferToLeft(absoluteID) : self.findFloorNode(absoluteID) else {
|
|
let message = "the node of the given id should be found: \(absoluteID.structureAsString)"
|
|
Logger.critical(message)
|
|
throw YorkieError.noSuchElement(message: message)
|
|
}
|
|
let index = self.treeByIndex.indexOf(node)
|
|
let offset = node.isRemoved ? 0 : absoluteID.offset - node.id.offset
|
|
|
|
return index + Int(offset)
|
|
}
|
|
|
|
/**
|
|
* `findNode` finds node of given id.
|
|
*/
|
|
public func findNode(_ id: RGATreeSplitNodeID) -> RGATreeSplitNode<T>? {
|
|
self.findFloorNode(id)
|
|
}
|
|
|
|
/**
|
|
* `length` returns size of RGATreeList.
|
|
*/
|
|
public var length: Int {
|
|
self.treeByIndex.length
|
|
}
|
|
|
|
/**
|
|
* `checkWeight` returns false when there is an incorrect weight node.
|
|
* for debugging purpose.
|
|
*/
|
|
public func checkWeight() -> Bool {
|
|
self.treeByIndex.checkWeight()
|
|
}
|
|
|
|
/**
|
|
* `toJSON` returns the JSON encoding of this Array.
|
|
*/
|
|
public var toJSON: String {
|
|
var result = [String]()
|
|
|
|
self.forEach {
|
|
if !$0.isRemoved {
|
|
result.append("\($0.value)")
|
|
}
|
|
}
|
|
|
|
return result.joined(separator: "")
|
|
}
|
|
|
|
/**
|
|
* `deepcopy` copies itself deeply.
|
|
*/
|
|
public func deepcopy() -> RGATreeSplit<T> {
|
|
let clone = RGATreeSplit<T>()
|
|
|
|
var node: RGATreeSplitNode<T>? = self.head.next
|
|
var prev: RGATreeSplitNode<T>? = clone.head
|
|
var current: RGATreeSplitNode<T>?
|
|
|
|
while node != nil {
|
|
current = clone.insertAfter(prev!, node!.deepcopy())
|
|
if let insPrevID = node!.insPrevID {
|
|
current?.setInsPrev(clone.findNode(insPrevID))
|
|
}
|
|
|
|
prev = current
|
|
node = node!.next
|
|
}
|
|
|
|
return clone
|
|
}
|
|
|
|
/**
|
|
* `structureAsString` returns a String containing the meta data of the node
|
|
* for debugging purpose.
|
|
*/
|
|
public var structureAsString: String {
|
|
var result = [String]()
|
|
|
|
self.forEach {
|
|
if !$0.isRemoved {
|
|
result.append("[\($0.structureAsString)]")
|
|
} else {
|
|
result.append("{\($0.structureAsString)}")
|
|
}
|
|
}
|
|
|
|
return result.joined(separator: "")
|
|
}
|
|
|
|
/**
|
|
* `insertAfter` inserts the given node after the given previous node.
|
|
*/
|
|
@discardableResult
|
|
public func insertAfter(_ prevNode: RGATreeSplitNode<T>, _ newNode: RGATreeSplitNode<T>) -> RGATreeSplitNode<T> {
|
|
let next = prevNode.next
|
|
newNode.setPrev(prevNode)
|
|
|
|
if next != nil {
|
|
next!.setPrev(newNode)
|
|
}
|
|
|
|
self.treeByID.put(newNode.id, newNode)
|
|
self.treeByIndex.insert(previousNode: prevNode, newNode: newNode)
|
|
|
|
return newNode
|
|
}
|
|
|
|
/**
|
|
* `findNodeWithSplit` splits and return nodes of the given position.
|
|
*/
|
|
public func findNodeWithSplit(_ pos: RGATreeSplitNodePos, _ editedAt: TimeTicket) throws -> (RGATreeSplitNode<T>, RGATreeSplitNode<T>?) {
|
|
let absoluteID = pos.absoluteID
|
|
var node = try self.findFloorNodePreferToLeft(absoluteID)
|
|
let relativeOffset = absoluteID.offset - node.id.offset
|
|
|
|
self.splitNode(node, relativeOffset)
|
|
|
|
while let next = node.next, next.createdAt.after(editedAt) {
|
|
node = next
|
|
}
|
|
|
|
return (node, node.next)
|
|
}
|
|
|
|
private func findFloorNodePreferToLeft(_ id: RGATreeSplitNodeID) throws -> RGATreeSplitNode<T> {
|
|
guard let node = self.findFloorNode(id) else {
|
|
let message = "the node of the given id should be found: \(id.structureAsString)"
|
|
Logger.critical(message)
|
|
throw YorkieError.noSuchElement(message: message)
|
|
}
|
|
|
|
if id.offset > 0, node.id.offset == id.offset {
|
|
// NOTE: InsPrev may not be present due to GC.
|
|
if let insPrev = node.insPrev {
|
|
return insPrev
|
|
}
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
private func findFloorNode(_ id: RGATreeSplitNodeID) -> RGATreeSplitNode<T>? {
|
|
guard let entry = self.treeByID.floorEntry(id) else {
|
|
return nil
|
|
}
|
|
|
|
if !(entry.key == id), !entry.key.hasSameCreatedAt(id) {
|
|
return nil
|
|
}
|
|
|
|
return entry.value
|
|
}
|
|
|
|
/**
|
|
* `findBetween` returns nodes between fromNode and toNode.
|
|
*/
|
|
public func findBetween(_ fromNode: RGATreeSplitNode<T>?, _ toNode: RGATreeSplitNode<T>?) -> [RGATreeSplitNode<T>] {
|
|
var nodes = [RGATreeSplitNode<T>]()
|
|
|
|
var current: RGATreeSplitNode<T>? = fromNode
|
|
while current != nil, current! !== toNode {
|
|
nodes.append(current!)
|
|
current = current!.next
|
|
}
|
|
|
|
return nodes
|
|
}
|
|
|
|
@discardableResult
|
|
private func splitNode(_ node: RGATreeSplitNode<T>, _ offset: Int32) -> RGATreeSplitNode<T>? {
|
|
guard offset <= node.contentLength else {
|
|
Logger.critical("offset should be less than or equal to length")
|
|
|
|
return nil
|
|
}
|
|
|
|
if offset == 0 {
|
|
return node
|
|
} else if offset == node.contentLength {
|
|
return node.next
|
|
}
|
|
|
|
let splitNode = node.split(offset)
|
|
self.treeByIndex.updateWeight(splitNode)
|
|
self.insertAfter(node, splitNode)
|
|
|
|
if node.hasInsNext {
|
|
node.insNext!.setInsPrev(splitNode)
|
|
}
|
|
splitNode.setInsPrev(node)
|
|
|
|
return splitNode
|
|
}
|
|
|
|
private func deleteNodes(_ candidates: [RGATreeSplitNode<T>],
|
|
_ editedAt: TimeTicket,
|
|
_ latestCreatedAtMapByActor: [String: TimeTicket]?) throws -> ([ContentChange<T>],
|
|
[String: TimeTicket],
|
|
[String: RGATreeSplitNode<T>])
|
|
{
|
|
guard !candidates.isEmpty else {
|
|
return ([], [:], [:])
|
|
}
|
|
|
|
// There are 2 types of nodes in `candidates`: should delete, should not delete.
|
|
// `nodesToKeep` contains nodes should not delete,
|
|
// then is used to find the boundary of the range to be deleted.
|
|
let (nodesToDelete, nodesToKeep) = try self.filterNodes(candidates, editedAt, latestCreatedAtMapByActor)
|
|
|
|
var createdAtMapByActor = [ActorID: TimeTicket]()
|
|
var removedNodeMap = [ActorID: RGATreeSplitNode<T>]()
|
|
// First we need to collect indexes for change.
|
|
let changes = try self.makeChanges(nodesToKeep, editedAt)
|
|
|
|
for node in nodesToDelete {
|
|
// Then make nodes be tombstones and map that.
|
|
if let actorID = node.createdAt.actorID {
|
|
if createdAtMapByActor[actorID] == nil ||
|
|
node.id.createdAt.after(createdAtMapByActor[actorID]!)
|
|
{
|
|
createdAtMapByActor[actorID] = node.id.createdAt
|
|
}
|
|
}
|
|
removedNodeMap[node.id.structureAsString] = node
|
|
node.remove(editedAt)
|
|
}
|
|
// Finally remove index nodes of tombstones.
|
|
self.deleteIndexNodes(nodesToKeep)
|
|
|
|
return (changes, createdAtMapByActor, removedNodeMap)
|
|
}
|
|
|
|
private func filterNodes(_ candidates: [RGATreeSplitNode<T>],
|
|
_ editedAt: TimeTicket,
|
|
_ latestCreatedAtMapByActor: [ActorID: TimeTicket]?) throws -> ([RGATreeSplitNode<T>], [RGATreeSplitNode<T>?])
|
|
{
|
|
let isRemote = latestCreatedAtMapByActor != nil
|
|
var nodesToDelete = [RGATreeSplitNode<T>]()
|
|
var nodesToKeep = [RGATreeSplitNode<T>?]()
|
|
|
|
let (leftEdge, rightEdge) = try self.findEdgesOfCandidates(candidates)
|
|
nodesToKeep.append(leftEdge)
|
|
|
|
for node in candidates {
|
|
let latestCreatedAt: TimeTicket
|
|
|
|
if isRemote {
|
|
if let actorID = node.createdAt.actorID, let latest = latestCreatedAtMapByActor?[actorID] {
|
|
latestCreatedAt = latest
|
|
} else {
|
|
latestCreatedAt = TimeTicket.initial
|
|
}
|
|
} else {
|
|
latestCreatedAt = TimeTicket.max
|
|
}
|
|
|
|
if node.canDelete(editedAt, latestCreatedAt) {
|
|
nodesToDelete.append(node)
|
|
} else {
|
|
nodesToKeep.append(node)
|
|
}
|
|
}
|
|
nodesToKeep.append(rightEdge)
|
|
|
|
return (nodesToDelete, nodesToKeep)
|
|
}
|
|
|
|
/**
|
|
* `findEdgesOfCandidates` finds the edges outside `candidates`,
|
|
* (which has not already been deleted, or be undefined but not yet implemented)
|
|
* right edge is undefined means `candidates` contains the end of text.
|
|
*/
|
|
private func findEdgesOfCandidates(_ candidates: [RGATreeSplitNode<T>]) throws -> (RGATreeSplitNode<T>, RGATreeSplitNode<T>?) {
|
|
guard let prev = candidates[0].prev else {
|
|
throw YorkieError.noSuchElement(message: "prev must not nil!")
|
|
}
|
|
|
|
return (prev, candidates[safe: candidates.count - 1]?.next)
|
|
}
|
|
|
|
private func makeChanges(_ boundaries: [RGATreeSplitNode<T>?], _ editedAt: TimeTicket) throws -> [ContentChange<T>] {
|
|
var changes = [ContentChange<T>]()
|
|
var fromIdx: Int, toIdx: Int
|
|
|
|
for index in 0 ..< (boundaries.count - 1) {
|
|
guard let leftBoundary = boundaries[index] else {
|
|
continue
|
|
}
|
|
|
|
let rightBoundary = boundaries[index + 1]
|
|
|
|
if leftBoundary.next === rightBoundary {
|
|
continue
|
|
}
|
|
|
|
guard let range = leftBoundary.next?.createRange else {
|
|
throw YorkieError.noSuchElement(message: "The next node of leftBoundary is nil")
|
|
}
|
|
|
|
fromIdx = try self.findIndexesFromRange(range).0
|
|
if rightBoundary != nil {
|
|
guard let range = rightBoundary!.prev?.createRange else {
|
|
throw YorkieError.noSuchElement(message: "The prev node of rightBoundary is nil")
|
|
}
|
|
|
|
toIdx = try self.findIndexesFromRange(range).1
|
|
} else {
|
|
toIdx = self.treeByIndex.length
|
|
}
|
|
|
|
if fromIdx < toIdx {
|
|
changes.append(ContentChange<T>(actor: editedAt.actorID!, from: fromIdx, to: toIdx, content: nil))
|
|
}
|
|
}
|
|
|
|
return changes.reversed()
|
|
}
|
|
|
|
/**
|
|
* `deleteIndexNodes` clears the index nodes of the given deletion boundaries.
|
|
* The boundaries mean the nodes that will not be deleted in the range.
|
|
*/
|
|
private func deleteIndexNodes(_ boundaries: [RGATreeSplitNode<T>?]) {
|
|
for index in 0 ..< (boundaries.count - 1) {
|
|
let leftBoundary = boundaries[index]
|
|
let rightBoundary = boundaries[index + 1]
|
|
// If there is no node to delete between boundaries, do notting.
|
|
if leftBoundary != nil, leftBoundary!.next !== rightBoundary {
|
|
self.treeByIndex.cutOffRange(leftBoundary!, rightBoundary)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `removedNodesLength` returns size of removed nodes.
|
|
*/
|
|
public var removedNodesLength: Int {
|
|
self.removedNodeMap.count
|
|
}
|
|
|
|
/**
|
|
* `purgeTextNodesWithGarbage` physically purges nodes that have been removed.
|
|
*/
|
|
public func purgeTextNodesWithGarbage(_ ticket: TimeTicket) -> Int {
|
|
var count = 0
|
|
|
|
for (_, node) in self.removedNodeMap {
|
|
if let removedAt = node.removedAt, ticket >= removedAt {
|
|
self.treeByIndex.delete(node)
|
|
self.purge(node)
|
|
self.treeByID.remove(node.id)
|
|
self.removedNodeMap.removeValue(forKey: node.id.structureAsString)
|
|
count += 1
|
|
}
|
|
}
|
|
|
|
return count
|
|
}
|
|
|
|
/**
|
|
* `purge` physically purges the given node from RGATreeSplit.
|
|
*/
|
|
public func purge(_ node: RGATreeSplitNode<T>) {
|
|
node.prev?.setNext(node.next)
|
|
node.next?.setPrev(node.prev)
|
|
|
|
node.setPrev(nil)
|
|
node.setNext(nil)
|
|
|
|
node.insPrev?.setInsNext(node.insNext)
|
|
node.insNext?.setInsPrev(node.insPrev)
|
|
|
|
node.setInsPrev(nil)
|
|
node.setInsNext(nil)
|
|
}
|
|
}
|
|
|
|
extension RGATreeSplit: Sequence {
|
|
public func makeIterator() -> NodeIterator {
|
|
NodeIterator(head: self.head)
|
|
}
|
|
|
|
public class NodeIterator: IteratorProtocol {
|
|
// swiftlint: disable nesting
|
|
typealias Element = RGATreeSplitNode<T>
|
|
|
|
private var head: Element?
|
|
|
|
init(head: Element?) {
|
|
self.head = head
|
|
}
|
|
|
|
public func next() -> Element? {
|
|
let next = self.head
|
|
self.head = self.head?.next
|
|
return next
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `Selection` represents the selection of text range in the editor.
|
|
*/
|
|
class Selection {
|
|
private let from: RGATreeSplitNodePos
|
|
private let to: RGATreeSplitNodePos
|
|
/**
|
|
* `updatedAt` returns update time of this selection.
|
|
*/
|
|
public let updatedAt: TimeTicket
|
|
|
|
init(_ from: RGATreeSplitNodePos, _ to: RGATreeSplitNodePos, _ updatedAt: TimeTicket) {
|
|
self.from = from
|
|
self.to = to
|
|
self.updatedAt = updatedAt
|
|
}
|
|
|
|
convenience init(_ range: RGATreeSplitNodeRange, _ updatedAt: TimeTicket) {
|
|
self.init(range.0, range.1, updatedAt)
|
|
}
|
|
}
|