Skip to content

Commit 42e7e0f

Browse files
committed
feat(input): 实现选择时自动滚动以保证光标可见
- select_to 方法中记录之前光标位置,计算移动方向后滚动视图 - 调用 scroll_to 方法滚动以保持光标可见,支持向上和向下移动 - 重构测试辅助代码,新建新代码编辑器窗口及绘制函数 - 调整测试用例,使用新的窗口及绘制函数,提高代码简洁性 - 新增测试用例验证反向选择时,视图自动滚动以确保锚点可见
1 parent 94c408d commit 42e7e0f

1 file changed

Lines changed: 79 additions & 32 deletions

File tree

‎crates/ui/src/input/state.rs‎

Lines changed: 79 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1996,6 +1996,7 @@ impl InputState {
19961996
pub(crate) fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
19971997
self.clear_inline_completion(cx);
19981998

1999+
let previous_cursor = self.cursor();
19992000
let offset = offset.clamp(0, self.text.len());
20002001
if self.selection_reversed {
20012002
self.selected_range.start = offset
@@ -2020,6 +2021,15 @@ impl InputState {
20202021
if self.selected_range.is_empty() {
20212022
self.update_preferred_column();
20222023
}
2024+
let cursor = self.cursor();
2025+
let direction = if cursor < previous_cursor {
2026+
Some(MoveDirection::Up)
2027+
} else if cursor > previous_cursor {
2028+
Some(MoveDirection::Down)
2029+
} else {
2030+
None
2031+
};
2032+
self.scroll_to(cursor, direction, cx);
20232033
cx.notify()
20242034
}
20252035

@@ -2816,45 +2826,32 @@ mod tests {
28162826
use crate::theme::Theme;
28172827
use gpui::{TestAppContext, VisualTestContext};
28182828

2819-
struct InputView {
2820-
input: Entity<InputState>,
2821-
window_handle: gpui::WindowHandle<Root>,
2822-
}
2823-
28242829
/// Helper to create an InputState in a window for testing
2825-
impl InputView {
2826-
pub fn new(cx: &mut TestAppContext) -> Self {
2827-
let mut input: Option<Entity<InputState>> = None;
2828-
2829-
let window = cx.update(|cx| {
2830-
cx.open_window(Default::default(), |window, cx| {
2831-
// Set up the theme first
2832-
cx.set_global(Theme::default());
2833-
// Initialize input keybindings
2834-
super::super::init(cx);
2835-
2836-
input = Some(cx.new(|cx| InputState::new(window, cx).code_editor("sql")));
2837-
2838-
cx.new(|cx| crate::Root::new(input.clone().unwrap(), window, cx))
2839-
})
2840-
.unwrap()
2841-
});
2830+
fn new_code_editor(cx: &mut TestAppContext) -> (Entity<InputState>, &mut VisualTestContext) {
2831+
cx.update(|cx| {
2832+
// Set up the theme first
2833+
cx.set_global(Theme::default());
2834+
// Initialize input keybindings
2835+
super::super::init(cx);
2836+
});
2837+
cx.add_window_view(|window, cx| InputState::new(window, cx).code_editor("sql"))
2838+
}
28422839

2843-
Self {
2844-
input: input.clone().unwrap(),
2845-
window_handle: window,
2846-
}
2847-
}
2840+
fn draw_input(cx: &mut VisualTestContext, input: &Entity<InputState>) {
2841+
cx.draw(
2842+
point(px(0.), px(0.)),
2843+
gpui::size(px(800.), px(240.)),
2844+
|_, _| input.clone().into_any_element(),
2845+
);
28482846
}
28492847

28502848
#[gpui::test]
28512849
fn test_highlighting_preserved_after_fold(cx: &mut TestAppContext) {
28522850
use crate::highlighter::HighlightTheme;
28532851
use crate::input::display_map::FoldRange;
28542852

2855-
let input_view = InputView::new(cx);
2856-
let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
2857-
let input = input_view.input;
2853+
let (input, cx) = new_code_editor(cx);
2854+
let cx: &mut VisualTestContext = cx;
28582855

28592856
// SQL text: fold the SELECT..WHERE block, verify comments keep highlighting.
28602857
// Lines 0-9: SELECT block (fold range 0..9 hides lines 1-8)
@@ -2880,7 +2877,7 @@ ORDER BY id
28802877
state.set_value(text, window, cx);
28812878
});
28822879
});
2883-
cx.run_until_parked();
2880+
draw_input(cx, &input);
28842881

28852882
// Grab styles for "-- Comment 1" (line 11) before folding
28862883
let theme = HighlightTheme::default_dark();
@@ -2911,7 +2908,7 @@ ORDER BY id
29112908
state.display_map.set_folded(0, true);
29122909
});
29132910
});
2914-
cx.run_until_parked();
2911+
draw_input(cx, &input);
29152912

29162913
// Verify fold is active and lines 1-8 are hidden
29172914
cx.update(|_, cx| {
@@ -2964,4 +2961,54 @@ ORDER BY id
29642961
colored_before, colored_after
29652962
);
29662963
}
2964+
2965+
#[gpui::test]
2966+
fn select_to_scrolls_reversed_selection_anchor_into_view(cx: &mut TestAppContext) {
2967+
let (input, cx) = new_code_editor(cx);
2968+
let cx: &mut VisualTestContext = cx;
2969+
let text = (0..120)
2970+
.map(|line| format!("select {line};"))
2971+
.collect::<Vec<_>>()
2972+
.join("\n");
2973+
2974+
cx.update(|window, cx| {
2975+
input.update(cx, |state, cx| state.set_value(text, window, cx));
2976+
});
2977+
draw_input(cx, &input);
2978+
2979+
let bottom_offset =
2980+
cx.update(|_, cx| input.read_with(cx, |state, _| state.text.line_start_offset(100)));
2981+
cx.update(|_, cx| {
2982+
input.update(cx, |state, cx| {
2983+
state.move_to(bottom_offset, Some(MoveDirection::Down), cx);
2984+
});
2985+
});
2986+
draw_input(cx, &input);
2987+
2988+
let before_scroll =
2989+
cx.update(|_, cx| input.read_with(cx, |state, _| state.scroll_offset()));
2990+
let target_offset =
2991+
cx.update(|_, cx| input.read_with(cx, |state, _| state.text.line_start_offset(40)));
2992+
cx.update(|_, cx| {
2993+
input.update(cx, |state, cx| {
2994+
state.select_to(target_offset, cx);
2995+
});
2996+
});
2997+
draw_input(cx, &input);
2998+
2999+
let after_select = cx.update(|_, cx| {
3000+
input.read_with(cx, |state, _| {
3001+
(
3002+
state.cursor(),
3003+
state.selection_reversed,
3004+
state.selected_range,
3005+
state.scroll_offset(),
3006+
)
3007+
})
3008+
});
3009+
assert!(
3010+
after_select.3.y > before_scroll.y,
3011+
"expected upward selection to move viewport up, bottom={bottom_offset}, target={target_offset}, before={before_scroll:?}, state={after_select:?}"
3012+
);
3013+
}
29673014
}

0 commit comments

Comments
 (0)