aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src/syntax_highlighting/injection.rs
diff options
context:
space:
mode:
authorZac Pullar-Strecker <[email protected]>2020-08-24 10:19:53 +0100
committerZac Pullar-Strecker <[email protected]>2020-08-24 10:20:13 +0100
commit7bbca7a1b3f9293d2f5cc5745199bc5f8396f2f0 (patch)
treebdb47765991cb973b2cd5481a088fac636bd326c /crates/ide/src/syntax_highlighting/injection.rs
parentca464650eeaca6195891199a93f4f76cf3e7e697 (diff)
parente65d48d1fb3d4d91d9dc1148a7a836ff5c9a3c87 (diff)
Merge remote-tracking branch 'upstream/master' into 503-hover-doc-links
Diffstat (limited to 'crates/ide/src/syntax_highlighting/injection.rs')
-rw-r--r--crates/ide/src/syntax_highlighting/injection.rs187
1 files changed, 187 insertions, 0 deletions
diff --git a/crates/ide/src/syntax_highlighting/injection.rs b/crates/ide/src/syntax_highlighting/injection.rs
new file mode 100644
index 000000000..43f4e6fea
--- /dev/null
+++ b/crates/ide/src/syntax_highlighting/injection.rs
@@ -0,0 +1,187 @@
1//! Syntax highlighting injections such as highlighting of documentation tests.
2
3use std::{collections::BTreeMap, convert::TryFrom};
4
5use ast::{HasQuotes, HasStringValue};
6use hir::Semantics;
7use itertools::Itertools;
8use syntax::{ast, AstToken, SyntaxNode, SyntaxToken, TextRange, TextSize};
9
10use crate::{
11 call_info::ActiveParameter, Analysis, Highlight, HighlightModifier, HighlightTag,
12 HighlightedRange, RootDatabase,
13};
14
15use super::HighlightedRangeStack;
16
17pub(super) fn highlight_injection(
18 acc: &mut HighlightedRangeStack,
19 sema: &Semantics<RootDatabase>,
20 literal: ast::RawString,
21 expanded: SyntaxToken,
22) -> Option<()> {
23 let active_parameter = ActiveParameter::at_token(&sema, expanded)?;
24 if !active_parameter.name.starts_with("ra_fixture") {
25 return None;
26 }
27 let value = literal.value()?;
28 let (analysis, tmp_file_id) = Analysis::from_single_file(value.into_owned());
29
30 if let Some(range) = literal.open_quote_text_range() {
31 acc.add(HighlightedRange {
32 range,
33 highlight: HighlightTag::StringLiteral.into(),
34 binding_hash: None,
35 })
36 }
37
38 for mut h in analysis.highlight(tmp_file_id).unwrap() {
39 if let Some(r) = literal.map_range_up(h.range) {
40 h.range = r;
41 acc.add(h)
42 }
43 }
44
45 if let Some(range) = literal.close_quote_text_range() {
46 acc.add(HighlightedRange {
47 range,
48 highlight: HighlightTag::StringLiteral.into(),
49 binding_hash: None,
50 })
51 }
52
53 Some(())
54}
55
56/// Mapping from extracted documentation code to original code
57type RangesMap = BTreeMap<TextSize, TextSize>;
58
59const RUSTDOC_FENCE: &'static str = "```";
60const RUSTDOC_FENCE_TOKENS: &[&'static str] =
61 &["", "rust", "should_panic", "ignore", "no_run", "compile_fail", "edition2015", "edition2018"];
62
63/// Extracts Rust code from documentation comments as well as a mapping from
64/// the extracted source code back to the original source ranges.
65/// Lastly, a vector of new comment highlight ranges (spanning only the
66/// comment prefix) is returned which is used in the syntax highlighting
67/// injection to replace the previous (line-spanning) comment ranges.
68pub(super) fn extract_doc_comments(
69 node: &SyntaxNode,
70) -> Option<(String, RangesMap, Vec<HighlightedRange>)> {
71 // wrap the doctest into function body to get correct syntax highlighting
72 let prefix = "fn doctest() {\n";
73 let suffix = "}\n";
74 // Mapping from extracted documentation code to original code
75 let mut range_mapping: RangesMap = BTreeMap::new();
76 let mut line_start = TextSize::try_from(prefix.len()).unwrap();
77 let mut is_codeblock = false;
78 let mut is_doctest = false;
79 // Replace the original, line-spanning comment ranges by new, only comment-prefix
80 // spanning comment ranges.
81 let mut new_comments = Vec::new();
82 let doctest = node
83 .children_with_tokens()
84 .filter_map(|el| el.into_token().and_then(ast::Comment::cast))
85 .filter(|comment| comment.kind().doc.is_some())
86 .filter(|comment| {
87 if let Some(idx) = comment.text().find(RUSTDOC_FENCE) {
88 is_codeblock = !is_codeblock;
89 // Check whether code is rust by inspecting fence guards
90 let guards = &comment.text()[idx + RUSTDOC_FENCE.len()..];
91 let is_rust =
92 guards.split(',').all(|sub| RUSTDOC_FENCE_TOKENS.contains(&sub.trim()));
93 is_doctest = is_codeblock && is_rust;
94 false
95 } else {
96 is_doctest
97 }
98 })
99 .map(|comment| {
100 let prefix_len = comment.prefix().len();
101 let line: &str = comment.text().as_str();
102 let range = comment.syntax().text_range();
103
104 // whitespace after comment is ignored
105 let pos = if let Some(ws) = line.chars().nth(prefix_len).filter(|c| c.is_whitespace()) {
106 prefix_len + ws.len_utf8()
107 } else {
108 prefix_len
109 };
110
111 // lines marked with `#` should be ignored in output, we skip the `#` char
112 let pos = if let Some(ws) = line.chars().nth(pos).filter(|&c| c == '#') {
113 pos + ws.len_utf8()
114 } else {
115 pos
116 };
117
118 range_mapping.insert(line_start, range.start() + TextSize::try_from(pos).unwrap());
119 new_comments.push(HighlightedRange {
120 range: TextRange::new(
121 range.start(),
122 range.start() + TextSize::try_from(pos).unwrap(),
123 ),
124 highlight: HighlightTag::Comment | HighlightModifier::Documentation,
125 binding_hash: None,
126 });
127 line_start += range.len() - TextSize::try_from(pos).unwrap();
128 line_start += TextSize::try_from('\n'.len_utf8()).unwrap();
129
130 line[pos..].to_owned()
131 })
132 .join("\n");
133
134 if doctest.is_empty() {
135 return None;
136 }
137
138 let doctest = format!("{}{}{}", prefix, doctest, suffix);
139 Some((doctest, range_mapping, new_comments))
140}
141
142/// Injection of syntax highlighting of doctests.
143pub(super) fn highlight_doc_comment(
144 text: String,
145 range_mapping: RangesMap,
146 new_comments: Vec<HighlightedRange>,
147 stack: &mut HighlightedRangeStack,
148) {
149 let (analysis, tmp_file_id) = Analysis::from_single_file(text);
150
151 stack.push();
152 for mut h in analysis.with_db(|db| super::highlight(db, tmp_file_id, None, true)).unwrap() {
153 // Determine start offset and end offset in case of multi-line ranges
154 let mut start_offset = None;
155 let mut end_offset = None;
156 for (line_start, orig_line_start) in range_mapping.range(..h.range.end()).rev() {
157 // It's possible for orig_line_start - line_start to be negative. Add h.range.start()
158 // here and remove it from the end range after the loop below so that the values are
159 // always non-negative.
160 let offset = h.range.start() + orig_line_start - line_start;
161 if line_start <= &h.range.start() {
162 start_offset.get_or_insert(offset);
163 break;
164 } else {
165 end_offset.get_or_insert(offset);
166 }
167 }
168 if let Some(start_offset) = start_offset {
169 h.range = TextRange::new(
170 start_offset,
171 h.range.end() + end_offset.unwrap_or(start_offset) - h.range.start(),
172 );
173
174 h.highlight |= HighlightModifier::Injected;
175 stack.add(h);
176 }
177 }
178
179 // Inject the comment prefix highlight ranges
180 stack.push();
181 for comment in new_comments {
182 stack.add(comment);
183 }
184 stack.pop_and_inject(None);
185 stack
186 .pop_and_inject(Some(Highlight::from(HighlightTag::Generic) | HighlightModifier::Injected));
187}