From d3011992a76e83e12f74402c2ade616cdc7f1497 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 17 Sep 2023 19:03:58 +0200 Subject: Implement basic syntax highlighting with `syntect` in `editor` example --- examples/editor/Cargo.toml | 4 +- examples/editor/src/main.rs | 168 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 170 insertions(+), 2 deletions(-) (limited to 'examples') diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index 528cf23c..930ee592 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -7,4 +7,6 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug"] \ No newline at end of file +iced.features = ["advanced", "debug"] + +syntect = "5.1" \ No newline at end of file diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 11819c69..a72feebc 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -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::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 { + 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, 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, parsing::ScopeStackOp); + + fn next(&mut self) -> Option { + 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)) + } + } +} -- cgit