From 9763f0a6bd0d576236ff126173d7df3462c22a52 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 17 Mar 2021 20:57:30 +0100 Subject: Semantic highlight intradoclinks in documentation --- crates/ide/src/doc_links.rs | 13 ++- crates/ide/src/syntax_highlighting/html.rs | 1 + crates/ide/src/syntax_highlighting/inject.rs | 120 +++++++++++++++------ crates/ide/src/syntax_highlighting/tags.rs | 10 +- .../test_data/highlight_assoc_functions.html | 1 + .../test_data/highlight_doctest.html | 6 ++ .../test_data/highlight_extern_crate.html | 1 + .../test_data/highlight_injection.html | 1 + .../test_data/highlight_strings.html | 1 + .../test_data/highlight_unsafe.html | 1 + .../test_data/highlighting.html | 1 + .../syntax_highlighting/test_data/injection.html | 1 + .../test_data/rainbow_highlighting.html | 1 + crates/ide/src/syntax_highlighting/tests.rs | 7 +- 14 files changed, 120 insertions(+), 45 deletions(-) (limited to 'crates/ide') diff --git a/crates/ide/src/doc_links.rs b/crates/ide/src/doc_links.rs index 5ea9fc4fb..c7c1f4fee 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, Range)> { 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 0ee7bc96e..1d34731ab 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 7a4f2645f..947cc974c 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, 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, 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, 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, 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, +) -> 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 3c02fdb11..ce46e5127 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/ide/src/syntax_highlighting/test_data/highlight_assoc_functions.html b/crates/ide/src/syntax_highlighting/test_data/highlight_assoc_functions.html index d421a7803..60c7518af 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 d792a23cf..5d802a647 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 } } +/// [`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); /// ``` 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 6f7a7ffff..4e312765c 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 753b535b5..57dfe7509 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 66d80c4b6..75dbd0f14 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 036cb6c11..423256a20 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 2f983c0b8..fffe8c0f5 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 78dfec951..34d8deb68 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 e64f2e5e9..d9ca3a4c4 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 cf0b86ad0..7b2922b0d 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); /// ``` -- cgit v1.2.3