1 //! "Recursive" Syntax highlighting for code in doctests and fixtures.
4 use ide_db::call_info::ActiveParameter;
5 use syntax::{ast, AstToken, SyntaxNode, SyntaxToken, TextRange, TextSize};
7 use crate::{Analysis, HlMod, HlRange, HlTag, RootDatabase};
9 use super::{highlights::Highlights, injector::Injector};
11 pub(super) fn ra_fixture(
13 sema: &Semantics<RootDatabase>,
15 expanded: SyntaxToken,
17 let active_parameter = ActiveParameter::at_token(&sema, expanded)?;
18 if !active_parameter.name.starts_with("ra_fixture") {
21 let value = literal.value()?;
23 if let Some(range) = literal.open_quote_text_range() {
24 hl.add(HlRange { range, highlight: HlTag::StringLiteral.into(), binding_hash: None })
27 let mut inj = Injector::default();
29 let mut text = &*value;
30 let mut offset: TextSize = 0.into();
32 while !text.is_empty() {
34 let idx = text.find(marker).unwrap_or(text.len());
35 let (chunk, next) = text.split_at(idx);
36 inj.add(chunk, TextRange::at(offset, TextSize::of(chunk)));
39 offset += TextSize::of(chunk);
41 if let Some(next) = text.strip_prefix(marker) {
42 if let Some(range) = literal.map_range_up(TextRange::at(offset, TextSize::of(marker))) {
43 hl.add(HlRange { range, highlight: HlTag::Keyword.into(), binding_hash: None });
48 let marker_len = TextSize::of(marker);
53 let (analysis, tmp_file_id) = Analysis::from_single_file(inj.text().to_string());
55 for mut hl_range in analysis.highlight(tmp_file_id).unwrap() {
56 for range in inj.map_range_up(hl_range.range) {
57 if let Some(range) = literal.map_range_up(range) {
58 hl_range.range = range;
59 hl.add(hl_range.clone());
64 if let Some(range) = literal.close_quote_text_range() {
65 hl.add(HlRange { range, highlight: HlTag::StringLiteral.into(), binding_hash: None })
71 const RUSTDOC_FENCE: &'static str = "```";
72 const RUSTDOC_FENCE_TOKENS: &[&'static str] = &[
84 /// Injection of syntax highlighting of doctests.
85 pub(super) fn doc_comment(hl: &mut Highlights, node: &SyntaxNode) {
86 let doc_comments = node
87 .children_with_tokens()
88 .filter_map(|it| it.into_token().and_then(ast::Comment::cast))
89 .filter(|it| it.kind().doc.is_some());
91 if !doc_comments.clone().any(|it| it.text().contains(RUSTDOC_FENCE)) {
95 let mut inj = Injector::default();
96 inj.add_unmapped("fn doctest() {\n");
98 let mut is_codeblock = false;
99 let mut is_doctest = false;
101 // Replace the original, line-spanning comment ranges by new, only comment-prefix
102 // spanning comment ranges.
103 let mut new_comments = Vec::new();
104 for comment in doc_comments {
105 match comment.text().find(RUSTDOC_FENCE) {
107 is_codeblock = !is_codeblock;
108 // Check whether code is rust by inspecting fence guards
109 let guards = &comment.text()[idx + RUSTDOC_FENCE.len()..];
111 guards.split(',').all(|sub| RUSTDOC_FENCE_TOKENS.contains(&sub.trim()));
112 is_doctest = is_codeblock && is_rust;
115 None if !is_doctest => continue,
119 let line: &str = comment.text();
120 let range = comment.syntax().text_range();
122 let mut pos = TextSize::of(comment.prefix());
123 // whitespace after comment is ignored
124 if let Some(ws) = line[pos.into()..].chars().next().filter(|c| c.is_whitespace()) {
125 pos += TextSize::of(ws);
127 // lines marked with `#` should be ignored in output, we skip the `#` char
128 if let Some(ws) = line[pos.into()..].chars().next().filter(|&c| c == '#') {
129 pos += TextSize::of(ws);
132 new_comments.push(TextRange::at(range.start(), pos));
134 inj.add(&line[pos.into()..], TextRange::new(range.start() + pos, range.end()));
135 inj.add_unmapped("\n");
137 inj.add_unmapped("\n}");
139 let (analysis, tmp_file_id) = Analysis::from_single_file(inj.text().to_string());
141 for h in analysis.with_db(|db| super::highlight(db, tmp_file_id, None, true)).unwrap() {
142 for r in inj.map_range_up(h.range) {
145 highlight: h.highlight | HlMod::Injected,
146 binding_hash: h.binding_hash,
151 for range in new_comments {
154 highlight: HlTag::Comment | HlMod::Documentation,