Skip to content

Commit f878527

Browse files
replaceString was crashing on acceptable input
1 parent ec8e5f8 commit f878527

File tree

3 files changed

+46
-6
lines changed

3 files changed

+46
-6
lines changed

TextViewPlus/NSTextView+AttributedString.swift

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,31 @@ public extension NSTextView {
2020
didChangeText()
2121
}
2222

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+
2338
func replaceString(in range: NSRange, with attributedString: NSAttributedString) {
2439
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()
2842

2943
let inverseLength = attributedString.length
3044
let inverseRange = NSRange(location: range.location, length: inverseLength)
3145

3246
manager.registerUndo(withTarget: self, handler: { (view) in
33-
view.replaceString(in: inverseRange, with: originalString)
47+
view.replaceString(in: inverseRange, with: usableReplacementString)
3448
})
3549
}
3650

TextViewPlus/TextViewPlus.xcconfig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
PRODUCT_NAME = TextViewPlus
1313
PRODUCT_BUNDLE_IDENTIFIER = com.chimehq.TextViewPlus
1414
PRODUCT_MODULE_NAME = TextViewPlus
15-
CURRENT_PROJECT_VERSION = 2
16-
MARKETING_VERSION = 1.0
15+
CURRENT_PROJECT_VERSION = 3
16+
MARKETING_VERSION = 1.0.1
1717

1818
INFOPLIST_FILE = TextViewPlus/Info.plist
1919
FRAMEWORK_SEARCH_PATHS = $(PROJECT_DIR)/Carthage/Build/Mac $(PROJECT_DIR)/Carthage/Build/Mac/Static

TextViewPlusTests/TextViewPlusTests.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,30 @@ class TextViewPlusTests: XCTestCase {
2222

2323
XCTAssertEqual(textView.string, "abc")
2424
}
25+
26+
func testProgrammaticModificationOfAttributedStringWithZeroLengthRange() {
27+
let textView = TestableTextView(string: "abc")
28+
29+
let attrString = NSAttributedString(string: "z")
30+
textView.replaceString(in: NSRange(location: 1, length: 0), with: attrString)
31+
32+
XCTAssertEqual(textView.string, "azbc")
33+
34+
textView.undoManager!.undo()
35+
36+
XCTAssertEqual(textView.string, "abc")
37+
}
38+
39+
func testProgrammaticModificationOfAttributedStringWithFullRange() {
40+
let textView = TestableTextView(string: "abc")
41+
42+
let attrString = NSAttributedString(string: "def")
43+
textView.replaceString(in: NSRange(location: 0, length: 3), with: attrString)
44+
45+
XCTAssertEqual(textView.string, "def")
46+
47+
textView.undoManager!.undo()
48+
49+
XCTAssertEqual(textView.string, "abc")
50+
}
2551
}

0 commit comments

Comments
 (0)