From f8a59adf5e9633aa5d10efcdbf70b408d280ef01 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Wed, 26 Aug 2020 18:56:41 +0200 Subject: Tease apart orthogonal concerns in markdown link rewriting `hir` should know nothing about URLs, markdown and html. It should only be able to: * resolve stringy path from documentation * generate canonical stringy path for a def In contrast, link rewriting should not care about semantics of paths and names resolution, and should be concern only with text mangling bits. --- Cargo.lock | 2 +- crates/hir/Cargo.toml | 1 - crates/hir/src/attrs.rs | 133 ++++++++++------------- crates/hir/src/code_model.rs | 10 ++ crates/hir/src/doc_links.rs | 238 ----------------------------------------- crates/hir/src/lib.rs | 4 +- crates/ide/Cargo.toml | 1 + crates/ide/src/hover.rs | 54 ++++++++++ crates/ide/src/link_rewrite.rs | 191 ++++++++++++++++++++++++++++++--- 9 files changed, 297 insertions(+), 337 deletions(-) delete mode 100644 crates/hir/src/doc_links.rs diff --git a/Cargo.lock b/Cargo.lock index 015f84ba8..f1a17fa90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -498,7 +498,6 @@ dependencies = [ "stdx", "syntax", "tt", - "url", ] [[package]] @@ -606,6 +605,7 @@ dependencies = [ "syntax", "test_utils", "text_edit", + "url", ] [[package]] diff --git a/crates/hir/Cargo.toml b/crates/hir/Cargo.toml index 72f941c46..6dc5ad63b 100644 --- a/crates/hir/Cargo.toml +++ b/crates/hir/Cargo.toml @@ -15,7 +15,6 @@ rustc-hash = "1.1.0" either = "1.5.3" arrayvec = "0.5.1" itertools = "0.9.0" -url = "2.1.1" stdx = { path = "../stdx", version = "0.0.0" } syntax = { path = "../syntax", version = "0.0.0" } diff --git a/crates/hir/src/attrs.rs b/crates/hir/src/attrs.rs index e5a539cb8..c3e820d89 100644 --- a/crates/hir/src/attrs.rs +++ b/crates/hir/src/attrs.rs @@ -1,20 +1,32 @@ //! Attributes & documentation for hir types. use hir_def::{ - attr::Attrs, - docs::Documentation, - resolver::{HasResolver, Resolver}, - AdtId, AttrDefId, FunctionId, GenericDefId, ModuleId, StaticId, TraitId, VariantId, + attr::Attrs, docs::Documentation, path::ModPath, resolver::HasResolver, AttrDefId, ModuleDefId, }; +use hir_expand::hygiene::Hygiene; use hir_ty::db::HirDatabase; +use syntax::ast; use crate::{ - doc_links::Resolvable, Adt, Const, Enum, EnumVariant, Field, Function, GenericDef, ImplDef, - Local, MacroDef, Module, ModuleDef, Static, Struct, Trait, TypeAlias, TypeParam, Union, + Adt, Const, Enum, EnumVariant, Field, Function, MacroDef, Module, ModuleDef, Static, Struct, + Trait, TypeAlias, Union, }; pub trait HasAttrs { fn attrs(self, db: &dyn HirDatabase) -> Attrs; fn docs(self, db: &dyn HirDatabase) -> Option; + fn resolve_doc_path( + self, + db: &dyn HirDatabase, + link: &str, + ns: Option, + ) -> Option; +} + +#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)] +pub enum Namespace { + Types, + Values, + Macros, } macro_rules! impl_has_attrs { @@ -28,6 +40,10 @@ macro_rules! impl_has_attrs { let def = AttrDefId::$def_id(self.into()); db.documentation(def) } + fn resolve_doc_path(self, db: &dyn HirDatabase, link: &str, ns: Option) -> Option { + let def = AttrDefId::$def_id(self.into()); + resolve_doc_path(db, def, link, ns).map(ModuleDef::from) + } } )*}; } @@ -54,83 +70,42 @@ macro_rules! impl_has_attrs_adt { fn docs(self, db: &dyn HirDatabase) -> Option { Adt::$adt(self).docs(db) } + fn resolve_doc_path(self, db: &dyn HirDatabase, link: &str, ns: Option) -> Option { + Adt::$adt(self).resolve_doc_path(db, link, ns) + } } )*}; } impl_has_attrs_adt![Struct, Union, Enum]; -impl Resolvable for ModuleDef { - fn resolver(&self, db: &dyn HirDatabase) -> Option { - Some(match self { - ModuleDef::Module(m) => ModuleId::from(m.clone()).resolver(db.upcast()), - ModuleDef::Function(f) => FunctionId::from(f.clone()).resolver(db.upcast()), - ModuleDef::Adt(adt) => AdtId::from(adt.clone()).resolver(db.upcast()), - ModuleDef::EnumVariant(ev) => { - GenericDefId::from(GenericDef::from(ev.clone())).resolver(db.upcast()) - } - ModuleDef::Const(c) => { - GenericDefId::from(GenericDef::from(c.clone())).resolver(db.upcast()) - } - ModuleDef::Static(s) => StaticId::from(s.clone()).resolver(db.upcast()), - ModuleDef::Trait(t) => TraitId::from(t.clone()).resolver(db.upcast()), - ModuleDef::TypeAlias(t) => ModuleId::from(t.module(db)).resolver(db.upcast()), - // FIXME: This should be a resolver relative to `std/core` - ModuleDef::BuiltinType(_t) => None?, - }) - } - - fn try_into_module_def(self) -> Option { - Some(self) - } -} - -impl Resolvable for TypeParam { - fn resolver(&self, db: &dyn HirDatabase) -> Option { - Some(ModuleId::from(self.module(db)).resolver(db.upcast())) - } - - fn try_into_module_def(self) -> Option { - None - } -} - -impl Resolvable for MacroDef { - fn resolver(&self, db: &dyn HirDatabase) -> Option { - Some(ModuleId::from(self.module(db)?).resolver(db.upcast())) - } - - fn try_into_module_def(self) -> Option { - None - } -} - -impl Resolvable for Field { - fn resolver(&self, db: &dyn HirDatabase) -> Option { - Some(VariantId::from(self.parent_def(db)).resolver(db.upcast())) - } - - fn try_into_module_def(self) -> Option { - None - } -} - -impl Resolvable for ImplDef { - fn resolver(&self, db: &dyn HirDatabase) -> Option { - Some(ModuleId::from(self.module(db)).resolver(db.upcast())) - } - - fn try_into_module_def(self) -> Option { - None - } -} - -impl Resolvable for Local { - fn resolver(&self, db: &dyn HirDatabase) -> Option { - Some(ModuleId::from(self.module(db)).resolver(db.upcast())) - } - - fn try_into_module_def(self) -> Option { - None - } +fn resolve_doc_path( + db: &dyn HirDatabase, + def: AttrDefId, + link: &str, + ns: Option, +) -> Option { + let resolver = match def { + AttrDefId::ModuleId(it) => it.resolver(db.upcast()), + AttrDefId::FieldId(it) => it.parent.resolver(db.upcast()), + AttrDefId::AdtId(it) => it.resolver(db.upcast()), + AttrDefId::FunctionId(it) => it.resolver(db.upcast()), + AttrDefId::EnumVariantId(it) => it.parent.resolver(db.upcast()), + AttrDefId::StaticId(it) => it.resolver(db.upcast()), + AttrDefId::ConstId(it) => it.resolver(db.upcast()), + AttrDefId::TraitId(it) => it.resolver(db.upcast()), + AttrDefId::TypeAliasId(it) => it.resolver(db.upcast()), + AttrDefId::ImplId(it) => it.resolver(db.upcast()), + AttrDefId::MacroDefId(_) => return None, + }; + let path = ast::Path::parse(link).ok()?; + let modpath = ModPath::from_src(path, &Hygiene::new_unhygienic()).unwrap(); + let resolved = resolver.resolve_module_path_in_items(db.upcast(), &modpath); + let def = match ns { + Some(Namespace::Types) => resolved.take_types()?, + Some(Namespace::Values) => resolved.take_values()?, + Some(Namespace::Macros) => return None, + None => resolved.iter_items().find_map(|it| it.as_module_def_id())?, + }; + Some(def.into()) } diff --git a/crates/hir/src/code_model.rs b/crates/hir/src/code_model.rs index c2ee20dbb..c2fc819e7 100644 --- a/crates/hir/src/code_model.rs +++ b/crates/hir/src/code_model.rs @@ -196,6 +196,16 @@ impl ModuleDef { } } + pub fn canonical_path(&self, db: &dyn HirDatabase) -> Option { + let mut segments = Vec::new(); + segments.push(self.name(db)?.to_string()); + for m in self.module(db)?.path_to_root(db) { + segments.extend(m.name(db).map(|it| it.to_string())) + } + segments.reverse(); + Some(segments.join("::")) + } + pub fn definition_visibility(&self, db: &dyn HirDatabase) -> Option { let module = match self { ModuleDef::Module(it) => it.parent(db)?, diff --git a/crates/hir/src/doc_links.rs b/crates/hir/src/doc_links.rs deleted file mode 100644 index ddaffbec2..000000000 --- a/crates/hir/src/doc_links.rs +++ /dev/null @@ -1,238 +0,0 @@ -//! Resolves links in markdown documentation. - -use std::iter::once; - -use hir_def::resolver::Resolver; -use itertools::Itertools; -use syntax::ast::Path; -use url::Url; - -use crate::{db::HirDatabase, Adt, AsName, Crate, Hygiene, ItemInNs, ModPath, ModuleDef}; - -pub fn resolve_doc_link( - db: &dyn HirDatabase, - definition: &T, - link_text: &str, - link_target: &str, -) -> Option<(String, String)> { - let resolver = definition.resolver(db)?; - let module_def = definition.clone().try_into_module_def(); - resolve_doc_link_impl(db, &resolver, module_def, link_text, link_target) -} - -fn resolve_doc_link_impl( - db: &dyn HirDatabase, - resolver: &Resolver, - module_def: Option, - link_text: &str, - link_target: &str, -) -> Option<(String, String)> { - try_resolve_intra(db, &resolver, link_text, &link_target).or_else(|| { - try_resolve_path(db, &module_def?, &link_target) - .map(|target| (target, link_text.to_string())) - }) -} - -/// Try to resolve path to local documentation via intra-doc-links (i.e. `super::gateway::Shard`). -/// -/// See [RFC1946](https://github.com/rust-lang/rfcs/blob/master/text/1946-intra-rustdoc-links.md). -fn try_resolve_intra( - db: &dyn HirDatabase, - resolver: &Resolver, - link_text: &str, - link_target: &str, -) -> Option<(String, String)> { - // Set link_target for implied shortlinks - let link_target = - if link_target.is_empty() { link_text.trim_matches('`') } else { link_target }; - - let doclink = IntraDocLink::from(link_target); - - // Parse link as a module path - let path = Path::parse(doclink.path).ok()?; - let modpath = ModPath::from_src(path, &Hygiene::new_unhygienic()).unwrap(); - - let resolved = resolver.resolve_module_path_in_items(db.upcast(), &modpath); - let (defid, namespace) = match doclink.namespace { - // FIXME: .or(resolved.macros) - None => resolved - .types - .map(|t| (t.0, Namespace::Types)) - .or(resolved.values.map(|t| (t.0, Namespace::Values)))?, - Some(ns @ Namespace::Types) => (resolved.types?.0, ns), - Some(ns @ Namespace::Values) => (resolved.values?.0, ns), - // FIXME: - Some(Namespace::Macros) => return None, - }; - - // Get the filepath of the final symbol - let def: ModuleDef = defid.into(); - let module = def.module(db)?; - let krate = module.krate(); - let ns = match namespace { - Namespace::Types => ItemInNs::Types(defid), - Namespace::Values => ItemInNs::Values(defid), - // FIXME: - Namespace::Macros => None?, - }; - let import_map = db.import_map(krate.into()); - let path = import_map.path_of(ns)?; - - Some(( - get_doc_url(db, &krate)? - .join(&format!("{}/", krate.display_name(db)?)) - .ok()? - .join(&path.segments.iter().map(|name| name.to_string()).join("/")) - .ok()? - .join(&get_symbol_filename(db, &def)?) - .ok()? - .into_string(), - strip_prefixes_suffixes(link_text).to_string(), - )) -} - -/// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`). -fn try_resolve_path(db: &dyn HirDatabase, moddef: &ModuleDef, link_target: &str) -> Option { - if !link_target.contains("#") && !link_target.contains(".html") { - return None; - } - let ns = ItemInNs::Types(moddef.clone().into()); - - let module = moddef.module(db)?; - let krate = module.krate(); - let import_map = db.import_map(krate.into()); - let base = once(format!("{}", krate.display_name(db)?)) - .chain(import_map.path_of(ns)?.segments.iter().map(|name| format!("{}", name))) - .join("/"); - - get_doc_url(db, &krate) - .and_then(|url| url.join(&base).ok()) - .and_then(|url| { - get_symbol_filename(db, moddef).as_deref().map(|f| url.join(f).ok()).flatten() - }) - .and_then(|url| url.join(link_target).ok()) - .map(|url| url.into_string()) -} - -/// Strip prefixes, suffixes, and inline code marks from the given string. -fn strip_prefixes_suffixes(mut s: &str) -> &str { - s = s.trim_matches('`'); - - [ - (TYPES.0.iter(), TYPES.1.iter()), - (VALUES.0.iter(), VALUES.1.iter()), - (MACROS.0.iter(), MACROS.1.iter()), - ] - .iter() - .for_each(|(prefixes, suffixes)| { - prefixes.clone().for_each(|prefix| s = s.trim_start_matches(*prefix)); - suffixes.clone().for_each(|suffix| s = s.trim_end_matches(*suffix)); - }); - let s = s.trim_start_matches("@").trim(); - s -} - -fn get_doc_url(db: &dyn HirDatabase, krate: &Crate) -> Option { - krate - .get_html_root_url(db) - .or_else(|| - // Fallback to docs.rs - // FIXME: Specify an exact version here. This may be difficult, as multiple versions of the same crate could exist. - Some(format!("https://docs.rs/{}/*/", krate.display_name(db)?))) - .and_then(|s| Url::parse(&s).ok()) -} - -/// Get the filename and extension generated for a symbol by rustdoc. -/// -/// Example: `struct.Shard.html` -fn get_symbol_filename(db: &dyn HirDatabase, definition: &ModuleDef) -> Option { - Some(match definition { - ModuleDef::Adt(adt) => match adt { - Adt::Struct(s) => format!("struct.{}.html", s.name(db)), - Adt::Enum(e) => format!("enum.{}.html", e.name(db)), - Adt::Union(u) => format!("union.{}.html", u.name(db)), - }, - ModuleDef::Module(_) => "index.html".to_string(), - ModuleDef::Trait(t) => format!("trait.{}.html", t.name(db)), - ModuleDef::TypeAlias(t) => format!("type.{}.html", t.name(db)), - ModuleDef::BuiltinType(t) => format!("primitive.{}.html", t.as_name()), - ModuleDef::Function(f) => format!("fn.{}.html", f.name(db)), - ModuleDef::EnumVariant(ev) => { - format!("enum.{}.html#variant.{}", ev.parent_enum(db).name(db), ev.name(db)) - } - ModuleDef::Const(c) => format!("const.{}.html", c.name(db)?), - ModuleDef::Static(s) => format!("static.{}.html", s.name(db)?), - }) -} - -struct IntraDocLink<'s> { - path: &'s str, - namespace: Option, -} - -impl<'s> From<&'s str> for IntraDocLink<'s> { - fn from(s: &'s str) -> Self { - Self { path: strip_prefixes_suffixes(s), namespace: Namespace::from_intra_spec(s) } - } -} - -#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)] -enum Namespace { - Types, - Values, - Macros, -} - -static TYPES: ([&str; 7], [&str; 0]) = - (["type", "struct", "enum", "mod", "trait", "union", "module"], []); -static VALUES: ([&str; 8], [&str; 1]) = - (["value", "function", "fn", "method", "const", "static", "mod", "module"], ["()"]); -static MACROS: ([&str; 1], [&str; 1]) = (["macro"], ["!"]); - -impl Namespace { - /// Extract the specified namespace from an intra-doc-link if one exists. - /// - /// # Examples - /// - /// * `struct MyStruct` -> `Namespace::Types` - /// * `panic!` -> `Namespace::Macros` - /// * `fn@from_intra_spec` -> `Namespace::Values` - fn from_intra_spec(s: &str) -> Option { - [ - (Namespace::Types, (TYPES.0.iter(), TYPES.1.iter())), - (Namespace::Values, (VALUES.0.iter(), VALUES.1.iter())), - (Namespace::Macros, (MACROS.0.iter(), MACROS.1.iter())), - ] - .iter() - .filter(|(_ns, (prefixes, suffixes))| { - prefixes - .clone() - .map(|prefix| { - s.starts_with(*prefix) - && s.chars() - .nth(prefix.len() + 1) - .map(|c| c == '@' || c == ' ') - .unwrap_or(false) - }) - .any(|cond| cond) - || suffixes - .clone() - .map(|suffix| { - s.starts_with(*suffix) - && s.chars() - .nth(suffix.len() + 1) - .map(|c| c == '@' || c == ' ') - .unwrap_or(false) - }) - .any(|cond| cond) - }) - .map(|(ns, (_, _))| *ns) - .next() - } -} - -/// Sealed trait used solely for the generic bound on [`resolve_doc_link`]. -pub trait Resolvable { - fn resolver(&self, db: &dyn HirDatabase) -> Option; - fn try_into_module_def(self) -> Option; -} diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index 03915ea1b..b9d9c7e25 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -27,19 +27,17 @@ pub mod diagnostics; mod from_id; mod code_model; -mod doc_links; mod attrs; mod has_source; pub use crate::{ - attrs::HasAttrs, + attrs::{HasAttrs, Namespace}, code_model::{ Access, Adt, AsAssocItem, AssocItem, AssocItemContainer, Callable, CallableKind, Const, Crate, CrateDependency, DefWithBody, Enum, EnumVariant, Field, FieldSource, Function, GenericDef, HasVisibility, ImplDef, Local, MacroDef, Module, ModuleDef, ScopeDef, Static, Struct, Trait, Type, TypeAlias, TypeParam, Union, VariantDef, Visibility, }, - doc_links::resolve_doc_link, has_source::HasSource, semantics::{original_range, PathResolution, Semantics, SemanticsScope}, }; diff --git a/crates/ide/Cargo.toml b/crates/ide/Cargo.toml index e61c276df..a15f704ca 100644 --- a/crates/ide/Cargo.toml +++ b/crates/ide/Cargo.toml @@ -18,6 +18,7 @@ rustc-hash = "1.1.0" oorandom = "11.1.2" pulldown-cmark-to-cmark = "5.0.0" pulldown-cmark = {version = "0.7.2", default-features = false} +url = "2.1.1" stdx = { path = "../stdx", version = "0.0.0" } syntax = { path = "../syntax", version = "0.0.0" } diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 536411704..efec0184e 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -1755,6 +1755,60 @@ pub struct B<|>ar ); } + #[test] + fn test_doc_links_enum_variant() { + check( + r#" +enum E { + /// [E] + V<|> { field: i32 } +} +"#, + expect![[r#" + *V* + + ```rust + test::E + ``` + + ```rust + V + ``` + + --- + + [E](https://docs.rs/test/*/test/enum.E.html) + "#]], + ); + } + + #[test] + fn test_doc_links_field() { + check( + r#" +struct S { + /// [`S`] + field<|>: i32 +} +"#, + expect![[r#" + *field* + + ```rust + test::S + ``` + + ```rust + field: i32 + ``` + + --- + + [`S`](https://docs.rs/test/*/test/struct.S.html) + "#]], + ); + } + #[test] fn test_hover_macro_generated_struct_fn_doc_comment() { mark::check!(hover_macro_generated_struct_fn_doc_comment); diff --git a/crates/ide/src/link_rewrite.rs b/crates/ide/src/link_rewrite.rs index ff3200eef..acedea71b 100644 --- a/crates/ide/src/link_rewrite.rs +++ b/crates/ide/src/link_rewrite.rs @@ -2,11 +2,11 @@ //! //! Most of the implementation can be found in [`hir::doc_links`]. +use hir::{Adt, Crate, HasAttrs, ModuleDef}; +use ide_db::{defs::Definition, RootDatabase}; use pulldown_cmark::{CowStr, Event, Options, Parser, Tag}; use pulldown_cmark_to_cmark::{cmark_with_options, Options as CmarkOptions}; - -use hir::resolve_doc_link; -use ide_db::{defs::Definition, RootDatabase}; +use url::Url; /// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs) pub fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String { @@ -26,19 +26,16 @@ pub fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) // Two posibilities: // * path-based links: `../../module/struct.MyStruct.html` // * module-based links (AKA intra-doc links): `super::super::module::MyStruct` - let resolved = match definition { - Definition::ModuleDef(t) => resolve_doc_link(db, t, title, target), - Definition::Macro(t) => resolve_doc_link(db, t, title, target), - Definition::Field(t) => resolve_doc_link(db, t, title, target), - Definition::SelfType(t) => resolve_doc_link(db, t, title, target), - Definition::Local(t) => resolve_doc_link(db, t, title, target), - Definition::TypeParam(t) => resolve_doc_link(db, t, title, target), - }; - - match resolved { - Some((target, title)) => (target, title), - None => (target.to_string(), title.to_string()), + if let Some(rewritten) = rewrite_intra_doc_link(db, *definition, target, title) { + return rewritten; + } + if let Definition::ModuleDef(def) = *definition { + if let Some(target) = rewrite_url_link(db, def, target) { + return (target, title.to_string()); + } } + + (target.to_string(), title.to_string()) } }); let mut out = String::new(); @@ -48,6 +45,64 @@ pub fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) out } +fn rewrite_intra_doc_link( + db: &RootDatabase, + def: Definition, + target: &str, + title: &str, +) -> Option<(String, String)> { + let link = if target.is_empty() { title } else { target }; + let (link, ns) = parse_link(link); + let resolved = match def { + Definition::ModuleDef(def) => match def { + ModuleDef::Module(it) => it.resolve_doc_path(db, link, ns), + ModuleDef::Function(it) => it.resolve_doc_path(db, link, ns), + ModuleDef::Adt(it) => it.resolve_doc_path(db, link, ns), + ModuleDef::EnumVariant(it) => it.resolve_doc_path(db, link, ns), + ModuleDef::Const(it) => it.resolve_doc_path(db, link, ns), + ModuleDef::Static(it) => it.resolve_doc_path(db, link, ns), + ModuleDef::Trait(it) => it.resolve_doc_path(db, link, ns), + ModuleDef::TypeAlias(it) => it.resolve_doc_path(db, link, ns), + ModuleDef::BuiltinType(_) => return None, + }, + 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, + }?; + let krate = resolved.module(db)?.krate(); + let canonical_path = resolved.canonical_path(db)?; + let new_target = get_doc_url(db, &krate)? + .join(&format!("{}/", krate.display_name(db)?)) + .ok()? + .join(&canonical_path.replace("::", "/")) + .ok()? + .join(&get_symbol_filename(db, &resolved)?) + .ok()? + .into_string(); + let new_title = strip_prefixes_suffixes(title); + Some((new_target, new_title.to_string())) +} + +/// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`). +fn rewrite_url_link(db: &RootDatabase, def: ModuleDef, target: &str) -> Option { + if !(target.contains("#") || target.contains(".html")) { + return None; + } + + let module = def.module(db)?; + let krate = module.krate(); + let canonical_path = def.canonical_path(db)?; + let base = format!("{}/{}", krate.display_name(db)?, canonical_path.replace("::", "/")); + + get_doc_url(db, &krate) + .and_then(|url| url.join(&base).ok()) + .and_then(|url| { + get_symbol_filename(db, &def).as_deref().map(|f| url.join(f).ok()).flatten() + }) + .and_then(|url| url.join(target).ok()) + .map(|url| url.into_string()) +} + // Rewrites a markdown document, resolving links using `callback` and additionally striping prefixes/suffixes on link titles. fn map_links<'e>( events: impl Iterator>, @@ -79,3 +134,109 @@ fn map_links<'e>( _ => evt, }) } + +fn parse_link(s: &str) -> (&str, Option) { + let path = strip_prefixes_suffixes(s); + let ns = ns_from_intra_spec(s); + (path, ns) +} + +/// Strip prefixes, suffixes, and inline code marks from the given string. +fn strip_prefixes_suffixes(mut s: &str) -> &str { + s = s.trim_matches('`'); + + [ + (TYPES.0.iter(), TYPES.1.iter()), + (VALUES.0.iter(), VALUES.1.iter()), + (MACROS.0.iter(), MACROS.1.iter()), + ] + .iter() + .for_each(|(prefixes, suffixes)| { + prefixes.clone().for_each(|prefix| s = s.trim_start_matches(*prefix)); + suffixes.clone().for_each(|suffix| s = s.trim_end_matches(*suffix)); + }); + s.trim_start_matches("@").trim() +} + +static TYPES: ([&str; 7], [&str; 0]) = + (["type", "struct", "enum", "mod", "trait", "union", "module"], []); +static VALUES: ([&str; 8], [&str; 1]) = + (["value", "function", "fn", "method", "const", "static", "mod", "module"], ["()"]); +static MACROS: ([&str; 1], [&str; 1]) = (["macro"], ["!"]); + +/// Extract the specified namespace from an intra-doc-link if one exists. +/// +/// # Examples +/// +/// * `struct MyStruct` -> `Namespace::Types` +/// * `panic!` -> `Namespace::Macros` +/// * `fn@from_intra_spec` -> `Namespace::Values` +fn ns_from_intra_spec(s: &str) -> Option { + [ + (hir::Namespace::Types, (TYPES.0.iter(), TYPES.1.iter())), + (hir::Namespace::Values, (VALUES.0.iter(), VALUES.1.iter())), + (hir::Namespace::Macros, (MACROS.0.iter(), MACROS.1.iter())), + ] + .iter() + .filter(|(_ns, (prefixes, suffixes))| { + prefixes + .clone() + .map(|prefix| { + s.starts_with(*prefix) + && s.chars() + .nth(prefix.len() + 1) + .map(|c| c == '@' || c == ' ') + .unwrap_or(false) + }) + .any(|cond| cond) + || suffixes + .clone() + .map(|suffix| { + s.starts_with(*suffix) + && s.chars() + .nth(suffix.len() + 1) + .map(|c| c == '@' || c == ' ') + .unwrap_or(false) + }) + .any(|cond| cond) + }) + .map(|(ns, (_, _))| *ns) + .next() +} + +fn get_doc_url(db: &RootDatabase, krate: &Crate) -> Option { + krate + .get_html_root_url(db) + .or_else(|| { + // Fallback to docs.rs. This uses `display_name` and can never be + // correct, but that's what fallbacks are about. + // + // FIXME: clicking on the link should just open the file in the editor, + // instead of falling back to external urls. + Some(format!("https://docs.rs/{}/*/", krate.display_name(db)?)) + }) + .and_then(|s| Url::parse(&s).ok()) +} + +/// Get the filename and extension generated for a symbol by rustdoc. +/// +/// Example: `struct.Shard.html` +fn get_symbol_filename(db: &RootDatabase, definition: &ModuleDef) -> Option { + Some(match definition { + ModuleDef::Adt(adt) => match adt { + Adt::Struct(s) => format!("struct.{}.html", s.name(db)), + Adt::Enum(e) => format!("enum.{}.html", e.name(db)), + Adt::Union(u) => format!("union.{}.html", u.name(db)), + }, + ModuleDef::Module(_) => "index.html".to_string(), + ModuleDef::Trait(t) => format!("trait.{}.html", t.name(db)), + ModuleDef::TypeAlias(t) => format!("type.{}.html", t.name(db)), + ModuleDef::BuiltinType(t) => format!("primitive.{}.html", t), + ModuleDef::Function(f) => format!("fn.{}.html", f.name(db)), + ModuleDef::EnumVariant(ev) => { + format!("enum.{}.html#variant.{}", ev.parent_enum(db).name(db), ev.name(db)) + } + ModuleDef::Const(c) => format!("const.{}.html", c.name(db)?), + ModuleDef::Static(s) => format!("static.{}.html", s.name(db)?), + }) +} -- cgit v1.2.3