]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/syntax_highlighting/inject.rs
Remove quadratic attr source lookup
[rust.git] / crates / ide / src / syntax_highlighting / inject.rs
1 //! "Recursive" Syntax highlighting for code in doctests and fixtures.
2
3 use either::Either;
4 use hir::{HasAttrs, Semantics};
5 use ide_db::call_info::ActiveParameter;
6 use syntax::{
7     ast::{self, AstNode, AttrsOwner, DocCommentsOwner},
8     match_ast, AstToken, NodeOrToken, SyntaxNode, SyntaxToken, TextRange, TextSize,
9 };
10
11 use crate::{Analysis, HlMod, HlRange, HlTag, RootDatabase};
12
13 use super::{highlights::Highlights, injector::Injector};
14
15 pub(super) fn ra_fixture(
16     hl: &mut Highlights,
17     sema: &Semantics<RootDatabase>,
18     literal: ast::String,
19     expanded: SyntaxToken,
20 ) -> Option<()> {
21     let active_parameter = ActiveParameter::at_token(&sema, expanded)?;
22     if !active_parameter.name.starts_with("ra_fixture") {
23         return None;
24     }
25     let value = literal.value()?;
26
27     if let Some(range) = literal.open_quote_text_range() {
28         hl.add(HlRange { range, highlight: HlTag::StringLiteral.into(), binding_hash: None })
29     }
30
31     let mut inj = Injector::default();
32
33     let mut text = &*value;
34     let mut offset: TextSize = 0.into();
35
36     while !text.is_empty() {
37         let marker = "$0";
38         let idx = text.find(marker).unwrap_or(text.len());
39         let (chunk, next) = text.split_at(idx);
40         inj.add(chunk, TextRange::at(offset, TextSize::of(chunk)));
41
42         text = next;
43         offset += TextSize::of(chunk);
44
45         if let Some(next) = text.strip_prefix(marker) {
46             if let Some(range) = literal.map_range_up(TextRange::at(offset, TextSize::of(marker))) {
47                 hl.add(HlRange { range, highlight: HlTag::Keyword.into(), binding_hash: None });
48             }
49
50             text = next;
51
52             let marker_len = TextSize::of(marker);
53             offset += marker_len;
54         }
55     }
56
57     let (analysis, tmp_file_id) = Analysis::from_single_file(inj.text().to_string());
58
59     for mut hl_range in analysis.highlight(tmp_file_id).unwrap() {
60         for range in inj.map_range_up(hl_range.range) {
61             if let Some(range) = literal.map_range_up(range) {
62                 hl_range.range = range;
63                 hl.add(hl_range.clone());
64             }
65         }
66     }
67
68     if let Some(range) = literal.close_quote_text_range() {
69         hl.add(HlRange { range, highlight: HlTag::StringLiteral.into(), binding_hash: None })
70     }
71
72     Some(())
73 }
74
75 const RUSTDOC_FENCE: &'static str = "```";
76 const RUSTDOC_FENCE_TOKENS: &[&'static str] = &[
77     "",
78     "rust",
79     "should_panic",
80     "ignore",
81     "no_run",
82     "compile_fail",
83     "edition2015",
84     "edition2018",
85     "edition2021",
86 ];
87
88 // Basically an owned dyn AttrsOwner without extra Boxing
89 struct AttrsOwnerNode {
90     node: SyntaxNode,
91 }
92
93 impl AttrsOwnerNode {
94     fn new<N: DocCommentsOwner>(node: N) -> Self {
95         AttrsOwnerNode { node: node.syntax().clone() }
96     }
97 }
98
99 impl AttrsOwner for AttrsOwnerNode {}
100 impl AstNode for AttrsOwnerNode {
101     fn can_cast(_: syntax::SyntaxKind) -> bool
102     where
103         Self: Sized,
104     {
105         false
106     }
107     fn cast(_: SyntaxNode) -> Option<Self>
108     where
109         Self: Sized,
110     {
111         None
112     }
113     fn syntax(&self) -> &SyntaxNode {
114         &self.node
115     }
116 }
117
118 fn doc_attributes<'node>(
119     sema: &Semantics<RootDatabase>,
120     node: &'node SyntaxNode,
121 ) -> Option<(AttrsOwnerNode, hir::Attrs)> {
122     match_ast! {
123         match node {
124             ast::SourceFile(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
125             ast::Fn(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
126             ast::Struct(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
127             ast::Union(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
128             ast::RecordField(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
129             ast::TupleField(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
130             ast::Enum(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
131             ast::Variant(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
132             ast::Trait(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
133             ast::Module(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
134             ast::Static(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
135             ast::Const(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
136             ast::TypeAlias(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
137             ast::Impl(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
138             ast::MacroRules(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
139             // ast::MacroDef(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))),
140             // ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))),
141             _ => return None
142         }
143     }
144 }
145
146 /// Injection of syntax highlighting of doctests.
147 pub(super) fn doc_comment(hl: &mut Highlights, sema: &Semantics<RootDatabase>, node: &SyntaxNode) {
148     let (owner, attributes) = match doc_attributes(sema, node) {
149         Some(it) => it,
150         None => return,
151     };
152
153     if attributes.docs().map_or(true, |docs| !String::from(docs).contains(RUSTDOC_FENCE)) {
154         return;
155     }
156     let attrs_source_map = attributes.source_map(&owner);
157
158     let mut inj = Injector::default();
159     inj.add_unmapped("fn doctest() {\n");
160
161     let mut is_codeblock = false;
162     let mut is_doctest = false;
163
164     // Replace the original, line-spanning comment ranges by new, only comment-prefix
165     // spanning comment ranges.
166     let mut new_comments = Vec::new();
167     let mut string;
168     for attr in attributes.by_key("doc").attrs() {
169         let src = attrs_source_map.source_of(&attr);
170         let (line, range, prefix) = match &src {
171             Either::Left(it) => {
172                 string = match find_doc_string_in_attr(attr, it) {
173                     Some(it) => it,
174                     None => continue,
175                 };
176                 let text_range = string.syntax().text_range();
177                 let text_range = TextRange::new(
178                     text_range.start() + TextSize::from(1),
179                     text_range.end() - TextSize::from(1),
180                 );
181                 let text = string.text();
182                 (&text[1..text.len() - 1], text_range, "")
183             }
184             Either::Right(comment) => {
185                 (comment.text(), comment.syntax().text_range(), comment.prefix())
186             }
187         };
188
189         match line.find(RUSTDOC_FENCE) {
190             Some(idx) => {
191                 is_codeblock = !is_codeblock;
192                 // Check whether code is rust by inspecting fence guards
193                 let guards = &line[idx + RUSTDOC_FENCE.len()..];
194                 let is_rust =
195                     guards.split(',').all(|sub| RUSTDOC_FENCE_TOKENS.contains(&sub.trim()));
196                 is_doctest = is_codeblock && is_rust;
197                 continue;
198             }
199             None if !is_doctest => continue,
200             None => (),
201         }
202
203         let mut pos = TextSize::of(prefix);
204         // whitespace after comment is ignored
205         if let Some(ws) = line[pos.into()..].chars().next().filter(|c| c.is_whitespace()) {
206             pos += TextSize::of(ws);
207         }
208         // lines marked with `#` should be ignored in output, we skip the `#` char
209         if let Some(ws) = line[pos.into()..].chars().next().filter(|&c| c == '#') {
210             pos += TextSize::of(ws);
211         }
212
213         new_comments.push(TextRange::at(range.start(), pos));
214
215         inj.add(&line[pos.into()..], TextRange::new(range.start() + pos, range.end()));
216         inj.add_unmapped("\n");
217     }
218     inj.add_unmapped("\n}");
219
220     let (analysis, tmp_file_id) = Analysis::from_single_file(inj.text().to_string());
221
222     for h in analysis.with_db(|db| super::highlight(db, tmp_file_id, None, true)).unwrap() {
223         for r in inj.map_range_up(h.range) {
224             hl.add(HlRange {
225                 range: r,
226                 highlight: h.highlight | HlMod::Injected,
227                 binding_hash: h.binding_hash,
228             });
229         }
230     }
231
232     for range in new_comments {
233         hl.add(HlRange {
234             range,
235             highlight: HlTag::Comment | HlMod::Documentation,
236             binding_hash: None,
237         });
238     }
239 }
240
241 fn find_doc_string_in_attr(attr: &hir::Attr, it: &ast::Attr) -> Option<ast::String> {
242     match it.literal() {
243         // #[doc = lit]
244         Some(lit) => match lit.kind() {
245             ast::LiteralKind::String(it) => Some(it),
246             _ => None,
247         },
248         // #[cfg_attr(..., doc = "", ...)]
249         None => {
250             // We gotta hunt the string token manually here
251             let text = attr.string_value()?;
252             // FIXME: We just pick the first string literal that has the same text as the doc attribute
253             // This means technically we might highlight the wrong one
254             it.syntax()
255                 .descendants_with_tokens()
256                 .filter_map(NodeOrToken::into_token)
257                 .filter_map(ast::String::cast)
258                 .find(|string| {
259                     string.text().get(1..string.text().len() - 1).map_or(false, |it| it == text)
260                 })
261         }
262     }
263 }