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