Unnamed repository; edit this file 'description' to name the repository.
Auto merge of #18010 - Veykril:inlay-hints-lt, r=Veykril
feat: Support fn-ptr and fn-path types for lifetime elision hints All still syntax based unfortunately but that won't change for quite a while
bors 2024-08-31
parent 64c538f · parent 1607797 · commit 9fd7051
-rw-r--r--crates/ide/src/inlay_hints.rs69
-rw-r--r--crates/ide/src/inlay_hints/fn_lifetime_fn.rs338
-rw-r--r--crates/ide/src/inlay_hints/lifetime.rs548
3 files changed, 597 insertions, 358 deletions
diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs
index 93dd56a450..be99510af2 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<Vec<SmolStr>>,
+ 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<InlayHint>,
- parent_impl: &mut Option<ast::Impl>,
+ 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 4d35e71a06..0000000000
--- 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<InlayHint>,
- 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<ast::Lifetime>| 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<SmolStr, usize> =
- 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 0000000000..de46367067
--- /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<InlayHint>,
+ 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<InlayHint>,
+ 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<InlayHint>,
+ 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<ast::RetType>)> {
+ path.segment().and_then(|it| it.param_list().zip(Some(it.ret_type())))
+}
+
+fn hints_(
+ acc: &mut Vec<InlayHint>,
+ ctx: &mut InlayHintCtx,
+ FamousDefs(_, _): &FamousDefs<'_, '_>,
+ config: &InlayHintsConfig,
+ _file_id: EditionedFileId,
+ param_list: ast::ParamList,
+ generic_param_list: Option<ast::GenericParamList>,
+ ret_type: Option<ast::RetType>,
+ self_param: Option<ast::SelfParam>,
+ on_missing_gpl: impl FnOnce(&mut Vec<InlayHint>, &[SmolStr]),
+ mut is_trivial: bool,
+) -> Option<()> {
+ let is_elided = |lt: &Option<ast::Lifetime>| 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<SmolStr, usize> =
+ 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
+"#,
+ );
+ }
+}