Skip to content

Commit 2486baa

Browse files
committed
Document Iterator Structs, Recursion Depth Limit
1 parent 4d35e1b commit 2486baa

File tree

1 file changed

+35
-5
lines changed

1 file changed

+35
-5
lines changed

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Iterator.swift

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,20 @@ public extension TextLayoutManager {
3232
func linesStartingAt(_ minY: CGFloat, until maxY: CGFloat) -> YPositionIterator {
3333
YPositionIterator(minY: minY, maxY: maxY, layoutManager: self)
3434
}
35-
35+
/// Iterate over all lines that overlap a document range.
36+
/// - Parameters:
37+
/// - range: The range in the document to iterate over.
38+
/// - Returns: An iterator for lines in the range. The iterator returns lines that *overlap* with the range.
39+
/// Returned lines may extend slightly before or after the queried range.
3640
func linesInRange(_ range: NSRange) -> RangeIterator {
3741
RangeIterator(range: range, layoutManager: self)
3842
}
3943

44+
/// This iterator iterates over "visible" text positions that overlap a range of vertical `y` positions
45+
/// using ``TextLayoutManager/determineVisiblePosition(for:)``.
46+
///
47+
/// Next elements are retrieved lazily. Additionally, this iterator uses a stable `index` rather than a y position
48+
/// or a range to fetch the next line. This means the line storage can be updated during iteration.
4049
struct YPositionIterator: LazySequenceProtocol, IteratorProtocol {
4150
typealias TextLinePosition = TextLineStorage<TextLine>.TextLinePosition
4251

@@ -72,6 +81,11 @@ public extension TextLayoutManager {
7281
}
7382
}
7483

84+
/// This iterator iterates over "visible" text positions that overlap a document using
85+
/// ``TextLayoutManager/determineVisiblePosition(for:)``.
86+
///
87+
/// Next elements are retrieved lazily. Additionally, this iterator uses a stable `index` rather than a y position
88+
/// or a range to fetch the next line. This means the line storage can be updated during iteration.
7589
struct RangeIterator: LazySequenceProtocol, IteratorProtocol {
7690
typealias TextLinePosition = TextLineStorage<TextLine>.TextLinePosition
7791

@@ -139,12 +153,25 @@ public extension TextLayoutManager {
139153
for originalPosition: TextLineStorage<TextLine>.TextLinePosition?
140154
) -> (position: TextLineStorage<TextLine>.TextLinePosition, indexRange: ClosedRange<Int>)? {
141155
guard let originalPosition else { return nil }
142-
return determineVisiblePosition(for: (originalPosition, originalPosition.index...originalPosition.index))
156+
return determineVisiblePositionRecursively(
157+
for: (originalPosition, originalPosition.index...originalPosition.index),
158+
recursionDepth: 0
159+
)
143160
}
144161

145-
func determineVisiblePosition(
146-
for originalPosition: (position: TextLineStorage<TextLine>.TextLinePosition, indexRange: ClosedRange<Int>)
162+
/// Private implementation of ``TextLayoutManager/determineVisiblePosition(for:)``.
163+
///
164+
/// Separated for readability. This method does not have an optional parameter, and keeps track of a recursion depth.
165+
private func determineVisiblePositionRecursively(
166+
for originalPosition: (position: TextLineStorage<TextLine>.TextLinePosition, indexRange: ClosedRange<Int>),
167+
recursionDepth: Int
147168
) -> (position: TextLineStorage<TextLine>.TextLinePosition, indexRange: ClosedRange<Int>)? {
169+
// Arbitrary max recursion depth. Ensures we don't spiral into in an infinite recursion.
170+
guard recursionDepth < 10 else {
171+
logger.warning("Visible position recursed for over 10 levels, returning early.")
172+
return originalPosition
173+
}
174+
148175
let attachments = attachments.get(overlapping: originalPosition.position.range)
149176
guard let firstAttachment = attachments.first, let lastAttachment = attachments.last else {
150177
// No change, either no attachments or attachment doesn't span multiple lines.
@@ -184,7 +211,10 @@ public extension TextLayoutManager {
184211
return (newPosition, minIndex...maxIndex)
185212
} else {
186213
// Recurse, to make sure we combine all necessary lines.
187-
return determineVisiblePosition(for: (newPosition, minIndex...maxIndex))
214+
return determineVisiblePositionRecursively(
215+
for: (newPosition, minIndex...maxIndex),
216+
recursionDepth: recursionDepth + 1
217+
)
188218
}
189219
}
190220
}

0 commit comments

Comments
 (0)