diff options
Diffstat (limited to 'crates')
-rw-r--r-- | crates/ra_ide/src/snapshots/highlight_doctest.html | 70 | ||||
-rw-r--r-- | crates/ra_ide/src/syntax_highlighting.rs | 126 | ||||
-rw-r--r-- | crates/ra_ide/src/syntax_highlighting/injection.rs | 168 | ||||
-rw-r--r-- | crates/ra_ide/src/syntax_highlighting/tests.rs | 50 |
4 files changed, 368 insertions, 46 deletions
diff --git a/crates/ra_ide/src/snapshots/highlight_doctest.html b/crates/ra_ide/src/snapshots/highlight_doctest.html new file mode 100644 index 000000000..2f2d8c900 --- /dev/null +++ b/crates/ra_ide/src/snapshots/highlight_doctest.html | |||
@@ -0,0 +1,70 @@ | |||
1 | |||
2 | <style> | ||
3 | body { margin: 0; } | ||
4 | pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } | ||
5 | |||
6 | .lifetime { color: #DFAF8F; font-style: italic; } | ||
7 | .comment { color: #7F9F7F; } | ||
8 | .struct, .enum { color: #7CB8BB; } | ||
9 | .enum_variant { color: #BDE0F3; } | ||
10 | .string_literal { color: #CC9393; } | ||
11 | .field { color: #94BFF3; } | ||
12 | .function { color: #93E0E3; } | ||
13 | .operator.unsafe { color: #E28C14; } | ||
14 | .parameter { color: #94BFF3; } | ||
15 | .text { color: #DCDCCC; } | ||
16 | .type { color: #7CB8BB; } | ||
17 | .builtin_type { color: #8CD0D3; } | ||
18 | .type_param { color: #DFAF8F; } | ||
19 | .attribute { color: #94BFF3; } | ||
20 | .numeric_literal { color: #BFEBBF; } | ||
21 | .bool_literal { color: #BFE6EB; } | ||
22 | .macro { color: #94BFF3; } | ||
23 | .module { color: #AFD8AF; } | ||
24 | .variable { color: #DCDCCC; } | ||
25 | .format_specifier { color: #CC696B; } | ||
26 | .mutable { text-decoration: underline; } | ||
27 | |||
28 | .keyword { color: #F0DFAF; font-weight: bold; } | ||
29 | .keyword.unsafe { color: #BC8383; font-weight: bold; } | ||
30 | .control { font-style: italic; } | ||
31 | </style> | ||
32 | <pre><code><span class="keyword">impl</span> <span class="unresolved_reference">Foo</span> { | ||
33 | <span class="comment">/// Constructs a new `Foo`.</span> | ||
34 | <span class="comment">///</span> | ||
35 | <span class="comment">/// # Examples</span> | ||
36 | <span class="comment">///</span> | ||
37 | <span class="comment">/// ```</span> | ||
38 | <span class="comment">/// #</span> <span class="attribute">#![</span><span class="function attribute">allow</span><span class="attribute">(unused_mut)]</span> | ||
39 | <span class="comment">/// </span><span class="keyword">let</span> <span class="keyword">mut</span> <span class="variable declaration mutable">foo</span>: <span class="unresolved_reference">Foo</span> = <span class="unresolved_reference">Foo</span>::<span class="unresolved_reference">new</span>(); | ||
40 | <span class="comment">/// ```</span> | ||
41 | <span class="keyword">pub</span> <span class="keyword">const</span> <span class="keyword">fn</span> <span class="function declaration">new</span>() -> <span class="unresolved_reference">Foo</span> { | ||
42 | <span class="unresolved_reference">Foo</span> { } | ||
43 | } | ||
44 | |||
45 | <span class="comment">/// `bar` method on `Foo`.</span> | ||
46 | <span class="comment">///</span> | ||
47 | <span class="comment">/// # Examples</span> | ||
48 | <span class="comment">///</span> | ||
49 | <span class="comment">/// ```</span> | ||
50 | <span class="comment">/// </span><span class="keyword">let</span> <span class="variable declaration">foo</span> = <span class="unresolved_reference">Foo</span>::<span class="unresolved_reference">new</span>(); | ||
51 | <span class="comment">///</span> | ||
52 | <span class="comment">/// </span><span class="comment">// calls bar on foo</span> | ||
53 | <span class="comment">/// </span><span class="macro">assert!</span>(foo.bar()); | ||
54 | <span class="comment">///</span> | ||
55 | <span class="comment">/// </span><span class="comment">/* multi-line | ||
56 | </span><span class="comment">/// </span><span class="comment"> comment */</span> | ||
57 | <span class="comment">///</span> | ||
58 | <span class="comment">/// </span><span class="keyword">let</span> <span class="variable declaration">multi_line_string</span> = <span class="string_literal">"Foo | ||
59 | </span><span class="comment">/// </span><span class="string_literal"> bar | ||
60 | </span><span class="comment">/// </span><span class="string_literal"> "</span>; | ||
61 | <span class="comment">///</span> | ||
62 | <span class="comment">/// ```</span> | ||
63 | <span class="comment">///</span> | ||
64 | <span class="comment">/// ```</span> | ||
65 | <span class="comment">/// </span><span class="keyword">let</span> <span class="variable declaration">foobar</span> = <span class="unresolved_reference">Foo</span>::<span class="unresolved_reference">new</span>().<span class="unresolved_reference">bar</span>(); | ||
66 | <span class="comment">/// ```</span> | ||
67 | <span class="keyword">pub</span> <span class="keyword">fn</span> <span class="function declaration">foo</span>(&<span class="self_keyword">self</span>) -> <span class="builtin_type">bool</span> { | ||
68 | <span class="bool_literal">true</span> | ||
69 | } | ||
70 | }</code></pre> \ No newline at end of file | ||
diff --git a/crates/ra_ide/src/syntax_highlighting.rs b/crates/ra_ide/src/syntax_highlighting.rs index 19ecd54d6..6903403b2 100644 --- a/crates/ra_ide/src/syntax_highlighting.rs +++ b/crates/ra_ide/src/syntax_highlighting.rs | |||
@@ -1,5 +1,6 @@ | |||
1 | mod tags; | 1 | mod tags; |
2 | mod html; | 2 | mod html; |
3 | mod injection; | ||
3 | #[cfg(test)] | 4 | #[cfg(test)] |
4 | mod tests; | 5 | mod tests; |
5 | 6 | ||
@@ -10,14 +11,14 @@ use ra_ide_db::{ | |||
10 | }; | 11 | }; |
11 | use ra_prof::profile; | 12 | use ra_prof::profile; |
12 | use ra_syntax::{ | 13 | use ra_syntax::{ |
13 | ast::{self, HasFormatSpecifier, HasQuotes, HasStringValue}, | 14 | ast::{self, HasFormatSpecifier}, |
14 | AstNode, AstToken, Direction, NodeOrToken, SyntaxElement, | 15 | AstNode, AstToken, Direction, NodeOrToken, SyntaxElement, |
15 | SyntaxKind::*, | 16 | SyntaxKind::*, |
16 | SyntaxToken, TextRange, WalkEvent, T, | 17 | TextRange, WalkEvent, T, |
17 | }; | 18 | }; |
18 | use rustc_hash::FxHashMap; | 19 | use rustc_hash::FxHashMap; |
19 | 20 | ||
20 | use crate::{call_info::ActiveParameter, Analysis, FileId}; | 21 | use crate::FileId; |
21 | 22 | ||
22 | use ast::FormatSpecifier; | 23 | use ast::FormatSpecifier; |
23 | pub(crate) use html::highlight_as_html; | 24 | pub(crate) use html::highlight_as_html; |
@@ -123,6 +124,23 @@ pub(crate) fn highlight( | |||
123 | _ => (), | 124 | _ => (), |
124 | } | 125 | } |
125 | 126 | ||
127 | // Check for Rust code in documentation | ||
128 | match &event { | ||
129 | WalkEvent::Leave(NodeOrToken::Node(node)) => { | ||
130 | if let Some((doctest, range_mapping, new_comments)) = | ||
131 | injection::extract_doc_comments(node) | ||
132 | { | ||
133 | injection::highlight_doc_comment( | ||
134 | doctest, | ||
135 | range_mapping, | ||
136 | new_comments, | ||
137 | &mut stack, | ||
138 | ); | ||
139 | } | ||
140 | } | ||
141 | _ => (), | ||
142 | } | ||
143 | |||
126 | let element = match event { | 144 | let element = match event { |
127 | WalkEvent::Enter(it) => it, | 145 | WalkEvent::Enter(it) => it, |
128 | WalkEvent::Leave(_) => continue, | 146 | WalkEvent::Leave(_) => continue, |
@@ -173,7 +191,7 @@ pub(crate) fn highlight( | |||
173 | 191 | ||
174 | if let Some(token) = element.as_token().cloned().and_then(ast::RawString::cast) { | 192 | if let Some(token) = element.as_token().cloned().and_then(ast::RawString::cast) { |
175 | let expanded = element_to_highlight.as_token().unwrap().clone(); | 193 | let expanded = element_to_highlight.as_token().unwrap().clone(); |
176 | if highlight_injection(&mut stack, &sema, token, expanded).is_some() { | 194 | if injection::highlight_injection(&mut stack, &sema, token, expanded).is_some() { |
177 | continue; | 195 | continue; |
178 | } | 196 | } |
179 | } | 197 | } |
@@ -259,9 +277,8 @@ impl HighlightedRangeStack { | |||
259 | let mut parent = prev.pop().unwrap(); | 277 | let mut parent = prev.pop().unwrap(); |
260 | for ele in children { | 278 | for ele in children { |
261 | assert!(parent.range.contains_range(ele.range)); | 279 | assert!(parent.range.contains_range(ele.range)); |
262 | let mut cloned = parent.clone(); | 280 | |
263 | parent.range = TextRange::new(parent.range.start(), ele.range.start()); | 281 | let cloned = Self::intersect(&mut parent, &ele); |
264 | cloned.range = TextRange::new(ele.range.end(), cloned.range.end()); | ||
265 | if !parent.range.is_empty() { | 282 | if !parent.range.is_empty() { |
266 | prev.push(parent); | 283 | prev.push(parent); |
267 | } | 284 | } |
@@ -274,6 +291,62 @@ impl HighlightedRangeStack { | |||
274 | } | 291 | } |
275 | } | 292 | } |
276 | 293 | ||
294 | /// Intersects the `HighlightedRange` `parent` with `child`. | ||
295 | /// `parent` is mutated in place, becoming the range before `child`. | ||
296 | /// Returns the range (of the same type as `parent`) *after* `child`. | ||
297 | fn intersect(parent: &mut HighlightedRange, child: &HighlightedRange) -> HighlightedRange { | ||
298 | assert!(parent.range.contains_range(child.range)); | ||
299 | |||
300 | let mut cloned = parent.clone(); | ||
301 | parent.range = TextRange::new(parent.range.start(), child.range.start()); | ||
302 | cloned.range = TextRange::new(child.range.end(), cloned.range.end()); | ||
303 | |||
304 | cloned | ||
305 | } | ||
306 | |||
307 | /// Similar to `pop`, but can modify arbitrary prior ranges (where `pop`) | ||
308 | /// can only modify the last range currently on the stack. | ||
309 | /// Can be used to do injections that span multiple ranges, like the | ||
310 | /// doctest injection below. | ||
311 | /// If `delete` is set to true, the parent range is deleted instead of | ||
312 | /// intersected. | ||
313 | /// | ||
314 | /// Note that `pop` can be simulated by `pop_and_inject(false)` but the | ||
315 | /// latter is computationally more expensive. | ||
316 | fn pop_and_inject(&mut self, delete: bool) { | ||
317 | let mut children = self.stack.pop().unwrap(); | ||
318 | let prev = self.stack.last_mut().unwrap(); | ||
319 | children.sort_by_key(|range| range.range.start()); | ||
320 | prev.sort_by_key(|range| range.range.start()); | ||
321 | |||
322 | for child in children { | ||
323 | if let Some(idx) = | ||
324 | prev.iter().position(|parent| parent.range.contains_range(child.range)) | ||
325 | { | ||
326 | let cloned = Self::intersect(&mut prev[idx], &child); | ||
327 | let insert_idx = if delete || prev[idx].range.is_empty() { | ||
328 | prev.remove(idx); | ||
329 | idx | ||
330 | } else { | ||
331 | idx + 1 | ||
332 | }; | ||
333 | prev.insert(insert_idx, child); | ||
334 | if !delete && !cloned.range.is_empty() { | ||
335 | prev.insert(insert_idx + 1, cloned); | ||
336 | } | ||
337 | } else if let Some(_idx) = | ||
338 | prev.iter().position(|parent| parent.range.contains(child.range.start())) | ||
339 | { | ||
340 | unreachable!("child range should be completely contained in parent range"); | ||
341 | } else { | ||
342 | let idx = prev | ||
343 | .binary_search_by_key(&child.range.start(), |range| range.range.start()) | ||
344 | .unwrap_or_else(|x| x); | ||
345 | prev.insert(idx, child); | ||
346 | } | ||
347 | } | ||
348 | } | ||
349 | |||
277 | fn add(&mut self, range: HighlightedRange) { | 350 | fn add(&mut self, range: HighlightedRange) { |
278 | self.stack | 351 | self.stack |
279 | .last_mut() | 352 | .last_mut() |
@@ -539,42 +612,3 @@ fn highlight_name_by_syntax(name: ast::Name) -> Highlight { | |||
539 | 612 | ||
540 | tag.into() | 613 | tag.into() |
541 | } | 614 | } |
542 | |||
543 | fn highlight_injection( | ||
544 | acc: &mut HighlightedRangeStack, | ||
545 | sema: &Semantics<RootDatabase>, | ||
546 | literal: ast::RawString, | ||
547 | expanded: SyntaxToken, | ||
548 | ) -> Option<()> { | ||
549 | let active_parameter = ActiveParameter::at_token(&sema, expanded)?; | ||
550 | if !active_parameter.name.starts_with("ra_fixture") { | ||
551 | return None; | ||
552 | } | ||
553 | let value = literal.value()?; | ||
554 | let (analysis, tmp_file_id) = Analysis::from_single_file(value); | ||
555 | |||
556 | if let Some(range) = literal.open_quote_text_range() { | ||
557 | acc.add(HighlightedRange { | ||
558 | range, | ||
559 | highlight: HighlightTag::StringLiteral.into(), | ||
560 | binding_hash: None, | ||
561 | }) | ||
562 | } | ||
563 | |||
564 | for mut h in analysis.highlight(tmp_file_id).unwrap() { | ||
565 | if let Some(r) = literal.map_range_up(h.range) { | ||
566 | h.range = r; | ||
567 | acc.add(h) | ||
568 | } | ||
569 | } | ||
570 | |||
571 | if let Some(range) = literal.close_quote_text_range() { | ||
572 | acc.add(HighlightedRange { | ||
573 | range, | ||
574 | highlight: HighlightTag::StringLiteral.into(), | ||
575 | binding_hash: None, | ||
576 | }) | ||
577 | } | ||
578 | |||
579 | Some(()) | ||
580 | } | ||
diff --git a/crates/ra_ide/src/syntax_highlighting/injection.rs b/crates/ra_ide/src/syntax_highlighting/injection.rs new file mode 100644 index 000000000..3575a0fc6 --- /dev/null +++ b/crates/ra_ide/src/syntax_highlighting/injection.rs | |||
@@ -0,0 +1,168 @@ | |||
1 | //! Syntax highlighting injections such as highlighting of documentation tests. | ||
2 | |||
3 | use std::{collections::BTreeMap, convert::TryFrom}; | ||
4 | |||
5 | use ast::{HasQuotes, HasStringValue}; | ||
6 | use hir::Semantics; | ||
7 | use ra_syntax::{ast, AstToken, SyntaxNode, SyntaxToken, TextRange, TextSize}; | ||
8 | use stdx::SepBy; | ||
9 | |||
10 | use crate::{call_info::ActiveParameter, Analysis, HighlightTag, HighlightedRange, RootDatabase}; | ||
11 | |||
12 | use super::HighlightedRangeStack; | ||
13 | |||
14 | pub(super) fn highlight_injection( | ||
15 | acc: &mut HighlightedRangeStack, | ||
16 | sema: &Semantics<RootDatabase>, | ||
17 | literal: ast::RawString, | ||
18 | expanded: SyntaxToken, | ||
19 | ) -> Option<()> { | ||
20 | let active_parameter = ActiveParameter::at_token(&sema, expanded)?; | ||
21 | if !active_parameter.name.starts_with("ra_fixture") { | ||
22 | return None; | ||
23 | } | ||
24 | let value = literal.value()?; | ||
25 | let (analysis, tmp_file_id) = Analysis::from_single_file(value); | ||
26 | |||
27 | if let Some(range) = literal.open_quote_text_range() { | ||
28 | acc.add(HighlightedRange { | ||
29 | range, | ||
30 | highlight: HighlightTag::StringLiteral.into(), | ||
31 | binding_hash: None, | ||
32 | }) | ||
33 | } | ||
34 | |||
35 | for mut h in analysis.highlight(tmp_file_id).unwrap() { | ||
36 | if let Some(r) = literal.map_range_up(h.range) { | ||
37 | h.range = r; | ||
38 | acc.add(h) | ||
39 | } | ||
40 | } | ||
41 | |||
42 | if let Some(range) = literal.close_quote_text_range() { | ||
43 | acc.add(HighlightedRange { | ||
44 | range, | ||
45 | highlight: HighlightTag::StringLiteral.into(), | ||
46 | binding_hash: None, | ||
47 | }) | ||
48 | } | ||
49 | |||
50 | Some(()) | ||
51 | } | ||
52 | |||
53 | /// Mapping from extracted documentation code to original code | ||
54 | type RangesMap = BTreeMap<TextSize, TextSize>; | ||
55 | |||
56 | /// Extracts Rust code from documentation comments as well as a mapping from | ||
57 | /// the extracted source code back to the original source ranges. | ||
58 | /// Lastly, a vector of new comment highlight ranges (spanning only the | ||
59 | /// comment prefix) is returned which is used in the syntax highlighting | ||
60 | /// injection to replace the previous (line-spanning) comment ranges. | ||
61 | pub(super) fn extract_doc_comments( | ||
62 | node: &SyntaxNode, | ||
63 | ) -> Option<(String, RangesMap, Vec<HighlightedRange>)> { | ||
64 | // wrap the doctest into function body to get correct syntax highlighting | ||
65 | let prefix = "fn doctest() {\n"; | ||
66 | let suffix = "}\n"; | ||
67 | // Mapping from extracted documentation code to original code | ||
68 | let mut range_mapping: RangesMap = BTreeMap::new(); | ||
69 | let mut line_start = TextSize::try_from(prefix.len()).unwrap(); | ||
70 | let mut is_doctest = false; | ||
71 | // Replace the original, line-spanning comment ranges by new, only comment-prefix | ||
72 | // spanning comment ranges. | ||
73 | let mut new_comments = Vec::new(); | ||
74 | let doctest = node | ||
75 | .children_with_tokens() | ||
76 | .filter_map(|el| el.into_token().and_then(ast::Comment::cast)) | ||
77 | .filter(|comment| comment.kind().doc.is_some()) | ||
78 | .filter(|comment| { | ||
79 | if comment.text().contains("```") { | ||
80 | is_doctest = !is_doctest; | ||
81 | false | ||
82 | } else { | ||
83 | is_doctest | ||
84 | } | ||
85 | }) | ||
86 | .map(|comment| { | ||
87 | let prefix_len = comment.prefix().len(); | ||
88 | let line: &str = comment.text().as_str(); | ||
89 | let range = comment.syntax().text_range(); | ||
90 | |||
91 | // whitespace after comment is ignored | ||
92 | let pos = if let Some(ws) = line.chars().nth(prefix_len).filter(|c| c.is_whitespace()) { | ||
93 | prefix_len + ws.len_utf8() | ||
94 | } else { | ||
95 | prefix_len | ||
96 | }; | ||
97 | |||
98 | // lines marked with `#` should be ignored in output, we skip the `#` char | ||
99 | let pos = if let Some(ws) = line.chars().nth(pos).filter(|&c| c == '#') { | ||
100 | pos + ws.len_utf8() | ||
101 | } else { | ||
102 | pos | ||
103 | }; | ||
104 | |||
105 | range_mapping.insert(line_start, range.start() + TextSize::try_from(pos).unwrap()); | ||
106 | new_comments.push(HighlightedRange { | ||
107 | range: TextRange::new( | ||
108 | range.start(), | ||
109 | range.start() + TextSize::try_from(pos).unwrap(), | ||
110 | ), | ||
111 | highlight: HighlightTag::Comment.into(), | ||
112 | binding_hash: None, | ||
113 | }); | ||
114 | line_start += range.len() - TextSize::try_from(pos).unwrap(); | ||
115 | line_start += TextSize::try_from('\n'.len_utf8()).unwrap(); | ||
116 | |||
117 | line[pos..].to_owned() | ||
118 | }) | ||
119 | .sep_by("\n") | ||
120 | .to_string(); | ||
121 | |||
122 | if doctest.is_empty() { | ||
123 | return None; | ||
124 | } | ||
125 | |||
126 | let doctest = format!("{}{}{}", prefix, doctest, suffix); | ||
127 | Some((doctest, range_mapping, new_comments)) | ||
128 | } | ||
129 | |||
130 | /// Injection of syntax highlighting of doctests. | ||
131 | pub(super) fn highlight_doc_comment( | ||
132 | text: String, | ||
133 | range_mapping: RangesMap, | ||
134 | new_comments: Vec<HighlightedRange>, | ||
135 | stack: &mut HighlightedRangeStack, | ||
136 | ) { | ||
137 | let (analysis, tmp_file_id) = Analysis::from_single_file(text); | ||
138 | |||
139 | stack.push(); | ||
140 | for mut h in analysis.highlight(tmp_file_id).unwrap() { | ||
141 | // Determine start offset and end offset in case of multi-line ranges | ||
142 | let mut start_offset = None; | ||
143 | let mut end_offset = None; | ||
144 | for (line_start, orig_line_start) in range_mapping.range(..h.range.end()).rev() { | ||
145 | if line_start <= &h.range.start() { | ||
146 | start_offset.get_or_insert(orig_line_start - line_start); | ||
147 | break; | ||
148 | } else { | ||
149 | end_offset.get_or_insert(orig_line_start - line_start); | ||
150 | } | ||
151 | } | ||
152 | if let Some(start_offset) = start_offset { | ||
153 | h.range = TextRange::new( | ||
154 | h.range.start() + start_offset, | ||
155 | h.range.end() + end_offset.unwrap_or(start_offset), | ||
156 | ); | ||
157 | stack.add(h); | ||
158 | } | ||
159 | } | ||
160 | |||
161 | // Inject the comment prefix highlight ranges | ||
162 | stack.push(); | ||
163 | for comment in new_comments { | ||
164 | stack.add(comment); | ||
165 | } | ||
166 | stack.pop_and_inject(false); | ||
167 | stack.pop_and_inject(true); | ||
168 | } | ||
diff --git a/crates/ra_ide/src/syntax_highlighting/tests.rs b/crates/ra_ide/src/syntax_highlighting/tests.rs index 5e42c5b55..ba345d90a 100644 --- a/crates/ra_ide/src/syntax_highlighting/tests.rs +++ b/crates/ra_ide/src/syntax_highlighting/tests.rs | |||
@@ -284,3 +284,53 @@ fn main() { | |||
284 | false, | 284 | false, |
285 | ); | 285 | ); |
286 | } | 286 | } |
287 | |||
288 | #[test] | ||
289 | fn test_highlight_doctest() { | ||
290 | check_highlighting( | ||
291 | r#" | ||
292 | impl Foo { | ||
293 | /// Constructs a new `Foo`. | ||
294 | /// | ||
295 | /// # Examples | ||
296 | /// | ||
297 | /// ``` | ||
298 | /// # #![allow(unused_mut)] | ||
299 | /// let mut foo: Foo = Foo::new(); | ||
300 | /// ``` | ||
301 | pub const fn new() -> Foo { | ||
302 | Foo { } | ||
303 | } | ||
304 | |||
305 | /// `bar` method on `Foo`. | ||
306 | /// | ||
307 | /// # Examples | ||
308 | /// | ||
309 | /// ``` | ||
310 | /// let foo = Foo::new(); | ||
311 | /// | ||
312 | /// // calls bar on foo | ||
313 | /// assert!(foo.bar()); | ||
314 | /// | ||
315 | /// /* multi-line | ||
316 | /// comment */ | ||
317 | /// | ||
318 | /// let multi_line_string = "Foo | ||
319 | /// bar | ||
320 | /// "; | ||
321 | /// | ||
322 | /// ``` | ||
323 | /// | ||
324 | /// ``` | ||
325 | /// let foobar = Foo::new().bar(); | ||
326 | /// ``` | ||
327 | pub fn foo(&self) -> bool { | ||
328 | true | ||
329 | } | ||
330 | } | ||
331 | "# | ||
332 | .trim(), | ||
333 | "crates/ra_ide/src/snapshots/highlight_doctest.html", | ||
334 | false, | ||
335 | ) | ||
336 | } | ||