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))
This commit is contained in:
dalton-oliveira 2024-02-08 12:13:15 -03:00 committed by GitHub
parent 89b1e76003
commit a0b2614d57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 219 additions and 15 deletions

View file

@ -238,5 +238,7 @@ gpui::actions!(
Undo,
UndoSelection,
UnfoldLines,
UniqueLinesCaseSensitive,
UniqueLinesCaseInsensitive
]
);

View file

@ -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);
});

View file

@ -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
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
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]

View file

@ -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(