Repository: kkawakam/rustyline
Pull request #231 (2019-05-19 12:42:34)

Input validation
Update #182


Patch:
diff --git a/README.md b/README.md
index 8559de0..fe7d81b 100644
--- a/README.md
+++ b/README.md
@@ -237,6 +237,9 @@ line instead of horizontally scrolling as more characters are
 typed. Currently this feature is always enabled and there is no
 configuration option to disable it.
 
-_This feature does not allow the end user to hit a special key
+This feature does not allow the end user to hit a special key
 sequence and enter a mode where hitting the return key will cause a
-literal newline to be added to the input buffer_.
+literal newline to be added to the input buffer.
+
+The way to achieve multi-line editing is to implement the `Validator`
+trait.
diff --git a/examples/example.rs b/examples/example.rs
index b9969d5..2a81394 100644
--- a/examples/example.rs
+++ b/examples/example.rs
@@ -7,9 +7,9 @@ use rustyline::error::ReadlineError;
 use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
 use rustyline::hint::{Hinter, HistoryHinter};
 use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, KeyPress};
-use rustyline_derive::Helper;
+use rustyline_derive::{Helper, Validator};
 
-#[derive(Helper)]
+#[derive(Helper, Validator)]
 struct MyHelper {
     completer: FilenameCompleter,
     highlighter: MatchingBracketHighlighter,
diff --git a/examples/input_validation.rs b/examples/input_validation.rs
new file mode 100644
index 0000000..389c5f5
--- /dev/null
+++ b/examples/input_validation.rs
@@ -0,0 +1,31 @@
+use rustyline::error::ReadlineError;
+use rustyline::validate::{ValidationContext, ValidationResult, Validator};
+use rustyline::Editor;
+use rustyline_derive::{Completer, Helper, Highlighter, Hinter};
+
+#[derive(Completer, Helper, Highlighter, Hinter)]
+struct InputValidator {}
+
+impl Validator for InputValidator {
+    fn validate(&self, ctx: &mut ValidationContext) -> Result<ValidationResult, ReadlineError> {
+        let input = ctx.input();
+        if !input.starts_with("SELECT") {
+            return Ok(ValidationResult::Invalid(Some(
+                " --< Expect: SELECT stmt".to_owned(),
+            )));
+        } else if !input.ends_with(';') {
+            return Ok(ValidationResult::Incomplete);
+        }
+        Ok(ValidationResult::Valid(None))
+    }
+}
+
+fn main() -> rustyline::Result<()> {
+    let h = InputValidator {};
+    let mut rl = Editor::new();
+    rl.set_helper(Some(h));
+
+    let input = rl.readline("> ")?;
+    println!("Input: {}", input);
+    Ok(())
+}
diff --git a/examples/read_password.rs b/examples/read_password.rs
index 54448e1..0440716 100644
--- a/examples/read_password.rs
+++ b/examples/read_password.rs
@@ -3,9 +3,9 @@ use std::borrow::Cow::{self, Borrowed, Owned};
 use rustyline::config::Configurer;
 use rustyline::highlight::Highlighter;
 use rustyline::{ColorMode, Editor};
-use rustyline_derive::{Completer, Helper, Hinter};
+use rustyline_derive::{Completer, Helper, Hinter, Validator};
 
-#[derive(Completer, Helper, Hinter)]
+#[derive(Completer, Helper, Hinter, Validator)]
 struct MaskingHighlighter {
     masking: bool,
 }
diff --git a/rustyline-derive/src/lib.rs b/rustyline-derive/src/lib.rs
index fd1af8a..31f974b 100644
--- a/rustyline-derive/src/lib.rs
+++ b/rustyline-derive/src/lib.rs
@@ -56,3 +56,16 @@ pub fn hinter_macro_derive(input: TokenStream) -> TokenStream {
     };
     TokenStream::from(expanded)
 }
