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