use syntax::{
ast::{self, NameOwner},
match_ast, AstNode, SmolStr,
- SyntaxKind::{self, IDENT_PAT, TYPE_PARAM},
+ SyntaxKind::{self, IDENT_PAT, LIFETIME_PARAM, TYPE_PARAM},
TextRange,
};
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)),
}
}
}
}
}
+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<Documentation> {
let parse = db.parse(symbol.file_id);
let node = symbol.ptr.to_node(parse.tree().syntax());
},
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)?;
+use either::Either;
use hir::Semantics;
use ide_db::{
base_db::FileId,
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);
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,
}
};
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,
}
pub(crate) fn reference_definition(
sema: &Semantics<RootDatabase>,
- 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) {
}
// 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)
}
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<|> ()) {}
+ //^^^^^^^
}"#,
)
}
Adt::Enum(it) => from_def_source(db, it, mod_path),
})
}
- Definition::TypeParam(_) => {
+ Definition::TypeParam(_) | Definition::LifetimeParam(_) => {
// FIXME: Hover for generic param
None
}
self.with_db(|db| references::rename::rename(db, position, new_name))
}
+ pub fn prepare_rename(
+ &self,
+ position: FilePosition,
+ ) -> Cancelable<Result<RangeInfo<()>, RenameError>> {
+ self.with_db(|db| references::rename::prepare_rename(db, position))
+ }
+
pub fn structural_search_replace(
&self,
query: &str,
kind = ReferenceKind::FieldShorthandForLocal;
}
}
+ } else if let Definition::LifetimeParam(_) = def {
+ kind = ReferenceKind::Lifetime;
};
let declaration = Declaration { nav, kind, access: decl_access(&def, &syntax, decl_range) };
let range = name.syntax().text_range();
return Some(RangeInfo::new(range, def));
}
- let name_ref =
- sema.find_node_at_offset_with_descend::<ast::NameRef>(&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::<ast::Lifetime>(&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::<ast::NameRef>(&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<ReferenceAccess> {
}
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
+ "#]],
+ );
+ }
}
impl Error for RenameError {}
+pub(crate) fn prepare_rename(
+ db: &RootDatabase,
+ position: FilePosition,
+) -> Result<RangeInfo<()>, 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,
position: FilePosition,
new_name: &str,
) -> Result<RangeInfo<SourceChange>, 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)))
}
}
},
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)
}
}
sema: &Semantics<RootDatabase>,
position: FilePosition,
new_name: &str,
+ is_lifetime_name: bool,
) -> Result<RangeInfo<SourceChange>, 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))
);
}
+ #[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(
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!()
+ }
+}
"#,
)
}
}
return h;
}
+ Definition::LifetimeParam(_) => HighlightTag::Lifetime,
}
.into()
}
// FIXME: this badly needs rename/rewrite (matklad, 2020-02-06).
use hir::{
- db::HirDatabase, Crate, Field, HasVisibility, ImplDef, Local, MacroDef, Module, ModuleDef,
- Name, PathResolution, Semantics, TypeParam, Visibility,
+ db::HirDatabase, Crate, Field, HasVisibility, ImplDef, LifetimeParam, Local, MacroDef, Module,
+ ModuleDef, Name, PathResolution, Semantics, TypeParam, Visibility,
};
use syntax::{
ast::{self, AstNode},
- match_ast, SyntaxNode,
+ match_ast, SyntaxKind, SyntaxNode,
};
use crate::RootDatabase;
SelfType(ImplDef),
Local(Local),
TypeParam(TypeParam),
+ LifetimeParam(LifetimeParam),
+ // FIXME: Label
}
impl Definition {
Definition::SelfType(it) => Some(it.module(db)),
Definition::Local(it) => Some(it.module(db)),
Definition::TypeParam(it) => Some(it.module(db)),
+ Definition::LifetimeParam(it) => Some(it.module(db)),
}
}
Definition::SelfType(_) => None,
Definition::Local(_) => None,
Definition::TypeParam(_) => None,
+ Definition::LifetimeParam(_) => None,
}
}
Definition::SelfType(_) => return None,
Definition::Local(it) => it.name(db)?,
Definition::TypeParam(it) => it.name(db),
+ Definition::LifetimeParam(it) => it.name(db),
};
Some(name)
}
}
}
}
+
+ pub fn classify_lifetime(
+ sema: &Semantics<RootDatabase>,
+ lifetime: &ast::Lifetime,
+ ) -> Option<NameClass> {
+ let _p = profile::span("classify_lifetime").detail(|| lifetime.to_string());
+ let parent = lifetime.syntax().parent()?;
+
+ match_ast! {
+ match parent {
+ ast::LifetimeParam(it) => {
+ let def = sema.to_def(&it)?;
+ Some(NameClass::Definition(Definition::LifetimeParam(def)))
+ },
+ ast::Label(_it) => None,
+ _ => None,
+ }
+ }
+ }
}
#[derive(Debug)]
let resolved = sema.resolve_extern_crate(&extern_crate)?;
Some(NameRefClass::ExternCrate(resolved))
}
+
+ pub fn classify_lifetime(
+ sema: &Semantics<RootDatabase>,
+ lifetime: &ast::Lifetime,
+ ) -> Option<NameRefClass> {
+ let _p = profile::span("classify_lifetime_ref").detail(|| lifetime.to_string());
+ let parent = lifetime.syntax().parent()?;
+ match parent.kind() {
+ SyntaxKind::LIFETIME_ARG
+ | SyntaxKind::SELF_PARAM
+ | SyntaxKind::TYPE_BOUND
+ | SyntaxKind::WHERE_PRED
+ | SyntaxKind::REF_TYPE => sema
+ .resolve_lifetime_param(lifetime)
+ .map(Definition::LifetimeParam)
+ .map(NameRefClass::Definition),
+ // lifetime bounds, as in the 'b in 'a: 'b aren't wrapped in TypeBound nodes so we gotta check
+ // if our lifetime is in a LifetimeParam without being the constrained lifetime
+ _ if ast::LifetimeParam::cast(parent).and_then(|param| param.lifetime()).as_ref()
+ != Some(lifetime) =>
+ {
+ sema.resolve_lifetime_param(lifetime)
+ .map(Definition::LifetimeParam)
+ .map(NameRefClass::Definition)
+ }
+ SyntaxKind::BREAK_EXPR | SyntaxKind::CONTINUE_EXPR => None,
+ _ => None,
+ }
+ }
}
impl From<PathResolution> for Definition {
RecordFieldExprOrPat,
SelfKw,
EnumLiteral,
+ Lifetime,
Other,
}
return SearchScope::new(res);
}
+ if let Definition::LifetimeParam(param) = self {
+ let range = match param.parent(db) {
+ hir::GenericDef::Function(it) => it.source(db).value.syntax().text_range(),
+ hir::GenericDef::Adt(it) => match it {
+ hir::Adt::Struct(it) => it.source(db).value.syntax().text_range(),
+ hir::Adt::Union(it) => it.source(db).value.syntax().text_range(),
+ hir::Adt::Enum(it) => it.source(db).value.syntax().text_range(),
+ },
+ hir::GenericDef::Trait(it) => it.source(db).value.syntax().text_range(),
+ hir::GenericDef::TypeAlias(it) => it.source(db).value.syntax().text_range(),
+ hir::GenericDef::ImplDef(it) => it.source(db).value.syntax().text_range(),
+ hir::GenericDef::EnumVariant(it) => it.source(db).value.syntax().text_range(),
+ hir::GenericDef::Const(it) => it.source(db).value.syntax().text_range(),
+ };
+ let mut res = FxHashMap::default();
+ res.insert(file_id, Some(range));
+ return SearchScope::new(res);
+ }
+
let vis = self.visibility(db);
if let Some(Visibility::Module(module)) = vis.and_then(|it| it.into()) {
continue;
}
- match sema.find_node_at_offset_with_descend(&tree, offset) {
- Some(name_ref) => {
- if self.found_name_ref(&name_ref, sink) {
- return;
- }
+ if let Some(name_ref) = sema.find_node_at_offset_with_descend(&tree, offset) {
+ if self.found_name_ref(&name_ref, sink) {
+ return;
+ }
+ } else if let Some(name) = sema.find_node_at_offset_with_descend(&tree, offset) {
+ if self.found_name(&name, sink) {
+ return;
+ }
+ } else if let Some(lifetime) = sema.find_node_at_offset_with_descend(&tree, offset)
+ {
+ if self.found_lifetime(&lifetime, sink) {
+ return;
}
- None => match sema.find_node_at_offset_with_descend(&tree, offset) {
- Some(name) => {
- if self.found_name(&name, sink) {
- return;
- }
- }
- None => {}
- },
}
}
}
}
+ fn found_lifetime(
+ &self,
+ lifetime: &ast::Lifetime,
+ sink: &mut dyn FnMut(Reference) -> bool,
+ ) -> bool {
+ match NameRefClass::classify_lifetime(self.sema, lifetime) {
+ Some(NameRefClass::Definition(def)) if &def == self.def => {
+ let reference = Reference {
+ file_range: self.sema.original_range(lifetime.syntax()),
+ kind: ReferenceKind::Lifetime,
+ access: None,
+ };
+ sink(reference)
+ }
+ _ => false, // not a usage
+ }
+ }
+
fn found_name_ref(
&self,
name_ref: &ast::NameRef,
query.search(&buf)
}
-pub fn index_resolve(db: &RootDatabase, name_ref: &ast::NameRef) -> Vec<FileSymbol> {
- let name = name_ref.text();
+pub fn index_resolve(db: &RootDatabase, name: &SmolStr) -> Vec<FileSymbol> {
let mut query = Query::new(name.to_string());
query.exact();
query.limit(4);
let _p = profile::span("handle_prepare_rename");
let position = from_proto::file_position(&snap, params)?;
- let change = snap.analysis.rename(position, "dummy")??;
+ let change = snap.analysis.prepare_rename(position)??;
let line_index = snap.analysis.file_line_index(position.file_id)?;
let range = to_proto::range(&line_index, change.range);
Ok(Some(PrepareRenameResponse::Range(range)))