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