diff --git a/crates/ide_completion/src/completions.rs b/crates/ide_completion/src/completions.rs index 380cfe95dd..91e6b84294 100644 --- a/crates/ide_completion/src/completions.rs +++ b/crates/ide_completion/src/completions.rs @@ -35,6 +35,7 @@ use crate::{ render_field, render_resolution, render_tuple_field, struct_literal::render_struct_literal, type_alias::{render_type_alias, render_type_alias_with_eq}, + union_literal::render_union_literal, RenderContext, }, CompletionContext, CompletionItem, CompletionItemKind, @@ -234,6 +235,17 @@ impl Completions { self.add_opt(item); } + pub(crate) fn add_union_literal( + &mut self, + ctx: &CompletionContext, + un: hir::Union, + path: Option, + local_name: Option, + ) { + let item = render_union_literal(RenderContext::new(ctx, false), un, path, 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 78d0623106..264b3784bf 100644 --- a/crates/ide_completion/src/completions/record.rs +++ b/crates/ide_completion/src/completions/record.rs @@ -14,32 +14,51 @@ pub(crate) fn complete_record(acc: &mut Completions, ctx: &CompletionContext) -> | ImmediateLocation::RecordExprUpdate(record_expr), ) => { let ty = ctx.sema.type_of_expr(&Expr::RecordExpr(record_expr.clone())); - let default_trait = ctx.famous_defs().core_default_Default(); - let impl_default_trait = default_trait.zip(ty).map_or(false, |(default_trait, ty)| { - ty.original.impls_trait(ctx.db, default_trait, &[]) - }); - let missing_fields = ctx.sema.record_literal_missing_fields(record_expr); - if impl_default_trait && !missing_fields.is_empty() && ctx.path_qual().is_none() { - let completion_text = "..Default::default()"; - let mut item = - CompletionItem::new(SymbolKind::Field, ctx.source_range(), completion_text); - let completion_text = - completion_text.strip_prefix(ctx.token.text()).unwrap_or(completion_text); - item.insert_text(completion_text).set_relevance(CompletionRelevance { - exact_postfix_snippet_match: true, - ..Default::default() - }); - item.add_to(acc); + if let Some(hir::Adt::Union(un)) = ty.as_ref().and_then(|t| t.original.as_adt()) { + // ctx.sema.record_literal_missing_fields will always return + // an empty Vec on a union literal. This is normally + // reasonable, but here we'd like to present the full list + // of fields if the literal is empty. + let were_fields_specified = record_expr + .record_expr_field_list() + .and_then(|fl| fl.fields().next()) + .is_some(); + + match were_fields_specified { + false => un.fields(ctx.db).into_iter().map(|f| (f, f.ty(ctx.db))).collect(), + true => vec![], + } + } else { + let missing_fields = ctx.sema.record_literal_missing_fields(record_expr); + + let default_trait = ctx.famous_defs().core_default_Default(); + let impl_default_trait = + default_trait.zip(ty.as_ref()).map_or(false, |(default_trait, ty)| { + ty.original.impls_trait(ctx.db, default_trait, &[]) + }); + + if impl_default_trait && !missing_fields.is_empty() && ctx.path_qual().is_none() { + let completion_text = "..Default::default()"; + let mut item = + CompletionItem::new(SymbolKind::Field, ctx.source_range(), completion_text); + let completion_text = + completion_text.strip_prefix(ctx.token.text()).unwrap_or(completion_text); + item.insert_text(completion_text).set_relevance(CompletionRelevance { + exact_postfix_snippet_match: true, + ..Default::default() + }); + item.add_to(acc); + } + if ctx.previous_token_is(T![.]) { + let mut item = + CompletionItem::new(CompletionItemKind::Snippet, ctx.source_range(), ".."); + item.insert_text("."); + item.add_to(acc); + return None; + } + missing_fields } - if ctx.previous_token_is(T![.]) { - let mut item = - CompletionItem::new(CompletionItemKind::Snippet, ctx.source_range(), ".."); - item.insert_text("."); - item.add_to(acc); - return None; - } - missing_fields } Some(ImmediateLocation::RecordPat(record_pat)) => { ctx.sema.record_pattern_missing_fields(record_pat) @@ -62,14 +81,21 @@ pub(crate) fn complete_record_literal( return None; } - if let hir::Adt::Struct(strukt) = ctx.expected_type.as_ref()?.as_adt()? { - if ctx.path_qual().is_none() { + match ctx.expected_type.as_ref()?.as_adt()? { + hir::Adt::Struct(strukt) if ctx.path_qual().is_none() => { let module = if let Some(module) = ctx.module { module } else { strukt.module(ctx.db) }; let path = module.find_use_path(ctx.db, hir::ModuleDef::from(strukt)); acc.add_struct_literal(ctx, strukt, path, None); } - } + hir::Adt::Union(un) if ctx.path_qual().is_none() => { + let module = if let Some(module) = ctx.module { module } else { un.module(ctx.db) }; + let path = module.find_use_path(ctx.db, hir::ModuleDef::from(un)); + + acc.add_union_literal(ctx, un, path, None); + } + _ => {} + }; Some(()) } diff --git a/crates/ide_completion/src/render.rs b/crates/ide_completion/src/render.rs index 8624a8c3d2..0ed346c55e 100644 --- a/crates/ide_completion/src/render.rs +++ b/crates/ide_completion/src/render.rs @@ -9,6 +9,7 @@ pub(crate) mod pattern; pub(crate) mod type_alias; pub(crate) mod struct_literal; pub(crate) mod compound; +pub(crate) mod union_literal; use hir::{AsAssocItem, HasAttrs, HirDisplay, ScopeDef}; use ide_db::{helpers::item_name, RootDatabase, SnippetCap, SymbolKind}; diff --git a/crates/ide_completion/src/render/union_literal.rs b/crates/ide_completion/src/render/union_literal.rs new file mode 100644 index 0000000000..80499e102b --- /dev/null +++ b/crates/ide_completion/src/render/union_literal.rs @@ -0,0 +1,76 @@ +//! Renderer for `union` literals. + +use hir::{HirDisplay, Name, StructKind}; +use itertools::Itertools; + +use crate::{ + render::{ + compound::{format_literal_label, visible_fields}, + RenderContext, + }, + CompletionItem, CompletionItemKind, +}; + +pub(crate) fn render_union_literal( + ctx: RenderContext, + un: hir::Union, + path: Option, + local_name: Option, +) -> Option { + let name = local_name.unwrap_or_else(|| un.name(ctx.db())).to_smol_str(); + + let qualified_name = match path { + Some(p) => p.to_string(), + None => name.to_string(), + }; + + let mut item = CompletionItem::new( + CompletionItemKind::Snippet, + ctx.source_range(), + format_literal_label(&name, StructKind::Record), + ); + + let fields = un.fields(ctx.db()); + let (fields, fields_omitted) = visible_fields(&ctx, &fields, un)?; + + if fields.is_empty() { + return None; + } + + let literal = if ctx.snippet_cap().is_some() { + format!( + "{} {{ ${{1|{}|}}: ${{2:()}} }}$0", + qualified_name, + fields.iter().map(|field| field.name(ctx.db())).format(",") + ) + } else { + format!( + "{} {{ {} }}", + qualified_name, + fields + .iter() + .format_with(", ", |field, f| { f(&format_args!("{}: ()", field.name(ctx.db()))) }) + ) + }; + + let detail = format!( + "{} {{ {}{} }}", + qualified_name, + fields.iter().format_with(", ", |field, f| { + f(&format_args!("{}: {}", field.name(ctx.db()), field.ty(ctx.db()).display(ctx.db()))) + }), + if fields_omitted { ", .." } else { "" } + ); + + item.set_documentation(ctx.docs(un)) + .set_deprecated(ctx.is_deprecated(un)) + .detail(&detail) + .set_relevance(ctx.completion_relevance()); + + match ctx.snippet_cap() { + Some(snippet_cap) => item.insert_snippet(snippet_cap, literal), + None => item.insert_text(literal), + }; + + Some(item.build()) +} diff --git a/crates/ide_completion/src/tests/record.rs b/crates/ide_completion/src/tests/record.rs index 87d0d853b6..5e9367960f 100644 --- a/crates/ide_completion/src/tests/record.rs +++ b/crates/ide_completion/src/tests/record.rs @@ -204,3 +204,39 @@ fn main() { "#]], ); } + +#[test] +fn empty_union_literal() { + check( + r#" +union Union { foo: u32, bar: f32 } + +fn foo() { + let other = Union { + $0 + }; +} + "#, + expect![[r#" + fd foo u32 + fd bar f32 + "#]], + ) +} + +#[test] +fn dont_suggest_additional_union_fields() { + check( + r#" +union Union { foo: u32, bar: f32 } + +fn foo() { + let other = Union { + foo: 1, + $0 + }; +} + "#, + expect![[r#""#]], + ) +}