ide_assists/handlers/
convert_comment_block.rsuse itertools::Itertools;
use syntax::{
ast::{self, edit::IndentLevel, Comment, CommentKind, CommentShape, Whitespace},
AstToken, Direction, SyntaxElement, TextRange,
};
use crate::{AssistContext, AssistId, AssistKind, Assists};
pub(crate) fn convert_comment_block(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
let comment = ctx.find_token_at_offset::<ast::Comment>()?;
if let Some(prev) = comment.syntax().prev_token() {
Whitespace::cast(prev).filter(|w| w.text().contains('\n'))?;
}
match comment.kind().shape {
ast::CommentShape::Block => block_to_line(acc, comment),
ast::CommentShape::Line => line_to_block(acc, comment),
}
}
fn block_to_line(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
let target = comment.syntax().text_range();
acc.add(
AssistId("block_to_line", AssistKind::RefactorRewrite),
"Replace block comment with line comments",
target,
|edit| {
let indentation = IndentLevel::from_token(comment.syntax());
let line_prefix = CommentKind { shape: CommentShape::Line, ..comment.kind() }.prefix();
let text = comment.text();
let text = &text[comment.prefix().len()..(text.len() - "*/".len())].trim();
let lines = text.lines().peekable();
let indent_spaces = indentation.to_string();
let output = lines
.map(|line| {
let line = line.trim_start_matches(&indent_spaces);
if line.is_empty() {
line_prefix.to_owned()
} else {
format!("{line_prefix} {line}")
}
})
.join(&format!("\n{indent_spaces}"));
edit.replace(target, output)
},
)
}
fn line_to_block(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
let comments = relevant_line_comments(&comment);
let target = TextRange::new(
comments[0].syntax().text_range().start(),
comments.last()?.syntax().text_range().end(),
);
acc.add(
AssistId("line_to_block", AssistKind::RefactorRewrite),
"Replace line comments with a single block comment",
target,
|edit| {
let indentation = IndentLevel::from_token(comment.syntax());
let block_comment_body = comments
.into_iter()
.map(|c| line_comment_text(indentation, c))
.collect::<Vec<String>>()
.into_iter()
.join("\n");
let block_prefix =
CommentKind { shape: CommentShape::Block, ..comment.kind() }.prefix();
let output = format!("{block_prefix}\n{block_comment_body}\n{indentation}*/");
edit.replace(target, output)
},
)
}
pub(crate) fn relevant_line_comments(comment: &ast::Comment) -> Vec<Comment> {
let prefix = comment.prefix();
let same_prefix = |c: &ast::Comment| c.prefix() == prefix;
let skippable = |not: &SyntaxElement| {
not.clone()
.into_token()
.and_then(Whitespace::cast)
.map(|w| !w.spans_multiple_lines())
.unwrap_or(false)
};
let prev_comments = comment
.syntax()
.siblings_with_tokens(Direction::Prev)
.filter(|s| !skippable(s))
.map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
.take_while(|opt_com| opt_com.is_some())
.flatten()
.skip(1); let next_comments = comment
.syntax()
.siblings_with_tokens(Direction::Next)
.filter(|s| !skippable(s))
.map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
.take_while(|opt_com| opt_com.is_some())
.flatten();
let mut comments: Vec<_> = prev_comments.collect();
comments.reverse();
comments.extend(next_comments);
comments
}
pub(crate) fn line_comment_text(indentation: IndentLevel, comm: ast::Comment) -> String {
let text = comm.text();
let contents_without_prefix = text.strip_prefix(comm.prefix()).unwrap_or(text);
let contents = contents_without_prefix.strip_prefix(' ').unwrap_or(contents_without_prefix);
if contents.is_empty() {
contents.to_owned()
} else {
indentation.to_string() + contents
}
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn single_line_to_block() {
check_assist(
convert_comment_block,
r#"
// line$0 comment
fn main() {
foo();
}
"#,
r#"
/*
line comment
*/
fn main() {
foo();
}
"#,
);
}
#[test]
fn single_line_to_block_indented() {
check_assist(
convert_comment_block,
r#"
fn main() {
// line$0 comment
foo();
}
"#,
r#"
fn main() {
/*
line comment
*/
foo();
}
"#,
);
}
#[test]
fn multiline_to_block() {
check_assist(
convert_comment_block,
r#"
fn main() {
// above
// line$0 comment
//
// below
foo();
}
"#,
r#"
fn main() {
/*
above
line comment
below
*/
foo();
}
"#,
);
}
#[test]
fn end_of_line_to_block() {
check_assist_not_applicable(
convert_comment_block,
r#"
fn main() {
foo(); // end-of-line$0 comment
}
"#,
);
}
#[test]
fn single_line_different_kinds() {
check_assist(
convert_comment_block,
r#"
fn main() {
/// different prefix
// line$0 comment
// below
foo();
}
"#,
r#"
fn main() {
/// different prefix
/*
line comment
below
*/
foo();
}
"#,
);
}
#[test]
fn single_line_separate_chunks() {
check_assist(
convert_comment_block,
r#"
fn main() {
// different chunk
// line$0 comment
// below
foo();
}
"#,
r#"
fn main() {
// different chunk
/*
line comment
below
*/
foo();
}
"#,
);
}
#[test]
fn doc_block_comment_to_lines() {
check_assist(
convert_comment_block,
r#"
/**
hi$0 there
*/
"#,
r#"
/// hi there
"#,
);
}
#[test]
fn block_comment_to_lines() {
check_assist(
convert_comment_block,
r#"
/*
hi$0 there
*/
"#,
r#"
// hi there
"#,
);
}
#[test]
fn inner_doc_block_to_lines() {
check_assist(
convert_comment_block,
r#"
/*!
hi$0 there
*/
"#,
r#"
//! hi there
"#,
);
}
#[test]
fn block_to_lines_indent() {
check_assist(
convert_comment_block,
r#"
fn main() {
/*!
hi$0 there
```
code_sample
```
*/
}
"#,
r#"
fn main() {
//! hi there
//!
//! ```
//! code_sample
//! ```
}
"#,
);
}
#[test]
fn end_of_line_block_to_line() {
check_assist_not_applicable(
convert_comment_block,
r#"
fn main() {
foo(); /* end-of-line$0 comment */
}
"#,
);
}
}