crates/editor/src/actions.rs 🔗
@@ -238,5 +238,7 @@ gpui::actions!(
Undo,
UndoSelection,
UnfoldLines,
+ UniqueLinesCaseSensitive,
+ UniqueLinesCaseInsensitive
]
);
dalton-oliveira created
Changes `Editor::manipulate_lines` to allow line adding and removal
through callback function.
- Added `editor::UniqueLinesCaseSensitive` and `editor::UniqueLinesCaseInsensitive` commands
([#4831](https://github.com/zed-industries/zed/issues/4831))
crates/editor/src/actions.rs | 2
crates/editor/src/editor.rs | 72 +++++++++++---
crates/editor/src/editor_tests.rs | 158 +++++++++++++++++++++++++++++++++
crates/editor/src/element.rs | 2
4 files changed, 219 insertions(+), 15 deletions(-)
@@ -238,5 +238,7 @@ gpui::actions!(
Undo,
UndoSelection,
UnfoldLines,
+ UniqueLinesCaseSensitive,
+ UniqueLinesCaseInsensitive
]
);
@@ -4655,6 +4655,28 @@ impl Editor {
self.manipulate_lines(cx, |lines| lines.sort_by_key(|line| line.to_lowercase()))
}
+ pub fn unique_lines_case_insensitive(
+ &mut self,
+ _: &UniqueLinesCaseInsensitive,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.manipulate_lines(cx, |lines| {
+ let mut seen = HashSet::default();
+ lines.retain(|line| seen.insert(line.to_lowercase()));
+ })
+ }
+
+ pub fn unique_lines_case_sensitive(
+ &mut self,
+ _: &UniqueLinesCaseSensitive,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.manipulate_lines(cx, |lines| {
+ let mut seen = HashSet::default();
+ lines.retain(|line| seen.insert(*line));
+ })
+ }
+
pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext<Self>) {
self.manipulate_lines(cx, |lines| lines.reverse())
}
@@ -4665,7 +4687,7 @@ impl Editor {
fn manipulate_lines<Fn>(&mut self, cx: &mut ViewContext<Self>, mut callback: Fn)
where
- Fn: FnMut(&mut [&str]),
+ Fn: FnMut(&mut Vec<&str>),
{
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = self.buffer.read(cx).snapshot(cx);
@@ -4676,6 +4698,8 @@ impl Editor {
let mut selections = selections.iter().peekable();
let mut contiguous_row_selections = Vec::new();
let mut new_selections = Vec::new();
+ let mut added_lines: usize = 0;
+ let mut removed_lines: usize = 0;
while let Some(selection) = selections.next() {
let (start_row, end_row) = consume_contiguous_rows(
@@ -4690,37 +4714,55 @@ impl Editor {
let text = buffer
.text_for_range(start_point..end_point)
.collect::<String>();
+
let mut lines = text.split("\n").collect_vec();
- let lines_len = lines.len();
+ let lines_before = lines.len();
callback(&mut lines);
-
- // This is a current limitation with selections.
- // If we wanted to support removing or adding lines, we'd need to fix the logic associated with selections.
- debug_assert!(
- lines.len() == lines_len,
- "callback should not change the number of lines"
- );
+ let lines_after = lines.len();
edits.push((start_point..end_point, lines.join("\n")));
- let start_anchor = buffer.anchor_after(start_point);
- let end_anchor = buffer.anchor_before(end_point);
- // Make selection and push
+ // Selections must change based on added and removed line count
+ let start_row = start_point.row + added_lines as u32 - removed_lines as u32;
+ let end_row = start_row + lines_after.saturating_sub(1) as u32;
new_selections.push(Selection {
id: selection.id,
- start: start_anchor.to_offset(&buffer),
- end: end_anchor.to_offset(&buffer),
+ start: start_row,
+ end: end_row,
goal: SelectionGoal::None,
reversed: selection.reversed,
});
+
+ if lines_after > lines_before {
+ added_lines += lines_after - lines_before;
+ } else if lines_before > lines_after {
+ removed_lines += lines_before - lines_after;
+ }
}
self.transact(cx, |this, cx| {
- this.buffer.update(cx, |buffer, cx| {
+ let buffer = this.buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
+ buffer.snapshot(cx)
});
+ // Recalculate offsets on newly edited buffer
+ let new_selections = new_selections
+ .iter()
+ .map(|s| {
+ let start_point = Point::new(s.start, 0);
+ let end_point = Point::new(s.end, buffer.line_len(s.end));
+ Selection {
+ id: s.id,
+ start: buffer.point_to_offset(start_point),
+ end: buffer.point_to_offset(end_point),
+ goal: s.goal,
+ reversed: s.reversed,
+ }
+ })
+ .collect();
+
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(new_selections);
});
@@ -2786,6 +2786,126 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
dddˇ»
"});
+
+ // Adding new line
+ cx.set_state(indoc! {"
+ aa«a
+ bbˇ»b
+ "});
+ cx.update_editor(|e, cx| e.manipulate_lines(cx, |lines| lines.push("added_line")));
+ cx.assert_editor_state(indoc! {"
+ «aaa
+ bbb
+ added_lineˇ»
+ "});
+
+ // Removing line
+ cx.set_state(indoc! {"
+ aa«a
+ bbbˇ»
+ "});
+ cx.update_editor(|e, cx| {
+ e.manipulate_lines(cx, |lines| {
+ lines.pop();
+ })
+ });
+ cx.assert_editor_state(indoc! {"
+ «aaaˇ»
+ "});
+
+ // Removing all lines
+ cx.set_state(indoc! {"
+ aa«a
+ bbbˇ»
+ "});
+ cx.update_editor(|e, cx| {
+ e.manipulate_lines(cx, |lines| {
+ lines.drain(..);
+ })
+ });
+ cx.assert_editor_state(indoc! {"
+ ˇ
+ "});
+}
+
+#[gpui::test]
+async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ // Consider continuous selection as single selection
+ cx.set_state(indoc! {"
+ Aaa«aa
+ cˇ»c«c
+ bb
+ aaaˇ»aa
+ "});
+ cx.update_editor(|e, cx| e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, cx));
+ cx.assert_editor_state(indoc! {"
+ «Aaaaa
+ ccc
+ bb
+ aaaaaˇ»
+ "});
+
+ cx.set_state(indoc! {"
+ Aaa«aa
+ cˇ»c«c
+ bb
+ aaaˇ»aa
+ "});
+ cx.update_editor(|e, cx| e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, cx));
+ cx.assert_editor_state(indoc! {"
+ «Aaaaa
+ ccc
+ bbˇ»
+ "});
+
+ // Consider non continuous selection as distinct dedup operations
+ cx.set_state(indoc! {"
+ «aaaaa
+ bb
+ aaaaa
+ aaaaaˇ»
+
+ aaa«aaˇ»
+ "});
+ cx.update_editor(|e, cx| e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, cx));
+ cx.assert_editor_state(indoc! {"
+ «aaaaa
+ bbˇ»
+
+ «aaaaaˇ»
+ "});
+}
+
+#[gpui::test]
+async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ cx.set_state(indoc! {"
+ «Aaa
+ aAa
+ Aaaˇ»
+ "});
+ cx.update_editor(|e, cx| e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, cx));
+ cx.assert_editor_state(indoc! {"
+ «Aaa
+ aAaˇ»
+ "});
+
+ cx.set_state(indoc! {"
+ «Aaa
+ aAa
+ aaAˇ»
+ "});
+ cx.update_editor(|e, cx| e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, cx));
+ cx.assert_editor_state(indoc! {"
+ «Aaaˇ»
+ "});
}
#[gpui::test]
@@ -2835,6 +2955,44 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
ccc
ddddˇ»
"});
+
+ // Adding lines on each selection
+ cx.set_state(indoc! {"
+ 2«
+ 1ˇ»
+
+ bb«bb
+ aaaˇ»aa
+ "});
+ cx.update_editor(|e, cx| e.manipulate_lines(cx, |lines| lines.push("added line")));
+ cx.assert_editor_state(indoc! {"
+ «2
+ 1
+ added lineˇ»
+
+ «bbbb
+ aaaaa
+ added lineˇ»
+ "});
+
+ // Removing lines on each selection
+ cx.set_state(indoc! {"
+ 2«
+ 1ˇ»
+
+ bb«bb
+ aaaˇ»aa
+ "});
+ cx.update_editor(|e, cx| {
+ e.manipulate_lines(cx, |lines| {
+ lines.pop();
+ })
+ });
+ cx.assert_editor_state(indoc! {"
+ «2ˇ»
+
+ «bbbbˇ»
+ "});
}
#[gpui::test]
@@ -330,6 +330,8 @@ impl EditorElement {
register_action(view, cx, Editor::context_menu_next);
register_action(view, cx, Editor::context_menu_last);
register_action(view, cx, Editor::display_cursor_names);
+ register_action(view, cx, Editor::unique_lines_case_insensitive);
+ register_action(view, cx, Editor::unique_lines_case_sensitive);
}
fn register_key_listeners(