1 use itertools::Itertools;
7 CommentPlacement::{Inner, Outer},
8 CommentShape::{self, Block, Line},
11 AstToken, Direction, SyntaxElement, TextRange,
14 use crate::{AssistContext, AssistId, AssistKind, Assists};
16 /// Assist: line_to_block
18 /// Converts comments between block and single-line form
31 pub(crate) fn convert_comment_block(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
32 if let Some(comment) = ctx.find_token_at_offset::<ast::Comment>() {
33 // Only allow comments which are alone on their line
34 if let Some(prev) = comment.syntax().prev_token() {
35 if Whitespace::cast(prev).filter(|w| w.text().contains('\n')).is_none() {
40 return match comment.kind().shape {
41 ast::CommentShape::Block => block_to_line(acc, comment),
42 ast::CommentShape::Line => line_to_block(acc, comment),
49 fn block_to_line(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
50 let target = comment.syntax().text_range();
53 AssistId("block_to_line", AssistKind::RefactorRewrite),
54 "Replace block comment with line comments",
57 let indentation = IndentLevel::from_token(comment.syntax());
59 comment_kind_prefix(CommentKind { shape: CommentShape::Line, ..comment.kind() });
61 let text = comment.text();
62 let text = &text[comment.prefix().len()..(text.len() - "*/".len())].trim();
64 let lines = text.lines().peekable();
66 let indent_spaces = indentation.to_string();
68 .map(|l| l.trim_start_matches(&indent_spaces))
70 // Don't introduce trailing whitespace
72 line_prefix.to_string()
74 format!("{} {}", line_prefix, l.trim_start_matches(&indent_spaces))
77 .join(&format!("\n{}", indent_spaces));
79 edit.replace(target, output)
84 fn line_to_block(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
85 // Find all the comments we'll be collapsing into a block
86 let comments = relevant_line_comments(&comment);
88 // Establish the target of our edit based on the comments we found
89 let target = TextRange::new(
90 comments[0].syntax().text_range().start(),
91 comments.last().unwrap().syntax().text_range().end(),
95 AssistId("line_to_block", AssistKind::RefactorRewrite),
96 "Replace line comments with a single block comment",
99 // We pick a single indentation level for the whole block comment based on the
100 // comment where the assist was invoked. This will be prepended to the
101 // contents of each line comment when they're put into the block comment.
102 let indentation = IndentLevel::from_token(&comment.syntax());
104 let block_comment_body =
105 comments.into_iter().map(|c| line_comment_text(indentation, c)).join("\n");
108 comment_kind_prefix(CommentKind { shape: CommentShape::Block, ..comment.kind() });
111 format!("{}\n{}\n{}*/", block_prefix, block_comment_body, indentation.to_string());
113 edit.replace(target, output)
118 /// The line -> block assist can be invoked from anywhere within a sequence of line comments.
119 /// relevant_line_comments crawls backwards and forwards finding the complete sequence of comments that will
121 fn relevant_line_comments(comment: &ast::Comment) -> Vec<Comment> {
122 // The prefix identifies the kind of comment we're dealing with
123 let prefix = comment.prefix();
124 let same_prefix = |c: &ast::Comment| c.prefix() == prefix;
126 // These tokens are allowed to exist between comments
127 let skippable = |not: &SyntaxElement| {
130 .and_then(Whitespace::cast)
131 .map(|w| !w.spans_multiple_lines())
135 // Find all preceding comments (in reverse order) that have the same prefix
136 let prev_comments = comment
138 .siblings_with_tokens(Direction::Prev)
139 .filter(|s| !skippable(s))
140 .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
141 .take_while(|opt_com| opt_com.is_some())
143 .skip(1); // skip the first element so we don't duplicate it in next_comments
145 let next_comments = comment
147 .siblings_with_tokens(Direction::Next)
148 .filter(|s| !skippable(s))
149 .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
150 .take_while(|opt_com| opt_com.is_some())
153 let mut comments: Vec<_> = prev_comments.collect();
155 comments.extend(next_comments);
159 // Line comments usually begin with a single space character following the prefix as seen here:
161 // But comments can also include indented text:
164 // We handle this by stripping *AT MOST* one space character from the start of the line
165 // This has its own problems because it can cause alignment issues:
172 // But since such comments aren't idiomatic we're okay with this.
173 fn line_comment_text(indentation: IndentLevel, comm: ast::Comment) -> String {
174 let contents_without_prefix = comm.text().strip_prefix(comm.prefix()).unwrap();
175 let contents = contents_without_prefix.strip_prefix(' ').unwrap_or(contents_without_prefix);
177 // Don't add the indentation if the line is empty
178 if contents.is_empty() {
181 indentation.to_string() + &contents
185 fn comment_kind_prefix(ck: ast::CommentKind) -> &'static str {
186 match (ck.shape, ck.doc) {
187 (Line, Some(Inner)) => "//!",
188 (Line, Some(Outer)) => "///",
189 (Line, None) => "//",
190 (Block, Some(Inner)) => "/*!",
191 (Block, Some(Outer)) => "/**",
192 (Block, None) => "/*",
198 use crate::tests::{check_assist, check_assist_not_applicable};
203 fn single_line_to_block() {
205 convert_comment_block,
224 fn single_line_to_block_indented() {
226 convert_comment_block,
245 fn multiline_to_block() {
247 convert_comment_block,
272 fn end_of_line_to_block() {
273 check_assist_not_applicable(
274 convert_comment_block,
277 foo(); // end-of-line$0 comment
284 fn single_line_different_kinds() {
286 convert_comment_block,
309 fn single_line_separate_chunks() {
311 convert_comment_block,
336 fn doc_block_comment_to_lines() {
338 convert_comment_block,
351 fn block_comment_to_lines() {
353 convert_comment_block,
366 fn inner_doc_block_to_lines() {
368 convert_comment_block,
381 fn block_to_lines_indent() {
383 convert_comment_block,
408 fn end_of_line_block_to_line() {
409 check_assist_not_applicable(
410 convert_comment_block,
413 foo(); /* end-of-line$0 comment */