Skip to content

A long line you can reach, and ⌥Z to fold it away - #20

Merged
necessarylion merged 2 commits into
mainfrom
long-lines-you-can-reach
Jul 30, 2026
Merged

A long line you can reach, and ⌥Z to fold it away#20
necessarylion merged 2 commits into
mainfrom
long-lines-you-can-reach

Conversation

@necessarylion

@necessarylion necessarylion commented Jul 30, 2026

Copy link
Copy Markdown
Owner

A line that ran off the right edge could not be reached. With wrapping off the editor drew the whole line and then refused to scroll sideways to any of it, while scrolling down worked perfectly — which is the shape of the bug, because the height is measured somewhere else entirely.

The width is measured in CodeEditTextView, and the measurement is thrown away:

private func layoutLine(, maxFoundLineWidth: inout CGFloat)  {
    
    var maxFoundLineWidth = maxFoundLineWidth   // shadows the parameter
    
    if maxFoundLineWidth < lineSize.width { maxFoundLineWidth = lineSize.width }
}

That var shadows the inout parameter with a local copy, so every line's width is written into the copy and dropped when the method returns. maxLineWidth never leaves zero, estimatedWidth() is therefore the gutter's inset alone, and updateFrameIfNeeded sizes the document to max(inset, pane width) — never wider than the pane, so the scroll view has nothing to scroll and the rest of the line is simply cut off.

So the measurement is made here instead, from the same numbers: WidenDocumentForLongLines takes the same route ClipFloatingSubviews already takes for the same kind of reason — a coordinator, and the controller it is handed. The widest line fragment actually laid out, plus the same insets, written to the text view's frame. Only the widest laid-out line, because that is all the package itself would ever have known: the width grows as the reader scrolls into longer lines and never shrinks, which is the behaviour maxLineWidth was written to have.

It has to be re-applied rather than set once, because the package puts its own too-narrow number back on every scroll — so the two notifications are exactly the two moments it does that: the clip view's bounds moving, and the frame being reset underneath.

Both observers run synchronously, and that is not a detail. Handed .main the block is an operation on the main queue and lands a turn later, so the narrow frame is live when the scroll view next tiles — and a clip view whose document has just shrunk under it clamps its origin back to zero. The reader would be thrown back to column one every time they scrolled down. Widening from inside the setter means the narrow width never survives long enough to be acted on.

Wrapping on is left alone entirely: there the pane's width is the answer and a wide document would be the bug. And the whole thing goes quiet by itself if this is ever fixed upstream, because the frame is only widened when the package has left it narrower than the text needs.

⌥Z folds a long line into the pane, and the same key lets it run off the edge again. VS Code's key for it, which is the one a reader coming from there tries first. It is a ShortcutAction rather than a key literal, so the Editor menu's Wrap Lines item wears it and Settings → Shortcuts can move it or take it away — there is a new Editor group holding it.

Checked

The defect and the fix were both run against the real pinned package in a headless harness, with a 6149pt line laid out in a 600pt pane:

BEFORE          docWidth=600  estimatedWidth=40   reachableX(400)=0      ← cannot move sideways
AFTER apply()   docWidth=6189 widest=6149         reachableX(400)=400 reachableX(99999)=5589
AFTER package   docWidth=600                      reachableX(400)=0      ← it resets on every scroll
AFTER re-apply  docWidth=6189 reapplied=true      reachableX(400)=400
WRAPPING ON     docWidth=600  touched=false                              ← left alone

(reachableX is what NSClipView.constrainBoundsRect will actually allow.) Scripts/run.sh — Debug build succeeded, no warnings, app launches.

Not checked by me: the scrolling as felt under a trackpad, and ⌥Z as pressed, both of which are the user's screen.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QZeMBBySiGJmqM81eXurS3

Summary by CodeRabbit

  • New Features

    • Added a “Wrap Lines” editor option, available from the command menu and via ⌥Z.
    • Toggle between wrapping long lines within the editor and scrolling horizontally.
    • Added the shortcut to the Editor section of keyboard shortcut settings.
  • Documentation

    • Updated the README with the new line-wrapping behavior and shortcut.

**A line that ran off the right edge could not be reached.** With wrapping off
the editor drew the whole line and then refused to scroll sideways to any of it,
while scrolling down worked perfectly — which is the shape of the bug, because
the height is measured somewhere else entirely.

The width is measured in CodeEditTextView, and the measurement is thrown away:

    private func layoutLine(…, maxFoundLineWidth: inout CGFloat) … {
        …
        var maxFoundLineWidth = maxFoundLineWidth   // shadows the parameter
        …
        if maxFoundLineWidth < lineSize.width { maxFoundLineWidth = lineSize.width }
    }

That `var` shadows the `inout` parameter with a local copy, so every line's width
is written into the copy and dropped when the method returns. `maxLineWidth`
never leaves zero, `estimatedWidth()` is therefore the gutter's inset alone, and
`updateFrameIfNeeded` sizes the document to `max(inset, pane width)` — never
wider than the pane, so the scroll view has nothing to scroll and the rest of the
line is simply cut off.

So the measurement is made here instead, from the same numbers:
`WidenDocumentForLongLines` takes the same route `ClipFloatingSubviews` already
takes for the same kind of reason — a coordinator, and the controller it is
handed. The widest line fragment actually laid out, plus the same insets, written
to the text view's frame. Only the widest *laid-out* line, because that is all
the package itself would ever have known: the width grows as the reader scrolls
into longer lines and never shrinks, which is the behaviour `maxLineWidth` was
written to have.

