From f53ba9e5554a4879a840835d42262d3c86d18715 Mon Sep 17 00:00:00 2001 From: junzhuo Date: Wed, 25 Sep 2024 16:21:08 +0800 Subject: [PATCH] new `Helper` to reduce parser overhead and support continuation prompt --- Cargo.toml | 4 + examples/continuation_prompt.rs | 76 +++++++++++++++++ examples/custom_key_bindings.rs | 7 +- examples/diy_hints.rs | 5 +- examples/example.rs | 15 ++-- examples/external_print.rs | 2 +- examples/input_multiline.rs | 3 +- examples/input_validation.rs | 7 +- examples/minimal.rs | 2 +- examples/numeric_input.rs | 2 +- examples/read_password.rs | 9 +- examples/sqlite_history.rs | 2 +- rustyline-derive/src/lib.rs | 44 +++++----- src/command.rs | 4 + src/completion.rs | 61 ++++++++------ src/edit.rs | 136 ++++++++++++++++-------------- src/highlight.rs | 32 ++++---- src/hint.rs | 10 +-- src/lib.rs | 141 +++++++++++++++++++------------- src/test/mod.rs | 16 ++-- src/tty/mod.rs | 4 +- src/tty/test.rs | 2 +- src/tty/unix.rs | 97 +++++++++++++--------- src/validate.rs | 26 ++++-- 24 files changed, 434 insertions(+), 273 deletions(-) create mode 100644 examples/continuation_prompt.rs diff --git a/Cargo.toml b/Cargo.toml index 62f6d7524..b04783b98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ with-fuzzy = ["skim"] case_insensitive_history_search = ["regex"] # For continuation prompt, indentation, scrolling split-highlight = [] +continuation-prompt = [] [[example]] name = "custom_key_bindings" @@ -97,6 +98,9 @@ required-features = ["derive"] [[example]] name = "sqlite_history" required-features = ["with-sqlite-history"] +[[example]] +name = "continuation_prompt" +required-features = ["custom-bindings", "derive", "continuation-prompt", "split-highlight"] [package.metadata.docs.rs] features = [ diff --git a/examples/continuation_prompt.rs b/examples/continuation_prompt.rs new file mode 100644 index 000000000..83390de16 --- /dev/null +++ b/examples/continuation_prompt.rs @@ -0,0 +1,76 @@ +use rustyline::highlight::Highlighter; +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; + } +} + +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_line<'l>( + &mut self, + line: &'l str, + _pos: usize, + ) -> impl Iterator { + use core::iter::once; + let mut lines = line.split('\n'); + self.need_render = false; + once(((), lines.next().unwrap())) + .chain(lines.flat_map(|line| once(((), "\n.. ")).chain(once(((), line))))) + } +} + +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..5159acd01 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<'h>(&mut self, hint: &'h str) -> Cow<'h, 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..efc915e96 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -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,25 @@ impl Highlighter for MyHelper { } } - fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + fn highlight_hint<'h>(&mut self, hint: &'h str) -> Cow<'h, 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> { + fn highlight<'l>(&mut 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, + &mut self, line: &'l str, pos: usize, ) -> impl Iterator { self.highlighter.highlight_line(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 +72,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 +81,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..29c01fe47 100644 --- a/examples/input_multiline.rs +++ b/examples/input_multiline.rs @@ -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..b92a2715d 100644 --- a/examples/read_password.rs +++ b/examples/read_password.rs @@ -10,7 +10,7 @@ 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<'l>(&mut 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())) @@ -37,7 +37,7 @@ impl Highlighter for MaskingHighlighter { } } - 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 +45,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..b68063083 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) } } } @@ -103,41 +103,41 @@ pub fn highlighter_macro_derive(input: TokenStream) -> TokenStream { #[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) + fn highlight<'l>(&mut self, line: &'l str, pos: usize) -> ::std::borrow::Cow<'l, str> { + ::rustyline::highlight::Highlighter::highlight(&mut self.#field_name_or_index, line, pos) } #[cfg(all(feature = "split-highlight", not(feature = "ansi-str")))] fn highlight_line<'l>( - &self, + &mut self, line: &'l str, pos: usize, ) -> impl Iterator { - ::rustyline::highlight::Highlighter::highlight_line(&self.#field_name_or_index, line, pos) + ::rustyline::highlight::Highlighter::highlight_line(&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) + ::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<'h>(&mut self, hint: &'h str) -> ::std::borrow::Cow<'h, str> { + ::rustyline::highlight::Highlighter::highlight_hint(&mut self.#field_name_or_index, hint) } fn highlight_candidate<'c>( - &self, + &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) + ::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 +166,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 +195,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..f51309864 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,7 +48,7 @@ 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()); @@ -69,13 +69,13 @@ 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 highlighter(&mut self) -> Option<&mut H> { + // if self.out.colors_enabled() { + // Some(self.helper) + // } else { + // None + // } + // } pub fn next_cmd( &mut self, @@ -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,11 +171,6 @@ 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 @@ -179,13 +178,14 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { debug!(target: "rustyline", "old layout: {:?}", self.layout); debug!(target: "rustyline", "new layout: {:?}", new_layout); + // if self.out.colors_enabled() { self.out.refresh_line( prompt, &self.line, info, &self.layout, &new_layout, - highlighter, + self.helper, )?; self.layout = new_layout; @@ -193,34 +193,43 @@ 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() { - let highlight_char = - highlighter.highlight_char(&self.line, self.line.pos(), self.forced_refresh); - if highlight_char { - self.highlight_char = true; - true - } else if self.highlight_char { - // previously highlighted => force a full refresh - self.highlight_char = false; - true - } else { - false - } + // if let Some(highlighter) = self.highlighter() { + let highlight_char = + self.helper + .highlight_char(&self.line, self.line.pos(), self.forced_refresh); + if highlight_char { + self.highlight_char = true; + true + } else if self.highlight_char { + // previously highlighted => force a full refresh + self.highlight_char = false; + true } else { false } + // } else { + // false + // } } pub fn is_default_prompt(&self) -> bool { @@ -228,30 +237,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 +274,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 +283,14 @@ 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()); + self.update_after_edit(); self.hint(); self.highlight_char(); self.refresh(prompt, prompt_size, false, Info::Hint) @@ -351,6 +364,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 +762,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 +795,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/highlight.rs b/src/highlight.rs index 56fb11443..8416ceede 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -127,7 +127,7 @@ pub trait Highlighter { docsrs, doc(cfg(any(not(feature = "split-highlight"), feature = "ansi-str"))) )] - fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { + fn highlight<'l>(&mut self, line: &'l str, pos: usize) -> Cow<'l, str> { let _ = pos; Borrowed(line) } @@ -140,7 +140,7 @@ pub trait Highlighter { doc(cfg(all(feature = "split-highlight", not(feature = "ansi-str")))) )] fn highlight_line<'l>( - &self, + &mut self, line: &'l str, pos: usize, ) -> impl Iterator { @@ -151,7 +151,7 @@ pub trait Highlighter { /// 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> { @@ -160,7 +160,7 @@ pub trait Highlighter { } /// Takes the `hint` and /// returns the highlighted version (with ANSI color). - fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + fn highlight_hint<'h>(&mut self, hint: &'h str) -> Cow<'h, str> { Borrowed(hint) } /// Takes the completion `candidate` and @@ -168,7 +168,7 @@ pub trait Highlighter { /// /// Currently, used only with `CompletionType::List`. fn highlight_candidate<'c>( - &self, + &mut self, candidate: &'c str, // FIXME should be Completer::Candidate completion: CompletionType, ) -> Cow<'c, str> { @@ -182,7 +182,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,15 +190,15 @@ pub trait Highlighter { impl Highlighter for () {} -impl<'r, H: Highlighter> Highlighter for &'r H { +impl<'r, H: Highlighter> Highlighter for &'r mut H { #[cfg(any(not(feature = "split-highlight"), feature = "ansi-str"))] - fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { + fn highlight<'l>(&mut 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, + &mut self, line: &'l str, pos: usize, ) -> impl Iterator { @@ -206,26 +206,26 @@ impl<'r, H: Highlighter> Highlighter for &'r H { } fn highlight_prompt<'b, 's: 'b, 'p: 'b>( - &'s self, + &'s mut self, prompt: &'p str, default: bool, ) -> Cow<'b, str> { (**self).highlight_prompt(prompt, default) } - fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + fn highlight_hint<'h>(&mut self, hint: &'h str) -> Cow<'h, str> { (**self).highlight_hint(hint) } fn highlight_candidate<'c>( - &self, + &mut self, candidate: &'c str, completion: CompletionType, ) -> Cow<'c, str> { (**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) } } @@ -261,7 +261,7 @@ impl MatchingBracketHighlighter { ))] 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> { + fn highlight<'l>(&mut self, line: &'l str, _pos: usize) -> Cow<'l, str> { if line.len() <= 1 { return Borrowed(line); } @@ -278,7 +278,7 @@ impl Highlighter for MatchingBracketHighlighter { #[cfg(all(feature = "split-highlight", not(feature = "ansi-str")))] fn highlight_line<'l>( - &self, + &mut self, line: &'l str, _pos: usize, ) -> impl Iterator { @@ -298,7 +298,7 @@ impl Highlighter for MatchingBracketHighlighter { vec![(AnsiStyle::default(), line)].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 { 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..2fac786e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,9 +85,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 +109,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 +153,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 +248,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,11 +338,15 @@ 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)); - } else { - ab.push_str(candidate); - } + ab.push_str( + &s.helper + .highlight_candidate(candidate, CompletionType::List), + ); + // if let Some(highlighter) = s.highlighter() { + // ab.push_str(&highlighter.highlight_candidate(candidate, CompletionType::List)); + // } else { + // ab.push_str(candidate); + // } if ((col + 1) * num_rows) + row < candidates.len() { for _ in width..max_width { ab.push(' '); @@ -482,7 +487,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 +511,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 +552,32 @@ 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); + } } impl Helper for () {} -impl<'h, H: Helper> Helper for &'h H {} +impl<'h, H: Helper> Helper for &'h mut H {} /// Completion/suggestion context pub struct Context<'h> { @@ -587,7 +614,7 @@ pub struct Editor { term: Terminal, buffer: Option, history: I, - helper: Option, + helper: H, kill_ring: KillRing, config: Config, custom_bindings: Bindings, @@ -599,19 +626,19 @@ 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()) + 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)) + 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 +651,7 @@ impl Editor { term, buffer: None, history, - helper: None, + helper, kill_ring: KillRing::new(60), config, custom_bindings: Bindings::new(), @@ -660,11 +687,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 +704,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 +722,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 +757,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; @@ -839,18 +867,17 @@ impl Editor { /// 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; - } + #[deprecated = "reason"] + 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..cfdf934f4 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, @@ -133,7 +133,7 @@ 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) } diff --git a/src/tty/test.rs b/src/tty/test.rs index 45828e61f..6e2cd9313 100644 --- a/src/tty/test.rs +++ b/src/tty/test.rs @@ -107,7 +107,7 @@ impl Renderer for Sink { _hint: Option<&str>, _old_layout: &Layout, _new_layout: &Layout, - _highlighter: Option<&H>, + _highlighter: &mut H, ) -> Result<()> { Ok(()) } diff --git a/src/tty/unix.rs b/src/tty/unix.rs index edaea5470..ace9978d1 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -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,41 +997,41 @@ impl Renderer for PosixRenderer { self.clear_old_rows(old_layout); - if let Some(highlighter) = highlighter { - // display the prompt - self.buffer - .push_str(&highlighter.highlight_prompt(prompt, default_prompt)); - // 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())?; - } + // if let Some(highlighter) = highlighter { + // display the prompt + self.buffer + .push_str(&highlighter.highlight_prompt(prompt, default_prompt)); + // 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())?; } } - } else { - // display the prompt - self.buffer.push_str(prompt); - // display the input line - self.buffer.push_str(line); } + // } else { + // // display the prompt + // self.buffer.push_str(prompt); + // // display the input line + // self.buffer.push_str(line); + // } // display hint if let Some(hint) = hint { - if let Some(highlighter) = highlighter { - self.buffer.push_str(&highlighter.highlight_hint(hint)); - } else { - self.buffer.push_str(hint); - } + // if let Some(highlighter) = highlighter { + self.buffer.push_str(&highlighter.highlight_hint(hint)); + // } else { + // self.buffer.push_str(hint); + // } } // we have to generate our own newline on line wrap if end_pos.col == 0 @@ -1088,6 +1088,14 @@ impl Renderer for PosixRenderer { pos.col = 0; pos.row += 1; } + cfg_if::cfg_if! { + if #[cfg(feature = "continuation-prompt")] { + // add continuation prompt offset + if pos.row > orig.row { + pos.col += orig.col; + } + } + } pos } @@ -1708,14 +1716,29 @@ mod test { line.insert('a', out.cols - prompt_size.col + 1, &mut NoListener) ); let new_layout = out.compute_layout(prompt_size, default_prompt, &line, None); - assert_eq!(Position { col: 1, row: 1 }, new_layout.cursor); + cfg_if::cfg_if! { + if #[cfg(feature = "continuation-prompt")] { + assert_eq!(Position { col: 3, row: 1 }, new_layout.cursor); + } else { + 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!( - "\r\u{1b}[K> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\u{1b}[1C", - out.buffer - ); + cfg_if::cfg_if! { + if #[cfg(feature = "continuation-prompt")] { + assert_eq!( + "\r\u{1b}[K> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\u{1b}[3C", + out.buffer + ); + } else { + assert_eq!( + "\r\u{1b}[K> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\u{1b}[1C", + out.buffer + ); + } + } } } 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) } }