//! FIXME: write short doc here use hir::{Adt, HirDisplay, Semantics, Type}; use ra_ide_db::RootDatabase; use ra_prof::profile; use ra_syntax::{ ast::{self, ArgListOwner, AstNode, TypeAscriptionOwner}, match_ast, SmolStr, TextRange, NodeOrToken, SyntaxKind, Direction }; use crate::{FileId, FunctionSignature}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct InlayHintsOptions { pub type_hints: bool, pub parameter_hints: bool, pub chaining_hints: bool, pub max_length: Option, } impl Default for InlayHintsOptions { fn default() -> Self { Self { type_hints: true, parameter_hints: true, chaining_hints: true, max_length: None } } } #[derive(Clone, Debug, PartialEq, Eq)] pub enum InlayKind { TypeHint, ParameterHint, ChainingHint, } #[derive(Debug)] pub struct InlayHint { pub range: TextRange, pub kind: InlayKind, pub label: SmolStr, } pub(crate) fn inlay_hints( db: &RootDatabase, file_id: FileId, options: &InlayHintsOptions, ) -> Vec { let _p = profile("inlay_hints"); let sema = Semantics::new(db); let file = sema.parse(file_id); let mut res = Vec::new(); for node in file.syntax().descendants() { if let Some(expr) = ast::Expr::cast(node.clone()) { get_chaining_hints(&mut res, &sema, options, expr); } match_ast! { match node { ast::CallExpr(it) => { get_param_name_hints(&mut res, &sema, options, ast::Expr::from(it)); }, ast::MethodCallExpr(it) => { get_param_name_hints(&mut res, &sema, options, ast::Expr::from(it)); }, ast::BindPat(it) => { get_bind_pat_hints(&mut res, &sema, options, it); }, _ => (), } } } res } fn get_chaining_hints( acc: &mut Vec, sema: &Semantics, options: &InlayHintsOptions, expr: ast::Expr, ) -> Option<()> { if !options.chaining_hints { return None; } let ty = sema.type_of_expr(&expr)?; let label = ty.display_truncated(sema.db, options.max_length).to_string(); if ty.is_unknown() { return None; } let mut tokens = expr.syntax() .siblings_with_tokens(Direction::Next) .filter_map(NodeOrToken::into_token) .filter(|t| match t.kind() { SyntaxKind::WHITESPACE if !t.text().contains('\n') => false, SyntaxKind::COMMENT => false, _ => true, }); // Chaining can be defined as an expression whose next sibling tokens are newline and dot // Ignoring extra whitespace and comments let next = tokens.next()?.kind(); let next_next = tokens.next()?.kind(); if next == SyntaxKind::WHITESPACE && next_next == SyntaxKind::DOT { acc.push(InlayHint { range: expr.syntax().text_range(), kind: InlayKind::ChainingHint, label: label.into(), }); } Some(()) } fn get_param_name_hints( acc: &mut Vec, sema: &Semantics, options: &InlayHintsOptions, expr: ast::Expr, ) -> Option<()> { if !options.parameter_hints { return None; } let args = match &expr { ast::Expr::CallExpr(expr) => expr.arg_list()?.args(), ast::Expr::MethodCallExpr(expr) => expr.arg_list()?.args(), _ => return None, }; let args_count = args.clone().count(); let fn_signature = get_fn_signature(sema, &expr)?; let n_params_to_skip = if fn_signature.has_self_param && fn_signature.parameter_names.len() > args_count { 1 } else { 0 }; let hints = fn_signature .parameter_names .iter() .skip(n_params_to_skip) .zip(args) .filter(|(param, arg)| should_show_param_hint(&fn_signature, param, &arg)) .map(|(param_name, arg)| InlayHint { range: arg.syntax().text_range(), kind: InlayKind::ParameterHint, label: param_name.into(), }); acc.extend(hints); Some(()) } fn get_bind_pat_hints( acc: &mut Vec, sema: &Semantics, options: &InlayHintsOptions, pat: ast::BindPat, ) -> Option<()> { if !options.type_hints { return None; } let ty = sema.type_of_pat(&pat.clone().into())?; if should_not_display_type_hint(sema.db, &pat, &ty) { return None; } acc.push(InlayHint { range: pat.syntax().text_range(), kind: InlayKind::TypeHint, label: ty.display_truncated(sema.db, options.max_length).to_string().into(), }); Some(()) } fn pat_is_enum_variant(db: &RootDatabase, bind_pat: &ast::BindPat, pat_ty: &Type) -> bool { if let Some(Adt::Enum(enum_data)) = pat_ty.as_adt() { let pat_text = bind_pat.syntax().to_string(); enum_data .variants(db) .into_iter() .map(|variant| variant.name(db).to_string()) .any(|enum_name| enum_name == pat_text) } else { false } } fn should_not_display_type_hint(db: &RootDatabase, bind_pat: &ast::BindPat, pat_ty: &Type) -> bool { if pat_ty.is_unknown() { return true; } if let Some(Adt::Struct(s)) = pat_ty.as_adt() { if s.fields(db).is_empty() && s.name(db).to_string() == bind_pat.syntax().to_string() { return true; } } for node in bind_pat.syntax().ancestors() { match_ast! { match node { ast::LetStmt(it) => { return it.ascribed_type().is_some() }, ast::Param(it) => { return it.ascribed_type().is_some() }, ast::MatchArm(_it) => { return pat_is_enum_variant(db, bind_pat, pat_ty); }, ast::IfExpr(it) => { return it.condition().and_then(|condition| condition.pat()).is_some() && pat_is_enum_variant(db, bind_pat, pat_ty); }, ast::WhileExpr(it) => { return it.condition().and_then(|condition| condition.pat()).is_some() && pat_is_enum_variant(db, bind_pat, pat_ty); }, _ => (), } } } false } fn should_show_param_hint( fn_signature: &FunctionSignature, param_name: &str, argument: &ast::Expr, ) -> bool { let argument_string = argument.syntax().to_string(); if param_name.is_empty() || argument_string.ends_with(param_name) { return false; } let parameters_len = if fn_signature.has_self_param { fn_signature.parameters.len() - 1 } else { fn_signature.parameters.len() }; // avoid displaying hints for common functions like map, filter, etc. if parameters_len == 1 && (param_name.len() == 1 || param_name == "predicate") { return false; } true } fn get_fn_signature(sema: &Semantics, expr: &ast::Expr) -> Option { match expr { ast::Expr::CallExpr(expr) => { // FIXME: Type::as_callable is broken for closures let callable_def = sema.type_of_expr(&expr.expr()?)?.as_callable()?; match callable_def { hir::CallableDef::FunctionId(it) => { Some(FunctionSignature::from_hir(sema.db, it.into())) } hir::CallableDef::StructId(it) => { FunctionSignature::from_struct(sema.db, it.into()) } hir::CallableDef::EnumVariantId(it) => { FunctionSignature::from_enum_variant(sema.db, it.into()) } } } ast::Expr::MethodCallExpr(expr) => { let fn_def = sema.resolve_method_call(&expr)?; Some(FunctionSignature::from_hir(sema.db, fn_def)) } _ => None, } } #[cfg(test)] mod tests { use crate::inlay_hints::InlayHintsOptions; use insta::assert_debug_snapshot; use crate::mock_analysis::single_file; #[test] fn param_hints_only() { let (analysis, file_id) = single_file( r#" fn foo(a: i32, b: i32) -> i32 { a + b } fn main() { let _x = foo(4, 4); }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions{ parameter_hints: true, type_hints: false, chaining_hints: false, max_length: None}).unwrap(), @r###" [ InlayHint { range: [106; 107), kind: ParameterHint, label: "a", }, InlayHint { range: [109; 110), kind: ParameterHint, label: "b", }, ]"###); } #[test] fn hints_disabled() { let (analysis, file_id) = single_file( r#" fn foo(a: i32, b: i32) -> i32 { a + b } fn main() { let _x = foo(4, 4); }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions{ type_hints: false, parameter_hints: false, chaining_hints: false, max_length: None}).unwrap(), @r###"[]"###); } #[test] fn type_hints_only() { let (analysis, file_id) = single_file( r#" fn foo(a: i32, b: i32) -> i32 { a + b } fn main() { let _x = foo(4, 4); }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions{ type_hints: true, parameter_hints: false, chaining_hints: false, max_length: None}).unwrap(), @r###" [ InlayHint { range: [97; 99), kind: TypeHint, label: "i32", }, ]"###); } #[test] fn default_generic_types_should_not_be_displayed() { let (analysis, file_id) = single_file( r#" struct Test { k: K, t: T, } fn main() { let zz = Test { t: 23, k: 33 }; let zz_ref = &zz; }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions::default()).unwrap(), @r###" [ InlayHint { range: [69; 71), kind: TypeHint, label: "Test", }, InlayHint { range: [105; 111), kind: TypeHint, label: "&Test", }, ] "### ); } #[test] fn let_statement() { let (analysis, file_id) = single_file( r#" #[derive(PartialEq)] enum CustomOption { None, Some(T), } #[derive(PartialEq)] struct Test { a: CustomOption, b: u8, } fn main() { struct InnerStruct {} let test = 54; let test: i32 = 33; let mut test = 33; let _ = 22; let test = "test"; let test = InnerStruct {}; let test = vec![222]; let test: Vec<_> = (0..3).collect(); let test = (0..3).collect::>(); let test = (0..3).collect::>(); let mut test = Vec::new(); test.push(333); let test = (42, 'a'); let (a, (b, c, (d, e), f)) = (2, (3, 4, (6.6, 7.7), 5)); let &x = &92; }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions::default()).unwrap(), @r###" [ InlayHint { range: [193; 197), kind: TypeHint, label: "i32", }, InlayHint { range: [236; 244), kind: TypeHint, label: "i32", }, InlayHint { range: [275; 279), kind: TypeHint, label: "&str", }, InlayHint { range: [539; 543), kind: TypeHint, label: "(i32, char)", }, InlayHint { range: [566; 567), kind: TypeHint, label: "i32", }, InlayHint { range: [570; 571), kind: TypeHint, label: "i32", }, InlayHint { range: [573; 574), kind: TypeHint, label: "i32", }, InlayHint { range: [577; 578), kind: TypeHint, label: "f64", }, InlayHint { range: [580; 581), kind: TypeHint, label: "f64", }, InlayHint { range: [584; 585), kind: TypeHint, label: "i32", }, InlayHint { range: [627; 628), kind: TypeHint, label: "i32", }, ] "### ); } #[test] fn closure_parameters() { let (analysis, file_id) = single_file( r#" fn main() { let mut start = 0; (0..2).for_each(|increment| { start += increment; }); let multiply = |a, b, c, d| a * b * c * d; let _: i32 = multiply(1, 2, 3, 4); let multiply_ref = &multiply; let return_42 = || 42; }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions::default()).unwrap(), @r###" [ InlayHint { range: [21; 30), kind: TypeHint, label: "i32", }, InlayHint { range: [57; 66), kind: TypeHint, label: "i32", }, InlayHint { range: [115; 123), kind: TypeHint, label: "|…| -> i32", }, InlayHint { range: [127; 128), kind: TypeHint, label: "i32", }, InlayHint { range: [130; 131), kind: TypeHint, label: "i32", }, InlayHint { range: [133; 134), kind: TypeHint, label: "i32", }, InlayHint { range: [136; 137), kind: TypeHint, label: "i32", }, InlayHint { range: [201; 213), kind: TypeHint, label: "&|…| -> i32", }, InlayHint { range: [236; 245), kind: TypeHint, label: "|| -> i32", }, ] "### ); } #[test] fn for_expression() { let (analysis, file_id) = single_file( r#" fn main() { let mut start = 0; for increment in 0..2 { start += increment; } }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions::default()).unwrap(), @r###" [ InlayHint { range: [21; 30), kind: TypeHint, label: "i32", }, InlayHint { range: [44; 53), kind: TypeHint, label: "i32", }, ] "### ); } #[test] fn if_expr() { let (analysis, file_id) = single_file( r#" #[derive(PartialEq)] enum CustomOption { None, Some(T), } #[derive(PartialEq)] struct Test { a: CustomOption, b: u8, } use CustomOption::*; fn main() { let test = Some(Test { a: Some(3), b: 1 }); if let None = &test {}; if let test = &test {}; if let Some(test) = &test {}; if let Some(Test { a, b }) = &test {}; if let Some(Test { a: x, b: y }) = &test {}; if let Some(Test { a: Some(x), b: y }) = &test {}; if let Some(Test { a: None, b: y }) = &test {}; if let Some(Test { b: y, .. }) = &test {}; if test == None {} }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions::default()).unwrap(), @r###" [ InlayHint { range: [188; 192), kind: TypeHint, label: "CustomOption", }, InlayHint { range: [267; 271), kind: TypeHint, label: "&CustomOption", }, InlayHint { range: [300; 304), kind: TypeHint, label: "&Test", }, InlayHint { range: [341; 342), kind: TypeHint, label: "&CustomOption", }, InlayHint { range: [344; 345), kind: TypeHint, label: "&u8", }, InlayHint { range: [387; 388), kind: TypeHint, label: "&CustomOption", }, InlayHint { range: [393; 394), kind: TypeHint, label: "&u8", }, InlayHint { range: [441; 442), kind: TypeHint, label: "&u32", }, InlayHint { range: [448; 449), kind: TypeHint, label: "&u8", }, InlayHint { range: [500; 501), kind: TypeHint, label: "&u8", }, InlayHint { range: [543; 544), kind: TypeHint, label: "&u8", }, ] "### ); } #[test] fn while_expr() { let (analysis, file_id) = single_file( r#" #[derive(PartialEq)] enum CustomOption { None, Some(T), } #[derive(PartialEq)] struct Test { a: CustomOption, b: u8, } use CustomOption::*; fn main() { let test = Some(Test { a: Some(3), b: 1 }); while let None = &test {}; while let test = &test {}; while let Some(test) = &test {}; while let Some(Test { a, b }) = &test {}; while let Some(Test { a: x, b: y }) = &test {}; while let Some(Test { a: Some(x), b: y }) = &test {}; while let Some(Test { a: None, b: y }) = &test {}; while let Some(Test { b: y, .. }) = &test {}; while test == None {} }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions::default()).unwrap(), @r###" [ InlayHint { range: [188; 192), kind: TypeHint, label: "CustomOption", }, InlayHint { range: [273; 277), kind: TypeHint, label: "&CustomOption", }, InlayHint { range: [309; 313), kind: TypeHint, label: "&Test", }, InlayHint { range: [353; 354), kind: TypeHint, label: "&CustomOption", }, InlayHint { range: [356; 357), kind: TypeHint, label: "&u8", }, InlayHint { range: [402; 403), kind: TypeHint, label: "&CustomOption", }, InlayHint { range: [408; 409), kind: TypeHint, label: "&u8", }, InlayHint { range: [459; 460), kind: TypeHint, label: "&u32", }, InlayHint { range: [466; 467), kind: TypeHint, label: "&u8", }, InlayHint { range: [521; 522), kind: TypeHint, label: "&u8", }, InlayHint { range: [567; 568), kind: TypeHint, label: "&u8", }, ] "### ); } #[test] fn match_arm_list() { let (analysis, file_id) = single_file( r#" #[derive(PartialEq)] enum CustomOption { None, Some(T), } #[derive(PartialEq)] struct Test { a: CustomOption, b: u8, } use CustomOption::*; fn main() { match Some(Test { a: Some(3), b: 1 }) { None => (), test => (), Some(test) => (), Some(Test { a, b }) => (), Some(Test { a: x, b: y }) => (), Some(Test { a: Some(x), b: y }) => (), Some(Test { a: None, b: y }) => (), Some(Test { b: y, .. }) => (), _ => {} } }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions::default()).unwrap(), @r###" [ InlayHint { range: [252; 256), kind: TypeHint, label: "CustomOption", }, InlayHint { range: [277; 281), kind: TypeHint, label: "Test", }, InlayHint { range: [310; 311), kind: TypeHint, label: "CustomOption", }, InlayHint { range: [313; 314), kind: TypeHint, label: "u8", }, InlayHint { range: [348; 349), kind: TypeHint, label: "CustomOption", }, InlayHint { range: [354; 355), kind: TypeHint, label: "u8", }, InlayHint { range: [394; 395), kind: TypeHint, label: "u32", }, InlayHint { range: [401; 402), kind: TypeHint, label: "u8", }, InlayHint { range: [445; 446), kind: TypeHint, label: "u8", }, InlayHint { range: [480; 481), kind: TypeHint, label: "u8", }, ] "### ); } #[test] fn hint_truncation() { let (analysis, file_id) = single_file( r#" struct Smol(T); struct VeryLongOuterName(T); fn main() { let a = Smol(0u32); let b = VeryLongOuterName(0usize); let c = Smol(Smol(0u32)) }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions { max_length: Some(8), ..Default::default() }).unwrap(), @r###" [ InlayHint { range: [74; 75), kind: TypeHint, label: "Smol", }, InlayHint { range: [98; 99), kind: TypeHint, label: "VeryLongOuterName<…>", }, InlayHint { range: [137; 138), kind: TypeHint, label: "Smol>", }, ] "### ); } #[test] fn function_call_parameter_hint() { let (analysis, file_id) = single_file( r#" enum CustomOption { None, Some(T), } use CustomOption::*; struct FileId {} struct SmolStr {} impl From<&str> for SmolStr { fn from(_: &str) -> Self { unimplemented!() } } struct TextRange {} struct SyntaxKind {} struct NavigationTarget {} struct Test {} impl Test { fn method(&self, mut param: i32) -> i32 { param * 2 } fn from_syntax( file_id: FileId, name: SmolStr, focus_range: CustomOption, full_range: TextRange, kind: SyntaxKind, docs: CustomOption, description: CustomOption, ) -> NavigationTarget { NavigationTarget {} } } fn test_func(mut foo: i32, bar: i32, msg: &str, _: i32, last: i32) -> i32 { foo + bar } fn main() { let not_literal = 1; let _: i32 = test_func(1, 2, "hello", 3, not_literal); let t: Test = Test {}; t.method(123); Test::method(&t, 3456); Test::from_syntax( FileId {}, "impl".into(), None, TextRange {}, SyntaxKind {}, None, None, ); }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions::default()).unwrap(), @r###" [ InlayHint { range: [798; 809), kind: TypeHint, label: "i32", }, InlayHint { range: [842; 843), kind: ParameterHint, label: "foo", }, InlayHint { range: [845; 846), kind: ParameterHint, label: "bar", }, InlayHint { range: [848; 855), kind: ParameterHint, label: "msg", }, InlayHint { range: [860; 871), kind: ParameterHint, label: "last", }, InlayHint { range: [914; 917), kind: ParameterHint, label: "param", }, InlayHint { range: [937; 939), kind: ParameterHint, label: "&self", }, InlayHint { range: [941; 945), kind: ParameterHint, label: "param", }, InlayHint { range: [980; 989), kind: ParameterHint, label: "file_id", }, InlayHint { range: [999; 1012), kind: ParameterHint, label: "name", }, InlayHint { range: [1022; 1026), kind: ParameterHint, label: "focus_range", }, InlayHint { range: [1036; 1048), kind: ParameterHint, label: "full_range", }, InlayHint { range: [1058; 1071), kind: ParameterHint, label: "kind", }, InlayHint { range: [1081; 1085), kind: ParameterHint, label: "docs", }, InlayHint { range: [1095; 1099), kind: ParameterHint, label: "description", }, ] "### ); } #[test] fn omitted_parameters_hints_heuristics() { let (analysis, file_id) = single_file( r#" fn map(f: i32) {} fn filter(predicate: i32) {} struct TestVarContainer { test_var: i32, } struct Test {} impl Test { fn map(self, f: i32) -> Self { self } fn filter(self, predicate: i32) -> Self { self } fn no_hints_expected(&self, _: i32, test_var: i32) {} } fn main() { let container: TestVarContainer = TestVarContainer { test_var: 42 }; let test: Test = Test {}; map(22); filter(33); let test_processed: Test = test.map(1).filter(2); let test_var: i32 = 55; test_processed.no_hints_expected(22, test_var); test_processed.no_hints_expected(33, container.test_var); }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions { max_length: Some(8), ..Default::default() }).unwrap(), @r###" [] "### ); } #[test] fn unit_structs_have_no_type_hints() { let (analysis, file_id) = single_file( r#" enum CustomResult { Ok(T), Err(E), } use CustomResult::*; struct SyntheticSyntax; fn main() { match Ok(()) { Ok(_) => (), Err(SyntheticSyntax) => (), } }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions { max_length: Some(8), ..Default::default() }).unwrap(), @r###" [] "### ); } #[test] fn chaining_hints_ignore_comments() { let (analysis, file_id) = single_file( r#" struct A(B); impl A { fn into_b(self) -> B { self.0 } } struct B(C) impl B { fn into_c(self) -> C { self.0 } } struct C; fn main() { let c = A(B(C)) .into_b() // This is a comment .into_c(); }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions{ parameter_hints: false, type_hints: false, chaining_hints: true, max_length: None}).unwrap(), @r###" [ InlayHint { range: [231; 268), kind: ChainingHint, label: "B", }, InlayHint { range: [231; 238), kind: ChainingHint, label: "A", }, ]"###); } #[test] fn chaining_hints_without_newlines() { let (analysis, file_id) = single_file( r#" struct A(B); impl A { fn into_b(self) -> B { self.0 } } struct B(C) impl B { fn into_c(self) -> C { self.0 } } struct C; fn main() { let c = A(B(C)).into_b().into_c(); }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions{ parameter_hints: false, type_hints: false, chaining_hints: true, max_length: None}).unwrap(), @r###"[]"###); } #[test] fn struct_access_chaining_hints() { let (analysis, file_id) = single_file( r#" struct A { pub b: B } struct B { pub c: C } struct C(pub bool); fn main() { let x = A { b: B { c: C(true) } } .b .c .0; }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions{ parameter_hints: false, type_hints: false, chaining_hints: true, max_length: None}).unwrap(), @r###" [ InlayHint { range: [150; 221), kind: ChainingHint, label: "C", }, InlayHint { range: [150; 198), kind: ChainingHint, label: "B", }, InlayHint { range: [150; 175), kind: ChainingHint, label: "A", }, ]"###); } #[test] fn generic_chaining_hints() { let (analysis, file_id) = single_file( r#" struct A(T); struct B(T); struct C(T); struct X(T, R); impl A { fn new(t: T) -> Self { A(t) } fn into_b(self) -> B { B(self.0) } } impl B { fn into_c(self) -> C { C(self.0) } } fn main() { let c = A::new(X(42, true)) .into_b() .into_c(); }"#, ); assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions{ parameter_hints: false, type_hints: false, chaining_hints: true, max_length: None}).unwrap(), @r###" [ InlayHint { range: [416; 465), kind: ChainingHint, label: "B>", }, InlayHint { range: [416; 435), kind: ChainingHint, label: "A>", }, ]"###); } }