Rework edit prediction preview mode (#24700)

Max Brunsfeld , danilo-leal , and agu-z created

Don't animate the cursor when previewing jumps.
Instead, display the jump popover with a line that resembles a cursor,
indicating the jump destination. If the jump destination is outside of
the view port, there is an extra step in which `tab` scrolls the
viewport to reveal the jump destination.

Release Notes:

- N/A

---------

Co-authored-by: danilo-leal <daniloleal09@gmail.com>
Co-authored-by: agu-z <hi@aguz.me>

Change summary

crates/editor/src/editor.rs            | 441 +++++----------------------
crates/editor/src/element.rs           | 284 +++++++++--------
crates/editor/src/scroll.rs            |  10 
crates/editor/src/scroll/autoscroll.rs |  23 -
crates/ui/src/traits/styled_ext.rs     |   4 
5 files changed, 244 insertions(+), 518 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -520,296 +520,15 @@ pub enum MenuInlineCompletionsPolicy {
     ByProvider,
 }
 
-// TODO az do we need this?
-#[derive(Clone)]
 pub enum EditPredictionPreview {
     /// Modifier is not pressed
     Inactive,
-    /// Modifier pressed, animating to active
-    MovingTo {
-        animation: Range<Instant>,
-        scroll_position_at_start: Option<gpui::Point<f32>>,
-        target_point: DisplayPoint,
-    },
-    Arrived {
-        scroll_position_at_start: Option<gpui::Point<f32>>,
-        scroll_position_at_arrival: Option<gpui::Point<f32>>,
-        target_point: Option<DisplayPoint>,
-    },
-    /// Modifier released, animating from active
-    MovingFrom {
-        animation: Range<Instant>,
-        target_point: DisplayPoint,
+    /// Modifier pressed
+    Active {
+        previous_scroll_position: Option<ScrollAnchor>,
     },
 }
 
-impl EditPredictionPreview {
-    fn start(
-        &mut self,
-        completion: &InlineCompletion,
-        snapshot: &EditorSnapshot,
-        cursor: DisplayPoint,
-    ) -> bool {
-        if matches!(self, Self::MovingTo { .. } | Self::Arrived { .. }) {
-            return false;
-        }
-        (*self, _) = Self::start_now(completion, snapshot, cursor);
-        true
-    }
-
-    fn restart(
-        &mut self,
-        completion: &InlineCompletion,
-        snapshot: &EditorSnapshot,
-        cursor: DisplayPoint,
-    ) -> bool {
-        match self {
-            Self::Inactive => false,
-            Self::MovingTo { target_point, .. }
-            | Self::Arrived {
-                target_point: Some(target_point),
-                ..
-            } => {
-                let (new_preview, new_target_point) = Self::start_now(completion, snapshot, cursor);
-
-                if new_target_point != Some(*target_point) {
-                    *self = new_preview;
-                    return true;
-                }
-
-                false
-            }
-            Self::Arrived {
-                target_point: None, ..
-            } => {
-                let (new_preview, _) = Self::start_now(completion, snapshot, cursor);
-
-                *self = new_preview;
-                true
-            }
-            Self::MovingFrom { .. } => false,
-        }
-    }
-
-    fn start_now(
-        completion: &InlineCompletion,
-        snapshot: &EditorSnapshot,
-        cursor: DisplayPoint,
-    ) -> (Self, Option<DisplayPoint>) {
-        let now = Instant::now();
-        match completion {
-            InlineCompletion::Edit { .. } => (
-                Self::Arrived {
-                    target_point: None,
-                    scroll_position_at_start: None,
-                    scroll_position_at_arrival: None,
-                },
-                None,
-            ),
-            InlineCompletion::Move { target, .. } => {
-                let target_point = target.to_display_point(&snapshot.display_snapshot);
-                let duration = Self::animation_duration(cursor, target_point);
-
-                (
-                    Self::MovingTo {
-                        animation: now..now + duration,
-                        scroll_position_at_start: Some(snapshot.scroll_position()),
-                        target_point,
-                    },
-                    Some(target_point),
-                )
-            }
-        }
-    }
-
-    fn animation_duration(a: DisplayPoint, b: DisplayPoint) -> Duration {
-        const SPEED: f32 = 8.0;
-
-        let row_diff = b.row().0.abs_diff(a.row().0);
-        let column_diff = b.column().abs_diff(a.column());
-        let distance = ((row_diff.pow(2) + column_diff.pow(2)) as f32).sqrt();
-        Duration::from_millis((distance * SPEED) as u64)
-    }
-
-    fn end(
-        &mut self,
-        cursor: DisplayPoint,
-        scroll_pixel_position: gpui::Point<Pixels>,
-        window: &mut Window,
-        cx: &mut Context<Editor>,
-    ) -> bool {
-        let (scroll_position, target_point) = match self {
-            Self::MovingTo {
-                scroll_position_at_start,
-                target_point,
-                ..
-            }
-            | Self::Arrived {
-                scroll_position_at_start,
-                scroll_position_at_arrival: None,
-                target_point: Some(target_point),
-                ..
-            } => (*scroll_position_at_start, target_point),
-            Self::Arrived {
-                scroll_position_at_start,
-                scroll_position_at_arrival: Some(scroll_at_arrival),
-                target_point: Some(target_point),
-            } => {
-                const TOLERANCE: f32 = 4.0;
-
-                let diff = *scroll_at_arrival - scroll_pixel_position.map(|p| p.0);
-
-                if diff.x.abs() < TOLERANCE && diff.y.abs() < TOLERANCE {
-                    (*scroll_position_at_start, target_point)
-                } else {
-                    (None, target_point)
-                }
-            }
-            Self::Arrived {
-                target_point: None, ..
-            } => {
-                *self = Self::Inactive;
-                return true;
-            }
-            Self::MovingFrom { .. } | Self::Inactive => return false,
-        };
-
-        let now = Instant::now();
-        let duration = Self::animation_duration(cursor, *target_point);
-        let target_point = *target_point;
-
-        *self = Self::MovingFrom {
-            animation: now..now + duration,
-            target_point,
-        };
-
-        if let Some(scroll_position) = scroll_position {
-            cx.spawn_in(window, |editor, mut cx| async move {
-                smol::Timer::after(duration).await;
-                editor
-                    .update_in(&mut cx, |editor, window, cx| {
-                        if let Self::MovingFrom { .. } | Self::Inactive =
-                            editor.edit_prediction_preview
-                        {
-                            editor.set_scroll_position(scroll_position, window, cx)
-                        }
-                    })
-                    .log_err();
-            })
-            .detach();
-        }
-
-        true
-    }
-
-    /// Whether the preview is active or we are animating to or from it.
-    fn is_active(&self) -> bool {
-        matches!(
-            self,
-            Self::MovingTo { .. } | Self::Arrived { .. } | Self::MovingFrom { .. }
-        )
-    }
-
-    /// Returns true if the preview is active, not cancelled, and the animation is settled.
-    fn is_active_settled(&self) -> bool {
-        matches!(self, Self::Arrived { .. })
-    }
-
-    #[allow(clippy::too_many_arguments)]
-    fn move_state(
-        &mut self,
-        snapshot: &EditorSnapshot,
-        visible_row_range: Range<DisplayRow>,
-        line_layouts: &[LineWithInvisibles],
-        scroll_pixel_position: gpui::Point<Pixels>,
-        line_height: Pixels,
-        target: Anchor,
-        cursor: Option<DisplayPoint>,
-    ) -> Option<EditPredictionMoveState> {
-        let delta = match self {
-            Self::Inactive => return None,
-            Self::Arrived { .. } => 1.,
-            Self::MovingTo {
-                animation,
-                scroll_position_at_start: original_scroll_position,
-                target_point,
-            } => {
-                let now = Instant::now();
-                if animation.end < now {
-                    *self = Self::Arrived {
-                        scroll_position_at_start: *original_scroll_position,
-                        scroll_position_at_arrival: Some(scroll_pixel_position.map(|p| p.0)),
-                        target_point: Some(*target_point),
-                    };
-                    1.0
-                } else {
-                    (now - animation.start).as_secs_f32()
-                        / (animation.end - animation.start).as_secs_f32()
-                }
-            }
-            Self::MovingFrom { animation, .. } => {
-                let now = Instant::now();
-                if animation.end < now {
-                    *self = Self::Inactive;
-                    return None;
-                } else {
-                    let delta = (now - animation.start).as_secs_f32()
-                        / (animation.end - animation.start).as_secs_f32();
-                    1.0 - delta
-                }
-            }
-        };
-
-        let cursor = cursor?;
-
-        if !visible_row_range.contains(&cursor.row()) {
-            return None;
-        }
-
-        let target_position = target.to_display_point(&snapshot.display_snapshot);
-
-        if !visible_row_range.contains(&target_position.row()) {
-            return None;
-        }
-
-        let target_row_layout =
-            &line_layouts[target_position.row().minus(visible_row_range.start) as usize];
-        let target_column = target_position.column() as usize;
-
-        let target_character_x = target_row_layout.x_for_index(target_column);
-
-        let target_x = target_character_x - scroll_pixel_position.x;
-        let target_y =
-            (target_position.row().as_f32() - scroll_pixel_position.y / line_height) * line_height;
-
-        let origin_x = line_layouts[cursor.row().minus(visible_row_range.start) as usize]
-            .x_for_index(cursor.column() as usize);
-        let origin_y =
-            (cursor.row().as_f32() - scroll_pixel_position.y / line_height) * line_height;
-
-        let delta = 1.0 - (-10.0 * delta).exp2();
-
-        let x = origin_x + (target_x - origin_x) * delta;
-        let y = origin_y + (target_y - origin_y) * delta;
-
-        Some(EditPredictionMoveState {
-            delta,
-            position: point(x, y),
-        })
-    }
-}
-
-pub(crate) struct EditPredictionMoveState {
-    delta: f32,
-    position: gpui::Point<Pixels>,
-}
-
-impl EditPredictionMoveState {
-    pub fn is_animation_completed(&self) -> bool {
-        self.delta >= 1.
-    }
-}
-
 #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)]
 struct EditorActionId(usize);
 
@@ -1786,6 +1505,15 @@ impl Editor {
     }
 
     fn key_context(&self, window: &Window, cx: &App) -> KeyContext {
+        self.key_context_internal(self.has_active_inline_completion(), window, cx)
+    }
+
+    fn key_context_internal(
+        &self,
+        has_active_edit_prediction: bool,
+        window: &Window,
+        cx: &App,
+    ) -> KeyContext {
         let mut key_context = KeyContext::new_with_defaults();
         key_context.add("Editor");
         let mode = match self.mode {
@@ -1836,10 +1564,9 @@ impl Editor {
             key_context.set("extension", extension.to_string());
         }
 
-        if self.has_active_inline_completion() {
+        if has_active_edit_prediction {
             key_context.add("copilot_suggestion");
             key_context.add(EDIT_PREDICTION_KEY_CONTEXT);
-
             if showing_completions || self.edit_prediction_requires_modifier() {
                 key_context.add(EDIT_PREDICTION_REQUIRES_MODIFIER_KEY_CONTEXT);
             }
@@ -1857,12 +1584,10 @@ impl Editor {
         window: &Window,
         cx: &App,
     ) -> AcceptEditPredictionBinding {
-        let mut context = self.key_context(window, cx);
-        context.add(EDIT_PREDICTION_KEY_CONTEXT);
-
+        let key_context = self.key_context_internal(true, window, cx);
         AcceptEditPredictionBinding(
             window
-                .bindings_for_action_in_context(&AcceptEditPrediction, context)
+                .bindings_for_action_in_context(&AcceptEditPrediction, key_context)
                 .into_iter()
                 .rev()
                 .next(),
@@ -5067,6 +4792,15 @@ impl Editor {
         self.snippet_stack.is_empty() && self.edit_predictions_enabled()
     }
 
+    pub fn edit_prediction_preview_is_active(&self) -> bool {
+        match self.edit_prediction_preview {
+            EditPredictionPreview::Inactive => false,
+            EditPredictionPreview::Active { .. } => {
+                self.edit_prediction_requires_modifier() || self.has_visible_completions_menu()
+            }
+        }
+    }
+
     pub fn inline_completions_enabled(&self, cx: &App) -> bool {
         let cursor = self.selections.newest_anchor().head();
         if let Some((buffer, cursor_position)) =
@@ -5232,10 +4966,40 @@ impl Editor {
         match &active_inline_completion.completion {
             InlineCompletion::Move { target, .. } => {
                 let target = *target;
-                // Note that this is also done in vim's handler of the Tab action.
-                self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| {
-                    selections.select_anchor_ranges([target..target]);
-                });
+
+                if let Some(position_map) = &self.last_position_map {
+                    if position_map
+                        .visible_row_range
+                        .contains(&target.to_display_point(&position_map.snapshot).row())
+                        || !self.edit_prediction_preview_is_active()
+                    {
+                        // Note that this is also done in vim's handler of the Tab action.
+                        self.change_selections(
+                            Some(Autoscroll::newest()),
+                            window,
+                            cx,
+                            |selections| {
+                                selections.select_anchor_ranges([target..target]);
+                            },
+                        );
+                        self.clear_row_highlights::<EditPredictionPreview>();
+
+                        self.edit_prediction_preview = EditPredictionPreview::Active {
+                            previous_scroll_position: None,
+                        };
+                    } else {
+                        self.edit_prediction_preview = EditPredictionPreview::Active {
+                            previous_scroll_position: Some(position_map.snapshot.scroll_anchor),
+                        };
+                        self.highlight_rows::<EditPredictionPreview>(
+                            target..target,
+                            cx.theme().colors().editor_highlighted_line_background,
+                            true,
+                            cx,
+                        );
+                        self.request_autoscroll(Autoscroll::fit(), cx);
+                    }
+                }
             }
             InlineCompletion::Edit { edits, .. } => {
                 if let Some(provider) = self.edit_prediction_provider() {
@@ -5403,7 +5167,7 @@ impl Editor {
     /// like we are not previewing and the LSP autocomplete menu is visible
     /// or we are in `when_holding_modifier` mode.
     pub fn edit_prediction_visible_in_cursor_popover(&self, has_completion: bool) -> bool {
-        if self.edit_prediction_preview.is_active()
+        if self.edit_prediction_preview_is_active()
             || !self.show_edit_predictions_in_menu()
             || !self.edit_predictions_enabled()
         {
@@ -5425,7 +5189,7 @@ impl Editor {
         cx: &mut Context<Self>,
     ) {
         if self.show_edit_predictions_in_menu() {
-            self.update_edit_prediction_preview(&modifiers, position_map, window, cx);
+            self.update_edit_prediction_preview(&modifiers, window, cx);
         }
 
         let mouse_position = window.mouse_position();
@@ -5445,7 +5209,6 @@ impl Editor {
     fn update_edit_prediction_preview(
         &mut self,
         modifiers: &Modifiers,
-        position_map: &PositionMap,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -5455,37 +5218,31 @@ impl Editor {
         };
 
         if &accept_keystroke.modifiers == modifiers {
-            let Some(completion) = self.active_inline_completion.as_ref() else {
-                return;
-            };
-
-            if !self.edit_prediction_requires_modifier() && !self.has_visible_completions_menu() {
-                return;
-            }
-
-            let transitioned = self.edit_prediction_preview.start(
-                &completion.completion,
-                &position_map.snapshot,
-                self.selections
-                    .newest_anchor()
-                    .head()
-                    .to_display_point(&position_map.snapshot),
-            );
+            if !self.edit_prediction_preview_is_active() {
+                self.edit_prediction_preview = EditPredictionPreview::Active {
+                    previous_scroll_position: None,
+                };
 
-            if transitioned {
-                self.request_autoscroll(Autoscroll::fit(), cx);
                 self.update_visible_inline_completion(window, cx);
                 cx.notify();
             }
-        } else if self.edit_prediction_preview.end(
-            self.selections
-                .newest_anchor()
-                .head()
-                .to_display_point(&position_map.snapshot),
-            position_map.scroll_pixel_position,
-            window,
-            cx,
-        ) {
+        } else if let EditPredictionPreview::Active {
+            previous_scroll_position,
+        } = self.edit_prediction_preview
+        {
+            if let (Some(previous_scroll_position), Some(position_map)) =
+                (previous_scroll_position, self.last_position_map.as_ref())
+            {
+                self.set_scroll_position(
+                    previous_scroll_position
+                        .scroll_position(&position_map.snapshot.display_snapshot),
+                    window,
+                    cx,
+                );
+            }
+
+            self.edit_prediction_preview = EditPredictionPreview::Inactive;
+            self.clear_row_highlights::<EditPredictionPreview>();
             self.update_visible_inline_completion(window, cx);
             cx.notify();
         }
@@ -5493,7 +5250,7 @@ impl Editor {
 
     fn update_visible_inline_completion(
         &mut self,
-        window: &mut Window,
+        _window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Option<()> {
         let selection = self.selections.newest_anchor();
@@ -5643,15 +5400,6 @@ impl Editor {
             ));
 
         self.stale_inline_completion_in_menu = None;
-        let editor_snapshot = self.snapshot(window, cx);
-        if self.edit_prediction_preview.restart(
-            &completion,
-            &editor_snapshot,
-            cursor.to_display_point(&editor_snapshot),
-        ) {
-            self.request_autoscroll(Autoscroll::fit(), cx);
-        }
-
         self.active_inline_completion = Some(InlineCompletionState {
             inlay_ids,
             completion,
@@ -5879,7 +5627,7 @@ impl Editor {
     }
 
     pub fn context_menu_visible(&self) -> bool {
-        !self.edit_prediction_preview.is_active()
+        !self.edit_prediction_preview_is_active()
             && self
                 .context_menu
                 .borrow()
@@ -5990,12 +5738,12 @@ impl Editor {
                                     Icon::new(IconName::ZedPredictUp)
                                 },
                             )
-                            .child(Label::new("Hold"))
+                            .child(Label::new("Hold").size(LabelSize::Small))
                             .children(ui::render_modifiers(
                                 &accept_keystroke.modifiers,
                                 PlatformStyle::platform(),
                                 Some(Color::Default),
-                                None,
+                                Some(IconSize::Small.rems().into()),
                                 true,
                             ))
                             .into_any(),
@@ -14056,23 +13804,6 @@ impl Editor {
         }
     }
 
-    pub fn previewing_edit_prediction_move(
-        &mut self,
-    ) -> Option<(Anchor, &mut EditPredictionPreview)> {
-        if !self.edit_prediction_preview.is_active() {
-            return None;
-        };
-
-        self.active_inline_completion
-            .as_ref()
-            .and_then(|completion| match completion.completion {
-                InlineCompletion::Move { target, .. } => {
-                    Some((target, &mut self.edit_prediction_preview))
-                }
-                _ => None,
-            })
-    }
-
     pub fn show_local_cursors(&self, window: &mut Window, cx: &mut App) -> bool {
         (self.read_only(cx) || self.blink_manager.read(cx).visible())
             && self.focus_handle.is_focused(window)
@@ -14905,7 +14636,7 @@ impl Editor {
     }
 
     pub fn has_visible_completions_menu(&self) -> bool {
-        !self.edit_prediction_preview.is_active()
+        !self.edit_prediction_preview_is_active()
             && self.context_menu.borrow().as_ref().map_or(false, |menu| {
                 menu.visible() && matches!(menu, CodeContextMenu::Completions(_))
             })

crates/editor/src/element.rs 🔗

@@ -16,12 +16,12 @@ use crate::{
     mouse_context_menu::{self, MenuPosition, MouseContextMenu},
     scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair},
     AcceptEditPrediction, BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayPoint,
-    DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode,
-    EditPredictionPreview, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
-    ExpandExcerpts, FocusedBlock, GoToHunk, GoToPrevHunk, GutterDimensions, HalfPageDown,
-    HalfPageUp, HandleInput, HoveredCursor, InlineCompletion, JumpData, LineDown, LineUp,
-    OpenExcerpts, PageDown, PageUp, Point, RevertSelectedHunks, RowExt, RowRangeExt, SelectPhase,
-    Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, CURSORS_VISIBLE_FOR,
+    DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
+    EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk,
+    GoToPrevHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
+    InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point,
+    RevertSelectedHunks, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap,
+    StickyHeaderExcerpt, ToPoint, ToggleFold, CURSORS_VISIBLE_FOR,
     EDIT_PREDICTION_REQUIRES_MODIFIER_KEY_CONTEXT, FILE_HEADER_HEIGHT,
     GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
 };
@@ -1116,7 +1116,6 @@ impl EditorElement {
         em_width: Pixels,
         em_advance: Pixels,
         autoscroll_containing_element: bool,
-        newest_selection_head: Option<DisplayPoint>,
         window: &mut Window,
         cx: &mut App,
     ) -> Vec<CursorLayout> {
@@ -1124,29 +1123,7 @@ impl EditorElement {
         let cursor_layouts = self.editor.update(cx, |editor, cx| {
             let mut cursors = Vec::new();
 
-            let previewing_move =
-                if let Some((target, preview)) = editor.previewing_edit_prediction_move() {
-                    cursors.extend(self.layout_edit_prediction_preview_cursor(
-                        snapshot,
-                        visible_display_row_range.clone(),
-                        line_layouts,
-                        content_origin,
-                        scroll_pixel_position,
-                        line_height,
-                        em_advance,
-                        preview,
-                        target,
-                        newest_selection_head,
-                        window,
-                        cx,
-                    ));
-
-                    true
-                } else {
-                    false
-                };
-
-            let show_local_cursors = !previewing_move && editor.show_local_cursors(window, cx);
+            let show_local_cursors = editor.show_local_cursors(window, cx);
 
             for (player_color, selections) in selections {
                 for selection in selections {
@@ -1288,50 +1265,6 @@ impl EditorElement {
         cursor_layouts
     }
 
-    #[allow(clippy::too_many_arguments)]
-    fn layout_edit_prediction_preview_cursor(
-        &self,
-        snapshot: &EditorSnapshot,
-        visible_row_range: Range<DisplayRow>,
-        line_layouts: &[LineWithInvisibles],
-        content_origin: gpui::Point<Pixels>,
-        scroll_pixel_position: gpui::Point<Pixels>,
-        line_height: Pixels,
-        em_advance: Pixels,
-        preview: &mut EditPredictionPreview,
-        target: Anchor,
-        cursor: Option<DisplayPoint>,
-        window: &mut Window,
-        cx: &mut App,
-    ) -> Option<CursorLayout> {
-        let state = preview.move_state(
-            snapshot,
-            visible_row_range,
-            line_layouts,
-            scroll_pixel_position,
-            line_height,
-            target,
-            cursor,
-        )?;
-
-        if !state.is_animation_completed() {
-            window.request_animation_frame();
-        }
-
-        let mut cursor = CursorLayout {
-            color: self.style.local_player.cursor,
-            block_width: em_advance,
-            origin: state.position,
-            line_height,
-            shape: CursorShape::Bar,
-            block_text: None,
-            cursor_name: None,
-        };
-
-        cursor.layout(content_origin, None, window, cx);
-        Some(cursor)
-    }
-
     fn layout_scrollbars(
         &self,
         snapshot: &EditorSnapshot,
@@ -3607,6 +3540,7 @@ impl EditorElement {
     fn layout_edit_prediction_popover(
         &self,
         text_bounds: &Bounds<Pixels>,
+        content_origin: gpui::Point<Pixels>,
         editor_snapshot: &EditorSnapshot,
         visible_row_range: Range<DisplayRow>,
         scroll_top: f32,
@@ -3631,61 +3565,118 @@ impl EditorElement {
         }
 
         // Adjust text origin for horizontal scrolling (in some cases here)
-        let start_point =
-            text_bounds.origin - gpui::Point::new(scroll_pixel_position.x, Pixels(0.0));
+        let start_point = content_origin - gpui::Point::new(scroll_pixel_position.x, Pixels(0.0));
 
         // Clamp left offset after extreme scrollings
         let clamp_start = |point: gpui::Point<Pixels>| gpui::Point {
-            x: point.x.max(text_bounds.origin.x),
+            x: point.x.max(content_origin.x),
             y: point.y,
         };
 
         match &active_inline_completion.completion {
             InlineCompletion::Move { target, .. } => {
-                if editor.edit_prediction_requires_modifier() {
-                    let cursor_position =
-                        target.to_display_point(&editor_snapshot.display_snapshot);
+                let target_display_point = target.to_display_point(editor_snapshot);
 
-                    if !editor.edit_prediction_preview.is_active_settled()
-                        || !visible_row_range.contains(&cursor_position.row())
-                    {
+                if editor.edit_prediction_requires_modifier() {
+                    if !editor.edit_prediction_preview_is_active() {
                         return None;
                     }
 
-                    let accept_keybind = editor.accept_edit_prediction_keybind(window, cx);
-                    let accept_keystroke = accept_keybind.keystroke()?;
+                    if target_display_point.row() < visible_row_range.start {
+                        let mut element = inline_completion_accept_indicator(
+                            "Scroll",
+                            Some(IconName::ZedPredictUp),
+                            editor,
+                            window,
+                            cx,
+                        )?
+                        .into_any();
 
-                    let mut element = div()
-                        .px_2()
-                        .py_1()
-                        .elevation_2(cx)
-                        .border_color(cx.theme().colors().border)
-                        .rounded_br(px(0.))
-                        .child(Label::new(accept_keystroke.key.clone()).buffer_font(cx))
+                        element.layout_as_root(AvailableSpace::min_size(), window, cx);
+
+                        let cursor = newest_selection_head?;
+                        let cursor_row_layout = line_layouts
+                            .get(cursor.row().minus(visible_row_range.start) as usize)?;
+                        let cursor_column = cursor.column() as usize;
+
+                        let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
+
+                        const PADDING_Y: Pixels = px(24.);
+
+                        let origin = start_point + point(cursor_character_x, PADDING_Y);
+
+                        element.prepaint_at(origin, window, cx);
+                        return Some(element);
+                    } else if target_display_point.row() >= visible_row_range.end {
+                        let mut element = inline_completion_accept_indicator(
+                            "Scroll",
+                            Some(IconName::ZedPredictDown),
+                            editor,
+                            window,
+                            cx,
+                        )?
                         .into_any();
 
-                    let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
+                        let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
 
-                    let cursor_row_layout = &line_layouts
-                        [cursor_position.row().minus(visible_row_range.start) as usize];
-                    let cursor_column = cursor_position.column() as usize;
+                        let cursor = newest_selection_head?;
+                        let cursor_row_layout = line_layouts
+                            .get(cursor.row().minus(visible_row_range.start) as usize)?;
+                        let cursor_column = cursor.column() as usize;
 
-                    let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
-                    let target_y = (cursor_position.row().as_f32()
-                        - scroll_pixel_position.y / line_height)
-                        * line_height;
+                        let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
+                        const PADDING_Y: Pixels = px(24.);
 
-                    let offset = point(
-                        cursor_character_x - size.width,
-                        target_y - size.height - PADDING_Y,
-                    );
+                        let origin = start_point
+                            + point(
+                                cursor_character_x,
+                                text_bounds.size.height - size.height - PADDING_Y,
+                            );
 
-                    element.prepaint_at(text_bounds.origin + offset, window, cx);
+                        element.prepaint_at(origin, window, cx);
+                        return Some(element);
+                    } else {
+                        const POLE_WIDTH: Pixels = px(2.);
+
+                        let mut element = v_flex()
+                            .child(
+                                inline_completion_accept_indicator(
+                                    "Jump", None, editor, window, cx,
+                                )?
+                                .rounded_br(px(0.)),
+                            )
+                            .child(
+                                div()
+                                    .w(POLE_WIDTH)
+                                    .bg(cx.theme().colors().border)
+                                    .h(line_height),
+                            )
+                            .items_end()
+                            .into_any();
+
+                        let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
+
+                        let line_layout =
+                            line_layouts
+                                .get(target_display_point.row().minus(visible_row_range.start)
+                                    as usize)?;
+                        let target_column = target_display_point.column() as usize;
+
+                        let target_x = line_layout.x_for_index(target_column);
+                        let target_y = (target_display_point.row().as_f32() * line_height)
+                            - scroll_pixel_position.y;
+
+                        let origin = clamp_start(
+                            start_point + point(target_x, target_y)
+                                - point(size.width - POLE_WIDTH, size.height - line_height),
+                        );
 
-                    return Some(element);
+                        element.prepaint_at(origin, window, cx);
+
+                        return Some(element);
+                    }
                 }
 
-                let target_display_point = target.to_display_point(editor_snapshot);
                 if target_display_point.row().as_f32() < scroll_top {
                     let mut element = inline_completion_accept_indicator(
                         "Jump to Edit",
@@ -3693,7 +3684,8 @@ impl EditorElement {
                         editor,
                         window,
                         cx,
-                    )?;
+                    )?
+                    .into_any();
                     let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
                     let offset = point((text_bounds.size.width - size.width) / 2., PADDING_Y);
 
@@ -3706,7 +3698,8 @@ impl EditorElement {
                         editor,
                         window,
                         cx,
-                    )?;
+                    )?
+                    .into_any();
                     let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
                     let offset = point(
                         (text_bounds.size.width - size.width) / 2.,
@@ -3722,7 +3715,8 @@ impl EditorElement {
                         editor,
                         window,
                         cx,
-                    )?;
+                    )?
+                    .into_any();
                     let target_line_end = DisplayPoint::new(
                         target_display_point.row(),
                         editor_snapshot.line_len(target_display_point.row()),
@@ -3782,7 +3776,8 @@ impl EditorElement {
                             Some((
                                 inline_completion_accept_indicator(
                                     "Accept", None, editor, window, cx,
-                                )?,
+                                )?
+                                .into_any(),
                                 editor.display_to_pixel_point(
                                     target_line_end,
                                     editor_snapshot,
@@ -5805,7 +5800,7 @@ fn inline_completion_accept_indicator(
     editor: &Editor,
     window: &mut Window,
     cx: &App,
-) -> Option<AnyElement> {
+) -> Option<Div> {
     let accept_binding = editor.accept_edit_prediction_keybind(window, cx);
     let accept_keystroke = accept_binding.keystroke()?;
 
@@ -5815,7 +5810,7 @@ fn inline_completion_accept_indicator(
         .text_size(TextSize::XSmall.rems(cx))
         .text_color(cx.theme().colors().text)
         .gap_1()
-        .when(!editor.edit_prediction_preview.is_active(), |parent| {
+        .when(!editor.edit_prediction_preview_is_active(), |parent| {
             parent.children(ui::render_modifiers(
                 &accept_keystroke.modifiers,
                 PlatformStyle::platform(),
@@ -5826,33 +5821,44 @@ fn inline_completion_accept_indicator(
         })
         .child(accept_keystroke.key.clone());
 
-    let padding_right = if icon.is_some() { px(4.) } else { px(8.) };
-    let accent_color = cx.theme().colors().text_accent;
-    let editor_bg_color = cx.theme().colors().editor_background;
-    let bg_color = editor_bg_color.blend(accent_color.opacity(0.2));
+    let result = h_flex()
+        .gap_1()
+        .border_1()
+        .rounded_md()
+        .shadow_sm()
+        .child(accept_key)
+        .child(Label::new(label).size(LabelSize::Small))
+        .when_some(icon, |element, icon| {
+            element.child(
+                div()
+                    .mt(px(1.5))
+                    .child(Icon::new(icon).size(IconSize::Small)),
+            )
+        });
+
+    let colors = cx.theme().colors();
 
-    Some(
-        h_flex()
+    let result = if editor.edit_prediction_requires_modifier() {
+        result
+            .py_1()
+            .px_2()
+            .elevation_2(cx)
+            .border_color(colors.border)
+    } else {
+        let accent_color = colors.text_accent;
+        let editor_bg_color = colors.editor_background;
+        let bg_color = editor_bg_color.blend(accent_color.opacity(0.2));
+        let padding_right = if icon.is_some() { px(4.) } else { px(8.) };
+
+        result
+            .bg(bg_color)
+            .border_color(colors.text_accent.opacity(0.8))
             .py_0p5()
             .pl_1()
             .pr(padding_right)
-            .gap_1()
-            .bg(bg_color)
-            .border_1()
-            .border_color(cx.theme().colors().text_accent.opacity(0.8))
-            .rounded_md()
-            .shadow_sm()
-            .child(accept_key)
-            .child(Label::new(label).size(LabelSize::Small))
-            .when_some(icon, |element, icon| {
-                element.child(
-                    div()
-                        .mt(px(1.5))
-                        .child(Icon::new(icon).size(IconSize::Small)),
-                )
-            })
-            .into_any(),
-    )
+    };
+
+    Some(result)
 }
 
 pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
@@ -7357,7 +7363,7 @@ impl Element for EditorElement {
                     let visible_row_range = start_row..end_row;
                     let non_visible_cursors = cursors
                         .iter()
-                        .any(move |c| !visible_row_range.contains(&c.0.row()));
+                        .any(|c| !visible_row_range.contains(&c.0.row()));
 
                     let visible_cursors = self.layout_visible_cursors(
                         &snapshot,
@@ -7373,7 +7379,6 @@ impl Element for EditorElement {
                         em_width,
                         em_advance,
                         autoscroll_containing_element,
-                        newest_selection_head,
                         window,
                         cx,
                     );
@@ -7527,6 +7532,7 @@ impl Element for EditorElement {
 
                     let inline_completion_popover = self.layout_edit_prediction_popover(
                         &text_hitbox.bounds,
+                        content_origin,
                         &snapshot,
                         start_row..end_row,
                         scroll_position.y,
@@ -7598,6 +7604,7 @@ impl Element for EditorElement {
 
                     let position_map = Rc::new(PositionMap {
                         size: bounds.size,
+                        visible_row_range,
                         scroll_pixel_position,
                         scroll_max,
                         line_layouts,
@@ -7991,6 +7998,7 @@ pub(crate) struct PositionMap {
     pub scroll_max: gpui::Point<f32>,
     pub em_width: Pixels,
     pub em_advance: Pixels,
+    pub visible_row_range: Range<DisplayRow>,
     pub line_layouts: Vec<LineWithInvisibles>,
     pub snapshot: EditorSnapshot,
     pub text_hitbox: Hitbox,

crates/editor/src/scroll.rs 🔗

@@ -3,6 +3,7 @@ pub(crate) mod autoscroll;
 pub(crate) mod scroll_amount;
 
 use crate::editor_settings::{ScrollBeyondLastLine, ScrollbarAxes};
+use crate::EditPredictionPreview;
 use crate::{
     display_map::{DisplaySnapshot, ToDisplayPoint},
     hover_popover::hide_hover,
@@ -495,6 +496,15 @@ impl Editor {
         hide_hover(self, cx);
         let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
 
+        if let EditPredictionPreview::Active {
+            previous_scroll_position,
+        } = &mut self.edit_prediction_preview
+        {
+            if !autoscroll {
+                previous_scroll_position.take();
+            }
+        }
+
         self.scroll_manager.set_scroll_position(
             scroll_position,
             &display_map,

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

@@ -145,29 +145,6 @@ impl Editor {
                 target_top = newest_selection_top;
                 target_bottom = newest_selection_top + 1.;
             }
-
-            if self.edit_prediction_preview.is_active() {
-                if let Some(completion) = self.active_inline_completion.as_ref() {
-                    match completion.completion {
-                        crate::InlineCompletion::Edit { .. } => {}
-                        crate::InlineCompletion::Move { target, .. } => {
-                            let target_row = target.to_display_point(&display_map).row().as_f32();
-
-                            if target_row < target_top {
-                                target_top = target_row;
-                            } else if target_row >= target_bottom {
-                                target_bottom = target_row + 1.;
-                            }
-
-                            let selections_fit = target_bottom - target_top <= visible_lines;
-                            if !selections_fit {
-                                target_top = target_row;
-                                target_bottom = target_row + 1.;
-                            }
-                        }
-                    }
-                }
-            }
         }
 
         let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) {

crates/ui/src/traits/styled_ext.rs 🔗

@@ -3,7 +3,7 @@ use gpui::{hsla, App, Styled};
 use crate::prelude::*;
 use crate::ElevationIndex;
 
-fn elevated<E: Styled>(this: E, cx: &mut App, index: ElevationIndex) -> E {
+fn elevated<E: Styled>(this: E, cx: &App, index: ElevationIndex) -> E {
     this.bg(cx.theme().colors().elevated_surface_background)
         .rounded_lg()
         .border_1()
@@ -54,7 +54,7 @@ pub trait StyledExt: Styled + Sized {
     /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
     ///
     /// Examples: Notifications, Palettes, Detached/Floating Windows, Detached/Floating Panels
-    fn elevation_2(self, cx: &mut App) -> Self {
+    fn elevation_2(self, cx: &App) -> Self {
         elevated(self, cx, ElevationIndex::ElevatedSurface)
     }