diff --git a/Cargo.lock b/Cargo.lock index 85a48943..10fd06e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2802,6 +2802,9 @@ dependencies = [ "slab", "strum", "syntect", + "tree-sitter", + "tree-sitter-highlight", + "tree-sitter-ruby", "wgpu", ] @@ -5535,6 +5538,37 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "tree-sitter" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e747b1f9b7b931ed39a548c1fae149101497de3c1fc8d9e18c62c1a66c683d3d" +dependencies = [ + "cc", + "regex", +] + +[[package]] +name = "tree-sitter-highlight" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "042342584c5a7a0b833d9fc4e2bdab3f9868ddc6c4b339a1e01451c6720868bc" +dependencies = [ + "regex", + "thiserror", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-ruby" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ac30cbb1560363ae76e1ccde543d6d99087421e228cc47afcec004b86bb711a" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "try-lock" version = "0.2.4" diff --git a/crates/components/Cargo.toml b/crates/components/Cargo.toml index 1611ca7f..2559d85e 100644 --- a/crates/components/Cargo.toml +++ b/crates/components/Cargo.toml @@ -33,6 +33,10 @@ syntect = { version = "5.1.0", default-features = false, features = [ "default-fancy", ] } +tree-sitter = "0.20.10" +tree-sitter-ruby = "0.20.0" +tree-sitter-highlight = "0.20.1" + parking_lot.workspace = true itertools.workspace = true diff --git a/crates/components/src/syntax_highlighting.rs b/crates/components/src/syntax_highlighting.rs index 6db23be8..82be8e29 100644 --- a/crates/components/src/syntax_highlighting.rs +++ b/crates/components/src/syntax_highlighting.rs @@ -24,14 +24,13 @@ #![allow(missing_docs)] -use egui::text::LayoutJob; +use egui::text::{LayoutJob, LayoutSection}; +use tree_sitter_highlight::{Highlight, HighlightConfiguration, HighlightEvent}; /// View some code with syntax highlighting and selection. pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str, theme: luminol_config::CodeTheme) { - let language = "rb"; - let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| { - let layout_job = highlight(ui.ctx(), theme, string, language); + let layout_job = highlight(ui.ctx(), theme, string); // layout_job.wrap.max_width = wrap_width; // no wrapping ui.fonts(|f| f.layout_job(layout_job)) }; @@ -48,20 +47,10 @@ pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str, theme: luminol_config::Co /// Memoized Code highlighting #[must_use] -pub fn highlight( - ctx: &egui::Context, - theme: luminol_config::CodeTheme, - code: &str, - language: &str, -) -> LayoutJob { - impl egui::util::cache::ComputerMut<(luminol_config::CodeTheme, &str, &str), LayoutJob> - for Highlighter - { - fn compute( - &mut self, - (theme, code, lang): (luminol_config::CodeTheme, &str, &str), - ) -> LayoutJob { - self.highlight(theme, code, lang) +pub fn highlight(ctx: &egui::Context, theme: luminol_config::CodeTheme, code: &str) -> LayoutJob { + impl egui::util::cache::ComputerMut<(luminol_config::CodeTheme, &str), LayoutJob> for Highlighter { + fn compute(&mut self, (theme, code): (luminol_config::CodeTheme, &str)) -> LayoutJob { + self.highlight(theme, code) } } @@ -69,29 +58,69 @@ pub fn highlight( ctx.memory_mut(|m| { let highlight_cache = m.caches.cache::(); - highlight_cache.get((theme, code, language)) + highlight_cache.get((theme, code)) }) } struct Highlighter { - ps: syntect::parsing::SyntaxSet, - ts: syntect::highlighting::ThemeSet, + highlighter: tree_sitter_highlight::Highlighter, + ruby_config: HighlightConfiguration, } impl Default for Highlighter { fn default() -> Self { - Self { - ps: syntect::parsing::SyntaxSet::load_defaults_newlines(), - ts: syntect::highlighting::ThemeSet::load_defaults(), - } + Self::new().expect("ruby language invalid") + } +} + +const RECOGNIZED_HIGHLIGHT_NAMES: &[&str] = &[ + "keyword", + "function.method.builtin", + "function.method", + "property", + "constant.builtin", + "constant", + "constructor", + "variable.builtin", + "variable.parameter", + "variable", + "string", + "string.special.symbol", + "escape", + "number", + "embedded", + "comment", + "operator", + "punctuation.delimiter", + "punctuation.bracket", +]; + +impl Highlighter { + pub fn new() -> Result { + let highlighter = tree_sitter_highlight::Highlighter::new(); + + let mut ruby_config = HighlightConfiguration::new( + tree_sitter_ruby::language(), + tree_sitter_ruby::HIGHLIGHT_QUERY, + "", + tree_sitter_ruby::LOCALS_QUERY, + )?; + + ruby_config.configure(RECOGNIZED_HIGHLIGHT_NAMES); + + Ok(Self { + highlighter, + ruby_config, + }) } } impl Highlighter { #[allow(clippy::unused_self, clippy::unnecessary_wraps)] - fn highlight(&self, theme: luminol_config::CodeTheme, code: &str, lang: &str) -> LayoutJob { - self.highlight_impl(theme, code, lang).unwrap_or_else(|| { - // Fallback: + fn highlight(&mut self, theme: luminol_config::CodeTheme, code: &str) -> LayoutJob { + self.highlight_impl(theme, code).unwrap_or_else(|err| { + eprintln!("highlighting error: {err}"); + LayoutJob::simple( code.into(), egui::FontId::monospace(12.0), @@ -106,55 +135,65 @@ impl Highlighter { } fn highlight_impl( - &self, + &mut self, theme: luminol_config::CodeTheme, text: &str, - language: &str, - ) -> Option { - use egui::text::{LayoutSection, TextFormat}; - use syntect::easy::HighlightLines; - use syntect::highlighting::FontStyle; - use syntect::util::LinesWithEndings; - - let syntax = self - .ps - .find_syntax_by_name(language) - .or_else(|| self.ps.find_syntax_by_extension(language))?; - - let theme = theme.syntect_theme.syntect_key_name(); - let mut h = HighlightLines::new(syntax, &self.ts.themes[theme]); + ) -> Result { + let highlights = + self.highlighter + .highlight(&self.ruby_config, text.as_bytes(), None, |_| None)?; let mut job = LayoutJob { - text: text.into(), + text: text.to_string(), ..Default::default() }; - for line in LinesWithEndings::from(text) { - for (style, range) in h.highlight_line(line, &self.ps).ok()? { - let fg = style.foreground; - let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b); - let italics = style.font_style.contains(FontStyle::ITALIC); - let underline = style.font_style.contains(FontStyle::ITALIC); - let underline = if underline { - egui::Stroke::new(1.0, text_color) - } else { - egui::Stroke::NONE - }; - job.sections.push(LayoutSection { - leading_space: 0.0, - byte_range: as_byte_range(text, range), - format: TextFormat { - font_id: egui::FontId::monospace(12.0), - color: text_color, - italics, - underline, - ..Default::default() - }, - }); + let mut highlight_stack = vec![]; + for event in highlights { + match event? { + HighlightEvent::Source { start, end } => { + job.sections.push(LayoutSection { + byte_range: start..end, + leading_space: 0.0, + format: egui::TextFormat { + font_id: egui::FontId::monospace(12.0), + color: highlight_stack + .last() + .copied() + .unwrap_or(egui::Color32::GRAY), + ..Default::default() + }, + }); + } + HighlightEvent::HighlightStart(Highlight(highlight)) => { + highlight_stack.push(match RECOGNIZED_HIGHLIGHT_NAMES[highlight] { + "embedded" => egui::Color32::from_rgb(210, 123, 83), + "escape" => egui::Color32::from_rgb(102, 204, 204), + "constant" | "number" => egui::Color32::from_rgb(249, 145, 87), + "string.special.symbol" => egui::Color32::from_rgb(153, 204, 153), + "function.method" => egui::Color32::from_rgb(102, 153, 204), + "property" | "variable" => egui::Color32::from_rgb(242, 119, 122), + "function.method.builtin" + | "constant.builtin" + | "variable.builtin" + | "keyword" => egui::Color32::from_rgb(204, 153, 204), + "operator" => egui::Color32::from_rgb(211, 208, 200), + "punctuation.delimiter" + | "punctuation.bracket" + | "string" + | "variable.parameters" => egui::Color32::from_rgb(211, 208, 200), + "comment" => egui::Color32::from_rgb(116, 115, 105), + "variable.parameter" => egui::Color32::from_rgb(211, 208, 200), + _ => egui::Color32::from_rgb(211, 208, 200), + }); + } + HighlightEvent::HighlightEnd => { + highlight_stack.pop(); + } } } - Some(job) + Ok(job) } } diff --git a/crates/ui/src/windows/preferences.rs b/crates/ui/src/windows/preferences.rs index 570b6829..24b9ae55 100644 --- a/crates/ui/src/windows/preferences.rs +++ b/crates/ui/src/windows/preferences.rs @@ -187,7 +187,6 @@ impl luminol_core::Window for Window { ui.ctx(), update_state.global_config.theme, CODE_SAMPLE, - "rb", )); }); }); diff --git a/crates/ui/src/windows/script_edit.rs b/crates/ui/src/windows/script_edit.rs index 8e99dd43..def91460 100644 --- a/crates/ui/src/windows/script_edit.rs +++ b/crates/ui/src/windows/script_edit.rs @@ -176,7 +176,6 @@ impl luminol_core::Tab for ScriptTab { ui.ctx(), update_state.global_config.theme, string, - "rb", ); layout_job.wrap.max_width = wrap_width; ui.fonts(|f| f.layout_job(layout_job))