diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs index 42009ea649..7a8edfea83 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 { + 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 4cd1fce8c9..a131427f5f 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); +fn test(v: S<(S, 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); + fn test(v: S<(S, S<()>)>, f: F) { + let a: S<(S, S<()>)> = v; + let S((b, c)) = v; + let a @ S((b, c)): S<(S, S<()>)> = v; + let a: F = f; + } + "#]], + ); + } + + #[test] + fn edit_for_closure_param() { + check_edit( + TEST_CONFIG, + r#" +fn test(t: T) { + let f = |a, b, c| {}; + let result = f(42, "", t); +} +"#, + expect![[r#" + fn test(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); +fn test() { + let f = || { 3 }; + let f = |a: S| { S(a) }; +} +"#, + expect![[r#" + struct S(T); + fn test() { + let f = || -> i32 { 3 }; + let f = |a: S| -> S> { S(a) }; + } + "#]], + ); + } + + #[test] + fn edit_prefixes_paths() { + check_edit( + TEST_CONFIG, + r#" +pub struct S(T); +mod middle { + pub struct S(T, U); + pub fn make() -> S, super::S> { loop {} } + + mod inner { + pub struct S(T); + } + + fn test() { + let a = make(); + } +} +"#, + expect![[r#" + pub struct S(T); + mod middle { + pub struct S(T, U); + pub fn make() -> S, super::S> { loop {} } + + mod inner { + pub struct S(T); + } + + fn test() { + let a: S, crate::S> = 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); +fn foo() -> impl Trait {} +fn bar() -> S {} +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); +fn test() { + let f = || 3; + let f = |a: S| S(a); +} +"#, + ); + } } diff --git a/crates/ide/src/inlay_hints/chaining.rs b/crates/ide/src/inlay_hints/chaining.rs index eed5a2554c..6db9b8b544 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 eb5a464e7c..6214e9c8e7 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, 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(()) }