Skip to content

editor: Support mirrored gutters for side-by-side diffs - #3417

Merged
huacnlee merged 8 commits into
longbridge:nextfrom
GigLaboCom:editor-gutter-right-side
Oct 9, 2026
Merged

huacnlee merged 8 commits into
longbridge:nextfrom
GigLaboCom:editor-gutter-right-side

Conversation

@glani

@glani glani commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Description

Builds on #3416 (ScrollbarPlacement, merged into next) and #3359's gutter markers.

In a side-by-side diff, both gutters can face the center, so the line numbers of corresponding lines sit next to each other across the divider and can be compared directly. The editor's gutter is always on the left of the text today, so the left pane cannot do that.

This adds gutter_side(Side) / set_gutter_side to the editor state for the left pane of such a diff, default Side::Left (unchanged). On the right the gutter is mirrored: the same columns, in the same order counted from the text, with the line numbers aligned toward it. The text and gutter x offsets are computed once per layout and used for painting, hit testing, IME bounds and scroll-into-view. A scrollbar on the gutter's side stays outermost. With the gutter on the right and the scrollbar on the left — the left pane's outer edge — the text keeps clear of the scrollbar's whole track: its effective width, the active thumb included, less the editor's left padding, at least the usual 10 px margin; that one reservation sets the text origin, width, wrap width and scroll size.

Line numbers are placed by their shaped width rather than padded with spaces: in a proportional font a space is about a third of a digit, so the default left gutter left right-aligned numbers ragged and 6–11 px short of the text; they now line up against it on either side. Editor puts its narrow padding on the gutter's side.

gutter_order([GutterColumn]) orders the gutter's columns from the text outward, on either side: the default is [FoldIcons, LineNumbers, Markers] (unchanged); [FoldIcons, Markers, LineNumbers] puts a diff's change markers between the text and the line numbers, as IntelliJ's diff does. A column left out follows the listed ones in its default order.

The Editor Diff story (70b271ad, @huacnlee) shows original and modified source with change markers; its Options menu switches between both gutters on the left and center-facing gutters, and optionally puts the markers before the line numbers. The two panes scroll together, by wheel and by dragging either scrollbar. The ordinary Editor story is unchanged.

Screenshot

Before After
scrollbar-left mirrored

A side-by-side diff with changed lines marked; the left pane uses .scrollbar_placement(ScrollbarPlacement::BottomLeft).gutter_side(Side::Right), mirrored:

Both gutters on the left Mirrored
both gutters on the left mirrored

The columns in IntelliJ's order, .gutter_order([GutterColumn::FoldIcons, GutterColumn::Markers, GutterColumn::LineNumbers]) on both panes — without and with folding (an empty fold column here):

IntelliJ's order With folding
IntelliJ's order IntelliJ's order with folding

Public API

gpui_base (re-exported by gpui_component):

  • InputBaseState::gutter_side(mut self, side: Side) -> Self — builder: which side of the text the gutter is drawn on; default Side::Left, Side::Right for the left pane of a side-by-side diff.
  • InputBaseState::set_gutter_side(&mut self, side: Side, cx: &mut Context<Self>) — the same, at runtime.
  • InputPresentation::gutter_side(&self) -> Side — the side the editor's gutter sits on.
  • GutterColumn { FoldIcons, LineNumbers, Markers } — a column of the gutter.
  • InputBaseState::gutter_order(mut self, columns: impl IntoIterator<Item = GutterColumn>) -> Self / set_gutter_order(&mut self, columns, cx: &mut Context<Self>) — the columns from the text outward; default [FoldIcons, LineNumbers, Markers]; a column left out follows in its default order, a repeat is ignored.

