From a0b2614d57c71d5421fd28fc2f694582b170644f Mon Sep 17 00:00:00 2001 From: dalton-oliveira Date: Thu, 8 Feb 2024 12:13:15 -0300 Subject: [PATCH] Add unique lines command (#7526) 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(-) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index ec8113a985a1575fd6954958918e4c8521481b84..a715515065a495eebbe66ac82935eb6d949b7d20 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -238,5 +238,7 @@ gpui::actions!( Undo, UndoSelection, UnfoldLines, + UniqueLinesCaseSensitive, + UniqueLinesCaseInsensitive ] ); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0085761e0b9d1d61059fed0eaa37648cb3fc3c50..8b9e125238f8fdec0e90ad12627a899163f27228 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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.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.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.manipulate_lines(cx, |lines| lines.reverse()) } @@ -4665,7 +4687,7 @@ impl Editor { fn manipulate_lines(&mut self, cx: &mut ViewContext, 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::(); + 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); }); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 865fcb1a5c37d54f1a77067593eeb0f835d45585..1c9c5f1db6224f0488c662789d7ea463affa32a8 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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] diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 24b0a06bcb53cd850dbb0decf45b06f914eb0ecb..ce348b21d514518c1ea5b3148457a755416ac9cc 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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(