@@ -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