on master help sign in
Fix range offsets for multiple shell insertions (#4619)

d6323b7cbc21a9d3ba29738c76581dad93f9f415 introduced a regression for shell commands like `|`, `!`, and `<A-!>` which caused the new selections to be incorrect. This caused a panic when piping (`|`) would cause the new range to extend past the document end. The paste version of this bug was fixed in 48a3965ab43718ce2a49724cbcc294b04c328b81. This change also inherits the direction of the new range from the old range and adds integration tests to ensure that the behavior isn't broken in the future.

commitc74b97447f7a84f7728ce41c796f435371563a68 authorMichael Davis <mcarsondavis@gmail.com> dateSun 13 Nov 2022, 18:45 (4 years ago) parentfd585c1
2 files changed+86−6
helix-term/src/commands.rs+18−6 helix-term/tests/test/commands.rs+68
helix-term/src/commands.rs+18−6
@@ -4773,6 +4773,8 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
47734773 let mut ranges = SmallVec::with_capacity(selection.len());
47744774 let text = doc.text().slice(..);
47754775
4776+ let mut offset = 0isize;
4777+
47764778 for range in selection.ranges() {
47774779 let fragment = range.slice(text);
47784780 let (output, success) = match shell_impl(shell, cmd, pipe.then(|| fragment.into())) {
@@ -4788,13 +4790,23 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
47884790 return;
47894791 }
47904792
4791− let (from, to) = match behavior {
4792− ShellBehavior::Replace => (range.from(), range.to()),
4793− ShellBehavior::Insert => (range.from(), range.from()),
4794− ShellBehavior::Append => (range.to(), range.to()),
4795− _ => (range.from(), range.from()),
4793+ let output_len = output.chars().count();
4794+
4795+ let (from, to, deleted_len) = match behavior {
4796+ ShellBehavior::Replace => (range.from(), range.to(), range.len()),
4797+ ShellBehavior::Insert => (range.from(), range.from(), 0),
4798+ ShellBehavior::Append => (range.to(), range.to(), 0),
4799+ _ => (range.from(), range.from(), 0),
47964800 };
4797− ranges.push(Range::new(to, to + output.chars().count()));
4801+
4802+ // These `usize`s cannot underflow because selection ranges cannot overlap.
4803+ // Once the MSRV is 1.66.0 (mixed_integer_ops is stabilized), we can use checked
4804+ // arithmetic to assert this.
4805+ let anchor = (to as isize + offset - deleted_len as isize) as usize;
4806+ let new_range = Range::new(anchor, anchor + output_len).with_direction(range.direction());
4807+ ranges.push(new_range);
4808+ offset = offset + output_len as isize - deleted_len as isize;
4809+
47984810 changes.push((from, to, Some(output)));
47994811 }
48004812
helix-term/tests/test/commands.rs+68
@@ -215,3 +215,71 @@ async fn test_multi_selection_paste() -> anyhow::Result<()> {
215215
216216 Ok(())
217217}
218+
219+#[tokio::test(flavor = "multi_thread")]
220+async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
221+ // pipe
222+ test((
223+ platform_line(indoc! {"\
224+ #[|lorem]#
225+ #(|ipsum)#
226+ #(|dolor)#
227+ "})
228+ .as_str(),
229+ "|echo foo<ret>",
230+ platform_line(indoc! {"\
231+ #[|foo
232+ ]#
233+ #(|foo
234+ )#
235+ #(|foo
236+ )#
237+ "})
238+ .as_str(),
239+ ))
240+ .await?;
241+
242+ // insert-output
243+ test((
244+ platform_line(indoc! {"\
245+ #[|lorem]#
246+ #(|ipsum)#
247+ #(|dolor)#
248+ "})
249+ .as_str(),
250+ "!echo foo<ret>",
251+ platform_line(indoc! {"\
252+ #[|foo
253+ ]#lorem
254+ #(|foo
255+ )#ipsum
256+ #(|foo
257+ )#dolor
258+ "})
259+ .as_str(),
260+ ))
261+ .await?;
262+
263+ // append-output
264+ test((
265+ platform_line(indoc! {"\
266+ #[|lorem]#
267+ #(|ipsum)#
268+ #(|dolor)#
269+ "})
270+ .as_str(),
271+ "<A-!>echo foo<ret>",
272+ platform_line(indoc! {"\
273+ lorem#[|foo
274+ ]#
275+ ipsum#(|foo
276+ )#
277+ dolor#(|foo
278+ )#
279+ "})
280+ .as_str(),
281+ ))
282+ .await?;
283+
284+ Ok(())
285+}