Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(editor): keep a sideways scroll near the start of a long line from snapping back (#2841) - #2848

Merged
datlechin merged 2 commits into
mainfrom
fix/editor-horizontal-scroll-snap
Sep 14, 2026
Merged

datlechin merged 2 commits into
mainfrom
fix/editor-horizontal-scroll-snap

Conversation

@datlechin

Copy link
Copy Markdown
Member

Summary

Scrolling sideways a few points into a long line in the query editor snapped the view back to the start of the line, and the bounce past the start was cut short. On a trackpad that reads as the text sliding back and forth. It is a regression from #2715 (0.74.0).

Root cause

#2715 reserved the gutter's width on the clip view by overriding SourceEditorScrollView.tile(). On every tile it:

  1. turned the clip view's automaticallyAdjustsContentInsets on for the length of super.tile(), to learn the insets AppKit computes,
  2. turned it off again and wrote those insets plus the gutter and minimap widths back,
  3. put the clip view back where it had been, treating any origin at or below 0.5 as "at the leading edge".

Step 1 drops the reservation for the length of the call, so AppKit re-clamps the clip view to the narrower edge. Measured with a verbatim copy in a standalone AppKit probe: a view scrolled 30pt into the document (origin −34 with a 64pt gutter) went to 0 inside super.tile(), and step 3 then sent it to −64, the start of the line.

AppKit tiles the scroll view at the start of a scroll gesture (measured: delivering a began-phase scroll event calls tile()), not only on resize. So any small scroll that starts inside the first gutter-width of a long line, and the elastic overscroll at the start of the line, was undone within a frame.

The reporter's 60fps recording matches this frame for frame. While resting near the start of the line, the whole document is displaced for a single frame (+76, −6, +24, +4 and +2 px on a 2x display) and then returns. The statement highlight, drawn at the text view's x = 0, moves with the text while the caret-line highlight, drawn from the visible rect, does not. The scroll position itself is jumping; the glyphs are drawn correctly.

The recording also has one jump of about 755px held for three frames. A tile that re-applied a main-thread position lagging behind a live responsive scroll would produce that, and this change removes every scroll from tile(), but I could not reproduce that case, so this PR does not claim it.

Fix

tile() lays out and no longer scrolls.

  • SourceEditorClipView (new) owns the reservation. Its contentInsets getter adds the gutter and minimap widths on top of the insets AppKit computes, and automatic adjustment stays on for good.
  • SourceEditorScrollView installs it as its contentView, drops the tile() override, and turns floatingSubviewInsets into a forward to the clip view. It re-places the clip view only when the reservation actually changes, and measures "resting at the leading edge" against the old reservation instead of 0.5. A gutter that gains a digit still moves a view at the start of the line over, and a view scrolled 30pt in keeps its offset.

Measured parity in a probe that compared an NSClipView with the merged insets written into it against the overriding clip view: identical clamped origins at the start and end, scrollToVisible in both directions, drag autoscroll past both edges, a document that narrows and widens, a window resize, overlay scroller frames, knob proportion and knob position. Legacy scroller room, ruler room and a top inset added later all still come from AppKit.

Considered and not taken:

  • Putting the reservation in NSScrollView.contentInsets: the controller overwrites that from the host configuration, AppKit ignores it while the scroll view adjusts its insets automatically, and it would also inset the scrollers.
  • Overriding NSClipView.constrainBoundsRect(_:): not needed, AppKit's clamping already follows the insets.
  • Going back to TextView.textInsets: that only offsets the glyphs, which is how When the excessively long characters are removed, the scrolling display becomes abnormal. #2709 happened.

The paste suggestion

The follow-up comment asks that a long paste leave the view where it was. This keeps scrolling to the caret after a paste. Measured in NSTextView (TextKit 1 and 2): pasting a long line at the end of a document scrolls the clip view to the caret at the end of the pasted text. VS Code (revealAll after CursorChangeReason.Paste) and IntelliJ (scrollToCaret in PasteHandler) do the same, and the HIG asks for the insertion point to stay in view.

Before / After

Not captured. The defect lasts one frame during a live trackpad gesture, so a still screenshot shows the same editor before and after. The reporter's 60fps recording on #2841 shows the before, and the frame numbers above come from it. A matching after recording needs a physical trackpad gesture, which I could not drive from automation.

Tests

  • SourceEditorScrollViewTests (new cases):
    • a tile leaves a view scrolled just past the leading edge where it is
    • a widening gutter keeps that offset
    • the gutter reservation going to 0 and back, at the edge and scrolled
    • a trailing reservation change while scrolled
    • the clip view keeps adjusting its own insets
    • SourceEditorScrollView() installs the reserving clip view
  • TextViewControllerFloatingInsetsTests: the real controller in a window, scrolled 20pt into a long line, keeps its position through a tile and a window layout pass.
  • SQLEditorLongLineScrollTests: the same through the editor TablePro ships.

No UI automation. I wrote one (type a long line, scroll to its start, scroll 24pt in, check the text view's frame stayed put) and it passed on this branch and on the old code alike. XCUICoordinate.scroll(byDeltaX:deltaY:) does not deliver a phased trackpad gesture, so it never runs the gesture-start tile that caused the snap, and a test that cannot fail on the bug guards nothing. The unit cases above run the same tile directly and do fail on the old code.

Verification

  • swift test --package-path LocalPackages/CodeEditSourceEditor --filter 'SourceEditorScrollViewTests|TextViewControllerFloatingInsetsTests|TextViewControllerTests': 36 of 36 Swift Testing cases pass. TextViewControllerTests.test_foldingRibbonToggle fails (66.47 vs 59.47) identically on main without this change; it measures the gutter's fold ribbon width and is unrelated.
  • Negative control, the same package tests with the old SourceEditorScrollView: the new tile, gutter-widening, clip-view and controller cases fail (the old tile moved a view at −40 to the leading edge), and every existing case passes.
  • TableProTests, verify.sh test over SQLEditorLongLineScrollTests FoldGutterLayoutTests FoldCommandBehaviourTests EditorLifecycleTeardownTests EditorKeyMonitorCompositionTests SQLEditorCoordinatorTests SQLEditorCoordinatorCleanupTests SQLEditorCoordinatorEscapeMenuTests: 84 of 84 pass.
  • Debug build and SwiftLint --strict on every changed file: clean.
  • Review: Codex could not run (the workspace hit its usage limit), so the diff was reviewed with the code-review skill, which found no defects.

Fixes #2841

https://claude.ai/code/session_01VKMXaV4k37enG9UZAXxop8

@datlechin
datlechin merged commit cb87416 into main Sep 14, 2026
4 checks passed
@datlechin
datlechin deleted the fix/editor-horizontal-scroll-snap branch September 14, 2026 13:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

When there is a long string, there will be an illusion of left-right sliding.

1 participant