From 8bfbfe919c9a9dfa211c2fe659c43948246fca4f Mon Sep 17 00:00:00 2001
From: gwenn <gtreguier@gmail.com>
Date: Sun, 8 Jan 2017 10:21:11 +0100
Subject: [PATCH] Move cursor by grapheme (not by char) #107

---
 src/keymap.rs      |  42 +++++-----
 src/lib.rs         |  35 ++++----
 src/line_buffer.rs | 205 ++++++++++++++++++++++++++-------------------
 3 files changed, 158 insertions(+), 124 deletions(-)

diff --git a/src/keymap.rs b/src/keymap.rs
index f7a5aceb..a769b828 100644
--- a/src/keymap.rs
+++ b/src/keymap.rs
@@ -8,34 +8,34 @@ use super::Result;
 pub enum Cmd {
     Abort, // Miscellaneous Command
     AcceptLine,
-    BackwardChar(u16),
-    BackwardDeleteChar(u16),
-    BackwardKillWord(u16, Word), // Backward until start of word
-    BackwardWord(u16, Word), // Backward until start of word
+    BackwardChar(usize),
+    BackwardDeleteChar(usize),
+    BackwardKillWord(usize, Word), // Backward until start of word
+    BackwardWord(usize, Word), // Backward until start of word
     BeginningOfHistory,
     BeginningOfLine,
     CapitalizeWord,
     ClearScreen,
     Complete,
-    DeleteChar(u16),
+    DeleteChar(usize),
     DowncaseWord,
     EndOfFile,
     EndOfHistory,
     EndOfLine,
-    ForwardChar(u16),
+    ForwardChar(usize),
     ForwardSearchHistory,
-    ForwardWord(u16, At, Word), // Forward until start/end of word
+    ForwardWord(usize, At, Word), // Forward until start/end of word
     Interrupt,
     KillLine,
     KillWholeLine,
-    KillWord(u16, At, Word), // Forward until start/end of word
+    KillWord(usize, At, Word), // Forward until start/end of word
     NextHistory,
     Noop,
     PreviousHistory,
     QuotedInsert,
-    Replace(u16, char), // TODO DeleteChar + SelfInsert
+    Replace(usize, char), // TODO DeleteChar + SelfInsert
     ReverseSearchHistory,
-    SelfInsert(u16, char),
+    SelfInsert(usize, char),
     Suspend,
     TransposeChars,
     TransposeWords,
@@ -43,9 +43,9 @@ pub enum Cmd {
     UnixLikeDiscard,
     // UnixWordRubout, // = BackwardKillWord(Word::Big)
     UpcaseWord,
-    ViCharSearch(u16, CharSearch),
-    ViDeleteTo(u16, CharSearch),
-    Yank(u16, Anchor),
+    ViCharSearch(usize, CharSearch),
+    ViDeleteTo(usize, CharSearch),
+    Yank(usize, Anchor),
     YankPop,
 }
 
@@ -375,7 +375,7 @@ impl EditState {
                                       rdr: &mut R,
                                       config: &Config,
                                       key: KeyPress,
-                                      n: u16)
+                                      n: usize)
                                       -> Result<Cmd> {
         let mut mvt = try!(rdr.next_key(config.keyseq_timeout()));
         if mvt == key {
@@ -432,7 +432,7 @@ impl EditState {
         })
     }
 
