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