From 29844db06ddaf96eef8348b36bc74c00e4ef9aa2 Mon Sep 17 00:00:00 2001 From: Ilia Sazonov Date: Mon, 27 Apr 2026 12:48:48 -0700 Subject: [PATCH] Fix viewport blanking after large programmatic scroll (setFrameSize re-entrance) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `relocateViewport(to: docStart)` calls `setFrameSize(width, lineHeight)` from a scrolled-down position, AppKit must synchronously retract the clip view to fit the new bounds. That fires `prepareContent` from inside `super.setFrameSize`, which calls `updateContentSizeIfNeeded` and re-enters `setFrameSize` with the full document height. The recursive call correctly resizes `contentView`. When control returns to the outer call, `newSize` is still the original (small) value, and `contentView.frame.size = newSize` stomps the recursive call's result. End state: textView at full height, contentView and contentViewportView at one-line height, fragment views past line 1 clipped. Manifests as Cmd-End → Cmd-Home blanking the editor on a multi-screen word-wrapped document. Resizing the window or toggling word-wrap restores rendering because both go through a fresh non-re-entrant `setFrameSize`. Fix: after `super.setFrameSize`, read `frame.size` (which reflects any recursive resize) rather than the stale `newSize` parameter. When no re-entrance occurred, `frame.size == newSize`, so the common path is unchanged. --- Sources/STTextViewAppKit/STTextView.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Sources/STTextViewAppKit/STTextView.swift b/Sources/STTextViewAppKit/STTextView.swift index 111b0f2..3f536d7 100644 --- a/Sources/STTextViewAppKit/STTextView.swift +++ b/Sources/STTextViewAppKit/STTextView.swift @@ -1465,11 +1465,18 @@ open class STTextView: NSView, NSTextInput, NSTextContent, STTextViewProtocol { override open func setFrameSize(_ newSize: NSSize) { super.setFrameSize(newSize) + // `super.setFrameSize` can synchronously fire `prepareContent`, which + // may recursively call back into `setFrameSize` with a different + // size. In that case `self.frame.size` no longer matches `newSize`; + // use the current frame so we don't stomp the recursive call's + // result and leave `contentView` pinned to the intermediate size. + let effectiveSize = frame.size + // contentView should always fill the entire STTextView contentView.frame.origin.x = gutterView?.frame.width ?? 0 - contentView.frame.size = newSize + contentView.frame.size = effectiveSize - updateTextContainerSize(proposedSize: newSize) + updateTextContainerSize(proposedSize: effectiveSize) if inLayout { needsRelayout = true