aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide/src/syntax_highlighting
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/ra_ide/src/syntax_highlighting
parentd8552d114c3b2cedbc485ebe26d7b784c0845bd8 (diff)
Implement syntax highlighting for doctests
Diffstat (limited to 'crates/ra_ide/src/syntax_highlighting')
-rw-r--r--crates/ra_ide/src/syntax_highlighting/injection.rs168
-rw-r--r--crates/ra_ide/src/syntax_highlighting/tests.rs50
2 files changed, 218 insertions, 0 deletions
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}