mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-26 13:03:31 +00:00
Implement line<->block comment assist
This commit is contained in:
parent
a307e4f31f
commit
9eecba4dbf
4 changed files with 427 additions and 5 deletions
416
crates/ide_assists/src/handlers/convert_comment_block.rs
Normal file
416
crates/ide_assists/src/handlers/convert_comment_block.rs
Normal file
|
@ -0,0 +1,416 @@
|
|||
use ast::{Comment, CommentShape};
|
||||
use itertools::Itertools;
|
||||
use std::convert::identity;
|
||||
use syntax::{
|
||||
ast::{self, edit::IndentLevel, CommentKind, Whitespace},
|
||||
AstToken, Direction, SyntaxElement, TextRange,
|
||||
};
|
||||
|
||||
use crate::{AssistContext, AssistId, AssistKind, Assists};
|
||||
|
||||
/// Assist: line_to_block
|
||||
///
|
||||
/// Converts comments between block and single-line form
|
||||
///
|
||||
/// ```
|
||||
/// // Multi-line
|
||||
/// // comment
|
||||
/// ```
|
||||
/// ->
|
||||
/// ```
|
||||
/// /**
|
||||
/// Multi-line
|
||||
/// comment
|
||||
/// */
|
||||
/// ```
|
||||
pub(crate) fn convert_comment_block(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
|
||||
if let Some(comment) = ctx.find_token_at_offset::<ast::Comment>() {
|
||||
// Only allow comments which are alone on their line
|
||||
if let Some(prev) = comment.syntax().prev_token() {
|
||||
if Whitespace::cast(prev).filter(|w| w.text().contains('\n')).is_none() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
return match comment.kind().shape {
|
||||
ast::CommentShape::Block => block_to_line(acc, comment),
|
||||
ast::CommentShape::Line => line_to_block(acc, comment),
|
||||
};
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
fn block_to_line(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
|
||||
let indentation = IndentLevel::from_token(comment.syntax());
|
||||
let line_prefix =
|
||||
comment_kind_prefix(CommentKind { shape: CommentShape::Line, ..comment.kind() });
|
||||
|
||||
let text = comment.text();
|
||||
let text = &text[comment.prefix().len()..(text.len() - "*/".len())].trim();
|
||||
|
||||
let lines = text.lines().peekable();
|
||||
|
||||
let indent_spaces = indentation.to_string();
|
||||
let output = lines
|
||||
.map(|l| l.trim_start_matches(&indent_spaces))
|
||||
.map(|l| {
|
||||
// Don't introduce trailing whitespace
|
||||
if l.is_empty() {
|
||||
line_prefix.to_string()
|
||||
} else {
|
||||
format!("{} {}", line_prefix, l.trim_start_matches(&indent_spaces))
|
||||
}
|
||||
})
|
||||
.join(&format!("\n{}", indent_spaces));
|
||||
|
||||
let target = comment.syntax().text_range();
|
||||
acc.add(
|
||||
AssistId("block_to_line", AssistKind::RefactorRewrite),
|
||||
"Replace block comment with line comments",
|
||||
target,
|
||||
|edit| edit.replace(target, output),
|
||||
)
|
||||
}
|
||||
|
||||
fn line_to_block(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
|
||||
// Find all the comments we'll be collapsing into a block
|
||||
let comments = relevant_line_comments(&comment);
|
||||
|
||||
// Establish the target of our edit based on the comments we found
|
||||
let target = TextRange::new(
|
||||
comments[0].syntax().text_range().start(),
|
||||
comments.last().unwrap().syntax().text_range().end(),
|
||||
);
|
||||
|
||||
// We pick a single indentation level for the whole block comment based on the
|
||||
// comment where the assist was invoked. This will be prepended to the
|
||||
// contents of each line comment when they're put into the block comment.
|
||||
let indentation = IndentLevel::from_token(&comment.syntax());
|
||||
|
||||
let block_comment_body =
|
||||
comments.into_iter().map(|c| line_comment_text(indentation, c)).join("\n");
|
||||
|
||||
let block_prefix =
|
||||
comment_kind_prefix(CommentKind { shape: CommentShape::Block, ..comment.kind() });
|
||||
|
||||
let output = format!("{}\n{}\n{}*/", block_prefix, block_comment_body, indentation.to_string());
|
||||
|
||||
acc.add(
|
||||
AssistId("line_to_block", AssistKind::RefactorRewrite),
|
||||
"Replace line comments with a single block comment",
|
||||
target,
|
||||
|edit| edit.replace(target, output),
|
||||
)
|
||||
}
|
||||
|
||||
/// The line -> block assist can be invoked from anywhere within a sequence of line comments.
|
||||
/// relevant_line_comments crawls backwards and forwards finding the complete sequence of comments that will
|
||||
/// be joined.
|
||||
fn relevant_line_comments(comment: &ast::Comment) -> Vec<Comment> {
|
||||
// The prefix identifies the kind of comment we're dealing with
|
||||
let prefix = comment.prefix();
|
||||
let same_prefix = |c: &ast::Comment| c.prefix() == prefix;
|
||||
|
||||
// These tokens are allowed to exist between comments
|
||||
let skippable = |not: &SyntaxElement| {
|
||||
not.clone()
|
||||
.into_token()
|
||||
.and_then(Whitespace::cast)
|
||||
.map(|w| !w.spans_multiple_lines())
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
// Find all preceding comments (in reverse order) that have the same prefix
|
||||
let prev_comments = comment
|
||||
.syntax()
|
||||
.siblings_with_tokens(Direction::Prev)
|
||||
.filter(|s| !skippable(s))
|
||||
.map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
|
||||
.take_while(|opt_com| opt_com.is_some())
|
||||
.filter_map(identity)
|
||||
.skip(1); // skip the first element so we don't duplicate it in next_comments
|
||||
|
||||
let next_comments = comment
|
||||
.syntax()
|
||||
.siblings_with_tokens(Direction::Next)
|
||||
.filter(|s| !skippable(s))
|
||||
.map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
|
||||
.take_while(|opt_com| opt_com.is_some())
|
||||
.filter_map(identity);
|
||||
|
||||
let mut comments: Vec<_> = prev_comments.collect();
|
||||
comments.reverse();
|
||||
comments.extend(next_comments);
|
||||
comments
|
||||
}
|
||||
|
||||
// Line comments usually begin with a single space character following the prefix as seen here:
|
||||
//^
|
||||
// But comments can also include indented text:
|
||||
// > Hello there
|
||||
//
|
||||
// We handle this by stripping *AT MOST* one space character from the start of the line
|
||||
// This has its own problems because it can cause alignment issues:
|
||||
//
|
||||
// /*
|
||||
// a ----> a
|
||||
//b ----> b
|
||||
// */
|
||||
//
|
||||
// But since such comments aren't idiomatic we're okay with this.
|
||||
fn line_comment_text(indentation: IndentLevel, comm: ast::Comment) -> String {
|
||||
let contents = trim_one(comm.text().strip_prefix(comm.prefix()).unwrap()).to_owned();
|
||||
|
||||
// Don't add the indentation if the line is empty
|
||||
if contents.is_empty() {
|
||||
contents
|
||||
} else {
|
||||
indentation.to_string() + &contents
|
||||
}
|
||||
}
|
||||
|
||||
fn trim_one(text: &str) -> &str {
|
||||
if text.starts_with(' ') {
|
||||
&text[1..]
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
fn comment_kind_prefix(ck: ast::CommentKind) -> &'static str {
|
||||
use ast::CommentPlacement::{Inner, Outer};
|
||||
use ast::CommentShape::{Block, Line};
|
||||
match (ck.shape, ck.doc) {
|
||||
(Line, Some(Inner)) => "//!",
|
||||
(Line, Some(Outer)) => "///",
|
||||
(Line, None) => "//",
|
||||
(Block, Some(Inner)) => "/*!",
|
||||
(Block, Some(Outer)) => "/**",
|
||||
(Block, None) => "/*",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::tests::{check_assist, check_assist_not_applicable};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn single_line_to_block() {
|
||||
check_assist(
|
||||
convert_comment_block,
|
||||
r#"
|
||||
// line$0 comment
|
||||
fn main() {
|
||||
foo();
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
/*
|
||||
line comment
|
||||
*/
|
||||
fn main() {
|
||||
foo();
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_line_to_block_indented() {
|
||||
check_assist(
|
||||
convert_comment_block,
|
||||
r#"
|
||||
fn main() {
|
||||
// line$0 comment
|
||||
foo();
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
fn main() {
|
||||
/*
|
||||
line comment
|
||||
*/
|
||||
foo();
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiline_to_block() {
|
||||
check_assist(
|
||||
convert_comment_block,
|
||||
r#"
|
||||
fn main() {
|
||||
// above
|
||||
// line$0 comment
|
||||
//
|
||||
// below
|
||||
foo();
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
fn main() {
|
||||
/*
|
||||
above
|
||||
line comment
|
||||
|
||||
below
|
||||
*/
|
||||
foo();
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_of_line_to_block() {
|
||||
check_assist_not_applicable(
|
||||
convert_comment_block,
|
||||
r#"
|
||||
fn main() {
|
||||
foo(); // end-of-line$0 comment
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_line_different_kinds() {
|
||||
check_assist(
|
||||
convert_comment_block,
|
||||
r#"
|
||||
fn main() {
|
||||
/// different prefix
|
||||
// line$0 comment
|
||||
// below
|
||||
foo();
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
fn main() {
|
||||
/// different prefix
|
||||
/*
|
||||
line comment
|
||||
below
|
||||
*/
|
||||
foo();
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_line_separate_chunks() {
|
||||
check_assist(
|
||||
convert_comment_block,
|
||||
r#"
|
||||
fn main() {
|
||||
// different chunk
|
||||
|
||||
// line$0 comment
|
||||
// below
|
||||
foo();
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
fn main() {
|
||||
// different chunk
|
||||
|
||||
/*
|
||||
line comment
|
||||
below
|
||||
*/
|
||||
foo();
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_block_comment_to_lines() {
|
||||
check_assist(
|
||||
convert_comment_block,
|
||||
r#"
|
||||
/**
|
||||
hi$0 there
|
||||
*/
|
||||
"#,
|
||||
r#"
|
||||
/// hi there
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_comment_to_lines() {
|
||||
check_assist(
|
||||
convert_comment_block,
|
||||
r#"
|
||||
/*
|
||||
hi$0 there
|
||||
*/
|
||||
"#,
|
||||
r#"
|
||||
// hi there
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_doc_block_to_lines() {
|
||||
check_assist(
|
||||
convert_comment_block,
|
||||
r#"
|
||||
/*!
|
||||
hi$0 there
|
||||
*/
|
||||
"#,
|
||||
r#"
|
||||
//! hi there
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_to_lines_indent() {
|
||||
check_assist(
|
||||
convert_comment_block,
|
||||
r#"
|
||||
fn main() {
|
||||
/*!
|
||||
hi$0 there
|
||||
|
||||
```
|
||||
code_sample
|
||||
```
|
||||
*/
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
fn main() {
|
||||
//! hi there
|
||||
//!
|
||||
//! ```
|
||||
//! code_sample
|
||||
//! ```
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_of_line_block_to_line() {
|
||||
check_assist_not_applicable(
|
||||
convert_comment_block,
|
||||
r#"
|
||||
fn main() {
|
||||
foo(); /* end-of-line$0 comment */
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -115,6 +115,7 @@ mod handlers {
|
|||
mod auto_import;
|
||||
mod change_visibility;
|
||||
mod convert_integer_literal;
|
||||
mod convert_comment_block;
|
||||
mod early_return;
|
||||
mod expand_glob_import;
|
||||
mod extract_function;
|
||||
|
@ -176,6 +177,7 @@ mod handlers {
|
|||
auto_import::auto_import,
|
||||
change_visibility::change_visibility,
|
||||
convert_integer_literal::convert_integer_literal,
|
||||
convert_comment_block::convert_comment_block,
|
||||
early_return::convert_to_guarded_return,
|
||||
expand_glob_import::expand_glob_import,
|
||||
move_module_to_file::move_module_to_file,
|
||||
|
|
|
@ -595,11 +595,14 @@ impl ops::Add<u8> for IndentLevel {
|
|||
|
||||
impl IndentLevel {
|
||||
pub fn from_node(node: &SyntaxNode) -> IndentLevel {
|
||||
let first_token = match node.first_token() {
|
||||
Some(it) => it,
|
||||
match node.first_token() {
|
||||
Some(it) => Self::from_token(&it),
|
||||
None => return IndentLevel(0),
|
||||
};
|
||||
for ws in prev_tokens(first_token).filter_map(ast::Whitespace::cast) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_token(token: &SyntaxToken) -> IndentLevel {
|
||||
for ws in prev_tokens(token.clone()).filter_map(ast::Whitespace::cast) {
|
||||
let text = ws.syntax().text();
|
||||
if let Some(pos) = text.rfind('\n') {
|
||||
let level = text[pos + 1..].chars().count() / 4;
|
||||
|
|
|
@ -85,8 +85,9 @@ pub enum CommentPlacement {
|
|||
}
|
||||
|
||||
impl CommentKind {
|
||||
const BY_PREFIX: [(&'static str, CommentKind); 8] = [
|
||||
const BY_PREFIX: [(&'static str, CommentKind); 9] = [
|
||||
("/**/", CommentKind { shape: CommentShape::Block, doc: None }),
|
||||
("/***", CommentKind { shape: CommentShape::Block, doc: None }),
|
||||
("////", CommentKind { shape: CommentShape::Line, doc: None }),
|
||||
("///", CommentKind { shape: CommentShape::Line, doc: Some(CommentPlacement::Outer) }),
|
||||
("//!", CommentKind { shape: CommentShape::Line, doc: Some(CommentPlacement::Inner) }),
|
||||
|
|
Loading…
Reference in a new issue