diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 27c852a7b..6a35e812b 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); - - // 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 { - (0, 0) - } else { - ( - line_end_char_index(&text, line_num), - doc.line_ending.len_chars(), - ) - }; + 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, cursor_line)) + .and_then(|tokens| comment::get_comment_token(text, tokens, curr_line_num)) } else { None }; - let line = text.line(cursor_line); + // Index to insert newlines after, as well as the char width + // to use to compensate for those inserted newlines. + 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, above_next_new_line_num), + doc.line_ending.len_chars(), + ) + }; + + 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())); diff --git a/helix-term/tests/test/commands/insert.rs b/helix-term/tests/test/commands/insert.rs index ac2d179ae..f7aa4a020 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 markdownO", + 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 markdownOa", + 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 markdownOarch", + 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 markdownOhelix", + 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 rustO", + 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 rustO", + 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 rustO", + 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 rustO", + indoc! {"\ + fn main() { } + ····// #[\n|]# + ····// yeetus deletus + "} + .replace("·", " "), + )) + .await?; + + Ok(()) +}