git: Fix desynced scrolling between LHS and RHS of side-by-side diff (#47913)

Cole Miller , cameron , Zed Zippy , and Jakub created

- Remove old attempt to sync scrolling
- Share a `ScrollAnchor` between the two sides, and be sure to resolve
it against the correct snapshot
- Allow either side to initiate an autoscroll request, and make sure
that request is processed in the same frame by the other side

Release Notes:

- N/A

---------

Co-authored-by: cameron <cameron.studdstreet@gmail.com>
Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
Co-authored-by: Jakub <jakub@zed.dev>

Change summary

crates/agent_ui/src/text_thread_editor.rs     |  26 +-
crates/collab_ui/src/channel_view.rs          |  28 +-
crates/debugger_ui/src/session.rs             |   6 
crates/editor/src/display_map.rs              |  38 +++
crates/editor/src/editor.rs                   | 121 +++++----
crates/editor/src/editor_tests.rs             |  46 ++-
crates/editor/src/element.rs                  |  76 ------
crates/editor/src/items.rs                    |  22 +
crates/editor/src/mouse_context_menu.rs       |   2 
crates/editor/src/movement.rs                 |   7 
crates/editor/src/scroll.rs                   | 236 ++++++++++++++++----
crates/editor/src/scroll/autoscroll.rs        |  10 
crates/editor/src/split.rs                    | 156 +++++++++++--
crates/editor/src/split_editor_view.rs        |  14 +
crates/editor/src/test.rs                     |  11 
crates/editor/src/test/editor_test_context.rs |   2 
crates/gpui/src/elements/div.rs               |  31 ++
crates/search/src/buffer_search.rs            |   2 
crates/vim/src/command.rs                     |   2 
crates/vim/src/helix.rs                       |   6 
crates/vim/src/indent.rs                      |   2 
crates/vim/src/motion.rs                      |  12 
crates/vim/src/normal.rs                      |   4 
crates/vim/src/normal/change.rs               |   2 
crates/vim/src/normal/convert.rs              |   2 
crates/vim/src/normal/delete.rs               |   2 
crates/vim/src/normal/paste.rs                |   4 
crates/vim/src/normal/scroll.rs               |  21 +
crates/vim/src/normal/substitute.rs           |   2 
crates/vim/src/normal/toggle_comments.rs      |   2 
crates/vim/src/normal/yank.rs                 |   2 
crates/vim/src/replace.rs                     |   2 
crates/vim/src/rewrap.rs                      |   2 
crates/vim/src/surrounds.rs                   |   2 
crates/vim/src/visual.rs                      |   4 
crates/workspace/src/item.rs                  |  13 
36 files changed, 602 insertions(+), 318 deletions(-)

Detailed changes

crates/agent_ui/src/text_thread_editor.rs 🔗

@@ -1009,8 +1009,7 @@ impl TextThreadEditor {
                 .as_f64();
             let scroll_position = editor
                 .scroll_manager
-                .anchor()
-                .scroll_position(&snapshot.display_snapshot);
+                .scroll_position(&snapshot.display_snapshot, cx);
 
             let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
             if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
@@ -3026,14 +3025,15 @@ impl FollowableItem for TextThreadEditor {
         self.remote_id
     }
 
-    fn to_state_proto(&self, window: &Window, cx: &App) -> Option<proto::view::Variant> {
-        let text_thread = self.text_thread.read(cx);
+    fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant> {
+        let context_id = self.text_thread.read(cx).id().to_proto();
+        let editor_proto = self
+            .editor
+            .update(cx, |editor, cx| editor.to_state_proto(window, cx));
         Some(proto::view::Variant::ContextEditor(
             proto::view::ContextEditor {
-                context_id: text_thread.id().to_proto(),
-                editor: if let Some(proto::view::Variant::Editor(proto)) =
-                    self.editor.read(cx).to_state_proto(window, cx)
-                {
+                context_id,
+                editor: if let Some(proto::view::Variant::Editor(proto)) = editor_proto {
                     Some(proto)
                 } else {
                     None
@@ -3100,12 +3100,12 @@ impl FollowableItem for TextThreadEditor {
         &self,
         event: &Self::Event,
         update: &mut Option<proto::update_view::Variant>,
-        window: &Window,
-        cx: &App,
+        window: &mut Window,
+        cx: &mut App,
     ) -> bool {
-        self.editor
-            .read(cx)
-            .add_event_to_update_proto(event, update, window, cx)
+        self.editor.update(cx, |editor, cx| {
+            editor.add_event_to_update_proto(event, update, window, cx)
+        })
     }
 
     fn apply_update_proto(

crates/collab_ui/src/channel_view.rs 🔗

@@ -563,18 +563,22 @@ impl FollowableItem for ChannelView {
         self.remote_id
     }
 
-    fn to_state_proto(&self, window: &Window, cx: &App) -> Option<proto::view::Variant> {
-        let channel_buffer = self.channel_buffer.read(cx);
-        if !channel_buffer.is_connected() {
+    fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant> {
+        let (is_connected, channel_id) = {
+            let channel_buffer = self.channel_buffer.read(cx);
+            (channel_buffer.is_connected(), channel_buffer.channel_id.0)
+        };
+        if !is_connected {
             return None;
         }
 
+        let editor_proto = self
+            .editor
+            .update(cx, |editor, cx| editor.to_state_proto(window, cx));
         Some(proto::view::Variant::ChannelView(
             proto::view::ChannelView {
-                channel_id: channel_buffer.channel_id.0,
-                editor: if let Some(proto::view::Variant::Editor(proto)) =
-                    self.editor.read(cx).to_state_proto(window, cx)
-                {
+                channel_id,
+                editor: if let Some(proto::view::Variant::Editor(proto)) = editor_proto {
                     Some(proto)
                 } else {
                     None
@@ -638,12 +642,12 @@ impl FollowableItem for ChannelView {
         &self,
         event: &EditorEvent,
         update: &mut Option<proto::update_view::Variant>,
-        window: &Window,
-        cx: &App,
+        window: &mut Window,
+        cx: &mut App,
     ) -> bool {
-        self.editor
-            .read(cx)
-            .add_event_to_update_proto(event, update, window, cx)
+        self.editor.update(cx, |editor, cx| {
+            editor.add_event_to_update_proto(event, update, window, cx)
+        })
     }
 
     fn apply_update_proto(

crates/debugger_ui/src/session.rs 🔗

@@ -141,7 +141,7 @@ impl FollowableItem for DebugSession {
         self.remote_id
     }
 
-    fn to_state_proto(&self, _window: &Window, _cx: &App) -> Option<proto::view::Variant> {
+    fn to_state_proto(&self, _window: &mut Window, _cx: &mut App) -> Option<proto::view::Variant> {
         None
     }
 
@@ -159,8 +159,8 @@ impl FollowableItem for DebugSession {
         &self,
         _event: &Self::Event,
         _update: &mut Option<proto::update_view::Variant>,
-        _window: &Window,
-        _cx: &App,
+        _window: &mut Window,
+        _cx: &mut App,
     ) -> bool {
         // update.get_or_insert_with(|| proto::update_view::Variant::DebugPanel(Default::default()));
 

crates/editor/src/display_map.rs 🔗

@@ -480,7 +480,37 @@ impl DisplayMap {
             });
         }
 
+        let companion_display_snapshot = self.companion.as_ref().and_then(|(companion_dm, _)| {
+            companion_dm
+                .update(cx, |dm, cx| Arc::new(dm.snapshot_simple(cx)))
+                .ok()
+        });
+
         DisplaySnapshot {
+            display_map_id: self.entity_id,
+            companion_display_snapshot,
+            block_snapshot,
+            diagnostics_max_severity: self.diagnostics_max_severity,
+            crease_snapshot: self.crease_map.snapshot(),
+            text_highlights: self.text_highlights.clone(),
+            inlay_highlights: self.inlay_highlights.clone(),
+            clip_at_line_ends: self.clip_at_line_ends,
+            masked: self.masked,
+            fold_placeholder: self.fold_placeholder.clone(),
+        }
+    }
+
+    fn snapshot_simple(&mut self, cx: &mut Context<Self>) -> DisplaySnapshot {
+        let (wrap_snapshot, wrap_edits) = self.sync_through_wrap(cx);
+
+        let block_snapshot = self
+            .block_map
+            .read(wrap_snapshot, wrap_edits, None, None)
+            .snapshot;
+
+        DisplaySnapshot {
+            display_map_id: self.entity_id,
+            companion_display_snapshot: None,
             block_snapshot,
             diagnostics_max_severity: self.diagnostics_max_severity,
             crease_snapshot: self.crease_map.snapshot(),
@@ -1547,6 +1577,8 @@ impl<'a> HighlightedChunk<'a> {
 
 #[derive(Clone)]
 pub struct DisplaySnapshot {
+    pub display_map_id: EntityId,
+    pub companion_display_snapshot: Option<Arc<DisplaySnapshot>>,
     pub crease_snapshot: CreaseSnapshot,
     block_snapshot: BlockSnapshot,
     text_highlights: TextHighlights,
@@ -1558,6 +1590,10 @@ pub struct DisplaySnapshot {
 }
 
 impl DisplaySnapshot {
+    pub fn companion_snapshot(&self) -> Option<&DisplaySnapshot> {
+        self.companion_display_snapshot.as_deref()
+    }
+
     pub fn wrap_snapshot(&self) -> &WrapSnapshot {
         &self.block_snapshot.wrap_snapshot
     }
@@ -2733,7 +2769,7 @@ pub mod tests {
 
         _ = cx.update_window(window, |_, window, cx| {
             let text_layout_details =
-                editor.update(cx, |editor, _cx| editor.text_layout_details(window));
+                editor.update(cx, |editor, cx| editor.text_layout_details(window, cx));
 
             let font_size = px(12.0);
             let wrap_width = Some(px(96.));

crates/editor/src/editor.rs 🔗

@@ -58,8 +58,8 @@ pub use editor_settings::{
     HideMouseMode, ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap,
 };
 pub use element::{
-    CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, OverlayPainter,
-    OverlayPainterData, PointForPosition, render_breadcrumb_text,
+    CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
+    render_breadcrumb_text,
 };
 pub use git::blame::BlameRenderer;
 pub use hover_popover::hover_markdown_style;
@@ -168,7 +168,7 @@ use project::{
 use rand::seq::SliceRandom;
 use regex::Regex;
 use rpc::{ErrorCode, ErrorExt, proto::PeerId};
-use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager};
+use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, SharedScrollAnchor};
 use selections_collection::{MutableSelectionsCollection, SelectionsCollection};
 use serde::{Deserialize, Serialize};
 use settings::{
@@ -691,7 +691,7 @@ pub enum EditPredictionPreview {
     /// Modifier pressed
     Active {
         since: Instant,
-        previous_scroll_position: Option<ScrollAnchor>,
+        previous_scroll_position: Option<SharedScrollAnchor>,
     },
 }
 
@@ -703,7 +703,7 @@ impl EditPredictionPreview {
         }
     }
 
-    pub fn set_previous_scroll_position(&mut self, scroll_position: Option<ScrollAnchor>) {
+    pub fn set_previous_scroll_position(&mut self, scroll_position: Option<SharedScrollAnchor>) {
         if let EditPredictionPreview::Active {
             previous_scroll_position,
             ..
@@ -1332,7 +1332,6 @@ pub struct Editor {
     folding_newlines: Task<()>,
     select_next_is_case_sensitive: Option<bool>,
     pub lookup_key: Option<Box<dyn Any + Send + Sync>>,
-    scroll_companion: Option<WeakEntity<Editor>>,
     on_local_selections_changed:
         Option<Box<dyn Fn(Point, &mut Window, &mut Context<Self>) + 'static>>,
     suppress_selection_callback: bool,
@@ -1388,7 +1387,7 @@ pub struct EditorSnapshot {
     pub display_snapshot: DisplaySnapshot,
     pub placeholder_display_snapshot: Option<DisplaySnapshot>,
     is_focused: bool,
-    scroll_anchor: ScrollAnchor,
+    scroll_anchor: SharedScrollAnchor,
     ongoing_scroll: OngoingScroll,
     current_line_highlight: CurrentLineHighlight,
     gutter_hovered: bool,
@@ -1939,15 +1938,19 @@ impl Editor {
             window,
             cx,
         );
-        self.display_map.update(cx, |display_map, cx| {
+        let my_snapshot = self.display_map.update(cx, |display_map, cx| {
             let snapshot = display_map.snapshot(cx);
             clone.display_map.update(cx, |display_map, cx| {
                 display_map.set_state(&snapshot, cx);
             });
+            snapshot
         });
+        let clone_snapshot = clone.display_map.update(cx, |map, cx| map.snapshot(cx));
         clone.folds_did_change(cx);
         clone.selections.clone_state(&self.selections);
-        clone.scroll_manager.clone_state(&self.scroll_manager);
+        clone
+            .scroll_manager
+            .clone_state(&self.scroll_manager, &my_snapshot, &clone_snapshot, cx);
         clone.searchable = self.searchable;
         clone.read_only = self.read_only;
         clone
@@ -1965,6 +1968,7 @@ impl Editor {
 
     pub fn sticky_headers(
         &self,
+        display_snapshot: &DisplaySnapshot,
         style: &EditorStyle,
         cx: &App,
     ) -> Option<Vec<OutlineItem<Anchor>>> {
@@ -1972,7 +1976,7 @@ impl Editor {
         let multi_buffer_snapshot = multi_buffer.snapshot(cx);
         let multi_buffer_visible_start = self
             .scroll_manager
-            .anchor()
+            .native_anchor(display_snapshot, cx)
             .anchor
             .to_point(&multi_buffer_snapshot);
         let max_row = multi_buffer_snapshot.max_point().row;
@@ -2542,7 +2546,6 @@ impl Editor {
             folding_newlines: Task::ready(()),
             lookup_key: None,
             select_next_is_case_sensitive: None,
-            scroll_companion: None,
             on_local_selections_changed: None,
             suppress_selection_callback: false,
             applicable_language_settings: HashMap::default(),
@@ -2576,8 +2579,10 @@ impl Editor {
                     if *local {
                         editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
                         editor.inline_blame_popover.take();
-                        let new_anchor = editor.scroll_manager.anchor();
                         let snapshot = editor.snapshot(window, cx);
+                        let new_anchor = editor
+                            .scroll_manager
+                            .native_anchor(&snapshot.display_snapshot, cx);
                         editor.update_restoration_data(cx, move |data| {
                             data.scroll_position = (
                                 new_anchor.top_row(snapshot.buffer_snapshot()),
@@ -3078,6 +3083,8 @@ impl Editor {
             })
             .flatten();
 
+        let display_snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
         EditorSnapshot {
             mode: self.mode.clone(),
             show_gutter: self.show_gutter,
@@ -3089,12 +3096,12 @@ impl Editor {
             show_runnables: self.show_runnables,
             show_breakpoints: self.show_breakpoints,
             git_blame_gutter_max_author_length,
-            display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
+            scroll_anchor: self.scroll_manager.shared_scroll_anchor(cx),
+            display_snapshot,
             placeholder_display_snapshot: self
                 .placeholder_display_map
                 .as_ref()
                 .map(|display_map| display_map.update(cx, |map, cx| map.snapshot(cx))),
-            scroll_anchor: self.scroll_manager.anchor(),
             ongoing_scroll: self.scroll_manager.ongoing_scroll(),
             is_focused: self.focus_handle.is_focused(window),
             current_line_highlight: self
@@ -5573,11 +5580,12 @@ impl Editor {
         cx: &mut Context<Editor>,
     ) -> HashMap<ExcerptId, (Entity<Buffer>, clock::Global, Range<usize>)> {
         let project = self.project().cloned();
+        let display_snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let multi_buffer = self.buffer().read(cx);
         let multi_buffer_snapshot = multi_buffer.snapshot(cx);
         let multi_buffer_visible_start = self
             .scroll_manager
-            .anchor()
+            .native_anchor(&display_snapshot, cx)
             .anchor
             .to_point(&multi_buffer_snapshot);
         let multi_buffer_visible_end = multi_buffer_snapshot.clip_point(
@@ -5623,12 +5631,12 @@ impl Editor {
             .collect()
     }
 
-    pub fn text_layout_details(&self, window: &mut Window) -> TextLayoutDetails {
+    pub fn text_layout_details(&self, window: &mut Window, cx: &mut App) -> TextLayoutDetails {
         TextLayoutDetails {
             text_system: window.text_system().clone(),
             editor_style: self.style.clone().unwrap(),
             rem_size: window.rem_size(),
-            scroll_anchor: self.scroll_manager.anchor(),
+            scroll_anchor: self.scroll_manager.shared_scroll_anchor(cx),
             visible_rows: self.visible_line_count(),
             vertical_scroll_margin: self.scroll_manager.vertical_scroll_margin,
         }
@@ -7537,6 +7545,7 @@ impl Editor {
             self.debounced_selection_highlight_complete = false;
             return;
         };
+        let display_snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
         let query_changed = self
             .quick_selection_highlight_task
@@ -7548,7 +7557,7 @@ impl Editor {
         if on_buffer_edit || query_changed {
             let multi_buffer_visible_start = self
                 .scroll_manager
-                .anchor()
+                .native_anchor(&display_snapshot, cx)
                 .anchor
                 .to_point(&multi_buffer_snapshot);
             let multi_buffer_visible_end = multi_buffer_snapshot.clip_point(
@@ -11078,7 +11087,7 @@ impl Editor {
                 (buffer.len(), rows.start.previous_row())
             };
 
-            let text_layout_details = self.text_layout_details(window);
+            let text_layout_details = self.text_layout_details(window, cx);
             let x = display_map.x_for_display_point(
                 selection.head().to_display_point(&display_map),
                 &text_layout_details,
@@ -12847,7 +12856,7 @@ impl Editor {
 
     pub fn transpose(&mut self, _: &Transpose, window: &mut Window, cx: &mut Context<Self>) {
         self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
-        let text_layout_details = &self.text_layout_details(window);
+        let text_layout_details = &self.text_layout_details(window, cx);
         self.transact(window, cx, |this, window, cx| {
             let edits = this.change_selections(Default::default(), window, cx, |s| {
                 let mut edits: Vec<(Range<MultiBufferOffset>, String)> = Default::default();
@@ -13846,7 +13855,7 @@ impl Editor {
 
         self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
 
-        let text_layout_details = &self.text_layout_details(window);
+        let text_layout_details = &self.text_layout_details(window, cx);
         let selection_count = self.selections.count();
         let first_selection = self.selections.first_anchor();
 
@@ -13889,7 +13898,7 @@ impl Editor {
 
         self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
 
-        let text_layout_details = &self.text_layout_details(window);
+        let text_layout_details = &self.text_layout_details(window, cx);
 
         self.change_selections(Default::default(), window, cx, |s| {
             s.move_with(|map, selection| {
@@ -13926,7 +13935,7 @@ impl Editor {
 
         self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
 
-        let text_layout_details = &self.text_layout_details(window);
+        let text_layout_details = &self.text_layout_details(window, cx);
 
         self.change_selections(Default::default(), window, cx, |s| {
             s.move_with(|map, selection| {
@@ -13953,7 +13962,7 @@ impl Editor {
         cx: &mut Context<Self>,
     ) {
         self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
-        let text_layout_details = &self.text_layout_details(window);
+        let text_layout_details = &self.text_layout_details(window, cx);
         self.change_selections(Default::default(), window, cx, |s| {
             s.move_heads_with(|map, head, goal| {
                 movement::down_by_rows(map, head, action.lines, goal, false, text_layout_details)
@@ -13968,7 +13977,7 @@ impl Editor {
         cx: &mut Context<Self>,
     ) {
         self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
-        let text_layout_details = &self.text_layout_details(window);
+        let text_layout_details = &self.text_layout_details(window, cx);
         self.change_selections(Default::default(), window, cx, |s| {
             s.move_heads_with(|map, head, goal| {
                 movement::up_by_rows(map, head, action.lines, goal, false, text_layout_details)
@@ -13988,7 +13997,7 @@ impl Editor {
 
         self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
 
-        let text_layout_details = &self.text_layout_details(window);
+        let text_layout_details = &self.text_layout_details(window, cx);
 
         self.change_selections(Default::default(), window, cx, |s| {
             s.move_heads_with(|map, head, goal| {
@@ -14034,7 +14043,7 @@ impl Editor {
             SelectionEffects::default()
         };
 
-        let text_layout_details = &self.text_layout_details(window);
+        let text_layout_details = &self.text_layout_details(window, cx);
 
         self.change_selections(effects, window, cx, |s| {
             s.move_with(|map, selection| {
@@ -14056,7 +14065,7 @@ impl Editor {
 
     pub fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context<Self>) {
         self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
-        let text_layout_details = &self.text_layout_details(window);
+        let text_layout_details = &self.text_layout_details(window, cx);
         self.change_selections(Default::default(), window, cx, |s| {
             s.move_heads_with(|map, head, goal| {
                 movement::up(map, head, goal, false, text_layout_details)
@@ -14074,7 +14083,7 @@ impl Editor {
 
         self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
 
-        let text_layout_details = &self.text_layout_details(window);
+        let text_layout_details = &self.text_layout_details(window, cx);
         let selection_count = self.selections.count();
         let first_selection = self.selections.first_anchor();
 
@@ -14112,7 +14121,7 @@ impl Editor {
 
         self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
 
-        let text_layout_details = &self.text_layout_details(window);
+        let text_layout_details = &self.text_layout_details(window, cx);
 
         self.change_selections(Default::default(), window, cx, |s| {
             s.move_heads_with(|map, head, goal| {
@@ -14158,7 +14167,7 @@ impl Editor {
             SelectionEffects::default()
         };
 
-        let text_layout_details = &self.text_layout_details(window);
+        let text_layout_details = &self.text_layout_details(window, cx);
         self.change_selections(effects, window, cx, |s| {
             s.move_with(|map, selection| {
                 if !selection.is_empty() {
@@ -14179,7 +14188,7 @@ impl Editor {
 
     pub fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context<Self>) {
         self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
-        let text_layout_details = &self.text_layout_details(window);
+        let text_layout_details = &self.text_layout_details(window, cx);
         self.change_selections(Default::default(), window, cx, |s| {
             s.move_heads_with(|map, head, goal| {
                 movement::down(map, head, goal, false, text_layout_details)
@@ -14990,10 +14999,11 @@ impl Editor {
         );
     }
 
-    fn navigation_data(&self, cursor_anchor: Anchor, cx: &App) -> NavigationData {
+    fn navigation_data(&self, cursor_anchor: Anchor, cx: &mut Context<Self>) -> NavigationData {
+        let display_snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = self.buffer.read(cx).read(cx);
         let cursor_position = cursor_anchor.to_point(&buffer);
-        let scroll_anchor = self.scroll_manager.anchor();
+        let scroll_anchor = self.scroll_manager.native_anchor(&display_snapshot, cx);
         let scroll_top_row = scroll_anchor.top_row(&buffer);
         drop(buffer);
 
@@ -15005,7 +15015,11 @@ impl Editor {
         }
     }
 
-    fn navigation_entry(&self, cursor_anchor: Anchor, cx: &App) -> Option<NavigationEntry> {
+    fn navigation_entry(
+        &self,
+        cursor_anchor: Anchor,
+        cx: &mut Context<Self>,
+    ) -> Option<NavigationEntry> {
         let Some(history) = self.nav_history.clone() else {
             return None;
         };
@@ -15161,7 +15175,7 @@ impl Editor {
 
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let all_selections = self.selections.all::<Point>(&display_map);
-        let text_layout_details = self.text_layout_details(window);
+        let text_layout_details = self.text_layout_details(window, cx);
 
         let (mut columnar_selections, new_selections_to_columnarize) = {
             if let Some(state) = self.add_selections_state.as_ref() {
@@ -15860,7 +15874,7 @@ impl Editor {
             return;
         }
         self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
-        let text_layout_details = &self.text_layout_details(window);
+        let text_layout_details = &self.text_layout_details(window, cx);
         self.transact(window, cx, |this, window, cx| {
             let mut selections = this
                 .selections
@@ -20777,7 +20791,14 @@ impl Editor {
             window,
             cx,
         );
-        minimap.scroll_manager.clone_state(&self.scroll_manager);
+        let my_snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let minimap_snapshot = minimap.display_map.update(cx, |map, cx| map.snapshot(cx));
+        minimap.scroll_manager.clone_state(
+            &self.scroll_manager,
+            &my_snapshot,
+            &minimap_snapshot,
+            cx,
+        );
         minimap.set_text_style_refinement(TextStyleRefinement {
             font_size: Some(MINIMAP_FONT_SIZE),
             font_weight: Some(MINIMAP_FONT_WEIGHT),
@@ -21086,14 +21107,6 @@ impl Editor {
         self.delegate_expand_excerpts = delegate;
     }
 
-    pub fn set_scroll_companion(&mut self, companion: Option<WeakEntity<Editor>>) {
-        self.scroll_companion = companion;
-    }
-
-    pub fn scroll_companion(&self) -> Option<&WeakEntity<Editor>> {
-        self.scroll_companion.as_ref()
-    }
-
     pub fn set_on_local_selections_changed(
         &mut self,
         callback: Option<Box<dyn Fn(Point, &mut Window, &mut Context<Self>) + 'static>>,
@@ -24756,10 +24769,10 @@ impl Editor {
 
     pub fn to_pixel_point(
         &mut self,
-        source: multi_buffer::Anchor,
+        source: Anchor,
         editor_snapshot: &EditorSnapshot,
         window: &mut Window,
-        cx: &App,
+        cx: &mut App,
     ) -> Option<gpui::Point<Pixels>> {
         let source_point = source.to_display_point(editor_snapshot);
         self.display_to_pixel_point(source_point, editor_snapshot, window, cx)
@@ -24770,10 +24783,10 @@ impl Editor {
         source: DisplayPoint,
         editor_snapshot: &EditorSnapshot,
         window: &mut Window,
-        cx: &App,
+        cx: &mut App,
     ) -> Option<gpui::Point<Pixels>> {
         let line_height = self.style(cx).text.line_height_in_pixels(window.rem_size());
-        let text_layout_details = self.text_layout_details(window);
+        let text_layout_details = self.text_layout_details(window, cx);
         let scroll_top = text_layout_details
             .scroll_anchor
             .scroll_position(editor_snapshot)
@@ -24820,8 +24833,8 @@ impl Editor {
             .and_then(|item| item.to_any_mut()?.downcast_mut::<T>())
     }
 
-    fn character_dimensions(&self, window: &mut Window) -> CharacterDimensions {
-        let text_layout_details = self.text_layout_details(window);
+    fn character_dimensions(&self, window: &mut Window, cx: &mut App) -> CharacterDimensions {
+        let text_layout_details = self.text_layout_details(window, cx);
         let style = &text_layout_details.editor_style;
         let font_id = window.text_system().resolve_font(&style.text.font());
         let font_size = style.text.font_size.to_pixels(window.rem_size());
@@ -27657,12 +27670,12 @@ impl EntityInputHandler for Editor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Option<gpui::Bounds<Pixels>> {
-        let text_layout_details = self.text_layout_details(window);
+        let text_layout_details = self.text_layout_details(window, cx);
         let CharacterDimensions {
             em_width,
             em_advance,
             line_height,
-        } = self.character_dimensions(window);
+        } = self.character_dimensions(window, cx);
 
         let snapshot = self.snapshot(window, cx);
         let scroll_position = snapshot.scroll_position();

crates/editor/src/editor_tests.rs 🔗

@@ -938,19 +938,34 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
 
             // Set scroll position to check later
             editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
-            let original_scroll_position = editor.scroll_manager.anchor();
+            let original_scroll_position = editor
+                .scroll_manager
+                .native_anchor(&editor.display_snapshot(cx), cx);
 
             // Jump to the end of the document and adjust scroll
             editor.move_to_end(&MoveToEnd, window, cx);
             editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
-            assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
+            assert_ne!(
+                editor
+                    .scroll_manager
+                    .native_anchor(&editor.display_snapshot(cx), cx),
+                original_scroll_position
+            );
 
             let nav_entry = pop_history(&mut editor, cx).unwrap();
             editor.navigate(nav_entry.data.unwrap(), window, cx);
-            assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
+            assert_eq!(
+                editor
+                    .scroll_manager
+                    .native_anchor(&editor.display_snapshot(cx), cx),
+                original_scroll_position
+            );
 
             // Ensure we don't panic when navigation data contains invalid anchors *and* points.
-            let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
+            let mut invalid_anchor = editor
+                .scroll_manager
+                .native_anchor(&editor.display_snapshot(cx), cx)
+                .anchor;
             invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
             let invalid_point = Point::new(9999, 0);
             editor.navigate(
@@ -17709,12 +17724,14 @@ async fn test_following(cx: &mut TestAppContext) {
                 &leader_entity,
                 window,
                 move |_, leader, event, window, cx| {
-                    leader.read(cx).add_event_to_update_proto(
-                        event,
-                        &mut update.borrow_mut(),
-                        window,
-                        cx,
-                    );
+                    leader.update(cx, |leader, cx| {
+                        leader.add_event_to_update_proto(
+                            event,
+                            &mut update.borrow_mut(),
+                            window,
+                            cx,
+                        );
+                    });
                 },
             )
             .detach();
@@ -17935,12 +17952,9 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
         let update = update_message.clone();
         |_, window, cx| {
             cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
-                leader.read(cx).add_event_to_update_proto(
-                    event,
-                    &mut update.borrow_mut(),
-                    window,
-                    cx,
-                );
+                leader.update(cx, |leader, cx| {
+                    leader.add_event_to_update_proto(event, &mut update.borrow_mut(), window, cx);
+                });
             })
             .detach();
         }

crates/editor/src/element.rs 🔗

@@ -192,23 +192,10 @@ struct RenderBlocksOutput {
     resized_blocks: Option<HashMap<CustomBlockId, u32>>,
 }
 
-/// Data passed to overlay painters during the paint phase.
-pub struct OverlayPainterData<'a> {
-    pub editor: &'a Entity<Editor>,
-    pub snapshot: &'a EditorSnapshot,
-    pub scroll_position: gpui::Point<ScrollOffset>,
-    pub line_height: Pixels,
-    pub visible_row_range: Range<DisplayRow>,
-    pub hitbox: &'a Hitbox,
-}
-
-pub type OverlayPainter = Box<dyn FnOnce(OverlayPainterData<'_>, &mut Window, &mut App)>;
-
 pub struct EditorElement {
     editor: Entity<Editor>,
     style: EditorStyle,
     split_side: Option<SplitSide>,
-    overlay_painter: Option<OverlayPainter>,
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -225,7 +212,6 @@ impl EditorElement {
             editor: editor.clone(),
             style,
             split_side: None,
-            overlay_painter: None,
         }
     }
 
@@ -233,10 +219,6 @@ impl EditorElement {
         self.split_side = Some(side);
     }
 
-    pub fn set_overlay_painter(&mut self, painter: OverlayPainter) {
-        self.overlay_painter = Some(painter);
-    }
-
     fn should_show_buffer_headers(&self) -> bool {
         self.split_side.is_none()
     }
@@ -1977,10 +1959,6 @@ impl EditorElement {
         window: &mut Window,
         cx: &mut App,
     ) -> Option<EditorScrollbars> {
-        if self.split_side == Some(SplitSide::Left) {
-            return None;
-        }
-
         let show_scrollbars = self.editor.read(cx).show_scrollbars;
         if (!show_scrollbars.horizontal && !show_scrollbars.vertical)
             || self.style.scrollbar_width.is_zero()
@@ -4537,7 +4515,9 @@ impl EditorElement {
         let mut end_rows = Vec::<DisplayRow>::new();
         let mut rows = Vec::<StickyHeader>::new();
 
-        let items = editor.sticky_headers(style, cx).unwrap_or_default();
+        let items = editor
+            .sticky_headers(&snapshot.display_snapshot, style, cx)
+            .unwrap_or_default();
 
         for item in items {
             let start_point = item.range.start.to_point(snapshot.buffer_snapshot());
@@ -5239,7 +5219,7 @@ impl EditorElement {
                 snapshot,
                 visible_display_row_range.clone(),
                 max_size,
-                &editor.text_layout_details(window),
+                &editor.text_layout_details(window, cx),
                 window,
                 cx,
             )
@@ -9632,38 +9612,6 @@ impl Element for EditorElement {
                         }
                     };
 
-                    // When jumping from one side of a side-by-side diff to the
-                    // other, we autoscroll autoscroll to keep the target range in view.
-                    //
-                    // If our scroll companion has a pending autoscroll request, process it
-                    // first so that both editors render with synchronized scroll positions.
-                    // This is important for split diff views where one editor may prepaint
-                    // before the other.
-                    if let Some(companion) = self
-                        .editor
-                        .read(cx)
-                        .scroll_companion()
-                        .and_then(|c| c.upgrade())
-                    {
-                        if companion.read(cx).scroll_manager.has_autoscroll_request() {
-                            companion.update(cx, |companion_editor, cx| {
-                                let companion_autoscroll_request =
-                                    companion_editor.scroll_manager.take_autoscroll_request();
-                                companion_editor.autoscroll_vertically(
-                                    bounds,
-                                    line_height,
-                                    max_scroll_top,
-                                    companion_autoscroll_request,
-                                    window,
-                                    cx,
-                                );
-                            });
-                            snapshot = self
-                                .editor
-                                .update(cx, |editor, cx| editor.snapshot(window, cx));
-                        }
-                    }
-
                     let (
                         autoscroll_request,
                         autoscroll_containing_element,
@@ -10261,8 +10209,8 @@ impl Element for EditorElement {
                     );
 
                     self.editor.update(cx, |editor, cx| {
-                        if editor.scroll_manager.clamp_scroll_left(scroll_max.x) {
-                            scroll_position.x = scroll_position.x.min(scroll_max.x);
+                        if editor.scroll_manager.clamp_scroll_left(scroll_max.x, cx) {
+                            scroll_position.x = scroll_max.x.min(scroll_position.x);
                         }
 
                         if needs_horizontal_autoscroll.0
@@ -10919,18 +10867,6 @@ impl Element for EditorElement {
                     self.paint_scrollbars(layout, window, cx);
                     self.paint_edit_prediction_popover(layout, window, cx);
                     self.paint_mouse_context_menu(layout, window, cx);
-
-                    if let Some(overlay_painter) = self.overlay_painter.take() {
-                        let data = OverlayPainterData {
-                            editor: &self.editor,
-                            snapshot: &layout.position_map.snapshot,
-                            scroll_position: layout.position_map.snapshot.scroll_position(),
-                            line_height: layout.position_map.line_height,
-                            visible_row_range: layout.visible_display_row_range.clone(),
-                            hitbox: &layout.hitbox,
-                        };
-                        overlay_painter(data, window, cx);
-                    }
                 });
             })
         })

crates/editor/src/items.rs 🔗

@@ -204,17 +204,20 @@ impl FollowableItem for Editor {
         cx.notify();
     }
 
-    fn to_state_proto(&self, _: &Window, cx: &App) -> Option<proto::view::Variant> {
-        let buffer = self.buffer.read(cx);
-        if buffer
+    fn to_state_proto(&self, _: &mut Window, cx: &mut App) -> Option<proto::view::Variant> {
+        let is_private = self
+            .buffer
+            .read(cx)
             .as_singleton()
             .and_then(|buffer| buffer.read(cx).file())
-            .is_some_and(|file| file.is_private())
-        {
+            .is_some_and(|file| file.is_private());
+        if is_private {
             return None;
         }
 
-        let scroll_anchor = self.scroll_manager.anchor();
+        let display_snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let scroll_anchor = self.scroll_manager.native_anchor(&display_snapshot, cx);
+        let buffer = self.buffer.read(cx);
         let excerpts = buffer
             .read(cx)
             .excerpts()
@@ -269,8 +272,8 @@ impl FollowableItem for Editor {
         &self,
         event: &EditorEvent,
         update: &mut Option<proto::update_view::Variant>,
-        _: &Window,
-        cx: &App,
+        _: &mut Window,
+        cx: &mut App,
     ) -> bool {
         let update =
             update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
@@ -305,8 +308,9 @@ impl FollowableItem for Editor {
                     true
                 }
                 EditorEvent::ScrollPositionChanged { autoscroll, .. } if !autoscroll => {
+                    let display_snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
                     let snapshot = self.buffer.read(cx).snapshot(cx);
-                    let scroll_anchor = self.scroll_manager.anchor();
+                    let scroll_anchor = self.scroll_manager.native_anchor(&display_snapshot, cx);
                     update.scroll_top_anchor =
                         Some(serialize_anchor(&scroll_anchor.anchor, &snapshot));
                     update.scroll_x = scroll_anchor.offset.x;

crates/editor/src/mouse_context_menu.rs 🔗

@@ -331,7 +331,7 @@ pub fn deploy_context_menu(
             cx,
         ),
         None => {
-            let character_size = editor.character_dimensions(window);
+            let character_size = editor.character_dimensions(window, cx);
             let menu_position = MenuPosition::PinnedToEditor {
                 source: source_anchor,
                 offset: gpui::point(character_size.em_width, character_size.line_height),

crates/editor/src/movement.rs 🔗

@@ -4,7 +4,7 @@
 use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
 use crate::{
     DisplayRow, EditorStyle, ToOffset, ToPoint,
-    scroll::{ScrollAnchor, ScrollOffset},
+    scroll::{ScrollOffset, SharedScrollAnchor},
 };
 use gpui::{Pixels, WindowTextSystem};
 use language::{CharClassifier, Point};
@@ -29,7 +29,7 @@ pub struct TextLayoutDetails {
     pub(crate) text_system: Arc<WindowTextSystem>,
     pub(crate) editor_style: EditorStyle,
     pub(crate) rem_size: Pixels,
-    pub scroll_anchor: ScrollAnchor,
+    pub scroll_anchor: SharedScrollAnchor,
     pub visible_rows: Option<f64>,
     pub vertical_scroll_margin: ScrollOffset,
 }
@@ -1224,7 +1224,8 @@ mod tests {
         let editor = cx.editor.clone();
         let window = cx.window;
         _ = cx.update_window(window, |_, window, cx| {
-            let text_layout_details = editor.read(cx).text_layout_details(window);
+            let text_layout_details =
+                editor.update(cx, |editor, cx| editor.text_layout_details(window, cx));
 
             let font = font("Helvetica");
 

crates/editor/src/scroll.rs 🔗

@@ -12,7 +12,9 @@ use crate::{
 };
 pub use autoscroll::{Autoscroll, AutoscrollStrategy};
 use core::fmt::Debug;
-use gpui::{Along, App, Axis, Context, Pixels, Task, Window, point, px};
+use gpui::{
+    Along, App, AppContext as _, Axis, Context, Entity, EntityId, Pixels, Task, Window, point, px,
+};
 use language::language_settings::{AllLanguageSettings, SoftWrap};
 use language::{Bias, Point};
 pub use scroll_amount::ScrollAmount;
@@ -68,6 +70,47 @@ pub struct OngoingScroll {
     axis: Option<Axis>,
 }
 
+/// In the side-by-side diff view, the two sides share a ScrollAnchor using this struct.
+/// Either side can set a ScrollAnchor that points to its own multibuffer, and we store the ID of the display map
+/// that the last-written anchor came from so that we know how to resolve it to a DisplayPoint.
+///
+/// For normal editors, this just acts as a wrapper around a ScrollAnchor.
+#[derive(Clone, Copy, Debug)]
+pub struct SharedScrollAnchor {
+    pub scroll_anchor: ScrollAnchor,
+    pub display_map_id: Option<EntityId>,
+}
+
+impl SharedScrollAnchor {
+    pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<ScrollOffset> {
+        let snapshot = if let Some(display_map_id) = self.display_map_id
+            && display_map_id != snapshot.display_map_id
+        {
+            let companion_snapshot = snapshot.companion_snapshot().unwrap();
+            assert_eq!(companion_snapshot.display_map_id, display_map_id);
+            companion_snapshot
+        } else {
+            snapshot
+        };
+
+        self.scroll_anchor.scroll_position(snapshot)
+    }
+
+    pub fn scroll_top_display_point(&self, snapshot: &DisplaySnapshot) -> DisplayPoint {
+        let snapshot = if let Some(display_map_id) = self.display_map_id
+            && display_map_id != snapshot.display_map_id
+        {
+            let companion_snapshot = snapshot.companion_snapshot().unwrap();
+            assert_eq!(companion_snapshot.display_map_id, display_map_id);
+            companion_snapshot
+        } else {
+            snapshot
+        };
+
+        self.scroll_anchor.anchor.to_display_point(snapshot)
+    }
+}
+
 impl OngoingScroll {
     fn new() -> Self {
         Self {
@@ -150,7 +193,13 @@ impl ActiveScrollbarState {
 
 pub struct ScrollManager {
     pub(crate) vertical_scroll_margin: ScrollOffset,
-    anchor: ScrollAnchor,
+    anchor: Entity<SharedScrollAnchor>,
+    /// Value to be used for clamping the x component of the SharedScrollAnchor's offset.
+    ///
+    /// We store this outside the SharedScrollAnchor so that the two sides of a side-by-side diff can share
+    /// a horizontal scroll offset that may be out of range for one of the editors (when one side is wider than the other).
+    /// Each side separately clamps the x component using its own scroll_max_x when reading from the SharedScrollAnchor.
+    scroll_max_x: Option<f64>,
     ongoing: OngoingScroll,
     /// Number of sticky header lines currently being rendered for the current scroll position.
     sticky_header_line_count: usize,
@@ -175,10 +224,15 @@ pub struct ScrollManager {
 }
 
 impl ScrollManager {
-    pub fn new(cx: &mut App) -> Self {
+    pub fn new(cx: &mut Context<Editor>) -> Self {
+        let anchor = cx.new(|_| SharedScrollAnchor {
+            scroll_anchor: ScrollAnchor::new(),
+            display_map_id: None,
+        });
         ScrollManager {
             vertical_scroll_margin: EditorSettings::get_global(cx).vertical_scroll_margin,
-            anchor: ScrollAnchor::new(),
+            anchor,
+            scroll_max_x: None,
             ongoing: OngoingScroll::new(),
             sticky_header_line_count: 0,
             autoscroll_request: None,
@@ -194,14 +248,95 @@ impl ScrollManager {
         }
     }
 
-    pub fn clone_state(&mut self, other: &Self) {
-        self.anchor = other.anchor;
+    pub fn set_native_display_map_id(
+        &mut self,
+        display_map_id: EntityId,
+        cx: &mut Context<Editor>,
+    ) {
+        self.anchor.update(cx, |shared, _| {
+            if shared.display_map_id.is_none() {
+                shared.display_map_id = Some(display_map_id);
+            }
+        });
+    }
+
+    pub fn clone_state(
+        &mut self,
+        other: &Self,
+        other_snapshot: &DisplaySnapshot,
+        my_snapshot: &DisplaySnapshot,
+        cx: &mut Context<Editor>,
+    ) {
+        let native_anchor = other.native_anchor(other_snapshot, cx);
+        self.anchor.update(cx, |this, _| {
+            this.scroll_anchor = native_anchor;
+            this.display_map_id = Some(my_snapshot.display_map_id);
+        });
         self.ongoing = other.ongoing;
         self.sticky_header_line_count = other.sticky_header_line_count;
     }
 
-    pub fn anchor(&self) -> ScrollAnchor {
-        self.anchor
+    pub fn offset(&self, cx: &App) -> gpui::Point<f64> {
+        let mut offset = self.anchor.read(cx).scroll_anchor.offset;
+        if let Some(max_x) = self.scroll_max_x {
+            offset.x = offset.x.min(max_x);
+        }
+        offset
+    }
+
+    /// Get a ScrollAnchor whose `anchor` field is guaranteed to point into the multibuffer for the provided snapshot.
+    ///
+    /// For normal editors, this just retrieves the internal ScrollAnchor and is lossless. When the editor is part of a side-by-side diff,
+    /// we may need to translate the anchor to point to the "native" multibuffer first. That translation is lossy,
+    /// so this method should be used sparingly---if you just need a scroll position or display point, call the appropriate helper method instead,
+    /// since they can losslessly handle the case where the ScrollAnchor was last set from the other side.
+    pub fn native_anchor(&self, snapshot: &DisplaySnapshot, cx: &App) -> ScrollAnchor {
+        let shared = self.anchor.read(cx);
+
+        let mut result = if let Some(display_map_id) = shared.display_map_id
+            && display_map_id != snapshot.display_map_id
+        {
+            let companion_snapshot = snapshot.companion_snapshot().unwrap();
+            assert_eq!(companion_snapshot.display_map_id, display_map_id);
+            let mut display_point = shared
+                .scroll_anchor
+                .anchor
+                .to_display_point(companion_snapshot);
+            *display_point.column_mut() = 0;
+            let buffer_point = snapshot.display_point_to_point(display_point, Bias::Left);
+            let anchor = snapshot.buffer_snapshot().anchor_before(buffer_point);
+            ScrollAnchor {
+                anchor,
+                offset: shared.scroll_anchor.offset,
+            }
+        } else {
+            shared.scroll_anchor
+        };
+
+        if let Some(max_x) = self.scroll_max_x {
+            result.offset.x = result.offset.x.min(max_x);
+        }
+        result
+    }
+
+    pub fn shared_scroll_anchor(&self, cx: &App) -> SharedScrollAnchor {
+        let mut shared = *self.anchor.read(cx);
+        if let Some(max_x) = self.scroll_max_x {
+            shared.scroll_anchor.offset.x = shared.scroll_anchor.offset.x.min(max_x);
+        }
+        shared
+    }
+
+    pub fn scroll_top_display_point(&self, snapshot: &DisplaySnapshot, cx: &App) -> DisplayPoint {
+        self.anchor.read(cx).scroll_top_display_point(snapshot)
+    }
+
+    pub fn scroll_anchor_entity(&self) -> Entity<SharedScrollAnchor> {
+        self.anchor.clone()
+    }
+
+    pub fn set_shared_scroll_anchor(&mut self, entity: Entity<SharedScrollAnchor>) {
+        self.anchor = entity;
     }
 
     pub fn ongoing_scroll(&self) -> OngoingScroll {
@@ -213,8 +348,16 @@ impl ScrollManager {
         self.ongoing.axis = axis;
     }
 
-    pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<ScrollOffset> {
-        self.anchor.scroll_position(snapshot)
+    pub fn scroll_position(
+        &self,
+        snapshot: &DisplaySnapshot,
+        cx: &App,
+    ) -> gpui::Point<ScrollOffset> {
+        let mut pos = self.anchor.read(cx).scroll_position(snapshot);
+        if let Some(max_x) = self.scroll_max_x {
+            pos.x = pos.x.min(max_x);
+        }
+        pos
     }
 
     pub fn sticky_header_line_count(&self) -> usize {
@@ -265,10 +408,6 @@ impl ScrollManager {
                 Bias::Left,
             )
             .to_point(map);
-        // Anchor the scroll position to the *left* of the first visible buffer point.
-        //
-        // This prevents the viewport from shifting down when blocks (e.g. expanded diff hunk
-        // deletions) are inserted *above* the first buffer character in the file.
         let top_anchor = map.buffer_snapshot().anchor_before(scroll_top_buffer_point);
 
         self.set_anchor(
@@ -279,6 +418,7 @@ impl ScrollManager {
                     scroll_top - top_anchor.to_display_point(map).row().as_f64(),
                 ),
             },
+            map,
             scroll_top_buffer_point.row,
             local,
             autoscroll,
@@ -291,6 +431,7 @@ impl ScrollManager {
     fn set_anchor(
         &mut self,
         anchor: ScrollAnchor,
+        display_map: &DisplaySnapshot,
         top_row: u32,
         local: bool,
         autoscroll: bool,
@@ -299,20 +440,27 @@ impl ScrollManager {
         cx: &mut Context<Editor>,
     ) -> WasScrolled {
         let adjusted_anchor = if self.forbid_vertical_scroll {
+            let current = self.anchor.read(cx);
             ScrollAnchor {
-                offset: gpui::Point::new(anchor.offset.x, self.anchor.offset.y),
-                anchor: self.anchor.anchor,
+                offset: gpui::Point::new(anchor.offset.x, current.scroll_anchor.offset.y),
+                anchor: current.scroll_anchor.anchor,
             }
         } else {
             anchor
         };
 
+        self.scroll_max_x.take();
         self.autoscroll_request.take();
-        if self.anchor == adjusted_anchor {
+
+        let current = self.anchor.read(cx);
+        if current.scroll_anchor == adjusted_anchor {
             return WasScrolled(false);
         }
 
-        self.anchor = adjusted_anchor;
+        self.anchor.update(cx, |shared, _| {
+            shared.scroll_anchor = adjusted_anchor;
+            shared.display_map_id = Some(display_map.display_map_id);
+        });
         cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
         self.show_scrollbars(window, cx);
         if let Some(workspace_id) = workspace_id {
@@ -466,13 +614,10 @@ impl ScrollManager {
         self.minimap_thumb_state
     }
 
-    pub fn clamp_scroll_left(&mut self, max: f64) -> bool {
-        if max < self.anchor.offset.x {
-            self.anchor.offset.x = max;
-            true
-        } else {
-            false
-        }
+    pub fn clamp_scroll_left(&mut self, max: f64, cx: &App) -> bool {
+        let current_x = self.anchor.read(cx).scroll_anchor.offset.x;
+        self.scroll_max_x = Some(max);
+        current_x > max
     }
 
     pub fn set_forbid_vertical_scroll(&mut self, forbid: bool) {
@@ -485,6 +630,10 @@ impl ScrollManager {
 }
 
 impl Editor {
+    pub fn has_autoscroll_request(&self) -> bool {
+        self.scroll_manager.has_autoscroll_request()
+    }
+
     pub fn vertical_scroll_margin(&self) -> usize {
         self.scroll_manager.vertical_scroll_margin as usize
     }
@@ -544,8 +693,7 @@ impl Editor {
             delta.y = 0.0;
         }
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        let position =
-            self.scroll_manager.anchor.scroll_position(&display_map) + delta.map(f64::from);
+        let position = self.scroll_manager.scroll_position(&display_map, cx) + delta.map(f64::from);
         self.set_scroll_position_taking_display_map(position, true, false, display_map, window, cx);
     }
 
@@ -603,20 +751,6 @@ impl Editor {
             cx,
         );
 
-        if local && was_scrolled.0 {
-            if let Some(companion) = self.scroll_companion.as_ref().and_then(|c| c.upgrade()) {
-                companion.update(cx, |companion_editor, cx| {
-                    companion_editor.set_scroll_position_internal(
-                        scroll_position,
-                        false,
-                        false,
-                        window,
-                        cx,
-                    );
-                });
-            }
-        }
-
         was_scrolled
     }
 
@@ -636,7 +770,7 @@ impl Editor {
             .set_previous_scroll_position(None);
 
         let adjusted_position = if self.scroll_manager.forbid_vertical_scroll {
-            let current_position = self.scroll_manager.anchor.scroll_position(&display_map);
+            let current_position = self.scroll_manager.scroll_position(&display_map, cx);
             gpui::Point::new(scroll_position.x, current_position.y)
         } else {
             scroll_position
@@ -655,7 +789,7 @@ impl Editor {
 
     pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<ScrollOffset> {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        self.scroll_manager.anchor.scroll_position(&display_map)
+        self.scroll_manager.scroll_position(&display_map, cx)
     }
 
     pub fn set_scroll_anchor(
@@ -666,12 +800,14 @@ impl Editor {
     ) {
         hide_hover(self, cx);
         let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let top_row = scroll_anchor
             .anchor
             .to_point(&self.buffer().read(cx).snapshot(cx))
             .row;
         self.scroll_manager.set_anchor(
             scroll_anchor,
+            &display_map,
             top_row,
             true,
             false,
@@ -689,14 +825,16 @@ impl Editor {
     ) {
         hide_hover(self, cx);
         let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
-        let snapshot = &self.buffer().read(cx).snapshot(cx);
-        if !scroll_anchor.anchor.is_valid(snapshot) {
+        let buffer_snapshot = self.buffer().read(cx).snapshot(cx);
+        if !scroll_anchor.anchor.is_valid(&buffer_snapshot) {
             log::warn!("Invalid scroll anchor: {:?}", scroll_anchor);
             return;
         }
-        let top_row = scroll_anchor.anchor.to_point(snapshot).row;
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let top_row = scroll_anchor.anchor.to_point(&buffer_snapshot).row;
         self.scroll_manager.set_anchor(
             scroll_anchor,
+            &display_map,
             top_row,
             false,
             false,
@@ -775,11 +913,7 @@ impl Editor {
             .newest_anchor()
             .head()
             .to_display_point(&snapshot);
-        let screen_top = self
-            .scroll_manager
-            .anchor
-            .anchor
-            .to_display_point(&snapshot);
+        let screen_top = self.scroll_manager.scroll_top_display_point(&snapshot, cx);
 
         if screen_top > newest_head {
             return Ordering::Less;

crates/editor/src/scroll/autoscroll.rs 🔗

@@ -115,7 +115,7 @@ impl Editor {
         let viewport_height = bounds.size.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 mut scroll_position = self.scroll_manager.scroll_position(&display_map, cx);
         let original_y = scroll_position.y;
         if let Some(last_bounds) = self.expect_bounds_change.take()
             && scroll_position.y != 0.
@@ -201,7 +201,7 @@ impl Editor {
                 .last_autoscroll
                 .as_ref()
                 .filter(|(offset, last_target_top, last_target_bottom, _)| {
-                    self.scroll_manager.anchor.offset == *offset
+                    self.scroll_manager.offset(cx) == *offset
                         && target_top == *last_target_top
                         && target_bottom == *last_target_bottom
                 })
@@ -264,7 +264,7 @@ impl Editor {
         };
 
         self.scroll_manager.last_autoscroll = Some((
-            self.scroll_manager.anchor.offset,
+            self.scroll_manager.offset(cx),
             target_top,
             target_bottom,
             strategy,
@@ -292,7 +292,7 @@ impl Editor {
 
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let selections = self.selections.all::<Point>(&display_map);
-        let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
+        let mut scroll_position = self.scroll_manager.scroll_position(&display_map, cx);
 
         let mut target_left;
         let mut target_right: f64;
@@ -334,7 +334,7 @@ impl Editor {
             return None;
         }
 
-        let scroll_left = self.scroll_manager.anchor.offset.x * em_advance;
+        let scroll_left = self.scroll_manager.offset(cx).x * em_advance;
         let scroll_right = scroll_left + viewport_width;
 
         let was_scrolled = if target_left < scroll_left {

crates/editor/src/split.rs 🔗

@@ -527,12 +527,19 @@ impl SplittableEditor {
             dm.set_companion(Some((rhs_display_map.downgrade(), companion)), cx);
         });
 
-        let rhs_weak = self.rhs_editor.downgrade();
-        let lhs_weak = lhs.editor.downgrade();
+        let shared_scroll_anchor = self
+            .rhs_editor
+            .read(cx)
+            .scroll_manager
+            .scroll_anchor_entity();
+        lhs.editor.update(cx, |editor, _cx| {
+            editor
+                .scroll_manager
+                .set_shared_scroll_anchor(shared_scroll_anchor);
+        });
 
         let this = cx.entity().downgrade();
         self.rhs_editor.update(cx, |editor, _cx| {
-            editor.set_scroll_companion(Some(lhs_weak));
             let this = this.clone();
             editor.set_on_local_selections_changed(Some(Box::new(
                 move |cursor_position, window, cx| {
@@ -549,7 +556,6 @@ impl SplittableEditor {
             )));
         });
         lhs.editor.update(cx, |editor, _cx| {
-            editor.set_scroll_companion(Some(rhs_weak));
             let this = this.clone();
             editor.set_on_local_selections_changed(Some(Box::new(
                 move |cursor_position, window, cx| {
@@ -566,13 +572,6 @@ impl SplittableEditor {
             )));
         });
 
-        let rhs_scroll_position = self
-            .rhs_editor
-            .update(cx, |editor, cx| editor.scroll_position(cx));
-        lhs.editor.update(cx, |editor, cx| {
-            editor.set_scroll_position_internal(rhs_scroll_position, false, false, window, cx);
-        });
-
         // Copy soft wrap state from rhs (source of truth) to lhs
         let rhs_soft_wrap_override = self.rhs_editor.read(cx).soft_wrap_mode_override;
         lhs.editor.update(cx, |editor, cx| {
@@ -848,8 +847,17 @@ impl SplittableEditor {
         };
         self.panes.remove(&lhs.pane, cx).unwrap();
         self.rhs_editor.update(cx, |rhs, cx| {
+            let rhs_snapshot = rhs.display_map.update(cx, |dm, cx| dm.snapshot(cx));
+            let native_anchor = rhs.scroll_manager.native_anchor(&rhs_snapshot, cx);
+            let rhs_display_map_id = rhs_snapshot.display_map_id;
+            rhs.scroll_manager
+                .scroll_anchor_entity()
+                .update(cx, |shared, _| {
+                    shared.scroll_anchor = native_anchor;
+                    shared.display_map_id = Some(rhs_display_map_id);
+                });
+
             rhs.set_on_local_selections_changed(None);
-            rhs.set_scroll_companion(None);
             rhs.set_delegate_expand_excerpts(false);
             rhs.buffer().update(cx, |buffer, cx| {
                 buffer.set_show_deleted_hunks(true, cx);
@@ -861,7 +869,6 @@ impl SplittableEditor {
         });
         lhs.editor.update(cx, |editor, _cx| {
             editor.set_on_local_selections_changed(None);
-            editor.set_scroll_companion(None);
         });
         cx.notify();
     }
@@ -1671,6 +1678,7 @@ mod tests {
 
     async fn init_test(
         cx: &mut gpui::TestAppContext,
+        soft_wrap: SoftWrap,
     ) -> (Entity<SplittableEditor>, &mut VisualTestContext) {
         cx.update(|cx| {
             let store = SettingsStore::test(cx);
@@ -1696,7 +1704,7 @@ mod tests {
             );
             editor.split(&Default::default(), window, cx);
             editor.rhs_editor.update(cx, |editor, cx| {
-                editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
+                editor.set_soft_wrap_mode(soft_wrap, cx);
             });
             editor
                 .lhs
@@ -1704,7 +1712,7 @@ mod tests {
                 .unwrap()
                 .editor
                 .update(cx, |editor, cx| {
-                    editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
+                    editor.set_soft_wrap_mode(soft_wrap, cx);
                 });
             editor
         });
@@ -1775,7 +1783,7 @@ mod tests {
     async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
         use rand::prelude::*;
 
-        let (editor, cx) = init_test(cx).await;
+        let (editor, cx) = init_test(cx, SoftWrap::EditorWidth).await;
         let operations = std::env::var("OPERATIONS")
             .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
             .unwrap_or(10);
@@ -1859,7 +1867,7 @@ mod tests {
         use rope::Point;
         use unindent::Unindent as _;
 
-        let (editor, mut cx) = init_test(cx).await;
+        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
 
         let base_text = "
             aaa
@@ -1988,7 +1996,7 @@ mod tests {
         use rope::Point;
         use unindent::Unindent as _;
 
-        let (editor, mut cx) = init_test(cx).await;
+        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
 
         let base_text1 = "
             aaa
@@ -2146,7 +2154,7 @@ mod tests {
         use rope::Point;
         use unindent::Unindent as _;
 
-        let (editor, mut cx) = init_test(cx).await;
+        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
 
         let base_text = "
             aaa
@@ -2265,7 +2273,7 @@ mod tests {
         use rope::Point;
         use unindent::Unindent as _;
 
-        let (editor, mut cx) = init_test(cx).await;
+        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
 
         let base_text = "
             aaa
@@ -2394,7 +2402,7 @@ mod tests {
         use rope::Point;
         use unindent::Unindent as _;
 
-        let (editor, mut cx) = init_test(cx).await;
+        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
 
         let base_text = "
             aaa
@@ -2519,7 +2527,7 @@ mod tests {
         use rope::Point;
         use unindent::Unindent as _;
 
-        let (editor, mut cx) = init_test(cx).await;
+        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
 
         let base_text = "
             aaa
@@ -2632,7 +2640,7 @@ mod tests {
         use rope::Point;
         use unindent::Unindent as _;
 
-        let (editor, mut cx) = init_test(cx).await;
+        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
 
         let text = "aaaa bbbb cccc dddd eeee ffff";
 
@@ -2700,7 +2708,7 @@ mod tests {
         use rope::Point;
         use unindent::Unindent as _;
 
-        let (editor, mut cx) = init_test(cx).await;
+        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
 
         let base_text = "
             aaaa bbbb cccc dddd eeee ffff
@@ -2762,7 +2770,7 @@ mod tests {
         use rope::Point;
         use unindent::Unindent as _;
 
-        let (editor, mut cx) = init_test(cx).await;
+        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
 
         let base_text = "
             aaaa bbbb cccc dddd eeee ffff
@@ -2831,7 +2839,7 @@ mod tests {
         use rope::Point;
         use unindent::Unindent as _;
 
-        let (editor, mut cx) = init_test(cx).await;
+        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
 
         let text = "
             aaaa bbbb cccc dddd eeee ffff
@@ -2943,7 +2951,7 @@ mod tests {
         use rope::Point;
         use unindent::Unindent as _;
 
-        let (editor, mut cx) = init_test(cx).await;
+        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
 
         let (buffer1, diff1) = buffer_with_diff("xxx\nyyy", "xxx\nyyy", &mut cx);
 
@@ -3047,7 +3055,7 @@ mod tests {
         use rope::Point;
         use unindent::Unindent as _;
 
-        let (editor, mut cx) = init_test(cx).await;
+        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
 
         let base_text = "
             aaa
@@ -3129,7 +3137,7 @@ mod tests {
         use rope::Point;
         use unindent::Unindent as _;
 
-        let (editor, mut cx) = init_test(cx).await;
+        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
 
         let base_text = "aaaa bbbb cccc dddd eeee ffff\n";
 
@@ -3208,7 +3216,7 @@ mod tests {
         use rope::Point;
         use unindent::Unindent as _;
 
-        let (editor, mut cx) = init_test(cx).await;
+        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
 
         let base_text = "
             aaa
@@ -3330,7 +3338,7 @@ mod tests {
         use rope::Point;
         use unindent::Unindent as _;
 
-        let (editor, mut cx) = init_test(cx).await;
+        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
 
         let base_text = "";
         let current_text = "
@@ -3406,7 +3414,7 @@ mod tests {
         use rope::Point;
         use unindent::Unindent as _;
 
-        let (editor, mut cx) = init_test(cx).await;
+        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
 
         let base_text = "
             aaa
@@ -3494,4 +3502,88 @@ mod tests {
             &mut cx,
         );
     }
+
+    #[gpui::test]
+    async fn test_scrolling(cx: &mut gpui::TestAppContext) {
+        use crate::test::editor_content_with_blocks_and_size;
+        use gpui::size;
+        use rope::Point;
+
+        let (editor, mut cx) = init_test(cx, SoftWrap::None).await;
+
+        let long_line = "x".repeat(200);
+        let mut lines: Vec<String> = (0..50).map(|i| format!("line {i}")).collect();
+        lines[25] = long_line;
+        let content = lines.join("\n");
+
+        let (buffer, diff) = buffer_with_diff(&content, &content, &mut cx);
+
+        editor.update(cx, |editor, cx| {
+            let path = PathKey::for_buffer(&buffer, cx);
+            editor.set_excerpts_for_path(
+                path,
+                buffer.clone(),
+                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
+                0,
+                diff.clone(),
+                cx,
+            );
+        });
+
+        cx.run_until_parked();
+
+        let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
+            let lhs = editor.lhs.as_ref().expect("should have lhs editor");
+            (editor.rhs_editor.clone(), lhs.editor.clone())
+        });
+
+        rhs_editor.update_in(cx, |e, window, cx| {
+            e.set_scroll_position(gpui::Point::new(0., 10.), window, cx);
+        });
+
+        let rhs_pos =
+            rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
+        let lhs_pos =
+            lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
+        assert_eq!(rhs_pos.y, 10., "RHS should be scrolled to row 10");
+        assert_eq!(
+            lhs_pos.y, rhs_pos.y,
+            "LHS should have same scroll position as RHS after set_scroll_position"
+        );
+
+        let draw_size = size(px(300.), px(300.));
+
+        rhs_editor.update_in(cx, |e, window, cx| {
+            e.change_selections(Some(crate::Autoscroll::fit()).into(), window, cx, |s| {
+                s.select_ranges([Point::new(25, 150)..Point::new(25, 150)]);
+            });
+        });
+
+        let _ = editor_content_with_blocks_and_size(&rhs_editor, draw_size, &mut cx);
+        cx.run_until_parked();
+        let _ = editor_content_with_blocks_and_size(&lhs_editor, draw_size, &mut cx);
+        cx.run_until_parked();
+
+        let rhs_pos =
+            rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
+        let lhs_pos =
+            lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
+
+        assert!(
+            rhs_pos.y > 0.,
+            "RHS should have scrolled vertically to show cursor at row 25"
+        );
+        assert!(
+            rhs_pos.x > 0.,
+            "RHS should have scrolled horizontally to show cursor at column 150"
+        );
+        assert_eq!(
+            lhs_pos.y, rhs_pos.y,
+            "LHS should have same vertical scroll position as RHS after autoscroll"
+        );
+        assert_eq!(
+            lhs_pos.x, rhs_pos.x,
+            "LHS should have same horizontal scroll position as RHS after autoscroll"
+        );
+    }
 }

crates/editor/src/split_editor_view.rs 🔗

@@ -9,6 +9,7 @@ use gpui::{
 };
 use multi_buffer::{Anchor, ExcerptId};
 use settings::Settings;
+use smallvec::smallvec;
 use text::BufferId;
 use theme::ActiveTheme;
 use ui::scrollbars::ShowScrollbar;
@@ -171,7 +172,10 @@ impl RenderOnce for SplitEditorView {
         let state_for_drag = self.split_state.downgrade();
         let state_for_drop = self.split_state.downgrade();
 
-        let buffer_headers = SplitBufferHeadersElement::new(rhs_editor, self.style.clone());
+        let buffer_headers = SplitBufferHeadersElement::new(rhs_editor.clone(), self.style.clone());
+
+        let lhs_editor_for_order = lhs_editor;
+        let rhs_editor_for_order = rhs_editor;
 
         div()
             .id("split-editor-view-container")
@@ -179,6 +183,14 @@ impl RenderOnce for SplitEditorView {
             .relative()
             .child(
                 h_flex()
+                    .with_dynamic_prepaint_order(move |_window, cx| {
+                        let lhs_needs = lhs_editor_for_order.read(cx).has_autoscroll_request();
+                        let rhs_needs = rhs_editor_for_order.read(cx).has_autoscroll_request();
+                        match (lhs_needs, rhs_needs) {
+                            (false, true) => smallvec![2, 1, 0],
+                            _ => smallvec![0, 1, 2],
+                        }
+                    })
                     .id("split-editor-view")
                     .size_full()
                     .on_drag_move::<DraggedSplitHandle>(move |event, window, cx| {

crates/editor/src/test.rs 🔗

@@ -5,7 +5,7 @@ use std::{rc::Rc, sync::LazyLock};
 
 pub use crate::rust_analyzer_ext::expand_macro_recursively;
 use crate::{
-    DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer, SelectionEffects,
+    DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer, SelectionEffects, Size,
     display_map::{
         Block, BlockPlacement, CustomBlockId, DisplayMap, DisplayRow, DisplaySnapshot,
         ToDisplayPoint,
@@ -184,7 +184,14 @@ pub fn editor_content_with_blocks_and_width(
     width: Pixels,
     cx: &mut VisualTestContext,
 ) -> String {
-    let draw_size = size(width, px(3000.0));
+    editor_content_with_blocks_and_size(editor, size(width, px(3000.0)), cx)
+}
+
+pub fn editor_content_with_blocks_and_size(
+    editor: &Entity<Editor>,
+    draw_size: Size<Pixels>,
+    cx: &mut VisualTestContext,
+) -> String {
     cx.simulate_resize(draw_size);
     cx.draw(gpui::Point::default(), draw_size, |_, _| editor.clone());
     let (snapshot, mut lines, blocks) = editor.update_in(cx, |editor, window, cx| {

crates/editor/src/test/editor_test_context.rs 🔗

@@ -287,7 +287,7 @@ impl EditorTestContext {
                 .text
                 .line_height_in_pixels(window.rem_size());
             let snapshot = editor.snapshot(window, cx);
-            let details = editor.text_layout_details(window);
+            let details = editor.text_layout_details(window, cx);
 
             let y = pixel_position.y
                 + f32::from(line_height)

crates/gpui/src/elements/div.rs 🔗

@@ -1295,6 +1295,7 @@ pub fn div() -> Div {
         children: SmallVec::default(),
         prepaint_listener: None,
         image_cache: None,
+        prepaint_order_fn: None,
     }
 }
 
@@ -1304,6 +1305,7 @@ pub struct Div {
     children: SmallVec<[StackSafe<AnyElement>; 2]>,
     prepaint_listener: Option<Box<dyn Fn(Vec<Bounds<Pixels>>, &mut Window, &mut App) + 'static>>,
     image_cache: Option<Box<dyn ImageCacheProvider>>,
+    prepaint_order_fn: Option<Box<dyn Fn(&mut Window, &mut App) -> SmallVec<[usize; 8]>>>,
 }
 
 impl Div {
@@ -1322,6 +1324,22 @@ impl Div {
         self.image_cache = Some(Box::new(cache));
         self
     }
+
+    /// Specify a function that determines the order in which children are prepainted.
+    ///
+    /// The function is called at prepaint time and should return a vector of child indices
+    /// in the desired prepaint order. Each index should appear exactly once.
+    ///
+    /// This is useful when the prepaint of one child affects state that another child reads.
+    /// For example, in split editor views, the editor with an autoscroll request should
+    /// be prepainted first so its scroll position update is visible to the other editor.
+    pub fn with_dynamic_prepaint_order(
+        mut self,
+        order_fn: impl Fn(&mut Window, &mut App) -> SmallVec<[usize; 8]> + 'static,
+    ) -> Self {
+        self.prepaint_order_fn = Some(Box::new(order_fn));
+        self
+    }
 }
 
 /// A frame state for a `Div` element, which contains layout IDs for its children.
@@ -1486,8 +1504,17 @@ impl Element for Div {
 
                 window.with_image_cache(image_cache, |window| {
                     window.with_element_offset(scroll_offset, |window| {
-                        for child in &mut self.children {
-                            child.prepaint(window, cx);
+                        if let Some(order_fn) = &self.prepaint_order_fn {
+                            let order = order_fn(window, cx);
+                            for idx in order {
+                                if let Some(child) = self.children.get_mut(idx) {
+                                    child.prepaint(window, cx);
+                                }
+                            }
+                        } else {
+                            for child in &mut self.children {
+                                child.prepaint(window, cx);
+                            }
                         }
                     });
 

crates/search/src/buffer_search.rs 🔗

@@ -1313,7 +1313,7 @@ impl BufferSearchBar {
                 let search = self.update_matches(false, true, window, cx);
 
                 let width = editor.update(cx, |editor, cx| {
-                    let text_layout_details = editor.text_layout_details(window);
+                    let text_layout_details = editor.text_layout_details(window, cx);
                     let snapshot = editor.snapshot(window, cx).display_snapshot;
 
                     snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)

crates/vim/src/command.rs 🔗

@@ -2355,7 +2355,7 @@ impl Vim {
             let start = editor
                 .selections
                 .newest_display(&editor.display_snapshot(cx));
-            let text_layout_details = editor.text_layout_details(window);
+            let text_layout_details = editor.text_layout_details(window, cx);
             let (mut range, _) = motion
                 .range(
                     &snapshot,

crates/vim/src/helix.rs 🔗

@@ -128,7 +128,7 @@ impl Vim {
         cx: &mut Context<Self>,
     ) {
         self.update_editor(cx, |_, editor, cx| {
-            let text_layout_details = editor.text_layout_details(window);
+            let text_layout_details = editor.text_layout_details(window, cx);
             editor.change_selections(Default::default(), window, cx, |s| {
                 if let Motion::ZedSearchResult { new_selections, .. } = &motion {
                     s.select_anchor_ranges(new_selections.clone());
@@ -319,7 +319,7 @@ impl Vim {
         cx: &mut Context<Self>,
     ) {
         self.update_editor(cx, |_, editor, cx| {
-            let text_layout_details = editor.text_layout_details(window);
+            let text_layout_details = editor.text_layout_details(window, cx);
             editor.change_selections(Default::default(), window, cx, |s| {
                 s.move_with(|map, selection| {
                     let goal = selection.goal;
@@ -399,7 +399,7 @@ impl Vim {
                 // In Helix mode, EndOfLine should position cursor ON the last character,
                 // not after it. We therefore need special handling for it.
                 self.update_editor(cx, |_, editor, cx| {
-                    let text_layout_details = editor.text_layout_details(window);
+                    let text_layout_details = editor.text_layout_details(window, cx);
                     editor.change_selections(Default::default(), window, cx, |s| {
                         s.move_with(|map, selection| {
                             let goal = selection.goal;

crates/vim/src/indent.rs 🔗

@@ -102,7 +102,7 @@ impl Vim {
     ) {
         self.stop_recording(cx);
         self.update_editor(cx, |_, editor, cx| {
-            let text_layout_details = editor.text_layout_details(window);
+            let text_layout_details = editor.text_layout_details(window, cx);
             editor.transact(window, cx, |editor, window, cx| {
                 let mut selection_starts: HashMap<_, _> = Default::default();
                 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {

crates/vim/src/motion.rs 🔗

@@ -2871,8 +2871,7 @@ fn window_top(
 ) -> (DisplayPoint, SelectionGoal) {
     let first_visible_line = text_layout_details
         .scroll_anchor
-        .anchor
-        .to_display_point(map);
+        .scroll_top_display_point(map);
 
     if first_visible_line.row() != DisplayRow(0)
         && text_layout_details.vertical_scroll_margin as usize > times
@@ -2907,8 +2906,7 @@ fn window_middle(
     if let Some(visible_rows) = text_layout_details.visible_rows {
         let first_visible_line = text_layout_details
             .scroll_anchor
-            .anchor
-            .to_display_point(map);
+            .scroll_top_display_point(map);
 
         let max_visible_rows =
             (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
@@ -2933,10 +2931,10 @@ fn window_bottom(
     if let Some(visible_rows) = text_layout_details.visible_rows {
         let first_visible_line = text_layout_details
             .scroll_anchor
-            .anchor
-            .to_display_point(map);
+            .scroll_top_display_point(map);
         let bottom_row = first_visible_line.row().0
-            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
+            + (visible_rows + text_layout_details.scroll_anchor.scroll_anchor.offset.y - 1.).floor()
+                as u32;
         if bottom_row < map.max_point().row().0
             && text_layout_details.vertical_scroll_margin as usize > times
         {

crates/vim/src/normal.rs 🔗

@@ -579,7 +579,7 @@ impl Vim {
         cx: &mut Context<Self>,
     ) {
         self.update_editor(cx, |vim, editor, cx| {
-            let text_layout_details = editor.text_layout_details(window);
+            let text_layout_details = editor.text_layout_details(window, cx);
 
             // If vim is in temporary mode and the motion being used is
             // `EndOfLine` ($), we'll want to disable clipping at line ends so
@@ -748,7 +748,7 @@ impl Vim {
         self.start_recording(cx);
         self.switch_mode(Mode::Insert, false, window, cx);
         self.update_editor(cx, |_, editor, cx| {
-            let text_layout_details = editor.text_layout_details(window);
+            let text_layout_details = editor.text_layout_details(window, cx);
             editor.transact(window, cx, |editor, window, cx| {
                 let selections = editor.selections.all::<Point>(&editor.display_snapshot(cx));
                 let snapshot = editor.buffer().read(cx).snapshot(cx);

crates/vim/src/normal/change.rs 🔗

@@ -35,7 +35,7 @@ impl Vim {
             None
         };
         self.update_editor(cx, |vim, editor, cx| {
-            let text_layout_details = editor.text_layout_details(window);
+            let text_layout_details = editor.text_layout_details(window, cx);
             editor.transact(window, cx, |editor, window, cx| {
                 // We are swapping to insert mode anyway. Just set the line end clipping behavior now
                 editor.set_clip_at_line_ends(false, cx);

crates/vim/src/normal/convert.rs 🔗

@@ -33,7 +33,7 @@ impl Vim {
         self.stop_recording(cx);
         self.update_editor(cx, |_, editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
-            let text_layout_details = editor.text_layout_details(window);
+            let text_layout_details = editor.text_layout_details(window, cx);
             editor.transact(window, cx, |editor, window, cx| {
                 let mut selection_starts: HashMap<_, _> = Default::default();
                 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {

crates/vim/src/normal/delete.rs 🔗

@@ -24,7 +24,7 @@ impl Vim {
     ) {
         self.stop_recording(cx);
         self.update_editor(cx, |vim, editor, cx| {
-            let text_layout_details = editor.text_layout_details(window);
+            let text_layout_details = editor.text_layout_details(window, cx);
             editor.transact(window, cx, |editor, window, cx| {
                 editor.set_clip_at_line_ends(false, cx);
                 let mut original_columns: HashMap<_, _> = Default::default();

crates/vim/src/normal/paste.rs 🔗

@@ -36,7 +36,7 @@ impl Vim {
         Vim::take_forced_motion(cx);
 
         self.update_editor(cx, |vim, editor, cx| {
-            let text_layout_details = editor.text_layout_details(window);
+            let text_layout_details = editor.text_layout_details(window, cx);
             editor.transact(window, cx, |editor, window, cx| {
                 editor.set_clip_at_line_ends(false, cx);
 
@@ -283,7 +283,7 @@ impl Vim {
         self.stop_recording(cx);
         let selected_register = self.selected_register.take();
         self.update_editor(cx, |_, editor, cx| {
-            let text_layout_details = editor.text_layout_details(window);
+            let text_layout_details = editor.text_layout_details(window, cx);
             editor.transact(window, cx, |editor, window, cx| {
                 editor.set_clip_at_line_ends(false, cx);
                 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {

crates/vim/src/normal/scroll.rs 🔗

@@ -1,7 +1,6 @@
 use crate::{Vim, state::Mode};
 use editor::{
-    DisplayPoint, Editor, EditorSettings, SelectionEffects,
-    display_map::{DisplayRow, ToDisplayPoint},
+    DisplayPoint, Editor, EditorSettings, SelectionEffects, display_map::DisplayRow,
     scroll::ScrollAmount,
 };
 use gpui::{Context, Window, actions};
@@ -113,7 +112,10 @@ fn scroll_editor(
     cx: &mut Context<Editor>,
 ) {
     let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
-    let old_top_anchor = editor.scroll_manager.anchor().anchor;
+    let display_snapshot = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
+    let old_top = editor
+        .scroll_manager
+        .scroll_top_display_point(&display_snapshot, cx);
 
     if editor.scroll_hover(amount, window, cx) {
         return;
@@ -144,7 +146,10 @@ fn scroll_editor(
         return;
     };
 
-    let top_anchor = editor.scroll_manager.anchor().anchor;
+    let display_snapshot = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
+    let top = editor
+        .scroll_manager
+        .scroll_top_display_point(&display_snapshot, cx);
     let vertical_scroll_margin = EditorSettings::get_global(cx).vertical_scroll_margin;
 
     editor.change_selections(
@@ -159,7 +164,6 @@ fn scroll_editor(
                 // so we don't need to calculate both and deal with logic for
                 // both.
                 let mut head = selection.head();
-                let top = top_anchor.to_display_point(map);
                 let max_point = map.max_point();
                 let starting_column = head.column();
 
@@ -167,7 +171,6 @@ fn scroll_editor(
                     (vertical_scroll_margin as u32).min(visible_line_count as u32 / 2);
 
                 if preserve_cursor_position {
-                    let old_top = old_top_anchor.to_display_point(map);
                     let new_row = if old_top.row() == top.row() {
                         DisplayRow(
                             head.row()
@@ -175,7 +178,9 @@ fn scroll_editor(
                                 .saturating_add_signed(amount.lines(visible_line_count) as i32),
                         )
                     } else {
-                        DisplayRow(top.row().0 + selection.head().row().0 - old_top.row().0)
+                        DisplayRow(top.row().0.saturating_add_signed(
+                            selection.head().row().0 as i32 - old_top.row().0 as i32,
+                        ))
                     };
                     head = map.clip_point(DisplayPoint::new(new_row, head.column()), Bias::Left)
                 }
@@ -222,7 +227,7 @@ fn scroll_editor(
                 // maximum column for the current line, so the minimum column
                 // would end up being the same as the maximum column.
                 let min_column = match preserve_cursor_position {
-                    true => old_top_anchor.to_display_point(map).column(),
+                    true => old_top.column(),
                     false => top.column(),
                 };
 

crates/vim/src/normal/substitute.rs 🔗

@@ -48,7 +48,7 @@ impl Vim {
         self.update_editor(cx, |vim, editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
             editor.transact(window, cx, |editor, window, cx| {
-                let text_layout_details = editor.text_layout_details(window);
+                let text_layout_details = editor.text_layout_details(window, cx);
                 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                     s.move_with(|map, selection| {
                         if selection.start == selection.end {

crates/vim/src/normal/toggle_comments.rs 🔗

@@ -15,7 +15,7 @@ impl Vim {
     ) {
         self.stop_recording(cx);
         self.update_editor(cx, |_, editor, cx| {
-            let text_layout_details = editor.text_layout_details(window);
+            let text_layout_details = editor.text_layout_details(window, cx);
             editor.transact(window, cx, |editor, window, cx| {
                 let mut selection_starts: HashMap<_, _> = Default::default();
                 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {

crates/vim/src/normal/yank.rs 🔗

@@ -25,7 +25,7 @@ impl Vim {
         cx: &mut Context<Self>,
     ) {
         self.update_editor(cx, |vim, editor, cx| {
-            let text_layout_details = editor.text_layout_details(window);
+            let text_layout_details = editor.text_layout_details(window, cx);
             editor.transact(window, cx, |editor, window, cx| {
                 editor.set_clip_at_line_ends(false, cx);
                 let mut original_positions: HashMap<_, _> = Default::default();

crates/vim/src/replace.rs 🔗

@@ -197,7 +197,7 @@ impl Vim {
         self.stop_recording(cx);
         self.update_editor(cx, |vim, editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
-            let text_layout_details = editor.text_layout_details(window);
+            let text_layout_details = editor.text_layout_details(window, cx);
             let mut selection = editor
                 .selections
                 .newest_display(&editor.display_snapshot(cx));

crates/vim/src/rewrap.rs 🔗

@@ -56,7 +56,7 @@ impl Vim {
     ) {
         self.stop_recording(cx);
         self.update_editor(cx, |_, editor, cx| {
-            let text_layout_details = editor.text_layout_details(window);
+            let text_layout_details = editor.text_layout_details(window, cx);
             editor.transact(window, cx, |editor, window, cx| {
                 let mut selection_starts: HashMap<_, _> = Default::default();
                 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {

crates/vim/src/surrounds.rs 🔗

@@ -88,7 +88,7 @@ impl Vim {
         let forced_motion = Vim::take_forced_motion(cx);
         let mode = self.mode;
         self.update_editor(cx, |_, editor, cx| {
-            let text_layout_details = editor.text_layout_details(window);
+            let text_layout_details = editor.text_layout_details(window, cx);
             editor.transact(window, cx, |editor, window, cx| {
                 editor.set_clip_at_line_ends(false, cx);
 

crates/vim/src/visual.rs 🔗

@@ -218,7 +218,7 @@ impl Vim {
         cx: &mut Context<Self>,
     ) {
         self.update_editor(cx, |vim, editor, cx| {
-            let text_layout_details = editor.text_layout_details(window);
+            let text_layout_details = editor.text_layout_details(window, cx);
             if vim.mode == Mode::VisualBlock
                 && !matches!(
                     motion,
@@ -302,7 +302,7 @@ impl Vim {
             SelectionGoal,
         ) -> Option<(DisplayPoint, SelectionGoal)>,
     ) {
-        let text_layout_details = editor.text_layout_details(window);
+        let text_layout_details = editor.text_layout_details(window, cx);
         editor.change_selections(Default::default(), window, cx, |s| {
             let map = &s.display_snapshot();
             let mut head = s.newest_anchor().head().to_display_point(map);

crates/workspace/src/item.rs 🔗

@@ -1190,7 +1190,7 @@ pub enum Dedup {
 
 pub trait FollowableItem: Item {
     fn remote_id(&self) -> Option<ViewId>;
-    fn to_state_proto(&self, window: &Window, cx: &App) -> Option<proto::view::Variant>;
+    fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant>;
     fn from_state_proto(
         project: Entity<Workspace>,
         id: ViewId,
@@ -1203,8 +1203,8 @@ pub trait FollowableItem: Item {
         &self,
         event: &Self::Event,
         update: &mut Option<proto::update_view::Variant>,
-        window: &Window,
-        cx: &App,
+        window: &mut Window,
+        cx: &mut App,
     ) -> bool;
     fn apply_update_proto(
         &mut self,
@@ -1284,7 +1284,7 @@ impl<T: FollowableItem> FollowableItemHandle for Entity<T> {
     }
 
     fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant> {
-        self.read(cx).to_state_proto(window, cx)
+        self.update(cx, |this, cx| this.to_state_proto(window, cx))
     }
 
     fn add_event_to_update_proto(
@@ -1295,8 +1295,9 @@ impl<T: FollowableItem> FollowableItemHandle for Entity<T> {
         cx: &mut App,
     ) -> bool {
         if let Some(event) = event.downcast_ref() {
-            self.read(cx)
-                .add_event_to_update_proto(event, update, window, cx)
+            self.update(cx, |this, cx| {
+                this.add_event_to_update_proto(event, update, window, cx)
+            })
         } else {
             false
         }