diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 087ce81c268ffcbc11f598031b22d9ee272a114e..bc1c904404972cb847947b8c60147bcf4a41186b 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -10,7 +10,6 @@ doctest = false [features] test-support = [ - "rand", "copilot/test-support", "text/test-support", "language/test-support", @@ -62,8 +61,8 @@ serde.workspace = true serde_derive.workspace = true smallvec.workspace = true smol.workspace = true +rand.workspace = true -rand = { workspace = true, optional = true } tree-sitter-rust = { workspace = true, optional = true } tree-sitter-html = { workspace = true, optional = true } tree-sitter-typescript = { workspace = true, optional = true } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f1cf5a942b05da76cf7a85046f3a8925bf375523..e05837740d4c50fbe7e02e51272bd6340884ec8a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -74,6 +74,7 @@ pub use multi_buffer::{ }; use ordered_float::OrderedFloat; use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction}; +use rand::{seq::SliceRandom, thread_rng}; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, }; @@ -226,6 +227,10 @@ actions!( MoveLineUp, MoveLineDown, JoinLines, + SortLinesCaseSensitive, + SortLinesCaseInsensitive, + ReverseLines, + ShuffleLines, Transpose, Cut, Copy, @@ -344,6 +349,10 @@ pub fn init(cx: &mut AppContext) { cx.add_action(Editor::outdent); cx.add_action(Editor::delete_line); cx.add_action(Editor::join_lines); + cx.add_action(Editor::sort_lines_case_sensitive); + cx.add_action(Editor::sort_lines_case_insensitive); + cx.add_action(Editor::reverse_lines); + cx.add_action(Editor::shuffle_lines); cx.add_action(Editor::delete_to_previous_word_start); cx.add_action(Editor::delete_to_previous_subword_start); cx.add_action(Editor::delete_to_next_word_end); @@ -4205,6 +4214,96 @@ impl Editor { }); } + pub fn sort_lines_case_sensitive( + &mut self, + _: &SortLinesCaseSensitive, + cx: &mut ViewContext, + ) { + self.manipulate_lines(cx, |text| text.sort()) + } + + pub fn sort_lines_case_insensitive( + &mut self, + _: &SortLinesCaseInsensitive, + cx: &mut ViewContext, + ) { + self.manipulate_lines(cx, |text| text.sort_by_key(|line| line.to_lowercase())) + } + + pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext) { + self.manipulate_lines(cx, |lines| lines.reverse()) + } + + pub fn shuffle_lines(&mut self, _: &ShuffleLines, cx: &mut ViewContext) { + self.manipulate_lines(cx, |lines| lines.shuffle(&mut thread_rng())) + } + + fn manipulate_lines(&mut self, cx: &mut ViewContext, mut callback: Fn) + where + Fn: FnMut(&mut [&str]), + { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut edits = Vec::new(); + + let selections = self.selections.all::(cx); + let mut selections = selections.iter().peekable(); + let mut contiguous_row_selections = Vec::new(); + let mut new_selections = Vec::new(); + + while let Some(selection) = selections.next() { + let (start_row, end_row) = consume_contiguous_rows( + &mut contiguous_row_selections, + selection, + &display_map, + &mut selections, + ); + + let start_point = Point::new(start_row, 0); + let end_point = Point::new(end_row - 1, buffer.line_len(end_row - 1)); + let text = buffer + .text_for_range(start_point..end_point) + .collect::(); + let mut text = text.split("\n").collect_vec(); + + let text_len = text.len(); + callback(&mut text); + + // This is a current limitation with selections. + // If we wanted to support removing or adding lines, we'd need to fix the logic associated with selections. + debug_assert!( + text.len() == text_len, + "callback should not change the number of lines" + ); + + edits.push((start_point..end_point, text.join("\n"))); + let start_anchor = buffer.anchor_after(start_point); + let end_anchor = buffer.anchor_before(end_point); + + // Make selection and push + new_selections.push(Selection { + id: selection.id, + start: start_anchor.to_offset(&buffer), + end: end_anchor.to_offset(&buffer), + goal: SelectionGoal::None, + reversed: selection.reversed, + }); + } + + self.transact(cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + }); + + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select(new_selections); + }); + + this.request_autoscroll(Autoscroll::fit(), cx); + }); + } + pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 247a7b021d77cac7d2386f67fc676d7723b037fc..eb03d2bdc0f103c2b94352312d3e86df860c821c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2500,6 +2500,156 @@ fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + // Test sort_lines_case_insensitive() + cx.set_state(indoc! {" + «z + y + x + Z + Y + Xˇ» + "}); + cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx)); + cx.assert_editor_state(indoc! {" + «x + X + y + Y + z + Zˇ» + "}); + + // Test reverse_lines() + cx.set_state(indoc! {" + «5 + 4 + 3 + 2 + 1ˇ» + "}); + cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx)); + cx.assert_editor_state(indoc! {" + «1 + 2 + 3 + 4 + 5ˇ» + "}); + + // Skip testing shuffle_line() + + // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive() + // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines) + + // Don't manipulate when cursor is on single line, but expand the selection + cx.set_state(indoc! {" + ddˇdd + ccc + bb + a + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + «ddddˇ» + ccc + bb + a + "}); + + // Basic manipulate case + // Start selection moves to column 0 + // End of selection shrinks to fit shorter line + cx.set_state(indoc! {" + dd«d + ccc + bb + aaaaaˇ» + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + «aaaaa + bb + ccc + dddˇ» + "}); + + // Manipulate case with newlines + cx.set_state(indoc! {" + dd«d + ccc + + bb + aaaaa + + ˇ» + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + « + + aaaaa + bb + ccc + dddˇ» + + "}); +} + +#[gpui::test] +async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + // Manipulate with multiple selections on a single line + cx.set_state(indoc! {" + dd«dd + cˇ»c«c + bb + aaaˇ»aa + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + «aaaaa + bb + ccc + ddddˇ» + "}); + + // Manipulate with multiple disjoin selections + cx.set_state(indoc! {" + 5« + 4 + 3 + 2 + 1ˇ» + + dd«dd + ccc + bb + aaaˇ»aa + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + «1 + 2 + 3 + 4 + 5ˇ» + + «aaaaa + bb + ccc + ddddˇ» + "}); +} + #[gpui::test] fn test_duplicate_line(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index a22506f751b1ab36d518cdc13f60dc61dfecf6c7..1921bc0738ec588c8b9218c717de497232a345ec 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -138,7 +138,7 @@ impl SelectionsCollection { .collect() } - // Returns all of the selections, adjusted to take into account the selection line_mode + /// Returns all of the selections, adjusted to take into account the selection line_mode pub fn all_adjusted(&self, cx: &mut AppContext) -> Vec> { let mut selections = self.all::(cx); if self.line_mode {