bpisano
bpisano

Reputation: 570

NSTextView cursor doesn't appear when typing text on macOS 10.14

I'm observing a strange issue on macOS 10.12 Mojave with NSTextView.

The cursor is not appearing when I delete or insert text.

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

Answers (2)

bpisano
bpisano

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.

NSTextStorage

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.

NSTextStorage + NSTextView

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)
    }

}

Syntaxing

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

bpisano
bpisano

Reputation: 570

I found the solution. I have to use the didProcessEditing method in NSTextStorageDelegate instead of didChangeText.

Upvotes: 0

Related Questions