diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2021-01-08 20:48:30 +0000 |
---|---|---|
committer | GitHub <[email protected]> | 2021-01-08 20:48:30 +0000 |
commit | 056cabf25d3ef7a96220f9f3a6f904b2841feab6 (patch) | |
tree | f2c780887fb41f87ef0872308501b09775275667 /crates/ide/src/syntax_highlighting.rs | |
parent | 903d1f89a51254b92ae6d1776d118a32a38acdf6 (diff) | |
parent | e30c1c3fbf8f70336d985b2b73e5b0f45f3b95f5 (diff) |
Merge #7212
7212: Simplify highlighting r=matklad a=matklad
Co-authored-by: Aleksey Kladov <[email protected]>
Diffstat (limited to 'crates/ide/src/syntax_highlighting.rs')
-rw-r--r-- | crates/ide/src/syntax_highlighting.rs | 194 |
1 files changed, 8 insertions, 186 deletions
diff --git a/crates/ide/src/syntax_highlighting.rs b/crates/ide/src/syntax_highlighting.rs index ba0085244..2eb63a0b7 100644 --- a/crates/ide/src/syntax_highlighting.rs +++ b/crates/ide/src/syntax_highlighting.rs | |||
@@ -1,3 +1,6 @@ | |||
1 | mod highlights; | ||
2 | mod injector; | ||
3 | |||
1 | mod format; | 4 | mod format; |
2 | mod html; | 5 | mod html; |
3 | mod injection; | 6 | mod injection; |
@@ -69,9 +72,7 @@ pub(crate) fn highlight( | |||
69 | }; | 72 | }; |
70 | 73 | ||
71 | let mut bindings_shadow_count: FxHashMap<Name, u32> = FxHashMap::default(); | 74 | let mut bindings_shadow_count: FxHashMap<Name, u32> = FxHashMap::default(); |
72 | // We use a stack for the DFS traversal below. | 75 | let mut stack = highlights::Highlights::new(range_to_highlight); |
73 | // When we leave a node, the we use it to flatten the highlighted ranges. | ||
74 | let mut stack = HighlightedRangeStack::new(); | ||
75 | 76 | ||
76 | let mut current_macro_call: Option<ast::MacroCall> = None; | 77 | let mut current_macro_call: Option<ast::MacroCall> = None; |
77 | let mut current_macro_rules: Option<ast::MacroRules> = None; | 78 | let mut current_macro_rules: Option<ast::MacroRules> = None; |
@@ -82,14 +83,8 @@ pub(crate) fn highlight( | |||
82 | // Walk all nodes, keeping track of whether we are inside a macro or not. | 83 | // Walk all nodes, keeping track of whether we are inside a macro or not. |
83 | // If in macro, expand it first and highlight the expanded code. | 84 | // If in macro, expand it first and highlight the expanded code. |
84 | for event in root.preorder_with_tokens() { | 85 | for event in root.preorder_with_tokens() { |
85 | match &event { | ||
86 | WalkEvent::Enter(_) => stack.push(), | ||
87 | WalkEvent::Leave(_) => stack.pop(), | ||
88 | }; | ||
89 | |||
90 | let event_range = match &event { | 86 | let event_range = match &event { |
91 | WalkEvent::Enter(it) => it.text_range(), | 87 | WalkEvent::Enter(it) | WalkEvent::Leave(it) => it.text_range(), |
92 | WalkEvent::Leave(it) => it.text_range(), | ||
93 | }; | 88 | }; |
94 | 89 | ||
95 | // Element outside of the viewport, no need to highlight | 90 | // Element outside of the viewport, no need to highlight |
@@ -138,15 +133,8 @@ pub(crate) fn highlight( | |||
138 | if ast::Attr::can_cast(node.kind()) { | 133 | if ast::Attr::can_cast(node.kind()) { |
139 | inside_attribute = false | 134 | inside_attribute = false |
140 | } | 135 | } |
141 | if let Some((doctest, range_mapping, new_comments)) = | 136 | if let Some((new_comments, inj)) = injection::extract_doc_comments(node) { |
142 | injection::extract_doc_comments(node) | 137 | injection::highlight_doc_comment(new_comments, inj, &mut stack); |
143 | { | ||
144 | injection::highlight_doc_comment( | ||
145 | doctest, | ||
146 | range_mapping, | ||
147 | new_comments, | ||
148 | &mut stack, | ||
149 | ); | ||
150 | } | 138 | } |
151 | } | 139 | } |
152 | WalkEvent::Enter(NodeOrToken::Node(node)) if ast::Attr::can_cast(node.kind()) => { | 140 | WalkEvent::Enter(NodeOrToken::Node(node)) if ast::Attr::can_cast(node.kind()) => { |
@@ -217,7 +205,6 @@ pub(crate) fn highlight( | |||
217 | format_string_highlighter.highlight_format_string(&mut stack, &string, range); | 205 | format_string_highlighter.highlight_format_string(&mut stack, &string, range); |
218 | // Highlight escape sequences | 206 | // Highlight escape sequences |
219 | if let Some(char_ranges) = string.char_ranges() { | 207 | if let Some(char_ranges) = string.char_ranges() { |
220 | stack.push(); | ||
221 | for (piece_range, _) in char_ranges.iter().filter(|(_, char)| char.is_ok()) { | 208 | for (piece_range, _) in char_ranges.iter().filter(|(_, char)| char.is_ok()) { |
222 | if string.text()[piece_range.start().into()..].starts_with('\\') { | 209 | if string.text()[piece_range.start().into()..].starts_with('\\') { |
223 | stack.add(HighlightedRange { | 210 | stack.add(HighlightedRange { |
@@ -227,177 +214,12 @@ pub(crate) fn highlight( | |||
227 | }); | 214 | }); |
228 | } | 215 | } |
229 | } | 216 | } |
230 | stack.pop_and_inject(None); | ||
231 | } | ||
232 | } | ||
233 | } | ||
234 | } | ||
235 | |||
236 | stack.flattened() | ||
237 | } | ||
238 | |||
239 | #[derive(Debug)] | ||
240 | struct HighlightedRangeStack { | ||
241 | stack: Vec<Vec<HighlightedRange>>, | ||
242 | } | ||
243 | |||
244 | /// We use a stack to implement the flattening logic for the highlighted | ||
245 | /// syntax ranges. | ||
246 | impl HighlightedRangeStack { | ||
247 | fn new() -> Self { | ||
248 | Self { stack: vec![Vec::new()] } | ||
249 | } | ||
250 | |||
251 | fn push(&mut self) { | ||
252 | self.stack.push(Vec::new()); | ||
253 | } | ||
254 | |||
255 | /// Flattens the highlighted ranges. | ||
256 | /// | ||
257 | /// For example `#[cfg(feature = "foo")]` contains the nested ranges: | ||
258 | /// 1) parent-range: Attribute [0, 23) | ||
259 | /// 2) child-range: String [16, 21) | ||
260 | /// | ||
261 | /// The following code implements the flattening, for our example this results to: | ||
262 | /// `[Attribute [0, 16), String [16, 21), Attribute [21, 23)]` | ||
263 | fn pop(&mut self) { | ||
264 | let children = self.stack.pop().unwrap(); | ||
265 | let prev = self.stack.last_mut().unwrap(); | ||
266 | let needs_flattening = !children.is_empty() | ||
267 | && !prev.is_empty() | ||
268 | && prev.last().unwrap().range.contains_range(children.first().unwrap().range); | ||
269 | if !needs_flattening { | ||
270 | prev.extend(children); | ||
271 | } else { | ||
272 | let mut parent = prev.pop().unwrap(); | ||
273 | for ele in children { | ||
274 | assert!(parent.range.contains_range(ele.range)); | ||
275 | |||
276 | let cloned = Self::intersect(&mut parent, &ele); | ||
277 | if !parent.range.is_empty() { | ||
278 | prev.push(parent); | ||
279 | } | ||
280 | prev.push(ele); | ||
281 | parent = cloned; | ||
282 | } | ||
283 | if !parent.range.is_empty() { | ||
284 | prev.push(parent); | ||
285 | } | ||
286 | } | ||
287 | } | ||
288 | |||
289 | /// Intersects the `HighlightedRange` `parent` with `child`. | ||
290 | /// `parent` is mutated in place, becoming the range before `child`. | ||
291 | /// Returns the range (of the same type as `parent`) *after* `child`. | ||
292 | fn intersect(parent: &mut HighlightedRange, child: &HighlightedRange) -> HighlightedRange { | ||
293 | assert!(parent.range.contains_range(child.range)); | ||
294 | |||
295 | let mut cloned = parent.clone(); | ||
296 | parent.range = TextRange::new(parent.range.start(), child.range.start()); | ||
297 | cloned.range = TextRange::new(child.range.end(), cloned.range.end()); | ||
298 | |||
299 | cloned | ||
300 | } | ||
301 | |||
302 | /// Remove the `HighlightRange` of `parent` that's currently covered by `child`. | ||
303 | fn intersect_partial(parent: &mut HighlightedRange, child: &HighlightedRange) { | ||
304 | assert!( | ||
305 | parent.range.start() <= child.range.start() | ||
306 | && parent.range.end() >= child.range.start() | ||
307 | && child.range.end() > parent.range.end() | ||
308 | ); | ||
309 | |||
310 | parent.range = TextRange::new(parent.range.start(), child.range.start()); | ||
311 | } | ||
312 | |||
313 | /// Similar to `pop`, but can modify arbitrary prior ranges (where `pop`) | ||
314 | /// can only modify the last range currently on the stack. | ||
315 | /// Can be used to do injections that span multiple ranges, like the | ||
316 | /// doctest injection below. | ||
317 | /// If `overwrite_parent` is non-optional, the highlighting of the parent range | ||
318 | /// is overwritten with the argument. | ||
319 | /// | ||
320 | /// Note that `pop` can be simulated by `pop_and_inject(false)` but the | ||
321 | /// latter is computationally more expensive. | ||
322 | fn pop_and_inject(&mut self, overwrite_parent: Option<Highlight>) { | ||
323 | let mut children = self.stack.pop().unwrap(); | ||
324 | let prev = self.stack.last_mut().unwrap(); | ||
325 | children.sort_by_key(|range| range.range.start()); | ||
326 | prev.sort_by_key(|range| range.range.start()); | ||
327 | |||
328 | for child in children { | ||
329 | if let Some(idx) = | ||
330 | prev.iter().position(|parent| parent.range.contains_range(child.range)) | ||
331 | { | ||
332 | if let Some(tag) = overwrite_parent { | ||
333 | prev[idx].highlight = tag; | ||
334 | } | ||
335 | |||
336 | let cloned = Self::intersect(&mut prev[idx], &child); | ||
337 | let insert_idx = if prev[idx].range.is_empty() { | ||
338 | prev.remove(idx); | ||
339 | idx | ||
340 | } else { | ||
341 | idx + 1 | ||
342 | }; | ||
343 | prev.insert(insert_idx, child); | ||
344 | if !cloned.range.is_empty() { | ||
345 | prev.insert(insert_idx + 1, cloned); | ||
346 | } | ||
347 | } else { | ||
348 | let maybe_idx = | ||
349 | prev.iter().position(|parent| parent.range.contains(child.range.start())); | ||
350 | match (overwrite_parent, maybe_idx) { | ||
351 | (Some(_), Some(idx)) => { | ||
352 | Self::intersect_partial(&mut prev[idx], &child); | ||
353 | let insert_idx = if prev[idx].range.is_empty() { | ||
354 | prev.remove(idx); | ||
355 | idx | ||
356 | } else { | ||
357 | idx + 1 | ||
358 | }; | ||
359 | prev.insert(insert_idx, child); | ||
360 | } | ||
361 | (_, None) => { | ||
362 | let idx = prev | ||
363 | .binary_search_by_key(&child.range.start(), |range| range.range.start()) | ||
364 | .unwrap_or_else(|x| x); | ||
365 | prev.insert(idx, child); | ||
366 | } | ||
367 | _ => { | ||
368 | unreachable!("child range should be completely contained in parent range"); | ||
369 | } | ||
370 | } | 217 | } |
371 | } | 218 | } |
372 | } | 219 | } |
373 | } | 220 | } |
374 | 221 | ||
375 | fn add(&mut self, range: HighlightedRange) { | 222 | stack.to_vec() |
376 | self.stack | ||
377 | .last_mut() | ||
378 | .expect("during DFS traversal, the stack must not be empty") | ||
379 | .push(range) | ||
380 | } | ||
381 | |||
382 | fn flattened(mut self) -> Vec<HighlightedRange> { | ||
383 | assert_eq!( | ||
384 | self.stack.len(), | ||
385 | 1, | ||
386 | "after DFS traversal, the stack should only contain a single element" | ||
387 | ); | ||
388 | let mut res = self.stack.pop().unwrap(); | ||
389 | res.sort_by_key(|range| range.range.start()); | ||
390 | // Check that ranges are sorted and disjoint | ||
391 | for (left, right) in res.iter().zip(res.iter().skip(1)) { | ||
392 | assert!( | ||
393 | left.range.end() <= right.range.start(), | ||
394 | "left: {:#?}, right: {:#?}", | ||
395 | left, | ||
396 | right | ||
397 | ); | ||
398 | } | ||
399 | res | ||
400 | } | ||
401 | } | 223 | } |
402 | 224 | ||
403 | fn macro_call_range(macro_call: &ast::MacroCall) -> Option<TextRange> { | 225 | fn macro_call_range(macro_call: &ast::MacroCall) -> Option<TextRange> { |