diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index bc95ee50..0fbe20f5 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -794,6 +794,7 @@ fn candidate_body_for_new_line<'a>( /// - The node from which to start the query (this is non-trivial due to `@extend` captures) /// - The indent captures for all relevant nodes. #[allow(clippy::too_many_arguments)] +#[allow(clippy::type_complexity)] fn init_indent_query<'a, 'b>( query: &IndentQuery, root: &Node<'a>, diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 9e8cae00..3bc9714a 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5474,19 +5474,44 @@ type CommentTransactionFn = fn( ) -> Transaction; fn toggle_comments_impl(cx: &mut Context, comment_transaction: CommentTransactionFn) { - let loader = cx.editor.syn_loader.load(); + let loader: &helix_core::syntax::Loader = &cx.editor.syn_loader.load(); let (view, doc) = current!(cx.editor); - // Pick the comment tokens of the layer at the primary cursor. let cursor = doc .selection(view.id) .primary() .cursor(doc.text().slice(..)); let byte_pos = doc.text().char_to_byte(cursor); - let lang_config = doc.language_config_at(&loader, byte_pos); + // Resolve the comment tokens from the enclosing injection layer that owns the comment, + // not the innermost layer at the cursor. Prefer the innermost layer that defines + // *line* comment tokens, falling back to the innermost layer with block tokens. + let mut line_layer = None; + let mut block_layer = None; + if let Some(syntax) = doc.syntax() { + for layer in syntax.layers_for_byte_range(byte_pos as u32, byte_pos as u32) { + let language = syntax.layer(layer).language; + let config = loader.language(language).config(); + if config.comment_tokens.is_some() { + line_layer = Some(language); + } + if config.block_comment_tokens.is_some() { + block_layer = Some(language); + } + } + } + let lang_config = line_layer + .or(block_layer) + .map(|language| &**loader.language(language).config()) + .or_else(|| doc.language_config()); + + // Pick the token the cursor's line is already commented with (longest match, so `///` wins over `//`). + // If the line isn't commented yet, fall back to the primary token for adding a comment. + let cursor_line = doc.text().char_to_line(cursor); let line_token: Option<&str> = lang_config .and_then(|lc| lc.comment_tokens.as_ref()) - .and_then(|tc| tc.first()) - .map(|tc| tc.as_str()); + .and_then(|tokens| { + comment::get_comment_token(doc.text().slice(..), tokens, cursor_line) + .or_else(|| tokens.first().map(|token| token.as_str())) + }); let block_tokens: Option<&[BlockCommentToken]> = lang_config .and_then(|lc| lc.block_comment_tokens.as_ref()) .map(|tc| &tc[..]); diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 97653c7e..91d7de28 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -722,6 +722,65 @@ async fn test_join_selections_comment() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn test_toggle_comments_inside_comment_injection() -> anyhow::Result<()> { + // A `//` line comment's text is injected as the `comment` language, which has no + // comment-tokens of its own. With the cursor inside the comment, toggling must + // resolve tokens from the enclosing language and un-comment the line, + // not fall back to the hardcoded default `#`. + test(( + indoc! {"\ + // #[a|]#bc + "}, + ":lang rust", + indoc! {"\ + #[a|]#bc + "}, + )) + .await?; + + // A `///` doc comment's text is injected as markdown (no line comment token of + // its own). Toggling must strip the whole `///` marker via Rust's tokens rather + // than insert a markdown `` inside or leave a stray `/`. + test(( + indoc! {"\ + /// #[a|]#bc + "}, + ":lang rust", + indoc! {"\ + #[a|]#bc + "}, + )) + .await?; + + // Likewise for the `//!` inner doc comment marker. + test(( + indoc! {"\ + //! #[a|]#bc + "}, + ":lang rust", + indoc! {"\ + #[a|]#bc + "}, + )) + .await?; + + // Commenting a normal code line still uses the top-level language's token + // (no injection layer at the cursor), no regression for the common case. + test(( + indoc! {"\ + #[l|]#et x = 5; + "}, + ":lang rust", + indoc! {"\ + // #[l|]#et x = 5; + "}, + )) + .await?; + + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn test_read_file() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?;