on master help sign in
Fix range offsets for multiple shell insertions (#4619)
c74b974Michael Davis
helix-term/src/commands.rs+18−6
@@ -4773,6 +4773,8 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
4773 let mut ranges = SmallVec::with_capacity(selection.len());
4773 let mut ranges = SmallVec::with_capacity(selection.len());
4774 let text = doc.text().slice(..);
4774 let text = doc.text().slice(..);
4775
4775
4776+ let mut offset = 0isize;
4777+
4776 for range in selection.ranges() {
4778 for range in selection.ranges() {
4777 let fragment = range.slice(text);
4779 let fragment = range.slice(text);
4778 let (output, success) = match shell_impl(shell, cmd, pipe.then(|| fragment.into())) {
4780 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) {
4788 return;
4790 return;
4789 }
4791 }
4790
4792
4793+ let output_len = output.chars().count();
4794+
4791− let (from, to) = match behavior {
4795+ let (from, to, deleted_len) = match behavior {
4792− ShellBehavior::Replace => (range.from(), range.to()),
4796+ ShellBehavior::Replace => (range.from(), range.to(), range.len()),
4793− ShellBehavior::Insert => (range.from(), range.from()),
4797+ ShellBehavior::Insert => (range.from(), range.from(), 0),
4794− ShellBehavior::Append => (range.to(), range.to()),
4798+ ShellBehavior::Append => (range.to(), range.to(), 0),
4795− _ => (range.from(), range.from()),
4799+ _ => (range.from(), range.from(), 0),
4796 };
4800 };
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+
4798 changes.push((from, to, Some(output)));
4810 changes.push((from, to, Some(output)));
4799 }
4811 }
4800
4812
helix-term/tests/test/commands.rs+68
@@ -215,3 +215,71 @@ async fn test_multi_selection_paste() -> anyhow::Result<()> {
215
215
216 Ok(())
216 Ok(())
217}
217}
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+}