Skip to content

Commit 1d09ede

Browse files
committed
Fix Some Typesetting Bugs, Add RangeIterator
1 parent 639104a commit 1d09ede

File tree

11 files changed

+92
-28
lines changed

11 files changed

+92
-28
lines changed

Sources/CodeEditTextView/Extensions/CTTypesetter+SuggestLineBreak.swift

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,20 @@ extension CTTypesetter {
1818
func suggestLineBreak(
1919
using string: NSAttributedString,
2020
strategy: LineBreakStrategy,
21-
startingOffset: Int,
21+
subrange: NSRange,
2222
constrainingWidth: CGFloat
2323
) -> Int {
2424
switch strategy {
2525
case .character:
2626
return suggestLineBreakForCharacter(
2727
string: string,
28-
startingOffset: startingOffset,
28+
startingOffset: subrange.location,
2929
constrainingWidth: constrainingWidth
3030
)
3131
case .word:
3232
return suggestLineBreakForWord(
3333
string: string,
34-
startingOffset: startingOffset,
34+
subrange: subrange,
3535
constrainingWidth: constrainingWidth
3636
)
3737
}
@@ -72,11 +72,11 @@ extension CTTypesetter {
7272
/// - Returns: An offset relative to the entire string indicating where to break.
7373
private func suggestLineBreakForWord(
7474
string: NSAttributedString,
75-
startingOffset: Int,
75+
subrange: NSRange,
7676
constrainingWidth: CGFloat
7777
) -> Int {
78-
var breakIndex = startingOffset + CTTypesetterSuggestClusterBreak(self, startingOffset, constrainingWidth)
79-
let isBreakAtEndOfString = breakIndex >= string.length
78+
var breakIndex = subrange.location + CTTypesetterSuggestClusterBreak(self, subrange.location, constrainingWidth)
79+
let isBreakAtEndOfString = breakIndex >= subrange.max
8080

8181
let isNextCharacterCarriageReturn = checkIfLineBreakOnCRLF(breakIndex, for: string)
8282
if isNextCharacterCarriageReturn {
@@ -92,7 +92,7 @@ extension CTTypesetter {
9292
// Try to walk backwards until we hit a whitespace or punctuation
9393
var index = breakIndex - 1
9494

95-
while breakIndex - index < 100 && index > startingOffset {
95+
while breakIndex - index < 100 && index > subrange.location {
9696
if ensureCharacterCanBreakLine(at: index, for: string) {
9797
return index + 1
9898
}
@@ -107,9 +107,8 @@ extension CTTypesetter {
107107
/// - Parameter index: The index to check at.
108108
/// - Returns: True, if the character is a whitespace or punctuation character.
109109
private func ensureCharacterCanBreakLine(at index: Int, for string: NSAttributedString) -> Bool {
110-
let set = CharacterSet(
111-
charactersIn: string.attributedSubstring(from: NSRange(location: index, length: 1)).string
112-
)
110+
let subrange = (string.string as NSString).rangeOfComposedCharacterSequence(at: index)
111+
let set = CharacterSet(charactersIn: (string.string as NSString).substring(with: subrange))
113112
return set.isSubset(of: .whitespacesAndNewlines) || set.isSubset(of: .punctuationCharacters)
114113
}
115114

Sources/CodeEditTextView/TextLayoutManager/TextAttachments/TextAttachment.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import AppKit
99

1010
public struct TextAttachmentBox: Equatable {
11-
let range: NSRange
11+
var range: NSRange
1212
let attachment: any TextAttachment
1313

1414
var width: CGFloat {

Sources/CodeEditTextView/TextLayoutManager/TextAttachments/TextAttachmentManager.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ public final class TextAttachmentManager {
9595

9696
return results
9797
}
98+
99+
package func textUpdated(atOffset: Int, delta: Int) {
100+
for (idx, box) in orderedAttachments.enumerated().reversed() {
101+
if box.range.contains(atOffset) {
102+
orderedAttachments.remove(at: idx)
103+
} else if box.range.location > atOffset {
104+
orderedAttachments[idx].range.location += delta
105+
}
106+
}
107+
}
98108
}
99109

100110
private extension TextAttachmentManager {

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Edits.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ extension TextLayoutManager: NSTextStorageDelegate {
4646
removeLayoutLinesIn(range: insertedStringRange)
4747
insertNewLines(for: editedRange)
4848

49-
attachments.get(overlapping: insertedStringRange).forEach { attachment in
50-
attachments.remove(atOffset: attachment.range.location)
51-
}
49+
attachments.textUpdated(atOffset: editedRange.location, delta: delta)
5250

5351
invalidateLayoutForRange(insertedStringRange)
5452
}

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Iterator.swift

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,30 @@ public extension TextLayoutManager {
1414
/// if there is no delegate from `0` to the estimated document height.
1515
///
1616
/// - Returns: An iterator to iterate through all visible lines.
17-
func visibleLines() -> Iterator {
17+
func visibleLines() -> YPositionIterator {
1818
let visibleRect = delegate?.visibleRect ?? NSRect(
1919
x: 0,
2020
y: 0,
2121
width: 0,
2222
height: estimatedHeight()
2323
)
24-
return Iterator(minY: max(visibleRect.minY, 0), maxY: max(visibleRect.maxY, 0), layoutManager: self)
24+
return YPositionIterator(minY: max(visibleRect.minY, 0), maxY: max(visibleRect.maxY, 0), layoutManager: self)
2525
}
2626

2727
/// Iterate over all lines in the y position range.
2828
/// - Parameters:
2929
/// - minY: The minimum y position to begin at.
3030
/// - maxY: The maximum y position to iterate to.
3131
/// - Returns: An iterator that will iterate through all text lines in the y position range.
32-
func linesStartingAt(_ minY: CGFloat, until maxY: CGFloat) -> Iterator {
33-
Iterator(minY: minY, maxY: maxY, layoutManager: self)
32+
func linesStartingAt(_ minY: CGFloat, until maxY: CGFloat) -> YPositionIterator {
33+
YPositionIterator(minY: minY, maxY: maxY, layoutManager: self)
3434
}
3535

36-
struct Iterator: LazySequenceProtocol, IteratorProtocol {
36+
func linesInRange(_ range: NSRange) -> RangeIterator {
37+
RangeIterator(range: range, layoutManager: self)
38+
}
39+
40+
struct YPositionIterator: LazySequenceProtocol, IteratorProtocol {
3741
typealias TextLinePosition = TextLineStorage<TextLine>.TextLinePosition
3842

3943
private weak var layoutManager: TextLayoutManager?
@@ -68,6 +72,39 @@ public extension TextLayoutManager {
6872
}
6973
}
7074

75+
struct RangeIterator: LazySequenceProtocol, IteratorProtocol {
76+
typealias TextLinePosition = TextLineStorage<TextLine>.TextLinePosition
77+
78+
private weak var layoutManager: TextLayoutManager?
79+
private let range: NSRange
80+
private var currentPosition: (position: TextLinePosition, indexRange: ClosedRange<Int>)?
81+
82+
init(range: NSRange, layoutManager: TextLayoutManager) {
83+
self.range = range
84+
self.layoutManager = layoutManager
85+
}
86+
87+
/// Iterates over the "visible" text positions.
88+
///
89+
/// See documentation on ``TextLayoutManager/determineVisiblePosition(for:)`` for details.
90+
public mutating func next() -> TextLineStorage<TextLine>.TextLinePosition? {
91+
if let currentPosition {
92+
guard let nextPosition = layoutManager?.lineStorage.getLine(
93+
atIndex: currentPosition.indexRange.upperBound + 1
94+
), nextPosition.range.location < range.max else {
95+
return nil
96+
}
97+
self.currentPosition = layoutManager?.determineVisiblePosition(for: nextPosition)
98+
return self.currentPosition?.position
99+
} else if let position = layoutManager?.lineStorage.getLine(atOffset: range.location) {
100+
currentPosition = layoutManager?.determineVisiblePosition(for: position)
101+
return currentPosition?.position
102+
}
103+
104+
return nil
105+
}
106+
}
107+
71108
/// Determines the “visible” line position by merging any consecutive lines
72109
/// that are spanned by text attachments. If an attachment overlaps beyond the
73110
/// bounds of the original line, this method will extend the returned range to

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Layout.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ extension TextLayoutManager {
7777
let minY = max(visibleRect.minY - verticalLayoutPadding, 0)
7878
let maxY = max(visibleRect.maxY + verticalLayoutPadding, 0)
7979
let originalHeight = lineStorage.height
80-
var usedFragmentIDs = Set<UUID>()
80+
var usedFragmentIDs = Set<LineFragment.ID>()
8181
var forceLayout: Bool = needsLayout
8282
var newVisibleLines: Set<TextLine.ID> = []
8383
var yContentAdjustment: CGFloat = 0
@@ -162,7 +162,7 @@ extension TextLayoutManager {
162162
_ position: TextLineStorage<TextLine>.TextLinePosition,
163163
textStorage: NSTextStorage,
164164
layoutData: LineLayoutData,
165-
laidOutFragmentIDs: inout Set<UUID>
165+
laidOutFragmentIDs: inout Set<LineFragment.ID>
166166
) -> CGSize {
167167
let lineDisplayData = TextLine.DisplayData(
168168
maxWidth: layoutData.maxWidth,

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public class TextLayoutManager: NSObject {
7171
weak var textStorage: NSTextStorage?
7272
var lineStorage: TextLineStorage<TextLine> = TextLineStorage()
7373
var markedTextManager: MarkedTextManager = MarkedTextManager()
74-
let viewReuseQueue: ViewReuseQueue<LineFragmentView, UUID> = ViewReuseQueue()
74+
let viewReuseQueue: ViewReuseQueue<LineFragmentView, LineFragment.ID> = ViewReuseQueue()
7575
package var visibleLineIds: Set<TextLine.ID> = []
7676
/// Used to force a complete re-layout using `setNeedsLayout`
7777
package var needsLayout: Bool = false

Sources/CodeEditTextView/TextLine/Typesetter/Typesetter.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ final public class Typesetter {
4848
attachments: attachments
4949
)
5050
lineFragments.build(from: lines, estimatedLineHeight: maxHeight)
51-
5251
}
5352

5453
private func makeString(string: NSAttributedString, markedRanges: MarkedRanges?) {
@@ -153,14 +152,16 @@ final public class Typesetter {
153152
typesetter: CTTypesetter,
154153
displayData: TextLine.DisplayData
155154
) {
155+
let substring = string.attributedSubstring(from: range)
156+
156157
// Layout as many fragments as possible in this content run
157158
while context.currentPosition < range.max {
158159
// The line break indicates the distance from the range we’re typesetting on that should be broken at.
159160
// It's relative to the range being typeset, not the line
160161
let lineBreak = typesetter.suggestLineBreak(
161-
using: string,
162+
using: substring,
162163
strategy: displayData.breakStrategy,
163-
startingOffset: context.currentPosition - range.location,
164+
subrange: NSRange(start: context.currentPosition - range.location, end: range.length),
164165
constrainingWidth: displayData.maxWidth - context.fragmentContext.width
165166
)
166167

Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+Draw.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ extension TextSelectionManager {
1313
public func drawSelections(in rect: NSRect) {
1414
guard let context = NSGraphicsContext.current?.cgContext else { return }
1515
context.saveGState()
16-
var highlightedLines: Set<UUID> = []
16+
var highlightedLines: Set<TextLine.ID> = []
1717
// For each selection in the rect
1818
for textSelection in textSelections {
1919
if textSelection.range.isEmpty {
@@ -41,7 +41,7 @@ extension TextSelectionManager {
4141
in rect: NSRect,
4242
for textSelection: TextSelection,
4343
context: CGContext,
44-
highlightedLines: inout Set<UUID>
44+
highlightedLines: inout Set<TextLine.ID>
4545
) {
4646
guard let linePosition = layoutManager?.textLineForOffset(textSelection.range.location),
4747
!highlightedLines.contains(linePosition.data.id) else {

Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ extension TextSelectionManager {
3737
height: rect.height
3838
).intersection(rect)
3939

40-
for linePosition in layoutManager.lineStorage.linesInRange(range) {
40+
for linePosition in layoutManager.linesInRange(range) {
4141
fillRects.append(
4242
contentsOf: getFillRects(in: validTextDrawingRect, selectionRange: range, forPosition: linePosition)
4343
)

0 commit comments

Comments
 (0)