Created
December 17, 2023 02:13
-
-
Save keyle/228ffd4a5622c1c328299f3ff9dd7c25 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* MacEditorTextView | |
* Copyright (c) Thiago Holanda 2020-2021 | |
* https://gist.github.com/unnamedd/6e8c3fbc806b8deb60fa65d6b9affab0 | |
* MIT license | |
*/ | |
import SwiftUI | |
import Neon | |
import SwiftTreeSitter | |
import TreeSitterClient | |
import TreeSitterC | |
struct MacEditorTextView: NSViewRepresentable { | |
// NOTE Also see CustomTextView | |
@Binding var text: String | |
var isEditable: Bool = true | |
var font: NSFont? = .systemFont(ofSize: 16, weight: .regular) | |
var onEditingChanged: () -> Void = {} | |
var onCommit : () -> Void = {} | |
var onTextChange : (String) -> Void = { _ in } | |
var shouldFocusTextView = true | |
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 | |
if shouldFocusTextView { | |
DispatchQueue.main.async { | |
view.focusTextView() | |
} | |
} | |
} | |
} | |
// 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 | |
} | |
// print("did begin editing") | |
self.parent.text = textView.string | |
self.parent.onEditingChanged() | |
} | |
func textDidChange(_ notification: Notification) { | |
guard let textView = notification.object as? NSTextView else { | |
return | |
} | |
// print("did change") | |
self.parent.text = textView.string | |
self.selectedRanges = textView.selectedRanges | |
} | |
func textDidEndEditing(_ notification: Notification) { | |
guard let textView = notification.object as? NSTextView else { | |
return | |
} | |
print("did end editing") | |
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 | |
} | |
} | |
func focusTextView() { | |
window?.makeFirstResponder(textView) | |
} | |
private lazy var scrollView: NSScrollView = { | |
let scrollView = NSScrollView() | |
scrollView.drawsBackground = false | |
scrollView.borderType = .noBorder | |
scrollView.hasVerticalScroller = true | |
scrollView.hasHorizontalRuler = false | |
scrollView.autoresizingMask = [.width, .height] | |
scrollView.translatesAutoresizingMaskIntoConstraints = false | |
return scrollView | |
}() | |
private lazy var textView: CustomNSTextView = { | |
let contentSize = scrollView.contentSize | |
let textStorage = NSTextStorage() | |
let layoutManager = NSLayoutManager() | |
textStorage.addLayoutManager(layoutManager) | |
let textContainer = NSTextContainer(containerSize: self.scrollView.frame.size) | |
textContainer.widthTracksTextView = true | |
textContainer.containerSize = NSSize( | |
width: contentSize.width, | |
height: CGFloat.greatestFiniteMagnitude | |
) | |
layoutManager.addTextContainer(textContainer) | |
let textView = CustomNSTextView(frame: .zero, textContainer: textContainer) | |
textView.autoresizingMask = .width | |
textView.backgroundColor = NSColor.textBackgroundColor | |
textView.delegate = self.delegate | |
textView.drawsBackground = true | |
textView.textContainerInset = .init(width: 7, height: 7) | |
// textView.textContainerInset = .init(width: 24, height: 10) | |
textView.font = self.font | |
textView.isRichText = false | |
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 | |
textView.insertionPointColor = .caret // Asset Catalog CaretColor | |
let provider: TokenAttributeProvider = { token in | |
return switch token.name { | |
case let keyword where keyword.hasPrefix("keyword"): [.foregroundColor: NSColor.red, .font: self.font!] | |
case "comment": [.foregroundColor: NSColor.green, .font: self.font!] | |
// Note: Default is not actually applied to unstyled/untokenized text. | |
default: [.foregroundColor: NSColor.blue, .font: self.font!] | |
} | |
} | |
let language = Language(language: tree_sitter_c()) | |
let url = Bundle.main | |
.resourceURL? | |
.appendingPathComponent("TreeSitterSwift_TreeSitterSwift.bundle") | |
.appendingPathComponent("Contents/Resources/queries/highlights.scm") | |
let query = try! language.query(contentsOf: url!) // !problem here! | |
let interface = TextStorageSystemInterface(textView: textView, attributeProvider: provider) | |
let highlight = try! TextViewHighlighter(textView: textView, | |
language: language, | |
highlightQuery: query, | |
interface: interface) | |
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() { | |
textView.isRichText = false | |
// TODO fix | |
let regularFont = NSFont.monospacedSystemFont(ofSize: 16, weight: .regular) | |
textView.typingAttributes = [ | |
.foregroundColor: NSColor.darkGray, | |
.font: regularFont, | |
] | |
scrollView.documentView = textView | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment