1use std::iter::Iterator;
7use std::ops::Range;
8
9use super::selector::ScopeSelector;
10use super::style::{Color, FontStyle, Style, StyleModifier};
11use super::theme::{Theme, ThemeItem};
12use crate::parsing::{
13 BasicScopeStackOp, MatchPower, Scope, ScopeStack, ScopeStackOp, ATOM_LEN_BITS,
14};
15
16#[derive(Debug)]
27pub struct Highlighter<'a> {
28 theme: &'a Theme,
29 single_selectors: Vec<(Scope, StyleModifier)>,
32 multi_selectors: Vec<(ScopeSelector, StyleModifier)>,
33 }
35
36#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct HighlightState {
60 styles: Vec<Style>,
61 single_caches: Vec<ScoredStyle>,
62 pub path: ScopeStack,
63}
64
65#[derive(Debug)]
74pub struct RangedHighlightIterator<'a, 'b> {
75 index: usize,
76 pos: usize,
77 changes: &'a [(usize, ScopeStackOp)],
78 text: &'b str,
79 highlighter: &'a Highlighter<'a>,
80 state: &'a mut HighlightState,
81}
82
83#[derive(Debug)]
94pub struct HighlightIterator<'a, 'b> {
95 ranged_iterator: RangedHighlightIterator<'a, 'b>,
96}
97
98impl HighlightState {
99 #[cfg(feature = "owner-census")]
101 pub fn owned_heap_bytes(&self) -> Option<usize> {
102 self.styles
103 .capacity()
104 .checked_mul(std::mem::size_of::<Style>())?
105 .checked_add(
106 self.single_caches
107 .capacity()
108 .checked_mul(std::mem::size_of::<ScoredStyle>())?,
109 )?
110 .checked_add(self.path.owned_heap_bytes()?)
111 }
112
113 pub fn new(highlighter: &Highlighter<'_>, initial_stack: ScopeStack) -> HighlightState {
122 let mut styles = vec![highlighter.get_default()];
123 let mut single_caches = vec![ScoredStyle::from_style(styles[0])];
124 for i in 0..initial_stack.len() {
125 let prefix = initial_stack.bottom_n(i + 1);
126 let new_cache = highlighter.update_single_cache_for_push(&single_caches[i], prefix);
127 styles.push(highlighter.finalize_style_with_multis(&new_cache, prefix));
128 single_caches.push(new_cache);
129 }
130
131 HighlightState {
132 styles,
133 single_caches,
134 path: initial_stack,
135 }
136 }
137}
138
139impl<'a, 'b> RangedHighlightIterator<'a, 'b> {
140 pub fn new(
141 state: &'a mut HighlightState,
142 changes: &'a [(usize, ScopeStackOp)],
143 text: &'b str,
144 highlighter: &'a Highlighter<'_>,
145 ) -> RangedHighlightIterator<'a, 'b> {
146 RangedHighlightIterator {
147 index: 0,
148 pos: 0,
149 changes,
150 text,
151 highlighter,
152 state,
153 }
154 }
155}
156
157impl<'b> Iterator for RangedHighlightIterator<'_, 'b> {
158 type Item = (Style, &'b str, Range<usize>);
159
160 fn next(&mut self) -> Option<(Style, &'b str, Range<usize>)> {
163 if self.pos == self.text.len() && self.index >= self.changes.len() {
164 return None;
165 }
166 let (end, command) = if self.index < self.changes.len() {
167 self.changes[self.index].clone()
168 } else {
169 (self.text.len(), ScopeStackOp::Noop)
170 };
171 let style = *self.state.styles.last().unwrap_or(&Style::default());
173 let text = &self.text[self.pos..end];
174 let range = Range {
175 start: self.pos,
176 end,
177 };
178 {
179 let m_path = &mut self.state.path;
181 let m_styles = &mut self.state.styles;
182 let m_caches = &mut self.state.single_caches;
183 let highlighter = &self.highlighter;
184 m_path
185 .apply_with_hook(&command, |op, cur_stack| {
186 match op {
188 BasicScopeStackOp::Push(_) => {
189 let new_cache = {
191 if let Some(prev_cache) = m_caches.last() {
192 highlighter.update_single_cache_for_push(prev_cache, cur_stack)
193 } else {
194 highlighter.update_single_cache_for_push(
195 &ScoredStyle::from_style(highlighter.get_default()),
196 cur_stack,
197 )
198 }
199 };
200 m_styles.push(
201 highlighter.finalize_style_with_multis(&new_cache, cur_stack),
202 );
203 m_caches.push(new_cache);
204 }
205 BasicScopeStackOp::Pop => {
206 m_styles.pop();
207 m_caches.pop();
208 }
209 }
210 })
211 .ok()?;
212 }
213 self.pos = end;
214 self.index += 1;
215 if text.is_empty() {
216 self.next()
217 } else {
218 Some((style, text, range))
219 }
220 }
221}
222impl<'a, 'b> HighlightIterator<'a, 'b> {
223 pub fn new(
224 state: &'a mut HighlightState,
225 changes: &'a [(usize, ScopeStackOp)],
226 text: &'b str,
227 highlighter: &'a Highlighter<'_>,
228 ) -> HighlightIterator<'a, 'b> {
229 HighlightIterator {
230 ranged_iterator: RangedHighlightIterator {
231 index: 0,
232 pos: 0,
233 changes,
234 text,
235 highlighter,
236 state,
237 },
238 }
239 }
240}
241
242impl<'b> Iterator for HighlightIterator<'_, 'b> {
243 type Item = (Style, &'b str);
244
245 fn next(&mut self) -> Option<(Style, &'b str)> {
248 self.ranged_iterator.next().map(|e| (e.0, e.1))
249 }
250}
251
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct ScoredStyle {
254 pub foreground: (MatchPower, Color),
255 pub background: (MatchPower, Color),
256 pub font_style: (MatchPower, FontStyle),
257}
258
259#[inline]
260fn update_scored<T: Clone>(scored: &mut (MatchPower, T), update: &Option<T>, score: MatchPower) {
261 if score > scored.0 {
262 if let Some(u) = update {
263 scored.0 = score;
264 scored.1 = u.clone();
265 }
266 }
267}
268
269impl ScoredStyle {
270 fn apply(&mut self, other: &StyleModifier, score: MatchPower) {
271 update_scored(&mut self.foreground, &other.foreground, score);
272 update_scored(&mut self.background, &other.background, score);
273 update_scored(&mut self.font_style, &other.font_style, score);
274 }
275
276 fn to_style(&self) -> Style {
277 Style {
278 foreground: self.foreground.1,
279 background: self.background.1,
280 font_style: self.font_style.1,
281 }
282 }
283
284 fn from_style(style: Style) -> ScoredStyle {
285 ScoredStyle {
286 foreground: (MatchPower(-1.0), style.foreground),
287 background: (MatchPower(-1.0), style.background),
288 font_style: (MatchPower(-1.0), style.font_style),
289 }
290 }
291}
292
293impl<'a> Highlighter<'a> {
294 pub fn new(theme: &'a Theme) -> Highlighter<'a> {
295 let mut single_selectors = Vec::new();
296 let mut multi_selectors = Vec::new();
297 for item in &theme.scopes {
298 for sel in &item.scope.selectors {
299 if let Some(scope) = sel.extract_single_scope() {
300 single_selectors.push((scope, item.style));
301 } else {
302 multi_selectors.push((sel.clone(), item.style));
303 }
304 }
305 }
306 single_selectors.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
308
309 Highlighter {
310 theme,
311 single_selectors,
312 multi_selectors,
313 }
314 }
315
316 pub fn get_default(&self) -> Style {
319 Style {
320 foreground: self.theme.settings.foreground.unwrap_or(Color::BLACK),
321 background: self.theme.settings.background.unwrap_or(Color::WHITE),
322 font_style: FontStyle::empty(),
323 }
324 }
325
326 fn update_single_cache_for_push(&self, cur: &ScoredStyle, path: &[Scope]) -> ScoredStyle {
327 let mut new_style = cur.clone();
328
329 let last_scope = path[path.len() - 1];
330 for &(scope, ref modif) in self
331 .single_selectors
332 .iter()
333 .filter(|a| a.0.is_prefix_of(last_scope))
334 {
335 let single_score = f64::from(scope.len())
336 * f64::from(ATOM_LEN_BITS * ((path.len() - 1) as u16)).exp2();
337 new_style.apply(modif, MatchPower(single_score));
338 }
339
340 new_style
341 }
342
343 fn finalize_style_with_multis(&self, cur: &ScoredStyle, path: &[Scope]) -> Style {
344 let mut new_style = cur.clone();
345
346 let mult_iter = self
347 .multi_selectors
348 .iter()
349 .filter_map(|(sel, style)| sel.does_match(path).map(|score| (score, style)));
350 for (score, modif) in mult_iter {
351 new_style.apply(modif, score);
352 }
353
354 new_style.to_style()
355 }
356
357 pub fn style_for_stack(&self, stack: &[Scope]) -> Style {
362 let mut single_cache = ScoredStyle::from_style(self.get_default());
363 for i in 0..stack.len() {
364 single_cache = self.update_single_cache_for_push(&single_cache, &stack[0..i + 1]);
365 }
366 self.finalize_style_with_multis(&single_cache, stack)
367 }
368
369 pub fn style_mod_for_stack(&self, path: &[Scope]) -> StyleModifier {
381 let mut matching_items: Vec<(MatchPower, &ThemeItem)> = self
382 .theme
383 .scopes
384 .iter()
385 .filter_map(|item| item.scope.does_match(path).map(|score| (score, item)))
386 .collect();
387 matching_items.sort_by_key(|&(score, _)| score);
388 let sorted = matching_items.iter().map(|(_, item)| item);
389
390 let mut modifier = StyleModifier {
391 background: None,
392 foreground: None,
393 font_style: None,
394 };
395 for item in sorted {
396 modifier = modifier.apply(item.style);
397 }
398 modifier
399 }
400}
401
402#[cfg(all(feature = "default-syntaxes", feature = "default-themes"))]
403#[cfg(test)]
404mod tests {
405 use super::*;
406 use crate::highlighting::{Color, FontStyle, Style, ThemeSet};
407 use crate::parsing::{ParseState, ScopeStack};
408 use crate::utils::testdata;
409
410 #[test]
411 fn can_parse() {
412 let ps = &*testdata::PACKAGES_SYN_SET;
413 let mut state = {
414 let syntax = ps.find_syntax_by_name("Ruby on Rails").unwrap();
415 ParseState::new(syntax)
416 };
417 let ts = ThemeSet::load_defaults();
418 let highlighter = Highlighter::new(&ts.themes["base16-ocean.dark"]);
419
420 let mut highlight_state = HighlightState::new(&highlighter, ScopeStack::new());
421 let line = "module Bob::Wow::Troll::Five; 5; end";
422 let ops = state.parse_line(line, &ps).expect("#[cfg(test)]");
423 let iter = HighlightIterator::new(&mut highlight_state, &ops[..], line, &highlighter);
424 let regions: Vec<(Style, &str)> = iter.collect();
425 assert_eq!(
427 regions[11],
428 (
429 Style {
430 foreground: Color {
431 r: 208,
432 g: 135,
433 b: 112,
434 a: 0xFF,
435 },
436 background: Color {
437 r: 43,
438 g: 48,
439 b: 59,
440 a: 0xFF,
441 },
442 font_style: FontStyle::empty(),
443 },
444 "5"
445 )
446 );
447 }
448
449 #[test]
450 fn can_parse_with_highlight_state_from_cache() {
451 let ps = &*testdata::PACKAGES_SYN_SET;
452 let mut state = {
453 let syntax = ps
454 .find_syntax_by_scope(Scope::new("source.python").unwrap())
455 .unwrap();
456 ParseState::new(syntax)
457 };
458 let ts = ThemeSet::load_defaults();
459 let highlighter = Highlighter::new(&ts.themes["base16-ocean.dark"]);
460
461 let mut highlight_state = HighlightState::new(&highlighter, ScopeStack::new());
463 let line = r#"""""#;
464 let ops = state.parse_line(line, &ps).expect("#[cfg(test)]");
465 let iter = HighlightIterator::new(&mut highlight_state, &ops[..], line, &highlighter);
466 assert_eq!(1, iter.count());
467 let path = highlight_state.path;
468
469 let mut highlight_state = HighlightState::new(&highlighter, path);
471 let line = "multiline comment";
472 let ops = state.parse_line(line, &ps).expect("#[cfg(test)]");
473 let iter = HighlightIterator::new(&mut highlight_state, &ops[..], line, &highlighter);
474 let regions: Vec<(Style, &str)> = iter.collect();
475
476 assert_eq!(
478 regions[0],
479 (
480 Style {
481 foreground: Color {
482 r: 101,
484 g: 115,
485 b: 126,
486 a: 0xFF,
487 },
488 background: Color {
489 r: 43,
490 g: 48,
491 b: 59,
492 a: 0xFF,
493 },
494 font_style: FontStyle::empty(),
495 },
496 "multiline comment"
497 )
498 );
499 }
500
501 #[test]
503 fn tricky_cases() {
504 use crate::highlighting::{ScopeSelectors, ThemeSettings};
505 use crate::parsing::ScopeStack;
506 use std::str::FromStr;
507 let c1 = Color {
508 r: 1,
509 g: 1,
510 b: 1,
511 a: 255,
512 };
513 let c2 = Color {
514 r: 2,
515 g: 2,
516 b: 2,
517 a: 255,
518 };
519 let def_bg = Color {
520 r: 255,
521 g: 255,
522 b: 255,
523 a: 255,
524 };
525 let test_color_scheme = Theme {
526 name: None,
527 author: None,
528 settings: ThemeSettings::default(),
529 scopes: vec![
530 ThemeItem {
531 scope: ScopeSelectors::from_str("comment.line").unwrap(),
532 style: StyleModifier {
533 foreground: Some(c1),
534 background: None,
535 font_style: None,
536 },
537 },
538 ThemeItem {
539 scope: ScopeSelectors::from_str("comment").unwrap(),
540 style: StyleModifier {
541 foreground: Some(c2),
542 background: None,
543 font_style: Some(FontStyle::ITALIC),
544 },
545 },
546 ThemeItem {
547 scope: ScopeSelectors::from_str("comment.line.rs - keyword").unwrap(),
548 style: StyleModifier {
549 foreground: None,
550 background: Some(c1),
551 font_style: None,
552 },
553 },
554 ThemeItem {
555 scope: ScopeSelectors::from_str("no.match").unwrap(),
556 style: StyleModifier {
557 foreground: None,
558 background: Some(c2),
559 font_style: Some(FontStyle::UNDERLINE),
560 },
561 },
562 ],
563 };
564 let highlighter = Highlighter::new(&test_color_scheme);
565
566 use crate::parsing::ScopeStackOp::*;
567 let ops = [
568 (0, Push(Scope::new("comment.line.rs").unwrap())),
570 (1, Push(Scope::new("keyword.control.rs").unwrap())),
572 (2, Pop(1)),
573 ];
574
575 let mut highlight_state = HighlightState::new(&highlighter, ScopeStack::new());
576 let iter = HighlightIterator::new(&mut highlight_state, &ops[..], "abcdef", &highlighter);
577 let regions: Vec<Style> = iter.map(|(s, _)| s).collect();
578
579 assert_eq!(
581 regions,
582 vec![
583 Style {
584 foreground: c1,
585 background: c1,
586 font_style: FontStyle::ITALIC
587 },
588 Style {
589 foreground: c1,
590 background: def_bg,
591 font_style: FontStyle::ITALIC
592 },
593 Style {
594 foreground: c1,
595 background: c1,
596 font_style: FontStyle::ITALIC
597 },
598 ]
599 );
600
601 let full_stack = ScopeStack::from_str("comment.line.rs keyword.control.rs").unwrap();
602 let full_style = highlighter.style_for_stack(full_stack.as_slice());
603 assert_eq!(
604 full_style,
605 Style {
606 foreground: c1,
607 background: def_bg,
608 font_style: FontStyle::ITALIC
609 }
610 );
611 let full_mod = highlighter.style_mod_for_stack(full_stack.as_slice());
612 assert_eq!(
613 full_mod,
614 StyleModifier {
615 foreground: Some(c1),
616 background: None,
617 font_style: Some(FontStyle::ITALIC)
618 }
619 );
620 }
621
622 #[test]
623 fn test_ranges() {
624 let ps = &*testdata::PACKAGES_SYN_SET;
625 let mut state = {
626 let syntax = ps.find_syntax_by_name("Ruby on Rails").unwrap();
627 ParseState::new(syntax)
628 };
629 let ts = ThemeSet::load_defaults();
630 let highlighter = Highlighter::new(&ts.themes["base16-ocean.dark"]);
631
632 let mut highlight_state = HighlightState::new(&highlighter, ScopeStack::new());
633 let line = "module Bob::Wow::Troll::Five; 5; end";
634 let ops = state.parse_line(line, &ps).expect("#[cfg(test)]");
635 let iter = RangedHighlightIterator::new(&mut highlight_state, &ops[..], line, &highlighter);
636 let regions: Vec<(Style, &str, Range<usize>)> = iter.collect();
637 assert_eq!(
639 regions[11],
640 (
641 Style {
642 foreground: Color {
643 r: 208,
644 g: 135,
645 b: 112,
646 a: 0xFF,
647 },
648 background: Color {
649 r: 43,
650 g: 48,
651 b: 59,
652 a: 0xFF,
653 },
654 font_style: FontStyle::empty(),
655 },
656 "5",
657 Range { start: 30, end: 31 }
658 )
659 );
660 }
661}