Merge pull request #21507 from Shourya742/2026-01-17-migrate-named-lifetime

migrate introduce_named_lifetime assist to SyntaxEditor
This commit is contained in:
Shoyu Vanilla (Flint) 2026-02-11 10:01:45 +00:00 committed by GitHub
commit 751508a968
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,11 +1,12 @@
use ide_db::FxHashSet;
use ide_db::{FileId, FxHashSet};
use syntax::{
AstNode, TextRange,
ast::{self, HasGenericParams, edit_in_place::GenericParamsOwnerEdit, make},
ted::{self, Position},
AstNode, SmolStr, T, TextRange, ToSmolStr,
ast::{self, HasGenericParams, HasName, syntax_factory::SyntaxFactory},
format_smolstr,
syntax_editor::{Element, Position, SyntaxEditor},
};
use crate::{AssistContext, AssistId, Assists, assist_context::SourceChangeBuilder};
use crate::{AssistContext, AssistId, Assists};
static ASSIST_NAME: &str = "introduce_named_lifetime";
static ASSIST_LABEL: &str = "Introduce named lifetime";
@ -38,100 +39,108 @@ pub(crate) fn introduce_named_lifetime(acc: &mut Assists, ctx: &AssistContext<'_
// FIXME: should also add support for the case fun(f: &Foo) -> &$0Foo
let lifetime =
ctx.find_node_at_offset::<ast::Lifetime>().filter(|lifetime| lifetime.text() == "'_")?;
let file_id = ctx.vfs_file_id();
let lifetime_loc = lifetime.lifetime_ident_token()?.text_range();
if let Some(fn_def) = lifetime.syntax().ancestors().find_map(ast::Fn::cast) {
generate_fn_def_assist(acc, fn_def, lifetime_loc, lifetime)
generate_fn_def_assist(acc, fn_def, lifetime_loc, lifetime, file_id)
} else if let Some(impl_def) = lifetime.syntax().ancestors().find_map(ast::Impl::cast) {
generate_impl_def_assist(acc, impl_def, lifetime_loc, lifetime)
generate_impl_def_assist(acc, impl_def, lifetime_loc, lifetime, file_id)
} else {
None
}
}
/// Generate the assist for the fn def case
/// Given a type parameter list, generate a unique lifetime parameter name
/// which is not in the list
fn generate_unique_lifetime_param_name(
existing_params: Option<ast::GenericParamList>,
) -> Option<SmolStr> {
let used_lifetime_param: FxHashSet<SmolStr> = existing_params
.iter()
.flat_map(|params| params.lifetime_params())
.map(|p| p.syntax().text().to_smolstr())
.collect();
('a'..='z').map(|c| format_smolstr!("'{c}")).find(|lt| !used_lifetime_param.contains(lt))
}
fn generate_fn_def_assist(
acc: &mut Assists,
fn_def: ast::Fn,
lifetime_loc: TextRange,
lifetime: ast::Lifetime,
file_id: FileId,
) -> 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 param_list = fn_def.param_list()?;
let new_lifetime_name = 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());
// compute the location which implicitly has the same lifetime as the anonymous lifetime
let loc_needing_lifetime = if let Some(self_param) = self_param {
// if we have a self reference, use that
Some(NeedsLifetime::SelfParam(self_param))
} else {
// otherwise, if there's a single reference parameter without a named lifetime, use that
let fn_params_without_lifetime: Vec<_> = param_list
let unnamed_refs: Vec<_> = param_list
.params()
.filter_map(|param| match param.ty() {
Some(ast::Type::RefType(ascribed_type)) if ascribed_type.lifetime().is_none() => {
Some(NeedsLifetime::RefType(ascribed_type))
Some(ast::Type::RefType(ref_type)) if ref_type.lifetime().is_none() => {
Some(NeedsLifetime::RefType(ref_type))
}
_ => None,
})
.collect();
match fn_params_without_lifetime.len() {
1 => Some(fn_params_without_lifetime.into_iter().next()?),
match unnamed_refs.len() {
1 => Some(unnamed_refs.into_iter().next()?),
0 => None,
// multiple unnamed is invalid. assist is not applicable
_ => return None,
}
};
acc.add(AssistId::refactor(ASSIST_NAME), ASSIST_LABEL, lifetime_loc, |builder| {
let fn_def = builder.make_mut(fn_def);
let lifetime = builder.make_mut(lifetime);
let loc_needing_lifetime =
loc_needing_lifetime.and_then(|it| it.make_mut(builder).to_position());
fn_def.get_or_create_generic_param_list().add_generic_param(
make::lifetime_param(new_lifetime_param.clone()).clone_for_update().into(),
);
ted::replace(lifetime.syntax(), new_lifetime_param.clone_for_update().syntax());
if let Some(position) = loc_needing_lifetime {
ted::insert(position, new_lifetime_param.clone_for_update().syntax());
acc.add(AssistId::refactor(ASSIST_NAME), ASSIST_LABEL, lifetime_loc, |edit| {
let root = fn_def.syntax().ancestors().last().unwrap().clone();
let mut editor = SyntaxEditor::new(root);
let factory = SyntaxFactory::with_mappings();
if let Some(generic_list) = fn_def.generic_param_list() {
insert_lifetime_param(&mut editor, &factory, &generic_list, &new_lifetime_name);
} else {
insert_new_generic_param_list_fn(&mut editor, &factory, &fn_def, &new_lifetime_name);
}
editor.replace(lifetime.syntax(), factory.lifetime(&new_lifetime_name).syntax());
if let Some(pos) = loc_needing_lifetime.and_then(|l| l.to_position()) {
editor.insert_all(
pos,
vec![
factory.lifetime(&new_lifetime_name).syntax().clone().into(),
factory.whitespace(" ").into(),
],
);
}
edit.add_file_edits(file_id, editor);
})
}
/// Generate the assist for the impl def case
fn generate_impl_def_assist(
acc: &mut Assists,
impl_def: ast::Impl,
lifetime_loc: TextRange,
lifetime: ast::Lifetime,
fn insert_new_generic_param_list_fn(
editor: &mut SyntaxEditor,
factory: &SyntaxFactory,
fn_def: &ast::Fn,
lifetime_name: &str,
) -> Option<()> {
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);
let name = fn_def.name()?;
impl_def.get_or_create_generic_param_list().add_generic_param(
make::lifetime_param(new_lifetime_param.clone()).clone_for_update().into(),
);
ted::replace(lifetime.syntax(), new_lifetime_param.clone_for_update().syntax());
})
}
editor.insert_all(
Position::after(name.syntax()),
vec![
factory.token(T![<]).syntax_element(),
factory.lifetime(lifetime_name).syntax().syntax_element(),
factory.token(T![>]).syntax_element(),
],
);
/// Given a type parameter list, generate a unique lifetime parameter name
/// which is not in the list
fn generate_unique_lifetime_param_name(
existing_type_param_list: Option<ast::GenericParamList>,
) -> Option<ast::Lifetime> {
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))
Some(())
}
enum NeedsLifetime {
@ -140,13 +149,6 @@ enum NeedsLifetime {
}
impl NeedsLifetime {
fn make_mut(self, builder: &mut SourceChangeBuilder) -> Self {
match self {
Self::SelfParam(it) => Self::SelfParam(builder.make_mut(it)),
Self::RefType(it) => Self::RefType(builder.make_mut(it)),
}
}
fn to_position(self) -> Option<Position> {
match self {
Self::SelfParam(it) => Some(Position::after(it.amp_token()?)),
@ -155,6 +157,75 @@ impl NeedsLifetime {
}
}
fn generate_impl_def_assist(
acc: &mut Assists,
impl_def: ast::Impl,
lifetime_loc: TextRange,
lifetime: ast::Lifetime,
file_id: FileId,
) -> Option<()> {
let new_lifetime_name = generate_unique_lifetime_param_name(impl_def.generic_param_list())?;
acc.add(AssistId::refactor(ASSIST_NAME), ASSIST_LABEL, lifetime_loc, |edit| {
let root = impl_def.syntax().ancestors().last().unwrap().clone();
let mut editor = SyntaxEditor::new(root);
let factory = SyntaxFactory::without_mappings();
if let Some(generic_list) = impl_def.generic_param_list() {
insert_lifetime_param(&mut editor, &factory, &generic_list, &new_lifetime_name);
} else {
insert_new_generic_param_list_imp(&mut editor, &factory, &impl_def, &new_lifetime_name);
}
editor.replace(lifetime.syntax(), factory.lifetime(&new_lifetime_name).syntax());
edit.add_file_edits(file_id, editor);
})
}
fn insert_new_generic_param_list_imp(
editor: &mut SyntaxEditor,
factory: &SyntaxFactory,
impl_: &ast::Impl,
lifetime_name: &str,
) -> Option<()> {
let impl_kw = impl_.impl_token()?;
editor.insert_all(
Position::after(impl_kw),
vec![
factory.token(T![<]).syntax_element(),
factory.lifetime(lifetime_name).syntax().syntax_element(),
factory.token(T![>]).syntax_element(),
],
);
Some(())
}
fn insert_lifetime_param(
editor: &mut SyntaxEditor,
factory: &SyntaxFactory,
generic_list: &ast::GenericParamList,
lifetime_name: &str,
) -> Option<()> {
let r_angle = generic_list.r_angle_token()?;
let needs_comma = generic_list.generic_params().next().is_some();
let mut elements = Vec::new();
if needs_comma {
elements.push(factory.token(T![,]).syntax_element());
elements.push(factory.whitespace(" ").syntax_element());
}
let lifetime = factory.lifetime(lifetime_name);
elements.push(lifetime.syntax().clone().into());
editor.insert_all(Position::before(r_angle), elements);
Some(())
}
#[cfg(test)]
mod tests {
use super::*;