Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/editor/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,10 @@ actions!(
ReloadFile,
/// Rewraps text to fit within the preferred line length.
Rewrap,
/// Rotates selections or lines backward.
RotateBackward,
/// Rotates selections or lines forward.
RotateForward,
/// Runs flycheck diagnostics.
RunFlycheck,
/// Scrolls the cursor to the bottom of the viewport.
Expand Down
123 changes: 123 additions & 0 deletions crates/editor/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11050,6 +11050,129 @@ impl Editor {
self.manipulate_immutable_lines(window, cx, |lines| lines.shuffle(&mut rand::rng()))
}

pub fn rotate_forward(
&mut self,
_: &RotateForward,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.rotate(window, cx, false)
}

pub fn rotate_backward(
&mut self,
_: &RotateBackward,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.rotate(window, cx, true)
}

fn rotate(&mut self, window: &mut Window, cx: &mut Context<Self>, reverse: bool) {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
let display_snapshot = self.display_snapshot(cx);
let selections = self.selections.all::<usize>(&display_snapshot);

if selections.len() < 2 {
return;
}

let (edits, new_selections) = {
let buffer = self.buffer.read(cx).read(cx);
let has_selections = selections.iter().any(|s| !s.is_empty());
if has_selections {
let mut selected_texts: Vec<String> = selections
.iter()
.map(|selection| {
buffer
.text_for_range(selection.start..selection.end)
.collect()
})
.collect();

if reverse {
selected_texts.rotate_left(1);
} else {
selected_texts.rotate_right(1);
}

let mut offset_delta = 0i64;
let mut new_sels = Vec::new();
let edits: Vec<_> = selections
.iter()
.zip(selected_texts.iter())
.map(|(selection, new_text)| {
let old_len = (selection.end - selection.start) as i64;
let new_len = new_text.len() as i64;
let adjusted_start = (selection.start as i64 + offset_delta) as usize;
let adjusted_end = (adjusted_start as i64 + new_len) as usize;

new_sels.push(Selection {
id: selection.id,
start: adjusted_start,
end: adjusted_end,
reversed: selection.reversed,
goal: selection.goal,
});

offset_delta += new_len - old_len;
(selection.start..selection.end, new_text.clone())
})
.collect();
(edits, Some(new_sels))
} else {
let mut all_rows: Vec<u32> = selections
.iter()
.map(|selection| buffer.offset_to_point(selection.start).row)
.collect();
all_rows.sort_unstable();
all_rows.dedup();

if all_rows.len() < 2 {
return;
}

let line_ranges: Vec<Range<usize>> = all_rows
.iter()
.map(|&row| {
let start = Point::new(row, 0);
let end = Point::new(row, buffer.line_len(MultiBufferRow(row)));
buffer.point_to_offset(start)..buffer.point_to_offset(end)
})
.collect();

let mut line_texts: Vec<String> = line_ranges
.iter()
.map(|range| buffer.text_for_range(range.clone()).collect())
.collect();

if reverse {
line_texts.rotate_left(1);
} else {
line_texts.rotate_right(1);
}

let edits = line_ranges
.iter()
.zip(line_texts.iter())
.map(|(range, new_text)| (range.clone(), new_text.clone()))
.collect();
(edits, None)
}
};

self.transact(window, cx, |this, window, cx| {
this.buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
});
if let Some(new_sels) = new_selections {
this.change_selections(Default::default(), window, cx, |s| {
s.select(new_sels);
});
}
});
}

fn manipulate_lines<M>(
&mut self,
window: &mut Window,
Expand Down
114 changes: 114 additions & 0 deletions crates/editor/src/editor_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5719,6 +5719,120 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
});
}

