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" + ); + }); + }); + } }