Repository: rust-analyzer/rust-analyzer
Pull request #8071 (2021-03-17 20:00:48)

Semantic highlight intradoclinks in documentation
Changelog feature highlight Intradoclinks in doc comments


Patch:
diff --git a/crates/ide/src/doc_links.rs b/crates/ide/src/doc_links.rs
index 5ea9fc4fb3..c7c1f4fee9 100644
--- a/crates/ide/src/doc_links.rs
+++ b/crates/ide/src/doc_links.rs
@@ -65,6 +65,8 @@ pub(crate) fn extract_definitions_from_markdown(
 ) -> Vec<(String, Option<hir::Namespace>, Range<usize>)> {
     let mut res = vec![];
     let mut cb = |link: BrokenLink| {
+        // These allocations are actually unnecessary but the lifetimes on BrokenLinkCallback are wrong
+        // this is fixed in the repo but not on the crates.io release yet
         Some((
             /*url*/ link.reference.to_owned().into(),
             /*title*/ link.reference.to_owned().into(),
@@ -72,13 +74,10 @@ pub(crate) fn extract_definitions_from_markdown(
     };
     let doc = Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(&mut cb));
     for (event, range) in doc.into_offset_iter() {
-        match event {
-            Event::Start(Tag::Link(_link_type, ref target, ref title)) => {
-                let link = if target.is_empty() { title } else { target };
-                let (link, ns) = parse_link(link);
-                res.push((link.to_string(), ns, range));
-            }
-            _ => {}
+        if let Event::Start(Tag::Link(_, target, title)) = event {
+            let link = if target.is_empty() { title } else { target };
+            let (link, ns) = parse_link(&link);
+            res.push((link.to_string(), ns, range));
         }
     }
     res
diff --git a/crates/ide/src/syntax_highlighting/html.rs b/crates/ide/src/syntax_highlighting/html.rs
index 0ee7bc96e2..1d34731ab9 100644
--- a/crates/ide/src/syntax_highlighting/html.rs
+++ b/crates/ide/src/syntax_highlighting/html.rs
@@ -59,6 +59,7 @@ pre                 { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
 .label              { color: #DFAF8F; font-style: italic; }
 .comment            { color: #7F9F7F; }
 .documentation      { color: #629755; }
+.intra_doc_link     { color: #A9C577; }
 .injected           { opacity: 0.65 ; }
 .struct, .enum      { color: #7CB8BB; }
 .enum_variant       { color: #BDE0F3; }
diff --git a/crates/ide/src/syntax_highlighting/inject.rs b/crates/ide/src/syntax_highlighting/inject.rs
index 7a4f2645fa..947cc974ce 100644
--- a/crates/ide/src/syntax_highlighting/inject.rs
+++ b/crates/ide/src/syntax_highlighting/inject.rs
@@ -1,16 +1,18 @@
 //! "Recursive" Syntax highlighting for code in doctests and fixtures.
 
-use std::mem;
+use std::{mem, ops::Range};
 
 use either::Either;
 use hir::{HasAttrs, Semantics};
-use ide_db::call_info::ActiveParameter;
+use ide_db::{call_info::ActiveParameter, defs::Definition};
 use syntax::{
     ast::{self, AstNode, AttrsOwner, DocCommentsOwner},
     match_ast, AstToken, NodeOrToken, SyntaxNode, SyntaxToken, TextRange, TextSize,
 };
 
-use crate::{Analysis, HlMod, HlRange, HlTag, RootDatabase};
+use crate::{
+    doc_links::extract_definitions_from_markdown, Analysis, HlMod, HlRange, HlTag, RootDatabase,
+};
 
 use super::{highlights::Highlights, injector::Injector};
 
@@ -120,24 +122,24 @@ impl AstNode for AttrsOwnerNode {
 fn doc_attributes<'node>(
     sema: &Semantics<RootDatabase>,
     node: &'node SyntaxNode,
-) -> Option<(AttrsOwnerNode, hir::Attrs)> {
+) -> Option<(AttrsOwnerNode, hir::Attrs, Definition)> {
     match_ast! {
         match node {
-            ast::SourceFile(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
-            ast::Fn(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
-            ast::Struct(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
-            ast::Union(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
-            ast::RecordField(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
-            ast::TupleField(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
-            ast::Enum(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
-            ast::Variant(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
-            ast::Trait(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
-            ast::Module(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
-            ast::Static(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
-            ast::Const(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
-            ast::TypeAlias(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
-            ast::Impl(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
-            ast::MacroRules(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
+            ast::SourceFile(it)  => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))),
+            ast::Module(it)      => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))),
+            ast::Fn(it)          => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Function(def)))),
+            ast::Struct(it)      => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Struct(def))))),
+            ast::Union(it)       => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Union(def))))),
+            ast::Enum(it)        => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Enum(def))))),
+            ast::Variant(it)     => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Variant(def)))),
+            ast::Trait(it)       => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Trait(def)))),
+            ast::Static(it)      => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Static(def)))),
+            ast::Const(it)       => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Const(def)))),
+            ast::TypeAlias(it)   => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::TypeAlias(def)))),
+            ast::Impl(it)        => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db), Definition::SelfType(def))),
+            ast::RecordField(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db), Definition::Field(def))),
+            ast::TupleField(it)  => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db), Definition::Field(def))),
+            ast::MacroRules(it)  => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db), Definition::Macro(def))),
             // ast::MacroDef(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))),
             // ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))),
             _ => return None
