Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide/src/doc_links.rs')
| -rw-r--r-- | crates/ide/src/doc_links.rs | 118 |
1 files changed, 83 insertions, 35 deletions
diff --git a/crates/ide/src/doc_links.rs b/crates/ide/src/doc_links.rs index f0247f32d7..2c983287d8 100644 --- a/crates/ide/src/doc_links.rs +++ b/crates/ide/src/doc_links.rs @@ -5,17 +5,21 @@ mod tests; mod intra_doc_links; +use std::ops::Range; + use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag}; use pulldown_cmark_to_cmark::{Options as CMarkOptions, cmark_resume_with_options}; use stdx::format_to; use url::Url; -use hir::{Adt, AsAssocItem, AssocItem, AssocItemContainer, HasAttrs, db::HirDatabase, sym}; +use hir::{ + Adt, AsAssocItem, AssocItem, AssocItemContainer, AttrsWithOwner, HasAttrs, db::HirDatabase, sym, +}; use ide_db::{ RootDatabase, base_db::{CrateOrigin, LangCrateOrigin, ReleaseChannel, RootQueryDb}, defs::{Definition, NameClass, NameRefClass}, - documentation::{Documentation, HasDocs, docs_with_rangemap}, + documentation::{DocsRangeMap, Documentation, HasDocs, docs_with_rangemap}, helpers::pick_best_token, }; use syntax::{ @@ -46,11 +50,17 @@ const MARKDOWN_OPTIONS: Options = Options::ENABLE_FOOTNOTES.union(Options::ENABLE_TABLES).union(Options::ENABLE_TASKLISTS); /// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs) -pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: Definition) -> String { +pub(crate) fn rewrite_links( + db: &RootDatabase, + markdown: &str, + definition: Definition, + range_map: Option<DocsRangeMap>, +) -> String { let mut cb = broken_link_clone_cb; - let doc = Parser::new_with_broken_link_callback(markdown, MARKDOWN_OPTIONS, Some(&mut cb)); + let doc = Parser::new_with_broken_link_callback(markdown, MARKDOWN_OPTIONS, Some(&mut cb)) + .into_offset_iter(); - let doc = map_links(doc, |target, title| { + let doc = map_links(doc, |target, title, range| { // This check is imperfect, there's some overlap between valid intra-doc links // and valid URLs so we choose to be too eager to try to resolve what might be // a URL. @@ -60,7 +70,16 @@ pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: Defin // Two possibilities: // * path-based links: `../../module/struct.MyStruct.html` // * module-based links (AKA intra-doc links): `super::super::module::MyStruct` - if let Some((target, title)) = rewrite_intra_doc_link(db, definition, target, title) { + let text_range = + TextRange::new(range.start.try_into().unwrap(), range.end.try_into().unwrap()); + let is_inner_doc = range_map + .as_ref() + .and_then(|range_map| range_map.map(text_range)) + .map(|(_, attr_id)| attr_id.is_inner_attr()) + .unwrap_or(false); + if let Some((target, title)) = + rewrite_intra_doc_link(db, definition, target, title, is_inner_doc) + { (None, target, title) } else if let Some(target) = rewrite_url_link(db, definition, target) { (Some(LinkType::Inline), target, title.to_owned()) @@ -195,22 +214,23 @@ pub(crate) fn resolve_doc_path_for_def( def: Definition, link: &str, ns: Option<hir::Namespace>, + is_inner_doc: bool, ) -> Option<Definition> { match def { - Definition::Module(it) => it.resolve_doc_path(db, link, ns), - Definition::Crate(it) => it.resolve_doc_path(db, link, ns), - Definition::Function(it) => it.resolve_doc_path(db, link, ns), - Definition::Adt(it) => it.resolve_doc_path(db, link, ns), - Definition::Variant(it) => it.resolve_doc_path(db, link, ns), - Definition::Const(it) => it.resolve_doc_path(db, link, ns), - Definition::Static(it) => it.resolve_doc_path(db, link, ns), - Definition::Trait(it) => it.resolve_doc_path(db, link, ns), - Definition::TraitAlias(it) => it.resolve_doc_path(db, link, ns), - Definition::TypeAlias(it) => it.resolve_doc_path(db, link, ns), - Definition::Macro(it) => it.resolve_doc_path(db, link, ns), - Definition::Field(it) => it.resolve_doc_path(db, link, ns), - Definition::SelfType(it) => it.resolve_doc_path(db, link, ns), - Definition::ExternCrateDecl(it) => it.resolve_doc_path(db, link, ns), + Definition::Module(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), + Definition::Crate(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), + Definition::Function(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), + Definition::Adt(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), + Definition::Variant(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), + Definition::Const(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), + Definition::Static(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), + Definition::Trait(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), + Definition::TraitAlias(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), + Definition::TypeAlias(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), + Definition::Macro(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), + Definition::Field(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), + Definition::SelfType(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), + Definition::ExternCrateDecl(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), Definition::BuiltinAttr(_) | Definition::BuiltinType(_) | Definition::BuiltinLifetime(_) @@ -289,31 +309,58 @@ impl DocCommentToken { let relative_comment_offset = offset - original_start - prefix_len; sema.descend_into_macros(doc_token).into_iter().find_map(|t| { - let (node, descended_prefix_len) = match_ast! { + let (node, descended_prefix_len, is_inner) = match_ast!{ match t { - ast::Comment(comment) => (t.parent()?, TextSize::try_from(comment.prefix().len()).ok()?), - ast::String(string) => (t.parent_ancestors().skip_while(|n| n.kind() != ATTR).nth(1)?, string.open_quote_text_range()?.len()), + ast::Comment(comment) => { + (t.parent()?, TextSize::try_from(comment.prefix().len()).ok()?, comment.is_inner()) + }, + ast::String(string) => { + let attr = t.parent_ancestors().find_map(ast::Attr::cast)?; + let attr_is_inner = attr.excl_token().map(|excl| excl.kind() == BANG).unwrap_or(false); + (attr.syntax().parent()?, string.open_quote_text_range()?.len(), attr_is_inner) + }, _ => return None, } }; let token_start = t.text_range().start(); let abs_in_expansion_offset = token_start + relative_comment_offset + descended_prefix_len; - - let (attributes, def) = doc_attributes(sema, &node)?; + let (attributes, def) = Self::doc_attributes(sema, &node, is_inner)?; let (docs, doc_mapping) = docs_with_rangemap(sema.db, &attributes)?; - let (in_expansion_range, link, ns) = + let (in_expansion_range, link, ns, is_inner) = extract_definitions_from_docs(&docs).into_iter().find_map(|(range, link, ns)| { - let mapped = doc_mapping.map(range)?; - (mapped.value.contains(abs_in_expansion_offset)).then_some((mapped.value, link, ns)) + let (mapped, idx) = doc_mapping.map(range)?; + (mapped.value.contains(abs_in_expansion_offset)).then_some((mapped.value, link, ns, idx.is_inner_attr())) })?; // get the relative range to the doc/attribute in the expansion let in_expansion_relative_range = in_expansion_range - descended_prefix_len - token_start; // Apply relative range to the original input comment let absolute_range = in_expansion_relative_range + original_start + prefix_len; - let def = resolve_doc_path_for_def(sema.db, def, &link, ns)?; + let def = resolve_doc_path_for_def(sema.db, def, &link, ns, is_inner)?; cb(def, node, absolute_range) }) } + + /// When we hover a inner doc item, this find a attached definition. + /// ``` + /// // node == ITEM_LIST + /// // node.parent == EXPR_BLOCK + /// // node.parent().parent() == FN + /// fn f() { + /// //! [`S$0`] + /// } + /// ``` + fn doc_attributes( + sema: &Semantics<'_, RootDatabase>, + node: &SyntaxNode, + is_inner_doc: bool, + ) -> Option<(AttrsWithOwner, Definition)> { + if is_inner_doc && node.kind() != SOURCE_FILE { + let parent = node.parent()?; + doc_attributes(sema, &parent).or(doc_attributes(sema, &parent.parent()?)) + } else { + doc_attributes(sema, node) + } + } } fn broken_link_clone_cb(link: BrokenLink<'_>) -> Option<(CowStr<'_>, CowStr<'_>)> { @@ -369,6 +416,7 @@ fn rewrite_intra_doc_link( def: Definition, target: &str, title: &str, + is_inner_doc: bool, ) -> Option<(String, String)> { let (link, ns) = parse_intra_doc_link(target); @@ -377,7 +425,7 @@ fn rewrite_intra_doc_link( None => (link, None), }; - let resolved = resolve_doc_path_for_def(db, def, link, ns)?; + let resolved = resolve_doc_path_for_def(db, def, link, ns, is_inner_doc)?; let mut url = get_doc_base_urls(db, resolved, None, None).0?; let (_, file, frag) = filename_and_frag_for_def(db, resolved)?; @@ -421,8 +469,8 @@ fn mod_path_of_def(db: &RootDatabase, def: Definition) -> Option<String> { /// Rewrites a markdown document, applying 'callback' to each link. fn map_links<'e>( - events: impl Iterator<Item = Event<'e>>, - callback: impl Fn(&str, &str) -> (Option<LinkType>, String, String), + events: impl Iterator<Item = (Event<'e>, Range<usize>)>, + callback: impl Fn(&str, &str, Range<usize>) -> (Option<LinkType>, String, String), ) -> impl Iterator<Item = Event<'e>> { let mut in_link = false; // holds the origin link target on start event and the rewritten one on end event @@ -432,7 +480,7 @@ fn map_links<'e>( // `Shortcut` type parsed from Start/End tags doesn't make sense for url links let mut end_link_type: Option<LinkType> = None; - events.map(move |evt| match evt { + events.map(move |(evt, range)| match evt { Event::Start(Tag::Link(link_type, ref target, _)) => { in_link = true; end_link_target = Some(target.clone()); @@ -449,7 +497,7 @@ fn map_links<'e>( } Event::Text(s) if in_link => { let (link_type, link_target_s, link_name) = - callback(&end_link_target.take().unwrap(), &s); + callback(&end_link_target.take().unwrap(), &s, range); end_link_target = Some(CowStr::Boxed(link_target_s.into())); if !matches!(end_link_type, Some(LinkType::Autolink)) { end_link_type = link_type; @@ -458,7 +506,7 @@ fn map_links<'e>( } Event::Code(s) if in_link => { let (link_type, link_target_s, link_name) = - callback(&end_link_target.take().unwrap(), &s); + callback(&end_link_target.take().unwrap(), &s, range); end_link_target = Some(CowStr::Boxed(link_target_s.into())); if !matches!(end_link_type, Some(LinkType::Autolink)) { end_link_type = link_type; |