diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs
index 0de4e96bde..cdd2aa119a 100644
--- a/crates/ui/src/input/element.rs
+++ b/crates/ui/src/input/element.rs
@@ -527,9 +527,17 @@ impl TextElement {
_ => 0.85,
} * line_height;
+ // Match the caret to the deferred scroll target (applied below) that
+ // the text paints at; otherwise the caret follows the cursor-scroll
+ // while the text uses the deferred offset, flashing it mid-field.
+ let cursor_scroll_x = state
+ .deferred_scroll_offset
+ .map(|offset| offset.x)
+ .unwrap_or(scroll_offset.x);
+
// For Right alignment, clamp cursor within the right edge of bounds so it
// stays visible without having to shift the text via scroll_offset.
- let cursor_x = bounds.left() + cursor_pos.x + line_number_width + scroll_offset.x;
+ let cursor_x = bounds.left() + cursor_pos.x + line_number_width + cursor_scroll_x;
let cursor_x = if last_layout.text_align == TextAlign::Right {
cursor_x.min(bounds.right() - CURSOR_WIDTH)
} else {
diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs
index 033fcf2318..b40f009338 100644
--- a/crates/ui/src/input/state.rs
+++ b/crates/ui/src/input/state.rs
@@ -777,7 +777,10 @@ impl InputState {
/// Set the text of the input field.
///
- /// And the selection_range will be reset to 0..0.
+ /// For single-line inputs the caret is placed at the end of the text while
+ /// the view is scrolled back to the start, so a long value shows its
+ /// beginning instead of its tail (matching HTML ``). Multi-line
+ /// inputs reset the selection to `0..0`.
pub fn set_value(
&mut self,
value: impl Into,
@@ -790,9 +793,11 @@ impl InputState {
self.history.ignore = false;
self.emit_events = true;
- // Ensure cursor to start when set text
+ // Place the caret at the end for single-line inputs (like HTML
+ // ``); multi-line inputs reset the selection to the start.
if self.mode.is_single_line() {
- self.selected_range = (self.text.len()..self.text.len()).into();
+ let end = self.text.len();
+ self.selected_range = (end..end).into();
} else {
self.selected_range.clear();
}
@@ -802,8 +807,13 @@ impl InputState {
self.lsp.reset();
}
- // Move scroll to top
+ // Move scroll to the start. For single-line the caret is at the end, so
+ // override the cursor-follow scroll for the next painted frame to keep
+ // the start visible; the deferred offset is consumed during that paint.
self.scroll_handle.set_offset(point(px(0.), px(0.)));
+ if self.mode.is_single_line() {
+ self.deferred_scroll_offset = Some(point(px(0.), px(0.)));
+ }
self.history.clear();
cx.notify();
@@ -3515,4 +3525,54 @@ ORDER BY id
});
});
}
+
+ /// After `set_value` on a single-line input the caret sits at the end (like
+ /// HTML ``), yet the view is scrolled back to the start so a long
+ /// value shows its beginning instead of its tail.
+ #[gpui::test]
+ fn test_set_value_single_line_caret_at_end_view_at_start(cx: &mut TestAppContext) {
+ let input_view = InputView::build(cx, |state| state);
+ let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
+ let input = input_view.input;
+
+ // Long enough to overflow any reasonable single-line input width.
+ let value = format!("https://example.com/v1/users?{}", "x=1&".repeat(120));
+ let len = value.len();
+
+ // Right after `set_value`, before the next paint consumes the deferred
+ // offset: caret is at the end, and the view is forced back to the start.
+ cx.update(|window, cx| {
+ input.update(cx, |state, cx| {
+ state.set_value(value.clone(), window, cx);
+
+ assert_eq!(
+ state.selected_range,
+ Selection::new(len, len),
+ "single-line caret should be at the end after set_value"
+ );
+ assert_eq!(
+ state.deferred_scroll_offset,
+ Some(point(px(0.), px(0.))),
+ "the view should be forced back to the start"
+ );
+ });
+ });
+
+ // After a paint, the steady-state view stays at the start (x == 0) even
+ // though the caret is at the far end.
+ cx.run_until_parked();
+ cx.update(|_, cx| {
+ input.read_with(cx, |state, _| {
+ assert!(
+ state.scroll_size.width > state.input_bounds.size.width,
+ "value must overflow the input width or this test is vacuous"
+ );
+ assert_eq!(
+ state.scroll_handle.offset().x,
+ px(0.),
+ "long value should display from its start, not its tail"
+ );
+ });
+ });
+ }
}