From 55faa2daa3fc8bd213038a012b1c5e9ad5fd3736 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 16 Dec 2020 21:35:15 +0100 Subject: Lifetime reference search --- crates/ide/src/display/navigation_target.rs | 20 +++++- crates/ide/src/doc_links.rs | 5 +- crates/ide/src/goto_definition.rs | 57 ++++++++++++--- crates/ide/src/hover.rs | 2 +- crates/ide/src/lib.rs | 7 ++ crates/ide/src/references.rs | 91 ++++++++++++++++++++++-- crates/ide/src/references/rename.rs | 104 ++++++++++++++++++++++++++-- crates/ide/src/syntax_highlighting.rs | 1 + 8 files changed, 265 insertions(+), 22 deletions(-) (limited to 'crates/ide') diff --git a/crates/ide/src/display/navigation_target.rs b/crates/ide/src/display/navigation_target.rs index 234f80a3a..ce0f4214c 100644 --- a/crates/ide/src/display/navigation_target.rs +++ b/crates/ide/src/display/navigation_target.rs @@ -9,7 +9,7 @@ use ide_db::{defs::Definition, RootDatabase}; use syntax::{ ast::{self, NameOwner}, match_ast, AstNode, SmolStr, - SyntaxKind::{self, IDENT_PAT, TYPE_PARAM}, + SyntaxKind::{self, IDENT_PAT, LIFETIME_PARAM, TYPE_PARAM}, TextRange, }; @@ -182,6 +182,7 @@ impl TryToNav for Definition { Definition::SelfType(it) => Some(it.to_nav(db)), Definition::Local(it) => Some(it.to_nav(db)), Definition::TypeParam(it) => Some(it.to_nav(db)), + Definition::LifetimeParam(it) => Some(it.to_nav(db)), } } } @@ -376,6 +377,23 @@ impl ToNav for hir::TypeParam { } } +impl ToNav for hir::LifetimeParam { + fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { + let src = self.source(db); + let full_range = src.value.syntax().text_range(); + NavigationTarget { + file_id: src.file_id.original_file(db), + name: self.name(db).to_string().into(), + kind: LIFETIME_PARAM, + full_range, + focus_range: Some(full_range), + container_name: None, + description: None, + docs: None, + } + } +} + pub(crate) fn docs_from_symbol(db: &RootDatabase, symbol: &FileSymbol) -> Option { let parse = db.parse(symbol.file_id); let node = symbol.ptr.to_node(parse.tree().syntax()); diff --git a/crates/ide/src/doc_links.rs b/crates/ide/src/doc_links.rs index 10263537a..2b5794a31 100644 --- a/crates/ide/src/doc_links.rs +++ b/crates/ide/src/doc_links.rs @@ -190,7 +190,10 @@ fn rewrite_intra_doc_link( }, Definition::Macro(it) => it.resolve_doc_path(db, link, ns), Definition::Field(it) => it.resolve_doc_path(db, link, ns), - Definition::SelfType(_) | Definition::Local(_) | Definition::TypeParam(_) => return None, + Definition::SelfType(_) + | Definition::Local(_) + | Definition::TypeParam(_) + | Definition::LifetimeParam(_) => return None, }?; let krate = resolved.module(db)?.krate(); let canonical_path = resolved.canonical_path(db)?; diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index b9810457f..173509b08 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -1,3 +1,4 @@ +use either::Either; use hir::Semantics; use ide_db::{ base_db::FileId, @@ -33,7 +34,7 @@ pub(crate) fn goto_definition( let nav_targets = match_ast! { match parent { ast::NameRef(name_ref) => { - reference_definition(&sema, &name_ref).to_vec() + reference_definition(&sema, Either::Right(&name_ref)).to_vec() }, ast::Name(name) => { let def = NameClass::classify(&sema, &name)?.referenced_or_defined(sema.db); @@ -53,6 +54,13 @@ pub(crate) fn goto_definition( let self_param = func.param_list()?.self_param()?; vec![self_to_nav_target(self_param, position.file_id)?] }, + ast::Lifetime(lt) => if let Some(name_class) = NameClass::classify_lifetime(&sema, <) { + let def = name_class.referenced_or_defined(sema.db); + let nav = def.try_to_nav(sema.db)?; + vec![nav] + } else { + reference_definition(&sema, Either::Left(<)).to_vec() + }, _ => return None, } }; @@ -64,7 +72,7 @@ fn pick_best(tokens: TokenAtOffset) -> Option { return tokens.max_by_key(priority); fn priority(n: &SyntaxToken) -> usize { match n.kind() { - IDENT | INT_NUMBER | T![self] => 2, + IDENT | INT_NUMBER | LIFETIME_IDENT | T![self] => 2, kind if kind.is_trivia() => 0, _ => 1, } @@ -102,9 +110,12 @@ impl ReferenceResult { pub(crate) fn reference_definition( sema: &Semantics, - name_ref: &ast::NameRef, + name_ref: Either<&ast::Lifetime, &ast::NameRef>, ) -> ReferenceResult { - let name_kind = NameRefClass::classify(sema, name_ref); + let name_kind = name_ref.either( + |lifetime| NameRefClass::classify_lifetime(sema, lifetime), + |name_ref| NameRefClass::classify(sema, name_ref), + ); if let Some(def) = name_kind { let def = def.referenced(sema.db); return match def.try_to_nav(sema.db) { @@ -114,10 +125,9 @@ pub(crate) fn reference_definition( } // Fallback index based approach: - let navs = symbol_index::index_resolve(sema.db, name_ref) - .into_iter() - .map(|s| s.to_nav(sema.db)) - .collect(); + let name = name_ref.either(ast::Lifetime::text, ast::NameRef::text); + let navs = + symbol_index::index_resolve(sema.db, name).into_iter().map(|s| s.to_nav(sema.db)).collect(); ReferenceResult::Approximate(navs) } @@ -1033,6 +1043,37 @@ impl Foo { fn bar(&self<|>) { //^^^^ } +}"#, + ) + } + + #[test] + fn goto_lifetime_param_on_decl() { + check( + r#" +fn foo<'foobar<|>>(_: &'foobar ()) { + //^^^^^^^ +}"#, + ) + } + + #[test] + fn goto_lifetime_param_decl() { + check( + r#" +fn foo<'foobar>(_: &'foobar<|> ()) { + //^^^^^^^ +}"#, + ) + } + + #[test] + fn goto_lifetime_param_decl_nested() { + check( + r#" +fn foo<'foobar>(_: &'foobar ()) { + fn foo<'foobar>(_: &'foobar<|> ()) {} + //^^^^^^^ }"#, ) } diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index ab017d2ad..a01b0c894 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -364,7 +364,7 @@ fn hover_for_definition(db: &RootDatabase, def: Definition) -> Option { Adt::Enum(it) => from_def_source(db, it, mod_path), }) } - Definition::TypeParam(_) => { + Definition::TypeParam(_) | Definition::LifetimeParam(_) => { // FIXME: Hover for generic param None } diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 71068cac2..c5c652cda 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -528,6 +528,13 @@ impl Analysis { self.with_db(|db| references::rename::rename(db, position, new_name)) } + pub fn prepare_rename( + &self, + position: FilePosition, + ) -> Cancelable, RenameError>> { + self.with_db(|db| references::rename::prepare_rename(db, position)) + } + pub fn structural_search_replace( &self, query: &str, diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 675957fff..98190a86b 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -130,6 +130,8 @@ pub(crate) fn find_all_refs( kind = ReferenceKind::FieldShorthandForLocal; } } + } else if let Definition::LifetimeParam(_) = def { + kind = ReferenceKind::Lifetime; }; let declaration = Declaration { nav, kind, access: decl_access(&def, &syntax, decl_range) }; @@ -148,11 +150,29 @@ fn find_name( let range = name.syntax().text_range(); return Some(RangeInfo::new(range, def)); } - let name_ref = - sema.find_node_at_offset_with_descend::(&syntax, position.offset)?; - let def = NameRefClass::classify(sema, &name_ref)?.referenced(sema.db); - let range = name_ref.syntax().text_range(); - Some(RangeInfo::new(range, def)) + + let (text_range, def) = if let Some(lifetime) = + sema.find_node_at_offset_with_descend::(&syntax, position.offset) + { + if let Some(def) = NameRefClass::classify_lifetime(sema, &lifetime) + .map(|class| NameRefClass::referenced(class, sema.db)) + { + (lifetime.syntax().text_range(), def) + } else { + ( + lifetime.syntax().text_range(), + NameClass::classify_lifetime(sema, &lifetime)?.referenced_or_defined(sema.db), + ) + } + } else { + let name_ref = + sema.find_node_at_offset_with_descend::(&syntax, position.offset)?; + ( + name_ref.syntax().text_range(), + NameRefClass::classify(sema, &name_ref)?.referenced(sema.db), + ) + }; + Some(RangeInfo::new(text_range, def)) } fn decl_access(def: &Definition, syntax: &SyntaxNode, range: TextRange) -> Option { @@ -1005,4 +1025,65 @@ impl Foo { } expect.assert_eq(&actual) } + + #[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_PARAM FileId(0) 55..57 55..57 Lifetime + + FileId(0) 63..65 Lifetime + FileId(0) 71..73 Lifetime + FileId(0) 82..84 Lifetime + FileId(0) 95..97 Lifetime + FileId(0) 106..108 Lifetime + "#]], + ); + } + + #[test] + fn test_find_lifetimes_type_alias() { + check( + r#" +type Foo<'a, T> where T: 'a<|> = &'a T; +"#, + expect![[r#" + 'a LIFETIME_PARAM FileId(0) 9..11 9..11 Lifetime + + FileId(0) 25..27 Lifetime + FileId(0) 31..33 Lifetime + "#]], + ); + } + + #[test] + fn test_find_lifetimes_trait_impl() { + check( + r#" +trait Foo<'a> { + fn foo() -> &'a (); +} +impl<'a> Foo<'a> for &'a () { + fn foo() -> &'a<|> () { + unimplemented!() + } +} +"#, + expect![[r#" + 'a LIFETIME_PARAM FileId(0) 47..49 47..49 Lifetime + + FileId(0) 55..57 Lifetime + FileId(0) 64..66 Lifetime + FileId(0) 89..91 Lifetime + "#]], + ); + } } diff --git a/crates/ide/src/references/rename.rs b/crates/ide/src/references/rename.rs index 44081f210..56e923841 100644 --- a/crates/ide/src/references/rename.rs +++ b/crates/ide/src/references/rename.rs @@ -35,6 +35,29 @@ impl fmt::Display for RenameError { impl Error for RenameError {} +pub(crate) fn prepare_rename( + db: &RootDatabase, + position: FilePosition, +) -> Result, RenameError> { + let sema = Semantics::new(db); + let source_file = sema.parse(position.file_id); + let syntax = source_file.syntax(); + if let Some(module) = find_module_at_offset(&sema, position, syntax) { + rename_mod(&sema, position, module, "dummy") + } else if let Some(self_token) = + syntax.token_at_offset(position.offset).find(|t| t.kind() == SyntaxKind::SELF_KW) + { + rename_self_to_param(&sema, position, self_token, "dummy") + } else { + let range = match find_all_refs(&sema, position, None) { + Some(RangeInfo { range, .. }) => range, + None => return Err(RenameError("No references found at position".to_string())), + }; + Ok(RangeInfo::new(range, SourceChange::from(vec![]))) + } + .map(|info| RangeInfo::new(info.range, ())) +} + pub(crate) fn rename( db: &RootDatabase, position: FilePosition, @@ -49,11 +72,18 @@ pub(crate) fn rename_with_semantics( position: FilePosition, new_name: &str, ) -> Result, RenameError> { - match lex_single_syntax_kind(new_name) { + let is_lifetime_name = match lex_single_syntax_kind(new_name) { Some(res) => match res { - (SyntaxKind::IDENT, _) => (), - (SyntaxKind::UNDERSCORE, _) => (), + (SyntaxKind::IDENT, _) => false, + (SyntaxKind::UNDERSCORE, _) => false, (SyntaxKind::SELF_KW, _) => return rename_to_self(&sema, position), + (SyntaxKind::LIFETIME_IDENT, _) if new_name != "'static" && new_name != "'_" => true, + (SyntaxKind::LIFETIME_IDENT, _) => { + return Err(RenameError(format!( + "Invalid name `{0}`: Cannot rename lifetime to {0}", + new_name + ))) + } (_, Some(syntax_error)) => { return Err(RenameError(format!("Invalid name `{}`: {}", new_name, syntax_error))) } @@ -62,18 +92,21 @@ pub(crate) fn rename_with_semantics( } }, None => return Err(RenameError(format!("Invalid name `{}`: not an identifier", new_name))), - } + }; let source_file = sema.parse(position.file_id); let syntax = source_file.syntax(); - if let Some(module) = find_module_at_offset(&sema, position, syntax) { + // this is here to prevent lifetime renames from happening on modules and self + if is_lifetime_name { + rename_reference(&sema, position, new_name, is_lifetime_name) + } else if let Some(module) = find_module_at_offset(&sema, position, syntax) { rename_mod(&sema, position, module, new_name) } else if let Some(self_token) = syntax.token_at_offset(position.offset).find(|t| t.kind() == SyntaxKind::SELF_KW) { rename_self_to_param(&sema, position, self_token, new_name) } else { - rename_reference(&sema, position, new_name) + rename_reference(&sema, position, new_name, is_lifetime_name) } } @@ -355,12 +388,26 @@ fn rename_reference( sema: &Semantics, position: FilePosition, new_name: &str, + is_lifetime_name: bool, ) -> Result, RenameError> { let RangeInfo { range, info: refs } = match find_all_refs(sema, position, None) { Some(range_info) => range_info, None => return Err(RenameError("No references found at position".to_string())), }; + match (refs.declaration.kind == ReferenceKind::Lifetime, is_lifetime_name) { + (true, false) => { + return Err(RenameError(format!( + "Invalid name `{}`: not a lifetime identifier", + new_name + ))) + } + (false, true) => { + return Err(RenameError(format!("Invalid name `{}`: not an identifier", new_name))) + } + _ => (), + } + let edit = refs .into_iter() .map(|reference| source_edit_from_reference(sema, reference, new_name)) @@ -464,6 +511,24 @@ mod tests { ); } + #[test] + fn test_rename_to_invalid_identifier_lifetime() { + check( + "'foo", + r#"fn main() { let i<|> = 1; }"#, + "error: Invalid name `'foo`: not an identifier", + ); + } + + #[test] + fn test_rename_to_invalid_identifier_lifetime2() { + check( + "foo", + r#"fn main<'a>(_: &'a<|> ()) {}"#, + "error: Invalid name `foo`: not a lifetime identifier", + ); + } + #[test] fn test_rename_for_local() { check( @@ -1393,6 +1458,33 @@ struct Foo { fn foo(Foo { i: bar }: foo) -> i32 { bar } +"#, + ) + } + + #[test] + fn test_rename_lifetimes() { + check( + "'yeeee", + r#" +trait Foo<'a> { + fn foo() -> &'a (); +} +impl<'a> Foo<'a> for &'a () { + fn foo() -> &'a<|> () { + unimplemented!() + } +} +"#, + r#" +trait Foo<'a> { + fn foo() -> &'a (); +} +impl<'yeeee> Foo<'yeeee> for &'yeeee () { + fn foo() -> &'yeeee () { + unimplemented!() + } +} "#, ) } diff --git a/crates/ide/src/syntax_highlighting.rs b/crates/ide/src/syntax_highlighting.rs index 990b0f7d9..488969f1a 100644 --- a/crates/ide/src/syntax_highlighting.rs +++ b/crates/ide/src/syntax_highlighting.rs @@ -806,6 +806,7 @@ fn highlight_def(db: &RootDatabase, def: Definition) -> Highlight { } return h; } + Definition::LifetimeParam(_) => HighlightTag::Lifetime, } .into() } -- cgit v1.2.3