You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Not a bug — a design question before I write anything, since CONTRIBUTING says an architectural change is better sketched than described. If the shape below sounds right I'll bring it as a draft PR.
Use case
A two-pane Markdown app: source on the left, rendered preview on the right, both NativeTextViewWrapper over the same text. Scrolling one pane should carry the other.
What's missing
In .scrolls (the default) an embedder has neither half of that:
No signal out. Nothing reports where the reader is. onPersistScrollOffset / restoreScrollOffset are scroll memory across document switches — they fire on switch-away and teardown, not while scrolling, and speak raw y points.
No command in. Nothing scrolls a pane to a position. scrollOffsets is internal, ClampedScrollView has no public surface, and the NSScrollView never reaches the embedder through SwiftUI.
heightBehavior: .fitsContent is a real workaround — the enclosing ScrollView becomes mine, so I can sync it myself — but it forgoes TextKit-2 viewport virtualization, which the docs already warn about and which #107 suggests matters for large notes. It also only buys me proportional sync, and that drifts precisely when the two panes are styled differently (a source pane with flattened heading multipliers and a mono font wraps differently from the rendered one).
Why this is a smaller ask than it sounds
The mapping problem that usually sinks editor↔preview sync doesn't exist here. Both panes are the same engine over the same string, so character offset is already a shared coordinate space — no source maps, no line tables. And both halves exist internally:
the clip view's boundsDidChangeNotification observer (NativeTextViewWrapper.swift:384) already fires on every scroll and already does five things
NativeTextView.scrollRangeToVisible(_:) (NativeTextView+FrameAndOverscroll.swift:302) already handles the reading-column and overscroll cases
textLayoutFragment(for:) is already used in four places for point → fragment
So the seam is roughly: report the top visible text location, and accept a request to bring a text location to the top.
Question
#89 proposed almost exactly the missing half — a ScrollHandler the embedder owns and the engine fills in makeNSView, plus a shared scrollRangeIntoView(_:in:) — for a TOC panel. It was closed by its own author with no discussion, and none of it is in main. Was there something wrong with that shape, or did it simply lapse? I'd rather build on your answer than guess.
Three things I'd want to get right, whatever the shape:
Feedback loop. A drives B, B's bounds change, B drives A. Needs an explicit "currently driven" state; I don't think it can be left to embedders to debounce.
Cost of the round trip. A @Binding in the pendingInlineReplacement style is clean but goes through SwiftUI's update cycle every frame — probably too slow for scrolling. That's an argument for feat(toc): add headings extraction, scroll-to-range, and per-editor TOC integration #89's imperative handle over a binding, which is why I'm asking rather than assuming.
Happy to take the smallest version of this you'd accept — even just the read-only half (a "visible range changed" callback), which is enough for a scrollbar-position indicator and half of the sync.
Environment
macOS 15.7.9, Xcode 26.3, Swift 6.2.4, MarkdownEngine main (08ff3c0)
Not a bug — a design question before I write anything, since CONTRIBUTING says an architectural change is better sketched than described. If the shape below sounds right I'll bring it as a draft PR.
Use case
A two-pane Markdown app: source on the left, rendered preview on the right, both
NativeTextViewWrapperover the same text. Scrolling one pane should carry the other.What's missing
In
.scrolls(the default) an embedder has neither half of that:onPersistScrollOffset/restoreScrollOffsetare scroll memory across document switches — they fire on switch-away and teardown, not while scrolling, and speak rawypoints.scrollOffsetsis internal,ClampedScrollViewhas no public surface, and theNSScrollViewnever reaches the embedder through SwiftUI.heightBehavior: .fitsContentis a real workaround — the enclosingScrollViewbecomes mine, so I can sync it myself — but it forgoes TextKit-2 viewport virtualization, which the docs already warn about and which #107 suggests matters for large notes. It also only buys me proportional sync, and that drifts precisely when the two panes are styled differently (a source pane with flattened heading multipliers and a mono font wraps differently from the rendered one).Why this is a smaller ask than it sounds
The mapping problem that usually sinks editor↔preview sync doesn't exist here. Both panes are the same engine over the same string, so character offset is already a shared coordinate space — no source maps, no line tables. And both halves exist internally:
boundsDidChangeNotificationobserver (NativeTextViewWrapper.swift:384) already fires on every scroll and already does five thingsNativeTextView.scrollRangeToVisible(_:)(NativeTextView+FrameAndOverscroll.swift:302) already handles the reading-column and overscroll casestextLayoutFragment(for:)is already used in four places for point → fragmentSo the seam is roughly: report the top visible text location, and accept a request to bring a text location to the top.
Question
#89 proposed almost exactly the missing half — a
ScrollHandlerthe embedder owns and the engine fills inmakeNSView, plus a sharedscrollRangeIntoView(_:in:)— for a TOC panel. It was closed by its own author with no discussion, and none of it is inmain. Was there something wrong with that shape, or did it simply lapse? I'd rather build on your answer than guess.Three things I'd want to get right, whatever the shape:
@Bindingin thependingInlineReplacementstyle is clean but goes through SwiftUI's update cycle every frame — probably too slow for scrolling. That's an argument for feat(toc): add headings extraction, scroll-to-range, and per-editor TOC integration #89's imperative handle over a binding, which is why I'm asking rather than assuming.makeNSViewobserver tokens are never released. Hanging a public callback off that same observer would deepen an existing leak, so it may want fixing first or together.Happy to take the smallest version of this you'd accept — even just the read-only half (a "visible range changed" callback), which is enough for a scrollbar-position indicator and half of the sync.
Environment
macOS 15.7.9, Xcode 26.3, Swift 6.2.4, MarkdownEngine
main(08ff3c0)