From c2972b529bb44db797f7efe72619c1e03e772c65 Mon Sep 17 00:00:00 2001 From: Hegui Dai Date: Sun, 9 Nov 2025 19:08:58 +0800 Subject: [PATCH 1/9] rewrite --- .../src/handlers/add_lifetime_to_type.rs | 569 +++++++++++++++--- 1 file changed, 472 insertions(+), 97 deletions(-) diff --git a/crates/ide-assists/src/handlers/add_lifetime_to_type.rs b/crates/ide-assists/src/handlers/add_lifetime_to_type.rs index 27dbdcf2c4d5..af3377a3e6b0 100644 --- a/crates/ide-assists/src/handlers/add_lifetime_to_type.rs +++ b/crates/ide-assists/src/handlers/add_lifetime_to_type.rs @@ -1,4 +1,13 @@ -use syntax::ast::{self, AstNode, HasGenericParams, HasName}; +use ide_db::{FxIndexSet, source_change::SourceChangeBuilder}; +use syntax::{ + NodeOrToken, SyntaxElement, SyntaxNode, SyntaxToken, T, + ast::{ + self, AstNode, HasGenericParams, HasName, Lifetime, + make::{self, tokens}, + syntax_factory::SyntaxFactory, + }, + syntax_editor::{Position, SyntaxEditor}, +}; use crate::{AssistContext, AssistId, Assists}; @@ -14,118 +23,198 @@ use crate::{AssistContext, AssistId, Assists}; // ``` // -> // ``` -// struct Point<'a> { -// x: &'a u32, +// struct Point<${1:'l}> { +// x: &${0:'l} 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; - } +pub(crate) fn add_lifetime_to_type(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { let node = ctx.find_node_at_offset::()?; - let has_lifetime = node + 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() - .is_some_and(|gen_list| gen_list.lifetime_params().next().is_some()); + .map(|gen_list| { + gen_list + .lifetime_params() + .filter_map(|lt| lt.lifetime()) + .map(|lt| lt.text().to_string()) + .collect() + }) + .unwrap_or_default(); + + let adt_undeclared_lifetimes: FxIndexSet = refs_with_lifetime + .iter() + .filter_map(|ref_type| ref_type.lifetime()) + .map(|lt| lt.text().to_string()) + .filter(|lt_text| !adt_declared_lifetimes.contains(lt_text)) + .collect(); - if has_lifetime { + if refs_without_lifetime.is_empty() && adt_undeclared_lifetimes.is_empty() { return None; } - let ref_types = fetch_borrowed_types(&node)?; - let target = node.syntax().text_range(); + add_and_declare_lifetimes(acc, ctx, &node, adt_undeclared_lifetimes, refs_without_lifetime) +} + +fn add_and_declare_lifetimes( + acc: &mut Assists, + ctx: &AssistContext<'_>, + node: &ast::Adt, + adt_undeclared_lifetimes: FxIndexSet, + refs_without_lifetime: Vec, +) -> Option<()> { + let has_refs_without_lifetime = !refs_without_lifetime.is_empty(); + let has_undeclared_lifetimes = !adt_undeclared_lifetimes.is_empty(); + + let message = match (has_refs_without_lifetime, has_undeclared_lifetimes) { + (false, true) => "Declare used lifetimes in type definition", + (true, false) => "Add lifetime to type", + (true, true) => "Declare used lifetimes and add new lifetime", + _ => return None, + }; - 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, "); + acc.add( + AssistId::quick_fix("add_lifetime_to_type"), + 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.clone())); + let new_lifetime = make.lifetime(lifetime_text); + lifetime_elements.push(new_lifetime.syntax().clone().into()); } } - None => { - if let Some(name) = node.name() { - builder.insert(name.syntax().text_range().end(), "<'a>"); + + if has_refs_without_lifetime { + has_undeclared_lifetimes.then(|| lifetime_elements.extend(comma_and_space.clone())); + let lifetime = make.lifetime("'l"); + new_lifetime_to_annotate = Some(lifetime.clone()); + lifetime_elements.push(lifetime.syntax().clone().into()); + } + + if let Some(gen_param) = node.generic_param_list() + && let Some(left_angle) = gen_param.l_angle_token() + { + if !lifetime_elements.is_empty() { + lifetime_elements.push(make::token(T![,]).into()); + lifetime_elements.push(tokens::single_space().into()); } + editor.insert_all(Position::after(&left_angle), lifetime_elements); + } else if let Some(name) = node.name() + && !lifetime_elements.is_empty() + { + let mut final_elements = vec![make::token(T![<]).into()]; + final_elements.append(&mut lifetime_elements); + final_elements.push(make::token(T![>]).into()); + editor.insert_all(Position::after(name.syntax()), final_elements); + } + + if let Some(lifetime) = new_lifetime_to_annotate + && let Some(cap) = ctx.config.snippet_cap + { + editor.add_annotation(lifetime.syntax(), builder.make_placeholder_snippet(cap)); } - } - for ref_type in ref_types { - if let Some(amp_token) = ref_type.amp_token() { - builder.insert(amp_token.text_range().end(), "'a "); + if has_refs_without_lifetime { + add_lifetime_to_refs(refs_without_lifetime, "'l", ctx, &mut editor, builder, &make); } - } - }) + + 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_borrowed_types(node: &ast::Adt) -> Option> { +fn fetch_all_refs(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() - } + 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(), }; - if ref_types.is_empty() { None } else { Some(ref_types) } + (!ref_types.is_empty()).then_some(ref_types) } -fn find_ref_types_from_field_list(field_list: &ast::FieldList) -> Option> { +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| { - if let ast::Type::RefType(ref_type) = f.ty()? - && ref_type.lifetime().is_none() - { - return Some(ref_type); - } - - None + 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| { - if let ast::Type::RefType(ref_type) = f.ty()? - && ref_type.lifetime().is_none() - { - return Some(ref_type); - } - - None + let ast::Type::RefType(ref_type) = f.ty()? else { return None }; + Some(ref_type) }) .collect(), }; - if ref_types.is_empty() { None } else { Some(ref_types) } + (!ref_types.is_empty()).then_some(ref_types) +} + +fn add_lifetime_to_refs( + refs_without_lifetime: Vec, + lifetime_text: &str, + ctx: &AssistContext<'_>, + editor: &mut SyntaxEditor, + builder: &mut SourceChangeBuilder, + make: &SyntaxFactory, +) { + for r#ref in refs_without_lifetime { + let Some(amp_token) = r#ref.amp_token() else { continue }; + let lifetime = make.lifetime(lifetime_text); + insert_elements_after( + &NodeOrToken::Token(amp_token), + &lifetime, + vec![lifetime.syntax().clone().into(), tokens::single_space().into()], + ctx, + editor, + builder, + ); + } +} + +fn insert_elements_after( + node_or_token: &NodeOrToken, + lifetime: &Lifetime, + elements: Vec, + ctx: &AssistContext<'_>, + editor: &mut SyntaxEditor, + builder: &mut SourceChangeBuilder, +) { + editor.insert_all(Position::after(node_or_token), elements); + if let Some(cap) = ctx.config.snippet_cap { + editor.add_annotation(lifetime.syntax(), builder.make_placeholder_snippet(cap)); + } } #[cfg(test)] @@ -134,54 +223,170 @@ mod tests { use super::*; + #[test] + fn add_lifetime() { + check_assist( + add_lifetime_to_type, + r#" +struct Foo { + a: &$0i32, + b: &usize +}"#, + r#" +struct Foo<${1:'l}> { + a: &${2:'l} i32, + b: &${0:'l} usize +}"#, + ); + + check_assist( + add_lifetime_to_type, + r#" +enum Foo { + Bar { a: i32 }, + Other, + Tuple(u32, &$0u32) +}"#, + r#" +enum Foo<${1:'l}> { + Bar { a: i32 }, + Other, + Tuple(u32, &${0:'l} u32) +}"#, + ); + + check_assist( + add_lifetime_to_type, + r#" +union Foo { + a: &$0T, + b: usize +}"#, + r#" +union Foo<${1:'l}, T> { + a: &${0:'l} T, + b: usize +}"#, + ); + } + #[test] fn add_lifetime_to_struct() { check_assist( add_lifetime_to_type, - r#"struct Foo { a: &$0i32 }"#, - r#"struct Foo<'a> { a: &'a i32 }"#, + r#" +struct Foo { + a: &$0i32 +}"#, + r#" +struct Foo<${1:'l}> { + a: &${0:'l} i32 +}"#, + ); + + check_assist( + add_lifetime_to_type, + r#" +struct Foo { + a: &$0i32, + b: &usize +}"#, + r#" +struct Foo<${1:'l}> { + a: &${2:'l} i32, + b: &${0:'l} usize +}"#, ); check_assist( add_lifetime_to_type, - r#"struct Foo { a: &$0i32, b: &usize }"#, - r#"struct Foo<'a> { a: &'a i32, b: &'a usize }"#, + r#" +struct Foo { + a: &$0i32, + b: usize +}"#, + r#" +struct Foo<${1:'l}> { + a: &${0:'l} i32, + b: usize +}"#, ); check_assist( add_lifetime_to_type, - r#"struct Foo { a: &$0i32, b: usize }"#, - r#"struct Foo<'a> { a: &'a i32, b: usize }"#, + r#" +struct Foo { + a: &$0T, + b: usize +}"#, + r#" +struct Foo<${1:'l}, T> { + a: &${0:'l} T, + 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 }"#, + r#" +struct Foo { + a: &'a$0 i32 +}"#, + r#" +struct Foo<'a> { + a: &'a i32 +}"#, ); 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)}"#, + r#" +enum Foo { + Bar { a: i32 }, + Other, + Tuple(u32, &$0u32) +}"#, + r#" +enum Foo<${1:'l}> { + Bar { a: i32 }, + Other, + Tuple(u32, &${0:'l} u32) +}"#, ); check_assist( add_lifetime_to_type, - r#"enum Foo { Bar { a: &$0i32 }}"#, - r#"enum Foo<'a> { Bar { a: &'a i32 }}"#, + r#" +enum Foo { + Bar { a: &$0i32 } +}"#, + r#" +enum Foo<${1:'l}> { + Bar { a: &${0:'l} 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 }}"#, + r#" +enum Foo { + Bar { + a: &$0i32, + b: &T + } +}"#, + r#" +enum Foo<${1:'l}, T> { + Bar { + a: &${2:'l} i32, + b: &${0:'l} T + } +}"#, ); check_assist_not_applicable( @@ -195,22 +400,192 @@ mod tests { fn add_lifetime_to_union() { check_assist( add_lifetime_to_type, - r#"union Foo { a: &$0i32 }"#, - r#"union Foo<'a> { a: &'a i32 }"#, + r#" +union Foo { + a: &$0i32 +}"#, + r#" +union Foo<${1:'l}> { + a: &${0:'l} i32 +}"#, ); check_assist( add_lifetime_to_type, - r#"union Foo { a: &$0i32, b: &usize }"#, - r#"union Foo<'a> { a: &'a i32, b: &'a usize }"#, + r#" +union Foo { + a: &$0i32, + b: &usize +}"#, + r#" +union Foo<${1:'l}> { + a: &${2:'l} i32, + b: &${0:'l} usize +}"#, ); check_assist( add_lifetime_to_type, - r#"union Foo { a: &$0T, b: usize }"#, - r#"union Foo<'a, T> { a: &'a T, b: usize }"#, + r#" +union Foo { + a: &$0T, + b: usize +}"#, + r#" +union Foo<${1:'l}, T> { + a: &${0:'l} T, + b: usize +}"#, ); check_assist_not_applicable(add_lifetime_to_type, r#"struct Foo<'a> { a: &'a $0i32 }"#); } + + #[test] + fn declare_undeclared_lifetimes() { + check_assist( + add_lifetime_to_type, + r#" +struct $0Foo { + x: &'a i32 +}"#, + r#" +struct Foo<'a> { + x: &'a i32 +}"#, + ); + check_assist( + add_lifetime_to_type, + r#" +struct $0Foo { + x: &'a i32, + y: &'b u32 +}"#, + r#" +struct Foo<'a, 'b> { + x: &'a i32, + y: &'b u32 +}"#, + ); + + check_assist( + add_lifetime_to_type, + r#" +struct $0Foo { + x: &'a T +}"#, + r#" +struct Foo<'a, T> { + x: &'a T +}"#, + ); + check_assist( + add_lifetime_to_type, + r#" +enum $0Foo { + Bar { + x: &'a i32, + y: &'b T + } +}"#, + r#" +enum Foo<'a, 'b, T> { + Bar { + x: &'a i32, + y: &'b T + } +}"#, + ); + } + + #[test] + fn add_lifetime_with_existing_declared() { + check_assist( + add_lifetime_to_type, + r#" +struct Foo<'a> { + x: &'a i32, + y: &$0u32 +}"#, + r#" +struct Foo<${1:'l}, 'a> { + x: &'a i32, + y: &${0:'l} u32 +}"#, + ); + + check_assist( + add_lifetime_to_type, + r#" +enum Foo<'a> { + Bar { + x: &'a i32, + y: &$0u32 + } +}"#, + r#" +enum Foo<${1:'l}, 'a> { + Bar { + x: &'a i32, + y: &${0:'l} u32 + } +}"#, + ); + } + + #[test] + fn declare_undeclared_and_add_new() { + check_assist( + add_lifetime_to_type, + r#" +struct $0Foo { + x: &'a i32, + y: &u32 +}"#, + r#" +struct Foo<'a, ${1:'l}> { + x: &'a i32, + y: &${0:'l} u32 +}"#, + ); + check_assist( + add_lifetime_to_type, + r#" +struct $0Foo { + x: &'a i32, + y: &T +}"#, + r#" +struct Foo<'a, ${1:'l}, T> { + x: &'a i32, + y: &${0:'l} T +}"#, + ); + check_assist( + add_lifetime_to_type, + r#" +enum $0Foo { + Bar { x: &'a i32 }, + Baz(&u32) +}"#, + r#" +enum Foo<'a, ${1:'l}> { + Bar { x: &'a i32 }, + Baz(&${0:'l} u32) +}"#, + ); + } + + #[test] + fn not_applicable_when_all_correct() { + check_assist_not_applicable(add_lifetime_to_type, r#"struct $0Foo<'a> { x: &'a i32 }"#); + check_assist_not_applicable( + add_lifetime_to_type, + r#"struct $0Foo<'a, 'b> { x: &'a i32, y: &'b u32 }"#, + ); + check_assist_not_applicable( + add_lifetime_to_type, + r#"enum $0Foo<'a> { Bar { x: &'a i32 } }"#, + ); + } } From b14c0007dfe31242ed07687a387bb7c4f6b0fda1 Mon Sep 17 00:00:00 2001 From: Hegui Dai Date: Mon, 10 Nov 2025 21:56:29 +0800 Subject: [PATCH 2/9] rename --- ...ime_to_type.rs => add_missing_lifetime.rs} | 85 +++++++++---------- crates/ide-assists/src/lib.rs | 4 +- crates/ide-assists/src/tests/generated.rs | 16 ++-- 3 files changed, 52 insertions(+), 53 deletions(-) rename crates/ide-assists/src/handlers/{add_lifetime_to_type.rs => add_missing_lifetime.rs} (88%) diff --git a/crates/ide-assists/src/handlers/add_lifetime_to_type.rs b/crates/ide-assists/src/handlers/add_missing_lifetime.rs similarity index 88% rename from crates/ide-assists/src/handlers/add_lifetime_to_type.rs rename to crates/ide-assists/src/handlers/add_missing_lifetime.rs index af3377a3e6b0..ec7d30376c83 100644 --- a/crates/ide-assists/src/handlers/add_lifetime_to_type.rs +++ b/crates/ide-assists/src/handlers/add_missing_lifetime.rs @@ -11,25 +11,25 @@ use syntax::{ use crate::{AssistContext, AssistId, Assists}; -// Assist: add_lifetime_to_type +// Assist: add_missing_lifetime // -// Adds a new lifetime to a struct, enum or union. +// Adds missing lifetimes to a struct, enum or union. // // ``` -// struct Point { -// x: &$0u32, -// y: u32, +// struct $0Foo { +// x: &'a i32, +// y: &T // } // ``` // -> // ``` -// struct Point<${1:'l}> { -// x: &${0:'l} u32, -// y: u32, +// struct Foo<'a, ${1:'l}, T> { +// x: &'a i32, +// y: &${0:'l} T // } // ``` -pub(crate) fn add_lifetime_to_type(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { +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<_>) = @@ -71,14 +71,13 @@ fn add_and_declare_lifetimes( let has_undeclared_lifetimes = !adt_undeclared_lifetimes.is_empty(); let message = match (has_refs_without_lifetime, has_undeclared_lifetimes) { - (false, true) => "Declare used lifetimes in type definition", - (true, false) => "Add lifetime to type", - (true, true) => "Declare used lifetimes and add new lifetime", + (false, true) => "Declare used lifetimes in generic parameters", + (true, false) | (true, true) => "Add missing lifetimes", _ => return None, }; acc.add( - AssistId::quick_fix("add_lifetime_to_type"), + AssistId::quick_fix("add_missing_lifetime"), message, node.syntax().text_range(), |builder| { @@ -226,7 +225,7 @@ mod tests { #[test] fn add_lifetime() { check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" struct Foo { a: &$0i32, @@ -240,7 +239,7 @@ struct Foo<${1:'l}> { ); check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" enum Foo { Bar { a: i32 }, @@ -256,7 +255,7 @@ enum Foo<${1:'l}> { ); check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" union Foo { a: &$0T, @@ -273,7 +272,7 @@ union Foo<${1:'l}, T> { #[test] fn add_lifetime_to_struct() { check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" struct Foo { a: &$0i32 @@ -285,7 +284,7 @@ struct Foo<${1:'l}> { ); check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" struct Foo { a: &$0i32, @@ -299,7 +298,7 @@ struct Foo<${1:'l}> { ); check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" struct Foo { a: &$0i32, @@ -313,7 +312,7 @@ struct Foo<${1:'l}> { ); check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" struct Foo { a: &$0T, @@ -327,7 +326,7 @@ struct Foo<${1:'l}, T> { ); check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" struct Foo { a: &'a$0 i32 @@ -338,13 +337,13 @@ struct Foo<'a> { }"#, ); - check_assist_not_applicable(add_lifetime_to_type, r#"struct Foo<'a> { a: &$0'a i32 }"#); + check_assist_not_applicable(add_missing_lifetime, r#"struct Foo<'a> { a: &$0'a i32 }"#); } #[test] fn add_lifetime_to_enum() { check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" enum Foo { Bar { a: i32 }, @@ -360,7 +359,7 @@ enum Foo<${1:'l}> { ); check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" enum Foo { Bar { a: &$0i32 } @@ -372,7 +371,7 @@ enum Foo<${1:'l}> { ); check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" enum Foo { Bar { @@ -390,16 +389,16 @@ enum Foo<${1:'l}, T> { ); check_assist_not_applicable( - add_lifetime_to_type, + add_missing_lifetime, r#"enum Foo<'a> { Bar { a: &$0'a i32 }}"#, ); - check_assist_not_applicable(add_lifetime_to_type, r#"enum Foo { Bar, $0Misc }"#); + check_assist_not_applicable(add_missing_lifetime, r#"enum Foo { Bar, $0Misc }"#); } #[test] fn add_lifetime_to_union() { check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" union Foo { a: &$0i32 @@ -411,7 +410,7 @@ union Foo<${1:'l}> { ); check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" union Foo { a: &$0i32, @@ -425,7 +424,7 @@ union Foo<${1:'l}> { ); check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" union Foo { a: &$0T, @@ -438,13 +437,13 @@ union Foo<${1:'l}, T> { }"#, ); - check_assist_not_applicable(add_lifetime_to_type, r#"struct Foo<'a> { a: &'a $0i32 }"#); + check_assist_not_applicable(add_missing_lifetime, r#"struct Foo<'a> { a: &'a $0i32 }"#); } #[test] fn declare_undeclared_lifetimes() { check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" struct $0Foo { x: &'a i32 @@ -455,7 +454,7 @@ struct Foo<'a> { }"#, ); check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" struct $0Foo { x: &'a i32, @@ -469,7 +468,7 @@ struct Foo<'a, 'b> { ); check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" struct $0Foo { x: &'a T @@ -480,7 +479,7 @@ struct Foo<'a, T> { }"#, ); check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" enum $0Foo { Bar { @@ -501,7 +500,7 @@ enum Foo<'a, 'b, T> { #[test] fn add_lifetime_with_existing_declared() { check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" struct Foo<'a> { x: &'a i32, @@ -515,7 +514,7 @@ struct Foo<${1:'l}, 'a> { ); check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" enum Foo<'a> { Bar { @@ -536,7 +535,7 @@ enum Foo<${1:'l}, 'a> { #[test] fn declare_undeclared_and_add_new() { check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" struct $0Foo { x: &'a i32, @@ -549,7 +548,7 @@ struct Foo<'a, ${1:'l}> { }"#, ); check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" struct $0Foo { x: &'a i32, @@ -562,7 +561,7 @@ struct Foo<'a, ${1:'l}, T> { }"#, ); check_assist( - add_lifetime_to_type, + add_missing_lifetime, r#" enum $0Foo { Bar { x: &'a i32 }, @@ -578,13 +577,13 @@ enum Foo<'a, ${1:'l}> { #[test] fn not_applicable_when_all_correct() { - check_assist_not_applicable(add_lifetime_to_type, r#"struct $0Foo<'a> { x: &'a i32 }"#); + check_assist_not_applicable(add_missing_lifetime, r#"struct $0Foo<'a> { x: &'a i32 }"#); check_assist_not_applicable( - add_lifetime_to_type, + add_missing_lifetime, r#"struct $0Foo<'a, 'b> { x: &'a i32, y: &'b u32 }"#, ); check_assist_not_applicable( - add_lifetime_to_type, + add_missing_lifetime, r#"enum $0Foo<'a> { Bar { x: &'a i32 } }"#, ); } 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..299f3573a740 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, ${1:'l}, T> { + x: &'a i32, + y: &${0:'l} T } "#####, ) From ce07a7576706ffca620a41dcfa74b938f77d81c8 Mon Sep 17 00:00:00 2001 From: Hegui Dai Date: Mon, 10 Nov 2025 23:11:09 +0800 Subject: [PATCH 3/9] reduce unnecessary disassembly --- .../src/handlers/add_missing_lifetime.rs | 32 +++++-------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/crates/ide-assists/src/handlers/add_missing_lifetime.rs b/crates/ide-assists/src/handlers/add_missing_lifetime.rs index ec7d30376c83..f3c7744d9a02 100644 --- a/crates/ide-assists/src/handlers/add_missing_lifetime.rs +++ b/crates/ide-assists/src/handlers/add_missing_lifetime.rs @@ -1,8 +1,8 @@ use ide_db::{FxIndexSet, source_change::SourceChangeBuilder}; use syntax::{ - NodeOrToken, SyntaxElement, SyntaxNode, SyntaxToken, T, + NodeOrToken, T, ast::{ - self, AstNode, HasGenericParams, HasName, Lifetime, + self, AstNode, HasGenericParams, HasName, make::{self, tokens}, syntax_factory::SyntaxFactory, }, @@ -191,28 +191,12 @@ fn add_lifetime_to_refs( for r#ref in refs_without_lifetime { let Some(amp_token) = r#ref.amp_token() else { continue }; let lifetime = make.lifetime(lifetime_text); - insert_elements_after( - &NodeOrToken::Token(amp_token), - &lifetime, - vec![lifetime.syntax().clone().into(), tokens::single_space().into()], - ctx, - editor, - builder, - ); - } -} - -fn insert_elements_after( - node_or_token: &NodeOrToken, - lifetime: &Lifetime, - elements: Vec, - ctx: &AssistContext<'_>, - editor: &mut SyntaxEditor, - builder: &mut SourceChangeBuilder, -) { - editor.insert_all(Position::after(node_or_token), elements); - if let Some(cap) = ctx.config.snippet_cap { - editor.add_annotation(lifetime.syntax(), builder.make_placeholder_snippet(cap)); + 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(cap) = ctx.config.snippet_cap { + editor.add_annotation(lifetime.syntax(), builder.make_placeholder_snippet(cap)); + }; } } From f1a648e781bae0dfb199fc02723a1f95db097128 Mon Sep 17 00:00:00 2001 From: Hegui Dai Date: Tue, 11 Nov 2025 08:41:22 +0800 Subject: [PATCH 4/9] Add method `for_lifetime` for NameGenerator --- .../ide-db/src/syntax_helpers/suggest_name.rs | 72 ++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) 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"); + } } From b1d4380d91296a994ad684d932eb0c5c0521733f Mon Sep 17 00:00:00 2001 From: Hegui Dai Date: Tue, 11 Nov 2025 07:44:13 +0800 Subject: [PATCH 5/9] Generate names using the for_lifetime method --- .../src/handlers/introduce_named_lifetime.rs | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) 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 { From d1e3c345eee990b3a847a25cf4a61bfd1f6bd6e7 Mon Sep 17 00:00:00 2001 From: Hegui Dai Date: Tue, 11 Nov 2025 07:44:13 +0800 Subject: [PATCH 6/9] Streamline tests --- .../src/handlers/add_missing_lifetime.rs | 312 +++++++----------- crates/ide-assists/src/tests/generated.rs | 4 +- 2 files changed, 114 insertions(+), 202 deletions(-) diff --git a/crates/ide-assists/src/handlers/add_missing_lifetime.rs b/crates/ide-assists/src/handlers/add_missing_lifetime.rs index f3c7744d9a02..da22e17b38bd 100644 --- a/crates/ide-assists/src/handlers/add_missing_lifetime.rs +++ b/crates/ide-assists/src/handlers/add_missing_lifetime.rs @@ -23,9 +23,9 @@ use crate::{AssistContext, AssistId, Assists}; // ``` // -> // ``` -// struct Foo<'a, ${1:'l}, T> { +// struct Foo<'a, ${0:'b}, T> { // x: &'a i32, -// y: &${0:'l} T +// y: &${0:'b} T // } // ``` @@ -207,354 +207,264 @@ mod tests { use super::*; #[test] - fn add_lifetime() { + fn struct_lifetime() { check_assist( add_missing_lifetime, r#" -struct Foo { - a: &$0i32, - b: &usize +struct $0Foo { + x: &'a i32, + y: &'b u32 }"#, r#" -struct Foo<${1:'l}> { - a: &${2:'l} i32, - b: &${0:'l} usize +struct Foo<'a, 'b> { + x: &'a i32, + y: &'b u32 }"#, ); - check_assist( add_missing_lifetime, r#" -enum Foo { - Bar { a: i32 }, - Other, - Tuple(u32, &$0u32) +struct $0Foo { + x: &'a T }"#, r#" -enum Foo<${1:'l}> { - Bar { a: i32 }, - Other, - Tuple(u32, &${0:'l} u32) +struct Foo<'a, T> { + x: &'a T }"#, ); - check_assist( add_missing_lifetime, r#" -union Foo { +struct Foo { a: &$0T, b: usize }"#, r#" -union Foo<${1:'l}, T> { - a: &${0:'l} T, +struct Foo<${0:'a}, T> { + a: &${0:'a} T, b: usize }"#, ); - } - - #[test] - fn add_lifetime_to_struct() { check_assist( add_missing_lifetime, r#" -struct Foo { - a: &$0i32 +struct Foo<'a> { + x: &'a i32, + y: &$0u32 }"#, r#" -struct Foo<${1:'l}> { - a: &${0:'l} i32 +struct Foo<${0:'b}, 'a> { + x: &'a i32, + y: &${0:'b} u32 }"#, ); - check_assist( add_missing_lifetime, r#" -struct Foo { - a: &$0i32, - b: &usize +struct $0Foo { + x: &'a i32, + y: &u32 }"#, r#" -struct Foo<${1:'l}> { - a: &${2:'l} i32, - b: &${0:'l} usize +struct Foo<'a, ${0:'b}> { + x: &'a i32, + y: &${0:'b} u32 }"#, ); - check_assist( add_missing_lifetime, r#" -struct Foo { - a: &$0i32, - b: usize +struct $0Foo { + x: &'a i32, + y: &T }"#, r#" -struct Foo<${1:'l}> { - a: &${0:'l} i32, - b: usize +struct Foo<'a, ${0:'b}, T> { + x: &'a i32, + y: &${0:'b} T }"#, ); + } + #[test] + fn enum_lifetime() { check_assist( add_missing_lifetime, r#" -struct Foo { - a: &$0T, - b: usize +enum $0Foo { + Bar { x: &'a i32 } }"#, r#" -struct Foo<${1:'l}, T> { - a: &${0:'l} T, - b: usize +enum Foo<'a> { + Bar { x: &'a i32 } }"#, ); - check_assist( add_missing_lifetime, r#" -struct Foo { - a: &'a$0 i32 +enum $0Foo { + Bar { + x: &'a i32, + y: &'b T + } }"#, r#" -struct Foo<'a> { - a: &'a i32 +enum Foo<'a, 'b, T> { + Bar { + x: &'a i32, + y: &'b T + } }"#, ); - - check_assist_not_applicable(add_missing_lifetime, r#"struct Foo<'a> { a: &$0'a i32 }"#); - } - - #[test] - fn add_lifetime_to_enum() { check_assist( add_missing_lifetime, r#" enum Foo { - Bar { a: i32 }, - Other, - Tuple(u32, &$0u32) + Bar { a: &$0i32 } }"#, r#" -enum Foo<${1:'l}> { - Bar { a: i32 }, - Other, - Tuple(u32, &${0:'l} u32) +enum Foo<${0:'a}> { + Bar { a: &${0:'a} i32 } }"#, ); - check_assist( add_missing_lifetime, r#" -enum Foo { - Bar { a: &$0i32 } +enum Foo { + Bar { a: T }, + Other, + Tuple(u32, &$0u32) }"#, r#" -enum Foo<${1:'l}> { - Bar { a: &${0:'l} i32 } +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 + a: &$0i32, + b: &T } }"#, r#" -enum Foo<${1:'l}, T> { +enum Foo<${0:'a}, T> { Bar { - a: &${2:'l} i32, - b: &${0:'l} T - } -}"#, - ); - - check_assist_not_applicable( - add_missing_lifetime, - r#"enum Foo<'a> { Bar { a: &$0'a i32 }}"#, - ); - check_assist_not_applicable(add_missing_lifetime, r#"enum Foo { Bar, $0Misc }"#); + a: &${0:'a} i32, + b: &${0:'a} T } - - #[test] - fn add_lifetime_to_union() { - check_assist( - add_missing_lifetime, - r#" -union Foo { - a: &$0i32 -}"#, - r#" -union Foo<${1:'l}> { - a: &${0:'l} i32 }"#, ); - check_assist( add_missing_lifetime, r#" -union Foo { - a: &$0i32, - b: &usize +enum Foo<'a> { + Bar { + x: &'a i32, + y: &$0u32 + } }"#, r#" -union Foo<${1:'l}> { - a: &${2:'l} i32, - b: &${0:'l} usize +enum Foo<${0:'b}, 'a> { + Bar { + x: &'a i32, + y: &${0:'b} u32 + } }"#, ); - check_assist( add_missing_lifetime, r#" -union Foo { - a: &$0T, - b: usize +enum $0Foo { + Bar { x: &'a i32 }, + Baz(&u32) }"#, r#" -union Foo<${1:'l}, T> { - a: &${0:'l} T, - b: usize +enum Foo<'a, ${0:'b}> { + Bar { x: &'a i32 }, + Baz(&${0:'b} u32) }"#, ); - - check_assist_not_applicable(add_missing_lifetime, r#"struct Foo<'a> { a: &'a $0i32 }"#); } #[test] - fn declare_undeclared_lifetimes() { + fn union_lifetime() { check_assist( add_missing_lifetime, r#" -struct $0Foo { +union $0Foo { x: &'a i32 }"#, r#" -struct Foo<'a> { +union Foo<'a> { x: &'a i32 }"#, ); 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 { +union $0Foo { x: &'a T }"#, r#" -struct Foo<'a, T> { +union Foo<'a, T> { x: &'a T }"#, ); 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 - } -}"#, - ); - } - - #[test] - fn add_lifetime_with_existing_declared() { - check_assist( - add_missing_lifetime, - r#" -struct Foo<'a> { - x: &'a i32, - y: &$0u32 -}"#, - r#" -struct Foo<${1:'l}, 'a> { - x: &'a i32, - y: &${0:'l} u32 -}"#, - ); - - check_assist( - add_missing_lifetime, - r#" -enum Foo<'a> { - Bar { - x: &'a i32, - y: &$0u32 - } +union Foo { + a: &$0i32, + b: &usize }"#, r#" -enum Foo<${1:'l}, 'a> { - Bar { - x: &'a i32, - y: &${0:'l} u32 - } +union Foo<${0:'a}> { + a: &${0:'a} i32, + b: &${0:'a} usize }"#, ); - } - - #[test] - fn declare_undeclared_and_add_new() { check_assist( add_missing_lifetime, r#" -struct $0Foo { - x: &'a i32, - y: &u32 +union Foo { + a: &$0T, + b: usize }"#, r#" -struct Foo<'a, ${1:'l}> { - x: &'a i32, - y: &${0:'l} u32 +union Foo<${0:'a}, T> { + a: &${0:'a} T, + b: usize }"#, ); check_assist( add_missing_lifetime, r#" -struct $0Foo { +union Foo<'a> { x: &'a i32, - y: &T + y: &$0u32 }"#, r#" -struct Foo<'a, ${1:'l}, T> { +union Foo<${0:'b}, 'a> { x: &'a i32, - y: &${0:'l} T + y: &${0:'b} u32 }"#, ); check_assist( add_missing_lifetime, r#" -enum $0Foo { - Bar { x: &'a i32 }, - Baz(&u32) +union $0Foo { + x: &'a T, + y: &i32 }"#, r#" -enum Foo<'a, ${1:'l}> { - Bar { x: &'a i32 }, - Baz(&${0:'l} u32) +union Foo<'a, ${0:'b}, T> { + x: &'a T, + y: &${0:'b} i32 }"#, ); } @@ -570,5 +480,7 @@ enum Foo<'a, ${1:'l}> { 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/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs index 299f3573a740..700805f3a815 100644 --- a/crates/ide-assists/src/tests/generated.rs +++ b/crates/ide-assists/src/tests/generated.rs @@ -203,9 +203,9 @@ struct $0Foo { } "#####, r#####" -struct Foo<'a, ${1:'l}, T> { +struct Foo<'a, ${0:'b}, T> { x: &'a i32, - y: &${0:'l} T + y: &${0:'b} T } "#####, ) From aafa13f1c3035634f21d081f3753501a7a1518bb Mon Sep 17 00:00:00 2001 From: Hegui Dai Date: Tue, 11 Nov 2025 07:44:13 +0800 Subject: [PATCH 7/9] Unify snippet numbers --- .../src/handlers/add_missing_lifetime.rs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/ide-assists/src/handlers/add_missing_lifetime.rs b/crates/ide-assists/src/handlers/add_missing_lifetime.rs index da22e17b38bd..8b0f4b933a71 100644 --- a/crates/ide-assists/src/handlers/add_missing_lifetime.rs +++ b/crates/ide-assists/src/handlers/add_missing_lifetime.rs @@ -120,14 +120,22 @@ fn add_and_declare_lifetimes( editor.insert_all(Position::after(name.syntax()), final_elements); } + let snippet = ctx.config.snippet_cap.map(|cap| builder.make_placeholder_snippet(cap)); + if let Some(lifetime) = new_lifetime_to_annotate - && let Some(cap) = ctx.config.snippet_cap + && let Some(snippet) = snippet { - editor.add_annotation(lifetime.syntax(), builder.make_placeholder_snippet(cap)); + editor.add_annotation(lifetime.syntax(), snippet); } if has_refs_without_lifetime { - add_lifetime_to_refs(refs_without_lifetime, "'l", ctx, &mut editor, builder, &make); + add_lifetime_to_refs( + refs_without_lifetime, + &new_lifetime_name, + &mut editor, + &make, + snippet, + ); } editor.add_mappings(make.finish_with_mappings()); @@ -183,10 +191,9 @@ fn find_all_ref_types_from_field_list(field_list: &ast::FieldList) -> Option, lifetime_text: &str, - ctx: &AssistContext<'_>, editor: &mut SyntaxEditor, - builder: &mut SourceChangeBuilder, make: &SyntaxFactory, + snippet: Option, ) { for r#ref in refs_without_lifetime { let Some(amp_token) = r#ref.amp_token() else { continue }; @@ -194,8 +201,8 @@ fn add_lifetime_to_refs( 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(cap) = ctx.config.snippet_cap { - editor.add_annotation(lifetime.syntax(), builder.make_placeholder_snippet(cap)); + if let Some(snippet) = snippet { + editor.add_annotation(lifetime.syntax(), snippet); }; } } From 6aa7edf257c09188e05ac80afce24015431be180 Mon Sep 17 00:00:00 2001 From: Hegui Dai Date: Tue, 11 Nov 2025 07:44:13 +0800 Subject: [PATCH 8/9] Generate names using the for_lifetime method --- .../src/handlers/add_missing_lifetime.rs | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/crates/ide-assists/src/handlers/add_missing_lifetime.rs b/crates/ide-assists/src/handlers/add_missing_lifetime.rs index 8b0f4b933a71..7b02c497f2d0 100644 --- a/crates/ide-assists/src/handlers/add_missing_lifetime.rs +++ b/crates/ide-assists/src/handlers/add_missing_lifetime.rs @@ -1,12 +1,12 @@ -use ide_db::{FxIndexSet, source_change::SourceChangeBuilder}; +use ide_db::{FxIndexSet, syntax_helpers::suggest_name::NameGenerator}; use syntax::{ - NodeOrToken, T, + NodeOrToken, SmolStr, T, ast::{ self, AstNode, HasGenericParams, HasName, make::{self, tokens}, syntax_factory::SyntaxFactory, }, - syntax_editor::{Position, SyntaxEditor}, + syntax_editor::{Position, SyntaxAnnotation, SyntaxEditor}, }; use crate::{AssistContext, AssistId, Assists}; @@ -35,21 +35,21 @@ pub(crate) fn add_missing_lifetime(acc: &mut Assists, ctx: &AssistContext<'_>) - 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 + 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().to_string()) + .map(|lt| lt.text().into()) .collect() }) .unwrap_or_default(); - let adt_undeclared_lifetimes: FxIndexSet = refs_with_lifetime + let adt_undeclared_lifetimes: FxIndexSet = refs_with_lifetime .iter() .filter_map(|ref_type| ref_type.lifetime()) - .map(|lt| lt.text().to_string()) + .map(|lt| lt.text().into()) .filter(|lt_text| !adt_declared_lifetimes.contains(lt_text)) .collect(); @@ -57,15 +57,26 @@ pub(crate) fn add_missing_lifetime(acc: &mut Assists, ctx: &AssistContext<'_>) - return None; } - add_and_declare_lifetimes(acc, ctx, &node, adt_undeclared_lifetimes, refs_without_lifetime) + 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, + ) } fn add_and_declare_lifetimes( acc: &mut Assists, ctx: &AssistContext<'_>, node: &ast::Adt, - adt_undeclared_lifetimes: FxIndexSet, + adt_undeclared_lifetimes: FxIndexSet, refs_without_lifetime: Vec, + all_existing_lifetimes: Vec, ) -> Option<()> { let has_refs_without_lifetime = !refs_without_lifetime.is_empty(); let has_undeclared_lifetimes = !adt_undeclared_lifetimes.is_empty(); @@ -76,6 +87,11 @@ fn add_and_declare_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, @@ -98,7 +114,7 @@ fn add_and_declare_lifetimes( if has_refs_without_lifetime { has_undeclared_lifetimes.then(|| lifetime_elements.extend(comma_and_space.clone())); - let lifetime = make.lifetime("'l"); + let lifetime = make.lifetime(&new_lifetime_name); new_lifetime_to_annotate = Some(lifetime.clone()); lifetime_elements.push(lifetime.syntax().clone().into()); } From 95888c84a12d4f3ea4d524f40dc84be2167c1b6a Mon Sep 17 00:00:00 2001 From: Hegui Dai Date: Wed, 12 Nov 2025 10:43:21 +0800 Subject: [PATCH 9/9] make the generated lifetime more orderly --- .../src/handlers/add_missing_lifetime.rs | 73 ++++++++++++------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/crates/ide-assists/src/handlers/add_missing_lifetime.rs b/crates/ide-assists/src/handlers/add_missing_lifetime.rs index 7b02c497f2d0..e3079f29146f 100644 --- a/crates/ide-assists/src/handlers/add_missing_lifetime.rs +++ b/crates/ide-assists/src/handlers/add_missing_lifetime.rs @@ -1,4 +1,5 @@ use ide_db::{FxIndexSet, syntax_helpers::suggest_name::NameGenerator}; +use itertools::chain; use syntax::{ NodeOrToken, SmolStr, T, ast::{ @@ -53,7 +54,10 @@ pub(crate) fn add_missing_lifetime(acc: &mut Assists, ctx: &AssistContext<'_>) - .filter(|lt_text| !adt_declared_lifetimes.contains(lt_text)) .collect(); - if refs_without_lifetime.is_empty() && adt_undeclared_lifetimes.is_empty() { + 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; } @@ -67,6 +71,8 @@ pub(crate) fn add_missing_lifetime(acc: &mut Assists, ctx: &AssistContext<'_>) - adt_undeclared_lifetimes, refs_without_lifetime, all_existing_lifetimes, + has_refs_without_lifetime, + has_undeclared_lifetimes, ) } @@ -77,10 +83,9 @@ fn add_and_declare_lifetimes( adt_undeclared_lifetimes: FxIndexSet, refs_without_lifetime: Vec, all_existing_lifetimes: Vec, + has_refs_without_lifetime: bool, + has_undeclared_lifetimes: bool, ) -> Option<()> { - let has_refs_without_lifetime = !refs_without_lifetime.is_empty(); - let has_undeclared_lifetimes = !adt_undeclared_lifetimes.is_empty(); - 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", @@ -99,41 +104,46 @@ fn add_and_declare_lifetimes( |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 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.clone())); - let new_lifetime = make.lifetime(lifetime_text); - lifetime_elements.push(new_lifetime.syntax().clone().into()); + (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.clone())); + has_undeclared_lifetimes.then(|| lifetime_elements.extend(comma_and_space())); let lifetime = make.lifetime(&new_lifetime_name); - new_lifetime_to_annotate = Some(lifetime.clone()); lifetime_elements.push(lifetime.syntax().clone().into()); + new_lifetime_to_annotate = Some(lifetime); } - if let Some(gen_param) = node.generic_param_list() - && let Some(left_angle) = gen_param.l_angle_token() - { - if !lifetime_elements.is_empty() { + 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); } - editor.insert_all(Position::after(&left_angle), lifetime_elements); - } else if let Some(name) = node.name() - && !lifetime_elements.is_empty() - { - let mut final_elements = vec![make::token(T![<]).into()]; - final_elements.append(&mut lifetime_elements); - final_elements.push(make::token(T![>]).into()); - editor.insert_all(Position::after(name.syntax()), final_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)); @@ -276,7 +286,7 @@ struct Foo<'a> { y: &$0u32 }"#, r#" -struct Foo<${0:'b}, 'a> { +struct Foo<'a, ${0:'b}> { x: &'a i32, y: &${0:'b} u32 }"#, @@ -300,13 +310,22 @@ struct Foo<'a, ${0:'b}> { struct $0Foo { x: &'a i32, y: &T + z: &'b u32 }"#, r#" -struct Foo<'a, ${0:'b}, T> { +struct Foo<'a, 'b, ${0:'c}, T> { x: &'a i32, - y: &${0:'b} T + 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] @@ -392,7 +411,7 @@ enum Foo<'a> { } }"#, r#" -enum Foo<${0:'b}, 'a> { +enum Foo<'a, ${0:'b}> { Bar { x: &'a i32, y: &${0:'b} u32 @@ -472,7 +491,7 @@ union Foo<'a> { y: &$0u32 }"#, r#" -union Foo<${0:'b}, 'a> { +union Foo<'a, ${0:'b}> { x: &'a i32, y: &${0:'b} u32 }"#,