Repository: rust-lang/rust-analyzer
Pull request #13638 (2022-11-19 06:06:36)

feat: adds hover hint to ".." in record pattern
Hovering on the "rest" pattern in struct destructuring,
```rust
struct Baz {
    a: u32,
    b: u32,
    c: u32,
    d: u32
}

let Baz { a, b, ..$0} = Baz { a: 1, b: 2, c: 3, d: 4 };
```
shows:

```
.., c: u32, d: u32
```

Currently only works with struct patterns.

![image](https://user-images.githubusercontent.com/51814158/202837115-f424cc26-c2d7-4027-8eea-eeb7749ad146.png)



Patch:
diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs
index 838fb18c3d..9cbfed4763 100644
--- a/crates/ide/src/hover.rs
+++ b/crates/ide/src/hover.rs
@@ -127,6 +127,7 @@ pub(crate) fn hover(
             original_token.parent().and_then(ast::TokenTree::cast),
             Some(tt) if tt.syntax().ancestors().any(|it| ast::Meta::can_cast(it.kind()))
         );
+
     // prefer descending the same token kind in attribute expansions, in normal macros text
     // equivalency is more important
     let descended = if in_attr {
@@ -135,54 +136,67 @@ pub(crate) fn hover(
         sema.descend_into_macros_with_same_text(original_token.clone())
     };
 
-    // FIXME: Definition should include known lints and the like instead of having this special case here
-    let hovered_lint = descended.iter().find_map(|token| {
-        let attr = token.parent_ancestors().find_map(ast::Attr::cast)?;
-        render::try_for_lint(&attr, token)
-    });
-    if let Some(res) = hovered_lint {
-        return Some(RangeInfo::new(original_token.text_range(), res));
-    }
-
+    // try lint hover
     let result = descended
         .iter()
-        .filter_map(|token| {
-            let node = token.parent()?;
-            let class = IdentClass::classify_token(sema, token)?;
-            if let IdentClass::Operator(OperatorClass::Await(_)) = class {
-                // It's better for us to fall back to the keyword hover here,
-                // rendering poll is very confusing
-                return None;
-            }
-            Some(class.definitions().into_iter().zip(iter::once(node).cycle()))
+        .find_map(|token| {
+            // FIXME: Definition should include known lints and the like instead of having this special case here
+            let attr = token.parent_ancestors().find_map(ast::Attr::cast)?;
+            render::try_for_lint(&attr, token)
         })
-        .flatten()
-        .unique_by(|&(def, _)| def)
-        .filter_map(|(def, node)| hover_for_definition(sema, file_id, def, &node, config))
-        .reduce(|mut acc: HoverResult, HoverResult { markup, actions }| {
-            acc.actions.extend(actions);
-            acc.markup = Markup::from(format!("{}\n---\n{}", acc.markup, markup));
-            acc
-        });
+        // try item definitions
+        .or_else(|| {
+            descended
+                .iter()
+                .filter_map(|token| {
+                    let node = token.parent()?;
+                    let class = IdentClass::classify_token(sema, token)?;
+                    if let IdentClass::Operator(OperatorClass::Await(_)) = class {
+                        // It's better for us to fall back to the keyword hover here,
+                        // rendering poll is very confusing
+                        return None;
+                    }
+                    Some(class.definitions().into_iter().zip(iter::once(node).cycle()))
+                })
+                .flatten()
+                .unique_by(|&(def, _)| def)
+                .filter_map(|(def, node)| hover_for_definition(sema, file_id, def, &node, config))
+                .reduce(|mut acc: HoverResult, HoverResult { markup, actions }| {
+                    acc.actions.extend(actions);
+                    acc.markup = Markup::from(format!("{}\n---\n{}", acc.markup, markup));
+                    acc
+                })
+        })
+        // try keywords
+        .or_else(|| descended.iter().find_map(|token| render::keyword(sema, config, token)))
+        // try rest item hover
+        .or_else(|| {
+            descended.iter().find_map(|token| {
+                if token.kind() != DOT2 {
+                    return None;
+                }
 
-    if result.is_none() {
-        // fallbacks, show keywords or types
+                let rest_pat = token.parent().and_then(ast::RestPat::cast)?;
+                let record_pat_field_list =
+                    rest_pat.syntax().parent().and_then(ast::RecordPatFieldList::cast)?;
 
-        let res = descended.iter().find_map(|token| render::keyword(sema, config, token));
-        if let Some(res) = res {
-            return Some(RangeInfo::new(original_token.text_range(), res));
-        }
-        let res = descended
-            .iter()
-            .find_map(|token| hover_type_fallback(sema, config, token, &original_token));
-        if let Some(_) = res {
-            return res;
-        }
-    }
-    result.map(|mut res: HoverResult| {
-        res.actions = dedupe_or_merge_hover_actions(res.actions);
-        RangeInfo::new(original_token.text_range(), res)
-    })
+                let record_pat =
+                    record_pat_field_list.syntax().parent().and_then(ast::RecordPat::cast)?;
+
+                Some(render::struct_rest_pat(sema, config, &record_pat))
+            })
+        });
+
+    result
+        .map(|mut res: HoverResult| {
+            res.actions = dedupe_or_merge_hover_actions(res.actions);
+            RangeInfo::new(original_token.text_range(), res)
+        })
+        // fallback to type hover if there aren't any other suggestions
+        // this finds its own range instead of using the closest token's range
+        .or_else(|| {
+            descended.iter().find_map(|token| hover_type_fallback(sema, config, token, &token))
+        })
 }
 
 pub(crate) fn hover_for_definition(
@@ -269,6 +283,7 @@ fn hover_type_fallback(
     };
 
     let res = render::type_info(sema, config, &expr_or_pat)?;
+
     let range = sema
         .original_range_opt(&node)
         .map(|frange| frange.range)
diff --git a/crates/ide/src/hover/render.rs b/crates/ide/src/hover/render.rs
index d109c07691..fb00a40f96 100644
--- a/crates/ide/src/hover/render.rs
+++ b/crates/ide/src/hover/render.rs
@@ -14,7 +14,9 @@ use ide_db::{
 use itertools::Itertools;
 use stdx::format_to;
 use syntax::{
-    algo, ast, match_ast, AstNode, Direction,
+    algo,
+    ast::{self, RecordPat},
+    match_ast, AstNode, Direction,
     SyntaxKind::{LET_EXPR, LET_STMT},
     SyntaxToken, T,
 };
@@ -250,6 +252,50 @@ pub(super) fn keyword(
     Some(HoverResult { markup, actions })
 }
 
+/// Returns missing types in a record pattern.
+/// Only makes sense when there's a rest pattern in the record pattern.
+/// i.e. `let S {a, ..} = S {a: 1, b: 2}`
+pub(super) fn struct_rest_pat(
+    sema: &Semantics<'_, RootDatabase>,
+    config: &HoverConfig,
+    pattern: &RecordPat,
+) -> HoverResult {
+    let missing_fields = sema.record_pattern_missing_fields(pattern);
+
+    // if there are no missing fields, the end result is a hover that shows ".."
+    // should be left in to indicate that there are no more fields in the pattern
+    // example, S {a: 1, b: 2, ..} when struct S {a: u32, b: u32}
+
+    let mut res = HoverResult::default();
+    let mut targets: Vec<hir::ModuleDef> = Vec::new();
+    let mut push_new_def = |item: hir::ModuleDef| {
+        if !targets.contains(&item) {
+            targets.push(item);
+        }
+    };
+    for (_, t) in &missing_fields {
+        walk_and_push_ty(sema.db, &t, &mut push_new_def);
+    }
+
+    res.markup = {
+        let mut s = String::from(".., ");
+        for (f, _) in &missing_fields {
+            s += f.display(sema.db).to_string().as_ref();
+            s += ", ";
+        }
+        // get rid of trailing comma
+        s.truncate(s.len() - 2);
+
+        if config.markdown() {
+            Markup::fenced_block(&s)
+        } else {
+            s.into()
+        }
+    };
+    res.actions.push(HoverAction::goto_type_from_targets(sema.db, targets));
+    res
+}
+
 pub(super) fn try_for_lint(attr: &ast::Attr, token: &SyntaxToken) -> Option<HoverResult> {
     let (path, tt) = attr.as_simple_call()?;
     if !tt.syntax().text_range().contains(token.text_range().start()) {


Test patch:
diff --git a/crates/ide/src/hover/tests.rs b/crates/ide/src/hover/tests.rs
index eb997e6fef..f8be4cfb04 100644
--- a/crates/ide/src/hover/tests.rs
+++ b/crates/ide/src/hover/tests.rs
@@ -5307,3 +5307,38 @@ fn main() { $0V; }
         "#]],
     );
 }
+
+#[test]
+fn hover_rest_pat() {
+    check(
+        r#"
+struct Struct {a: u32, b: u32, c: u8, d: u16};
+
+fn main() {
+    let Struct {a, c, .$0.} = Struct {a: 1, b: 2, c: 3, d: 4};
+}
+"#,
+        expect![[r#"
+            *..*
+            ```rust
+            .., b: u32, d: u16
+            ```
+        "#]],
+    );
+
+    check(
+        r#"
+struct Struct {a: u32, b: u32, c: u8, d: u16};
+
+fn main() {
+    let Struct {a, b, c, d, .$0.} = Struct {a: 1, b: 2, c: 3, d: 4};
+}
+"#,
+        expect![[r#"
+            *..*
+            ```rust
+            ..
+            ```
+        "#]],
+    );
+}