From 1b0c7701cc97cd7bef8bb9729011d4cf291a60c5 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Thu, 13 Aug 2020 17:42:52 +0200 Subject: Rename ra_ide -> ide --- crates/ide/src/display/navigation_target.rs | 491 ++++++++++++++++++++++++++++ crates/ide/src/display/short_label.rs | 111 +++++++ 2 files changed, 602 insertions(+) create mode 100644 crates/ide/src/display/navigation_target.rs create mode 100644 crates/ide/src/display/short_label.rs (limited to 'crates/ide/src/display') diff --git a/crates/ide/src/display/navigation_target.rs b/crates/ide/src/display/navigation_target.rs new file mode 100644 index 000000000..e77106177 --- /dev/null +++ b/crates/ide/src/display/navigation_target.rs @@ -0,0 +1,491 @@ +//! FIXME: write short doc here + +use base_db::{FileId, SourceDatabase}; +use either::Either; +use hir::{original_range, AssocItem, FieldSource, HasSource, InFile, ModuleSource}; +use ide_db::{defs::Definition, RootDatabase}; +use syntax::{ + ast::{self, DocCommentsOwner, NameOwner}, + match_ast, AstNode, SmolStr, + SyntaxKind::{self, IDENT_PAT, TYPE_PARAM}, + TextRange, +}; + +use crate::FileSymbol; + +use super::short_label::ShortLabel; + +/// `NavigationTarget` represents and 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(Debug, 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 withing the `full_range`. + /// + /// Typically, `full_range` is the whole syntax node, including doc + /// comments, and `focus_range` is the range of the identifier. "Most + /// interesting" range within the full range, typically the range of + /// identifier. + /// + /// Clients should place the cursor on this range when navigating to this target. + pub focus_range: Option, + pub name: SmolStr, + pub kind: SyntaxKind, + pub container_name: Option, + pub description: Option, + pub docs: Option, +} + +pub(crate) trait ToNav { + fn to_nav(&self, db: &RootDatabase) -> NavigationTarget; +} + +pub(crate) trait TryToNav { + fn try_to_nav(&self, db: &RootDatabase) -> Option; +} + +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_string().into()).unwrap_or_default(); + if let Some(src) = module.declaration_source(db) { + let frange = original_range(db, src.as_ref().map(|it| it.syntax())); + let mut res = NavigationTarget::from_syntax( + frange.file_id, + name, + None, + frange.range, + src.value.syntax().kind(), + ); + res.docs = src.value.doc_comment_text(); + res.description = src.value.short_label(); + return res; + } + module.to_nav(db) + } + + #[cfg(test)] + pub(crate) fn assert_match(&self, expected: &str) { + let actual = self.debug_render(); + test_utils::assert_eq_text!(expected.trim(), actual.trim(),); + } + + #[cfg(test)] + pub(crate) fn debug_render(&self) -> String { + let mut buf = + format!("{} {:?} {:?} {:?}", self.name, self.kind, 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::NameOwner>, + ) -> NavigationTarget { + let name = + node.value.name().map(|it| it.text().clone()).unwrap_or_else(|| SmolStr::new("_")); + let focus_range = + node.value.name().map(|it| original_range(db, node.with_value(it.syntax())).range); + let frange = original_range(db, node.map(|it| it.syntax())); + + NavigationTarget::from_syntax( + frange.file_id, + name, + focus_range, + frange.range, + node.value.syntax().kind(), + ) + } + + /// Allows `NavigationTarget` to be created from a `DocCommentsOwner` and a `NameOwner` + pub(crate) fn from_doc_commented( + db: &RootDatabase, + named: InFile<&dyn ast::NameOwner>, + node: InFile<&dyn ast::DocCommentsOwner>, + ) -> NavigationTarget { + let name = + named.value.name().map(|it| it.text().clone()).unwrap_or_else(|| SmolStr::new("_")); + let frange = original_range(db, node.map(|it| it.syntax())); + + NavigationTarget::from_syntax( + frange.file_id, + name, + None, + frange.range, + node.value.syntax().kind(), + ) + } + + fn from_syntax( + file_id: FileId, + name: SmolStr, + focus_range: Option, + full_range: TextRange, + kind: SyntaxKind, + ) -> NavigationTarget { + NavigationTarget { + file_id, + name, + 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: self.kind, + full_range: self.range, + focus_range: self.name_range, + container_name: self.container_name.clone(), + description: description_from_symbol(db, self), + docs: docs_from_symbol(db, self), + } + } +} + +impl TryToNav for Definition { + fn try_to_nav(&self, db: &RootDatabase) -> Option { + match self { + Definition::Macro(it) => Some(it.to_nav(db)), + Definition::Field(it) => Some(it.to_nav(db)), + Definition::ModuleDef(it) => it.try_to_nav(db), + Definition::SelfType(it) => Some(it.to_nav(db)), + Definition::Local(it) => Some(it.to_nav(db)), + Definition::TypeParam(it) => Some(it.to_nav(db)), + } + } +} + +impl TryToNav for hir::ModuleDef { + fn try_to_nav(&self, db: &RootDatabase) -> Option { + let res = match self { + hir::ModuleDef::Module(it) => it.to_nav(db), + hir::ModuleDef::Function(it) => it.to_nav(db), + hir::ModuleDef::Adt(it) => it.to_nav(db), + hir::ModuleDef::EnumVariant(it) => it.to_nav(db), + hir::ModuleDef::Const(it) => it.to_nav(db), + hir::ModuleDef::Static(it) => it.to_nav(db), + hir::ModuleDef::Trait(it) => it.to_nav(db), + hir::ModuleDef::TypeAlias(it) => it.to_nav(db), + hir::ModuleDef::BuiltinType(_) => return None, + }; + Some(res) + } +} + +pub(crate) trait ToNavFromAst {} +impl ToNavFromAst for hir::Function {} +impl ToNavFromAst for hir::Const {} +impl ToNavFromAst for hir::Static {} +impl ToNavFromAst for hir::Struct {} +impl ToNavFromAst for hir::Enum {} +impl ToNavFromAst for hir::EnumVariant {} +impl ToNavFromAst for hir::Union {} +impl ToNavFromAst for hir::TypeAlias {} +impl ToNavFromAst for hir::Trait {} + +impl ToNav for D +where + D: HasSource + ToNavFromAst + Copy, + D::Ast: ast::DocCommentsOwner + ast::NameOwner + ShortLabel, +{ + fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { + let src = self.source(db); + let mut res = + NavigationTarget::from_named(db, src.as_ref().map(|it| it as &dyn ast::NameOwner)); + res.docs = src.value.doc_comment_text(); + res.description = src.value.short_label(); + 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_string().into()).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())) + } + }; + let frange = original_range(db, src.with_value(syntax)); + NavigationTarget::from_syntax(frange.file_id, name, focus, frange.range, syntax.kind()) + } +} + +impl ToNav for hir::ImplDef { + fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { + let src = self.source(db); + let derive_attr = self.is_builtin_derive(db); + let frange = if let Some(item) = &derive_attr { + original_range(db, item.syntax()) + } else { + original_range(db, src.as_ref().map(|it| it.syntax())) + }; + let focus_range = if derive_attr.is_some() { + None + } else { + src.value.self_ty().map(|ty| original_range(db, src.with_value(ty.syntax())).range) + }; + + NavigationTarget::from_syntax( + frange.file_id, + "impl".into(), + focus_range, + frange.range, + src.value.syntax().kind(), + ) + } +} + +impl ToNav for hir::Field { + fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { + let src = self.source(db); + + match &src.value { + FieldSource::Named(it) => { + let mut res = NavigationTarget::from_named(db, src.with_value(it)); + res.docs = it.doc_comment_text(); + res.description = it.short_label(); + res + } + FieldSource::Pos(it) => { + let frange = original_range(db, src.with_value(it.syntax())); + NavigationTarget::from_syntax( + frange.file_id, + "".into(), + None, + frange.range, + it.syntax().kind(), + ) + } + } + } +} + +impl ToNav for hir::MacroDef { + fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { + let src = self.source(db); + log::debug!("nav target {:#?}", src.value.syntax()); + let mut res = + NavigationTarget::from_named(db, src.as_ref().map(|it| it as &dyn ast::NameOwner)); + res.docs = src.value.doc_comment_text(); + res + } +} + +impl ToNav for hir::Adt { + fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { + match self { + hir::Adt::Struct(it) => it.to_nav(db), + hir::Adt::Union(it) => it.to_nav(db), + hir::Adt::Enum(it) => it.to_nav(db), + } + } +} + +impl ToNav for hir::AssocItem { + fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { + match self { + AssocItem::Function(it) => it.to_nav(db), + AssocItem::Const(it) => it.to_nav(db), + AssocItem::TypeAlias(it) => it.to_nav(db), + } + } +} + +impl ToNav for hir::Local { + fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { + let src = self.source(db); + let node = match &src.value { + Either::Left(bind_pat) => { + bind_pat.name().map_or_else(|| bind_pat.syntax().clone(), |it| it.syntax().clone()) + } + Either::Right(it) => it.syntax().clone(), + }; + let full_range = original_range(db, src.with_value(&node)); + let name = match self.name(db) { + Some(it) => it.to_string().into(), + None => "".into(), + }; + NavigationTarget { + file_id: full_range.file_id, + name, + kind: IDENT_PAT, + full_range: full_range.range, + focus_range: None, + container_name: None, + description: None, + docs: None, + } + } +} + +impl ToNav for hir::TypeParam { + fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { + let src = self.source(db); + let full_range = match &src.value { + Either::Left(it) => it.syntax().text_range(), + Either::Right(it) => it.syntax().text_range(), + }; + let focus_range = match &src.value { + Either::Left(_) => None, + Either::Right(it) => it.name().map(|it| it.syntax().text_range()), + }; + NavigationTarget { + file_id: src.file_id.original_file(db), + name: self.name(db).to_string().into(), + kind: TYPE_PARAM, + full_range, + focus_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()); + + match_ast! { + match node { + ast::Fn(it) => it.doc_comment_text(), + ast::Struct(it) => it.doc_comment_text(), + ast::Enum(it) => it.doc_comment_text(), + ast::Trait(it) => it.doc_comment_text(), + ast::Module(it) => it.doc_comment_text(), + ast::TypeAlias(it) => it.doc_comment_text(), + ast::Const(it) => it.doc_comment_text(), + ast::Static(it) => it.doc_comment_text(), + ast::RecordField(it) => it.doc_comment_text(), + ast::Variant(it) => it.doc_comment_text(), + ast::MacroCall(it) => it.doc_comment_text(), + _ => 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 { + let parse = db.parse(symbol.file_id); + let node = symbol.ptr.to_node(parse.tree().syntax()); + + match_ast! { + match node { + ast::Fn(it) => it.short_label(), + ast::Struct(it) => it.short_label(), + ast::Enum(it) => it.short_label(), + ast::Trait(it) => it.short_label(), + ast::Module(it) => it.short_label(), + ast::TypeAlias(it) => it.short_label(), + ast::Const(it) => it.short_label(), + ast::Static(it) => it.short_label(), + ast::RecordField(it) => it.short_label(), + ast::Variant(it) => it.short_label(), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use expect::expect; + + use crate::{mock_analysis::single_file, Query}; + + #[test] + fn test_nav_for_symbol() { + let (analysis, _) = single_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( + 1, + ), + full_range: 0..17, + focus_range: Some( + 5..13, + ), + name: "FooInner", + kind: ENUM, + container_name: None, + description: Some( + "enum FooInner", + ), + docs: None, + }, + NavigationTarget { + file_id: FileId( + 1, + ), + full_range: 29..46, + focus_range: Some( + 34..42, + ), + name: "FooInner", + kind: ENUM, + container_name: Some( + "foo", + ), + description: Some( + "enum FooInner", + ), + docs: None, + }, + ] + "#]] + .assert_debug_eq(&navs); + } + + #[test] + fn test_world_symbols_are_case_sensitive() { + let (analysis, _) = single_file( + r#" +fn foo() {} +struct Foo; +"#, + ); + + let navs = analysis.symbol_search(Query::new("foo".to_string())).unwrap(); + assert_eq!(navs.len(), 2) + } +} diff --git a/crates/ide/src/display/short_label.rs b/crates/ide/src/display/short_label.rs new file mode 100644 index 000000000..ea49d9f97 --- /dev/null +++ b/crates/ide/src/display/short_label.rs @@ -0,0 +1,111 @@ +//! FIXME: write short doc here + +use stdx::format_to; +use syntax::ast::{self, AstNode, NameOwner, VisibilityOwner}; + +pub(crate) trait ShortLabel { + fn short_label(&self) -> Option; +} + +impl ShortLabel for ast::Fn { + fn short_label(&self) -> Option { + Some(crate::display::function_declaration(self)) + } +} + +impl ShortLabel for ast::Struct { + fn short_label(&self) -> Option { + short_label_from_node(self, "struct ") + } +} + +impl ShortLabel for ast::Union { + fn short_label(&self) -> Option { + short_label_from_node(self, "union ") + } +} + +impl ShortLabel for ast::Enum { + fn short_label(&self) -> Option { + short_label_from_node(self, "enum ") + } +} + +impl ShortLabel for ast::Trait { + fn short_label(&self) -> Option { + if self.unsafe_token().is_some() { + short_label_from_node(self, "unsafe trait ") + } else { + short_label_from_node(self, "trait ") + } + } +} + +impl ShortLabel for ast::Module { + fn short_label(&self) -> Option { + short_label_from_node(self, "mod ") + } +} + +impl ShortLabel for ast::SourceFile { + fn short_label(&self) -> Option { + None + } +} + +impl ShortLabel for ast::TypeAlias { + fn short_label(&self) -> Option { + short_label_from_node(self, "type ") + } +} + +impl ShortLabel for ast::Const { + fn short_label(&self) -> Option { + let mut new_buf = short_label_from_ty(self, self.ty(), "const ")?; + if let Some(expr) = self.body() { + format_to!(new_buf, " = {}", expr.syntax()); + } + Some(new_buf) + } +} + +impl ShortLabel for ast::Static { + fn short_label(&self) -> Option { + short_label_from_ty(self, self.ty(), "static ") + } +} + +impl ShortLabel for ast::RecordField { + fn short_label(&self) -> Option { + short_label_from_ty(self, self.ty(), "") + } +} + +impl ShortLabel for ast::Variant { + fn short_label(&self) -> Option { + Some(self.name()?.text().to_string()) + } +} + +fn short_label_from_ty(node: &T, ty: Option, prefix: &str) -> Option +where + T: NameOwner + VisibilityOwner, +{ + let mut buf = short_label_from_node(node, prefix)?; + + if let Some(type_ref) = ty { + format_to!(buf, ": {}", type_ref.syntax()); + } + + Some(buf) +} + +fn short_label_from_node(node: &T, label: &str) -> Option +where + T: NameOwner + VisibilityOwner, +{ + let mut buf = node.visibility().map(|v| format!("{} ", v.syntax())).unwrap_or_default(); + buf.push_str(label); + buf.push_str(node.name()?.text().as_str()); + Some(buf) +} -- cgit v1.2.3