Support custom syntect settings in syntax highlighter (#7084)
<!-- Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md) before opening a Pull Request! * Keep your PR:s small and focused. * The PR title is what ends up in the changelog, so make it descriptive! * If applicable, add a screenshot or gif. * If it is a non-trivial addition, consider adding a demo for it to `egui_demo_lib`, or a new example. * Do NOT open PR:s from your `master` branch, as that makes it hard for maintainers to test and add commits to your PR. * Remember to run `cargo fmt` and `cargo clippy`. * Open the PR as a draft until you have self-reviewed it and run `./scripts/check.sh`. * When you have addressed a PR comment, mark it as resolved. Please be patient! I will review your PR, but my time is limited! --> * Closes <https://github.com/emilk/egui/issues/3964> * [X] I have followed the instructions in the PR template This PR adds support for syntax highlighting with custom `syntect::parsing::SyntaxSet` and `syntect::highlighting::ThemeSet`. It adds a new `egui_extras::highlight_with` function (enabled with `feature = "syntect"`), which takes a new public`struct SyntectSettings` containing the syntax and theme sets. ```rust let mut builder = SyntaxSetBuilder::new(); builder.add_from_folder("syntax", true).unwrap(); let ps = builder.build(); let ts = syntect::highlighting::ThemeSet::load_defaults(); let syntax = egui_extras::syntax_highlighting::SyntectSettings { ps, ts }; // ...elsewhere egui_extras::syntax_highlighting::highlight_with( ui.ctx(), ui.style(), &theme, buf, "rhai", &syntax, ); ``` There's a little bit of architectural complexity, but it all emerges naturally from the problem's constraints. Previously, the `Highlighter` both contained the `syntect` settings _and_ implemented `egui::cache::ComputerMut` to highlight a string; the settings would never change. After this change, the `syntect` settings have become part of the cache key, so we should redo highlighting if they change. The `Highlighter` becomes an empty `struct` which just serves to implement `ComputerMut`. `SyntaxSet` and `ThemeSet` are not hasheable themselves, so can't be used as cache keys direction. Instead, we can use the *address* of the `&SyntectSettings` as the key. This requires an object with a custom `Hash` implementation, so I added a new `HighlightSettings(&'a SyntectSettings)`, implementing `Hash` using `std::ptr::hash` on the reference. I think using the address is reasonable – it would be _weird_ for a user to be constantly moving around their `SyntectSettings`, and there's a warning in the docstring to this effect. To work _without_ custom settings, `SyntectSettings::default` preserves the same behavior as before, using `SyntaxSet::load_defaults_newlines` and `ThemeSet::load_defaults`. If the user doesn't provide custom settings, then we instantiate a singleton `SyntectSettings` in `data` and use it; this will only be constructed once. Finally, in cases where the `syntect` feature is disabled, `SyntectSettings` are replaced with a unit `()`. This adds a _tiny_ amount of overhead – one singleton `Arc<()>` allocation and a lookup in `data` per `highlight` – but I think that's better than dramatically different implementations. If this is an issue, I can refactor to make it zero-cost when the feature is disabled.
This commit is contained in:
parent
853feea464
commit
f11a3510ba
|
|
@ -28,18 +28,65 @@ pub fn highlight(
|
||||||
theme: &CodeTheme,
|
theme: &CodeTheme,
|
||||||
code: &str,
|
code: &str,
|
||||||
language: &str,
|
language: &str,
|
||||||
|
) -> LayoutJob {
|
||||||
|
highlight_inner(ctx, style, theme, code, language, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add syntax highlighting to a code string, with custom `syntect` settings
|
||||||
|
///
|
||||||
|
/// The results are memoized, so you can call this every frame without performance penalty.
|
||||||
|
///
|
||||||
|
/// The `syntect` settings are memoized by *address*, so a stable reference should
|
||||||
|
/// be used to avoid unnecessary recomputation.
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
pub fn highlight_with(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
style: &egui::Style,
|
||||||
|
theme: &CodeTheme,
|
||||||
|
code: &str,
|
||||||
|
language: &str,
|
||||||
|
settings: &SyntectSettings,
|
||||||
|
) -> LayoutJob {
|
||||||
|
highlight_inner(
|
||||||
|
ctx,
|
||||||
|
style,
|
||||||
|
theme,
|
||||||
|
code,
|
||||||
|
language,
|
||||||
|
Some(HighlightSettings(settings)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlight_inner(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
style: &egui::Style,
|
||||||
|
theme: &CodeTheme,
|
||||||
|
code: &str,
|
||||||
|
language: &str,
|
||||||
|
settings: Option<HighlightSettings<'_>>,
|
||||||
) -> LayoutJob {
|
) -> LayoutJob {
|
||||||
// We take in both context and style so that in situations where ui is not available such as when
|
// We take in both context and style so that in situations where ui is not available such as when
|
||||||
// performing it at a separate thread (ctx, ctx.style()) can be used and when ui is available
|
// performing it at a separate thread (ctx, ctx.style()) can be used and when ui is available
|
||||||
// (ui.ctx(), ui.style()) can be used
|
// (ui.ctx(), ui.style()) can be used
|
||||||
|
|
||||||
#[expect(non_local_definitions)]
|
#[expect(non_local_definitions)]
|
||||||
impl egui::cache::ComputerMut<(&egui::FontId, &CodeTheme, &str, &str), LayoutJob> for Highlighter {
|
impl
|
||||||
|
egui::cache::ComputerMut<
|
||||||
|
(&egui::FontId, &CodeTheme, &str, &str, HighlightSettings<'_>),
|
||||||
|
LayoutJob,
|
||||||
|
> for Highlighter
|
||||||
|
{
|
||||||
fn compute(
|
fn compute(
|
||||||
&mut self,
|
&mut self,
|
||||||
(font_id, theme, code, lang): (&egui::FontId, &CodeTheme, &str, &str),
|
(font_id, theme, code, lang, settings): (
|
||||||
|
&egui::FontId,
|
||||||
|
&CodeTheme,
|
||||||
|
&str,
|
||||||
|
&str,
|
||||||
|
HighlightSettings<'_>,
|
||||||
|
),
|
||||||
) -> LayoutJob {
|
) -> LayoutJob {
|
||||||
self.highlight(font_id.clone(), theme, code, lang)
|
Self::highlight(font_id.clone(), theme, code, lang, settings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,10 +97,27 @@ pub fn highlight(
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| TextStyle::Monospace.resolve(style));
|
.unwrap_or_else(|| TextStyle::Monospace.resolve(style));
|
||||||
|
|
||||||
|
// Private type, so that users can't interfere with it in the `IdTypeMap`
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
struct PrivateSettings(std::sync::Arc<SyntectSettings>);
|
||||||
|
|
||||||
|
// Dummy private settings, to minimize code changes without `syntect`
|
||||||
|
#[cfg(not(feature = "syntect"))]
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
struct PrivateSettings(std::sync::Arc<()>);
|
||||||
|
|
||||||
ctx.memory_mut(|mem| {
|
ctx.memory_mut(|mem| {
|
||||||
|
let settings = settings.unwrap_or_else(|| {
|
||||||
|
HighlightSettings(
|
||||||
|
&mem.data
|
||||||
|
.get_temp_mut_or_default::<PrivateSettings>(egui::Id::NULL)
|
||||||
|
.0,
|
||||||
|
)
|
||||||
|
});
|
||||||
mem.caches
|
mem.caches
|
||||||
.cache::<HighlightCache>()
|
.cache::<HighlightCache>()
|
||||||
.get((&font_id, theme, code, language))
|
.get((&font_id, theme, code, language, settings))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -396,13 +460,13 @@ impl CodeTheme {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
#[cfg(feature = "syntect")]
|
||||||
struct Highlighter {
|
pub struct SyntectSettings {
|
||||||
ps: syntect::parsing::SyntaxSet,
|
pub ps: syntect::parsing::SyntaxSet,
|
||||||
ts: syntect::highlighting::ThemeSet,
|
pub ts: syntect::highlighting::ThemeSet,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
#[cfg(feature = "syntect")]
|
||||||
impl Default for Highlighter {
|
impl Default for SyntectSettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -412,15 +476,33 @@ impl Default for Highlighter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Highlight settings are memoized by reference address, rather than value
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
struct HighlightSettings<'a>(&'a SyntectSettings);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "syntect"))]
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
struct HighlightSettings<'a>(&'a ());
|
||||||
|
|
||||||
|
impl std::hash::Hash for HighlightSettings<'_> {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
std::ptr::hash(self.0, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Highlighter;
|
||||||
|
|
||||||
impl Highlighter {
|
impl Highlighter {
|
||||||
fn highlight(
|
fn highlight(
|
||||||
&self,
|
|
||||||
font_id: egui::FontId,
|
font_id: egui::FontId,
|
||||||
theme: &CodeTheme,
|
theme: &CodeTheme,
|
||||||
code: &str,
|
code: &str,
|
||||||
lang: &str,
|
lang: &str,
|
||||||
|
settings: HighlightSettings<'_>,
|
||||||
) -> LayoutJob {
|
) -> LayoutJob {
|
||||||
self.highlight_impl(theme, code, lang).unwrap_or_else(|| {
|
Self::highlight_impl(theme, code, lang, settings).unwrap_or_else(|| {
|
||||||
// Fallback:
|
// Fallback:
|
||||||
LayoutJob::simple(
|
LayoutJob::simple(
|
||||||
code.into(),
|
code.into(),
|
||||||
|
|
@ -436,19 +518,25 @@ impl Highlighter {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
#[cfg(feature = "syntect")]
|
||||||
fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option<LayoutJob> {
|
fn highlight_impl(
|
||||||
|
theme: &CodeTheme,
|
||||||
|
text: &str,
|
||||||
|
language: &str,
|
||||||
|
highlighter: HighlightSettings<'_>,
|
||||||
|
) -> Option<LayoutJob> {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
use syntect::easy::HighlightLines;
|
use syntect::easy::HighlightLines;
|
||||||
use syntect::highlighting::FontStyle;
|
use syntect::highlighting::FontStyle;
|
||||||
use syntect::util::LinesWithEndings;
|
use syntect::util::LinesWithEndings;
|
||||||
|
|
||||||
let syntax = self
|
let syntax = highlighter
|
||||||
|
.0
|
||||||
.ps
|
.ps
|
||||||
.find_syntax_by_name(language)
|
.find_syntax_by_name(language)
|
||||||
.or_else(|| self.ps.find_syntax_by_extension(language))?;
|
.or_else(|| highlighter.0.ps.find_syntax_by_extension(language))?;
|
||||||
|
|
||||||
let syn_theme = theme.syntect_theme.syntect_key_name();
|
let syn_theme = theme.syntect_theme.syntect_key_name();
|
||||||
let mut h = HighlightLines::new(syntax, &self.ts.themes[syn_theme]);
|
let mut h = HighlightLines::new(syntax, &highlighter.0.ts.themes[syn_theme]);
|
||||||
|
|
||||||
use egui::text::{LayoutSection, TextFormat};
|
use egui::text::{LayoutSection, TextFormat};
|
||||||
|
|
||||||
|
|
@ -458,7 +546,7 @@ impl Highlighter {
|
||||||
};
|
};
|
||||||
|
|
||||||
for line in LinesWithEndings::from(text) {
|
for line in LinesWithEndings::from(text) {
|
||||||
for (style, range) in h.highlight_line(line, &self.ps).ok()? {
|
for (style, range) in h.highlight_line(line, &highlighter.0.ps).ok()? {
|
||||||
let fg = style.foreground;
|
let fg = style.foreground;
|
||||||
let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
|
let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
|
||||||
let italics = style.font_style.contains(FontStyle::ITALIC);
|
let italics = style.font_style.contains(FontStyle::ITALIC);
|
||||||
|
|
@ -505,18 +593,13 @@ fn as_byte_range(whole: &str, range: &str) -> std::ops::Range<usize> {
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
|
||||||
#[derive(Default)]
|
|
||||||
struct Highlighter {}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
#[cfg(not(feature = "syntect"))]
|
||||||
impl Highlighter {
|
impl Highlighter {
|
||||||
#[expect(clippy::unused_self)]
|
|
||||||
fn highlight_impl(
|
fn highlight_impl(
|
||||||
&self,
|
|
||||||
theme: &CodeTheme,
|
theme: &CodeTheme,
|
||||||
mut text: &str,
|
mut text: &str,
|
||||||
language: &str,
|
language: &str,
|
||||||
|
_settings: HighlightSettings<'_>,
|
||||||
) -> Option<LayoutJob> {
|
) -> Option<LayoutJob> {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue