aboutsummaryrefslogtreecommitdiff
path: root/crates/ide
diff options
context:
space:
mode:
authorAleksey Kladov <[email protected]>2020-08-26 17:56:41 +0100
committerAleksey Kladov <[email protected]>2020-08-26 19:24:00 +0100
commitf8a59adf5e9633aa5d10efcdbf70b408d280ef01 (patch)
treed088d683f75bbd2bed837f1ca0aca61fdba5c11b /crates/ide
parent3d6c4c143b4b4c74810318eca1b5493e43535fff (diff)
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.
Diffstat (limited to 'crates/ide')
-rw-r--r--crates/ide/Cargo.toml1
-rw-r--r--crates/ide/src/hover.rs54
-rw-r--r--crates/ide/src/link_rewrite.rs191
3 files changed, 231 insertions, 15 deletions
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"
18oorandom = "11.1.2" 18oorandom = "11.1.2"
19pulldown-cmark-to-cmark = "5.0.0" 19pulldown-cmark-to-cmark = "5.0.0"
20pulldown-cmark = {version = "0.7.2", default-features = false} 20pulldown-cmark = {version = "0.7.2", default-features = false}
21url = "2.1.1"
21 22
22stdx = { path = "../stdx", version = "0.0.0" } 23stdx = { path = "../stdx", version = "0.0.0" }
23syntax = { path = "../syntax", version = "0.0.0" } 24syntax = { 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
@@ -1756,6 +1756,60 @@ pub struct B<|>ar
1756 } 1756 }
1757 1757
1758 #[test] 1758 #[test]
1759 fn test_doc_links_enum_variant() {
1760 check(
1761 r#"
1762enum E {
1763 /// [E]
1764 V<|> { field: i32 }
1765}
1766"#,
1767 expect![[r#"
1768 *V*
1769
1770 ```rust
1771 test::E
1772 ```
1773
1774 ```rust
1775 V
1776 ```
1777
1778 ---
1779
1780 [E](https://docs.rs/test/*/test/enum.E.html)
1781 "#]],
1782 );
1783 }
1784
1785 #[test]
1786 fn test_doc_links_field() {
1787 check(
1788 r#"
1789struct S {
1790 /// [`S`]
1791 field<|>: i32
1792}
1793"#,
1794 expect![[r#"
1795 *field*
1796
1797 ```rust
1798 test::S
1799 ```
1800
1801 ```rust
1802 field: i32
1803 ```
1804
1805 ---
1806
1807 [`S`](https://docs.rs/test/*/test/struct.S.html)
1808 "#]],
1809 );
1810 }
1811
1812 #[test]
1759 fn test_hover_macro_generated_struct_fn_doc_comment() { 1813 fn test_hover_macro_generated_struct_fn_doc_comment() {
1760 mark::check!(hover_macro_generated_struct_fn_doc_comment); 1814 mark::check!(hover_macro_generated_struct_fn_doc_comment);
1761 1815
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 @@
2//! 2//!
3//! Most of the implementation can be found in [`hir::doc_links`]. 3//! Most of the implementation can be found in [`hir::doc_links`].
4 4
5use hir::{Adt, Crate, HasAttrs, ModuleDef};
6use ide_db::{defs::Definition, RootDatabase};
5use pulldown_cmark::{CowStr, Event, Options, Parser, Tag}; 7use pulldown_cmark::{CowStr, Event, Options, Parser, Tag};
6use pulldown_cmark_to_cmark::{cmark_with_options, Options as CmarkOptions}; 8use pulldown_cmark_to_cmark::{cmark_with_options, Options as CmarkOptions};
7 9use url::Url;
8use hir::resolve_doc_link;
9use ide_db::{defs::Definition, RootDatabase};
10 10
11/// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs) 11/// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs)
12pub fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String { 12pub fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String {
@@ -26,19 +26,16 @@ pub fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition)
26 // Two posibilities: 26 // Two posibilities:
27 // * path-based links: `../../module/struct.MyStruct.html` 27 // * path-based links: `../../module/struct.MyStruct.html`
28 // * module-based links (AKA intra-doc links): `super::super::module::MyStruct` 28 // * module-based links (AKA intra-doc links): `super::super::module::MyStruct`
29 let resolved = match definition { 29 if let Some(rewritten) = rewrite_intra_doc_link(db, *definition, target, title) {
30 Definition::ModuleDef(t) => resolve_doc_link(db, t, title, target), 30 return rewritten;
31 Definition::Macro(t) => resolve_doc_link(db, t, title, target), 31 }
32 Definition::Field(t) => resolve_doc_link(db, t, title, target), 32 if let Definition::ModuleDef(def) = *definition {
33 Definition::SelfType(t) => resolve_doc_link(db, t, title, target), 33 if let Some(target) = rewrite_url_link(db, def, target) {
34 Definition::Local(t) => resolve_doc_link(db, t, title, target), 34 return (target, title.to_string());
35 Definition::TypeParam(t) => resolve_doc_link(db, t, title, target), 35 }
36 };
37
38 match resolved {
39 Some((target, title)) => (target, title),
40 None => (target.to_string(), title.to_string()),
41 } 36 }
37
38 (target.to_string(), title.to_string())
42 } 39 }
43 }); 40 });
44 let mut out = String::new(); 41 let mut out = String::new();
@@ -48,6 +45,64 @@ pub fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition)
48 out 45 out
49} 46}
50 47
48fn rewrite_intra_doc_link(
49 db: &RootDatabase,
50 def: Definition,
51 target: &str,
52 title: &str,
53) -> Option<(String, String)> {
54 let link = if target.is_empty() { title } else { target };
55 let (link, ns) = parse_link(link);
56 let resolved = match def {
57 Definition::ModuleDef(def) => match def {
58 ModuleDef::Module(it) => it.resolve_doc_path(db, link, ns),
59 ModuleDef::Function(it) => it.resolve_doc_path(db, link, ns),
60 ModuleDef::Adt(it) => it.resolve_doc_path(db, link, ns),
61 ModuleDef::EnumVariant(it) => it.resolve_doc_path(db, link, ns),
62 ModuleDef::Const(it) => it.resolve_doc_path(db, link, ns),
63 ModuleDef::Static(it) => it.resolve_doc_path(db, link, ns),
64 ModuleDef::Trait(it) => it.resolve_doc_path(db, link, ns),
65 ModuleDef::TypeAlias(it) => it.resolve_doc_path(db, link, ns),
66 ModuleDef::BuiltinType(_) => return None,
67 },
68 Definition::Macro(it) => it.resolve_doc_path(db, link, ns),
69 Definition::Field(it) => it.resolve_doc_path(db, link, ns),
70 Definition::SelfType(_) | Definition::Local(_) | Definition::TypeParam(_) => return None,
71 }?;
72 let krate = resolved.module(db)?.krate();
73 let canonical_path = resolved.canonical_path(db)?;
74 let new_target = get_doc_url(db, &krate)?
75 .join(&format!("{}/", krate.display_name(db)?))
76 .ok()?
77 .join(&canonical_path.replace("::", "/"))
78 .ok()?
79 .join(&get_symbol_filename(db, &resolved)?)
80 .ok()?
81 .into_string();
82 let new_title = strip_prefixes_suffixes(title);
83 Some((new_target, new_title.to_string()))
84}
85
86/// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`).
87fn rewrite_url_link(db: &RootDatabase, def: ModuleDef, target: &str) -> Option<String> {
88 if !(target.contains("#") || target.contains(".html")) {
89 return None;
90 }
91
92 let module = def.module(db)?;
93 let krate = module.krate();
94 let canonical_path = def.canonical_path(db)?;
95 let base = format!("{}/{}", krate.display_name(db)?, canonical_path.replace("::", "/"));
96
97 get_doc_url(db, &krate)
98 .and_then(|url| url.join(&base).ok())
99 .and_then(|url| {
100 get_symbol_filename(db, &def).as_deref().map(|f| url.join(f).ok()).flatten()
101 })
102 .and_then(|url| url.join(target).ok())
103 .map(|url| url.into_string())
104}
105
51// Rewrites a markdown document, resolving links using `callback` and additionally striping prefixes/suffixes on link titles. 106// Rewrites a markdown document, resolving links using `callback` and additionally striping prefixes/suffixes on link titles.
52fn map_links<'e>( 107fn map_links<'e>(
53 events: impl Iterator<Item = Event<'e>>, 108 events: impl Iterator<Item = Event<'e>>,
@@ -79,3 +134,109 @@ fn map_links<'e>(
79 _ => evt, 134 _ => evt,
80 }) 135 })
81} 136}
137
138fn parse_link(s: &str) -> (&str, Option<hir::Namespace>) {
139 let path = strip_prefixes_suffixes(s);
140 let ns = ns_from_intra_spec(s);
141 (path, ns)
142}
143
144/// Strip prefixes, suffixes, and inline code marks from the given string.
145fn strip_prefixes_suffixes(mut s: &str) -> &str {
146 s = s.trim_matches('`');
147
148 [
149 (TYPES.0.iter(), TYPES.1.iter()),
150 (VALUES.0.iter(), VALUES.1.iter()),
151 (MACROS.0.iter(), MACROS.1.iter()),
152 ]
153 .iter()
154 .for_each(|(prefixes, suffixes)| {
155 prefixes.clone().for_each(|prefix| s = s.trim_start_matches(*prefix));
156 suffixes.clone().for_each(|suffix| s = s.trim_end_matches(*suffix));
157 });
158 s.trim_start_matches("@").trim()
159}
160
161static TYPES: ([&str; 7], [&str; 0]) =
162 (["type", "struct", "enum", "mod", "trait", "union", "module"], []);
163static VALUES: ([&str; 8], [&str; 1]) =
164 (["value", "function", "fn", "method", "const", "static", "mod", "module"], ["()"]);
165static MACROS: ([&str; 1], [&str; 1]) = (["macro"], ["!"]);
166
167/// Extract the specified namespace from an intra-doc-link if one exists.
168///
169/// # Examples
170///
171/// * `struct MyStruct` -> `Namespace::Types`
172/// * `panic!` -> `Namespace::Macros`
173/// * `fn@from_intra_spec` -> `Namespace::Values`
174fn ns_from_intra_spec(s: &str) -> Option<hir::Namespace> {
175 [
176 (hir::Namespace::Types, (TYPES.0.iter(), TYPES.1.iter())),
177 (hir::Namespace::Values, (VALUES.0.iter(), VALUES.1.iter())),
178 (hir::Namespace::Macros, (MACROS.0.iter(), MACROS.1.iter())),
179 ]
180 .iter()
181 .filter(|(_ns, (prefixes, suffixes))| {
182 prefixes
183 .clone()
184 .map(|prefix| {
185 s.starts_with(*prefix)
186 && s.chars()
187 .nth(prefix.len() + 1)
188 .map(|c| c == '@' || c == ' ')
189 .unwrap_or(false)
190 })
191 .any(|cond| cond)
192 || suffixes
193 .clone()
194 .map(|suffix| {
195 s.starts_with(*suffix)
196 && s.chars()
197 .nth(suffix.len() + 1)
198 .map(|c| c == '@' || c == ' ')
199 .unwrap_or(false)
200 })
201 .any(|cond| cond)
202 })
203 .map(|(ns, (_, _))| *ns)
204 .next()
205}
206
207fn get_doc_url(db: &RootDatabase, krate: &Crate) -> Option<Url> {
208 krate
209 .get_html_root_url(db)
210 .or_else(|| {
211 // Fallback to docs.rs. This uses `display_name` and can never be
212 // correct, but that's what fallbacks are about.
213 //
214 // FIXME: clicking on the link should just open the file in the editor,
215 // instead of falling back to external urls.
216 Some(format!("https://docs.rs/{}/*/", krate.display_name(db)?))
217 })
218 .and_then(|s| Url::parse(&s).ok())
219}
220
221/// Get the filename and extension generated for a symbol by rustdoc.
222///
223/// Example: `struct.Shard.html`
224fn get_symbol_filename(db: &RootDatabase, definition: &ModuleDef) -> Option<String> {
225 Some(match definition {
226 ModuleDef::Adt(adt) => match adt {
227 Adt::Struct(s) => format!("struct.{}.html", s.name(db)),
228 Adt::Enum(e) => format!("enum.{}.html", e.name(db)),
229 Adt::Union(u) => format!("union.{}.html", u.name(db)),
230 },
231 ModuleDef::Module(_) => "index.html".to_string(),
232 ModuleDef::Trait(t) => format!("trait.{}.html", t.name(db)),
233 ModuleDef::TypeAlias(t) => format!("type.{}.html", t.name(db)),
234 ModuleDef::BuiltinType(t) => format!("primitive.{}.html", t),
235 ModuleDef::Function(f) => format!("fn.{}.html", f.name(db)),
236 ModuleDef::EnumVariant(ev) => {
237 format!("enum.{}.html#variant.{}", ev.parent_enum(db).name(db), ev.name(db))
238 }
239 ModuleDef::Const(c) => format!("const.{}.html", c.name(db)?),
240 ModuleDef::Static(s) => format!("static.{}.html", s.name(db)?),
241 })
242}