Skip to content

editor: Smooth scrolling#44827

Open
marcocondrache wants to merge 73 commits intozed-industries:mainfrom
marcocondrache:feat/smooth-scrolling
Open

editor: Smooth scrolling#44827
marcocondrache wants to merge 73 commits intozed-industries:mainfrom
marcocondrache:feat/smooth-scrolling

Conversation

@marcocondrache
Copy link
Copy Markdown
Contributor

@marcocondrache marcocondrache commented Dec 14, 2025

Closes #4355

Before:

zed-no-smooth.mp4

After:

zed.-.smooth.scroll.mp4

Ref: https://pavelfatin.com/scrolling-with-pleasure

Release Notes:

  • Editor now supports smooth scrolling

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
@cla-bot cla-bot Bot added the cla-signed The user has signed the Contributor License Agreement label Dec 14, 2025
@github-actions github-actions Bot added the community champion Issues filed by our amazing community champions! 🫶 label Dec 14, 2025
@github-project-automation github-project-automation Bot moved this to Community Champion PRs in Quality Week – December 2025 Dec 14, 2025
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
@MrSubidubi MrSubidubi self-assigned this Dec 15, 2025
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
@niekdomi
Copy link
Copy Markdown
Contributor

Thanks for this PR, feels smooth. Are there any plans to integrate this into vim-mode as well?

(e.g., ctr-y, ctrl-e, ctrl-b, ctrl-f, ctrl-d, ctrl-u, zz, zt, zb)

or is this not part of this PR?

@marcocondrache
Copy link
Copy Markdown
Contributor Author

@niekdomi yep, I still need to make the changes. I'm currently perfecting the scroll animation

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
the set_scroll_position of the editor saves to the db the position and
creates event which is wasteful for an animation

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Comment thread crates/editor/src/element.rs Outdated
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Copy link
Copy Markdown

@ttytm ttytm left a comment

Choose a reason for hiding this comment

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

Amazing effort!

Hope giving input already doesn't bother you.

Taking it through manual testing, I found that it doesn’t work with the optional preceding [count].

For example, my personal default for C+e / Ctrl+y is to scroll 5 lines. Currently it is reverted to 1 line when smooth scrolling is enabled.
No matter which scrolling motion is pressed [count] is ignored.

I wouldn’t want this to be a blocker, though. So a fix could also be part of a separate PR.

@marcocondrache
Copy link
Copy Markdown
Contributor Author

@ttytm, thank you for the feedback. I'm primarily focusing on improving the animation, especially for trackpads. I'm waiting for some input from the team before I make significant changes and dive deep into the vim crate

@marcocondrache marcocondrache marked this pull request as ready for review December 16, 2025 13:43
slavpetroff added a commit to slavpetroff/zed that referenced this pull request Dec 18, 2025
@niekdomi
Copy link
Copy Markdown
Contributor

niekdomi commented Mar 1, 2026

there seems to be a bug/edge case: If I enter a negative value for the duration, zed crashes. I think there should be some kind of clamp

Copy link
Copy Markdown
Member

@MrSubidubi MrSubidubi left a comment

Choose a reason for hiding this comment

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

Sorry for the delay, as you know, was out sick and still a bit slow to pick up on some things.

Next round here, some nits although I'd argue we better are safe than sorry here, given that this reaches really deep. There is also the following issue, which we obviously need to figure out, where trackpad scrolling on my Mac is not acceptable with the native acceleration from the OS:

Bildschirmaufnahme.2026-03-09.um.22.24.54.mov

Which I think might very well be related to one of my comments.

