about summary refs log tree commit diff
diff options
context:
space:
mode:
authorRyo Yoshida <low.ryoshida@gmail.com>2023-04-08 04:06:32 +0900
committerRyo Yoshida <low.ryoshida@gmail.com>2023-04-12 19:03:57 +0900
commitc978d4bf0c92e77ef94f354af8402af71bd7b5ab (patch)
treeb3ab82616b6453c666f8ba5fc97905c10c610e68
parentfcbc250723cdaf329ffd941dccb9e37568b88736 (diff)
Implement text edits for inlay hints
-rw-r--r--crates/ide/src/inlay_hints.rs50
-rw-r--r--crates/ide/src/inlay_hints/bind_pat.rs187
-rw-r--r--crates/ide/src/inlay_hints/chaining.rs11
-rw-r--r--crates/ide/src/inlay_hints/closure_ret.rs31
4 files changed, 262 insertions, 17 deletions
diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs
index 42009ea6491..7a8edfea835 100644
--- a/crates/ide/src/inlay_hints.rs
+++ b/crates/ide/src/inlay_hints.rs
@@ -14,7 +14,7 @@ use smallvec::{smallvec, SmallVec};
 use stdx::never;
 use syntax::{
     ast::{self, AstNode},
-    match_ast, NodeOrToken, SyntaxNode, TextRange,
+    match_ast, NodeOrToken, SyntaxNode, TextRange, TextSize,
 };
 use text_edit::TextEdit;
 
@@ -359,6 +359,23 @@ fn label_of_ty(
     Some(r)
 }
 
+fn ty_to_text_edit(
+    sema: &Semantics<'_, RootDatabase>,
+    node_for_hint: &SyntaxNode,
+    ty: &hir::Type,
+    offset_to_insert: TextSize,
+    prefix: String,
+) -> Option<TextEdit> {
+    let scope = sema.scope(node_for_hint)?;
+    // FIXME: Limit the length and bail out on excess somehow?
+    let rendered = ty.display_source_code(scope.db, scope.module().into(), false).ok()?;
+
+    let mut builder = TextEdit::builder();
+    builder.insert(offset_to_insert, prefix);
+    builder.insert(offset_to_insert, rendered);
+    Some(builder.finish())
+}
+
 // Feature: Inlay Hints
 //
 // rust-analyzer shows additional information inline with the source code.
@@ -566,6 +583,37 @@ mod tests {
         expect.assert_debug_eq(&inlay_hints)
     }
 
