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. --- crates/ide/Cargo.toml | 1 + crates/ide/src/hover.rs | 54 ++++++++++++ crates/ide/src/link_rewrite.rs | 191 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 231 insertions(+), 15 deletions(-) (limited to 'crates/ide') 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