diff options
Diffstat (limited to 'Map/Presentation/Base Components/MapTextEditor.swift')
| -rw-r--r-- | Map/Presentation/Base Components/MapTextEditor.swift | 167 |
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") + } } } |