-
Star
(185)
You must be signed in to star a gist -
Fork
(13)
You must be signed in to fork a gist
-
-
Save unnamedd/6e8c3fbc806b8deb60fa65d6b9affab0 to your computer and use it in GitHub Desktop.
| /** | |
| * MacEditorTextView | |
| * Copyright (c) Thiago Holanda 2020-2021 | |
| * https://bsky.app/profile/tholanda.com | |
| * | |
| * (the twitter account is now deleted, please, do not try to reach me there) | |
| * https://twitter.com/tholanda | |
| * | |
| * MIT license | |
| */ | |
| import Combine | |
| import SwiftUI | |
| struct MacEditorTextView: NSViewRepresentable { | |
| @Binding var text: String | |
| var isEditable: Bool = true | |
| var font: NSFont? = .systemFont(ofSize: 14, weight: .regular) | |
| var onEditingChanged: () -> Void = {} | |
| var onCommit : () -> Void = {} | |
| var onTextChange : (String) -> Void = { _ in } | |
| func makeCoordinator() -> Coordinator { | |
| Coordinator(self) | |
| } | |
| func makeNSView(context: Context) -> CustomTextView { | |
| let textView = CustomTextView( | |
| text: text, | |
| isEditable: isEditable, | |
| font: font | |
| ) | |
| textView.delegate = context.coordinator | |
| return textView | |
| } | |
| func updateNSView(_ view: CustomTextView, context: Context) { | |
| view.text = text | |
| view.selectedRanges = context.coordinator.selectedRanges | |
| } | |
| } | |
| // MARK: - Preview | |
| #if DEBUG | |
| struct MacEditorTextView_Previews: PreviewProvider { | |
| static var previews: some View { | |
| Group { | |
| MacEditorTextView( | |
| text: .constant("{ \n planets { \n name \n }\n}"), | |
| isEditable: true, | |
| font: .userFixedPitchFont(ofSize: 14) | |
| ) | |
| .environment(\.colorScheme, .dark) | |
| .previewDisplayName("Dark Mode") | |
| MacEditorTextView( | |
| text: .constant("{ \n planets { \n name \n }\n}"), | |
| isEditable: false | |
| ) | |
| .environment(\.colorScheme, .light) | |
| .previewDisplayName("Light Mode") | |
| } | |
| } | |
| } | |
| #endif | |
| // MARK: - Coordinator | |
| extension MacEditorTextView { | |
| class Coordinator: NSObject, NSTextViewDelegate { | |
| var parent: MacEditorTextView | |
| var selectedRanges: [NSValue] = [] | |
| init(_ parent: MacEditorTextView) { | |
| self.parent = parent | |
| } | |
| func textDidBeginEditing(_ notification: Notification) { | |
| guard let textView = notification.object as? NSTextView else { | |
| return | |
| } | |
| self.parent.text = textView.string | |
| self.parent.onEditingChanged() | |
| } | |
| func textDidChange(_ notification: Notification) { | |
| guard let textView = notification.object as? NSTextView else { | |
| return | |
| } | |
| self.parent.text = textView.string | |
| self.selectedRanges = textView.selectedRanges | |
| } | |
| func textDidEndEditing(_ notification: Notification) { | |
| guard let textView = notification.object as? NSTextView else { | |
| return | |
| } | |
| self.parent.text = textView.string | |
| self.parent.onCommit() | |
| } | |
| } | |
| } | |
| // MARK: - CustomTextView | |
| final class CustomTextView: NSView { | |
| private var isEditable: Bool | |
| private var font: NSFont? | |
| weak var delegate: NSTextViewDelegate? | |
| var text: String { | |
| didSet { | |
| textView.string = text | |
| } | |
| } | |
| var selectedRanges: [NSValue] = [] { | |
| didSet { | |
| guard selectedRanges.count > 0 else { | |
| return | |
| } | |
| textView.selectedRanges = selectedRanges | |
| } | |
| } | |
| private lazy var scrollView: NSScrollView = { | |
| let scrollView = NSScrollView() | |
| scrollView.drawsBackground = true | |
| scrollView.borderType = .noBorder | |
| scrollView.hasVerticalScroller = true | |
| scrollView.hasHorizontalRuler = false | |
| scrollView.autoresizingMask = [.width, .height] | |
| scrollView.translatesAutoresizingMaskIntoConstraints = false | |
| return scrollView | |
| }() | |
| private lazy var textView: NSTextView = { | |
| let contentSize = scrollView.contentSize | |
| let textStorage = NSTextStorage() | |
| let layoutManager = NSLayoutManager() | |
| textStorage.addLayoutManager(layoutManager) | |
| let textContainer = NSTextContainer(containerSize: scrollView.frame.size) | |
| textContainer.widthTracksTextView = true | |
| textContainer.containerSize = NSSize( | |
| width: contentSize.width, | |
| height: CGFloat.greatestFiniteMagnitude | |
| ) | |
| layoutManager.addTextContainer(textContainer) | |
| let textView = NSTextView(frame: .zero, textContainer: textContainer) | |
| textView.autoresizingMask = .width | |
| textView.backgroundColor = NSColor.textBackgroundColor | |
| textView.delegate = self.delegate | |
| textView.drawsBackground = true | |
| textView.font = self.font | |
| textView.isEditable = self.isEditable | |
| textView.isHorizontallyResizable = false | |
| textView.isVerticallyResizable = true | |
| textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) | |
| textView.minSize = NSSize(width: 0, height: contentSize.height) | |
| textView.textColor = NSColor.labelColor | |
| textView.allowsUndo = true | |
| return textView | |
| }() | |
| // MARK: - Init | |
| init(text: String, isEditable: Bool, font: NSFont?) { | |
| self.font = font | |
| self.isEditable = isEditable | |
| self.text = text | |
| super.init(frame: .zero) | |
| } | |
| required init?(coder: NSCoder) { | |
| fatalError("init(coder:) has not been implemented") | |
| } | |
| // MARK: - Life cycle | |
| override func viewWillDraw() { | |
| super.viewWillDraw() | |
| setupScrollViewConstraints() | |
| setupTextView() | |
| } | |
| func setupScrollViewConstraints() { | |
| scrollView.translatesAutoresizingMaskIntoConstraints = false | |
| addSubview(scrollView) | |
| NSLayoutConstraint.activate([ | |
| scrollView.topAnchor.constraint(equalTo: topAnchor), | |
| scrollView.trailingAnchor.constraint(equalTo: trailingAnchor), | |
| scrollView.bottomAnchor.constraint(equalTo: bottomAnchor), | |
| scrollView.leadingAnchor.constraint(equalTo: leadingAnchor) | |
| ]) | |
| } | |
| func setupTextView() { | |
| scrollView.documentView = textView | |
| } | |
| } |
| /** | |
| * MacEditorTextView | |
| * Copyright (c) Thiago Holanda 2020-2021 | |
| * https://bsky.app/profile/tholanda.com | |
| * | |
| * (the twitter account is now deleted, please, do not try to reach me there) | |
| * https://twitter.com/tholanda | |
| * | |
| * MIT license | |
| */ | |
| import SwiftUI | |
| import Combine | |
| struct ContentQueryView: View { | |
| @State private var queryText = "{ \n planets { \n name \n }\n}" | |
| @State private var responseJSONText = "{ \"name\": \"Earth\"}" | |
| var body: some View { | |
| let queryTextView = MacEditorTextView( | |
| text: $queryText, | |
| isEditable: false, | |
| font: .systemFont(ofSize: 14, weight: .regular) | |
| ) | |
| .frame(minWidth: 300, | |
| maxWidth: .infinity, | |
| minHeight: 300, | |
| maxHeight: .infinity) | |
| let responseTextView = MacEditorTextView( | |
| text: $responseJSONText, | |
| isEditable: false, | |
| font: .userFixedPitchFont(ofSize: 14) | |
| ) | |
| .frame(minWidth: 300, | |
| maxWidth: .infinity, | |
| minHeight: 300, | |
| maxHeight: .infinity) | |
| return HSplitView { | |
| queryTextView | |
| responseTextView | |
| } | |
| } | |
| } |
It's always interesting to see which "small" thing we put out there turns out to have long-lasting impact, and which "big" things fade quickly into obscurity.... ;)
As I mentioned, the change above seems to work. I'm not certain what downstream effects, if any, there may be. I'll defer to you as to whether it is a good fit for "upstream merging", but either way it is something that any user of MacEditorTextView should be aware of.
I had two initial desires for a more customizable text editor component as I experiment with SwiftUI to re-engineer my older Objective-C code. One part was custom typing behavior (e.g. typing a double quote should insert a pair of double quotes and move the selected range in between them.) The other was to customize autocompletion behavior (in one case to be based on data stored via SwiftData). MacEditorTextView made it easy for me substitute my own NSTextView subclass to handle the first part. (I also had a custom NSTextStorage that supports syntax highlighting that was easily swapped in as well.)
As I experiment with the autocompletion aspect, I think I am beginning to bump up against the edges of the architecture of MacEditorTextView. For example, I sometimes want to stick with the default NSTextStorage, and sometimes want to use a custom subclass that supports syntax highlighting. And now I need to use a custom NSTextViewDelegate for autocompletion, but it looks like that would require modifying the Coordinator rather than an easier swap. I suspect that the right approach for me will probably be to build something new that uses the parts of MacEditorTextView that I need and rearchitects the pieces that need to be behave differently for me.
Regardless, I am very appreciative for MacEditorTextView!!!
Hi @fletcher, it is amazing to know that MacEditorTextView, even after all that time, is still useful for someone in the community, like you.
Perhaps you can make the changes yourself in your code to confirm all your guesses, and after that you can make an improvement in the code and I will be very happy to update the code here with your changes.
Would have been amazing if I had created this as a proper library instead of just code in a GitHub Gist, in this way, more people could have submitted suggestions via pull requests. As I didn't do that in the past, I am not sure if it makes sense to do it now, therefore, your changes might be very helpful here for our future friends to use them.