mirror of
https://github.com/nushell/reedline.git
synced 2024-09-11 15:55:50 +03:00
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:
parent
0216c84aa2
commit
5240130c9f
115
src/core_editor/edit_stack.rs
Normal file
115
src/core_editor/edit_stack.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
mod clip_buffer;
|
||||
mod edit_stack;
|
||||
mod editor;
|
||||
mod line_buffer;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user