Skip to content

Semantic highlight intradoclinks in documentation #8071

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions crates/ide/src/doc_links.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,19 @@ 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(),
))
};
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
Expand Down
1 change: 1 addition & 0 deletions crates/ide/src/syntax_highlighting/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
120 changes: 87 additions & 33 deletions crates/ide/src/syntax_highlighting/inject.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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 => (),
}

Expand All @@ -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 });
}
}

Expand Down Expand Up @@ -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()
}
10 changes: 6 additions & 4 deletions crates/ide/src/syntax_highlighting/tags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
.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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
.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; }
Expand Down Expand Up @@ -98,6 +99,11 @@
<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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
.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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
.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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
.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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
.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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
.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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
.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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
.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; }
Expand Down
7 changes: 6 additions & 1 deletion crates/ide/src/syntax_highlighting/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ fn main() {
}

#[test]
fn test_highlight_doctest() {
fn test_highlight_doc_comment() {
check_highlighting(
r#"
/// ```
Expand Down Expand Up @@ -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);
/// ```
Expand Down
7 changes: 4 additions & 3 deletions crates/rust-analyzer/src/semantic_tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a separate token type here? I think that we might want to set the type to the type of the pointed to item, and set modifier to "doc link". That is, treat links (the part that is a rust path) as small snippets of injected rust code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that's a nice idea actually

(LABEL, "label"),
(LIFETIME, "lifetime"),
(PARENTHESIS, "parenthesis"),
(PUNCTUATION, "punctuation"),
(SELF_KEYWORD, "selfKeyword"),
Expand Down
Loading