aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/ide_assists/src/handlers/convert_comment_block.rs419
-rw-r--r--crates/ide_assists/src/lib.rs2
-rw-r--r--crates/syntax/src/ast/edit.rs11
-rw-r--r--crates/syntax/src/ast/token_ext.rs3
4 files changed, 430 insertions, 5 deletions
diff --git a/crates/ide_assists/src/handlers/convert_comment_block.rs b/crates/ide_assists/src/handlers/convert_comment_block.rs
new file mode 100644
index 000000000..cdc45fc42
--- /dev/null
+++ b/crates/ide_assists/src/handlers/convert_comment_block.rs
@@ -0,0 +1,419 @@
1use itertools::Itertools;
2use std::convert::identity;
3use syntax::{
4 ast::{
5 self,
6 edit::IndentLevel,
7 Comment, CommentKind,
8 CommentPlacement::{Inner, Outer},
9 CommentShape::{self, Block, Line},
10 Whitespace,
11 },
12 AstToken, Direction, SyntaxElement, TextRange,
13};
14
15use crate::{AssistContext, AssistId, AssistKind, Assists};
16
17/// Assist: line_to_block
18///
19/// Converts comments between block and single-line form
20///
21/// ```
22/// // Multi-line
23/// // comment
24/// ```
25/// ->
26/// ```
27/// /**
28/// Multi-line
29/// comment
30/// */
31/// ```
32pub(crate) fn convert_comment_block(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
33 if let Some(comment) = ctx.find_token_at_offset::<ast::Comment>() {
34 // Only allow comments which are alone on their line
35 if let Some(prev) = comment.syntax().prev_token() {
36 if Whitespace::cast(prev).filter(|w| w.text().contains('\n')).is_none() {
37 return None;
38 }
39 }
40
41 return match comment.kind().shape {
42 ast::CommentShape::Block => block_to_line(acc, comment),
43 ast::CommentShape::Line => line_to_block(acc, comment),
44 };
45 }
46
47 return None;
48}
49
50fn block_to_line(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
51 let target = comment.syntax().text_range();
52
53 acc.add(
54 AssistId("block_to_line", AssistKind::RefactorRewrite),
55 "Replace block comment with line comments",
56 target,
57 |edit| {
58 let indentation = IndentLevel::from_token(comment.syntax());
59 let line_prefix =
60 comment_kind_prefix(CommentKind { shape: CommentShape::Line, ..comment.kind() });
61
62 let text = comment.text();
63 let text = &text[comment.prefix().len()..(text.len() - "*/".len())].trim();
64
65 let lines = text.lines().peekable();
66
67 let indent_spaces = indentation.to_string();
68 let output = lines
69 .map(|l| l.trim_start_matches(&indent_spaces))
70 .map(|l| {
71 // Don't introduce trailing whitespace
72 if l.is_empty() {
73 line_prefix.to_string()
74 } else {
75 format!("{} {}", line_prefix, l.trim_start_matches(&indent_spaces))
76 }
77 })
78 .join(&format!("\n{}", indent_spaces));
79
80 edit.replace(target, output)
81 },
82 )
83}
84
85fn line_to_block(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
86 // Find all the comments we'll be collapsing into a block
87 let comments = relevant_line_comments(&comment);
88
89 // Establish the target of our edit based on the comments we found
90 let target = TextRange::new(
91 comments[0].syntax().text_range().start(),
92 comments.last().unwrap().syntax().text_range().end(),
93 );
94
95 acc.add(
96 AssistId("line_to_block", AssistKind::RefactorRewrite),
97 "Replace line comments with a single block comment",
98 target,
99 |edit| {
100 // We pick a single indentation level for the whole block comment based on the
101 // comment where the assist was invoked. This will be prepended to the
102 // contents of each line comment when they're put into the block comment.
103 let indentation = IndentLevel::from_token(&comment.syntax());
104
105 let block_comment_body =
106 comments.into_iter().map(|c| line_comment_text(indentation, c)).join("\n");
107
108 let block_prefix =
109 comment_kind_prefix(CommentKind { shape: CommentShape::Block, ..comment.kind() });
110
111 let output =
112 format!("{}\n{}\n{}*/", block_prefix, block_comment_body, indentation.to_string());
113
114 edit.replace(target, output)
115 },
116 )
117}
118
119/// The line -> block assist can be invoked from anywhere within a sequence of line comments.
120/// relevant_line_comments crawls backwards and forwards finding the complete sequence of comments that will
121/// be joined.
122fn relevant_line_comments(comment: &ast::Comment) -> Vec<Comment> {
123 // The prefix identifies the kind of comment we're dealing with
124 let prefix = comment.prefix();
125 let same_prefix = |c: &ast::Comment| c.prefix() == prefix;
126
127 // These tokens are allowed to exist between comments
128 let skippable = |not: &SyntaxElement| {
129 not.clone()
130 .into_token()
131 .and_then(Whitespace::cast)
132 .map(|w| !w.spans_multiple_lines())
133 .unwrap_or(false)
134 };
135
136 // Find all preceding comments (in reverse order) that have the same prefix
137 let prev_comments = comment
138 .syntax()
139 .siblings_with_tokens(Direction::Prev)
140 .filter(|s| !skippable(s))
141 .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
142 .take_while(|opt_com| opt_com.is_some())
143 .filter_map(identity)
144 .skip(1); // skip the first element so we don't duplicate it in next_comments
145
146 let next_comments = comment
147 .syntax()
148 .siblings_with_tokens(Direction::Next)
149 .filter(|s| !skippable(s))
150 .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
151 .take_while(|opt_com| opt_com.is_some())
152 .filter_map(identity);
153
154 let mut comments: Vec<_> = prev_comments.collect();
155 comments.reverse();
156 comments.extend(next_comments);
157 comments
158}
159
160// Line comments usually begin with a single space character following the prefix as seen here:
161//^
162// But comments can also include indented text:
163// > Hello there
164//
165// We handle this by stripping *AT MOST* one space character from the start of the line
166// This has its own problems because it can cause alignment issues:
167//
168// /*
169// a ----> a
170//b ----> b
171// */
172//
173// But since such comments aren't idiomatic we're okay with this.
174fn line_comment_text(indentation: IndentLevel, comm: ast::Comment) -> String {
175 let contents_without_prefix = comm.text().strip_prefix(comm.prefix()).unwrap();
176 let contents = contents_without_prefix.strip_prefix(' ').unwrap_or(contents_without_prefix);
177
178 // Don't add the indentation if the line is empty
179 if contents.is_empty() {
180 contents.to_owned()
181 } else {
182 indentation.to_string() + &contents
183 }
184}
185
186fn comment_kind_prefix(ck: ast::CommentKind) -> &'static str {
187 match (ck.shape, ck.doc) {
188 (Line, Some(Inner)) => "//!",
189 (Line, Some(Outer)) => "///",
190 (Line, None) => "//",
191 (Block, Some(Inner)) => "/*!",
192 (Block, Some(Outer)) => "/**",
193 (Block, None) => "/*",
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use crate::tests::{check_assist, check_assist_not_applicable};
200
201 use super::*;
202
203 #[test]
204 fn single_line_to_block() {
205 check_assist(
206 convert_comment_block,
207 r#"
208// line$0 comment
209fn main() {
210 foo();
211}
212"#,
213 r#"
214/*
215line comment
216*/
217fn main() {
218 foo();
219}
220"#,
221 );
222 }
223
224 #[test]
225 fn single_line_to_block_indented() {
226 check_assist(
227 convert_comment_block,
228 r#"
229fn main() {
230 // line$0 comment
231 foo();
232}
233"#,
234 r#"
235fn main() {
236 /*
237 line comment
238 */
239 foo();
240}
241"#,
242 );
243 }
244
245 #[test]
246 fn multiline_to_block() {
247 check_assist(
248 convert_comment_block,
249 r#"
250fn main() {
251 // above
252 // line$0 comment
253 //
254 // below
255 foo();
256}
257"#,
258 r#"
259fn main() {
260 /*
261 above
262 line comment
263
264 below
265 */
266 foo();
267}
268"#,
269 );
270 }
271
272 #[test]
273 fn end_of_line_to_block() {
274 check_assist_not_applicable(
275 convert_comment_block,
276 r#"
277fn main() {
278 foo(); // end-of-line$0 comment
279}
280"#,
281 );
282 }
283
284 #[test]
285 fn single_line_different_kinds() {
286 check_assist(
287 convert_comment_block,
288 r#"
289fn main() {
290 /// different prefix
291 // line$0 comment
292 // below
293 foo();
294}
295"#,
296 r#"
297fn main() {
298 /// different prefix
299 /*
300 line comment
301 below
302 */
303 foo();
304}
305"#,
306 );
307 }
308
309 #[test]
310 fn single_line_separate_chunks() {
311 check_assist(
312 convert_comment_block,
313 r#"
314fn main() {
315 // different chunk
316
317 // line$0 comment
318 // below
319 foo();
320}
321"#,
322 r#"
323fn main() {
324 // different chunk
325
326 /*
327 line comment
328 below
329 */
330 foo();
331}
332"#,
333 );
334 }
335
336 #[test]
337 fn doc_block_comment_to_lines() {
338 check_assist(
339 convert_comment_block,
340 r#"
341/**
342 hi$0 there
343*/
344"#,
345 r#"
346/// hi there
347"#,
348 );
349 }
350
351 #[test]
352 fn block_comment_to_lines() {
353 check_assist(
354 convert_comment_block,
355 r#"
356/*
357 hi$0 there
358*/
359"#,
360 r#"
361// hi there
362"#,
363 );
364 }
365
366 #[test]
367 fn inner_doc_block_to_lines() {
368 check_assist(
369 convert_comment_block,
370 r#"
371/*!
372 hi$0 there
373*/
374"#,
375 r#"
376//! hi there
377"#,
378 );
379 }
380
381 #[test]
382 fn block_to_lines_indent() {
383 check_assist(
384 convert_comment_block,
385 r#"
386fn main() {
387 /*!
388 hi$0 there
389
390 ```
391 code_sample
392 ```
393 */
394}
395"#,
396 r#"
397fn main() {
398 //! hi there
399 //!
400 //! ```
401 //! code_sample
402 //! ```
403}
404"#,
405 );
406 }
407
408 #[test]
409 fn end_of_line_block_to_line() {
410 check_assist_not_applicable(
411 convert_comment_block,
412 r#"
413fn main() {
414 foo(); /* end-of-line$0 comment */
415}
416"#,
417 );
418 }
419}
diff --git a/crates/ide_assists/src/lib.rs b/crates/ide_assists/src/lib.rs
index 53542d433..9c8148462 100644
--- a/crates/ide_assists/src/lib.rs
+++ b/crates/ide_assists/src/lib.rs
@@ -115,6 +115,7 @@ mod handlers {
115 mod auto_import; 115 mod auto_import;
116 mod change_visibility; 116 mod change_visibility;
117 mod convert_integer_literal; 117 mod convert_integer_literal;
118 mod convert_comment_block;
118 mod early_return; 119 mod early_return;
119 mod expand_glob_import; 120 mod expand_glob_import;
120 mod extract_function; 121 mod extract_function;
@@ -178,6 +179,7 @@ mod handlers {
178 auto_import::auto_import, 179 auto_import::auto_import,
179 change_visibility::change_visibility, 180 change_visibility::change_visibility,
180 convert_integer_literal::convert_integer_literal, 181 convert_integer_literal::convert_integer_literal,
182 convert_comment_block::convert_comment_block,
181 early_return::convert_to_guarded_return, 183 early_return::convert_to_guarded_return,
182 expand_glob_import::expand_glob_import, 184 expand_glob_import::expand_glob_import,
183 extract_struct_from_enum_variant::extract_struct_from_enum_variant, 185 extract_struct_from_enum_variant::extract_struct_from_enum_variant,
diff --git a/crates/syntax/src/ast/edit.rs b/crates/syntax/src/ast/edit.rs
index 824ebf41c..0b3b76d4a 100644
--- a/crates/syntax/src/ast/edit.rs
+++ b/crates/syntax/src/ast/edit.rs
@@ -595,11 +595,14 @@ impl ops::Add<u8> for IndentLevel {
595 595
596impl IndentLevel { 596impl IndentLevel {
597 pub fn from_node(node: &SyntaxNode) -> IndentLevel { 597 pub fn from_node(node: &SyntaxNode) -> IndentLevel {
598 let first_token = match node.first_token() { 598 match node.first_token() {
599 Some(it) => it, 599 Some(it) => Self::from_token(&it),
600 None => return IndentLevel(0), 600 None => return IndentLevel(0),
601 }; 601 }
602 for ws in prev_tokens(first_token).filter_map(ast::Whitespace::cast) { 602 }
603
604 pub fn from_token(token: &SyntaxToken) -> IndentLevel {
605 for ws in prev_tokens(token.clone()).filter_map(ast::Whitespace::cast) {
603 let text = ws.syntax().text(); 606 let text = ws.syntax().text();
604 if let Some(pos) = text.rfind('\n') { 607 if let Some(pos) = text.rfind('\n') {
605 let level = text[pos + 1..].chars().count() / 4; 608 let level = text[pos + 1..].chars().count() / 4;
diff --git a/crates/syntax/src/ast/token_ext.rs b/crates/syntax/src/ast/token_ext.rs
index 044e3e5e8..977eb8181 100644
--- a/crates/syntax/src/ast/token_ext.rs
+++ b/crates/syntax/src/ast/token_ext.rs
@@ -85,8 +85,9 @@ pub enum CommentPlacement {
85} 85}
86 86
87impl CommentKind { 87impl CommentKind {
88 const BY_PREFIX: [(&'static str, CommentKind); 8] = [ 88 const BY_PREFIX: [(&'static str, CommentKind); 9] = [
89 ("/**/", CommentKind { shape: CommentShape::Block, doc: None }), 89 ("/**/", CommentKind { shape: CommentShape::Block, doc: None }),
90 ("/***", CommentKind { shape: CommentShape::Block, doc: None }),
90 ("////", CommentKind { shape: CommentShape::Line, doc: None }), 91 ("////", CommentKind { shape: CommentShape::Line, doc: None }),
91 ("///", CommentKind { shape: CommentShape::Line, doc: Some(CommentPlacement::Outer) }), 92 ("///", CommentKind { shape: CommentShape::Line, doc: Some(CommentPlacement::Outer) }),
92 ("//!", CommentKind { shape: CommentShape::Line, doc: Some(CommentPlacement::Inner) }), 93 ("//!", CommentKind { shape: CommentShape::Line, doc: Some(CommentPlacement::Inner) }),