diff --git a/crates/ide_completion/src/completions.rs b/crates/ide_completion/src/completions.rs index e19edb2125..5f751d83a8 100644 --- a/crates/ide_completion/src/completions.rs +++ b/crates/ide_completion/src/completions.rs @@ -29,6 +29,7 @@ use crate::{ macro_::render_macro, pattern::{render_struct_pat, render_variant_pat}, render_field, render_resolution, render_tuple_field, + struct_literal::render_struct_literal, type_alias::{render_type_alias, render_type_alias_with_eq}, RenderContext, }, @@ -168,6 +169,16 @@ impl Completions { self.add(item); } + pub(crate) fn add_struct_literal( + &mut self, + ctx: &CompletionContext, + strukt: hir::Struct, + local_name: Option, + ) { + let item = render_struct_literal(RenderContext::new(ctx), strukt, local_name); + self.add_opt(item); + } + pub(crate) fn add_tuple_field( &mut self, ctx: &CompletionContext, diff --git a/crates/ide_completion/src/completions/record.rs b/crates/ide_completion/src/completions/record.rs index 8ede825a62..c9c09551f9 100644 --- a/crates/ide_completion/src/completions/record.rs +++ b/crates/ide_completion/src/completions/record.rs @@ -45,10 +45,81 @@ pub(crate) fn complete_record(acc: &mut Completions, ctx: &CompletionContext) -> Some(()) } +pub(crate) fn complete_record_literal( + acc: &mut Completions, + ctx: &CompletionContext, +) -> Option<()> { + if !ctx.expects_expression() { + return None; + } + + if let hir::Adt::Struct(strukt) = ctx.expected_type.as_ref()?.as_adt()? { + acc.add_struct_literal(ctx, strukt, None); + } + + Some(()) +} + #[cfg(test)] mod tests { use crate::tests::check_edit; + #[test] + fn literal_struct_completion_edit() { + check_edit( + "FooDesc {…}", + r#" +struct FooDesc { pub bar: bool } + +fn create_foo(foo_desc: &FooDesc) -> () { () } + +fn baz() { + let foo = create_foo(&$0); +} + "#, + r#" +struct FooDesc { pub bar: bool } + +fn create_foo(foo_desc: &FooDesc) -> () { () } + +fn baz() { + let foo = create_foo(&FooDesc { bar: ${1:()} }$0); +} + "#, + ) + } + + #[test] + fn literal_struct_complexion_module() { + check_edit( + "FooDesc {…}", + r#" +mod _69latrick { + pub struct FooDesc { pub six: bool, pub neuf: Vec, pub bar: bool } + pub fn create_foo(foo_desc: &FooDesc) -> () { () } +} + +fn baz() { + use _69latrick::*; + + let foo = create_foo(&$0); +} + "#, + r#" +mod _69latrick { + pub struct FooDesc { pub six: bool, pub neuf: Vec, pub bar: bool } + pub fn create_foo(foo_desc: &FooDesc) -> () { () } +} + +fn baz() { + use _69latrick::*; + + let foo = create_foo(&FooDesc { six: ${1:()}, neuf: ${2:()}, bar: ${3:()} }$0); +} + "#, + ); + } + #[test] fn default_completion_edit() { check_edit( diff --git a/crates/ide_completion/src/lib.rs b/crates/ide_completion/src/lib.rs index 386b6bf0e7..f10f3772b1 100644 --- a/crates/ide_completion/src/lib.rs +++ b/crates/ide_completion/src/lib.rs @@ -156,6 +156,7 @@ pub fn completions( completions::unqualified_path::complete_unqualified_path(&mut acc, &ctx); completions::dot::complete_dot(&mut acc, &ctx); completions::record::complete_record(&mut acc, &ctx); + completions::record::complete_record_literal(&mut acc, &ctx); completions::pattern::complete_pattern(&mut acc, &ctx); completions::postfix::complete_postfix(&mut acc, &ctx); completions::trait_impl::complete_trait_impl(&mut acc, &ctx); diff --git a/crates/ide_completion/src/render.rs b/crates/ide_completion/src/render.rs index 23be915bbc..527838e8bf 100644 --- a/crates/ide_completion/src/render.rs +++ b/crates/ide_completion/src/render.rs @@ -7,6 +7,7 @@ pub(crate) mod enum_variant; pub(crate) mod const_; pub(crate) mod pattern; pub(crate) mod type_alias; +pub(crate) mod struct_literal; mod builder_ext; diff --git a/crates/ide_completion/src/render/struct_literal.rs b/crates/ide_completion/src/render/struct_literal.rs new file mode 100644 index 0000000000..a6571eb022 --- /dev/null +++ b/crates/ide_completion/src/render/struct_literal.rs @@ -0,0 +1,121 @@ +//! Renderer for `struct` literal. + +use hir::{db::HirDatabase, HasAttrs, HasVisibility, Name, StructKind}; +use ide_db::helpers::SnippetCap; +use itertools::Itertools; + +use crate::{item::CompletionKind, render::RenderContext, CompletionItem, CompletionItemKind}; + +pub(crate) fn render_struct_literal( + ctx: RenderContext<'_>, + strukt: hir::Struct, + local_name: Option, +) -> Option { + let _p = profile::span("render_struct_literal"); + + let fields = strukt.fields(ctx.db()); + let (visible_fields, fields_omitted) = visible_fields(&ctx, &fields, strukt)?; + + if fields_omitted { + // If some fields are private you can't make `struct` literal. + return None; + } + + let name = local_name.unwrap_or_else(|| strukt.name(ctx.db())).to_string(); + let literal = render_literal(&ctx, &name, strukt.kind(ctx.db()), &visible_fields)?; + + Some(build_completion(ctx, name, literal, strukt)) +} + +fn build_completion( + ctx: RenderContext<'_>, + name: String, + literal: String, + def: impl HasAttrs + Copy, +) -> CompletionItem { + let mut item = CompletionItem::new(CompletionKind::Snippet, ctx.source_range(), name + " {…}"); + item.kind(CompletionItemKind::Snippet) + .set_documentation(ctx.docs(def)) + .set_deprecated(ctx.is_deprecated(def)) + .detail(&literal); + if let Some(snippet_cap) = ctx.snippet_cap() { + item.insert_snippet(snippet_cap, literal); + } else { + item.insert_text(literal); + }; + item.build() +} + +fn render_literal( + ctx: &RenderContext<'_>, + name: &str, + kind: StructKind, + fields: &[hir::Field], +) -> Option { + let mut literal = match kind { + StructKind::Tuple if ctx.snippet_cap().is_some() => render_tuple_as_literal(fields, name), + StructKind::Record => render_record_as_literal(ctx.db(), ctx.snippet_cap(), fields, name), + _ => return None, + }; + + if ctx.completion.is_param { + literal.push(':'); + literal.push(' '); + literal.push_str(name); + } + if ctx.snippet_cap().is_some() { + literal.push_str("$0"); + } + Some(literal) +} + +fn render_record_as_literal( + db: &dyn HirDatabase, + snippet_cap: Option, + fields: &[hir::Field], + name: &str, +) -> String { + let fields = fields.iter(); + if snippet_cap.is_some() { + format!( + "{name} {{ {} }}", + fields + .enumerate() + .map(|(idx, field)| format!("{}: ${{{}:()}}", field.name(db), idx + 1)) + .format(", "), + name = name + ) + } else { + format!( + "{name} {{ {} }}", + fields.map(|field| format!("{}: ()", field.name(db))).format(", "), + name = name + ) + } +} + +fn render_tuple_as_literal(fields: &[hir::Field], name: &str) -> String { + format!( + "{name}({})", + fields.iter().enumerate().map(|(idx, _)| format!("${}", idx + 1)).format(", "), + name = name + ) +} + +fn visible_fields( + ctx: &RenderContext<'_>, + fields: &[hir::Field], + item: impl HasAttrs, +) -> Option<(Vec, bool)> { + let module = ctx.completion.scope.module()?; + let n_fields = fields.len(); + let fields = fields + .iter() + .filter(|field| field.is_visible_from(ctx.db(), module)) + .copied() + .collect::>(); + + let fields_omitted = + n_fields - fields.len() > 0 || item.attrs(ctx.db()).by_key("non_exhaustive").exists(); + Some((fields, fields_omitted)) +} diff --git a/crates/ide_completion/src/tests/expression.rs b/crates/ide_completion/src/tests/expression.rs index 5c8dccc780..95aaff01ac 100644 --- a/crates/ide_completion/src/tests/expression.rs +++ b/crates/ide_completion/src/tests/expression.rs @@ -13,6 +13,60 @@ fn check_empty(ra_fixture: &str, expect: Expect) { expect.assert_eq(&actual); } +#[test] +fn complete_literal_struct_with_a_private_field() { + // `FooDesc.bar` is private, the completion should not be triggered. + check( + r#" +mod _69latrick { + pub struct FooDesc { pub six: bool, pub neuf: Vec, bar: bool } + pub fn create_foo(foo_desc: &FooDesc) -> () { () } +} + +fn baz() { + use _69latrick::*; + + let foo = create_foo(&$0); +} + "#, + // This should not contain `FooDesc {…}`. + expect![[r##" + kw unsafe + kw match + kw while + kw while let + kw loop + kw if + kw if let + kw for + kw true + kw false + kw mut + kw return + kw self + kw super + kw crate + st FooDesc + fn create_foo(…) fn(&FooDesc) + bt u32 + tt Trait + en Enum + st Record + st Tuple + md module + fn baz() fn() + st Unit + md _69latrick + ma makro!(…) #[macro_export] macro_rules! makro + fn function() fn() + sc STATIC + un Union + ev TupleV(…) (u32) + ct CONST + "##]], + ) +} + #[test] fn completes_various_bindings() { check_empty(