From 5cf9b613e7164f0279ac005d93cf52bf17a7d688 Mon Sep 17 00:00:00 2001
From: Gwenael Treguier <gwen@cas75-5-78-192-41-37.fbxo.proxad.net>
Date: Mon, 17 Aug 2015 20:19:04 +0200
Subject: [PATCH] Add basic move and edit commands.

---
 src/lib.rs | 262 ++++++++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 241 insertions(+), 21 deletions(-)

diff --git a/src/lib.rs b/src/lib.rs
index 6b1ce1cf..7d5f6a20 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -13,6 +13,7 @@
 //!     Err(_)   => println!("No input"),
 //! }
 //!```
+#![feature(drain)]
 #![feature(io)]
 #![feature(str_char)]
 #![feature(unicode)]
@@ -40,7 +41,7 @@ struct State<'prompt> {
     prompt: &'prompt str, // Prompt to display
     prompt_width: usize, // Prompt Unicode width
     buf: String, // Edited line buffer
-    pos: usize, // Current cursor position
+    pos: usize, // Current cursor position (byte position)
 //    oldpos: usize, // Previous refresh cursor position
     cols: usize, // Number of columns in terminal
     bytes: [u8; 4]
@@ -138,12 +139,23 @@ fn get_columns() -> usize {
     }
 }
 
