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