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();
99 let output = format!("{}\n{}\n{}*/", block_prefix, block_comment_body, indentation);
101 edit.replace(target, output)
106 /// The line -> block assist can be invoked from anywhere within a sequence of line comments.
107 /// relevant_line_comments crawls backwards and forwards finding the complete sequence of comments that will
109 fn relevant_line_comments(comment: &ast::Comment) -> Vec<Comment> {
110 // The prefix identifies the kind of comment we're dealing with
111 let prefix = comment.prefix();
112 let same_prefix = |c: &ast::Comment| c.prefix() == prefix;
114 // These tokens are allowed to exist between comments
115 let skippable = |not: &SyntaxElement| {
118 .and_then(Whitespace::cast)
119 .map(|w| !w.spans_multiple_lines())
123 // Find all preceding comments (in reverse order) that have the same prefix
124 let prev_comments = comment
126 .siblings_with_tokens(Direction::Prev)
127 .filter(|s| !skippable(s))
128 .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
129 .take_while(|opt_com| opt_com.is_some())
131 .skip(1); // skip the first element so we don't duplicate it in next_comments
133 let next_comments = comment
135 .siblings_with_tokens(Direction::Next)
136 .filter(|s| !skippable(s))
137 .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
138 .take_while(|opt_com| opt_com.is_some())
141 let mut comments: Vec<_> = prev_comments.collect();
143 comments.extend(next_comments);
147 // Line comments usually begin with a single space character following the prefix as seen here:
149 // But comments can also include indented text:
152 // We handle this by stripping *AT MOST* one space character from the start of the line
153 // This has its own problems because it can cause alignment issues:
160 // But since such comments aren't idiomatic we're okay with this.
161 fn line_comment_text(indentation: IndentLevel, comm: ast::Comment) -> String {
162 let contents_without_prefix = comm.text().strip_prefix(comm.prefix()).unwrap();
163 let contents = contents_without_prefix.strip_prefix(' ').unwrap_or(contents_without_prefix);
165 // Don't add the indentation if the line is empty
166 if contents.is_empty() {
169 indentation.to_string() + contents
175 use crate::tests::{check_assist, check_assist_not_applicable};
180 fn single_line_to_block() {
182 convert_comment_block,
201 fn single_line_to_block_indented() {
203 convert_comment_block,
222 fn multiline_to_block() {
224 convert_comment_block,
249 fn end_of_line_to_block() {
250 check_assist_not_applicable(
251 convert_comment_block,
254 foo(); // end-of-line$0 comment
261 fn single_line_different_kinds() {
263 convert_comment_block,
286 fn single_line_separate_chunks() {
288 convert_comment_block,
313 fn doc_block_comment_to_lines() {
315 convert_comment_block,
328 fn block_comment_to_lines() {
330 convert_comment_block,
343 fn inner_doc_block_to_lines() {
345 convert_comment_block,
358 fn block_to_lines_indent() {
360 convert_comment_block,
385 fn end_of_line_block_to_line() {
386 check_assist_not_applicable(
387 convert_comment_block,
390 foo(); /* end-of-line$0 comment */