Implement basic syntax highlighting with syntect in editor example

This commit is contained in:
Héctor Ramón Jiménez 2023-09-17 19:03:58 +02:00
parent 76dc82e8e8
commit d3011992a7
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
5 changed files with 211 additions and 6 deletions

View file

@ -3,7 +3,7 @@ use crate::Color;
use std::hash::Hash;
use std::ops::Range;
pub trait Highlighter: Clone + 'static {
pub trait Highlighter: 'static {
type Settings: Hash;
type Highlight;

View file

@ -7,4 +7,6 @@ publish = false
[dependencies]
iced.workspace = true
iced.features = ["debug"]
iced.features = ["advanced", "debug"]
syntect = "5.1"

View file

@ -1,6 +1,8 @@
use iced::widget::{container, text_editor};
use iced::{Element, Font, Sandbox, Settings, Theme};
use highlighter::Highlighter;
pub fn main() -> iced::Result {
Editor::run(Settings::default())
}
@ -41,7 +43,10 @@ impl Sandbox for Editor {
container(
text_editor(&self.content)
.on_edit(Message::Edit)
.font(Font::with_name("Hasklug Nerd Font Mono")),
.font(Font::with_name("Hasklug Nerd Font Mono"))
.highlight::<Highlighter>(highlighter::Settings {
token: String::from("md"),
}),
)
.padding(20)
.into()
@ -51,3 +56,164 @@ impl Sandbox for Editor {
Theme::Dark
}
}
mod highlighter {
use iced::advanced::text::highlighter;
use iced::widget::text_editor;
use iced::{Color, Font, Theme};
use std::ops::Range;
use syntect::highlighting;
use syntect::parsing;
#[derive(Debug, Clone, Hash)]
pub struct Settings {
pub token: String,
}
pub struct Highlight(highlighting::StyleModifier);
impl text_editor::Highlight for Highlight {
fn format(&self, _theme: &Theme) -> highlighter::Format<Font> {
highlighter::Format {
color: self.0.foreground.map(|color| {
Color::from_rgba8(
color.r,
color.g,
color.b,
color.a as f32 / 255.0,
)
}),
font: None,
}
}
}
pub struct Highlighter {
syntaxes: parsing::SyntaxSet,
parser: parsing::ParseState,
stack: parsing::ScopeStack,
theme: highlighting::Theme,
token: String,
current_line: usize,
}
impl highlighter::Highlighter for Highlighter {
type Settings = Settings;
type Highlight = Highlight;
type Iterator<'a> =
Box<dyn Iterator<Item = (Range<usize>, Self::Highlight)> + 'a>;
fn new(settings: &Self::Settings) -> Self {
let syntaxes = parsing::SyntaxSet::load_defaults_nonewlines();
let syntax = syntaxes
.find_syntax_by_token(&settings.token)
.unwrap_or_else(|| syntaxes.find_syntax_plain_text());
let parser = parsing::ParseState::new(&syntax);
let stack = parsing::ScopeStack::new();
let theme = highlighting::ThemeSet::load_defaults()
.themes
.remove("base16-mocha.dark")
.unwrap();
Highlighter {
syntaxes,
parser,
stack,
theme,
token: settings.token.clone(),
current_line: 0,
}
}
fn change_line(&mut self, _line: usize) {
// TODO: Caching
let syntax = self
.syntaxes
.find_syntax_by_token(&self.token)
.unwrap_or_else(|| self.syntaxes.find_syntax_plain_text());
self.parser = parsing::ParseState::new(&syntax);
self.stack = parsing::ScopeStack::new();
self.current_line = 0;
}
fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> {
self.current_line += 1;
let ops = self
.parser
.parse_line(line, &self.syntaxes)
.unwrap_or_default();
Box::new(
ScopeRangeIterator {
ops,
line_length: line.len(),
index: 0,
last_str_index: 0,
}
.filter_map(move |(range, scope)| {
let highlighter =
highlighting::Highlighter::new(&self.theme);
let _ = self.stack.apply(&scope);
if range.is_empty() {
None
} else {
Some((
range,
Highlight(
highlighter
.style_mod_for_stack(&self.stack.scopes),
),
))
}
}),
)
}
fn current_line(&self) -> usize {
self.current_line
}
}
pub struct ScopeRangeIterator {
ops: Vec<(usize, parsing::ScopeStackOp)>,
line_length: usize,
index: usize,
last_str_index: usize,
}
impl Iterator for ScopeRangeIterator {
type Item = (std::ops::Range<usize>, parsing::ScopeStackOp);
fn next(&mut self) -> Option<Self::Item> {
if self.index > self.ops.len() {
return None;
}
let next_str_i = if self.index == self.ops.len() {
self.line_length
} else {
self.ops[self.index].0
};
let range = self.last_str_index..next_str_i;
self.last_str_index = next_str_i;
let op = if self.index == 0 {
parsing::ScopeStackOp::Noop
} else {
self.ops[self.index - 1].1.clone()
};
self.index += 1;
Some((range, op))
}
}
}

