Skip to content

Instantly share code, notes, and snippets.

@keyle
Created December 17, 2023 02:13
Show Gist options
  • Save keyle/228ffd4a5622c1c328299f3ff9dd7c25 to your computer and use it in GitHub Desktop.
Save keyle/228ffd4a5622c1c328299f3ff9dd7c25 to your computer and use it in GitHub Desktop.
/**
* 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