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, HighlightedRange, HlMod, 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(HighlightedRange {
31 highlight: HlTag::StringLiteral.into(),
36 for mut h in analysis.highlight(tmp_file_id).unwrap() {
37 let range = marker_info.map_range_up(h.range);
38 if let Some(range) = literal.map_range_up(range) {
44 if let Some(range) = literal.close_quote_text_range() {
45 acc.add(HighlightedRange {
47 highlight: HlTag::StringLiteral.into(),
55 /// Data to remove `$0` from string and map ranges
56 #[derive(Default, Debug)]
59 markers: Vec<TextRange>,
63 fn new(mut text: &str) -> Self {
66 let mut res = MarkerInfo::default();
67 let mut offset: TextSize = 0.into();
68 while !text.is_empty() {
69 let idx = text.find(marker).unwrap_or(text.len());
70 let (chunk, next) = text.split_at(idx);
72 res.cleaned_text.push_str(chunk);
73 offset += TextSize::of(chunk);
75 if let Some(next) = text.strip_prefix(marker) {
78 let marker_len = TextSize::of(marker);
79 res.markers.push(TextRange::at(offset, marker_len));
85 fn map_range_up(&self, range: TextRange) -> TextRange {
87 self.map_offset_up(range.start(), true),
88 self.map_offset_up(range.end(), false),
91 fn map_offset_up(&self, mut offset: TextSize, start: bool) -> TextSize {
92 for r in &self.markers {
93 if r.start() < offset || (start && r.start() == offset) {
101 const RUSTDOC_FENCE: &'static str = "```";
102 const RUSTDOC_FENCE_TOKENS: &[&'static str] = &[
114 /// Extracts Rust code from documentation comments as well as a mapping from
115 /// the extracted source code back to the original source ranges.
116 /// Lastly, a vector of new comment highlight ranges (spanning only the
117 /// comment prefix) is returned which is used in the syntax highlighting
118 /// injection to replace the previous (line-spanning) comment ranges.
119 pub(super) fn extract_doc_comments(node: &SyntaxNode) -> Option<(Vec<HighlightedRange>, Injector)> {
120 let mut inj = Injector::default();
121 // wrap the doctest into function body to get correct syntax highlighting
122 let prefix = "fn doctest() {\n";
125 let mut line_start = TextSize::of(prefix);
126 let mut is_codeblock = false;
127 let mut is_doctest = false;
128 // Replace the original, line-spanning comment ranges by new, only comment-prefix
129 // spanning comment ranges.
130 let mut new_comments = Vec::new();
132 inj.add_unmapped(prefix);
134 .children_with_tokens()
135 .filter_map(|el| el.into_token().and_then(ast::Comment::cast))
136 .filter(|comment| comment.kind().doc.is_some())
138 if let Some(idx) = comment.text().find(RUSTDOC_FENCE) {
139 is_codeblock = !is_codeblock;
140 // Check whether code is rust by inspecting fence guards
141 let guards = &comment.text()[idx + RUSTDOC_FENCE.len()..];
143 guards.split(',').all(|sub| RUSTDOC_FENCE_TOKENS.contains(&sub.trim()));
144 is_doctest = is_codeblock && is_rust;
151 let prefix_len = comment.prefix().len();
152 let line: &str = comment.text().as_str();
153 let range = comment.syntax().text_range();
155 // whitespace after comment is ignored
156 let pos = if let Some(ws) = line.chars().nth(prefix_len).filter(|c| c.is_whitespace()) {
157 prefix_len + ws.len_utf8()
162 // lines marked with `#` should be ignored in output, we skip the `#` char
163 let pos = if let Some(ws) = line.chars().nth(pos).filter(|&c| c == '#') {
169 new_comments.push(HighlightedRange {
170 range: TextRange::new(
172 range.start() + TextSize::try_from(pos).unwrap(),
174 highlight: HlTag::Comment | HlMod::Documentation,
177 line_start += range.len() - TextSize::try_from(pos).unwrap();
178 line_start += TextSize::of("\n");
182 TextRange::new(range.start() + TextSize::try_from(pos).unwrap(), range.end()),
184 inj.add_unmapped("\n");
185 line[pos..].to_owned()
188 inj.add_unmapped(suffix);
190 if doctest.is_empty() {
194 Some((new_comments, inj))
197 /// Injection of syntax highlighting of doctests.
198 pub(super) fn highlight_doc_comment(
199 new_comments: Vec<HighlightedRange>,
201 stack: &mut Highlights,
203 let (analysis, tmp_file_id) = Analysis::from_single_file(inj.text().to_string());
204 for comment in new_comments {
208 for h in analysis.with_db(|db| super::highlight(db, tmp_file_id, None, true)).unwrap() {
209 for r in inj.map_range_up(h.range) {
210 stack.add(HighlightedRange {
212 highlight: h.highlight | HlMod::Injected,
213 binding_hash: h.binding_hash,