aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorLeander Tentrup <[email protected]>2020-04-28 10:01:51 +0100
committerLeander Tentrup <[email protected]>2020-06-08 13:18:16 +0100
commit4a2efb2f42494f62891ac801e0a27d246bd36684 (patch)
tree42beb8f761b80edaf57fdb9c0a76a2818f7bb862 /crates
parentd8552d114c3b2cedbc485ebe26d7b784c0845bd8 (diff)
Implement syntax highlighting for doctests
Diffstat (limited to 'crates')
-rw-r--r--crates/ra_ide/src/snapshots/highlight_doctest.html70
-rw-r--r--crates/ra_ide/src/syntax_highlighting.rs126
-rw-r--r--crates/ra_ide/src/syntax_highlighting/injection.rs168
-rw-r--r--crates/ra_ide/src/syntax_highlighting/tests.rs50
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>
3body { margin: 0; }
4pre { 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>() -&gt; <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>) -&gt; <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 @@
1mod tags; 1mod tags;
2mod html; 2mod html;
3mod injection;
3#[cfg(test)] 4#[cfg(test)]
4mod tests; 5mod tests;
5 6
@@ -10,14 +11,14 @@ use ra_ide_db::{
10}; 11};
11use ra_prof::profile; 12use ra_prof::profile;
12use ra_syntax::{ 13use 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};
18use rustc_hash::FxHashMap; 19use rustc_hash::FxHashMap;
19 20
20use crate::{call_info::ActiveParameter, Analysis, FileId}; 21use crate::FileId;
21 22
22use ast::FormatSpecifier; 23use ast::FormatSpecifier;
23pub(crate) use html::highlight_as_html; 24pub(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
543fn 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
3use std::{collections::BTreeMap, convert::TryFrom};
4
5use ast::{HasQuotes, HasStringValue};
6use hir::Semantics;
7use ra_syntax::{ast, AstToken, SyntaxNode, SyntaxToken, TextRange, TextSize};
8use stdx::SepBy;
9
10use crate::{call_info::ActiveParameter, Analysis, HighlightTag, HighlightedRange, RootDatabase};
11
12use super::HighlightedRangeStack;
13
14pub(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
54type 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.
61pub(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.
131pub(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]
289fn test_highlight_doctest() {
290 check_highlighting(
291 r#"
292impl 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}