1 //! Syntax highlighting injections such as highlighting of documentation tests.
3 use std::convert::TryFrom;
6 use ide_db::call_info::ActiveParameter;
7 use itertools::Itertools;
8 use syntax::{ast, AstToken, SyntaxNode, SyntaxToken, TextRange, TextSize};
10 use crate::{Analysis, HlMod, HlRange, HlTag, RootDatabase};
12 use super::{highlights::Highlights, injector::Injector};
14 pub(super) fn highlight_injection(
16 sema: &Semantics<RootDatabase>,
18 expanded: SyntaxToken,
20 let active_parameter = ActiveParameter::at_token(&sema, expanded)?;
21 if !active_parameter.name.starts_with("ra_fixture") {
24 let value = literal.value()?;
26 if let Some(range) = literal.open_quote_text_range() {
27 hl.add(HlRange { range, highlight: HlTag::StringLiteral.into(), binding_hash: None })
30 let mut inj = Injector::default();
32 let mut text = &*value;
33 let mut offset: TextSize = 0.into();
35 while !text.is_empty() {
37 let idx = text.find(marker).unwrap_or(text.len());
38 let (chunk, next) = text.split_at(idx);
39 inj.add(chunk, TextRange::at(offset, TextSize::of(chunk)));
42 offset += TextSize::of(chunk);
44 if let Some(next) = text.strip_prefix(marker) {
47 let marker_len = TextSize::of(marker);
52 let (analysis, tmp_file_id) = Analysis::from_single_file(inj.text().to_string());
54 for mut hl_range in analysis.highlight(tmp_file_id).unwrap() {
55 for range in inj.map_range_up(hl_range.range) {
56 if let Some(range) = literal.map_range_up(range) {
57 hl_range.range = range;
58 hl.add(hl_range.clone());
63 if let Some(range) = literal.close_quote_text_range() {
64 hl.add(HlRange { range, highlight: HlTag::StringLiteral.into(), binding_hash: None })
70 const RUSTDOC_FENCE: &'static str = "```";
71 const RUSTDOC_FENCE_TOKENS: &[&'static str] = &[
83 /// Extracts Rust code from documentation comments as well as a mapping from
84 /// the extracted source code back to the original source ranges.
85 /// Lastly, a vector of new comment highlight ranges (spanning only the
86 /// comment prefix) is returned which is used in the syntax highlighting
87 /// injection to replace the previous (line-spanning) comment ranges.
88 pub(super) fn extract_doc_comments(node: &SyntaxNode) -> Option<(Vec<HlRange>, Injector)> {
89 let mut inj = Injector::default();
90 // wrap the doctest into function body to get correct syntax highlighting
91 let prefix = "fn doctest() {\n";
94 let mut line_start = TextSize::of(prefix);
95 let mut is_codeblock = false;
96 let mut is_doctest = false;
97 // Replace the original, line-spanning comment ranges by new, only comment-prefix
98 // spanning comment ranges.
99 let mut new_comments = Vec::new();
101 inj.add_unmapped(prefix);
103 .children_with_tokens()
104 .filter_map(|el| el.into_token().and_then(ast::Comment::cast))
105 .filter(|comment| comment.kind().doc.is_some())
107 if let Some(idx) = comment.text().find(RUSTDOC_FENCE) {
108 is_codeblock = !is_codeblock;
109 // Check whether code is rust by inspecting fence guards
110 let guards = &comment.text()[idx + RUSTDOC_FENCE.len()..];
112 guards.split(',').all(|sub| RUSTDOC_FENCE_TOKENS.contains(&sub.trim()));
113 is_doctest = is_codeblock && is_rust;
120 let prefix_len = comment.prefix().len();
121 let line: &str = comment.text().as_str();
122 let range = comment.syntax().text_range();
124 // whitespace after comment is ignored
125 let pos = if let Some(ws) = line.chars().nth(prefix_len).filter(|c| c.is_whitespace()) {
126 prefix_len + ws.len_utf8()
131 // lines marked with `#` should be ignored in output, we skip the `#` char
132 let pos = if let Some(ws) = line.chars().nth(pos).filter(|&c| c == '#') {
138 new_comments.push(HlRange {
139 range: TextRange::new(
141 range.start() + TextSize::try_from(pos).unwrap(),
143 highlight: HlTag::Comment | HlMod::Documentation,
146 line_start += range.len() - TextSize::try_from(pos).unwrap();
147 line_start += TextSize::of("\n");
151 TextRange::new(range.start() + TextSize::try_from(pos).unwrap(), range.end()),
153 inj.add_unmapped("\n");
154 line[pos..].to_owned()
157 inj.add_unmapped(suffix);
159 if doctest.is_empty() {
163 Some((new_comments, inj))
166 /// Injection of syntax highlighting of doctests.
167 pub(super) fn highlight_doc_comment(
168 new_comments: Vec<HlRange>,
170 stack: &mut Highlights,
172 let (analysis, tmp_file_id) = Analysis::from_single_file(inj.text().to_string());
173 for comment in new_comments {
177 for h in analysis.with_db(|db| super::highlight(db, tmp_file_id, None, true)).unwrap() {
178 for r in inj.map_range_up(h.range) {
181 highlight: h.highlight | HlMod::Injected,
182 binding_hash: h.binding_hash,