1 use itertools::Itertools;
3 ast::{self, edit::IndentLevel, Comment, CommentKind, CommentShape, Whitespace},
4 AstToken, Direction, SyntaxElement, TextRange,
7 use crate::{AssistContext, AssistId, AssistKind, Assists};
9 /// Assist: line_to_block
11 /// Converts comments between block and single-line form
24 pub(crate) fn convert_comment_block(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
25 let comment = ctx.find_token_at_offset::<ast::Comment>()?;
26 // Only allow comments which are alone on their line
27 if let Some(prev) = comment.syntax().prev_token() {
28 if Whitespace::cast(prev).filter(|w| w.text().contains('\n')).is_none() {
33 match comment.kind().shape {
34 ast::CommentShape::Block => block_to_line(acc, comment),
35 ast::CommentShape::Line => line_to_block(acc, comment),
39 fn block_to_line(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
40 let target = comment.syntax().text_range();
43 AssistId("block_to_line", AssistKind::RefactorRewrite),
44 "Replace block comment with line comments",
47 let indentation = IndentLevel::from_token(comment.syntax());
48 let line_prefix = CommentKind { shape: CommentShape::Line, ..comment.kind() }.prefix();
50 let text = comment.text();
51 let text = &text[comment.prefix().len()..(text.len() - "*/".len())].trim();
53 let lines = text.lines().peekable();
55 let indent_spaces = indentation.to_string();
57 .map(|l| l.trim_start_matches(&indent_spaces))
59 // Don't introduce trailing whitespace
61 line_prefix.to_string()
63 format!("{} {}", line_prefix, l.trim_start_matches(&indent_spaces))
66 .join(&format!("\n{}", indent_spaces));
68 edit.replace(target, output)
73 fn line_to_block(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
74 // Find all the comments we'll be collapsing into a block
75 let comments = relevant_line_comments(&comment);
77 // Establish the target of our edit based on the comments we found
78 let target = TextRange::new(
79 comments[0].syntax().text_range().start(),
80 comments.last().unwrap().syntax().text_range().end(),
84 AssistId("line_to_block", AssistKind::RefactorRewrite),
85 "Replace line comments with a single block comment",
88 // We pick a single indentation level for the whole block comment based on the
89 // comment where the assist was invoked. This will be prepended to the
90 // contents of each line comment when they're put into the block comment.
91 let indentation = IndentLevel::from_token(&comment.syntax());
93 let block_comment_body =
94 comments.into_iter().map(|c| line_comment_text(indentation, c)).join("\n");
97 CommentKind { shape: CommentShape::Block, ..comment.kind() }.prefix();
100 format!("{}\n{}\n{}*/", block_prefix, block_comment_body, indentation.to_string());
102 edit.replace(target, output)
107 /// The line -> block assist can be invoked from anywhere within a sequence of line comments.
108 /// relevant_line_comments crawls backwards and forwards finding the complete sequence of comments that will
110 fn relevant_line_comments(comment: &ast::Comment) -> Vec<Comment> {
111 // The prefix identifies the kind of comment we're dealing with
112 let prefix = comment.prefix();
113 let same_prefix = |c: &ast::Comment| c.prefix() == prefix;
115 // These tokens are allowed to exist between comments
116 let skippable = |not: &SyntaxElement| {
119 .and_then(Whitespace::cast)
120 .map(|w| !w.spans_multiple_lines())
124 // Find all preceding comments (in reverse order) that have the same prefix
125 let prev_comments = comment
127 .siblings_with_tokens(Direction::Prev)
128 .filter(|s| !skippable(s))
129 .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
130 .take_while(|opt_com| opt_com.is_some())
132 .skip(1); // skip the first element so we don't duplicate it in next_comments
134 let next_comments = comment
136 .siblings_with_tokens(Direction::Next)
137 .filter(|s| !skippable(s))
138 .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
139 .take_while(|opt_com| opt_com.is_some())
142 let mut comments: Vec<_> = prev_comments.collect();
144 comments.extend(next_comments);
148 // Line comments usually begin with a single space character following the prefix as seen here:
150 // But comments can also include indented text:
153 // We handle this by stripping *AT MOST* one space character from the start of the line
154 // This has its own problems because it can cause alignment issues:
161 // But since such comments aren't idiomatic we're okay with this.
162 fn line_comment_text(indentation: IndentLevel, comm: ast::Comment) -> String {
163 let contents_without_prefix = comm.text().strip_prefix(comm.prefix()).unwrap();
164 let contents = contents_without_prefix.strip_prefix(' ').unwrap_or(contents_without_prefix);
166 // Don't add the indentation if the line is empty
167 if contents.is_empty() {
170 indentation.to_string() + &contents
176 use crate::tests::{check_assist, check_assist_not_applicable};
181 fn single_line_to_block() {
183 convert_comment_block,
202 fn single_line_to_block_indented() {
204 convert_comment_block,
223 fn multiline_to_block() {
225 convert_comment_block,
250 fn end_of_line_to_block() {
251 check_assist_not_applicable(
252 convert_comment_block,
255 foo(); // end-of-line$0 comment
262 fn single_line_different_kinds() {
264 convert_comment_block,
287 fn single_line_separate_chunks() {
289 convert_comment_block,
314 fn doc_block_comment_to_lines() {
316 convert_comment_block,
329 fn block_comment_to_lines() {
331 convert_comment_block,
344 fn inner_doc_block_to_lines() {
346 convert_comment_block,
359 fn block_to_lines_indent() {
361 convert_comment_block,
386 fn end_of_line_block_to_line() {
387 check_assist_not_applicable(
388 convert_comment_block,
391 foo(); /* end-of-line$0 comment */