diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f1cf5a942b..fe7667cd2b 100644 --- a/crates/editor/src/editor.rs +++ b/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.manipulate_lines(cx, { + |mut lines| { + lines.sort(); + lines + } + }) + } + + pub fn sort_lines_case_insensitive( + &mut self, + _: &SortLinesCaseInsensitive, + cx: &mut ViewContext, + ) { + 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.manipulate_lines(cx, { + |mut lines| { + lines.reverse(); + lines + } + }) + } + + // pub fn shuffle_lines(&mut self, _: &ShuffleLines, cx: &mut ViewContext) { + // self.manipulate_lines(cx, { + // |mut lines| { + // lines.shuffle(&mut thread_rng()); + // lines + // } + // }) + // } + + fn manipulate_lines(&mut self, cx: &mut ViewContext, 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::(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::(); + // TODO SORT LINES: Is there a smarter / more effificent way to obtain lines? + let lines = text.split("\n").collect::>(); + 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) { 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 247a7b021d..18028eb8ee 100644 --- a/crates/editor/src/editor_tests.rs +++ b/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::(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::(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::(cx), + &[Point::new(0, 5)..Point::new(2, 2)] + ); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 5)..Point::new(2, 2)] + ); + + // assert_eq!( + // editor.selections.ranges::(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, |_| {});