diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a3417ea1b..219744faa 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5319,46 +5319,71 @@ fn rotate_selections_last(cx: &mut Context) { doc.set_selection(view.id, selection); } +#[derive(Debug)] enum ReorderStrategy { RotateForward, RotateBackward, Reverse, } +/// Like `usize::wrapping_sub`, but provide a custom range from `0` to `range_length` +fn wrapping_sub_in_range(index: usize, range_length: usize, delta: usize) -> usize { + (index + range_length - delta) % range_length +} + +/// Like `usize::wrapping_add`, but provide a custom range from `0` to `range_length` +fn wrapping_add_in_range(index: usize, range_length: usize, delta: usize) -> usize { + (index + range_length + delta) % range_length +} + fn reorder_selection_contents(cx: &mut Context, strategy: ReorderStrategy) { let count = cx.count; let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); let selection = doc.selection(view.id); - let mut fragments: Vec<_> = selection + + let mut ranges: Vec<_> = selection .slices(text) .map(|fragment| fragment.chunks().collect()) .collect(); - let group = count - .map(|count| count.get()) - .unwrap_or(fragments.len()) // default to rotating everything as one group - .min(fragments.len()); + let rotate_by = count.map_or(1, |count| count.get().min(ranges.len())); - for chunk in fragments.chunks_mut(group) { - // TODO: also modify main index - match strategy { - ReorderStrategy::RotateForward => chunk.rotate_right(1), - ReorderStrategy::RotateBackward => chunk.rotate_left(1), - ReorderStrategy::Reverse => chunk.reverse(), - }; - } + let primary_index = match strategy { + ReorderStrategy::RotateForward => { + ranges.rotate_right(rotate_by); + wrapping_add_in_range(selection.primary_index(), ranges.len(), rotate_by) + } + ReorderStrategy::RotateBackward => { + ranges.rotate_left(rotate_by); + wrapping_sub_in_range(selection.primary_index(), ranges.len(), rotate_by) + } + ReorderStrategy::Reverse => { + if rotate_by % 2 == 0 { + // nothing changed, if we reverse something an even + // amount of times, the output will be the same + return; + } + ranges.reverse(); + // -1 to turn 1-based len into 0-based index + (ranges.len() - 1) - selection.primary_index() + } + }; let transaction = Transaction::change( doc.text(), selection .ranges() .iter() - .zip(fragments) + .zip(ranges) .map(|(range, fragment)| (range.from(), range.to(), Some(fragment))), ); + doc.set_selection( + view.id, + Selection::new(selection.ranges().into(), primary_index), + ); doc.apply(&transaction, view.id); } diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 20e8ac9a7..90ff4cf0c 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -4,6 +4,8 @@ use super::*; mod insert; mod movement; +mod reverse_selection_contents; +mod rotate_selection_contents; mod write; #[tokio::test(flavor = "multi_thread")] diff --git a/helix-term/tests/test/commands/reverse_selection_contents.rs b/helix-term/tests/test/commands/reverse_selection_contents.rs new file mode 100644 index 000000000..043a03c3f --- /dev/null +++ b/helix-term/tests/test/commands/reverse_selection_contents.rs @@ -0,0 +1,49 @@ +use super::*; + +const A: &str = indoc! {" + #(a|)# + #(b|)# + #(c|)# + #[d|]# + #(e|)#" +}; +const A_REV: &str = indoc! {" + #(e|)# + #[d|]# + #(c|)# + #(b|)# + #(a|)#" +}; +const B: &str = indoc! {" + #(a|)# + #(b|)# + #[c|]# + #(d|)# + #(e|)#" +}; +const B_REV: &str = indoc! {" + #(e|)# + #(d|)# + #[c|]# + #(b|)# + #(a|)#" +}; + +const CMD: &str = "?reverse_selection_contents"; + +#[tokio::test(flavor = "multi_thread")] +async fn reverse_selection_contents() -> anyhow::Result<()> { + test((A, CMD, A_REV)).await?; + test((B, CMD, B_REV)).await?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn reverse_selection_contents_with_count() -> anyhow::Result<()> { + test((B, format!("2{CMD}"), B)).await?; + test((B, format!("3{CMD}"), B_REV)).await?; + test((B, format!("4{CMD}"), B)).await?; + + Ok(()) +} diff --git a/helix-term/tests/test/commands/rotate_selection_contents.rs b/helix-term/tests/test/commands/rotate_selection_contents.rs new file mode 100644 index 000000000..b19300059 --- /dev/null +++ b/helix-term/tests/test/commands/rotate_selection_contents.rs @@ -0,0 +1,71 @@ +use super::*; + +// Progression: A -> B -> C -> D +// as we press `A-)` +const A: &str = indoc! {" + #(a|)# + #(b|)# + #(c|)# + #[d|]# + #(e|)#" +}; + +const B: &str = indoc! {" + #(e|)# + #(a|)# + #(b|)# + #(c|)# + #[d|]#" +}; + +const C: &str = indoc! {" + #[d|]# + #(e|)# + #(a|)# + #(b|)# + #(c|)#" +}; + +const D: &str = indoc! {" + #(c|)# + #[d|]# + #(e|)# + #(a|)# + #(b|)#" +}; + +#[tokio::test(flavor = "multi_thread")] +async fn rotate_selection_contents_forward_repeated() -> anyhow::Result<()> { + test((A, "", B)).await?; + test((B, "", C)).await?; + test((C, "", D)).await?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn rotate_selection_contents_forward_with_count() -> anyhow::Result<()> { + test((A, "2", C)).await?; + test((A, "3", D)).await?; + test((B, "2", D)).await?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn rotate_selection_contents_backward_repeated() -> anyhow::Result<()> { + test((D, "", C)).await?; + test((C, "", B)).await?; + test((B, "", A)).await?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn rotate_selection_contents_backward_with_count() -> anyhow::Result<()> { + test((D, "2", B)).await?; + test((D, "3", A)).await?; + test((C, "2", A)).await?; + + Ok(()) +}