aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src/syntax_highlighting.rs
diff options
context:
space:
mode:
authorAleksey Kladov <[email protected]>2021-01-07 22:39:02 +0000
committerAleksey Kladov <[email protected]>2021-01-08 20:47:35 +0000
commite30c1c3fbf8f70336d985b2b73e5b0f45f3b95f5 (patch)
treea3cdc2d2f667ab5a122758152eb338a654d387cd /crates/ide/src/syntax_highlighting.rs
parent981a0d708ec352969f9ca075a3e0e50c6da48197 (diff)
Simplify highlighting infra
This also fixes the killer whale bug
Diffstat (limited to 'crates/ide/src/syntax_highlighting.rs')
-rw-r--r--crates/ide/src/syntax_highlighting.rs194
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 @@
1mod highlights;
2mod injector;
3
1mod format; 4mod format;
2mod html; 5mod html;
3mod injection; 6mod 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)]
240struct HighlightedRangeStack {
241 stack: Vec<Vec<HighlightedRange>>,
242}
243
244/// We use a stack to implement the flattening logic for the highlighted
245/// syntax ranges.
246impl 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
403fn macro_call_range(macro_call: &ast::MacroCall) -> Option<TextRange> { 225fn macro_call_range(macro_call: &ast::MacroCall) -> Option<TextRange> {