Issue #587

Use NSTextVIew

From https://github.com/twostraws/ControlRoom/blob/main/ControlRoom/NSViewWrappers/TextView.swift

import SwiftUI

/// A wrapper around NSTextView so we can get multiline text editing in SwiftUI.
struct TextView: NSViewRepresentable {
    @Binding private var text: String
    private let isEditable: Bool

    init(text: Binding<String>, isEditable: Bool = true) {
        _text = text
        self.isEditable = isEditable
    }

    init(text: String) {
        self.init(text: Binding<String>.constant(text), isEditable: false)
    }

    func makeNSView(context: Context) -> NSScrollView {
        let text = NSTextView()
        text.backgroundColor = isEditable ? .textBackgroundColor : .clear
        text.delegate = context.coordinator
        text.isRichText = false
        text.font = NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular)
        text.autoresizingMask = [.width]
        text.translatesAutoresizingMaskIntoConstraints = true
        text.isVerticallyResizable = true
        text.isHorizontallyResizable = false
        text.isEditable = isEditable

        let scroll = NSScrollView()
        scroll.hasVerticalScroller = true
        scroll.documentView = text
        scroll.drawsBackground = false

        return scroll
    }

    func updateNSView(_ view: NSScrollView, context: Context) {
        let text = view.documentView as? NSTextView
        text?.string = self.text

        guard context.coordinator.selectedRanges.count > 0 else {
            return
        }

        text?.selectedRanges = context.coordinator.selectedRanges
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, NSTextViewDelegate {
        var parent: TextView
        var selectedRanges = [NSValue]()

        init(_ parent: TextView) {
            self.parent = parent
        }

        func textDidChange(_ notification: Notification) {
            guard let textView = notification.object as? NSTextView else { return }
            parent.text = textView.string
            selectedRanges = textView.selectedRanges
        }
    }
}

Use xib

Create a xib called ScrollableTextView, and drag just Scrollable text view as top object

Connect just the textView property

import AppKit

class ScrollableTextView: NSScrollView {
    @IBOutlet var textView: NSTextView!
}

Conform to NSViewRepresentable

import SwiftUI

struct TextView: NSViewRepresentable {
    @Binding var text: String

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeNSView(context: Context) -> ScrollableTextView {
        var views: NSArray?
        Bundle.main.loadNibNamed("ScrollableTextView", owner: nil, topLevelObjects: &views)
        let scrollableTextView = views!.compactMap({ $0 as? ScrollableTextView }).first!
        scrollableTextView.textView.delegate = context.coordinator
        return scrollableTextView
    }

    func updateNSView(_ nsView: ScrollableTextView, context: Context) {
        guard nsView.textView.string != text else { return }
        nsView.textView.string = text
    }

    class Coordinator: NSObject, NSTextViewDelegate {
        let parent: TextView

        init(_ textView: TextView) {
            self.parent = textView
        }

        func textDidChange(_ notification: Notification) {
            guard let textView = notification.object as? NSTextView else { return }
            self.parent.text = textView.string
        }
    }
}

There seems to be a bug that if we have open and close curly braces, any character typed into NSTextView will move the cursor to the end. This is easily fixed with a check in updateNSView