+    /// Computes inlay hints for the fixture, applies all the provided text edits and then runs
+    /// expect test.
+    #[track_caller]
+    pub(super) fn check_edit(config: InlayHintsConfig, ra_fixture: &str, expect: Expect) {
+        let (analysis, file_id) = fixture::file(ra_fixture);
+        let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
+
+        let edits = inlay_hints
+            .into_iter()
+            .filter_map(|hint| hint.text_edit)
+            .reduce(|mut acc, next| {
+                acc.union(next).expect("merging text edits failed");
+                acc
+            })
+            .expect("no edit returned");
+
+        let mut actual = analysis.file_text(file_id).unwrap().to_string();
+        edits.apply(&mut actual);
+        expect.assert_eq(&actual);
+    }
+
+    #[track_caller]
+    pub(super) fn check_no_edit(config: InlayHintsConfig, ra_fixture: &str) {
+        let (analysis, file_id) = fixture::file(ra_fixture);
+        let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
+
+        let edits: Vec<_> = inlay_hints.into_iter().filter_map(|hint| hint.text_edit).collect();
+
+        assert!(edits.is_empty(), "unexpected edits: {edits:?}");
+    }
+
     #[test]
     fn hints_disabled() {
         check_with_config(
diff --git a/crates/ide/src/inlay_hints/bind_pat.rs b/crates/ide/src/inlay_hints/bind_pat.rs
index 4cd1fce8c92..a131427f5fd 100644
--- a/crates/ide/src/inlay_hints/bind_pat.rs
+++ b/crates/ide/src/inlay_hints/bind_pat.rs
@@ -13,7 +13,7 @@ use syntax::{
 };
 
 use crate::{
-    inlay_hints::{closure_has_block_body, label_of_ty},
+    inlay_hints::{closure_has_block_body, label_of_ty, ty_to_text_edit},
     InlayHint, InlayHintsConfig, InlayKind,
 };
 
@@ -36,7 +36,7 @@ pub(super) fn hints(
         return None;
     }
 
-    let label = label_of_ty(famous_defs, config, ty)?;
+    let label = label_of_ty(famous_defs, config, ty.clone())?;
 
     if config.hide_named_constructor_hints
         && is_named_constructor(sema, pat, &label.to_string()).is_some()
@@ -44,6 +44,23 @@ pub(super) fn hints(
         return None;
     }
 
+    let type_annotation_is_valid = desc_pat
+        .syntax()
+        .parent()
+        .map(|it| ast::LetStmt::can_cast(it.kind()) || ast::Param::can_cast(it.kind()))
+        .unwrap_or(false);
+    let text_edit = if type_annotation_is_valid {
+        ty_to_text_edit(
+            sema,
+            desc_pat.syntax(),
+            &ty,
+            pat.syntax().text_range().end(),
+            String::from(": "),
+        )
+    } else {
+        None
+    };
+
     acc.push(InlayHint {
         range: match pat.name() {
             Some(name) => name.syntax().text_range(),
@@ -51,7 +68,7 @@ pub(super) fn hints(
         },
         kind: InlayKind::Type,
         label,
-        text_edit: None,
+        text_edit,
     });
 
     Some(())
@@ -178,14 +195,16 @@ fn pat_is_enum_variant(db: &RootDatabase, bind_pat: &ast::IdentPat, pat_ty: &hir
 mod tests {
     // This module also contains tests for super::closure_ret
 
+    use expect_test::expect;
     use hir::ClosureStyle;
     use syntax::{TextRange, TextSize};
     use test_utils::extract_annotations;
 
-    use crate::{fixture, inlay_hints::InlayHintsConfig};
+    use crate::{fixture, inlay_hints::InlayHintsConfig, ClosureReturnTypeHints};
 
-    use crate::inlay_hints::tests::{check, check_with_config, DISABLED_CONFIG, TEST_CONFIG};
-    use crate::ClosureReturnTypeHints;
+    use crate::inlay_hints::tests::{
+        check, check_edit, check_no_edit, check_with_config, DISABLED_CONFIG, TEST_CONFIG,
+    };
 
     #[track_caller]
     fn check_types(ra_fixture: &str) {
@@ -1014,4 +1033,160 @@ fn main() {
 }"#,
         );
     }
+
+    #[test]
+    fn edit_for_let_stmt() {
+        check_edit(
+            TEST_CONFIG,
+            r#"
+struct S<T>(T);
+fn test<F>(v: S<(S<i32>, S<()>)>, f: F) {
+    let a = v;
+    let S((b, c)) = v;
+    let a @ S((b, c)) = v;
+    let a = f;
+}
+"#,
+            expect![[r#"
+                struct S<T>(T);
+                fn test<F>(v: S<(S<i32>, S<()>)>, f: F) {
+                    let a: S<(S<i32>, S<()>)> = v;
+                    let S((b, c)) = v;
+                    let a @ S((b, c)): S<(S<i32>, S<()>)> = v;
+                    let a: F = f;
+                }
+            "#]],
+        );
+    }
+
+    #[test]
+    fn edit_for_closure_param() {
+        check_edit(
+            TEST_CONFIG,
+            r#"
+fn test<T>(t: T) {
+    let f = |a, b, c| {};
+    let result = f(42, "", t);
+}
+"#,
+            expect![[r#"
+                fn test<T>(t: T) {
+                    let f = |a: i32, b: &str, c: T| {};
+                    let result: () = f(42, "", t);
+                }
+            "#]],
+        );
+    }
+
+    #[test]
+    fn edit_for_closure_ret() {
+        check_edit(
+            TEST_CONFIG,
+            r#"
+struct S<T>(T);
+fn test() {
+    let f = || { 3 };
+    let f = |a: S<usize>| { S(a) };
+}
+"#,
+            expect![[r#"
+                struct S<T>(T);
+                fn test() {
+                    let f = || -> i32 { 3 };
+                    let f = |a: S<usize>| -> S<S<usize>> { S(a) };
+                }
+            "#]],
+        );
+    }
+
+    #[test]
+    fn edit_prefixes_paths() {
+        check_edit(
+            TEST_CONFIG,
+            r#"
+pub struct S<T>(T);
+mod middle {
+    pub struct S<T, U>(T, U);
+    pub fn make() -> S<inner::S<i64>, super::S<usize>> { loop {} }
+
+    mod inner {
+        pub struct S<T>(T);
+    }
+
+    fn test() {
+        let a = make();
+    }
+}
+"#,
+            expect![[r#"
+                pub struct S<T>(T);
+                mod middle {
+                    pub struct S<T, U>(T, U);
+                    pub fn make() -> S<inner::S<i64>, super::S<usize>> { loop {} }
+
+                    mod inner {
+                        pub struct S<T>(T);
+                    }
+
+                    fn test() {
+                        let a: S<inner::S<i64>, crate::S<usize>> = make();
+                    }
+                }
+            "#]],
+        );
+    }
+
+    #[test]
+    fn no_edit_for_top_pat_where_type_annotation_is_invalid() {
+        check_no_edit(
+            TEST_CONFIG,
+            r#"
+fn test() {
+    if let a = 42 {}
+    while let a = 42 {}
+    match 42 {
+        a => (),
+    }
+}
+"#,
+        )
+    }
+
+    #[test]
+    fn no_edit_for_opaque_type() {
+        check_no_edit(
+            TEST_CONFIG,
+            r#"
+trait Trait {}
+struct S<T>(T);
+fn foo() -> impl Trait {}
+fn bar() -> S<impl Trait> {}
+fn test() {
+    let a = foo();
+    let a = bar();
+    let f = || { foo() };
+    let f = || { bar() };
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn no_edit_for_closure_return_without_body_block() {
+        // We can lift this limitation; see FIXME in closure_ret module.
+        let config = InlayHintsConfig {
+            closure_return_type_hints: ClosureReturnTypeHints::Always,
+            ..TEST_CONFIG
+        };
+        check_no_edit(
+            config,
+            r#"
+struct S<T>(T);
+fn test() {
+    let f = || 3;
+    let f = |a: S<usize>| S(a);
+}
+"#,
+        );
+    }
 }
diff --git a/crates/ide/src/inlay_hints/chaining.rs b/crates/ide/src/inlay_hints/chaining.rs
index eed5a2554cc..6db9b8b5443 100644
--- a/crates/ide/src/inlay_hints/chaining.rs
+++ b/crates/ide/src/inlay_hints/chaining.rs
@@ -603,7 +603,16 @@ fn main() {
                             },
                             "",
                         ],
-                        text_edit: None,
+                        text_edit: Some(
+                            TextEdit {
+                                indels: [
+                                    Indel {
+                                        insert: ": Struct",
+                                        delete: 130..130,
+                                    },
+                                ],
+                            },
+                        ),
                     },
                     InlayHint {
                         range: 145..185,
diff --git a/crates/ide/src/inlay_hints/closure_ret.rs b/crates/ide/src/inlay_hints/closure_ret.rs
index eb5a464e7c8..6214e9c8e7f 100644
--- a/crates/ide/src/inlay_hints/closure_ret.rs
+++ b/crates/ide/src/inlay_hints/closure_ret.rs
@@ -1,14 +1,14 @@
 //! Implementation of "closure return type" inlay hints.
+//!
+//! Tests live in [`bind_pat`][super::bind_pat] module.
 use ide_db::{base_db::FileId, famous_defs::FamousDefs};
 use syntax::ast::{self, AstNode};
 
 use crate::{
-    inlay_hints::closure_has_block_body, ClosureReturnTypeHints, InlayHint, InlayHintsConfig,
-    InlayKind,
+    inlay_hints::{closure_has_block_body, label_of_ty, ty_to_text_edit},
+    ClosureReturnTypeHints, InlayHint, InlayHintsConfig, InlayKind,
 };
 
-use super::label_of_ty;
-
 pub(super) fn hints(
     acc: &mut Vec<InlayHint>,
     famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>,
@@ -24,26 +24,39 @@ pub(super) fn hints(
         return None;
     }
 
-    if !closure_has_block_body(&closure)
-        && config.closure_return_type_hints == ClosureReturnTypeHints::WithBlock
-    {
+    let has_block_body = closure_has_block_body(&closure);
+    if !has_block_body && config.closure_return_type_hints == ClosureReturnTypeHints::WithBlock {
         return None;
     }
 
     let param_list = closure.param_list()?;
 
     let closure = sema.descend_node_into_attributes(closure).pop()?;
-    let ty = sema.type_of_expr(&ast::Expr::ClosureExpr(closure))?.adjusted();
+    let ty = sema.type_of_expr(&ast::Expr::ClosureExpr(closure.clone()))?.adjusted();
     let callable = ty.as_callable(sema.db)?;
     let ty = callable.return_type();
     if ty.is_unit() {
         return None;
     }
+
+    // FIXME?: We could provide text edit to insert braces for closures with non-block body.
+    let text_edit = if has_block_body {
+        ty_to_text_edit(
+            sema,
+            closure.syntax(),
+            &ty,
+            param_list.syntax().text_range().end(),
+            String::from(" -> "),
+        )
+    } else {
+        None
+    };
+
     acc.push(InlayHint {
         range: param_list.syntax().text_range(),
         kind: InlayKind::ClosureReturnType,
         label: label_of_ty(famous_defs, config, ty)?,
-        text_edit: None,
+        text_edit,
     });
     Some(())
 }