Repository: kkawakam/rustyline
Pull request #379 (2020-05-08 12:46:06)

Flexible AcceptLine, fix bug in multi-line accept
1. This commit allows keybinding for forcing accept line (even if
  validator fails). This is useful if you have a separate keybinding
  for accepting, such as `Alt-Enter`. It makes sense to show an error
  to user rather than insert an unexpected newline.
2. Forcing `Newline` is also accepted
3. `AcceptLine` meaning is changed (now it forces accept),
   but default `Enter` keybinding is not
4. `AcceptOrInsertLine` now has a flag that modifies it's behavior in
  the middle of the line (at the end it always submits, when validator
  returned false it's always a newline)
5. Fixed bug with exiting readline (both on success and error) when
  cursor is not on the last line

I'm not sure that current command-names are the best possible, so let me know if we can do better. The primary motivator was (1) from above.

This conflicts with #342. I'll rebase other one when either of them is merged.


Patch:
diff --git a/src/keymap.rs b/src/keymap.rs
index 171f4f3..e4f81f7 100644
--- a/src/keymap.rs
+++ b/src/keymap.rs
@@ -20,6 +20,8 @@ pub enum Cmd {
     /// abort
     Abort, // Miscellaneous Command
     /// accept-line
+    ///
+    /// See also AcceptOrInsertLine
     AcceptLine,
     /// beginning-of-history
     BeginningOfHistory,
@@ -99,9 +101,23 @@ pub enum Cmd {
     /// moves cursor to the line below or switches to next history entry if
     /// the cursor is already on the last line
     LineDownOrNextHistory(RepeatCount),
-    /// accepts the line when cursor is at the end of the text (non including
-    /// trailing whitespace), inserts newline character otherwise
-    AcceptOrInsertLine,
+    /// Inserts a newline
+    Newline,
+    /// Either accepts or inserts a newline
+    ///
+    /// Always inserts newline if input is non-valid. Can also insert newline
+    /// if cursor is in the middle of the text
+    ///
+    /// If you support multi-line input:
+    /// * Use `accept_in_the_middle: true` for mostly single-line cases,
+    ///   for example command-line.
+    /// * Use `accept_in_the_middle: false` for mostly multi-line cases,
+    ///   for example SQL or JSON input.
+    AcceptOrInsertLine {
+        /// Whether this commands accepts input if the cursor not at the end
+        /// of the current input
+        accept_in_the_middle: bool
+    },
 }
 
 impl Cmd {
@@ -913,7 +929,9 @@ impl InputState {
                 }
             }
             KeyPress::Ctrl('J') |
-            KeyPress::Enter => Cmd::AcceptLine,
+            KeyPress::Enter => {
+                Cmd::AcceptOrInsertLine { accept_in_the_middle: true }
+            }
             KeyPress::Down => Cmd::LineDownOrNextHistory(1),
             KeyPress::Up => Cmd::LineUpOrPreviousHistory(1),
             KeyPress::Ctrl('R') => Cmd::ReverseSearchHistory,
diff --git a/src/lib.rs b/src/lib.rs
index f75253e..cd68b47 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -281,7 +281,8 @@ fn page_completions<C: Candidate, H: Helper>(
                 && cmd != Cmd::SelfInsert(1, ' ')
                 && cmd != Cmd::Kill(Movement::BackwardChar(1))
                 && cmd != Cmd::AcceptLine
-                && cmd != Cmd::AcceptOrInsertLine
+                && cmd != Cmd::Newline
+                && !matches!(cmd, Cmd::AcceptOrInsertLine { .. })
             {
                 cmd = s.next_cmd(input_state, rdr, false)?;
             }
@@ -289,7 +290,7 @@ fn page_completions<C: Candidate, H: Helper>(
                 Cmd::SelfInsert(1, 'y') | Cmd::SelfInsert(1, 'Y') | Cmd::SelfInsert(1, ' ') => {
                     pause_row += s.out.get_rows() - 1;
                 }
-                Cmd::AcceptLine | Cmd::AcceptOrInsertLine => {
+                Cmd::AcceptLine | Cmd::Newline | Cmd::AcceptOrInsertLine { .. } => {
                     pause_row += 1;
                 }
                 _ => break,
@@ -427,6 +428,7 @@ fn readline_edit<H: Helper>(
     editor.reset_kill_ring(); // TODO recreate a new kill ring vs Arc<Mutex<KillRing>>
     let ctx = Context::new(&editor.history);
     let mut s = State::new(&mut stdout, prompt, helper, ctx);
+
     let mut input_state = InputState::new(&editor.config, Arc::clone(&editor.custom_bindings));
 
     s.line.set_delete_listener(editor.kill_ring.clone());
@@ -509,13 +511,19 @@ fn readline_edit<H: Helper>(
                 s.edit_overwrite_char(c)?;
             }
             Cmd::EndOfFile => {
-                if !input_state.is_emacs_mode() && !s.line.is_empty() {
-                    s.edit_move_end()?;
-                    break;
-                } else if s.line.is_empty() {
-                    return Err(error::ReadlineError::Eof);
-                } else {
+                if input_state.is_emacs_mode() && !s.line.is_empty() {
                     s.edit_delete(1)?
+                } else {
+                    if s.has_hint() || !s.is_default_prompt() {
+                        // Force a refresh without hints to leave the previous
+                        // line as the user typed it after a newline.
+                        s.refresh_line_with_msg(None)?;
+                    }
+                    if s.line.is_empty() {
+                        return Err(error::ReadlineError::Eof);
+                    } else if !input_state.is_emacs_mode() {
+                        break;
+                    }
                 }
             }
             Cmd::Move(Movement::EndOfLine) => {
@@ -575,7 +583,7 @@ fn readline_edit<H: Helper>(
                     kill_ring.kill(&text, Mode::Append)
                 }
             }
-            Cmd::AcceptLine | Cmd::AcceptOrInsertLine => {
+            Cmd::AcceptLine | Cmd::AcceptOrInsertLine { .. } | Cmd::Newline => {
                 #[cfg(test)]
                 {
                     editor.term.cursor = s.layout.cursor.col;
@@ -585,13 +593,23 @@ fn readline_edit<H: Helper>(
                     // line as the user typed it after a newline.
                     s.refresh_line_with_msg(None)?;
                 }
-                // Only accept value if cursor is at the end of the buffer
-                if s.validate()? && (cmd == Cmd::AcceptLine || s.line.is_end_of_input()) {
-                    break;
-                } else {
-                    s.edit_insert('\n', 1)?;
+                let valid = s.validate()?;
+                let end = s.line.is_end_of_input();
+                match (cmd, valid, end) {
+                    | (Cmd::AcceptLine, _, _)
+                    | (Cmd::AcceptOrInsertLine { .. }, true, true)
+                    | (Cmd::AcceptOrInsertLine { accept_in_the_middle: true }, true, _)
+                    => {
+                        break;
+                    }
+                    | (Cmd::Newline, _, _)
+                    | (Cmd::AcceptOrInsertLine { .. }, false, _)
+                    | (Cmd::AcceptOrInsertLine { .. }, true, false)
+                    => {
+                        s.edit_insert('\n', 1)?;
+                    }
+                    _ => unreachable!(),
                 }
-                continue;
             }
             Cmd::BeginningOfHistory => {
                 // move to first entry in history
@@ -656,6 +674,10 @@ fn readline_edit<H: Helper>(
                 }
             }
             Cmd::Interrupt => {
+                // Move to end, in case cursor was in the middle of the
+                // line, so that next thing application prints goes after
+                // the input
+                s.edit_move_buffer_end()?;
                 return Err(error::ReadlineError::Interrupted);
             }
             #[cfg(unix)]
@@ -671,6 +693,11 @@ fn readline_edit<H: Helper>(
             }
         }
     }
+
+    // Move to end, in case cursor was in the middle of the line, so that
+    // next thing application prints goes after the input
+    s.edit_move_buffer_end()?;
+
     if cfg!(windows) {
         let _ = original_mode; // silent warning
     }


Test patch:
diff --git a/src/test/mod.rs b/src/test/mod.rs
index 365fe91..b4046d0 100644
--- a/src/test/mod.rs
+++ b/src/test/mod.rs
@@ -56,7 +56,7 @@ fn complete_line() {
     let keys = vec![KeyPress::Enter];
     let mut rdr: IntoIter<KeyPress> = keys.into_iter();
     let cmd = super::complete_line(&mut rdr, &mut s, &mut input_state, &Config::default()).unwrap();
-    assert_eq!(Some(Cmd::AcceptLine), cmd);
+    assert_eq!(Some(Cmd::AcceptOrInsertLine { accept_in_the_middle: true }), cmd);
     assert_eq!("rust", s.line.as_str());
     assert_eq!(4, s.line.pos());
 }