aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2021-03-17 11:13:54 +0000
committerGitHub <[email protected]>2021-03-17 11:13:54 +0000
commit0fbfab3b45af7a02b1224bca4a53e9b8f6ec049e (patch)
tree6ba39c051e758d64c75948716d599a9a2a427ee3
parentf7fbea509f1e5f840e715c912ee38aa997d1bfbc (diff)
parentcdfb5c353f09138540ae66a2eb80a6a81802bbd6 (diff)
Merge #8059
8059: Move doc-comment highlight injection from AST to HIR r=matklad,jonas-schievink a=Veykril Fixes #5016 Co-authored-by: Lukas Wirth <[email protected]>
-rw-r--r--crates/hir/src/attrs.rs5
-rw-r--r--crates/hir/src/lib.rs2
-rw-r--r--crates/hir/src/semantics.rs1
-rw-r--r--crates/hir/src/semantics/source_to_def.rs6
-rw-r--r--crates/hir_def/src/attr.rs45
-rw-r--r--crates/ide/src/syntax_highlighting.rs2
-rw-r--r--crates/ide/src/syntax_highlighting/inject.rs135
-rw-r--r--crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html18
-rw-r--r--crates/ide/src/syntax_highlighting/tests.rs16
-rw-r--r--crates/syntax/src/ast/traits.rs2
10 files changed, 205 insertions, 27 deletions
diff --git a/crates/hir/src/attrs.rs b/crates/hir/src/attrs.rs
index b9c695921..dab8da7bb 100644
--- a/crates/hir/src/attrs.rs
+++ b/crates/hir/src/attrs.rs
@@ -11,8 +11,8 @@ use hir_ty::db::HirDatabase;
11use syntax::ast; 11use syntax::ast;
12 12
13use crate::{ 13use crate::{
14 Adt, Const, ConstParam, Enum, Field, Function, GenericParam, LifetimeParam, MacroDef, Module, 14 Adt, Const, ConstParam, Enum, Field, Function, GenericParam, Impl, LifetimeParam, MacroDef,
15 ModuleDef, Static, Struct, Trait, TypeAlias, TypeParam, Union, Variant, 15 Module, ModuleDef, Static, Struct, Trait, TypeAlias, TypeParam, Union, Variant,
16}; 16};
17 17
18pub trait HasAttrs { 18pub trait HasAttrs {
@@ -64,6 +64,7 @@ impl_has_attrs![
64 (Adt, AdtId), 64 (Adt, AdtId),
65 (Module, ModuleId), 65 (Module, ModuleId),
66 (GenericParam, GenericParamId), 66 (GenericParam, GenericParamId),
67 (Impl, ImplId),
67]; 68];
68 69
69macro_rules! impl_has_attrs_enum { 70macro_rules! impl_has_attrs_enum {
diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs
index 861b7329e..b41a36a78 100644
--- a/crates/hir/src/lib.rs
+++ b/crates/hir/src/lib.rs
@@ -89,7 +89,7 @@ pub use crate::{
89pub use { 89pub use {
90 hir_def::{ 90 hir_def::{
91 adt::StructKind, 91 adt::StructKind,
92 attr::{Attrs, Documentation}, 92 attr::{Attr, Attrs, Documentation},
93 body::scope::ExprScopes, 93 body::scope::ExprScopes,
94 find_path::PrefixKind, 94 find_path::PrefixKind,
95 import_map, 95 import_map,
diff --git a/crates/hir/src/semantics.rs b/crates/hir/src/semantics.rs
index 00b076175..e0eb2a66d 100644
--- a/crates/hir/src/semantics.rs
+++ b/crates/hir/src/semantics.rs
@@ -752,6 +752,7 @@ macro_rules! to_def_impls {
752 752
753to_def_impls![ 753to_def_impls![
754 (crate::Module, ast::Module, module_to_def), 754 (crate::Module, ast::Module, module_to_def),
755 (crate::Module, ast::SourceFile, source_file_to_def),
755 (crate::Struct, ast::Struct, struct_to_def), 756 (crate::Struct, ast::Struct, struct_to_def),
756 (crate::Enum, ast::Enum, enum_to_def), 757 (crate::Enum, ast::Enum, enum_to_def),
757 (crate::Union, ast::Union, union_to_def), 758 (crate::Union, ast::Union, union_to_def),
diff --git a/crates/hir/src/semantics/source_to_def.rs b/crates/hir/src/semantics/source_to_def.rs
index e9d820140..c6ad5ecb5 100644
--- a/crates/hir/src/semantics/source_to_def.rs
+++ b/crates/hir/src/semantics/source_to_def.rs
@@ -71,6 +71,12 @@ impl SourceToDefCtx<'_, '_> {
71 Some(def_map.module_id(child_id)) 71 Some(def_map.module_id(child_id))
72 } 72 }
73 73
74 pub(super) fn source_file_to_def(&mut self, src: InFile<ast::SourceFile>) -> Option<ModuleId> {
75 let _p = profile::span("source_file_to_def");
76 let file_id = src.file_id.original_file(self.db.upcast());
77 self.file_to_def(file_id).get(0).copied()
78 }
79
74 pub(super) fn trait_to_def(&mut self, src: InFile<ast::Trait>) -> Option<TraitId> { 80 pub(super) fn trait_to_def(&mut self, src: InFile<ast::Trait>) -> Option<TraitId> {
75 self.to_def(src, keys::TRAIT) 81 self.to_def(src, keys::TRAIT)
76 } 82 }
diff --git a/crates/hir_def/src/attr.rs b/crates/hir_def/src/attr.rs
index b0b4b5052..7ba53ee5c 100644
--- a/crates/hir_def/src/attr.rs
+++ b/crates/hir_def/src/attr.rs
@@ -136,16 +136,15 @@ impl RawAttrs {
136 let new_attrs = self 136 let new_attrs = self
137 .iter() 137 .iter()
138 .flat_map(|attr| -> SmallVec<[_; 1]> { 138 .flat_map(|attr| -> SmallVec<[_; 1]> {
139 let attr = attr.clone();
140 let is_cfg_attr = 139 let is_cfg_attr =
141 attr.path.as_ident().map_or(false, |name| *name == hir_expand::name![cfg_attr]); 140 attr.path.as_ident().map_or(false, |name| *name == hir_expand::name![cfg_attr]);
142 if !is_cfg_attr { 141 if !is_cfg_attr {
143 return smallvec![attr]; 142 return smallvec![attr.clone()];
144 } 143 }
145 144
146 let subtree = match &attr.input { 145 let subtree = match &attr.input {
147 Some(AttrInput::TokenTree(it)) => it, 146 Some(AttrInput::TokenTree(it)) => it,
148 _ => return smallvec![attr], 147 _ => return smallvec![attr.clone()],
149 }; 148 };
150 149
151 // Input subtree is: `(cfg, $(attr),+)` 150 // Input subtree is: `(cfg, $(attr),+)`
@@ -157,11 +156,13 @@ impl RawAttrs {
157 let cfg = parts.next().unwrap(); 156 let cfg = parts.next().unwrap();
158 let cfg = Subtree { delimiter: subtree.delimiter, token_trees: cfg.to_vec() }; 157 let cfg = Subtree { delimiter: subtree.delimiter, token_trees: cfg.to_vec() };
159 let cfg = CfgExpr::parse(&cfg); 158 let cfg = CfgExpr::parse(&cfg);
159 let index = attr.index;
160 let attrs = parts.filter(|a| !a.is_empty()).filter_map(|attr| { 160 let attrs = parts.filter(|a| !a.is_empty()).filter_map(|attr| {
161 let tree = Subtree { delimiter: None, token_trees: attr.to_vec() }; 161 let tree = Subtree { delimiter: None, token_trees: attr.to_vec() };
162 let attr = ast::Attr::parse(&format!("#[{}]", tree)).ok()?; 162 let attr = ast::Attr::parse(&format!("#[{}]", tree)).ok()?;
163 let hygiene = Hygiene::new_unhygienic(); // FIXME 163 // FIXME hygiene
164 Attr::from_src(attr, &hygiene) 164 let hygiene = Hygiene::new_unhygienic();
165 Attr::from_src(attr, &hygiene).map(|attr| Attr { index, ..attr })
165 }); 166 });
166 167
167 let cfg_options = &crate_graph[krate].cfg_options; 168 let cfg_options = &crate_graph[krate].cfg_options;
@@ -293,6 +294,13 @@ impl Attrs {
293 Arc::new(res) 294 Arc::new(res)
294 } 295 }
295 296
297 /// Constructs a map that maps the lowered `Attr`s in this `Attrs` back to its original syntax nodes.
298 ///
299 /// `owner` must be the original owner of the attributes.
300 pub fn source_map(&self, owner: &dyn AttrsOwner) -> AttrSourceMap {
301 AttrSourceMap { attrs: collect_attrs(owner).collect() }
302 }
303
296 pub fn by_key(&self, key: &'static str) -> AttrQuery<'_> { 304 pub fn by_key(&self, key: &'static str) -> AttrQuery<'_> {
297 AttrQuery { attrs: self, key } 305 AttrQuery { attrs: self, key }
298 } 306 }
@@ -365,6 +373,24 @@ fn inner_attributes(
365 Some((attrs, docs)) 373 Some((attrs, docs))
366} 374}
367 375
376pub struct AttrSourceMap {
377 attrs: Vec<Either<ast::Attr, ast::Comment>>,
378}
379
380impl AttrSourceMap {
381 /// Maps the lowered `Attr` back to its original syntax node.
382 ///
383 /// `attr` must come from the `owner` used for AttrSourceMap
384 ///
385 /// Note that the returned syntax node might be a `#[cfg_attr]`, or a doc comment, instead of
386 /// the attribute represented by `Attr`.
387 pub fn source_of(&self, attr: &Attr) -> &Either<ast::Attr, ast::Comment> {
388 self.attrs
389 .get(attr.index as usize)
390 .unwrap_or_else(|| panic!("cannot find `Attr` at index {}", attr.index))
391 }
392}
393
368#[derive(Debug, Clone, PartialEq, Eq)] 394#[derive(Debug, Clone, PartialEq, Eq)]
369pub struct Attr { 395pub struct Attr {
370 index: u32, 396 index: u32,
@@ -448,6 +474,13 @@ impl Attr {
448 _ => None, 474 _ => None,
449 } 475 }
450 } 476 }
477
478 pub fn string_value(&self) -> Option<&SmolStr> {
479 match self.input.as_ref()? {
480 AttrInput::Literal(it) => Some(it),
481 _ => None,
482 }
483 }
451} 484}
452 485
453#[derive(Debug, Clone, Copy)] 486#[derive(Debug, Clone, Copy)]
@@ -475,7 +508,7 @@ impl<'a> AttrQuery<'a> {
475 self.attrs().next().is_some() 508 self.attrs().next().is_some()
476 } 509 }
477 510
478 pub(crate) fn attrs(self) -> impl Iterator<Item = &'a Attr> { 511 pub fn attrs(self) -> impl Iterator<Item = &'a Attr> {
479 let key = self.key; 512 let key = self.key;
480 self.attrs 513 self.attrs
481 .iter() 514 .iter()
diff --git a/crates/ide/src/syntax_highlighting.rs b/crates/ide/src/syntax_highlighting.rs
index 870146d24..ba3447b3a 100644
--- a/crates/ide/src/syntax_highlighting.rs
+++ b/crates/ide/src/syntax_highlighting.rs
@@ -150,7 +150,7 @@ fn traverse(
150 WalkEvent::Enter(it) => it, 150 WalkEvent::Enter(it) => it,
151 WalkEvent::Leave(it) => { 151 WalkEvent::Leave(it) => {
152 if let Some(node) = it.as_node() { 152 if let Some(node) = it.as_node() {
153 inject::doc_comment(hl, node); 153 inject::doc_comment(hl, sema, node);
154 } 154 }
155 continue; 155 continue;
156 } 156 }
diff --git a/crates/ide/src/syntax_highlighting/inject.rs b/crates/ide/src/syntax_highlighting/inject.rs
index 4f825523c..d57ce4027 100644
--- a/crates/ide/src/syntax_highlighting/inject.rs
+++ b/crates/ide/src/syntax_highlighting/inject.rs
@@ -1,8 +1,12 @@
1//! "Recursive" Syntax highlighting for code in doctests and fixtures. 1//! "Recursive" Syntax highlighting for code in doctests and fixtures.
2 2
3use hir::Semantics; 3use either::Either;
4use hir::{HasAttrs, Semantics};
4use ide_db::call_info::ActiveParameter; 5use ide_db::call_info::ActiveParameter;
5use syntax::{ast, AstToken, SyntaxNode, SyntaxToken, TextRange, TextSize}; 6use syntax::{
7 ast::{self, AstNode, AttrsOwner, DocCommentsOwner},
8 match_ast, AstToken, NodeOrToken, SyntaxNode, SyntaxToken, TextRange, TextSize,
9};
6 10
7use crate::{Analysis, HlMod, HlRange, HlTag, RootDatabase}; 11use crate::{Analysis, HlMod, HlRange, HlTag, RootDatabase};
8 12
@@ -81,16 +85,75 @@ const RUSTDOC_FENCE_TOKENS: &[&'static str] = &[
81 "edition2021", 85 "edition2021",
82]; 86];
83 87
88// Basically an owned dyn AttrsOwner without extra Boxing
89struct AttrsOwnerNode {
90 node: SyntaxNode,
91}
92
93impl AttrsOwnerNode {
94 fn new<N: DocCommentsOwner>(node: N) -> Self {
95 AttrsOwnerNode { node: node.syntax().clone() }
96 }
97}
98
99impl AttrsOwner for AttrsOwnerNode {}
100impl AstNode for AttrsOwnerNode {
101 fn can_cast(_: syntax::SyntaxKind) -> bool
102 where
103 Self: Sized,
104 {
105 false
106 }
107 fn cast(_: SyntaxNode) -> Option<Self>
108 where
109 Self: Sized,
110 {
111 None
112 }
113 fn syntax(&self) -> &SyntaxNode {
114 &self.node
115 }
116}
117
118fn doc_attributes<'node>(
119 sema: &Semantics<RootDatabase>,
120 node: &'node SyntaxNode,
121) -> Option<(AttrsOwnerNode, hir::Attrs)> {
122 match_ast! {
123 match node {
124 ast::SourceFile(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
125 ast::Fn(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
126 ast::Struct(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
127 ast::Union(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
128 ast::RecordField(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
129 ast::TupleField(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
130 ast::Enum(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
131 ast::Variant(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
132 ast::Trait(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
133 ast::Module(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
134 ast::Static(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
135 ast::Const(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
136 ast::TypeAlias(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
137 ast::Impl(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
138 ast::MacroRules(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
139 // ast::MacroDef(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))),
140 // ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))),
141 _ => return None
142 }
143 }
144}
145
84/// Injection of syntax highlighting of doctests. 146/// Injection of syntax highlighting of doctests.
85pub(super) fn doc_comment(hl: &mut Highlights, node: &SyntaxNode) { 147pub(super) fn doc_comment(hl: &mut Highlights, sema: &Semantics<RootDatabase>, node: &SyntaxNode) {
86 let doc_comments = node 148 let (owner, attributes) = match doc_attributes(sema, node) {
87 .children_with_tokens() 149 Some(it) => it,
88 .filter_map(|it| it.into_token().and_then(ast::Comment::cast)) 150 None => return,
89 .filter(|it| it.kind().doc.is_some()); 151 };
90 152
91 if !doc_comments.clone().any(|it| it.text().contains(RUSTDOC_FENCE)) { 153 if attributes.docs().map_or(true, |docs| !String::from(docs).contains(RUSTDOC_FENCE)) {
92 return; 154 return;
93 } 155 }
156 let attrs_source_map = attributes.source_map(&owner);
94 157
95 let mut inj = Injector::default(); 158 let mut inj = Injector::default();
96 inj.add_unmapped("fn doctest() {\n"); 159 inj.add_unmapped("fn doctest() {\n");
@@ -101,12 +164,33 @@ pub(super) fn doc_comment(hl: &mut Highlights, node: &SyntaxNode) {
101 // Replace the original, line-spanning comment ranges by new, only comment-prefix 164 // Replace the original, line-spanning comment ranges by new, only comment-prefix
102 // spanning comment ranges. 165 // spanning comment ranges.
103 let mut new_comments = Vec::new(); 166 let mut new_comments = Vec::new();
104 for comment in doc_comments { 167 let mut string;
105 match comment.text().find(RUSTDOC_FENCE) { 168 for attr in attributes.by_key("doc").attrs() {
169 let src = attrs_source_map.source_of(&attr);
170 let (line, range, prefix) = match &src {
171 Either::Left(it) => {
172 string = match find_doc_string_in_attr(attr, it) {
173 Some(it) => it,
174 None => continue,
175 };
176 let text_range = string.syntax().text_range();
177 let text_range = TextRange::new(
178 text_range.start() + TextSize::from(1),
179 text_range.end() - TextSize::from(1),
180 );
181 let text = string.text();
182 (&text[1..text.len() - 1], text_range, "")
183 }
184 Either::Right(comment) => {
185 (comment.text(), comment.syntax().text_range(), comment.prefix())
186 }
187 };
188
189 match line.find(RUSTDOC_FENCE) {
106 Some(idx) => { 190 Some(idx) => {
107 is_codeblock = !is_codeblock; 191 is_codeblock = !is_codeblock;
108 // Check whether code is rust by inspecting fence guards 192 // Check whether code is rust by inspecting fence guards
109 let guards = &comment.text()[idx + RUSTDOC_FENCE.len()..]; 193 let guards = &line[idx + RUSTDOC_FENCE.len()..];
110 let is_rust = 194 let is_rust =
111 guards.split(',').all(|sub| RUSTDOC_FENCE_TOKENS.contains(&sub.trim())); 195 guards.split(',').all(|sub| RUSTDOC_FENCE_TOKENS.contains(&sub.trim()));
112 is_doctest = is_codeblock && is_rust; 196 is_doctest = is_codeblock && is_rust;
@@ -116,10 +200,7 @@ pub(super) fn doc_comment(hl: &mut Highlights, node: &SyntaxNode) {
116 None => (), 200 None => (),
117 } 201 }
118 202
119 let line: &str = comment.text(); 203 let mut pos = TextSize::of(prefix);
120 let range = comment.syntax().text_range();
121
122 let mut pos = TextSize::of(comment.prefix());
123 // whitespace after comment is ignored 204 // whitespace after comment is ignored
124 if let Some(ws) = line[pos.into()..].chars().next().filter(|c| c.is_whitespace()) { 205 if let Some(ws) = line[pos.into()..].chars().next().filter(|c| c.is_whitespace()) {
125 pos += TextSize::of(ws); 206 pos += TextSize::of(ws);
@@ -156,3 +237,27 @@ pub(super) fn doc_comment(hl: &mut Highlights, node: &SyntaxNode) {
156 }); 237 });
157 } 238 }
158} 239}
240
241fn find_doc_string_in_attr(attr: &hir::Attr, it: &ast::Attr) -> Option<ast::String> {
242 match it.literal() {
243 // #[doc = lit]
244 Some(lit) => match lit.kind() {
245 ast::LiteralKind::String(it) => Some(it),
246 _ => None,
247 },
248 // #[cfg_attr(..., doc = "", ...)]
249 None => {
250 // We gotta hunt the string token manually here
251 let text = attr.string_value()?;
252 // FIXME: We just pick the first string literal that has the same text as the doc attribute
253 // This means technically we might highlight the wrong one
254 it.syntax()
255 .descendants_with_tokens()
256 .filter_map(NodeOrToken::into_token)
257 .filter_map(ast::String::cast)
258 .find(|string| {
259 string.text().get(1..string.text().len() - 1).map_or(false, |it| it == text)
260 })
261 }
262 }
263}
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 5e877df88..45817faf9 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html
@@ -105,4 +105,20 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
105 <span class="parenthesis">(</span><span class="punctuation">$</span>expr<span class="colon">:</span>expr<span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">&gt;</span> <span class="brace">{</span> 105 <span class="parenthesis">(</span><span class="punctuation">$</span>expr<span class="colon">:</span>expr<span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">&gt;</span> <span class="brace">{</span>
106 <span class="punctuation">$</span>expr 106 <span class="punctuation">$</span>expr
107 <span class="brace">}</span> 107 <span class="brace">}</span>
108<span class="brace">}</span></code></pre> \ No newline at end of file 108<span class="brace">}</span>
109
110<span class="comment documentation">/// ```rust</span>
111<span class="comment documentation">/// </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="punctuation injected">_</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="function injected">example</span><span class="parenthesis injected">(</span><span class="operator injected">&</span><span class="bracket injected">[</span><span class="numeric_literal injected">1</span><span class="comma injected">,</span><span class="none injected"> </span><span class="numeric_literal injected">2</span><span class="comma injected">,</span><span class="none injected"> </span><span class="numeric_literal injected">3</span><span class="bracket injected">]</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span>
112<span class="comment documentation">/// ```</span>
113<span class="comment documentation">///</span>
114<span class="comment documentation">/// ```</span>
115<span class="comment documentation">/// </span><span class="keyword control injected">loop</span><span class="none injected"> </span><span class="brace injected">{</span><span class="brace injected">}</span>
116<span class="attribute attribute">#</span><span class="attribute attribute">[</span><span class="function attribute">cfg_attr</span><span class="parenthesis attribute">(</span><span class="attribute attribute">not</span><span class="parenthesis attribute">(</span><span class="attribute attribute">feature </span><span class="operator attribute">=</span><span class="attribute attribute"> </span><span class="string_literal attribute">"false"</span><span class="parenthesis attribute">)</span><span class="comma attribute">,</span><span class="attribute attribute"> doc </span><span class="operator attribute">=</span><span class="attribute attribute"> </span><span class="string_literal attribute">"</span><span class="keyword control injected">loop</span><span class="none injected"> </span><span class="brace injected">{</span><span class="brace injected">}</span><span class="string_literal attribute">"</span><span class="parenthesis attribute">)</span><span class="attribute attribute">]</span>
117<span class="attribute attribute">#</span><span class="attribute attribute">[</span><span class="function attribute">doc</span><span class="attribute attribute"> </span><span class="operator attribute">=</span><span class="attribute attribute"> </span><span class="string_literal attribute">"</span><span class="keyword control injected">loop</span><span class="none injected"> </span><span class="brace injected">{</span><span class="brace injected">}</span><span class="string_literal attribute">"</span><span class="attribute attribute">]</span>
118<span class="comment documentation">/// ```</span>
119<span class="comment documentation">///</span>
120<span class="attribute attribute">#</span><span class="attribute attribute">[</span><span class="function attribute">cfg_attr</span><span class="parenthesis attribute">(</span><span class="attribute attribute">feature </span><span class="operator attribute">=</span><span class="attribute attribute"> </span><span class="string_literal attribute">"alloc"</span><span class="comma attribute">,</span><span class="attribute attribute"> doc </span><span class="operator attribute">=</span><span class="attribute attribute"> </span><span class="string_literal attribute">"```rust"</span><span class="parenthesis attribute">)</span><span class="attribute attribute">]</span>
121<span class="attribute attribute">#</span><span class="attribute attribute">[</span><span class="function attribute">cfg_attr</span><span class="parenthesis attribute">(</span><span class="attribute attribute">not</span><span class="parenthesis attribute">(</span><span class="attribute attribute">feature </span><span class="operator attribute">=</span><span class="attribute attribute"> </span><span class="string_literal attribute">"alloc"</span><span class="parenthesis attribute">)</span><span class="comma attribute">,</span><span class="attribute attribute"> doc </span><span class="operator attribute">=</span><span class="attribute attribute"> </span><span class="string_literal attribute">"```ignore"</span><span class="parenthesis attribute">)</span><span class="attribute attribute">]</span>
122<span class="comment documentation">/// </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="punctuation injected">_</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="function injected">example</span><span class="parenthesis injected">(</span><span class="operator injected">&</span><span class="none injected">alloc::</span><span class="macro injected">vec!</span><span class="bracket injected">[</span><span class="numeric_literal injected">1</span><span class="comma injected">,</span><span class="none injected"> </span><span class="numeric_literal injected">2</span><span class="comma injected">,</span><span class="none injected"> </span><span class="numeric_literal injected">3</span><span class="bracket injected">]</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span>
123<span class="comment documentation">/// ```</span>
124<span class="keyword">pub</span> <span class="keyword">fn</span> <span class="function declaration">mix_and_match</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span></code></pre> \ No newline at end of file
diff --git a/crates/ide/src/syntax_highlighting/tests.rs b/crates/ide/src/syntax_highlighting/tests.rs
index 9d0cd1af5..a5ef2d29b 100644
--- a/crates/ide/src/syntax_highlighting/tests.rs
+++ b/crates/ide/src/syntax_highlighting/tests.rs
@@ -541,6 +541,22 @@ macro_rules! noop {
541 $expr 541 $expr
542 } 542 }
543} 543}
544
545/// ```rust
546/// let _ = example(&[1, 2, 3]);
547/// ```
548///
549/// ```
550/// loop {}
551#[cfg_attr(not(feature = "false"), doc = "loop {}")]
552#[doc = "loop {}"]
553/// ```
554///
555#[cfg_attr(feature = "alloc", doc = "```rust")]
556#[cfg_attr(not(feature = "alloc"), doc = "```ignore")]
557/// let _ = example(&alloc::vec![1, 2, 3]);
558/// ```
559pub fn mix_and_match() {}
544"# 560"#
545 .trim(), 561 .trim(),
546 expect_file!["./test_data/highlight_doctest.html"], 562 expect_file!["./test_data/highlight_doctest.html"],
diff --git a/crates/syntax/src/ast/traits.rs b/crates/syntax/src/ast/traits.rs
index 13a769d51..96d4cc997 100644
--- a/crates/syntax/src/ast/traits.rs
+++ b/crates/syntax/src/ast/traits.rs
@@ -72,7 +72,7 @@ pub trait AttrsOwner: AstNode {
72 } 72 }
73} 73}
74 74
75pub trait DocCommentsOwner: AstNode { 75pub trait DocCommentsOwner: AttrsOwner {
76 fn doc_comments(&self) -> CommentIter { 76 fn doc_comments(&self) -> CommentIter {
77 CommentIter { iter: self.syntax().children_with_tokens() } 77 CommentIter { iter: self.syntax().children_with_tokens() }
78 } 78 }