diff options
author | Zac Pullar-Strecker <[email protected]> | 2020-08-24 10:19:53 +0100 |
---|---|---|
committer | Zac Pullar-Strecker <[email protected]> | 2020-08-24 10:20:13 +0100 |
commit | 7bbca7a1b3f9293d2f5cc5745199bc5f8396f2f0 (patch) | |
tree | bdb47765991cb973b2cd5481a088fac636bd326c /crates/ide/src/syntax_highlighting/injection.rs | |
parent | ca464650eeaca6195891199a93f4f76cf3e7e697 (diff) | |
parent | e65d48d1fb3d4d91d9dc1148a7a836ff5c9a3c87 (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.rs | 187 |
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 | |||
3 | use std::{collections::BTreeMap, convert::TryFrom}; | ||
4 | |||
5 | use ast::{HasQuotes, HasStringValue}; | ||
6 | use hir::Semantics; | ||
7 | use itertools::Itertools; | ||
8 | use syntax::{ast, AstToken, SyntaxNode, SyntaxToken, TextRange, TextSize}; | ||
9 | |||
10 | use crate::{ | ||
11 | call_info::ActiveParameter, Analysis, Highlight, HighlightModifier, HighlightTag, | ||
12 | HighlightedRange, RootDatabase, | ||
13 | }; | ||
14 | |||
15 | use super::HighlightedRangeStack; | ||
16 | |||
17 | pub(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 | ||
57 | type RangesMap = BTreeMap<TextSize, TextSize>; | ||
58 | |||
59 | const RUSTDOC_FENCE: &'static str = "```"; | ||
60 | const 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. | ||
68 | pub(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. | ||
143 | pub(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 | } | ||