diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-06-08 13:26:33 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2020-06-08 13:26:33 +0100 |
commit | db36a25839be0a799be2a229b9fd0d45da57635b (patch) | |
tree | c6787e78879c7c2c86938e8d7c659fd756609ffd /crates/ra_ide/src/syntax_highlighting.rs | |
parent | ab86f15ff2c237ac3f4237961e4484ae71cedf00 (diff) | |
parent | 4a2efb2f42494f62891ac801e0a27d246bd36684 (diff) |
Merge #4683
4683: Implement syntax highlighting for doctests r=ltentrup a=ltentrup
The implementation is more complicated than the previous injection logic as the doctest comments consist of multiple ranges. The implementation extracts the doctests together with an offset-mapping, applies the syntax highlighting, and updates the text ranges.
<img width="478" alt="Bildschirmfoto 2020-06-01 um 15 45 25" src="https://user-images.githubusercontent.com/201808/83415249-1f0b5800-a41f-11ea-8fa6-c282434d6ff7.png">
Part of #4170.
Co-authored-by: Leander Tentrup <[email protected]>
Diffstat (limited to 'crates/ra_ide/src/syntax_highlighting.rs')
-rw-r--r-- | crates/ra_ide/src/syntax_highlighting.rs | 126 |
1 files changed, 80 insertions, 46 deletions
diff --git a/crates/ra_ide/src/syntax_highlighting.rs b/crates/ra_ide/src/syntax_highlighting.rs index 19ecd54d6..6903403b2 100644 --- a/crates/ra_ide/src/syntax_highlighting.rs +++ b/crates/ra_ide/src/syntax_highlighting.rs | |||
@@ -1,5 +1,6 @@ | |||
1 | mod tags; | 1 | mod tags; |
2 | mod html; | 2 | mod html; |
3 | mod injection; | ||
3 | #[cfg(test)] | 4 | #[cfg(test)] |
4 | mod tests; | 5 | mod tests; |
5 | 6 | ||
@@ -10,14 +11,14 @@ use ra_ide_db::{ | |||
10 | }; | 11 | }; |
11 | use ra_prof::profile; | 12 | use ra_prof::profile; |
12 | use ra_syntax::{ | 13 | use ra_syntax::{ |
13 | ast::{self, HasFormatSpecifier, HasQuotes, HasStringValue}, | 14 | ast::{self, HasFormatSpecifier}, |
14 | AstNode, AstToken, Direction, NodeOrToken, SyntaxElement, | 15 | AstNode, AstToken, Direction, NodeOrToken, SyntaxElement, |
15 | SyntaxKind::*, | 16 | SyntaxKind::*, |
16 | SyntaxToken, TextRange, WalkEvent, T, | 17 | TextRange, WalkEvent, T, |
17 | }; | 18 | }; |
18 | use rustc_hash::FxHashMap; | 19 | use rustc_hash::FxHashMap; |
19 | 20 | ||
20 | use crate::{call_info::ActiveParameter, Analysis, FileId}; | 21 | use crate::FileId; |
21 | 22 | ||
22 | use ast::FormatSpecifier; | 23 | use ast::FormatSpecifier; |
23 | pub(crate) use html::highlight_as_html; | 24 | pub(crate) use html::highlight_as_html; |
@@ -123,6 +124,23 @@ pub(crate) fn highlight( | |||
123 | _ => (), | 124 | _ => (), |
124 | } | 125 | } |
125 | 126 | ||
127 | // Check for Rust code in documentation | ||
128 | match &event { | ||
129 | WalkEvent::Leave(NodeOrToken::Node(node)) => { | ||
130 | if let Some((doctest, range_mapping, new_comments)) = | ||
131 | injection::extract_doc_comments(node) | ||
132 | { | ||
133 | injection::highlight_doc_comment( | ||
134 | doctest, | ||
135 | range_mapping, | ||
136 | new_comments, | ||
137 | &mut stack, | ||
138 | ); | ||
139 | } | ||
140 | } | ||
141 | _ => (), | ||
142 | } | ||
143 | |||
126 | let element = match event { | 144 | let element = match event { |
127 | WalkEvent::Enter(it) => it, | 145 | WalkEvent::Enter(it) => it, |
128 | WalkEvent::Leave(_) => continue, | 146 | WalkEvent::Leave(_) => continue, |
@@ -173,7 +191,7 @@ pub(crate) fn highlight( | |||
173 | 191 | ||
174 | if let Some(token) = element.as_token().cloned().and_then(ast::RawString::cast) { | 192 | if let Some(token) = element.as_token().cloned().and_then(ast::RawString::cast) { |
175 | let expanded = element_to_highlight.as_token().unwrap().clone(); | 193 | let expanded = element_to_highlight.as_token().unwrap().clone(); |
176 | if highlight_injection(&mut stack, &sema, token, expanded).is_some() { | 194 | if injection::highlight_injection(&mut stack, &sema, token, expanded).is_some() { |
177 | continue; | 195 | continue; |
178 | } | 196 | } |
179 | } | 197 | } |
@@ -259,9 +277,8 @@ impl HighlightedRangeStack { | |||
259 | let mut parent = prev.pop().unwrap(); | 277 | let mut parent = prev.pop().unwrap(); |
260 | for ele in children { | 278 | for ele in children { |
261 | assert!(parent.range.contains_range(ele.range)); | 279 | assert!(parent.range.contains_range(ele.range)); |
262 | let mut cloned = parent.clone(); | 280 | |
263 | parent.range = TextRange::new(parent.range.start(), ele.range.start()); | 281 | let cloned = Self::intersect(&mut parent, &ele); |
264 | cloned.range = TextRange::new(ele.range.end(), cloned.range.end()); | ||
265 | if !parent.range.is_empty() { | 282 | if !parent.range.is_empty() { |
266 | prev.push(parent); | 283 | prev.push(parent); |
267 | } | 284 | } |
@@ -274,6 +291,62 @@ impl HighlightedRangeStack { | |||
274 | } | 291 | } |
275 | } | 292 | } |
276 | 293 | ||
294 | /// Intersects the `HighlightedRange` `parent` with `child`. | ||
295 | /// `parent` is mutated in place, becoming the range before `child`. | ||
296 | /// Returns the range (of the same type as `parent`) *after* `child`. | ||
297 | fn intersect(parent: &mut HighlightedRange, child: &HighlightedRange) -> HighlightedRange { | ||
298 | assert!(parent.range.contains_range(child.range)); | ||
299 | |||
300 | let mut cloned = parent.clone(); | ||
301 | parent.range = TextRange::new(parent.range.start(), child.range.start()); | ||
302 | cloned.range = TextRange::new(child.range.end(), cloned.range.end()); | ||
303 | |||
304 | cloned | ||
305 | } | ||
306 | |||
307 | /// Similar to `pop`, but can modify arbitrary prior ranges (where `pop`) | ||
308 | /// can only modify the last range currently on the stack. | ||
309 | /// Can be used to do injections that span multiple ranges, like the | ||
310 | /// doctest injection below. | ||
311 | /// If `delete` is set to true, the parent range is deleted instead of | ||
312 | /// intersected. | ||
313 | /// | ||
314 | /// Note that `pop` can be simulated by `pop_and_inject(false)` but the | ||
315 | /// latter is computationally more expensive. | ||
316 | fn pop_and_inject(&mut self, delete: bool) { | ||
317 | let mut children = self.stack.pop().unwrap(); | ||
318 | let prev = self.stack.last_mut().unwrap(); | ||
319 | children.sort_by_key(|range| range.range.start()); | ||
320 | prev.sort_by_key(|range| range.range.start()); | ||
321 | |||
322 | for child in children { | ||
323 | if let Some(idx) = | ||
324 | prev.iter().position(|parent| parent.range.contains_range(child.range)) | ||
325 | { | ||
326 | let cloned = Self::intersect(&mut prev[idx], &child); | ||
327 | let insert_idx = if delete || prev[idx].range.is_empty() { | ||
328 | prev.remove(idx); | ||
329 | idx | ||
330 | } else { | ||
331 | idx + 1 | ||
332 | }; | ||
333 | prev.insert(insert_idx, child); | ||
334 | if !delete && !cloned.range.is_empty() { | ||
335 | prev.insert(insert_idx + 1, cloned); | ||
336 | } | ||
337 | } else if let Some(_idx) = | ||
338 | prev.iter().position(|parent| parent.range.contains(child.range.start())) | ||
339 | { | ||
340 | unreachable!("child range should be completely contained in parent range"); | ||
341 | } else { | ||
342 | let idx = prev | ||
343 | .binary_search_by_key(&child.range.start(), |range| range.range.start()) | ||
344 | .unwrap_or_else(|x| x); | ||
345 | prev.insert(idx, child); | ||
346 | } | ||
347 | } | ||
348 | } | ||
349 | |||
277 | fn add(&mut self, range: HighlightedRange) { | 350 | fn add(&mut self, range: HighlightedRange) { |
278 | self.stack | 351 | self.stack |
279 | .last_mut() | 352 | .last_mut() |
@@ -539,42 +612,3 @@ fn highlight_name_by_syntax(name: ast::Name) -> Highlight { | |||
539 | 612 | ||
540 | tag.into() | 613 | tag.into() |
541 | } | 614 | } |
542 | |||
543 | fn highlight_injection( | ||
544 | acc: &mut HighlightedRangeStack, | ||
545 | sema: &Semantics<RootDatabase>, | ||
546 | literal: ast::RawString, | ||
547 | expanded: SyntaxToken, | ||
548 | ) -> Option<()> { | ||
549 | let active_parameter = ActiveParameter::at_token(&sema, expanded)?; | ||
550 | if !active_parameter.name.starts_with("ra_fixture") { | ||
551 | return None; | ||
552 | } | ||
553 | let value = literal.value()?; | ||
554 | let (analysis, tmp_file_id) = Analysis::from_single_file(value); | ||
555 | |||
556 | if let Some(range) = literal.open_quote_text_range() { | ||
557 | acc.add(HighlightedRange { | ||
558 | range, | ||
559 | highlight: HighlightTag::StringLiteral.into(), | ||
560 | binding_hash: None, | ||
561 | }) | ||
562 | } | ||
563 | |||
564 | for mut h in analysis.highlight(tmp_file_id).unwrap() { | ||
565 | if let Some(r) = literal.map_range_up(h.range) { | ||
566 | h.range = r; | ||
567 | acc.add(h) | ||
568 | } | ||
569 | } | ||
570 | |||
571 | if let Some(range) = literal.close_quote_text_range() { | ||
572 | acc.add(HighlightedRange { | ||
573 | range, | ||
574 | highlight: HighlightTag::StringLiteral.into(), | ||
575 | binding_hash: None, | ||
576 | }) | ||
577 | } | ||
578 | |||
579 | Some(()) | ||
580 | } | ||