I have been getting crash reports from users of my Mac app on Sonoma 14.0 and 14.1 when typing into an NSTextView subclass. The crash logs I have show involvement of the spell-checking system - NSTestCheckingController
, NSSpellChecker
, and NSCorrectionPanel
. The crash is because of an exception being thrown. The throwing method is either [NSString getParagraphStart:end:contentsEnd:forRange:]
or [NSTextStorage ensureAttributesAreFixedInRange:]
.
I have not yet reproduced the crash. I have tried modifying the reference finding process to simply link every word, via NSStringEnumerationByWords
.
The text view in question recognizes certain things in the entered text and adds hyperlinks to the text while the user is typing. It re-parses and re-adds the links on every key press (via overriding the didChangeText
method), on a background thread.
From user reports, I have learned that:
- The crash only occurs on macOS 14.0 and 14.1, not on previous versions
- The call stack always involves the spell checker, and sometimes involves adding recognized links to the text storage (the call to DispatchQueue.main.async in the code below)
- The crash stops happening if the user turns off the system spell checker in System Settings -> Keyboard -> Edit on an Input Source -> Correct Spelling Automatically switch
- The crash does not happen when there are no links in the text view.
Here is the relevant code:
extension NSMutableAttributedString {
func batchUpdates(_ updates: () -> ()) {
self.beginEditing()
updates()
self.endEditing()
}
}
class MyTextView : NSTextView {
func didChangeText() {
super.didChangeText()
findReferences()
}
var parseToken: CancelationToken? = nil
let parseQueue = DispatchQueue(label: "com.myapp.ref_parser")
private func findReferences() {
guard let storage = self.textStorage else { return }
self.parseToken?.requestCancel()
let token = CancelationToken()
self.parseToken = token
let text = storage.string
self.parseQueue.async {
if token.cancelRequested { return }
let refs = RefParser.findReferences(inText: text, cancelationToken: token)
DispatchQueue.main.async {
if !token.cancelRequested {
storage.batchUpdates {
var linkRanges: [NSRange] = []
storage.enumerateAttribute(.link, in: NSRange(location: 0, length: storage.length)) { linkValue, linkRange, stop in
if let linkUrl = linkValue as? NSURL {
linkRanges.append(linkRange)
}
}
for rng in linkRanges {
storage.removeAttribute(.link, range: rng)
}
for r in refs {
storage.addAttribute(.link, value: r.url, range: r.range)
}
}
self.verseParseToken = nil
}
}
}
}
}
I've filed this as FB13306015 if any engineers see this. Can anyone