Skip to content

Commit ea98ec8

Browse files
the-mikedavisFrederik Vestre
authored andcommitted
Fix range offsets for multiple shell insertions (helix-editor#4619)
d6323b7 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 48a3965. 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.
1 parent eb6b3b7 commit ea98ec8

2 files changed

Lines changed: 86 additions & 6 deletions

File tree

helix-term/src/commands.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)