aboutsummaryrefslogtreecommitdiff
path: root/Map/Presentation/Base Components
diff options
context:
space:
mode:
authorRuben Beltran del Rio <git@r.bdr.sh>2025-07-09 01:55:50 +0200
committerRuben Beltran del Rio <git@r.bdr.sh>2025-07-09 01:55:50 +0200
commit6926feead1a7fadb7f5f974996bef8c30b3bfdbe (patch)
tree5a16e64f70bea3b9f33220cce5ca524e686dbedf /Map/Presentation/Base Components
parentc21a68c807dcb84b4ce910ee0f73238a8019ffe9 (diff)
Use AST based text editor
Diffstat (limited to 'Map/Presentation/Base Components')
-rw-r--r--Map/Presentation/Base Components/MapTextEditor.swift167
1 files changed, 70 insertions, 97 deletions
diff --git a/Map/Presentation/Base Components/MapTextEditor.swift b/Map/Presentation/Base Components/MapTextEditor.swift
index a2070e4..c4ac561 100644
--- a/Map/Presentation/Base Components/MapTextEditor.swift
+++ b/Map/Presentation/Base Components/MapTextEditor.swift
@@ -19,6 +19,13 @@ class MapTextEditorController: NSViewController {
@Binding var text: String
+ var parsedMap: [Wmap.Entity] = [] {
+ didSet {
+ syntaxHighlighter.updateSyntaxElements(from: parsedMap)
+ // Don't re-highlight entire document here - let the text change handler do it
+ }
+ }
+
var highlightRanges: [Range<String.Index>] {
didSet {
updateHighlights()
@@ -34,25 +41,26 @@ class MapTextEditorController: NSViewController {
let onChange: () -> Void
- private let vertexRegex = MapParsingPatterns.vertex
- private let edgeRegex = MapParsingPatterns.edge
- private let inertiaRegex = MapParsingPatterns.inertia
- private let opportunityRegex = MapParsingPatterns.opportunity
- private let noteRegex = MapParsingPatterns.note
- private let stageRegex = MapParsingPatterns.stage
- private let groupRegex = MapParsingPatterns.group
-
+ private let syntaxHighlighter = Wmap.SyntaxHighlighter()
private let changeDebouncer: Debouncer = Debouncer(seconds: 1)
init(
- text: Binding<String>, highlightRanges: [Range<String.Index>], selectedRange: Int,
+ text: Binding<String>,
+ parsedMap: [Wmap.Entity] = [],
+ highlightRanges: [Range<String.Index>],
+ selectedRange: Int,
onChange: @escaping () -> Void
) {
self._text = text
+ self.parsedMap = parsedMap
self.onChange = onChange
self.highlightRanges = highlightRanges
self.selectedRange = selectedRange
super.init(nibName: nil, bundle: nil)
+
+ Task {
+ syntaxHighlighter.updateSyntaxElements(from: parsedMap)
+ }
}
required init?(coder: NSCoder) {
@@ -73,6 +81,13 @@ class MapTextEditorController: NSViewController {
textView.isEditable = true
textView.font = NSFont.Theme.Editor.regular
textView.textContainerInset = NSSize(width: 0, height: Dimensions.Spacing.coziest)
+
+ // Disable word wrapping to prevent flashing on soft-wrapped lines
+ textView.textContainer?.widthTracksTextView = false
+ textView.textContainer?.containerSize = NSSize(
+ width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
+ textView.isHorizontallyResizable = true
+ textView.isVerticallyResizable = true
self.view = scrollView
}
@@ -154,123 +169,63 @@ extension MapTextEditorController: NSTextStorageDelegate {
override func textStorageDidProcessEditing(_ obj: Notification) {
if let textStorage = obj.object as? NSTextStorage {
- // Only colorize the edited range instead of the entire document
+ // Only process if this is a text content change, not an attribute change
+ guard textStorage.editedMask.contains(.editedCharacters) else {
+ return
+ }
+
let editedRange = textStorage.editedRange
if editedRange.location != NSNotFound {
- self.colorizeEditedText(textStorage: textStorage, editedRange: editedRange)
+ // Use a small delay to batch rapid changes and avoid flashing
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
+ guard let self = self,
+ let currentTextView = self.currentTextView,
+ let currentTextStorage = currentTextView.textStorage
+ else { return }
+
+ self.colorizeEditedText(textStorage: currentTextStorage, editedRange: editedRange)
+ }
}
}
}
- nonisolated private func colorizeEditedText(textStorage: NSTextStorage, editedRange: NSRange) {
+ private func colorizeEditedText(textStorage: NSTextStorage, editedRange: NSRange) {
// Expand range to include the entire line(s) that were edited
let string = textStorage.string as NSString
let lineRange = string.lineRange(for: editedRange)
- // Clear existing attributes in the range
+ // Clear existing syntax highlighting attributes in the range, but preserve background colors
textStorage.removeAttribute(.foregroundColor, range: lineRange)
- // Apply syntax highlighting only to the affected lines
- self.applySyntaxHighlighting(textStorage: textStorage, range: lineRange)
+ // Apply syntax highlighting
+ syntaxHighlighter.applySyntaxHighlighting(textStorage: textStorage, range: lineRange)
}
- nonisolated private func colorizeText(textStorage: NSTextStorage) {
+ private func colorizeText(textStorage: NSTextStorage) {
let range = NSMakeRange(0, textStorage.length)
- self.applySyntaxHighlighting(textStorage: textStorage, range: range)
- }
-
- nonisolated private func applySyntaxHighlighting(textStorage: NSTextStorage, range: NSRange) {
- var matches = vertexRegex.matches(in: textStorage.string, options: [], range: range)
-
- for match in matches {
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.vertex], range: match.range(at: 1))
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.number], range: match.range(at: 2))
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.number], range: match.range(at: 3))
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.option], range: match.range(at: 4))
- }
-
- matches = edgeRegex.matches(in: textStorage.string, options: [], range: range)
-
- for match in matches {
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.vertex], range: match.range(at: 1))
- let arrowRange = match.range(at: 2)
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.symbol],
- range: NSMakeRange(arrowRange.lowerBound - 1, arrowRange.length + 1))
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.vertex], range: match.range(at: 3))
- }
- matches = opportunityRegex.matches(in: textStorage.string, options: [], range: range)
+ // Clear all existing attributes
+ textStorage.removeAttribute(.foregroundColor, range: range)
- for match in matches {
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.option], range: match.range(at: 1))
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.vertex], range: match.range(at: 2))
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.symbol], range: match.range(at: 3))
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.number], range: match.range(at: 4))
- }
-
- matches = inertiaRegex.matches(in: textStorage.string, options: [], range: range)
-
- for match in matches {
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.option], range: match.range(at: 1))
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.vertex], range: match.range(at: 2))
- }
-
- matches = noteRegex.matches(in: textStorage.string, options: [], range: range)
-
- for match in matches {
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.option], range: match.range(at: 1))
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.number], range: match.range(at: 2))
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.number], range: match.range(at: 3))
- }
-
- matches = stageRegex.matches(in: textStorage.string, options: [], range: range)
-
- for match in matches {
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.option], range: match.range(at: 1))
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.number], range: match.range(at: 2))
- }
-
- matches = groupRegex.matches(in: textStorage.string, options: [], range: range)
-
- for match in matches {
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.option], range: match.range(at: 1))
- textStorage.addAttributes(
- [.foregroundColor: NSColor.Theme.Syntax.vertex], range: match.range(at: 2))
- }
+ syntaxHighlighter.applySyntaxHighlighting(textStorage: textStorage, range: range)
}
}
struct MapTextEditor: NSViewControllerRepresentable {
@Binding var text: String
+ var parsedMap: [Wmap.Entity] = []
var highlightRanges: [Range<String.Index>]
var selectedRange: Int
var onChange: () -> Void = {}
init(
- text: Binding<String>, highlightRanges: [Range<String.Index>] = [], selectedRange: Int = 0,
+ text: Binding<String>, parsedMap: [Wmap.Entity] = [],
+ highlightRanges: [Range<String.Index>] = [], selectedRange: Int = 0,
onChange: @escaping () -> Void = {}
) {
self._text = text
+ self.parsedMap = parsedMap
self.highlightRanges = highlightRanges
self.selectedRange = selectedRange
self.onChange = onChange
@@ -280,7 +235,8 @@ struct MapTextEditor: NSViewControllerRepresentable {
context: NSViewControllerRepresentableContext<MapTextEditor>
) -> MapTextEditorController {
return MapTextEditorController(
- text: $text, highlightRanges: highlightRanges, selectedRange: selectedRange,
+ text: $text, parsedMap: parsedMap, highlightRanges: highlightRanges,
+ selectedRange: selectedRange,
onChange: onChange)
}
@@ -288,16 +244,33 @@ struct MapTextEditor: NSViewControllerRepresentable {
_ nsViewController: MapTextEditorController,
context: NSViewControllerRepresentableContext<MapTextEditor>
) {
- nsViewController.highlightRanges = highlightRanges
+ // Only update if something actually changed to prevent excessive updates
+ var hasChanges = false
+
+ // Always update parsed map since the content may have changed even if count is same
+ nsViewController.parsedMap = parsedMap
+
+ if nsViewController.highlightRanges != highlightRanges {
+ nsViewController.highlightRanges = highlightRanges
+ hasChanges = true
+ }
+
if nsViewController.selectedRange != selectedRange {
nsViewController.selectedRange = selectedRange
+ hasChanges = true
}
// Update text view content if text has changed
if let textView = nsViewController.currentTextView {
if textView.string != text {
textView.string = text
+ hasChanges = true
}
}
+
+ // Only log if we made changes (for debugging)
+ if hasChanges {
+ print("MapTextEditor: Updated view controller")
+ }
}
}