View file

@ -447,17 +447,26 @@ impl editor::Editor for Editor {
text::font_system().write().expect("Write font system");
if font_system.version() != internal.version {
log::trace!("Updating `FontSystem` of `Editor`...");
for line in internal.editor.buffer_mut().lines.iter_mut() {
line.reset();
}
}
if new_font != internal.font {
log::trace!("Updating font of `Editor`...");
for line in internal.editor.buffer_mut().lines.iter_mut() {
let _ = line.set_attrs_list(cosmic_text::AttrsList::new(
text::to_attributes(new_font),
));
}
internal.font = new_font;
internal.topmost_line_changed = Some(0);
internal.editor.shape_as_needed(font_system.raw());
}
let metrics = internal.editor.buffer().metrics();
@ -466,6 +475,8 @@ impl editor::Editor for Editor {
if new_size.0 != metrics.font_size
|| new_line_height.0 != metrics.line_height
{
log::trace!("Updating `Metrics` of `Editor`...");
internal.editor.buffer_mut().set_metrics(
font_system.raw(),
cosmic_text::Metrics::new(new_size.0, new_line_height.0),
@ -473,6 +484,8 @@ impl editor::Editor for Editor {
}
if new_bounds != internal.bounds {
log::trace!("Updating size of `Editor`...");
internal.editor.buffer_mut().set_size(
font_system.raw(),
new_bounds.width,
@ -484,6 +497,10 @@ impl editor::Editor for Editor {
if let Some(topmost_line_changed) = internal.topmost_line_changed.take()
{
log::trace!(
"Notifying highlighter of line change: {topmost_line_changed}"
);
new_highlighter.change_line(topmost_line_changed);
}
@ -497,10 +514,12 @@ impl editor::Editor for Editor {
format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>,
) {
let internal = self.internal();
let buffer = internal.editor.buffer();
let scroll = internal.editor.buffer().scroll();
let visible_lines = internal.editor.buffer().visible_lines();
let last_visible_line = (scroll + visible_lines - 1) as usize;
let scroll = buffer.scroll();
let visible_lines = buffer.visible_lines();
let last_visible_line =
((scroll + visible_lines) as usize).min(buffer.lines.len()) - 1;
let current_line = highlighter.current_line();

View file

@ -81,6 +81,24 @@ where
self.padding = padding.into();
self
}
pub fn highlight<H: text::Highlighter>(
self,
settings: H::Settings,
) -> TextEditor<'a, H, Message, Renderer> {
TextEditor {
content: self.content,
font: self.font,
text_size: self.text_size,
line_height: self.line_height,
width: self.width,
height: self.height,
padding: self.padding,
style: self.style,
on_edit: self.on_edit,
highlighter_settings: settings,
}
}
}
pub struct Content<R = crate::Renderer>(RefCell<Internal<R>>)