editor: Represent scroll offset with more precision (#39367)

Closes #5355

Release Notes:

- Fixed rendering glitches with files with more than 16 million lines
(that occured due to floating number rounding errors).

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
Piotr Osiewicz
2025-10-02 23:04:31 +02:00
committed by GitHub
co-authored by Smit Barmase
parent 4c35274b6e
commit d359a814f8
61 changed files with 711 additions and 530 deletions
+2 -2
View File
@@ -2,7 +2,7 @@ use super::Axis;
use crate::{
Autoscroll, Editor, EditorMode, NextScreen, NextScrollCursorCenterTopBottom,
SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT, ScrollCursorBottom, ScrollCursorCenter,
ScrollCursorCenterTopBottom, ScrollCursorTop, display_map::DisplayRow,
ScrollCursorCenterTopBottom, ScrollCursorTop, display_map::DisplayRow, scroll::ScrollOffset,
};
use gpui::{Context, Point, Window};
@@ -25,7 +25,7 @@ impl Editor {
pub fn scroll(
&mut self,
scroll_position: Point<f32>,
scroll_position: Point<ScrollOffset>,
axis: Option<Axis>,
window: &mut Window,
cx: &mut Context<Self>,
+30 -24
View File
@@ -1,11 +1,12 @@
use crate::{
DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, SelectionEffects,
display_map::ToDisplayPoint, scroll::WasScrolled,
display_map::ToDisplayPoint,
scroll::{ScrollOffset, WasScrolled},
};
use gpui::{Bounds, Context, Pixels, Window, px};
use gpui::{Bounds, Context, Pixels, Window};
use language::Point;
use multi_buffer::Anchor;
use std::{cmp, f32};
use std::cmp;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Autoscroll {
@@ -106,20 +107,21 @@ impl Editor {
&mut self,
bounds: Bounds<Pixels>,
line_height: Pixels,
max_scroll_top: f32,
max_scroll_top: ScrollOffset,
autoscroll_request: Option<(Autoscroll, bool)>,
window: &mut Window,
cx: &mut Context<Editor>,
) -> (NeedsHorizontalAutoscroll, WasScrolled) {
let viewport_height = bounds.size.height;
let visible_lines = viewport_height / line_height;
let visible_lines = ScrollOffset::from(viewport_height / line_height);
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
let original_y = scroll_position.y;
if let Some(last_bounds) = self.expect_bounds_change.take()
&& scroll_position.y != 0.
{
scroll_position.y += (bounds.top() - last_bounds.top()) / line_height;
scroll_position.y +=
ScrollOffset::from((bounds.top() - last_bounds.top()) / line_height);
if scroll_position.y < 0. {
scroll_position.y = 0.;
}
@@ -143,7 +145,7 @@ impl Editor {
if let Some(first_highlighted_row) =
self.highlighted_display_row_for_autoscroll(&display_map)
{
target_top = first_highlighted_row.as_f32();
target_top = first_highlighted_row.as_f64();
target_bottom = target_top + 1.;
} else {
let selections = self.selections.all::<Point>(cx);
@@ -154,7 +156,7 @@ impl Editor {
.head()
.to_display_point(&display_map)
.row()
.as_f32();
.as_f64();
target_bottom = selections
.last()
.unwrap()
@@ -162,7 +164,7 @@ impl Editor {
.to_display_point(&display_map)
.row()
.next_row()
.as_f32();
.as_f64();
let selections_fit = target_bottom - target_top <= visible_lines;
if matches!(
@@ -178,7 +180,7 @@ impl Editor {
.head()
.to_display_point(&display_map)
.row()
.as_f32();
.as_f64();
target_top = newest_selection_top;
target_bottom = newest_selection_top + 1.;
}
@@ -209,7 +211,7 @@ impl Editor {
}
};
if let Autoscroll::Strategy(_, Some(anchor)) = autoscroll {
target_top = anchor.to_display_point(&display_map).row().as_f32();
target_top = anchor.to_display_point(&display_map).row().as_f64();
target_bottom = target_top + 1.;
}
@@ -254,11 +256,11 @@ impl Editor {
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
}
AutoscrollStrategy::TopRelative(lines) => {
scroll_position.y = target_top - lines as f32;
scroll_position.y = target_top - lines as ScrollOffset;
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
}
AutoscrollStrategy::BottomRelative(lines) => {
scroll_position.y = target_bottom + lines as f32;
scroll_position.y = target_bottom + lines as ScrollOffset;
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
}
};
@@ -284,22 +286,25 @@ impl Editor {
autoscroll_request: Option<(Autoscroll, bool)>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<gpui::Point<f32>> {
) -> Option<gpui::Point<ScrollOffset>> {
let (_, local) = autoscroll_request?;
let em_advance = ScrollOffset::from(em_advance);
let viewport_width = ScrollOffset::from(viewport_width);
let scroll_width = ScrollOffset::from(scroll_width);
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all::<Point>(cx);
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
let mut target_left;
let mut target_right;
let mut target_right: f64;
if self
.highlighted_display_row_for_autoscroll(&display_map)
.is_none()
{
target_left = px(f32::INFINITY);
target_right = px(0.);
target_left = f64::INFINITY;
target_right = 0.;
for selection in selections {
let head = selection.head().to_display_point(&display_map);
if head.row() >= start_row
@@ -307,21 +312,22 @@ impl Editor {
{
let start_column = head.column();
let end_column = cmp::min(display_map.line_len(head.row()), head.column());
target_left = target_left.min(
target_left = target_left.min(ScrollOffset::from(
layouts[head.row().minus(start_row) as usize]
.x_for_index(start_column as usize)
+ self.gutter_dimensions.margin,
);
));
target_right = target_right.max(
layouts[head.row().minus(start_row) as usize]
.x_for_index(end_column as usize)
+ em_advance,
ScrollOffset::from(
layouts[head.row().minus(start_row) as usize]
.x_for_index(end_column as usize),
) + em_advance,
);
}
}
} else {
target_left = px(0.);
target_right = px(0.);
target_left = 0.;
target_right = 0.;
}
target_right = target_right.min(scroll_width);
+11 -11
View File
@@ -1,5 +1,5 @@
use serde::Deserialize;
use ui::{Pixels, px};
use ui::Pixels;
#[derive(Debug)]
pub enum ScrollDirection {
@@ -28,41 +28,41 @@ pub enum ScrollAmount {
}
impl ScrollAmount {
pub fn lines(&self, mut visible_line_count: f32) -> f32 {
pub fn lines(&self, mut visible_line_count: f64) -> f64 {
match self {
Self::Line(count) => *count,
Self::Line(count) => *count as f64,
Self::Page(count) => {
// for full pages subtract one to leave an anchor line
if self.is_full_page() {
visible_line_count -= 1.0
}
(visible_line_count * count).trunc()
(visible_line_count * (*count as f64)).trunc()
}
Self::Column(_count) => 0.0,
Self::PageWidth(_count) => 0.0,
}
}
pub fn columns(&self, visible_column_count: f32) -> f32 {
pub fn columns(&self, visible_column_count: f64) -> f64 {
match self {
Self::Line(_count) => 0.0,
Self::Page(_count) => 0.0,
Self::Column(count) => *count,
Self::PageWidth(count) => (visible_column_count * count).trunc(),
Self::Column(count) => *count as f64,
Self::PageWidth(count) => (visible_column_count * *count as f64).trunc(),
}
}
pub fn pixels(&self, line_height: Pixels, height: Pixels) -> Pixels {
match self {
ScrollAmount::Line(x) => px(line_height.0 * x),
ScrollAmount::Page(x) => px(height.0 * x),
ScrollAmount::Line(x) => line_height * *x,
ScrollAmount::Page(x) => height * *x,
// This function seems to only be leveraged by the popover that is
// displayed by the editor when, for example, viewing a function's
// documentation. Right now that only supports vertical scrolling,
// so I'm leaving this at 0.0 for now to try and make it clear that
// this should not have an impact on that?
ScrollAmount::Column(_) => px(0.0),
ScrollAmount::PageWidth(_) => px(0.0),
ScrollAmount::Column(_) => Pixels::ZERO,
ScrollAmount::PageWidth(_) => Pixels::ZERO,
}
}