diff --git a/crates/ide-assists/src/handlers/generate_documentation_from_trait.rs b/crates/ide-assists/src/handlers/generate_documentation_from_trait.rs new file mode 100644 index 000000000000..9d384b92af80 --- /dev/null +++ b/crates/ide-assists/src/handlers/generate_documentation_from_trait.rs @@ -0,0 +1,190 @@ +use hir::{AsAssocItem, HasSource}; +use ide_db::assists::AssistId; +use syntax::{ + AstNode, NodeOrToken, SyntaxElement, + ast::{ + self, AnyHasName, HasDocComments, HasName, + edit::{AstNodeEdit, IndentLevel}, + make, + }, + syntax_editor::Position, +}; + +use crate::assist_context::{AssistContext, Assists}; + +// Assist: generate_documentation_from_trait +// +// Generate documents from items defined in the trait. +// +// ``` +// trait Foo { +// /// some docs +// fn foo(&self); +// } +// impl Foo for () { +// fn $0foo(&self) {} +// } +// ``` +// -> +// ``` +// trait Foo { +// /// some docs +// fn foo(&self); +// } +// impl Foo for () { +// /// some docs +// fn foo(&self) {} +// } +// ``` +pub(crate) fn generate_documentation_from_trait( + acc: &mut Assists, + ctx: &AssistContext<'_>, +) -> Option<()> { + let name = ctx.find_node_at_offset::()?; + let ast_func = name.syntax().parent().and_then(ast::Fn::cast)?; + if ast_func.doc_comments().next().is_some() { + return None; + } + let assoc_item = ctx.sema.to_def(&ast_func)?.as_assoc_item(ctx.db())?; + let trait_ = assoc_item.implemented_trait(ctx.db())?.source(ctx.db())?.value; + let origin_item = trait_.assoc_item_list()?.assoc_items().find(|it| { + AnyHasName::cast(it.syntax().clone()) + .and_then(|it| it.name()) + .is_some_and(|it| it.text() == name.text()) + })?; + let _first = origin_item.doc_comments().next()?; + + let comments = origin_item.doc_comments(); + let indent = ast_func.indent_level(); + let origin_indent = origin_item.indent_level(); + + acc.add( + AssistId::generate("generate_documentation_from_trait"), + "Generate a documentation from trait", + ast_func.syntax().text_range(), + |builder| { + let mut edit = builder.make_editor(ast_func.syntax()); + + let comments = comments + .flat_map(|doc| { + generate_docs(doc.doc_comment().unwrap_or(""), indent, origin_indent) + }) + .collect(); + edit.insert_all(Position::before(ast_func.syntax()), comments); + + builder.add_file_edits(ctx.vfs_file_id(), edit); + }, + ) +} + +fn generate_docs(doc: &str, indent: IndentLevel, origin_indent: IndentLevel) -> Vec { + let ws = format!("\n{indent}"); + let trim_indent = origin_indent.to_string(); + + doc.trim_end() + .split('\n') + .flat_map(|doc| { + let trimmed_doc = line_doc(&trim_indent, doc); + [make::tokens::doc_comment(&trimmed_doc), make::tokens::whitespace(&ws)] + }) + .map(NodeOrToken::Token) + .collect() +} + +fn line_doc(trim_indent: &str, line: &str) -> String { + let text = line.strip_prefix(trim_indent).unwrap_or(line.trim()).trim_end(); + if text.is_empty() { "///".to_owned() } else { format!("/// {text}") } +} + +#[cfg(test)] +mod tests { + use crate::tests::{check_assist, check_assist_not_applicable}; + + use super::*; + + #[test] + fn test_generate_documentation_from_trait() { + check_assist( + generate_documentation_from_trait, + r#" +trait Foo { + /// some docs + /// + /// # Examples + /// ... + fn foo(&self); +} +impl Foo for () { + fn $0foo(&self) {} +} + "#, + r#" +trait Foo { + /// some docs + /// + /// # Examples + /// ... + fn foo(&self); +} +impl Foo for () { + /// some docs + /// + /// # Examples + /// ... + fn foo(&self) {} +} + "#, + ); + } + + #[test] + fn test_generate_documentation_from_trait_with_multi_line() { + check_assist( + generate_documentation_from_trait, + r#" +trait Foo { + /** some docs + ... + */ + fn foo(&self); +} +impl Foo for () { + fn $0foo(&self) {} +} + "#, + r#" +trait Foo { + /** some docs + ... + */ + fn foo(&self); +} +impl Foo for () { + /// some docs + /// ... + fn foo(&self) {} +} + "#, + ); + } + + #[test] + fn test_generate_documentation_from_trait_not_applicable_existing_doc() { + check_assist_not_applicable( + generate_documentation_from_trait, + r#" +trait Foo { + /// some docs + /// + /// # Examples + /// ... + fn foo(&self); +} +impl Foo for () { + /// existing docs + fn $0foo(&self) {} +} + "#, + ); + } +} diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs index 4682c0473238..f7df1f3ce423 100644 --- a/crates/ide-assists/src/lib.rs +++ b/crates/ide-assists/src/lib.rs @@ -160,6 +160,7 @@ mod handlers { mod generate_delegate_trait; mod generate_deref; mod generate_derive; + mod generate_documentation_from_trait; mod generate_documentation_template; mod generate_enum_is_method; mod generate_enum_projection_method; @@ -293,6 +294,7 @@ mod handlers { generate_derive::generate_derive, generate_documentation_template::generate_doc_example, generate_documentation_template::generate_documentation_template, + generate_documentation_from_trait::generate_documentation_from_trait, generate_enum_is_method::generate_enum_is_method, generate_enum_projection_method::generate_enum_as_method, generate_enum_projection_method::generate_enum_try_into_method, diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs index 91348be97eb7..f8b59e6845b3 100644 --- a/crates/ide-assists/src/tests/generated.rs +++ b/crates/ide-assists/src/tests/generated.rs @@ -1563,6 +1563,32 @@ pub fn add(a: i32, b: i32) -> i32 { a + b } ) } +#[test] +fn doctest_generate_documentation_from_trait() { + check_doc_test( + "generate_documentation_from_trait", + r#####" +trait Foo { + /// some docs + fn foo(&self); +} +impl Foo for () { + fn $0foo(&self) {} +} +"#####, + r#####" +trait Foo { + /// some docs + fn foo(&self); +} +impl Foo for () { + /// some docs + fn foo(&self) {} +} +"#####, + ) +} + #[test] fn doctest_generate_documentation_template() { check_doc_test( diff --git a/crates/syntax/src/ast/make.rs b/crates/syntax/src/ast/make.rs index 9897fd094157..bde6f7f113c8 100644 --- a/crates/syntax/src/ast/make.rs +++ b/crates/syntax/src/ast/make.rs @@ -1399,7 +1399,7 @@ pub mod tokens { pub fn doc_comment(text: &str) -> SyntaxToken { assert!(!text.trim().is_empty()); let sf = SourceFile::parse(text, Edition::CURRENT).ok().unwrap(); - sf.syntax().first_child_or_token().unwrap().into_token().unwrap() + sf.syntax().clone_for_update().first_child_or_token().unwrap().into_token().unwrap() } pub fn literal(text: &str) -> SyntaxToken {