@@ -147,25 +149,23 @@ fn doc_attributes<'node>(
 
 /// Injection of syntax highlighting of doctests.
 pub(super) fn doc_comment(hl: &mut Highlights, sema: &Semantics<RootDatabase>, node: &SyntaxNode) {
-    let (owner, attributes) = match doc_attributes(sema, node) {
+    let (owner, attributes, def) = match doc_attributes(sema, node) {
         Some(it) => it,
         None => return,
     };
 
-    if attributes.docs().map_or(true, |docs| !String::from(docs).contains(RUSTDOC_FENCE)) {
-        return;
-    }
-    let attrs_source_map = attributes.source_map(&owner);
-
     let mut inj = Injector::default();
     inj.add_unmapped("fn doctest() {\n");
 
+    let attrs_source_map = attributes.source_map(&owner);
+
     let mut is_codeblock = false;
     let mut is_doctest = false;
 
     // Replace the original, line-spanning comment ranges by new, only comment-prefix
     // spanning comment ranges.
     let mut new_comments = Vec::new();
+    let mut intra_doc_links = Vec::new();
     let mut string;
     for attr in attributes.by_key("doc").attrs() {
         let src = attrs_source_map.source_of(&attr);
@@ -209,7 +209,22 @@ pub(super) fn doc_comment(hl: &mut Highlights, sema: &Semantics<RootDatabase>, n
                     is_doctest = is_codeblock && is_rust;
                     continue;
                 }
-                None if !is_doctest => continue,
+                None if !is_doctest => {
+                    intra_doc_links.extend(
+                        extract_definitions_from_markdown(line)
+                            .into_iter()
+                            .filter(|(link, ns, _)| {
+                                validate_intra_doc_link(sema.db, &def, link, *ns)
+                            })
+                            .map(|(.., Range { start, end })| {
+                                TextRange::at(
+                                    prev_range_start + TextSize::from(start as u32),
+                                    TextSize::from((end - start) as u32),
+                                )
+                            }),
+                    );
+                    continue;
+                }
                 None => (),
             }
 
@@ -227,17 +242,28 @@ pub(super) fn doc_comment(hl: &mut Highlights, sema: &Semantics<RootDatabase>, n
             inj.add_unmapped("\n");
         }
     }
+
+    for range in intra_doc_links {
+        hl.add(HlRange {
+            range,
+            highlight: HlTag::IntraDocLink | HlMod::Documentation,
+            binding_hash: None,
+        });
+    }
+
+    if new_comments.is_empty() {
+        return; // no need to run an analysis on an empty file
+    }
+
     inj.add_unmapped("\n}");
 
     let (analysis, tmp_file_id) = Analysis::from_single_file(inj.text().to_string());
 
-    for h in analysis.with_db(|db| super::highlight(db, tmp_file_id, None, true)).unwrap() {
-        for r in inj.map_range_up(h.range) {
-            hl.add(HlRange {
-                range: r,
-                highlight: h.highlight | HlMod::Injected,
-                binding_hash: h.binding_hash,
-            });
+    for HlRange { range, highlight, binding_hash } in
+        analysis.with_db(|db| super::highlight(db, tmp_file_id, None, true)).unwrap()
+    {
+        for range in inj.map_range_up(range) {
+            hl.add(HlRange { range, highlight: highlight | HlMod::Injected, binding_hash });
         }
     }
 
@@ -273,3 +299,31 @@ fn find_doc_string_in_attr(attr: &hir::Attr, it: &ast::Attr) -> Option<ast::Stri
         }
     }
 }
