mirror of
https://github.com/zed-industries/zed.git
synced 2025-02-07 11:11:30 +00:00
Add support for joining lines (#2636)
🚧 We have a couple more refinements to this to add 🚧 Release Notes: You can now join lines with `ctrl-j` or `shift-J` in Vim normal mode. 🍐'd with @ConradIrwin
This commit is contained in:
commit
cd008d1503
4 changed files with 195 additions and 3 deletions
|
@ -411,6 +411,7 @@
|
||||||
"ctrl-shift-k": "editor::DeleteLine",
|
"ctrl-shift-k": "editor::DeleteLine",
|
||||||
"cmd-shift-d": "editor::DuplicateLine",
|
"cmd-shift-d": "editor::DuplicateLine",
|
||||||
"cmd-shift-l": "editor::SplitSelectionIntoLines",
|
"cmd-shift-l": "editor::SplitSelectionIntoLines",
|
||||||
|
"ctrl-j": "editor::JoinLines",
|
||||||
"ctrl-cmd-up": "editor::MoveLineUp",
|
"ctrl-cmd-up": "editor::MoveLineUp",
|
||||||
"ctrl-cmd-down": "editor::MoveLineDown",
|
"ctrl-cmd-down": "editor::MoveLineDown",
|
||||||
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
|
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||||
|
|
|
@ -143,6 +143,7 @@
|
||||||
"Delete"
|
"Delete"
|
||||||
],
|
],
|
||||||
"shift-d": "vim::DeleteToEndOfLine",
|
"shift-d": "vim::DeleteToEndOfLine",
|
||||||
|
"shift-j": "editor::JoinLines",
|
||||||
"y": [
|
"y": [
|
||||||
"vim::PushOperator",
|
"vim::PushOperator",
|
||||||
"Yank"
|
"Yank"
|
||||||
|
|
|
@ -206,6 +206,7 @@ actions!(
|
||||||
DuplicateLine,
|
DuplicateLine,
|
||||||
MoveLineUp,
|
MoveLineUp,
|
||||||
MoveLineDown,
|
MoveLineDown,
|
||||||
|
JoinLines,
|
||||||
Transpose,
|
Transpose,
|
||||||
Cut,
|
Cut,
|
||||||
Copy,
|
Copy,
|
||||||
|
@ -321,6 +322,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(Editor::indent);
|
cx.add_action(Editor::indent);
|
||||||
cx.add_action(Editor::outdent);
|
cx.add_action(Editor::outdent);
|
||||||
cx.add_action(Editor::delete_line);
|
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_word_start);
|
||||||
cx.add_action(Editor::delete_to_previous_subword_start);
|
cx.add_action(Editor::delete_to_previous_subword_start);
|
||||||
cx.add_action(Editor::delete_to_next_word_end);
|
cx.add_action(Editor::delete_to_next_word_end);
|
||||||
|
@ -3956,6 +3958,60 @@ impl Editor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext<Self>) {
|
||||||
|
let mut row_ranges = Vec::<Range<u32>>::new();
|
||||||
|
for selection in self.selections.all::<Point>(cx) {
|
||||||
|
let start = selection.start.row;
|
||||||
|
let end = if selection.start.row == selection.end.row {
|
||||||
|
selection.start.row + 1
|
||||||
|
} else {
|
||||||
|
selection.end.row
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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) > indent.len {
|
||||||
|
" "
|
||||||
|
} 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)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) {
|
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 display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||||
let buffer = &display_map.buffer_snapshot;
|
let buffer = &display_map.buffer_snapshot;
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::test::{
|
use crate::{
|
||||||
assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
|
test::{
|
||||||
editor_test_context::EditorTestContext, select_ranges,
|
assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
|
||||||
|
editor_test_context::EditorTestContext, select_ranges,
|
||||||
|
},
|
||||||
|
JoinLines,
|
||||||
};
|
};
|
||||||
use drag_and_drop::DragAndDrop;
|
use drag_and_drop::DragAndDrop;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
@ -2325,6 +2328,137 @@ fn test_delete_line(cx: &mut TestAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_join_lines_with_single_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();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
editor.selections.ranges::<Point>(cx),
|
||||||
|
&[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!(
|
||||||
|
editor.selections.ranges::<Point>(cx),
|
||||||
|
&[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)])
|
||||||
|
});
|
||||||
|
editor.join_lines(&JoinLines, cx);
|
||||||
|
assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
|
||||||
|
assert_eq!(
|
||||||
|
editor.selections.ranges::<Point>(cx),
|
||||||
|
&[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!(
|
||||||
|
editor.selections.ranges::<Point>(cx),
|
||||||
|
&[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)])
|
||||||
|
});
|
||||||
|
editor.join_lines(&JoinLines, cx);
|
||||||
|
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
|
||||||
|
assert_eq!(
|
||||||
|
editor.selections.ranges::<Point>(cx),
|
||||||
|
[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!(
|
||||||
|
editor.selections.ranges::<Point>(cx),
|
||||||
|
[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!(
|
||||||
|
editor.selections.ranges::<Point>(cx),
|
||||||
|
[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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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::<Point>(cx),
|
||||||
|
[
|
||||||
|
Point::new(0, 7)..Point::new(0, 7),
|
||||||
|
Point::new(1, 3)..Point::new(1, 3)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
editor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_duplicate_line(cx: &mut TestAppContext) {
|
fn test_duplicate_line(cx: &mut TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
|
Loading…
Reference in a new issue