aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide/src')
-rw-r--r--crates/ra_ide/src/hover.rs439
-rw-r--r--crates/ra_ide/src/mock_analysis.rs2
2 files changed, 436 insertions, 5 deletions
diff --git a/crates/ra_ide/src/hover.rs b/crates/ra_ide/src/hover.rs
index aa48cb412..ad68bc43c 100644
--- a/crates/ra_ide/src/hover.rs
+++ b/crates/ra_ide/src/hover.rs
@@ -1,16 +1,26 @@
1use std::collections::{HashMap, HashSet};
2use std::iter::once;
3
1use hir::{ 4use hir::{
2 Adt, AsAssocItem, AssocItemContainer, Documentation, FieldSource, HasSource, HirDisplay, 5 db::DefDatabase, Adt, AsAssocItem, AsName, AssocItemContainer, AttrDef, Crate, Documentation,
3 Module, ModuleDef, ModuleSource, Semantics, 6 FieldSource, HasSource, HirDisplay, Hygiene, ItemInNs, ModPath, Module, ModuleDef,
7 ModuleSource, Semantics,
4}; 8};
5use itertools::Itertools; 9use itertools::Itertools;
10use lazy_static::lazy_static;
11use maplit::{hashmap, hashset};
12use pulldown_cmark::{CowStr, Event, Options, Parser, Tag};
13use pulldown_cmark_to_cmark::cmark;
6use ra_db::SourceDatabase; 14use ra_db::SourceDatabase;
7use ra_ide_db::{ 15use ra_ide_db::{
8 defs::{classify_name, classify_name_ref, Definition}, 16 defs::{classify_name, classify_name_ref, Definition},
9 RootDatabase, 17 RootDatabase,
10}; 18};
11use ra_syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset, T}; 19use ra_syntax::{ast, ast::Path, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset, T};
20use ra_tt::{Ident, Leaf, Literal, TokenTree};
12use stdx::format_to; 21use stdx::format_to;
13use test_utils::mark; 22use test_utils::mark;
23use url::Url;
14 24
15use crate::{ 25use crate::{
16 display::{macro_label, ShortLabel, ToNav, TryToNav}, 26 display::{macro_label, ShortLabel, ToNav, TryToNav},
@@ -92,7 +102,8 @@ pub(crate) fn hover(db: &RootDatabase, position: FilePosition) -> Option<RangeIn
92 }; 102 };
93 if let Some(definition) = definition { 103 if let Some(definition) = definition {
94 if let Some(markup) = hover_for_definition(db, definition) { 104 if let Some(markup) = hover_for_definition(db, definition) {
95 res.markup = markup; 105 let markup = rewrite_links(db, &markup.as_str(), &definition);
106 res.markup = Markup::from(markup);
96 if let Some(action) = show_implementations_action(db, definition) { 107 if let Some(action) = show_implementations_action(db, definition) {
97 res.actions.push(action); 108 res.actions.push(action);
98 } 109 }
@@ -335,6 +346,277 @@ fn hover_for_definition(db: &RootDatabase, def: Definition) -> Option<Markup> {
335 } 346 }
336} 347}
337 348
349// Rewrites a markdown document, resolving links using `callback` and additionally striping prefixes/suffixes on link titles.
350fn map_links<'e>(
351 events: impl Iterator<Item = Event<'e>>,
352 callback: impl Fn(&str, &str) -> (String, String),
353) -> impl Iterator<Item = Event<'e>> {
354 let mut in_link = false;
355 let mut link_target: Option<CowStr> = None;
356
357 events.map(move |evt| match evt {
358 Event::Start(Tag::Link(_link_type, ref target, _)) => {
359 in_link = true;
360 link_target = Some(target.clone());
361 evt
362 }
363 Event::End(Tag::Link(link_type, _target, _)) => {
364 in_link = false;
365 Event::End(Tag::Link(link_type, link_target.take().unwrap(), CowStr::Borrowed("")))
366 }
367 Event::Text(s) if in_link => {
368 let (link_target_s, link_name) = callback(&link_target.take().unwrap(), &s);
369 link_target = Some(CowStr::Boxed(link_target_s.into()));
370 Event::Text(CowStr::Boxed(link_name.into()))
371 }
372 Event::Code(s) if in_link => {
373 let (link_target_s, link_name) = callback(&link_target.take().unwrap(), &s);
374 link_target = Some(CowStr::Boxed(link_target_s.into()));
375 Event::Code(CowStr::Boxed(link_name.into()))
376 }
377 _ => evt,
378 })
379}
380
381/// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs)
382fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String {
383 let doc = Parser::new_with_broken_link_callback(
384 markdown,
385 Options::empty(),
386 Some(&|label, _| Some((/*url*/ label.to_string(), /*title*/ label.to_string()))),
387 );
388
389 let doc = map_links(doc, |target, title: &str| {
390 // This check is imperfect, there's some overlap between valid intra-doc links
391 // and valid URLs so we choose to be too eager to try to resolve what might be
392 // a URL.
393 if target.contains("://") {
394 (target.to_string(), title.to_string())
395 } else {
396 // Two posibilities:
397 // * path-based links: `../../module/struct.MyStruct.html`
398 // * module-based links (AKA intra-doc links): `super::super::module::MyStruct`
399 let resolved = try_resolve_intra(db, definition, title, &target).or_else(|| {
400 try_resolve_path(db, definition, &target).map(|target| (target, title.to_string()))
401 });
402
403 if let Some((target, title)) = resolved {
404 (target, title)
405 } else {
406 (target.to_string(), title.to_string())
407 }
408 }
409 });
410 let mut out = String::new();
411 cmark(doc, &mut out, None).ok();
412 out
413}
414
415#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
416enum Namespace {
417 Types,
418 Values,
419 Macros,
420}
421
422lazy_static!(
423 /// Map of namespaces to identifying prefixes and suffixes as defined by RFC1946.
424 static ref NS_MAP: HashMap<Namespace, (HashSet<&'static str>, HashSet<&'static str>)> = hashmap!{
425 Namespace::Types => (hashset!{"type", "struct", "enum", "mod", "trait", "union", "module"}, hashset!{}),
426 Namespace::Values => (hashset!{"value", "function", "fn", "method", "const", "static", "mod", "module"}, hashset!{"()"}),
427 Namespace::Macros => (hashset!{"macro"}, hashset!{"!"})
428 };
429);
430
431impl Namespace {
432 /// Extract the specified namespace from an intra-doc-link if one exists.
433 fn from_intra_spec(s: &str) -> Option<Self> {
434 NS_MAP
435 .iter()
436 .filter(|(_ns, (prefixes, suffixes))| {
437 prefixes
438 .iter()
439 .map(|prefix| {
440 s.starts_with(prefix)
441 && s.chars()
442 .nth(prefix.len() + 1)
443 .map(|c| c == '@' || c == ' ')
444 .unwrap_or(false)
445 })
446 .any(|cond| cond)
447 || suffixes
448 .iter()
449 .map(|suffix| {
450 s.starts_with(suffix)
451 && s.chars()
452 .nth(suffix.len() + 1)
453 .map(|c| c == '@' || c == ' ')
454 .unwrap_or(false)
455 })
456 .any(|cond| cond)
457 })
458 .map(|(ns, (_, _))| *ns)
459 .next()
460 }
461}
462
463// Strip prefixes, suffixes, and inline code marks from the given string.
464fn strip_prefixes_suffixes(mut s: &str) -> &str {
465 s = s.trim_matches('`');
466 NS_MAP.iter().for_each(|(_, (prefixes, suffixes))| {
467 prefixes.iter().for_each(|prefix| s = s.trim_start_matches(prefix));
468 suffixes.iter().for_each(|suffix| s = s.trim_end_matches(suffix));
469 });
470 s.trim_start_matches("@").trim()
471}
472
473/// Try to resolve path to local documentation via intra-doc-links (i.e. `super::gateway::Shard`).
474///
475/// See [RFC1946](https://github.com/rust-lang/rfcs/blob/master/text/1946-intra-rustdoc-links.md).
476fn try_resolve_intra(
477 db: &RootDatabase,
478 definition: &Definition,
479 link_text: &str,
480 link_target: &str,
481) -> Option<(String, String)> {
482 // Set link_target for implied shortlinks
483 let link_target =
484 if link_target.is_empty() { link_text.trim_matches('`') } else { link_target };
485
486 // Namespace disambiguation
487 let namespace = Namespace::from_intra_spec(link_target);
488
489 // Strip prefixes/suffixes
490 let link_target = strip_prefixes_suffixes(link_target);
491
492 // Parse link as a module path
493 let path = Path::parse(link_target).ok()?;
494 let modpath = ModPath::from_src(path, &Hygiene::new_unhygienic()).unwrap();
495
496 // Resolve it relative to symbol's location (according to the RFC this should consider small scopes
497 let resolver = definition.resolver(db)?;
498
499 let resolved = resolver.resolve_module_path_in_items(db, &modpath);
500 let (defid, namespace) = match namespace {
501 // FIXME: .or(resolved.macros)
502 None => resolved
503 .types
504 .map(|t| (t.0, Namespace::Types))
505 .or(resolved.values.map(|t| (t.0, Namespace::Values)))?,
506 Some(ns @ Namespace::Types) => (resolved.types?.0, ns),
507 Some(ns @ Namespace::Values) => (resolved.values?.0, ns),
508 // FIXME:
509 Some(Namespace::Macros) => None?,
510 };
511
512 // Get the filepath of the final symbol
513 let def: ModuleDef = defid.into();
514 let module = def.module(db)?;
515 let krate = module.krate();
516 let ns = match namespace {
517 Namespace::Types => ItemInNs::Types(defid),
518 Namespace::Values => ItemInNs::Values(defid),
519 // FIXME:
520 Namespace::Macros => None?,
521 };
522 let import_map = db.import_map(krate.into());
523 let path = import_map.path_of(ns)?;
524
525 Some((
526 get_doc_url(db, &krate)?
527 .join(&format!("{}/", krate.display_name(db)?))
528 .ok()?
529 .join(&path.segments.iter().map(|name| format!("{}", name)).join("/"))
530 .ok()?
531 .join(&get_symbol_filename(db, &Definition::ModuleDef(def))?)
532 .ok()?
533 .into_string(),
534 strip_prefixes_suffixes(link_text).to_string(),
535 ))
536}
537
538/// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`).
539fn try_resolve_path(db: &RootDatabase, definition: &Definition, link: &str) -> Option<String> {
540 if !link.contains("#") && !link.contains(".html") {
541 return None;
542 }
543 let ns = if let Definition::ModuleDef(moddef) = definition {
544 ItemInNs::Types(moddef.clone().into())
545 } else {
546 return None;
547 };
548 let module = definition.module(db)?;
549 let krate = module.krate();
550 let import_map = db.import_map(krate.into());
551 let base = once(format!("{}", krate.display_name(db)?))
552 .chain(import_map.path_of(ns)?.segments.iter().map(|name| format!("{}", name)))
553 .join("/");
554
555 get_doc_url(db, &krate)
556 .and_then(|url| url.join(&base).ok())
557 .and_then(|url| {
558 get_symbol_filename(db, definition).as_deref().map(|f| url.join(f).ok()).flatten()
559 })
560 .and_then(|url| url.join(link).ok())
561 .map(|url| url.into_string())
562}
563
564/// Try to get the root URL of the documentation of a crate.
565fn get_doc_url(db: &RootDatabase, krate: &Crate) -> Option<Url> {
566 // Look for #![doc(html_root_url = "...")]
567 let attrs = db.attrs(AttrDef::from(krate.root_module(db)?).into());
568 let doc_attr_q = attrs.by_key("doc");
569
570 let doc_url = if doc_attr_q.exists() {
571 doc_attr_q.tt_values().map(|tt| {
572 let name = tt.token_trees.iter()
573 .skip_while(|tt| !matches!(tt, TokenTree::Leaf(Leaf::Ident(Ident{text: ref ident, ..})) if ident == "html_root_url"))
574 .skip(2)
575 .next();
576
577 match name {
578 Some(TokenTree::Leaf(Leaf::Literal(Literal{ref text, ..}))) => Some(text),
579 _ => None
580 }
581 }).flat_map(|t| t).next().map(|s| s.to_string())
582 } else {
583 // Fallback to docs.rs
584 // FIXME: Specify an exact version here (from Cargo.lock)
585 Some(format!("https://docs.rs/{}/*", krate.display_name(db)?))
586 };
587
588 doc_url
589 .map(|s| s.trim_matches('"').trim_end_matches("/").to_owned() + "/")
590 .and_then(|s| Url::parse(&s).ok())
591}
592
593/// Get the filename and extension generated for a symbol by rustdoc.
594///
595/// Example: `struct.Shard.html`
596fn get_symbol_filename(db: &RootDatabase, definition: &Definition) -> Option<String> {
597 Some(match definition {
598 Definition::ModuleDef(def) => match def {
599 ModuleDef::Adt(adt) => match adt {
600 Adt::Struct(s) => format!("struct.{}.html", s.name(db)),
601 Adt::Enum(e) => format!("enum.{}.html", e.name(db)),
602 Adt::Union(u) => format!("union.{}.html", u.name(db)),
603 },
604 ModuleDef::Module(_) => "index.html".to_string(),
605 ModuleDef::Trait(t) => format!("trait.{}.html", t.name(db)),
606 ModuleDef::TypeAlias(t) => format!("type.{}.html", t.name(db)),
607 ModuleDef::BuiltinType(t) => format!("primitive.{}.html", t.as_name()),
608 ModuleDef::Function(f) => format!("fn.{}.html", f.name(db)),
609 ModuleDef::EnumVariant(ev) => {
610 format!("enum.{}.html#variant.{}", ev.parent_enum(db).name(db), ev.name(db))
611 }
612 ModuleDef::Const(c) => format!("const.{}.html", c.name(db)?),
613 ModuleDef::Static(s) => format!("static.{}.html", s.name(db)?),
614 },
615 Definition::Macro(m) => format!("macro.{}.html", m.name(db)?),
616 _ => None?,
617 })
618}
619
338fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> { 620fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> {
339 return tokens.max_by_key(priority); 621 return tokens.max_by_key(priority);
340 fn priority(n: &SyntaxToken) -> usize { 622 fn priority(n: &SyntaxToken) -> usize {
@@ -1194,6 +1476,155 @@ fn foo() { let bar = Ba<|>r; }
1194 } 1476 }
1195 1477
1196 #[test] 1478 #[test]
1479 fn test_hover_path_link() {
1480 check_hover_result(
1481 r"
1482 //- /lib.rs
1483 pub struct Foo;
1484 /// [Foo](struct.Foo.html)
1485 pub struct B<|>ar
1486 ",
1487 &["pub struct Bar\n```\n___\n\n[Foo](https://docs.rs/test/*/test/struct.Foo.html)"],
1488 );
1489 }
1490
1491 #[test]
1492 fn test_hover_path_link_no_strip() {
1493 check_hover_result(
1494 r"
1495 //- /lib.rs
1496 pub struct Foo;
1497 /// [struct Foo](struct.Foo.html)
1498 pub struct B<|>ar
1499 ",
1500 &["pub struct Bar\n```\n___\n\n[struct Foo](https://docs.rs/test/*/test/struct.Foo.html)"],
1501 );
1502 }
1503
1504 #[test]
1505 fn test_hover_intra_link() {
1506 check_hover_result(
1507 r"
1508 //- /lib.rs
1509 pub mod foo {
1510 pub struct Foo;
1511 }
1512 /// [Foo](foo::Foo)
1513 pub struct B<|>ar
1514 ",
1515 &["pub struct Bar\n```\n___\n\n[Foo](https://docs.rs/test/*/test/foo/struct.Foo.html)"],
1516 );
1517 }
1518
1519 #[test]
1520 fn test_hover_intra_link_shortlink() {
1521 check_hover_result(
1522 r"
1523 //- /lib.rs
1524 pub struct Foo;
1525 /// [Foo]
1526 pub struct B<|>ar
1527 ",
1528 &["pub struct Bar\n```\n___\n\n[Foo](https://docs.rs/test/*/test/struct.Foo.html)"],
1529 );
1530 }
1531
1532 #[test]
1533 fn test_hover_intra_link_shortlink_code() {
1534 check_hover_result(
1535 r"
1536 //- /lib.rs
1537 pub struct Foo;
1538 /// [`Foo`]
1539 pub struct B<|>ar
1540 ",
1541 &["pub struct Bar\n```\n___\n\n[`Foo`](https://docs.rs/test/*/test/struct.Foo.html)"],
1542 );
1543 }
1544
1545 #[test]
1546 fn test_hover_intra_link_namespaced() {
1547 check_hover_result(
1548 r"
1549 //- /lib.rs
1550 pub struct Foo;
1551 fn Foo() {}
1552 /// [Foo()]
1553 pub struct B<|>ar
1554 ",
1555 &["pub struct Bar\n```\n___\n\n[Foo](https://docs.rs/test/*/test/struct.Foo.html)"],
1556 );
1557 }
1558
1559 #[test]
1560 fn test_hover_intra_link_shortlink_namspaced_code() {
1561 check_hover_result(
1562 r"
1563 //- /lib.rs
1564 pub struct Foo;
1565 /// [`struct Foo`]
1566 pub struct B<|>ar
1567 ",
1568 &["pub struct Bar\n```\n___\n\n[`Foo`](https://docs.rs/test/*/test/struct.Foo.html)"],
1569 );
1570 }
1571
1572 #[test]
1573 fn test_hover_intra_link_shortlink_namspaced_code_with_at() {
1574 check_hover_result(
1575 r"
1576 //- /lib.rs
1577 pub struct Foo;
1578 /// [`struct@Foo`]
1579 pub struct B<|>ar
1580 ",
1581 &["pub struct Bar\n```\n___\n\n[`Foo`](https://docs.rs/test/*/test/struct.Foo.html)"],
1582 );
1583 }
1584
1585 #[test]
1586 fn test_hover_intra_link_reference() {
1587 check_hover_result(
1588 r"
1589 //- /lib.rs
1590 pub struct Foo;
1591 /// [my Foo][foo]
1592 ///
1593 /// [foo]: Foo
1594 pub struct B<|>ar
1595 ",
1596 &["pub struct Bar\n```\n___\n\n[my Foo](https://docs.rs/test/*/test/struct.Foo.html)"],
1597 );
1598 }
1599
1600 #[test]
1601 fn test_hover_external_url() {
1602 check_hover_result(
1603 r"
1604 //- /lib.rs
1605 pub struct Foo;
1606 /// [external](https://www.google.com)
1607 pub struct B<|>ar
1608 ",
1609 &["pub struct Bar\n```\n___\n\n[external](https://www.google.com)"],
1610 );
1611 }
1612
1613 // Check that we don't rewrite links which we can't identify
1614 #[test]
1615 fn test_hover_unknown_target() {
1616 check_hover_result(
1617 r"
1618 //- /lib.rs
1619 pub struct Foo;
1620 /// [baz](Baz)
1621 pub struct B<|>ar
1622 ",
1623 &["pub struct Bar\n```\n___\n\n[baz](Baz)"],
1624 );
1625 }
1626
1627 #[test]
1197 fn test_hover_macro_generated_struct_fn_doc_comment() { 1628 fn test_hover_macro_generated_struct_fn_doc_comment() {
1198 mark::check!(hover_macro_generated_struct_fn_doc_comment); 1629 mark::check!(hover_macro_generated_struct_fn_doc_comment);
1199 1630
diff --git a/crates/ra_ide/src/mock_analysis.rs b/crates/ra_ide/src/mock_analysis.rs
index c7e0f4b58..cf2ee1bfa 100644
--- a/crates/ra_ide/src/mock_analysis.rs
+++ b/crates/ra_ide/src/mock_analysis.rs
@@ -115,7 +115,7 @@ impl MockAnalysis {
115 root_crate = Some(crate_graph.add_crate_root( 115 root_crate = Some(crate_graph.add_crate_root(
116 file_id, 116 file_id,
117 edition, 117 edition,
118 None, 118 Some("test".to_string()),
119 cfg, 119 cfg,
120 env, 120 env,
121 Default::default(), 121 Default::default(),