aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorZac Pullar-Strecker <[email protected]>2020-06-09 08:53:09 +0100
committerZac Pullar-Strecker <[email protected]>2020-06-30 09:02:46 +0100
commit68ee0337622f4025202687ccfac79c04d2046de8 (patch)
tree65f52f88ad89d3e969e8f766ef9cf8f75c173d5c /crates
parent2023af53f09ed9466c6d7442d6830276eba19b45 (diff)
URL doc outputs in hover
Diffstat (limited to 'crates')
-rw-r--r--crates/ra_ide/Cargo.toml1
-rw-r--r--crates/ra_ide/src/hover.rs83
2 files changed, 68 insertions, 16 deletions
diff --git a/crates/ra_ide/Cargo.toml b/crates/ra_ide/Cargo.toml
index e5fdf5aa1..fa925fc5a 100644
--- a/crates/ra_ide/Cargo.toml
+++ b/crates/ra_ide/Cargo.toml
@@ -34,6 +34,7 @@ ra_assists = { path = "../ra_assists" }
34ra_ssr = { path = "../ra_ssr" } 34ra_ssr = { path = "../ra_ssr" }
35ra_project_model = { path = "../ra_project_model" } 35ra_project_model = { path = "../ra_project_model" }
36ra_hir_def = { path = "../ra_hir_def" } 36ra_hir_def = { path = "../ra_hir_def" }
37ra_tt = { path = "../ra_tt" }
37 38
38# ra_ide should depend only on the top-level `hir` package. if you need 39# ra_ide should depend only on the top-level `hir` package. if you need
39# something from some `hir_xxx` subpackage, reexport the API via `hir`. 40# something from some `hir_xxx` subpackage, reexport the API via `hir`.
diff --git a/crates/ra_ide/src/hover.rs b/crates/ra_ide/src/hover.rs
index 079195184..91167e30a 100644
--- a/crates/ra_ide/src/hover.rs
+++ b/crates/ra_ide/src/hover.rs
@@ -2,7 +2,7 @@ use std::iter::once;
2 2
3use hir::{ 3use hir::{
4 Adt, AsAssocItem, AssocItemContainer, FieldSource, HasSource, HirDisplay, ModuleDef, 4 Adt, AsAssocItem, AssocItemContainer, FieldSource, HasSource, HirDisplay, ModuleDef,
5 ModuleSource, Semantics, Module, Documentation 5 ModuleSource, Semantics, Module, Documentation, AttrDef, Crate
6}; 6};
7use itertools::Itertools; 7use itertools::Itertools;
8use ra_db::SourceDatabase; 8use ra_db::SourceDatabase;
@@ -13,6 +13,7 @@ use ra_ide_db::{
13use ra_syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset}; 13use ra_syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset};
14use ra_project_model::ProjectWorkspace; 14use ra_project_model::ProjectWorkspace;
15use ra_hir_def::{item_scope::ItemInNs, db::DefDatabase, ModuleDefId}; 15use ra_hir_def::{item_scope::ItemInNs, db::DefDatabase, ModuleDefId};
16use ra_tt::{Literal, Ident, Punct, TokenTree, Leaf, Subtree, SmolStr};
16 17
17use crate::{ 18use crate::{
18 display::{ 19 display::{
@@ -415,7 +416,7 @@ fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition, wor
415 // If contains :: module-based link 416 // If contains :: module-based link
416 Err(_) => { 417 Err(_) => {
417 let link_str = String::from_utf8(link.url.clone()).unwrap(); 418 let link_str = String::from_utf8(link.url.clone()).unwrap();
418 let resolved = try_resolve_path(db, &mut doc_target_dirs.clone(), definition, &link_str) 419 let resolved = try_resolve_path(db, &mut doc_target_dirs.clone(), definition, &link_str, UrlMode::Url)
419 .or_else(|| try_resolve_intra(db, &mut doc_target_dirs.clone(), definition, &link_str)); 420 .or_else(|| try_resolve_intra(db, &mut doc_target_dirs.clone(), definition, &link_str));
420 421
421 if let Some(resolved) = resolved { 422 if let Some(resolved) = resolved {
@@ -442,27 +443,77 @@ fn try_resolve_intra(db: &RootDatabase, doc_target_dirs: impl Iterator<Item = Pa
442 None 443 None
443} 444}
444 445
446enum UrlMode {
447 Url,
448 File
449}
450
445/// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`) 451/// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`)
446fn try_resolve_path(db: &RootDatabase, doc_target_dirs: impl Iterator<Item = PathBuf>, definition: &Definition, link: &str) -> Option<String> { 452fn try_resolve_path(db: &RootDatabase, doc_target_dirs: impl Iterator<Item = PathBuf>, definition: &Definition, link: &str, mode: UrlMode) -> Option<String> {
447 let ns = if let Definition::ModuleDef(moddef) = definition { 453 let ns = if let Definition::ModuleDef(moddef) = definition {
448 ItemInNs::Types(moddef.clone().into()) 454 ItemInNs::Types(moddef.clone().into())
449 } else { 455 } else {
450 return None; 456 return None;
451 }; 457 };
452 let krate = definition.module(db)?.krate(); 458 let module = definition.module(db)?;
459 let krate = module.krate();
453 let import_map = db.import_map(krate.into()); 460 let import_map = db.import_map(krate.into());
454 let base = import_map.path_of(ns).unwrap(); 461 // TODO: It should be possible to fall back to not-necessarilly-public paths if we can't find a public one,
455 let base = base.segments.iter().map(|name| format!("{}", name)).collect::<PathBuf>(); 462 // then hope rustdoc was run locally with `--document-private-items`
456 463 let base = import_map.path_of(ns)?;
457 doc_target_dirs 464 let mut base = once(format!("{}", krate.display_name(db)?)).chain(base.segments.iter().map(|name| format!("{}", name)));
458 .map(|dir| dir.join(format!("{}", krate.display_name(db).unwrap())).join(base.join("..").join(link))) 465
459 .inspect(|path| eprintln!("candidate {}", path.display())) 466 match mode {
460 .filter(|path| path.exists()) 467 UrlMode::Url => {
461 // slice out the UNC '\?\' added by canonicalize 468 let root = get_doc_url(db, &krate);
462 .map(|path| format!("file:///{}", path.display())) 469 let mut base = base.join("/");
463 // \. is treated as an escape in vscode's markdown hover rendering 470 if let Some(url) = root {
464 .map(|path_str| path_str.replace("\\", "/")) 471 eprintln!("root: {:?} base: {:?} link: {} root&base: {} root&base&link: {}", url, &base, link, url.join(&base).unwrap(), url.join(&base).unwrap().join(link).unwrap());
465 .next() 472 if link.starts_with("#") {
473 base = base + "/"
474 };
475 Some(url.join(&base)?.join(link)?.into_string())
476 } else {None}
477 },
478 UrlMode::File => {
479 let base = base.collect::<PathBuf>();
480 doc_target_dirs
481 .map(|dir| dir.join(format!("{}", krate.display_name(db).unwrap())).join(base.join("..").join(link)))
482 .inspect(|path| eprintln!("candidate {}", path.display()))
483 .filter(|path| path.exists())
484 .map(|path| format!("file:///{}", path.display()))
485 // \. is treated as an escape in vscode's markdown hover rendering
486 .map(|path_str| path_str.replace("\\", "/"))
487 .next()
488 }
489 }
490}
491
492/// Try to get the root URL of the documentation of a crate.
493fn get_doc_url(db: &RootDatabase, krate: &Crate) -> Option<Url> {
494 // Look for #![doc(html_root_url = "https://docs.rs/...")]
495 let attrs = db.attrs(AttrDef::from(krate.root_module(db)?).into());
496 let doc_attr_q = attrs.by_key("doc");
497 let doc_url = if doc_attr_q.exists() {
498 doc_attr_q.tt_values().filter_map(|tt| match tt.token_trees.as_slice() {
499 &[
500 TokenTree::Leaf(Leaf::Ident(Ident{text: ref ident_text, ..})),
501 TokenTree::Leaf(Leaf::Punct(Punct{r#char: '=', ..})),
502 TokenTree::Leaf(Leaf::Literal(Literal{ref text, ..}))
503 ] if ident_text == "html_root_url" => Some(text),
504 _ => {
505 None
506 }
507 }).next()
508 } else {
509 None
510 };
511 eprintln!("url {:?}", doc_url);
512
513 // TODO: It should be possible to fallback to `format!("https://docs.rs/{}/*", crate_name, *)`
514 let url = doc_url.map(|s| s.trim_matches('"').to_owned() + "/").and_then(|s| Url::parse(&s).ok());
515 eprintln!("url {:?}", url);
516 url
466} 517}
467 518
468fn iter_nodes<'a, F>(node: &'a comrak::nodes::AstNode<'a>, f: &F) 519fn iter_nodes<'a, F>(node: &'a comrak::nodes::AstNode<'a>, f: &F)