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

Issue:
Incorrect behaviour for adding newline before a line with a comment on it
## Example 1

If you have a comment and your cursor is at |:

```rs
/// this is a comme|nt
```

Pressing `I` then `Enter` will have this effect:

```rs
/// |/// this is a comment
```

While this was expected:

```rs
<newline>
|/// this is a comment
```

## Example 2

Also, if you have that same comment *at the top of the file*:

```rs
/// this is a comm|ent
```

Pressing `O` will have this effect:

```rs
<newline>
/// | /// this is a comment
```

While this was expected

```rs
/// |
/// this is a comment
```


Discussion / hints:
Ooof, I think this is introduced from my PR ;-; https://github.com/helix-editor/helix/pull/10996 I can try to create a follow PR for this but currently it's a bit full for me but I can add a fix until the end of next week I think

Fix patch:
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 27c852a7b2e2..6a35e812b8b0 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -3458,43 +3458,42 @@ fn open(cx: &mut Context, open: Open) {
     let selection = doc.selection(view.id);
 
     let mut ranges = SmallVec::with_capacity(selection.len());
-    let mut offs = 0;
 
     let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
-        let cursor_line = text.char_to_line(match open {
+        // the line number, where the cursor is currently
+        let curr_line_num = text.char_to_line(match open {
             Open::Below => graphemes::prev_grapheme_boundary(text, range.to()),
             Open::Above => range.from(),
         });
 
-        let new_line = match open {
-            // adjust position to the end of the line (next line - 1)
-            Open::Below => cursor_line + 1,
-            // adjust position to the end of the previous line (current line - 1)
-            Open::Above => cursor_line,
+        // the next line number, where the cursor will be, after finishing the transaction
+        let next_new_line_num = match open {
+            Open::Below => curr_line_num + 1,
+            Open::Above => curr_line_num,
         };
 
-        let line_num = new_line.saturating_sub(1);
+        let above_next_new_line_num = next_new_line_num.saturating_sub(1);
+
+        let continue_comment_token = if doc.config.load().continue_comments {
+            doc.language_config()
+                .and_then(|config| config.comment_tokens.as_ref())
+                .and_then(|tokens| comment::get_comment_token(text, tokens, curr_line_num))
+        } else {
+            None
+        };
 
         // Index to insert newlines after, as well as the char width
         // to use to compensate for those inserted newlines.
-        let (line_end_index, line_end_offset_width) = if new_line == 0 {
+        let (above_next_line_end_index, above_next_line_end_width) = if next_new_line_num == 0 {
             (0, 0)
         } else {
             (
-                line_end_char_index(&text, line_num),
+                line_end_char_index(&text, above_next_new_line_num),
                 doc.line_ending.len_chars(),
             )
         };
 
-        let continue_comment_token = if doc.config.load().continue_comments {
-            doc.language_config()
-                .and_then(|config| config.comment_tokens.as_ref())
-                .and_then(|tokens| comment::get_comment_token(text, tokens, cursor_line))
-        } else {
-            None
-        };
-
-        let line = text.line(cursor_line);
+        let line = text.line(curr_line_num);
         let indent = match line.first_non_whitespace_char() {
             Some(pos) if continue_comment_token.is_some() => line.slice(..pos).to_string(),
             _ => indent::indent_for_newline(
@@ -3504,26 +3503,36 @@ fn open(cx: &mut Context, open: Open) {
                 &doc.indent_style,
                 doc.tab_width(),
                 text,
-                line_num,
-                line_end_index,
-                cursor_line,
+                above_next_new_line_num,
+                above_next_line_end_index,
+                curr_line_num,
             ),
         };
 
         let indent_len = indent.len();
         let mut text = String::with_capacity(1 + indent_len);
-        text.push_str(doc.line_ending.as_str());
-        text.push_str(&indent);
 
-        if let Some(token) = continue_comment_token {
-            text.push_str(token);
-            text.push(' ');
+        if open == Open::Above && next_new_line_num == 0 {
+            text.push_str(&indent);
+            if let Some(token) = continue_comment_token {
+                text.push_str(token);
+                text.push(' ');
+            }
+            text.push_str(doc.line_ending.as_str());
+        } else {
+            text.push_str(doc.line_ending.as_str());
+            text.push_str(&indent);
+
+            if let Some(token) = continue_comment_token {
+                text.push_str(token);
+                text.push(' ');
+            }
         }
 
         let text = text.repeat(count);
 
         // calculate new selection ranges
-        let pos = offs + line_end_index + line_end_offset_width;
+        let pos = 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();
@@ -3536,9 +3545,11 @@ fn open(cx: &mut Context, open: Open) {
             ));
         }
 
-        offs += text.chars().count();
-
-        (line_end_index, line_end_index, Some(text.into()))
+        (
+            above_next_line_end_index,
+            above_next_line_end_index,
+            Some(text.into()),
+        )
     });
 
     transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));


Test patch:
diff --git a/helix-term/tests/test/commands/insert.rs b/helix-term/tests/test/commands/insert.rs
index ac2d179aebdb..f7aa4a020fb4 100644
--- a/helix-term/tests/test/commands/insert.rs
+++ b/helix-term/tests/test/commands/insert.rs
@@ -119,3 +119,128 @@ async fn insert_newline_continue_line_comment() -> anyhow::Result<()> {
 
     Ok(())
 }
+
+/// NOTE: Language is set to markdown to check if the indentation is correct for the new line
+#[tokio::test(flavor = "multi_thread")]
+async fn test_open_above() -> anyhow::Result<()> {
+    // `O` is pressed in the first line
+    test((
+        indoc! {"Helix #[is|]# cool"},
+        ":lang markdown<ret>O",
+        indoc! {"\
+            #[\n|]#
+            Helix is cool
+        "},
+    ))
+    .await?;
+
+    // `O` is pressed in the first line, but the current line has some indentation
+    test((
+        indoc! {"\
+            ··This line has 2 spaces in front of it#[\n|]#
+        "}
+        .replace('·', " "),
+        ":lang markdown<ret>Oa",
+        indoc! {"\
+            ··a#[\n|]#
+            ··This line has 2 spaces in front of it
+        "}
+        .replace('·', " "),
+    ))
+    .await?;
+
+    // `O` is pressed but *not* in the first line
+    test((
+        indoc! {"\
+            I use
+            b#[t|]#w.
+        "},
+        ":lang markdown<ret>Oarch",
+        indoc! {"\
+            I use
+            arch#[\n|]#
+            btw.
+        "},
+    ))
+    .await?;
+
+    // `O` is pressed but *not* in the first line and the line has some indentation
+    test((
+        indoc! {"\
+            I use
+            ····b#[t|]#w.
+        "}
+        .replace("·", " "),
+        ":lang markdown<ret>Ohelix",
+        indoc! {"\
+            I use
+            ····helix#[\n|]#
+            ····btw.
+        "}
+        .replace("·", " "),
+    ))
+    .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<()> {
+    // `O` is pressed in the first line inside a line comment
+    test((
+        indoc! {"// a commen#[t|]#"},
+        ":lang rust<ret>O",
+        indoc! {"\
+            // #[\n|]#
+            // a comment
+        "},
+    ))
+    .await?;
+
+    // `O` is pressed in the first line inside a line comment, but with indentation
+    test((
+        indoc! {"····// a comm#[e|]#nt"}.replace("·", " "),
+        ":lang rust<ret>O",
+        indoc! {"\
+            ····// #[\n|]#
+            ····// a comment
+        "}
+        .replace("·", " "),
+    ))
+    .await?;
+
+    // `O` is pressed but not in the first line but inside a line comment
+    test((
+        indoc! {"\
+            fn main() { }
+            // yeetus deletus#[\n|]#
+        "},
+        ":lang rust<ret>O",
+        indoc! {"\
+            fn main() { }
+            // #[\n|]#
+            // yeetus deletus
+        "},
+    ))
+    .await?;
+
+    // `O` is pressed but not in the first line but inside a line comment and with indentation
+    test((
+        indoc! {"\
+            fn main() { }
+            ····// yeetus deletus#[\n|]#
+        "}
+        .replace("·", " "),
+        ":lang rust<ret>O",
+        indoc! {"\
+            fn main() { }
+            ····// #[\n|]#
+            ····// yeetus deletus
+        "}
+        .replace("·", " "),
+    ))
+    .await?;
+
+    Ok(())
+}