+
+#[proc_macro_derive(Validator)]
+pub fn validator_macro_derive(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
+    let name = &input.ident;
+    let generics = input.generics;
+    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+    let expanded = quote! {
+        impl #impl_generics rustyline::validate::Validator for #name #ty_generics #where_clause {
+        }
+    };
+    TokenStream::from(expanded)
+}
diff --git a/src/edit.rs b/src/edit.rs
index bce3bae..5b264cf 100644
--- a/src/edit.rs
+++ b/src/edit.rs
@@ -1,9 +1,9 @@
 //! Command processor
 
+use log::debug;
 use std::cell::RefCell;
 use std::fmt;
 use std::rc::Rc;
-use log::debug;
 use unicode_segmentation::UnicodeSegmentation;
 use unicode_width::UnicodeWidthChar;
 
@@ -11,11 +11,12 @@ use super::{Context, Helper, Result};
 use crate::highlight::Highlighter;
 use crate::history::Direction;
 use crate::keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word};
-use crate::keymap::{InputState, Refresher};
+use crate::keymap::{InputState, Invoke, Refresher};
 use crate::layout::{Layout, Position};
 use crate::line_buffer::{LineBuffer, WordAction, MAX_LINE};
 use crate::tty::{Renderer, Term, Terminal};
 use crate::undo::Changeset;
+use crate::validate::{ValidationContext, ValidationResult};
 
 /// Represent the state during line editing.
 /// Implement rendering.
@@ -217,6 +218,46 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
     pub fn is_default_prompt(&self) -> bool {
         self.layout.default_prompt
     }
