| //! Undo API |
| use std::fmt::Debug; |
| |
| use keymap::RepeatCount; |
| use line_buffer::{ChangeListener, DeleteListener, Direction, LineBuffer}; |
| use unicode_segmentation::UnicodeSegmentation; |
| |
| enum Change { |
| Begin, |
| End, |
| Insert { |
| idx: usize, |
| text: String, |
| }, // QuotedInsert, SelfInsert, Yank |
| Delete { |
| idx: usize, |
| text: String, |
| }, /* BackwardDeleteChar, BackwardKillWord, DeleteChar, |
| * KillLine, KillWholeLine, KillWord, |
| * UnixLikeDiscard, ViDeleteTo */ |
| Replace { |
| idx: usize, |
| old: String, |
| new: String, |
| }, /* CapitalizeWord, Complete, DowncaseWord, Replace, TransposeChars, TransposeWords, |
| * UpcaseWord, YankPop */ |
| } |
| |
| impl Change { |
| fn undo(&self, line: &mut LineBuffer) { |
| match *self { |
| Change::Begin | Change::End => { |
| unreachable!(); |
| } |
| Change::Insert { idx, ref text } => { |
| line.delete_range(idx..idx + text.len()); |
| } |
| Change::Delete { idx, ref text } => { |
| line.insert_str(idx, text); |
| line.set_pos(idx + text.len()); |
| } |
| Change::Replace { |
| idx, |
| ref old, |
| ref new, |
| } => { |
| line.replace(idx..idx + new.len(), old); |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| fn redo(&self, line: &mut LineBuffer) { |
| match *self { |
| Change::Begin | Change::End => { |
| unreachable!(); |
| } |
| Change::Insert { idx, ref text } => { |
| line.insert_str(idx, text); |
| } |
| Change::Delete { idx, ref text } => { |
| line.delete_range(idx..idx + text.len()); |
| } |
| Change::Replace { |
| idx, |
| ref old, |
| ref new, |
| } => { |
| line.replace(idx..idx + old.len(), new); |
| } |
| } |
| } |
| |
| fn insert_seq(&self, indx: usize) -> bool { |
| if let Change::Insert { idx, ref text } = *self { |
| idx + text.len() == indx |
| } else { |
| false |
| } |
| } |
| |
| fn delete_seq(&self, indx: usize, len: usize) -> bool { |
| if let Change::Delete { idx, .. } = *self { |
| // delete or backspace |
| idx == indx || idx == indx + len |
| } else { |
| false |
| } |
| } |
| |
| fn replace_seq(&self, indx: usize) -> bool { |
| if let Change::Replace { idx, ref new, .. } = *self { |
| idx + new.len() == indx |
| } else { |
| false |
| } |
| } |
| } |
| |
| pub struct Changeset { |
| undo_group_level: u32, |
| undos: Vec<Change>, // undoable changes |
| redos: Vec<Change>, // undone changes, redoable |
| } |
| |
| impl Changeset { |
| pub fn new() -> Changeset { |
| Changeset { |
| undo_group_level: 0, |
| undos: Vec::new(), |
| redos: Vec::new(), |
| } |
| } |
| |
| pub fn begin(&mut self) -> usize { |
| debug!(target: "rustyline", "Changeset::begin"); |
| self.redos.clear(); |
| let mark = self.undos.len(); |
| self.undos.push(Change::Begin); |
| self.undo_group_level += 1; |
| mark |
| } |
| |
| pub fn end(&mut self) { |
| debug!(target: "rustyline", "Changeset::end"); |
| self.redos.clear(); |
| while self.undo_group_level > 0 { |
| self.undo_group_level -= 1; |
| if let Some(&Change::Begin) = self.undos.last() { |
| // emtpy Begin..End |
| self.undos.pop(); |
| } else { |
| self.undos.push(Change::End); |
| } |
| } |
| } |
| |
| fn insert_char(idx: usize, c: char) -> Change { |
| let mut text = String::new(); |
| text.push(c); |
| Change::Insert { idx, text } |
| } |
| |
| pub fn insert(&mut self, idx: usize, c: char) { |
| debug!(target: "rustyline", "Changeset::insert({}, {:?})", idx, c); |
| self.redos.clear(); |
| if !c.is_alphanumeric() || !self.undos.last().map_or(false, |lc| lc.insert_seq(idx)) { |
| self.undos.push(Self::insert_char(idx, c)); |
| return; |
| } |
| // merge consecutive char insertions when char is alphanumeric |
| let mut last_change = self.undos.pop().unwrap(); |
| if let Change::Insert { ref mut text, .. } = last_change { |
| text.push(c); |
| } else { |
| unreachable!(); |
| } |
| self.undos.push(last_change); |
| } |
| |
| pub fn insert_str<S: AsRef<str> + Into<String> + Debug>(&mut self, idx: usize, string: S) { |
| debug!(target: "rustyline", "Changeset::insert_str({}, {:?})", idx, string); |
| self.redos.clear(); |
| if string.as_ref().is_empty() { |
| return; |
| } |
| self.undos.push(Change::Insert { |
| idx, |
| text: string.into(), |
| }); |
| } |
| |
| pub fn delete<S: AsRef<str> + Into<String> + Debug>(&mut self, indx: usize, string: S) { |
| debug!(target: "rustyline", "Changeset::delete({}, {:?})", indx, string); |
| self.redos.clear(); |
| if string.as_ref().is_empty() { |
| return; |
| } |
| |
| if !Self::single_char(string.as_ref()) |
| || !self |
| .undos |
| .last() |
| .map_or(false, |lc| lc.delete_seq(indx, string.as_ref().len())) |
| { |
| self.undos.push(Change::Delete { |
| idx: indx, |
| text: string.into(), |
| }); |
| return; |
| } |
| // merge consecutive char deletions when char is alphanumeric |
| let mut last_change = self.undos.pop().unwrap(); |
| if let Change::Delete { |
| ref mut idx, |
| ref mut text, |
| } = last_change |
| { |
| if *idx == indx { |
| text.push_str(string.as_ref()); |
| } else { |
| text.insert_str(0, string.as_ref()); |
| *idx = indx; |
| } |
| } else { |
| unreachable!(); |
| } |
| self.undos.push(last_change); |
| } |
| |
| fn single_char(s: &str) -> bool { |
| let mut graphemes = s.graphemes(true); |
| graphemes.next().map_or(false, |grapheme| { |
| grapheme.chars().all(|c| c.is_alphanumeric()) |
| }) && graphemes.next().is_none() |
| } |
| |
| pub fn replace<S: AsRef<str> + Into<String> + Debug>(&mut self, indx: usize, old_: S, new_: S) { |
| debug!(target: "rustyline", "Changeset::replace({}, {:?}, {:?})", indx, old_, new_); |
| self.redos.clear(); |
| |
| if !self.undos.last().map_or(false, |lc| lc.replace_seq(indx)) { |
| self.undos.push(Change::Replace { |
| idx: indx, |
| old: old_.into(), |
| new: new_.into(), |
| }); |
| return; |
| } |
| |
| // merge consecutive char replacements |
| let mut last_change = self.undos.pop().unwrap(); |
| if let Change::Replace { |
| ref mut old, |
| ref mut new, |
| .. |
| } = last_change |
| { |
| old.push_str(old_.as_ref()); |
| new.push_str(new_.as_ref()); |
| } else { |
| unreachable!(); |
| } |
| self.undos.push(last_change); |
| } |
| |
| pub fn undo(&mut self, line: &mut LineBuffer, n: RepeatCount) -> bool { |
| debug!(target: "rustyline", "Changeset::undo"); |
| let mut count = 0; |
| let mut waiting_for_begin = 0; |
| let mut undone = false; |
| loop { |
| if let Some(change) = self.undos.pop() { |
| match change { |
| Change::Begin => { |
| waiting_for_begin -= 1; |
| } |
| Change::End => { |
| waiting_for_begin += 1; |
| } |
| _ => { |
| change.undo(line); |
| undone = true; |
| } |
| }; |
| self.redos.push(change); |
| } else { |
| break; |
| } |
| if waiting_for_begin <= 0 { |
| count += 1; |
| if count >= n { |
| break; |
| } |
| } |
| } |
| undone |
| } |
| |
| pub fn truncate(&mut self, len: usize) { |
| debug!(target: "rustyline", "Changeset::truncate({})", len); |
| self.undos.truncate(len); |
| } |
| |
| #[cfg(test)] |
| pub fn redo(&mut self, line: &mut LineBuffer) -> bool { |
| let mut waiting_for_end = 0; |
| let mut redone = false; |
| loop { |
| if let Some(change) = self.redos.pop() { |
| match change { |
| Change::Begin => { |
| waiting_for_end += 1; |
| } |
| Change::End => { |
| waiting_for_end -= 1; |
| } |
| _ => { |
| change.redo(line); |
| redone = true; |
| } |
| }; |
| self.undos.push(change); |
| } else { |
| break; |
| } |
| if waiting_for_end <= 0 { |
| break; |
| } |
| } |
| redone |
| } |
| |
| pub fn last_insert(&self) -> Option<String> { |
| for change in self.undos.iter().rev() { |
| match change { |
| Change::Insert { ref text, .. } => return Some(text.to_owned()), |
| Change::Replace { ref new, .. } => return Some(new.to_owned()), |
| Change::End => { |
| continue; |
| } |
| _ => { |
| return None; |
| } |
| } |
| } |
| None |
| } |
| } |
| |
| impl DeleteListener for Changeset { |
| fn start_killing(&mut self) {} |
| fn delete(&mut self, idx: usize, string: &str, _: Direction) { |
| self.delete(idx, string); |
| } |
| fn stop_killing(&mut self) {} |
| } |
| impl ChangeListener for Changeset { |
| fn insert_char(&mut self, idx: usize, c: char) { |
| self.insert(idx, c); |
| } |
| fn insert_str(&mut self, idx: usize, string: &str) { |
| self.insert_str(idx, string); |
| } |
| fn replace(&mut self, idx: usize, old: &str, new: &str) { |
| self.replace(idx, old, new); |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::Changeset; |
| use line_buffer::LineBuffer; |
| |
| #[test] |
| fn test_insert_chars() { |
| let mut cs = Changeset::new(); |
| cs.insert(0, 'H'); |
| cs.insert(1, 'i'); |
| assert_eq!(1, cs.undos.len()); |
| assert_eq!(0, cs.redos.len()); |
| cs.insert(0, ' '); |
| assert_eq!(2, cs.undos.len()); |
| } |
| |
| #[test] |
| fn test_insert_strings() { |
| let mut cs = Changeset::new(); |
| cs.insert_str(0, "Hello"); |
| cs.insert_str(5, ", "); |
| assert_eq!(2, cs.undos.len()); |
| assert_eq!(0, cs.redos.len()); |
| } |
| |
| #[test] |
| fn test_undo_insert() { |
| let mut buf = LineBuffer::init("", 0, None); |
| buf.insert_str(0, "Hello"); |
| buf.insert_str(5, ", world!"); |
| let mut cs = Changeset::new(); |
| assert_eq!(buf.as_str(), "Hello, world!"); |
| |
| cs.insert_str(5, ", world!"); |
| |
| cs.undo(&mut buf, 1); |
| assert_eq!(0, cs.undos.len()); |
| assert_eq!(1, cs.redos.len()); |
| assert_eq!(buf.as_str(), "Hello"); |
| |
| cs.redo(&mut buf); |
| assert_eq!(1, cs.undos.len()); |
| assert_eq!(0, cs.redos.len()); |
| assert_eq!(buf.as_str(), "Hello, world!"); |
| } |
| |
| #[test] |
| fn test_undo_delete() { |
| let mut buf = LineBuffer::init("", 0, None); |
| buf.insert_str(0, "Hello"); |
| let mut cs = Changeset::new(); |
| assert_eq!(buf.as_str(), "Hello"); |
| |
| cs.delete(5, ", world!"); |
| |
| cs.undo(&mut buf, 1); |
| assert_eq!(buf.as_str(), "Hello, world!"); |
| |
| cs.redo(&mut buf); |
| assert_eq!(buf.as_str(), "Hello"); |
| } |
| |
| #[test] |
| fn test_delete_chars() { |
| let mut buf = LineBuffer::init("", 0, None); |
| buf.insert_str(0, "Hlo"); |
| |
| let mut cs = Changeset::new(); |
| cs.delete(1, "e"); |
| cs.delete(1, "l"); |
| assert_eq!(1, cs.undos.len()); |
| |
| cs.undo(&mut buf, 1); |
| assert_eq!(buf.as_str(), "Hello"); |
| } |
| |
| #[test] |
| fn test_backspace_chars() { |
| let mut buf = LineBuffer::init("", 0, None); |
| buf.insert_str(0, "Hlo"); |
| |
| let mut cs = Changeset::new(); |
| cs.delete(2, "l"); |
| cs.delete(1, "e"); |
| assert_eq!(1, cs.undos.len()); |
| |
| cs.undo(&mut buf, 1); |
| assert_eq!(buf.as_str(), "Hello"); |
| } |
| |
| #[test] |
| fn test_undo_replace() { |
| let mut buf = LineBuffer::init("", 0, None); |
| buf.insert_str(0, "Hello, world!"); |
| let mut cs = Changeset::new(); |
| assert_eq!(buf.as_str(), "Hello, world!"); |
| |
| buf.replace(1..5, "i"); |
| assert_eq!(buf.as_str(), "Hi, world!"); |
| cs.replace(1, "ello", "i"); |
| |
| cs.undo(&mut buf, 1); |
| assert_eq!(buf.as_str(), "Hello, world!"); |
| |
| cs.redo(&mut buf); |
| assert_eq!(buf.as_str(), "Hi, world!"); |
| } |
| |
| #[test] |
| fn test_last_insert() { |
| let mut cs = Changeset::new(); |
| cs.begin(); |
| cs.delete(0, "Hello"); |
| cs.insert_str(0, "Bye"); |
| cs.end(); |
| let insert = cs.last_insert(); |
| assert_eq!(Some("Bye".to_owned()), insert); |
| } |
| } |