Add syntax highlighing feature to `egui_extras` (#3333)

* 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

* Check --no-default-features of egui_extras on CI

* spelling

* Fix building egui_extras without additional features
This commit is contained in:
Emil Ernerfeldt 2023-09-13 20:39:40 +02:00 committed by GitHub
parent 4b5146d35d
commit 5e785ae00a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 307 additions and 98 deletions

View File

@ -49,7 +49,7 @@ jobs:
run: cargo check --locked --all-features --all-targets run: cargo check --locked --all-features --all-targets
- name: check egui_extras --all-features - name: check egui_extras --all-features
run: cargo check --locked --all-features --all-targets -p egui_extras run: cargo check --locked --all-features -p egui_extras
- name: check default features - name: check default features
run: cargo check --locked --all-targets run: cargo check --locked --all-targets
@ -57,11 +57,14 @@ jobs:
- name: check --no-default-features - name: check --no-default-features
run: cargo check --locked --no-default-features --lib --all-targets run: cargo check --locked --no-default-features --lib --all-targets
- name: check epaint --no-default-features
run: cargo check --locked --no-default-features --lib --all-targets -p epaint
- name: check eframe --no-default-features - name: check eframe --no-default-features
run: cargo check --locked --no-default-features --features x11 --lib --all-targets -p eframe run: cargo check --locked --no-default-features --features x11 --lib -p eframe
- name: check egui_extras --no-default-features
run: cargo check --locked --no-default-features --lib -p egui_extras
- name: check epaint --no-default-features
run: cargo check --locked --no-default-features --lib -p epaint
- name: Test doc-tests - name: Test doc-tests
run: cargo test --doc --all-features run: cargo test --doc --all-features

4
Cargo.lock generated
View File

@ -1238,10 +1238,8 @@ dependencies = [
"egui", "egui",
"egui_extras", "egui_extras",
"egui_plot", "egui_plot",
"enum-map",
"log", "log",
"serde", "serde",
"syntect",
"unicode_names2", "unicode_names2",
] ]
@ -1253,12 +1251,14 @@ dependencies = [
"document-features", "document-features",
"egui", "egui",
"ehttp", "ehttp",
"enum-map",
"image", "image",
"log", "log",
"mime_guess", "mime_guess",
"puffin", "puffin",
"resvg", "resvg",
"serde", "serde",
"syntect",
"tiny-skia", "tiny-skia",
"usvg", "usvg",
] ]

View File

