From 56719adab3ab6c31ed22a7982e7d9dde6e871557 Mon Sep 17 00:00:00 2001
From: gwenn <gtreguier@gmail.com>
Date: Tue, 5 Apr 2016 19:59:56 +0200
Subject: [PATCH] Multiline (and unicode) support

---
 src/lib.rs | 189 ++++++++++++++++++++++++++---------------------------
 1 file changed, 94 insertions(+), 95 deletions(-)

diff --git a/src/lib.rs b/src/lib.rs
index 8c12cfd0..8656bd32 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -47,16 +47,21 @@ pub type Result<T> = result::Result<T, error::ReadlineError>;
 struct State<'out, 'prompt> {
     out: &'out mut Write,
     prompt: &'prompt str, // Prompt to display
-    prompt_width: usize, // Prompt Unicode width
+    prompt_size: Position, // Prompt Unicode width and height
     buf: String, // Edited line buffer
     pos: usize, // Current cursor position (byte position)
-    old_pos: usize, // Previous refresh cursor position (multiline mode)
+    cursor: Position, // Cursor position (relative to the start of the prompt for `row`)
     cols: usize, // Number of columns in terminal
-    max_rows: usize, // Maximum num of rows used so far (multiline mode)
     history_index: usize, // The history index we are currently editing.
     history_end: String, // Current edited line before history browsing
 }
 
