diff options
-rw-r--r-- | crates/ra_ide/src/hover.rs | 88 |
1 files changed, 53 insertions, 35 deletions
diff --git a/crates/ra_ide/src/hover.rs b/crates/ra_ide/src/hover.rs index 8c972bb41..f7a0af037 100644 --- a/crates/ra_ide/src/hover.rs +++ b/crates/ra_ide/src/hover.rs | |||
@@ -17,7 +17,7 @@ use ra_ide_db::{ | |||
17 | RootDatabase, | 17 | RootDatabase, |
18 | }; | 18 | }; |
19 | use ra_syntax::{ | 19 | use ra_syntax::{ |
20 | ast, ast::Path, match_ast, AstNode, SyntaxKind::*, SyntaxNode, SyntaxToken, TokenAtOffset, | 20 | ast, ast::{Path, MacroCall}, match_ast, AstNode, SyntaxKind::*, SyntaxNode, SyntaxToken, TokenAtOffset, |
21 | }; | 21 | }; |
22 | use ra_tt::{Ident, Leaf, Literal, TokenTree}; | 22 | use ra_tt::{Ident, Leaf, Literal, TokenTree}; |
23 | use url::Url; | 23 | use url::Url; |
@@ -394,35 +394,36 @@ fn hover_text_from_name_kind(db: &RootDatabase, def: Definition) -> Option<Strin | |||
394 | // Rewrites a markdown document, resolving links using `callback` and additionally striping prefixes/suffixes on link titles. | 394 | // Rewrites a markdown document, resolving links using `callback` and additionally striping prefixes/suffixes on link titles. |
395 | fn map_links<'e>( | 395 | fn map_links<'e>( |
396 | events: impl Iterator<Item = Event<'e>>, | 396 | events: impl Iterator<Item = Event<'e>>, |
397 | callback: impl Fn(&str, &str) -> String, | 397 | callback: impl Fn(&str, &str) -> (String, String), |
398 | ) -> impl Iterator<Item = Event<'e>> { | 398 | ) -> impl Iterator<Item = Event<'e>> { |
399 | let mut in_link = false; | 399 | let mut in_link = false; |
400 | let mut link_text = CowStr::Borrowed(""); | 400 | let mut link_target: Option<CowStr> = None; |
401 | |||
401 | events.map(move |evt| match evt { | 402 | events.map(move |evt| match evt { |
402 | Event::Start(Tag::Link(..)) => { | 403 | Event::Start(Tag::Link(_link_type, ref target, _)) => { |
403 | in_link = true; | 404 | in_link = true; |
405 | link_target = Some(target.clone()); | ||
404 | evt | 406 | evt |
405 | } | 407 | } |
406 | Event::End(Tag::Link(link_type, target, _)) => { | 408 | Event::End(Tag::Link(link_type, _target, _)) => { |
407 | in_link = false; | 409 | in_link = false; |
408 | let target = callback(&target, &link_text); | 410 | Event::End(Tag::Link(link_type, link_target.take().unwrap(), CowStr::Borrowed(""))) |
409 | Event::End(Tag::Link(link_type, CowStr::Boxed(target.into()), CowStr::Borrowed(""))) | ||
410 | } | 411 | } |
411 | Event::Text(s) if in_link => { | 412 | Event::Text(s) if in_link => { |
412 | link_text = s.clone(); | 413 | let (link_target_s, link_name) = callback(&link_target.take().unwrap(), &s); |
413 | // TODO: This can unintentionally strip words from path-based links. | 414 | link_target = Some(CowStr::Boxed(link_target_s.into())); |
414 | // See std::box::Box -> std::box link as an example. | 415 | Event::Text(CowStr::Boxed(link_name.into())) |
415 | Event::Text(CowStr::Boxed(strip_prefixes_suffixes(&s).into())) | ||
416 | } | 416 | } |
417 | Event::Code(s) if in_link => { | 417 | Event::Code(s) if in_link => { |
418 | link_text = s.clone(); | 418 | let (link_target_s, link_name) = callback(&link_target.take().unwrap(), &s); |
419 | Event::Code(CowStr::Boxed(strip_prefixes_suffixes(&s).into())) | 419 | link_target = Some(CowStr::Boxed(link_target_s.into())); |
420 | Event::Code(CowStr::Boxed(link_name.into())) | ||
420 | } | 421 | } |
421 | _ => evt, | 422 | _ => evt, |
422 | }) | 423 | }) |
423 | } | 424 | } |
424 | 425 | ||
425 | /// Rewrite documentation links in markdown to point to local documentation/docs.rs | 426 | /// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs) |
426 | fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> Option<String> { | 427 | fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> Option<String> { |
427 | let doc = Parser::new_with_broken_link_callback( | 428 | let doc = Parser::new_with_broken_link_callback( |
428 | markdown, | 429 | markdown, |
@@ -431,21 +432,22 @@ fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> | |||
431 | ); | 432 | ); |
432 | 433 | ||
433 | let doc = map_links(doc, |target, title: &str| { | 434 | let doc = map_links(doc, |target, title: &str| { |
434 | match Url::parse(target) { | 435 | // This check is imperfect, there's some overlap between valid intra-doc links |
435 | // If this is a valid absolute URL don't touch it | 436 | // and valid URLs so we choose to be too eager to try to resolve what might be |
436 | Ok(_) => target.to_string(), | 437 | // a URL. |
437 | // Otherwise there are two main possibilities | 438 | if target.contains("://") { |
438 | // path-based links: `../../module/struct.MyStruct.html` | 439 | (target.to_string(), title.to_string()) |
439 | // module-based links (AKA intra-doc links): `super::super::module::MyStruct` | 440 | } else { |
440 | Err(_) => { | 441 | // Two posibilities: |
441 | let resolved = try_resolve_intra(db, definition, title, &target) | 442 | // * path-based links: `../../module/struct.MyStruct.html` |
442 | .or_else(|| try_resolve_path(db, definition, &target)); | 443 | // * module-based links (AKA intra-doc links): `super::super::module::MyStruct` |
443 | 444 | let resolved = try_resolve_intra(db, definition, title, &target) | |
444 | if let Some(resolved) = resolved { | 445 | .or_else(|| try_resolve_path(db, definition, &target).map(|target| (target, title.to_string()))); |
445 | resolved | 446 | |
446 | } else { | 447 | if let Some((target, title)) = resolved { |
447 | target.to_string() | 448 | (target, title) |
448 | } | 449 | } else { |
450 | (target.to_string(), title.to_string()) | ||
449 | } | 451 | } |
450 | } | 452 | } |
451 | }); | 453 | }); |
@@ -520,7 +522,7 @@ fn try_resolve_intra( | |||
520 | definition: &Definition, | 522 | definition: &Definition, |
521 | link_text: &str, | 523 | link_text: &str, |
522 | link_target: &str, | 524 | link_target: &str, |
523 | ) -> Option<String> { | 525 | ) -> Option<(String, String)> { |
524 | // Set link_target for implied shortlinks | 526 | // Set link_target for implied shortlinks |
525 | let link_target = | 527 | let link_target = |
526 | if link_target.is_empty() { link_text.trim_matches('`') } else { link_target }; | 528 | if link_target.is_empty() { link_text.trim_matches('`') } else { link_target }; |
@@ -534,6 +536,7 @@ fn try_resolve_intra( | |||
534 | // Parse link as a module path | 536 | // Parse link as a module path |
535 | // This expects a full document, which a single path isn't, but we can just ignore the errors. | 537 | // This expects a full document, which a single path isn't, but we can just ignore the errors. |
536 | let parsed = SyntaxNode::new_root(ra_syntax::parse_text(link_target).0); | 538 | let parsed = SyntaxNode::new_root(ra_syntax::parse_text(link_target).0); |
539 | // TODO: Proper parsing | ||
537 | let path = parsed.descendants().filter_map(Path::cast).next()?; | 540 | let path = parsed.descendants().filter_map(Path::cast).next()?; |
538 | let modpath = ModPath::from_src(path, &Hygiene::new_unhygienic()).unwrap(); | 541 | let modpath = ModPath::from_src(path, &Hygiene::new_unhygienic()).unwrap(); |
539 | 542 | ||
@@ -566,7 +569,7 @@ fn try_resolve_intra( | |||
566 | let import_map = db.import_map(krate.into()); | 569 | let import_map = db.import_map(krate.into()); |
567 | let path = import_map.path_of(ns)?; | 570 | let path = import_map.path_of(ns)?; |
568 | 571 | ||
569 | Some( | 572 | Some(( |
570 | get_doc_url(db, &krate)? | 573 | get_doc_url(db, &krate)? |
571 | .join(&format!("{}/", krate.display_name(db)?)) | 574 | .join(&format!("{}/", krate.display_name(db)?)) |
572 | .ok()? | 575 | .ok()? |
@@ -575,7 +578,7 @@ fn try_resolve_intra( | |||
575 | .join(&get_symbol_filename(db, &Definition::ModuleDef(def))?) | 578 | .join(&get_symbol_filename(db, &Definition::ModuleDef(def))?) |
576 | .ok()? | 579 | .ok()? |
577 | .into_string(), | 580 | .into_string(), |
578 | ) | 581 | strip_prefixes_suffixes(link_text).to_string())) |
579 | } | 582 | } |
580 | 583 | ||
581 | /// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`). | 584 | /// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`). |
@@ -1485,15 +1488,30 @@ fn func(foo: i32) { if true { <|>foo; }; } | |||
1485 | } | 1488 | } |
1486 | 1489 | ||
1487 | #[test] | 1490 | #[test] |
1488 | fn test_hover_intra_link() { | 1491 | fn test_hover_path_link_no_strip() { |
1489 | check_hover_result( | 1492 | check_hover_result( |
1490 | r" | 1493 | r" |
1491 | //- /lib.rs | 1494 | //- /lib.rs |
1492 | pub struct Foo; | 1495 | pub struct Foo; |
1493 | /// [Foo](Foo) | 1496 | /// [struct Foo](struct.Foo.html) |
1494 | pub struct B<|>ar | 1497 | pub struct B<|>ar |
1495 | ", | 1498 | ", |
1496 | &["pub struct Bar\n```\n___\n\n[Foo](https://docs.rs/test/*/test/struct.Foo.html)"], | 1499 | &["pub struct Bar\n```\n___\n\n[struct Foo](https://docs.rs/test/*/test/struct.Foo.html)"], |
1500 | ); | ||
1501 | } | ||
1502 | |||
1503 | #[test] | ||
1504 | fn test_hover_intra_link() { | ||
1505 | check_hover_result( | ||
1506 | r" | ||
1507 | //- /lib.rs | ||
1508 | pub mod foo { | ||
1509 | pub struct Foo; | ||
1510 | } | ||
1511 | /// [Foo](foo::Foo) | ||
1512 | pub struct B<|>ar | ||
1513 | ", | ||
1514 | &["pub struct Bar\n```\n___\n\n[Foo](https://docs.rs/test/*/test/foo/struct.Foo.html)"], | ||
1497 | ); | 1515 | ); |
1498 | } | 1516 | } |
1499 | 1517 | ||