diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 7395b81bd291..39beed3fdb03 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -13,6 +13,7 @@ pub(crate) mod rename; use hir::Semantics; use ide_db::{ + base_db::FileId, defs::{Definition, NameClass, NameRefClass}, search::Reference, search::{ReferenceAccess, ReferenceKind, SearchScope}, @@ -20,8 +21,9 @@ use ide_db::{ }; use syntax::{ algo::find_node_at_offset, - ast::{self, NameOwner}, - match_ast, AstNode, SyntaxKind, SyntaxNode, TextRange, TokenAtOffset, + ast::{self, GenericParamsOwner, LoopBodyOwner, NameOwner, TypeBoundsOwner}, + match_ast, AstNode, NodeOrToken, SmolStr, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, + TokenAtOffset, WalkEvent, T, }; use crate::{display::TryToNav, FilePosition, FileRange, NavigationTarget, RangeInfo}; @@ -93,6 +95,10 @@ pub(crate) fn find_all_refs( return Some(res); } + if let Some(res) = try_find_lifetime_references(&syntax, position) { + return Some(res); + } + let (opt_name, search_kind) = if let Some(name) = get_struct_def_name_for_struct_literal_search(&sema, &syntax, position) { @@ -198,6 +204,359 @@ fn get_struct_def_name_for_struct_literal_search( None } +fn try_find_lifetime_references( + syntax: &SyntaxNode, + position: FilePosition, +) -> Option> { + let lifetime_token = + syntax.token_at_offset(position.offset).find(|t| t.kind() == SyntaxKind::LIFETIME)?; + let parent = lifetime_token.parent(); + + match_ast! { + match parent { + ast::LifetimeArg(_it) => find_all_lifetime_references(position, lifetime_token, &parent), + ast::SelfParam(_it) => find_all_lifetime_references(position, lifetime_token, &parent), + ast::RefType(_it) => find_all_lifetime_references(position, lifetime_token, &parent), + ast::LifetimeParam(_it) => find_all_lifetime_references(position, lifetime_token, &parent),// decl + ast::WherePred(_it) => find_all_lifetime_references(position, lifetime_token, &parent), + ast::TypeBound(_it) => find_all_lifetime_references(position, lifetime_token, &parent), + ast::Label(_it) => find_all_label_references(position, lifetime_token, &parent), // decl + ast::BreakExpr(_it) => find_all_label_references(position, lifetime_token, &parent), + ast::ContinueExpr(_it) => find_all_label_references(position, lifetime_token, &parent), + _ => None, + } + } +} + +fn find_all_label_references( + position: FilePosition, + lifetime_token: SyntaxToken, + syntax: &SyntaxNode, +) -> Option> { + let label_text = lifetime_token.text(); + let label = syntax.ancestors().find_map(|syn| { + (match_ast! { + // all of these expressions declare loop labels + match syn { + ast::EffectExpr(it) => it.label(), + ast::LoopExpr(it) => it.label(), + ast::WhileExpr(it) => it.label(), + ast::ForExpr(it) => it.label(), + _ => None, + } + }) + .filter(|label| label.lifetime_token().as_ref().map(|lt| lt.text()) == Some(label_text)) + })?; + let label_parent = label.syntax().parent()?; + let expr = match_ast! { + match label_parent { + ast::EffectExpr(it) => it.block_expr()?.syntax().clone(), + ast::LoopExpr(it) => it.loop_body()?.syntax().clone(), + ast::WhileExpr(it) => it.loop_body()?.syntax().clone(), + ast::ForExpr(it) => it.loop_body()?.syntax().clone(), + _ => return None, + } + }; + let references = expr + .descendants() + .filter_map(|syn| { + match_ast! { + // only these expressions may refer to labels + match syn { + ast::BreakExpr(it) => it.lifetime_token().filter(|lt| lt.text() == label_text), + ast::ContinueExpr(it) => it.lifetime_token().filter(|lt| lt.text() == label_text), + _ => None, + } + } + }) + .map(|token| Reference { + file_range: FileRange { file_id: position.file_id, range: token.text_range() }, + kind: ReferenceKind::Lifetime, + access: None, + }) + .collect(); + + let lt = label.lifetime_token()?; + let declaration = Declaration { + nav: NavigationTarget { + file_id: position.file_id, + full_range: lt.text_range(), + focus_range: Some(lt.text_range()), + name: label_text.clone(), + kind: lt.kind(), + container_name: None, + description: None, + docs: None, + }, + kind: ReferenceKind::Lifetime, + access: None, + }; + Some(RangeInfo::new( + lifetime_token.text_range(), + ReferenceSearchResult { declaration, references }, + )) +} + +fn find_all_lifetime_references( + position: FilePosition, + lifetime_token: SyntaxToken, + syntax: &SyntaxNode, +) -> Option> { + let lifetime_text = lifetime_token.text(); + // we need to look for something that holds a GenericParamList as this is the declaration site for lifetimes + let (lifetime_param, generic_param_list, where_clause) = + syntax.ancestors().find_map(|syn| { + let (gpl, where_clause) = match_ast! { + match syn { + ast::Fn(it) => (it.generic_param_list()?, it.where_clause()), + ast::TypeAlias(it) => (it.generic_param_list()?, it.where_clause()), + ast::Struct(it) => (it.generic_param_list()?, it.where_clause()), + ast::Enum(it) => (it.generic_param_list()?, it.where_clause()), + ast::Union(it) => (it.generic_param_list()?, it.where_clause()), + ast::Trait(it) => (it.generic_param_list()?, it.where_clause()), + ast::Impl(it) => (it.generic_param_list()?, it.where_clause()), + ast::WherePred(it) => (it.generic_param_list()?, None), + ast::ForType(it) => (it.generic_param_list()?, None), + _ => return None, + } + }; + Some(( + gpl.lifetime_params().find(|tp| { + tp.lifetime_token().as_ref().map(|lt| lt.text()) == Some(lifetime_text) + })?, + gpl, + where_clause, + )) + })?; + let lt = lifetime_param.lifetime_token()?; + let mut references = Vec::new(); + + // find references in the GenericParamList itself + for param in generic_param_list.generic_params().filter(|gp| { + !matches!( + gp, + ast::GenericParam::LifetimeParam(lp) if lp.lifetime_token().as_ref() == Some(<) + ) + }) { + find_lifetime_references_in( + &mut references, + lifetime_text, + position.file_id, + param.syntax(), + ); + } + // find references in the WhereClause if it exists + if let Some(where_clause) = where_clause { + for predicate in where_clause.predicates() { + find_lifetime_references_in( + &mut references, + lifetime_text, + position.file_id, + predicate.syntax(), + ); + } + } + let gpl_parent = generic_param_list.syntax().parent()?; + // find references in the other inner nodes of whatever we are in + find_lifetime_references_in_declaring_node( + &mut references, + lifetime_text, + position.file_id, + gpl_parent, + ); + + let declaration = Declaration { + nav: NavigationTarget { + file_id: position.file_id, + full_range: lt.text_range(), + focus_range: Some(lt.text_range()), + name: lifetime_text.clone(), + kind: lt.kind(), + container_name: None, + description: None, + docs: None, + }, + kind: ReferenceKind::Lifetime, + access: None, + }; + Some(RangeInfo::new( + lifetime_token.text_range(), + ReferenceSearchResult { declaration, references }, + )) +} + +/// Searches for lifetime references in the node that declared the lifetime ignoring its WhereClause +/// and GenericParamlist. +fn find_lifetime_references_in_declaring_node( + references: &mut Vec, + lifetime_text: &SmolStr, + file_id: FileId, + gpl_parent: SyntaxNode, +) { + match_ast! { + match gpl_parent { + ast::Fn(it) => { + if let Some(param_list) = it.param_list() { + find_lifetime_references_in(references, lifetime_text, file_id, param_list.syntax()); + } + if let Some(ret_type) = it.ret_type() { + find_lifetime_references_in(references, lifetime_text, file_id, ret_type.syntax()); + } + if let Some(body) = it.body() { + find_lifetime_references_in_fn(references, lifetime_text, file_id, &body); + } + }, + ast::TypeAlias(it) => { + if let Some(type_bound_list) = it.type_bound_list() { + find_lifetime_references_in(references, lifetime_text, file_id, type_bound_list.syntax()); + } + if let Some(ty) = it.ty() { + find_lifetime_references_in(references, lifetime_text, file_id, ty.syntax()); + } + }, + ast::Struct(it) => if let Some(field_list) = it.field_list() { + find_lifetime_references_in(references, lifetime_text, file_id, field_list.syntax()); + }, + ast::Enum(it) => if let Some(variant_list) = it.variant_list() { + find_lifetime_references_in(references, lifetime_text, file_id, variant_list.syntax()); + }, + ast::Union(it) => if let Some(record_field_list) = it.record_field_list() { + find_lifetime_references_in(references, lifetime_text, file_id, record_field_list.syntax()); + }, + ast::Trait(it) => { + if let Some(type_bound_list) = it.type_bound_list() { + find_lifetime_references_in(references, lifetime_text, file_id, type_bound_list.syntax()); + } + if let Some(assoc_item_list) = it.assoc_item_list() { + find_lifetime_references_in_assoc_list(references, lifetime_text, file_id, &assoc_item_list); + } + }, + ast::Impl(it) => { + if let Some(trait_) = it.trait_() { + find_lifetime_references_in(references, lifetime_text, file_id, trait_.syntax()); + } + if let Some(self_ty) = it.self_ty() { + find_lifetime_references_in(references, lifetime_text, file_id, self_ty.syntax()); + } + if let Some(assoc_item_list) = it.assoc_item_list() { + find_lifetime_references_in_assoc_list(references, lifetime_text, file_id, &assoc_item_list); + } + }, + ast::WherePred(it) => if let Some(ty) = it.ty() { + find_lifetime_references_in(references, lifetime_text, file_id, ty.syntax()); + }, + ast::ForType(it) => if let Some(ty) = it.ty() { + find_lifetime_references_in(references, lifetime_text, file_id, ty.syntax()); + }, + _ => (), + } + }; +} + +/// Searches for lifetime references in the given syntax node by doing token text matching. +fn find_lifetime_references_in( + references: &mut Vec, + lifetime_text: &SmolStr, + file_id: FileId, + syntax: &SyntaxNode, +) { + references.extend(syntax.descendants_with_tokens().filter_map(|ele| match ele { + NodeOrToken::Token(token) if token.text() == lifetime_text => Some(Reference { + file_range: FileRange { file_id, range: token.text_range() }, + kind: ReferenceKind::Lifetime, + access: None, + }), + _ => None, + })); +} + +/// Searches for lifetimes in an AssocItemList from an Impl or Trait, this is required to delegate +/// the special searching for associated functions via [`find_lifetime_references_in_fn`]. +fn find_lifetime_references_in_assoc_list( + references: &mut Vec, + lifetime_text: &SmolStr, + file_id: FileId, + assoc_items: &ast::AssocItemList, +) { + for assoc_item in assoc_items.assoc_items() { + // assoc items can't shadow lifetime variables + match assoc_item { + ast::AssocItem::Fn(it) => { + if let Some(it) = it.generic_param_list() { + find_lifetime_references_in(references, lifetime_text, file_id, it.syntax()); + } + if let Some(it) = it.param_list() { + find_lifetime_references_in(references, lifetime_text, file_id, it.syntax()); + } + if let Some(it) = it.ret_type() { + find_lifetime_references_in(references, lifetime_text, file_id, it.syntax()); + } + if let Some(it) = it.where_clause() { + find_lifetime_references_in(references, lifetime_text, file_id, it.syntax()); + } + if let Some(body) = it.body() { + find_lifetime_references_in_fn(references, lifetime_text, file_id, &body); + } + } + ast::AssocItem::MacroCall(_) => (/* FIXME */), + _ => { + find_lifetime_references_in(references, lifetime_text, file_id, assoc_item.syntax()) + } + } + } +} + +/// Searches for the given lifetime text in the functions body. This is special cased to skip inner +/// defined items as these can shadow the lifetime we are looking for. +fn find_lifetime_references_in_fn( + references: &mut Vec, + lifetime_text: &SmolStr, + file_id: FileId, + body: &ast::BlockExpr, +) { + // skip inner items inside the function as they may redeclare a lifetime with the same name + // as the one we are looking for, inner items may also not re-use outer generics without redeclaring + // them so they are safe to skip + let items = [ + SyntaxKind::STRUCT, + SyntaxKind::ENUM, + SyntaxKind::FN, + SyntaxKind::UNION, + SyntaxKind::TYPE_ALIAS, + SyntaxKind::IMPL, + SyntaxKind::TRAIT, + ]; + let mut skip_until = None; + for event in body.syntax().preorder_with_tokens() { + if let Some(kind) = skip_until { + if matches!( + event, + WalkEvent::Leave(NodeOrToken::Node(node)) if node.kind() == kind + ) { + skip_until = None + } + } else { + match event { + WalkEvent::Enter(NodeOrToken::Node(node)) => { + if items.contains(&node.kind()) { + skip_until = Some(node.kind()); + } + } + WalkEvent::Enter(NodeOrToken::Token(token)) + if token.kind() == T![lifetime] && token.text() == lifetime_text => + { + references.push(Reference { + file_range: FileRange { file_id, range: token.text_range() }, + kind: ReferenceKind::Lifetime, + access: None, + }); + } + _ => (), + } + } + } +} + fn try_find_self_references( syntax: &SyntaxNode, position: FilePosition, @@ -863,6 +1222,113 @@ impl Foo { ); } + #[test] + fn test_find_labels() { + check( + r#" +fn main() { + 'foo: loop { + 'bar: while true { + continue 'foo; + } + break 'foo<|>; + } +} +"#, + expect![[r#" + 'foo LIFETIME FileId(0) 16..20 16..20 Lifetime + + FileId(0) 77..81 Lifetime + FileId(0) 107..111 Lifetime + "#]], + ); + } + + #[test] + fn test_find_labels_decl() { + check( + r#" +fn main() { + 'foo<|>: loop { + 'bar: while true { + continue 'foo; + } + break 'foo; + } +} +"#, + expect![[r#" + 'foo LIFETIME FileId(0) 16..20 16..20 Lifetime + + FileId(0) 77..81 Lifetime + FileId(0) 107..111 Lifetime + "#]], + ); + } + + #[test] + fn test_find_lifetimes_function() { + check( + r#" +trait Foo<'a> {} +impl<'a> Foo<'a> for &'a () {} + +fn foo<'a, 'b: 'a>(x: &'a<|> ()) -> &'a () where &'a (): Foo<'a> { + fn bar<'a>(_: &'a ()) {} + x +} +"#, + expect![[r#" + 'a LIFETIME FileId(0) 56..58 56..58 Lifetime + + FileId(0) 64..66 Lifetime + FileId(0) 96..98 Lifetime + FileId(0) 107..109 Lifetime + FileId(0) 72..74 Lifetime + FileId(0) 83..85 Lifetime + "#]], + ); + } + + #[test] + fn test_find_type_alias() { + check( + r#" +type Foo<'a, T> where T: 'a<|> = &'a T; +"#, + expect![[r#" + 'a LIFETIME FileId(0) 9..11 9..11 Lifetime + + FileId(0) 25..27 Lifetime + FileId(0) 31..33 Lifetime + "#]], + ); + } + + #[test] + fn test_find_trait_impl() { + check( + r#" +trait Foo<'a> { + fn foo() -> &'a (); +} + +impl<'a> Foo<'a> for &'a () { + fn foo() -> &'a<|> () { + unimplemented!() + } +} +"#, + expect![[r#" + 'a LIFETIME FileId(0) 48..50 48..50 Lifetime + + FileId(0) 56..58 Lifetime + FileId(0) 65..67 Lifetime + FileId(0) 90..92 Lifetime + "#]], + ); + } + fn check(ra_fixture: &str, expect: Expect) { check_with_scope(ra_fixture, None, expect) } diff --git a/crates/ide/src/references/rename.rs b/crates/ide/src/references/rename.rs index 64fe8bd654ca..8d3e470bdff7 100644 --- a/crates/ide/src/references/rename.rs +++ b/crates/ide/src/references/rename.rs @@ -360,6 +360,10 @@ fn rename_reference( None => return Err(RenameError("No references found at position".to_string())), }; + if refs.declaration().kind == ReferenceKind::Lifetime { + return Err(RenameError("Renaming references is not yet suported".to_string())); + } + let edit = refs .into_iter() .map(|reference| source_edit_from_reference(sema, reference, new_name)) diff --git a/crates/ide_db/src/search.rs b/crates/ide_db/src/search.rs index 607185ca97f4..6a2f97346405 100644 --- a/crates/ide_db/src/search.rs +++ b/crates/ide_db/src/search.rs @@ -32,6 +32,7 @@ pub enum ReferenceKind { StructLiteral, RecordFieldExprOrPat, SelfKw, + Lifetime, Other, }