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