-fn write_and_flush(stdout: &mut io::Stdout, buf: &[u8]) -> Result<()> {
-    try!(stdout.write_all(buf));
-    try!(stdout.flush());
+fn write_and_flush(w: &mut Write, buf: &[u8]) -> Result<()> {
+    try!(w.write_all(buf));
+    try!(w.flush());
     Ok(())
 }
 
+/// Clear the screen. Used to handle ctrl+l
+fn clear_screen(stdout: &mut io::Stdout) -> Result<()> {
+    write_and_flush(stdout, b"\x1b[H\x1b[2J")
+}
+
+/// Beep, used for completion when there is nothing to complete or when all
+/// the choices were already shown.
+/*fn beep() -> Result<()> {
+    write_and_flush(&mut io::stderr(), b"\x07")
+}*/
+
 // Control characters are treated as having zero width.
 fn width(s: &str) -> usize {
     unicode_width::UnicodeWidthStr::width(s)
@@ -151,7 +163,7 @@ fn width(s: &str) -> usize {
 
 /// Rewrite the currently edited line accordingly to the buffer content,
 /// cursor position, and number of columns of the terminal.
-fn refresh_line(s: &mut State, stdout: &mut io::Stdout) -> Result<()> {
+fn refresh_line(s: &mut State, stdout: &mut Write) -> Result<()> {
     use std::fmt::Write;
     use unicode_width::UnicodeWidthChar;
 
@@ -185,7 +197,7 @@ fn refresh_line(s: &mut State, stdout: &mut io::Stdout) -> Result<()> {
 }
 
 /// Insert the character 'c' at cursor current position.
-fn edit_insert(s: &mut State, stdout: &mut io::Stdout, ch: char) -> Result<()> {
+fn edit_insert(s: &mut State, stdout: &mut Write, ch: char) -> Result<()> {
     if s.buf.len() < s.buf.capacity() {
         if s.buf.len() == s.pos {
             s.buf.push(ch);
@@ -199,6 +211,7 @@ fn edit_insert(s: &mut State, stdout: &mut io::Stdout, ch: char) -> Result<()> {
             }
         } else {
             s.buf.insert(s.pos, ch);
+            s.pos += ch.len_utf8();
             refresh_line(s, stdout)
         }
     } else {
@@ -206,6 +219,118 @@ fn edit_insert(s: &mut State, stdout: &mut io::Stdout, ch: char) -> Result<()> {
     }
 }
 
+/// Move cursor on the left.
+fn edit_move_left(s: &mut State, stdout: &mut Write) -> Result<()> {
+    if s.pos > 0 {
+        let ch = s.buf.char_at_reverse(s.pos);
+        let size = ch.len_utf8();
+        s.pos -= size;
+        refresh_line(s, stdout)
+    } else {
+        Ok(())
+    }
+}
+
+/// Move cursor on the right.
+fn edit_move_right(s: &mut State, stdout: &mut Write) -> Result<()> {
+    if s.pos != s.buf.len() {
+        let ch = s.buf.char_at(s.pos);
+        let size = ch.len_utf8();
+        s.pos += size;
+        refresh_line(s, stdout)
+    } else {
+        Ok(())
+    }
+}
+
+/// Move cursor to the start of the line.
+fn edit_move_home(s: &mut State, stdout: &mut Write) -> Result<()> {
+    if s.pos > 0 {
+        s.pos = 0;
+        refresh_line(s, stdout)
+    } else {
+        Ok(())
+    }
+}
+
+/// Move cursor to the end of the line.
+fn edit_move_end(s: &mut State, stdout: &mut Write) -> Result<()> {
+    if s.pos != s.buf.len() {
+        s.pos = s.buf.len();
+        refresh_line(s, stdout)
+    } else {
+        Ok(())
+    }
+}
+
+/// Delete the character at the right of the cursor without altering the cursor
+/// position. Basically this is what happens with the "Delete" keyboard key.
+fn edit_delete(s: &mut State, stdout: &mut Write) -> Result<()> {
+    if s.buf.len() > 0 && s.pos < s.buf.len() {
+        s.buf.remove(s.pos);
+        refresh_line(s, stdout)
+    } else {
+        Ok(())
+    }
+}
+
+/// Backspace implementation.
+fn edit_backspace(s: &mut State, stdout: &mut Write) -> Result<()> {
+    if s.pos > 0 && s.buf.len() > 0 {
+        let ch = s.buf.char_at_reverse(s.pos);
+        let size = ch.len_utf8();
+        s.pos -= size;
+        s.buf.remove(s.pos);
+        refresh_line(s, stdout)
+    } else {
+        Ok(())
+    }
+}
+
+/// Kill the text from point to the end of the line.
+fn edit_kill_line(s: &mut State, stdout: &mut Write) -> Result<()> {
+    if s.buf.len() > 0 && s.pos < s.buf.len() {
+        s.buf.drain(s.pos..);
+        refresh_line(s, stdout)
+    } else {
+        Ok(())
+    }
+}
+
+/// Kill backward from point to the beginning of the line.
+fn edit_discard_line(s: &mut State, stdout: &mut Write) -> Result<()> {
+    if s.pos > 0 && s.buf.len() > 0 {
+        s.buf.drain(..s.pos);
+        s.pos = 0;
+        refresh_line(s, stdout)
+    } else {
+        Ok(())
+    }
+}
+
+/// Exchange the char before cursor with the character at cursor.
+fn edit_transpose_chars(s: &mut State, stdout: &mut Write) -> Result<()> {
+    if s.pos > 0 && s.pos < s.buf.len() {
+        let ch = s.buf.remove(s.pos);
+        let size = ch.len_utf8();
+        let och = s.buf.char_at_reverse(s.pos);
+        let osize = och.len_utf8();
+        s.buf.insert(s.pos - osize, ch);
+        if s.pos != s.buf.len()-size {
+            s.pos += size;
+        } else {
+            if size >= osize {
+                s.pos += size - osize;
+            } else {
+                s.pos -= osize - size;
+            }
+        }
+        refresh_line(s, stdout)
+    } else {
+        Ok(())
+    }
+}
+
 /// Handles reading and editting the readline buffer.
 /// It will also handle special inputs in an appropriate fashion
 /// (e.g., C-c will exit readline)
@@ -227,25 +352,34 @@ fn readline_edit(prompt: &str) -> Result<String> {
     loop {
         let ch = try!(chars.next().unwrap());
         match char_to_key_press(ch) {
-            KeyPress::CTRL_A => print!("Pressed C-a"),
-            KeyPress::CTRL_B => print!("Pressed C-b"),
+            KeyPress::CTRL_A => try!(edit_move_home(&mut s, &mut stdout)), // Move to the beginning of line.
+            KeyPress::CTRL_B => try!(edit_move_left(&mut s, &mut stdout)), // Move back a character.
             KeyPress::CTRL_C => {
                 return Err(from_errno(Errno::EAGAIN))
             },
-            KeyPress::CTRL_D => print!("Pressed C-d"),
-            KeyPress::CTRL_E => print!("Pressed C-e"),
-            KeyPress::CTRL_F => print!("Pressed C-f"),
-            KeyPress::CTRL_H => print!("Pressed C-h"),
-            KeyPress::CTRL_K => print!("Pressed C-k"),
-            KeyPress::CTRL_L => print!("Pressed C-l"),
-            KeyPress::CTRL_N => print!("Pressed C-n"),
-            KeyPress::CTRL_P => print!("Pressed C-p"),
-            KeyPress::CTRL_T => print!("Pressed C-t"),
-            KeyPress::CTRL_U => print!("Pressed C-u"),
-            KeyPress::CTRL_W => print!("Pressed C-w"),
+            KeyPress::CTRL_D => {
+                if s.buf.len() > 0 { // Delete one character at point.
+                    try!(edit_delete(&mut s, &mut stdout))
+                } else {
+                    break
+                }
+            },
+            KeyPress::CTRL_E => try!(edit_move_end(&mut s, &mut stdout)), // Move to the end of line.
+            KeyPress::CTRL_F => try!(edit_move_right(&mut s, &mut stdout)), // Move forward a character.
+            KeyPress::CTRL_H | KeyPress::BACKSPACE => try!(edit_backspace(&mut s, &mut stdout)), // Delete one character backward.
+            KeyPress::CTRL_K => try!(edit_kill_line(&mut s, &mut stdout)), // Kill the text from point to the end of the line.
+            KeyPress::CTRL_L => { // Clear the screen leaving the current line at the top of the screen.
+                try!(clear_screen(&mut stdout));
+                try!(refresh_line(&mut s, &mut stdout))
+            },
+            KeyPress::CTRL_N => print!("Pressed C-n"), // Fetch the next command from the history list.
+            KeyPress::CTRL_P => print!("Pressed C-p"), // Fetch the previous command from the history list.
+            KeyPress::CTRL_T => try!(edit_transpose_chars(&mut s, &mut stdout)), // Exchange the char before cursor with the character at cursor.
+            KeyPress::CTRL_U => try!(edit_discard_line(&mut s, &mut stdout)), // Kill backward from point to the beginning of the line.
+            KeyPress::CTRL_W => print!("Pressed C-w"), // Kill the word behind point, using white space as a word boundary
             KeyPress::ESC    => print!("Pressed esc") ,
-            KeyPress::ENTER  => break,
-            _      => try!(edit_insert(&mut s, &mut stdout, ch)),
+            KeyPress::ENTER  => break, // Accept the line regardless of where the cursor is.
+            _      => try!(edit_insert(&mut s, &mut stdout, ch)), // Insert the character typed.
         }
     }
     Ok(s.buf)
@@ -283,3 +417,89 @@ pub fn readline(prompt: &str) -> Result<String> {
         readline_raw(prompt)
     }
 }
+
+#[cfg(test)]
+mod test {
+    use State;
+
+    #[test]
+    fn test_insert() {
+        let mut s = State { prompt: "", prompt_width: 0, buf: String::with_capacity(128), pos: 0, cols: 80, bytes: [0; 4]};
+        let mut stdout = ::std::io::sink();
+        super::edit_insert(&mut s, &mut stdout, 'α').unwrap();
+        assert_eq!("α", s.buf);
+        assert_eq!(2, s.pos);
+
+        super::edit_insert(&mut s, &mut stdout, 'ß').unwrap();
+        assert_eq!("αß", s.buf);
+        assert_eq!(4, s.pos);
+
+        s.pos = 0;
+        super::edit_insert(&mut s, &mut stdout, 'γ').unwrap();
+        assert_eq!("γαß", s.buf);
+        assert_eq!(2, s.pos);
+    }
+
+    #[test]
+    fn test_move() {
+        let mut s = State { prompt: "", prompt_width: 0, buf: String::from("αß"), pos: 4, cols: 80, bytes: [0; 4]};
+        let mut stdout = ::std::io::sink();
+        super::edit_move_left(&mut s, &mut stdout).unwrap();
+        assert_eq!("αß", s.buf);
+        assert_eq!(2, s.pos);
+
+        super::edit_move_right(&mut s, &mut stdout).unwrap();
+        assert_eq!("αß", s.buf);
+        assert_eq!(4, s.pos);
+
+        super::edit_move_home(&mut s, &mut stdout).unwrap();
+        assert_eq!("αß", s.buf);
+        assert_eq!(0, s.pos);
+
+        super::edit_move_end(&mut s, &mut stdout).unwrap();
+        assert_eq!("αß", s.buf);
+        assert_eq!(4, s.pos);
+    }
+
+    #[test]
+    fn test_delete() {
+        let mut s = State { prompt: "", prompt_width: 0, buf: String::from("αß"), pos: 2, cols: 80, bytes: [0; 4]};
+        let mut stdout = ::std::io::sink();
+        super::edit_delete(&mut s, &mut stdout).unwrap();
+        assert_eq!("α", s.buf);
+        assert_eq!(2, s.pos);
+
+        super::edit_backspace(&mut s, &mut stdout).unwrap();
+        assert_eq!("", s.buf);
+        assert_eq!(0, s.pos);
+    }
+
+    #[test]
+    fn test_kill() {
+        let mut s = State { prompt: "", prompt_width: 0, buf: String::from("αßγδε"), pos: 6, cols: 80, bytes: [0; 4]};
+        let mut stdout = ::std::io::sink();
+        super::edit_kill_line(&mut s, &mut stdout).unwrap();
+        assert_eq!("αßγ", s.buf);
+        assert_eq!(6, s.pos);
+
+        s.pos = 4;
+        super::edit_discard_line(&mut s, &mut stdout).unwrap();
+        assert_eq!("γ", s.buf);
+        assert_eq!(0, s.pos);
+    }
+
+    #[test]
+    fn test_transpose() {
+        let mut s = State { prompt: "", prompt_width: 0, buf: String::from("aßc"), pos: 1, cols: 80, bytes: [0; 4]};
+        let mut stdout = ::std::io::sink();
+        super::edit_transpose_chars(&mut s, &mut stdout).unwrap();
+        assert_eq!("ßac", s.buf);
+        assert_eq!(3, s.pos);
+
+        s.buf = String::from("aßc");
+        s.pos = 3;
+        super::edit_transpose_chars(&mut s, &mut stdout).unwrap();
+        assert_eq!("acß", s.buf);
+        assert_eq!(2, s.pos);
+    }
+}
-- 
GitLab