How to Test

  • cargo test -p gpui-base --lib: new tests for the text and gutter origins with an unchanged wrap width, the mirrored fold icons and columns, the scrollbar staying outermost (left/left, right/left, right/right), clicks in the text and in the gutter on both sides, caret / IME / range_to_bounds / selection-path x including a long line scrolled clear of the gutter, a right-side case in the existing gutter-bounds test, every gutter column — markers included — mirrored to 0.01 px for all six orders with folding on and off, the order normalised and counted from the text on both sides, clicks on the fold icon and the line number in every side/order combination, Editor keeping its gutter-side padding, and the text clear of the whole track of a left scrollbar with zero padding and with a wider themed track. Each fails with its code reverted.
  • cargo test -p gpui-component-story --lib editor_diff: layout switching keeps both sources, and the panes stay aligned on wheel input and while either scrollbar is dragged.
  • cargo run -p gpui-component-story -- 'Editor Diff', then Options.

Checklist

  • I have read the CONTRIBUTING document and followed the guidelines.
  • Reviewed the changes in this PR and confirmed AI generated code (If any) is accurate.
  • Passed cargo run for story tests related to the changes.
  • Tested macOS, Windows and Linux platforms performance (if the change is platform-specific)

@glani
glani force-pushed the editor-gutter-right-side branch 2 times, most recently from 48a3393 to fb4861c Compare October 8, 2026 21:14
@glani
glani force-pushed the editor-gutter-right-side branch from fb4861c to f7844c9 Compare October 8, 2026 21:30
@glani
glani changed the base branch from main to next October 8, 2026 21:30
@glani
glani marked this pull request as ready for review October 8, 2026 21:36
@glani
glani force-pushed the editor-gutter-right-side branch from f7844c9 to a38e740 Compare October 9, 2026 12:06
@huacnlee huacnlee changed the title editor: Let the gutter sit on the right side editor: Support mirrored gutters and configurable column order Oct 9, 2026

@huacnlee huacnlee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This PR adds right-side editor gutters and configurable column ordering for mirrored diff panes. The requirement and shared-geometry approach are reasonable. I reviewed only the three commits after #3416's 14ef7932c, at head a38e74079.

Please address the following before merging:

  1. P2 — Reserve the effective scrollbar width when keeping text clear. In crates/base/src/input/base/element.rs:2779–2782, clearance is fixed at RIGHT_MARGIN (10 px), while the default scrollbar track is 16 px wide and theme configuration can make it wider. A Base editor with zero padding, a right gutter, and a left scrollbar starts text at x = 10: the track intercepts clicks over the first 6 px of text, and the active thumb overlaps the first 2 px. The current test supplies 6 px of left padding, masking this case. Compute clearance from the effective scrollbar geometry and editor padding, and apply it consistently to text origin, width, wrapping, and scrolling. Please cover zero padding and a wider themed track.

  2. Qualify the marker placement documentation. website/component/editor.md:389 and crates/base/src/input/editor/line_decorations.rs:212 describe markers as always outside the line numbers. That is only the default order; gutter_order can move them inward. Update the Rust documentation and both website locales accordingly.

I also found that the Public API inventory omitted InputPresentation::gutter_side(&self) -> Side; I have included it while updating the English title and description.

Validation: source/diff review and git diff --check passed. Tests and real-window validation were not run. No other concrete regressions were identified in mirrored columns, hit testing, or IME coordinate handling.

@huacnlee huacnlee changed the title editor: Support mirrored gutters and configurable column order editor: Let the gutter sit on the right side Oct 9, 2026
@glani
glani force-pushed the editor-gutter-right-side branch from a38e740 to f617384 Compare October 9, 2026 13:43
@glani