-    fn common(&mut self, key: KeyPress, n: u16, positive: bool) -> Cmd {
+    fn common(&mut self, key: KeyPress, n: usize, positive: bool) -> Cmd {
         match key {
             KeyPress::Home => Cmd::BeginningOfLine,
             KeyPress::Left => {
@@ -498,25 +498,25 @@ impl EditState {
         num_args
     }
 
-    fn emacs_num_args(&mut self) -> (u16, bool) {
+    fn emacs_num_args(&mut self) -> (usize, bool) {
         let num_args = self.num_args();
         if num_args < 0 {
             if let (n, false) = num_args.overflowing_abs() {
-                (n as u16, false)
+                (n as usize, false)
             } else {
-                (u16::max_value(), false)
+                (usize::max_value(), false)
             }
         } else {
-            (num_args as u16, true)
+            (num_args as usize, true)
         }
     }
 
-    fn vi_num_args(&mut self) -> u16 {
+    fn vi_num_args(&mut self) -> usize {
         let num_args = self.num_args();
         if num_args < 0 {
             unreachable!()
         } else {
-            num_args.abs() as u16
+            num_args.abs() as usize
         }
     }
 }
diff --git a/src/lib.rs b/src/lib.rs
index 7a70ef16..b087456c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -44,6 +44,7 @@ use std::io::{self, Write};
 use std::mem;
 use std::path::Path;
 use std::result;
+use unicode_segmentation::UnicodeSegmentation;
 use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
 
 use tty::{RawMode, RawReader, Terminal, Term};
@@ -309,7 +310,7 @@ fn calculate_position(s: &str, orig: Position, cols: usize) -> Position {
 }
 
 /// Insert the character `ch` at cursor current position.
-fn edit_insert(s: &mut State, ch: char, n: u16) -> Result<()> {
+fn edit_insert(s: &mut State, ch: char, n: usize) -> Result<()> {
     if let Some(push) = s.line.insert(ch, n) {
         if push {
             if n == 1 && s.cursor.col + ch.width().unwrap_or(0) < s.cols {
@@ -331,9 +332,9 @@ fn edit_insert(s: &mut State, ch: char, n: u16) -> Result<()> {
 }
 
 /// Replace a single (or n) character(s) under the cursor (Vi mode)
-fn edit_replace_char(s: &mut State, ch: char, n: u16) -> Result<()> {
-    let count = s.line.delete(n);
-    if count > 0 {
+fn edit_replace_char(s: &mut State, ch: char, n: usize) -> Result<()> {
+    if let Some(chars) = s.line.delete(n) {
+        let count = chars.graphemes(true).count();
         s.line.insert(ch, count);
         s.line.move_left(1);
         s.refresh_line()
@@ -343,7 +344,7 @@ fn edit_replace_char(s: &mut State, ch: char, n: u16) -> Result<()> {
 }
 
 // Yank/paste `text` at current position.
-fn edit_yank(s: &mut State, text: &str, anchor: Anchor, n: u16) -> Result<()> {
+fn edit_yank(s: &mut State, text: &str, anchor: Anchor, n: usize) -> Result<()> {
     if s.line.yank(text, anchor, n).is_some() {
         s.refresh_line()
     } else {
@@ -358,7 +359,7 @@ fn edit_yank_pop(s: &mut State, yank_size: usize, text: &str) -> Result<()> {
 }
 
 /// Move cursor on the left.
-fn edit_move_left(s: &mut State, n: u16) -> Result<()> {
+fn edit_move_left(s: &mut State, n: usize) -> Result<()> {
     if s.line.move_left(n) {
         s.refresh_line()
     } else {
@@ -367,7 +368,7 @@ fn edit_move_left(s: &mut State, n: u16) -> Result<()> {
 }
 
 /// Move cursor on the right.
-fn edit_move_right(s: &mut State, n: u16) -> Result<()> {
+fn edit_move_right(s: &mut State, n: usize) -> Result<()> {
     if s.line.move_right(n) {
         s.refresh_line()
     } else {
@@ -395,8 +396,8 @@ fn edit_move_end(s: &mut State) -> Result<()> {
 
 /// 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, n: u16) -> Result<()> {
-    if s.line.delete(n) > 0 {
+fn edit_delete(s: &mut State, n: usize) -> Result<()> {
+    if s.line.delete(n).is_some() {
         s.refresh_line()
     } else {
         Ok(())
@@ -404,8 +405,8 @@ fn edit_delete(s: &mut State, n: u16) -> Result<()> {
 }
 
 /// Backspace implementation.
-fn edit_backspace(s: &mut State, n: u16) -> Result<()> {
-    if s.line.backspace(n) {
+fn edit_backspace(s: &mut State, n: usize) -> Result<()> {
+    if s.line.backspace(n).is_some() {
         s.refresh_line()
     } else {
         Ok(())
@@ -441,7 +442,7 @@ fn edit_transpose_chars(s: &mut State) -> Result<()> {
     }
 }
 
-fn edit_move_to_prev_word(s: &mut State, word_def: Word, n: u16) -> Result<()> {
+fn edit_move_to_prev_word(s: &mut State, word_def: Word, n: usize) -> Result<()> {
     if s.line.move_to_prev_word(word_def, n) {
         s.refresh_line()
     } else {
@@ -451,7 +452,7 @@ fn edit_move_to_prev_word(s: &mut State, word_def: Word, n: u16) -> Result<()> {
 
 /// Delete the previous word, maintaining the cursor at the start of the
 /// current word.
-fn edit_delete_prev_word(s: &mut State, word_def: Word, n: u16) -> Result<Option<String>> {
+fn edit_delete_prev_word(s: &mut State, word_def: Word, n: usize) -> Result<Option<String>> {
     if let Some(text) = s.line.delete_prev_word(word_def, n) {
         try!(s.refresh_line());
         Ok(Some(text))
@@ -460,7 +461,7 @@ fn edit_delete_prev_word(s: &mut State, word_def: Word, n: u16) -> Result<Option
     }
 }
 
-fn edit_move_to_next_word(s: &mut State, at: At, word_def: Word, n: u16) -> Result<()> {
+fn edit_move_to_next_word(s: &mut State, at: At, word_def: Word, n: usize) -> Result<()> {
     if s.line.move_to_next_word(at, word_def, n) {
         s.refresh_line()
     } else {
@@ -468,7 +469,7 @@ fn edit_move_to_next_word(s: &mut State, at: At, word_def: Word, n: u16) -> Resu
     }
 }
 
-fn edit_move_to(s: &mut State, cs: CharSearch, n: u16) -> Result<()> {
+fn edit_move_to(s: &mut State, cs: CharSearch, n: usize) -> Result<()> {
     if s.line.move_to(cs, n) {
         s.refresh_line()
     } else {
@@ -477,7 +478,7 @@ fn edit_move_to(s: &mut State, cs: CharSearch, n: u16) -> Result<()> {
 }
 
 /// Kill from the cursor to the end of the current word, or, if between words, to the end of the next word.
-fn edit_delete_word(s: &mut State, at: At, word_def: Word, n: u16) -> Result<Option<String>> {
+fn edit_delete_word(s: &mut State, at: At, word_def: Word, n: usize) -> Result<Option<String>> {
     if let Some(text) = s.line.delete_word(at, word_def, n) {
         try!(s.refresh_line());
         Ok(Some(text))
@@ -486,7 +487,7 @@ fn edit_delete_word(s: &mut State, at: At, word_def: Word, n: u16) -> Result<Opt
     }
 }
 
-fn edit_delete_to(s: &mut State, cs: CharSearch, n: u16) -> Result<Option<String>> {
+fn edit_delete_to(s: &mut State, cs: CharSearch, n: usize) -> Result<Option<String>> {
     if let Some(text) = s.line.delete_to(cs, n) {
         try!(s.refresh_line());
         Ok(Some(text))
diff --git a/src/line_buffer.rs b/src/line_buffer.rs
index 5fc2bbb8..46da50a4 100644
--- a/src/line_buffer.rs
+++ b/src/line_buffer.rs
@@ -1,6 +1,7 @@
 //! Line buffer with current cursor position
 use std::iter;
 use std::ops::{Deref, Range};
+use unicode_segmentation::UnicodeSegmentation;
 use keymap::{Anchor, At, CharSearch, Word};
 
 /// Maximum buffer size for the line read
@@ -96,21 +97,36 @@ impl LineBuffer {
             self.buf[self.pos..].chars().next()
         }
     }
-    /// Returns the character just before the current cursor position.
-    fn char_before_cursor(&self) -> Option<char> {
+
+    fn next_pos(&self, n: usize) -> Option<usize> {
+        if self.pos == self.buf.len() {
+            return None;
+        }
+        self.buf[self.pos..]
+            .grapheme_indices(true)
+            .take(n)
+            .last()
+            .map(|(i, s)| i + self.pos + s.len())
+    }
+    /// Returns the position of the character just before the current cursor position.
+    fn prev_pos(&self, n: usize) -> Option<usize> {
         if self.pos == 0 {
-            None
-        } else {
-            self.buf[..self.pos].chars().next_back()
+            return None;
         }
+        self.buf[..self.pos]
+            .grapheme_indices(true)
+            .rev()
+            .take(n)
+            .last()
+            .map(|(i, _)| i)
     }
 
     /// Insert the character `ch` at current cursor position
     /// and advance cursor position accordingly.
     /// Return `None` when maximum buffer size has been reached,
     /// `true` when the character has been appended to the end of the line.
-    pub fn insert(&mut self, ch: char, n: u16) -> Option<bool> {
-        let shift = ch.len_utf8() * n as usize;
+    pub fn insert(&mut self, ch: char, n: usize) -> Option<bool> {
+        let shift = ch.len_utf8() * n;
         if self.buf.len() + shift > self.buf.capacity() {
             return None;
         }
@@ -123,7 +139,7 @@ impl LineBuffer {
         } else if n == 1 {
             self.buf.insert(self.pos, ch);
         } else {
-            let text = iter::repeat(ch).take(n as usize).collect::<String>();
+            let text = iter::repeat(ch).take(n).collect::<String>();
             let pos = self.pos;
             self.insert_str(pos, &text);
         }
@@ -134,8 +150,8 @@ impl LineBuffer {
     /// Yank/paste `text` at current position.
     /// Return `None` when maximum buffer size has been reached,
     /// `true` when the character has been appended to the end of the line.
-    pub fn yank(&mut self, text: &str, anchor: Anchor, n: u16) -> Option<bool> {
-        let shift = text.len() * n as usize;
+    pub fn yank(&mut self, text: &str, anchor: Anchor, n: usize) -> Option<bool> {
+        let shift = text.len() * n;
         if text.is_empty() || (self.buf.len() + shift) > self.buf.capacity() {
             return None;
         }
@@ -149,7 +165,7 @@ impl LineBuffer {
                 self.buf.push_str(text);
             }
         } else {
-            let text = iter::repeat(text).take(n as usize).collect::<String>();
+            let text = iter::repeat(text).take(n).collect::<String>();
             let pos = self.pos;
             self.insert_str(pos, &text);
         }
@@ -165,31 +181,25 @@ impl LineBuffer {
     }
 
     /// Move cursor on the left.
-    pub fn move_left(&mut self, n: u16) -> bool {
-        let mut moved = false;
-        for _ in 0..n {
-            if let Some(ch) = self.char_before_cursor() {
-                self.pos -= ch.len_utf8();
-                moved = true
-            } else {
-                break;
+    pub fn move_left(&mut self, n: usize) -> bool {
+        match self.prev_pos(n) {
+            Some(pos) => {
+                self.pos = pos;
+                true
             }
+            None => false,
         }
-        moved
     }
 
     /// Move cursor on the right.
-    pub fn move_right(&mut self, n: u16) -> bool {
-        let mut moved = false;
-        for _ in 0..n {
-            if let Some(ch) = self.char_at_cursor() {
-                self.pos += ch.len_utf8();
-                moved = true
-            } else {
-                break;
+    pub fn move_right(&mut self, n: usize) -> bool {
+        match self.next_pos(n) {
+            Some(pos) => {
+                self.pos = pos;
+                true
             }
+            None => false,
         }
-        moved
     }
 
     /// Move cursor to the start of the line.
@@ -215,33 +225,27 @@ impl LineBuffer {
     /// Delete the character at the right of the cursor without altering the cursor
     /// position. Basically this is what happens with the "Delete" keyboard key.
     /// Return the number of characters deleted.
-    pub fn delete(&mut self, n: u16) -> u16 {
-        let mut count = 0;
-        for _ in 0..n {
-            if !self.buf.is_empty() && self.pos < self.buf.len() {
-                self.buf.remove(self.pos);
-                count += 1
-            } else {
-                break;
+    pub fn delete(&mut self, n: usize) -> Option<String> {
+        match self.next_pos(n) {
+            Some(pos) => {
+                let chars = self.buf.drain(self.pos..pos).collect::<String>();
+                Some(chars)
             }
+            None => None,
         }
-        count
     }
 
     /// Delete the character at the left of the cursor.
     /// Basically that is what happens with the "Backspace" keyboard key.
-    pub fn backspace(&mut self, n: u16) -> bool {
-        let mut deleted = false;
-        for _ in 0..n {
-            if let Some(ch) = self.char_before_cursor() {
-                self.pos -= ch.len_utf8();
-                self.buf.remove(self.pos);
-                deleted = true
-            } else {
-                break;
+    pub fn backspace(&mut self, n: usize) -> Option<String> {
+        match self.prev_pos(n) {
+            Some(pos) => {
+                let chars = self.buf.drain(pos..self.pos).collect::<String>();
+                self.pos = pos;
+                Some(chars)
             }
+            None => None,
         }
-        deleted
     }
 
     /// Kill all characters on the current line.
@@ -279,23 +283,15 @@ impl LineBuffer {
         if self.pos == self.buf.len() {
             self.move_left(1);
         }
-        let ch = self.buf.remove(self.pos);
-        let size = ch.len_utf8();
-        let other_ch = self.char_before_cursor().unwrap();
-        let other_size = other_ch.len_utf8();
-        self.buf.insert(self.pos - other_size, ch);
-        if self.pos != self.buf.len() - size {
-            self.pos += size;
-        } else if size >= other_size {
-            self.pos += size - other_size;
-        } else {
-            self.pos -= other_size - size;
-        }
+        let chars = self.delete(1).unwrap();
+        self.move_left(1);
+        self.yank(&chars, Anchor::Before, 1);
+        self.move_right(1);
         true
     }
 
     /// Go left until start of word
-    fn prev_word_pos(&self, pos: usize, word_def: Word, n: u16) -> Option<usize> {
+    fn prev_word_pos(&self, pos: usize, word_def: Word, n: usize) -> Option<usize> {
         if pos == 0 {
             return None;
         }
@@ -326,7 +322,7 @@ impl LineBuffer {
     }
 
     /// Moves the cursor to the beginning of previous word.
-    pub fn move_to_prev_word(&mut self, word_def: Word, n: u16) -> bool {
+    pub fn move_to_prev_word(&mut self, word_def: Word, n: usize) -> bool {
         if let Some(pos) = self.prev_word_pos(self.pos, word_def, n) {
             self.pos = pos;
             true
@@ -337,7 +333,7 @@ impl LineBuffer {
 
     /// Delete the previous word, maintaining the cursor at the start of the
     /// current word.
-    pub fn delete_prev_word(&mut self, word_def: Word, n: u16) -> Option<String> {
+    pub fn delete_prev_word(&mut self, word_def: Word, n: usize) -> Option<String> {
         if let Some(pos) = self.prev_word_pos(self.pos, word_def, n) {
             let word = self.buf.drain(pos..self.pos).collect();
             self.pos = pos;
@@ -347,10 +343,10 @@ impl LineBuffer {
         }
     }
 
-    fn next_pos(&self, pos: usize, at: At, word_def: Word, n: u16) -> Option<usize> {
+    fn next_word_pos(&self, pos: usize, at: At, word_def: Word, n: usize) -> Option<usize> {
         match at {
             At::End => {
-                match self.next_word_pos(pos, word_def, n) {
+                match self.next_end_of_word_pos(pos, word_def, n) {
                     Some((_, end)) => Some(end),
                     _ => None,
                 }
@@ -360,7 +356,7 @@ impl LineBuffer {
     }
 
     /// Go right until start of word
-    fn next_start_of_word_pos(&self, pos: usize, word_def: Word, n: u16) -> Option<usize> {
+    fn next_start_of_word_pos(&self, pos: usize, word_def: Word, n: usize) -> Option<usize> {
         if pos == self.buf.len() {
             return None;
         }
@@ -390,7 +386,7 @@ impl LineBuffer {
 
     /// Go right until end of word
     /// Returns the position (start, end) of the next word.
-    fn next_word_pos(&self, pos: usize, word_def: Word, n: u16) -> Option<(usize, usize)> {
+    fn next_end_of_word_pos(&self, pos: usize, word_def: Word, n: usize) -> Option<(usize, usize)> {
         if pos == self.buf.len() {
             return None;
         }
@@ -421,8 +417,8 @@ impl LineBuffer {
     }
 
     /// Moves the cursor to the end of next word.
-    pub fn move_to_next_word(&mut self, at: At, word_def: Word, n: u16) -> bool {
-        if let Some(pos) = self.next_pos(self.pos, at, word_def, n) {
+    pub fn move_to_next_word(&mut self, at: At, word_def: Word, n: usize) -> bool {
+        if let Some(pos) = self.next_word_pos(self.pos, at, word_def, n) {
             self.pos = pos;
             true
         } else {
@@ -430,7 +426,7 @@ impl LineBuffer {
         }
     }
 
-    fn search_char_pos(&mut self, cs: &CharSearch, n: u16) -> Option<usize> {
+    fn search_char_pos(&mut self, cs: &CharSearch, n: usize) -> Option<usize> {
         let mut shift = 0;
         let search_result = match *cs {
             CharSearch::Backward(c) |
@@ -439,7 +435,7 @@ impl LineBuffer {
                     .char_indices()
                     .rev()
                     .filter(|&(_, ch)| ch == c)
-                    .nth(n as usize - 1)
+                    .nth(n - 1)
                     .map(|(i, _)| i)
             }
             CharSearch::Forward(c) |
@@ -450,7 +446,7 @@ impl LineBuffer {
                         self.buf[shift..]
                             .char_indices()
                             .filter(|&(_, ch)| ch == c)
-                            .nth(n as usize - 1)
+                            .nth(n - 1)
                             .map(|(i, _)| i)
                     } else {
                         None
@@ -474,7 +470,7 @@ impl LineBuffer {
         }
     }
 
-    pub fn move_to(&mut self, cs: CharSearch, n: u16) -> bool {
+    pub fn move_to(&mut self, cs: CharSearch, n: usize) -> bool {
         if let Some(pos) = self.search_char_pos(&cs, n) {
             self.pos = pos;
             true
@@ -484,8 +480,8 @@ impl LineBuffer {
     }
 
     /// Kill from the cursor to the end of the current word, or, if between words, to the end of the next word.
-    pub fn delete_word(&mut self, at: At, word_def: Word, n: u16) -> Option<String> {
-        if let Some(pos) = self.next_pos(self.pos, at, word_def, n) {
+    pub fn delete_word(&mut self, at: At, word_def: Word, n: usize) -> Option<String> {
+        if let Some(pos) = self.next_word_pos(self.pos, at, word_def, n) {
             let word = self.buf.drain(self.pos..pos).collect();
             Some(word)
         } else {
@@ -493,7 +489,7 @@ impl LineBuffer {
         }
     }
 
-    pub fn delete_to(&mut self, cs: CharSearch, n: u16) -> Option<String> {
+    pub fn delete_to(&mut self, cs: CharSearch, n: usize) -> Option<String> {
         let search_result = match cs {
             CharSearch::ForwardBefore(c) => self.search_char_pos(&CharSearch::Forward(c), n),
             _ => self.search_char_pos(&cs, n),
@@ -517,7 +513,7 @@ impl LineBuffer {
 
     /// Alter the next word.
     pub fn edit_word(&mut self, a: WordAction) -> bool {
-        if let Some((start, end)) = self.next_word_pos(self.pos, Word::Emacs, 1) {
+        if let Some((start, end)) = self.next_end_of_word_pos(self.pos, Word::Emacs, 1) {
             if start == end {
                 return false;
             }
@@ -550,14 +546,14 @@ impl LineBuffer {
         let word_def = Word::Emacs;
         if let Some(start) = self.prev_word_pos(self.pos, word_def, 1) {
             if let Some(prev_start) = self.prev_word_pos(start, word_def, 1) {
-                let (_, prev_end) = self.next_word_pos(prev_start, word_def, 1).unwrap();
+                let (_, prev_end) = self.next_end_of_word_pos(prev_start, word_def, 1).unwrap();
                 if prev_end >= start {
                     return false;
                 }
-                let (_, mut end) = self.next_word_pos(start, word_def, 1).unwrap();
+                let (_, mut end) = self.next_end_of_word_pos(start, word_def, 1).unwrap();
                 if end < self.pos {
                     if self.pos < self.buf.len() {
-                        let (s, _) = self.next_word_pos(self.pos, word_def, 1).unwrap();
+                        let (s, _) = self.next_end_of_word_pos(self.pos, word_def, 1).unwrap();
                         end = s;
                     } else {
                         end = self.pos;
@@ -632,6 +628,30 @@ mod test {
     use keymap::{Anchor, At, CharSearch, Word};
     use super::{LineBuffer, MAX_LINE, WordAction};
 
+    #[test]
+    fn next_pos() {
+        let s = LineBuffer::init("ö̲g̈", 0);
+        assert_eq!(7, s.len());
+        let pos = s.next_pos(1);
+        assert_eq!(Some(4), pos);
+
+        let s = LineBuffer::init("ö̲g̈", 4);
+        let pos = s.next_pos(1);
+        assert_eq!(Some(7), pos);
+    }
+
+    #[test]
+    fn prev_pos() {
+        let s = LineBuffer::init("ö̲g̈", 4);
+        assert_eq!(7, s.len());
+        let pos = s.prev_pos(1);
+        assert_eq!(Some(0), pos);
+
+        let s = LineBuffer::init("ö̲g̈", 7);
+        let pos = s.prev_pos(1);
+        assert_eq!(Some(4), pos);
+    }
+
     #[test]
     fn insert() {
         let mut s = LineBuffer::with_capacity(MAX_LINE);
@@ -694,18 +714,31 @@ mod test {
         assert_eq!(true, ok);
     }
 
+    #[test]
+    fn move_grapheme() {
+        let mut s = LineBuffer::init("ag̈", 4);
+        assert_eq!(4, s.len());
+        let ok = s.move_left(1);
+        assert_eq!(true, ok);
+        assert_eq!(1, s.pos);
+
+        let ok = s.move_right(1);
+        assert_eq!(true, ok);
+        assert_eq!(4, s.pos);
+    }
+
     #[test]
     fn delete() {
         let mut s = LineBuffer::init("αß", 2);
-        let n = s.delete(1);
+        let chars = s.delete(1);
         assert_eq!("α", s.buf);
         assert_eq!(2, s.pos);
-        assert_eq!(1, n);
+        assert_eq!(Some("ß".to_string()), chars);
 
-        let ok = s.backspace(1);
+        let chars = s.backspace(1);
         assert_eq!("", s.buf);
         assert_eq!(0, s.pos);
-        assert_eq!(true, ok);
+        assert_eq!(Some("α".to_string()), chars);
     }
 
     #[test]
@@ -735,14 +768,14 @@ mod test {
         s.pos = 3;
         let ok = s.transpose_chars();
         assert_eq!("acß", s.buf);
-        assert_eq!(2, s.pos);
+        assert_eq!(4, s.pos);
         assert_eq!(true, ok);
 
         s.buf = String::from("aßc");
         s.pos = 4;
         let ok = s.transpose_chars();
         assert_eq!("acß", s.buf);
-        assert_eq!(2, s.pos);
+        assert_eq!(4, s.pos);
         assert_eq!(true, ok);
     }
 
-- 
GitLab