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
8b5e70fc0e

* 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.
This commit is contained in:
bnprks 2022-08-14 15:09:06 -07:00 committed by GitHub
parent f41a5c2605
commit 174255535d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 370 additions and 393 deletions

View File

@ -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<LineBuffer>,
}
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"));
}
}

View File

@ -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;

View File

@ -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<dyn Clipboard>,
edit_stack: EditStack<LineBuffer>,
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<R>(&mut self, range: R)
/// Edit the line buffer in an undo-safe manner.
pub fn edit_buffer<F>(&mut self, func: F, undo_behavior: UndoBehavior)
where
R: std::ops::RangeBounds<usize>,
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");
}
}

View File

@ -25,11 +25,6 @@ impl LineBuffer {
Self::default()
}
/// Replaces the content between [`start`..`end`] with `text`
pub fn replace(&mut self, range: Range<usize>, 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<R>(&mut self, range: R, replace_with: &str)
pub fn replace_range<R>(&mut self, range: R, replace_with: &str)
where
R: std::ops::RangeBounds<usize>,
{
@ -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<usize> {
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)]

View File

@ -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<dyn Highlighter>,
@ -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,

View File

@ -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<char>),
/// Delete command, tracking the deleted character (right of cursor)
/// Warning: this does not track the whole grapheme, just the character
Delete(Option<char>),
/// 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"),

View File

@ -205,6 +205,7 @@
#![warn(missing_docs)]
// #![deny(warnings)]
mod core_editor;
pub use core_editor::Editor;
pub use core_editor::LineBuffer;
mod enums;

View File

@ -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);
}
)*
}

View File

@ -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);
}
}

View File

@ -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 {