about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2022-03-12 15:44:51 +0000
committerGitHub <noreply@github.com>2022-03-12 15:44:51 +0000
commitff7e057dca58865a670581ab5dcaa006ed17696c (patch)
tree98fa62c4c65549e6a4b6ead4fa686346de46cc0d
parent421d9643ba5d99446269decfc5bb82a948d16029 (diff)
parentf922b805fef6f38da2f5c901fbcd76b9e4ccad92 (diff)
downloadrust-ff7e057dca58865a670581ab5dcaa006ed17696c.tar.gz
rust-ff7e057dca58865a670581ab5dcaa006ed17696c.zip
Merge #11691
11691: feat: Suggest union literals, suggest union fields within an empty union literal r=Veykril a=m0rg-dev

Adds a `Union {…}` completion in contexts where a union is expected, expanding to a choice of available fields (if snippets are supported):

![image](https://user-images.githubusercontent.com/38578268/158023335-84c03e39-daf0-4a52-b969-f40b01501cc8.png)
![image](https://user-images.githubusercontent.com/38578268/158023354-db49d0bb-034c-49d3-bc02-07414179cb61.png)

Also, adds support for listing possible fields in an empty union literal.

![image](https://user-images.githubusercontent.com/38578268/158023398-4695ae34-ce64-4f40-8494-68731a3030c6.png)
![image](https://user-images.githubusercontent.com/38578268/158023406-be96dd95-125a-47ac-9628-0bce634ca2eb.png)

Closes #11568.

Co-authored-by: Morgan Thomas <corp@m0rg.dev>
-rw-r--r--crates/ide_completion/src/completions.rs12
-rw-r--r--crates/ide_completion/src/completions/record.rs82
-rw-r--r--crates/ide_completion/src/render.rs1
-rw-r--r--crates/ide_completion/src/render/union_literal.rs76
-rw-r--r--crates/ide_completion/src/tests/record.rs36
5 files changed, 179 insertions, 28 deletions
diff --git a/crates/ide_completion/src/completions.rs b/crates/ide_completion/src/completions.rs
index 380cfe95ddf..91e6b84294e 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<hir::ModPath>,
+        local_name: Option<hir::Name>,
+    ) {
+        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 78d06231060..264b3784bf1 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 ctx.previous_token_is(T![.]) {
-                let mut item =
-                    CompletionItem::new(CompletionItemKind::Snippet, ctx.source_range(), "..");
-                item.insert_text(".");
-                item.add_to(acc);
-                return None;
+
+            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
             }
-            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 8624a8c3d2d..0ed346c55e2 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 00000000000..80499e102b4
--- /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<hir::ModPath>,
+    local_name: Option<Name>,
+) -> Option<CompletionItem> {
+    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 87d0d853b6f..5e9367960f7 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#""#]],
+    )
+}