diff options
author | Zac Pullar-Strecker <[email protected]> | 2020-06-09 04:43:57 +0100 |
---|---|---|
committer | Zac Pullar-Strecker <[email protected]> | 2020-06-30 09:02:46 +0100 |
commit | 2023af53f09ed9466c6d7442d6830276eba19b45 (patch) | |
tree | 84924a0e47a70b0255b1811f127147e9a9d92682 /crates/ra_ide/src/hover.rs | |
parent | 2bd717139918e15e537dcd833bb003e85d24b3d1 (diff) |
Hover doc link rewriting
Diffstat (limited to 'crates/ra_ide/src/hover.rs')
-rw-r--r-- | crates/ra_ide/src/hover.rs | 102 |
1 files changed, 98 insertions, 4 deletions
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 @@ | |||
1 | use std::iter::once; | 1 | use std::iter::once; |
2 | 2 | ||
3 | use hir::{ | 3 | use hir::{ |
4 | Adt, AsAssocItem, AssocItemContainer, Documentation, FieldSource, HasSource, HirDisplay, | 4 | Adt, AsAssocItem, AssocItemContainer, FieldSource, HasSource, HirDisplay, ModuleDef, |
5 | Module, ModuleDef, ModuleSource, Semantics, | 5 | ModuleSource, Semantics, Module, Documentation |
6 | }; | 6 | }; |
7 | use itertools::Itertools; | 7 | use itertools::Itertools; |
8 | use ra_db::SourceDatabase; | 8 | use ra_db::SourceDatabase; |
@@ -11,6 +11,8 @@ use ra_ide_db::{ | |||
11 | RootDatabase, | 11 | RootDatabase, |
12 | }; | 12 | }; |
13 | use ra_syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset}; | 13 | use ra_syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset}; |
14 | use ra_project_model::ProjectWorkspace; | ||
15 | use ra_hir_def::{item_scope::ItemInNs, db::DefDatabase, ModuleDefId}; | ||
14 | 16 | ||
15 | use crate::{ | 17 | use crate::{ |
16 | display::{ | 18 | display::{ |
@@ -65,6 +67,13 @@ pub struct HoverGotoTypeData { | |||
65 | pub nav: NavigationTarget, | 67 | pub nav: NavigationTarget, |
66 | } | 68 | } |
67 | 69 | ||
70 | use std::path::{Path, PathBuf}; | ||
71 | use std::sync::Arc; | ||
72 | use comrak::{parse_document,format_commonmark, ComrakOptions, Arena}; | ||
73 | use comrak::nodes::NodeValue; | ||
74 | use url::Url; | ||
75 | use ra_ide_db::imports_locator::ImportsLocator; | ||
76 | |||
68 | /// Contains the results when hovering over an item | 77 | /// Contains the results when hovering over an item |
69 | #[derive(Debug, Default)] | 78 | #[derive(Debug, Default)] |
70 | pub struct HoverResult { | 79 | pub struct HoverResult { |
@@ -118,7 +127,7 @@ impl HoverResult { | |||
118 | // | 127 | // |
119 | // Shows additional information, like type of an expression or documentation for definition when "focusing" code. | 128 | // Shows additional information, like type of an expression or documentation for definition when "focusing" code. |
120 | // Focusing is usually hovering with a mouse, but can also be triggered with a shortcut. | 129 | // Focusing is usually hovering with a mouse, but can also be triggered with a shortcut. |
121 | pub(crate) fn hover(db: &RootDatabase, position: FilePosition) -> Option<RangeInfo<HoverResult>> { | 130 | pub(crate) fn hover(db: &RootDatabase, position: FilePosition, workspaces: Arc<Vec<ProjectWorkspace>>) -> Option<RangeInfo<HoverResult>> { |
122 | let sema = Semantics::new(db); | 131 | let sema = Semantics::new(db); |
123 | let file = sema.parse(position.file_id).syntax().clone(); | 132 | let file = sema.parse(position.file_id).syntax().clone(); |
124 | let token = pick_best(file.token_at_offset(position.offset))?; | 133 | let token = pick_best(file.token_at_offset(position.offset))?; |
@@ -138,7 +147,8 @@ pub(crate) fn hover(db: &RootDatabase, position: FilePosition) -> Option<RangeIn | |||
138 | } | 147 | } |
139 | } { | 148 | } { |
140 | let range = sema.original_range(&node).range; | 149 | let range = sema.original_range(&node).range; |
141 | res.extend(hover_text_from_name_kind(db, name_kind)); | 150 | let text = hover_text_from_name_kind(db, name_kind.clone()).map(|text| rewrite_links(db, &text, &name_kind, workspaces).unwrap_or(text)); |
151 | res.extend(text); | ||
142 | 152 | ||
143 | if !res.is_empty() { | 153 | if !res.is_empty() { |
144 | if let Some(action) = show_implementations_action(db, name_kind) { | 154 | if let Some(action) = show_implementations_action(db, name_kind) { |
@@ -379,6 +389,90 @@ fn hover_text_from_name_kind(db: &RootDatabase, def: Definition) -> Option<Strin | |||
379 | } | 389 | } |
380 | } | 390 | } |
381 | 391 | ||
392 | /// Rewrite documentation links in markdown to point to local documentation/docs.rs | ||
393 | fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition, workspaces: Arc<Vec<ProjectWorkspace>>) -> Option<String> { | ||
394 | // FIXME: Fail early | ||
395 | if let (Some(name), Some(module)) = (definition.name(db), definition.module(db)) { | ||
396 | let krate_name = module.krate().display_name(db)?; | ||
397 | let arena = Arena::new(); | ||
398 | let doc = parse_document(&arena, markdown, &ComrakOptions::default()); | ||
399 | let path = module.path_to_root(db); | ||
400 | let mut doc_target_dirs = workspaces | ||
401 | .iter() | ||
402 | .filter_map(|workspace| if let ProjectWorkspace::Cargo{cargo: cargo_workspace, ..} = workspace {Some(cargo_workspace)} else {None}) | ||
403 | .map(|workspace| workspace.workspace_root()) | ||
404 | // TODO: `target` is configurable in cargo config, we should respect it | ||
405 | .map(|root| root.join("target/doc")); | ||
406 | |||
407 | iter_nodes(doc, &|node| { | ||
408 | match &mut node.data.borrow_mut().value { | ||
409 | &mut NodeValue::Link(ref mut link) => { | ||
410 | match Url::parse(&String::from_utf8(link.url.clone()).unwrap()) { | ||
411 | // If this is a valid absolute URL don't touch it | ||
412 | Ok(_) => (), | ||
413 | // If contains .html file-based link to new page | ||
414 | // If starts with #fragment file-based link to fragment on current page | ||
415 | // If contains :: module-based link | ||
416 | Err(_) => { | ||
417 | 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 | .or_else(|| try_resolve_intra(db, &mut doc_target_dirs.clone(), definition, &link_str)); | ||
420 | |||
421 | if let Some(resolved) = resolved { | ||
422 | link.url = resolved.as_bytes().to_vec(); | ||
423 | } | ||
424 | |||
425 | } | ||
426 | } | ||
427 | }, | ||
428 | _ => () | ||
429 | } | ||
430 | }); | ||
431 | let mut out = Vec::new(); | ||
432 | format_commonmark(doc, &ComrakOptions::default(), &mut out); | ||
433 | Some(String::from_utf8(out).unwrap()) | ||
434 | } else { | ||
435 | // eprintln!("WARN: Unable to determine name or module for hover; link rewriting disabled."); | ||
436 | None | ||
437 | } | ||
438 | } | ||
439 | |||
440 | /// Try to resolve path to local documentation via intra-doc-links (i.e. `super::gateway::Shard`) | ||
441 | fn try_resolve_intra(db: &RootDatabase, doc_target_dirs: impl Iterator<Item = PathBuf>, definition: &Definition, link: &str) -> Option<String> { | ||
442 | None | ||
443 | } | ||
444 | |||
445 | /// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`) | ||
446 | fn try_resolve_path(db: &RootDatabase, doc_target_dirs: impl Iterator<Item = PathBuf>, definition: &Definition, link: &str) -> Option<String> { | ||
447 | let ns = if let Definition::ModuleDef(moddef) = definition { | ||
448 | ItemInNs::Types(moddef.clone().into()) | ||
449 | } else { | ||
450 | return None; | ||
451 | }; | ||
452 | let krate = definition.module(db)?.krate(); | ||
453 | let import_map = db.import_map(krate.into()); | ||
454 | let base = import_map.path_of(ns).unwrap(); | ||
455 | let base = base.segments.iter().map(|name| format!("{}", name)).collect::<PathBuf>(); | ||
456 | |||
457 | doc_target_dirs | ||
458 | .map(|dir| dir.join(format!("{}", krate.display_name(db).unwrap())).join(base.join("..").join(link))) | ||
459 | .inspect(|path| eprintln!("candidate {}", path.display())) | ||
460 | .filter(|path| path.exists()) | ||
461 | // slice out the UNC '\?\' added by canonicalize | ||
462 | .map(|path| format!("file:///{}", path.display())) | ||
463 | // \. is treated as an escape in vscode's markdown hover rendering | ||
464 | .map(|path_str| path_str.replace("\\", "/")) | ||
465 | .next() | ||
466 | } | ||
467 | |||
468 | fn iter_nodes<'a, F>(node: &'a comrak::nodes::AstNode<'a>, f: &F) | ||
469 | where F : Fn(&'a comrak::nodes::AstNode<'a>) { | ||
470 | f(node); | ||
471 | for c in node.children() { | ||
472 | iter_nodes(c, f); | ||
473 | } | ||
474 | } | ||
475 | |||
382 | fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> { | 476 | fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> { |
383 | return tokens.max_by_key(priority); | 477 | return tokens.max_by_key(priority); |
384 | fn priority(n: &SyntaxToken) -> usize { | 478 | fn priority(n: &SyntaxToken) -> usize { |