diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 98a84673375c..42a7e4c067bb 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4813,6 +4813,8 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { let mut ranges = SmallVec::with_capacity(selection.len()); let text = doc.text().slice(..); + let mut offset = 0isize; + for range in selection.ranges() { let fragment = range.slice(text); let (output, success) = match shell_impl(shell, cmd, pipe.then(|| fragment.into())) { @@ -4828,13 +4830,23 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { return; } - let (from, to) = match behavior { - ShellBehavior::Replace => (range.from(), range.to()), - ShellBehavior::Insert => (range.from(), range.from()), - ShellBehavior::Append => (range.to(), range.to()), - _ => (range.from(), range.from()), + let output_len = output.chars().count(); + + let (from, to, deleted_len) = match behavior { + ShellBehavior::Replace => (range.from(), range.to(), range.len()), + ShellBehavior::Insert => (range.from(), range.from(), 0), + ShellBehavior::Append => (range.to(), range.to(), 0), + _ => (range.from(), range.from(), 0), }; - ranges.push(Range::new(to, to + output.chars().count())); + + // These `usize`s cannot underflow because selection ranges cannot overlap. + // Once the MSRV is 1.66.0 (mixed_integer_ops is stabilized), we can use checked + // arithmetic to assert this. + let anchor = (to as isize + offset - deleted_len as isize) as usize; + let new_range = Range::new(anchor, anchor + output_len).with_direction(range.direction()); + ranges.push(new_range); + offset = offset + output_len as isize - deleted_len as isize; + changes.push((from, to, Some(output))); } diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index e78e6c9f9964..114bf22211a5 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -215,3 +215,71 @@ async fn test_multi_selection_paste() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn test_multi_selection_shell_commands() -> anyhow::Result<()> { + // pipe + test(( + platform_line(indoc! {"\ + #[|lorem]# + #(|ipsum)# + #(|dolor)# + "}) + .as_str(), + "|echo foo", + platform_line(indoc! {"\ + #[|foo + ]# + #(|foo + )# + #(|foo + )# + "}) + .as_str(), + )) + .await?; + + // insert-output + test(( + platform_line(indoc! {"\ + #[|lorem]# + #(|ipsum)# + #(|dolor)# + "}) + .as_str(), + "!echo foo", + platform_line(indoc! {"\ + #[|foo + ]#lorem + #(|foo + )#ipsum + #(|foo + )#dolor + "}) + .as_str(), + )) + .await?; + + // append-output + test(( + platform_line(indoc! {"\ + #[|lorem]# + #(|ipsum)# + #(|dolor)# + "}) + .as_str(), + "echo foo", + platform_line(indoc! {"\ + lorem#[|foo + ]# + ipsum#(|foo + )# + dolor#(|foo + )# + "}) + .as_str(), + )) + .await?; + + Ok(()) +}