Reputation: 570
I'm observing a strange issue on macOS 10.12 Mojave with NSTextView.
.
I'm changing the textStorage attributes in didChangeText()
like this :
self.textStorage?.beginEditing()
ARTokenManager.getToken(text: text, language: language) { (tokens) in
// This line reset the attributes
// If I remove it, the cursor appear properly
// But the attributes are conserved
self.textStorage?.setAttributes([NSAttributedString.Key.font: self.font!,
NSAttributedString.Key.foregroundColor: self.defaultTextColor], range: range)
for token in tokens {
let attributeRange = NSRange(location: token.range.location + range.location, length: token.range.length)
self.textStorage?.addAttributes(token.attributes, range: attributeRange)
}
}
self.textStorage?.endEditing()
When I remove the setAttributes
method, everything works as expected, but I can't explain why. I'm possibly resetting the attributes wrong. This issue only works with Mojave.
Does someone have the same issue or can explain me what I'm doing wrong ?
Thank you.
Upvotes: 2
Views: 590
Reputation: 570
After some research, I discovered that my question was more about syntax highlighting with NSTextView. I know this is a question that a lot of macOS developers are asking about and there are a lot of solutions for that. This is not probably the best one, but this is how I’ve solved this problem.
To achieve that, I’ve used a subclass of NSTextStorage. This is where all the syntax work will be done. NSTextStorage is not protocol oriented so you have to override method by yourself as the Apple documentation suggest :
class SyntaxTextStorage: NSTextStorage {
private var storage: NSTextStorage
override var string: String {
return storage.string
}
override init() {
self.storage = NSTextStorage(string: "")
super.init()
}
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key : Any] {
return storage.attributes(at: location, effectiveRange: range)
}
override func replaceCharacters(in range: NSRange, with str: String) {
beginEditing()
storage.replaceCharacters(in: range, with: str)
edited(.editedCharacters, range: range, changeInLength: str.count - range.length)
endEditing()
}
override func setAttributes(_ attrs: [NSAttributedString.Key : Any]?, range: NSRange) {
beginEditing()
storage.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
endEditing()
}
}
This is the basic to create your text storage.
The next step is to set your text storage into your textView. To do so, you can use the replaceTextStorage()
method accessible in the textView layoutManager
.
class SyntaxTextView: NSTextView {
var storage: SyntaxTextStorage!
override func awakeFromNib() {
super.awakeFromNib()
configureTextStorage()
}
private func configureTextStorage() {
storage = SyntaxTextStorage()
layoutManager?.replaceTextStorage(storage)
}
}
The final step is to do your syntax job. The CPU cost of this process is very hight. There is a lot of way to do it to have the best performances. I suggest you to implement a class that will returns you a list of NSAttributedString
and NSRange
. The job of the text storage should only be applying the style to your text.
Personally, I've used the processEditing
method to perform my text analyze :
override func processEditing() {
super.processEditing()
syntaxCurrentParagraph()
}
I recommend you to do you syntax analyze in background, then, if there is no text change since your last analyze, apply the change to your text. Always in my text storage, I've implemented a syntax
method that apply the style to the text :
private func syntax(range: NSRange, completion: ((_ succeed: Bool) -> Void)? = nil) {
guard range.length > 0 else {
completion?(true)
return
}
// Save your data to do the job in background
let currentString = self.string
let substring = currentString.substring(range: range)
let mutableAttributedString = NSMutableAttributedString(string: substring, attributes: NSAttributedString.defaultAttributes as [NSAttributedString.Key : Any])
DispatchQueue.global(qos: .background).async {
ARSyntaxManager.tokens(text: substring, language: self.language) { (tokens) in
// Fill you attributed string
for token in tokens {
mutableAttributedString.addAttributes(token.attributes, range: token.range)
}
DispatchQueue.main.async {
// Check if there is no change
guard self.string.count == currentString.count else {
completion?(false)
return
}
completion?(true)
// Apply your change
self.storage.replaceCharacters(in: range, with: mutableAttributedString)
self.displayVisibleRect()
}
}
}
}
That's it. Hope it will help some of you.
Upvotes: 3
Reputation: 570
I found the solution. I have to use the didProcessEditing
method in NSTextStorageDelegate
instead of didChangeText
.
Upvotes: 0