import Cocoa import SwiftUI class MapTextEditorController: NSViewController { @Binding var text: String var colorScheme: ColorScheme private let vertexRegex = MapParsingPatterns.vertex private let edgeRegex = MapParsingPatterns.edge private let blockerRegex = MapParsingPatterns.blocker private let opportunityRegex = MapParsingPatterns.opportunity private let stageRegex = MapParsingPatterns.stage private let debouncer: Debouncer = Debouncer(seconds: 0.2) init(text: Binding, colorScheme: ColorScheme) { self._text = text self.colorScheme = colorScheme super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { let scrollView = NSTextView.scrollableTextView() let textView = scrollView.documentView as! NSTextView scrollView.translatesAutoresizingMaskIntoConstraints = false textView.allowsUndo = true textView.delegate = self textView.textStorage?.delegate = self textView.string = self.text textView.isEditable = true textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular) self.view = scrollView } override func viewDidAppear() { self.view.window?.makeFirstResponder(self.view) } func updateColorScheme(_ colorScheme: ColorScheme) { self.colorScheme = colorScheme } } extension MapTextEditorController: NSTextViewDelegate { func textDidChange(_ obj: Notification) { if let textField = obj.object as? NSTextView { self.text = textField.string } } func textView(_ view: NSTextView, shouldChangeTextIn: NSRange, replacementString: String?) -> Bool { let range = Range(shouldChangeTextIn, in: view.string) let target = view.string[range!] if target == "--" { return false } return true } } extension MapTextEditorController: NSTextStorageDelegate { override func textStorageDidProcessEditing(_ obj: Notification) { if let textStorage = obj.object as? NSTextStorage { debouncer.debounce { DispatchQueue.main.async { self.colorizeText(textStorage: textStorage) } } } } private func colorizeText(textStorage: NSTextStorage) { let range = NSMakeRange(0, textStorage.length) var matches = vertexRegex.matches(in: textStorage.string, options: [], range: range) let colors = MapColor.colorForScheme(colorScheme) for match in matches { textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 1)) textStorage.addAttributes([.foregroundColor: colors.syntax.number], range: match.range(at: 2)) textStorage.addAttributes([.foregroundColor: colors.syntax.number], range: match.range(at: 3)) textStorage.addAttributes([.foregroundColor: colors.syntax.option], range: match.range(at: 4)) } matches = edgeRegex.matches(in: textStorage.string, options: [], range: range) for match in matches { textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 1)) let arrowRange = match.range(at: 2) textStorage.addAttributes( [.foregroundColor: colors.syntax.symbol], range: NSMakeRange(arrowRange.lowerBound - 1, arrowRange.length + 1)) textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 3)) } matches = opportunityRegex.matches(in: textStorage.string, options: [], range: range) for match in matches { textStorage.addAttributes([.foregroundColor: colors.syntax.option], range: match.range(at: 1)) textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 2)) textStorage.addAttributes([.foregroundColor: colors.syntax.symbol], range: match.range(at: 3)) textStorage.addAttributes([.foregroundColor: colors.syntax.number], range: match.range(at: 4)) } matches = blockerRegex.matches(in: textStorage.string, options: [], range: range) for match in matches { textStorage.addAttributes([.foregroundColor: colors.syntax.option], range: match.range(at: 1)) textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 2)) } matches = stageRegex.matches(in: textStorage.string, options: [], range: range) for match in matches { textStorage.addAttributes([.foregroundColor: colors.syntax.option], range: match.range(at: 1)) textStorage.addAttributes([.foregroundColor: colors.syntax.number], range: match.range(at: 2)) } } } struct MapTextEditor: NSViewControllerRepresentable { @Binding var text: String let colorScheme: ColorScheme func makeNSViewController( context: NSViewControllerRepresentableContext ) -> MapTextEditorController { return MapTextEditorController(text: $text, colorScheme: colorScheme) } func updateNSViewController( _ nsViewController: MapTextEditorController, context: NSViewControllerRepresentableContext ) { nsViewController.updateColorScheme(context.environment.colorScheme) } }