diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 169b31f9b..41c690aa2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -48,6 +48,13 @@ jobs: run: cargo check --workspace --no-default-features env: RUSTFLAGS: "-D warnings" + - name: Check split-highlight feature + run: | + cargo check --workspace --all-targets --features 'split-highlight' + cargo check --workspace --all-targets --features 'split-highlight ansi-str' + cargo check --workspace --all-targets --features 'split-highlight anstyle' + cargo check --workspace --all-targets --features 'split-highlight ansi-str derive' + cargo check --workspace --all-targets --features 'split-highlight anstyle derive' direct-minimal-versions: name: Test min versions diff --git a/Cargo.toml b/Cargo.toml index d3f87d557..49eaee147 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,10 @@ maintenance = { status = "actively-developed" } members = ["rustyline-derive"] [dependencies] +# For convenience / compatibilty, you can highlight the whole input buffer, ansi-str helps to split +ansi-str = { version = "0.8.0", optional = true } +# ansi_str::Style is immutable so we use anstyle::Style instead +anstyle = { version = "1.0.8", optional = true } bitflags = "2.6" cfg-if = "1.0" # For file completion @@ -81,6 +85,8 @@ with-file-history = ["fd-lock"] with-sqlite-history = ["rusqlite"] with-fuzzy = ["skim"] case_insensitive_history_search = ["regex"] +# For continuation prompt, indentation, scrolling +split-highlight = [] [[example]] name = "custom_key_bindings" @@ -114,6 +120,8 @@ features = [ "with-dirs", "with-file-history", "with-fuzzy", + "split-highlight", + "anstyle", ] all-features = false no-default-features = true diff --git a/examples/example.rs b/examples/example.rs index d258064c6..12de9bd4f 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -37,10 +37,20 @@ impl Highlighter for MyHelper { Owned("\x1b[1m".to_owned() + hint + "\x1b[m") } + #[cfg(any(not(feature = "split-highlight"), feature = "ansi-str"))] fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { self.highlighter.highlight(line, pos) } + #[cfg(all(feature = "split-highlight", not(feature = "ansi-str")))] + fn highlight_line<'l>( + &self, + line: &'l str, + pos: usize, + ) -> impl Iterator { + self.highlighter.highlight_line(line, pos) + } + fn highlight_char(&self, line: &str, pos: usize, kind: CmdKind) -> bool { self.highlighter.highlight_char(line, pos, kind) } diff --git a/examples/read_password.rs b/examples/read_password.rs index d93418ae2..2fde46936 100644 --- a/examples/read_password.rs +++ b/examples/read_password.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow::{self, Borrowed, Owned}; - use rustyline::config::Configurer; use rustyline::highlight::{CmdKind, Highlighter}; use rustyline::{ColorMode, Editor, Result}; @@ -11,12 +9,31 @@ struct MaskingHighlighter { } impl Highlighter for MaskingHighlighter { - fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { + #[cfg(any(not(feature = "split-highlight"), feature = "ansi-str"))] + fn highlight<'l>(&self, line: &'l str, _pos: usize) -> std::borrow::Cow<'l, str> { + use unicode_width::UnicodeWidthStr; + if self.masking { + std::borrow::Cow::Owned(" ".repeat(line.width())) + } else { + std::borrow::Cow::Borrowed(line) + } + } + + #[cfg(all(feature = "split-highlight", not(feature = "ansi-str")))] + fn highlight_line<'l>( + &self, + line: &'l str, + _pos: usize, + ) -> impl Iterator { use unicode_width::UnicodeWidthStr; if self.masking { - Owned(" ".repeat(line.width())) + vec![( + rustyline::highlight::AnsiStyle::default(), + " ".repeat(line.width()), + )] + .into_iter() } else { - Borrowed(line) + vec![(rustyline::highlight::AnsiStyle::default(), line.to_owned())].into_iter() } } diff --git a/rustyline-derive/src/lib.rs b/rustyline-derive/src/lib.rs index e7082ab6d..4301de49a 100644 --- a/rustyline-derive/src/lib.rs +++ b/rustyline-derive/src/lib.rs @@ -102,10 +102,20 @@ pub fn highlighter_macro_derive(input: TokenStream) -> TokenStream { quote! { #[automatically_derived] impl #impl_generics ::rustyline::highlight::Highlighter for #name #ty_generics #where_clause { + #[cfg(any(not(feature = "split-highlight"), feature = "ansi-str"))] fn highlight<'l>(&self, line: &'l str, pos: usize) -> ::std::borrow::Cow<'l, str> { ::rustyline::highlight::Highlighter::highlight(&self.#field_name_or_index, line, pos) } + #[cfg(all(feature = "split-highlight", not(feature = "ansi-str")))] + fn highlight_line<'l>( + &self, + line: &'l str, + pos: usize, + ) -> impl Iterator { + ::rustyline::highlight::Highlighter::highlight_line(&self.#field_name_or_index, line, pos) + } + fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, diff --git a/src/edit.rs b/src/edit.rs index 9d85ab6b5..ccff1daa5 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -6,7 +6,7 @@ use unicode_segmentation::UnicodeSegmentation; use super::{Context, Helper, Result}; use crate::error::ReadlineError; -use crate::highlight::{CmdKind, Highlighter}; +use crate::highlight::CmdKind; use crate::hint::Hint; use crate::history::SearchDirection; use crate::keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word}; @@ -67,9 +67,9 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { } } - pub fn highlighter(&self) -> Option<&dyn Highlighter> { + pub fn highlighter(&self) -> Option<&H> { if self.out.colors_enabled() { - self.helper.map(|h| h as &dyn Highlighter) + self.helper } else { None } @@ -166,7 +166,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { Info::Msg(msg) => msg, }; let highlighter = if self.out.colors_enabled() { - self.helper.map(|h| h as &dyn Highlighter) + self.helper } else { None }; diff --git a/src/error.rs b/src/error.rs index 038ae4b2e..326c6ddff 100644 --- a/src/error.rs +++ b/src/error.rs @@ -125,7 +125,7 @@ impl From for ReadlineError { } } -#[cfg(unix)] +#[cfg(any(unix, all(feature = "split-highlight", not(feature = "ansi-str"))))] impl From for ReadlineError { fn from(err: fmt::Error) -> Self { Self::Io(io::Error::new(io::ErrorKind::Other, err)) diff --git a/src/highlight.rs b/src/highlight.rs index 6cc718a07..9490c3ea2 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -1,8 +1,116 @@ //! Syntax highlighting use crate::config::CompletionType; -use std::borrow::Cow::{self, Borrowed, Owned}; +use std::borrow::Cow::{self, Borrowed}; use std::cell::Cell; +#[cfg(feature = "split-highlight")] +use std::fmt::Display; + +/// ANSI style +#[cfg(feature = "split-highlight")] +#[cfg_attr(docsrs, doc(cfg(feature = "split-highlight")))] +pub trait Style: Default { + /// Produce a ansi sequences which sets the graphic mode + fn start(&self) -> impl Display; + /// Produce a ansi sequences which ends the graphic mode + fn end(&self) -> impl Display; +} + +#[cfg(feature = "split-highlight")] +impl Style for () { + fn start(&self) -> impl Display { + "" + } + + fn end(&self) -> impl Display { + "" + } +} + +/*#[cfg(feature = "ansi-str")] +#[cfg_attr(docsrs, doc(cfg(feature = "ansi-str")))] +impl Style for ansi_str::Style { + fn start(&self) -> impl Display { + self.start() + } + + fn end(&self) -> impl Display { + self.end() + } +}*/ +#[cfg(feature = "anstyle")] +#[cfg_attr(docsrs, doc(cfg(feature = "anstyle")))] +impl Style for anstyle::Style { + fn start(&self) -> impl Display { + self.render() + } + + fn end(&self) -> impl Display { + self.render_reset() + } +} + +/// ANSI Style +#[cfg(feature = "anstyle")] +#[cfg_attr(docsrs, doc(cfg(feature = "anstyle")))] +pub type AnsiStyle = anstyle::Style; +/// ANSI Style +#[cfg(not(feature = "anstyle"))] +pub type AnsiStyle = (); + +/// Styled text +#[cfg(feature = "split-highlight")] +#[cfg_attr(docsrs, doc(cfg(feature = "split-highlight")))] +pub trait StyledBlock { + /// Style impl + type Style: Style + where + Self: Sized; + /// Raw text to be styled + fn text(&self) -> &str; + /// `Style` to be applied on `text` + fn style(&self) -> &Self::Style + where + Self: Sized; +} +/*#[cfg(feature = "ansi-str")] +#[cfg_attr(docsrs, doc(cfg(feature = "ansi-str")))] +impl StyledBlock for ansi_str::AnsiBlock<'_> { + type Style = ansi_str::Style; + + fn text(&self) -> &str { + self.text() + } + + fn style(&self) -> &Self::Style { + self.style() + } +}*/ +#[cfg(feature = "split-highlight")] +#[cfg_attr(docsrs, doc(cfg(feature = "split-highlight")))] +impl StyledBlock for (AnsiStyle, &str) { + type Style = AnsiStyle; + + fn text(&self) -> &str { + self.1 + } + + fn style(&self) -> &Self::Style { + &self.0 + } +} +#[cfg(all(feature = "split-highlight", not(feature = "ansi-str")))] +impl StyledBlock for (AnsiStyle, String) { + type Style = AnsiStyle; + + fn text(&self) -> &str { + &self.1 + } + + fn style(&self) -> &Self::Style { + &self.0 + } +} /// Describe which kind of action has been triggering the call to `Highlighter`. #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -26,10 +134,32 @@ pub trait Highlighter { /// /// For example, you can implement /// [blink-matching-paren](https://www.gnu.org/software/bash/manual/html_node/Readline-Init-File-Syntax.html). + #[cfg(any(not(feature = "split-highlight"), feature = "ansi-str"))] + #[cfg_attr( + docsrs, + doc(cfg(any(not(feature = "split-highlight"), feature = "ansi-str"))) + )] fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { let _ = pos; Borrowed(line) } + + /// Takes the currently edited `line` with the cursor `pos`ition and + /// returns the styled blocks. + #[cfg(all(feature = "split-highlight", not(feature = "ansi-str")))] + #[cfg_attr( + docsrs, + doc(cfg(all(feature = "split-highlight", not(feature = "ansi-str")))) + )] + fn highlight_line<'l>( + &self, + line: &'l str, + pos: usize, + ) -> impl Iterator { + let _ = (line, pos); + vec![(AnsiStyle::default(), line)].into_iter() + } + /// Takes the `prompt` and /// returns the highlighted version (with ANSI color). fn highlight_prompt<'b, 's: 'b, 'p: 'b>( @@ -75,6 +205,8 @@ impl Highlighter for () {} /// Highlight matching bracket when typed or cursor moved on. #[derive(Default)] pub struct MatchingBracketHighlighter { + #[cfg(feature = "anstyle")] + style: anstyle::Style, bracket: Cell>, // memorize the character to search... } @@ -83,12 +215,22 @@ impl MatchingBracketHighlighter { #[must_use] pub fn new() -> Self { Self { + #[cfg(feature = "anstyle")] + style: anstyle::Style::new() + .bold() + .fg_color(Some(anstyle::AnsiColor::Blue.into())), bracket: Cell::new(None), } } } +#[cfg(any( + not(feature = "split-highlight"), + feature = "anstyle", + feature = "ansi-str" +))] impl Highlighter for MatchingBracketHighlighter { + #[cfg(any(not(feature = "split-highlight"), feature = "ansi-str"))] fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { if line.len() <= 1 { return Borrowed(line); @@ -98,12 +240,34 @@ impl Highlighter for MatchingBracketHighlighter { if let Some((matching, idx)) = find_matching_bracket(line, pos, bracket) { let mut copy = line.to_owned(); copy.replace_range(idx..=idx, &format!("\x1b[1;34m{}\x1b[0m", matching as char)); - return Owned(copy); + return Cow::Owned(copy); } } Borrowed(line) } + #[cfg(all(feature = "split-highlight", not(feature = "ansi-str")))] + fn highlight_line<'l>( + &self, + line: &'l str, + _pos: usize, + ) -> impl Iterator { + if line.len() <= 1 { + return vec![(AnsiStyle::default(), line)].into_iter(); + } + if let Some((bracket, pos)) = self.bracket.get() { + if let Some((_, idx)) = find_matching_bracket(line, pos, bracket) { + return vec![ + (AnsiStyle::default(), &line[0..idx]), + (self.style, &line[idx..=idx]), + (AnsiStyle::default(), &line[idx + 1..]), + ] + .into_iter(); + } + } + vec![(AnsiStyle::default(), line)].into_iter() + } + fn highlight_char(&self, line: &str, pos: usize, kind: CmdKind) -> bool { if kind == CmdKind::ForcedRefresh { self.bracket.set(None); @@ -256,4 +420,16 @@ mod tests { assert!(is_open_bracket(b'(')); assert!(is_close_bracket(b')')); } + + #[test] + #[cfg(feature = "ansi-str")] + pub fn styled_text() { + use ansi_str::get_blocks; + + let mut blocks = get_blocks("\x1b[1;32mHello \x1b[3mworld\x1b[23m!\x1b[0m"); + assert_eq!(blocks.next(), get_blocks("\x1b[1;32mHello ").next()); + assert_eq!(blocks.next(), get_blocks("\x1b[1;32m\x1b[3mworld").next()); + assert_eq!(blocks.next(), get_blocks("\x1b[1;32m!").next()); + assert!(blocks.next().is_none()) + } } diff --git a/src/tty/mod.rs b/src/tty/mod.rs index abc02ccec..a2de3e942 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -49,14 +49,14 @@ pub trait Renderer { fn move_cursor(&mut self, old: Position, new: Position) -> Result<()>; /// Display `prompt`, line and cursor in terminal output - fn refresh_line( + fn refresh_line( &mut self, prompt: &str, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, new_layout: &Layout, - highlighter: Option<&dyn Highlighter>, + highlighter: Option<&H>, ) -> Result<()>; /// Compute layout for rendering prompt + line + some info (either hint, diff --git a/src/tty/test.rs b/src/tty/test.rs index 97470dcec..147a5103b 100644 --- a/src/tty/test.rs +++ b/src/tty/test.rs @@ -100,14 +100,14 @@ impl Renderer for Sink { Ok(()) } - fn refresh_line( + fn refresh_line( &mut self, _prompt: &str, _line: &LineBuffer, _hint: Option<&str>, _old_layout: &Layout, _new_layout: &Layout, - _highlighter: Option<&dyn Highlighter>, + _highlighter: Option<&H>, ) -> Result<()> { Ok(()) } diff --git a/src/tty/unix.rs b/src/tty/unix.rs index 1c25bbb07..6b62d05a4 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -990,14 +990,14 @@ impl Renderer for PosixRenderer { Ok(()) } - fn refresh_line( + fn refresh_line( &mut self, prompt: &str, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, new_layout: &Layout, - highlighter: Option<&dyn Highlighter>, + highlighter: Option<&H>, ) -> Result<()> { use std::fmt::Write; self.buffer.clear(); @@ -1013,8 +1013,23 @@ impl Renderer for PosixRenderer { self.buffer .push_str(&highlighter.highlight_prompt(prompt, default_prompt)); // display the input line - self.buffer - .push_str(&highlighter.highlight(line, line.pos())); + cfg_if::cfg_if! { + if #[cfg(not(feature = "split-highlight"))] { + self.buffer + .push_str(&highlighter.highlight(line, line.pos())); + } else if #[cfg(feature = "ansi-str")] { + self.buffer + .push_str(&highlighter.highlight(line, line.pos())); + } else { + use crate::highlight::{Style, StyledBlock}; + for sb in highlighter.highlight_line(line, line.pos()) { + let style = sb.style(); + write!(self.buffer, "{}", style.start())?; + self.buffer.push_str(sb.text()); + write!(self.buffer, "{}", style.end())?; + } + } + } } else { // display the prompt self.buffer.push_str(prompt); @@ -1698,7 +1713,7 @@ mod test { let new_layout = out.compute_layout(prompt_size, default_prompt, &line, None); assert_eq!(Position { col: 1, row: 1 }, new_layout.cursor); assert_eq!(new_layout.cursor, new_layout.end); - out.refresh_line(prompt, &line, None, &old_layout, &new_layout, None) + out.refresh_line::<()>(prompt, &line, None, &old_layout, &new_layout, None) .unwrap(); #[rustfmt::skip] assert_eq!( diff --git a/src/tty/windows.rs b/src/tty/windows.rs index dc714031e..13246a1cf 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -428,14 +428,14 @@ impl Renderer for ConsoleRenderer { .map(|_| ()) } - fn refresh_line( + fn refresh_line( &mut self, prompt: &str, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, new_layout: &Layout, - highlighter: Option<&dyn Highlighter>, + highlighter: Option<&H>, ) -> Result<()> { let default_prompt = new_layout.default_prompt; let cursor = new_layout.cursor; @@ -448,7 +448,22 @@ impl Renderer for ConsoleRenderer { // append the prompt col = self.wrap_at_eol(&highlighter.highlight_prompt(prompt, default_prompt), col); // append the input line - col = self.wrap_at_eol(&highlighter.highlight(line, line.pos()), col); + cfg_if::cfg_if! { + if #[cfg(not(feature = "split-highlight"))] { + col = self.wrap_at_eol(&highlighter.highlight(line, line.pos()), col); + } else if #[cfg(feature = "ansi-str")] { + col = self.wrap_at_eol(&highlighter.highlight(line, line.pos()), col); + } else { + use std::fmt::Write; + use crate::highlight::{Style, StyledBlock}; + for sb in highlighter.highlight_line(line, line.pos()) { + let style = sb.style(); + write!(self.buffer, "{}", style.start())?; + col = self.wrap_at_eol(sb.text(), col); + write!(self.buffer, "{}", style.end())?; + } + } + } } else if self.colors_enabled { // append the prompt col = self.wrap_at_eol(prompt, col);