diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs index 93dd56a450d9..be99510af2a1 100644 --- a/crates/ide/src/inlay_hints.rs +++ b/crates/ide/src/inlay_hints.rs @@ -14,8 +14,8 @@ use smallvec::{smallvec, SmallVec}; use span::{Edition, EditionedFileId}; use stdx::never; use syntax::{ - ast::{self, AstNode}, - match_ast, NodeOrToken, SyntaxNode, TextRange, TextSize, WalkEvent, + ast::{self, AstNode, HasGenericParams}, + format_smolstr, match_ast, NodeOrToken, SmolStr, SyntaxNode, TextRange, TextSize, WalkEvent, }; use text_edit::TextEdit; @@ -29,10 +29,10 @@ mod closing_brace; mod closure_captures; mod closure_ret; mod discriminant; -mod fn_lifetime_fn; mod generic_param; mod implicit_drop; mod implicit_static; +mod lifetime; mod param_name; mod range_exclusive; @@ -94,8 +94,8 @@ pub(crate) fn inlay_hints( }; let famous_defs = FamousDefs(&sema, scope.krate()); - let parent_impl = &mut None; - let hints = |node| hints(&mut acc, parent_impl, &famous_defs, config, file_id, node); + let ctx = &mut InlayHintCtx::default(); + let hints = |node| hints(&mut acc, ctx, &famous_defs, config, file_id, node); match range_limit { // FIXME: This can miss some hints that require the parent of the range to calculate Some(range) => match file.covering_element(range) { @@ -111,6 +111,12 @@ pub(crate) fn inlay_hints( acc } +#[derive(Default)] +struct InlayHintCtx { + lifetime_stacks: Vec>, + is_param_list: bool, +} + pub(crate) fn inlay_hints_resolve( db: &RootDatabase, file_id: FileId, @@ -131,8 +137,8 @@ pub(crate) fn inlay_hints_resolve( let famous_defs = FamousDefs(&sema, scope.krate()); let mut acc = Vec::new(); - let parent_impl = &mut None; - let hints = |node| hints(&mut acc, parent_impl, &famous_defs, config, file_id, node); + let ctx = &mut InlayHintCtx::default(); + let hints = |node| hints(&mut acc, ctx, &famous_defs, config, file_id, node); let mut res = file.clone(); let res = loop { @@ -146,9 +152,11 @@ pub(crate) fn inlay_hints_resolve( acc.into_iter().find(|hint| hasher(hint) == hash) } +// FIXME: At some point when our hir infra is fleshed out enough we should flip this and traverse the +// HIR instead of the syntax tree. fn hints( hints: &mut Vec, - parent_impl: &mut Option, + ctx: &mut InlayHintCtx, famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>, config: &InlayHintsConfig, file_id: EditionedFileId, @@ -157,12 +165,30 @@ fn hints( let node = match node { WalkEvent::Enter(node) => node, WalkEvent::Leave(n) => { - if ast::Impl::can_cast(n.kind()) { - parent_impl.take(); + if ast::AnyHasGenericParams::can_cast(n.kind()) { + ctx.lifetime_stacks.pop(); + // pop + } + if ast::ParamList::can_cast(n.kind()) { + ctx.is_param_list = false; + // pop } return; } }; + + if let Some(node) = ast::AnyHasGenericParams::cast(node.clone()) { + let params = node + .generic_param_list() + .map(|it| { + it.lifetime_params() + .filter_map(|it| it.lifetime().map(|it| format_smolstr!("{}", &it.text()[1..]))) + .collect() + }) + .unwrap_or_default(); + ctx.lifetime_stacks.push(params); + } + closing_brace::hints(hints, sema, config, file_id, node.clone()); if let Some(any_has_generic_args) = ast::AnyHasGenericArgs::cast(node.clone()) { generic_param::hints(hints, sema, config, any_has_generic_args); @@ -183,7 +209,7 @@ fn hints( closure_ret::hints(hints, famous_defs, config, file_id, it) }, ast::Expr::RangeExpr(it) => range_exclusive::hints(hints, famous_defs, config, file_id, it), - _ => None, + _ => Some(()), } }, ast::Pat(it) => { @@ -200,14 +226,9 @@ fn hints( 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(impl_) => { - *parent_impl = Some(impl_); - None - }, ast::Item::Fn(it) => { implicit_drop::hints(hints, famous_defs, config, file_id, &it); - fn_lifetime_fn::hints(hints, famous_defs, config, file_id, it) + lifetime::fn_hints(hints, ctx, famous_defs, config, file_id, it) }, // static type elisions ast::Item::Static(it) => implicit_static::hints(hints, famous_defs, config, file_id, Either::Left(it)), @@ -215,9 +236,17 @@ fn hints( ast::Item::Enum(it) => discriminant::enum_hints(hints, famous_defs, config, file_id, it), _ => None, }, - // FIXME: fn-ptr type, dyn fn type, and trait object type elisions - ast::Type(_) => None, - _ => None, + // FIXME: trait object type elisions + ast::Type(ty) => match ty { + ast::Type::FnPtrType(ptr) => lifetime::fn_ptr_hints(hints, ctx, famous_defs, config, file_id, ptr), + ast::Type::PathType(path) => lifetime::fn_path_hints(hints, ctx, famous_defs, config, file_id, path), + _ => Some(()), + }, + ast::ParamList(_) => { + ctx.is_param_list = true; + Some(()) + }, + _ => Some(()), } }; } diff --git a/crates/ide/src/inlay_hints/fn_lifetime_fn.rs b/crates/ide/src/inlay_hints/fn_lifetime_fn.rs deleted file mode 100644 index 4d35e71a06fe..000000000000 --- a/crates/ide/src/inlay_hints/fn_lifetime_fn.rs +++ /dev/null @@ -1,338 +0,0 @@ -//! Implementation of "lifetime elision" inlay hints: -//! ```no_run -//! fn example/* <'0> */(a: &/* '0 */()) {} -//! ``` -use ide_db::{famous_defs::FamousDefs, syntax_helpers::node_ext::walk_ty, FxHashMap}; -use itertools::Itertools; -use span::EditionedFileId; -use syntax::{ - ast::{self, AstNode, HasGenericParams, HasName}, - SyntaxToken, -}; -use syntax::{format_smolstr, SmolStr}; - -use crate::{InlayHint, InlayHintPosition, InlayHintsConfig, InlayKind, LifetimeElisionHints}; - -pub(super) fn hints( - acc: &mut Vec, - FamousDefs(_, _): &FamousDefs<'_, '_>, - config: &InlayHintsConfig, - _file_id: EditionedFileId, - 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::Lifetime, - label: label.into(), - text_edit: None, - position: InlayHintPosition::After, - pad_left: false, - pad_right: true, - resolve_parent: None, - }; - - 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, - )); - false - } - ast::Type::FnPtrType(_) => true, - ast::Type::PathType(t) => { - t.path().and_then(|it| it.segment()).and_then(|it| it.param_list()).is_some() - } - _ => false, - }) - }); - 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_smolstr!("'{idx}"), - }); - 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 self_param.is_some() && potential_lt_refs.next().is_some() { - 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.first().cloned(), - "'static" => None, - name => Some(name.into()), - }, - None => allocated_lifetimes.first().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())); - } - false - } - ast::Type::FnPtrType(_) => true, - ast::Type::PathType(t) => { - t.path().and_then(|it| it.segment()).and_then(|it| it.param_list()).is_some() - } - _ => false, - }) - } - } - - 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::Lifetime, - label: format!( - "{}{}", - allocated_lifetimes.iter().format(", "), - if is_empty { "" } else { ", " } - ) - .into(), - text_edit: None, - position: InlayHintPosition::After, - pad_left: false, - pad_right: true, - resolve_parent: None, - }); - } - (None, allocated_lifetimes) => acc.push(InlayHint { - range: func.name()?.syntax().text_range(), - kind: InlayKind::GenericParamList, - label: format!("<{}>", allocated_lifetimes.iter().format(", "),).into(), - text_edit: None, - position: InlayHintPosition::After, - pad_left: false, - pad_right: false, - resolve_parent: None, - }), - } - Some(()) -} - -#[cfg(test)] -mod tests { - use crate::{ - inlay_hints::tests::{check, check_with_config, TEST_CONFIG}, - InlayHintsConfig, LifetimeElisionHints, - }; - - #[test] - fn hints_lifetimes() { - check( - r#" -fn empty() {} - -fn no_gpl(a: &()) {} - //^^^^^^<'0> - // ^'0 -fn empty_gpl<>(a: &()) {} - // ^'0 ^'0 -fn partial<'b>(a: &(), b: &'b ()) {} -// ^'0, $ ^'0 -fn partial<'a>(a: &'a (), b: &()) {} -// ^'0, $ ^'0 - -fn single_ret(a: &()) -> &() {} -// ^^^^^^^^^^<'0> - // ^'0 ^'0 -fn full_mul(a: &(), b: &()) {} -// ^^^^^^^^<'0, '1> - // ^'0 ^'1 - -fn foo<'c>(a: &'c ()) -> &() {} - // ^'c - -fn nested_in(a: & &X< &()>) {} -// ^^^^^^^^^<'0, '1, '2> - //^'0 ^'1 ^'2 -fn nested_out(a: &()) -> & &X< &()>{} -// ^^^^^^^^^^<'0> - //^'0 ^'0 ^'0 ^'0 - -impl () { - fn foo(&self) {} - // ^^^<'0> - // ^'0 - fn foo(&self) -> &() {} - // ^^^<'0> - // ^'0 ^'0 - fn foo(&self, a: &()) -> &() {} - // ^^^<'0, '1> - // ^'0 ^'1 ^'0 -} -"#, - ); - } - - #[test] - fn hints_lifetimes_named() { - check_with_config( - InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }, - r#" -fn nested_in<'named>(named: & &X< &()>) {} -// ^'named1, 'named2, 'named3, $ - //^'named1 ^'named2 ^'named3 -"#, - ); - } - - #[test] - fn hints_lifetimes_trivial_skip() { - check_with_config( - InlayHintsConfig { - lifetime_elision_hints: LifetimeElisionHints::SkipTrivial, - ..TEST_CONFIG - }, - r#" -fn no_gpl(a: &()) {} -fn empty_gpl<>(a: &()) {} -fn partial<'b>(a: &(), b: &'b ()) {} -fn partial<'a>(a: &'a (), b: &()) {} - -fn single_ret(a: &()) -> &() {} -// ^^^^^^^^^^<'0> - // ^'0 ^'0 -fn full_mul(a: &(), b: &()) {} - -fn foo<'c>(a: &'c ()) -> &() {} - // ^'c - -fn nested_in(a: & &X< &()>) {} -fn nested_out(a: &()) -> & &X< &()>{} -// ^^^^^^^^^^<'0> - //^'0 ^'0 ^'0 ^'0 - -impl () { - fn foo(&self) {} - fn foo(&self) -> &() {} - // ^^^<'0> - // ^'0 ^'0 - fn foo(&self, a: &()) -> &() {} - // ^^^<'0, '1> - // ^'0 ^'1 ^'0 -} -"#, - ); - } - - #[test] - fn hints_lifetimes_skip_fn_likes() { - check_with_config( - InlayHintsConfig { - lifetime_elision_hints: LifetimeElisionHints::Always, - ..TEST_CONFIG - }, - r#" -fn fn_ptr(a: fn(&()) -> &()) {} -fn fn_trait<>(a: impl Fn(&()) -> &()) {} -"#, - ); - } -} diff --git a/crates/ide/src/inlay_hints/lifetime.rs b/crates/ide/src/inlay_hints/lifetime.rs new file mode 100644 index 000000000000..de463670677c --- /dev/null +++ b/crates/ide/src/inlay_hints/lifetime.rs @@ -0,0 +1,548 @@ +//! Implementation of "lifetime elision" inlay hints: +//! ```no_run +//! fn example/* <'0> */(a: &/* '0 */()) {} +//! ``` +use std::iter; + +use ide_db::{famous_defs::FamousDefs, syntax_helpers::node_ext::walk_ty, FxHashMap}; +use itertools::Itertools; +use span::EditionedFileId; +use syntax::{ + ast::{self, AstNode, HasGenericParams, HasName}, + SyntaxKind, SyntaxToken, +}; +use syntax::{format_smolstr, SmolStr}; + +use crate::{ + inlay_hints::InlayHintCtx, InlayHint, InlayHintPosition, InlayHintsConfig, InlayKind, + LifetimeElisionHints, +}; + +pub(super) fn fn_hints( + acc: &mut Vec, + ctx: &mut InlayHintCtx, + fd: &FamousDefs<'_, '_>, + config: &InlayHintsConfig, + file_id: EditionedFileId, + func: ast::Fn, +) -> Option<()> { + if config.lifetime_elision_hints == LifetimeElisionHints::Never { + return None; + } + + 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 gpl_append_range = func.name()?.syntax().text_range(); + hints_( + acc, + ctx, + fd, + config, + file_id, + param_list, + generic_param_list, + ret_type, + self_param, + |acc, allocated_lifetimes| { + acc.push(InlayHint { + range: gpl_append_range, + kind: InlayKind::GenericParamList, + label: format!("<{}>", allocated_lifetimes.iter().format(", "),).into(), + text_edit: None, + position: InlayHintPosition::After, + pad_left: false, + pad_right: false, + resolve_parent: None, + }) + }, + true, + ) +} + +pub(super) fn fn_ptr_hints( + acc: &mut Vec, + ctx: &mut InlayHintCtx, + fd: &FamousDefs<'_, '_>, + config: &InlayHintsConfig, + file_id: EditionedFileId, + func: ast::FnPtrType, +) -> Option<()> { + if config.lifetime_elision_hints == LifetimeElisionHints::Never { + return None; + } + + let parent_for_type = func + .syntax() + .ancestors() + .skip(1) + .take_while(|it| matches!(it.kind(), SyntaxKind::PAREN_TYPE | SyntaxKind::FOR_TYPE)) + .find_map(ast::ForType::cast); + + let param_list = func.param_list()?; + let generic_param_list = parent_for_type.as_ref().and_then(|it| it.generic_param_list()); + let ret_type = func.ret_type(); + let for_kw = parent_for_type.as_ref().and_then(|it| it.for_token()); + hints_( + acc, + ctx, + fd, + config, + file_id, + param_list, + generic_param_list, + ret_type, + None, + |acc, allocated_lifetimes| { + let has_for = for_kw.is_some(); + let for_ = if has_for { "" } else { "for" }; + acc.push(InlayHint { + range: for_kw.map_or_else( + || func.syntax().first_token().unwrap().text_range(), + |it| it.text_range(), + ), + kind: InlayKind::GenericParamList, + label: format!("{for_}<{}>", allocated_lifetimes.iter().format(", "),).into(), + text_edit: None, + position: if has_for { + InlayHintPosition::After + } else { + InlayHintPosition::Before + }, + pad_left: false, + pad_right: true, + resolve_parent: None, + }); + }, + false, + ) +} + +pub(super) fn fn_path_hints( + acc: &mut Vec, + ctx: &mut InlayHintCtx, + fd: &FamousDefs<'_, '_>, + config: &InlayHintsConfig, + file_id: EditionedFileId, + func: ast::PathType, +) -> Option<()> { + if config.lifetime_elision_hints == LifetimeElisionHints::Never { + return None; + } + + // FIXME: Support general path types + let (param_list, ret_type) = func.path().as_ref().and_then(path_as_fn)?; + let parent_for_type = func + .syntax() + .ancestors() + .skip(1) + .take_while(|it| matches!(it.kind(), SyntaxKind::PAREN_TYPE | SyntaxKind::FOR_TYPE)) + .find_map(ast::ForType::cast); + + let generic_param_list = parent_for_type.as_ref().and_then(|it| it.generic_param_list()); + let for_kw = parent_for_type.as_ref().and_then(|it| it.for_token()); + hints_( + acc, + ctx, + fd, + config, + file_id, + param_list, + generic_param_list, + ret_type, + None, + |acc, allocated_lifetimes| { + let has_for = for_kw.is_some(); + let for_ = if has_for { "" } else { "for" }; + acc.push(InlayHint { + range: for_kw.map_or_else( + || func.syntax().first_token().unwrap().text_range(), + |it| it.text_range(), + ), + kind: InlayKind::GenericParamList, + label: format!("{for_}<{}>", allocated_lifetimes.iter().format(", "),).into(), + text_edit: None, + position: if has_for { + InlayHintPosition::After + } else { + InlayHintPosition::Before + }, + pad_left: false, + pad_right: true, + resolve_parent: None, + }); + }, + false, + ) +} + +fn path_as_fn(path: &ast::Path) -> Option<(ast::ParamList, Option)> { + path.segment().and_then(|it| it.param_list().zip(Some(it.ret_type()))) +} + +fn hints_( + acc: &mut Vec, + ctx: &mut InlayHintCtx, + FamousDefs(_, _): &FamousDefs<'_, '_>, + config: &InlayHintsConfig, + _file_id: EditionedFileId, + param_list: ast::ParamList, + generic_param_list: Option, + ret_type: Option, + self_param: Option, + on_missing_gpl: impl FnOnce(&mut Vec, &[SmolStr]), + mut is_trivial: bool, +) -> Option<()> { + let is_elided = |lt: &Option| match lt { + Some(lt) => matches!(lt.text().as_str(), "'_"), + None => true, + }; + + let mk_lt_hint = |t: SyntaxToken, label: String| InlayHint { + range: t.text_range(), + kind: InlayKind::Lifetime, + label: label.into(), + text_edit: None, + position: InlayHintPosition::After, + pad_left: false, + pad_right: true, + resolve_parent: None, + }; + + 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().and_then(|it| match it { + ast::Pat::IdentPat(p) => p.name(), + _ => None, + }), + it.ty()?, + )) + }) + .for_each(|(name, 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((name.clone(), r.amp_token(), lifetime, is_elided)); + false + } + ast::Type::FnPtrType(_) => { + is_trivial = false; + true + } + ast::Type::PathType(t) => { + if t.path() + .and_then(|it| it.segment()) + .and_then(|it| it.param_list()) + .is_some() + { + is_trivial = false; + true + } else { + false + } + } + _ => false, + }) + }); + acc + }; + + let mut used_names: FxHashMap = + ctx.lifetime_stacks.iter().flat_map(|it| it.iter()).cloned().zip(iter::repeat(0)).collect(); + // 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_smolstr!("'{idx}"), + }); + let ctx = &*ctx; + move || { + gen.by_ref() + .find(|s| ctx.lifetime_stacks.iter().flat_map(|it| it.iter()).all(|n| n != s)) + .unwrap_or_default() + } + }; + let mut allocated_lifetimes = vec![]; + + { + let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided); + if self_param.is_some() && potential_lt_refs.next().is_some() { + 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; + format_smolstr!("'{}{c}", it.text().as_str()) + } else { + used_names.insert(it.text().as_str().into(), 0); + format_smolstr!("'{}", 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.first().cloned(), + "'static" => None, + name => Some(name.into()), + }, + None => allocated_lifetimes.first().cloned(), + } + } + [..] => None, + }; + + if allocated_lifetimes.is_empty() && output.is_none() { + return None; + } + + // apply hints + // apply output if required + 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())); + } + false + } + ast::Type::FnPtrType(_) => { + is_trivial = false; + true + } + ast::Type::PathType(t) => { + if t.path().and_then(|it| it.segment()).and_then(|it| it.param_list()).is_some() + { + is_trivial = false; + true + } else { + false + } + } + _ => false, + }) + } + } + + 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::Lifetime, + label: format!( + "{}{}", + allocated_lifetimes.iter().format(", "), + if is_empty { "" } else { ", " } + ) + .into(), + text_edit: None, + position: InlayHintPosition::After, + pad_left: false, + pad_right: true, + resolve_parent: None, + }); + } + (None, allocated_lifetimes) => on_missing_gpl(acc, allocated_lifetimes), + } + ctx.lifetime_stacks.last_mut().unwrap().extend(allocated_lifetimes); + Some(()) +} + +#[cfg(test)] +mod tests { + use crate::{ + inlay_hints::tests::{check, check_with_config, TEST_CONFIG}, + InlayHintsConfig, LifetimeElisionHints, + }; + + #[test] + fn hints_lifetimes() { + check( + r#" +fn empty() {} + +fn no_gpl(a: &()) {} + //^^^^^^<'0> + // ^'0 +fn empty_gpl<>(a: &()) {} + // ^'0 ^'0 +fn partial<'b>(a: &(), b: &'b ()) {} +// ^'0, $ ^'0 +fn partial<'a>(a: &'a (), b: &()) {} +// ^'0, $ ^'0 + +fn single_ret(a: &()) -> &() {} +// ^^^^^^^^^^<'0> + // ^'0 ^'0 +fn full_mul(a: &(), b: &()) {} +// ^^^^^^^^<'0, '1> + // ^'0 ^'1 + +fn foo<'c>(a: &'c ()) -> &() {} + // ^'c + +fn nested_in(a: & &X< &()>) {} +// ^^^^^^^^^<'0, '1, '2> + //^'0 ^'1 ^'2 +fn nested_out(a: &()) -> & &X< &()>{} +// ^^^^^^^^^^<'0> + //^'0 ^'0 ^'0 ^'0 + +impl () { + fn foo(&self) {} + // ^^^<'0> + // ^'0 + fn foo(&self) -> &() {} + // ^^^<'0> + // ^'0 ^'0 + fn foo(&self, a: &()) -> &() {} + // ^^^<'0, '1> + // ^'0 ^'1 ^'0 +} +"#, + ); + } + + #[test] + fn hints_lifetimes_named() { + check_with_config( + InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }, + r#" +fn nested_in<'named>(named: & &X< &()>) {} +// ^'named1, 'named2, 'named3, $ + //^'named1 ^'named2 ^'named3 +"#, + ); + } + + #[test] + fn hints_lifetimes_trivial_skip() { + check_with_config( + InlayHintsConfig { + lifetime_elision_hints: LifetimeElisionHints::SkipTrivial, + ..TEST_CONFIG + }, + r#" +fn no_gpl(a: &()) {} +fn empty_gpl<>(a: &()) {} +fn partial<'b>(a: &(), b: &'b ()) {} +fn partial<'a>(a: &'a (), b: &()) {} + +fn single_ret(a: &()) -> &() {} +// ^^^^^^^^^^<'0> + // ^'0 ^'0 +fn full_mul(a: &(), b: &()) {} + +fn foo<'c>(a: &'c ()) -> &() {} + // ^'c + +fn nested_in(a: & &X< &()>) {} +fn nested_out(a: &()) -> & &X< &()>{} +// ^^^^^^^^^^<'0> + //^'0 ^'0 ^'0 ^'0 + +impl () { + fn foo(&self) {} + fn foo(&self) -> &() {} + // ^^^<'0> + // ^'0 ^'0 + fn foo(&self, a: &()) -> &() {} + // ^^^<'0, '1> + // ^'0 ^'1 ^'0 +} +"#, + ); + } + + #[test] + fn no_collide() { + check_with_config( + InlayHintsConfig { + lifetime_elision_hints: LifetimeElisionHints::Always, + param_names_for_lifetime_elision_hints: true, + ..TEST_CONFIG + }, + r#" +impl<'foo> { + fn foo(foo: &()) {} + // ^^^ <'foo1> + // ^ 'foo1 +} +"#, + ); + } + + #[test] + fn hints_lifetimes_fn_ptr() { + check_with_config( + InlayHintsConfig { + lifetime_elision_hints: LifetimeElisionHints::Always, + ..TEST_CONFIG + }, + r#" +fn fn_ptr(a: fn(&()) -> &fn(&()) -> &()) {} + //^^ for<'0> + //^'0 + //^'0 + //^^ for<'1> + //^'1 + //^'1 +fn fn_ptr2(a: for<'a> fn(&()) -> &())) {} + //^'0, $ + //^'0 + //^'0 +fn fn_trait(a: &impl Fn(&()) -> &()) {} +// ^^^^^^^^<'0> + // ^'0 + // ^^ for<'1> + //^'1 + // ^'1 +"#, + ); + } +}