NSTextView Bug: Automatically Scroll Insertion Point Into View Broken When Setting NSTextContainer's lineFragmentPadding to Zero

Today I found that NSTextView has a bug where it won’t automatically scroll to keep the insertion point visible when you hit Enter or Return to insert a line break.

Update 2022-04-26: This bug is resolved on macOS 12 Monterey! So I’d suggest something like:

let textView: NSTextView = ...
if #available(macOS 12, *) {
    textView.textContainer?.lineFragmentPadding = 0.0
} else {
    textView.textContainer?.lineFragmentPadding = 0.1  // Or higher

Usually, a text view keeps the insertion point visible as soon as the user types. You can scroll away, but as soon as the text view content change via user input, the insertion point scrolls into view automatically.

Unless you set NSTextContainer.lineFragmentPadding to 0.0: then it still works for all characters but line breaks.

The text view's scroll position sticks to y:0.0 when you hit enter.

This means when you’re at the bottom edge and hit Enter, your insertion point will vanish from screen. The expected behavior is for the text view to scroll down a line or two.

Any positive value that’s not 0.0 will work – including 0.1, which isn’t even a visible pixel’s width.

So if you are customizing a NSTextView in your app and find that entering line breaks doesn’t automatically scroll anymore, check that the lineFragmentPadding isn’t set to 0.0! The default value is 5.0, by the way, and setting this to 0 is probably a bad idea for your user interface anyway. Some horizontal margin from the edge of the text view makes the overall appearance look way less cramped.

(Submitted to Apple as FB9302224. – It’s resolved as of 2022-04-26!)

The example code is so simple that I didn’t upload it to GitHub since it fits into 20 lines of code. So if your code looks something like this, you’ll notice the problem, at least on Big Sur:

class AppDelegate: NSObject, NSApplicationDelegate {
    @IBOutlet var window: NSWindow!

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let scrollView = NSTextView.scrollableTextView()
        scrollView.translatesAutoresizingMaskIntoConstraints = false

        // Add scroll view to window
        let containerView = window.contentView!
        containerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[scrollView]|", options: [], metrics: nil, views: ["scrollView" : scrollView]))
        containerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[scrollView]|", options: [], metrics: nil, views: ["scrollView" : scrollView]))

        // Break scroll-insertion-point-to-visible
        let textView = scrollView.documentView as! NSTextView
        // textView.textContainer?.lineFragmentPadding = 0.1  // works
        textView.textContainer?.lineFragmentPadding = 0.0    // breaks