diff --git a/crates/base/src/input/base/element.rs b/crates/base/src/input/base/element.rs index cb1959c406..cf7bdba68b 100644 --- a/crates/base/src/input/base/element.rs +++ b/crates/base/src/input/base/element.rs @@ -535,6 +535,7 @@ impl TextElement { state: &InputBaseState, line_height: Pixels, viewport_height: Pixels, + window: &Window, ) -> Pixels { if state.is_single_line() { return px(0.); @@ -615,16 +616,18 @@ impl TextElement { }, }; - // Clamp to the text's height as this layout wraps it, so the frame shows the - // offset that persists. Inline-completion ghost lines are left out: they only - // ever add height, so this range lies inside the one the paint persists. + // Clamp to the height this layout scrolls over, so the frame shows the offset + // that persists: the text as it wraps, and past its last row the larger of the + // empty bottom and an inline completion's ghost lines, as the paint sizes it. + let ghost_lines_height = line_height * Self::ghost_line_count(state, window) as f32; let content_height = line_height * state.display_map.wrap_row_count() + empty_bottom_height( state.is_code_editor(), state.scroll_beyond_last_line, viewport_height, line_height, - ); + ) + .max(ghost_lines_height); let min_top = (viewport_height - content_height).min(px(0.)); scroll_top.clamp(min_top, px(0.)) } @@ -1250,6 +1253,19 @@ impl TextElement { Some(Rc::new(WhitespaceIndicators { space, tab })) } + /// How many lines an inline completion adds under the caret's line: those of its + /// text past the first, shown while the input has the focus. Counted without + /// shaping them, for the scroll resolved before any line is laid out. + fn ghost_line_count(state: &InputBaseState, window: &Window) -> usize { + if !state.focus_handle.is_focused(window) { + return 0; + } + state + .extras + .inline_completion_item() + .map_or(0, |item| item.insert_text.split('\n').count() - 1) + } + /// Compute inline completion ghost lines for rendering. /// /// Returns (first_line, ghost_lines) where: @@ -1747,7 +1763,7 @@ impl TextElement { .iter() .any(|span| !cache.widths.contains_key(span.token())) }); - let scroll_top = Self::resolve_scroll_top(state, line_height, viewport); + let scroll_top = Self::resolve_scroll_top(state, line_height, viewport, window); let (visible, _, _) = self.calculate_visible_range(state, line_height, viewport, scroll_top); let start = state.text.line_start_offset(visible.start); @@ -2664,7 +2680,7 @@ impl Element for TextElement { ); let state = self.state.read(cx); - let scroll_top = Self::resolve_scroll_top(state, line_height, bounds.size.height); + let scroll_top = Self::resolve_scroll_top(state, line_height, bounds.size.height, window); let (visible_range, visible_buffer_lines, visible_top) = self.calculate_visible_range(state, line_height, bounds.size.height, scroll_top); let visible_start_offset = state.text.line_start_offset(visible_range.start); @@ -3694,6 +3710,53 @@ mod tests { (editor.unwrap(), window) } + /// Inline-completion ghost lines under the caret's last line add to the + /// scrollable height, and a scroll down to them holds from frame to frame. + #[gpui::test] + fn ghost_lines_below_the_last_line_can_be_scrolled_to(cx: &mut TestAppContext) { + let text = (0..10) + .map(|n| format!("line {n}")) + .collect::>() + .join("\n"); + let (editor, window) = decoration_editor(cx, &text, false); + let mut cx = VisualTestContext::from_window(window.into(), cx); + cx.update(|window, cx| { + editor.update(cx, |state, cx| { + state.set_cursor_position(lsp_types::Position::new(9, 6), window, cx); + state.extras.inline_completion.item = Some(lsp_types::InlineCompletionItem { + insert_text: format!("()\n{}", ["ghost"; 30].join("\n")), + filter_text: None, + range: None, + command: None, + insert_text_format: None, + }); + }); + window.draw(cx).clear(cx); + let (line_height, scroll_height, viewport) = editor.read_with(cx, |state, _| { + let line_height = state.last_layout.as_ref().unwrap().line_height; + ( + line_height, + state.scroll_size.height, + state.input_bounds.size.height, + ) + }); + assert!( + scroll_height >= line_height * 40., + "the ghost lines count in the scrollable height: {scroll_height:?}" + ); + + // Down to the last ghost line, as the wheel scrolls. + let bottom = viewport - scroll_height; + editor.update(cx, |state, _| { + state.scroll_handle.set_offset(point(px(0.), bottom)); + }); + window.draw(cx).clear(cx); + assert_eq!(editor.read(cx).scroll_handle.offset().y, bottom); + window.draw(cx).clear(cx); + assert_eq!(editor.read(cx).scroll_handle.offset().y, bottom); + }); + } + #[gpui::test] fn horizontal_scroll_is_clamped_before_text_and_caret_layout(cx: &mut TestAppContext) { let (editor, window) = decoration_editor(cx, "short text", false);