+
+fn validate_intra_doc_link(
+    db: &RootDatabase,
+    def: &Definition,
+    link: &str,
+    ns: Option<hir::Namespace>,
+) -> bool {
+    match def {
+        Definition::ModuleDef(def) => match def {
+            hir::ModuleDef::Module(it) => it.resolve_doc_path(db, &link, ns),
+            hir::ModuleDef::Function(it) => it.resolve_doc_path(db, &link, ns),
+            hir::ModuleDef::Adt(it) => it.resolve_doc_path(db, &link, ns),
+            hir::ModuleDef::Variant(it) => it.resolve_doc_path(db, &link, ns),
+            hir::ModuleDef::Const(it) => it.resolve_doc_path(db, &link, ns),
+            hir::ModuleDef::Static(it) => it.resolve_doc_path(db, &link, ns),
+            hir::ModuleDef::Trait(it) => it.resolve_doc_path(db, &link, ns),
+            hir::ModuleDef::TypeAlias(it) => it.resolve_doc_path(db, &link, ns),
+            hir::ModuleDef::BuiltinType(_) => None,
+        },
+        Definition::Macro(it) => it.resolve_doc_path(db, &link, ns),
+        Definition::Field(it) => it.resolve_doc_path(db, &link, ns),
+        Definition::SelfType(_)
+        | Definition::Local(_)
+        | Definition::GenericParam(_)
+        | Definition::Label(_) => None,
+    }
+    .is_some()
+}
diff --git a/crates/ide/src/syntax_highlighting/tags.rs b/crates/ide/src/syntax_highlighting/tags.rs
index 3c02fdb115..ce46e5127a 100644
--- a/crates/ide/src/syntax_highlighting/tags.rs
+++ b/crates/ide/src/syntax_highlighting/tags.rs
@@ -18,19 +18,20 @@ pub struct HlMods(u32);
 pub enum HlTag {
     Symbol(SymbolKind),
 
+    Attribute,
     BoolLiteral,
     BuiltinType,
     ByteLiteral,
     CharLiteral,
-    NumericLiteral,
-    StringLiteral,
-    Attribute,
     Comment,
     EscapeSequence,
     FormatSpecifier,
+    IntraDocLink,
     Keyword,
-    Punctuation(HlPunct),
+    NumericLiteral,
     Operator,
+    Punctuation(HlPunct),
+    StringLiteral,
     UnresolvedReference,
 
     // For things which don't have a specific highlight.
@@ -116,6 +117,7 @@ impl HlTag {
             HlTag::Comment => "comment",
             HlTag::EscapeSequence => "escape_sequence",
             HlTag::FormatSpecifier => "format_specifier",
+            HlTag::IntraDocLink => "intra_doc_link",
             HlTag::Keyword => "keyword",
             HlTag::Punctuation(punct) => match punct {
                 HlPunct::Bracket => "bracket",
diff --git a/crates/rust-analyzer/src/semantic_tokens.rs b/crates/rust-analyzer/src/semantic_tokens.rs
index be0bea00b7..0cb7d12a76 100644
--- a/crates/rust-analyzer/src/semantic_tokens.rs
+++ b/crates/rust-analyzer/src/semantic_tokens.rs
@@ -45,15 +45,16 @@ define_semantic_token_types![
     (BRACKET, "bracket"),
     (BUILTIN_TYPE, "builtinType"),
     (CHAR_LITERAL, "characterLiteral"),
-    (COMMA, "comma"),
     (COLON, "colon"),
+    (COMMA, "comma"),
+    (CONST_PARAMETER, "constParameter"),
     (DOT, "dot"),
     (ESCAPE_SEQUENCE, "escapeSequence"),
     (FORMAT_SPECIFIER, "formatSpecifier"),
     (GENERIC, "generic"),
-    (CONST_PARAMETER, "constParameter"),
-    (LIFETIME, "lifetime"),
+    (INTRA_DOC_LINK, "intraDocLink"),
     (LABEL, "label"),
+    (LIFETIME, "lifetime"),
     (PARENTHESIS, "parenthesis"),
     (PUNCTUATION, "punctuation"),
     (SELF_KEYWORD, "selfKeyword"),
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs
index c63fe2915e..70501618e8 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -435,19 +435,20 @@ fn semantic_token_type_and_modifiers(
             SymbolKind::Trait => lsp_types::SemanticTokenType::INTERFACE,
             SymbolKind::Macro => lsp_types::SemanticTokenType::MACRO,
         },
+        HlTag::Attribute => semantic_tokens::ATTRIBUTE,
+        HlTag::BoolLiteral => semantic_tokens::BOOLEAN,
         HlTag::BuiltinType => semantic_tokens::BUILTIN_TYPE,
-        HlTag::None => semantic_tokens::GENERIC,
         HlTag::ByteLiteral | HlTag::NumericLiteral => lsp_types::SemanticTokenType::NUMBER,
-        HlTag::BoolLiteral => semantic_tokens::BOOLEAN,
-        HlTag::StringLiteral => lsp_types::SemanticTokenType::STRING,
         HlTag::CharLiteral => semantic_tokens::CHAR_LITERAL,
         HlTag::Comment => lsp_types::SemanticTokenType::COMMENT,
-        HlTag::Attribute => semantic_tokens::ATTRIBUTE,
-        HlTag::Keyword => lsp_types::SemanticTokenType::KEYWORD,
-        HlTag::UnresolvedReference => semantic_tokens::UNRESOLVED_REFERENCE,
+        HlTag::EscapeSequence => semantic_tokens::ESCAPE_SEQUENCE,
         HlTag::FormatSpecifier => semantic_tokens::FORMAT_SPECIFIER,
+        HlTag::IntraDocLink => semantic_tokens::INTRA_DOC_LINK,
+        HlTag::Keyword => lsp_types::SemanticTokenType::KEYWORD,
+        HlTag::None => semantic_tokens::GENERIC,
         HlTag::Operator => lsp_types::SemanticTokenType::OPERATOR,
-        HlTag::EscapeSequence => semantic_tokens::ESCAPE_SEQUENCE,
+        HlTag::StringLiteral => lsp_types::SemanticTokenType::STRING,
+        HlTag::UnresolvedReference => semantic_tokens::UNRESOLVED_REFERENCE,
         HlTag::Punctuation(punct) => match punct {
             HlPunct::Bracket => semantic_tokens::BRACKET,
             HlPunct::Brace => semantic_tokens::BRACE,


Test patch:
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlight_assoc_functions.html b/crates/ide/src/syntax_highlighting/test_data/highlight_assoc_functions.html
index d421a78038..60c7518af8 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlight_assoc_functions.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlight_assoc_functions.html
@@ -7,6 +7,7 @@ pre                 { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
 .label              { color: #DFAF8F; font-style: italic; }
 .comment            { color: #7F9F7F; }
 .documentation      { color: #629755; }
+.intra_doc_link     { color: #A9C577; }
 .injected           { opacity: 0.65 ; }
 .struct, .enum      { color: #7CB8BB; }
 .enum_variant       { color: #BDE0F3; }
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html b/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html
index d792a23cfc..5d802a6470 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html
@@ -7,6 +7,7 @@ pre                 { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
 .label              { color: #DFAF8F; font-style: italic; }
 .comment            { color: #7F9F7F; }
 .documentation      { color: #629755; }
+.intra_doc_link     { color: #A9C577; }
 .injected           { opacity: 0.65 ; }
 .struct, .enum      { color: #7CB8BB; }
 .enum_variant       { color: #BDE0F3; }
@@ -98,6 +99,11 @@ pre                 { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
     <span class="brace">}</span>
 <span class="brace">}</span>
 
+<span class="comment documentation">/// </span><span class="intra_doc_link documentation">[`Foo`](Foo)</span><span class="comment documentation"> is a struct</span>
+<span class="comment documentation">/// </span><span class="intra_doc_link documentation">[`all_the_links`](all_the_links)</span><span class="comment documentation"> is this function</span>
+<span class="comment documentation">/// [`noop`](noop) is a macro below</span>
+<span class="keyword">pub</span> <span class="keyword">fn</span> <span class="function declaration">all_the_links</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span>
+
 <span class="comment documentation">/// ```</span>
 <span class="comment documentation">/// </span><span class="macro injected">noop!</span><span class="parenthesis injected">(</span><span class="numeric_literal injected">1</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span>
 <span class="comment documentation">/// ```</span>
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlight_extern_crate.html b/crates/ide/src/syntax_highlighting/test_data/highlight_extern_crate.html
index 6f7a7ffff3..4e312765c0 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlight_extern_crate.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlight_extern_crate.html
@@ -7,6 +7,7 @@ pre                 { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
 .label              { color: #DFAF8F; font-style: italic; }
 .comment            { color: #7F9F7F; }
 .documentation      { color: #629755; }
+.intra_doc_link     { color: #A9C577; }
 .injected           { opacity: 0.65 ; }
 .struct, .enum      { color: #7CB8BB; }
 .enum_variant       { color: #BDE0F3; }
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlight_injection.html b/crates/ide/src/syntax_highlighting/test_data/highlight_injection.html
index 753b535b55..57dfe75096 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlight_injection.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlight_injection.html
@@ -7,6 +7,7 @@ pre                 { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
 .label              { color: #DFAF8F; font-style: italic; }
 .comment            { color: #7F9F7F; }
 .documentation      { color: #629755; }
+.intra_doc_link     { color: #A9C577; }
 .injected           { opacity: 0.65 ; }
 .struct, .enum      { color: #7CB8BB; }
 .enum_variant       { color: #BDE0F3; }
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlight_strings.html b/crates/ide/src/syntax_highlighting/test_data/highlight_strings.html
index 66d80c4b66..75dbd0f142 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlight_strings.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlight_strings.html
@@ -7,6 +7,7 @@ pre                 { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
 .label              { color: #DFAF8F; font-style: italic; }
 .comment            { color: #7F9F7F; }
 .documentation      { color: #629755; }
+.intra_doc_link     { color: #A9C577; }
 .injected           { opacity: 0.65 ; }
 .struct, .enum      { color: #7CB8BB; }
 .enum_variant       { color: #BDE0F3; }
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlight_unsafe.html b/crates/ide/src/syntax_highlighting/test_data/highlight_unsafe.html
index 036cb6c115..423256a202 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlight_unsafe.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlight_unsafe.html
@@ -7,6 +7,7 @@ pre                 { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
 .label              { color: #DFAF8F; font-style: italic; }
 .comment            { color: #7F9F7F; }
 .documentation      { color: #629755; }
+.intra_doc_link     { color: #A9C577; }
 .injected           { opacity: 0.65 ; }
 .struct, .enum      { color: #7CB8BB; }
 .enum_variant       { color: #BDE0F3; }
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlighting.html b/crates/ide/src/syntax_highlighting/test_data/highlighting.html
index 2f983c0b8a..fffe8c0f57 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlighting.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlighting.html
@@ -7,6 +7,7 @@ pre                 { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
 .label              { color: #DFAF8F; font-style: italic; }
 .comment            { color: #7F9F7F; }
 .documentation      { color: #629755; }
+.intra_doc_link     { color: #A9C577; }
 .injected           { opacity: 0.65 ; }
 .struct, .enum      { color: #7CB8BB; }
 .enum_variant       { color: #BDE0F3; }
diff --git a/crates/ide/src/syntax_highlighting/test_data/injection.html b/crates/ide/src/syntax_highlighting/test_data/injection.html
index 78dfec9518..34d8deb684 100644
--- a/crates/ide/src/syntax_highlighting/test_data/injection.html
+++ b/crates/ide/src/syntax_highlighting/test_data/injection.html
@@ -7,6 +7,7 @@ pre                 { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
 .label              { color: #DFAF8F; font-style: italic; }
 .comment            { color: #7F9F7F; }
 .documentation      { color: #629755; }
+.intra_doc_link     { color: #A9C577; }
 .injected           { opacity: 0.65 ; }
 .struct, .enum      { color: #7CB8BB; }
 .enum_variant       { color: #BDE0F3; }
diff --git a/crates/ide/src/syntax_highlighting/test_data/rainbow_highlighting.html b/crates/ide/src/syntax_highlighting/test_data/rainbow_highlighting.html
index e64f2e5e93..d9ca3a4c46 100644
--- a/crates/ide/src/syntax_highlighting/test_data/rainbow_highlighting.html
+++ b/crates/ide/src/syntax_highlighting/test_data/rainbow_highlighting.html
@@ -7,6 +7,7 @@ pre                 { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
 .label              { color: #DFAF8F; font-style: italic; }
 .comment            { color: #7F9F7F; }
 .documentation      { color: #629755; }
+.intra_doc_link     { color: #A9C577; }
 .injected           { opacity: 0.65 ; }
 .struct, .enum      { color: #7CB8BB; }
 .enum_variant       { color: #BDE0F3; }
diff --git a/crates/ide/src/syntax_highlighting/tests.rs b/crates/ide/src/syntax_highlighting/tests.rs
index cf0b86ad00..7b2922b0d5 100644
--- a/crates/ide/src/syntax_highlighting/tests.rs
+++ b/crates/ide/src/syntax_highlighting/tests.rs
@@ -468,7 +468,7 @@ fn main() {
 }
 
 #[test]
-fn test_highlight_doctest() {
+fn test_highlight_doc_comment() {
     check_highlighting(
         r#"
 /// ```
@@ -533,6 +533,11 @@ impl Foo {
     }
 }
 
+/// [`Foo`](Foo) is a struct
+/// [`all_the_links`](all_the_links) is this function
+/// [`noop`](noop) is a macro below
+pub fn all_the_links() {}
+
 /// ```
 /// noop!(1);
 /// ```