diff --git a/Vendor/swift-markdown-engine/Sources/MarkdownEngine/Diagnostics/PerfTrace.swift b/Vendor/swift-markdown-engine/Sources/MarkdownEngine/Diagnostics/PerfTrace.swift index 31dae22..527e2e1 100644 --- a/Vendor/swift-markdown-engine/Sources/MarkdownEngine/Diagnostics/PerfTrace.swift +++ b/Vendor/swift-markdown-engine/Sources/MarkdownEngine/Diagnostics/PerfTrace.swift @@ -9,7 +9,9 @@ // which costs grow with file size instead of staying constant. The whole point: // type in a short file, then a long one, and compare `total` for the same edit. // -// Toggle: set the env var MD_PERF=0 in the run scheme to silence. +// Toggle: set the env var MD_PERF=1 in the run scheme to enable. +// Off by default even in Debug — opt-in, not opt-out, so a normal +// debug run stays quiet. // Debug-only — the whole thing compiles out in Release. // Remove before shipping (this file + the `PerfTrace.` call sites). // @@ -18,7 +20,7 @@ import Foundation enum PerfTrace { #if DEBUG - static var enabled = ProcessInfo.processInfo.environment["MD_PERF"] != "0" + static var enabled = ProcessInfo.processInfo.environment["MD_PERF"] == "1" /// Opt-in for the sampled full-rebuild verifier asserts (wiki splice, /// backtick census, parse buffer). They run 3× O(doc) work synchronously /// on every 64th keystroke — periodic spikes that pollute the PERF diff --git a/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+CodeBlocks.swift b/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+CodeBlocks.swift index 01e38f9..707d5c4 100644 --- a/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+CodeBlocks.swift +++ b/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+CodeBlocks.swift @@ -15,7 +15,7 @@ import AppKit extension NativeTextViewCoordinator { func updateCodeBlockSelection(textView: NSTextView, parsed: ParsedDocument? = nil) { guard let textContainer = textView.textContainer else { - onCodeBlockSelectionChange?([]) + fireCodeBlockSelectionChange([]) return } @@ -24,7 +24,7 @@ extension NativeTextViewCoordinator { // no per-call full-token filter. cachedCodeBlockTokens = parsed.codeBlockTokensWithIndices } else if cachedCodeBlockTokens.isEmpty { - onCodeBlockSelectionChange?([]) + fireCodeBlockSelectionChange([]) return } @@ -91,6 +91,6 @@ extension NativeTextViewCoordinator { ) } - onCodeBlockSelectionChange?(selections) + fireCodeBlockSelectionChange(selections) } } diff --git a/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+CommentAnchors.swift b/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+CommentAnchors.swift index ffa133e..5b36ad5 100644 --- a/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+CommentAnchors.swift +++ b/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+CommentAnchors.swift @@ -16,7 +16,7 @@ import AppKit extension NativeTextViewCoordinator { func updateCommentAnchorRects(textView: NSTextView) { guard !commentAnchorQueries.isEmpty else { - onCommentAnchorRectsChange?([]) + fireCommentAnchorRectsChange([]) return } let nsText = textView.string as NSString @@ -28,6 +28,6 @@ extension NativeTextViewCoordinator { let rect = textView.viewRect(forCharacterRange: found, using: layoutBridge) else { continue } results.append(CommentAnchorRect(id: query.id, rect: rect)) } - onCommentAnchorRectsChange?(results) + fireCommentAnchorRectsChange(results) } } diff --git a/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+TextDelegate.swift b/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+TextDelegate.swift index b603db5..b8fd178 100644 --- a/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+TextDelegate.swift +++ b/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+TextDelegate.swift @@ -362,7 +362,7 @@ extension NativeTextViewCoordinator { // selection at all. if !isRebuildingDocument { let selRange = tv.selectedRange() - onSelectedTextChange?(selRange.length > 0 ? (tv.string as NSString).substring(with: selRange) : nil) + fireSelectedTextChange(selRange.length > 0 ? (tv.string as NSString).substring(with: selRange) : nil) } // Raw mode: plain source — no reveal, snap-back, or inline previews. if configuration.rawSourceMode { return } diff --git a/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator.swift b/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator.swift index e7c9471..2830553 100644 --- a/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator.swift +++ b/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator.swift @@ -87,6 +87,26 @@ public final class NativeTextViewCoordinator: NSObject, NSTextViewDelegate { var onSelectedTextChange: ((String?) -> Void)? var commentAnchorQueries: [CommentAnchorQuery] = [] var onCommentAnchorRectsChange: (([CommentAnchorRect]) -> Void)? + + /// These three callbacks can fire from inside `NativeTextViewWrapper.updateNSView` + /// itself (a programmatic edit re-enters `textViewDidChangeSelection`/ + /// `textDidChange` synchronously — see `isRebuildingDocument` above), which is a + /// SwiftUI view update already in progress on the call stack. Calling straight into + /// an embedder's `@State` setter there trips "Modifying state during view update" — + /// deferring one runloop tick is enough to land outside it, same as how + /// `NativeTextViewWrapper` already clears its `pending*` bindings. + func fireCodeBlockSelectionChange(_ selections: [CodeBlockSelection]) { + DispatchQueue.main.async { [onCodeBlockSelectionChange] in onCodeBlockSelectionChange?(selections) } + } + + func fireSelectedTextChange(_ text: String?) { + DispatchQueue.main.async { [onSelectedTextChange] in onSelectedTextChange?(text) } + } + + func fireCommentAnchorRectsChange(_ rects: [CommentAnchorRect]) { + DispatchQueue.main.async { [onCommentAnchorRectsChange] in onCommentAnchorRectsChange?(rects) } + } + var didInitialFormatting: Bool = false /// One-shot guard so `updateCodeBlockSelection` only forces a full-document layout once per document. var didEnsureLayoutForCurrentDocument: Bool = false