about summary refs log tree commit diff
diff options
context:
space:
mode:
authorLukas Wirth <lukastw97@gmail.com>2024-08-30 18:18:39 +0200
committerLukas Wirth <lukastw97@gmail.com>2024-08-30 19:36:18 +0200
commit3f602089dc09866d4bde2efe6e2a6304459b7f20 (patch)
treeeca138a9c9ccaf21da974f89c8ffecfc7dc61002
parent6b151031520f02724322882147e52d1a52368742 (diff)
downloadrust-3f602089dc09866d4bde2efe6e2a6304459b7f20.tar.gz
rust-3f602089dc09866d4bde2efe6e2a6304459b7f20.zip
Support fn-ptr and fn-path types for lifetime elision hints
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/inlay_hints.rs69
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/inlay_hints/fn_lifetime_fn.rs338
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/inlay_hints/lifetime.rs548
3 files changed, 597 insertions, 358 deletions
diff --git a/src/tools/rust-analyzer/crates/ide/src/inlay_hints.rs b/src/tools/rust-analyzer/crates/ide/src/inlay_hints.rs
index 93dd56a450d..be99510af2a 100644
--- a/src/tools/rust-analyzer/crates/ide/src/inlay_hints.rs
+++ b/src/tools/rust-analyzer/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/src/tools/rust-analyzer/crates/ide/src/inlay_hints/fn_lifetime_fn.rs b/src/tools/rust-analyzer/crates/ide/src/inlay_hints/fn_lifetime_fn.rs
deleted file mode 100644
index 4d35e71a06f..00000000000
--- a/src/tools/rust-analyzer/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/src/tools/rust-analyzer/crates/ide/src/inlay_hints/lifetime.rs b/src/tools/rust-analyzer/crates/ide/src/inlay_hints/lifetime.rs
new file mode 100644
index 00000000000..de463670677
--- /dev/null
+++ b/src/tools/rust-analyzer/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
+"#,
+        );
+    }
+}