diff --git a/crates/ra_assists/src/handlers/add_from_impl_for_enum.rs b/crates/ra_assists/src/handlers/add_from_impl_for_enum.rs new file mode 100644 index 0000000000..cf94a214a4 --- /dev/null +++ b/crates/ra_assists/src/handlers/add_from_impl_for_enum.rs @@ -0,0 +1,218 @@ +use hir::ImplDef; +use ra_syntax::{ + ast::{self, AstNode, NameOwner}, + TextUnit, +}; +use stdx::format_to; + +use crate::{Assist, AssistCtx, AssistId}; +use ra_ide_db::RootDatabase; + +// Assist add_from_impl_for_enum +// +// Adds a From impl for an enum variant with one tuple field +// +// ``` +// enum A { <|>One(u32) } +// ``` +// -> +// ``` +// enum A { One(u32) } +// +// impl From for A { +// fn from(v: u32) -> Self { +// A::One(v) +// } +// } +// ``` +pub(crate) fn add_from_impl_for_enum(ctx: AssistCtx) -> Option { + let variant = ctx.find_node_at_offset::()?; + let variant_name = variant.name()?; + let enum_name = variant.parent_enum().name()?; + let field_list = match variant.kind() { + ast::StructKind::Tuple(field_list) => field_list, + _ => return None, + }; + if field_list.fields().count() != 1 { + return None; + } + let field_type = field_list.fields().next()?.type_ref()?; + let path = match field_type { + ast::TypeRef::PathType(p) => p, + _ => return None, + }; + + if already_has_from_impl(ctx.sema, &variant) { + return None; + } + + ctx.add_assist( + AssistId("add_from_impl_for_enum"), + "Add From impl for this enum variant", + |edit| { + let start_offset = variant.parent_enum().syntax().text_range().end(); + let mut buf = String::new(); + format_to!( + buf, + r#" + +impl From<{0}> for {1} {{ + fn from(v: {0}) -> Self {{ + {1}::{2}(v) + }} +}}"#, + path.syntax(), + enum_name, + variant_name + ); + edit.insert(start_offset, buf); + edit.set_cursor(start_offset + TextUnit::of_str("\n\n")); + }, + ) +} + +fn already_has_from_impl( + sema: &'_ hir::Semantics<'_, RootDatabase>, + variant: &ast::EnumVariant, +) -> bool { + let scope = sema.scope(&variant.syntax()); + + let from_path = ast::make::path_from_text("From"); + let from_hir_path = match hir::Path::from_ast(from_path) { + Some(p) => p, + None => return false, + }; + let from_trait = match scope.resolve_hir_path(&from_hir_path) { + Some(hir::PathResolution::Def(hir::ModuleDef::Trait(t))) => t, + _ => return false, + }; + + let e: hir::Enum = match sema.to_def(&variant.parent_enum()) { + Some(e) => e, + None => return false, + }; + let e_ty = e.ty(sema.db); + + let hir_enum_var: hir::EnumVariant = match sema.to_def(variant) { + Some(ev) => ev, + None => return false, + }; + let var_ty = hir_enum_var.fields(sema.db)[0].signature_ty(sema.db); + + let krate = match scope.module() { + Some(s) => s.krate(), + _ => return false, + }; + let impls = ImplDef::for_trait(sema.db, krate, from_trait); + let imp = impls.iter().find(|imp| { + let targets_enum = imp.target_ty(sema.db) == e_ty; + let param_matches = imp.target_trait_substs_matches(sema.db, &[var_ty.clone()]); + targets_enum && param_matches + }); + + imp.is_some() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::{check_assist, check_assist_not_applicable}; + + #[test] + fn test_add_from_impl_for_enum() { + check_assist( + add_from_impl_for_enum, + "enum A { <|>One(u32) }", + r#"enum A { One(u32) } + +<|>impl From for A { + fn from(v: u32) -> Self { + A::One(v) + } +}"#, + ); + } + + #[test] + fn test_add_from_impl_for_enum_complicated_path() { + check_assist( + add_from_impl_for_enum, + "enum A { <|>One(foo::bar::baz::Boo) }", + r#"enum A { One(foo::bar::baz::Boo) } + +<|>impl From for A { + fn from(v: foo::bar::baz::Boo) -> Self { + A::One(v) + } +}"#, + ); + } + + #[test] + fn test_add_from_impl_no_element() { + check_assist_not_applicable(add_from_impl_for_enum, "enum A { <|>One }"); + } + + #[test] + fn test_add_from_impl_more_than_one_element_in_tuple() { + check_assist_not_applicable(add_from_impl_for_enum, "enum A { <|>One(u32, String) }"); + } + + #[test] + fn test_add_from_impl_struct_variant() { + check_assist_not_applicable(add_from_impl_for_enum, "enum A { <|>One { x: u32 } }"); + } + + #[test] + fn test_add_from_impl_already_exists() { + check_assist_not_applicable( + add_from_impl_for_enum, + r#"enum A { <|>One(u32), } + +impl From for A { + fn from(v: u32) -> Self { + A::One(v) + } +} + +pub trait From { + fn from(T) -> Self; +}"#, + ); + } + + #[test] + fn test_add_from_impl_different_variant_impl_exists() { + check_assist( + add_from_impl_for_enum, + r#"enum A { <|>One(u32), Two(String), } + +impl From for A { + fn from(v: String) -> Self { + A::Two(v) + } +} + +pub trait From { + fn from(T) -> Self; +}"#, + r#"enum A { One(u32), Two(String), } + +<|>impl From for A { + fn from(v: u32) -> Self { + A::One(v) + } +} + +impl From for A { + fn from(v: String) -> Self { + A::Two(v) + } +} + +pub trait From { + fn from(T) -> Self; +}"#, + ); + } +} diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index fa1f3dd26f..6b4c56dcdc 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs @@ -122,6 +122,7 @@ mod handlers { mod replace_qualified_name_with_use; mod replace_unwrap_with_match; mod split_import; + mod add_from_impl_for_enum; pub(crate) fn all() -> &'static [AssistHandler] { &[ @@ -159,6 +160,7 @@ mod handlers { replace_qualified_name_with_use::replace_qualified_name_with_use, replace_unwrap_with_match::replace_unwrap_with_match, split_import::split_import, + add_from_impl_for_enum::add_from_impl_for_enum, ] } } diff --git a/crates/ra_hir/src/code_model.rs b/crates/ra_hir/src/code_model.rs index cd2a8fc620..3889a7e5a1 100644 --- a/crates/ra_hir/src/code_model.rs +++ b/crates/ra_hir/src/code_model.rs @@ -23,7 +23,7 @@ use hir_expand::{ }; use hir_ty::{ autoderef, display::HirFormatter, expr::ExprValidator, method_resolution, ApplicationTy, - Canonical, InEnvironment, Substs, TraitEnvironment, Ty, TyDefId, TypeCtor, + Canonical, InEnvironment, Substs, TraitEnvironment, Ty, TyDefId, TypeCtor, TypeWalk, }; use ra_db::{CrateId, Edition, FileId}; use ra_prof::profile; @@ -960,6 +960,38 @@ impl ImplDef { db.impl_data(self.id).target_trait.clone() } + pub fn target_trait_substs_matches(&self, db: &dyn HirDatabase, typs: &[Type]) -> bool { + let type_ref = match self.target_trait(db) { + Some(typ_ref) => typ_ref, + None => return false, + }; + let resolver = self.id.resolver(db.upcast()); + let ctx = hir_ty::TyLoweringContext::new(db, &resolver); + let ty = Ty::from_hir(&ctx, &type_ref); + let d = match ty.dyn_trait_ref() { + Some(d) => d, + None => return false, + }; + let mut matches = true; + let mut i = 0; + d.substs.walk(&mut |t| { + if matches { + if i >= typs.len() { + matches = false; + return; + } + match t { + Ty::Bound(_) => matches = i == 0, + _ => { + matches = *t == typs[i].ty.value; + i += 1; + } + } + } + }); + matches + } + pub fn target_type(&self, db: &dyn HirDatabase) -> TypeRef { db.impl_data(self.id).target_type.clone() } diff --git a/crates/ra_syntax/src/ast/make.rs b/crates/ra_syntax/src/ast/make.rs index 0c908573d9..c49cf9a3b8 100644 --- a/crates/ra_syntax/src/ast/make.rs +++ b/crates/ra_syntax/src/ast/make.rs @@ -22,7 +22,8 @@ pub fn path_unqualified(segment: ast::PathSegment) -> ast::Path { pub fn path_qualified(qual: ast::Path, segment: ast::PathSegment) -> ast::Path { path_from_text(&format!("{}::{}", qual, segment)) } -fn path_from_text(text: &str) -> ast::Path { + +pub fn path_from_text(text: &str) -> ast::Path { ast_from_text(text) }