#[gpui::test]
fn test_rotate(cx: &mut TestAppContext) {
init_test(cx, |_| {});

// Rotate text selections (horizontal)
let (text, selection_ranges) = marked_text_ranges("x=«1ˇ», y=«2ˇ», z=«3ˇ»", true);
_ = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple(&text, cx);
let mut editor = build_editor(buffer, window, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(selection_ranges)
});
editor.rotate_forward(&RotateForward, window, cx);
assert_eq!(editor.display_text(cx), "x=3, y=1, z=2");
assert_eq!(
editor.selections.display_ranges(cx),
vec![
DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 8),
DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 13),
]
);

editor.rotate_backward(&RotateBackward, window, cx);
assert_eq!(editor.display_text(cx), "x=1, y=2, z=3");

editor
});

// Rotate text selections (vertical)
let (text, selection_ranges) = marked_text_ranges("x=«1ˇ»\ny=«2ˇ»\nz=«3ˇ»\n", true);
_ = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple(&text, cx);
let mut editor = build_editor(buffer, window, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(selection_ranges)
});
editor.rotate_forward(&RotateForward, window, cx);
assert_eq!(editor.display_text(cx), "x=3\ny=1\nz=2\n");
assert_eq!(
editor.selections.display_ranges(cx),
vec![
DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 3),
DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 3),
]
);

editor.rotate_backward(&RotateBackward, window, cx);
assert_eq!(editor.display_text(cx), "x=1\ny=2\nz=3\n");

editor
});

// Rotate text selections (vertical, different lengths)
let (text, selection_ranges) = marked_text_ranges("x=\"«ˇ»\"\ny=\"«aˇ»\"\nz=\"«aaˇ»\"\n", true);
_ = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple(&text, cx);
let mut editor = build_editor(buffer, window, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(selection_ranges)
});
editor.rotate_forward(&RotateForward, window, cx);
assert_eq!(editor.display_text(cx), "x=\"aa\"\ny=\"\"\nz=\"a\"\n");
assert_eq!(
editor.selections.display_ranges(cx),
vec![
DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 5),
DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 3),
DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4),
]
);

editor.rotate_backward(&RotateBackward, window, cx);
assert_eq!(editor.display_text(cx), "x=\"\"\ny=\"a\"\nz=\"aa\"\n");

editor
});

// Rotate whole lines
let (text, selection_ranges) = marked_text_ranges("ˇline1\nˇline2\nˇline3\n", true);
_ = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple(&text, cx);
let mut editor = build_editor(buffer, window, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(selection_ranges)
});
editor.rotate_forward(&RotateForward, window, cx);
assert_eq!(editor.display_text(cx), "line3\nline1\nline2\n");

editor.rotate_backward(&RotateBackward, window, cx);
assert_eq!(editor.display_text(cx), "line1\nline2\nline3\n");

editor
});

// Rotate whole lines, multiple cursors per line
let (text, selection_ranges) = marked_text_ranges("ˇlinˇe1\nˇline2\nˇline3\n", true);
_ = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple(&text, cx);
let mut editor = build_editor(buffer, window, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(selection_ranges)
});
editor.rotate_forward(&RotateForward, window, cx);
assert_eq!(editor.display_text(cx), "line3\nline1\nline2\n");

editor.rotate_backward(&RotateBackward, window, cx);
assert_eq!(editor.display_text(cx), "line1\nline2\nline3\n");

editor
});
}

#[gpui::test]
fn test_move_line_up_down(cx: &mut TestAppContext) {
init_test(cx, |_| {});
Expand Down
2 changes: 2 additions & 0 deletions crates/editor/src/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ impl EditorElement {
register_action(editor, window, Editor::sort_lines_case_insensitive);
register_action(editor, window, Editor::reverse_lines);
register_action(editor, window, Editor::shuffle_lines);
register_action(editor, window, Editor::rotate_forward);
register_action(editor, window, Editor::rotate_backward);
register_action(editor, window, Editor::convert_indentation_to_spaces);
register_action(editor, window, Editor::convert_indentation_to_tabs);
register_action(editor, window, Editor::convert_to_upper_case);
Expand Down
Loading