Edit stack (#368)

* Add edit stack

* Use edit stack in the core editor

* Edit stack is just a struct

* Test undo-redo behaviour instead of implementation details
This commit is contained in:
Sherub Thakur 2022-03-29 21:47:51 +05:30 committed by GitHub
parent 0216c84aa2
commit 5240130c9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 157 additions and 67 deletions

View File

@ -0,0 +1,115 @@
#[derive(Debug, PartialEq, Eq)]
pub struct EditStack<T> {
internal_list: Vec<T>,
index: usize,
}
impl<T> EditStack<T> {
pub fn new() -> Self
where
T: Default,
{
EditStack {
internal_list: vec![T::default()],
index: 0,
}
}
}
impl<T> EditStack<T>
where
T: Default,
T: Clone,
T: Send,
{
/// Go back one point in the undo stack. If present on first edit do nothing
pub(super) fn undo(&mut self) -> &T {
self.index = if self.index == 0 { 0 } else { self.index - 1 };
&self.internal_list[self.index]
}
/// Go forward one point in the undo stack. If present on the last edit do nothing
pub(super) fn redo(&mut self) -> &T {
self.index = if self.index == self.internal_list.len() - 1 {
self.index
} else {
self.index + 1
};
&self.internal_list[self.index]
}
/// Insert a new entry to the undo stack.
/// NOTE: (IMP): If we have hit undo a few times then discard all the other values that come
/// after the current point
pub(super) fn insert(&mut self, value: T) {
if self.index < self.internal_list.len() - 1 {
self.internal_list.resize_with(self.index + 1, || {
panic!("Impossible state reached: Bug in UndoStack logic")
});
}
self.internal_list.push(value);
self.index += 1;
}
/// Reset the stack to the initial state
pub(super) fn reset(&mut self) {
self.index = 0;
self.internal_list = vec![T::default()];
}
/// Return the entry currently being pointed to
pub(super) fn current(&mut self) -> &T {
&self.internal_list[self.index]
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;
fn edit_stack<T>(values: &[T], index: usize) -> EditStack<T>
where
T: Clone,
{
EditStack {
internal_list: values.to_vec(),
index,
}
}
#[rstest]
#[case(edit_stack(&[1, 2, 3][..], 2), 2)]
#[case(edit_stack(&[1][..], 0), 1)]
fn undo_works(#[case] stack: EditStack<isize>, #[case] value_after_undo: isize) {
let mut stack = stack;
let value = stack.undo();
assert_eq!(*value, value_after_undo);
}
#[rstest]
#[case(edit_stack(&[1, 2, 3][..], 1), 3)]
#[case(edit_stack(&[1][..], 0), 1)]
fn redo_works(#[case] stack: EditStack<isize>, #[case] value_after_undo: isize) {
let mut stack = stack;
let value = stack.redo();
assert_eq!(*value, value_after_undo);
}
#[rstest]
#[case(edit_stack(&[1, 2, 3][..], 1), 4, edit_stack(&[1, 2, 4], 2))]
#[case(edit_stack(&[1, 2, 3][..], 2), 3, edit_stack(&[1, 2, 3, 3], 3))]
fn insert_works(
#[case] old_stack: EditStack<isize>,
#[case] value_to_insert: isize,
#[case] expected_stack: EditStack<isize>,
) {
let mut stack = old_stack;
stack.insert(value_to_insert);
assert_eq!(stack, expected_stack);
}
}

View File

@ -1,4 +1,4 @@
use super::{Clipboard, ClipboardMode, LineBuffer};
use super::{edit_stack::EditStack, Clipboard, ClipboardMode, LineBuffer};
use crate::{core_editor::get_default_clipboard, EditCommand, UndoBehavior};
/// Stateful editor executing changes to the underlying [`LineBuffer`]
@ -9,8 +9,7 @@ pub struct Editor {
line_buffer: LineBuffer,
cut_buffer: Box<dyn Clipboard>,
edits: Vec<LineBuffer>,
index_undo: usize,
edit_stack: EditStack<LineBuffer>,
}
impl Default for Editor {
@ -18,10 +17,7 @@ impl Default for Editor {
Editor {
line_buffer: LineBuffer::new(),
cut_buffer: Box::new(get_default_clipboard()),
// Note: Using list-zipper we can reduce these to one field
edits: vec![LineBuffer::new()],
index_undo: 2,
edit_stack: EditStack::new(),
}
}
}
@ -155,8 +151,7 @@ impl Editor {
}
pub fn reset_undo_stack(&mut self) {
self.edits = vec![LineBuffer::new()];
self.index_undo = 2;
self.edit_stack.reset();
}
pub fn move_to_start(&mut self) {
@ -176,60 +171,27 @@ impl Editor {
self.line_buffer.move_to_line_end();
}
fn get_index_undo(&self) -> usize {
if let Some(c) = self.edits.len().checked_sub(self.index_undo) {
c
} else {
0
}
}
fn undo(&mut self) {
// NOTE: Try-blocks should help us get rid of this indirection too
self.undo_internal();
let val = self.edit_stack.undo();
self.line_buffer = val.clone();
}
fn redo(&mut self) {
// NOTE: Try-blocks should help us get rid of this indirection too
self.redo_internal();
}
fn redo_internal(&mut self) -> Option<()> {
if self.index_undo > 2 {
self.index_undo = self.index_undo.checked_sub(2)?;
self.undo_internal()
} else {
None
}
}
fn undo_internal(&mut self) -> Option<()> {
self.line_buffer = self.edits.get(self.get_index_undo())?.clone();
if self.index_undo <= self.edits.len() {
self.index_undo = self.index_undo.checked_add(1)?;
}
Some(())
let val = self.edit_stack.redo();
self.line_buffer = val.clone();
}
pub fn remember_undo_state(&mut self, is_after_action: bool) -> Option<()> {
self.reset_index_undo();
if self.edits.len() > 1
&& self.edits.last()?.word_count() == self.line_buffer.word_count()
if self.edit_stack.current().word_count() == self.line_buffer.word_count()
&& !is_after_action
{
self.edits.pop();
self.edit_stack.undo();
}
self.edits.push(self.line_buffer.clone());
self.edit_stack.insert(self.line_buffer.clone());
Some(())
}
fn reset_index_undo(&mut self) {
self.index_undo = 2;
}
fn cut_current_line(&mut self) {
let deletion_range = self.line_buffer.current_line_range();
@ -413,25 +375,37 @@ impl Editor {
mod test {
use super::*;
#[test]
fn test_undo_initial_char() {
fn editor_with(buffer: &str) -> Editor {
let mut editor = Editor::default();
editor.line_buffer().set_buffer(String::from("a"));
editor.remember_undo_state(false);
editor.line_buffer().set_buffer(String::from("ab"));
editor.remember_undo_state(false);
editor.line_buffer().set_buffer(String::from("ab "));
editor.remember_undo_state(false);
editor.line_buffer().set_buffer(String::from("ab c"));
editor.remember_undo_state(true);
editor.line_buffer.set_buffer(buffer.to_string());
editor
}
assert_eq!(
vec![
LineBuffer::from(""),
LineBuffer::from("ab "),
LineBuffer::from("ab c")
],
editor.edits
);
fn str_to_edit_commands(s: &str) -> Vec<EditCommand> {
s.chars().map(|c| EditCommand::InsertChar(c)).collect()
}
#[test]
fn test_undo_works_on_work_boundries() {
let mut editor = editor_with("This is a");
for cmd in str_to_edit_commands(" test") {
editor.run_edit_command(&cmd);
}
assert_eq!(editor.get_buffer(), "This is a test");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "This is a ");
}
#[test]
fn test_redo_works_on_word_boundries() {
let mut editor = editor_with("This is a");
for cmd in str_to_edit_commands(" test") {
editor.run_edit_command(&cmd);
}
assert_eq!(editor.get_buffer(), "This is a test");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "This is a ");
editor.run_edit_command(&EditCommand::Redo);
assert_eq!(editor.get_buffer(), "This is a test");
}
}

View File

@ -1,4 +1,5 @@
mod clip_buffer;
mod edit_stack;
mod editor;
mod line_buffer;