Nonetheless, we are getting closer though and I'd say after this round we are getting to the topic of tests, so feel free to start thinking about them (not necessarily add them just yet, but you are obviously free to already start working on these). Arguably, I think we want to test stuff amongst the lines of

  • expected vs. calculated scroll positions on a per-frame basis.
  • change in direction during animation
  • instant scroll request during scroll animation
  • animation scroll request during animation scroll request
  • animated scroll after animated scroll to ensure state is in sync (can perhaps be part of the first test
  • settings update test during animation - perhaps we might not need this, but what I'd like to test here is that we do not end up in a state we cannot escape

and perhaps some other nice cases you can think about. As said, food for thought, I think it might be easier to tacke these after this batch here.

Comment thread crates/editor/src/scroll.rs Outdated
Comment on lines +201 to +207
pub enum ScrollAnimationPhase {
Intermediate,
Final,
}

#[derive(Clone, Copy, Debug)]
pub struct ScrollAnimation {
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.

I think the animation phase should be pub(crate) at best, the state itself should perhaps even only be visible within this file even?

Copy link
Copy Markdown
Contributor Author

@marcocondrache marcocondrache Mar 10, 2026

Choose a reason for hiding this comment

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

The animation must be at least pub(crate) since it's used in the EditorSnapshot. Should be fixed

Comment thread crates/editor/src/scroll.rs Outdated
}
}
};
let (anchor, top_row) = self.calculate_scroll_anchor(scroll_position, map, cx);
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.

Let's revert the change here since calculate_scroll_anchor is only called once now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Super right, should be done

Comment thread assets/settings/default.json Outdated
// Whether to animate scrolling with a smooth easing effect.
"enabled": false,
// Duration of the animation
"duration": 0.125,
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.

I am not entirely sure we should make the duration configurable at all - I am sold on having the behavior change based on the input type used, and feel like making the duration configurable might be better to do at a later step if at all. This feels like a case where good defaults might just be more worth than to make this configurable in the first place. The fact that VSCode does not have a setting for this makes me feel like we might not need it as well or at least not in the first place.

Copy link
Copy Markdown
Contributor Author

@marcocondrache marcocondrache Mar 10, 2026

Choose a reason for hiding this comment

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

That makes sense. The main reason I added it initially was that scrolling felt much slower when smooth scrolling was enabled. Later I discovered that increasing the sensitivity fixes that, so it definitely makes sense to remove it.

Should be done 😊

Comment thread crates/editor/src/editor.rs Outdated
self.scroll_anchor.scroll_position(&self.display_snapshot)
}

