]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/syntax_highlighting/injection.rs
13dde1dc44659db5db0dd9e936558c36a12bbed6
[rust.git] / crates / ide / src / syntax_highlighting / injection.rs
1 //! Syntax highlighting injections such as highlighting of documentation tests.
2
3 use std::convert::TryFrom;
4
5 use hir::Semantics;
6 use ide_db::call_info::ActiveParameter;
7 use itertools::Itertools;
8 use syntax::{ast, AstToken, SyntaxNode, SyntaxToken, TextRange, TextSize};
9
10 use crate::{Analysis, HlMod, HlRange, HlTag, RootDatabase};
11
12 use super::{highlights::Highlights, injector::Injector};
13
14 pub(super) fn highlight_injection(
15     acc: &mut Highlights,
16     sema: &Semantics<RootDatabase>,
17     literal: ast::String,
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 marker_info = MarkerInfo::new(&*value);
26     let (analysis, tmp_file_id) = Analysis::from_single_file(marker_info.cleaned_text.clone());
27
28     if let Some(range) = literal.open_quote_text_range() {
29         acc.add(HlRange { range, highlight: HlTag::StringLiteral.into(), binding_hash: None })
30     }
31
32     for mut h in analysis.highlight(tmp_file_id).unwrap() {
33         let range = marker_info.map_range_up(h.range);
34         if let Some(range) = literal.map_range_up(range) {
35             h.range = range;
36             acc.add(h);
37         }
38     }
39
40     if let Some(range) = literal.close_quote_text_range() {
41         acc.add(HlRange { range, highlight: HlTag::StringLiteral.into(), binding_hash: None })
42     }
43
44     Some(())
45 }
46
47 /// Data to remove `$0` from string and map ranges
48 #[derive(Default, Debug)]
49 struct MarkerInfo {
50     cleaned_text: String,
51     markers: Vec<TextRange>,
52 }
53
54 impl MarkerInfo {
55     fn new(mut text: &str) -> Self {
56         let marker = "$0";
57
58         let mut res = MarkerInfo::default();
59         let mut offset: TextSize = 0.into();
60         while !text.is_empty() {
61             let idx = text.find(marker).unwrap_or(text.len());
62             let (chunk, next) = text.split_at(idx);
63             text = next;
64             res.cleaned_text.push_str(chunk);
65             offset += TextSize::of(chunk);
66
67             if let Some(next) = text.strip_prefix(marker) {
68                 text = next;
69
70                 let marker_len = TextSize::of(marker);
71                 res.markers.push(TextRange::at(offset, marker_len));
72                 offset += marker_len;
73             }
74         }
75         res
76     }
77     fn map_range_up(&self, range: TextRange) -> TextRange {
78         TextRange::new(
79             self.map_offset_up(range.start(), true),
80             self.map_offset_up(range.end(), false),
81         )
82     }
83     fn map_offset_up(&self, mut offset: TextSize, start: bool) -> TextSize {
84         for r in &self.markers {
85             if r.start() < offset || (start && r.start() == offset) {
86                 offset += r.len()
87             }
88         }
89         offset
90     }
91 }
92
93 const RUSTDOC_FENCE: &'static str = "```";
94 const RUSTDOC_FENCE_TOKENS: &[&'static str] = &[
95     "",
96     "rust",
97     "should_panic",
98     "ignore",
99     "no_run",
100     "compile_fail",
101     "edition2015",
102     "edition2018",
103     "edition2021",
104 ];
105
106 /// Extracts Rust code from documentation comments as well as a mapping from
107 /// the extracted source code back to the original source ranges.
108 /// Lastly, a vector of new comment highlight ranges (spanning only the
109 /// comment prefix) is returned which is used in the syntax highlighting
110 /// injection to replace the previous (line-spanning) comment ranges.
111 pub(super) fn extract_doc_comments(node: &SyntaxNode) -> Option<(Vec<HlRange>, Injector)> {
112     let mut inj = Injector::default();
113     // wrap the doctest into function body to get correct syntax highlighting
114     let prefix = "fn doctest() {\n";
115     let suffix = "}\n";
116
117     let mut line_start = TextSize::of(prefix);
118     let mut is_codeblock = false;
119     let mut is_doctest = false;
120     // Replace the original, line-spanning comment ranges by new, only comment-prefix
121     // spanning comment ranges.
122     let mut new_comments = Vec::new();
123
124     inj.add_unmapped(prefix);
125     let doctest = node
126         .children_with_tokens()
127         .filter_map(|el| el.into_token().and_then(ast::Comment::cast))
128         .filter(|comment| comment.kind().doc.is_some())
129         .filter(|comment| {
130             if let Some(idx) = comment.text().find(RUSTDOC_FENCE) {
131                 is_codeblock = !is_codeblock;
132                 // Check whether code is rust by inspecting fence guards
133                 let guards = &comment.text()[idx + RUSTDOC_FENCE.len()..];
134                 let is_rust =
135                     guards.split(',').all(|sub| RUSTDOC_FENCE_TOKENS.contains(&sub.trim()));
136                 is_doctest = is_codeblock && is_rust;
137                 false
138             } else {
139                 is_doctest
140             }
141         })
142         .map(|comment| {
143             let prefix_len = comment.prefix().len();
144             let line: &str = comment.text().as_str();
145             let range = comment.syntax().text_range();
146
147             // whitespace after comment is ignored
148             let pos = if let Some(ws) = line.chars().nth(prefix_len).filter(|c| c.is_whitespace()) {
149                 prefix_len + ws.len_utf8()
150             } else {
151                 prefix_len
152             };
153
154             // lines marked with `#` should be ignored in output, we skip the `#` char
155             let pos = if let Some(ws) = line.chars().nth(pos).filter(|&c| c == '#') {
156                 pos + ws.len_utf8()
157             } else {
158                 pos
159             };
160
161             new_comments.push(HlRange {
162                 range: TextRange::new(
163                     range.start(),
164                     range.start() + TextSize::try_from(pos).unwrap(),
165                 ),
166                 highlight: HlTag::Comment | HlMod::Documentation,
167                 binding_hash: None,
168             });
169             line_start += range.len() - TextSize::try_from(pos).unwrap();
170             line_start += TextSize::of("\n");
171
172             inj.add(
173                 &line[pos..],
174                 TextRange::new(range.start() + TextSize::try_from(pos).unwrap(), range.end()),
175             );
176             inj.add_unmapped("\n");
177             line[pos..].to_owned()
178         })
179         .join("\n");
180     inj.add_unmapped(suffix);
181
182     if doctest.is_empty() {
183         return None;
184     }
185
186     Some((new_comments, inj))
187 }
188
189 /// Injection of syntax highlighting of doctests.
190 pub(super) fn highlight_doc_comment(
191     new_comments: Vec<HlRange>,
192     inj: Injector,
193     stack: &mut Highlights,
194 ) {
195     let (analysis, tmp_file_id) = Analysis::from_single_file(inj.text().to_string());
196     for comment in new_comments {
197         stack.add(comment);
198     }
199
200     for h in analysis.with_db(|db| super::highlight(db, tmp_file_id, None, true)).unwrap() {
201         for r in inj.map_range_up(h.range) {
202             stack.add(HlRange {
203                 range: r,
204                 highlight: h.highlight | HlMod::Injected,
205                 binding_hash: h.binding_hash,
206             });
207         }
208     }
209 }