Skip to content

Commit

Permalink
Merge pull request #8 from HWienhold/feature/syntax-highlighting
Browse files Browse the repository at this point in the history
added some syntax highlighting for regex
  • Loading branch information
rotmh authored Jan 10, 2025
2 parents 00a8cc5 + e46d13f commit 6f7d585
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 3 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
182 changes: 182 additions & 0 deletions src/highlight.rs
Original file line number Diff line number Diff line change
@@ -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<String> {
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::<Vec<String>>()
.join("\n")
}

fn highlight_configuration() -> Result<HighlightConfiguration, Error> {
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<Result<HighlightEvent, Error>>,
pos: usize,
limit: usize,
stack: Vec<HighlightGroup>,
}

impl HighlightEventWrapper {
pub fn new(re: &[u8]) -> Result<Self, Error> {
let mut highlighter = Highlighter::new();
let config = highlight_configuration()?;
let highlights = highlighter.highlight(&config, re, None, |_| None)?;
Ok(HighlightEventWrapper {
iter: highlights.collect::<Vec<_>>().into_iter(),
..Default::default()
})
}
}

impl Iterator for HighlightEventWrapper {
type Item = Color;
fn next(&mut self) -> Option<Color> {
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
}
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use persist::Session;
use regex::Cache as RegexCache;
use render::Render;

mod highlight;
mod input;
pub mod persist;
mod regex;
Expand Down
12 changes: 9 additions & 3 deletions src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: io::Write>(W);

Expand Down Expand Up @@ -56,8 +56,14 @@ impl<W: io::Write> Render<W> {
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;
Expand All @@ -68,7 +74,7 @@ impl<W: io::Write> Render<W> {
layer = layer.saturating_sub(1);
color
}
_ => Color::Reset,
_ => syntax_color,
};
self.draw(color, ch)?;
}
Expand Down

0 comments on commit 6f7d585

Please sign in to comment.