From cf38264edab689cd20b5ffa2a055ebda1a3e8be7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 21 Jun 2023 11:47:06 -0600 Subject: [PATCH] Handle multiple selections when joining lines Co-Authored-By: Conrad Irwin --- crates/editor/src/editor.rs | 73 +++++++++++++++++++------------ crates/editor/src/editor_tests.rs | 39 ++++++++++++++++- 2 files changed, 84 insertions(+), 28 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 84c1948a3a..e5fa551b3b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3955,36 +3955,55 @@ impl Editor { } pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext) { - let selection = self.selections.newest::(cx); - let snapshot = self.buffer.read(cx).snapshot(cx); + let mut row_ranges = Vec::>::new(); + for selection in self.selections.ranges::(cx) { + let start = selection.start.row; + let end = if selection.start.row == selection.end.row { + selection.start.row + 1 + } else { + selection.end.row + }; - let row_range = if selection.start.row == selection.end.row { - selection.start.row..selection.start.row + 1 - } else { - selection.start.row..selection.end.row - }; - - self.transact(cx, |this, cx| { - for (ix, row) in row_range.rev().enumerate() { - let end_of_line = Point::new(row, snapshot.line_len(row)); - let start_of_next_line = end_of_line + Point::new(1, 0); - - let replace = if snapshot.line_len(row + 1) > 0 { - " " - } else { - "" - }; - - this.buffer.update(cx, |buffer, cx| { - buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx) - }); - - if ix == 0 { - this.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges([end_of_line..end_of_line]) - }) + if let Some(last_row_range) = row_ranges.last_mut() { + if start <= last_row_range.end { + last_row_range.end = end; + continue; } } + row_ranges.push(start..end); + } + + let snapshot = self.buffer.read(cx).snapshot(cx); + let mut cursor_positions = Vec::new(); + for row_range in &row_ranges { + let anchor = snapshot.anchor_before(Point::new( + row_range.end - 1, + snapshot.line_len(row_range.end - 1), + )); + cursor_positions.push(anchor.clone()..anchor); + } + + self.transact(cx, |this, cx| { + for row_range in row_ranges.into_iter().rev() { + for row in row_range.rev() { + let end_of_line = Point::new(row, snapshot.line_len(row)); + let start_of_next_line = end_of_line + Point::new(1, 0); + + let replace = if snapshot.line_len(row + 1) > 0 { + " " + } else { + "" + }; + + this.buffer.update(cx, |buffer, cx| { + buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx) + }); + } + } + + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_anchor_ranges(cursor_positions) + }); }); } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 27ea26f788..ab950fd5a5 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2329,7 +2329,7 @@ fn test_delete_line(cx: &mut TestAppContext) { } #[gpui::test] -fn test_join_lines(cx: &mut TestAppContext) { +fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { init_test(cx, |_| {}); cx.add_window(|cx| { @@ -2342,6 +2342,7 @@ fn test_join_lines(cx: &mut TestAppContext) { &[Point::new(0, 0)..Point::new(0, 0)] ); + // When on single line, replace newline at end by space editor.join_lines(&JoinLines, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); assert_eq!( @@ -2349,6 +2350,7 @@ fn test_join_lines(cx: &mut TestAppContext) { &[Point::new(0, 3)..Point::new(0, 3)] ); + // When multiple lines are selected, remove newlines that are spanned by the selection editor.change_selections(None, cx, |s| { s.select_ranges([Point::new(0, 5)..Point::new(2, 2)]) }); @@ -2359,6 +2361,7 @@ fn test_join_lines(cx: &mut TestAppContext) { &[Point::new(0, 11)..Point::new(0, 11)] ); + // Undo should be transactional editor.undo(&Undo, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); assert_eq!( @@ -2366,6 +2369,7 @@ fn test_join_lines(cx: &mut TestAppContext) { &[Point::new(0, 5)..Point::new(2, 2)] ); + // When joining an empty line don't insert a space editor.change_selections(None, cx, |s| { s.select_ranges([Point::new(2, 1)..Point::new(2, 2)]) }); @@ -2376,6 +2380,7 @@ fn test_join_lines(cx: &mut TestAppContext) { [Point::new(2, 3)..Point::new(2, 3)] ); + // We can remove trailing newlines editor.join_lines(&JoinLines, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); assert_eq!( @@ -2383,6 +2388,7 @@ fn test_join_lines(cx: &mut TestAppContext) { [Point::new(2, 3)..Point::new(2, 3)] ); + // We don't blow up on the last line editor.join_lines(&JoinLines, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); assert_eq!( @@ -2394,6 +2400,37 @@ fn test_join_lines(cx: &mut TestAppContext) { }); } +#[gpui::test] +fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\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(1, 1), + Point::new(1, 2)..Point::new(1, 2), + Point::new(3, 1)..Point::new(3, 2), + ]) + }); + + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n"); + + 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, |_| {});