editor: Add align selections action (#44769)
Closes #17090 Hello! This is a feature that I really wanted, so I've created an implementation of it. Hope u guys like it. It was alto mentioned at [this discussion](https://github.com/zed-industries/zed/discussions/30473) The use cases were based on the [vscode extension](https://marketplace.visualstudio.com/items?itemName=yo1dog.cursor-align) (only spaces case was implemented) - The algorithm is O(n + m × 2k), where: - n = total cursors (for initial analysis, to understand how many alignments will be performed) - m = number of alignment columns - k = number of alignment rows (first time to get the most-right occurrence, the next one to create the `Vec` of edits) Release Notes: - Added `editor: align selections` command  <br> Thank you for creating zed! --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
1 parent
be3a5e2c06
commit
c7870cb93d
4 files changed
+243
No files matched your search
@@ -883,6 +883,8 @@ actions!(
|
||||
UnwrapSyntaxNode,
|
||||
/// Wraps selections in tag specified by language.
|
||||
WrapSelectionsInTag,
|
||||
/// Aligns selections from different rows into the same column
|
||||
AlignSelections,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -11839,6 +11839,98 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn align_selections(
|
||||
&mut self,
|
||||
_: &crate::actions::AlignSelections,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
|
||||
|
||||
let display_snapshot = self.display_snapshot(cx);
|
||||
|
||||
struct CursorData {
|
||||
anchor: Anchor,
|
||||
point: Point,
|
||||
}
|
||||
let cursor_data: Vec<CursorData> = self
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
.map(|selection| {
|
||||
let anchor = if selection.reversed {
|
||||
selection.head()
|
||||
} else {
|
||||
selection.tail()
|
||||
};
|
||||
CursorData {
|
||||
anchor: anchor,
|
||||
point: anchor.to_point(&display_snapshot.buffer_snapshot()),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let rows_anchors_count: Vec<usize> = cursor_data
|
||||
.iter()
|
||||
.map(|cursor| cursor.point.row)
|
||||
.chunk_by(|&row| row)
|
||||
.into_iter()
|
||||
.map(|(_, group)| group.count())
|
||||
.collect();
|
||||
let max_columns = rows_anchors_count.iter().max().copied().unwrap_or(0);
|
||||
let mut rows_column_offset = vec![0; rows_anchors_count.len()];
|
||||
let mut edits = Vec::new();
|
||||
|
||||
for column_idx in 0..max_columns {
|
||||
let mut cursor_index = 0;
|
||||
|
||||
// Calculate target_column => position that the selections will go
|
||||
let mut target_column = 0;
|
||||
for (row_idx, cursor_count) in rows_anchors_count.iter().enumerate() {
|
||||
// Skip rows that don't have this column
|
||||
if column_idx >= *cursor_count {
|
||||
cursor_index += cursor_count;
|
||||
continue;
|
||||
}
|
||||
|
||||
let point = &cursor_data[cursor_index + column_idx].point;
|
||||
let adjusted_column = point.column + rows_column_offset[row_idx];
|
||||
if adjusted_column > target_column {
|
||||
target_column = adjusted_column;
|
||||
}
|
||||
cursor_index += cursor_count;
|
||||
}
|
||||
|
||||
// Collect edits for this column
|
||||
cursor_index = 0;
|
||||
for (row_idx, cursor_count) in rows_anchors_count.iter().enumerate() {
|
||||
// Skip rows that don't have this column
|
||||
if column_idx >= *cursor_count {
|
||||
cursor_index += *cursor_count;
|
||||
continue;
|
||||
}
|
||||
|
||||
let point = &cursor_data[cursor_index + column_idx].point;
|
||||
let spaces_needed = target_column - point.column - rows_column_offset[row_idx];
|
||||
if spaces_needed > 0 {
|
||||
let anchor = cursor_data[cursor_index + column_idx]
|
||||
.anchor
|
||||
.bias_left(&display_snapshot);
|
||||
edits.push((anchor..anchor, " ".repeat(spaces_needed as usize)));
|
||||
}
|
||||
rows_column_offset[row_idx] += spaces_needed;
|
||||
|
||||
cursor_index += *cursor_count;
|
||||
}
|
||||
}
|
||||
|
||||
if !edits.is_empty() {
|
||||
self.transact(window, cx, |editor, _window, cx| {
|
||||
editor.edit(edits, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disable_breakpoint(
|
||||
&mut self,
|
||||
_: &crate::actions::DisableBreakpoint,
|
||||
|
||||
@@ -34119,3 +34119,151 @@ async fn test_restore_and_next(cx: &mut TestAppContext) {
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_align_selections(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
// 1) one cursor, no action
|
||||
let before = " abc\n abc\nabc\n ˇabc";
|
||||
cx.set_state(before);
|
||||
cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
|
||||
cx.assert_editor_state(before);
|
||||
|
||||
// 2) multiple cursors at different rows
|
||||
let before = indoc!(
|
||||
r#"
|
||||
let aˇbc = 123;
|
||||
let xˇyz = 456;
|
||||
let fˇoo = 789;
|
||||
let bˇar = 0;
|
||||
"#
|
||||
);
|
||||
let after = indoc!(
|
||||
r#"
|
||||
let a ˇbc = 123;
|
||||
let x ˇyz = 456;
|
||||
let f ˇoo = 789;
|
||||
let bˇar = 0;
|
||||
"#
|
||||
);
|
||||
cx.set_state(before);
|
||||
cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
|
||||
cx.assert_editor_state(after);
|
||||
|
||||
// 3) multiple selections at different rows
|
||||
let before = indoc!(
|
||||
r#"
|
||||
let «ˇabc» = 123;
|
||||
let «ˇxyz» = 456;
|
||||
let «ˇfoo» = 789;
|
||||
let «ˇbar» = 0;
|
||||
"#
|
||||
);
|
||||
let after = indoc!(
|
||||
r#"
|
||||
let «ˇabc» = 123;
|
||||
let «ˇxyz» = 456;
|
||||
let «ˇfoo» = 789;
|
||||
let «ˇbar» = 0;
|
||||
"#
|
||||
);
|
||||
cx.set_state(before);
|
||||
cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
|
||||
cx.assert_editor_state(after);
|
||||
|
||||
// 4) multiple selections at different rows, inverted head
|
||||
let before = indoc!(
|
||||
r#"
|
||||
let «abcˇ» = 123;
|
||||
// comment
|
||||
let «xyzˇ» = 456;
|
||||
let «fooˇ» = 789;
|
||||
let «barˇ» = 0;
|
||||
"#
|
||||
);
|
||||
let after = indoc!(
|
||||
r#"
|
||||
let «abcˇ» = 123;
|
||||
// comment
|
||||
let «xyzˇ» = 456;
|
||||
let «fooˇ» = 789;
|
||||
let «barˇ» = 0;
|
||||
"#
|
||||
);
|
||||
cx.set_state(before);
|
||||
cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
|
||||
cx.assert_editor_state(after);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_align_selections_multicolumn(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
// 1) Multicolumn, one non affected editor row
|
||||
let before = indoc!(
|
||||
r#"
|
||||
name «|ˇ» age «|ˇ» height «|ˇ» note
|
||||
Matthew «|ˇ» 7 «|ˇ» 2333 «|ˇ» smart
|
||||
Mike «|ˇ» 1234 «|ˇ» 567 «|ˇ» lazy
|
||||
Anything that is not selected
|
||||
Miles «|ˇ» 88 «|ˇ» 99 «|ˇ» funny
|
||||
"#
|
||||
);
|
||||
let after = indoc!(
|
||||
r#"
|
||||
name «|ˇ» age «|ˇ» height «|ˇ» note
|
||||
Matthew «|ˇ» 7 «|ˇ» 2333 «|ˇ» smart
|
||||
Mike «|ˇ» 1234 «|ˇ» 567 «|ˇ» lazy
|
||||
Anything that is not selected
|
||||
Miles «|ˇ» 88 «|ˇ» 99 «|ˇ» funny
|
||||
"#
|
||||
);
|
||||
cx.set_state(before);
|
||||
cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
|
||||
cx.assert_editor_state(after);
|
||||
|
||||
// 2) not all alignment rows has the number of alignment columns
|
||||
let before = indoc!(
|
||||
r#"
|
||||
name «|ˇ» age «|ˇ» height
|
||||
Matthew «|ˇ» 7 «|ˇ» 2333
|
||||
Mike «|ˇ» 1234
|
||||
Miles «|ˇ» 88 «|ˇ» 99
|
||||
"#
|
||||
);
|
||||
let after = indoc!(
|
||||
r#"
|
||||
name «|ˇ» age «|ˇ» height
|
||||
Matthew «|ˇ» 7 «|ˇ» 2333
|
||||
Mike «|ˇ» 1234
|
||||
Miles «|ˇ» 88 «|ˇ» 99
|
||||
"#
|
||||
);
|
||||
cx.set_state(before);
|
||||
cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
|
||||
cx.assert_editor_state(after);
|
||||
|
||||
// 3) A aligned column shall stay aligned
|
||||
let before = indoc!(
|
||||
r#"
|
||||
$ ˇa ˇa
|
||||
$ ˇa ˇa
|
||||
$ ˇa ˇa
|
||||
$ ˇa ˇa
|
||||
"#
|
||||
);
|
||||
let after = indoc!(
|
||||
r#"
|
||||
$ ˇa ˇa
|
||||
$ ˇa ˇa
|
||||
$ ˇa ˇa
|
||||
$ ˇa ˇa
|
||||
"#
|
||||
);
|
||||
cx.set_state(before);
|
||||
cx.update_editor(|e, window, cx| e.align_selections(&AlignSelections, window, cx));
|
||||
cx.assert_editor_state(after);
|
||||
}
|
||||
@@ -653,6 +653,7 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::enable_breakpoint);
|
||||
register_action(editor, window, Editor::disable_breakpoint);
|
||||
register_action(editor, window, Editor::toggle_read_only);
|
||||
register_action(editor, window, Editor::align_selections);
|
||||
if editor.read(cx).enable_wrap_selections_in_tag(cx) {
|
||||
register_action(editor, window, Editor::wrap_selections_in_tag);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user