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> { diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index 47ba178f0..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 { @@ -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 } @@ -477,7 +478,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 @@ -831,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() @@ -1211,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() { diff --git a/crates/kas-widgets/src/edit/highlight.rs b/crates/kas-widgets/src/edit/highlight.rs index aeef57861..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] @@ -70,35 +75,48 @@ 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 /// 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 /// /// This method allows usage of the highlighter's colors by the editor. fn scheme_colors(&self) -> SchemeColors; - /// Highlight a `text` as a single item + /// 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 - /// using `push_token`. These must be yielded in order (i.e. `index` must be - /// strictly increasing). + /// (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_text( - &mut self, - text: &str, - push_token: &mut dyn FnMut(usize, Token), + fn highlight_line( + &self, + state: &mut Self::State, + line: &str, + push_token: impl FnMut(usize, Token), ) -> Result<(), Self::Error>; } @@ -107,10 +125,11 @@ 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 { - false + fn configure(&mut self, _: &mut ConfigCx) -> Option { + None } #[inline] @@ -119,10 +138,16 @@ impl Highlighter for Plain { } #[inline] - fn highlight_text( - &mut self, + fn new_state(&self) -> Self::State { + () + } + + #[inline] + fn highlight_line( + &self, + _: &mut Self::State, _: &str, - _: &mut dyn FnMut(usize, Token), + _: 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 89cf93386..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}; @@ -37,8 +38,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 +84,21 @@ impl Cache { state = token; }; - if let Err(err) = 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 + 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}"); } diff --git a/crates/kas-widgets/src/edit/highlight/syntect.rs b/crates/kas-widgets/src/edit/highlight/syntect.rs index fd1d9a5e9..c4e3c2f45 100644 --- a/crates/kas-widgets/src/edit/highlight/syntect.rs +++ b/crates/kas-widgets/src/edit/highlight/syntect.rs @@ -5,10 +5,9 @@ //! 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::LineIterator; use kas::text::fonts::FontWeight; use kas::text::format::{Color, DecorationType}; use std::sync::OnceLock; @@ -95,20 +94,24 @@ 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 { + 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 { @@ -149,42 +152,42 @@ impl super::Highlighter for SyntectHighlighter { } } - fn highlight_text( - &mut self, - text: &str, - push_token: &mut dyn FnMut(usize, Token), - ) -> Result<(), Self::Error> { - let syntaxes = Self::syntaxes(); + #[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) + } - let mut state = HighlightState::new(&self.highlighter, Default::default()); - let mut parse_state = ParseState::new(&self.syntax); - - 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); + #[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(())