From c48bd394c16f818bd7fe42bca6f4d8063c8b26c0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 21 Jun 2023 10:32:02 -0600 Subject: [PATCH 1/7] Start on joining lines Co-Authored-By: Conrad Irwin --- crates/editor/src/editor.rs | 13 +++++++++++ crates/editor/src/editor_tests.rs | 36 ++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index dd9952e870..b1027eeadd 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -206,6 +206,7 @@ actions!( DuplicateLine, MoveLineUp, MoveLineDown, + JoinLines, Transpose, Cut, Copy, @@ -321,6 +322,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(Editor::indent); cx.add_action(Editor::outdent); cx.add_action(Editor::delete_line); + cx.add_action(Editor::join_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); @@ -3952,6 +3954,17 @@ impl Editor { }); } + pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext) { + let cursor_position = self.selections.newest::(cx).head(); + let snapshot = self.buffer.read(cx).snapshot(cx); + let end_of_line = Point::new(cursor_position.row, snapshot.line_len(cursor_position.row)); + let start_of_next_line = end_of_line + Point::new(1, 0); + + self.buffer.update(cx, |buffer, cx| { + buffer.edit([(end_of_line..start_of_next_line, " ")], None, 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 e2b876f4b7..1e9a745e9c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1,7 +1,10 @@ use super::*; -use crate::test::{ - assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext, - editor_test_context::EditorTestContext, select_ranges, +use crate::{ + test::{ + assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext, + editor_test_context::EditorTestContext, select_ranges, + }, + JoinLines, }; use drag_and_drop::DragAndDrop; use futures::StreamExt; @@ -2325,6 +2328,33 @@ fn test_delete_line(cx: &mut TestAppContext) { }); } +#[gpui::test] +fn test_join_lines(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let (_, editor) = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + let mut editor = build_editor(buffer.clone(), cx); + let buffer = buffer.read(cx).as_singleton().unwrap(); + + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 0)..Point::new(0, 0)] + ); + + editor.join_lines(&JoinLines, cx); + + // assert_eq!( + // editor.selections.ranges::(cx), + // &[Point::new(0, 0), Point::new(0, 0)] + // ); + + assert_eq!(buffer.read(cx).text(), "abc def\nghi\n"); + + editor + }); +} + #[gpui::test] fn test_duplicate_line(cx: &mut TestAppContext) { init_test(cx, |_| {}); From f07a929350d55499fc91271a199a3538e3eae7ac Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 21 Jun 2023 11:18:28 -0600 Subject: [PATCH 2/7] Allow joining multiple lines and deleting empty lines Co-Authored-By: Conrad Irwin --- crates/editor/src/editor.rs | 33 ++++++++++++++++++++++---- crates/editor/src/editor_tests.rs | 39 +++++++++++++++++++++++++------ 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b1027eeadd..84c1948a3a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3955,13 +3955,36 @@ impl Editor { } pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext) { - let cursor_position = self.selections.newest::(cx).head(); + let selection = self.selections.newest::(cx); let snapshot = self.buffer.read(cx).snapshot(cx); - let end_of_line = Point::new(cursor_position.row, snapshot.line_len(cursor_position.row)); - let start_of_next_line = end_of_line + Point::new(1, 0); - self.buffer.update(cx, |buffer, cx| { - buffer.edit([(end_of_line..start_of_next_line, " ")], None, cx) + 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]) + }) + } + } }); } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 1e9a745e9c..d11ddb5324 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2332,8 +2332,8 @@ fn test_delete_line(cx: &mut TestAppContext) { fn test_join_lines(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, editor) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", 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(); @@ -2343,13 +2343,38 @@ fn test_join_lines(cx: &mut TestAppContext) { ); editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 3)..Point::new(0, 3)] + ); - // assert_eq!( - // editor.selections.ranges::(cx), - // &[Point::new(0, 0), Point::new(0, 0)] - // ); + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 5)..Point::new(2, 2)]) + }); + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n"); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 11)..Point::new(0, 11)] + ); - assert_eq!(buffer.read(cx).text(), "abc def\nghi\n"); + editor.undo(&Undo, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 5)..Point::new(2, 2)] + ); + + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(2, 1)..Point::new(2, 2)]) + }); + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n"); + assert_eq!( + editor.selections.ranges::(cx), + [Point::new(2, 3)..Point::new(2, 3)] + ); editor }); From 61352f68ea36f02576148b1c29580ce167fc9887 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 21 Jun 2023 11:21:09 -0600 Subject: [PATCH 3/7] Add more assertions for joining lines at the end of the file Co-Authored-By: Conrad Irwin --- crates/editor/src/editor_tests.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index d11ddb5324..27ea26f788 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2376,6 +2376,20 @@ fn test_join_lines(cx: &mut TestAppContext) { [Point::new(2, 3)..Point::new(2, 3)] ); + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); + assert_eq!( + editor.selections.ranges::(cx), + [Point::new(2, 3)..Point::new(2, 3)] + ); + + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); + assert_eq!( + editor.selections.ranges::(cx), + [Point::new(2, 3)..Point::new(2, 3)] + ); + editor }); } From cf38264edab689cd20b5ffa2a055ebda1a3e8be7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 21 Jun 2023 11:47:06 -0600 Subject: [PATCH 4/7] 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, |_| {}); From cde8d7d747261e1c52763a3e88b3c73a640ab01b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 21 Jun 2023 11:53:11 -0600 Subject: [PATCH 5/7] Don't use test-only method; add ctrl-j binding Co-Authored-By: Conrad Irwin --- assets/keymaps/default.json | 1 + crates/editor/src/editor.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index a642697a37..84b11b53a1 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -411,6 +411,7 @@ "ctrl-shift-k": "editor::DeleteLine", "cmd-shift-d": "editor::DuplicateLine", "cmd-shift-l": "editor::SplitSelectionIntoLines", + "ctrl-j": "editor::JoinLines", "ctrl-cmd-up": "editor::MoveLineUp", "ctrl-cmd-down": "editor::MoveLineDown", "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e5fa551b3b..c8daff3fd5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3956,7 +3956,7 @@ impl Editor { pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext) { let mut row_ranges = Vec::>::new(); - for selection in self.selections.ranges::(cx) { + for selection in self.selections.all::(cx) { let start = selection.start.row; let end = if selection.start.row == selection.end.row { selection.start.row + 1 From a365b2f1775ad80886270975159ce5e62455a0c8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 21 Jun 2023 11:58:31 -0600 Subject: [PATCH 6/7] Add shift-J binding to Vim normal mode Co-Authored-By: Conrad Irwin --- assets/keymaps/vim.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 47c5f8c458..bf5eed5350 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -143,6 +143,7 @@ "Delete" ], "shift-d": "vim::DeleteToEndOfLine", + "shift-j": "editor::JoinLines", "y": [ "vim::PushOperator", "Yank" From 91bd8e305e773b6300577568ab35c72b9352ccf2 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 21 Jun 2023 13:14:21 -0600 Subject: [PATCH 7/7] join_lines: Skip over leading indentation --- crates/editor/src/editor.rs | 5 +++-- crates/editor/src/editor_tests.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c8daff3fd5..3c6410cef9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3987,9 +3987,10 @@ impl Editor { 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 indent = snapshot.indent_size_for_line(row + 1); + let start_of_next_line = Point::new(row + 1, indent.len); - let replace = if snapshot.line_len(row + 1) > 0 { + let replace = if snapshot.line_len(row + 1) > indent.len { " " } else { "" diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index ab950fd5a5..6fcb6f778f 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2396,6 +2396,34 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { [Point::new(2, 3)..Point::new(2, 3)] ); + // reset to test indentation + editor.buffer.update(cx, |buffer, cx| { + buffer.edit( + [ + (Point::new(1, 0)..Point::new(1, 2), " "), + (Point::new(2, 0)..Point::new(2, 3), " \n\td"), + ], + None, + cx, + ) + }); + + // We remove any leading spaces + assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td"); + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 1)..Point::new(0, 1)]) + }); + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td"); + + // We don't insert a space for a line containing only spaces + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td"); + + // We ignore any leading tabs + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb c d"); + editor }); }