Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide/src/navigation_target.rs')
-rw-r--r--crates/ide/src/navigation_target.rs597
1 files changed, 597 insertions, 0 deletions
diff --git a/crates/ide/src/navigation_target.rs b/crates/ide/src/navigation_target.rs
new file mode 100644
index 0000000000..3121cdd4a2
--- /dev/null
+++ b/crates/ide/src/navigation_target.rs
@@ -0,0 +1,597 @@
+//! See [`NavigationTarget`].
+
+use std::fmt;
+
+use either::Either;
+use hir::{
+ AssocItem, Documentation, FieldSource, HasAttrs, HasSource, HirDisplay, InFile, ModuleSource,
+ Semantics,
+};
+use ide_db::{
+ base_db::{FileId, FileRange},
+ symbol_index::FileSymbolKind,
+ SymbolKind,
+};
+use ide_db::{defs::Definition, RootDatabase};
+use syntax::{
+ ast::{self, HasName},
+ match_ast, AstNode, SmolStr, TextRange,
+};
+
+use crate::FileSymbol;
+
+/// `NavigationTarget` represents an element in the editor's UI which you can
+/// click on to navigate to a particular piece of code.
+///
+/// Typically, a `NavigationTarget` corresponds to some element in the source
+/// code, like a function or a struct, but this is not strictly required.
+#[derive(Clone, PartialEq, Eq, Hash)]
+pub struct NavigationTarget {
+ pub file_id: FileId,
+ /// Range which encompasses the whole element.
+ ///
+ /// Should include body, doc comments, attributes, etc.
+ ///
+ /// Clients should use this range to answer "is the cursor inside the
+ /// element?" question.
+ pub full_range: TextRange,
+ /// A "most interesting" range within the `full_range`.
+ ///
+ /// Typically, `full_range` is the whole syntax node, including doc
+ /// comments, and `focus_range` is the range of the identifier.
+ ///
+ /// Clients should place the cursor on this range when navigating to this target.
+ pub focus_range: Option<TextRange>,
+ pub name: SmolStr,
+ pub kind: Option<SymbolKind>,
+ pub container_name: Option<SmolStr>,
+ pub description: Option<String>,
+ pub docs: Option<Documentation>,
+}
+
+impl fmt::Debug for NavigationTarget {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut f = f.debug_struct("NavigationTarget");
+ macro_rules! opt {
+ ($($name:ident)*) => {$(
+ if let Some(it) = &self.$name {
+ f.field(stringify!($name), it);
+ }
+ )*}
+ }
+ f.field("file_id", &self.file_id).field("full_range", &self.full_range);
+ opt!(focus_range);
+ f.field("name", &self.name);
+ opt!(kind container_name description docs);
+ f.finish()
+ }
+}
+
+pub(crate) trait ToNav {
+ fn to_nav(&self, db: &RootDatabase) -> NavigationTarget;
+}
+
+pub(crate) trait TryToNav {
+ fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget>;
+}
+
+impl<T: TryToNav, U: TryToNav> TryToNav for Either<T, U> {
+ fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
+ match self {
+ Either::Left(it) => it.try_to_nav(db),
+ Either::Right(it) => it.try_to_nav(db),
+ }
+ }
+}
+
+impl NavigationTarget {
+ pub fn focus_or_full_range(&self) -> TextRange {
+ self.focus_range.unwrap_or(self.full_range)
+ }
+
+ pub(crate) fn from_module_to_decl(db: &RootDatabase, module: hir::Module) -> NavigationTarget {
+ let name = module.name(db).map(|it| it.to_smol_str()).unwrap_or_default();
+ if let Some(src) = module.declaration_source(db) {
+ let node = src.syntax();
+ let full_range = node.original_file_range(db);
+ let focus_range = src
+ .value
+ .name()
+ .map(|name| src.with_value(name.syntax()).original_file_range(db).range);
+ let mut res = NavigationTarget::from_syntax(
+ full_range.file_id,
+ name,
+ focus_range,
+ full_range.range,
+ SymbolKind::Module,
+ );
+ res.docs = module.attrs(db).docs();
+ res.description = Some(module.display(db).to_string());
+ return res;
+ }
+ module.to_nav(db)
+ }
+
+ #[cfg(test)]
+ pub(crate) fn debug_render(&self) -> String {
+ let mut buf = format!(
+ "{} {:?} {:?} {:?}",
+ self.name,
+ self.kind.unwrap(),
+ self.file_id,
+ self.full_range
+ );
+ if let Some(focus_range) = self.focus_range {
+ buf.push_str(&format!(" {:?}", focus_range))
+ }
+ if let Some(container_name) = &self.container_name {
+ buf.push_str(&format!(" {}", container_name))
+ }
+ buf
+ }
+
+ /// Allows `NavigationTarget` to be created from a `NameOwner`
+ pub(crate) fn from_named(
+ db: &RootDatabase,
+ node: InFile<&dyn ast::HasName>,
+ kind: SymbolKind,
+ ) -> NavigationTarget {
+ let name = node.value.name().map(|it| it.text().into()).unwrap_or_else(|| "_".into());
+ let focus_range = node
+ .value
+ .name()
+ .and_then(|it| node.with_value(it.syntax()).original_file_range_opt(db))
+ .map(|it| it.range);
+ let frange = node.map(|it| it.syntax()).original_file_range(db);
+
+ NavigationTarget::from_syntax(frange.file_id, name, focus_range, frange.range, kind)
+ }
+
+ fn from_syntax(
+ file_id: FileId,
+ name: SmolStr,
+ focus_range: Option<TextRange>,
+ full_range: TextRange,
+ kind: SymbolKind,
+ ) -> NavigationTarget {
+ NavigationTarget {
+ file_id,
+ name,
+ kind: Some(kind),
+ full_range,
+ focus_range,
+ container_name: None,
+ description: None,
+ docs: None,
+ }
+ }
+}
+
+impl ToNav for FileSymbol {
+ fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
+ NavigationTarget {
+ file_id: self.file_id,
+ name: self.name.clone(),
+ kind: Some(match self.kind {
+ FileSymbolKind::Function => SymbolKind::Function,
+ FileSymbolKind::Struct => SymbolKind::Struct,
+ FileSymbolKind::Enum => SymbolKind::Enum,
+ FileSymbolKind::Trait => SymbolKind::Trait,
+ FileSymbolKind::Module => SymbolKind::Module,
+ FileSymbolKind::TypeAlias => SymbolKind::TypeAlias,
+ FileSymbolKind::Const => SymbolKind::Const,
+ FileSymbolKind::Static => SymbolKind::Static,
+ FileSymbolKind::Macro => SymbolKind::Macro,
+ FileSymbolKind::Union => SymbolKind::Union,
+ }),
+ full_range: self.range,
+ focus_range: self.name_range,
+ container_name: self.container_name.clone(),
+ description: description_from_symbol(db, self),
+ docs: None,
+ }
+ }
+}
+
+impl TryToNav for Definition {
+ fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
+ match self {
+ Definition::Local(it) => Some(it.to_nav(db)),
+ Definition::Label(it) => Some(it.to_nav(db)),
+ Definition::Module(it) => Some(it.to_nav(db)),
+ Definition::Macro(it) => it.try_to_nav(db),
+ Definition::Field(it) => it.try_to_nav(db),
+ Definition::SelfType(it) => it.try_to_nav(db),
+ Definition::GenericParam(it) => it.try_to_nav(db),
+ Definition::Function(it) => it.try_to_nav(db),
+ Definition::Adt(it) => it.try_to_nav(db),
+ Definition::Variant(it) => it.try_to_nav(db),
+ Definition::Const(it) => it.try_to_nav(db),
+ Definition::Static(it) => it.try_to_nav(db),
+ Definition::Trait(it) => it.try_to_nav(db),
+ Definition::TypeAlias(it) => it.try_to_nav(db),
+ Definition::BuiltinType(_) => None,
+ }
+ }
+}
+
+impl TryToNav for hir::ModuleDef {
+ fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
+ match self {
+ hir::ModuleDef::Module(it) => Some(it.to_nav(db)),
+ hir::ModuleDef::Function(it) => it.try_to_nav(db),
+ hir::ModuleDef::Adt(it) => it.try_to_nav(db),
+ hir::ModuleDef::Variant(it) => it.try_to_nav(db),
+ hir::ModuleDef::Const(it) => it.try_to_nav(db),
+ hir::ModuleDef::Static(it) => it.try_to_nav(db),
+ hir::ModuleDef::Trait(it) => it.try_to_nav(db),
+ hir::ModuleDef::TypeAlias(it) => it.try_to_nav(db),
+ hir::ModuleDef::BuiltinType(_) => None,
+ }
+ }
+}
+
+pub(crate) trait ToNavFromAst {
+ const KIND: SymbolKind;
+}
+impl ToNavFromAst for hir::Function {
+ const KIND: SymbolKind = SymbolKind::Function;
+}
+impl ToNavFromAst for hir::Const {
+ const KIND: SymbolKind = SymbolKind::Const;
+}
+impl ToNavFromAst for hir::Static {
+ const KIND: SymbolKind = SymbolKind::Static;
+}
+impl ToNavFromAst for hir::Struct {
+ const KIND: SymbolKind = SymbolKind::Struct;
+}
+impl ToNavFromAst for hir::Enum {
+ const KIND: SymbolKind = SymbolKind::Enum;
+}
+impl ToNavFromAst for hir::Variant {
+ const KIND: SymbolKind = SymbolKind::Variant;
+}
+impl ToNavFromAst for hir::Union {
+ const KIND: SymbolKind = SymbolKind::Union;
+}
+impl ToNavFromAst for hir::TypeAlias {
+ const KIND: SymbolKind = SymbolKind::TypeAlias;
+}
+impl ToNavFromAst for hir::Trait {
+ const KIND: SymbolKind = SymbolKind::Trait;
+}
+
+impl<D> TryToNav for D
+where
+ D: HasSource + ToNavFromAst + Copy + HasAttrs + HirDisplay,
+ D::Ast: ast::HasName,
+{
+ fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
+ let src = self.source(db)?;
+ let mut res = NavigationTarget::from_named(
+ db,
+ src.as_ref().map(|it| it as &dyn ast::HasName),
+ D::KIND,
+ );
+ res.docs = self.docs(db);
+ res.description = Some(self.display(db).to_string());
+ Some(res)
+ }
+}
+
+impl ToNav for hir::Module {
+ fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
+ let src = self.definition_source(db);
+ let name = self.name(db).map(|it| it.to_smol_str()).unwrap_or_default();
+ let (syntax, focus) = match &src.value {
+ ModuleSource::SourceFile(node) => (node.syntax(), None),
+ ModuleSource::Module(node) => {
+ (node.syntax(), node.name().map(|it| it.syntax().text_range()))
+ }
+ ModuleSource::BlockExpr(node) => (node.syntax(), None),
+ };
+ let frange = src.with_value(syntax).original_file_range(db);
+ NavigationTarget::from_syntax(frange.file_id, name, focus, frange.range, SymbolKind::Module)
+ }
+}
+
+impl TryToNav for hir::Impl {
+ fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
+ let src = self.source(db)?;
+ let derive_attr = self.is_builtin_derive(db);
+ let frange = match &derive_attr {
+ Some(item) => item.syntax().original_file_range(db),
+ None => src.syntax().original_file_range(db),
+ };
+ let focus_range = if derive_attr.is_some() {
+ None
+ } else {
+ src.value.self_ty().map(|ty| src.with_value(ty.syntax()).original_file_range(db).range)
+ };
+
+ Some(NavigationTarget::from_syntax(
+ frange.file_id,
+ "impl".into(),
+ focus_range,
+ frange.range,
+ SymbolKind::Impl,
+ ))
+ }
+}
+
+impl TryToNav for hir::Field {
+ fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
+ let src = self.source(db)?;
+
+ let field_source = match &src.value {
+ FieldSource::Named(it) => {
+ let mut res =
+ NavigationTarget::from_named(db, src.with_value(it), SymbolKind::Field);
+ res.docs = self.docs(db);
+ res.description = Some(self.display(db).to_string());
+ res
+ }
+ FieldSource::Pos(it) => {
+ let frange = src.with_value(it.syntax()).original_file_range(db);
+ NavigationTarget::from_syntax(
+ frange.file_id,
+ "".into(),
+ None,
+ frange.range,
+ SymbolKind::Field,
+ )
+ }
+ };
+ Some(field_source)
+ }
+}
+
+impl TryToNav for hir::MacroDef {
+ fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
+ let src = self.source(db)?;
+ let name_owner: &dyn ast::HasName = match &src.value {
+ Either::Left(it) => it,
+ Either::Right(it) => it,
+ };
+ tracing::debug!("nav target {:#?}", name_owner.syntax());
+ let mut res = NavigationTarget::from_named(
+ db,
+ src.as_ref().with_value(name_owner),
+ SymbolKind::Macro,
+ );
+ res.docs = self.docs(db);
+ Some(res)
+ }
+}
+
+impl TryToNav for hir::Adt {
+ fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
+ match self {
+ hir::Adt::Struct(it) => it.try_to_nav(db),
+ hir::Adt::Union(it) => it.try_to_nav(db),
+ hir::Adt::Enum(it) => it.try_to_nav(db),
+ }
+ }
+}
+
+impl TryToNav for hir::AssocItem {
+ fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
+ match self {
+ AssocItem::Function(it) => it.try_to_nav(db),
+ AssocItem::Const(it) => it.try_to_nav(db),
+ AssocItem::TypeAlias(it) => it.try_to_nav(db),
+ }
+ }
+}
+
+impl TryToNav for hir::GenericParam {
+ fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
+ match self {
+ hir::GenericParam::TypeParam(it) => it.try_to_nav(db),
+ hir::GenericParam::ConstParam(it) => it.try_to_nav(db),
+ hir::GenericParam::LifetimeParam(it) => it.try_to_nav(db),
+ }
+ }
+}
+
+impl ToNav for hir::Local {
+ fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
+ let src = self.source(db);
+ let (node, name) = match &src.value {
+ Either::Left(bind_pat) => (bind_pat.syntax().clone(), bind_pat.name()),
+ Either::Right(it) => (it.syntax().clone(), it.name()),
+ };
+ let focus_range =
+ name.map(|it| src.with_value(&it.syntax().clone()).original_file_range(db).range);
+
+ let full_range = src.with_value(&node).original_file_range(db);
+ let name = match self.name(db) {
+ Some(it) => it.to_smol_str(),
+ None => "".into(),
+ };
+ let kind = if self.is_self(db) {
+ SymbolKind::SelfParam
+ } else if self.is_param(db) {
+ SymbolKind::ValueParam
+ } else {
+ SymbolKind::Local
+ };
+ NavigationTarget {
+ file_id: full_range.file_id,
+ name,
+ kind: Some(kind),
+ full_range: full_range.range,
+ focus_range,
+ container_name: None,
+ description: None,
+ docs: None,
+ }
+ }
+}
+
+impl ToNav for hir::Label {
+ fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
+ let src = self.source(db);
+ let node = src.value.syntax();
+ let FileRange { file_id, range } = src.with_value(node).original_file_range(db);
+ let focus_range =
+ src.value.lifetime().and_then(|lt| lt.lifetime_ident_token()).map(|lt| lt.text_range());
+ let name = self.name(db).to_smol_str();
+ NavigationTarget {
+ file_id,
+ name,
+ kind: Some(SymbolKind::Label),
+ full_range: range,
+ focus_range,
+ container_name: None,
+ description: None,
+ docs: None,
+ }
+ }
+}
+
+impl TryToNav for hir::TypeParam {
+ fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
+ let src = self.source(db)?;
+ let full_range = match &src.value {
+ Either::Left(type_param) => type_param.syntax().text_range(),
+ Either::Right(trait_) => trait_
+ .name()
+ .map_or_else(|| trait_.syntax().text_range(), |name| name.syntax().text_range()),
+ };
+ let focus_range = match &src.value {
+ Either::Left(it) => it.name(),
+ Either::Right(it) => it.name(),
+ }
+ .map(|it| it.syntax().text_range());
+ Some(NavigationTarget {
+ file_id: src.file_id.original_file(db),
+ name: self.name(db).to_smol_str(),
+ kind: Some(SymbolKind::TypeParam),
+ full_range,
+ focus_range,
+ container_name: None,
+ description: None,
+ docs: None,
+ })
+ }
+}
+
+impl TryToNav for hir::LifetimeParam {
+ fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
+ let src = self.source(db)?;
+ let full_range = src.value.syntax().text_range();
+ Some(NavigationTarget {
+ file_id: src.file_id.original_file(db),
+ name: self.name(db).to_smol_str(),
+ kind: Some(SymbolKind::LifetimeParam),
+ full_range,
+ focus_range: Some(full_range),
+ container_name: None,
+ description: None,
+ docs: None,
+ })
+ }
+}
+
+impl TryToNav for hir::ConstParam {
+ fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> {
+ let src = self.source(db)?;
+ let full_range = src.value.syntax().text_range();
+ Some(NavigationTarget {
+ file_id: src.file_id.original_file(db),
+ name: self.name(db).to_smol_str(),
+ kind: Some(SymbolKind::ConstParam),
+ full_range,
+ focus_range: src.value.name().map(|n| n.syntax().text_range()),
+ container_name: None,
+ description: None,
+ docs: None,
+ })
+ }
+}
+
+/// Get a description of a symbol.
+///
+/// e.g. `struct Name`, `enum Name`, `fn Name`
+pub(crate) fn description_from_symbol(db: &RootDatabase, symbol: &FileSymbol) -> Option<String> {
+ let sema = Semantics::new(db);
+ let parse = sema.parse(symbol.file_id);
+ let node = symbol.ptr.to_node(parse.syntax());
+
+ match_ast! {
+ match node {
+ ast::Fn(it) => sema.to_def(&it).map(|it| it.display(db).to_string()),
+ ast::Struct(it) => sema.to_def(&it).map(|it| it.display(db).to_string()),
+ ast::Enum(it) => sema.to_def(&it).map(|it| it.display(db).to_string()),
+ ast::Trait(it) => sema.to_def(&it).map(|it| it.display(db).to_string()),
+ ast::Module(it) => sema.to_def(&it).map(|it| it.display(db).to_string()),
+ ast::TypeAlias(it) => sema.to_def(&it).map(|it| it.display(db).to_string()),
+ ast::Const(it) => sema.to_def(&it).map(|it| it.display(db).to_string()),
+ ast::Static(it) => sema.to_def(&it).map(|it| it.display(db).to_string()),
+ ast::RecordField(it) => sema.to_def(&it).map(|it| it.display(db).to_string()),
+ ast::Variant(it) => sema.to_def(&it).map(|it| it.display(db).to_string()),
+ ast::Union(it) => sema.to_def(&it).map(|it| it.display(db).to_string()),
+ _ => None,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use expect_test::expect;
+
+ use crate::{fixture, Query};
+
+ #[test]
+ fn test_nav_for_symbol() {
+ let (analysis, _) = fixture::file(
+ r#"
+enum FooInner { }
+fn foo() { enum FooInner { } }
+"#,
+ );
+
+ let navs = analysis.symbol_search(Query::new("FooInner".to_string())).unwrap();
+ expect![[r#"
+ [
+ NavigationTarget {
+ file_id: FileId(
+ 0,
+ ),
+ full_range: 0..17,
+ focus_range: 5..13,
+ name: "FooInner",
+ kind: Enum,
+ description: "enum FooInner",
+ },
+ NavigationTarget {
+ file_id: FileId(
+ 0,
+ ),
+ full_range: 29..46,
+ focus_range: 34..42,
+ name: "FooInner",
+ kind: Enum,
+ container_name: "foo",
+ description: "enum FooInner",
+ },
+ ]
+ "#]]
+ .assert_debug_eq(&navs);
+ }
+
+ #[test]
+ fn test_world_symbols_are_case_sensitive() {
+ let (analysis, _) = fixture::file(
+ r#"
+fn foo() {}
+struct Foo;
+"#,
+ );
+
+ let navs = analysis.symbol_search(Query::new("foo".to_string())).unwrap();
+ assert_eq!(navs.len(), 2)
+ }
+}