8071: Semantic highlight intradoclinks in documentation r=Veykril a=Veykril



Co-authored-by: Lukas Wirth <lukastw97@gmail.com>
This commit is contained in:
bors[bot] 2021-03-17 20:08:42 +00:00 committed by GitHub
commit bba474bb52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 134 additions and 57 deletions

View File

@ -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

View File

@ -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; }

View File

@ -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()
}

View File

@ -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",

View File

@ -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; }

View File

@ -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>

View File

@ -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; }

View File

@ -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; }

View File

@ -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; }

View File

@ -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; }

View File

@ -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; }

View File

@ -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; }

View File

@ -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; }

View File

@ -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);
/// ```

View File

@ -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"),

View File

@ -435,19 +435,20 @@ fn semantic_token_type_and_modifiers(
SymbolKind::Trait => lsp_types::SemanticTokenType::INTERFACE,
SymbolKind::Macro => lsp_types::SemanticTokenType::MACRO,
},
HlTag::BuiltinType => semantic_tokens::BUILTIN_TYPE,
HlTag::None => semantic_tokens::GENERIC,
HlTag::ByteLiteral | HlTag::NumericLiteral => lsp_types::SemanticTokenType::NUMBER,
HlTag::Attribute => semantic_tokens::ATTRIBUTE,
HlTag::BoolLiteral => semantic_tokens::BOOLEAN,
HlTag::StringLiteral => lsp_types::SemanticTokenType::STRING,
HlTag::BuiltinType => semantic_tokens::BUILTIN_TYPE,
HlTag::ByteLiteral | HlTag::NumericLiteral => lsp_types::SemanticTokenType::NUMBER,
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::FormatSpecifier => semantic_tokens::FORMAT_SPECIFIER,
HlTag::Operator => lsp_types::SemanticTokenType::OPERATOR,
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::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,