diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 41c690aa2..b3d981db1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -48,13 +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' + # - 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 62f6d7524..1b35864b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = ["rustyline-derive"] # 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 } +anstyle = "1.0.8" bitflags = "2.6" cfg-if = "1.0" # For file completion @@ -71,7 +71,7 @@ with-sqlite-history = ["rusqlite"] with-fuzzy = ["skim"] case_insensitive_history_search = ["regex"] # For continuation prompt, indentation, scrolling -split-highlight = [] +# split-highlight = [] [[example]] name = "custom_key_bindings" @@ -97,6 +97,9 @@ required-features = ["derive"] [[example]] name = "sqlite_history" required-features = ["with-sqlite-history"] +[[example]] +name = "continuation_prompt" +required-features = ["custom-bindings", "derive"] [package.metadata.docs.rs] features = [ @@ -105,8 +108,8 @@ features = [ "with-dirs", "with-file-history", "with-fuzzy", - "split-highlight", - "anstyle", + # "split-highlight", + # "anstyle", ] all-features = false no-default-features = true diff --git a/README.md b/README.md index 47d4e1b79..79a60a300 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,8 @@ use rustyline::{DefaultEditor, Result}; fn main() -> Result<()> { // `()` can be used when no completer is required - let mut rl = DefaultEditor::new()?; + // Use `new(())` for default Helper. + let mut rl = DefaultEditor::new(())?; #[cfg(feature = "with-file-history")] if rl.load_history("history.txt").is_err() { println!("No previous history."); diff --git a/examples/continuation_prompt.rs b/examples/continuation_prompt.rs new file mode 100644 index 000000000..065e28622 --- /dev/null +++ b/examples/continuation_prompt.rs @@ -0,0 +1,80 @@ +use rustyline::highlight::{DisplayOnce, Highlighter, StyledBlocks}; +use rustyline::validate::{ValidationContext, ValidationResult, Validator}; +use rustyline::{Cmd, Editor, EventHandler, Helper, KeyCode, KeyEvent, Modifiers, Result}; +use rustyline::{Completer, Hinter}; + +#[derive(Completer, Hinter)] +struct InputValidator { + bracket_level: i32, + /// re-render only when input just changed + /// not render after cursor moving + need_render: bool, +} + +impl Helper for InputValidator { + fn update_after_edit(&mut self, line: &str, _pos: usize, _forced_refresh: bool) { + self.bracket_level = line.chars().fold(0, |level, c| { + if c == '(' { + level + 1 + } else if c == ')' { + level - 1 + } else { + level + } + }); + self.need_render = true; + } + fn continuation_prompt_width<'b, 's: 'b, 'p: 'b>(&'s self, prompt: &'p str) -> usize { + 3 + } +} + +impl Validator for InputValidator { + fn validate(&mut self, _ctx: &mut ValidationContext) -> Result { + if self.bracket_level > 0 { + Ok(ValidationResult::Incomplete(2)) + } else if self.bracket_level < 0 { + Ok(ValidationResult::Invalid(Some(format!( + " - excess {} close bracket", + -self.bracket_level + )))) + } else { + Ok(ValidationResult::Valid(None)) + } + } +} + +impl Highlighter for InputValidator { + fn highlight_char(&mut self, _line: &str, _pos: usize, _forced: bool) -> bool { + self.need_render + } + fn highlight<'b, 's: 'b, 'l: 'b>( + &'s mut self, + line: &'l str, + _pos: usize, + ) -> impl 'b + DisplayOnce { + use core::iter::once; + let mut lines = line.split('\n'); + self.need_render = false; + let iter = once(((), lines.next().unwrap())) + .chain(lines.flat_map(|line| once(((), "\n.. ")).chain(once(((), line))))); + StyledBlocks::new(iter) + } +} + +fn main() -> Result<()> { + let h = InputValidator { + bracket_level: 0, + need_render: true, + }; + let mut rl = Editor::new(h)?; + rl.bind_sequence( + KeyEvent(KeyCode::Char('s'), Modifiers::CTRL), + EventHandler::Simple(Cmd::Newline), + ); + + let input = rl.readline(">> ")?; + println!("Input: {input}"); + + Ok(()) +} diff --git a/examples/custom_key_bindings.rs b/examples/custom_key_bindings.rs index 7d6624af7..ac0a09e26 100644 --- a/examples/custom_key_bindings.rs +++ b/examples/custom_key_bindings.rs @@ -14,7 +14,7 @@ struct MyHelper(#[rustyline(Hinter)] HistoryHinter); impl Highlighter for MyHelper { fn highlight_prompt<'b, 's: 'b, 'p: 'b>( - &'s self, + &'s mut self, prompt: &'p str, default: bool, ) -> Cow<'b, str> { @@ -25,7 +25,7 @@ impl Highlighter for MyHelper { } } - fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + fn highlight_hint<'b, 's: 'b, 'h: 'b>(&'s mut self, hint: &'h str) -> Cow<'b, str> { Owned(format!("\x1b[1m{hint}\x1b[m")) } } @@ -85,8 +85,7 @@ impl ConditionalEventHandler for TabEventHandler { } fn main() -> Result<()> { - let mut rl = Editor::::new()?; - rl.set_helper(Some(MyHelper(HistoryHinter::new()))); + let mut rl = Editor::::new(MyHelper(HistoryHinter::new()))?; let ceh = Box::new(CompleteHintHandler); rl.bind_sequence(KeyEvent::ctrl('E'), EventHandler::Conditional(ceh.clone())); diff --git a/examples/diy_hints.rs b/examples/diy_hints.rs index af8fadc80..7bc67caa0 100644 --- a/examples/diy_hints.rs +++ b/examples/diy_hints.rs @@ -52,7 +52,7 @@ impl CommandHint { impl Hinter for DIYHinter { type Hint = CommandHint; - fn hint(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Option { + fn hint(&mut self, line: &str, pos: usize, _ctx: &Context<'_>) -> Option { if line.is_empty() || pos < line.len() { return None; } @@ -86,8 +86,7 @@ fn main() -> Result<()> { println!("This is a DIY hint hack of rustyline"); let h = DIYHinter { hints: diy_hints() }; - let mut rl: Editor = Editor::new()?; - rl.set_helper(Some(h)); + let mut rl: Editor = Editor::new(h)?; loop { let input = rl.readline("> ")?; diff --git a/examples/example.rs b/examples/example.rs index f2f5c79af..df501e7f2 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -2,7 +2,7 @@ use std::borrow::Cow::{self, Borrowed, Owned}; use rustyline::completion::FilenameCompleter; use rustyline::error::ReadlineError; -use rustyline::highlight::{Highlighter, MatchingBracketHighlighter}; +use rustyline::highlight::{DisplayOnce, Highlighter, MatchingBracketHighlighter}; use rustyline::hint::HistoryHinter; use rustyline::validate::MatchingBracketValidator; use rustyline::{Cmd, CompletionType, Config, EditMode, Editor, KeyEvent}; @@ -22,7 +22,7 @@ struct MyHelper { impl Highlighter for MyHelper { fn highlight_prompt<'b, 's: 'b, 'p: 'b>( - &'s self, + &'s mut self, prompt: &'p str, default: bool, ) -> Cow<'b, str> { @@ -33,25 +33,19 @@ impl Highlighter for MyHelper { } } - fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + fn highlight_hint<'b, 's: 'b, 'h: 'b>(&'s mut self, hint: &'h str) -> Cow<'b, str> { 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, + fn highlight<'b, 's: 'b, 'l: 'b>( + &'s mut self, line: &'l str, pos: usize, - ) -> impl Iterator { - self.highlighter.highlight_line(line, pos) + ) -> impl 'b + DisplayOnce { + self.highlighter.highlight(line, pos) } - fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool { + fn highlight_char(&mut self, line: &str, pos: usize, forced: bool) -> bool { self.highlighter.highlight_char(line, pos, forced) } } @@ -72,8 +66,7 @@ fn main() -> rustyline::Result<()> { colored_prompt: "".to_owned(), validator: MatchingBracketValidator::new(), }; - let mut rl = Editor::with_config(config)?; - rl.set_helper(Some(h)); + let mut rl = Editor::with_config(config, h)?; rl.bind_sequence(KeyEvent::alt('n'), Cmd::HistorySearchForward); rl.bind_sequence(KeyEvent::alt('p'), Cmd::HistorySearchBackward); if rl.load_history("history.txt").is_err() { @@ -82,7 +75,7 @@ fn main() -> rustyline::Result<()> { let mut count = 1; loop { let p = format!("{count}> "); - rl.helper_mut().expect("No helper").colored_prompt = format!("\x1b[1;32m{p}\x1b[0m"); + rl.helper_mut().colored_prompt = format!("\x1b[1;32m{p}\x1b[0m"); let readline = rl.readline(&p); match readline { Ok(line) => { diff --git a/examples/external_print.rs b/examples/external_print.rs index f93769e95..b85dce890 100644 --- a/examples/external_print.rs +++ b/examples/external_print.rs @@ -6,7 +6,7 @@ use rand::{thread_rng, Rng}; use rustyline::{DefaultEditor, ExternalPrinter, Result}; fn main() -> Result<()> { - let mut rl = DefaultEditor::new()?; + let mut rl = DefaultEditor::new(())?; let mut printer = rl.create_external_printer()?; thread::spawn(move || { let mut rng = thread_rng(); diff --git a/examples/input_multiline.rs b/examples/input_multiline.rs index ead5e8825..e72e49556 100644 --- a/examples/input_multiline.rs +++ b/examples/input_multiline.rs @@ -1,4 +1,4 @@ -use rustyline::highlight::MatchingBracketHighlighter; +use rustyline::highlight::{DisplayOnce, MatchingBracketHighlighter}; use rustyline::validate::MatchingBracketValidator; use rustyline::{Cmd, Editor, EventHandler, KeyCode, KeyEvent, Modifiers, Result}; use rustyline::{Completer, Helper, Highlighter, Hinter, Validator}; @@ -16,8 +16,7 @@ fn main() -> Result<()> { brackets: MatchingBracketValidator::new(), highlighter: MatchingBracketHighlighter::new(), }; - let mut rl = Editor::new()?; - rl.set_helper(Some(h)); + let mut rl = Editor::new(h)?; rl.bind_sequence( KeyEvent(KeyCode::Char('s'), Modifiers::CTRL), EventHandler::Simple(Cmd::Newline), diff --git a/examples/input_validation.rs b/examples/input_validation.rs index 583d94f62..9c5f17f0c 100644 --- a/examples/input_validation.rs +++ b/examples/input_validation.rs @@ -6,13 +6,13 @@ use rustyline::{Editor, Result}; struct InputValidator {} impl Validator for InputValidator { - fn validate(&self, ctx: &mut ValidationContext) -> Result { + fn validate(&mut self, ctx: &mut ValidationContext) -> Result { use ValidationResult::{Incomplete, Invalid, Valid}; let input = ctx.input(); let result = if !input.starts_with("SELECT") { Invalid(Some(" --< Expect: SELECT stmt".to_owned())) } else if !input.ends_with(';') { - Incomplete + Incomplete(0) } else { Valid(None) }; @@ -22,8 +22,7 @@ impl Validator for InputValidator { fn main() -> Result<()> { let h = InputValidator {}; - let mut rl = Editor::new()?; - rl.set_helper(Some(h)); + let mut rl = Editor::new(h)?; let input = rl.readline("> ")?; println!("Input: {input}"); diff --git a/examples/minimal.rs b/examples/minimal.rs index 9fc627ee4..d1d562b19 100644 --- a/examples/minimal.rs +++ b/examples/minimal.rs @@ -3,7 +3,7 @@ use rustyline::{DefaultEditor, Result}; /// Minimal REPL fn main() -> Result<()> { env_logger::init(); - let mut rl = DefaultEditor::new()?; + let mut rl = DefaultEditor::new(())?; loop { let line = rl.readline("> ")?; // read println!("Line: {line}"); // eval / print diff --git a/examples/numeric_input.rs b/examples/numeric_input.rs index 49eb6f94b..1060af660 100644 --- a/examples/numeric_input.rs +++ b/examples/numeric_input.rs @@ -19,7 +19,7 @@ impl ConditionalEventHandler for FilteringEventHandler { } fn main() -> Result<()> { - let mut rl = DefaultEditor::new()?; + let mut rl = DefaultEditor::new(())?; rl.bind_sequence( Event::Any, diff --git a/examples/read_password.rs b/examples/read_password.rs index 249b7d8ed..79d0f5d60 100644 --- a/examples/read_password.rs +++ b/examples/read_password.rs @@ -9,8 +9,11 @@ struct MaskingHighlighter { } impl Highlighter for MaskingHighlighter { - #[cfg(any(not(feature = "split-highlight"), feature = "ansi-str"))] - fn highlight<'l>(&self, line: &'l str, _pos: usize) -> std::borrow::Cow<'l, str> { + fn highlight<'b, 's: 'b, 'l: 'b>( + &'s mut self, + line: &'l str, + pos: usize, + ) -> std::borrow::Cow<'b, str> { use unicode_width::UnicodeWidthStr; if self.masking { std::borrow::Cow::Owned(" ".repeat(line.width())) @@ -19,25 +22,7 @@ impl Highlighter for MaskingHighlighter { } } - #[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 { - vec![( - rustyline::highlight::AnsiStyle::default(), - " ".repeat(line.width()), - )] - .into_iter() - } else { - vec![(rustyline::highlight::AnsiStyle::default(), line.to_owned())].into_iter() - } - } - - fn highlight_char(&self, _line: &str, _pos: usize, _forced: bool) -> bool { + fn highlight_char(&mut self, _line: &str, _pos: usize, _forced: bool) -> bool { self.masking } } @@ -45,13 +30,12 @@ impl Highlighter for MaskingHighlighter { fn main() -> Result<()> { println!("This is just a hack. Reading passwords securely requires more than that."); let h = MaskingHighlighter { masking: false }; - let mut rl = Editor::new()?; - rl.set_helper(Some(h)); + let mut rl = Editor::new(h)?; let username = rl.readline("Username:")?; println!("Username: {username}"); - rl.helper_mut().expect("No helper").masking = true; + rl.helper_mut().masking = true; rl.set_color_mode(ColorMode::Forced); // force masking rl.set_auto_add_history(false); // make sure password is not added to history let mut guard = rl.set_cursor_visibility(false)?; diff --git a/examples/sqlite_history.rs b/examples/sqlite_history.rs index a1efac840..04f9c3910 100644 --- a/examples/sqlite_history.rs +++ b/examples/sqlite_history.rs @@ -9,7 +9,7 @@ fn main() -> Result<()> { // file rustyline::sqlite_history::SQLiteHistory::open(config, "history.sqlite3")? }; - let mut rl: Editor<(), _> = Editor::with_history(config, history)?; + let mut rl: Editor<(), _> = Editor::with_history(config, history, ())?; loop { let line = rl.readline("> ")?; println!("{line}"); diff --git a/rustyline-derive/src/lib.rs b/rustyline-derive/src/lib.rs index 2672136f7..8b80481a7 100644 --- a/rustyline-derive/src/lib.rs +++ b/rustyline-derive/src/lib.rs @@ -51,16 +51,16 @@ pub fn completer_macro_derive(input: TokenStream) -> TokenStream { type Candidate = <#field_type as ::rustyline::completion::Completer>::Candidate; fn complete( - &self, + &mut self, line: &str, pos: usize, ctx: &::rustyline::Context<'_>, ) -> ::rustyline::Result<(usize, ::std::vec::Vec)> { - ::rustyline::completion::Completer::complete(&self.#field_name_or_index, line, pos, ctx) + ::rustyline::completion::Completer::complete(&mut self.#field_name_or_index, line, pos, ctx) } - fn update(&self, line: &mut ::rustyline::line_buffer::LineBuffer, start: usize, elected: &str, cl: &mut ::rustyline::Changeset) { - ::rustyline::completion::Completer::update(&self.#field_name_or_index, line, start, elected, cl) + fn update(&mut self, line: &mut ::rustyline::line_buffer::LineBuffer, start: usize, elected: &str, cl: &mut ::rustyline::Changeset) { + ::rustyline::completion::Completer::update(&mut self.#field_name_or_index, line, start, elected, cl) } } } @@ -102,42 +102,32 @@ 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<'b,'h:'b,'l:'b>(&'h mut self, line: &'l str, pos: usize) -> impl 'b + ::rustyline::highlight::DisplayOnce { + ::rustyline::highlight::Highlighter::highlight(&mut self.#field_name_or_index, line, pos) } fn highlight_prompt<'b, 's: 'b, 'p: 'b>( - &'s self, + &'s mut self, prompt: &'p str, default: bool, - ) -> ::std::borrow::Cow<'b, str> { - ::rustyline::highlight::Highlighter::highlight_prompt(&self.#field_name_or_index, prompt, default) + ) -> impl 'b + ::rustyline::highlight::DisplayOnce { + ::rustyline::highlight::Highlighter::highlight_prompt(&mut self.#field_name_or_index, prompt, default) } - fn highlight_hint<'h>(&self, hint: &'h str) -> ::std::borrow::Cow<'h, str> { - ::rustyline::highlight::Highlighter::highlight_hint(&self.#field_name_or_index, hint) + fn highlight_hint<'b, 's: 'b, 'h: 'b>(&'s mut self, hint: &'h str) -> impl 'b + ::rustyline::highlight::DisplayOnce { + ::rustyline::highlight::Highlighter::highlight_hint(&mut self.#field_name_or_index, hint) } - fn highlight_candidate<'c>( - &self, + fn highlight_candidate<'b, 's: 'b, 'c: 'b>( + &'s mut self, candidate: &'c str, completion: ::rustyline::config::CompletionType, - ) -> ::std::borrow::Cow<'c, str> { - ::rustyline::highlight::Highlighter::highlight_candidate(&self.#field_name_or_index, candidate, completion) + ) -> impl 'b + ::rustyline::highlight::DisplayOnce { + ::rustyline::highlight::Highlighter::highlight_candidate(&mut self.#field_name_or_index, candidate, completion) } - fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool { - ::rustyline::highlight::Highlighter::highlight_char(&self.#field_name_or_index, line, pos, forced) + fn highlight_char(&mut self, line: &str, pos: usize, forced: bool) -> bool { + ::rustyline::highlight::Highlighter::highlight_char(&mut self.#field_name_or_index, line, pos, forced) } } } @@ -166,8 +156,8 @@ pub fn hinter_macro_derive(input: TokenStream) -> TokenStream { impl #impl_generics ::rustyline::hint::Hinter for #name #ty_generics #where_clause { type Hint = <#field_type as ::rustyline::hint::Hinter>::Hint; - fn hint(&self, line: &str, pos: usize, ctx: &::rustyline::Context<'_>) -> ::std::option::Option { - ::rustyline::hint::Hinter::hint(&self.#field_name_or_index, line, pos, ctx) + fn hint(&mut self, line: &str, pos: usize, ctx: &::rustyline::Context<'_>) -> ::std::option::Option { + ::rustyline::hint::Hinter::hint(&mut self.#field_name_or_index, line, pos, ctx) } } } @@ -195,14 +185,14 @@ pub fn validator_macro_derive(input: TokenStream) -> TokenStream { #[automatically_derived] impl #impl_generics ::rustyline::validate::Validator for #name #ty_generics #where_clause { fn validate( - &self, + &mut self, ctx: &mut ::rustyline::validate::ValidationContext, ) -> ::rustyline::Result<::rustyline::validate::ValidationResult> { - ::rustyline::validate::Validator::validate(&self.#field_name_or_index, ctx) + ::rustyline::validate::Validator::validate(&mut self.#field_name_or_index, ctx) } - fn validate_while_typing(&self) -> bool { - ::rustyline::validate::Validator::validate_while_typing(&self.#field_name_or_index) + fn validate_while_typing(&mut self) -> bool { + ::rustyline::validate::Validator::validate_while_typing(&mut self.#field_name_or_index) } } } diff --git a/src/command.rs b/src/command.rs index f0185e2cb..1bad231f9 100644 --- a/src/command.rs +++ b/src/command.rs @@ -133,6 +133,7 @@ pub fn execute( Cmd::AcceptLine | Cmd::AcceptOrInsertLine { .. } => { let validation_result = s.validate()?; let valid = validation_result.is_valid(); + let incomplete = validation_result.is_incomplete(); let end = s.line.is_end_of_input(); match (cmd, valid, end) { (Cmd::AcceptLine, ..) @@ -151,6 +152,9 @@ pub fn execute( if valid || !validation_result.has_message() { s.edit_insert('\n', 1)?; } + if let Some(indent) = incomplete { + s.edit_insert(' ', indent)?; + } } _ => unreachable!(), } diff --git a/src/completion.rs b/src/completion.rs index 114d1aa9e..ca8620281 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -90,7 +90,7 @@ pub trait Completer { /// /// ("ls /usr/loc", 11) => Ok((3, vec!["/usr/local/"])) fn complete( - &self, // FIXME should be `&mut self` + &mut self, // FIXME should be `&mut self` line: &str, pos: usize, ctx: &Context<'_>, @@ -99,7 +99,7 @@ pub trait Completer { Ok((0, Vec::with_capacity(0))) } /// Updates the edited `line` with the `elected` candidate. - fn update(&self, line: &mut LineBuffer, start: usize, elected: &str, cl: &mut Changeset) { + fn update(&mut self, line: &mut LineBuffer, start: usize, elected: &str, cl: &mut Changeset) { let end = line.pos(); line.replace(start..end, elected, cl); } @@ -108,16 +108,22 @@ pub trait Completer { impl Completer for () { type Candidate = String; - fn update(&self, _line: &mut LineBuffer, _start: usize, _elected: &str, _cl: &mut Changeset) { + fn update( + &mut self, + _line: &mut LineBuffer, + _start: usize, + _elected: &str, + _cl: &mut Changeset, + ) { unreachable!(); } } -impl<'c, C: ?Sized + Completer> Completer for &'c C { +impl<'c, C: ?Sized + Completer> Completer for &'c mut C { type Candidate = C::Candidate; fn complete( - &self, + &mut self, line: &str, pos: usize, ctx: &Context<'_>, @@ -125,31 +131,31 @@ impl<'c, C: ?Sized + Completer> Completer for &'c C { (**self).complete(line, pos, ctx) } - fn update(&self, line: &mut LineBuffer, start: usize, elected: &str, cl: &mut Changeset) { + fn update(&mut self, line: &mut LineBuffer, start: usize, elected: &str, cl: &mut Changeset) { (**self).update(line, start, elected, cl); } } -macro_rules! box_completer { - ($($id: ident)*) => { - $( - impl Completer for $id { - type Candidate = C::Candidate; - - fn complete(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Result<(usize, Vec)> { - (**self).complete(line, pos, ctx) - } - fn update(&self, line: &mut LineBuffer, start: usize, elected: &str, cl: &mut Changeset) { - (**self).update(line, start, elected, cl) - } - } - )* - } -} +// macro_rules! box_completer { +// ($($id: ident)*) => { +// $( +// impl Completer for $id { +// type Candidate = C::Candidate; + +// fn complete(&mut self, line: &str, pos: usize, ctx: &Context<'_>) -> Result<(usize, Vec)> { +// (**self).complete(line, pos, ctx) +// } +// fn update(&mut self, line: &mut LineBuffer, start: usize, elected: &str, cl: &mut Changeset) { +// (**self).update(line, start, elected, cl) +// } +// } +// )* +// } +// } use crate::undo::Changeset; use std::rc::Rc; -use std::sync::Arc; -box_completer! { Box Rc Arc } +// use std::sync::Arc; +// box_completer! { Box Rc Arc } /// A `Completer` for file and folder names. pub struct FilenameCompleter { @@ -257,7 +263,12 @@ impl Default for FilenameCompleter { impl Completer for FilenameCompleter { type Candidate = Pair; - fn complete(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Result<(usize, Vec)> { + fn complete( + &mut self, + line: &str, + pos: usize, + _ctx: &Context<'_>, + ) -> Result<(usize, Vec)> { self.complete_path(line, pos) } } diff --git a/src/edit.rs b/src/edit.rs index fa02a4249..3cedbd6c6 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -31,7 +31,7 @@ pub struct State<'out, 'prompt, H: Helper> { saved_line_for_history: LineBuffer, // Current edited line before history browsing byte_buffer: [u8; 4], pub changes: Changeset, // changes to line, for undo/redo - pub helper: Option<&'out H>, + pub helper: &'out mut H, pub ctx: Context<'out>, // Give access to history for `hinter` pub hint: Option>, // last hint displayed pub highlight_char: bool, // `true` if a char has been highlighted @@ -48,10 +48,14 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { pub fn new( out: &'out mut ::Writer, prompt: &'prompt str, - helper: Option<&'out H>, + helper: &'out mut H, ctx: Context<'out>, ) -> Self { - let prompt_size = out.calculate_position(prompt, Position::default()); + let prompt_size = out.calculate_position( + prompt, + Position::default(), + helper.continuation_prompt_width(prompt), + ); Self { out, prompt, @@ -69,14 +73,6 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { } } - pub fn highlighter(&self) -> Option<&H> { - if self.out.colors_enabled() { - self.helper - } else { - None - } - } - pub fn next_cmd( &mut self, input_state: &mut InputState, @@ -94,9 +90,11 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { if new_cols != old_cols && (self.layout.end.row > 0 || self.layout.end.col >= new_cols) { - self.prompt_size = self - .out - .calculate_position(self.prompt, Position::default()); + self.prompt_size = self.out.calculate_position( + self.prompt, + Position::default(), + self.helper.continuation_prompt_width(self.prompt), + ); self.refresh_line()?; } continue; @@ -123,9 +121,11 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { pub fn move_cursor(&mut self) -> Result<()> { // calculate the desired position of the cursor - let cursor = self - .out - .calculate_position(&self.line[..self.line.pos()], self.prompt_size); + let cursor = self.out.calculate_position( + &self.line[..self.line.pos()], + self.prompt_size, + self.helper.continuation_prompt_width(&self.prompt), + ); if self.layout.cursor == cursor { return Ok(()); } @@ -139,6 +139,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { debug_assert!(self.layout.prompt_size <= self.layout.cursor); debug_assert!(self.layout.cursor <= self.layout.end); } + self.update_after_move_cursor(); Ok(()) } @@ -148,11 +149,14 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { } self.out.move_cursor(self.layout.cursor, self.layout.end)?; self.layout.cursor = self.layout.end; + self.update_after_move_cursor(); Ok(()) } pub fn move_cursor_at_leftmost(&mut self, rdr: &mut ::Reader) -> Result<()> { - self.out.move_cursor_at_leftmost(rdr) + self.out.move_cursor_at_leftmost(rdr)?; + self.update_after_move_cursor(); + Ok(()) } fn refresh( @@ -167,15 +171,14 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { Info::Hint => self.hint.as_ref().map(|h| h.display()), Info::Msg(msg) => msg, }; - let highlighter = if self.out.colors_enabled() { - self.helper - } else { - None - }; - let new_layout = self - .out - .compute_layout(prompt_size, default_prompt, &self.line, info); + let new_layout = self.out.compute_layout( + prompt_size, + default_prompt, + self.helper.continuation_prompt_width(prompt), + &self.line, + info, + ); debug!(target: "rustyline", "old layout: {:?}", self.layout); debug!(target: "rustyline", "new layout: {:?}", new_layout); @@ -185,7 +188,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { info, &self.layout, &new_layout, - highlighter, + self.helper, )?; self.layout = new_layout; @@ -193,21 +196,30 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { } pub fn hint(&mut self) { - if let Some(hinter) = self.helper { - let hint = hinter.hint(self.line.as_str(), self.line.pos(), &self.ctx); - self.hint = match hint { - Some(val) if !val.display().is_empty() => Some(Box::new(val) as Box), - _ => None, - }; - } else { - self.hint = None; - } + let hint = self + .helper + .hint(self.line.as_str(), self.line.pos(), &self.ctx); + self.hint = match hint { + Some(val) if !val.display().is_empty() => Some(Box::new(val) as Box), + _ => None, + }; + } + + fn update_after_edit(&mut self) { + self.helper + .update_after_edit(&self.line, self.line.pos(), self.forced_refresh); + } + + fn update_after_move_cursor(&mut self) { + self.helper + .update_after_move_cursor(&self.line, self.line.pos()); } fn highlight_char(&mut self) -> bool { - if let Some(highlighter) = self.highlighter() { + if self.out.colors_enabled() { let highlight_char = - highlighter.highlight_char(&self.line, self.line.pos(), self.forced_refresh); + self.helper + .highlight_char(&self.line, self.line.pos(), self.forced_refresh); if highlight_char { self.highlight_char = true; true @@ -228,30 +240,31 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { } pub fn validate(&mut self) -> Result { - if let Some(validator) = self.helper { - self.changes.begin(); - let result = validator.validate(&mut ValidationContext::new(self))?; - let corrected = self.changes.end(); - match result { - ValidationResult::Incomplete => {} - ValidationResult::Valid(ref msg) => { - // Accept the line regardless of where the cursor is. - if corrected || self.has_hint() || msg.is_some() { - // Force a refresh without hints to leave the previous - // line as the user typed it after a newline. - self.refresh_line_with_msg(msg.as_deref())?; - } + self.changes.begin(); + let mut invoke: &str = &self.line; + let result = self + .helper + .validate(&mut ValidationContext::new(&mut invoke))?; + let corrected = self.changes.end(); + match result { + ValidationResult::Incomplete(_) => { + self.refresh_line()?; + } + ValidationResult::Valid(ref msg) => { + // Accept the line regardless of where the cursor is. + if corrected || self.has_hint() || msg.is_some() { + // Force a refresh without hints to leave the previous + // line as the user typed it after a newline. + self.refresh_line_with_msg(msg.as_deref())?; } - ValidationResult::Invalid(ref msg) => { - if corrected || self.has_hint() || msg.is_some() { - self.refresh_line_with_msg(msg.as_deref())?; - } + } + ValidationResult::Invalid(ref msg) => { + if corrected || self.has_hint() || msg.is_some() { + self.refresh_line_with_msg(msg.as_deref())?; } } - Ok(result) - } else { - Ok(ValidationResult::Valid(None)) } + Ok(result) } } @@ -264,6 +277,7 @@ impl<'out, 'prompt, H: Helper> Invoke for State<'out, 'prompt, H> { impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> { fn refresh_line(&mut self) -> Result<()> { let prompt_size = self.prompt_size; + self.update_after_edit(); self.hint(); self.highlight_char(); self.refresh(self.prompt, prompt_size, true, Info::Hint) @@ -272,12 +286,18 @@ impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> { fn refresh_line_with_msg(&mut self, msg: Option<&str>) -> Result<()> { let prompt_size = self.prompt_size; self.hint = None; + self.update_after_edit(); self.highlight_char(); self.refresh(self.prompt, prompt_size, true, Info::Msg(msg)) } fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> { - let prompt_size = self.out.calculate_position(prompt, Position::default()); + let prompt_size = self.out.calculate_position( + prompt, + Position::default(), + self.helper.continuation_prompt_width(prompt), + ); + self.update_after_edit(); self.hint(); self.highlight_char(); self.refresh(prompt, prompt_size, false, Info::Hint) @@ -351,6 +371,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { /// Insert the character `ch` at cursor current position. pub fn edit_insert(&mut self, ch: char, n: RepeatCount) -> Result<()> { if let Some(push) = self.line.insert(ch, n, &mut self.changes) { + self.update_after_edit(); if push { let prompt_size = self.prompt_size; let no_previous_hint = self.hint.is_none(); @@ -748,7 +769,7 @@ pub fn init_state<'out, H: Helper>( out: &'out mut ::Writer, line: &str, pos: usize, - helper: Option<&'out H>, + helper: &'out mut H, history: &'out crate::history::DefaultHistory, ) -> State<'out, 'static, H> { State { @@ -781,8 +802,8 @@ mod test { history.add("line0").unwrap(); history.add("line1").unwrap(); let line = "current edited line"; - let helper: Option<()> = None; - let mut s = init_state(&mut out, line, 6, helper.as_ref(), &history); + let mut binding = (); + let mut s = init_state(&mut out, line, 6, &mut binding, &history); s.ctx.history_index = history.len(); for _ in 0..2 { diff --git a/src/error.rs b/src/error.rs index 1ddfc5f64..b75101199 100644 --- a/src/error.rs +++ b/src/error.rs @@ -125,7 +125,7 @@ impl From for ReadlineError { } } -#[cfg(any(unix, all(feature = "split-highlight", not(feature = "ansi-str"))))] +#[cfg(unix)] 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 56fb11443..671497244 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -1,22 +1,128 @@ //! Syntax highlighting use crate::config::CompletionType; -use std::borrow::Cow::{self, Borrowed}; +use core::fmt::Display; use std::cell::Cell; -#[cfg(feature = "split-highlight")] -use std::fmt::Display; +use std::marker::PhantomData; /// ANSI style -#[cfg(feature = "split-highlight")] -#[cfg_attr(docsrs, doc(cfg(feature = "split-highlight")))] -pub trait Style: Default { +pub trait Style { /// 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")] +/// The general trait that consume self to display +/// +/// For **normal** highlight, all types that impl [`core::fmt::Display`] will auto-impl [`DisplayOnce`] +/// +/// For **split-highlight**, you can use `impl Iterator` +/// to get a [`StyledBlocks`], which is also impl [`DisplayOnce`]: +/// +/// ``` +/// use rustyline::highlight::{DisplayOnce, StyledBlock, StyledBlocks}; +/// use anstyle::{Ansi256Color, Style}; +/// struct Helper; +/// fn highlight<'b, 's: 'b, 'l: 'b>( +/// helper: &'s mut Helper, +/// line: &'l str, +/// ) -> impl 'b + DisplayOnce { +/// fn get_style(i: usize) -> Style { +/// Style::new().fg_color(Some(Ansi256Color((i % 16) as u8).into())) +/// } +/// let iter = (0..line.len()).map(move |i| (get_style(i), &line[i..i + 1])); +/// StyledBlocks::new(iter) +/// } +/// let mut helper = Helper; +/// highlight(&mut helper, "hello world\n").print(); +/// ``` +pub trait DisplayOnce { + /// consume self to display + fn fmt(self, f: &mut W) -> core::fmt::Result; + /// consume self to print + fn print(self) -> core::fmt::Result + where + Self: Sized, + { + struct StdoutWriter; + impl core::fmt::Write for StdoutWriter { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + use std::io::Write; + std::io::stdout() + .write_all(s.as_bytes()) + .map_err(|_| core::fmt::Error) + } + } + let mut stdout = StdoutWriter; + Self::fmt(self, &mut stdout) + } +} + +impl<'l, T: Display> DisplayOnce for T { + fn fmt(self, f: &mut W) -> core::fmt::Result { + write!(f, "{}", self) + } +} + +/// A wrapper of `impl Iterator` +/// that impl [`DisplayOnce`]: +/// +/// ``` +/// use rustyline::highlight::{DisplayOnce, StyledBlock, StyledBlocks}; +/// use anstyle::{Ansi256Color, Style}; +/// struct Helper; +/// fn highlight<'b, 's: 'b, 'l: 'b>( +/// helper: &'s mut Helper, +/// line: &'l str, +/// ) -> impl 'b + DisplayOnce { +/// fn get_style(i: usize) -> Style { +/// Style::new().fg_color(Some(Ansi256Color((i % 16) as u8).into())) +/// } +/// let iter = (0..line.len()).map(move |i| (get_style(i), &line[i..i + 1])); +/// StyledBlocks::new(iter) +/// } +/// let mut helper = Helper; +/// highlight(&mut helper, "hello world\n").print(); +/// ``` +pub struct StyledBlocks<'l, B, I> +where + B: 'l + StyledBlock, + I: Iterator, +{ + iter: I, + _marker: PhantomData<&'l ()>, +} + +impl<'l, B, I> StyledBlocks<'l, B, I> +where + B: 'l + StyledBlock, + I: Iterator, +{ + /// create a new [`StyledBlocks`] wrapper + pub const fn new(iter: I) -> Self { + Self { + iter, + _marker: PhantomData, + } + } +} + +impl<'l, B, I> DisplayOnce for StyledBlocks<'l, B, I> +where + B: 'l + StyledBlock, + I: Iterator, +{ + fn fmt(self, f: &mut W) -> core::fmt::Result { + self.iter + .map(|block| { + let style = block.style(); + write!(f, "{}{}{}", style.start(), block.text(), style.end()) + }) + .collect() + } +} + impl Style for () { fn start(&self) -> impl Display { "" @@ -38,8 +144,7 @@ impl Style for ansi_str::Style { self.end() } }*/ -#[cfg(feature = "anstyle")] -#[cfg_attr(docsrs, doc(cfg(feature = "anstyle")))] + impl Style for anstyle::Style { fn start(&self) -> impl Display { self.render() @@ -50,17 +155,7 @@ impl Style for anstyle::Style { } } -/// 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 @@ -86,25 +181,12 @@ impl StyledBlock for ansi_str::AnsiBlock<'_> { 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; +impl> StyledBlock for (S, T) { + type Style = S; fn text(&self) -> &str { - &self.1 + self.1.as_ref() } fn style(&self) -> &Self::Style { @@ -122,58 +204,41 @@ 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, + fn highlight<'b, 's: 'b, 'l: 'b>( + &'s mut self, line: &'l str, pos: usize, - ) -> impl Iterator { - let _ = (line, pos); - vec![(AnsiStyle::default(), line)].into_iter() + ) -> impl 'b + DisplayOnce { + let _ = pos; + line } /// Takes the `prompt` and /// returns the highlighted version (with ANSI color). fn highlight_prompt<'b, 's: 'b, 'p: 'b>( - &'s self, + &'s mut self, prompt: &'p str, default: bool, - ) -> Cow<'b, str> { + ) -> impl 'b + DisplayOnce { let _ = default; - Borrowed(prompt) + prompt } /// Takes the `hint` and /// returns the highlighted version (with ANSI color). - fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { - Borrowed(hint) + fn highlight_hint<'b, 's: 'b, 'h: 'b>(&'s mut self, hint: &'h str) -> impl 'b + DisplayOnce { + hint } /// Takes the completion `candidate` and /// returns the highlighted version (with ANSI color). /// /// Currently, used only with `CompletionType::List`. - fn highlight_candidate<'c>( - &self, + fn highlight_candidate<'b, 's: 'b, 'c: 'b>( + &'s mut self, candidate: &'c str, // FIXME should be Completer::Candidate completion: CompletionType, - ) -> Cow<'c, str> { + ) -> impl 'b + DisplayOnce { let _ = completion; - Borrowed(candidate) + candidate } /// Tells if `line` needs to be highlighted when a specific char is typed or /// when cursor is moved under a specific char. @@ -182,7 +247,7 @@ pub trait Highlighter { /// /// Used to optimize refresh when a character is inserted or the cursor is /// moved. - fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool { + fn highlight_char(&mut self, line: &str, pos: usize, forced: bool) -> bool { let _ = (line, pos, forced); false } @@ -190,42 +255,36 @@ pub trait Highlighter { impl Highlighter for () {} -impl<'r, H: Highlighter> Highlighter for &'r H { - #[cfg(any(not(feature = "split-highlight"), feature = "ansi-str"))] - fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { - (**self).highlight(line, pos) - } - - #[cfg(all(feature = "split-highlight", not(feature = "ansi-str")))] - fn highlight_line<'l>( - &self, +impl<'r, H: Highlighter> Highlighter for &'r mut H { + fn highlight<'b, 's: 'b, 'l: 'b>( + &'s mut self, line: &'l str, pos: usize, - ) -> impl Iterator { - (**self).highlight_line(line, pos) + ) -> impl 'b + DisplayOnce { + (**self).highlight(line, pos) } fn highlight_prompt<'b, 's: 'b, 'p: 'b>( - &'s self, + &'s mut self, prompt: &'p str, default: bool, - ) -> Cow<'b, str> { + ) -> impl 'b + DisplayOnce { (**self).highlight_prompt(prompt, default) } - fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + fn highlight_hint<'b, 's: 'b, 'h: 'b>(&'s mut self, hint: &'h str) -> impl 'b + DisplayOnce { (**self).highlight_hint(hint) } - fn highlight_candidate<'c>( - &self, + fn highlight_candidate<'b, 's: 'b, 'c: 'b>( + &'s mut self, candidate: &'c str, completion: CompletionType, - ) -> Cow<'c, str> { + ) -> impl 'b + DisplayOnce { (**self).highlight_candidate(candidate, completion) } - fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool { + fn highlight_char(&mut self, line: &str, pos: usize, forced: bool) -> bool { (**self).highlight_char(line, pos, forced) } } @@ -235,7 +294,6 @@ impl<'r, H: Highlighter> Highlighter for &'r H { /// 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... } @@ -245,7 +303,6 @@ impl MatchingBracketHighlighter { #[must_use] pub fn new() -> Self { Self { - #[cfg(feature = "anstyle")] style: anstyle::Style::new() .bold() .fg_color(Some(anstyle::AnsiColor::Blue.into())), @@ -254,51 +311,33 @@ impl MatchingBracketHighlighter { } } -#[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); - } - // highlight matching brace/bracket/parenthesis if it exists - if let Some((bracket, pos)) = self.bracket.get() { - 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 Cow::Owned(copy); - } - } - Borrowed(line) - } - - #[cfg(all(feature = "split-highlight", not(feature = "ansi-str")))] - fn highlight_line<'l>( - &self, + fn highlight<'b, 's: 'b, 'l: 'b>( + &'s mut 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, forced: bool) -> bool { + ) -> impl 'b + DisplayOnce { + let empty_style = anstyle::Style::new(); + let iter = if line.len() <= 1 { + vec![(empty_style, line)].into_iter() + } else { + // highlight matching brace/bracket/parenthesis if it exists + self.bracket + .get() + .and_then(|(bracket, pos)| find_matching_bracket(line, pos, bracket)) + .map_or(vec![(empty_style, line)].into_iter(), |(_, idx)| { + vec![ + (empty_style, &line[0..idx]), + (self.style, &line[idx..=idx]), + (empty_style, &line[idx + 1..]), + ] + .into_iter() + }) + }; + StyledBlocks::new(iter) + } + + fn highlight_char(&mut self, line: &str, pos: usize, forced: bool) -> bool { if forced { self.bracket.set(None); return false; diff --git a/src/hint.rs b/src/hint.rs index efff9357c..71ee0595d 100644 --- a/src/hint.rs +++ b/src/hint.rs @@ -30,7 +30,7 @@ pub trait Hinter { /// returns the string that should be displayed or `None` /// if no hint is available for the text the user currently typed. // TODO Validate: called while editing line but not while moving cursor. - fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { + fn hint(&mut self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { let _ = (line, pos, ctx); None } @@ -40,10 +40,10 @@ impl Hinter for () { type Hint = String; } -impl<'r, H: ?Sized + Hinter> Hinter for &'r H { +impl<'r, H: ?Sized + Hinter> Hinter for &'r mut H { type Hint = H::Hint; - fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { + fn hint(&mut self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { (**self).hint(line, pos, ctx) } } @@ -63,7 +63,7 @@ impl HistoryHinter { impl Hinter for HistoryHinter { type Hint = String; - fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { + fn hint(&mut self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { if line.is_empty() || pos < line.len() { return None; } @@ -96,7 +96,7 @@ mod test { pub fn empty_history() { let history = DefaultHistory::new(); let ctx = Context::new(&history); - let hinter = HistoryHinter {}; + let mut hinter = HistoryHinter {}; let hint = hinter.hint("test", 4, &ctx); assert_eq!(None, hint); } diff --git a/src/lib.rs b/src/lib.rs index b9e2f0fb2..626909df9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,7 @@ use std::io::{self, BufRead, Write}; use std::path::Path; use std::result; +use highlight::DisplayOnce; use log::debug; #[cfg(feature = "derive")] #[cfg_attr(docsrs, doc(cfg(feature = "derive")))] @@ -85,9 +86,9 @@ fn complete_line( unbounded, Skim, SkimItem, SkimItemReceiver, SkimItemSender, SkimOptionsBuilder, }; - let completer = s.helper.unwrap(); + // let completer = &s.helper; // get a list of completions - let (start, candidates) = completer.complete(&s.line, s.line.pos(), &s.ctx)?; + let (start, candidates) = s.helper.complete(&s.line, s.line.pos(), &s.ctx)?; // if no completions, we are done if candidates.is_empty() { s.out.beep()?; @@ -109,7 +110,8 @@ fn complete_line( } else { Borrowed(candidate) };*/ - completer.update(&mut s.line, start, candidate, &mut s.changes); + s.helper + .update(&mut s.line, start, candidate, &mut s.changes); } else { // Restore current edited line s.line.update(&backup, backup_pos, &mut s.changes); @@ -152,7 +154,7 @@ fn complete_line( if let Some(lcp) = longest_common_prefix(&candidates) { // if we can extend the item, extend it if lcp.len() > s.line.pos() - start || candidates.len() == 1 { - completer.update(&mut s.line, start, lcp, &mut s.changes); + s.helper.update(&mut s.line, start, lcp, &mut s.changes); s.refresh_line()?; } } @@ -247,7 +249,7 @@ fn complete_line( .downcast_ref::() // downcast to concrete type .expect("something wrong with downcast"); if let Some(candidate) = candidates.get(item.index) { - completer.update( + s.helper.update( &mut s.line, start, candidate.replacement(), @@ -337,8 +339,12 @@ fn page_completions( if i < candidates.len() { let candidate = &candidates[i].display(); let width = candidate.width(); - if let Some(highlighter) = s.highlighter() { - ab.push_str(&highlighter.highlight_candidate(candidate, CompletionType::List)); + if s.out.colors_enabled() { + DisplayOnce::fmt( + s.helper + .highlight_candidate(candidate, CompletionType::List), + &mut ab, + )?; } else { ab.push_str(candidate); } @@ -482,7 +488,7 @@ fn apply_backspace_direct(input: &str) -> String { fn readline_direct( mut reader: impl BufRead, mut writer: impl Write, - validator: &Option, + validator: &mut impl Validator, ) -> Result { let mut input = String::new(); @@ -506,35 +512,36 @@ fn readline_direct( input = apply_backspace_direct(&input); - match validator.as_ref() { - None => return Ok(input), - Some(v) => { - let mut ctx = input.as_str(); - let mut ctx = validate::ValidationContext::new(&mut ctx); - - match v.validate(&mut ctx)? { - validate::ValidationResult::Valid(msg) => { - if let Some(msg) = msg { - writer.write_all(msg.as_bytes())?; - } - return Ok(input); - } - validate::ValidationResult::Invalid(Some(msg)) => { - writer.write_all(msg.as_bytes())?; - } - validate::ValidationResult::Incomplete => { - // Add newline and keep on taking input - if trailing_r { - input.push('\r'); - } - if trailing_n { - input.push('\n'); - } - } - _ => {} + // match validator.as_ref() { + // None => return Ok(input), + // Some(v) => { + let mut ctx = input.as_str(); + let mut ctx = validate::ValidationContext::new(&mut ctx); + + match validator.validate(&mut ctx)? { + validate::ValidationResult::Valid(msg) => { + if let Some(msg) = msg { + writer.write_all(msg.as_bytes())?; } + return Ok(input); + } + validate::ValidationResult::Invalid(Some(msg)) => { + writer.write_all(msg.as_bytes())?; } + validate::ValidationResult::Incomplete(indent) => { + // Add newline and keep on taking input + if trailing_r { + input.push('\r'); + } + if trailing_n { + input.push('\n'); + } + input += &" ".repeat(indent); + } + _ => {} } + // } + // } } } @@ -546,11 +553,49 @@ pub trait Helper where Self: Completer + Hinter + Highlighter + Validator, { + /// Update helper when line has been modified. + /// + /// This is the first-called function just after the editing is done, + /// before all other functions within [Completer], [Hinter], [Highlighter], and [Validator]. + /// + /// You can put the tokenizer/parser here so that other APIs can directly use + /// results generate here, and reduce the overhead. + fn update_after_edit(&mut self, line: &str, pos: usize, forced_refresh: bool) { + _ = (line, forced_refresh, pos); + } + + /// Update helper when cursor has been moved. + /// + /// This is the first-called function just after the cursor moving is done, + /// before all other functions within [Completer], [Hinter], [Highlighter], and [Validator]. + /// + /// You can put the tokenizer/parser here so that other APIs can directly use + /// results generate here, and reduce the overhead. + fn update_after_move_cursor(&mut self, line: &str, pos: usize) { + _ = (line, pos); + } + + /// Return the width of continuation prompt, + /// the continuation prompt should be implemented at highlight + fn continuation_prompt_width<'b, 's: 'b, 'p: 'b>(&'s self, prompt: &'p str) -> usize { + _ = prompt; + 0 + } } impl Helper for () {} -impl<'h, H: Helper> Helper for &'h H {} +impl<'h, H: Helper> Helper for &'h mut H { + fn update_after_edit(&mut self, line: &str, pos: usize, forced_refresh: bool) { + (**self).update_after_edit(line, pos, forced_refresh) + } + fn update_after_move_cursor(&mut self, line: &str, pos: usize) { + (**self).update_after_move_cursor(line, pos) + } + fn continuation_prompt_width<'b, 's: 'b, 'p: 'b>(&'s self, prompt: &'p str) -> usize { + (**self).continuation_prompt_width(prompt) + } +} /// Completion/suggestion context pub struct Context<'h> { @@ -587,7 +632,7 @@ pub struct Editor { term: Terminal, buffer: Option, history: I, - helper: Option, + helper: H, kill_ring: KillRing, config: Config, custom_bindings: Bindings, @@ -598,20 +643,24 @@ pub type DefaultEditor = Editor<(), DefaultHistory>; #[allow(clippy::new_without_default)] impl Editor { - /// Create an editor with the default configuration - pub fn new() -> Result { - Self::with_config(Config::default()) + /// Create an editor with a helper and the default configuration. + /// + /// Use `new(())` for default Helper. + pub fn new(helper: H) -> Result { + Self::with_config(Config::default(), helper) } - /// Create an editor with a specific configuration. - pub fn with_config(config: Config) -> Result { - Self::with_history(config, DefaultHistory::with_config(config)) + /// Create an editor with a specific configuration and a helper. + /// + /// Use `with_config(config,())` for default Helper. + pub fn with_config(config: Config, helper: H) -> Result { + Self::with_history(config, DefaultHistory::with_config(config), helper) } } impl Editor { /// Create an editor with a custom history impl. - pub fn with_history(config: Config, history: I) -> Result { + pub fn with_history(config: Config, history: I, helper: H) -> Result { let term = Terminal::new( config.color_mode(), config.behavior(), @@ -624,7 +673,7 @@ impl Editor { term, buffer: None, history, - helper: None, + helper, kill_ring: KillRing::new(60), config, custom_bindings: Bindings::new(), @@ -660,11 +709,12 @@ impl Editor { stdout.write_all(prompt.as_bytes())?; stdout.flush()?; - readline_direct(io::stdin().lock(), io::stderr(), &self.helper) + readline_direct(io::stdin().lock(), io::stderr(), &mut self.helper) } else if self.term.is_input_tty() { let (original_mode, term_key_map) = self.term.enable_raw_mode()?; let guard = Guard(&original_mode); - let user_input = self.readline_edit(prompt, initial, &original_mode, term_key_map); + let user_input: result::Result = + self.readline_edit(prompt, initial, &original_mode, term_key_map); if self.config.auto_add_history() { if let Ok(ref line) = user_input { self.add_history_entry(line.as_str())?; @@ -676,7 +726,7 @@ impl Editor { } else { debug!(target: "rustyline", "stdin is not a tty"); // Not a tty: read from file / pipe. - readline_direct(io::stdin().lock(), io::stderr(), &self.helper) + readline_direct(io::stdin().lock(), io::stderr(), &mut self.helper) } } @@ -694,7 +744,7 @@ impl Editor { self.kill_ring.reset(); // TODO recreate a new kill ring vs reset let ctx = Context::new(&self.history); - let mut s = State::new(&mut stdout, prompt, self.helper.as_ref(), ctx); + let mut s = State::new(&mut stdout, prompt, &mut self.helper, ctx); let mut input_state = InputState::new(&self.config, &self.custom_bindings); @@ -729,7 +779,7 @@ impl Editor { // First trigger commands that need extra input - if cmd == Cmd::Complete && s.helper.is_some() { + if cmd == Cmd::Complete { let next = complete_line(&mut rdr, &mut s, &mut input_state, &self.config)?; if let Some(next) = next { cmd = next; @@ -837,20 +887,20 @@ impl Editor { &self.history } - /// Register a callback function to be called for tab-completion - /// or to show hints to the user at the right of the prompt. - pub fn set_helper(&mut self, helper: Option) { - self.helper = helper; - } + /// This will do nothing and will remove in future version. + /// + /// Now we should specify the helper when create the editor + #[deprecated = "specify the helper when crate the editor"] + pub fn set_helper(&mut self) {} /// Return a mutable reference to the helper. - pub fn helper_mut(&mut self) -> Option<&mut H> { - self.helper.as_mut() + pub fn helper_mut(&mut self) -> &mut H { + &mut self.helper } /// Return an immutable reference to the helper. - pub fn helper(&self) -> Option<&H> { - self.helper.as_ref() + pub fn helper(&self) -> &H { + &self.helper } /// Bind a sequence to a command. diff --git a/src/test/mod.rs b/src/test/mod.rs index 14ff9052b..0c030a694 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -20,7 +20,7 @@ mod vi_insert; fn init_editor(mode: EditMode, keys: &[KeyEvent]) -> DefaultEditor { let config = Config::builder().edit_mode(mode).build(); - let mut editor = DefaultEditor::with_config(config).unwrap(); + let mut editor = DefaultEditor::with_config(config, ()).unwrap(); editor.term.keys.extend(keys.iter().copied()); editor } @@ -30,7 +30,7 @@ impl Completer for SimpleCompleter { type Candidate = String; fn complete( - &self, + &mut self, line: &str, _pos: usize, _ctx: &Context<'_>, @@ -50,7 +50,7 @@ impl Completer for SimpleCompleter { impl Hinter for SimpleCompleter { type Hint = String; - fn hint(&self, _line: &str, _pos: usize, _ctx: &Context<'_>) -> Option { + fn hint(&mut self, _line: &str, _pos: usize, _ctx: &Context<'_>) -> Option { None } } @@ -63,8 +63,8 @@ impl Validator for SimpleCompleter {} fn complete_line() { let mut out = Sink::default(); let history = crate::history::DefaultHistory::new(); - let helper = Some(SimpleCompleter); - let mut s = init_state(&mut out, "rus", 3, helper.as_ref(), &history); + let mut helper = SimpleCompleter; + let mut s = init_state(&mut out, "rus", 3, &mut helper, &history); let config = Config::default(); let bindings = Bindings::new(); let mut input_state = InputState::new(&config, &bindings); @@ -85,8 +85,8 @@ fn complete_line() { fn complete_symbol() { let mut out = Sink::default(); let history = crate::history::DefaultHistory::new(); - let helper = Some(SimpleCompleter); - let mut s = init_state(&mut out, "\\hbar", 5, helper.as_ref(), &history); + let mut helper = SimpleCompleter; + let mut s = init_state(&mut out, "\\hbar", 5, &mut helper, &history); let config = Config::builder() .completion_type(CompletionType::List) .build(); @@ -188,7 +188,7 @@ fn test_readline_direct() { let output = readline_direct( Cursor::new("([)\n\u{0008}\n\n\r\n])".as_bytes()), Cursor::new(&mut write_buf), - &Some(crate::validate::MatchingBracketValidator::new()), + &mut crate::validate::MatchingBracketValidator::new(), ); assert_eq!( diff --git a/src/tty/mod.rs b/src/tty/mod.rs index f2abf0df5..2dcdf6568 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -54,7 +54,7 @@ pub trait Renderer { hint: Option<&str>, old_layout: &Layout, new_layout: &Layout, - highlighter: Option<&H>, + highlighter: &mut H, ) -> Result<()>; /// Compute layout for rendering prompt + line + some info (either hint, @@ -64,20 +64,21 @@ pub trait Renderer { &self, prompt_size: Position, default_prompt: bool, + continuation_prompt_width: usize, line: &LineBuffer, info: Option<&str>, ) -> Layout { // calculate the desired position of the cursor let pos = line.pos(); - let cursor = self.calculate_position(&line[..pos], prompt_size); + let cursor = self.calculate_position(&line[..pos], prompt_size, continuation_prompt_width); // calculate the position of the end of the input line let mut end = if pos == line.len() { cursor } else { - self.calculate_position(&line[pos..], cursor) + self.calculate_position(&line[pos..], cursor, continuation_prompt_width) }; if let Some(info) = info { - end = self.calculate_position(info, end); + end = self.calculate_position(info, end, continuation_prompt_width); } let new_layout = Layout { @@ -93,7 +94,12 @@ pub trait Renderer { /// Calculate the number of columns and rows used to display `s` on a /// `cols` width terminal starting at `orig`. - fn calculate_position(&self, s: &str, orig: Position) -> Position; + fn calculate_position( + &self, + s: &str, + orig: Position, + continuation_prompt_width: usize, + ) -> Position; fn write_and_flush(&mut self, buf: &str) -> Result<()>; @@ -133,13 +139,18 @@ impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R { hint: Option<&str>, old_layout: &Layout, new_layout: &Layout, - highlighter: Option<&H>, + highlighter: &mut H, ) -> Result<()> { (**self).refresh_line(prompt, line, hint, old_layout, new_layout, highlighter) } - fn calculate_position(&self, s: &str, orig: Position) -> Position { - (**self).calculate_position(s, orig) + fn calculate_position( + &self, + s: &str, + orig: Position, + continuation_prompt_width: usize, + ) -> Position { + (**self).calculate_position(s, orig, continuation_prompt_width) } fn write_and_flush(&mut self, buf: &str) -> Result<()> { diff --git a/src/tty/test.rs b/src/tty/test.rs index 45828e61f..56c230bbc 100644 --- a/src/tty/test.rs +++ b/src/tty/test.rs @@ -107,12 +107,18 @@ impl Renderer for Sink { _hint: Option<&str>, _old_layout: &Layout, _new_layout: &Layout, - _highlighter: Option<&H>, + _highlighter: &mut H, ) -> Result<()> { Ok(()) } - fn calculate_position(&self, s: &str, orig: Position) -> Position { + fn calculate_position( + &self, + s: &str, + orig: Position, + continuation_prompt_width: usize, + ) -> Position { + _ = continuation_prompt_width; let mut pos = orig; pos.col += s.len(); pos diff --git a/src/tty/unix.rs b/src/tty/unix.rs index edaea5470..73c8999c1 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -27,7 +27,7 @@ use utf8parse::{Parser, Receiver}; use super::{width, Event, RawMode, RawReader, Renderer, Term}; use crate::config::{Behavior, BellStyle, ColorMode, Config}; -use crate::highlight::Highlighter; +use crate::highlight::{DisplayOnce, Highlighter}; use crate::keys::{KeyCode as K, KeyEvent, KeyEvent as E, Modifiers as M}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; @@ -986,7 +986,7 @@ impl Renderer for PosixRenderer { hint: Option<&str>, old_layout: &Layout, new_layout: &Layout, - highlighter: Option<&H>, + highlighter: &mut H, ) -> Result<()> { use std::fmt::Write; self.buffer.clear(); @@ -997,28 +997,14 @@ impl Renderer for PosixRenderer { self.clear_old_rows(old_layout); - if let Some(highlighter) = highlighter { + if self.colors_enabled() { // display the prompt - self.buffer - .push_str(&highlighter.highlight_prompt(prompt, default_prompt)); + DisplayOnce::fmt( + highlighter.highlight_prompt(prompt, default_prompt), + &mut self.buffer, + )?; // display the input line - 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())?; - } - } - } + DisplayOnce::fmt(highlighter.highlight(line, line.pos()), &mut self.buffer)?; } else { // display the prompt self.buffer.push_str(prompt); @@ -1027,8 +1013,8 @@ impl Renderer for PosixRenderer { } // display hint if let Some(hint) = hint { - if let Some(highlighter) = highlighter { - self.buffer.push_str(&highlighter.highlight_hint(hint)); + if self.colors_enabled() { + DisplayOnce::fmt(highlighter.highlight_hint(hint), &mut self.buffer)?; } else { self.buffer.push_str(hint); } @@ -1064,7 +1050,12 @@ impl Renderer for PosixRenderer { /// Control characters are treated as having zero width. /// Characters with 2 column width are correctly handled (not split). - fn calculate_position(&self, s: &str, orig: Position) -> Position { + fn calculate_position( + &self, + s: &str, + orig: Position, + continuation_prompt_width: usize, + ) -> Position { let mut pos = orig; let mut esc_seq = 0; for c in s.graphemes(true) { @@ -1088,6 +1079,10 @@ impl Renderer for PosixRenderer { pos.col = 0; pos.row += 1; } + // add continuation prompt offset + if pos.row > orig.row { + pos.col += continuation_prompt_width; + } pos } @@ -1665,7 +1660,7 @@ mod test { #[ignore] fn prompt_with_ansi_escape_codes() { let out = PosixRenderer::new(libc::STDOUT_FILENO, 4, true, BellStyle::default()); - let pos = out.calculate_position("\x1b[1;32m>>\x1b[0m ", Position::default()); + let pos = out.calculate_position("\x1b[1;32m>>\x1b[0m ", Position::default(), 0); assert_eq!(3, pos.col); assert_eq!(0, pos.row); } @@ -1696,10 +1691,10 @@ mod test { let mut out = PosixRenderer::new(libc::STDOUT_FILENO, 4, true, BellStyle::default()); let prompt = "> "; let default_prompt = true; - let prompt_size = out.calculate_position(prompt, Position::default()); + let prompt_size = out.calculate_position(prompt, Position::default(), 0); let mut line = LineBuffer::init("", 0); - let old_layout = out.compute_layout(prompt_size, default_prompt, &line, None); + let old_layout = out.compute_layout(prompt_size, default_prompt, 0, &line, None); assert_eq!(Position { col: 2, row: 0 }, old_layout.cursor); assert_eq!(old_layout.cursor, old_layout.end); @@ -1707,10 +1702,10 @@ mod test { Some(true), line.insert('a', out.cols - prompt_size.col + 1, &mut NoListener) ); - let new_layout = out.compute_layout(prompt_size, default_prompt, &line, None); + let new_layout = out.compute_layout(prompt_size, default_prompt, 0, &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, &mut ()) .unwrap(); #[rustfmt::skip] assert_eq!( diff --git a/src/tty/windows.rs b/src/tty/windows.rs index 2d2c04796..4a323deeb 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -21,7 +21,7 @@ use windows_sys::Win32::UI::Input::KeyboardAndMouse; use super::{width, Event, RawMode, RawReader, Renderer, Term}; use crate::config::{Behavior, BellStyle, ColorMode, Config}; -use crate::highlight::Highlighter; +use crate::highlight::{DisplayOnce, Highlighter}; use crate::keys::{KeyCode as K, KeyEvent, Modifiers as M}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; @@ -444,44 +444,25 @@ impl Renderer for ConsoleRenderer { self.buffer.clear(); let mut col = 0; - if let Some(highlighter) = highlighter { + if self.colors_enabled() { // TODO handle ansi escape code (SetConsoleTextAttribute) // append the prompt - col = self.wrap_at_eol(&highlighter.highlight_prompt(prompt, default_prompt), col); + DisplayOnce::fmt( + highlighter.highlight_prompt(prompt, default_prompt), + &mut self.buffer, + )?; // append the input line - 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); - // append the input line - col = self.wrap_at_eol(line, col); + DisplayOnce::fmt(highlighter.highlight(line, line.pos()), &mut self.buffer)?; } else { // append the prompt self.buffer.push_str(prompt); // append the input line self.buffer.push_str(line); } - // append hint + // display hint if let Some(hint) = hint { - if let Some(highlighter) = highlighter { - self.wrap_at_eol(&highlighter.highlight_hint(hint), col); - } else if self.colors_enabled { - self.wrap_at_eol(hint, col); + if self.colors_enabled() { + DisplayOnce::fmt(highlighter.highlight_hint(hint), &mut self.buffer)?; } else { self.buffer.push_str(hint); } diff --git a/src/validate.rs b/src/validate.rs index 12b7f3314..2829e6f35 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -6,8 +6,8 @@ use crate::Result; /// Input validation result #[non_exhaustive] pub enum ValidationResult { - /// Incomplete input - Incomplete, + /// Incomplete input with indent count + Incomplete(usize), /// Validation fails with an optional error message. User must fix the /// input. Invalid(Option), @@ -20,6 +20,14 @@ impl ValidationResult { matches!(self, Self::Valid(_)) } + pub(crate) fn is_incomplete(&self) -> Option { + if let Self::Incomplete(indent) = self { + Some(*indent) + } else { + None + } + } + pub(crate) fn has_message(&self) -> bool { matches!(self, Self::Valid(Some(_)) | Self::Invalid(Some(_))) } @@ -67,7 +75,7 @@ pub trait Validator { /// /// For auto-correction like a missing closing quote or to reject invalid /// char while typing, the input will be mutable (TODO). - fn validate(&self, ctx: &mut ValidationContext) -> Result { + fn validate(&mut self, ctx: &mut ValidationContext) -> Result { let _ = ctx; Ok(ValidationResult::Valid(None)) } @@ -79,19 +87,19 @@ pub trait Validator { /// /// This feature is not yet implemented, so this function is currently a /// no-op - fn validate_while_typing(&self) -> bool { + fn validate_while_typing(&mut self) -> bool { false } } impl Validator for () {} -impl<'v, V: ?Sized + Validator> Validator for &'v V { - fn validate(&self, ctx: &mut ValidationContext) -> Result { +impl<'v, V: ?Sized + Validator> Validator for &'v mut V { + fn validate(&mut self, ctx: &mut ValidationContext) -> Result { (**self).validate(ctx) } - fn validate_while_typing(&self) -> bool { + fn validate_while_typing(&mut self) -> bool { (**self).validate_while_typing() } } @@ -111,7 +119,7 @@ impl MatchingBracketValidator { } impl Validator for MatchingBracketValidator { - fn validate(&self, ctx: &mut ValidationContext) -> Result { + fn validate(&mut self, ctx: &mut ValidationContext) -> Result { Ok(validate_brackets(ctx.input())) } } @@ -140,6 +148,6 @@ fn validate_brackets(input: &str) -> ValidationResult { if stack.is_empty() { ValidationResult::Valid(None) } else { - ValidationResult::Incomplete + ValidationResult::Incomplete(0) } }