diff --git a/crates/ide-assists/src/handlers/add_lifetime_to_type.rs b/crates/ide-assists/src/handlers/add_lifetime_to_type.rs deleted file mode 100644 index 27dbdcf2c4d5..000000000000 --- a/crates/ide-assists/src/handlers/add_lifetime_to_type.rs +++ /dev/null @@ -1,216 +0,0 @@ -use syntax::ast::{self, AstNode, HasGenericParams, HasName}; - -use crate::{AssistContext, AssistId, Assists}; - -// Assist: add_lifetime_to_type -// -// Adds a new lifetime to a struct, enum or union. -// -// ``` -// struct Point { -// x: &$0u32, -// y: u32, -// } -// ``` -// -> -// ``` -// struct Point<'a> { -// x: &'a u32, -// y: u32, -// } -// ``` -pub(crate) fn add_lifetime_to_type(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { - let ref_type_focused = ctx.find_node_at_offset::()?; - if ref_type_focused.lifetime().is_some() { - return None; - } - - let node = ctx.find_node_at_offset::()?; - let has_lifetime = node - .generic_param_list() - .is_some_and(|gen_list| gen_list.lifetime_params().next().is_some()); - - if has_lifetime { - return None; - } - - let ref_types = fetch_borrowed_types(&node)?; - let target = node.syntax().text_range(); - - acc.add(AssistId::generate("add_lifetime_to_type"), "Add lifetime", target, |builder| { - match node.generic_param_list() { - Some(gen_param) => { - if let Some(left_angle) = gen_param.l_angle_token() { - builder.insert(left_angle.text_range().end(), "'a, "); - } - } - None => { - if let Some(name) = node.name() { - builder.insert(name.syntax().text_range().end(), "<'a>"); - } - } - } - - for ref_type in ref_types { - if let Some(amp_token) = ref_type.amp_token() { - builder.insert(amp_token.text_range().end(), "'a "); - } - } - }) -} - -fn fetch_borrowed_types(node: &ast::Adt) -> Option> { - let ref_types: Vec = match node { - ast::Adt::Enum(enum_) => { - let variant_list = enum_.variant_list()?; - variant_list - .variants() - .filter_map(|variant| { - let field_list = variant.field_list()?; - - find_ref_types_from_field_list(&field_list) - }) - .flatten() - .collect() - } - ast::Adt::Struct(strukt) => { - let field_list = strukt.field_list()?; - find_ref_types_from_field_list(&field_list)? - } - ast::Adt::Union(un) => { - let record_field_list = un.record_field_list()?; - record_field_list - .fields() - .filter_map(|r_field| { - if let ast::Type::RefType(ref_type) = r_field.ty()? - && ref_type.lifetime().is_none() - { - return Some(ref_type); - } - - None - }) - .collect() - } - }; - - if ref_types.is_empty() { None } else { Some(ref_types) } -} - -fn find_ref_types_from_field_list(field_list: &ast::FieldList) -> Option> { - let ref_types: Vec = match field_list { - ast::FieldList::RecordFieldList(record_list) => record_list - .fields() - .filter_map(|f| { - if let ast::Type::RefType(ref_type) = f.ty()? - && ref_type.lifetime().is_none() - { - return Some(ref_type); - } - - None - }) - .collect(), - ast::FieldList::TupleFieldList(tuple_field_list) => tuple_field_list - .fields() - .filter_map(|f| { - if let ast::Type::RefType(ref_type) = f.ty()? - && ref_type.lifetime().is_none() - { - return Some(ref_type); - } - - None - }) - .collect(), - }; - - if ref_types.is_empty() { None } else { Some(ref_types) } -} - -#[cfg(test)] -mod tests { - use crate::tests::{check_assist, check_assist_not_applicable}; - - use super::*; - - #[test] - fn add_lifetime_to_struct() { - check_assist( - add_lifetime_to_type, - r#"struct Foo { a: &$0i32 }"#, - r#"struct Foo<'a> { a: &'a i32 }"#, - ); - - check_assist( - add_lifetime_to_type, - r#"struct Foo { a: &$0i32, b: &usize }"#, - r#"struct Foo<'a> { a: &'a i32, b: &'a usize }"#, - ); - - check_assist( - add_lifetime_to_type, - r#"struct Foo { a: &$0i32, b: usize }"#, - r#"struct Foo<'a> { a: &'a i32, b: usize }"#, - ); - - check_assist( - add_lifetime_to_type, - r#"struct Foo { a: &$0T, b: usize }"#, - r#"struct Foo<'a, T> { a: &'a T, b: usize }"#, - ); - - check_assist_not_applicable(add_lifetime_to_type, r#"struct Foo<'a> { a: &$0'a i32 }"#); - check_assist_not_applicable(add_lifetime_to_type, r#"struct Foo { a: &'a$0 i32 }"#); - } - - #[test] - fn add_lifetime_to_enum() { - check_assist( - add_lifetime_to_type, - r#"enum Foo { Bar { a: i32 }, Other, Tuple(u32, &$0u32)}"#, - r#"enum Foo<'a> { Bar { a: i32 }, Other, Tuple(u32, &'a u32)}"#, - ); - - check_assist( - add_lifetime_to_type, - r#"enum Foo { Bar { a: &$0i32 }}"#, - r#"enum Foo<'a> { Bar { a: &'a i32 }}"#, - ); - - check_assist( - add_lifetime_to_type, - r#"enum Foo { Bar { a: &$0i32, b: &T }}"#, - r#"enum Foo<'a, T> { Bar { a: &'a i32, b: &'a T }}"#, - ); - - check_assist_not_applicable( - add_lifetime_to_type, - r#"enum Foo<'a> { Bar { a: &$0'a i32 }}"#, - ); - check_assist_not_applicable(add_lifetime_to_type, r#"enum Foo { Bar, $0Misc }"#); - } - - #[test] - fn add_lifetime_to_union() { - check_assist( - add_lifetime_to_type, - r#"union Foo { a: &$0i32 }"#, - r#"union Foo<'a> { a: &'a i32 }"#, - ); - - check_assist( - add_lifetime_to_type, - r#"union Foo { a: &$0i32, b: &usize }"#, - r#"union Foo<'a> { a: &'a i32, b: &'a usize }"#, - ); - - check_assist( - add_lifetime_to_type, - r#"union Foo { a: &$0T, b: usize }"#, - r#"union Foo<'a, T> { a: &'a T, b: usize }"#, - ); - - check_assist_not_applicable(add_lifetime_to_type, r#"struct Foo<'a> { a: &'a $0i32 }"#); - } -} diff --git a/crates/ide-assists/src/handlers/add_missing_lifetime.rs b/crates/ide-assists/src/handlers/add_missing_lifetime.rs new file mode 100644 index 000000000000..e3079f29146f --- /dev/null +++ b/crates/ide-assists/src/handlers/add_missing_lifetime.rs @@ -0,0 +1,528 @@ +use ide_db::{FxIndexSet, syntax_helpers::suggest_name::NameGenerator}; +use itertools::chain; +use syntax::{ + NodeOrToken, SmolStr, T, + ast::{ + self, AstNode, HasGenericParams, HasName, + make::{self, tokens}, + syntax_factory::SyntaxFactory, + }, + syntax_editor::{Position, SyntaxAnnotation, SyntaxEditor}, +}; + +use crate::{AssistContext, AssistId, Assists}; + +// Assist: add_missing_lifetime +// +// Adds missing lifetimes to a struct, enum or union. +// +// ``` +// struct $0Foo { +// x: &'a i32, +// y: &T +// } +// ``` +// -> +// ``` +// struct Foo<'a, ${0:'b}, T> { +// x: &'a i32, +// y: &${0:'b} T +// } +// ``` + +pub(crate) fn add_missing_lifetime(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { + let node = ctx.find_node_at_offset::()?; + let all_inner_refs = fetch_all_refs(&node)?; + let (refs_without_lifetime, refs_with_lifetime): (Vec<_>, Vec<_>) = + all_inner_refs.into_iter().partition(|ref_type| ref_type.lifetime().is_none()); + + let adt_declared_lifetimes: FxIndexSet = node + .generic_param_list() + .map(|gen_list| { + gen_list + .lifetime_params() + .filter_map(|lt| lt.lifetime()) + .map(|lt| lt.text().into()) + .collect() + }) + .unwrap_or_default(); + + let adt_undeclared_lifetimes: FxIndexSet = refs_with_lifetime + .iter() + .filter_map(|ref_type| ref_type.lifetime()) + .map(|lt| lt.text().into()) + .filter(|lt_text| !adt_declared_lifetimes.contains(lt_text)) + .collect(); + + let has_refs_without_lifetime = !refs_without_lifetime.is_empty(); + let has_undeclared_lifetimes = !adt_undeclared_lifetimes.is_empty(); + + if !has_refs_without_lifetime && !has_undeclared_lifetimes { + return None; + } + + let all_existing_lifetimes: Vec = + adt_declared_lifetimes.iter().chain(adt_undeclared_lifetimes.iter()).cloned().collect(); + + add_and_declare_lifetimes( + acc, + ctx, + &node, + adt_undeclared_lifetimes, + refs_without_lifetime, + all_existing_lifetimes, + has_refs_without_lifetime, + has_undeclared_lifetimes, + ) +} + +fn add_and_declare_lifetimes( + acc: &mut Assists, + ctx: &AssistContext<'_>, + node: &ast::Adt, + adt_undeclared_lifetimes: FxIndexSet, + refs_without_lifetime: Vec, + all_existing_lifetimes: Vec, + has_refs_without_lifetime: bool, + has_undeclared_lifetimes: bool, +) -> Option<()> { + let message = match (has_refs_without_lifetime, has_undeclared_lifetimes) { + (false, true) => "Declare used lifetimes in generic parameters", + (true, false) | (true, true) => "Add missing lifetimes", + _ => return None, + }; + + let mut name_gen = + NameGenerator::new_with_names(all_existing_lifetimes.iter().map(|s| s.as_str())); + let new_lifetime_name = + if has_refs_without_lifetime { name_gen.for_lifetime() } else { SmolStr::default() }; + + acc.add( + AssistId::quick_fix("add_missing_lifetime"), + message, + node.syntax().text_range(), + |builder| { + let make = SyntaxFactory::with_mappings(); + let mut editor = builder.make_editor(node.syntax()); + let comma_and_space = || [make::token(T![,]).into(), tokens::single_space().into()]; + + let mut lifetime_elements = vec![]; + let mut new_lifetime_to_annotate = None; + + if has_undeclared_lifetimes { + for (i, lifetime_text) in adt_undeclared_lifetimes.iter().enumerate() { + (i > 0).then(|| lifetime_elements.extend(comma_and_space())); + lifetime_elements.push(make.lifetime(lifetime_text).syntax().clone().into()); + } + } + + if has_refs_without_lifetime { + has_undeclared_lifetimes.then(|| lifetime_elements.extend(comma_and_space())); + let lifetime = make.lifetime(&new_lifetime_name); + lifetime_elements.push(lifetime.syntax().clone().into()); + new_lifetime_to_annotate = Some(lifetime); + } + + if let Some(gen_param) = node.generic_param_list() { + if let Some(last_lifetime) = gen_param.lifetime_params().last() { + editor.insert_all( + Position::after(last_lifetime.syntax()), + chain!(comma_and_space(), lifetime_elements).collect(), + ); + } else if let Some(l_angle) = gen_param.l_angle_token() { + lifetime_elements.push(make::token(T![,]).into()); + lifetime_elements.push(tokens::single_space().into()); + editor.insert_all(Position::after(&l_angle), lifetime_elements); + } + } else if let Some(name) = node.name() { + editor.insert_all( + Position::after(name.syntax()), + chain!( + [make::token(T![<]).into()], + lifetime_elements, + [make::token(T![>]).into()] + ) + .collect(), + ); + } + + let snippet = ctx.config.snippet_cap.map(|cap| builder.make_placeholder_snippet(cap)); + + if let Some(lifetime) = new_lifetime_to_annotate + && let Some(snippet) = snippet + { + editor.add_annotation(lifetime.syntax(), snippet); + } + + if has_refs_without_lifetime { + add_lifetime_to_refs( + refs_without_lifetime, + &new_lifetime_name, + &mut editor, + &make, + snippet, + ); + } + + editor.add_mappings(make.finish_with_mappings()); + builder.add_file_edits(ctx.vfs_file_id(), editor); + has_refs_without_lifetime.then(|| builder.rename()); + }, + ) +} + +fn fetch_all_refs(node: &ast::Adt) -> Option> { + let ref_types: Vec = match node { + ast::Adt::Enum(enum_) => enum_ + .variant_list()? + .variants() + .filter_map(|variant| find_all_ref_types_from_field_list(&variant.field_list()?)) + .flatten() + .collect(), + ast::Adt::Struct(strukt) => find_all_ref_types_from_field_list(&strukt.field_list()?)?, + ast::Adt::Union(union) => union + .record_field_list()? + .fields() + .filter_map(|r_field| { + let ast::Type::RefType(ref_type) = r_field.ty()? else { return None }; + Some(ref_type) + }) + .collect(), + }; + + (!ref_types.is_empty()).then_some(ref_types) +} + +fn find_all_ref_types_from_field_list(field_list: &ast::FieldList) -> Option> { + let ref_types: Vec = match field_list { + ast::FieldList::RecordFieldList(record_list) => record_list + .fields() + .filter_map(|f| { + let ast::Type::RefType(ref_type) = f.ty()? else { return None }; + Some(ref_type) + }) + .collect(), + ast::FieldList::TupleFieldList(tuple_field_list) => tuple_field_list + .fields() + .filter_map(|f| { + let ast::Type::RefType(ref_type) = f.ty()? else { return None }; + Some(ref_type) + }) + .collect(), + }; + + (!ref_types.is_empty()).then_some(ref_types) +} + +fn add_lifetime_to_refs( + refs_without_lifetime: Vec, + lifetime_text: &str, + editor: &mut SyntaxEditor, + make: &SyntaxFactory, + snippet: Option, +) { + for r#ref in refs_without_lifetime { + let Some(amp_token) = r#ref.amp_token() else { continue }; + let lifetime = make.lifetime(lifetime_text); + let node_or_token = &NodeOrToken::Token(amp_token); + let elements = vec![lifetime.syntax().clone().into(), tokens::single_space().into()]; + editor.insert_all(Position::after(node_or_token), elements); + if let Some(snippet) = snippet { + editor.add_annotation(lifetime.syntax(), snippet); + }; + } +} + +#[cfg(test)] +mod tests { + use crate::tests::{check_assist, check_assist_not_applicable}; + + use super::*; + + #[test] + fn struct_lifetime() { + check_assist( + add_missing_lifetime, + r#" +struct $0Foo { + x: &'a i32, + y: &'b u32 +}"#, + r#" +struct Foo<'a, 'b> { + x: &'a i32, + y: &'b u32 +}"#, + ); + check_assist( + add_missing_lifetime, + r#" +struct $0Foo { + x: &'a T +}"#, + r#" +struct Foo<'a, T> { + x: &'a T +}"#, + ); + check_assist( + add_missing_lifetime, + r#" +struct Foo { + a: &$0T, + b: usize +}"#, + r#" +struct Foo<${0:'a}, T> { + a: &${0:'a} T, + b: usize +}"#, + ); + check_assist( + add_missing_lifetime, + r#" +struct Foo<'a> { + x: &'a i32, + y: &$0u32 +}"#, + r#" +struct Foo<'a, ${0:'b}> { + x: &'a i32, + y: &${0:'b} u32 +}"#, + ); + check_assist( + add_missing_lifetime, + r#" +struct $0Foo { + x: &'a i32, + y: &u32 +}"#, + r#" +struct Foo<'a, ${0:'b}> { + x: &'a i32, + y: &${0:'b} u32 +}"#, + ); + check_assist( + add_missing_lifetime, + r#" +struct $0Foo { + x: &'a i32, + y: &T + z: &'b u32 +}"#, + r#" +struct Foo<'a, 'b, ${0:'c}, T> { + x: &'a i32, + y: &${0:'c} T + z: &'b u32 +}"#, + ); + check_assist( + add_missing_lifetime, + r#" +struct $0Foo(&fn(&str) -> &str);"#, + r#" +struct Foo<${0:'a}>(&${0:'a} fn(&str) -> &str);"#, + ); + } + + #[test] + fn enum_lifetime() { + check_assist( + add_missing_lifetime, + r#" +enum $0Foo { + Bar { x: &'a i32 } +}"#, + r#" +enum Foo<'a> { + Bar { x: &'a i32 } +}"#, + ); + check_assist( + add_missing_lifetime, + r#" +enum $0Foo { + Bar { + x: &'a i32, + y: &'b T + } +}"#, + r#" +enum Foo<'a, 'b, T> { + Bar { + x: &'a i32, + y: &'b T + } +}"#, + ); + check_assist( + add_missing_lifetime, + r#" +enum Foo { + Bar { a: &$0i32 } +}"#, + r#" +enum Foo<${0:'a}> { + Bar { a: &${0:'a} i32 } +}"#, + ); + check_assist( + add_missing_lifetime, + r#" +enum Foo { + Bar { a: T }, + Other, + Tuple(u32, &$0u32) +}"#, + r#" +enum Foo<${0:'a}, T> { + Bar { a: T }, + Other, + Tuple(u32, &${0:'a} u32) +}"#, + ); + check_assist( + add_missing_lifetime, + r#" +enum Foo { + Bar { + a: &$0i32, + b: &T + } +}"#, + r#" +enum Foo<${0:'a}, T> { + Bar { + a: &${0:'a} i32, + b: &${0:'a} T + } +}"#, + ); + check_assist( + add_missing_lifetime, + r#" +enum Foo<'a> { + Bar { + x: &'a i32, + y: &$0u32 + } +}"#, + r#" +enum Foo<'a, ${0:'b}> { + Bar { + x: &'a i32, + y: &${0:'b} u32 + } +}"#, + ); + check_assist( + add_missing_lifetime, + r#" +enum $0Foo { + Bar { x: &'a i32 }, + Baz(&u32) +}"#, + r#" +enum Foo<'a, ${0:'b}> { + Bar { x: &'a i32 }, + Baz(&${0:'b} u32) +}"#, + ); + } + + #[test] + fn union_lifetime() { + check_assist( + add_missing_lifetime, + r#" +union $0Foo { + x: &'a i32 +}"#, + r#" +union Foo<'a> { + x: &'a i32 +}"#, + ); + check_assist( + add_missing_lifetime, + r#" +union $0Foo { + x: &'a T +}"#, + r#" +union Foo<'a, T> { + x: &'a T +}"#, + ); + check_assist( + add_missing_lifetime, + r#" +union Foo { + a: &$0i32, + b: &usize +}"#, + r#" +union Foo<${0:'a}> { + a: &${0:'a} i32, + b: &${0:'a} usize +}"#, + ); + check_assist( + add_missing_lifetime, + r#" +union Foo { + a: &$0T, + b: usize +}"#, + r#" +union Foo<${0:'a}, T> { + a: &${0:'a} T, + b: usize +}"#, + ); + check_assist( + add_missing_lifetime, + r#" +union Foo<'a> { + x: &'a i32, + y: &$0u32 +}"#, + r#" +union Foo<'a, ${0:'b}> { + x: &'a i32, + y: &${0:'b} u32 +}"#, + ); + check_assist( + add_missing_lifetime, + r#" +union $0Foo { + x: &'a T, + y: &i32 +}"#, + r#" +union Foo<'a, ${0:'b}, T> { + x: &'a T, + y: &${0:'b} i32 +}"#, + ); + } + + #[test] + fn not_applicable_when_all_correct() { + check_assist_not_applicable(add_missing_lifetime, r#"struct $0Foo<'a> { x: &'a i32 }"#); + check_assist_not_applicable( + add_missing_lifetime, + r#"struct $0Foo<'a, 'b> { x: &'a i32, y: &'b u32 }"#, + ); + check_assist_not_applicable( + add_missing_lifetime, + r#"enum $0Foo<'a> { Bar { x: &'a i32 } }"#, + ); + check_assist_not_applicable(add_missing_lifetime, r#"enum Foo { Bar, $0Misc }"#); + check_assist_not_applicable(add_missing_lifetime, r#"union Foo<'a> { a: &'a $0i32 }"#); + } +} diff --git a/crates/ide-assists/src/handlers/introduce_named_lifetime.rs b/crates/ide-assists/src/handlers/introduce_named_lifetime.rs index 264e3767a232..8123f45b051c 100644 --- a/crates/ide-assists/src/handlers/introduce_named_lifetime.rs +++ b/crates/ide-assists/src/handlers/introduce_named_lifetime.rs @@ -1,6 +1,6 @@ -use ide_db::FxHashSet; +use ide_db::syntax_helpers::suggest_name::NameGenerator; use syntax::{ - AstNode, TextRange, + AstNode, SmolStr, TextRange, ast::{self, HasGenericParams, edit_in_place::GenericParamsOwnerEdit, make}, ted::{self, Position}, }; @@ -57,7 +57,7 @@ fn generate_fn_def_assist( lifetime: ast::Lifetime, ) -> Option<()> { let param_list: ast::ParamList = fn_def.param_list()?; - let new_lifetime_param = generate_unique_lifetime_param_name(fn_def.generic_param_list())?; + let new_lifetime_param = generate_unique_lifetime_param_name(fn_def.generic_param_list()); let self_param = // use the self if it's a reference and has no explicit lifetime param_list.self_param().filter(|p| p.lifetime().is_none() && p.amp_token().is_some()); @@ -106,7 +106,7 @@ fn generate_impl_def_assist( lifetime_loc: TextRange, lifetime: ast::Lifetime, ) -> Option<()> { - let new_lifetime_param = generate_unique_lifetime_param_name(impl_def.generic_param_list())?; + let new_lifetime_param = generate_unique_lifetime_param_name(impl_def.generic_param_list()); acc.add(AssistId::refactor(ASSIST_NAME), ASSIST_LABEL, lifetime_loc, |builder| { let impl_def = builder.make_mut(impl_def); let lifetime = builder.make_mut(lifetime); @@ -122,16 +122,19 @@ fn generate_impl_def_assist( /// which is not in the list fn generate_unique_lifetime_param_name( existing_type_param_list: Option, -) -> Option { - match existing_type_param_list { - Some(type_params) => { - let used_lifetime_params: FxHashSet<_> = - type_params.lifetime_params().map(|p| p.syntax().text().to_string()).collect(); - ('a'..='z').map(|it| format!("'{it}")).find(|it| !used_lifetime_params.contains(it)) - } - None => Some("'a".to_owned()), - } - .map(|it| make::lifetime(&it)) +) -> ast::Lifetime { + let existing_lifetimes: Vec = existing_type_param_list + .map(|type_params| { + type_params + .lifetime_params() + .filter_map(|param| param.lifetime()) + .map(|lt| lt.text().into()) + .collect() + }) + .unwrap_or_default(); + + let mut name_gen = NameGenerator::new_with_names(existing_lifetimes.iter().map(|s| s.as_str())); + make::lifetime(&name_gen.for_lifetime()) } enum NeedsLifetime { diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs index 4b4aa9427955..be00ff509497 100644 --- a/crates/ide-assists/src/lib.rs +++ b/crates/ide-assists/src/lib.rs @@ -108,8 +108,8 @@ mod handlers { mod add_explicit_enum_discriminant; mod add_explicit_type; mod add_label_to_loop; - mod add_lifetime_to_type; mod add_missing_impl_members; + mod add_missing_lifetime; mod add_missing_match_arms; mod add_return_type; mod add_turbo_fish; @@ -244,7 +244,7 @@ mod handlers { add_explicit_enum_discriminant::add_explicit_enum_discriminant, add_explicit_type::add_explicit_type, add_label_to_loop::add_label_to_loop, - add_lifetime_to_type::add_lifetime_to_type, + add_missing_lifetime::add_missing_lifetime, add_missing_match_arms::add_missing_match_arms, add_return_type::add_return_type, add_turbo_fish::add_turbo_fish, diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs index 160b31af0ae9..700805f3a815 100644 --- a/crates/ide-assists/src/tests/generated.rs +++ b/crates/ide-assists/src/tests/generated.rs @@ -193,19 +193,19 @@ fn main() { } #[test] -fn doctest_add_lifetime_to_type() { +fn doctest_add_missing_lifetime() { check_doc_test( - "add_lifetime_to_type", + "add_missing_lifetime", r#####" -struct Point { - x: &$0u32, - y: u32, +struct $0Foo { + x: &'a i32, + y: &T } "#####, r#####" -struct Point<'a> { - x: &'a u32, - y: u32, +struct Foo<'a, ${0:'b}, T> { + x: &'a i32, + y: &${0:'b} T } "#####, ) diff --git a/crates/ide-db/src/syntax_helpers/suggest_name.rs b/crates/ide-db/src/syntax_helpers/suggest_name.rs index 1a0ef55a8b25..f2a2cc79c2dd 100644 --- a/crates/ide-db/src/syntax_helpers/suggest_name.rs +++ b/crates/ide-db/src/syntax_helpers/suggest_name.rs @@ -9,7 +9,7 @@ use stdx::to_lower_snake_case; use syntax::{ AstNode, Edition, SmolStr, SmolStrBuilder, ToSmolStr, ast::{self, HasName}, - match_ast, + format_smolstr, match_ast, }; use crate::RootDatabase; @@ -232,6 +232,40 @@ impl NameGenerator { self.suggest_name("var_name") } + /// Suggest a unique lifetime name following Rust conventions. + /// + /// Generates lifetime names in alphabetical order: `'a`, `'b`, `'c`, ..., `'z`. + /// This follows Rust's idiomatic lifetime naming conventions. + /// + /// # Examples + /// + /// ``` + /// # use ide_db::syntax_helpers::suggest_name::NameGenerator; + /// let mut gen = NameGenerator::default(); + /// assert_eq!(gen.for_lifetime(), "'a"); + /// assert_eq!(gen.for_lifetime(), "'b"); + /// assert_eq!(gen.for_lifetime(), "'c"); + /// ``` + /// + /// When initialized with existing lifetimes: + /// + /// ``` + /// # use ide_db::syntax_helpers::suggest_name::NameGenerator; + /// let mut gen = NameGenerator::new_with_names(["'a", "'c"].iter().copied()); + /// assert_eq!(gen.for_lifetime(), "'b"); + /// assert_eq!(gen.for_lifetime(), "'d"); + /// ``` + pub fn for_lifetime(&mut self) -> SmolStr { + for c in 'a'..='z' { + let candidate = format_smolstr!("'{c}"); + if !self.pool.contains_key(&candidate) { + self.pool.insert(candidate.clone(), 0); + return candidate; + } + } + self.suggest_name("'a") + } + /// Insert a name into the pool fn insert(&mut self, name: &str) { let (prefix, suffix) = Self::split_numeric_suffix(name); @@ -1142,4 +1176,40 @@ fn main() { assert_eq!(generator.suggest_name("c"), "c5"); } + + #[test] + fn for_lifetime_generates_alphabetical_names() { + let mut generator = NameGenerator::default(); + assert_eq!(generator.for_lifetime(), "'a"); + assert_eq!(generator.for_lifetime(), "'b"); + assert_eq!(generator.for_lifetime(), "'c"); + } + + #[test] + fn for_lifetime_avoids_existing_names() { + let mut generator = + NameGenerator::new_with_names(["'a", "'b", "'d", "'e", "'f"].into_iter()); + assert_eq!(generator.for_lifetime(), "'c"); + assert_eq!(generator.for_lifetime(), "'g"); + } + + #[test] + fn for_lifetime_exhaustive() { + let mut generator = NameGenerator::default(); + let mut lifetimes = Vec::new(); + for _ in 0..10 { + lifetimes.push(generator.for_lifetime()); + } + assert_eq!(lifetimes[0], "'a"); + assert_eq!(lifetimes[1], "'b"); + assert_eq!(lifetimes[9], "'j"); + } + + #[test] + fn for_lifetime_fallback_when_exhausted() { + let all_lifetimes: Vec<_> = ('a'..='z').map(|c| format!("'{c}")).collect(); + let mut generator = NameGenerator::new_with_names(all_lifetimes.iter().map(|s| s.as_str())); + let fallback = generator.for_lifetime(); + assert_eq!(fallback, "'a1"); + } }