pub fn scroll_target_or_position(&self) -> gpui::Point<ScrollOffset> {
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.

Can we not just have scroll_position, do we really need to distinguish these here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We don't need to distinguish them anymore, should be done

Comment thread crates/editor/src/editor.rs Outdated
pub placeholder_display_snapshot: Option<DisplaySnapshot>,
is_focused: bool,
scroll_anchor: SharedScrollAnchor,
pub scroll_animation: Option<ScrollAnimation>,
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.

Is there a good reason for this to be pub?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Should be done

Comment thread crates/editor/src/scroll.rs Outdated
Comment on lines +240 to +244
if self.is_finished() {
self.phase = ScrollAnimationPhase::Final;
self.position = self.target_position;
} else {
let progress = self.progress();
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.

We calculate the progress twice here in rapid succession - Perhaps we can have progress return a newtype ScrollAnimationProgress(f32) that then has the method is_finished on that so we avoid doing this twice?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nice idea, I have created ScrollAnimationProgress

Comment on lines +216 to +226
impl ScrollAnimation {
pub fn instant(target: gpui::Point<ScrollOffset>) -> Self {
Self {
phase: ScrollAnimationPhase::Final,
duration: Duration::ZERO,
position: target,
start_position: target,
target_position: target,
start_time: Instant::now(),
}
}
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.

Looking at this and the surrounding methods, I can't help but wonder whether we should manage the state here using an enum as opposed to the struct - that way, we can get rid of plenty of the duplication here and can avoid some invalid states a bit better.
This might be more verbose in parts, but might also help more with what the current state is? Since ScrollAnimationPhase::Final basically implies position == target_position for example. Would like your view on this obviously

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That’s a really nice idea, not sure why it didn’t come to my mind earlier. Looks a lot better now

Comment thread crates/vim/src/normal/scroll.rs Outdated
let snapshot = editor.snapshot(window, cx);
let scroll_position = snapshot.scroll_target_or_position();
let old_top_row = DisplayRow(scroll_position.y as u32);
let old_top_column = scroll_position.x as u32;
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 is much better already, thank you.

We should make scroll_manager private as part of this then though - if these methods can no longer be used freely due to these changes, we should ensure that no one can actually use them (and again (sorry), ensure that visibility of methods matches what we enforce here). I know our policies on comments, but for this case, might even be worth to have a comment within editor to ensure peole do not easily violate that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's right. I checked where scroll_manager is accessed, and it looks like the agent_ui crate is still accessing it directly. Should we address that in this PR?

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.

I think to save you on conflicts, I'd be best if we could move this out into another PR. I'll get that one in then ASAP

} else {
ScrollAnimationPhase::Intermediate
}
}
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.

I think one issue we might face here is when we invoke this method, we reset the current acceleration, which I am not sure is what we want - you lose the current scroll momentum as we start the easing method from the very beginning again, which means we might be in the mid of easing - and then are at the very start again. Instead, I think we should consider the current acceleration/speed at which we are animating and try to find a suitable progress at which we can continue the animation/perhaps change the duration slightly. So this should not be a hard restart I believe but instead more calculate a new animation based on the current state.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That’s also why increasing the sensitivity makes such a big difference. I’ve added an initial implementation that computes the velocity of the current animation using the derivative of the easing function, and then adjusts the duration of the next animation accordingly. The approach is inspired by Chromium’s smooth scrolling implementation (ref 1, ref 2).

The results are already much better than the previous approach, but I think with some additional tuning and research we can improve it even further.

Comment on lines +7677 to +7687

let base_scroll_position = if is_precise {
editor.scroll_manager.cancel_animation();
current_scroll_position
} else {
position_map
.snapshot
.scroll_animation
.map(|animation| animation.target_position)
.unwrap_or(current_scroll_position)
};
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.

Could you briefly elaborate on the logic here?

This comment was marked as spam.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@MrSubidubi I've removed this logic it's not necessary anymore. It was an attempt to understand the scrolling slowness difference between mouse and trackpad.

@marcocondrache marcocondrache force-pushed the feat/smooth-scrolling branch 7 times, most recently from 00be4d6 to 49f54fc Compare March 10, 2026 11:00
@marcocondrache marcocondrache force-pushed the feat/smooth-scrolling branch from 49f54fc to 0d8fd32 Compare March 10, 2026 12:19
@marcocondrache marcocondrache force-pushed the feat/smooth-scrolling branch from 1a8fa0f to 97119a7 Compare March 12, 2026 09:06
@Zakleby

This comment was marked as spam.

@marcocondrache marcocondrache force-pushed the feat/smooth-scrolling branch from b9928b1 to b2d2f6a Compare March 14, 2026 17:09
@seanstrom
Copy link
Copy Markdown
Contributor

@marcocondrache do you have some time to rebase this branch and resolve the conflicts?

Copy link
Copy Markdown
Member

@MrSubidubi MrSubidubi left a comment

Choose a reason for hiding this comment

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

I agree this is at a good point to resolve conflicts - the PR is in very good shape now and I'd love to see this progress further. Currently, at a high level, see only three things left:

  • the comment below
  • tests as mentioned in the prior round
  • how we want to handle trackpad scrolls - if we can, I am thinking about whether it would make sense to distinguish between already smoothed scrolls and non-smoothed scrolls on the platform level, or do you have a better idea for that? Would love your opinion on this

Comment thread crates/vim/src/normal/scroll.rs Outdated
let snapshot = editor.snapshot(window, cx);
let scroll_position = snapshot.scroll_target_or_position();
let old_top_row = DisplayRow(scroll_position.y as u32);
let old_top_column = scroll_position.x as u32;
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.

I think to save you on conflicts, I'd be best if we could move this out into another PR. I'll get that one in then ASAP

@marcocondrache marcocondrache force-pushed the feat/smooth-scrolling branch from 3126841 to e9bd4e5 Compare April 21, 2026 06:50
@marcocondrache
Copy link
Copy Markdown
Contributor Author

@MrSubidubi @seanstrom I've resolved the conflicts and merged main.

For everyone following the status of this PR: I’ve recently been pulled into leading a project at my current job, so I have very little, if any, time to dedicate to open source right now. Since this PR is nearly ready and it’s something the community really values, I’m aiming to get this closed out soon.

Apologies for this becoming a bit stale and for the lack of progress recently. I appreciate everyone’s patience

@marcocondrache marcocondrache force-pushed the feat/smooth-scrolling branch from e9bd4e5 to 3addcc6 Compare April 21, 2026 07:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement community champion Issues filed by our amazing community champions! 🫶

Projects

Status: Community Champion PRs

Development

Successfully merging this pull request may close these issues.

Smooth scrolling