From 2023af53f09ed9466c6d7442d6830276eba19b45 Mon Sep 17 00:00:00 2001 From: Zac Pullar-Strecker Date: Tue, 9 Jun 2020 15:43:57 +1200 Subject: Hover doc link rewriting --- Cargo.lock | 236 +++++++++++++++++++++++++++++++++-- crates/ra_ide/Cargo.toml | 4 + crates/ra_ide/src/hover.rs | 102 ++++++++++++++- crates/ra_ide/src/lib.rs | 5 +- crates/ra_syntax/src/ast/test.txt | 15 +++ crates/ra_syntax/src/ast/traits.rs | 36 ++++++ crates/rust-analyzer/src/handlers.rs | 1 + 7 files changed, 382 insertions(+), 17 deletions(-) create mode 100644 crates/ra_syntax/src/ast/test.txt diff --git a/Cargo.lock b/Cargo.lock index ca3d14a09..bc36f0fab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,6 +94,27 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + [[package]] name = "bstr" version = "0.2.13" @@ -103,6 +124,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + [[package]] name = "byteorder" version = "1.3.4" @@ -196,15 +223,18 @@ dependencies = [ ] [[package]] -name = "clicolors-control" -version = "1.0.1" +name = "clap" +version = "2.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90082ee5dcdd64dc4e9e0d37fbf3ee325419e39c0092191e0393df65518f741e" +checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129" dependencies = [ + "ansi_term", "atty", - "lazy_static", - "libc", - "winapi 0.3.9", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", ] [[package]] @@ -216,13 +246,29 @@ dependencies = [ "bitflags", ] +[[package]] +name = "comrak" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17058cc536cf290563e88787d7b9e6030ce4742943017cc2ffb71f88034021c" +dependencies = [ + "clap", + "entities", + "lazy_static", + "pest", + "pest_derive", + "regex", + "twoway", + "typed-arena", + "unicode_categories", +] + [[package]] name = "console" -version = "0.10.3" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586208b33573b7f76ccfbe5adb076394c88deaf81b84d7213969805b0a952a7" +checksum = "8c0994e656bba7b922d8dd1245db90672ffb701e684e45be58f20719d69abc5a" dependencies = [ - "clicolors-control", "encode_unicode", "lazy_static", "libc", @@ -309,6 +355,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array", +] + [[package]] name = "drop_bomb" version = "0.1.4" @@ -342,6 +397,12 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "entities" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" + [[package]] name = "env_logger" version = "0.7.1" @@ -351,6 +412,12 @@ dependencies = [ "log", ] +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + [[package]] name = "filetime" version = "0.2.10" @@ -434,6 +501,15 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +[[package]] +name = "generic-array" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" +dependencies = [ + "typenum", +] + [[package]] name = "getrandom" version = "0.1.14" @@ -544,9 +620,9 @@ dependencies = [ [[package]] name = "insta" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8386e795fb3927131ea4cede203c529a333652eb6dc4ff29616b832b27e9b096" +checksum = "617e921abc813f96a3b00958c079e7bf1e2db998f8a04f1546dd967373a418ee" dependencies = [ "console", "difference", @@ -711,6 +787,12 @@ dependencies = [ "url", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "matchers" version = "0.0.1" @@ -881,6 +963,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + [[package]] name = "parking_lot" version = "0.11.0" @@ -936,6 +1024,49 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +dependencies = [ + "maplit", + "pest", + "sha-1", +] + [[package]] name = "petgraph" version = "0.5.1" @@ -1133,6 +1264,7 @@ dependencies = [ name = "ra_ide" version = "0.1.0" dependencies = [ + "comrak", "either", "indexmap", "insta", @@ -1143,8 +1275,10 @@ dependencies = [ "ra_db", "ra_fmt", "ra_hir", + "ra_hir_def", "ra_ide_db", "ra_prof", + "ra_project_model", "ra_ssr", "ra_syntax", "ra_text_edit", @@ -1152,6 +1286,7 @@ dependencies = [ "rustc-hash", "stdx", "test_utils", + "url", ] [[package]] @@ -1653,6 +1788,18 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +dependencies = [ + "block-buffer", + "digest", + "fake-simd", + "opaque-debug", +] + [[package]] name = "sharded-slab" version = "0.0.9" @@ -1687,6 +1834,12 @@ dependencies = [ name = "stdx" version = "0.1.0" +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "superslice" version = "1.0.0" @@ -1766,6 +1919,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f03e7efdedc3bc78cb2337f1e2785c39e45f5ef762d9e4ebb137fff7380a6d8a" +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "thin-dst" version = "1.1.0" @@ -1878,6 +2040,40 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "twoway" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc" +dependencies = [ + "memchr", + "unchecked-index", +] + +[[package]] +name = "typed-arena" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d" + +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[package]] +name = "unchecked-index" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c" + [[package]] name = "unicode-bidi" version = "0.3.4" @@ -1902,12 +2098,24 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + [[package]] name = "unicode-xid" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "url" version = "2.1.1" @@ -1920,6 +2128,12 @@ dependencies = [ "serde", ] +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "vfs" version = "0.1.0" diff --git a/crates/ra_ide/Cargo.toml b/crates/ra_ide/Cargo.toml index bbc6a5c9b..e5fdf5aa1 100644 --- a/crates/ra_ide/Cargo.toml +++ b/crates/ra_ide/Cargo.toml @@ -17,6 +17,8 @@ itertools = "0.9.0" log = "0.4.8" rustc-hash = "1.1.0" rand = { version = "0.7.3", features = ["small_rng"] } +comrak = "0.7.0" +url = "*" stdx = { path = "../stdx" } @@ -30,6 +32,8 @@ ra_prof = { path = "../ra_prof" } test_utils = { path = "../test_utils" } ra_assists = { path = "../ra_assists" } ra_ssr = { path = "../ra_ssr" } +ra_project_model = { path = "../ra_project_model" } +ra_hir_def = { path = "../ra_hir_def" } # ra_ide should depend only on the top-level `hir` package. if you need # 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 c3e36a387..079195184 100644 --- a/crates/ra_ide/src/hover.rs +++ b/crates/ra_ide/src/hover.rs @@ -1,8 +1,8 @@ use std::iter::once; use hir::{ - Adt, AsAssocItem, AssocItemContainer, Documentation, FieldSource, HasSource, HirDisplay, - Module, ModuleDef, ModuleSource, Semantics, + Adt, AsAssocItem, AssocItemContainer, FieldSource, HasSource, HirDisplay, ModuleDef, + ModuleSource, Semantics, Module, Documentation }; use itertools::Itertools; use ra_db::SourceDatabase; @@ -11,6 +11,8 @@ use ra_ide_db::{ RootDatabase, }; use ra_syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset}; +use ra_project_model::ProjectWorkspace; +use ra_hir_def::{item_scope::ItemInNs, db::DefDatabase, ModuleDefId}; use crate::{ display::{ @@ -65,6 +67,13 @@ pub struct HoverGotoTypeData { pub nav: NavigationTarget, } +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use comrak::{parse_document,format_commonmark, ComrakOptions, Arena}; +use comrak::nodes::NodeValue; +use url::Url; +use ra_ide_db::imports_locator::ImportsLocator; + /// Contains the results when hovering over an item #[derive(Debug, Default)] pub struct HoverResult { @@ -118,7 +127,7 @@ impl HoverResult { // // Shows additional information, like type of an expression or documentation for definition when "focusing" code. // Focusing is usually hovering with a mouse, but can also be triggered with a shortcut. -pub(crate) fn hover(db: &RootDatabase, position: FilePosition) -> Option> { +pub(crate) fn hover(db: &RootDatabase, position: FilePosition, workspaces: Arc>) -> Option> { let sema = Semantics::new(db); let file = sema.parse(position.file_id).syntax().clone(); let token = pick_best(file.token_at_offset(position.offset))?; @@ -138,7 +147,8 @@ pub(crate) fn hover(db: &RootDatabase, position: FilePosition) -> Option Option>) -> Option { + // FIXME: Fail early + if let (Some(name), Some(module)) = (definition.name(db), definition.module(db)) { + let krate_name = module.krate().display_name(db)?; + let arena = Arena::new(); + let doc = parse_document(&arena, markdown, &ComrakOptions::default()); + let path = module.path_to_root(db); + let mut doc_target_dirs = workspaces + .iter() + .filter_map(|workspace| if let ProjectWorkspace::Cargo{cargo: cargo_workspace, ..} = workspace {Some(cargo_workspace)} else {None}) + .map(|workspace| workspace.workspace_root()) + // TODO: `target` is configurable in cargo config, we should respect it + .map(|root| root.join("target/doc")); + + iter_nodes(doc, &|node| { + match &mut node.data.borrow_mut().value { + &mut NodeValue::Link(ref mut link) => { + match Url::parse(&String::from_utf8(link.url.clone()).unwrap()) { + // If this is a valid absolute URL don't touch it + Ok(_) => (), + // If contains .html file-based link to new page + // If starts with #fragment file-based link to fragment on current page + // If contains :: module-based link + Err(_) => { + let link_str = String::from_utf8(link.url.clone()).unwrap(); + let resolved = try_resolve_path(db, &mut doc_target_dirs.clone(), definition, &link_str) + .or_else(|| try_resolve_intra(db, &mut doc_target_dirs.clone(), definition, &link_str)); + + if let Some(resolved) = resolved { + link.url = resolved.as_bytes().to_vec(); + } + + } + } + }, + _ => () + } + }); + let mut out = Vec::new(); + format_commonmark(doc, &ComrakOptions::default(), &mut out); + Some(String::from_utf8(out).unwrap()) + } else { + // eprintln!("WARN: Unable to determine name or module for hover; link rewriting disabled."); + None + } +} + +/// Try to resolve path to local documentation via intra-doc-links (i.e. `super::gateway::Shard`) +fn try_resolve_intra(db: &RootDatabase, doc_target_dirs: impl Iterator, definition: &Definition, link: &str) -> Option { + None +} + +/// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`) +fn try_resolve_path(db: &RootDatabase, doc_target_dirs: impl Iterator, definition: &Definition, link: &str) -> Option { + let ns = if let Definition::ModuleDef(moddef) = definition { + ItemInNs::Types(moddef.clone().into()) + } else { + return None; + }; + let krate = definition.module(db)?.krate(); + let import_map = db.import_map(krate.into()); + let base = import_map.path_of(ns).unwrap(); + let base = base.segments.iter().map(|name| format!("{}", name)).collect::(); + + doc_target_dirs + .map(|dir| dir.join(format!("{}", krate.display_name(db).unwrap())).join(base.join("..").join(link))) + .inspect(|path| eprintln!("candidate {}", path.display())) + .filter(|path| path.exists()) + // slice out the UNC '\?\' added by canonicalize + .map(|path| format!("file:///{}", path.display())) + // \. is treated as an escape in vscode's markdown hover rendering + .map(|path_str| path_str.replace("\\", "/")) + .next() +} + +fn iter_nodes<'a, F>(node: &'a comrak::nodes::AstNode<'a>, f: &F) + where F : Fn(&'a comrak::nodes::AstNode<'a>) { + f(node); + for c in node.children() { + iter_nodes(c, f); + } +} + fn pick_best(tokens: TokenAtOffset) -> Option { return tokens.max_by_key(priority); fn priority(n: &SyntaxToken) -> usize { diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs index ecac5134e..d56d52d30 100644 --- a/crates/ra_ide/src/lib.rs +++ b/crates/ra_ide/src/lib.rs @@ -54,6 +54,7 @@ use ra_ide_db::{ LineIndexDatabase, }; use ra_syntax::{SourceFile, TextRange, TextSize}; +use ra_project_model::ProjectWorkspace; use crate::display::ToNav; @@ -389,8 +390,8 @@ impl Analysis { } /// Returns a short text describing element at position. - pub fn hover(&self, position: FilePosition) -> Cancelable>> { - self.with_db(|db| hover::hover(db, position)) + pub fn hover(&self, position: FilePosition, workspaces: Arc>) -> Cancelable>> { + self.with_db(|db| hover::hover(db, position, workspaces)) } /// Computes parameter information for the given call expression. diff --git a/crates/ra_syntax/src/ast/test.txt b/crates/ra_syntax/src/ast/test.txt new file mode 100644 index 000000000..f746bf1e7 --- /dev/null +++ b/crates/ra_syntax/src/ast/test.txt @@ -0,0 +1,15 @@ +The context is a general utility struct provided on event dispatches, which +helps with dealing with the current "context" of the event dispatch. +The context also acts as a general high-level interface over the associated +[`Shard`] which received the event, or the low-level [`http`] module. + +The context contains "shortcuts", like for interacting with the shard. +Methods like [`set_activity`] will unlock the shard and perform an update for +you to save a bit of work. + +A context will only live for the event it was dispatched for. After the +event handler finished, it is destroyed and will not be re-used. + +[`Shard`]: ../gateway/struct.Shard.html +[`http`]: ../http/index.html +[`set_activity`]: #method.set_activity diff --git a/crates/ra_syntax/src/ast/traits.rs b/crates/ra_syntax/src/ast/traits.rs index a8f2454fd..323d78bbc 100644 --- a/crates/ra_syntax/src/ast/traits.rs +++ b/crates/ra_syntax/src/ast/traits.rs @@ -146,3 +146,39 @@ impl Iterator for CommentIter { self.iter.by_ref().find_map(|el| el.into_token().and_then(ast::Comment::cast)) } } + +#[cfg(test)] +mod tests { + use comrak::{parse_document,format_commonmark, ComrakOptions, Arena}; + use comrak::nodes::{AstNode, NodeValue}; + + fn iter_nodes<'a, F>(node: &'a AstNode<'a>, f: &F) + where F : Fn(&'a AstNode<'a>) { + f(node); + for c in node.children() { + iter_nodes(c, f); + } + } + + #[allow(non_snake_case)] + #[test] + fn test_link_rewrite() { + let src = include_str!("./test.txt"); + + let arena = Arena::new(); + let doc = parse_document(&arena, src, &ComrakOptions::default()); + + iter_nodes(doc, &|node| { + match &mut node.data.borrow_mut().value { + &mut NodeValue::Link(ref mut link) => { + link.url = "https://www.google.com".as_bytes().to_vec(); + }, + _ => () + } + }); + + let mut out = Vec::new(); + format_commonmark(doc, &ComrakOptions::default(), &mut out); + panic!("{}", String::from_utf8(out).unwrap()); + } +} diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs index e35a5e846..19da25f96 100644 --- a/crates/rust-analyzer/src/handlers.rs +++ b/crates/rust-analyzer/src/handlers.rs @@ -5,6 +5,7 @@ use std::{ io::Write as _, process::{self, Stdio}, + sync::Arc }; use lsp_server::ErrorCode; -- cgit v1.2.3