From d8d36ff2f3f7f27d9abd5413b9ce260a001e34e9 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 28 Mar 2026 10:03:59 +0000 Subject: [PATCH 1/9] Add fn highlight::Cache::try_highlight --- crates/kas-widgets/src/edit/highlight/cache.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/kas-widgets/src/edit/highlight/cache.rs b/crates/kas-widgets/src/edit/highlight/cache.rs index 89cf93386..cc8496c11 100644 --- a/crates/kas-widgets/src/edit/highlight/cache.rs +++ b/crates/kas-widgets/src/edit/highlight/cache.rs @@ -37,8 +37,12 @@ impl Default for Cache { } impl Cache { - /// Highlight the text (from scratch) - pub fn highlight(&mut self, text: &str, highlighter: &mut H) { + /// Highlight a whole `text`, returning errors + pub fn try_highlight( + &mut self, + text: &str, + highlighter: &mut H, + ) -> Result<(), H::Error> { self.fonts.clear(); self.fonts.push(Fmt::default()); self.colors.clear(); @@ -79,7 +83,12 @@ impl Cache { state = token; }; - if let Err(err) = highlighter.highlight_text(text, &mut push_token) { + highlighter.highlight_text(text, &mut push_token) + } + + /// Highlight a whole `text`, logging errors + pub fn highlight(&mut self, text: &str, highlighter: &mut H) { + if let Err(err) = self.try_highlight(text, highlighter) { log::error!("Highlighting failed: {err}"); debug_assert!(false, "Highlighter: {err}"); } From c4d8e4fa27c536df34f2934eaa9f96bea2744fd3 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 28 Mar 2026 09:12:58 +0000 Subject: [PATCH 2/9] Make fn Highlighter::highlight_text take &self; use non-dyn closure --- crates/kas-widgets/src/edit/highlight.rs | 10 +++------- crates/kas-widgets/src/edit/highlight/syntect.rs | 4 ++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/crates/kas-widgets/src/edit/highlight.rs b/crates/kas-widgets/src/edit/highlight.rs index aeef57861..1eea23a3e 100644 --- a/crates/kas-widgets/src/edit/highlight.rs +++ b/crates/kas-widgets/src/edit/highlight.rs @@ -96,9 +96,9 @@ pub trait Highlighter { /// tokens' `index` value will result in a panic, while in release builds /// these will merely result in a log error and interrupt highlighting. fn highlight_text( - &mut self, + &self, text: &str, - push_token: &mut dyn FnMut(usize, Token), + push_token: impl FnMut(usize, Token), ) -> Result<(), Self::Error>; } @@ -119,11 +119,7 @@ impl Highlighter for Plain { } #[inline] - fn highlight_text( - &mut self, - _: &str, - _: &mut dyn FnMut(usize, Token), - ) -> Result<(), Self::Error> { + fn highlight_text(&self, _: &str, _: impl FnMut(usize, Token)) -> Result<(), Self::Error> { Ok::<(), std::convert::Infallible>(()) } } diff --git a/crates/kas-widgets/src/edit/highlight/syntect.rs b/crates/kas-widgets/src/edit/highlight/syntect.rs index fd1d9a5e9..87fc235a9 100644 --- a/crates/kas-widgets/src/edit/highlight/syntect.rs +++ b/crates/kas-widgets/src/edit/highlight/syntect.rs @@ -150,9 +150,9 @@ impl super::Highlighter for SyntectHighlighter { } fn highlight_text( - &mut self, + &self, text: &str, - push_token: &mut dyn FnMut(usize, Token), + mut push_token: impl FnMut(usize, Token), ) -> Result<(), Self::Error> { let syntaxes = Self::syntaxes(); From 5e9617e6a75eeecbc9f0ca829ec4c53bc1da2d99 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 28 Mar 2026 09:34:34 +0000 Subject: [PATCH 3/9] Add fn Highlighter::highlight_line --- crates/kas-widgets/src/edit/highlight.rs | 48 ++++++++++++ .../kas-widgets/src/edit/highlight/syntect.rs | 76 ++++++++++++------- 2 files changed, 97 insertions(+), 27 deletions(-) diff --git a/crates/kas-widgets/src/edit/highlight.rs b/crates/kas-widgets/src/edit/highlight.rs index 1eea23a3e..7089aeca5 100644 --- a/crates/kas-widgets/src/edit/highlight.rs +++ b/crates/kas-widgets/src/edit/highlight.rs @@ -70,6 +70,9 @@ pub trait Highlighter { /// TODO(associated_type_defaults): default to [`std::convert::Infallible`] type Error: std::error::Error; + /// State used to save/resume highlighting + type State: Clone + Eq; + /// Configure the highlighter /// /// This is called when the widget is configured. It may be used to set the @@ -84,6 +87,35 @@ pub trait Highlighter { /// This method allows usage of the highlighter's colors by the editor. fn scheme_colors(&self) -> SchemeColors; + /// Construct a new highlighting state + fn new_state(&self) -> Self::State; + + /// Highlight a `line` of text using a `state` + /// + /// The `state` used tracks the parse state and highlighting scope across + /// lines. At the start of a document a [new state](Self::new_state) must be + /// used; in other cases the input `state` must be the output `state` from + /// highlighting the previous line. + /// + /// The `line` passed must represent a single whole line of text (including + /// terminating line-break characters) for correct parsing. + /// + /// The method should yield a sequence of tokens each with a text index + /// (within `line`) using `push_token`. These must be yielded in order (i.e. + /// `index` must be strictly increasing). + /// + /// # Error handling + /// + /// In debug builds errors returned by this method or errors in the order of + /// tokens' `index` value will result in a panic, while in release builds + /// these will merely result in a log error and interrupt highlighting. + fn highlight_line( + &self, + state: &mut Self::State, + line: &str, + push_token: impl FnMut(usize, Token), + ) -> Result<(), Self::Error>; + /// Highlight a `text` as a single item /// /// The method should yield a sequence of tokens each with a text index @@ -107,6 +139,7 @@ pub trait Highlighter { pub struct Plain; impl Highlighter for Plain { type Error = std::convert::Infallible; + type State = (); #[inline] fn configure(&mut self, _: &mut ConfigCx) -> bool { @@ -118,6 +151,21 @@ impl Highlighter for Plain { SchemeColors::default() } + #[inline] + fn new_state(&self) -> Self::State { + () + } + + #[inline] + fn highlight_line( + &self, + _: &mut Self::State, + _: &str, + _: impl FnMut(usize, Token), + ) -> Result<(), Self::Error> { + Ok::<(), std::convert::Infallible>(()) + } + #[inline] fn highlight_text(&self, _: &str, _: impl FnMut(usize, Token)) -> Result<(), Self::Error> { Ok::<(), std::convert::Infallible>(()) diff --git a/crates/kas-widgets/src/edit/highlight/syntect.rs b/crates/kas-widgets/src/edit/highlight/syntect.rs index 87fc235a9..e59e0cc6e 100644 --- a/crates/kas-widgets/src/edit/highlight/syntect.rs +++ b/crates/kas-widgets/src/edit/highlight/syntect.rs @@ -95,8 +95,12 @@ impl SyntectHighlighter { } } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct State(HighlightState, ParseState); + impl super::Highlighter for SyntectHighlighter { type Error = ParsingError; + type State = State; fn configure(&mut self, cx: &mut ConfigCx) -> bool { let dark = cx.config().theme().get_active_scheme().is_dark; @@ -149,42 +153,60 @@ impl super::Highlighter for SyntectHighlighter { } } + #[inline] + fn new_state(&self) -> Self::State { + let state = HighlightState::new(&self.highlighter, Default::default()); + let parse_state = ParseState::new(&self.syntax); + State(state, parse_state) + } + + #[inline] + fn highlight_line( + &self, + state: &mut Self::State, + line: &str, + mut push_token: impl FnMut(usize, Token), + ) -> Result<(), Self::Error> { + let changes = state.1.parse_line(line, Self::syntaxes())?; + let line_highlighter = + RangedHighlightIterator::new(&mut state.0, &changes, line, &self.highlighter); + + for (style, _, range) in line_highlighter { + let mut token = Token::default(); + token.colors.foreground = into_kas_text_color(style.foreground); + token.colors.background = if style.background.a == 0 { + None + } else { + Some(into_kas_text_color(style.background)) + }; + if style.font_style.contains(FontStyle::BOLD) { + token.weight = FontWeight::BOLD; + } + if style.font_style.contains(FontStyle::UNDERLINE) { + token.decoration.dec = DecorationType::Underline; + } + if style.font_style.contains(FontStyle::ITALIC) { + token.style = kas::text::fonts::FontStyle::Italic; + } + push_token(range.start, token); + } + + Ok(()) + } + fn highlight_text( &self, text: &str, mut push_token: impl FnMut(usize, Token), ) -> Result<(), Self::Error> { - let syntaxes = Self::syntaxes(); - - let mut state = HighlightState::new(&self.highlighter, Default::default()); - let mut parse_state = ParseState::new(&self.syntax); + let mut state = self.new_state(); for line_range in LineIterator::new(text) { let line_start = line_range.start; let line = &text[line_range]; - let changes = parse_state.parse_line(line, &syntaxes)?; - let line_highlighter = - RangedHighlightIterator::new(&mut state, &changes, line, &self.highlighter); - - for (style, _, range) in line_highlighter { - let mut token = Token::default(); - token.colors.foreground = into_kas_text_color(style.foreground); - token.colors.background = if style.background.a == 0 { - None - } else { - Some(into_kas_text_color(style.background)) - }; - if style.font_style.contains(FontStyle::BOLD) { - token.weight = FontWeight::BOLD; - } - if style.font_style.contains(FontStyle::UNDERLINE) { - token.decoration.dec = DecorationType::Underline; - } - if style.font_style.contains(FontStyle::ITALIC) { - token.style = kas::text::fonts::FontStyle::Italic; - } - push_token(line_start + range.start, token); - } + self.highlight_line(&mut state, line, &mut |index, token| { + push_token(line_start + index, token) + })?; } Ok(()) From 9d2e3f9d8ad3fa17fd141d975df22cc5e29f334a Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 28 Mar 2026 09:49:07 +0000 Subject: [PATCH 4/9] Remove fn Highlighter::highlight_text --- crates/kas-widgets/src/edit/highlight.rs | 22 ------------------- .../kas-widgets/src/edit/highlight/cache.rs | 12 +++++++++- .../kas-widgets/src/edit/highlight/syntect.rs | 19 ---------------- 3 files changed, 11 insertions(+), 42 deletions(-) diff --git a/crates/kas-widgets/src/edit/highlight.rs b/crates/kas-widgets/src/edit/highlight.rs index 7089aeca5..f8b9baaac 100644 --- a/crates/kas-widgets/src/edit/highlight.rs +++ b/crates/kas-widgets/src/edit/highlight.rs @@ -115,23 +115,6 @@ pub trait Highlighter { line: &str, push_token: impl FnMut(usize, Token), ) -> Result<(), Self::Error>; - - /// Highlight a `text` as a single item - /// - /// The method should yield a sequence of tokens each with a text index - /// using `push_token`. These must be yielded in order (i.e. `index` must be - /// strictly increasing). - /// - /// # Error handling - /// - /// In debug builds errors returned by this method or errors in the order of - /// tokens' `index` value will result in a panic, while in release builds - /// these will merely result in a log error and interrupt highlighting. - fn highlight_text( - &self, - text: &str, - push_token: impl FnMut(usize, Token), - ) -> Result<(), Self::Error>; } /// An implementation of [`Highlighter`] which doesn't highlight anything @@ -165,9 +148,4 @@ impl Highlighter for Plain { ) -> Result<(), Self::Error> { Ok::<(), std::convert::Infallible>(()) } - - #[inline] - fn highlight_text(&self, _: &str, _: impl FnMut(usize, Token)) -> Result<(), Self::Error> { - Ok::<(), std::convert::Infallible>(()) - } } diff --git a/crates/kas-widgets/src/edit/highlight/cache.rs b/crates/kas-widgets/src/edit/highlight/cache.rs index cc8496c11..f414eee18 100644 --- a/crates/kas-widgets/src/edit/highlight/cache.rs +++ b/crates/kas-widgets/src/edit/highlight/cache.rs @@ -7,6 +7,7 @@ use super::*; use kas::cast::Cast; +use kas::text::LineIterator; use kas::text::fonts::{FontSelector, FontStyle, FontWeight}; use kas::text::format::{Colors, Decoration, FontToken}; @@ -83,7 +84,16 @@ impl Cache { state = token; }; - highlighter.highlight_text(text, &mut push_token) + let mut state = highlighter.new_state(); + for line_range in LineIterator::new(text) { + let line_start = line_range.start; + let line = &text[line_range]; + highlighter.highlight_line(&mut state, line, &mut |index, token| { + push_token(line_start + index, token) + })?; + } + + Ok(()) } /// Highlight a whole `text`, logging errors diff --git a/crates/kas-widgets/src/edit/highlight/syntect.rs b/crates/kas-widgets/src/edit/highlight/syntect.rs index e59e0cc6e..c6159b641 100644 --- a/crates/kas-widgets/src/edit/highlight/syntect.rs +++ b/crates/kas-widgets/src/edit/highlight/syntect.rs @@ -8,7 +8,6 @@ use super::{SchemeColors, Token}; use kas::draw::color::Rgba8Srgb; use kas::event::ConfigCx; -use kas::text::LineIterator; use kas::text::fonts::FontWeight; use kas::text::format::{Color, DecorationType}; use std::sync::OnceLock; @@ -193,24 +192,6 @@ impl super::Highlighter for SyntectHighlighter { Ok(()) } - - fn highlight_text( - &self, - text: &str, - mut push_token: impl FnMut(usize, Token), - ) -> Result<(), Self::Error> { - let mut state = self.new_state(); - - for line_range in LineIterator::new(text) { - let line_start = line_range.start; - let line = &text[line_range]; - self.highlight_line(&mut state, line, &mut |index, token| { - push_token(line_start + index, token) - })?; - } - - Ok(()) - } } /// Convert to `Color`, even if transparent From bc351b7ac04cc3304e3eaabd0a7d7962e15dd06e Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Fri, 27 Mar 2026 18:13:38 +0000 Subject: [PATCH 5/9] Remove errant dbg! --- crates/kas-widgets/src/edit/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index 47ba178f0..798f02398 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -477,7 +477,7 @@ impl Part { part.status = Status::LevelRuns; if part.direction.is_auto() { - part.direction = if dbg!(part.display.text_is_rtl()) { + part.direction = if part.display.text_is_rtl() { Direction::AutoRtl } else { Direction::Auto From dcb3776a8a31b2d162db0d026eb274ec5bb09ee0 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Fri, 27 Mar 2026 18:12:01 +0000 Subject: [PATCH 6/9] Default cursor position to the start of multi-line editors --- crates/kas-widgets/src/edit/editor.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index 798f02398..af89ac1fc 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -394,7 +394,8 @@ impl Part { let text = text.to_string(); let len = text.len(); self.text = text; - self.selection.set_cursor(len); + let index = if self.wrap { 0 } else { len }; + self.selection.set_cursor(index); self } From 2c1706b6464c9947fbd9b2eeace0099a2ba97b67 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Fri, 27 Mar 2026 18:30:05 +0000 Subject: [PATCH 7/9] Inline fn received_text --- crates/kas-widgets/src/edit/editor.rs | 32 ++++++++------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index af89ac1fc..da035c30c 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -832,14 +832,17 @@ impl Part { } Err(NotReady) => EventAction::Used, }, - Event::Key(event, false) if event.state == ElementState::Pressed => { + Event::Key(event, false) if event.state == ElementState::Pressed && !self.read_only => { if let Some(text) = &event.text { self.save_undo_state(Some(EditOp::KeyInput)); - if self.received_text(cx, text) == Used { - EventAction::Edit - } else { - EventAction::Unused - } + self.cancel_selection_and_ime(cx); + + let selection = self.selection.range(); + self.replace_range(selection.clone(), text); + self.selection.set_cursor(selection.start + text.len()); + self.edit_x_coord = None; + + EventAction::Edit } else { let opt_cmd = cx .config() @@ -1212,23 +1215,6 @@ impl Part { .try_push((self.as_str().to_string(), *self.selection)); } - /// Insert `text` at the cursor position - /// - /// Committing undo state is the responsibility of the caller. - fn received_text(&mut self, cx: &mut EventCx, text: &str) -> IsUsed { - if self.read_only { - return Unused; - } - self.cancel_selection_and_ime(cx); - - let selection = self.selection.range(); - self.replace_range(selection.clone(), text); - self.selection.set_cursor(selection.start + text.len()); - self.edit_x_coord = None; - - Used - } - /// Request key focus, if we don't have it or IME fn request_key_focus(&self, cx: &mut EventCx, source: FocusSource) { if !self.has_key_focus && !self.current.is_ime_enabled() { From e9478a6a7b4c6b9da6fa00761e9e01877c2c266f Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 11 Apr 2026 15:29:57 +0000 Subject: [PATCH 8/9] Add highlight::ActionRestart --- crates/kas-widgets/src/edit/editor.rs | 2 +- crates/kas-widgets/src/edit/highlight.rs | 13 ++++++++----- crates/kas-widgets/src/edit/highlight/syntect.rs | 8 ++++---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index da035c30c..b52674e9c 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -94,7 +94,7 @@ impl Common { #[inline] #[must_use] pub fn configure(&mut self, cx: &mut ConfigCx) -> Option { - if self.highlighter.configure(cx) { + if let Some(_) = self.highlighter.configure(cx) { self.colors = self.highlighter.scheme_colors(); Some(ActionResetStatus) } else { diff --git a/crates/kas-widgets/src/edit/highlight.rs b/crates/kas-widgets/src/edit/highlight.rs index f8b9baaac..55d6368fc 100644 --- a/crates/kas-widgets/src/edit/highlight.rs +++ b/crates/kas-widgets/src/edit/highlight.rs @@ -19,6 +19,11 @@ use kas::event::ConfigCx; use kas::text::fonts::{FontStyle, FontWeight}; use kas::text::format::{Color, Colors, Decoration}; +/// Action: highlighting must be restarted +#[must_use] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct ActionRestart; + impl_scope! { /// Colors provided by the highlighter's color scheme #[impl_default] @@ -77,10 +82,8 @@ pub trait Highlighter { /// /// This is called when the widget is configured. It may be used to set the /// theme / color scheme. - /// - /// The method should return `true` when the highlighter should be re-run. #[must_use] - fn configure(&mut self, cx: &mut ConfigCx) -> bool; + fn configure(&mut self, cx: &mut ConfigCx) -> Option; /// Get scheme colors /// @@ -125,8 +128,8 @@ impl Highlighter for Plain { type State = (); #[inline] - fn configure(&mut self, _: &mut ConfigCx) -> bool { - false + fn configure(&mut self, _: &mut ConfigCx) -> Option { + None } #[inline] diff --git a/crates/kas-widgets/src/edit/highlight/syntect.rs b/crates/kas-widgets/src/edit/highlight/syntect.rs index c6159b641..c4e3c2f45 100644 --- a/crates/kas-widgets/src/edit/highlight/syntect.rs +++ b/crates/kas-widgets/src/edit/highlight/syntect.rs @@ -5,7 +5,7 @@ //! Syntax highlighting using [`syntect`](https://crates.io/crates/syntect) -use super::{SchemeColors, Token}; +use super::{ActionRestart, SchemeColors, Token}; use kas::draw::color::Rgba8Srgb; use kas::event::ConfigCx; use kas::text::fonts::FontWeight; @@ -101,17 +101,17 @@ impl super::Highlighter for SyntectHighlighter { type Error = ParsingError; type State = State; - fn configure(&mut self, cx: &mut ConfigCx) -> bool { + fn configure(&mut self, cx: &mut ConfigCx) -> Option { let dark = cx.config().theme().get_active_scheme().is_dark; if dark == self.dark { - return false; + return None; } self.dark = dark; let name = if dark { "base16-ocean.dark" } else { "InspiredGitHub" }; self.theme = themes().themes.get(name).unwrap(); self.highlighter = Highlighter::new(self.theme); - true + Some(ActionRestart) } fn scheme_colors(&self) -> SchemeColors { From fd1dea85f83e6933fb8c63a4753137f43bb9f7b1 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 11 Apr 2026 15:46:18 +0000 Subject: [PATCH 9/9] Use pre-expansion cfg for feature spec See: https://github.com/rust-lang/rust/issues/154045 --- crates/kas-core/src/util.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/kas-core/src/util.rs b/crates/kas-core/src/util.rs index d54da32c1..0540b7b24 100644 --- a/crates/kas-core/src/util.rs +++ b/crates/kas-core/src/util.rs @@ -143,19 +143,20 @@ impl<'a> fmt::Display for WidgetHierarchy<'a> { /// This requires the "spec" feature and nightly rustc to be useful. pub struct TryFormat<'a, T: ?Sized>(pub &'a T); -#[cfg(not(feature = "spec"))] -impl<'a, T: ?Sized> fmt::Debug for TryFormat<'a, T> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{{{}}}", std::any::type_name::()) - } +macro_rules! impl_debug_for_try_format { + ( $($default:ident)? ) => { + impl<'a, T: ?Sized> fmt::Debug for TryFormat<'a, T> { + $($default)? fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{{{}}}", std::any::type_name::()) + } + } + }; } +#[cfg(not(feature = "spec"))] +impl_debug_for_try_format!(); #[cfg(feature = "spec")] -impl<'a, T: ?Sized> fmt::Debug for TryFormat<'a, T> { - default fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{{{}}}", std::any::type_name::()) - } -} +impl_debug_for_try_format!(default); #[cfg(feature = "spec")] impl<'a, T: fmt::Debug + ?Sized> fmt::Debug for TryFormat<'a, T> {