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()?;
25 let marker_info = MarkerInfo::new(&*value);
26 let (analysis, tmp_file_id) = Analysis::from_single_file(marker_info.cleaned_text.clone());
28 if let Some(range) = literal.open_quote_text_range() {
29 acc.add(HlRange { range, highlight: HlTag::StringLiteral.into(), binding_hash: None })
32 for mut h in analysis.highlight(tmp_file_id).unwrap() {
33 let range = marker_info.map_range_up(h.range);
34 if let Some(range) = literal.map_range_up(range) {
40 if let Some(range) = literal.close_quote_text_range() {
41 acc.add(HlRange { range, highlight: HlTag::StringLiteral.into(), binding_hash: None })
47 /// Data to remove `$0` from string and map ranges
48 #[derive(Default, Debug)]
51 markers: Vec<TextRange>,
55 fn new(mut text: &str) -> Self {
58 let mut res = MarkerInfo::default();
59 let mut offset: TextSize = 0.into();
60 while !text.is_empty() {
61 let idx = text.find(marker).unwrap_or(text.len());
62 let (chunk, next) = text.split_at(idx);
64 res.cleaned_text.push_str(chunk);
65 offset += TextSize::of(chunk);
67 if let Some(next) = text.strip_prefix(marker) {
70 let marker_len = TextSize::of(marker);
71 res.markers.push(TextRange::at(offset, marker_len));
77 fn map_range_up(&self, range: TextRange) -> TextRange {
79 self.map_offset_up(range.start(), true),
80 self.map_offset_up(range.end(), false),
83 fn map_offset_up(&self, mut offset: TextSize, start: bool) -> TextSize {
84 for r in &self.markers {
85 if r.start() < offset || (start && r.start() == offset) {
93 const RUSTDOC_FENCE: &'static str = "```";
94 const RUSTDOC_FENCE_TOKENS: &[&'static str] = &[
106 /// Extracts Rust code from documentation comments as well as a mapping from
107 /// the extracted source code back to the original source ranges.
108 /// Lastly, a vector of new comment highlight ranges (spanning only the
109 /// comment prefix) is returned which is used in the syntax highlighting
110 /// injection to replace the previous (line-spanning) comment ranges.
111 pub(super) fn extract_doc_comments(node: &SyntaxNode) -> Option<(Vec<HlRange>, Injector)> {
112 let mut inj = Injector::default();
113 // wrap the doctest into function body to get correct syntax highlighting
114 let prefix = "fn doctest() {\n";
117 let mut line_start = TextSize::of(prefix);
118 let mut is_codeblock = false;
119 let mut is_doctest = false;
120 // Replace the original, line-spanning comment ranges by new, only comment-prefix
121 // spanning comment ranges.
122 let mut new_comments = Vec::new();
124 inj.add_unmapped(prefix);
126 .children_with_tokens()
127 .filter_map(|el| el.into_token().and_then(ast::Comment::cast))
128 .filter(|comment| comment.kind().doc.is_some())
130 if let Some(idx) = comment.text().find(RUSTDOC_FENCE) {
131 is_codeblock = !is_codeblock;
132 // Check whether code is rust by inspecting fence guards
133 let guards = &comment.text()[idx + RUSTDOC_FENCE.len()..];
135 guards.split(',').all(|sub| RUSTDOC_FENCE_TOKENS.contains(&sub.trim()));
136 is_doctest = is_codeblock && is_rust;
143 let prefix_len = comment.prefix().len();
144 let line: &str = comment.text().as_str();
145 let range = comment.syntax().text_range();
147 // whitespace after comment is ignored
148 let pos = if let Some(ws) = line.chars().nth(prefix_len).filter(|c| c.is_whitespace()) {
149 prefix_len + ws.len_utf8()
154 // lines marked with `#` should be ignored in output, we skip the `#` char
155 let pos = if let Some(ws) = line.chars().nth(pos).filter(|&c| c == '#') {
161 new_comments.push(HlRange {
162 range: TextRange::new(
164 range.start() + TextSize::try_from(pos).unwrap(),
166 highlight: HlTag::Comment | HlMod::Documentation,
169 line_start += range.len() - TextSize::try_from(pos).unwrap();
170 line_start += TextSize::of("\n");
174 TextRange::new(range.start() + TextSize::try_from(pos).unwrap(), range.end()),
176 inj.add_unmapped("\n");
177 line[pos..].to_owned()
180 inj.add_unmapped(suffix);
182 if doctest.is_empty() {
186 Some((new_comments, inj))
189 /// Injection of syntax highlighting of doctests.
190 pub(super) fn highlight_doc_comment(
191 new_comments: Vec<HlRange>,
193 stack: &mut Highlights,
195 let (analysis, tmp_file_id) = Analysis::from_single_file(inj.text().to_string());
196 for comment in new_comments {
200 for h in analysis.with_db(|db| super::highlight(db, tmp_file_id, None, true)).unwrap() {
201 for r in inj.map_range_up(h.range) {
204 highlight: h.highlight | HlMod::Injected,
205 binding_hash: h.binding_hash,