@@ -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;
@@ -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, |_| {});