From 174255535dccb0fea5e2d0e055fc5884830df898 Mon Sep 17 00:00:00 2001 From: bnprks Date: Sun, 14 Aug 2022 15:09:06 -0700 Subject: [PATCH] Improve undo functionality (#460) * Delete unused code in `CircularCompletionHandler` I could not find anywhere that the `Reedline::ActionHandler` event is actually created, except for a bit of deserialization code in nushell (reedline_config.rs:817). I suspect its functionality is no longer needed in the current version of reedline. * Improve word-level undo and completions undo * Adds completions and history searches to the undo stack This required changing all the menu interfaces to do their edits through the `Editor` struct, so it can track undo state * Improves the system of coalescing word-level edits on the undo stack This involved expanding the `UndoBehavior` system. Now, every edit gets tagged with `UndoBehavior`, and by comparing the `UndoBehavior` of the current and previous edit, we can decide whether these changes should be grouped together on the undo stack. Chains of repeated backspace, delete, insertion, or history naviagition can each be grouped together to form a single undo entry when appropriate. * Removing low-usage wrapper methods from editor.rs Removes LineBuffer wrappers from Editor wherever the wrapper is used only once, or only internally to the `editor.rs` file. * Run cargo fmt * Fixed undo coalescing for backspace/delete with \n The logic was a bit trickier than I thought. Added some tests to confirm the intended behavior where adding/deleting newlines creates additional undo points * Remove debugging logging code * Revert "Removing low-usage wrapper methods from editor.rs" - Also revert reintroduction of `CutFromStart` bug in vi mode 8b5e70fc0ef3131227e1af8824ae2749851d204b * Fix undo coalescing for CRLF deletions * Ensure proper undo tracking in public Editor fns Delete or make private all Editor functions that modify the line buffer without tracking UndoBehavior * Make Editor crate level pub Added documentation for pub methods on Editor. Kept the public API smaller by making many convenience methods pub(crate) if they can be implemented via the pub methods * Cleanup changes * word_starts and word_ends functions no longer needed with current word-level undo tracking algorithm * `undo` and `redo` don't need to call `update_undo_behavior` because they are private methods and `run_edit_command` already calls `update_undo_behavior` * Fix for new clippy warning * Editor+LineBuffer API updates for nushell merge * Make `LineBuffer::replace_range` pub rather than pub(crate) * Make `Editor::set_line_buffer` and `Editor::set_buffer` pub(crate) in favor of the new `Editor::edit_buffer`. I believe the `edit_buffer` change is a better API to expose for `Menu` implementations, as it avoids the need for `clones` while enforcing proper undo tracking. --- src/completion/circular.rs | 154 ------------------- src/completion/mod.rs | 2 - src/core_editor/editor.rs | 267 ++++++++++++++++++++++----------- src/core_editor/line_buffer.rs | 35 ++--- src/engine.rs | 61 +++----- src/enums.rs | 93 +++++++++--- src/lib.rs | 1 + src/menu/columnar_menu.rs | 53 ++++--- src/menu/list_menu.rs | 38 +++-- src/menu/mod.rs | 59 ++++---- 10 files changed, 370 insertions(+), 393 deletions(-) delete mode 100644 src/completion/circular.rs diff --git a/src/completion/circular.rs b/src/completion/circular.rs deleted file mode 100644 index 8132ffc..0000000 --- a/src/completion/circular.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::{core_editor::LineBuffer, Completer}; - -/// A simple handler that will do a cycle-based rotation through the options given by the Completer -pub struct CircularCompletionHandler { - initial_line: LineBuffer, - index: usize, - - last_buffer: Option, -} - -impl Default for CircularCompletionHandler { - fn default() -> Self { - CircularCompletionHandler { - initial_line: LineBuffer::new(), - index: 0, - last_buffer: None, - } - } -} - -impl CircularCompletionHandler { - fn reset_index(&mut self) { - self.index = 0; - } - // With this function we handle the tab events. - // - // If completions vector is not empty we proceed to replace - // in the line_buffer only the specified range of characters. - // If internal index is 0 it means that is the first tab event pressed. - // If internal index is greater than completions vector, we bring it back to 0. - pub(crate) fn handle( - &mut self, - completer: &mut dyn Completer, - present_buffer: &mut LineBuffer, - ) { - if let Some(last_buffer) = &self.last_buffer { - if last_buffer != present_buffer { - self.reset_index(); - } - } - - // NOTE: This is required to cycle through the tabs for what is presently present in the - // buffer. Without this `repetitive_calls_to_handle_works` will not work - if self.index == 0 { - self.initial_line = present_buffer.clone(); - } else { - *present_buffer = self.initial_line.clone(); - } - - let completions = completer.complete( - present_buffer.get_buffer(), - present_buffer.insertion_point(), - ); - - if !completions.is_empty() { - match self.index { - index if index < completions.len() => { - self.index += 1; - let span = completions[index].span; - - let mut offset = present_buffer.insertion_point(); - offset += completions[index].value.len() - (span.end - span.start); - - // TODO improve the support for multiline replace - present_buffer.replace(span.start..span.end, &completions[index].value); - present_buffer.set_insertion_point(offset); - } - _ => { - self.reset_index(); - } - } - } - self.last_buffer = Some(present_buffer.clone()); - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::DefaultCompleter; - use pretty_assertions::assert_eq; - - fn get_completer(values: Vec<&'_ str>) -> DefaultCompleter { - let mut completer = DefaultCompleter::default(); - completer.insert(values.iter().map(|s| s.to_string()).collect()); - - completer - } - - fn buffer_with(content: &str) -> LineBuffer { - let mut line_buffer = LineBuffer::new(); - line_buffer.insert_str(content); - - line_buffer - } - - #[test] - fn repetitive_calls_to_handle_works() { - let mut tab = CircularCompletionHandler::default(); - let mut comp = get_completer(vec!["login", "logout"]); - let mut buf = buffer_with("lo"); - tab.handle(&mut comp, &mut buf); - - assert_eq!(buf, buffer_with("login")); - tab.handle(&mut comp, &mut buf); - assert_eq!(buf, buffer_with("logout")); - tab.handle(&mut comp, &mut buf); - assert_eq!(buf, buffer_with("lo")); - } - - #[test] - fn behaviour_with_hyphens_and_underscores() { - let mut tab = CircularCompletionHandler::default(); - let mut comp = get_completer(vec!["test-hyphen", "test_underscore"]); - let mut buf = buffer_with("te"); - tab.handle(&mut comp, &mut buf); - - assert_eq!(buf, buffer_with("test")); - tab.handle(&mut comp, &mut buf); - assert_eq!(buf, buffer_with("te")); - } - - #[test] - fn auto_resets_on_new_query() { - let mut tab = CircularCompletionHandler::default(); - let mut comp = get_completer(vec!["login", "logout", "exit"]); - let mut buf = buffer_with("log"); - tab.handle(&mut comp, &mut buf); - - assert_eq!(buf, buffer_with("login")); - let mut new_buf = buffer_with("ex"); - tab.handle(&mut comp, &mut new_buf); - assert_eq!(new_buf, buffer_with("exit")); - } - - #[test] - fn same_string_different_places() { - let mut tab = CircularCompletionHandler::default(); - let mut comp = get_completer(vec!["that", "this"]); - let mut buf = buffer_with("th is my test th"); - - // Hitting tab after `th` fills the first completion `that` - buf.set_insertion_point(2); - tab.handle(&mut comp, &mut buf); - let mut expected_buffer = buffer_with("that is my test th"); - expected_buffer.set_insertion_point(4); - assert_eq!(buf, expected_buffer); - - // updating the cursor to end should reset the completions - buf.set_insertion_point(18); - tab.handle(&mut comp, &mut buf); - assert_eq!(buf, buffer_with("that is my test that")); - } -} diff --git a/src/completion/mod.rs b/src/completion/mod.rs index 0f6eef9..10f196e 100644 --- a/src/completion/mod.rs +++ b/src/completion/mod.rs @@ -1,8 +1,6 @@ mod base; -mod circular; mod default; pub(crate) mod history; pub use base::{Completer, Span, Suggestion}; -pub use circular::CircularCompletionHandler; pub use default::DefaultCompleter; diff --git a/src/core_editor/editor.rs b/src/core_editor/editor.rs index 75b013b..cb2a875 100644 --- a/src/core_editor/editor.rs +++ b/src/core_editor/editor.rs @@ -1,5 +1,6 @@ use super::{edit_stack::EditStack, Clipboard, ClipboardMode, LineBuffer}; -use crate::{core_editor::get_default_clipboard, EditCommand, UndoBehavior}; +use crate::enums::{EditType, UndoBehavior}; +use crate::{core_editor::get_default_clipboard, EditCommand}; /// Stateful editor executing changes to the underlying [`LineBuffer`] /// @@ -10,6 +11,7 @@ pub struct Editor { cut_buffer: Box, edit_stack: EditStack, + last_undo_behavior: UndoBehavior, } impl Default for Editor { @@ -18,23 +20,25 @@ impl Default for Editor { line_buffer: LineBuffer::new(), cut_buffer: Box::new(get_default_clipboard()), edit_stack: EditStack::new(), + last_undo_behavior: UndoBehavior::CreateUndoPoint, } } } impl Editor { - pub fn line_buffer_immut(&self) -> &LineBuffer { + /// Get the current LineBuffer + pub fn line_buffer(&self) -> &LineBuffer { &self.line_buffer } - pub fn line_buffer(&mut self) -> &mut LineBuffer { - &mut self.line_buffer - } - pub fn set_line_buffer(&mut self, line_buffer: LineBuffer) { + /// Set the current LineBuffer. + /// Undo behavior specifies how this change should be reflected on the undo stack. + pub(crate) fn set_line_buffer(&mut self, line_buffer: LineBuffer, undo_behavior: UndoBehavior) { self.line_buffer = line_buffer; + self.update_undo_state(undo_behavior); } - pub fn run_edit_command(&mut self, command: &EditCommand) { + pub(crate) fn run_edit_command(&mut self, command: &EditCommand) { match command { EditCommand::MoveToStart => self.line_buffer.move_to_start(), EditCommand::MoveToLineStart => self.line_buffer.move_to_line_start(), @@ -50,7 +54,7 @@ impl Editor { EditCommand::MoveBigWordRightStart => self.line_buffer.move_big_word_right_start(), EditCommand::MoveWordRightEnd => self.line_buffer.move_word_right_end(), EditCommand::MoveBigWordRightEnd => self.line_buffer.move_big_word_right_end(), - EditCommand::InsertChar(c) => self.insert_char(*c), + EditCommand::InsertChar(c) => self.line_buffer.insert_char(*c), EditCommand::InsertString(str) => self.line_buffer.insert_str(str), EditCommand::InsertNewline => self.line_buffer.insert_newline(), EditCommand::ReplaceChar(chr) => self.replace_char(*chr), @@ -92,99 +96,98 @@ impl Editor { EditCommand::MoveLeftUntil(c) => self.move_left_until_char(*c, false, true), EditCommand::MoveLeftBefore(c) => self.move_left_until_char(*c, true, true), } - match command.undo_behavior() { - UndoBehavior::Ignore => {} - UndoBehavior::Full => { - self.remember_undo_state(true); + + let new_undo_behavior = match (command, command.edit_type()) { + (_, EditType::MoveCursor) => UndoBehavior::MoveCursor, + (EditCommand::InsertChar(c), EditType::EditText) => UndoBehavior::InsertCharacter(*c), + (EditCommand::Delete, EditType::EditText) => { + let deleted_char = self.edit_stack.current().grapheme_right().chars().next(); + UndoBehavior::Delete(deleted_char) } - UndoBehavior::Coalesce => { - self.remember_undo_state(false); + (EditCommand::Backspace, EditType::EditText) => { + let deleted_char = self.edit_stack.current().grapheme_left().chars().next(); + UndoBehavior::Backspace(deleted_char) } - } + (_, EditType::UndoRedo) => UndoBehavior::UndoRedo, + (_, _) => UndoBehavior::CreateUndoPoint, + }; + self.update_undo_state(new_undo_behavior); } - pub fn move_line_up(&mut self) { + pub(crate) fn move_line_up(&mut self) { self.line_buffer.move_line_up(); + self.update_undo_state(UndoBehavior::MoveCursor); } - pub fn move_line_down(&mut self) { + pub(crate) fn move_line_down(&mut self) { self.line_buffer.move_line_down(); + self.update_undo_state(UndoBehavior::MoveCursor); } - pub fn insert_char(&mut self, c: char) { - self.line_buffer.insert_char(c); - } - - /// Directly change the cursor position measured in bytes in the buffer - /// - /// ## Unicode safety: - /// Not checked, inproper use may cause panics in following operations - pub(crate) fn set_insertion_point(&mut self, pos: usize) { - self.line_buffer.set_insertion_point(pos); - } - + /// Get the text of the current LineBuffer pub fn get_buffer(&self) -> &str { self.line_buffer.get_buffer() } - pub fn set_buffer(&mut self, buffer: String) { - self.line_buffer.set_buffer(buffer); - } - - pub fn clear_to_end(&mut self) { - self.line_buffer.clear_to_end(); - } - - fn clear_to_insertion_point(&mut self) { - self.line_buffer.clear_to_insertion_point(); - } - - fn clear_range(&mut self, range: R) + /// Edit the line buffer in an undo-safe manner. + pub fn edit_buffer(&mut self, func: F, undo_behavior: UndoBehavior) where - R: std::ops::RangeBounds, + F: FnOnce(&mut LineBuffer), { - self.line_buffer.clear_range(range); + self.update_undo_state(undo_behavior); + func(&mut self.line_buffer); } - pub fn insertion_point(&self) -> usize { + /// Set the text of the current LineBuffer given the specified UndoBehavior + /// Insertion point update to the end of the buffer. + pub(crate) fn set_buffer(&mut self, buffer: String, undo_behavior: UndoBehavior) { + self.line_buffer.set_buffer(buffer); + self.update_undo_state(undo_behavior); + } + + pub(crate) fn insertion_point(&self) -> usize { self.line_buffer.insertion_point() } - pub fn is_empty(&self) -> bool { + pub(crate) fn is_empty(&self) -> bool { self.line_buffer.is_empty() } - pub fn is_cursor_at_first_line(&self) -> bool { + pub(crate) fn is_cursor_at_first_line(&self) -> bool { self.line_buffer.is_cursor_at_first_line() } - pub fn is_cursor_at_last_line(&self) -> bool { + pub(crate) fn is_cursor_at_last_line(&self) -> bool { self.line_buffer.is_cursor_at_last_line() } - pub fn is_cursor_at_buffer_end(&self) -> bool { + pub(crate) fn is_cursor_at_buffer_end(&self) -> bool { self.line_buffer.insertion_point() == self.get_buffer().len() } - pub fn reset_undo_stack(&mut self) { + pub(crate) fn reset_undo_stack(&mut self) { self.edit_stack.reset(); } - pub fn move_to_start(&mut self) { + pub(crate) fn move_to_start(&mut self, undo_behavior: UndoBehavior) { self.line_buffer.move_to_start(); + self.update_undo_state(undo_behavior); } - pub fn move_to_end(&mut self) { + pub(crate) fn move_to_end(&mut self, undo_behavior: UndoBehavior) { self.line_buffer.move_to_end(); + self.update_undo_state(undo_behavior); } #[allow(dead_code)] - pub fn move_to_line_start(&mut self) { + pub(crate) fn move_to_line_start(&mut self, undo_behavior: UndoBehavior) { self.line_buffer.move_to_line_start(); + self.update_undo_state(undo_behavior); } - pub fn move_to_line_end(&mut self) { + pub(crate) fn move_to_line_end(&mut self, undo_behavior: UndoBehavior) { self.line_buffer.move_to_line_end(); + self.update_undo_state(undo_behavior); } fn undo(&mut self) { @@ -197,13 +200,16 @@ impl Editor { self.line_buffer = val.clone(); } - pub fn remember_undo_state(&mut self, is_after_action: bool) { - if self.edit_stack.current().word_count() == self.line_buffer.word_count() - && !is_after_action - { + fn update_undo_state(&mut self, undo_behavior: UndoBehavior) { + if matches!(undo_behavior, UndoBehavior::UndoRedo) { + self.last_undo_behavior = UndoBehavior::UndoRedo; + return; + } + if !undo_behavior.create_undo_point_after(&self.last_undo_behavior) { self.edit_stack.undo(); } self.edit_stack.insert(self.line_buffer.clone()); + self.last_undo_behavior = undo_behavior; } fn cut_current_line(&mut self) { @@ -212,8 +218,8 @@ impl Editor { let cut_slice = &self.line_buffer.get_buffer()[deletion_range.clone()]; if !cut_slice.is_empty() { self.cut_buffer.set(cut_slice, ClipboardMode::Lines); - self.set_insertion_point(deletion_range.start); - self.clear_range(deletion_range); + self.line_buffer.set_insertion_point(deletion_range.start); + self.line_buffer.clear_range(deletion_range); } } @@ -224,7 +230,7 @@ impl Editor { &self.line_buffer.get_buffer()[..insertion_offset], ClipboardMode::Normal, ); - self.clear_to_insertion_point(); + self.line_buffer.clear_to_insertion_point(); } } @@ -239,11 +245,11 @@ impl Editor { } } - pub fn cut_from_end(&mut self) { + fn cut_from_end(&mut self) { let cut_slice = &self.line_buffer.get_buffer()[self.line_buffer.insertion_point()..]; if !cut_slice.is_empty() { self.cut_buffer.set(cut_slice, ClipboardMode::Normal); - self.clear_to_end(); + self.line_buffer.clear_to_end(); } } @@ -265,7 +271,7 @@ impl Editor { &self.line_buffer.get_buffer()[cut_range.clone()], ClipboardMode::Normal, ); - self.clear_range(cut_range); + self.line_buffer.clear_range(cut_range); self.line_buffer.set_insertion_point(left_index); } } @@ -279,7 +285,7 @@ impl Editor { &self.line_buffer.get_buffer()[cut_range.clone()], ClipboardMode::Normal, ); - self.clear_range(cut_range); + self.line_buffer.clear_range(cut_range); self.line_buffer.set_insertion_point(left_index); } } @@ -293,7 +299,7 @@ impl Editor { &self.line_buffer.get_buffer()[cut_range.clone()], ClipboardMode::Normal, ); - self.clear_range(cut_range); + self.line_buffer.clear_range(cut_range); } } @@ -306,7 +312,7 @@ impl Editor { &self.line_buffer.get_buffer()[cut_range.clone()], ClipboardMode::Normal, ); - self.clear_range(cut_range); + self.line_buffer.clear_range(cut_range); } } @@ -319,7 +325,7 @@ impl Editor { &self.line_buffer.get_buffer()[cut_range.clone()], ClipboardMode::Normal, ); - self.clear_range(cut_range); + self.line_buffer.clear_range(cut_range); } } @@ -332,7 +338,7 @@ impl Editor { &self.line_buffer.get_buffer()[cut_range.clone()], ClipboardMode::Normal, ); - self.clear_range(cut_range); + self.line_buffer.clear_range(cut_range); } } @@ -345,7 +351,7 @@ impl Editor { &self.line_buffer.get_buffer()[cut_range.clone()], ClipboardMode::Normal, ); - self.clear_range(cut_range); + self.line_buffer.clear_range(cut_range); } } @@ -465,7 +471,7 @@ mod test { fn editor_with(buffer: &str) -> Editor { let mut editor = Editor::default(); - editor.line_buffer.set_buffer(buffer.to_string()); + editor.set_buffer(buffer.to_string(), UndoBehavior::CreateUndoPoint); editor } @@ -475,7 +481,7 @@ mod test { #[case("abc def.ghi", 11, "abc ")] fn test_cut_word_left(#[case] input: &str, #[case] position: usize, #[case] expected: &str) { let mut editor = editor_with(input); - editor.set_insertion_point(position); + editor.line_buffer.set_insertion_point(position); editor.cut_word_left(); @@ -492,7 +498,7 @@ mod test { #[case] expected: &str, ) { let mut editor = editor_with(input); - editor.set_insertion_point(position); + editor.line_buffer.set_insertion_point(position); editor.cut_big_word_left(); @@ -511,7 +517,7 @@ mod test { #[case] expected: &str, ) { let mut editor = editor_with(input); - editor.set_insertion_point(position); + editor.line_buffer.set_insertion_point(position); editor.replace_char(replacement); @@ -523,26 +529,119 @@ mod test { } #[test] - fn test_undo_works_on_work_boundries() { - let mut editor = editor_with("This is a"); + fn test_undo_insert_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"); + assert_eq!(editor.get_buffer(), "This is a test"); editor.run_edit_command(&EditCommand::Undo); - assert_eq!(editor.get_buffer(), "This is a "); + assert_eq!(editor.get_buffer(), "This is a"); + editor.run_edit_command(&EditCommand::Redo); + assert_eq!(editor.get_buffer(), "This is a test"); } #[test] - fn test_redo_works_on_word_boundries() { + fn test_undo_backspace_works_on_word_boundaries() { + let mut editor = editor_with("This is a test"); + for _ in 0..6 { + editor.run_edit_command(&EditCommand::Backspace); + } + assert_eq!(editor.get_buffer(), "This is "); + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(editor.get_buffer(), "This is a"); + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(editor.get_buffer(), "This is a test"); + } + + #[test] + fn test_undo_delete_works_on_word_boundaries() { + let mut editor = editor_with("This is a test"); + editor.line_buffer.set_insertion_point(0); + for _ in 0..7 { + editor.run_edit_command(&EditCommand::Delete); + } + assert_eq!(editor.get_buffer(), "s a test"); + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(editor.get_buffer(), "is a test"); + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(editor.get_buffer(), "This is a test"); + } + + #[test] + fn test_undo_insert_with_newline() { let mut editor = editor_with("This is a"); - for cmd in str_to_edit_commands(" test") { + for cmd in str_to_edit_commands(" \n test") { editor.run_edit_command(&cmd); } - assert_eq!(editor.get_buffer(), "This is a test"); + assert_eq!(editor.get_buffer(), "This is a \n 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"); + assert_eq!(editor.get_buffer(), "This is a \n"); + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(editor.get_buffer(), "This is a"); + } + + #[test] + fn test_undo_backspace_with_newline() { + let mut editor = editor_with("This is a \n test"); + for _ in 0..8 { + editor.run_edit_command(&EditCommand::Backspace); + } + assert_eq!(editor.get_buffer(), "This is "); + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(editor.get_buffer(), "This is a"); + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(editor.get_buffer(), "This is a \n"); + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(editor.get_buffer(), "This is a \n test"); + } + + #[test] + fn test_undo_backspace_with_crlf() { + let mut editor = editor_with("This is a \r\n test"); + for _ in 0..8 { + editor.run_edit_command(&EditCommand::Backspace); + } + assert_eq!(editor.get_buffer(), "This is "); + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(editor.get_buffer(), "This is a"); + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(editor.get_buffer(), "This is a \r\n"); + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(editor.get_buffer(), "This is a \r\n test"); + } + + #[test] + fn test_undo_delete_with_newline() { + let mut editor = editor_with("This \n is a test"); + editor.line_buffer.set_insertion_point(0); + for _ in 0..8 { + editor.run_edit_command(&EditCommand::Delete); + } + assert_eq!(editor.get_buffer(), "s a test"); + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(editor.get_buffer(), "is a test"); + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(editor.get_buffer(), "\n is a test"); + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(editor.get_buffer(), "This \n is a test"); + } + + #[test] + fn test_undo_delete_with_crlf() { + // CLRF delete is a special case, since the first character of the + // grapheme is \r rather than \n + let mut editor = editor_with("This \r\n is a test"); + editor.line_buffer.set_insertion_point(0); + for _ in 0..8 { + editor.run_edit_command(&EditCommand::Delete); + } + assert_eq!(editor.get_buffer(), "s a test"); + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(editor.get_buffer(), "is a test"); + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(editor.get_buffer(), "\r\n is a test"); + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(editor.get_buffer(), "This \r\n is a test"); } } diff --git a/src/core_editor/line_buffer.rs b/src/core_editor/line_buffer.rs index a7c46ec..f5db2aa 100644 --- a/src/core_editor/line_buffer.rs +++ b/src/core_editor/line_buffer.rs @@ -25,11 +25,6 @@ impl LineBuffer { Self::default() } - /// Replaces the content between [`start`..`end`] with `text` - pub fn replace(&mut self, range: Range, text: &str) { - self.lines.replace_range(range, text); - } - /// Check to see if the line buffer is empty pub fn is_empty(&self) -> bool { self.lines.is_empty() @@ -71,6 +66,8 @@ impl LineBuffer { } /// Sets the current edit position + /// ## Unicode safety: + /// Not checked, inproper use may cause panics in following operations pub fn set_insertion_point(&mut self, offset: usize) { self.insertion_point = offset; } @@ -406,7 +403,7 @@ impl LineBuffer { /// Substitute text covered by `range` in the current line /// /// Safety: Does not change the insertion point/offset and is thus not unicode safe! - pub(crate) fn replace_range(&mut self, range: R, replace_with: &str) + pub fn replace_range(&mut self, range: R, replace_with: &str) where R: std::ops::RangeBounds, { @@ -422,6 +419,16 @@ impl LineBuffer { .unwrap_or(false) } + /// Get the grapheme immediately to the right of the cursor, if any + pub fn grapheme_right(&self) -> &str { + &self.lines[self.insertion_point..self.grapheme_right_index()] + } + + /// Get the grapheme immediately to the left of the cursor, if any + pub fn grapheme_left(&self) -> &str { + &self.lines[self.grapheme_left_index()..self.insertion_point] + } + /// Gets the range of the word the current edit position is pointing to pub fn current_word_range(&self) -> Range { let right_index = self.word_right_index(); @@ -489,11 +496,6 @@ impl LineBuffer { } } - /// Counts the number of words in the buffer - pub fn word_count(&self) -> usize { - self.lines.split_whitespace().count() - } - /// Capitalize the character at insertion point (or the first character /// following the whitespace at the insertion point) and move the insertion /// point right one grapheme. @@ -901,17 +903,6 @@ mod test { line_buffer.assert_valid(); } - #[rstest] - #[case("This is a te", 4)] - #[case("This is a test", 4)] - #[case("This is a test", 4)] - fn word_count_works(#[case] input: &str, #[case] expected_count: usize) { - let line_buffer = buffer_with(input); - - assert_eq!(expected_count, line_buffer.word_count()); - line_buffer.assert_valid(); - } - #[rstest] #[case("This is a test", 13, "This is a tesT", 14)] #[case("This is a test", 10, "This is a Test", 11)] diff --git a/src/engine.rs b/src/engine.rs index 747a05c..3e59751 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -7,7 +7,7 @@ use crate::{ use crate::result::{ReedlineError, ReedlineErrorVariants}; use { crate::{ - completion::{CircularCompletionHandler, Completer, DefaultCompleter}, + completion::{Completer, DefaultCompleter}, core_editor::Editor, edit_mode::{EditMode, Emacs}, enums::{EventStatus, ReedlineEvent}, @@ -21,7 +21,7 @@ use { prompt::{PromptEditMode, PromptHistorySearchStatus}, utils::text_manipulation, EditCommand, ExampleHighlighter, Highlighter, LineBuffer, Menu, MenuEvent, Prompt, - PromptHistorySearch, ReedlineMenu, Signal, ValidationResult, Validator, + PromptHistorySearch, ReedlineMenu, Signal, UndoBehavior, ValidationResult, Validator, }, crossterm::{ event, @@ -104,9 +104,6 @@ pub struct Reedline { quick_completions: bool, partial_completions: bool, - // Performs bash style circular rotation through the available completions - circular_completion_handler: CircularCompletionHandler, - // Highlight the edit buffer highlighter: Box, @@ -163,7 +160,6 @@ impl Reedline { completer, quick_completions: false, partial_completions: false, - circular_completion_handler: CircularCompletionHandler::default(), highlighter: buffer_highlighter, hinter, hide_hints: false, @@ -578,8 +574,8 @@ impl Reedline { } ReedlineEvent::Enter | ReedlineEvent::HistoryHintComplete => { if let Some(string) = self.history_cursor.string_at_cursor() { - self.editor.set_buffer(string); - self.editor.remember_undo_state(true); + self.editor + .set_buffer(string, UndoBehavior::CreateUndoPoint); } self.input_mode = InputMode::Regular; @@ -627,7 +623,6 @@ impl Reedline { // TODO: Check if events should be handled ReedlineEvent::Right | ReedlineEvent::Left - | ReedlineEvent::ActionHandler | ReedlineEvent::Multiple(_) | ReedlineEvent::None | ReedlineEvent::HistoryHintWordComplete @@ -658,7 +653,7 @@ impl Reedline { if self.quick_completions && menu.can_quick_complete() { menu.update_values( - self.editor.line_buffer(), + &mut self.editor, self.completer.as_mut(), self.history.as_ref(), ); @@ -671,7 +666,7 @@ impl Reedline { if self.partial_completions && menu.can_partially_complete( self.quick_completions, - self.editor.line_buffer(), + &mut self.editor, self.completer.as_mut(), self.history.as_ref(), ) @@ -768,12 +763,6 @@ impl Reedline { } Ok(EventStatus::Inapplicable) } - ReedlineEvent::ActionHandler => { - let line_buffer = self.editor.line_buffer(); - self.circular_completion_handler - .handle(self.completer.as_mut(), line_buffer); - Ok(EventStatus::Handled) - } ReedlineEvent::Esc => { self.deactivate_menus(); Ok(EventStatus::Handled) @@ -806,7 +795,7 @@ impl Reedline { ReedlineEvent::Enter => { for menu in self.menus.iter_mut() { if menu.is_active() { - menu.replace_in_buffer(self.editor.line_buffer()); + menu.replace_in_buffer(&mut self.editor); menu.menu_event(MenuEvent::Deactivate); return Ok(EventStatus::Handled); @@ -859,7 +848,7 @@ impl Reedline { if self.quick_completions && menu.can_quick_complete() { menu.menu_event(MenuEvent::Edit(self.quick_completions)); menu.update_values( - self.editor.line_buffer(), + &mut self.editor, self.completer.as_mut(), self.history.as_ref(), ); @@ -912,9 +901,6 @@ impl Reedline { Ok(EventStatus::Handled) } ReedlineEvent::SearchHistory => { - // Make sure we are able to undo the result of a reverse history search - self.editor.remember_undo_state(true); - self.enter_history_search(); Ok(EventStatus::Handled) } @@ -980,8 +966,9 @@ impl Reedline { .back(self.history.as_ref()) .expect("todo: error handling"); self.update_buffer_from_history(); - self.editor.move_to_start(); - self.editor.move_to_line_end(); + self.editor.move_to_start(UndoBehavior::HistoryNavigation); + self.editor + .move_to_line_end(UndoBehavior::HistoryNavigation); } fn next_history(&mut self) { @@ -995,7 +982,7 @@ impl Reedline { .forward(self.history.as_ref()) .expect("todo: error handling"); self.update_buffer_from_history(); - self.editor.move_to_end(); + self.editor.move_to_end(UndoBehavior::HistoryNavigation); } /// Enable the search and navigation through the history from the line buffer prompt @@ -1006,7 +993,7 @@ impl Reedline { // Perform bash-style basic up/down entry walking HistoryNavigationQuery::Normal( // Hack: Tight coupling point to be able to restore previously typed input - self.editor.line_buffer_immut().clone(), + self.editor.line_buffer().clone(), ) } else { // Prefix search like found in fish, zsh, etc. @@ -1078,20 +1065,21 @@ impl Reedline { match self.history_cursor.get_navigation() { HistoryNavigationQuery::Normal(original) => { if let Some(buffer_to_paint) = self.history_cursor.string_at_cursor() { - self.editor.set_buffer(buffer_to_paint.clone()); - self.editor.set_insertion_point(buffer_to_paint.len()); + self.editor + .set_buffer(buffer_to_paint, UndoBehavior::HistoryNavigation); } else { // Hack - self.editor.set_line_buffer(original); + self.editor + .set_line_buffer(original, UndoBehavior::HistoryNavigation); } } HistoryNavigationQuery::PrefixSearch(prefix) => { if let Some(prefix_result) = self.history_cursor.string_at_cursor() { - self.editor.set_buffer(prefix_result.clone()); - self.editor.set_insertion_point(prefix_result.len()); + self.editor + .set_buffer(prefix_result, UndoBehavior::HistoryNavigation); } else { - self.editor.set_buffer(prefix.clone()); - self.editor.set_insertion_point(prefix.len()); + self.editor + .set_buffer(prefix, UndoBehavior::HistoryNavigation); } } HistoryNavigationQuery::SubstringSearch(_) => todo!(), @@ -1106,7 +1094,8 @@ impl Reedline { HistoryNavigationQuery::Normal(_) ) { if let Some(string) = self.history_cursor.string_at_cursor() { - self.editor.set_buffer(string); + self.editor + .set_buffer(string, UndoBehavior::HistoryNavigation); } } self.input_mode = InputMode::Regular; @@ -1273,7 +1262,7 @@ impl Reedline { let res = std::fs::read_to_string(temp_file)?; let res = res.trim_end().to_string(); - self.editor.line_buffer().set_buffer(res); + self.editor.set_buffer(res, UndoBehavior::CreateUndoPoint); Ok(()) } @@ -1369,7 +1358,7 @@ impl Reedline { for menu in self.menus.iter_mut() { if menu.is_active() { menu.update_working_details( - self.editor.line_buffer(), + &mut self.editor, self.completer.as_mut(), self.history.as_ref(), &self.painter, diff --git a/src/enums.rs b/src/enums.rs index bf2625a..7741f36 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -252,7 +252,7 @@ impl Display for EditCommand { impl EditCommand { /// Determine if a certain operation should be undoable /// or if the operations should be coalesced for undoing - pub fn undo_behavior(&self) -> UndoBehavior { + pub fn edit_type(&self) -> EditType { match self { // Cursor moves EditCommand::MoveToStart @@ -272,13 +272,11 @@ impl EditCommand { | EditCommand::MoveRightUntil(_) | EditCommand::MoveRightBefore(_) | EditCommand::MoveLeftUntil(_) - | EditCommand::MoveLeftBefore(_) => UndoBehavior::Full, + | EditCommand::MoveLeftBefore(_) => EditType::MoveCursor, - // Coalesceable insert - EditCommand::InsertChar(_) => UndoBehavior::Coalesce, - - // Full edits - EditCommand::Backspace + // Text edits + EditCommand::InsertChar(_) + | EditCommand::Backspace | EditCommand::Delete | EditCommand::CutChar | EditCommand::InsertString(_) @@ -311,25 +309,76 @@ impl EditCommand { | EditCommand::CutRightUntil(_) | EditCommand::CutRightBefore(_) | EditCommand::CutLeftUntil(_) - | EditCommand::CutLeftBefore(_) => UndoBehavior::Full, + | EditCommand::CutLeftBefore(_) => EditType::EditText, - EditCommand::Undo | EditCommand::Redo => UndoBehavior::Ignore, + EditCommand::Undo | EditCommand::Redo => EditType::UndoRedo, } } } -/// Specifies how the (previously executed) operation should be treated in the Undo stack. +/// Specifies the types of edit commands, used to simplify grouping edits +/// to mark undo behavior +#[derive(PartialEq, Eq)] +pub enum EditType { + /// Cursor movement commands + MoveCursor, + /// Undo/Redo commands + UndoRedo, + /// Text editing commands + EditText, +} + +/// Every line change should come with an UndoBehavior tag, which can be used to +/// calculate how the change should be reflected on the undo stack +#[derive(Debug)] pub enum UndoBehavior { - /// Operation is not affecting the LineBuffers content and should be ignored - /// - /// e.g. the undo commands themselves are not stored in the undo stack - Ignore, - /// The operation is one logical unit of work that should be stored in the undo stack - Full, - /// The operation is a single operation that should be best coalesced in logical units such as words - /// - /// e.g. insertion of characters by typing - Coalesce, + /// Character insertion, tracking the character inserted + InsertCharacter(char), + /// Backspace command, tracking the deleted character (left of cursor) + /// Warning: this does not track the whole grapheme, just the character + Backspace(Option), + /// Delete command, tracking the deleted character (right of cursor) + /// Warning: this does not track the whole grapheme, just the character + Delete(Option), + /// Move the cursor position + MoveCursor, + /// Navigated the history using up or down arrows + HistoryNavigation, + /// Catch-all for actions that should always form a unique undo point and never be + /// grouped with later edits + CreateUndoPoint, + /// Undo/Redo actions shouldn't be reflected on the edit stack + UndoRedo, +} + +impl UndoBehavior { + /// Return if the current operation should start a new undo set, or be + /// combined with the previous operation + pub fn create_undo_point_after(&self, previous: &UndoBehavior) -> bool { + use UndoBehavior as UB; + match (previous, self) { + // Never start an undo set with cursor movement + (_, UB::MoveCursor) => false, + (UB::HistoryNavigation, UB::HistoryNavigation) => false, + // When inserting/deleting repeatedly, each undo set should encompass + // inserting/deleting a complete word and the associated whitespace + (UB::InsertCharacter(c_prev), UB::InsertCharacter(c_new)) => { + (*c_prev == '\n' || *c_prev == '\r') + || (!c_prev.is_whitespace() && c_new.is_whitespace()) + } + (UB::Backspace(Some(c_prev)), UB::Backspace(Some(c_new))) => { + (*c_new == '\n' || *c_new == '\r') + || (c_prev.is_whitespace() && !c_new.is_whitespace()) + } + (UB::Backspace(_), UB::Backspace(_)) => false, + (UB::Delete(Some(c_prev)), UB::Delete(Some(c_new))) => { + (*c_new == '\n' || *c_new == '\r') + || (c_prev.is_whitespace() && !c_new.is_whitespace()) + } + (UB::Delete(_), UB::Delete(_)) => false, + (_, _) => true, + } + } } /// Reedline supported actions. @@ -344,9 +393,6 @@ pub enum ReedlineEvent { /// Complete a single token/word of the history hint HistoryHintWordComplete, - /// Action event - ActionHandler, - /// Handle EndOfLine event /// /// Expected Behavior: @@ -462,7 +508,6 @@ impl Display for ReedlineEvent { ReedlineEvent::None => write!(f, "None"), ReedlineEvent::HistoryHintComplete => write!(f, "HistoryHintComplete"), ReedlineEvent::HistoryHintWordComplete => write!(f, "HistoryHintWordComplete"), - ReedlineEvent::ActionHandler => write!(f, "ActionHandler"), ReedlineEvent::CtrlD => write!(f, "CtrlD"), ReedlineEvent::CtrlC => write!(f, "CtrlC"), ReedlineEvent::ClearScreen => write!(f, "ClearScreen"), diff --git a/src/lib.rs b/src/lib.rs index da4e4c1..f8e371d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -205,6 +205,7 @@ #![warn(missing_docs)] // #![deny(warnings)] mod core_editor; +pub use core_editor::Editor; pub use core_editor::LineBuffer; mod enums; diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index c71f21b..f3d1b6a 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -1,6 +1,7 @@ use super::{menu_functions::find_common_string, Menu, MenuEvent, MenuTextStyle}; use crate::{ - menu_functions::string_difference, painting::Painter, Completer, LineBuffer, Suggestion, + core_editor::Editor, menu_functions::string_difference, painting::Painter, Completer, + Suggestion, UndoBehavior, }; use nu_ansi_term::{ansi::RESET, Style}; @@ -464,13 +465,13 @@ impl Menu for ColumnarMenu { fn can_partially_complete( &mut self, values_updated: bool, - line_buffer: &mut LineBuffer, + editor: &mut Editor, completer: &mut dyn Completer, ) -> bool { // If the values were already updated (e.g. quick completions are true) // there is no need to update the values from the menu if !values_updated { - self.update_values(line_buffer, completer); + self.update_values(editor, completer); } let values = self.get_values(); @@ -479,11 +480,11 @@ impl Menu for ColumnarMenu { let matching = &value[0..index]; // make sure that the partial completion does not overwrite user entered input - let extends_input = - matching.starts_with(&line_buffer.get_buffer()[span.start..span.end]); + let extends_input = matching.starts_with(&editor.get_buffer()[span.start..span.end]); if !matching.is_empty() && extends_input { - line_buffer.replace(span.start..span.end, matching); + let mut line_buffer = editor.line_buffer().clone(); + line_buffer.replace_range(span.start..span.end, matching); let offset = if matching.len() < (span.end - span.start) { line_buffer @@ -494,10 +495,11 @@ impl Menu for ColumnarMenu { }; line_buffer.set_insertion_point(offset); + editor.set_line_buffer(line_buffer, UndoBehavior::CreateUndoPoint); // The values need to be updated because the spans need to be // recalculated for accurate replacement in the string - self.update_values(line_buffer, completer); + self.update_values(editor, completer); true } else { @@ -523,10 +525,10 @@ impl Menu for ColumnarMenu { } /// Updates menu values - fn update_values(&mut self, line_buffer: &mut LineBuffer, completer: &mut dyn Completer) { + fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) { if self.only_buffer_difference { if let Some(old_string) = &self.input { - let (start, input) = string_difference(line_buffer.get_buffer(), old_string); + let (start, input) = string_difference(editor.get_buffer(), old_string); if !input.is_empty() { self.values = completer.complete(input, start); self.reset_position(); @@ -538,9 +540,8 @@ impl Menu for ColumnarMenu { // editing a multiline buffer. // Also, by replacing the new line character with a space, the insert // position is maintain in the line buffer. - let trimmed_buffer = line_buffer.get_buffer().replace('\n', " "); - self.values = - completer.complete(trimmed_buffer.as_str(), line_buffer.insertion_point()); + let trimmed_buffer = editor.get_buffer().replace('\n', " "); + self.values = completer.complete(trimmed_buffer.as_str(), editor.insertion_point()); self.reset_position(); } } @@ -549,7 +550,7 @@ impl Menu for ColumnarMenu { /// collected from the completer fn update_working_details( &mut self, - line_buffer: &mut LineBuffer, + editor: &mut Editor, completer: &mut dyn Completer, painter: &Painter, ) { @@ -618,13 +619,13 @@ impl Menu for ColumnarMenu { self.reset_position(); self.input = if self.only_buffer_difference { - Some(line_buffer.get_buffer().to_string()) + Some(editor.get_buffer().to_string()) } else { None }; if !updated { - self.update_values(line_buffer, completer); + self.update_values(editor, completer); } } MenuEvent::Deactivate => self.active = false, @@ -632,7 +633,7 @@ impl Menu for ColumnarMenu { self.reset_position(); if !updated { - self.update_values(line_buffer, completer); + self.update_values(editor, completer); } } MenuEvent::NextElement => self.move_next(), @@ -649,7 +650,7 @@ impl Menu for ColumnarMenu { } /// The buffer gets replaced in the Span location - fn replace_in_buffer(&self, line_buffer: &mut LineBuffer) { + fn replace_in_buffer(&self, editor: &mut Editor) { if let Some(Suggestion { mut value, span, @@ -657,16 +658,18 @@ impl Menu for ColumnarMenu { .. }) = self.get_value() { - let start = span.start.min(line_buffer.len()); - let end = span.end.min(line_buffer.len()); + let start = span.start.min(editor.line_buffer().len()); + let end = span.end.min(editor.line_buffer().len()); if append_whitespace { value.push(' '); } - line_buffer.replace(start..end, &value); + let mut line_buffer = editor.line_buffer().clone(); + line_buffer.replace_range(start..end, &value); let mut offset = line_buffer.insertion_point(); offset += value.len().saturating_sub(end.saturating_sub(start)); line_buffer.set_insertion_point(offset); + editor.set_line_buffer(line_buffer, UndoBehavior::CreateUndoPoint); } } @@ -728,7 +731,7 @@ mod tests { macro_rules! partial_completion_tests { (name: $test_group_name:ident, completions: $completions:expr, test_cases: $($name:ident: $value:expr,)*) => { mod $test_group_name { - use crate::{menu::Menu, ColumnarMenu, LineBuffer}; + use crate::{menu::Menu, ColumnarMenu, core_editor::Editor, enums::UndoBehavior}; use super::FakeCompleter; $( @@ -736,13 +739,13 @@ mod tests { fn $name() { let (input, expected) = $value; let mut menu = ColumnarMenu::default(); - let mut line_buffer = LineBuffer::default(); - line_buffer.set_buffer(input.to_string()); + let mut editor = Editor::default(); + editor.set_buffer(input.to_string(), UndoBehavior::CreateUndoPoint); let mut completer = FakeCompleter::new(&$completions); - menu.can_partially_complete(false, &mut line_buffer, &mut completer); + menu.can_partially_complete(false, &mut editor, &mut completer); - assert_eq!(line_buffer.get_buffer(), expected); + assert_eq!(editor.get_buffer(), expected); } )* } diff --git a/src/menu/list_menu.rs b/src/menu/list_menu.rs index d6baf5a..adf41a2 100644 --- a/src/menu/list_menu.rs +++ b/src/menu/list_menu.rs @@ -1,3 +1,5 @@ +use crate::{core_editor::Editor, UndoBehavior}; + use { super::{ menu_functions::{parse_selection_char, string_difference}, @@ -5,7 +7,7 @@ use { }, crate::{ painting::{estimate_single_line_wraps, Painter}, - Completer, LineBuffer, Suggestion, + Completer, Suggestion, }, nu_ansi_term::{ansi::RESET, Style}, std::iter::Sum, @@ -371,7 +373,7 @@ impl Menu for ListMenu { fn can_partially_complete( &mut self, _values_updated: bool, - _line_buffer: &mut LineBuffer, + _editor: &mut Editor, _completer: &mut dyn Completer, ) -> bool { false @@ -392,7 +394,8 @@ impl Menu for ListMenu { } /// Collecting the value from the completer to be shown in the menu - fn update_values(&mut self, line_buffer: &mut LineBuffer, completer: &mut dyn Completer) { + fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) { + let line_buffer = editor.line_buffer(); let (start, input) = if self.only_buffer_difference { match &self.input { Some(old_string) => { @@ -463,7 +466,7 @@ impl Menu for ListMenu { } /// The buffer gets cleared with the actual value - fn replace_in_buffer(&self, line_buffer: &mut LineBuffer) { + fn replace_in_buffer(&self, editor: &mut Editor) { if let Some(Suggestion { mut value, span, @@ -471,22 +474,25 @@ impl Menu for ListMenu { .. }) = self.get_value() { - let start = span.start.min(line_buffer.len()); - let end = span.end.min(line_buffer.len()); + let buffer_len = editor.line_buffer().len(); + let start = span.start.min(buffer_len); + let end = span.end.min(buffer_len); if append_whitespace { value.push(' '); } - line_buffer.replace(start..end, &value); + let mut line_buffer = editor.line_buffer().clone(); + line_buffer.replace_range(start..end, &value); let mut offset = line_buffer.insertion_point(); offset += value.len().saturating_sub(end.saturating_sub(start)); line_buffer.set_insertion_point(offset); + editor.set_line_buffer(line_buffer, UndoBehavior::CreateUndoPoint); } } fn update_working_details( &mut self, - line_buffer: &mut LineBuffer, + editor: &mut Editor, completer: &mut dyn Completer, painter: &Painter, ) { @@ -496,12 +502,12 @@ impl Menu for ListMenu { self.reset_position(); self.input = if self.only_buffer_difference { - Some(line_buffer.get_buffer().to_string()) + Some(editor.get_buffer().to_string()) } else { None }; - self.update_values(line_buffer, completer); + self.update_values(editor, completer); self.pages.push(Page { size: self.printable_entries(painter), @@ -513,7 +519,7 @@ impl Menu for ListMenu { self.input = None; } MenuEvent::Edit(_) => { - self.update_values(line_buffer, completer); + self.update_values(editor, completer); self.pages.push(Page { size: self.printable_entries(painter), full: false, @@ -525,7 +531,7 @@ impl Menu for ListMenu { if let Some(page) = self.pages.get(self.page) { if new_pos >= page.size as u16 { self.event = Some(MenuEvent::NextPage); - self.update_working_details(line_buffer, completer, painter); + self.update_working_details(editor, completer, painter); } else { self.row_position = new_pos; } @@ -546,7 +552,7 @@ impl Menu for ListMenu { } self.event = Some(MenuEvent::PreviousPage); - self.update_working_details(line_buffer, completer, painter); + self.update_working_details(editor, completer, painter); } } MenuEvent::NextPage => { @@ -566,12 +572,12 @@ impl Menu for ListMenu { } } - self.update_values(line_buffer, completer); + self.update_values(editor, completer); self.set_actual_page_size(self.printable_entries(painter)); } else { self.row_position = 0; self.page = 0; - self.update_values(line_buffer, completer); + self.update_values(editor, completer); } } MenuEvent::PreviousPage => { @@ -579,7 +585,7 @@ impl Menu for ListMenu { Some(page_num) => self.page = page_num, None => self.page = self.pages.len().saturating_sub(1), } - self.update_values(line_buffer, completer); + self.update_values(editor, completer); } } diff --git a/src/menu/mod.rs b/src/menu/mod.rs index 31e061d..fb55958 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -2,10 +2,9 @@ mod columnar_menu; mod list_menu; pub mod menu_functions; +use crate::core_editor::Editor; use crate::History; -use crate::{ - completion::history::HistoryCompleter, painting::Painter, Completer, LineBuffer, Suggestion, -}; +use crate::{completion::history::HistoryCompleter, painting::Painter, Completer, Suggestion}; pub use columnar_menu::ColumnarMenu; pub use list_menu::ListMenu; use nu_ansi_term::{Color, Style}; @@ -82,7 +81,7 @@ pub trait Menu: Send { fn can_partially_complete( &mut self, values_updated: bool, - line_buffer: &mut LineBuffer, + editor: &mut Editor, completer: &mut dyn Completer, ) -> bool; @@ -91,7 +90,7 @@ pub trait Menu: Send { /// activated or the `quick_completion` option is true, the len of the values /// is calculated to know if there is only one value so it can be selected /// immediately - fn update_values(&mut self, line_buffer: &mut LineBuffer, completer: &mut dyn Completer); + fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer); /// The working details of a menu are values that could change based on /// the menu conditions before it being printed, such as the number or size @@ -100,13 +99,13 @@ pub trait Menu: Send { /// it is called just before painting the menu fn update_working_details( &mut self, - line_buffer: &mut LineBuffer, + editor: &mut Editor, completer: &mut dyn Completer, painter: &Painter, ); /// Indicates how to replace in the line buffer the selected value from the menu - fn replace_in_buffer(&self, line_buffer: &mut LineBuffer); + fn replace_in_buffer(&self, editor: &mut Editor); /// Calculates the real required lines for the menu considering how many lines /// wrap the terminal or if entries have multiple lines @@ -157,66 +156,66 @@ impl ReedlineMenu { pub(crate) fn can_partially_complete( &mut self, values_updated: bool, - line_buffer: &mut LineBuffer, + editor: &mut Editor, completer: &mut dyn Completer, history: &dyn History, ) -> bool { match self { Self::EngineCompleter(menu) => { - menu.can_partially_complete(values_updated, line_buffer, completer) + menu.can_partially_complete(values_updated, editor, completer) } Self::HistoryMenu(menu) => { let mut history_completer = HistoryCompleter::new(history); - menu.can_partially_complete(values_updated, line_buffer, &mut history_completer) + menu.can_partially_complete(values_updated, editor, &mut history_completer) } Self::WithCompleter { menu, completer: own_completer, - } => menu.can_partially_complete(values_updated, line_buffer, own_completer.as_mut()), + } => menu.can_partially_complete(values_updated, editor, own_completer.as_mut()), } } pub(crate) fn update_values( &mut self, - line_buffer: &mut LineBuffer, + editor: &mut Editor, completer: &mut dyn Completer, history: &dyn History, ) { match self { - Self::EngineCompleter(menu) => menu.update_values(line_buffer, completer), + Self::EngineCompleter(menu) => menu.update_values(editor, completer), Self::HistoryMenu(menu) => { let mut history_completer = HistoryCompleter::new(history); - menu.update_values(line_buffer, &mut history_completer); + menu.update_values(editor, &mut history_completer); } Self::WithCompleter { menu, completer: own_completer, } => { - menu.update_values(line_buffer, own_completer.as_mut()); + menu.update_values(editor, own_completer.as_mut()); } } } pub(crate) fn update_working_details( &mut self, - line_buffer: &mut LineBuffer, + editor: &mut Editor, completer: &mut dyn Completer, history: &dyn History, painter: &Painter, ) { match self { Self::EngineCompleter(menu) => { - menu.update_working_details(line_buffer, completer, painter); + menu.update_working_details(editor, completer, painter); } Self::HistoryMenu(menu) => { let mut history_completer = HistoryCompleter::new(history); - menu.update_working_details(line_buffer, &mut history_completer, painter); + menu.update_working_details(editor, &mut history_completer, painter); } Self::WithCompleter { menu, completer: own_completer, } => { - menu.update_working_details(line_buffer, own_completer.as_mut(), painter); + menu.update_working_details(editor, own_completer.as_mut(), painter); } } } @@ -246,55 +245,55 @@ impl Menu for ReedlineMenu { fn can_partially_complete( &mut self, values_updated: bool, - line_buffer: &mut LineBuffer, + editor: &mut Editor, completer: &mut dyn Completer, ) -> bool { match self { Self::EngineCompleter(menu) | Self::HistoryMenu(menu) => { - menu.can_partially_complete(values_updated, line_buffer, completer) + menu.can_partially_complete(values_updated, editor, completer) } Self::WithCompleter { menu, completer: own_completer, - } => menu.can_partially_complete(values_updated, line_buffer, own_completer.as_mut()), + } => menu.can_partially_complete(values_updated, editor, own_completer.as_mut()), } } - fn update_values(&mut self, line_buffer: &mut LineBuffer, completer: &mut dyn Completer) { + fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) { match self { Self::EngineCompleter(menu) | Self::HistoryMenu(menu) => { - menu.update_values(line_buffer, completer); + menu.update_values(editor, completer); } Self::WithCompleter { menu, completer: own_completer, } => { - menu.update_values(line_buffer, own_completer.as_mut()); + menu.update_values(editor, own_completer.as_mut()); } } } fn update_working_details( &mut self, - line_buffer: &mut LineBuffer, + editor: &mut Editor, completer: &mut dyn Completer, painter: &Painter, ) { match self { Self::EngineCompleter(menu) | Self::HistoryMenu(menu) => { - menu.update_working_details(line_buffer, completer, painter); + menu.update_working_details(editor, completer, painter); } Self::WithCompleter { menu, completer: own_completer, } => { - menu.update_working_details(line_buffer, own_completer.as_mut(), painter); + menu.update_working_details(editor, own_completer.as_mut(), painter); } } } - fn replace_in_buffer(&self, line_buffer: &mut LineBuffer) { - self.as_ref().replace_in_buffer(line_buffer); + fn replace_in_buffer(&self, editor: &mut Editor) { + self.as_ref().replace_in_buffer(editor); } fn menu_required_lines(&self, terminal_columns: u16) -> u16 {