From 64de6bd2a8bf65fbe8e922a36a720caf29a6da17 Mon Sep 17 00:00:00 2001 From: neunato Date: Wed, 9 Apr 2025 19:50:14 +0200 Subject: [PATCH] Don't scroll the editor on select all matches (#28435) Part of https://github.com/zed-industries/zed/issues/9309 Release Notes: - Improved scroll behavior of `editor: select all matches` --------- Co-authored-by: Kirill Bulatov --- crates/editor/src/editor.rs | 61 +++++++------------------------ crates/editor/src/editor_tests.rs | 31 ++++++++++++++++ 2 files changed, 45 insertions(+), 47 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b255cbbdcf7dbb278cf524f8efc1281acc511a63..0fc53debe9418cb1c43d173f98e1bacd6bd65241 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -11573,7 +11573,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - this.unfold_ranges(&[range.clone()], false, true, cx); + this.unfold_ranges(&[range.clone()], false, auto_scroll.is_some(), cx); this.change_selections(auto_scroll, window, cx, |s| { if replace_newest { s.delete(s.newest_anchor().id); @@ -11748,16 +11748,21 @@ impl Editor { return Ok(()); } - let mut new_selections = self.selections.all::(cx); + let mut new_selections = Vec::new(); + let reversed = self.selections.oldest::(cx).reversed; let buffer = &display_map.buffer_snapshot; let query_matches = select_next_state .query .stream_find_iter(buffer.bytes_in_range(0..buffer.len())); - for query_match in query_matches { - let query_match = query_match.unwrap(); // can only fail due to I/O - let offset_range = query_match.start()..query_match.end(); + for query_match in query_matches.into_iter() { + let query_match = query_match.context("query match for select all action")?; // can only fail due to I/O + let offset_range = if reversed { + query_match.end()..query_match.start() + } else { + query_match.start()..query_match.end() + }; let display_range = offset_range.start.to_display_point(&display_map) ..offset_range.end.to_display_point(&display_map); @@ -11765,52 +11770,14 @@ impl Editor { || (!movement::is_inside_word(&display_map, display_range.start) && !movement::is_inside_word(&display_map, display_range.end)) { - self.selections.change_with(cx, |selections| { - new_selections.push(Selection { - id: selections.new_selection_id(), - start: offset_range.start, - end: offset_range.end, - reversed: false, - goal: SelectionGoal::None, - }); - }); + new_selections.push(offset_range.start..offset_range.end); } } - new_selections.sort_by_key(|selection| selection.start); - let mut ix = 0; - while ix + 1 < new_selections.len() { - let current_selection = &new_selections[ix]; - let next_selection = &new_selections[ix + 1]; - if current_selection.range().overlaps(&next_selection.range()) { - if current_selection.id < next_selection.id { - new_selections.remove(ix + 1); - } else { - new_selections.remove(ix); - } - } else { - ix += 1; - } - } - - let reversed = self.selections.oldest::(cx).reversed; - - for selection in new_selections.iter_mut() { - selection.reversed = reversed; - } - select_next_state.done = true; - self.unfold_ranges( - &new_selections - .iter() - .map(|selection| selection.range()) - .collect::>(), - false, - false, - cx, - ); - self.change_selections(Some(Autoscroll::fit()), window, cx, |selections| { - selections.select(new_selections) + self.unfold_ranges(&new_selections.clone(), false, false, cx); + self.change_selections(None, window, cx, |selections| { + selections.select_ranges(new_selections) }); Ok(()) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 68eff95163b8b7a5be9717f619ac3d44f5462113..d7bb0eb33e631a8d66388e24650f168b0cf982fe 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5842,6 +5842,37 @@ async fn test_select_all_matches(cx: &mut TestAppContext) { cx.assert_editor_state("abc\n« ˇ»abc\nabc"); } +#[gpui::test] +async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let large_body_1 = "\nd".repeat(200); + let large_body_2 = "\ne".repeat(200); + + cx.set_state(&format!( + "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc" + )); + let initial_scroll_position = cx.update_editor(|editor, _, cx| { + let scroll_position = editor.scroll_position(cx); + assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it"); + scroll_position + }); + + cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx)) + .unwrap(); + cx.assert_editor_state(&format!( + "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc" + )); + let scroll_position_after_selection = + cx.update_editor(|editor, _, cx| editor.scroll_position(cx)); + assert_eq!( + initial_scroll_position, scroll_position_after_selection, + "Scroll position should not change after selecting all matches" + ); +} + #[gpui::test] async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) { init_test(cx, |_| {});