diff --git a/Cargo.toml b/Cargo.toml index d626337..31cb8d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,7 @@ regex = "1.11" anyhow = "1.0" dirs = "5.0" thiserror = "2" +tree-sitter = "0.24.6" +tree-sitter-highlight = "0.24.6" +tree-sitter-regex = "0.24.3" # clap = { version = "4.5", features = ["derive"] } diff --git a/src/highlight.rs b/src/highlight.rs new file mode 100644 index 0000000..2230fc5 --- /dev/null +++ b/src/highlight.rs @@ -0,0 +1,182 @@ +use crossterm::style::Color; +use tree_sitter::Parser; +use tree_sitter_highlight::{ + Error, Highlight, HighlightConfiguration, HighlightEvent, Highlighter, +}; + +#[derive(PartialOrd, PartialEq, Eq, Ord, Clone)] +// Order is Priotity when multiple groups are active +enum HighlightGroup { + Flags, + Anchors, + Quantifiers, + CharacterClass, + Operator, + Escape, + Group, +} + +macro_rules! group_data { + ($group:ident, $group_name:expr, $color:expr, [$($token:expr),*]) => { + HighlightGroupData { + group: HighlightGroup::$group, + group_name: $group_name, + color: $color, + query: { + const MATCHES: &str = concat!( + "[ ", + $(concat!("(", $token, ") "),)* + "]" + ); + Some(MATCHES) + } + } + }; + ($group:ident, $group_name:expr, $color:expr) => { + HighlightGroupData { + group: HighlightGroup::$group, + group_name: $group_name, + color: $color, + query: None, + } + }; +} + +struct HighlightGroupData { + group: HighlightGroup, + group_name: &'static str, + color: Color, + query: Option<&'static str>, +} + +impl HighlightGroup { + fn all() -> &'static [HighlightGroupData] { + &[ + // if no token used, then the group definition comes from tree_sitter_regex::HIGHLIGHTS_QUERY + group_data!(Flags, "flags", Color::Blue, ["flags", "inline_flags_group"]), + group_data!(Anchors, "anchors", Color::Magenta, [ + "start_assertion", + "end_assertion", + "boundary_assertion", + "non_boundary_assertion" + ]), + group_data!(Quantifiers, "quantifiers", Color::DarkMagenta, [ + "one_or_more", + "optional", + "zero_or_more", + "count_quantifier" + ]), + group_data!(CharacterClass, "character_class", Color::DarkBlue, [ + "character_class_escape", + "character_class" + ]), + group_data!(Operator, "operator", Color::DarkYellow), + group_data!(Escape, "escape", Color::Black), + group_data!(Group, "property", Color::Black), + ] + } + + fn data(&self) -> &'static HighlightGroupData { + Self::all().iter().find(|x| &x.group == self).unwrap() + } + + fn color(&self) -> Color { + self.data().color + } + + fn group_name(&self) -> &'static str { + self.data().group_name + } + + fn group_names() -> Vec<&'static str> { + Self::all().iter().map(|x| x.group_name).collect() + } + + fn query(&self) -> Option { + let data = self.data(); + data.query.map(|q| format!("{} @{}", q, self.group_name())) + } +} + +fn custom_queries() -> String { + HighlightGroup::all() + .iter() + .flat_map(|x| x.group.query()) + .collect::>() + .join("\n") +} + +fn highlight_configuration() -> Result { + let mut parser = Parser::new(); + parser + .set_language(&tree_sitter_regex::LANGUAGE.into()) + .map_err(|_| Error::InvalidLanguage)?; + + let highlights_query = [tree_sitter_regex::HIGHLIGHTS_QUERY, &custom_queries()].join("\n"); + + let mut highlight_configuration = HighlightConfiguration::new( + tree_sitter_regex::LANGUAGE.into(), + "regex", + &highlights_query, + "", + "", + ) + .map_err(|_| Error::Unknown)?; + + let hightlight_groups = HighlightGroup::group_names(); + + highlight_configuration.configure(&hightlight_groups); + Ok(highlight_configuration) +} + +#[derive(Default)] +pub struct HighlightEventWrapper { + iter: std::vec::IntoIter>, + pos: usize, + limit: usize, + stack: Vec, +} + +impl HighlightEventWrapper { + pub fn new(re: &[u8]) -> Result { + let mut highlighter = Highlighter::new(); + let config = highlight_configuration()?; + let highlights = highlighter.highlight(&config, re, None, |_| None)?; + Ok(HighlightEventWrapper { + iter: highlights.collect::>().into_iter(), + ..Default::default() + }) + } +} + +impl Iterator for HighlightEventWrapper { + type Item = Color; + fn next(&mut self) -> Option { + if self.pos < self.limit { + self.pos += 1; + return self + .stack + .iter() + .min() + .map(|group| group.color()) + .or(Some(Color::Reset)); + } + + if let Some(Ok(event)) = self.iter.next() { + match event { + HighlightEvent::HighlightStart(Highlight(num)) => { + self.stack.push(HighlightGroup::all()[num].group.clone()); + } + HighlightEvent::Source { start: _, end } => { + self.limit = end; + } + HighlightEvent::HighlightEnd => { + self.stack.pop(); + } + } + self.next() + } else { + None + } + } +} diff --git a/src/lib.rs b/src/lib.rs index df5c7df..f9a9ba5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ use persist::Session; use regex::Cache as RegexCache; use render::Render; +mod highlight; mod input; pub mod persist; mod regex; diff --git a/src/render.rs b/src/render.rs index 4d89107..86f90f7 100644 --- a/src/render.rs +++ b/src/render.rs @@ -8,7 +8,7 @@ use crossterm::{ terminal::{Clear, ClearType}, }; -use crate::{Group, LAYER_COLORS}; +use crate::{Group, LAYER_COLORS, highlight::HighlightEventWrapper}; pub struct Render(W); @@ -56,8 +56,14 @@ impl Render { pub fn draw_regex_query(&mut self, s: &str, col: u16, row: u16) -> io::Result<()> { self.move_to(col, row)?; let mut layer = 0; - + let mut syntax_highlighting = HighlightEventWrapper::new(s.as_bytes()).unwrap_or_default(); for ch in s.chars() { + let syntax_color = syntax_highlighting + .by_ref() + .take(ch.len_utf8()) + .last() + .unwrap_or(Color::Reset); + let color = match ch { '(' => { layer += 1; @@ -68,7 +74,7 @@ impl Render { layer = layer.saturating_sub(1); color } - _ => Color::Reset, + _ => syntax_color, }; self.draw(color, ch)?; }