Repository: helix-editor/helix (language: rust)

Issue:
Open above and below are broken in multi-range selections
### Summary

`open_above` and `open_below` are both broken on the secondary ranges of a multi-range selection. The secondary cursor ends up on a line above where it should be for both commands. e.g.,

```toml
[workspace]
resolver = "2"
members = [
  "#[|h]#elix-core",
  "#(|h)#elix-view",
  "#(|h)#elix-term",
  "helix-tui",
  "helix-lsp-types",
  "helix-lsp",
  "helix-event",
  "helix-dap",
  "helix-loader",
  "helix-vcs",
  "helix-parsec",
  "helix-stdx",
  "xtask",
]
```

Pressing `o` becomes:

```toml
[workspace]
resolver = "2"
members = [
  "helix-core",
  #[|]#
  "helix-view",#(|)#

  "helix-ter#(|m)#",

  "helix-tui",
  "helix-lsp-types",
  "helix-lsp",
  "helix-event",
  "helix-dap",
  "helix-loader",
  "helix-vcs",
  "helix-parsec",
  "helix-stdx",
  "xtask",
]
```

and pressing `O` becomes:

```toml
[workspace]
resolver = "2"
members = [
  #[|]#
  "helix-core",#(|)#

  "helix-vie#(|w)#",

  "helix-term",
  "helix-tui",
  "helix-lsp-types",
  "helix-lsp",
  "helix-event",
  "helix-dap",
  "helix-loader",
  "helix-vcs",
  "helix-parsec",
  "helix-stdx",
  "xtask",
]
```

From a bisect, it looks like the breakage happened in https://github.com/helix-editor/helix/pull/12215


Discussion / hints:
I thought something seemed off, this was it. I can confirm this behavior too. Thought it was me doing wrong selections, and it ended up wonky because of that.
Ooof, damn it, sorry.
I can investigate that (and add a test for the future xD).

Fix patch:
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 3b90648742dd..755a7dc077bb 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -3474,6 +3474,7 @@ fn open(cx: &mut Context, open: Open) {
     let text = doc.text().slice(..);
     let contents = doc.text();
     let selection = doc.selection(view.id);
+    let mut offs = 0;
 
     let mut ranges = SmallVec::with_capacity(selection.len());
 
@@ -3550,7 +3551,7 @@ fn open(cx: &mut Context, open: Open) {
         let text = text.repeat(count);
 
         // calculate new selection ranges
-        let pos = above_next_line_end_index + above_next_line_end_width;
+        let pos = offs + above_next_line_end_index + above_next_line_end_width;
         let comment_len = continue_comment_token
             .map(|token| token.len() + 1) // `+ 1` for the extra space added
             .unwrap_or_default();
@@ -3563,6 +3564,9 @@ fn open(cx: &mut Context, open: Open) {
             ));
         }
 
+        // update the offset for the next range
+        offs += text.chars().count();
+
         (
             above_next_line_end_index,
             above_next_line_end_index,


Test patch:
diff --git a/helix-term/tests/test/commands/insert.rs b/helix-term/tests/test/commands/insert.rs
index f7aa4a020fb4..f23876dfbf8e 100644
--- a/helix-term/tests/test/commands/insert.rs
+++ b/helix-term/tests/test/commands/insert.rs
@@ -184,6 +184,127 @@ async fn test_open_above() -> anyhow::Result<()> {
     Ok(())
 }
 
+#[tokio::test(flavor = "multi_thread")]
+async fn test_open_above_with_multiple_cursors() -> anyhow::Result<()> {
+    // the primary cursor is also in the top line
+    test((
+        indoc! {"#[H|]#elix
+            #(i|)#s
+            #(c|)#ool"},
+        "O",
+        indoc! {
+            "#[\n|]#
+            Helix
+            #(\n|)#
+            is
+            #(\n|)#
+            cool
+            "
+        },
+    ))
+    .await?;
+
+    // now with some additional indentation
+    test((
+        indoc! {"····#[H|]#elix
+            ····#(i|)#s
+            ····#(c|)#ool"}
+        .replace("·", " "),
+        ":indent-style 4<ret>O",
+        indoc! {
+            "····#[\n|]#
+            ····Helix
+            ····#(\n|)#
+            ····is
+            ····#(\n|)#
+            ····cool
+            "
+        }
+        .replace("·", " "),
+    ))
+    .await?;
+
+    // the first line is within a comment, the second not.
+    // However, if we open above, the first newly added line should start within a comment
+    // while the other should be a normal line
+    test((
+        indoc! {"fn main() {
+                // #[VIP|]# comment
+                l#(e|)#t yes = false;
+            }"},
+        ":lang rust<ret>O",
+        indoc! {"fn main() {
+                // #[\n|]#
+                // VIP comment
+                #(\n|)#
+                let yes = false;
+            }"},
+    ))
+    .await?;
+
+    Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_open_below_with_multiple_cursors() -> anyhow::Result<()> {
+    // the primary cursor is also in the top line
+    test((
+        indoc! {"#[H|]#elix
+            #(i|)#s
+            #(c|)#ool"},
+        "o",
+        indoc! {"Helix
+            #[\n|]#
+            is
+            #(\n|)#
+            cool
+            #(\n|)#
+            "
+        },
+    ))
+    .await?;
+
+    // now with some additional indentation
+    test((
+        indoc! {"····#[H|]#elix
+            ····#(i|)#s
+            ····#(c|)#ool"}
+        .replace("·", " "),
+        ":indent-style 4<ret>o",
+        indoc! {
+            "····Helix
+            ····#[\n|]#
+            ····is
+            ····#(\n|)#
+            ····cool
+            ····#(\n|)#
+            "
+        }
+        .replace("·", " "),
+    ))
+    .await?;
+
+    // the first line is within a comment, the second not.
+    // However, if we open below, the first newly added line should start within a comment
+    // while the other should be a normal line
+    test((
+        indoc! {"fn main() {
+                // #[VIP|]# comment
+                l#(e|)#t yes = false;
+            }"},
+        ":lang rust<ret>o",
+        indoc! {"fn main() {
+                // VIP comment
+                // #[\n|]#
+                let yes = false;
+                #(\n|)#
+            }"},
+    ))
+    .await?;
+
+    Ok(())
+}
+
 /// NOTE: To make the `open_above` comment-aware, we're setting the language for each test to rust.
 #[tokio::test(flavor = "multi_thread")]
 async fn test_open_above_with_comments() -> anyhow::Result<()> {