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,
|
||||
code: &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 {
|
||||
// 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
|
||||
// (ui.ctx(), ui.style()) can be used
|
||||
|
||||
#[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(
|
||||
&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 {
|
||||
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()
|
||||
.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| {
|
||||
let settings = settings.unwrap_or_else(|| {
|
||||
HighlightSettings(
|
||||
&mem.data
|
||||
.get_temp_mut_or_default::<PrivateSettings>(egui::Id::NULL)
|
||||
.0,
|
||||
)
|
||||
});
|
||||
mem.caches
|
||||
.cache::<HighlightCache>()
|
||||
.get((&font_id, theme, code, language))
|
||||
.get((&font_id, theme, code, language, settings))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -396,13 +460,13 @@ impl CodeTheme {
|
|||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
struct Highlighter {
|
||||
ps: syntect::parsing::SyntaxSet,
|
||||
ts: syntect::highlighting::ThemeSet,
|
||||
pub struct SyntectSettings {
|
||||
pub ps: syntect::parsing::SyntaxSet,
|
||||
pub ts: syntect::highlighting::ThemeSet,
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
impl Default for Highlighter {
|
||||
impl Default for SyntectSettings {
|
||||
fn default() -> Self {
|
||||
profiling::function_scope!();
|
||||
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 {
|
||||
fn highlight(
|
||||
&self,
|
||||
font_id: egui::FontId,
|
||||
theme: &CodeTheme,
|
||||
code: &str,
|
||||
lang: &str,
|
||||
settings: HighlightSettings<'_>,
|
||||
) -> LayoutJob {
|
||||
self.highlight_impl(theme, code, lang).unwrap_or_else(|| {
|
||||
Self::highlight_impl(theme, code, lang, settings).unwrap_or_else(|| {
|
||||
// Fallback:
|
||||
LayoutJob::simple(
|
||||
code.into(),
|
||||
|
|
@ -436,19 +518,25 @@ impl Highlighter {
|
|||
}
|
||||
|
||||
#[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!();
|
||||
use syntect::easy::HighlightLines;
|
||||
use syntect::highlighting::FontStyle;
|
||||
use syntect::util::LinesWithEndings;
|
||||
|
||||
let syntax = self
|
||||
let syntax = highlighter
|
||||
.0
|
||||
.ps
|
||||
.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 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};
|
||||
|
||||
|
|
@ -458,7 +546,7 @@ impl Highlighter {
|
|||
};
|
||||
|
||||
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 text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
|
||||
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"))]
|
||||
impl Highlighter {
|
||||
#[expect(clippy::unused_self)]
|
||||
fn highlight_impl(
|
||||
&self,
|
||||
theme: &CodeTheme,
|
||||
mut text: &str,
|
||||
language: &str,
|
||||
_settings: HighlightSettings<'_>,
|
||||
) -> Option<LayoutJob> {
|
||||
profiling::function_scope!();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue