Three review agents assessed the TextRope and RopeBuffer implementation. Findings below, grouped by priority.
TextRope+Delete.swift has its own leafSplitPoint that does NOT check for \r\n boundaries. The construction and insert paths both respect this invariant. After a delete triggers a leaf merge that exceeds maxChunkUTF8, the re-split can place \r and \n in separate chunks.
Fix: Add the same \r\n boundary check from TextRope+Construction.swift's splitPoint to leafSplitPoint in the delete file.
insert(_:at:), delete(in:), and content(in:) accept negative or out-of-range offsets silently. Negative offsets cause undefined behavior; offsets beyond utf16Count silently append or truncate.
Fix: Add precondition guards at the top of each public method:
precondition(utf16Offset >= 0 && utf16Offset <= utf16Count)
precondition(utf16Range.location >= 0 && utf16Range.location + utf16Range.length <= utf16Count)MutableStringBuffer has init<Wrapped: Buffer>(copying:). RopeBuffer does not. Generic code that copies any Buffer will fail to compile with RopeBuffer.
Fix:
public convenience init<Wrapped: Buffer>(
copying buffer: Wrapped
) where Wrapped: Buffer, Wrapped.Range == NSRange, Wrapped.Content == String {
self.init(buffer.content)
self.selectedRange = buffer.selectedRange
}Tests only check rope.content — never verify tree structure. Rebalancing bugs could silently violate height balance, child count (4–8), or leaf size (1024–2048 bytes) while content stays correct.
Fix: Write an internal verifyTreeInvariants(_:) helper that recursively checks:
- All leaves at same depth
- Inner nodes have
minChildren...maxChildrenchildren (except root) - Leaf chunks between
minChunkUTF8andmaxChunkUTF8(except single-leaf trees) - Summary matches actual content at every node
Use it in all rebalancing-related tests.
MutableStringBuffer has the «selection» / ˇinsertion debug format. RopeBuffer prints as a generic class reference. This breaks assertBufferState readability for RopeBuffer.
Fix: Add the same conformance from MutableStringBuffer (uses NSMutableString on self.content).
The default TextAnalysisCapable implementation calls self.content (O(n) full reconstruction) on every wordRange call. MutableStringBuffer doesn't have this problem because its content is a stored property.
Fix (short-term): Override wordRange(for:) in RopeBuffer to extract only the relevant portion via content(in:). Investigate: the free function computeWordRange in Buffer+wordRange.swift takes an NSString — can we pass a smaller substring?
TextRope.content concatenates all leaves on every access. Consumers may call it in tight loops expecting O(1). RopeBuffer.content delegates to it.
Investigate: Consider whether to rename to a method (makeContent()) to signal cost, or cache with invalidation. The Equatable conformance on TextRope also calls content — could be expensive on large ropes.
mergeUndersizedLeaves and mergeUndersizedInnerNodes in TextRope+Delete.swift have multiple branches (merge succeeds, merge fails because combined too large, split after merge, parent undersize propagation). None are tested in isolation.
Fix: Add targeted tests that construct ropes with specific leaf/node sizes to exercise each branch. Use tree invariant validator (#4) to verify results.
RopeBufferDriftTests has 10 tests. BufferBehaviorDriftTests has ~20 scenarios. Missing from RopeBuffer drift tests:
- Insert at selection start / within selection / at selection end
- Delete overlapping selection start / overlapping selection end
- Replace with shorter / longer / multi-byte content
- Mixed insert + delete accumulation
Fix: Port remaining scenarios from BufferBehaviorDriftTests.
Tests use emoji and CJK but never position them at exact chunk boundaries (2048 bytes). A surrogate pair or \r\n landing exactly at a split point during insert or delete could expose bugs not caught by random stress testing.
Fix: Add tests that construct strings with multi-byte characters at positions 2046–2050 (near maxChunkUTF8), then insert/delete around those positions.
testRandomOperationsMatchString uses SeededRNG(state: 42) only. A failure won't log which seed/iteration failed.
Fix: Run multiple seeds (e.g., 0, 42, 12345, UInt64.max). On failure, log seed and iteration number in the XCTFail message.
Summary.lines is computed and maintained but TextRope has no public lineCount property, unlike utf16Count and utf8Count.
Fix: public var lineCount: Int { root.summary.lines }
Not a bug — returns .zero correctly — but skipping the withUTF8 closure for empty strings is a trivial optimization.
TextRope.root is nonisolated(unsafe) for Sendable conformance. Concurrent mutations are undefined. This is acceptable for value-type COW semantics but should be documented.
lhs.root === rhs.root || lhs.content == rhs.content — the reference check is a fast path for COW copies. Semantically correct but calls O(n) content on the slow path. Consider whether this is acceptable for large ropes, or whether a tree-structural comparison would be better.
TextRope+Insert.swift uses remaining < childUTF16 || i == node.children.count - 1 to select the target child. Works correctly but the fallthrough-to-last-child logic is subtle. Consider making the append-at-end case explicit.