+#[derive(Copy, Clone, Debug, Default)]
+struct Position {
+    col: usize,
+    row: usize,
+}
+
 impl<'out, 'prompt> State<'out, 'prompt> {
     fn new(out: &'out mut Write,
            prompt: &'prompt str,
@@ -64,15 +69,15 @@ impl<'out, 'prompt> State<'out, 'prompt> {
            cols: usize,
            history_index: usize)
            -> State<'out, 'prompt> {
+        let prompt_size = calculate_position(prompt, Default::default(), cols);
         State {
             out: out,
             prompt: prompt,
-            prompt_width: width(prompt),
+            prompt_size: prompt_size,
             buf: String::with_capacity(capacity),
             pos: 0,
-            old_pos: 0,
+            cursor: prompt_size,
             cols: cols,
-            max_rows: 0,
             history_index: history_index,
             history_end: String::new(),
         }
@@ -89,74 +94,51 @@ impl<'out, 'prompt> State<'out, 'prompt> {
     /// Rewrite the currently edited line accordingly to the buffer content,
     /// cursor position, and number of columns of the terminal.
     fn refresh_line(&mut self) -> Result<()> {
-        let prompt_width = self.prompt_width;
-        self.refresh(self.prompt, prompt_width)
+        let prompt_size = self.prompt_size;
+        self.refresh(self.prompt, prompt_size)
     }
 
     fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> {
-        let prompt_width = width(prompt);
-        self.refresh(prompt, prompt_width)
+        let prompt_size = calculate_position(prompt, Default::default(), self.cols);
+        self.refresh(prompt, prompt_size)
     }
 
-    fn refresh(&mut self, prompt: &str, prompt_width: usize) -> Result<()> {
+    fn refresh(&mut self, prompt: &str, prompt_size: Position) -> Result<()> {
         use std::fmt::Write;
 
-        let buf = &self.buf;
-        // rows used by current buf.
-        let mut rows = (prompt_width + width(&buf)) / self.cols; // FIXME does not work with a char which width > 1...
-        // cursor relative row.
-        let rpos = (prompt_width + self.old_pos + self.cols) / self.cols;
-        //
-
-        let old_rows = self.max_rows;
-        // Update maxrows if needed.
-        if rows > self.max_rows {
-            self.max_rows = rows;
-        }
+        let end_pos = calculate_position(&self.buf, prompt_size, self.cols);
+        let cursor = calculate_position(&self.buf[..self.pos], prompt_size, self.cols);
 
-        // First step: clear all the lines used before. To do so start by going to the last row.
         let mut ab = String::new();
-        if old_rows > rpos {
-            ab.write_fmt(format_args!("\r\x1b[{}B", old_rows - rpos)).unwrap();
+        let cursor_row_movement = self.cursor.row - self.prompt_size.row;
+        // move the cursor up as required
+        if cursor_row_movement > 0 {
+            ab.write_fmt(format_args!("\x1b[{}A", cursor_row_movement)).unwrap();
         }
-        // Now for every row clear it, go up.
-        for _ in 1..old_rows {
-            ab.push_str("\r\x1b[0K\x1b[1A");
-        }
-        // Clean the top line.
-        ab.push_str("\r\x1b[0K");
-
-        // Write the prompt and the current buffer content
+        // position at the start of the prompt, clear to end of screen
+        ab.push_str("\r\x1b[J");
+        // display the prompt
         ab.push_str(prompt);
-        ab.push_str(&buf);
-
-        // If we are at the very end of the screen with our prompt, we need to
-        // emit a newline and move the prompt to the first column.
-        if self.pos > 0 && self.pos == buf.len() && (self.pos + prompt_width) % self.cols == 0 {
-            ab.push_str("\n\r");
-            rows += 1;
-            if rows > self.max_rows {
-                self.max_rows = rows;
-            }
+        // display the input line
+        ab.push_str(&self.buf);
+        // we have to generate our own newline on line wrap
+        if end_pos.col == 0 && end_pos.row > 0 {
+            ab.push_str("\n");
         }
-
-        // Move cursor to right position.
-        // current cursor relative row.
-        let rpos2 = (prompt_width + self.pos + self.cols) / self.cols;
-        // Go up till we reach the expected positon.
-        if rows > rpos2 {
-            ab.write_fmt(format_args!("\x1b[{}A", rows - rpos2)).unwrap();
+        // position the cursor
+        let cursor_row_movement = end_pos.row - cursor.row;
+        // move the cursor up as required
+        if cursor_row_movement > 0 {
+            ab.write_fmt(format_args!("\x1b[{}A", cursor_row_movement)).unwrap();
         }
-
-        // Set column.
-        let col = (prompt_width + self.pos) % self.cols;
-        if col != 0 {
-            ab.write_fmt(format_args!("\r\x1b[{}C", col)).unwrap();
+        // position the cursor within the line
+        if cursor.col > 0 {
+            ab.write_fmt(format_args!("\r\x1b[{}C", cursor.col)).unwrap();
         } else {
             ab.push('\r');
         }
 
-        self.old_pos = self.pos;
+        self.cursor = cursor;
 
         write_and_flush(self.out, ab.as_bytes())
     }
@@ -166,11 +148,12 @@ impl<'out, 'prompt> fmt::Debug for State<'out, 'prompt> {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         f.debug_struct("State")
          .field("prompt", &self.prompt)
-         .field("prompt_width", &self.prompt_width)
+         .field("prompt_size", &self.prompt_size)
          .field("buf", &self.buf)
          .field("buf length", &self.buf.len())
          .field("buf capacity", &self.buf.capacity())
          .field("pos", &self.pos)
+         .field("cursor", &self.cursor)
          .field("cols", &self.cols)
          .field("history_index", &self.history_index)
          .field("history_end", &self.history_end)
@@ -286,40 +269,56 @@ fn beep() -> Result<()> {
     write_and_flush(&mut io::stderr(), b"\x07") // TODO bell-style
 }
 
-// Control characters are treated as having zero width.
-fn width(s: &str) -> usize {
-    if s.contains('\x1b') {
-        let mut w = 0;
-        let mut esc_seq = 0;
-        for c in s.chars() {
-            if esc_seq == 1 {
-                if c == '[' {
-                    // CSI
-                    esc_seq = 2;
-                } else {
-                    // two-character sequence
-                    esc_seq = 0;
-                }
-            } else if esc_seq == 2 {
-                if c == ';' || (c >= '0' && c <= '9') {
-                } else if c == 'm' {
-                    // last
-                    esc_seq = 0
-                } else {
-                    // not supported
-                    w += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
-                    esc_seq = 0
-                }
-            } else if c == '\x1b' {
-                esc_seq = 1;
+/// Calculate the number of columns and rows used to display `s` on a `cols` width terminal
+/// starting at `orig`.
+/// Control characters are treated as having zero width.
+/// Characters with 2 column width are correctly handled (not splitted).
+fn calculate_position(s: &str, orig: Position, cols: usize) -> Position {
+    let mut pos = orig.clone();
+    let mut esc_seq = 0;
+    for c in s.chars() {
+        let cw = if esc_seq == 1 {
+            if c == '[' {
+                // CSI
+                esc_seq = 2;
             } else {
-                w += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
+                // two-character sequence
+                esc_seq = 0;
+            }
+            None
+        } else if esc_seq == 2 {
+            if c == ';' || (c >= '0' && c <= '9') {
+            } else if c == 'm' {
+                // last
+                esc_seq = 0;
+            } else {
+                // not supported
+                esc_seq = 0;
+            }
+            None
+        } else if c == '\x1b' {
+            esc_seq = 1;
+            None
+        } else if c == '\n' {
+            pos.col = 0;
+            pos.row += 1;
+            None
+        } else {
+            unicode_width::UnicodeWidthChar::width(c)
+        };
+        if let Some(cw) = cw {
+            pos.col += cw;
+            if pos.col > cols {
+                pos.row += 1;
+                pos.col = cw;
             }
         }
-        w
-    } else {
-        unicode_width::UnicodeWidthStr::width(s)
     }
+    if pos.col == cols {
+        pos.col = 0;
+        pos.row += 1;
+    }
+    pos
 }
 
 /// Insert the character `ch` at cursor current position.
@@ -328,7 +327,7 @@ fn edit_insert(s: &mut State, ch: char) -> Result<()> {
         if s.pos == s.buf.len() {
             s.buf.push(ch);
             s.pos += ch.len_utf8();
-            if s.prompt_width + width(&s.buf) < s.cols {
+            if s.cursor.col + unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0) < s.cols {
                 // Avoid a full update of the line in the trivial case.
                 let bits = ch.encode_utf8();
                 let bits = bits.as_slice();
@@ -356,7 +355,7 @@ fn edit_yank(s: &mut State, text: &str) -> Result<()> {
     } else {
         insert_str(&mut s.buf, s.pos, text);
     }
-    s.pos += width(text);
+    s.pos += text.len();
     s.refresh_line()
 }
 
@@ -1034,12 +1033,11 @@ mod test {
         State {
             out: out,
             prompt: "",
-            prompt_width: 0,
+            prompt_size: Default::default(),
             buf: String::from(line),
             pos: pos,
-            old_pos: pos,
+            cursor: Default::default(),
             cols: cols,
-            max_rows: 0,
             history_index: 0,
             history_end: String::new(),
         }
@@ -1201,7 +1199,8 @@ mod test {
 
     #[test]
     fn prompt_with_ansi_escape_codes() {
-        let w = super::width("\x1b[1;32m>>\x1b[0m ");
-        assert_eq!(3, w);
+        let pos = super::calculate_position("\x1b[1;32m>>\x1b[0m ", Default::default(), 80);
+        assert_eq!(3, pos.col);
+        assert_eq!(0, pos.row);
     }
 }
-- 
GitLab