Basic undo functionality.

This commit is contained in:
Nathan Vegdahl 2015-01-09 22:02:32 -08:00
parent 0fb338f05b
commit b837d488a5
4 changed files with 1252 additions and 1126 deletions

View File

@ -5,8 +5,6 @@ use std::mem;
use std::str::Graphemes; use std::str::Graphemes;
use string_utils::{grapheme_count, grapheme_pos_to_byte_pos, is_line_ending}; use string_utils::{grapheme_count, grapheme_pos_to_byte_pos, is_line_ending};
const TAB_WIDTH: usize = 4;
/// Returns the visual width of a grapheme given a starting /// Returns the visual width of a grapheme given a starting
/// position on a line. /// position on a line.
@ -690,6 +688,11 @@ impl<'a> Iterator for LineGraphemeVisIter<'a> {
// Line tests // Line tests
//========================================================================= //=========================================================================
mod tests {
use super::{Line, LineEnding, LineGraphemeIter, LineGraphemeVisIter};
const TAB_WIDTH: usize = 4;
#[test] #[test]
fn new_text_line() { fn new_text_line() {
let tl = Line::new(); let tl = Line::new();
@ -934,28 +937,28 @@ fn text_line_split_beginning() {
fn grapheme_index_to_closest_vis_pos_1() { fn grapheme_index_to_closest_vis_pos_1() {
let tl = Line::new_from_str("Hello!"); let tl = Line::new_from_str("Hello!");
assert!(tl.grapheme_index_to_closest_vis_pos(0) == 0); assert!(tl.grapheme_index_to_closest_vis_pos(0, TAB_WIDTH) == 0);
} }
#[test] #[test]
fn grapheme_index_to_closest_vis_pos_2() { fn grapheme_index_to_closest_vis_pos_2() {
let tl = Line::new_from_str("\tHello!"); let tl = Line::new_from_str("\tHello!");
assert!(tl.grapheme_index_to_closest_vis_pos(1) == TAB_WIDTH); assert!(tl.grapheme_index_to_closest_vis_pos(1, TAB_WIDTH) == TAB_WIDTH);
} }
#[test] #[test]
fn vis_pos_to_closest_grapheme_index_1() { fn vis_pos_to_closest_grapheme_index_1() {
let tl = Line::new_from_str("Hello!"); let tl = Line::new_from_str("Hello!");
assert!(tl.vis_pos_to_closest_grapheme_index(0) == 0); assert!(tl.vis_pos_to_closest_grapheme_index(0, TAB_WIDTH) == 0);
} }
#[test] #[test]
fn vis_pos_to_closest_grapheme_index_2() { fn vis_pos_to_closest_grapheme_index_2() {
let tl = Line::new_from_str("\tHello!"); let tl = Line::new_from_str("\tHello!");
assert!(tl.vis_pos_to_closest_grapheme_index(TAB_WIDTH) == 1); assert!(tl.vis_pos_to_closest_grapheme_index(TAB_WIDTH, TAB_WIDTH) == 1);
} }
@ -1035,3 +1038,5 @@ fn text_line_grapheme_iter_at_index_at_lf() {
assert!(iter.next() == Some("\n")); assert!(iter.next() == Some("\n"));
assert!(iter.next() == None); assert!(iter.next() == None);
} }
}

View File

@ -4,7 +4,7 @@ use std::mem;
use self::node::{BufferNode, BufferNodeGraphemeIter, BufferNodeLineIter}; use self::node::{BufferNode, BufferNodeGraphemeIter, BufferNodeLineIter};
use self::line::{Line, LineEnding}; use self::line::{Line, LineEnding};
use string_utils::{is_line_ending}; use string_utils::{is_line_ending, grapheme_count};
pub mod line; pub mod line;
mod node; mod node;
@ -18,6 +18,7 @@ mod node;
pub struct Buffer { pub struct Buffer {
text: BufferNode, text: BufferNode,
pub line_ending_type: LineEnding, pub line_ending_type: LineEnding,
undo_stack: Vec<Operation>,
} }
@ -26,6 +27,7 @@ impl Buffer {
Buffer { Buffer {
text: BufferNode::new(), text: BufferNode::new(),
line_ending_type: LineEnding::LF, line_ending_type: LineEnding::LF,
undo_stack: Vec::new(),
} }
} }
@ -138,12 +140,27 @@ impl Buffer {
/// Insert 'text' at grapheme position 'pos'. /// Insert 'text' at grapheme position 'pos'.
pub fn insert_text(&mut self, text: &str, pos: usize) { pub fn insert_text(&mut self, text: &str, pos: usize) {
self._insert_text(text, pos);
self.undo_stack.push(Operation::InsertText(String::from_str(text), pos));
}
fn _insert_text(&mut self, text: &str, pos: usize) {
self.text.insert_text(text, pos); self.text.insert_text(text, pos);
} }
/// Remove the text between grapheme positions 'pos_a' and 'pos_b'. /// Remove the text between grapheme positions 'pos_a' and 'pos_b'.
pub fn remove_text(&mut self, pos_a: usize, pos_b: usize) { pub fn remove_text(&mut self, pos_a: usize, pos_b: usize) {
let removed_text = self.string_from_range(pos_a, pos_b);
self._remove_text(pos_a, pos_b);
// Push operation to the undo stack
self.undo_stack.push(Operation::RemoveText(removed_text, pos_a));
}
fn _remove_text(&mut self, pos_a: usize, pos_b: usize) {
// Nothing to do // Nothing to do
if pos_a == pos_b { if pos_a == pos_b {
return; return;
@ -171,6 +188,58 @@ impl Buffer {
} }
/// Undoes operations that were pushed to the undo stack, and returns a
/// cursor position that the cursor should jump to, if any.
pub fn undo(&mut self) -> Option<usize> {
if let Some(op) = self.undo_stack.pop() {
match op {
Operation::InsertText(ref s, p) => {
let size = grapheme_count(s.as_slice());
self._remove_text(p, p+size);
return Some(p);
},
Operation::RemoveText(ref s, p) => {
let size = grapheme_count(s.as_slice());
self._insert_text(s.as_slice(), p);
return Some(p+size);
},
}
}
return None;
}
/// Creates a String from the buffer text in grapheme range [pos_a, posb).
fn string_from_range(&self, pos_a: usize, pos_b: usize) -> String {
// Bounds checks
if pos_b < pos_a {
panic!("Buffer::string_from_range(): pos_a must be less than or equal to pos_b.");
}
else if pos_b > self.len() {
panic!("Buffer::string_from_range(): specified range is past end of buffer text.");
}
let mut s = String::with_capacity(pos_b - pos_a);
let mut iter = self.grapheme_iter_at_index(pos_a);
let mut i = 0;
let i_end = pos_b - pos_a;
for g in iter {
if i == i_end {
break;
}
s.push_str(g);
i += 1;
}
return s;
}
/// Creates an iterator at the first character /// Creates an iterator at the first character
pub fn grapheme_iter<'a>(&'a self) -> BufferGraphemeIter<'a> { pub fn grapheme_iter<'a>(&'a self) -> BufferGraphemeIter<'a> {
BufferGraphemeIter { BufferGraphemeIter {
@ -278,11 +347,25 @@ impl<'a> Iterator for BufferLineIter<'a> {
//================================================================
// Buffer undo structures
//================================================================
enum Operation {
InsertText(String, usize),
RemoveText(String, usize),
}
//================================================================ //================================================================
// TESTS // TESTS
//================================================================ //================================================================
mod tests {
use super::{Buffer, Operation, BufferGraphemeIter, BufferLineIter};
#[test] #[test]
fn insert_text() { fn insert_text() {
let mut buf = Buffer::new(); let mut buf = Buffer::new();
@ -1037,6 +1120,28 @@ fn pos_1d_to_closest_2d_2() {
} }
#[test]
fn string_from_range_1() {
let mut buf = Buffer::new();
buf.insert_text("Hi\nthere\npeople\nof\nthe\nworld!", 0);
let s = buf.string_from_range(1, 12);
assert!(s.as_slice() == "i\nthere\npeo");
}
#[test]
fn string_from_range_2() {
let mut buf = Buffer::new();
buf.insert_text("Hi\nthere\npeople\nof\nthe\nworld!", 0);
let s = buf.string_from_range(0, 29);
assert!(s.as_slice() == "Hi\nthere\npeople\nof\nthe\nworld!");
}
#[test] #[test]
fn grapheme_iter_at_index_1() { fn grapheme_iter_at_index_1() {
let mut buf = Buffer::new(); let mut buf = Buffer::new();
@ -1072,5 +1177,5 @@ fn grapheme_iter_at_index_2() {
} }
}

View File

@ -189,6 +189,17 @@ impl Editor {
} }
pub fn undo(&mut self) {
if let Some(pos) = self.buffer.undo() {
self.cursor.range.0 = pos;
self.cursor.range.1 = pos;
self.cursor.update_vis_start(&(self.buffer), self.tab_width);
self.move_view_to_cursor();
}
}
/// Moves the editor's view the minimum amount to show the cursor /// Moves the editor's view the minimum amount to show the cursor
pub fn move_view_to_cursor(&mut self) { pub fn move_view_to_cursor(&mut self) {
let (v, h) = self.buffer.pos_1d_to_closest_vis_2d(self.cursor.range.0, self.tab_width); let (v, h) = self.buffer.pos_1d_to_closest_vis_2d(self.cursor.range.0, self.tab_width);

View File

@ -25,6 +25,7 @@ const K_CTRL_L: u16 = 12;
const K_CTRL_O: u16 = 15; const K_CTRL_O: u16 = 15;
const K_CTRL_Q: u16 = 17; const K_CTRL_Q: u16 = 17;
const K_CTRL_S: u16 = 19; const K_CTRL_S: u16 = 19;
const K_CTRL_Z: u16 = 26;
pub struct TermUI { pub struct TermUI {
@ -98,6 +99,10 @@ impl TermUI {
self.editor.save_if_dirty(); self.editor.save_if_dirty();
}, },
K_CTRL_Z => {
self.editor.undo();
},
K_CTRL_L => { K_CTRL_L => {
self.go_to_line_ui_loop(); self.go_to_line_ui_loop();
}, },