From 56c2d543c4b2f695ab88650ce4295a61f0cd3cc9 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 13 Sep 2023 19:53:12 +0200 Subject: [PATCH] Add syntax highlighing feature to egui_extras Enable "syntect" feature for great syntax highlighting of any language. If not a simple fallback is used that works fine for C++, Rust, Python --- Cargo.lock | 3 +- crates/egui/src/load/bytes_loader.rs | 4 +- crates/egui_demo_app/Cargo.toml | 2 +- crates/egui_demo_app/src/apps/http_app.rs | 12 +- crates/egui_demo_lib/Cargo.toml | 5 +- crates/egui_demo_lib/src/demo/about.rs | 3 +- crates/egui_demo_lib/src/demo/code_editor.rs | 4 +- crates/egui_demo_lib/src/demo/code_example.rs | 12 +- crates/egui_demo_lib/src/lib.rs | 8 +- crates/egui_extras/Cargo.toml | 10 +- crates/egui_extras/src/lib.rs | 2 + crates/egui_extras/src/sizing.rs | 2 +- .../src/syntax_highlighting.rs | 301 +++++++++++++++--- 13 files changed, 284 insertions(+), 84 deletions(-) rename crates/{egui_demo_lib => egui_extras}/src/syntax_highlighting.rs (67%) diff --git a/Cargo.lock b/Cargo.lock index 00dcba233c88..22dc3fa7629c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1241,7 +1241,6 @@ dependencies = [ "enum-map", "log", "serde", - "syntect", "unicode_names2", ] @@ -1253,12 +1252,14 @@ dependencies = [ "document-features", "egui", "ehttp", + "enum-map", "image", "log", "mime_guess", "puffin", "resvg", "serde", + "syntect", "tiny-skia", "usvg", ] diff --git a/crates/egui/src/load/bytes_loader.rs b/crates/egui/src/load/bytes_loader.rs index 451ce150e280..062011c55896 100644 --- a/crates/egui/src/load/bytes_loader.rs +++ b/crates/egui/src/load/bytes_loader.rs @@ -10,11 +10,11 @@ impl DefaultBytesLoader { self.cache .lock() .entry(uri.into()) - .or_insert_with_key(|uri| { + .or_insert_with_key(|_uri| { let bytes: Bytes = bytes.into(); #[cfg(feature = "log")] - log::trace!("loaded {} bytes for uri {uri:?}", bytes.len()); + log::trace!("loaded {} bytes for uri {_uri:?}", bytes.len()); bytes }); diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 422513364d39..0c50f2146c1e 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -23,7 +23,7 @@ image_viewer = ["image", "egui_extras/all-loaders", "rfd"] persistence = ["eframe/persistence", "egui/persistence", "serde"] web_screen_reader = ["eframe/web_screen_reader"] # experimental serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"] -syntax_highlighting = ["egui_demo_lib/syntax_highlighting"] +syntect = ["egui_demo_lib/syntect"] glow = ["eframe/glow"] wgpu = ["eframe/wgpu", "bytemuck"] diff --git a/crates/egui_demo_app/src/apps/http_app.rs b/crates/egui_demo_app/src/apps/http_app.rs index fe43c477b136..ef5c2987af3c 100644 --- a/crates/egui_demo_app/src/apps/http_app.rs +++ b/crates/egui_demo_app/src/apps/http_app.rs @@ -223,25 +223,19 @@ fn selectable_text(ui: &mut egui::Ui, mut text: &str) { // ---------------------------------------------------------------------------- // Syntax highlighting: -#[cfg(feature = "syntect")] fn syntax_highlighting( ctx: &egui::Context, response: &ehttp::Response, text: &str, ) -> Option { let extension_and_rest: Vec<&str> = response.url.rsplitn(2, '.').collect(); - let extension = extension_and_rest.get(0)?; - let theme = crate::syntax_highlighting::CodeTheme::from_style(&ctx.style()); - Some(ColoredText(crate::syntax_highlighting::highlight( + let extension = extension_and_rest.first()?; + let theme = egui_extras::syntax_highlighting::CodeTheme::from_style(&ctx.style()); + Some(ColoredText(egui_extras::syntax_highlighting::highlight( ctx, &theme, text, extension, ))) } -#[cfg(not(feature = "syntect"))] -fn syntax_highlighting(_ctx: &egui::Context, _: &ehttp::Response, _: &str) -> Option { - None -} - struct ColoredText(egui::text::LayoutJob); impl ColoredText { diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index 3fd6a6939cdd..495747560016 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -28,7 +28,7 @@ chrono = ["egui_extras/datepicker", "dep:chrono"] serde = ["egui/serde", "egui_plot/serde", "dep:serde"] ## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect). -syntax_highlighting = ["syntect"] +syntect = ["egui_extras/syntect"] [dependencies] @@ -46,9 +46,6 @@ chrono = { version = "0.4", optional = true, features = ["js-sys", "wasmbind"] } ## Enable this when generating docs. document-features = { version = "0.2", optional = true } serde = { version = "1", optional = true, features = ["derive"] } -syntect = { version = "5", optional = true, default-features = false, features = [ - "default-fancy", -] } [dev-dependencies] diff --git a/crates/egui_demo_lib/src/demo/about.rs b/crates/egui_demo_lib/src/demo/about.rs index 4972b224b1db..57cbcd0c5d83 100644 --- a/crates/egui_demo_lib/src/demo/about.rs +++ b/crates/egui_demo_lib/src/demo/about.rs @@ -45,7 +45,6 @@ impl super::View for About { } fn about_immediate_mode(ui: &mut egui::Ui) { - use crate::syntax_highlighting::code_view_ui; ui.style_mut().spacing.interact_size.y = 0.0; // hack to make `horizontal_wrapped` work better with text. ui.horizontal_wrapped(|ui| { @@ -56,7 +55,7 @@ fn about_immediate_mode(ui: &mut egui::Ui) { }); ui.add_space(8.0); - code_view_ui( + crate::rust_view_ui( ui, r#" if ui.button("Save").clicked() { diff --git a/crates/egui_demo_lib/src/demo/code_editor.rs b/crates/egui_demo_lib/src/demo/code_editor.rs index e9632a208ec1..2a8f6a6c97e5 100644 --- a/crates/egui_demo_lib/src/demo/code_editor.rs +++ b/crates/egui_demo_lib/src/demo/code_editor.rs @@ -67,7 +67,7 @@ impl super::View for CodeEditor { }); } - let mut theme = crate::syntax_highlighting::CodeTheme::from_memory(ui.ctx()); + let mut theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx()); ui.collapsing("Theme", |ui| { ui.group(|ui| { theme.ui(ui); @@ -77,7 +77,7 @@ impl super::View for CodeEditor { let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| { let mut layout_job = - crate::syntax_highlighting::highlight(ui.ctx(), &theme, string, language); + egui_extras::syntax_highlighting::highlight(ui.ctx(), &theme, string, language); layout_job.wrap.max_width = wrap_width; ui.fonts(|f| f.layout_job(layout_job)) }; diff --git a/crates/egui_demo_lib/src/demo/code_example.rs b/crates/egui_demo_lib/src/demo/code_example.rs index 7b13514762f1..7c97dacccba7 100644 --- a/crates/egui_demo_lib/src/demo/code_example.rs +++ b/crates/egui_demo_lib/src/demo/code_example.rs @@ -81,13 +81,11 @@ impl super::Demo for CodeExample { impl super::View for CodeExample { fn ui(&mut self, ui: &mut egui::Ui) { - use crate::syntax_highlighting::code_view_ui; - ui.vertical_centered(|ui| { ui.add(crate::egui_github_link_file!()); }); - code_view_ui( + crate::rust_view_ui( ui, r" pub struct CodeExample { @@ -117,15 +115,15 @@ impl CodeExample { }); }); - code_view_ui(ui, " }\n}"); + crate::rust_view_ui(ui, " }\n}"); ui.separator(); - code_view_ui(ui, &format!("{self:#?}")); + crate::rust_view_ui(ui, &format!("{self:#?}")); ui.separator(); - let mut theme = crate::syntax_highlighting::CodeTheme::from_memory(ui.ctx()); + let mut theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx()); ui.collapsing("Theme", |ui| { theme.ui(ui); theme.store_in_memory(ui.ctx()); @@ -135,7 +133,7 @@ impl CodeExample { fn show_code(ui: &mut egui::Ui, code: &str) { let code = remove_leading_indentation(code.trim_start_matches('\n')); - crate::syntax_highlighting::code_view_ui(ui, &code); + crate::rust_view_ui(ui, &code); } fn remove_leading_indentation(code: &str) -> String { diff --git a/crates/egui_demo_lib/src/lib.rs b/crates/egui_demo_lib/src/lib.rs index 43db89f2d9df..9d7eaa536223 100644 --- a/crates/egui_demo_lib/src/lib.rs +++ b/crates/egui_demo_lib/src/lib.rs @@ -15,11 +15,17 @@ mod color_test; mod demo; pub mod easy_mark; -pub mod syntax_highlighting; pub use color_test::ColorTest; pub use demo::DemoWindows; +/// View some Rust code with syntax highlighting and selection. +pub(crate) fn rust_view_ui(ui: &mut egui::Ui, code: &str) { + let language = "rs"; + let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx()); + egui_extras::syntax_highlighting::code_view_ui(ui, &theme, code, language); +} + // ---------------------------------------------------------------------------- /// Create a [`Hyperlink`](egui::Hyperlink) to this egui source code file on github. diff --git a/crates/egui_extras/Cargo.toml b/crates/egui_extras/Cargo.toml index cfbb845a2d45..7517c71fb6c7 100644 --- a/crates/egui_extras/Cargo.toml +++ b/crates/egui_extras/Cargo.toml @@ -49,9 +49,13 @@ puffin = ["dep:puffin", "egui/puffin"] ## Support loading svg images. svg = ["resvg", "tiny-skia", "usvg"] +## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect). +syntect = ["dep:syntect"] + + [dependencies] egui = { version = "0.22.0", path = "../egui", default-features = false } - +enum-map = { version = "2", features = ["serde"] } serde = { version = "1", features = ["derive"] } #! ### Optional dependencies @@ -83,6 +87,10 @@ mime_guess = { version = "2.0.4", optional = true, default-features = false } puffin = { version = "0.16", optional = true } +syntect = { version = "5", optional = true, default-features = false, features = [ + "default-fancy", +] } + # svg feature resvg = { version = "0.28", optional = true, default-features = false } tiny-skia = { version = "0.8", optional = true, default-features = false } # must be updated in lock-step with resvg diff --git a/crates/egui_extras/src/lib.rs b/crates/egui_extras/src/lib.rs index 4850b6f3347d..8ab0a0e9ba2e 100644 --- a/crates/egui_extras/src/lib.rs +++ b/crates/egui_extras/src/lib.rs @@ -13,6 +13,8 @@ #[cfg(feature = "chrono")] mod datepicker; +pub mod syntax_highlighting; + #[doc(hidden)] pub mod image; mod layout; diff --git a/crates/egui_extras/src/sizing.rs b/crates/egui_extras/src/sizing.rs index c6ae9d6daaef..70a2ca4e6931 100644 --- a/crates/egui_extras/src/sizing.rs +++ b/crates/egui_extras/src/sizing.rs @@ -153,7 +153,7 @@ impl From> for Sizing { #[test] fn test_sizing() { let sizing: Sizing = vec![].into(); - assert_eq!(sizing.to_lengths(50.0, 0.0), vec![]); + assert_eq!(sizing.to_lengths(50.0, 0.0), Vec::::new()); let sizing: Sizing = vec![Size::remainder().at_least(20.0), Size::remainder()].into(); assert_eq!(sizing.to_lengths(50.0, 0.0), vec![25.0, 25.0]); diff --git a/crates/egui_demo_lib/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs similarity index 67% rename from crates/egui_demo_lib/src/syntax_highlighting.rs rename to crates/egui_extras/src/syntax_highlighting.rs index 50f1ff93933b..5809b1e0701e 100644 --- a/crates/egui_demo_lib/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -1,12 +1,19 @@ +//! Syntax highlighting for code. +//! +//! Turn on the `syntect` feature for great syntax highlighting of any language. +//! Otherwise, a very simple fallback will be used, that works okish for C, C++, Rust, and Python. + use egui::text::LayoutJob; /// View some code with syntax highlighting and selection. -pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) { - let language = "rs"; - let theme = CodeTheme::from_memory(ui.ctx()); - +pub fn code_view_ui( + ui: &mut egui::Ui, + theme: &CodeTheme, + mut code: &str, + language: &str, +) -> egui::Response { 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, language); // layout_job.wrap.max_width = wrap_width; // no wrapping ui.fonts(|f| f.layout_job(layout_job)) }; @@ -18,10 +25,12 @@ pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) { .desired_rows(1) .lock_focus(true) .layouter(&mut layouter), - ); + ) } -/// Memoized Code highlighting +/// Add syntax highlighing to a code string. +/// +/// The results are memoized, so you can call this every frame without performance penalty. pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob { impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highlighter { fn compute(&mut self, (theme, code, lang): (&CodeTheme, &str, &str)) -> LayoutJob { @@ -118,6 +127,7 @@ impl SyntectTheme { } } +/// A selected color theme. #[derive(Clone, Hash, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] @@ -138,6 +148,7 @@ impl Default for CodeTheme { } impl CodeTheme { + /// Selects either dark or light theme based on the given style. pub fn from_style(style: &egui::Style) -> Self { if style.visuals.dark_mode { Self::dark() @@ -146,6 +157,10 @@ impl CodeTheme { } } + /// Load code theme from egui memory. + /// + /// There is one dark and one light theme stored at any one time. + #[cfg(feature = "serde")] pub fn from_memory(ctx: &egui::Context) -> Self { if ctx.style().visuals.dark_mode { ctx.data_mut(|d| { @@ -160,6 +175,10 @@ impl CodeTheme { } } + /// Store theme to egui memory. + /// + /// There is one dark and one light theme stored at any one time. + #[cfg(feature = "serde")] pub fn store_in_memory(self, ctx: &egui::Context) { if self.dark_mode { ctx.data_mut(|d| d.insert_persisted(egui::Id::new("dark"), self)); @@ -167,6 +186,36 @@ impl CodeTheme { ctx.data_mut(|d| d.insert_persisted(egui::Id::new("light"), self)); } } + + /// Load code theme from egui memory. + /// + /// There is one dark and one light theme stored at any one time. + #[cfg(not(feature = "serde"))] + pub fn from_memory(ctx: &egui::Context) -> Self { + if ctx.style().visuals.dark_mode { + ctx.data_mut(|d| { + d.get_temp(egui::Id::new("dark")) + .unwrap_or_else(CodeTheme::dark) + }) + } else { + ctx.data_mut(|d| { + d.get_temp(egui::Id::new("light")) + .unwrap_or_else(CodeTheme::light) + }) + } + } + + /// Store theme to egui memory. + /// + /// There is one dark and one light theme stored at any one time. + #[cfg(not(feature = "serde"))] + pub fn store_in_memory(self, ctx: &egui::Context) { + if self.dark_mode { + ctx.data_mut(|d| d.insert_temp(egui::Id::new("dark"), self)); + } else { + ctx.data_mut(|d| d.insert_temp(egui::Id::new("light"), self)); + } + } } #[cfg(feature = "syntect")] @@ -185,6 +234,7 @@ impl CodeTheme { } } + /// Show UI for changing the color theme. pub fn ui(&mut self, ui: &mut egui::Ui) { egui::widgets::global_dark_light_mode_buttons(ui); @@ -231,11 +281,16 @@ impl CodeTheme { } } + /// Show UI for changing the color theme. pub fn ui(&mut self, ui: &mut egui::Ui) { ui.horizontal_top(|ui| { let selected_id = egui::Id::null(); + #[cfg(feature = "serde")] let mut selected_tt: TokenType = ui.data_mut(|d| *d.get_persisted_mut_or(selected_id, TokenType::Comment)); + #[cfg(not(feature = "serde"))] + let mut selected_tt: TokenType = + ui.data_mut(|d| *d.get_temp_mut_or(selected_id, TokenType::Comment)); ui.vertical(|ui| { ui.set_width(150.0); @@ -277,7 +332,10 @@ impl CodeTheme { ui.add_space(16.0); + #[cfg(feature = "serde")] ui.data_mut(|d| d.insert_persisted(selected_id, selected_tt)); + #[cfg(not(feature = "serde"))] + ui.data_mut(|d| d.insert_temp(selected_id, selected_tt)); egui::Frame::group(ui.style()) .inner_margin(egui::Vec2::splat(2.0)) @@ -306,6 +364,7 @@ struct Highlighter { #[cfg(feature = "syntect")] impl Default for Highlighter { fn default() -> Self { + crate::profile_function!(); Self { ps: syntect::parsing::SyntaxSet::load_defaults_newlines(), ts: syntect::highlighting::ThemeSet::load_defaults(), @@ -313,7 +372,6 @@ impl Default for Highlighter { } } -#[cfg(feature = "syntect")] impl Highlighter { #[allow(clippy::unused_self, clippy::unnecessary_wraps)] fn highlight(&self, theme: &CodeTheme, code: &str, lang: &str) -> LayoutJob { @@ -332,7 +390,10 @@ impl Highlighter { }) } + #[cfg(feature = "syntect")] fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option { + crate::profile_function!(); + use syntect::easy::HighlightLines; use syntect::highlighting::FontStyle; use syntect::util::LinesWithEndings; @@ -400,13 +461,24 @@ struct Highlighter {} #[cfg(not(feature = "syntect"))] impl Highlighter { #[allow(clippy::unused_self, clippy::unnecessary_wraps)] - fn highlight(&self, theme: &CodeTheme, mut text: &str, _language: &str) -> LayoutJob { + fn highlight_impl( + &self, + theme: &CodeTheme, + mut text: &str, + language: &str, + ) -> Option { + crate::profile_function!(); + + let language = Language::new(language)?; + // Extremely simple syntax highlighter for when we compile without syntect let mut job = LayoutJob::default(); while !text.is_empty() { - if text.starts_with("//") { + if language.double_slash_comments && text.starts_with("//") + || language.hash_comments && text.starts_with('#') + { let end = text.find('\n').unwrap_or(text.len()); job.append(&text[..end], 0.0, theme.formats[TokenType::Comment].clone()); text = &text[end..]; @@ -427,7 +499,7 @@ impl Highlighter { .find(|c: char| !c.is_ascii_alphanumeric()) .map_or_else(|| text.len(), |i| i + 1); let word = &text[..end]; - let tt = if is_keyword(word) { + let tt = if language.is_keyword(word) { TokenType::Keyword } else { TokenType::Literal @@ -457,50 +529,173 @@ impl Highlighter { } } - job + Some(job) } } #[cfg(not(feature = "syntect"))] -fn is_keyword(word: &str) -> bool { - matches!( - word, - "as" | "async" - | "await" - | "break" - | "const" - | "continue" - | "crate" - | "dyn" - | "else" - | "enum" - | "extern" - | "false" - | "fn" - | "for" - | "if" - | "impl" - | "in" - | "let" - | "loop" - | "match" - | "mod" - | "move" - | "mut" - | "pub" - | "ref" - | "return" - | "self" - | "Self" - | "static" - | "struct" - | "super" - | "trait" - | "true" - | "type" - | "unsafe" - | "use" - | "where" - | "while" - ) +struct Language { + /// `// comment` + double_slash_comments: bool, + + /// `# comment` + hash_comments: bool, + + keywords: std::collections::BTreeSet<&'static str>, +} + +#[cfg(not(feature = "syntect"))] +impl Language { + fn new(language: &str) -> Option { + match language.to_lowercase().as_str() { + "c" | "h" | "hpp" | "cpp" | "c++" => Some(Self::cpp()), + "py" | "python" => Some(Self::python()), + "rs" | "rust" => Some(Self::rust()), + _ => { + None // unsupported language + } + } + } + + fn is_keyword(&self, word: &str) -> bool { + self.keywords.contains(word) + } + + fn cpp() -> Self { + Self { + double_slash_comments: true, + hash_comments: false, + keywords: [ + "alignas", + "alignof", + "and_eq", + "and", + "asm", + "atomic_cancel", + "atomic_commit", + "atomic_noexcept", + "auto", + "bitand", + "bitor", + "bool", + "break", + "case", + "catch", + "char", + "char16_t", + "char32_t", + "char8_t", + "class", + "co_await", + "co_return", + "co_yield", + "compl", + "concept", + "const_cast", + "const", + "consteval", + "constexpr", + "constinit", + "continue", + "decltype", + "default", + "delete", + "do", + "double", + "dynamic_cast", + "else", + "enum", + "explicit", + "export", + "extern", + "false", + "float", + "for", + "friend", + "goto", + "if", + "inline", + "int", + "long", + "mutable", + "namespace", + "new", + "noexcept", + "not_eq", + "not", + "nullptr", + "operator", + "or_eq", + "or", + "private", + "protected", + "public", + "reflexpr", + "register", + "reinterpret_cast", + "requires", + "return", + "short", + "signed", + "sizeof", + "static_assert", + "static_cast", + "static", + "struct", + "switch", + "synchronized", + "template", + "this", + "thread_local", + "throw", + "true", + "try", + "typedef", + "typeid", + "typename", + "union", + "unsigned", + "using", + "virtual", + "void", + "volatile", + "wchar_t", + "while", + "xor_eq", + "xor", + ] + .into_iter() + .collect(), + } + } + + fn python() -> Self { + Self { + double_slash_comments: false, + hash_comments: true, + keywords: [ + "and", "as", "assert", "break", "class", "continue", "def", "del", "elif", "else", + "except", "False", "finally", "for", "from", "global", "if", "import", "in", "is", + "lambda", "None", "nonlocal", "not", "or", "pass", "raise", "return", "True", + "try", "while", "with", "yield", + ] + .into_iter() + .collect(), + } + } + + fn rust() -> Self { + Self { + double_slash_comments: true, + hash_comments: false, + keywords: [ + "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", + "enum", "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", + "mod", "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct", + "super", "trait", "true", "type", "unsafe", "use", "where", "while", + ] + .into_iter() + .collect(), + } + } }