]> git.r.bdr.sh - rbdr/map/blob - Map/Presentation/Base Components/MapTextEditor.swift
d7aa9f86b18eb0cef014a3b7dbe40af2a1214abc
[rbdr/map] / Map / Presentation / Base Components / MapTextEditor.swift
1 import Cocoa
2 import SwiftUI
3
4 class MapTextEditorController: NSViewController {
5
6 @Binding var document: MapDocument
7 let onChange: () -> Void
8
9 private let vertexRegex = MapParsingPatterns.vertex
10 private let edgeRegex = MapParsingPatterns.edge
11 private let blockerRegex = MapParsingPatterns.blocker
12 private let opportunityRegex = MapParsingPatterns.opportunity
13 private let noteRegex = MapParsingPatterns.note
14 private let stageRegex = MapParsingPatterns.stage
15 private let groupRegex = MapParsingPatterns.group
16
17 private let changeDebouncer: Debouncer = Debouncer(seconds: 1)
18
19 init(document: Binding<MapDocument>, onChange: @escaping () -> Void) {
20 self._document = document
21 self.onChange = onChange
22 super.init(nibName: nil, bundle: nil)
23 }
24
25 required init?(coder: NSCoder) {
26 fatalError("init(coder:) has not been implemented")
27 }
28
29 override func loadView() {
30 let scrollView = NSTextView.scrollableTextView()
31 let textView = scrollView.documentView as! NSTextView
32
33 scrollView.translatesAutoresizingMaskIntoConstraints = false
34
35 textView.backgroundColor = .ui.background
36 textView.allowsUndo = true
37 textView.delegate = self
38 textView.textStorage?.delegate = self
39 textView.string = self.document.text
40 textView.isEditable = true
41 textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular)
42 self.view = scrollView
43 }
44
45 override func viewDidAppear() {
46 self.view.window?.makeFirstResponder(self.view)
47 }
48 }
49
50 extension MapTextEditorController: NSTextViewDelegate {
51
52 func textDidChange(_ obj: Notification) {
53 if let textField = obj.object as? NSTextView {
54 self.document.text = textField.string
55
56 changeDebouncer.debounce {
57 DispatchQueue.main.async {
58 self.onChange()
59 }
60 }
61 }
62 }
63
64 func textView(_ view: NSTextView, shouldChangeTextIn: NSRange, replacementString: String?) -> Bool
65 {
66 let range = Range(shouldChangeTextIn, in: view.string)
67 let target = view.string[range!]
68
69 if target == "--" {
70 return false
71 }
72
73 return true
74 }
75 }
76
77 extension MapTextEditorController: NSTextStorageDelegate {
78
79 override func textStorageDidProcessEditing(_ obj: Notification) {
80 if let textStorage = obj.object as? NSTextStorage {
81 self.colorizeText(textStorage: textStorage)
82 }
83 }
84
85 private func colorizeText(textStorage: NSTextStorage) {
86 let range = NSMakeRange(0, textStorage.length)
87 var matches = vertexRegex.matches(in: textStorage.string, options: [], range: range)
88
89 for match in matches {
90 textStorage.addAttributes(
91 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 1))
92 textStorage.addAttributes(
93 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 2))
94 textStorage.addAttributes(
95 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 3))
96 textStorage.addAttributes(
97 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 4))
98 }
99
100 matches = edgeRegex.matches(in: textStorage.string, options: [], range: range)
101
102 for match in matches {
103 textStorage.addAttributes(
104 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 1))
105 let arrowRange = match.range(at: 2)
106 textStorage.addAttributes(
107 [.foregroundColor: NSColor.syntax.symbol],
108 range: NSMakeRange(arrowRange.lowerBound - 1, arrowRange.length + 1))
109 textStorage.addAttributes(
110 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 3))
111 }
112
113 matches = opportunityRegex.matches(in: textStorage.string, options: [], range: range)
114
115 for match in matches {
116 textStorage.addAttributes(
117 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
118 textStorage.addAttributes(
119 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2))
120 textStorage.addAttributes(
121 [.foregroundColor: NSColor.syntax.symbol], range: match.range(at: 3))
122 textStorage.addAttributes(
123 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 4))
124 }
125
126 matches = blockerRegex.matches(in: textStorage.string, options: [], range: range)
127
128 for match in matches {
129 textStorage.addAttributes(
130 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
131 textStorage.addAttributes(
132 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2))
133 }
134
135 matches = noteRegex.matches(in: textStorage.string, options: [], range: range)
136
137 for match in matches {
138 textStorage.addAttributes(
139 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
140 textStorage.addAttributes(
141 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 2))
142 textStorage.addAttributes(
143 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 3))
144 }
145
146 matches = stageRegex.matches(in: textStorage.string, options: [], range: range)
147
148 for match in matches {
149 textStorage.addAttributes(
150 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
151 textStorage.addAttributes(
152 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 2))
153 }
154
155 matches = groupRegex.matches(in: textStorage.string, options: [], range: range)
156
157 for match in matches {
158 textStorage.addAttributes(
159 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
160 textStorage.addAttributes(
161 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2))
162 }
163 }
164 }
165
166 struct MapTextEditor: NSViewControllerRepresentable {
167
168 @Binding var document: MapDocument
169 var onChange: () -> Void = {}
170
171 func makeNSViewController(
172 context: NSViewControllerRepresentableContext<MapTextEditor>
173 ) -> MapTextEditorController {
174 return MapTextEditorController(document: $document, onChange: onChange)
175 }
176
177 func updateNSViewController(
178 _ nsViewController: MapTextEditorController,
179 context: NSViewControllerRepresentableContext<MapTextEditor>
180 ) {}
181 }