diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ce9c4215cc356c4f3128ff04cc6d44b8e306b576..30b0a73d37e093bcb456d4fa6cf8e0c2ff98d5ff 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6471,42 +6471,79 @@ impl Editor { } self.select_next_state = Some(select_next_state); - } else if selections.len() == 1 { - let selection = selections.last_mut().unwrap(); - if selection.start == selection.end { - let word_range = movement::surrounding_word( - &display_map, - selection.start.to_display_point(&display_map), - ); - selection.start = word_range.start.to_offset(&display_map, Bias::Left); - selection.end = word_range.end.to_offset(&display_map, Bias::Left); - selection.goal = SelectionGoal::None; - selection.reversed = false; - - let query = buffer - .text_for_range(selection.start..selection.end) - .collect::(); - - let is_empty = query.is_empty(); - let select_state = SelectNextState { - query: AhoCorasick::new(&[query])?, - wordwise: true, - done: is_empty, - }; - select_next_match_ranges( - self, - selection.start..selection.end, - replace_newest, - autoscroll, - cx, - ); - self.select_next_state = Some(select_state); - } else { - let query = buffer - .text_for_range(selection.start..selection.end) - .collect::(); + } else { + let mut only_carets = true; + let mut same_text_selected = true; + let mut selected_text = None; + + let mut selections_iter = selections.iter().peekable(); + while let Some(selection) = selections_iter.next() { + if selection.start != selection.end { + only_carets = false; + } + + if same_text_selected { + if selected_text.is_none() { + selected_text = + Some(buffer.text_for_range(selection.range()).collect::()); + } + + if let Some(next_selection) = selections_iter.peek() { + if next_selection.range().len() == selection.range().len() { + let next_selected_text = buffer + .text_for_range(next_selection.range()) + .collect::(); + if Some(next_selected_text) != selected_text { + same_text_selected = false; + selected_text = None; + } + } else { + same_text_selected = false; + selected_text = None; + } + } + } + } + + if only_carets { + for selection in &mut selections { + let word_range = movement::surrounding_word( + &display_map, + selection.start.to_display_point(&display_map), + ); + selection.start = word_range.start.to_offset(&display_map, Bias::Left); + selection.end = word_range.end.to_offset(&display_map, Bias::Left); + selection.goal = SelectionGoal::None; + selection.reversed = false; + select_next_match_ranges( + self, + selection.start..selection.end, + replace_newest, + autoscroll, + cx, + ); + } + + if selections.len() == 1 { + let selection = selections + .last() + .expect("ensured that there's only one selection"); + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let is_empty = query.is_empty(); + let select_state = SelectNextState { + query: AhoCorasick::new(&[query])?, + wordwise: true, + done: is_empty, + }; + self.select_next_state = Some(select_state); + } else { + self.select_next_state = None; + } + } else if let Some(selected_text) = selected_text { self.select_next_state = Some(SelectNextState { - query: AhoCorasick::new(&[query])?, + query: AhoCorasick::new(&[selected_text])?, wordwise: false, done: false, }); @@ -6610,39 +6647,81 @@ impl Editor { } self.select_prev_state = Some(select_prev_state); - } else if selections.len() == 1 { - let selection = selections.last_mut().unwrap(); - if selection.start == selection.end { - let word_range = movement::surrounding_word( - &display_map, - selection.start.to_display_point(&display_map), + } else { + let mut only_carets = true; + let mut same_text_selected = true; + let mut selected_text = None; + + let mut selections_iter = selections.iter().peekable(); + while let Some(selection) = selections_iter.next() { + if selection.start != selection.end { + only_carets = false; + } + + if same_text_selected { + if selected_text.is_none() { + selected_text = + Some(buffer.text_for_range(selection.range()).collect::()); + } + + if let Some(next_selection) = selections_iter.peek() { + if next_selection.range().len() == selection.range().len() { + let next_selected_text = buffer + .text_for_range(next_selection.range()) + .collect::(); + if Some(next_selected_text) != selected_text { + same_text_selected = false; + selected_text = None; + } + } else { + same_text_selected = false; + selected_text = None; + } + } + } + } + + if only_carets { + for selection in &mut selections { + let word_range = movement::surrounding_word( + &display_map, + selection.start.to_display_point(&display_map), + ); + selection.start = word_range.start.to_offset(&display_map, Bias::Left); + selection.end = word_range.end.to_offset(&display_map, Bias::Left); + selection.goal = SelectionGoal::None; + selection.reversed = false; + } + if selections.len() == 1 { + let selection = selections + .last() + .expect("ensured that there's only one selection"); + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let is_empty = query.is_empty(); + let select_state = SelectNextState { + query: AhoCorasick::new(&[query.chars().rev().collect::()])?, + wordwise: true, + done: is_empty, + }; + self.select_prev_state = Some(select_state); + } else { + self.select_prev_state = None; + } + + self.unfold_ranges( + selections.iter().map(|s| s.range()).collect::>(), + false, + true, + cx, ); - selection.start = word_range.start.to_offset(&display_map, Bias::Left); - selection.end = word_range.end.to_offset(&display_map, Bias::Left); - selection.goal = SelectionGoal::None; - selection.reversed = false; - - let query = buffer - .text_for_range(selection.start..selection.end) - .collect::(); - let query = query.chars().rev().collect::(); - let select_state = SelectNextState { - query: AhoCorasick::new(&[query])?, - wordwise: true, - done: false, - }; - self.unfold_ranges([selection.start..selection.end], false, true, cx); self.change_selections(Some(Autoscroll::newest()), cx, |s| { s.select(selections); }); - self.select_prev_state = Some(select_state); - } else { - let query = buffer - .text_for_range(selection.start..selection.end) - .collect::(); - let query = query.chars().rev().collect::(); + } else if let Some(selected_text) = selected_text { self.select_prev_state = Some(SelectNextState { - query: AhoCorasick::new(&[query])?, + query: AhoCorasick::new(&[selected_text.chars().rev().collect::()])?, wordwise: false, done: false, }); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 520c3714d3d529dbcd2df4d4cc4d750db2a7a53c..a6e3d19995c2126e57b698178e8d0cac1bc264c5 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -3821,62 +3821,137 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_select_previous(cx: &mut gpui::TestAppContext) { +async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); - { - // `Select previous` without a selection (selects wordwise) - let mut cx = EditorTestContext::new(cx).await; - cx.set_state("abc\nˇabc abc\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state( + r#"let foo = 2; +lˇet foo = 2; +let fooˇ = 2; +let foo = 2; +let foo = ˇ2;"#, + ); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); + cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + .unwrap(); + cx.assert_editor_state( + r#"let foo = 2; +«letˇ» foo = 2; +let «fooˇ» = 2; +let foo = 2; +let foo = «2ˇ»;"#, + ); - cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); - cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + // noop for multiple selections with different contents + cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + .unwrap(); + cx.assert_editor_state( + r#"let foo = 2; +«letˇ» foo = 2; +let «fooˇ» = 2; +let foo = 2; +let foo = «2ˇ»;"#, + ); +} - cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); - cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); +#[gpui::test] +async fn test_select_previous_with_single_caret(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»"); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\nˇabc abc\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); - } - { - // `Select previous` with a selection - let mut cx = EditorTestContext::new(cx).await; - cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc"); + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); - cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»"); + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); - } + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); +} + +#[gpui::test] +async fn test_select_previous_with_multiple_carets(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_state( + r#"let foo = 2; +lˇet foo = 2; +let fooˇ = 2; +let foo = 2; +let foo = ˇ2;"#, + ); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state( + r#"let foo = 2; +«letˇ» foo = 2; +let «fooˇ» = 2; +let foo = 2; +let foo = «2ˇ»;"#, + ); + + // noop for multiple selections with different contents + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state( + r#"let foo = 2; +«letˇ» foo = 2; +let «fooˇ» = 2; +let foo = 2; +let foo = «2ˇ»;"#, + ); +} + +#[gpui::test] +async fn test_select_previous_with_single_selection(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); } #[gpui::test] diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index ba70739942c429e6b5eb11139a395b98db38475a..2a5ac568b79bb9df45489bf5e6b37f37ffcba25b 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -5,7 +5,7 @@ use language::Point; use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles}; -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone, Copy)] pub enum Autoscroll { Next, Strategy(AutoscrollStrategy), @@ -25,7 +25,7 @@ impl Autoscroll { } } -#[derive(PartialEq, Eq, Default)] +#[derive(PartialEq, Eq, Default, Clone, Copy)] pub enum AutoscrollStrategy { Fit, Newest,