aboutsummaryrefslogtreecommitdiff
path: root/crates/hir/src
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2020-08-25 10:11:26 +0100
committerGitHub <[email protected]>2020-08-25 10:11:26 +0100
commit96cbad9fb5a67dd6cd83fe217716d932300122b5 (patch)
treeb62378abb477d2cdfedf462c1b25b6d9fecc9f75 /crates/hir/src
parentb4bc34649857cfcf7fcb39b9af02342fb7b8c89e (diff)
parentb835f06cecd2189cb32a431fdb85245fbf53032a (diff)
Merge #4873
4873: Resolve links in hover documentation r=matklad a=zacps This PR resolves links in hover documentation. Both the upcoming intra-doc-links style and the old "path-based" style. ## Todo * [x] More tests * [ ] Benchmark (Is there an easy way to benchmark this?) * [x] ~~Resolve issues with the markdown parser/get rid of it~~ Migrate to `pulldown_cmark_to_cmark` * [x] Reorganise code (Tips appreciated) --- Fixes #503 Co-authored-by: Zac Pullar-Strecker <[email protected]>
Diffstat (limited to 'crates/hir/src')
-rw-r--r--crates/hir/src/code_model.rs103
-rw-r--r--crates/hir/src/doc_links.rs233
-rw-r--r--crates/hir/src/lib.rs7
3 files changed, 339 insertions, 4 deletions
diff --git a/crates/hir/src/code_model.rs b/crates/hir/src/code_model.rs
index 3d92d0c0d..94dd7f6f5 100644
--- a/crates/hir/src/code_model.rs
+++ b/crates/hir/src/code_model.rs
@@ -20,7 +20,7 @@ use hir_def::{
20 type_ref::{Mutability, TypeRef}, 20 type_ref::{Mutability, TypeRef},
21 AdtId, AssocContainerId, ConstId, DefWithBodyId, EnumId, FunctionId, GenericDefId, HasModule, 21 AdtId, AssocContainerId, ConstId, DefWithBodyId, EnumId, FunctionId, GenericDefId, HasModule,
22 ImplId, LocalEnumVariantId, LocalFieldId, LocalModuleId, Lookup, ModuleId, StaticId, StructId, 22 ImplId, LocalEnumVariantId, LocalFieldId, LocalModuleId, Lookup, ModuleId, StaticId, StructId,
23 TraitId, TypeAliasId, TypeParamId, UnionId, 23 TraitId, TypeAliasId, TypeParamId, UnionId, VariantId,
24}; 24};
25use hir_expand::{ 25use hir_expand::{
26 diagnostics::DiagnosticSink, 26 diagnostics::DiagnosticSink,
@@ -39,9 +39,11 @@ use syntax::{
39 ast::{self, AttrsOwner, NameOwner}, 39 ast::{self, AttrsOwner, NameOwner},
40 AstNode, SmolStr, 40 AstNode, SmolStr,
41}; 41};
42use tt::{Ident, Leaf, Literal, TokenTree};
42 43
43use crate::{ 44use crate::{
44 db::{DefDatabase, HirDatabase}, 45 db::{DefDatabase, HirDatabase},
46 doc_links::Resolvable,
45 has_source::HasSource, 47 has_source::HasSource,
46 HirDisplay, InFile, Name, 48 HirDisplay, InFile, Name,
47}; 49};
@@ -122,6 +124,31 @@ impl Crate {
122 pub fn all(db: &dyn HirDatabase) -> Vec<Crate> { 124 pub fn all(db: &dyn HirDatabase) -> Vec<Crate> {
123 db.crate_graph().iter().map(|id| Crate { id }).collect() 125 db.crate_graph().iter().map(|id| Crate { id }).collect()
124 } 126 }
127
128 /// Try to get the root URL of the documentation of a crate.
129 pub fn get_html_root_url(self: &Crate, db: &dyn HirDatabase) -> Option<String> {
130 // Look for #![doc(html_root_url = "...")]
131 let attrs = db.attrs(AttrDef::from(self.root_module(db)).into());
132 let doc_attr_q = attrs.by_key("doc");
133
134 if !doc_attr_q.exists() {
135 return None;
136 }
137
138 let doc_url = doc_attr_q.tt_values().map(|tt| {
139 let name = tt.token_trees.iter()
140 .skip_while(|tt| !matches!(tt, TokenTree::Leaf(Leaf::Ident(Ident{text: ref ident, ..})) if ident == "html_root_url"))
141 .skip(2)
142 .next();
143
144 match name {
145 Some(TokenTree::Leaf(Leaf::Literal(Literal{ref text, ..}))) => Some(text),
146 _ => None
147 }
148 }).flat_map(|t| t).next();
149
150 doc_url.map(|s| s.trim_matches('"').trim_end_matches("/").to_owned() + "/")
151 }
125} 152}
126 153
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -198,7 +225,6 @@ impl ModuleDef {
198 ModuleDef::Function(it) => Some(it.name(db)), 225 ModuleDef::Function(it) => Some(it.name(db)),
199 ModuleDef::EnumVariant(it) => Some(it.name(db)), 226 ModuleDef::EnumVariant(it) => Some(it.name(db)),
200 ModuleDef::TypeAlias(it) => Some(it.name(db)), 227 ModuleDef::TypeAlias(it) => Some(it.name(db)),
201
202 ModuleDef::Module(it) => it.name(db), 228 ModuleDef::Module(it) => it.name(db),
203 ModuleDef::Const(it) => it.name(db), 229 ModuleDef::Const(it) => it.name(db),
204 ModuleDef::Static(it) => it.name(db), 230 ModuleDef::Static(it) => it.name(db),
@@ -1771,3 +1797,76 @@ pub trait HasVisibility {
1771 vis.is_visible_from(db.upcast(), module.id) 1797 vis.is_visible_from(db.upcast(), module.id)
1772 } 1798 }
1773} 1799}
1800
1801impl Resolvable for ModuleDef {
1802 fn resolver<D: DefDatabase + HirDatabase>(&self, db: &D) -> Option<Resolver> {
1803 Some(match self {
1804 ModuleDef::Module(m) => ModuleId::from(m.clone()).resolver(db),
1805 ModuleDef::Function(f) => FunctionId::from(f.clone()).resolver(db),
1806 ModuleDef::Adt(adt) => AdtId::from(adt.clone()).resolver(db),
1807 ModuleDef::EnumVariant(ev) => {
1808 GenericDefId::from(GenericDef::from(ev.clone())).resolver(db)
1809 }
1810 ModuleDef::Const(c) => GenericDefId::from(GenericDef::from(c.clone())).resolver(db),
1811 ModuleDef::Static(s) => StaticId::from(s.clone()).resolver(db),
1812 ModuleDef::Trait(t) => TraitId::from(t.clone()).resolver(db),
1813 ModuleDef::TypeAlias(t) => ModuleId::from(t.module(db)).resolver(db),
1814 // FIXME: This should be a resolver relative to `std/core`
1815 ModuleDef::BuiltinType(_t) => None?,
1816 })
1817 }
1818
1819 fn try_into_module_def(self) -> Option<ModuleDef> {
1820 Some(self)
1821 }
1822}
1823
1824impl Resolvable for TypeParam {
1825 fn resolver<D: DefDatabase + HirDatabase>(&self, db: &D) -> Option<Resolver> {
1826 Some(Into::<ModuleId>::into(self.module(db)).resolver(db))
1827 }
1828
1829 fn try_into_module_def(self) -> Option<ModuleDef> {
1830 None
1831 }
1832}
1833
1834impl Resolvable for MacroDef {
1835 fn resolver<D: DefDatabase + HirDatabase>(&self, db: &D) -> Option<Resolver> {
1836 Some(Into::<ModuleId>::into(self.module(db)?).resolver(db))
1837 }
1838
1839 fn try_into_module_def(self) -> Option<ModuleDef> {
1840 None
1841 }
1842}
1843
1844impl Resolvable for Field {
1845 fn resolver<D: DefDatabase + HirDatabase>(&self, db: &D) -> Option<Resolver> {
1846 Some(Into::<VariantId>::into(Into::<VariantDef>::into(self.parent_def(db))).resolver(db))
1847 }
1848
1849 fn try_into_module_def(self) -> Option<ModuleDef> {
1850 None
1851 }
1852}
1853
1854impl Resolvable for ImplDef {
1855 fn resolver<D: DefDatabase + HirDatabase>(&self, db: &D) -> Option<Resolver> {
1856 Some(Into::<ModuleId>::into(self.module(db)).resolver(db))
1857 }
1858
1859 fn try_into_module_def(self) -> Option<ModuleDef> {
1860 None
1861 }
1862}
1863
1864impl Resolvable for Local {
1865 fn resolver<D: DefDatabase + HirDatabase>(&self, db: &D) -> Option<Resolver> {
1866 Some(Into::<ModuleId>::into(self.module(db)).resolver(db))
1867 }
1868
1869 fn try_into_module_def(self) -> Option<ModuleDef> {
1870 None
1871 }
1872}
diff --git a/crates/hir/src/doc_links.rs b/crates/hir/src/doc_links.rs
new file mode 100644
index 000000000..dd2379bfc
--- /dev/null
+++ b/crates/hir/src/doc_links.rs
@@ -0,0 +1,233 @@
1//! Resolves links in markdown documentation.
2
3use std::iter::once;
4
5use itertools::Itertools;
6use url::Url;
7
8use crate::{db::HirDatabase, Adt, AsName, Crate, Hygiene, ItemInNs, ModPath, ModuleDef};
9use hir_def::{db::DefDatabase, resolver::Resolver};
10use syntax::ast::Path;
11
12pub fn resolve_doc_link<T: Resolvable + Clone, D: DefDatabase + HirDatabase>(
13 db: &D,
14 definition: &T,
15 link_text: &str,
16 link_target: &str,
17) -> Option<(String, String)> {
18 try_resolve_intra(db, definition, link_text, &link_target).or_else(|| {
19 if let Some(definition) = definition.clone().try_into_module_def() {
20 try_resolve_path(db, &definition, &link_target)
21 .map(|target| (target, link_text.to_string()))
22 } else {
23 None
24 }
25 })
26}
27
28/// Try to resolve path to local documentation via intra-doc-links (i.e. `super::gateway::Shard`).
29///
30/// See [RFC1946](https://github.com/rust-lang/rfcs/blob/master/text/1946-intra-rustdoc-links.md).
31fn try_resolve_intra<T: Resolvable, D: DefDatabase + HirDatabase>(
32 db: &D,
33 definition: &T,
34 link_text: &str,
35 link_target: &str,
36) -> Option<(String, String)> {
37 // Set link_target for implied shortlinks
38 let link_target =
39 if link_target.is_empty() { link_text.trim_matches('`') } else { link_target };
40
41 let doclink = IntraDocLink::from(link_target);
42
43 // Parse link as a module path
44 let path = Path::parse(doclink.path).ok()?;
45 let modpath = ModPath::from_src(path, &Hygiene::new_unhygienic()).unwrap();
46
47 // Resolve it relative to symbol's location (according to the RFC this should consider small scopes)
48 let resolver = definition.resolver(db)?;
49
50 let resolved = resolver.resolve_module_path_in_items(db, &modpath);
51 let (defid, namespace) = match doclink.namespace {
52 // FIXME: .or(resolved.macros)
53 None => resolved
54 .types
55 .map(|t| (t.0, Namespace::Types))
56 .or(resolved.values.map(|t| (t.0, Namespace::Values)))?,
57 Some(ns @ Namespace::Types) => (resolved.types?.0, ns),
58 Some(ns @ Namespace::Values) => (resolved.values?.0, ns),
59 // FIXME:
60 Some(Namespace::Macros) => None?,
61 };
62
63 // Get the filepath of the final symbol
64 let def: ModuleDef = defid.into();
65 let module = def.module(db)?;
66 let krate = module.krate();
67 let ns = match namespace {
68 Namespace::Types => ItemInNs::Types(defid),
69 Namespace::Values => ItemInNs::Values(defid),
70 // FIXME:
71 Namespace::Macros => None?,
72 };
73 let import_map = db.import_map(krate.into());
74 let path = import_map.path_of(ns)?;
75
76 Some((
77 get_doc_url(db, &krate)?
78 .join(&format!("{}/", krate.display_name(db)?))
79 .ok()?
80 .join(&path.segments.iter().map(|name| name.to_string()).join("/"))
81 .ok()?
82 .join(&get_symbol_filename(db, &def)?)
83 .ok()?
84 .into_string(),
85 strip_prefixes_suffixes(link_text).to_string(),
86 ))
87}
88
89/// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`).
90fn try_resolve_path(db: &dyn HirDatabase, moddef: &ModuleDef, link_target: &str) -> Option<String> {
91 if !link_target.contains("#") && !link_target.contains(".html") {
92 return None;
93 }
94 let ns = ItemInNs::Types(moddef.clone().into());
95
96 let module = moddef.module(db)?;
97 let krate = module.krate();
98 let import_map = db.import_map(krate.into());
99 let base = once(format!("{}", krate.display_name(db)?))
100 .chain(import_map.path_of(ns)?.segments.iter().map(|name| format!("{}", name)))
101 .join("/");
102
103 get_doc_url(db, &krate)
104 .and_then(|url| url.join(&base).ok())
105 .and_then(|url| {
106 get_symbol_filename(db, moddef).as_deref().map(|f| url.join(f).ok()).flatten()
107 })
108 .and_then(|url| url.join(link_target).ok())
109 .map(|url| url.into_string())
110}
111
112/// Strip prefixes, suffixes, and inline code marks from the given string.
113fn strip_prefixes_suffixes(mut s: &str) -> &str {
114 s = s.trim_matches('`');
115
116 [
117 (TYPES.0.iter(), TYPES.1.iter()),
118 (VALUES.0.iter(), VALUES.1.iter()),
119 (MACROS.0.iter(), MACROS.1.iter()),
120 ]
121 .iter()
122 .for_each(|(prefixes, suffixes)| {
123 prefixes.clone().for_each(|prefix| s = s.trim_start_matches(*prefix));
124 suffixes.clone().for_each(|suffix| s = s.trim_end_matches(*suffix));
125 });
126 let s = s.trim_start_matches("@").trim();
127 s
128}
129
130fn get_doc_url(db: &dyn HirDatabase, krate: &Crate) -> Option<Url> {
131 krate
132 .get_html_root_url(db)
133 .or_else(||
134 // Fallback to docs.rs
135 // FIXME: Specify an exact version here. This may be difficult, as multiple versions of the same crate could exist.
136 Some(format!("https://docs.rs/{}/*/", krate.display_name(db)?)))
137 .and_then(|s| Url::parse(&s).ok())
138}
139
140/// Get the filename and extension generated for a symbol by rustdoc.
141///
142/// Example: `struct.Shard.html`
143fn get_symbol_filename(db: &dyn HirDatabase, definition: &ModuleDef) -> Option<String> {
144 Some(match definition {
145 ModuleDef::Adt(adt) => match adt {
146 Adt::Struct(s) => format!("struct.{}.html", s.name(db)),
147 Adt::Enum(e) => format!("enum.{}.html", e.name(db)),
148 Adt::Union(u) => format!("union.{}.html", u.name(db)),
149 },
150 ModuleDef::Module(_) => "index.html".to_string(),
151 ModuleDef::Trait(t) => format!("trait.{}.html", t.name(db)),
152 ModuleDef::TypeAlias(t) => format!("type.{}.html", t.name(db)),
153 ModuleDef::BuiltinType(t) => format!("primitive.{}.html", t.as_name()),
154 ModuleDef::Function(f) => format!("fn.{}.html", f.name(db)),
155 ModuleDef::EnumVariant(ev) => {
156 format!("enum.{}.html#variant.{}", ev.parent_enum(db).name(db), ev.name(db))
157 }
158 ModuleDef::Const(c) => format!("const.{}.html", c.name(db)?),
159 ModuleDef::Static(s) => format!("static.{}.html", s.name(db)?),
160 })
161}
162
163struct IntraDocLink<'s> {
164 path: &'s str,
165 namespace: Option<Namespace>,
166}
167
168impl<'s> From<&'s str> for IntraDocLink<'s> {
169 fn from(s: &'s str) -> Self {
170 Self { path: strip_prefixes_suffixes(s), namespace: Namespace::from_intra_spec(s) }
171 }
172}
173
174#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
175enum Namespace {
176 Types,
177 Values,
178 Macros,
179}
180
181static TYPES: ([&str; 7], [&str; 0]) =
182 (["type", "struct", "enum", "mod", "trait", "union", "module"], []);
183static VALUES: ([&str; 8], [&str; 1]) =
184 (["value", "function", "fn", "method", "const", "static", "mod", "module"], ["()"]);
185static MACROS: ([&str; 1], [&str; 1]) = (["macro"], ["!"]);
186
187impl Namespace {
188 /// Extract the specified namespace from an intra-doc-link if one exists.
189 ///
190 /// # Examples
191 ///
192 /// * `struct MyStruct` -> `Namespace::Types`
193 /// * `panic!` -> `Namespace::Macros`
194 /// * `fn@from_intra_spec` -> `Namespace::Values`
195 fn from_intra_spec(s: &str) -> Option<Self> {
196 [
197 (Namespace::Types, (TYPES.0.iter(), TYPES.1.iter())),
198 (Namespace::Values, (VALUES.0.iter(), VALUES.1.iter())),
199 (Namespace::Macros, (MACROS.0.iter(), MACROS.1.iter())),
200 ]
201 .iter()
202 .filter(|(_ns, (prefixes, suffixes))| {
203 prefixes
204 .clone()
205 .map(|prefix| {
206 s.starts_with(*prefix)
207 && s.chars()
208 .nth(prefix.len() + 1)
209 .map(|c| c == '@' || c == ' ')
210 .unwrap_or(false)
211 })
212 .any(|cond| cond)
213 || suffixes
214 .clone()
215 .map(|suffix| {
216 s.starts_with(*suffix)
217 && s.chars()
218 .nth(suffix.len() + 1)
219 .map(|c| c == '@' || c == ' ')
220 .unwrap_or(false)
221 })
222 .any(|cond| cond)
223 })
224 .map(|(ns, (_, _))| *ns)
225 .next()
226 }
227}
228
229/// Sealed trait used solely for the generic bound on [`resolve_doc_link`].
230pub trait Resolvable {
231 fn resolver<D: DefDatabase + HirDatabase>(&self, db: &D) -> Option<Resolver>;
232 fn try_into_module_def(self) -> Option<ModuleDef>;
233}
diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs
index 78d8651cb..d1f4d7813 100644
--- a/crates/hir/src/lib.rs
+++ b/crates/hir/src/lib.rs
@@ -27,6 +27,7 @@ pub mod diagnostics;
27 27
28mod from_id; 28mod from_id;
29mod code_model; 29mod code_model;
30mod doc_links;
30 31
31mod has_source; 32mod has_source;
32 33
@@ -37,6 +38,7 @@ pub use crate::{
37 Function, GenericDef, HasAttrs, HasVisibility, ImplDef, Local, MacroDef, Module, ModuleDef, 38 Function, GenericDef, HasAttrs, HasVisibility, ImplDef, Local, MacroDef, Module, ModuleDef,
38 ScopeDef, Static, Struct, Trait, Type, TypeAlias, TypeParam, Union, VariantDef, Visibility, 39 ScopeDef, Static, Struct, Trait, Type, TypeAlias, TypeParam, Union, VariantDef, Visibility,
39 }, 40 },
41 doc_links::resolve_doc_link,
40 has_source::HasSource, 42 has_source::HasSource,
41 semantics::{original_range, PathResolution, Semantics, SemanticsScope}, 43 semantics::{original_range, PathResolution, Semantics, SemanticsScope},
42}; 44};
@@ -47,13 +49,14 @@ pub use hir_def::{
47 body::scope::ExprScopes, 49 body::scope::ExprScopes,
48 builtin_type::BuiltinType, 50 builtin_type::BuiltinType,
49 docs::Documentation, 51 docs::Documentation,
52 item_scope::ItemInNs,
50 nameres::ModuleSource, 53 nameres::ModuleSource,
51 path::ModPath, 54 path::ModPath,
52 type_ref::{Mutability, TypeRef}, 55 type_ref::{Mutability, TypeRef},
53}; 56};
54pub use hir_expand::{ 57pub use hir_expand::{
55 name::Name, HirFileId, InFile, MacroCallId, MacroCallLoc, /* FIXME */ MacroDefId, 58 name::AsName, name::Name, HirFileId, InFile, MacroCallId, MacroCallLoc,
56 MacroFile, Origin, 59 /* FIXME */ MacroDefId, MacroFile, Origin,
57}; 60};
58pub use hir_ty::display::HirDisplay; 61pub use hir_ty::display::HirDisplay;
59 62