+
+    pub fn validate(&mut self) -> Result<bool> {
+        if let Some(validator) = self.helper {
+            self.changes.borrow_mut().begin();
+            let result = validator.validate(&mut ValidationContext::new(self))?;
+            let corrected = self.changes.borrow_mut().end();
+            let validated = match result {
+                ValidationResult::Incomplete => {
+                    self.edit_move_end()?;
+                    self.edit_insert('\n', 1)?;
+                    false
+                }
+                ValidationResult::Valid(msg) => {
+                    // Accept the line regardless of where the cursor is.
+                    self.edit_move_end()?;
+                    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)?;
+                    }
+                    true
+                }
+                ValidationResult::Invalid(msg) => {
+                    if corrected || self.has_hint() || msg.is_some() {
+                        self.refresh_line_with_msg(msg)?;
+                    }
+                    false
+                }
+            };
+            Ok(validated)
+        } else {
+            Ok(true)
+        }
+    }
+}
+
+impl<'out, 'prompt, H: Helper> Invoke for State<'out, 'prompt, H> {
+    fn input(&self) -> &str {
+        self.line.as_str()
+    }
 }
 
 impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> {
diff --git a/src/keymap.rs b/src/keymap.rs
index 37ec0a8..59b6455 100644
--- a/src/keymap.rs
+++ b/src/keymap.rs
@@ -288,6 +288,14 @@ pub struct InputState {
     last_char_search: Option<CharSearch>, // vi only
 }
 
+/// Provide indirect mutation to user input.
+pub trait Invoke {
+    /// currently edited line
+    fn input(&self) -> &str;
+    // TODO
+    //fn invoke(&mut self, cmd: Cmd) -> Result<?>;
+}
+
 pub trait Refresher {
     /// Rewrite the currently edited line accordingly to the buffer content,
     /// cursor position, and number of columns of the terminal.
diff --git a/src/lib.rs b/src/lib.rs
index 2754d72..8daef67 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -29,9 +29,9 @@ mod keys;
 mod kill_ring;
 mod layout;
 pub mod line_buffer;
-mod undo;
-
 mod tty;
+mod undo;
+pub mod validate;
 
 use std::collections::HashMap;
 use std::fmt;
@@ -58,6 +58,7 @@ use crate::keymap::{InputState, Refresher};
 pub use crate::keys::KeyPress;
 use crate::kill_ring::{KillRing, Mode};
 use crate::line_buffer::WordAction;
+use crate::validate::Validator;
 
 /// The error type for I/O and Linux Syscalls (Errno)
 pub type Result<T> = result::Result<T, error::ReadlineError>;
@@ -532,7 +533,10 @@ fn readline_edit<H: Helper>(
                     s.refresh_line_with_msg(None)?;
                 }
                 s.edit_move_end()?;
-                break;
+                if s.validate()? {
+                    break;
+                }
+                continue;
             }
             Cmd::BeginningOfHistory => {
                 // move to first entry in history
@@ -652,7 +656,7 @@ fn readline_direct() -> Result<String> {
 /// (parse current line once)
 pub trait Helper
 where
-    Self: Completer + Hinter + Highlighter,
+    Self: Completer + Hinter + Highlighter + Validator,
 {
 }
 
diff --git a/src/validate.rs b/src/validate.rs
new file mode 100644
index 0000000..e718aa0
--- /dev/null
+++ b/src/validate.rs
@@ -0,0 +1,81 @@
+//! Input validation API (Multi-line editing)
+
+use crate::keymap::Invoke;
+use crate::Result;
+
+/// Input validation result
+#[non_exhaustive]
+pub enum ValidationResult {
+    /// Incomplete input
+    Incomplete,
+    /// Validation fails with an optional error message. User must fix the
+    /// input.
+    Invalid(Option<String>),
+    /// Validation succeeds with an optional message
+    Valid(Option<String>),
+}
+
+pub struct ValidationContext<'i> {
+    i: &'i mut dyn Invoke,
+}
+
+impl<'i> ValidationContext<'i> {
+    pub(crate) fn new(i: &'i mut dyn Invoke) -> Self {
+        ValidationContext { i }
+    }
+
+    pub fn input(&self) -> &str {
+        self.i.input()
+    }
+
+    // TODO
+    //fn invoke(&mut self, cmd: Cmd) -> Result<?> {
+    //    self.i.invoke(cmd)
+    //}
+}
+
+/// This trait provides an extension interface for determining whether
+/// the current input buffer is valid. Rustyline uses the method
+/// provided by this trait to decide whether hitting the enter key
+/// will end the current editing session and return the current line
+/// buffer to the caller of `Editor::readline` or variants.
+pub trait Validator {
+    /// Takes the currently edited `input` and returns a
+    /// `ValidationResult` indicating whether it is valid or not along
+    /// with an option message to display about the result. The most
+    /// common validity check to implement is probably whether the
+    /// input is complete or not, for instance ensuring that all
+    /// delimiters are fully balanced.
+    ///
+    /// If you implement more complex validation checks it's probably
+    /// a good idea to also implement a `Hinter` to provide feedback
+    /// about what is invalid.
+    ///
+    /// 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<ValidationResult> {
+        let _ = ctx;
+        Ok(ValidationResult::Valid(None))
+    }
+
+    /// Configure whether validation is performed while typing or only
+    /// when user presses the Enter key.
+    ///
+    /// Default is `false`.
+    // TODO we can implement this later.
+    fn validate_while_typing(&self) -> bool {
+        false
+    }
+}
+
+impl Validator for () {}
+
+impl<'v, V: ?Sized + Validator> Validator for &'v V {
+    fn validate(&self, ctx: &mut ValidationContext) -> Result<ValidationResult> {
+        (**self).validate(ctx)
+    }
+
+    fn validate_while_typing(&self) -> bool {
+        (**self).validate_while_typing()
+    }
+}


Test patch:
diff --git a/src/test/mod.rs b/src/test/mod.rs
index b995b90..365fe91 100644
--- a/src/test/mod.rs
+++ b/src/test/mod.rs
@@ -10,6 +10,7 @@ use crate::hint::Hinter;
 use crate::keymap::{Cmd, InputState};
 use crate::keys::KeyPress;
 use crate::tty::Sink;
+use crate::validate::Validator;
 use crate::{Context, Editor, Helper, Result};
 
 mod common;
@@ -42,6 +43,7 @@ impl Completer for SimpleCompleter {
 impl Helper for SimpleCompleter {}
 impl Hinter for SimpleCompleter {}
 impl Highlighter for SimpleCompleter {}
+impl Validator for SimpleCompleter {}
 
 #[test]
 fn complete_line() {