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