**It has to be re-applied rather than set once**, because the package puts its
own too-narrow number back on every scroll — so the two notifications are exactly
the two moments it does that: the clip view's bounds moving, and the frame being
reset underneath.

**Both observers run synchronously, and that is not a detail.** Handed `.main`
the block is an operation on the main queue and lands a turn later, so the narrow
frame is live when the scroll view next tiles — and a clip view whose document
has just shrunk under it clamps its origin back to zero. The reader would be
thrown back to column one every time they scrolled down. Widening from inside the
setter means the narrow width never survives long enough to be acted on.

Wrapping on is left alone entirely: there the pane's width is the answer and a
wide document would be the bug. And the whole thing goes quiet by itself if this
is ever fixed upstream, because the frame is only widened when the package has
left it narrower than the text needs.

**⌥Z folds a long line into the pane, and the same key lets it run off the edge
again.** VS Code's key for it, which is the one a reader coming from there tries
first. It is a `ShortcutAction` rather than a key literal, so the Editor menu's
Wrap Lines item wears it and Settings → Shortcuts can move it or take it away —
there is a new **Editor** group holding it.

Checked: the defect and the fix were both run against the real pinned package in
a headless harness. Before, with a 6149pt line laid out: `estimatedWidth=40`,
document 600 wide, and the clip view refusing any horizontal origin at all
(`constrainBoundsRect` clamps x=400 to 0). After the coordinator's measurement:
document 6189, x=400 reachable, up to 5589. Then the package's own
`updateFrameIfNeeded` puts 600 back, and re-applying restores it — which is the
loop the notifications close. With wrapping on the frame is untouched.
`Scripts/run.sh` — Debug build succeeded, no warnings, app launches. Not checked
by me: the scrolling as felt under a trackpad, which is the user's screen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZeMBBySiGJmqM81eXurS3
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@necessarylion, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0eaabf9a-a672-4912-ba58-1c1285128e77

📥 Commits

Reviewing files that changed from the base of the PR and between 0996b12 and 1d483bf.

📒 Files selected for processing (1)
  • Package.swift
📝 Walkthrough

Walkthrough

The editor adds an ⌥Z shortcut for toggling long-line wrapping, registers it under the Editor shortcut group, and widens the text view when visible lines exceed its frame width. README documentation describes the new behavior.

Changes

Editor wrapping behavior

Layer / File(s) Summary
Shortcut registration
Sources/Workspace/Models/KeyboardShortcuts.swift
Registers toggleWrap under the Editor group with an ⌥Z default chord and user-facing title and detail text.
Editor long-line handling
Sources/Workspace/WorkspaceApp.swift, Sources/Workspace/Editor/CodeEditorView.swift
Adds the shortcut to the Wrap Lines command and introduces a coordinator that widens the text view based on visible line fragment widths.
Shortcut documentation
README.md
Documents ⌥Z as the shortcut for wrapping long lines or enabling horizontal scrolling.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WorkspaceApp
  participant EditorPane
  participant WidenDocumentForLongLines
  participant TextView
  WorkspaceApp->>EditorPane: Toggle Wrap Lines with toggleWrap
  EditorPane->>WidenDocumentForLongLines: Apply editor sizing
  WidenDocumentForLongLines->>TextView: Measure visible line fragments
  WidenDocumentForLongLines->>TextView: Widen frame for overflow lines
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is clearly related to the long-line scrolling fix and the new ⌥Z wrap shortcut.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch long-lines-you-can-reach

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Workspace/Editor/CodeEditorView.swift`:
- Around line 621-632: Update Package.swift to declare CodeEditTextView as a
direct dependency with an exact version pin at 0.12.1, matching the internals
used by the coordinator around lineStorage, lineFragments, fragment.data.width,
and edgeInsets. Keep the existing CodeEditSourceEditor dependency intact, and
ensure the explicit product/package reference resolves to the pinned version.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 82e77cdb-310d-4657-92a4-4f263991f2e5

📥 Commits

Reviewing files that changed from the base of the PR and between 5398676 and 0996b12.

📒 Files selected for processing (4)
  • README.md
  • Sources/Workspace/Editor/CodeEditorView.swift
  • Sources/Workspace/Models/KeyboardShortcuts.swift
  • Sources/Workspace/WorkspaceApp.swift

Comment thread Sources/Workspace/Editor/CodeEditorView.swift
`WidenDocumentForLongLines` walks CodeEditTextView's layout internals —
`lineStorage`, a line's `lineFragments`, a fragment's `width`, the layout
manager's `edgeInsets` — and that package was reaching us transitively through
CodeEditSourceEditor, which is to say nothing in the manifest promised any
version of it at all. Three of our files import it directly (the coordinator,
the gutter markers and the language server join), so it is named here now and
pinned to the version already resolved.

`exact: "0.12.1"` narrows the `from: "0.12.1"` CodeEditSourceEditor 0.15.2 asks
for rather than contradicting it, so `Package.resolved` does not move — verified
byte-identical. The pin is the point: internal layout can be rearranged in a
patch release, the compiler only notices if a name changes, and raising it is an
audit of that coordinator rather than a routine update.

Raised by CodeRabbit on #20.

Checked: `Scripts/bundle.sh` — build succeeded, `Package.resolved` unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZeMBBySiGJmqM81eXurS3
@necessarylion
necessarylion merged commit e29ad6e into main Jul 30, 2026
2 checks passed
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.

1 participant