diff --git a/Cargo.lock b/Cargo.lock index 69affa0ea897..015f84ba8cce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -497,6 +497,8 @@ dependencies = [ "rustc-hash", "stdx", "syntax", + "tt", + "url", ] [[package]] @@ -596,6 +598,8 @@ dependencies = [ "log", "oorandom", "profile", + "pulldown-cmark", + "pulldown-cmark-to-cmark", "rustc-hash", "ssr", "stdx", @@ -824,6 +828,12 @@ dependencies = [ "tt", ] +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + [[package]] name = "memmap" version = "0.7.0" @@ -1140,6 +1150,26 @@ dependencies = [ "toolchain", ] +[[package]] +name = "pulldown-cmark" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca36dea94d187597e104a5c8e4b07576a8a45aa5db48a65e12940d3eb7461f55" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32accf4473121d8c0b508ca5673363703762d6cc59cf25af1df48f653346f736" +dependencies = [ + "pulldown-cmark", +] + [[package]] name = "quote" version = "1.0.7" @@ -1692,6 +1722,15 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca4d39065b45f658d33013f7cc93ee050708cd543f6e07dd15b4293fcf217e12" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.4" @@ -1734,6 +1773,12 @@ dependencies = [ "serde", ] +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + [[package]] name = "vfs" version = "0.0.0" diff --git a/crates/hir/Cargo.toml b/crates/hir/Cargo.toml index 60a48170e6df..72f941c463fd 100644 --- a/crates/hir/Cargo.toml +++ b/crates/hir/Cargo.toml @@ -15,6 +15,7 @@ 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" } @@ -23,3 +24,4 @@ profile = { path = "../profile", version = "0.0.0" } hir_expand = { path = "../hir_expand", version = "0.0.0" } hir_def = { path = "../hir_def", version = "0.0.0" } hir_ty = { path = "../hir_ty", version = "0.0.0" } +tt = { path = "../tt", version = "0.0.0" } diff --git a/crates/hir/src/code_model.rs b/crates/hir/src/code_model.rs index 3d92d0c0d191..94dd7f6f57d5 100644 --- a/crates/hir/src/code_model.rs +++ b/crates/hir/src/code_model.rs @@ -20,7 +20,7 @@ use hir_def::{ type_ref::{Mutability, TypeRef}, AdtId, AssocContainerId, ConstId, DefWithBodyId, EnumId, FunctionId, GenericDefId, HasModule, ImplId, LocalEnumVariantId, LocalFieldId, LocalModuleId, Lookup, ModuleId, StaticId, StructId, - TraitId, TypeAliasId, TypeParamId, UnionId, + TraitId, TypeAliasId, TypeParamId, UnionId, VariantId, }; use hir_expand::{ diagnostics::DiagnosticSink, @@ -39,9 +39,11 @@ use syntax::{ ast::{self, AttrsOwner, NameOwner}, AstNode, SmolStr, }; +use tt::{Ident, Leaf, Literal, TokenTree}; use crate::{ db::{DefDatabase, HirDatabase}, + doc_links::Resolvable, has_source::HasSource, HirDisplay, InFile, Name, }; @@ -122,6 +124,31 @@ impl Crate { pub fn all(db: &dyn HirDatabase) -> Vec { db.crate_graph().iter().map(|id| Crate { id }).collect() } + + /// Try to get the root URL of the documentation of a crate. + pub fn get_html_root_url(self: &Crate, db: &dyn HirDatabase) -> Option { + // Look for #![doc(html_root_url = "...")] + let attrs = db.attrs(AttrDef::from(self.root_module(db)).into()); + let doc_attr_q = attrs.by_key("doc"); + + if !doc_attr_q.exists() { + return None; + } + + let doc_url = doc_attr_q.tt_values().map(|tt| { + let name = tt.token_trees.iter() + .skip_while(|tt| !matches!(tt, TokenTree::Leaf(Leaf::Ident(Ident{text: ref ident, ..})) if ident == "html_root_url")) + .skip(2) + .next(); + + match name { + Some(TokenTree::Leaf(Leaf::Literal(Literal{ref text, ..}))) => Some(text), + _ => None + } + }).flat_map(|t| t).next(); + + doc_url.map(|s| s.trim_matches('"').trim_end_matches("/").to_owned() + "/") + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -198,7 +225,6 @@ impl ModuleDef { ModuleDef::Function(it) => Some(it.name(db)), ModuleDef::EnumVariant(it) => Some(it.name(db)), ModuleDef::TypeAlias(it) => Some(it.name(db)), - ModuleDef::Module(it) => it.name(db), ModuleDef::Const(it) => it.name(db), ModuleDef::Static(it) => it.name(db), @@ -1771,3 +1797,76 @@ pub trait HasVisibility { vis.is_visible_from(db.upcast(), module.id) } } + +impl Resolvable for ModuleDef { + fn resolver(&self, db: &D) -> Option { + Some(match self { + ModuleDef::Module(m) => ModuleId::from(m.clone()).resolver(db), + ModuleDef::Function(f) => FunctionId::from(f.clone()).resolver(db), + ModuleDef::Adt(adt) => AdtId::from(adt.clone()).resolver(db), + ModuleDef::EnumVariant(ev) => { + GenericDefId::from(GenericDef::from(ev.clone())).resolver(db) + } + ModuleDef::Const(c) => GenericDefId::from(GenericDef::from(c.clone())).resolver(db), + ModuleDef::Static(s) => StaticId::from(s.clone()).resolver(db), + ModuleDef::Trait(t) => TraitId::from(t.clone()).resolver(db), + ModuleDef::TypeAlias(t) => ModuleId::from(t.module(db)).resolver(db), + // 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: &D) -> Option { + Some(Into::::into(self.module(db)).resolver(db)) + } + + fn try_into_module_def(self) -> Option { + None + } +} + +impl Resolvable for MacroDef { + fn resolver(&self, db: &D) -> Option { + Some(Into::::into(self.module(db)?).resolver(db)) + } + + fn try_into_module_def(self) -> Option { + None + } +} + +impl Resolvable for Field { + fn resolver(&self, db: &D) -> Option { + Some(Into::::into(Into::::into(self.parent_def(db))).resolver(db)) + } + + fn try_into_module_def(self) -> Option { + None + } +} + +impl Resolvable for ImplDef { + fn resolver(&self, db: &D) -> Option { + Some(Into::::into(self.module(db)).resolver(db)) + } + + fn try_into_module_def(self) -> Option { + None + } +} + +impl Resolvable for Local { + fn resolver(&self, db: &D) -> Option { + Some(Into::::into(self.module(db)).resolver(db)) + } + + fn try_into_module_def(self) -> Option { + None + } +} diff --git a/crates/hir/src/doc_links.rs b/crates/hir/src/doc_links.rs new file mode 100644 index 000000000000..dd2379bfc666 --- /dev/null +++ b/crates/hir/src/doc_links.rs @@ -0,0 +1,233 @@ +//! Resolves links in markdown documentation. + +use std::iter::once; + +use itertools::Itertools; +use url::Url; + +use crate::{db::HirDatabase, Adt, AsName, Crate, Hygiene, ItemInNs, ModPath, ModuleDef}; +use hir_def::{db::DefDatabase, resolver::Resolver}; +use syntax::ast::Path; + +pub fn resolve_doc_link( + db: &D, + definition: &T, + link_text: &str, + link_target: &str, +) -> Option<(String, String)> { + try_resolve_intra(db, definition, link_text, &link_target).or_else(|| { + if let Some(definition) = definition.clone().try_into_module_def() { + try_resolve_path(db, &definition, &link_target) + .map(|target| (target, link_text.to_string())) + } else { + None + } + }) +} + +/// 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: &D, + definition: &T, + 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(); + + // Resolve it relative to symbol's location (according to the RFC this should consider small scopes) + let resolver = definition.resolver(db)?; + + let resolved = resolver.resolve_module_path_in_items(db, &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) => 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: &D) -> Option; + fn try_into_module_def(self) -> Option; +} diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index 78d8651cba8e..d1f4d7813236 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -27,6 +27,7 @@ pub mod diagnostics; mod from_id; mod code_model; +mod doc_links; mod has_source; @@ -37,6 +38,7 @@ pub use crate::{ Function, GenericDef, HasAttrs, 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}, }; @@ -47,13 +49,14 @@ pub use hir_def::{ body::scope::ExprScopes, builtin_type::BuiltinType, docs::Documentation, + item_scope::ItemInNs, nameres::ModuleSource, path::ModPath, type_ref::{Mutability, TypeRef}, }; pub use hir_expand::{ - name::Name, HirFileId, InFile, MacroCallId, MacroCallLoc, /* FIXME */ MacroDefId, - MacroFile, Origin, + name::AsName, name::Name, HirFileId, InFile, MacroCallId, MacroCallLoc, + /* FIXME */ MacroDefId, MacroFile, Origin, }; pub use hir_ty::display::HirDisplay; diff --git a/crates/ide/Cargo.toml b/crates/ide/Cargo.toml index 336e9d2aa30a..e61c276df5e6 100644 --- a/crates/ide/Cargo.toml +++ b/crates/ide/Cargo.toml @@ -16,6 +16,8 @@ itertools = "0.9.0" log = "0.4.8" 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} 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 c75b2a51056a..b012e4900f14 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -14,6 +14,7 @@ use test_utils::mark; use crate::{ display::{macro_label, ShortLabel, ToNav, TryToNav}, + link_rewrite::rewrite_links, markup::Markup, runnables::runnable, FileId, FilePosition, NavigationTarget, RangeInfo, Runnable, @@ -92,7 +93,8 @@ pub(crate) fn hover(db: &RootDatabase, position: FilePosition) -> Option>, |&mut u32, &u32, &mut u32| -> Option, u32>> ``` @@ -443,6 +446,11 @@ fn main() { let foo_test = fo<|>o(); } "#, expect![[r#" *foo* + + ```rust + test + ``` + ```rust pub fn foo() -> u32 ``` @@ -487,6 +495,11 @@ fn main() { let foo_test = fo<|>o(); } "#, expect![[r#" *foo* + + ```rust + test + ``` + ```rust pub fn foo<'a, T: AsRef>(b: &'a T) -> &'a str ``` @@ -504,6 +517,11 @@ fn main() { } "#, expect![[r#" *foo* + + ```rust + test + ``` + ```rust pub fn foo(a: u32, b: u32) -> u32 ``` @@ -525,20 +543,27 @@ pub fn foo<|>(_: &Path) {} fn main() { } "#, - expect![[r#" + expect![[r##" *foo* + + ```rust + test + ``` + ```rust pub fn foo(_: &Path) ``` - ___ + + --- # Example + ``` # use std::path::Path; # foo(Path::new("hello, world!")) ``` - "#]], + "##]], ); } @@ -555,8 +580,9 @@ fn main() { "#, expect![[r#" *field_a* + ```rust - Foo + test::Foo ``` ```rust @@ -576,8 +602,9 @@ fn main() { "#, expect![[r#" *field_a* + ```rust - Foo + test::Foo ``` ```rust @@ -593,6 +620,11 @@ fn main() { r#"const foo<|>: u32 = 123;"#, expect![[r#" *foo* + + ```rust + test + ``` + ```rust const foo: u32 = 123 ``` @@ -602,6 +634,11 @@ fn main() { r#"static foo<|>: u32 = 456;"#, expect![[r#" *foo* + + ```rust + test + ``` + ```rust static foo: u32 ``` @@ -620,6 +657,7 @@ fn main() { }"#, expect![[r#" *zz* + ```rust Test ``` @@ -638,8 +676,9 @@ fn main() { So<|>me(12); } "#, expect![[r#" *Some* + ```rust - Option + test::Option ``` ```rust @@ -657,6 +696,7 @@ fn main() { let b<|>ar = Some(12); } "#, expect![[r#" *bar* + ```rust Option ``` @@ -675,14 +715,16 @@ enum Option { "#, expect![[r#" *None* + ```rust - Option + test::Option ``` ```rust None ``` - ___ + + --- The None variant "#]], @@ -700,14 +742,16 @@ fn main() { "#, expect![[r#" *Some* + ```rust - Option + test::Option ``` ```rust Some ``` - ___ + + --- The Some variant "#]], @@ -720,6 +764,7 @@ fn main() { r#"fn func(foo: i32) { fo<|>o; }"#, expect![[r#" *foo* + ```rust i32 ``` @@ -733,6 +778,7 @@ fn main() { r#"fn func(fo<|>o: i32) {}"#, expect![[r#" *foo* + ```rust i32 ``` @@ -746,6 +792,7 @@ fn main() { r#"fn func(foo: i32) { if true { <|>foo; }; }"#, expect![[r#" *foo* + ```rust i32 ``` @@ -759,6 +806,7 @@ fn main() { r#"fn func(<|>foo: i32) {}"#, expect![[r#" *foo* + ```rust i32 ``` @@ -778,6 +826,7 @@ fn main() { fn f(_x<|>: impl Deref + DerefMut) {}"#, expect![[r#" *_x* + ```rust impl Deref + DerefMut ``` @@ -799,6 +848,7 @@ fn main() { let foo_<|>test = Thing::new(); } "#, expect![[r#" *foo_test* + ```rust Thing ``` @@ -822,8 +872,9 @@ fn main() { let foo_test = wrapper::Thing::new<|>(); } "#, expect![[r#" *new* + ```rust - wrapper::Thing + test::wrapper::Thing ``` ```rust @@ -852,6 +903,11 @@ fn main() { "#, expect![[r#" *C* + + ```rust + test + ``` + ```rust const C: u32 = 1 ``` @@ -929,6 +985,7 @@ fn y() { "#, expect![[r#" *x* + ```rust i32 ``` @@ -946,6 +1003,11 @@ fn f() { fo<|>o!(); } "#, expect![[r#" *foo* + + ```rust + test + ``` + ```rust macro_rules! foo ``` @@ -976,6 +1038,11 @@ id! { "#, expect![[r#" *foo* + + ```rust + test + ``` + ```rust fn foo() ``` @@ -992,6 +1059,7 @@ fn foo(bar:u32) { let a = id!(ba<|>r); } "#, expect![[r#" *bar* + ```rust u32 ``` @@ -1009,6 +1077,7 @@ fn foo(bar:u32) { let a = id!(ba<|>r); } "#, expect![[r#" *bar* + ```rust u32 ``` @@ -1067,6 +1136,11 @@ fn foo() { "#, expect![[r#" *bar* + + ```rust + test + ``` + ```rust fn bar() -> bool ``` @@ -1099,12 +1173,18 @@ fn bar() { fo<|>o(); } ", expect![[r#" *foo* + + ```rust + test + ``` + ```rust fn foo() ``` - ___ - <- ` ` here + --- + + \<- ` ` here "#]], ); } @@ -1115,6 +1195,11 @@ fn bar() { fo<|>o(); } r#"async fn foo<|>() {}"#, expect![[r#" *foo* + + ```rust + test + ``` + ```rust async fn foo() ``` @@ -1124,6 +1209,11 @@ fn bar() { fo<|>o(); } r#"pub const unsafe fn foo<|>() {}"#, expect![[r#" *foo* + + ```rust + test + ``` + ```rust pub const unsafe fn foo() ``` @@ -1133,6 +1223,11 @@ fn bar() { fo<|>o(); } r#"pub(crate) async unsafe extern "C" fn foo<|>() {}"#, expect![[r#" *foo* + + ```rust + test + ``` + ```rust pub(crate) async unsafe extern "C" fn foo() ``` @@ -1210,6 +1305,11 @@ fn my() {} "#, expect![[r#" *my* + + ```rust + test + ``` + ```rust mod my ``` @@ -1228,10 +1328,16 @@ fn foo() { let bar = Ba<|>r; } "#, expect![[r#" *Bar* + + ```rust + test + ``` + ```rust struct Bar ``` - ___ + + --- bar docs "#]], @@ -1249,10 +1355,16 @@ fn foo() { let bar = Ba<|>r; } "#, expect![[r#" *Bar* + + ```rust + test + ``` + ```rust struct Bar ``` - ___ + + --- bar docs "#]], @@ -1272,10 +1384,16 @@ fn foo() { let bar = Ba<|>r; } "#, expect![[r#" *Bar* + + ```rust + test + ``` + ```rust struct Bar ``` - ___ + + --- bar docs 0 @@ -1286,6 +1404,371 @@ fn foo() { let bar = Ba<|>r; } ); } + #[test] + fn test_hover_path_link() { + check( + r" + //- /lib.rs + pub struct Foo; + /// [Foo](struct.Foo.html) + pub struct B<|>ar + ", + expect![[r#" + *Bar* + + ```rust + test + ``` + + ```rust + pub struct Bar + ``` + + --- + + [Foo](https://docs.rs/test/*/test/struct.Foo.html) + "#]], + ); + } + + #[test] + fn test_hover_path_link_no_strip() { + check( + r" + //- /lib.rs + pub struct Foo; + /// [struct Foo](struct.Foo.html) + pub struct B<|>ar + ", + expect![[r#" + *Bar* + + ```rust + test + ``` + + ```rust + pub struct Bar + ``` + + --- + + [struct Foo](https://docs.rs/test/*/test/struct.Foo.html) + "#]], + ); + } + + #[ignore = "path based links currently only support documentation on ModuleDef items"] + #[test] + fn test_hover_path_link_field() { + check( + r" + //- /lib.rs + pub struct Foo; + pub struct Bar { + /// [Foo](struct.Foo.html) + fie<|>ld: () + } + ", + expect![[r#" + *field* + + ```rust + test::Bar + ``` + + ```rust + field: () + ``` + + --- + + [Foo](https://docs.rs/test/*/test/struct.Foo.html) + "#]], + ); + } + + #[test] + fn test_hover_intra_link() { + check( + r" + //- /lib.rs + pub mod foo { + pub struct Foo; + } + /// [Foo](foo::Foo) + pub struct B<|>ar + ", + expect![[r#" + *Bar* + + ```rust + test + ``` + + ```rust + pub struct Bar + ``` + + --- + + [Foo](https://docs.rs/test/*/test/foo/struct.Foo.html) + "#]], + ); + } + + #[test] + fn test_hover_intra_link_html_root_url() { + check( + r#" + //- /lib.rs + + #![doc(arbitrary_attribute = "test", html_root_url = "https:/example.com", arbitrary_attribute2)] + + pub mod foo { + pub struct Foo; + } + /// [Foo](foo::Foo) + pub struct B<|>ar + "#, + expect![[r#" + *Bar* + + ```rust + test + ``` + + ```rust + pub struct Bar + ``` + + --- + + [Foo](https://example.com/test/foo/struct.Foo.html) + "#]], + ); + } + + #[test] + fn test_hover_intra_link_shortlink() { + check( + r" + //- /lib.rs + pub struct Foo; + /// [Foo] + pub struct B<|>ar + ", + expect![[r#" + *Bar* + + ```rust + test + ``` + + ```rust + pub struct Bar + ``` + + --- + + [Foo](https://docs.rs/test/*/test/struct.Foo.html) + "#]], + ); + } + + #[test] + fn test_hover_intra_link_shortlink_code() { + check( + r" + //- /lib.rs + pub struct Foo; + /// [`Foo`] + pub struct B<|>ar + ", + expect![[r#" + *Bar* + + ```rust + test + ``` + + ```rust + pub struct Bar + ``` + + --- + + [`Foo`](https://docs.rs/test/*/test/struct.Foo.html) + "#]], + ); + } + + #[test] + fn test_hover_intra_link_namespaced() { + check( + r" + //- /lib.rs + pub struct Foo; + fn Foo() {} + /// [Foo()] + pub struct B<|>ar + ", + expect![[r#" + *Bar* + + ```rust + test + ``` + + ```rust + pub struct Bar + ``` + + --- + + [Foo](https://docs.rs/test/*/test/struct.Foo.html) + "#]], + ); + } + + #[test] + fn test_hover_intra_link_shortlink_namspaced_code() { + check( + r" + //- /lib.rs + pub struct Foo; + /// [`struct Foo`] + pub struct B<|>ar + ", + expect![[r#" + *Bar* + + ```rust + test + ``` + + ```rust + pub struct Bar + ``` + + --- + + [`Foo`](https://docs.rs/test/*/test/struct.Foo.html) + "#]], + ); + } + + #[test] + fn test_hover_intra_link_shortlink_namspaced_code_with_at() { + check( + r" + //- /lib.rs + pub struct Foo; + /// [`struct@Foo`] + pub struct B<|>ar + ", + expect![[r#" + *Bar* + + ```rust + test + ``` + + ```rust + pub struct Bar + ``` + + --- + + [`Foo`](https://docs.rs/test/*/test/struct.Foo.html) + "#]], + ); + } + + #[test] + fn test_hover_intra_link_reference() { + check( + r" + //- /lib.rs + pub struct Foo; + /// [my Foo][foo] + /// + /// [foo]: Foo + pub struct B<|>ar + ", + expect![[r#" + *Bar* + + ```rust + test + ``` + + ```rust + pub struct Bar + ``` + + --- + + [my Foo](https://docs.rs/test/*/test/struct.Foo.html) + "#]], + ); + } + + #[test] + fn test_hover_external_url() { + check( + r" + //- /lib.rs + pub struct Foo; + /// [external](https://www.google.com) + pub struct B<|>ar + ", + expect![[r#" + *Bar* + + ```rust + test + ``` + + ```rust + pub struct Bar + ``` + + --- + + [external](https://www.google.com) + "#]], + ); + } + + // Check that we don't rewrite links which we can't identify + #[test] + fn test_hover_unknown_target() { + check( + r" + //- /lib.rs + pub struct Foo; + /// [baz](Baz) + pub struct B<|>ar + ", + expect![[r#" + *Bar* + + ```rust + test + ``` + + ```rust + pub struct Bar + ``` + + --- + + [baz](Baz) + "#]], + ); + } + #[test] fn test_hover_macro_generated_struct_fn_doc_comment() { mark::check!(hover_macro_generated_struct_fn_doc_comment); @@ -1308,16 +1791,18 @@ fn foo() { let bar = Bar; bar.fo<|>o(); } "#, expect![[r#" *foo* + ```rust - Bar + test::Bar ``` ```rust fn foo(&self) ``` - ___ - Do the foo + --- + + Do the foo "#]], ); } @@ -1344,14 +1829,16 @@ fn foo() { let bar = Bar; bar.fo<|>o(); } "#, expect![[r#" *foo* + ```rust - Bar + test::Bar ``` ```rust fn foo(&self) ``` - ___ + + --- Do the foo "#]], @@ -1526,7 +2013,7 @@ fn main() { let s<|>t = S{ f1:0 }; } GoToType( [ HoverGotoTypeData { - mod_path: "S", + mod_path: "test::S", nav: NavigationTarget { file_id: FileId( 1, @@ -1565,7 +2052,7 @@ fn main() { let s<|>t = S{ f1:Arg(0) }; } GoToType( [ HoverGotoTypeData { - mod_path: "S", + mod_path: "test::S", nav: NavigationTarget { file_id: FileId( 1, @@ -1584,7 +2071,7 @@ fn main() { let s<|>t = S{ f1:Arg(0) }; } }, }, HoverGotoTypeData { - mod_path: "Arg", + mod_path: "test::Arg", nav: NavigationTarget { file_id: FileId( 1, @@ -1623,7 +2110,7 @@ fn main() { let s<|>t = S{ f1: S{ f1: Arg(0) } }; } GoToType( [ HoverGotoTypeData { - mod_path: "S", + mod_path: "test::S", nav: NavigationTarget { file_id: FileId( 1, @@ -1642,7 +2129,7 @@ fn main() { let s<|>t = S{ f1: S{ f1: Arg(0) } }; } }, }, HoverGotoTypeData { - mod_path: "Arg", + mod_path: "test::Arg", nav: NavigationTarget { file_id: FileId( 1, @@ -1684,7 +2171,7 @@ fn main() { let s<|>t = (A(1), B(2), M::C(3) ); } GoToType( [ HoverGotoTypeData { - mod_path: "A", + mod_path: "test::A", nav: NavigationTarget { file_id: FileId( 1, @@ -1703,7 +2190,7 @@ fn main() { let s<|>t = (A(1), B(2), M::C(3) ); } }, }, HoverGotoTypeData { - mod_path: "B", + mod_path: "test::B", nav: NavigationTarget { file_id: FileId( 1, @@ -1722,7 +2209,7 @@ fn main() { let s<|>t = (A(1), B(2), M::C(3) ); } }, }, HoverGotoTypeData { - mod_path: "M::C", + mod_path: "test::M::C", nav: NavigationTarget { file_id: FileId( 1, @@ -1761,7 +2248,7 @@ fn main() { let s<|>t = foo(); } GoToType( [ HoverGotoTypeData { - mod_path: "Foo", + mod_path: "test::Foo", nav: NavigationTarget { file_id: FileId( 1, @@ -1801,7 +2288,7 @@ fn main() { let s<|>t = foo(); } GoToType( [ HoverGotoTypeData { - mod_path: "Foo", + mod_path: "test::Foo", nav: NavigationTarget { file_id: FileId( 1, @@ -1820,7 +2307,7 @@ fn main() { let s<|>t = foo(); } }, }, HoverGotoTypeData { - mod_path: "S", + mod_path: "test::S", nav: NavigationTarget { file_id: FileId( 1, @@ -1860,7 +2347,7 @@ fn main() { let s<|>t = foo(); } GoToType( [ HoverGotoTypeData { - mod_path: "Foo", + mod_path: "test::Foo", nav: NavigationTarget { file_id: FileId( 1, @@ -1879,7 +2366,7 @@ fn main() { let s<|>t = foo(); } }, }, HoverGotoTypeData { - mod_path: "Bar", + mod_path: "test::Bar", nav: NavigationTarget { file_id: FileId( 1, @@ -1922,7 +2409,7 @@ fn main() { let s<|>t = foo(); } GoToType( [ HoverGotoTypeData { - mod_path: "Foo", + mod_path: "test::Foo", nav: NavigationTarget { file_id: FileId( 1, @@ -1941,7 +2428,7 @@ fn main() { let s<|>t = foo(); } }, }, HoverGotoTypeData { - mod_path: "Bar", + mod_path: "test::Bar", nav: NavigationTarget { file_id: FileId( 1, @@ -1960,7 +2447,7 @@ fn main() { let s<|>t = foo(); } }, }, HoverGotoTypeData { - mod_path: "S1", + mod_path: "test::S1", nav: NavigationTarget { file_id: FileId( 1, @@ -1979,7 +2466,7 @@ fn main() { let s<|>t = foo(); } }, }, HoverGotoTypeData { - mod_path: "S2", + mod_path: "test::S2", nav: NavigationTarget { file_id: FileId( 1, @@ -2016,7 +2503,7 @@ fn foo(ar<|>g: &impl Foo) {} GoToType( [ HoverGotoTypeData { - mod_path: "Foo", + mod_path: "test::Foo", nav: NavigationTarget { file_id: FileId( 1, @@ -2056,7 +2543,7 @@ fn foo(ar<|>g: &impl Foo + Bar) {} GoToType( [ HoverGotoTypeData { - mod_path: "Foo", + mod_path: "test::Foo", nav: NavigationTarget { file_id: FileId( 1, @@ -2075,7 +2562,7 @@ fn foo(ar<|>g: &impl Foo + Bar) {} }, }, HoverGotoTypeData { - mod_path: "Bar", + mod_path: "test::Bar", nav: NavigationTarget { file_id: FileId( 1, @@ -2094,7 +2581,7 @@ fn foo(ar<|>g: &impl Foo + Bar) {} }, }, HoverGotoTypeData { - mod_path: "S", + mod_path: "test::S", nav: NavigationTarget { file_id: FileId( 1, @@ -2132,7 +2619,7 @@ fn foo(ar<|>g: &impl Foo) {} GoToType( [ HoverGotoTypeData { - mod_path: "Foo", + mod_path: "test::Foo", nav: NavigationTarget { file_id: FileId( 1, @@ -2151,7 +2638,7 @@ fn foo(ar<|>g: &impl Foo) {} }, }, HoverGotoTypeData { - mod_path: "S", + mod_path: "test::S", nav: NavigationTarget { file_id: FileId( 1, @@ -2194,7 +2681,7 @@ fn main() { let s<|>t = foo(); } GoToType( [ HoverGotoTypeData { - mod_path: "B", + mod_path: "test::B", nav: NavigationTarget { file_id: FileId( 1, @@ -2213,7 +2700,7 @@ fn main() { let s<|>t = foo(); } }, }, HoverGotoTypeData { - mod_path: "Foo", + mod_path: "test::Foo", nav: NavigationTarget { file_id: FileId( 1, @@ -2250,7 +2737,7 @@ fn foo(ar<|>g: &dyn Foo) {} GoToType( [ HoverGotoTypeData { - mod_path: "Foo", + mod_path: "test::Foo", nav: NavigationTarget { file_id: FileId( 1, @@ -2288,7 +2775,7 @@ fn foo(ar<|>g: &dyn Foo) {} GoToType( [ HoverGotoTypeData { - mod_path: "Foo", + mod_path: "test::Foo", nav: NavigationTarget { file_id: FileId( 1, @@ -2307,7 +2794,7 @@ fn foo(ar<|>g: &dyn Foo) {} }, }, HoverGotoTypeData { - mod_path: "S", + mod_path: "test::S", nav: NavigationTarget { file_id: FileId( 1, @@ -2348,7 +2835,7 @@ fn foo(a<|>rg: &impl ImplTrait>>>) {} GoToType( [ HoverGotoTypeData { - mod_path: "ImplTrait", + mod_path: "test::ImplTrait", nav: NavigationTarget { file_id: FileId( 1, @@ -2367,7 +2854,7 @@ fn foo(a<|>rg: &impl ImplTrait>>>) {} }, }, HoverGotoTypeData { - mod_path: "B", + mod_path: "test::B", nav: NavigationTarget { file_id: FileId( 1, @@ -2386,7 +2873,7 @@ fn foo(a<|>rg: &impl ImplTrait>>>) {} }, }, HoverGotoTypeData { - mod_path: "DynTrait", + mod_path: "test::DynTrait", nav: NavigationTarget { file_id: FileId( 1, @@ -2405,7 +2892,7 @@ fn foo(a<|>rg: &impl ImplTrait>>>) {} }, }, HoverGotoTypeData { - mod_path: "S", + mod_path: "test::S", nav: NavigationTarget { file_id: FileId( 1, @@ -2453,7 +2940,7 @@ fn main() { let s<|>t = test().get(); } GoToType( [ HoverGotoTypeData { - mod_path: "Foo", + mod_path: "test::Foo", nav: NavigationTarget { file_id: FileId( 1, diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index e3af6d5bc945..570790384ee5 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -43,6 +43,7 @@ mod status; mod syntax_highlighting; mod syntax_tree; mod typing; +mod link_rewrite; use std::sync::Arc; diff --git a/crates/ide/src/link_rewrite.rs b/crates/ide/src/link_rewrite.rs new file mode 100644 index 000000000000..ff3200eefb4d --- /dev/null +++ b/crates/ide/src/link_rewrite.rs @@ -0,0 +1,81 @@ +//! Resolves and rewrites links in markdown documentation. +//! +//! Most of the implementation can be found in [`hir::doc_links`]. + +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}; + +/// 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 { + let doc = Parser::new_with_broken_link_callback( + markdown, + Options::empty(), + Some(&|label, _| Some((/*url*/ label.to_string(), /*title*/ label.to_string()))), + ); + + let doc = map_links(doc, |target, title: &str| { + // This check is imperfect, there's some overlap between valid intra-doc links + // and valid URLs so we choose to be too eager to try to resolve what might be + // a URL. + if target.contains("://") { + (target.to_string(), title.to_string()) + } else { + // 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()), + } + } + }); + let mut out = String::new(); + let mut options = CmarkOptions::default(); + options.code_block_backticks = 3; + cmark_with_options(doc, &mut out, None, options).ok(); + out +} + +// Rewrites a markdown document, resolving links using `callback` and additionally striping prefixes/suffixes on link titles. +fn map_links<'e>( + events: impl Iterator>, + callback: impl Fn(&str, &str) -> (String, String), +) -> impl Iterator> { + let mut in_link = false; + let mut link_target: Option = None; + + events.map(move |evt| match evt { + Event::Start(Tag::Link(_link_type, ref target, _)) => { + in_link = true; + link_target = Some(target.clone()); + evt + } + Event::End(Tag::Link(link_type, _target, _)) => { + in_link = false; + Event::End(Tag::Link(link_type, link_target.take().unwrap(), CowStr::Borrowed(""))) + } + Event::Text(s) if in_link => { + let (link_target_s, link_name) = callback(&link_target.take().unwrap(), &s); + link_target = Some(CowStr::Boxed(link_target_s.into())); + Event::Text(CowStr::Boxed(link_name.into())) + } + Event::Code(s) if in_link => { + let (link_target_s, link_name) = callback(&link_target.take().unwrap(), &s); + link_target = Some(CowStr::Boxed(link_target_s.into())); + Event::Code(CowStr::Boxed(link_name.into())) + } + _ => evt, + }) +} diff --git a/crates/ide/src/mock_analysis.rs b/crates/ide/src/mock_analysis.rs index 363e6d27e5a5..235796dbce78 100644 --- a/crates/ide/src/mock_analysis.rs +++ b/crates/ide/src/mock_analysis.rs @@ -115,7 +115,7 @@ impl MockAnalysis { root_crate = Some(crate_graph.add_crate_root( file_id, edition, - None, + Some("test".to_string()), cfg, env, Default::default(), diff --git a/crates/rust-analyzer/tests/rust-analyzer/main.rs b/crates/rust-analyzer/tests/rust-analyzer/main.rs index fa315ff8eefc..e97784c477df 100644 --- a/crates/rust-analyzer/tests/rust-analyzer/main.rs +++ b/crates/rust-analyzer/tests/rust-analyzer/main.rs @@ -690,5 +690,5 @@ pub fn foo(_input: TokenStream) -> TokenStream { }); let value = res.get("contents").unwrap().get("value").unwrap().to_string(); - assert_eq!(value, r#""```rust\nfoo::Bar\n```\n\n```rust\nfn bar()\n```""#) + assert_eq!(value, r#""\n```rust\nfoo::Bar\n```\n\n```rust\nfn bar()\n```""#) } diff --git a/xtask/tests/tidy.rs b/xtask/tests/tidy.rs index 4fdff62ee39a..3b140822245e 100644 --- a/xtask/tests/tidy.rs +++ b/xtask/tests/tidy.rs @@ -68,6 +68,7 @@ See https://github.com/rust-lang/rust-clippy/issues/5537 for discussion. fn check_licenses() { let expected = " 0BSD OR MIT OR Apache-2.0 +Apache-2.0 Apache-2.0 OR BSL-1.0 Apache-2.0 OR MIT Apache-2.0/MIT