Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide/src/inlay_hints.rs')
| -rw-r--r-- | crates/ide/src/inlay_hints.rs | 379 |
1 files changed, 336 insertions, 43 deletions
diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs index d04bd87b7b..13e7a0ac88 100644 --- a/crates/ide/src/inlay_hints.rs +++ b/crates/ide/src/inlay_hints.rs @@ -1,10 +1,13 @@ use either::Either; use hir::{known, Callable, HasVisibility, HirDisplay, Semantics, TypeInfo}; -use ide_db::{base_db::FileRange, famous_defs::FamousDefs, RootDatabase}; +use ide_db::{ + base_db::FileRange, famous_defs::FamousDefs, syntax_helpers::node_ext::walk_ty, RootDatabase, +}; use itertools::Itertools; +use rustc_hash::FxHashMap; use stdx::to_lower_snake_case; use syntax::{ - ast::{self, AstNode, HasArgList, HasName, UnaryOp}, + ast::{self, AstNode, HasArgList, HasGenericParams, HasName, UnaryOp}, match_ast, Direction, NodeOrToken, SmolStr, SyntaxKind, SyntaxNode, TextRange, T, }; @@ -17,16 +20,27 @@ pub struct InlayHintsConfig { pub parameter_hints: bool, pub chaining_hints: bool, pub closure_return_type_hints: bool, + pub lifetime_elision_hints: LifetimeElisionHints, + pub param_names_for_lifetime_elision_hints: bool, pub hide_named_constructor_hints: bool, pub max_length: Option<usize>, } #[derive(Clone, Debug, PartialEq, Eq)] +pub enum LifetimeElisionHints { + Always, + SkipTrivial, + Never, +} + +#[derive(Clone, Debug, PartialEq, Eq)] pub enum InlayKind { TypeHint, ParameterHint, ClosureReturnTypeHint, ChainingHint, + GenericParamListHint, + LifetimeHint, } #[derive(Debug)] @@ -41,12 +55,17 @@ pub struct InlayHint { // rust-analyzer shows additional information inline with the source code. // Editors usually render this using read-only virtual text snippets interspersed with code. // -// rust-analyzer shows hints for +// rust-analyzer by default shows hints for // // * types of local variables // * names of function arguments // * types of chained expressions // +// Optionally, one can enable additional hints for +// +// * return types of closure expressions with blocks +// * elided lifetimes +// // **Note:** VS Code does not have native support for inlay hints https://github.com/microsoft/vscode/issues/16221[yet] and the hints are implemented using decorations. // This approach has limitations, the caret movement and bracket highlighting near the edges of the hint may be weird: // https://github.com/rust-analyzer/rust-analyzer/issues/1623[1], https://github.com/rust-analyzer/rust-analyzer/issues/3453[2]. @@ -108,9 +127,210 @@ fn hints( } _ => (), } - } else if let Some(it) = ast::IdentPat::cast(node) { + } else if let Some(it) = ast::IdentPat::cast(node.clone()) { bind_pat_hints(hints, sema, config, &it); + } else if let Some(it) = ast::Fn::cast(node) { + lifetime_hints(hints, config, it); + } +} + +fn lifetime_hints( + acc: &mut Vec<InlayHint>, + config: &InlayHintsConfig, + 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 mut used_names: FxHashMap<SmolStr, usize> = generic_param_list + .iter() + .filter(|_| config.param_names_for_lifetime_elision_hints) + .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(); + + let mut allocated_lifetimes = vec![]; + 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 potential_lt_refs: Vec<_> = vec![]; + param_list + .params() + .filter_map(|it| { + Some(( + config.param_names_for_lifetime_elision_hints.then(|| it.pat()).flatten(), + it.ty()?, + )) + }) + .for_each(|(pat, ty)| { + // FIXME: check path types + walk_ty(&ty, &mut |ty| match ty { + ast::Type::RefType(r) => potential_lt_refs.push(( + pat.as_ref().and_then(|it| match it { + ast::Pat::IdentPat(p) => p.name(), + _ => None, + }), + r, + )), + _ => (), + }) + }); + + enum LifetimeKind { + Elided, + Named(SmolStr), + Static, + } + + let fetch_lt_text = |lt: Option<ast::Lifetime>| match lt { + Some(lt) => match lt.text().as_str() { + "'_" => LifetimeKind::Elided, + "'static" => LifetimeKind::Static, + name => LifetimeKind::Named(name.into()), + }, + None => LifetimeKind::Elided, + }; + let is_elided = |lt: Option<ast::Lifetime>| match lt { + Some(lt) => matches!(lt.text().as_str(), "'_"), + None => true, + }; + + // allocate names + if let Some(self_param) = &self_param { + if is_elided(self_param.lifetime()) { + 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.iter().for_each(|(name, it)| { + if is_elided(it.lifetime()) { + let name = match name { + Some(it) => { + 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 = if let Some(self_param) = &self_param { + match fetch_lt_text(self_param.lifetime()) { + LifetimeKind::Elided => allocated_lifetimes.get(0).cloned(), + LifetimeKind::Named(name) => Some(name), + LifetimeKind::Static => None, + } + } else { + match potential_lt_refs.as_slice() { + [(_, r)] => match fetch_lt_text(r.lifetime()) { + LifetimeKind::Elided => allocated_lifetimes.get(0).cloned(), + LifetimeKind::Named(name) => Some(name), + LifetimeKind::Static => None, + }, + [..] => 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(InlayHint { + range: amp.text_range(), + kind: InlayKind::LifetimeHint, + label: output_lt.clone(), + }); + } + } + _ => (), + }) + } + } + + if config.lifetime_elision_hints == LifetimeElisionHints::SkipTrivial && is_trivial { + return None; + } + + let mut idx = match &self_param { + Some(self_param) if is_elided(self_param.lifetime()) => { + if let Some(amp) = self_param.amp_token() { + let lt = allocated_lifetimes[0].clone(); + acc.push(InlayHint { + range: amp.text_range(), + kind: InlayKind::LifetimeHint, + label: lt, + }); + } + 1 + } + _ => 0, + }; + + for (_, p) in potential_lt_refs.iter() { + if is_elided(p.lifetime()) { + let t = p.amp_token()?; + let lt = allocated_lifetimes[idx].clone(); + acc.push(InlayHint { range: t.text_range(), kind: InlayKind::LifetimeHint, label: lt }); + idx += 1; + } + } + + // 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::GenericParamListHint, + label: format!( + "{}{}", + allocated_lifetimes.iter().format(", "), + if is_empty { "" } else { ", " } + ) + .into(), + }); + } + (None, allocated_lifetimes) => acc.push(InlayHint { + range: func.name()?.syntax().text_range(), + kind: InlayKind::GenericParamListHint, + label: format!("<{}>", allocated_lifetimes.iter().format(", "),).into(), + }), } + Some(()) } fn closure_ret_hints( @@ -600,18 +820,21 @@ fn get_callable( mod tests { use expect_test::{expect, Expect}; use ide_db::base_db::FileRange; + use itertools::Itertools; use syntax::{TextRange, TextSize}; use test_utils::extract_annotations; - use crate::{fixture, inlay_hints::InlayHintsConfig}; + use crate::{fixture, inlay_hints::InlayHintsConfig, LifetimeElisionHints}; const DISABLED_CONFIG: InlayHintsConfig = InlayHintsConfig { render_colons: false, type_hints: false, parameter_hints: false, chaining_hints: false, + lifetime_elision_hints: LifetimeElisionHints::Never, hide_named_constructor_hints: false, closure_return_type_hints: false, + param_names_for_lifetime_elision_hints: false, max_length: None, }; const TEST_CONFIG: InlayHintsConfig = InlayHintsConfig { @@ -619,6 +842,7 @@ mod tests { parameter_hints: true, chaining_hints: true, closure_return_type_hints: true, + lifetime_elision_hints: LifetimeElisionHints::Always, ..DISABLED_CONFIG }; @@ -648,10 +872,15 @@ mod tests { #[track_caller] fn check_with_config(config: InlayHintsConfig, ra_fixture: &str) { let (analysis, file_id) = fixture::file(ra_fixture); - let expected = extract_annotations(&*analysis.file_text(file_id).unwrap()); + let mut expected = extract_annotations(&*analysis.file_text(file_id).unwrap()); let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap(); - let actual = - inlay_hints.into_iter().map(|it| (it.range, it.label.to_string())).collect::<Vec<_>>(); + let actual = inlay_hints + .into_iter() + .map(|it| (it.range, it.label.to_string())) + .sorted_by_key(|(range, _)| range.start()) + .collect::<Vec<_>>(); + expected.sort_by_key(|(range, _)| range.start()); + assert_eq!(expected, actual, "\nExpected:\n{:#?}\n\nActual:\n{:#?}", expected, actual); } @@ -1290,41 +1519,6 @@ fn main() { } #[test] - fn incomplete_for_no_hint() { - check_types( - r#" -fn main() { - let data = &[1i32, 2, 3]; - //^^^^ &[i32; 3] - for i -}"#, - ); - check( - r#" -pub struct Vec<T> {} - -impl<T> Vec<T> { - pub fn new() -> Self { Vec {} } - pub fn push(&mut self, t: T) {} -} - -impl<T> IntoIterator for Vec<T> { - type Item=T; -} - -fn main() { - let mut data = Vec::new(); - //^^^^ Vec<&str> - data.push("foo"); - for i in - - println!("Unit expr"); -} -"#, - ); - } - - #[test] fn complete_for_hint() { check_types( r#" @@ -1825,4 +2019,103 @@ fn main() { "#]], ); } + + #[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 +} +"#, + ); + } } |