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.rs118
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;