@ -10,11 +10,11 @@ impl DefaultBytesLoader {
self.cache self.cache
.lock() .lock()
.entry(uri.into()) .entry(uri.into())
.or_insert_with_key(|uri| { .or_insert_with_key(|_uri| {
let bytes: Bytes = bytes.into(); let bytes: Bytes = bytes.into();
#[cfg(feature = "log")] #[cfg(feature = "log")]
log::trace!("loaded {} bytes for uri {uri:?}", bytes.len()); log::trace!("loaded {} bytes for uri {_uri:?}", bytes.len());
bytes bytes
}); });

View File

@ -23,7 +23,7 @@ image_viewer = ["image", "egui_extras/all-loaders", "rfd"]
persistence = ["eframe/persistence", "egui/persistence", "serde"] persistence = ["eframe/persistence", "egui/persistence", "serde"]
web_screen_reader = ["eframe/web_screen_reader"] # experimental web_screen_reader = ["eframe/web_screen_reader"] # experimental
serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"] serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]
syntax_highlighting = ["egui_demo_lib/syntax_highlighting"] syntect = ["egui_demo_lib/syntect"]
glow = ["eframe/glow"] glow = ["eframe/glow"]
wgpu = ["eframe/wgpu", "bytemuck"] wgpu = ["eframe/wgpu", "bytemuck"]

View File

@ -223,25 +223,19 @@ fn selectable_text(ui: &mut egui::Ui, mut text: &str) {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Syntax highlighting: // Syntax highlighting:
#[cfg(feature = "syntect")]
fn syntax_highlighting( fn syntax_highlighting(
ctx: &egui::Context, ctx: &egui::Context,
response: &ehttp::Response, response: &ehttp::Response,
text: &str, text: &str,
) -> Option<ColoredText> { ) -> Option<ColoredText> {
let extension_and_rest: Vec<&str> = response.url.rsplitn(2, '.').collect(); let extension_and_rest: Vec<&str> = response.url.rsplitn(2, '.').collect();
let extension = extension_and_rest.get(0)?; let extension = extension_and_rest.first()?;
let theme = crate::syntax_highlighting::CodeTheme::from_style(&ctx.style()); let theme = egui_extras::syntax_highlighting::CodeTheme::from_style(&ctx.style());
Some(ColoredText(crate::syntax_highlighting::highlight( Some(ColoredText(egui_extras::syntax_highlighting::highlight(
ctx, &theme, text, extension, ctx, &theme, text, extension,
))) )))
} }
#[cfg(not(feature = "syntect"))]
fn syntax_highlighting(_ctx: &egui::Context, _: &ehttp::Response, _: &str) -> Option<ColoredText> {
None
}
struct ColoredText(egui::text::LayoutJob); struct ColoredText(egui::text::LayoutJob);
impl ColoredText { impl ColoredText {

View File

@ -28,7 +28,7 @@ chrono = ["egui_extras/datepicker", "dep:chrono"]
serde = ["egui/serde", "egui_plot/serde", "dep:serde"] serde = ["egui/serde", "egui_plot/serde", "dep:serde"]
## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect). ## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect).
syntax_highlighting = ["syntect"] syntect = ["egui_extras/syntect"]
[dependencies] [dependencies]
@ -37,7 +37,6 @@ egui_extras = { version = "0.22.0", path = "../egui_extras", features = [
"log", "log",
] } ] }
egui_plot = { version = "0.22.0", path = "../egui_plot" } egui_plot = { version = "0.22.0", path = "../egui_plot" }
enum-map = { version = "2", features = ["serde"] }
log = { version = "0.4", features = ["std"] } log = { version = "0.4", features = ["std"] }
unicode_names2 = { version = "0.6.0", default-features = false } unicode_names2 = { version = "0.6.0", default-features = false }
@ -46,9 +45,6 @@ chrono = { version = "0.4", optional = true, features = ["js-sys", "wasmbind"] }
## Enable this when generating docs. ## Enable this when generating docs.
document-features = { version = "0.2", optional = true } document-features = { version = "0.2", optional = true }
serde = { version = "1", optional = true, features = ["derive"] } serde = { version = "1", optional = true, features = ["derive"] }
syntect = { version = "5", optional = true, default-features = false, features = [
"default-fancy",
] }
[dev-dependencies] [dev-dependencies]

View File

@ -45,7 +45,6 @@ impl super::View for About {
} }
fn about_immediate_mode(ui: &mut egui::Ui) { 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.style_mut().spacing.interact_size.y = 0.0; // hack to make `horizontal_wrapped` work better with text.
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
@ -56,7 +55,7 @@ fn about_immediate_mode(ui: &mut egui::Ui) {
}); });
ui.add_space(8.0); ui.add_space(8.0);
code_view_ui( crate::rust_view_ui(
ui, ui,
r#" r#"
if ui.button("Save").clicked() { if ui.button("Save").clicked() {

View File

@ -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.collapsing("Theme", |ui| {
ui.group(|ui| { ui.group(|ui| {
theme.ui(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 layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
let mut layout_job = 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; layout_job.wrap.max_width = wrap_width;
ui.fonts(|f| f.layout_job(layout_job)) ui.fonts(|f| f.layout_job(layout_job))
}; };

View File

@ -81,13 +81,11 @@ impl super::Demo for CodeExample {
impl super::View for CodeExample { impl super::View for CodeExample {
fn ui(&mut self, ui: &mut egui::Ui) { fn ui(&mut self, ui: &mut egui::Ui) {
use crate::syntax_highlighting::code_view_ui;
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file!()); ui.add(crate::egui_github_link_file!());
}); });
code_view_ui( crate::rust_view_ui(
ui, ui,
r" r"
pub struct CodeExample { pub struct CodeExample {
@ -117,15 +115,15 @@ impl CodeExample {
}); });
}); });
code_view_ui(ui, " }\n}"); crate::rust_view_ui(ui, " }\n}");
ui.separator(); ui.separator();
code_view_ui(ui, &format!("{self:#?}")); crate::rust_view_ui(ui, &format!("{self:#?}"));
ui.separator(); 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| { ui.collapsing("Theme", |ui| {
theme.ui(ui); theme.ui(ui);
theme.store_in_memory(ui.ctx()); theme.store_in_memory(ui.ctx());
@ -135,7 +133,7 @@ impl CodeExample {
fn show_code(ui: &mut egui::Ui, code: &str) { fn show_code(ui: &mut egui::Ui, code: &str) {
let code = remove_leading_indentation(code.trim_start_matches('\n')); 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 { fn remove_leading_indentation(code: &str) -> String {

View File

@ -15,11 +15,17 @@
mod color_test; mod color_test;
mod demo; mod demo;
pub mod easy_mark; pub mod easy_mark;
pub mod syntax_highlighting;
pub use color_test::ColorTest; pub use color_test::ColorTest;
pub use demo::DemoWindows; 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. /// Create a [`Hyperlink`](egui::Hyperlink) to this egui source code file on github.

View File

@ -49,9 +49,13 @@ puffin = ["dep:puffin", "egui/puffin"]
## Support loading svg images. ## Support loading svg images.
svg = ["resvg", "tiny-skia", "usvg"] svg = ["resvg", "tiny-skia", "usvg"]
## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect).
syntect = ["dep:syntect"]
[dependencies] [dependencies]
egui = { version = "0.22.0", path = "../egui", default-features = false } egui = { version = "0.22.0", path = "../egui", default-features = false }
enum-map = { version = "2", features = ["serde"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
#! ### Optional dependencies #! ### Optional dependencies
@ -83,6 +87,10 @@ mime_guess = { version = "2.0.4", optional = true, default-features = false }
puffin = { version = "0.16", optional = true } puffin = { version = "0.16", optional = true }
syntect = { version = "5", optional = true, default-features = false, features = [
"default-fancy",
] }
# svg feature # svg feature
resvg = { version = "0.28", optional = true, default-features = false } 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 tiny-skia = { version = "0.8", optional = true, default-features = false } # must be updated in lock-step with resvg

View File

@ -13,6 +13,8 @@
#[cfg(feature = "chrono")] #[cfg(feature = "chrono")]
mod datepicker; mod datepicker;
pub mod syntax_highlighting;
#[doc(hidden)] #[doc(hidden)]
pub mod image; pub mod image;
mod layout; mod layout;

View File

@ -63,21 +63,29 @@ impl BytesLoader for FileLoader {
.spawn({ .spawn({
let ctx = ctx.clone(); let ctx = ctx.clone();
let cache = self.cache.clone(); let cache = self.cache.clone();
let uri = uri.to_owned(); let _uri = uri.to_owned();
move || { move || {
let result = match std::fs::read(&path) { let result = match std::fs::read(&path) {
Ok(bytes) => Ok(File { Ok(bytes) => {
bytes: bytes.into(), #[cfg(feature = "mime_guess")]
mime: mime_guess::from_path(&path) let mime = mime_guess::from_path(&path)
.first_raw() .first_raw()
.map(|v| v.to_owned()), .map(|v| v.to_owned());
}),
#[cfg(not(feature = "mime_guess"))]
let mime = None;
Ok(File {
bytes: bytes.into(),
mime,
})
}
Err(err) => Err(err.to_string()), Err(err) => Err(err.to_string()),
}; };
let prev = cache.lock().insert(path, Poll::Ready(result)); let prev = cache.lock().insert(path, Poll::Ready(result));
assert!(matches!(prev, Some(Poll::Pending))); assert!(matches!(prev, Some(Poll::Pending)));
ctx.request_repaint(); ctx.request_repaint();
crate::log_trace!("finished loading {uri:?}"); crate::log_trace!("finished loading {_uri:?}");
} }
}) })
.expect("failed to spawn thread"); .expect("failed to spawn thread");

View File

@ -153,7 +153,7 @@ impl From<Vec<Size>> for Sizing {
#[test] #[test]
fn test_sizing() { fn test_sizing() {
let sizing: Sizing = vec![].into(); 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::<f32>::new());
let sizing: Sizing = vec![Size::remainder().at_least(20.0), Size::remainder()].into(); 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]); assert_eq!(sizing.to_lengths(50.0, 0.0), vec![25.0, 25.0]);

View File

@ -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; use egui::text::LayoutJob;
/// View some code with syntax highlighting and selection. /// View some code with syntax highlighting and selection.
pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) { pub fn code_view_ui(
let language = "rs"; ui: &mut egui::Ui,
let theme = CodeTheme::from_memory(ui.ctx()); theme: &CodeTheme,
mut code: &str,
language: &str,
) -> egui::Response {
let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| { 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 // layout_job.wrap.max_width = wrap_width; // no wrapping
ui.fonts(|f| f.layout_job(layout_job)) 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) .desired_rows(1)
.lock_focus(true) .lock_focus(true)
.layouter(&mut layouter), .layouter(&mut layouter),
); )
} }
/// Memoized Code highlighting /// Add syntax highlighting 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 { pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob {
impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highlighter { impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highlighter {
fn compute(&mut self, (theme, code, lang): (&CodeTheme, &str, &str)) -> LayoutJob { 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)] #[derive(Clone, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
@ -138,6 +148,7 @@ impl Default for CodeTheme {
} }
impl CodeTheme { impl CodeTheme {
/// Selects either dark or light theme based on the given style.
pub fn from_style(style: &egui::Style) -> Self { pub fn from_style(style: &egui::Style) -> Self {
if style.visuals.dark_mode { if style.visuals.dark_mode {
Self::dark() 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 { pub fn from_memory(ctx: &egui::Context) -> Self {
if ctx.style().visuals.dark_mode { if ctx.style().visuals.dark_mode {
ctx.data_mut(|d| { 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) { pub fn store_in_memory(self, ctx: &egui::Context) {
if self.dark_mode { if self.dark_mode {
ctx.data_mut(|d| d.insert_persisted(egui::Id::new("dark"), self)); 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)); 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")] #[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) { pub fn ui(&mut self, ui: &mut egui::Ui) {
egui::widgets::global_dark_light_mode_buttons(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) { pub fn ui(&mut self, ui: &mut egui::Ui) {
ui.horizontal_top(|ui| { ui.horizontal_top(|ui| {
let selected_id = egui::Id::null(); let selected_id = egui::Id::null();
#[cfg(feature = "serde")]
let mut selected_tt: TokenType = let mut selected_tt: TokenType =
ui.data_mut(|d| *d.get_persisted_mut_or(selected_id, TokenType::Comment)); 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.vertical(|ui| {
ui.set_width(150.0); ui.set_width(150.0);
@ -277,7 +332,10 @@ impl CodeTheme {
ui.add_space(16.0); ui.add_space(16.0);
#[cfg(feature = "serde")]
ui.data_mut(|d| d.insert_persisted(selected_id, selected_tt)); 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()) egui::Frame::group(ui.style())
.inner_margin(egui::Vec2::splat(2.0)) .inner_margin(egui::Vec2::splat(2.0))
@ -306,6 +364,7 @@ struct Highlighter {
#[cfg(feature = "syntect")] #[cfg(feature = "syntect")]
impl Default for Highlighter { impl Default for Highlighter {
fn default() -> Self { fn default() -> Self {
crate::profile_function!();
Self { Self {
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(), ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
ts: syntect::highlighting::ThemeSet::load_defaults(), ts: syntect::highlighting::ThemeSet::load_defaults(),
@ -313,7 +372,6 @@ impl Default for Highlighter {
} }
} }
#[cfg(feature = "syntect")]
impl Highlighter { impl Highlighter {
#[allow(clippy::unused_self, clippy::unnecessary_wraps)] #[allow(clippy::unused_self, clippy::unnecessary_wraps)]
fn highlight(&self, theme: &CodeTheme, code: &str, lang: &str) -> LayoutJob { 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<LayoutJob> { fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option<LayoutJob> {
crate::profile_function!();
use syntect::easy::HighlightLines; use syntect::easy::HighlightLines;
use syntect::highlighting::FontStyle; use syntect::highlighting::FontStyle;
use syntect::util::LinesWithEndings; use syntect::util::LinesWithEndings;
@ -400,13 +461,24 @@ struct Highlighter {}
#[cfg(not(feature = "syntect"))] #[cfg(not(feature = "syntect"))]
impl Highlighter { impl Highlighter {
#[allow(clippy::unused_self, clippy::unnecessary_wraps)] #[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<LayoutJob> {
crate::profile_function!();
let language = Language::new(language)?;
// Extremely simple syntax highlighter for when we compile without syntect // Extremely simple syntax highlighter for when we compile without syntect
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
while !text.is_empty() { 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()); let end = text.find('\n').unwrap_or(text.len());
job.append(&text[..end], 0.0, theme.formats[TokenType::Comment].clone()); job.append(&text[..end], 0.0, theme.formats[TokenType::Comment].clone());
text = &text[end..]; text = &text[end..];
@ -427,7 +499,7 @@ impl Highlighter {
.find(|c: char| !c.is_ascii_alphanumeric()) .find(|c: char| !c.is_ascii_alphanumeric())
.map_or_else(|| text.len(), |i| i + 1); .map_or_else(|| text.len(), |i| i + 1);
let word = &text[..end]; let word = &text[..end];
let tt = if is_keyword(word) { let tt = if language.is_keyword(word) {
TokenType::Keyword TokenType::Keyword
} else { } else {
TokenType::Literal TokenType::Literal
@ -457,50 +529,173 @@ impl Highlighter {
} }
} }
job Some(job)
} }
} }
#[cfg(not(feature = "syntect"))] #[cfg(not(feature = "syntect"))]
fn is_keyword(word: &str) -> bool { struct Language {
matches!( /// `// comment`
word, double_slash_comments: bool,
"as" | "async"
| "await" /// `# comment`
| "break" hash_comments: bool,
| "const"
| "continue" keywords: std::collections::BTreeSet<&'static str>,
| "crate" }
| "dyn"
| "else" #[cfg(not(feature = "syntect"))]
| "enum" impl Language {
| "extern" fn new(language: &str) -> Option<Self> {
| "false" match language.to_lowercase().as_str() {
| "fn" "c" | "h" | "hpp" | "cpp" | "c++" => Some(Self::cpp()),
| "for" "py" | "python" => Some(Self::python()),
| "if" "rs" | "rust" => Some(Self::rust()),
| "impl" _ => {
| "in" None // unsupported language
| "let" }
| "loop" }
| "match" }
| "mod"
| "move" fn is_keyword(&self, word: &str) -> bool {
| "mut" self.keywords.contains(word)
| "pub" }
| "ref"
| "return" fn cpp() -> Self {
| "self" Self {
| "Self" double_slash_comments: true,
| "static" hash_comments: false,
| "struct" keywords: [
| "super" "alignas",
| "trait" "alignof",
| "true" "and_eq",
| "type" "and",
| "unsafe" "asm",
| "use" "atomic_cancel",
| "where" "atomic_commit",
| "while" "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(),
}
}
} }