glani commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, both addressed (rebased onto #3416's d8057f92):

  1. The clearance is now the scrollbar's effective width — Scrollbar::full_width, the track width the scrollbar paints with or the active thumb's inset + width when that reaches further — less the editor's left padding, at least RIGHT_MARGIN; it is one reservation that sets the text origin, width, wrap width and scroll size. New test the_text_keeps_clear_of_the_whole_track_of_a_scrollbar_on_the_left: zero padding, with the default track and with a 24 px themed track; it asserts the text origin and that a click 1 px into the text lands in the text. Squashed into editor: Keep the text clear of a scrollbar on the left.
  2. The marker docs (line_decorations.rs and both website locales) now say markers sit outside the line numbers in the default order and that gutter_order can put them by the text.

InputPresentation::gutter_side is in the Public API list now. Each commit's tests also compile on their own.

huacnlee added a commit that referenced this pull request Oct 9, 2026
## Description

The vertical scrollbar is hard-coded to the right edge and the
horizontal one to the bottom. In a side-by-side diff the left pane's
scrollbar belongs on its outer (left) edge, so the two panes mirror each
other.

This adds `ScrollbarPlacement`, one value for both axes, set with one
call — `Scrollbar::placement`, and `scrollbar_placement` /
`set_scrollbar_placement` on the editor state:

| | vertical | horizontal |
| --- | --- | --- |
| `BottomRight` (default, unchanged) | right | bottom |
| `BottomLeft` | left | bottom |
| `TopRight` | right | top |
| `TopLeft` | left | top |

Each axis follows its half of the placement: the track sits on that
edge, the thumb and its hit area are anchored to it, and the bar slides
in from that edge. When both bars show, the vertical one keeps its full
height and the horizontal one stops short of it — at its start when the
vertical bar is on the left, at its end when it is on the right. A
single-axis scrollbar uses only its own half. The editor reserves no
room for its horizontal bar at the bottom today, so at the top it
overlays the first line the same way. Nothing changes by default.

Targets `next`: it is the first half of a mirrored side-by-side diff
whose second half, #3417, builds on #3359's gutter markers, which are on
`next`.

## Screenshot

| Before | After |
| ------ | ----- |
| <img width="800" alt="before"
src="https://raw.githubusercontent.com/GigLaboCom/gpui-component/pr-assets/screenshots/before.png"
/> | <img width="800" alt="scrollbar-left"
src="https://raw.githubusercontent.com/GigLaboCom/gpui-component/pr-assets/screenshots/scrollbar-left.png"
/> |

The four placements (`ScrollbarMode::Always`); the story switches
Dataset and Placement from one Options menu (`d8057f92`):

<img width="1000" alt="scrollbar placements, light"
src="https://raw.githubusercontent.com/GigLaboCom/gpui-component/pr-assets/screenshots/scrollbar-placement-light.png"
/>

<img width="1000" alt="scrollbar placements, dark"
src="https://raw.githubusercontent.com/GigLaboCom/gpui-component/pr-assets/screenshots/scrollbar-placement-dark.png"
/>

## Public API

`gpui_base` (re-exported by `gpui_component`, also from
`gpui_component::scroll`):

- `ScrollbarPlacement { BottomRight, BottomLeft, TopRight, TopLeft }` —
default `BottomRight`; `is_left()` (vertical bar on the left),
`is_top()` (horizontal bar at the top).
- `Scrollbar::placement(mut self, placement: ScrollbarPlacement) ->
Self`.
- `InputBaseState::scrollbar_placement(mut self, placement:
ScrollbarPlacement) -> Self` — builder for the editor's scrollbars.
- `InputBaseState::set_scrollbar_placement(&mut self, placement:
ScrollbarPlacement, cx: &mut Context<Self>)` — replaces the whole
placement at runtime.

## How to Test

- `cargo test -p gpui-base --lib scrollbar`: the `Scrollbar` harness
clicks and drags on each placement — the vertical track and thumb on the
left, the horizontal track and thumb at the top, the horizontal track
starting after a left bar and ending before a right one with both bars
at the top, a thumb drag and a track click moving the offset the right
way — and `visibility_translation_moves_toward_the_nearest_edge` covers
all four placements on both axes; an editor layout test for `TopLeft`.
Each fails with its code reverted.
- `cargo run -p gpui-component-story -- scrollbar`, then Options →
Placement.

## Checklist

- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and
followed the guidelines.
- [x] Reviewed the changes in this PR and confirmed AI generated code
(If any) is accurate.
- [x] Passed `cargo run` for story tests related to the changes.
- [x] Tested macOS, Windows and Linux platforms performance (if the
change is platform-specific)

---------

Co-authored-by: Jason Lee <huacnlee@gmail.com>
@glani
glani force-pushed the editor-gutter-right-side branch from f617384 to 36d1e43 Compare October 9, 2026 14:29
@glani

glani commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto next now that #3416 is merged — the three commits only, the tree is the same as before (f617384d), so both review points are in 576cd2f4 and 36d1e432.

@huacnlee

huacnlee commented Oct 9, 2026

Copy link
Copy Markdown
Member

Please cherry-pick this follow-up commit into the PR:

70b271ad

git fetch https://github.com/longbridge/gpui-kit.git pr-3417-diff-story
git cherry-pick 70b271ad

The useful case here is a side-by-side diff with both gutters facing the center, where corresponding line numbers can be compared directly. Please frame the feature around that use case rather than presenting right-side line numbers as a general Editor preference or RTL support.

This commit adds an Editor Diff story showing original and modified source with change markers. Its Options menu switches between both gutters on the left and center-facing gutters, and optionally places markers before line numbers. The ordinary Editor story remains unchanged.

It also synchronizes vertical scrolling before either pane is laid out. The initial observer-only approach missed scrollbar-thumb dragging; the regression test now checks real wheel input and dragging either scrollbar, including corresponding row positions on the same frame, as well as source preservation when switching layouts.

Validation: the targeted Story regression test, build, formatting, and diff checks passed. Launch with:

cargo run -p gpui-component-story -- 'Editor Diff'

This follow-up was implemented with AI assistance and tested locally.

@huacnlee huacnlee changed the title editor: Let the gutter sit on the right side editor: Support mirrored gutters for side-by-side diffs Oct 9, 2026
huacnlee and others added 2 commits October 9, 2026 16:42
AI-assisted implementation; verified layout switching, wheel scrolling, and same-frame alignment when dragging either scrollbar.
@glani

glani commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — cherry-picked as d9c5f2f5, and cargo test -p gpui-component-story --lib editor_diff passes here.

Reframed around that case in 073cbaf0: the editor docs (en and zh-CN) now lead with a side-by-side diff whose gutters face the center so corresponding line numbers sit side by side, then the API, then the Editor Diff story; gutter_side's doc says Side::Right is for the left pane of such a diff. The description is updated the same way.

@glani

glani commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

CI failed on every_public_component_and_story_is_accounted_for: the editor_diff story had no entry in component-inventory.json. Added in 260dee0a as infrastructure — two Editors, registered by the Editor story, laid out as a diff, no new constructor — and the inventory test passes locally.

@glani

glani commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

One more: verify-coverage.mjs then wanted a JavaScript gallery route for every inventoried Story. 2de94fff lists editor_diff in its NOT_MIRRORED with the reason (two Editors as a diff; the editor route already mirrors the component). If you would rather mirror it as a route, I can add the record, the order entry and the coveredBy line instead.

huacnlee added a commit that referenced this pull request Oct 9, 2026
@huacnlee

huacnlee commented Oct 9, 2026

Copy link
Copy Markdown
Member

Please cherry-pick this additional fix from pr-3417-diff-story:

git fetch https://github.com/longbridge/gpui-kit.git pr-3417-diff-story
git cherry-pick e30f26152f31f91cbdefdadc42810a4baf997bdb

If the previous diff story commit (70b271ad) has not been applied yet, please cherry-pick it first.

The horizontal scrollbar now excludes the center-facing gutter in the original pane, mirroring the modified pane. The vertical scrollbars remain on the outer edges, and horizontal scrolling remains independent. This also prevents the horizontal scrollbar from intercepting clicks in the right gutter. No public API changes.

Validation: horizontal scrollbar, scrollbar, editor element, and diff scrolling regression tests passed; formatting and Base Clippy checks passed.

Attached screenshot of the side-by-side diff (the two panes have independent horizontal scroll offsets):

Side-by-side diff with center-facing gutters and mirrored horizontal scrollbar tracks

AI-assisted implementation; verified fixed-gutter click handling, unchanged scrolling ranges, scrollbar regressions, and synchronized diff scrolling.
@glani

glani commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

Cherry-picked as 4a70f007. Here cargo test -p gpui-base --lib passes the scrollbar and editor tests and the editor_diff story test passes; rustfmt is clean.

@huacnlee
huacnlee merged commit d9b7c42 into longbridge:next Oct 9, 2026
12 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.

2 participants