From 9beefef8f9bb9a976ba9bda3fe5fe2f057bba548 Mon Sep 17 00:00:00 2001 From: Alexandre Fourcat Date: Wed, 4 Aug 2021 00:46:50 +0200 Subject: [PATCH] Add completion for struct literal in which all fields are visible. Fix ide_completion tests. Move 'complete_record_literal' call to the main completion function. Fix a rendering bug when snippet not available. Checks if an expression is expected before adding completion for struct literal. Move 'completion struct literal with private field' test to 'expressions.rs' test file. Update 'expect' tests with new check in 'complete record literal'. --- crates/ide_completion/src/completions.rs | 11 ++ .../ide_completion/src/completions/record.rs | 71 ++++++++++ crates/ide_completion/src/lib.rs | 1 + crates/ide_completion/src/render.rs | 1 + .../src/render/struct_literal.rs | 121 ++++++++++++++++++ crates/ide_completion/src/tests/expression.rs | 54 ++++++++ 6 files changed, 259 insertions(+) create mode 100644 crates/ide_completion/src/render/struct_literal.rs diff --git a/crates/ide_completion/src/completions.rs b/crates/ide_completion/src/completions.rs index e19edb21252..5f751d83a83 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 8ede825a622..c9c09551f9c 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 386b6bf0e76..f10f3772b19 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 23be915bbc9..527838e8bf5 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 00000000000..a6571eb0223 --- /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 5c8dccc7803..95aaff01ac3 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(