@@ -20,17 +20,31 @@ public extension NSTextView {
20
20
didChangeText ( )
21
21
}
22
22
23
+ // swiftlint:disable line_length
24
+ private func safeAttributedSubstring( forProposedRange range: NSRange , actualRange: NSRangePointer ? ) -> NSAttributedString ? {
25
+ // attributedSubstring returns nil for out-of-range AND for length == 0. But, zero-length ranges
26
+ // are totally acceptable, and I'm not sure that's reasonable behavior.
27
+ let maxLength = textStorage? . length ?? 0
28
+
29
+ guard NSMaxRange ( range) <= maxLength else {
30
+ fatalError ( " Range out of bounds for underlying storage " )
31
+ }
32
+
33
+ // also, we must have a copy, as the underlying TextStorage can change, and it seems the
34
+ // string is changing along with it.
35
+ return attributedSubstring ( forProposedRange: range, actualRange: actualRange) ? . copy ( ) as? NSAttributedString
36
+ }
37
+
23
38
func replaceString( in range: NSRange , with attributedString: NSAttributedString ) {
24
39
if let manager = undoManager {
25
- guard let originalString = attributedSubstring ( forProposedRange: range, actualRange: nil ) else {
26
- fatalError ( " Range invalid for string " )
27
- }
40
+ let originalString = safeAttributedSubstring ( forProposedRange: range, actualRange: nil )
41
+ let usableReplacementString = originalString ?? NSAttributedString ( )
28
42
29
43
let inverseLength = attributedString. length
30
44
let inverseRange = NSRange ( location: range. location, length: inverseLength)
31
45
32
46
manager. registerUndo ( withTarget: self , handler: { ( view) in
33
- view. replaceString ( in: inverseRange, with: originalString )
47
+ view. replaceString ( in: inverseRange, with: usableReplacementString )
34
48
} )
35
49
}
36
50
0 commit comments