Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 69 additions & 6 deletions crates/base/src/input/base/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ impl<M: InputModeKind> TextElement<M> {
state: &InputBaseState<M>,
line_height: Pixels,
viewport_height: Pixels,
window: &Window,
) -> Pixels {
if state.is_single_line() {
return px(0.);
Expand Down Expand Up @@ -615,16 +616,18 @@ impl<M: InputModeKind> TextElement<M> {
},
};

// 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.))
}
Expand Down Expand Up @@ -1250,6 +1253,19 @@ impl<M: InputModeKind> TextElement<M> {
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<M>, 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:
Expand Down Expand Up @@ -1747,7 +1763,7 @@ impl<M: InputModeKind> TextElement<M> {
.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);
Expand Down Expand Up @@ -2664,7 +2680,7 @@ impl<M: InputModeKind> Element for TextElement<M> {
);
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);
Expand Down Expand Up @@ -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::<Vec<_>>()
.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);
Expand Down
Loading