]> git.r.bdr.sh - rbdr/map/blame - Map/Presentation/Base Components/MapTextEditor.swift
Add some debouncing
[rbdr/map] / Map / Presentation / Base Components / MapTextEditor.swift
CommitLineData
be897af3
RBR
1// Copyright (C) 2024 Rubén Beltrán del Río
2
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// (at your option) any later version.
7
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see https://map.tranquil.systems.
5e8ff485
RBR
15import Cocoa
16import SwiftUI
17
18class MapTextEditorController: NSViewController {
19
e2c37ac1 20 @Binding var document: MapDocument
14491563
RBR
21 var highlightRanges: [Range<String.Index>] {
22 didSet {
23 updateHighlights()
24 }
25 }
26
27 var selectedRange: Int {
28 didSet {
29 updateHighlights()
30 focusOnResult()
31 }
32 }
33
fdb4633d 34 let onChange: () -> Void
5e8ff485 35
77d0155b
RBR
36 private let vertexRegex = MapParsingPatterns.vertex
37 private let edgeRegex = MapParsingPatterns.edge
38 private let blockerRegex = MapParsingPatterns.blocker
39 private let opportunityRegex = MapParsingPatterns.opportunity
fdb4633d 40 private let noteRegex = MapParsingPatterns.note
77d0155b 41 private let stageRegex = MapParsingPatterns.stage
e2c37ac1 42 private let groupRegex = MapParsingPatterns.group
77d0155b 43
fdb4633d 44 private let changeDebouncer: Debouncer = Debouncer(seconds: 1)
77d0155b 45
14491563
RBR
46 init(
47 document: Binding<MapDocument>, highlightRanges: [Range<String.Index>], selectedRange: Int,
48 onChange: @escaping () -> Void
49 ) {
e2c37ac1 50 self._document = document
fdb4633d 51 self.onChange = onChange
14491563
RBR
52 self.highlightRanges = highlightRanges
53 self.selectedRange = selectedRange
5e8ff485
RBR
54 super.init(nibName: nil, bundle: nil)
55 }
56
57 required init?(coder: NSCoder) {
58 fatalError("init(coder:) has not been implemented")
59 }
60
61 override func loadView() {
62 let scrollView = NSTextView.scrollableTextView()
63 let textView = scrollView.documentView as! NSTextView
64
65 scrollView.translatesAutoresizingMaskIntoConstraints = false
66
be897af3 67 textView.backgroundColor = .UI.background
75a0e450 68 textView.allowsUndo = true
5e8ff485 69 textView.delegate = self
77d0155b 70 textView.textStorage?.delegate = self
e2c37ac1 71 textView.string = self.document.text
5e8ff485
RBR
72 textView.isEditable = true
73 textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular)
74 self.view = scrollView
75 }
76
77 override func viewDidAppear() {
78 self.view.window?.makeFirstResponder(self.view)
14491563
RBR
79 updateHighlights()
80 }
81
82 private var textView: NSTextView? {
83 return (view as? NSScrollView)?.documentView as? NSTextView
84 }
85
86 private func updateHighlights() {
87 if let textView {
88 if let textStorage = textView.textStorage {
89 textStorage.removeAttribute(
90 .backgroundColor, range: NSRange(location: 0, length: textStorage.length))
91
ed10ac19 92 for (index, range) in highlightRanges.enumerated() {
14491563
RBR
93 let nsRange = NSRange(range, in: textStorage.string)
94
ed10ac19
RBR
95 let color = index == selectedRange ? NSColor.Syntax.highlightMatch : NSColor.Syntax.match
96 textStorage.addAttribute(.backgroundColor, value: color, range: nsRange)
14491563
RBR
97 }
98
99 textView.needsDisplay = true
100
101 }
102 }
103 }
104
105 private func focusOnResult() {
106 if let textView {
107 if let textStorage = textView.textStorage {
108 if selectedRange < highlightRanges.count {
109 let range = highlightRanges[selectedRange]
110 let nsRange = NSRange(range, in: textStorage.string)
111 textView.scrollRangeToVisible(nsRange)
14491563
RBR
112 }
113 }
114 }
115 }
5e8ff485
RBR
116}
117
118extension MapTextEditorController: NSTextViewDelegate {
119
120 func textDidChange(_ obj: Notification) {
121 if let textField = obj.object as? NSTextView {
e2c37ac1
RBR
122 self.document.text = textField.string
123
124 changeDebouncer.debounce {
125 DispatchQueue.main.async {
126 self.onChange()
fdb4633d 127 }
e2c37ac1 128 }
5e8ff485
RBR
129 }
130 }
131
132 func textView(_ view: NSTextView, shouldChangeTextIn: NSRange, replacementString: String?) -> Bool
133 {
134 let range = Range(shouldChangeTextIn, in: view.string)
135 let target = view.string[range!]
136
137 if target == "--" {
138 return false
139 }
140
141 return true
142 }
143}
144
77d0155b 145extension MapTextEditorController: NSTextStorageDelegate {
fdb4633d 146
77d0155b
RBR
147 override func textStorageDidProcessEditing(_ obj: Notification) {
148 if let textStorage = obj.object as? NSTextStorage {
fdb4633d 149 self.colorizeText(textStorage: textStorage)
77d0155b
RBR
150 }
151 }
152
153 private func colorizeText(textStorage: NSTextStorage) {
154 let range = NSMakeRange(0, textStorage.length)
155 var matches = vertexRegex.matches(in: textStorage.string, options: [], range: range)
77d0155b
RBR
156
157 for match in matches {
e2c37ac1 158 textStorage.addAttributes(
be897af3 159 [.foregroundColor: NSColor.Syntax.vertex], range: match.range(at: 1))
e2c37ac1 160 textStorage.addAttributes(
be897af3 161 [.foregroundColor: NSColor.Syntax.number], range: match.range(at: 2))
e2c37ac1 162 textStorage.addAttributes(
be897af3 163 [.foregroundColor: NSColor.Syntax.number], range: match.range(at: 3))
e2c37ac1 164 textStorage.addAttributes(
be897af3 165 [.foregroundColor: NSColor.Syntax.option], range: match.range(at: 4))
77d0155b
RBR
166 }
167
168 matches = edgeRegex.matches(in: textStorage.string, options: [], range: range)
169
170 for match in matches {
e2c37ac1 171 textStorage.addAttributes(
be897af3 172 [.foregroundColor: NSColor.Syntax.vertex], range: match.range(at: 1))
77d0155b
RBR
173 let arrowRange = match.range(at: 2)
174 textStorage.addAttributes(
be897af3 175 [.foregroundColor: NSColor.Syntax.symbol],
77d0155b 176 range: NSMakeRange(arrowRange.lowerBound - 1, arrowRange.length + 1))
e2c37ac1 177 textStorage.addAttributes(
be897af3 178 [.foregroundColor: NSColor.Syntax.vertex], range: match.range(at: 3))
77d0155b
RBR
179 }
180
181 matches = opportunityRegex.matches(in: textStorage.string, options: [], range: range)
182
183 for match in matches {
e2c37ac1 184 textStorage.addAttributes(
be897af3 185 [.foregroundColor: NSColor.Syntax.option], range: match.range(at: 1))
e2c37ac1 186 textStorage.addAttributes(
be897af3 187 [.foregroundColor: NSColor.Syntax.vertex], range: match.range(at: 2))
e2c37ac1 188 textStorage.addAttributes(
be897af3 189 [.foregroundColor: NSColor.Syntax.symbol], range: match.range(at: 3))
e2c37ac1 190 textStorage.addAttributes(
be897af3 191 [.foregroundColor: NSColor.Syntax.number], range: match.range(at: 4))
77d0155b
RBR
192 }
193
194 matches = blockerRegex.matches(in: textStorage.string, options: [], range: range)
195
196 for match in matches {
e2c37ac1 197 textStorage.addAttributes(
be897af3 198 [.foregroundColor: NSColor.Syntax.option], range: match.range(at: 1))
e2c37ac1 199 textStorage.addAttributes(
be897af3 200 [.foregroundColor: NSColor.Syntax.vertex], range: match.range(at: 2))
fdb4633d 201 }
e2c37ac1 202
fdb4633d
RBR
203 matches = noteRegex.matches(in: textStorage.string, options: [], range: range)
204
205 for match in matches {
e2c37ac1 206 textStorage.addAttributes(
be897af3 207 [.foregroundColor: NSColor.Syntax.option], range: match.range(at: 1))
e2c37ac1 208 textStorage.addAttributes(
be897af3 209 [.foregroundColor: NSColor.Syntax.number], range: match.range(at: 2))
e2c37ac1 210 textStorage.addAttributes(
be897af3 211 [.foregroundColor: NSColor.Syntax.number], range: match.range(at: 3))
77d0155b
RBR
212 }
213
214 matches = stageRegex.matches(in: textStorage.string, options: [], range: range)
215
216 for match in matches {
e2c37ac1 217 textStorage.addAttributes(
be897af3 218 [.foregroundColor: NSColor.Syntax.option], range: match.range(at: 1))
e2c37ac1 219 textStorage.addAttributes(
be897af3 220 [.foregroundColor: NSColor.Syntax.number], range: match.range(at: 2))
e2c37ac1
RBR
221 }
222
223 matches = groupRegex.matches(in: textStorage.string, options: [], range: range)
224
225 for match in matches {
226 textStorage.addAttributes(
be897af3 227 [.foregroundColor: NSColor.Syntax.option], range: match.range(at: 1))
e2c37ac1 228 textStorage.addAttributes(
be897af3 229 [.foregroundColor: NSColor.Syntax.vertex], range: match.range(at: 2))
77d0155b
RBR
230 }
231 }
232}
233
5e8ff485
RBR
234struct MapTextEditor: NSViewControllerRepresentable {
235
e2c37ac1 236 @Binding var document: MapDocument
14491563
RBR
237 var highlightRanges: [Range<String.Index>]
238 var selectedRange: Int
fdb4633d 239 var onChange: () -> Void = {}
5e8ff485
RBR
240
241 func makeNSViewController(
242 context: NSViewControllerRepresentableContext<MapTextEditor>
243 ) -> MapTextEditorController {
14491563
RBR
244 return MapTextEditorController(
245 document: $document, highlightRanges: highlightRanges, selectedRange: selectedRange,
246 onChange: onChange)
5e8ff485
RBR
247 }
248
249 func updateNSViewController(
250 _ nsViewController: MapTextEditorController,
251 context: NSViewControllerRepresentableContext<MapTextEditor>
14491563
RBR
252 ) {
253 nsViewController.highlightRanges = highlightRanges
ed10ac19
RBR
254 if nsViewController.selectedRange != selectedRange {
255 nsViewController.selectedRange = selectedRange
256 }
14491563 257 }
5e8ff485 258}