From 191cfba9d26137233d8fef91dcf5d32ef1a1841f Mon Sep 17 00:00:00 2001 From: Maybe Waffle Date: Fri, 16 Dec 2022 16:13:46 +0000 Subject: [PATCH] Split inlay hints into modules --- crates/ide/src/inlay_hints.rs | 1042 +---------------- crates/ide/src/inlay_hints/adjustment.rs | 102 ++ crates/ide/src/inlay_hints/bind_pat.rs | 187 +++ crates/ide/src/inlay_hints/binding_mode.rs | 78 ++ crates/ide/src/inlay_hints/chaining.rs | 72 ++ crates/ide/src/inlay_hints/closing_brace.rs | 116 ++ crates/ide/src/inlay_hints/closure_ret.rs | 50 + crates/ide/src/inlay_hints/fn_lifetime_fn.rs | 190 +++ crates/ide/src/inlay_hints/implicit_static.rs | 39 + crates/ide/src/inlay_hints/param_name.rs | 237 ++++ 10 files changed, 1096 insertions(+), 1017 deletions(-) create mode 100644 crates/ide/src/inlay_hints/adjustment.rs create mode 100644 crates/ide/src/inlay_hints/bind_pat.rs create mode 100644 crates/ide/src/inlay_hints/binding_mode.rs create mode 100644 crates/ide/src/inlay_hints/chaining.rs create mode 100644 crates/ide/src/inlay_hints/closing_brace.rs create mode 100644 crates/ide/src/inlay_hints/closure_ret.rs create mode 100644 crates/ide/src/inlay_hints/fn_lifetime_fn.rs create mode 100644 crates/ide/src/inlay_hints/implicit_static.rs create mode 100644 crates/ide/src/inlay_hints/param_name.rs diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs index 66a40627dc4..6387ec55a7f 100644 --- a/crates/ide/src/inlay_hints.rs +++ b/crates/ide/src/inlay_hints.rs @@ -1,24 +1,26 @@ use std::fmt; use either::Either; -use hir::{ - known, Adjust, AutoBorrow, Callable, HasVisibility, HirDisplay, Mutability, OverloadedDeref, - PointerCast, Safety, Semantics, TypeInfo, -}; -use ide_db::{ - base_db::FileRange, famous_defs::FamousDefs, syntax_helpers::node_ext::walk_ty, FxHashMap, - RootDatabase, -}; +use hir::{known, HasVisibility, HirDisplay, Semantics}; +use ide_db::{base_db::FileRange, famous_defs::FamousDefs, RootDatabase}; use itertools::Itertools; -use stdx::to_lower_snake_case; use syntax::{ - ast::{self, AstNode, HasArgList, HasGenericParams, HasName, UnaryOp}, - match_ast, Direction, NodeOrToken, SmolStr, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, - TextSize, T, + ast::{self, AstNode}, + match_ast, NodeOrToken, SyntaxNode, TextRange, TextSize, }; use crate::FileId; +mod closing_brace; +mod implicit_static; +mod fn_lifetime_fn; +mod closure_ret; +mod adjustment; +mod chaining; +mod param_name; +mod binding_mode; +mod bind_pat; + #[derive(Clone, Debug, PartialEq, Eq)] pub struct InlayHintsConfig { pub render_colons: bool, @@ -227,18 +229,18 @@ fn hints( file_id: FileId, node: SyntaxNode, ) { - closing_brace_hints(hints, sema, config, file_id, node.clone()); + closing_brace::hints(hints, sema, config, file_id, node.clone()); match_ast! { match node { ast::Expr(expr) => { - chaining_hints(hints, sema, &famous_defs, config, file_id, &expr); - adjustment_hints(hints, sema, config, &expr); + chaining::hints(hints, sema, &famous_defs, config, file_id, &expr); + adjustment::hints(hints, sema, config, &expr); match expr { - ast::Expr::CallExpr(it) => param_name_hints(hints, sema, config, ast::Expr::from(it)), + ast::Expr::CallExpr(it) => param_name::hints(hints, sema, config, ast::Expr::from(it)), ast::Expr::MethodCallExpr(it) => { - param_name_hints(hints, sema, config, ast::Expr::from(it)) + param_name::hints(hints, sema, config, ast::Expr::from(it)) } - ast::Expr::ClosureExpr(it) => closure_ret_hints(hints, sema, &famous_defs, config, file_id, it), + ast::Expr::ClosureExpr(it) => closure_ret::hints(hints, sema, &famous_defs, config, file_id, it), // We could show reborrows for all expressions, but usually that is just noise to the user // and the main point here is to show why "moving" a mutable reference doesn't necessarily move it // ast::Expr::PathExpr(_) => reborrow_hints(hints, sema, config, &expr), @@ -246,19 +248,19 @@ fn hints( } }, ast::Pat(it) => { - binding_mode_hints(hints, sema, config, &it); + binding_mode::hints(hints, sema, config, &it); if let ast::Pat::IdentPat(it) = it { - bind_pat_hints(hints, sema, config, file_id, &it); + bind_pat::hints(hints, sema, config, file_id, &it); } Some(()) }, ast::Item(it) => match it { // FIXME: record impl lifetimes so they aren't being reused in assoc item lifetime inlay hints ast::Item::Impl(_) => None, - ast::Item::Fn(it) => fn_lifetime_fn_hints(hints, config, it), + ast::Item::Fn(it) => fn_lifetime_fn::hints(hints, config, it), // static type elisions - ast::Item::Static(it) => implicit_static_hints(hints, config, Either::Left(it)), - ast::Item::Const(it) => implicit_static_hints(hints, config, Either::Right(it)), + ast::Item::Static(it) => implicit_static::hints(hints, config, Either::Left(it)), + ast::Item::Const(it) => implicit_static::hints(hints, config, Either::Right(it)), _ => None, }, // FIXME: fn-ptr type, dyn fn type, and trait object type elisions @@ -268,755 +270,6 @@ fn hints( }; } -fn closing_brace_hints( - acc: &mut Vec, - sema: &Semantics<'_, RootDatabase>, - config: &InlayHintsConfig, - file_id: FileId, - node: SyntaxNode, -) -> Option<()> { - let min_lines = config.closing_brace_hints_min_lines?; - - let name = |it: ast::Name| it.syntax().text_range(); - - let mut closing_token; - let (label, name_range) = if let Some(item_list) = ast::AssocItemList::cast(node.clone()) { - closing_token = item_list.r_curly_token()?; - - let parent = item_list.syntax().parent()?; - match_ast! { - match parent { - ast::Impl(imp) => { - let imp = sema.to_def(&imp)?; - let ty = imp.self_ty(sema.db); - let trait_ = imp.trait_(sema.db); - let hint_text = match trait_ { - Some(tr) => format!("impl {} for {}", tr.name(sema.db), ty.display_truncated(sema.db, config.max_length)), - None => format!("impl {}", ty.display_truncated(sema.db, config.max_length)), - }; - (hint_text, None) - }, - ast::Trait(tr) => { - (format!("trait {}", tr.name()?), tr.name().map(name)) - }, - _ => return None, - } - } - } else if let Some(list) = ast::ItemList::cast(node.clone()) { - closing_token = list.r_curly_token()?; - - let module = ast::Module::cast(list.syntax().parent()?)?; - (format!("mod {}", module.name()?), module.name().map(name)) - } else if let Some(block) = ast::BlockExpr::cast(node.clone()) { - closing_token = block.stmt_list()?.r_curly_token()?; - - let parent = block.syntax().parent()?; - match_ast! { - match parent { - ast::Fn(it) => { - // FIXME: this could include parameters, but `HirDisplay` prints too much info - // and doesn't respect the max length either, so the hints end up way too long - (format!("fn {}", it.name()?), it.name().map(name)) - }, - ast::Static(it) => (format!("static {}", it.name()?), it.name().map(name)), - ast::Const(it) => { - if it.underscore_token().is_some() { - ("const _".into(), None) - } else { - (format!("const {}", it.name()?), it.name().map(name)) - } - }, - _ => return None, - } - } - } else if let Some(mac) = ast::MacroCall::cast(node.clone()) { - let last_token = mac.syntax().last_token()?; - if last_token.kind() != T![;] && last_token.kind() != SyntaxKind::R_CURLY { - return None; - } - closing_token = last_token; - - ( - format!("{}!", mac.path()?), - mac.path().and_then(|it| it.segment()).map(|it| it.syntax().text_range()), - ) - } else { - return None; - }; - - if let Some(mut next) = closing_token.next_token() { - if next.kind() == T![;] { - if let Some(tok) = next.next_token() { - closing_token = next; - next = tok; - } - } - if !(next.kind() == SyntaxKind::WHITESPACE && next.text().contains('\n')) { - // Only display the hint if the `}` is the last token on the line - return None; - } - } - - let mut lines = 1; - node.text().for_each_chunk(|s| lines += s.matches('\n').count()); - if lines < min_lines { - return None; - } - - let linked_location = name_range.map(|range| FileRange { file_id, range }); - acc.push(InlayHint { - range: closing_token.text_range(), - kind: InlayKind::ClosingBraceHint, - label: InlayHintLabel { parts: vec![InlayHintLabelPart { text: label, linked_location }] }, - tooltip: None, // provided by label part location - }); - - None -} - -fn implicit_static_hints( - acc: &mut Vec, - config: &InlayHintsConfig, - statik_or_const: Either, -) -> Option<()> { - if config.lifetime_elision_hints != LifetimeElisionHints::Always { - return None; - } - - if let Either::Right(it) = &statik_or_const { - if ast::AssocItemList::can_cast( - it.syntax().parent().map_or(SyntaxKind::EOF, |it| it.kind()), - ) { - return None; - } - } - - if let Some(ast::Type::RefType(ty)) = statik_or_const.either(|it| it.ty(), |it| it.ty()) { - if ty.lifetime().is_none() { - let t = ty.amp_token()?; - acc.push(InlayHint { - range: t.text_range(), - kind: InlayKind::LifetimeHint, - label: "'static".to_owned().into(), - tooltip: Some(InlayTooltip::String("Elided static lifetime".into())), - }); - } - } - - Some(()) -} - -fn fn_lifetime_fn_hints( - acc: &mut Vec, - config: &InlayHintsConfig, - func: ast::Fn, -) -> Option<()> { - if config.lifetime_elision_hints == LifetimeElisionHints::Never { - return None; - } - - let mk_lt_hint = |t: SyntaxToken, label: String| InlayHint { - range: t.text_range(), - kind: InlayKind::LifetimeHint, - label: label.into(), - tooltip: Some(InlayTooltip::String("Elided lifetime".into())), - }; - - let param_list = func.param_list()?; - let generic_param_list = func.generic_param_list(); - let ret_type = func.ret_type(); - let self_param = param_list.self_param().filter(|it| it.amp_token().is_some()); - - let is_elided = |lt: &Option| match lt { - Some(lt) => matches!(lt.text().as_str(), "'_"), - None => true, - }; - - let potential_lt_refs = { - let mut acc: Vec<_> = vec![]; - if let Some(self_param) = &self_param { - let lifetime = self_param.lifetime(); - let is_elided = is_elided(&lifetime); - acc.push((None, self_param.amp_token(), lifetime, is_elided)); - } - param_list.params().filter_map(|it| Some((it.pat(), it.ty()?))).for_each(|(pat, ty)| { - // FIXME: check path types - walk_ty(&ty, &mut |ty| match ty { - ast::Type::RefType(r) => { - let lifetime = r.lifetime(); - let is_elided = is_elided(&lifetime); - acc.push(( - pat.as_ref().and_then(|it| match it { - ast::Pat::IdentPat(p) => p.name(), - _ => None, - }), - r.amp_token(), - lifetime, - is_elided, - )) - } - _ => (), - }) - }); - acc - }; - - // allocate names - let mut gen_idx_name = { - let mut gen = (0u8..).map(|idx| match idx { - idx if idx < 10 => SmolStr::from_iter(['\'', (idx + 48) as char]), - idx => format!("'{idx}").into(), - }); - move || gen.next().unwrap_or_default() - }; - let mut allocated_lifetimes = vec![]; - - let mut used_names: FxHashMap = - match config.param_names_for_lifetime_elision_hints { - true => generic_param_list - .iter() - .flat_map(|gpl| gpl.lifetime_params()) - .filter_map(|param| param.lifetime()) - .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0))) - .collect(), - false => Default::default(), - }; - { - let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided); - if let Some(_) = &self_param { - if let Some(_) = potential_lt_refs.next() { - allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints { - // self can't be used as a lifetime, so no need to check for collisions - "'self".into() - } else { - gen_idx_name() - }); - } - } - potential_lt_refs.for_each(|(name, ..)| { - let name = match name { - Some(it) if config.param_names_for_lifetime_elision_hints => { - if let Some(c) = used_names.get_mut(it.text().as_str()) { - *c += 1; - SmolStr::from(format!("'{text}{c}", text = it.text().as_str())) - } else { - used_names.insert(it.text().as_str().into(), 0); - SmolStr::from_iter(["\'", it.text().as_str()]) - } - } - _ => gen_idx_name(), - }; - allocated_lifetimes.push(name); - }); - } - - // fetch output lifetime if elision rule applies - let output = match potential_lt_refs.as_slice() { - [(_, _, lifetime, _), ..] if self_param.is_some() || potential_lt_refs.len() == 1 => { - match lifetime { - Some(lt) => match lt.text().as_str() { - "'_" => allocated_lifetimes.get(0).cloned(), - "'static" => None, - name => Some(name.into()), - }, - None => allocated_lifetimes.get(0).cloned(), - } - } - [..] => None, - }; - - if allocated_lifetimes.is_empty() && output.is_none() { - return None; - } - - // apply hints - // apply output if required - let mut is_trivial = true; - if let (Some(output_lt), Some(r)) = (&output, ret_type) { - if let Some(ty) = r.ty() { - walk_ty(&ty, &mut |ty| match ty { - ast::Type::RefType(ty) if ty.lifetime().is_none() => { - if let Some(amp) = ty.amp_token() { - is_trivial = false; - acc.push(mk_lt_hint(amp, output_lt.to_string())); - } - } - _ => (), - }) - } - } - - if config.lifetime_elision_hints == LifetimeElisionHints::SkipTrivial && is_trivial { - return None; - } - - let mut a = allocated_lifetimes.iter(); - for (_, amp_token, _, is_elided) in potential_lt_refs { - if is_elided { - let t = amp_token?; - let lt = a.next()?; - acc.push(mk_lt_hint(t, lt.to_string())); - } - } - - // generate generic param list things - match (generic_param_list, allocated_lifetimes.as_slice()) { - (_, []) => (), - (Some(gpl), allocated_lifetimes) => { - let angle_tok = gpl.l_angle_token()?; - let is_empty = gpl.generic_params().next().is_none(); - acc.push(InlayHint { - range: angle_tok.text_range(), - kind: InlayKind::LifetimeHint, - label: format!( - "{}{}", - allocated_lifetimes.iter().format(", "), - if is_empty { "" } else { ", " } - ) - .into(), - tooltip: Some(InlayTooltip::String("Elided lifetimes".into())), - }); - } - (None, allocated_lifetimes) => acc.push(InlayHint { - range: func.name()?.syntax().text_range(), - kind: InlayKind::GenericParamListHint, - label: format!("<{}>", allocated_lifetimes.iter().format(", "),).into(), - tooltip: Some(InlayTooltip::String("Elided lifetimes".into())), - }), - } - Some(()) -} - -fn closure_ret_hints( - acc: &mut Vec, - sema: &Semantics<'_, RootDatabase>, - famous_defs: &FamousDefs<'_, '_>, - config: &InlayHintsConfig, - file_id: FileId, - closure: ast::ClosureExpr, -) -> Option<()> { - if config.closure_return_type_hints == ClosureReturnTypeHints::Never { - return None; - } - - if closure.ret_type().is_some() { - return None; - } - - if !closure_has_block_body(&closure) - && config.closure_return_type_hints == ClosureReturnTypeHints::WithBlock - { - return None; - } - - let param_list = closure.param_list()?; - - let closure = sema.descend_node_into_attributes(closure.clone()).pop()?; - let ty = sema.type_of_expr(&ast::Expr::ClosureExpr(closure))?.adjusted(); - let callable = ty.as_callable(sema.db)?; - let ty = callable.return_type(); - if ty.is_unit() { - return None; - } - acc.push(InlayHint { - range: param_list.syntax().text_range(), - kind: InlayKind::ClosureReturnTypeHint, - label: hint_iterator(sema, &famous_defs, config, &ty) - .unwrap_or_else(|| ty.display_truncated(sema.db, config.max_length).to_string()) - .into(), - tooltip: Some(InlayTooltip::HoverRanged(file_id, param_list.syntax().text_range())), - }); - Some(()) -} - -fn adjustment_hints( - acc: &mut Vec, - sema: &Semantics<'_, RootDatabase>, - config: &InlayHintsConfig, - expr: &ast::Expr, -) -> Option<()> { - if config.adjustment_hints == AdjustmentHints::Never { - return None; - } - - // These inherit from the inner expression which would result in duplicate hints - if let ast::Expr::ParenExpr(_) - | ast::Expr::IfExpr(_) - | ast::Expr::BlockExpr(_) - | ast::Expr::MatchExpr(_) = expr - { - return None; - } - - let parent = expr.syntax().parent().and_then(ast::Expr::cast); - let descended = sema.descend_node_into_attributes(expr.clone()).pop(); - let desc_expr = descended.as_ref().unwrap_or(expr); - let adjustments = sema.expr_adjustments(desc_expr).filter(|it| !it.is_empty())?; - let needs_parens = match parent { - Some(parent) => { - match parent { - ast::Expr::AwaitExpr(_) - | ast::Expr::CallExpr(_) - | ast::Expr::CastExpr(_) - | ast::Expr::FieldExpr(_) - | ast::Expr::MethodCallExpr(_) - | ast::Expr::TryExpr(_) => true, - // FIXME: shorthands need special casing, though not sure if adjustments are even valid there - ast::Expr::RecordExpr(_) => false, - ast::Expr::IndexExpr(index) => index.base().as_ref() == Some(expr), - _ => false, - } - } - None => false, - }; - if needs_parens { - acc.push(InlayHint { - range: expr.syntax().text_range(), - kind: InlayKind::OpeningParenthesis, - label: "(".into(), - tooltip: None, - }); - } - for adjustment in adjustments.into_iter().rev() { - // FIXME: Add some nicer tooltips to each of these - let text = match adjustment { - Adjust::NeverToAny if config.adjustment_hints == AdjustmentHints::Always => { - "" - } - Adjust::Deref(None) => "*", - Adjust::Deref(Some(OverloadedDeref(Mutability::Mut))) => "*", - Adjust::Deref(Some(OverloadedDeref(Mutability::Shared))) => "*", - Adjust::Borrow(AutoBorrow::Ref(Mutability::Shared)) => "&", - Adjust::Borrow(AutoBorrow::Ref(Mutability::Mut)) => "&mut ", - Adjust::Borrow(AutoBorrow::RawPtr(Mutability::Shared)) => "&raw const ", - Adjust::Borrow(AutoBorrow::RawPtr(Mutability::Mut)) => "&raw mut ", - // some of these could be represented via `as` casts, but that's not too nice and - // handling everything as a prefix expr makes the `(` and `)` insertion easier - Adjust::Pointer(cast) if config.adjustment_hints == AdjustmentHints::Always => { - match cast { - PointerCast::ReifyFnPointer => "", - PointerCast::UnsafeFnPointer => "", - PointerCast::ClosureFnPointer(Safety::Unsafe) => { - "" - } - PointerCast::ClosureFnPointer(Safety::Safe) => "", - PointerCast::MutToConstPointer => "", - PointerCast::ArrayToPointer => "", - PointerCast::Unsize => "", - } - } - _ => continue, - }; - acc.push(InlayHint { - range: expr.syntax().text_range(), - kind: InlayKind::AdjustmentHint, - label: text.into(), - tooltip: None, - }); - } - if needs_parens { - acc.push(InlayHint { - range: expr.syntax().text_range(), - kind: InlayKind::ClosingParenthesis, - label: ")".into(), - tooltip: None, - }); - } - Some(()) -} - -fn chaining_hints( - acc: &mut Vec, - sema: &Semantics<'_, RootDatabase>, - famous_defs: &FamousDefs<'_, '_>, - config: &InlayHintsConfig, - file_id: FileId, - expr: &ast::Expr, -) -> Option<()> { - if !config.chaining_hints { - return None; - } - - if matches!(expr, ast::Expr::RecordExpr(_)) { - return None; - } - - let descended = sema.descend_node_into_attributes(expr.clone()).pop(); - let desc_expr = descended.as_ref().unwrap_or(expr); - - 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(); - if next == SyntaxKind::WHITESPACE { - let mut next_next = tokens.next()?.kind(); - while next_next == SyntaxKind::WHITESPACE { - next_next = tokens.next()?.kind(); - } - if next_next == T![.] { - let ty = sema.type_of_expr(desc_expr)?.original; - if ty.is_unknown() { - return None; - } - if matches!(expr, ast::Expr::PathExpr(_)) { - if let Some(hir::Adt::Struct(st)) = ty.as_adt() { - if st.fields(sema.db).is_empty() { - return None; - } - } - } - acc.push(InlayHint { - range: expr.syntax().text_range(), - kind: InlayKind::ChainingHint, - label: hint_iterator(sema, &famous_defs, config, &ty) - .unwrap_or_else(|| ty.display_truncated(sema.db, config.max_length).to_string()) - .into(), - tooltip: Some(InlayTooltip::HoverRanged(file_id, expr.syntax().text_range())), - }); - } - } - Some(()) -} - -fn param_name_hints( - acc: &mut Vec, - sema: &Semantics<'_, RootDatabase>, - config: &InlayHintsConfig, - expr: ast::Expr, -) -> Option<()> { - if !config.parameter_hints { - return None; - } - - let (callable, arg_list) = get_callable(sema, &expr)?; - let hints = callable - .params(sema.db) - .into_iter() - .zip(arg_list.args()) - .filter_map(|((param, _ty), arg)| { - // Only annotate hints for expressions that exist in the original file - let range = sema.original_range_opt(arg.syntax())?; - let (param_name, name_syntax) = match param.as_ref()? { - Either::Left(pat) => ("self".to_string(), pat.name()), - Either::Right(pat) => match pat { - ast::Pat::IdentPat(it) => (it.name()?.to_string(), it.name()), - _ => return None, - }, - }; - Some((name_syntax, param_name, arg, range)) - }) - .filter(|(_, param_name, arg, _)| { - !should_hide_param_name_hint(sema, &callable, param_name, arg) - }) - .map(|(param, param_name, _, FileRange { range, .. })| { - let mut tooltip = None; - if let Some(name) = param { - if let hir::CallableKind::Function(f) = callable.kind() { - // assert the file is cached so we can map out of macros - if let Some(_) = sema.source(f) { - tooltip = sema.original_range_opt(name.syntax()); - } - } - } - - InlayHint { - range, - kind: InlayKind::ParameterHint, - label: param_name.into(), - tooltip: tooltip.map(|it| InlayTooltip::HoverOffset(it.file_id, it.range.start())), - } - }); - - acc.extend(hints); - Some(()) -} - -fn binding_mode_hints( - acc: &mut Vec, - sema: &Semantics<'_, RootDatabase>, - config: &InlayHintsConfig, - pat: &ast::Pat, -) -> Option<()> { - if !config.binding_mode_hints { - return None; - } - - let outer_paren_pat = pat - .syntax() - .ancestors() - .skip(1) - .map_while(ast::Pat::cast) - .map_while(|pat| match pat { - ast::Pat::ParenPat(pat) => Some(pat), - _ => None, - }) - .last(); - let range = - outer_paren_pat.as_ref().map_or_else(|| pat.syntax(), |it| it.syntax()).text_range(); - sema.pattern_adjustments(&pat).iter().for_each(|ty| { - let reference = ty.is_reference(); - let mut_reference = ty.is_mutable_reference(); - let r = match (reference, mut_reference) { - (true, true) => "&mut", - (true, false) => "&", - _ => return, - }; - acc.push(InlayHint { - range, - kind: InlayKind::BindingModeHint, - label: r.to_string().into(), - tooltip: Some(InlayTooltip::String("Inferred binding mode".into())), - }); - }); - match pat { - ast::Pat::IdentPat(pat) if pat.ref_token().is_none() && pat.mut_token().is_none() => { - let bm = sema.binding_mode_of_pat(pat)?; - let bm = match bm { - hir::BindingMode::Move => return None, - hir::BindingMode::Ref(Mutability::Mut) => "ref mut", - hir::BindingMode::Ref(Mutability::Shared) => "ref", - }; - acc.push(InlayHint { - range: pat.syntax().text_range(), - kind: InlayKind::BindingModeHint, - label: bm.to_string().into(), - tooltip: Some(InlayTooltip::String("Inferred binding mode".into())), - }); - } - ast::Pat::OrPat(pat) if outer_paren_pat.is_none() => { - acc.push(InlayHint { - range: pat.syntax().text_range(), - kind: InlayKind::OpeningParenthesis, - label: "(".into(), - tooltip: None, - }); - acc.push(InlayHint { - range: pat.syntax().text_range(), - kind: InlayKind::ClosingParenthesis, - label: ")".into(), - tooltip: None, - }); - } - _ => (), - } - - Some(()) -} - -fn bind_pat_hints( - acc: &mut Vec, - sema: &Semantics<'_, RootDatabase>, - config: &InlayHintsConfig, - file_id: FileId, - pat: &ast::IdentPat, -) -> Option<()> { - if !config.type_hints { - return None; - } - - let descended = sema.descend_node_into_attributes(pat.clone()).pop(); - let desc_pat = descended.as_ref().unwrap_or(pat); - let ty = sema.type_of_pat(&desc_pat.clone().into())?.original; - - if should_not_display_type_hint(sema, config, pat, &ty) { - return None; - } - - let krate = sema.scope(desc_pat.syntax())?.krate(); - let famous_defs = FamousDefs(sema, krate); - let label = hint_iterator(sema, &famous_defs, config, &ty); - - let label = match label { - Some(label) => label, - None => { - let ty_name = ty.display_truncated(sema.db, config.max_length).to_string(); - if config.hide_named_constructor_hints - && is_named_constructor(sema, pat, &ty_name).is_some() - { - return None; - } - ty_name - } - }; - - acc.push(InlayHint { - range: match pat.name() { - Some(name) => name.syntax().text_range(), - None => pat.syntax().text_range(), - }, - kind: InlayKind::TypeHint, - label: label.into(), - tooltip: pat - .name() - .map(|it| it.syntax().text_range()) - .map(|it| InlayTooltip::HoverRanged(file_id, it)), - }); - - Some(()) -} - -fn is_named_constructor( - sema: &Semantics<'_, RootDatabase>, - pat: &ast::IdentPat, - ty_name: &str, -) -> Option<()> { - let let_node = pat.syntax().parent()?; - let expr = match_ast! { - match let_node { - ast::LetStmt(it) => it.initializer(), - ast::LetExpr(it) => it.expr(), - _ => None, - } - }?; - - let expr = sema.descend_node_into_attributes(expr.clone()).pop().unwrap_or(expr); - // unwrap postfix expressions - let expr = match expr { - ast::Expr::TryExpr(it) => it.expr(), - ast::Expr::AwaitExpr(it) => it.expr(), - expr => Some(expr), - }?; - let expr = match expr { - ast::Expr::CallExpr(call) => match call.expr()? { - ast::Expr::PathExpr(path) => path, - _ => return None, - }, - ast::Expr::PathExpr(path) => path, - _ => return None, - }; - let path = expr.path()?; - - let callable = sema.type_of_expr(&ast::Expr::PathExpr(expr))?.original.as_callable(sema.db); - let callable_kind = callable.map(|it| it.kind()); - let qual_seg = match callable_kind { - Some(hir::CallableKind::Function(_) | hir::CallableKind::TupleEnumVariant(_)) => { - path.qualifier()?.segment() - } - _ => path.segment(), - }?; - - let ctor_name = match qual_seg.kind()? { - ast::PathSegmentKind::Name(name_ref) => { - match qual_seg.generic_arg_list().map(|it| it.generic_args()) { - Some(generics) => format!("{}<{}>", name_ref, generics.format(", ")), - None => name_ref.to_string(), - } - } - ast::PathSegmentKind::Type { type_ref: Some(ty), trait_ref: None } => ty.to_string(), - _ => return None, - }; - (ctor_name == ty_name).then(|| ()) -} - /// Checks if the type is an Iterator from std::iter and replaces its hint with an `impl Iterator`. fn hint_iterator( sema: &Semantics<'_, RootDatabase>, @@ -1067,255 +320,10 @@ fn hint_iterator( None } -fn pat_is_enum_variant(db: &RootDatabase, bind_pat: &ast::IdentPat, pat_ty: &hir::Type) -> bool { - if let Some(hir::Adt::Enum(enum_data)) = pat_ty.as_adt() { - let pat_text = bind_pat.to_string(); - enum_data - .variants(db) - .into_iter() - .map(|variant| variant.name(db).to_smol_str()) - .any(|enum_name| enum_name == pat_text) - } else { - false - } -} - -fn should_not_display_type_hint( - sema: &Semantics<'_, RootDatabase>, - config: &InlayHintsConfig, - bind_pat: &ast::IdentPat, - pat_ty: &hir::Type, -) -> bool { - let db = sema.db; - - if pat_ty.is_unknown() { - return true; - } - - if let Some(hir::Adt::Struct(s)) = pat_ty.as_adt() { - if s.fields(db).is_empty() && s.name(db).to_smol_str() == bind_pat.to_string() { - return true; - } - } - - if config.hide_closure_initialization_hints { - if let Some(parent) = bind_pat.syntax().parent() { - if let Some(it) = ast::LetStmt::cast(parent.clone()) { - if let Some(ast::Expr::ClosureExpr(closure)) = it.initializer() { - if closure_has_block_body(&closure) { - return true; - } - } - } - } - } - - for node in bind_pat.syntax().ancestors() { - match_ast! { - match node { - ast::LetStmt(it) => return it.ty().is_some(), - // FIXME: We might wanna show type hints in parameters for non-top level patterns as well - ast::Param(it) => return it.ty().is_some(), - ast::MatchArm(_) => return pat_is_enum_variant(db, bind_pat, pat_ty), - ast::LetExpr(_) => return pat_is_enum_variant(db, bind_pat, pat_ty), - ast::IfExpr(_) => return false, - ast::WhileExpr(_) => return false, - ast::ForExpr(it) => { - // We *should* display hint only if user provided "in {expr}" and we know the type of expr (and it's not unit). - // Type of expr should be iterable. - return it.in_token().is_none() || - it.iterable() - .and_then(|iterable_expr| sema.type_of_expr(&iterable_expr)) - .map(TypeInfo::original) - .map_or(true, |iterable_ty| iterable_ty.is_unknown() || iterable_ty.is_unit()) - }, - _ => (), - } - } - } - false -} - fn closure_has_block_body(closure: &ast::ClosureExpr) -> bool { matches!(closure.body(), Some(ast::Expr::BlockExpr(_))) } -fn should_hide_param_name_hint( - sema: &Semantics<'_, RootDatabase>, - callable: &hir::Callable, - param_name: &str, - argument: &ast::Expr, -) -> bool { - // These are to be tested in the `parameter_hint_heuristics` test - // hide when: - // - the parameter name is a suffix of the function's name - // - the argument is a qualified constructing or call expression where the qualifier is an ADT - // - exact argument<->parameter match(ignoring leading underscore) or parameter is a prefix/suffix - // of argument with _ splitting it off - // - param starts with `ra_fixture` - // - param is a well known name in a unary function - - let param_name = param_name.trim_start_matches('_'); - if param_name.is_empty() { - return true; - } - - if matches!(argument, ast::Expr::PrefixExpr(prefix) if prefix.op_kind() == Some(UnaryOp::Not)) { - return false; - } - - let fn_name = match callable.kind() { - hir::CallableKind::Function(it) => Some(it.name(sema.db).to_smol_str()), - _ => None, - }; - let fn_name = fn_name.as_deref(); - is_param_name_suffix_of_fn_name(param_name, callable, fn_name) - || is_argument_similar_to_param_name(argument, param_name) - || param_name.starts_with("ra_fixture") - || (callable.n_params() == 1 && is_obvious_param(param_name)) - || is_adt_constructor_similar_to_param_name(sema, argument, param_name) -} - -fn is_argument_similar_to_param_name(argument: &ast::Expr, param_name: &str) -> bool { - // check whether param_name and argument are the same or - // whether param_name is a prefix/suffix of argument(split at `_`) - let argument = match get_string_representation(argument) { - Some(argument) => argument, - None => return false, - }; - - // std is honestly too panic happy... - let str_split_at = |str: &str, at| str.is_char_boundary(at).then(|| argument.split_at(at)); - - let param_name = param_name.trim_start_matches('_'); - let argument = argument.trim_start_matches('_'); - - match str_split_at(argument, param_name.len()) { - Some((prefix, rest)) if prefix.eq_ignore_ascii_case(param_name) => { - return rest.is_empty() || rest.starts_with('_'); - } - _ => (), - } - match argument.len().checked_sub(param_name.len()).and_then(|at| str_split_at(argument, at)) { - Some((rest, suffix)) if param_name.eq_ignore_ascii_case(suffix) => { - return rest.is_empty() || rest.ends_with('_'); - } - _ => (), - } - false -} - -/// Hide the parameter name of a unary function if it is a `_` - prefixed suffix of the function's name, or equal. -/// -/// `fn strip_suffix(suffix)` will be hidden. -/// `fn stripsuffix(suffix)` will not be hidden. -fn is_param_name_suffix_of_fn_name( - param_name: &str, - callable: &Callable, - fn_name: Option<&str>, -) -> bool { - match (callable.n_params(), fn_name) { - (1, Some(function)) => { - function == param_name - || function - .len() - .checked_sub(param_name.len()) - .and_then(|at| function.is_char_boundary(at).then(|| function.split_at(at))) - .map_or(false, |(prefix, suffix)| { - suffix.eq_ignore_ascii_case(param_name) && prefix.ends_with('_') - }) - } - _ => false, - } -} - -fn is_adt_constructor_similar_to_param_name( - sema: &Semantics<'_, RootDatabase>, - argument: &ast::Expr, - param_name: &str, -) -> bool { - let path = match argument { - ast::Expr::CallExpr(c) => c.expr().and_then(|e| match e { - ast::Expr::PathExpr(p) => p.path(), - _ => None, - }), - ast::Expr::PathExpr(p) => p.path(), - ast::Expr::RecordExpr(r) => r.path(), - _ => return false, - }; - let path = match path { - Some(it) => it, - None => return false, - }; - (|| match sema.resolve_path(&path)? { - hir::PathResolution::Def(hir::ModuleDef::Adt(_)) => { - Some(to_lower_snake_case(&path.segment()?.name_ref()?.text()) == param_name) - } - hir::PathResolution::Def(hir::ModuleDef::Function(_) | hir::ModuleDef::Variant(_)) => { - if to_lower_snake_case(&path.segment()?.name_ref()?.text()) == param_name { - return Some(true); - } - let qual = path.qualifier()?; - match sema.resolve_path(&qual)? { - hir::PathResolution::Def(hir::ModuleDef::Adt(_)) => { - Some(to_lower_snake_case(&qual.segment()?.name_ref()?.text()) == param_name) - } - _ => None, - } - } - _ => None, - })() - .unwrap_or(false) -} - -fn get_string_representation(expr: &ast::Expr) -> Option { - match expr { - ast::Expr::MethodCallExpr(method_call_expr) => { - let name_ref = method_call_expr.name_ref()?; - match name_ref.text().as_str() { - "clone" | "as_ref" => method_call_expr.receiver().map(|rec| rec.to_string()), - name_ref => Some(name_ref.to_owned()), - } - } - ast::Expr::MacroExpr(macro_expr) => { - Some(macro_expr.macro_call()?.path()?.segment()?.to_string()) - } - ast::Expr::FieldExpr(field_expr) => Some(field_expr.name_ref()?.to_string()), - ast::Expr::PathExpr(path_expr) => Some(path_expr.path()?.segment()?.to_string()), - ast::Expr::PrefixExpr(prefix_expr) => get_string_representation(&prefix_expr.expr()?), - ast::Expr::RefExpr(ref_expr) => get_string_representation(&ref_expr.expr()?), - ast::Expr::CastExpr(cast_expr) => get_string_representation(&cast_expr.expr()?), - _ => None, - } -} - -fn is_obvious_param(param_name: &str) -> bool { - // avoid displaying hints for common functions like map, filter, etc. - // or other obvious words used in std - let is_obvious_param_name = - matches!(param_name, "predicate" | "value" | "pat" | "rhs" | "other"); - param_name.len() == 1 || is_obvious_param_name -} - -fn get_callable( - sema: &Semantics<'_, RootDatabase>, - expr: &ast::Expr, -) -> Option<(hir::Callable, ast::ArgList)> { - match expr { - ast::Expr::CallExpr(expr) => { - let descended = sema.descend_node_into_attributes(expr.clone()).pop(); - let expr = descended.as_ref().unwrap_or(expr); - sema.type_of_expr(&expr.expr()?)?.original.as_callable(sema.db).zip(expr.arg_list()) - } - ast::Expr::MethodCallExpr(expr) => { - let descended = sema.descend_node_into_attributes(expr.clone()).pop(); - let expr = descended.as_ref().unwrap_or(expr); - sema.resolve_method_call_as_callable(expr).zip(expr.arg_list()) - } - _ => None, - } -} - #[cfg(test)] mod tests { use expect_test::{expect, Expect}; diff --git a/crates/ide/src/inlay_hints/adjustment.rs b/crates/ide/src/inlay_hints/adjustment.rs new file mode 100644 index 00000000000..3fa2ed3307d --- /dev/null +++ b/crates/ide/src/inlay_hints/adjustment.rs @@ -0,0 +1,102 @@ +use hir::{Adjust, AutoBorrow, Mutability, OverloadedDeref, PointerCast, Safety, Semantics}; +use ide_db::RootDatabase; + +use syntax::ast::{self, AstNode}; + +use crate::{AdjustmentHints, InlayHint, InlayHintsConfig, InlayKind}; + +pub(super) fn hints( + acc: &mut Vec, + sema: &Semantics<'_, RootDatabase>, + config: &InlayHintsConfig, + expr: &ast::Expr, +) -> Option<()> { + if config.adjustment_hints == AdjustmentHints::Never { + return None; + } + + // These inherit from the inner expression which would result in duplicate hints + if let ast::Expr::ParenExpr(_) + | ast::Expr::IfExpr(_) + | ast::Expr::BlockExpr(_) + | ast::Expr::MatchExpr(_) = expr + { + return None; + } + + let parent = expr.syntax().parent().and_then(ast::Expr::cast); + let descended = sema.descend_node_into_attributes(expr.clone()).pop(); + let desc_expr = descended.as_ref().unwrap_or(expr); + let adjustments = sema.expr_adjustments(desc_expr).filter(|it| !it.is_empty())?; + let needs_parens = match parent { + Some(parent) => { + match parent { + ast::Expr::AwaitExpr(_) + | ast::Expr::CallExpr(_) + | ast::Expr::CastExpr(_) + | ast::Expr::FieldExpr(_) + | ast::Expr::MethodCallExpr(_) + | ast::Expr::TryExpr(_) => true, + // FIXME: shorthands need special casing, though not sure if adjustments are even valid there + ast::Expr::RecordExpr(_) => false, + ast::Expr::IndexExpr(index) => index.base().as_ref() == Some(expr), + _ => false, + } + } + None => false, + }; + if needs_parens { + acc.push(InlayHint { + range: expr.syntax().text_range(), + kind: InlayKind::OpeningParenthesis, + label: "(".into(), + tooltip: None, + }); + } + for adjustment in adjustments.into_iter().rev() { + // FIXME: Add some nicer tooltips to each of these + let text = match adjustment { + Adjust::NeverToAny if config.adjustment_hints == AdjustmentHints::Always => { + "" + } + Adjust::Deref(None) => "*", + Adjust::Deref(Some(OverloadedDeref(Mutability::Mut))) => "*", + Adjust::Deref(Some(OverloadedDeref(Mutability::Shared))) => "*", + Adjust::Borrow(AutoBorrow::Ref(Mutability::Shared)) => "&", + Adjust::Borrow(AutoBorrow::Ref(Mutability::Mut)) => "&mut ", + Adjust::Borrow(AutoBorrow::RawPtr(Mutability::Shared)) => "&raw const ", + Adjust::Borrow(AutoBorrow::RawPtr(Mutability::Mut)) => "&raw mut ", + // some of these could be represented via `as` casts, but that's not too nice and + // handling everything as a prefix expr makes the `(` and `)` insertion easier + Adjust::Pointer(cast) if config.adjustment_hints == AdjustmentHints::Always => { + match cast { + PointerCast::ReifyFnPointer => "", + PointerCast::UnsafeFnPointer => "", + PointerCast::ClosureFnPointer(Safety::Unsafe) => { + "" + } + PointerCast::ClosureFnPointer(Safety::Safe) => "", + PointerCast::MutToConstPointer => "", + PointerCast::ArrayToPointer => "", + PointerCast::Unsize => "", + } + } + _ => continue, + }; + acc.push(InlayHint { + range: expr.syntax().text_range(), + kind: InlayKind::AdjustmentHint, + label: text.into(), + tooltip: None, + }); + } + if needs_parens { + acc.push(InlayHint { + range: expr.syntax().text_range(), + kind: InlayKind::ClosingParenthesis, + label: ")".into(), + tooltip: None, + }); + } + Some(()) +} diff --git a/crates/ide/src/inlay_hints/bind_pat.rs b/crates/ide/src/inlay_hints/bind_pat.rs new file mode 100644 index 00000000000..68beb8fc35c --- /dev/null +++ b/crates/ide/src/inlay_hints/bind_pat.rs @@ -0,0 +1,187 @@ +use hir::{HirDisplay, Semantics, TypeInfo}; +use ide_db::{base_db::FileId, famous_defs::FamousDefs, RootDatabase}; + +use itertools::Itertools; +use syntax::{ + ast::{self, AstNode, HasName}, + match_ast, +}; + +use crate::{ + inlay_hints::{closure_has_block_body, hint_iterator}, + InlayHint, InlayHintsConfig, InlayKind, InlayTooltip, +}; + +pub(super) fn hints( + acc: &mut Vec, + sema: &Semantics<'_, RootDatabase>, + config: &InlayHintsConfig, + file_id: FileId, + pat: &ast::IdentPat, +) -> Option<()> { + if !config.type_hints { + return None; + } + + let descended = sema.descend_node_into_attributes(pat.clone()).pop(); + let desc_pat = descended.as_ref().unwrap_or(pat); + let ty = sema.type_of_pat(&desc_pat.clone().into())?.original; + + if should_not_display_type_hint(sema, config, pat, &ty) { + return None; + } + + let krate = sema.scope(desc_pat.syntax())?.krate(); + let famous_defs = FamousDefs(sema, krate); + let label = hint_iterator(sema, &famous_defs, config, &ty); + + let label = match label { + Some(label) => label, + None => { + let ty_name = ty.display_truncated(sema.db, config.max_length).to_string(); + if config.hide_named_constructor_hints + && is_named_constructor(sema, pat, &ty_name).is_some() + { + return None; + } + ty_name + } + }; + + acc.push(InlayHint { + range: match pat.name() { + Some(name) => name.syntax().text_range(), + None => pat.syntax().text_range(), + }, + kind: InlayKind::TypeHint, + label: label.into(), + tooltip: pat + .name() + .map(|it| it.syntax().text_range()) + .map(|it| InlayTooltip::HoverRanged(file_id, it)), + }); + + Some(()) +} + +fn should_not_display_type_hint( + sema: &Semantics<'_, RootDatabase>, + config: &InlayHintsConfig, + bind_pat: &ast::IdentPat, + pat_ty: &hir::Type, +) -> bool { + let db = sema.db; + + if pat_ty.is_unknown() { + return true; + } + + if let Some(hir::Adt::Struct(s)) = pat_ty.as_adt() { + if s.fields(db).is_empty() && s.name(db).to_smol_str() == bind_pat.to_string() { + return true; + } + } + + if config.hide_closure_initialization_hints { + if let Some(parent) = bind_pat.syntax().parent() { + if let Some(it) = ast::LetStmt::cast(parent.clone()) { + if let Some(ast::Expr::ClosureExpr(closure)) = it.initializer() { + if closure_has_block_body(&closure) { + return true; + } + } + } + } + } + + for node in bind_pat.syntax().ancestors() { + match_ast! { + match node { + ast::LetStmt(it) => return it.ty().is_some(), + // FIXME: We might wanna show type hints in parameters for non-top level patterns as well + ast::Param(it) => return it.ty().is_some(), + ast::MatchArm(_) => return pat_is_enum_variant(db, bind_pat, pat_ty), + ast::LetExpr(_) => return pat_is_enum_variant(db, bind_pat, pat_ty), + ast::IfExpr(_) => return false, + ast::WhileExpr(_) => return false, + ast::ForExpr(it) => { + // We *should* display hint only if user provided "in {expr}" and we know the type of expr (and it's not unit). + // Type of expr should be iterable. + return it.in_token().is_none() || + it.iterable() + .and_then(|iterable_expr| sema.type_of_expr(&iterable_expr)) + .map(TypeInfo::original) + .map_or(true, |iterable_ty| iterable_ty.is_unknown() || iterable_ty.is_unit()) + }, + _ => (), + } + } + } + false +} + +fn is_named_constructor( + sema: &Semantics<'_, RootDatabase>, + pat: &ast::IdentPat, + ty_name: &str, +) -> Option<()> { + let let_node = pat.syntax().parent()?; + let expr = match_ast! { + match let_node { + ast::LetStmt(it) => it.initializer(), + ast::LetExpr(it) => it.expr(), + _ => None, + } + }?; + + let expr = sema.descend_node_into_attributes(expr.clone()).pop().unwrap_or(expr); + // unwrap postfix expressions + let expr = match expr { + ast::Expr::TryExpr(it) => it.expr(), + ast::Expr::AwaitExpr(it) => it.expr(), + expr => Some(expr), + }?; + let expr = match expr { + ast::Expr::CallExpr(call) => match call.expr()? { + ast::Expr::PathExpr(path) => path, + _ => return None, + }, + ast::Expr::PathExpr(path) => path, + _ => return None, + }; + let path = expr.path()?; + + let callable = sema.type_of_expr(&ast::Expr::PathExpr(expr))?.original.as_callable(sema.db); + let callable_kind = callable.map(|it| it.kind()); + let qual_seg = match callable_kind { + Some(hir::CallableKind::Function(_) | hir::CallableKind::TupleEnumVariant(_)) => { + path.qualifier()?.segment() + } + _ => path.segment(), + }?; + + let ctor_name = match qual_seg.kind()? { + ast::PathSegmentKind::Name(name_ref) => { + match qual_seg.generic_arg_list().map(|it| it.generic_args()) { + Some(generics) => format!("{}<{}>", name_ref, generics.format(", ")), + None => name_ref.to_string(), + } + } + ast::PathSegmentKind::Type { type_ref: Some(ty), trait_ref: None } => ty.to_string(), + _ => return None, + }; + (ctor_name == ty_name).then(|| ()) +} + +fn pat_is_enum_variant(db: &RootDatabase, bind_pat: &ast::IdentPat, pat_ty: &hir::Type) -> bool { + if let Some(hir::Adt::Enum(enum_data)) = pat_ty.as_adt() { + let pat_text = bind_pat.to_string(); + enum_data + .variants(db) + .into_iter() + .map(|variant| variant.name(db).to_smol_str()) + .any(|enum_name| enum_name == pat_text) + } else { + false + } +} diff --git a/crates/ide/src/inlay_hints/binding_mode.rs b/crates/ide/src/inlay_hints/binding_mode.rs new file mode 100644 index 00000000000..f8fe9f5819a --- /dev/null +++ b/crates/ide/src/inlay_hints/binding_mode.rs @@ -0,0 +1,78 @@ +use hir::{Mutability, Semantics}; +use ide_db::RootDatabase; + +use syntax::ast::{self, AstNode}; + +use crate::{InlayHint, InlayHintsConfig, InlayKind, InlayTooltip}; + +pub(super) fn hints( + acc: &mut Vec, + sema: &Semantics<'_, RootDatabase>, + config: &InlayHintsConfig, + pat: &ast::Pat, +) -> Option<()> { + if !config.binding_mode_hints { + return None; + } + + let outer_paren_pat = pat + .syntax() + .ancestors() + .skip(1) + .map_while(ast::Pat::cast) + .map_while(|pat| match pat { + ast::Pat::ParenPat(pat) => Some(pat), + _ => None, + }) + .last(); + let range = + outer_paren_pat.as_ref().map_or_else(|| pat.syntax(), |it| it.syntax()).text_range(); + sema.pattern_adjustments(&pat).iter().for_each(|ty| { + let reference = ty.is_reference(); + let mut_reference = ty.is_mutable_reference(); + let r = match (reference, mut_reference) { + (true, true) => "&mut", + (true, false) => "&", + _ => return, + }; + acc.push(InlayHint { + range, + kind: InlayKind::BindingModeHint, + label: r.to_string().into(), + tooltip: Some(InlayTooltip::String("Inferred binding mode".into())), + }); + }); + match pat { + ast::Pat::IdentPat(pat) if pat.ref_token().is_none() && pat.mut_token().is_none() => { + let bm = sema.binding_mode_of_pat(pat)?; + let bm = match bm { + hir::BindingMode::Move => return None, + hir::BindingMode::Ref(Mutability::Mut) => "ref mut", + hir::BindingMode::Ref(Mutability::Shared) => "ref", + }; + acc.push(InlayHint { + range: pat.syntax().text_range(), + kind: InlayKind::BindingModeHint, + label: bm.to_string().into(), + tooltip: Some(InlayTooltip::String("Inferred binding mode".into())), + }); + } + ast::Pat::OrPat(pat) if outer_paren_pat.is_none() => { + acc.push(InlayHint { + range: pat.syntax().text_range(), + kind: InlayKind::OpeningParenthesis, + label: "(".into(), + tooltip: None, + }); + acc.push(InlayHint { + range: pat.syntax().text_range(), + kind: InlayKind::ClosingParenthesis, + label: ")".into(), + tooltip: None, + }); + } + _ => (), + } + + Some(()) +} diff --git a/crates/ide/src/inlay_hints/chaining.rs b/crates/ide/src/inlay_hints/chaining.rs new file mode 100644 index 00000000000..99b08a339b6 --- /dev/null +++ b/crates/ide/src/inlay_hints/chaining.rs @@ -0,0 +1,72 @@ +use hir::{HirDisplay, Semantics}; +use ide_db::{famous_defs::FamousDefs, RootDatabase}; +use syntax::{ + ast::{self, AstNode}, + Direction, NodeOrToken, SyntaxKind, T, +}; + +use crate::{ + inlay_hints::hint_iterator, FileId, InlayHint, InlayHintsConfig, InlayKind, InlayTooltip, +}; + +pub(super) fn hints( + acc: &mut Vec, + sema: &Semantics<'_, RootDatabase>, + famous_defs: &FamousDefs<'_, '_>, + config: &InlayHintsConfig, + file_id: FileId, + expr: &ast::Expr, +) -> Option<()> { + if !config.chaining_hints { + return None; + } + + if matches!(expr, ast::Expr::RecordExpr(_)) { + return None; + } + + let descended = sema.descend_node_into_attributes(expr.clone()).pop(); + let desc_expr = descended.as_ref().unwrap_or(expr); + + 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(); + if next == SyntaxKind::WHITESPACE { + let mut next_next = tokens.next()?.kind(); + while next_next == SyntaxKind::WHITESPACE { + next_next = tokens.next()?.kind(); + } + if next_next == T![.] { + let ty = sema.type_of_expr(desc_expr)?.original; + if ty.is_unknown() { + return None; + } + if matches!(expr, ast::Expr::PathExpr(_)) { + if let Some(hir::Adt::Struct(st)) = ty.as_adt() { + if st.fields(sema.db).is_empty() { + return None; + } + } + } + acc.push(InlayHint { + range: expr.syntax().text_range(), + kind: InlayKind::ChainingHint, + label: hint_iterator(sema, &famous_defs, config, &ty) + .unwrap_or_else(|| ty.display_truncated(sema.db, config.max_length).to_string()) + .into(), + tooltip: Some(InlayTooltip::HoverRanged(file_id, expr.syntax().text_range())), + }); + } + } + Some(()) +} diff --git a/crates/ide/src/inlay_hints/closing_brace.rs b/crates/ide/src/inlay_hints/closing_brace.rs new file mode 100644 index 00000000000..a33b64b5854 --- /dev/null +++ b/crates/ide/src/inlay_hints/closing_brace.rs @@ -0,0 +1,116 @@ +use hir::{HirDisplay, Semantics}; +use ide_db::{base_db::FileRange, RootDatabase}; +use syntax::{ + ast::{self, AstNode, HasName}, + match_ast, SyntaxKind, SyntaxNode, T, +}; + +use crate::{ + inlay_hints::InlayHintLabelPart, FileId, InlayHint, InlayHintLabel, InlayHintsConfig, InlayKind, +}; + +pub(super) fn hints( + acc: &mut Vec, + sema: &Semantics<'_, RootDatabase>, + config: &InlayHintsConfig, + file_id: FileId, + node: SyntaxNode, +) -> Option<()> { + let min_lines = config.closing_brace_hints_min_lines?; + + let name = |it: ast::Name| it.syntax().text_range(); + + let mut closing_token; + let (label, name_range) = if let Some(item_list) = ast::AssocItemList::cast(node.clone()) { + closing_token = item_list.r_curly_token()?; + + let parent = item_list.syntax().parent()?; + match_ast! { + match parent { + ast::Impl(imp) => { + let imp = sema.to_def(&imp)?; + let ty = imp.self_ty(sema.db); + let trait_ = imp.trait_(sema.db); + let hint_text = match trait_ { + Some(tr) => format!("impl {} for {}", tr.name(sema.db), ty.display_truncated(sema.db, config.max_length)), + None => format!("impl {}", ty.display_truncated(sema.db, config.max_length)), + }; + (hint_text, None) + }, + ast::Trait(tr) => { + (format!("trait {}", tr.name()?), tr.name().map(name)) + }, + _ => return None, + } + } + } else if let Some(list) = ast::ItemList::cast(node.clone()) { + closing_token = list.r_curly_token()?; + + let module = ast::Module::cast(list.syntax().parent()?)?; + (format!("mod {}", module.name()?), module.name().map(name)) + } else if let Some(block) = ast::BlockExpr::cast(node.clone()) { + closing_token = block.stmt_list()?.r_curly_token()?; + + let parent = block.syntax().parent()?; + match_ast! { + match parent { + ast::Fn(it) => { + // FIXME: this could include parameters, but `HirDisplay` prints too much info + // and doesn't respect the max length either, so the hints end up way too long + (format!("fn {}", it.name()?), it.name().map(name)) + }, + ast::Static(it) => (format!("static {}", it.name()?), it.name().map(name)), + ast::Const(it) => { + if it.underscore_token().is_some() { + ("const _".into(), None) + } else { + (format!("const {}", it.name()?), it.name().map(name)) + } + }, + _ => return None, + } + } + } else if let Some(mac) = ast::MacroCall::cast(node.clone()) { + let last_token = mac.syntax().last_token()?; + if last_token.kind() != T![;] && last_token.kind() != SyntaxKind::R_CURLY { + return None; + } + closing_token = last_token; + + ( + format!("{}!", mac.path()?), + mac.path().and_then(|it| it.segment()).map(|it| it.syntax().text_range()), + ) + } else { + return None; + }; + + if let Some(mut next) = closing_token.next_token() { + if next.kind() == T![;] { + if let Some(tok) = next.next_token() { + closing_token = next; + next = tok; + } + } + if !(next.kind() == SyntaxKind::WHITESPACE && next.text().contains('\n')) { + // Only display the hint if the `}` is the last token on the line + return None; + } + } + + let mut lines = 1; + node.text().for_each_chunk(|s| lines += s.matches('\n').count()); + if lines < min_lines { + return None; + } + + let linked_location = name_range.map(|range| FileRange { file_id, range }); + acc.push(InlayHint { + range: closing_token.text_range(), + kind: InlayKind::ClosingBraceHint, + label: InlayHintLabel { parts: vec![InlayHintLabelPart { text: label, linked_location }] }, + tooltip: None, // provided by label part location + }); + + None +} diff --git a/crates/ide/src/inlay_hints/closure_ret.rs b/crates/ide/src/inlay_hints/closure_ret.rs new file mode 100644 index 00000000000..1843c4a80fa --- /dev/null +++ b/crates/ide/src/inlay_hints/closure_ret.rs @@ -0,0 +1,50 @@ +use hir::{HirDisplay, Semantics}; +use ide_db::{base_db::FileId, famous_defs::FamousDefs, RootDatabase}; +use syntax::ast::{self, AstNode}; + +use crate::{ + inlay_hints::{closure_has_block_body, hint_iterator}, + ClosureReturnTypeHints, InlayHint, InlayHintsConfig, InlayKind, InlayTooltip, +}; + +pub(super) fn hints( + acc: &mut Vec, + sema: &Semantics<'_, RootDatabase>, + famous_defs: &FamousDefs<'_, '_>, + config: &InlayHintsConfig, + file_id: FileId, + closure: ast::ClosureExpr, +) -> Option<()> { + if config.closure_return_type_hints == ClosureReturnTypeHints::Never { + return None; + } + + if closure.ret_type().is_some() { + return None; + } + + if !closure_has_block_body(&closure) + && config.closure_return_type_hints == ClosureReturnTypeHints::WithBlock + { + return None; + } + + let param_list = closure.param_list()?; + + let closure = sema.descend_node_into_attributes(closure.clone()).pop()?; + let ty = sema.type_of_expr(&ast::Expr::ClosureExpr(closure))?.adjusted(); + let callable = ty.as_callable(sema.db)?; + let ty = callable.return_type(); + if ty.is_unit() { + return None; + } + acc.push(InlayHint { + range: param_list.syntax().text_range(), + kind: InlayKind::ClosureReturnTypeHint, + label: hint_iterator(sema, &famous_defs, config, &ty) + .unwrap_or_else(|| ty.display_truncated(sema.db, config.max_length).to_string()) + .into(), + tooltip: Some(InlayTooltip::HoverRanged(file_id, param_list.syntax().text_range())), + }); + Some(()) +} diff --git a/crates/ide/src/inlay_hints/fn_lifetime_fn.rs b/crates/ide/src/inlay_hints/fn_lifetime_fn.rs new file mode 100644 index 00000000000..ae8f4549ddc --- /dev/null +++ b/crates/ide/src/inlay_hints/fn_lifetime_fn.rs @@ -0,0 +1,190 @@ +use ide_db::{syntax_helpers::node_ext::walk_ty, FxHashMap}; +use itertools::Itertools; +use syntax::SmolStr; +use syntax::{ + ast::{self, AstNode, HasGenericParams, HasName}, + SyntaxToken, +}; + +use crate::{InlayHint, InlayHintsConfig, InlayKind, InlayTooltip, LifetimeElisionHints}; + +pub(super) fn hints( + acc: &mut Vec, + config: &InlayHintsConfig, + func: ast::Fn, +) -> Option<()> { + if config.lifetime_elision_hints == LifetimeElisionHints::Never { + return None; + } + + let mk_lt_hint = |t: SyntaxToken, label: String| InlayHint { + range: t.text_range(), + kind: InlayKind::LifetimeHint, + label: label.into(), + tooltip: Some(InlayTooltip::String("Elided lifetime".into())), + }; + + let param_list = func.param_list()?; + let generic_param_list = func.generic_param_list(); + let ret_type = func.ret_type(); + let self_param = param_list.self_param().filter(|it| it.amp_token().is_some()); + + let is_elided = |lt: &Option| match lt { + Some(lt) => matches!(lt.text().as_str(), "'_"), + None => true, + }; + + let potential_lt_refs = { + let mut acc: Vec<_> = vec![]; + if let Some(self_param) = &self_param { + let lifetime = self_param.lifetime(); + let is_elided = is_elided(&lifetime); + acc.push((None, self_param.amp_token(), lifetime, is_elided)); + } + param_list.params().filter_map(|it| Some((it.pat(), it.ty()?))).for_each(|(pat, ty)| { + // FIXME: check path types + walk_ty(&ty, &mut |ty| match ty { + ast::Type::RefType(r) => { + let lifetime = r.lifetime(); + let is_elided = is_elided(&lifetime); + acc.push(( + pat.as_ref().and_then(|it| match it { + ast::Pat::IdentPat(p) => p.name(), + _ => None, + }), + r.amp_token(), + lifetime, + is_elided, + )) + } + _ => (), + }) + }); + acc + }; + + // allocate names + let mut gen_idx_name = { + let mut gen = (0u8..).map(|idx| match idx { + idx if idx < 10 => SmolStr::from_iter(['\'', (idx + 48) as char]), + idx => format!("'{idx}").into(), + }); + move || gen.next().unwrap_or_default() + }; + let mut allocated_lifetimes = vec![]; + + let mut used_names: FxHashMap = + match config.param_names_for_lifetime_elision_hints { + true => generic_param_list + .iter() + .flat_map(|gpl| gpl.lifetime_params()) + .filter_map(|param| param.lifetime()) + .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0))) + .collect(), + false => Default::default(), + }; + { + let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided); + if let Some(_) = &self_param { + if let Some(_) = potential_lt_refs.next() { + allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints { + // self can't be used as a lifetime, so no need to check for collisions + "'self".into() + } else { + gen_idx_name() + }); + } + } + potential_lt_refs.for_each(|(name, ..)| { + let name = match name { + Some(it) if config.param_names_for_lifetime_elision_hints => { + if let Some(c) = used_names.get_mut(it.text().as_str()) { + *c += 1; + SmolStr::from(format!("'{text}{c}", text = it.text().as_str())) + } else { + used_names.insert(it.text().as_str().into(), 0); + SmolStr::from_iter(["\'", it.text().as_str()]) + } + } + _ => gen_idx_name(), + }; + allocated_lifetimes.push(name); + }); + } + + // fetch output lifetime if elision rule applies + let output = match potential_lt_refs.as_slice() { + [(_, _, lifetime, _), ..] if self_param.is_some() || potential_lt_refs.len() == 1 => { + match lifetime { + Some(lt) => match lt.text().as_str() { + "'_" => allocated_lifetimes.get(0).cloned(), + "'static" => None, + name => Some(name.into()), + }, + None => allocated_lifetimes.get(0).cloned(), + } + } + [..] => None, + }; + + if allocated_lifetimes.is_empty() && output.is_none() { + return None; + } + + // apply hints + // apply output if required + let mut is_trivial = true; + if let (Some(output_lt), Some(r)) = (&output, ret_type) { + if let Some(ty) = r.ty() { + walk_ty(&ty, &mut |ty| match ty { + ast::Type::RefType(ty) if ty.lifetime().is_none() => { + if let Some(amp) = ty.amp_token() { + is_trivial = false; + acc.push(mk_lt_hint(amp, output_lt.to_string())); + } + } + _ => (), + }) + } + } + + if config.lifetime_elision_hints == LifetimeElisionHints::SkipTrivial && is_trivial { + return None; + } + + let mut a = allocated_lifetimes.iter(); + for (_, amp_token, _, is_elided) in potential_lt_refs { + if is_elided { + let t = amp_token?; + let lt = a.next()?; + acc.push(mk_lt_hint(t, lt.to_string())); + } + } + + // generate generic param list things + match (generic_param_list, allocated_lifetimes.as_slice()) { + (_, []) => (), + (Some(gpl), allocated_lifetimes) => { + let angle_tok = gpl.l_angle_token()?; + let is_empty = gpl.generic_params().next().is_none(); + acc.push(InlayHint { + range: angle_tok.text_range(), + kind: InlayKind::LifetimeHint, + label: format!( + "{}{}", + allocated_lifetimes.iter().format(", "), + if is_empty { "" } else { ", " } + ) + .into(), + tooltip: Some(InlayTooltip::String("Elided lifetimes".into())), + }); + } + (None, allocated_lifetimes) => acc.push(InlayHint { + range: func.name()?.syntax().text_range(), + kind: InlayKind::GenericParamListHint, + label: format!("<{}>", allocated_lifetimes.iter().format(", "),).into(), + tooltip: Some(InlayTooltip::String("Elided lifetimes".into())), + }), + } + Some(()) +} diff --git a/crates/ide/src/inlay_hints/implicit_static.rs b/crates/ide/src/inlay_hints/implicit_static.rs new file mode 100644 index 00000000000..8e0dbe88dfd --- /dev/null +++ b/crates/ide/src/inlay_hints/implicit_static.rs @@ -0,0 +1,39 @@ +use either::Either; +use syntax::{ + ast::{self, AstNode}, + SyntaxKind, +}; + +use crate::{InlayHint, InlayHintsConfig, InlayKind, InlayTooltip, LifetimeElisionHints}; + +pub(super) fn hints( + acc: &mut Vec, + config: &InlayHintsConfig, + statik_or_const: Either, +) -> Option<()> { + if config.lifetime_elision_hints != LifetimeElisionHints::Always { + return None; + } + + if let Either::Right(it) = &statik_or_const { + if ast::AssocItemList::can_cast( + it.syntax().parent().map_or(SyntaxKind::EOF, |it| it.kind()), + ) { + return None; + } + } + + if let Some(ast::Type::RefType(ty)) = statik_or_const.either(|it| it.ty(), |it| it.ty()) { + if ty.lifetime().is_none() { + let t = ty.amp_token()?; + acc.push(InlayHint { + range: t.text_range(), + kind: InlayKind::LifetimeHint, + label: "'static".to_owned().into(), + tooltip: Some(InlayTooltip::String("Elided static lifetime".into())), + }); + } + } + + Some(()) +} diff --git a/crates/ide/src/inlay_hints/param_name.rs b/crates/ide/src/inlay_hints/param_name.rs new file mode 100644 index 00000000000..2675e7e80fc --- /dev/null +++ b/crates/ide/src/inlay_hints/param_name.rs @@ -0,0 +1,237 @@ +use either::Either; +use hir::{Callable, Semantics}; +use ide_db::{base_db::FileRange, RootDatabase}; + +use stdx::to_lower_snake_case; +use syntax::ast::{self, AstNode, HasArgList, HasName, UnaryOp}; + +use crate::{InlayHint, InlayHintsConfig, InlayKind, InlayTooltip}; + +pub(super) fn hints( + acc: &mut Vec, + sema: &Semantics<'_, RootDatabase>, + config: &InlayHintsConfig, + expr: ast::Expr, +) -> Option<()> { + if !config.parameter_hints { + return None; + } + + let (callable, arg_list) = get_callable(sema, &expr)?; + let hints = callable + .params(sema.db) + .into_iter() + .zip(arg_list.args()) + .filter_map(|((param, _ty), arg)| { + // Only annotate hints for expressions that exist in the original file + let range = sema.original_range_opt(arg.syntax())?; + let (param_name, name_syntax) = match param.as_ref()? { + Either::Left(pat) => ("self".to_string(), pat.name()), + Either::Right(pat) => match pat { + ast::Pat::IdentPat(it) => (it.name()?.to_string(), it.name()), + _ => return None, + }, + }; + Some((name_syntax, param_name, arg, range)) + }) + .filter(|(_, param_name, arg, _)| { + !should_hide_param_name_hint(sema, &callable, param_name, arg) + }) + .map(|(param, param_name, _, FileRange { range, .. })| { + let mut tooltip = None; + if let Some(name) = param { + if let hir::CallableKind::Function(f) = callable.kind() { + // assert the file is cached so we can map out of macros + if let Some(_) = sema.source(f) { + tooltip = sema.original_range_opt(name.syntax()); + } + } + } + + InlayHint { + range, + kind: InlayKind::ParameterHint, + label: param_name.into(), + tooltip: tooltip.map(|it| InlayTooltip::HoverOffset(it.file_id, it.range.start())), + } + }); + + acc.extend(hints); + Some(()) +} + +fn get_callable( + sema: &Semantics<'_, RootDatabase>, + expr: &ast::Expr, +) -> Option<(hir::Callable, ast::ArgList)> { + match expr { + ast::Expr::CallExpr(expr) => { + let descended = sema.descend_node_into_attributes(expr.clone()).pop(); + let expr = descended.as_ref().unwrap_or(expr); + sema.type_of_expr(&expr.expr()?)?.original.as_callable(sema.db).zip(expr.arg_list()) + } + ast::Expr::MethodCallExpr(expr) => { + let descended = sema.descend_node_into_attributes(expr.clone()).pop(); + let expr = descended.as_ref().unwrap_or(expr); + sema.resolve_method_call_as_callable(expr).zip(expr.arg_list()) + } + _ => None, + } +} + +fn should_hide_param_name_hint( + sema: &Semantics<'_, RootDatabase>, + callable: &hir::Callable, + param_name: &str, + argument: &ast::Expr, +) -> bool { + // These are to be tested in the `parameter_hint_heuristics` test + // hide when: + // - the parameter name is a suffix of the function's name + // - the argument is a qualified constructing or call expression where the qualifier is an ADT + // - exact argument<->parameter match(ignoring leading underscore) or parameter is a prefix/suffix + // of argument with _ splitting it off + // - param starts with `ra_fixture` + // - param is a well known name in a unary function + + let param_name = param_name.trim_start_matches('_'); + if param_name.is_empty() { + return true; + } + + if matches!(argument, ast::Expr::PrefixExpr(prefix) if prefix.op_kind() == Some(UnaryOp::Not)) { + return false; + } + + let fn_name = match callable.kind() { + hir::CallableKind::Function(it) => Some(it.name(sema.db).to_smol_str()), + _ => None, + }; + let fn_name = fn_name.as_deref(); + is_param_name_suffix_of_fn_name(param_name, callable, fn_name) + || is_argument_similar_to_param_name(argument, param_name) + || param_name.starts_with("ra_fixture") + || (callable.n_params() == 1 && is_obvious_param(param_name)) + || is_adt_constructor_similar_to_param_name(sema, argument, param_name) +} + +/// Hide the parameter name of a unary function if it is a `_` - prefixed suffix of the function's name, or equal. +/// +/// `fn strip_suffix(suffix)` will be hidden. +/// `fn stripsuffix(suffix)` will not be hidden. +fn is_param_name_suffix_of_fn_name( + param_name: &str, + callable: &Callable, + fn_name: Option<&str>, +) -> bool { + match (callable.n_params(), fn_name) { + (1, Some(function)) => { + function == param_name + || function + .len() + .checked_sub(param_name.len()) + .and_then(|at| function.is_char_boundary(at).then(|| function.split_at(at))) + .map_or(false, |(prefix, suffix)| { + suffix.eq_ignore_ascii_case(param_name) && prefix.ends_with('_') + }) + } + _ => false, + } +} + +fn is_argument_similar_to_param_name(argument: &ast::Expr, param_name: &str) -> bool { + // check whether param_name and argument are the same or + // whether param_name is a prefix/suffix of argument(split at `_`) + let argument = match get_string_representation(argument) { + Some(argument) => argument, + None => return false, + }; + + // std is honestly too panic happy... + let str_split_at = |str: &str, at| str.is_char_boundary(at).then(|| argument.split_at(at)); + + let param_name = param_name.trim_start_matches('_'); + let argument = argument.trim_start_matches('_'); + + match str_split_at(argument, param_name.len()) { + Some((prefix, rest)) if prefix.eq_ignore_ascii_case(param_name) => { + return rest.is_empty() || rest.starts_with('_'); + } + _ => (), + } + match argument.len().checked_sub(param_name.len()).and_then(|at| str_split_at(argument, at)) { + Some((rest, suffix)) if param_name.eq_ignore_ascii_case(suffix) => { + return rest.is_empty() || rest.ends_with('_'); + } + _ => (), + } + false +} + +fn get_string_representation(expr: &ast::Expr) -> Option { + match expr { + ast::Expr::MethodCallExpr(method_call_expr) => { + let name_ref = method_call_expr.name_ref()?; + match name_ref.text().as_str() { + "clone" | "as_ref" => method_call_expr.receiver().map(|rec| rec.to_string()), + name_ref => Some(name_ref.to_owned()), + } + } + ast::Expr::MacroExpr(macro_expr) => { + Some(macro_expr.macro_call()?.path()?.segment()?.to_string()) + } + ast::Expr::FieldExpr(field_expr) => Some(field_expr.name_ref()?.to_string()), + ast::Expr::PathExpr(path_expr) => Some(path_expr.path()?.segment()?.to_string()), + ast::Expr::PrefixExpr(prefix_expr) => get_string_representation(&prefix_expr.expr()?), + ast::Expr::RefExpr(ref_expr) => get_string_representation(&ref_expr.expr()?), + ast::Expr::CastExpr(cast_expr) => get_string_representation(&cast_expr.expr()?), + _ => None, + } +} + +fn is_obvious_param(param_name: &str) -> bool { + // avoid displaying hints for common functions like map, filter, etc. + // or other obvious words used in std + let is_obvious_param_name = + matches!(param_name, "predicate" | "value" | "pat" | "rhs" | "other"); + param_name.len() == 1 || is_obvious_param_name +} + +fn is_adt_constructor_similar_to_param_name( + sema: &Semantics<'_, RootDatabase>, + argument: &ast::Expr, + param_name: &str, +) -> bool { + let path = match argument { + ast::Expr::CallExpr(c) => c.expr().and_then(|e| match e { + ast::Expr::PathExpr(p) => p.path(), + _ => None, + }), + ast::Expr::PathExpr(p) => p.path(), + ast::Expr::RecordExpr(r) => r.path(), + _ => return false, + }; + let path = match path { + Some(it) => it, + None => return false, + }; + (|| match sema.resolve_path(&path)? { + hir::PathResolution::Def(hir::ModuleDef::Adt(_)) => { + Some(to_lower_snake_case(&path.segment()?.name_ref()?.text()) == param_name) + } + hir::PathResolution::Def(hir::ModuleDef::Function(_) | hir::ModuleDef::Variant(_)) => { + if to_lower_snake_case(&path.segment()?.name_ref()?.text()) == param_name { + return Some(true); + } + let qual = path.qualifier()?; + match sema.resolve_path(&qual)? { + hir::PathResolution::Def(hir::ModuleDef::Adt(_)) => { + Some(to_lower_snake_case(&qual.segment()?.name_ref()?.text()) == param_name) + } + _ => None, + } + } + _ => None, + })() + .unwrap_or(false) +}