Skip to content

Commit

Permalink
Sorta get tree-sitter working
Browse files Browse the repository at this point in the history
  • Loading branch information
melody-rs committed Nov 29, 2023
1 parent d60b4aa commit b90ca29
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 69 deletions.
34 changes: 34 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/components/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
173 changes: 106 additions & 67 deletions crates/components/src/syntax_highlighting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
};
Expand All @@ -48,50 +47,80 @@ 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)
}
}

type HighlightCache = egui::util::cache::FrameCache<LayoutJob, Highlighter>;

ctx.memory_mut(|m| {
let highlight_cache = m.caches.cache::<HighlightCache>();
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<Self, tree_sitter::QueryError> {
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),
Expand All @@ -106,55 +135,65 @@ impl Highlighter {
}

fn highlight_impl(
&self,
&mut self,
theme: luminol_config::CodeTheme,
text: &str,
language: &str,
) -> Option<LayoutJob> {
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<LayoutJob, tree_sitter_highlight::Error> {
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)
}
}

Expand Down
1 change: 0 additions & 1 deletion crates/ui/src/windows/preferences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ impl luminol_core::Window for Window {
ui.ctx(),
update_state.global_config.theme,
CODE_SAMPLE,
"rb",
));
});
});
Expand Down
1 change: 0 additions & 1 deletion crates/ui/src/windows/script_edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down

0 comments on commit b90ca29

Please sign in to comment.