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:
parent
89b1e76003
commit
a0b2614d57
4 changed files with 219 additions and 15 deletions
|
@ -238,5 +238,7 @@ gpui::actions!(
|
||||||
Undo,
|
Undo,
|
||||||
UndoSelection,
|
UndoSelection,
|
||||||
UnfoldLines,
|
UnfoldLines,
|
||||||
|
UniqueLinesCaseSensitive,
|
||||||
|
UniqueLinesCaseInsensitive
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
|
@ -4655,6 +4655,28 @@ impl Editor {
|
||||||
self.manipulate_lines(cx, |lines| lines.sort_by_key(|line| line.to_lowercase()))
|
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>) {
|
pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext<Self>) {
|
||||||
self.manipulate_lines(cx, |lines| lines.reverse())
|
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)
|
fn manipulate_lines<Fn>(&mut self, cx: &mut ViewContext<Self>, mut callback: Fn)
|
||||||
where
|
where
|
||||||
Fn: FnMut(&mut [&str]),
|
Fn: FnMut(&mut Vec<&str>),
|
||||||
{
|
{
|
||||||
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 = self.buffer.read(cx).snapshot(cx);
|
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||||
|
@ -4676,6 +4698,8 @@ impl Editor {
|
||||||
let mut selections = selections.iter().peekable();
|
let mut selections = selections.iter().peekable();
|
||||||
let mut contiguous_row_selections = Vec::new();
|
let mut contiguous_row_selections = Vec::new();
|
||||||
let mut new_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() {
|
while let Some(selection) = selections.next() {
|
||||||
let (start_row, end_row) = consume_contiguous_rows(
|
let (start_row, end_row) = consume_contiguous_rows(
|
||||||
|
@ -4690,37 +4714,55 @@ impl Editor {
|
||||||
let text = buffer
|
let text = buffer
|
||||||
.text_for_range(start_point..end_point)
|
.text_for_range(start_point..end_point)
|
||||||
.collect::<String>();
|
.collect::<String>();
|
||||||
|
|
||||||
let mut lines = text.split("\n").collect_vec();
|
let mut lines = text.split("\n").collect_vec();
|
||||||
|
|
||||||
let lines_len = lines.len();
|
let lines_before = lines.len();
|
||||||
callback(&mut lines);
|
callback(&mut lines);
|
||||||
|
let lines_after = lines.len();
|
||||||
// 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"
|
|
||||||
);
|
|
||||||
|
|
||||||
edits.push((start_point..end_point, lines.join("\n")));
|
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 {
|
new_selections.push(Selection {
|
||||||
id: selection.id,
|
id: selection.id,
|
||||||
start: start_anchor.to_offset(&buffer),
|
start: start_row,
|
||||||
end: end_anchor.to_offset(&buffer),
|
end: end_row,
|
||||||
goal: SelectionGoal::None,
|
goal: SelectionGoal::None,
|
||||||
reversed: selection.reversed,
|
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| {
|
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.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| {
|
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
s.select(new_selections);
|
s.select(new_selections);
|
||||||
});
|
});
|
||||||
|
|
|
@ -2786,6 +2786,126 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
|
||||||
dddˇ»
|
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]
|
#[gpui::test]
|
||||||
|
@ -2835,6 +2955,44 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
|
||||||
ccc
|
ccc
|
||||||
ddddˇ»
|
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]
|
#[gpui::test]
|
||||||
|
|
|
@ -330,6 +330,8 @@ impl EditorElement {
|
||||||
register_action(view, cx, Editor::context_menu_next);
|
register_action(view, cx, Editor::context_menu_next);
|
||||||
register_action(view, cx, Editor::context_menu_last);
|
register_action(view, cx, Editor::context_menu_last);
|
||||||
register_action(view, cx, Editor::display_cursor_names);
|
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(
|
fn register_key_listeners(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue