WIP

Joseph T. Lyons created

Change summary

crates/editor/src/editor.rs       | 103 +++++++++++++++++++++++++++++++++
crates/editor/src/editor_tests.rs |  79 +++++++++++++++++++++++++
2 files changed, 182 insertions(+)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -74,6 +74,8 @@ pub use multi_buffer::{
 };
 use ordered_float::OrderedFloat;
 use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction};
+use rand::seq::SliceRandom;
+use rand::thread_rng;
 use scroll::{
     autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
 };
@@ -226,6 +228,10 @@ actions!(
         MoveLineUp,
         MoveLineDown,
         JoinLines,
+        SortLinesCaseSensitive,
+        SortLinesCaseInsensitive,
+        ReverseLines,
+        ShuffleLines,
         Transpose,
         Cut,
         Copy,
@@ -344,6 +350,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 +4215,99 @@ impl Editor {
         });
     }
 
+    pub fn sort_lines_case_sensitive(
+        &mut self,
+        _: &SortLinesCaseSensitive,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.manipulate_lines(cx, {
+            |mut lines| {
+                lines.sort();
+                lines
+            }
+        })
+    }
+
+    pub fn sort_lines_case_insensitive(
+        &mut self,
+        _: &SortLinesCaseInsensitive,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.manipulate_lines(cx, {
+            |mut lines| {
+                lines.sort_by_key(|line| line.to_lowercase());
+                lines
+            }
+        })
+    }
+
+    pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext<Self>) {
+        self.manipulate_lines(cx, {
+            |mut lines| {
+                lines.reverse();
+                lines
+            }
+        })
+    }
+
+    // pub fn shuffle_lines(&mut self, _: &ShuffleLines, cx: &mut ViewContext<Self>) {
+    //     self.manipulate_lines(cx, {
+    //         |mut lines| {
+    //             lines.shuffle(&mut thread_rng());
+    //             lines
+    //         }
+    //     })
+    // }
+
+    fn manipulate_lines<Fn>(&mut self, cx: &mut ViewContext<Self>, mut callback: Fn)
+    where
+        Fn: FnMut(Vec<&str>) -> Vec<&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::<Point>(cx);
+        let mut selections = selections.iter().peekable();
+        let mut contiguous_row_selections = Vec::new();
+
+        while let Some(selection) = selections.next() {
+            // Find all the selections that span a contiguous row range
+            let (start_row, end_row) = consume_contiguous_rows(
+                &mut contiguous_row_selections,
+                selection,
+                &display_map,
+                &mut selections,
+            );
+            let start = Point::new(start_row, 0);
+            let end = Point::new(end_row - 1, buffer.line_len(end_row - 1));
+            let text = buffer.text_for_range(start..end).collect::<String>();
+            // TODO SORT LINES: Is there a smarter / more effificent way to obtain lines?
+            let lines = text.split("\n").collect::<Vec<_>>();
+            let lines = callback(lines);
+            edits.push((start..end, lines.join("\n")));
+        }
+
+        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(contiguous_row_selections);
+            });
+
+            this.request_autoscroll(Autoscroll::fit(), cx);
+        });
+
+        // TODO:
+        // Write tests
+        // - Use cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six "); in tests
+        // Mikayla check for perf stuff
+        // Shuffle
+    }
+
     pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = &display_map.buffer_snapshot;

crates/editor/src/editor_tests.rs 🔗

@@ -2500,6 +2500,85 @@ fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
     });
 }
 
+#[gpui::test]
+fn test_sort_lines_with_single_selection(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("dddd\nccc\nbb\na\n\n", cx);
+        let mut editor = build_editor(buffer.clone(), cx);
+        let buffer = buffer.read(cx).as_singleton().unwrap();
+
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(0, 2)..Point::new(0, 2)])
+        });
+        editor.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx);
+        assert_eq!(
+            buffer.read(cx).text(),
+            "dddd\nccc\nbb\na\n\n",
+            "no sorting when single cursor parked on single line"
+        );
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            &[Point::new(0, 2)..Point::new(0, 2)]
+        );
+
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(0, 2)..Point::new(5, 1)])
+        });
+        editor.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx);
+        assert_eq!(
+            buffer.read(cx).text(),
+            "a\nbb\nccc\ndddd\n\n",
+            "single selection is sorted"
+        );
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            &[Point::new(0, 0)..Point::new(5, 1)]
+        );
+
+        editor
+    });
+}
+
+#[gpui::test]
+fn test_sort_lines_with_multi_selection(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("dddd\nccc\nbb\na\n\n3\n2\n1\n\n", cx);
+        let mut editor = build_editor(buffer.clone(), cx);
+        let buffer = buffer.read(cx).as_singleton().unwrap();
+
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([
+                Point::new(0, 2)..Point::new(3, 2),
+                Point::new(5, 0)..Point::new(7, 1),
+            ])
+        });
+
+        editor.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx);
+        assert_eq!(buffer.read(cx).text(), "a\nbb\nccc\ndddd\n\n1\n2\n3\n\n");
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            &[Point::new(0, 5)..Point::new(2, 2)]
+        );
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            &[Point::new(0, 5)..Point::new(2, 2)]
+        );
+
+        // assert_eq!(
+        //     editor.selections.ranges::<Point>(cx),
+        //     [
+        //         Point::new(0, 7)..Point::new(0, 7),
+        //         Point::new(1, 3)..Point::new(1, 3)
+        //     ]
+        // );
+        editor
+    });
+}
+
 #[gpui::test]
 fn test_duplicate_line(cx: &mut TestAppContext) {
     init_test(cx, |_| {});