diff --git a/src/core_editor/editor.rs b/src/core_editor/editor.rs index 3cabe1ae..e59f531c 100644 --- a/src/core_editor/editor.rs +++ b/src/core_editor/editor.rs @@ -9,9 +9,9 @@ use crate::{core_editor::get_default_clipboard, EditCommand}; pub struct Editor { line_buffer: LineBuffer, cut_buffer: Box, - edit_stack: EditStack, last_undo_behavior: UndoBehavior, + selection_anchor: Option, } impl Default for Editor { @@ -21,6 +21,7 @@ impl Default for Editor { cut_buffer: Box::new(get_default_clipboard()), edit_stack: EditStack::new(), last_undo_behavior: UndoBehavior::CreateUndoPoint, + selection_anchor: None, } } } @@ -40,28 +41,32 @@ impl Editor { 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(), - EditCommand::MoveToEnd => self.line_buffer.move_to_end(), - EditCommand::MoveToLineEnd => self.line_buffer.move_to_line_end(), - EditCommand::MoveToPosition(pos) => self.line_buffer.set_insertion_point(*pos), - EditCommand::MoveLeft => self.line_buffer.move_left(), - EditCommand::MoveRight => self.line_buffer.move_right(), - EditCommand::MoveWordLeft => self.line_buffer.move_word_left(), - EditCommand::MoveBigWordLeft => self.line_buffer.move_big_word_left(), - EditCommand::MoveWordRight => self.line_buffer.move_word_right(), - EditCommand::MoveWordRightStart => self.line_buffer.move_word_right_start(), - 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.line_buffer.insert_char(*c), + EditCommand::MoveToStart { select } => self.move_to_start(*select), + EditCommand::MoveToLineStart { select } => self.move_to_line_start(*select), + EditCommand::MoveToEnd { select } => self.move_to_end(*select), + EditCommand::MoveToLineEnd { select } => self.move_to_line_end(*select), + EditCommand::MoveToPosition { position, select } => { + self.move_to_position(*position, *select) + } + EditCommand::MoveLeft { select } => self.move_left(*select), + EditCommand::MoveRight { select } => self.move_right(*select), + EditCommand::MoveWordLeft { select } => self.move_word_left(*select), + EditCommand::MoveBigWordLeft { select } => self.move_big_word_left(*select), + EditCommand::MoveWordRight { select } => self.move_word_right(*select), + EditCommand::MoveWordRightStart { select } => self.move_word_right_start(*select), + EditCommand::MoveBigWordRightStart { select } => { + self.move_big_word_right_start(*select) + } + EditCommand::MoveWordRightEnd { select } => self.move_word_right_end(*select), + EditCommand::MoveBigWordRightEnd { select } => self.move_big_word_right_end(*select), + EditCommand::InsertChar(c) => self.insert_char(*c), EditCommand::Complete => {} - EditCommand::InsertString(str) => self.line_buffer.insert_str(str), - EditCommand::InsertNewline => self.line_buffer.insert_newline(), + EditCommand::InsertString(str) => self.insert_str(str), + EditCommand::InsertNewline => self.insert_newline(), EditCommand::ReplaceChar(chr) => self.replace_char(*chr), EditCommand::ReplaceChars(n_chars, str) => self.replace_chars(*n_chars, str), - EditCommand::Backspace => self.line_buffer.delete_left_grapheme(), - EditCommand::Delete => self.line_buffer.delete_right_grapheme(), + EditCommand::Backspace => self.backspace(), + EditCommand::Delete => self.delete(), EditCommand::CutChar => self.cut_char(), EditCommand::BackspaceWord => self.line_buffer.delete_word_left(), EditCommand::DeleteWord => self.line_buffer.delete_word_right(), @@ -90,16 +95,31 @@ impl Editor { EditCommand::Redo => self.redo(), EditCommand::CutRightUntil(c) => self.cut_right_until_char(*c, false, true), EditCommand::CutRightBefore(c) => self.cut_right_until_char(*c, true, true), - EditCommand::MoveRightUntil(c) => self.move_right_until_char(*c, false, true), - EditCommand::MoveRightBefore(c) => self.move_right_until_char(*c, true, true), + EditCommand::MoveRightUntil { c, select } => { + self.move_right_until_char(*c, false, true, *select) + } + EditCommand::MoveRightBefore { c, select } => { + self.move_right_until_char(*c, true, true, *select) + } EditCommand::CutLeftUntil(c) => self.cut_left_until_char(*c, false, true), EditCommand::CutLeftBefore(c) => self.cut_left_until_char(*c, true, true), - EditCommand::MoveLeftUntil(c) => self.move_left_until_char(*c, false, true), - EditCommand::MoveLeftBefore(c) => self.move_left_until_char(*c, true, true), + EditCommand::MoveLeftUntil { c, select } => { + self.move_left_until_char(*c, false, true, *select) + } + EditCommand::MoveLeftBefore { c, select } => { + self.move_left_until_char(*c, true, true, *select) + } + EditCommand::SelectAll => self.select_all(), + EditCommand::CutSelection => self.cut_selection(), + EditCommand::CopySelection => self.copy_selection(), } + if !matches!(command.edit_type(), EditType::MoveCursor { select: true }) { + self.selection_anchor = None; + } + if let EditType::MoveCursor { select: true } = command.edit_type() {} let new_undo_behavior = match (command, command.edit_type()) { - (_, EditType::MoveCursor) => UndoBehavior::MoveCursor, + (_, 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(); @@ -112,8 +132,21 @@ impl Editor { (_, EditType::UndoRedo) => UndoBehavior::UndoRedo, (_, _) => UndoBehavior::CreateUndoPoint, }; + self.update_undo_state(new_undo_behavior); } + fn update_selection_anchor(&mut self, select: bool) { + self.selection_anchor = if select { + self.selection_anchor + .or_else(|| Some(self.insertion_point())) + } else { + None + }; + } + fn move_to_position(&mut self, position: usize, select: bool) { + self.update_selection_anchor(select); + self.line_buffer.set_insertion_point(position) + } pub(crate) fn move_line_up(&mut self) { self.line_buffer.move_line_up(); @@ -170,25 +203,24 @@ impl Editor { self.edit_stack.reset(); } - pub(crate) fn move_to_start(&mut self, undo_behavior: UndoBehavior) { + pub(crate) fn move_to_start(&mut self, select: bool) { + self.update_selection_anchor(select); self.line_buffer.move_to_start(); - self.update_undo_state(undo_behavior); } - pub(crate) fn move_to_end(&mut self, undo_behavior: UndoBehavior) { + pub(crate) fn move_to_end(&mut self, select: bool) { + self.update_selection_anchor(select); self.line_buffer.move_to_end(); - self.update_undo_state(undo_behavior); } - #[allow(dead_code)] - pub(crate) fn move_to_line_start(&mut self, undo_behavior: UndoBehavior) { + pub(crate) fn move_to_line_start(&mut self, select: bool) { + self.update_selection_anchor(select); self.line_buffer.move_to_line_start(); - self.update_undo_state(undo_behavior); } - pub(crate) fn move_to_line_end(&mut self, undo_behavior: UndoBehavior) { + pub(crate) fn move_to_line_end(&mut self, select: bool) { + self.update_selection_anchor(select); self.line_buffer.move_to_line_end(); - self.update_undo_state(undo_behavior); } fn undo(&mut self) { @@ -201,7 +233,7 @@ impl Editor { self.line_buffer = val.clone(); } - fn update_undo_state(&mut self, undo_behavior: UndoBehavior) { + pub(crate) fn update_undo_state(&mut self, undo_behavior: UndoBehavior) { if matches!(undo_behavior, UndoBehavior::UndoRedo) { self.last_undo_behavior = UndoBehavior::UndoRedo; return; @@ -357,6 +389,7 @@ impl Editor { } fn insert_cut_buffer_before(&mut self) { + self.delete_selection(); match self.cut_buffer.get() { (content, ClipboardMode::Normal) => { self.line_buffer.insert_str(&content); @@ -375,6 +408,7 @@ impl Editor { } fn insert_cut_buffer_after(&mut self) { + self.delete_selection(); match self.cut_buffer.get() { (content, ClipboardMode::Normal) => { self.line_buffer.move_right(); @@ -393,7 +427,14 @@ impl Editor { } } - fn move_right_until_char(&mut self, c: char, before_char: bool, current_line: bool) { + fn move_right_until_char( + &mut self, + c: char, + before_char: bool, + current_line: bool, + select: bool, + ) { + self.update_selection_anchor(select); if before_char { self.line_buffer.move_right_before(c, current_line); } else { @@ -401,7 +442,14 @@ impl Editor { } } - fn move_left_until_char(&mut self, c: char, before_char: bool, current_line: bool) { + fn move_left_until_char( + &mut self, + c: char, + before_char: bool, + current_line: bool, + select: bool, + ) { + self.update_selection_anchor(select); if before_char { self.line_buffer.move_left_before(c, current_line); } else { @@ -462,6 +510,115 @@ impl Editor { self.line_buffer.insert_str(string); } + + fn move_left(&mut self, select: bool) { + self.update_selection_anchor(select); + self.line_buffer.move_left(); + } + + fn move_right(&mut self, select: bool) { + self.update_selection_anchor(select); + self.line_buffer.move_right(); + } + + fn select_all(&mut self) { + self.selection_anchor = Some(0); + self.line_buffer.move_to_end(); + } + + fn cut_selection(&mut self) { + if let Some((start, end)) = self.get_selection() { + let cut_slice = &self.line_buffer.get_buffer()[start..end]; + self.cut_buffer.set(cut_slice, ClipboardMode::Normal); + self.line_buffer.clear_range_safe(start, end); + self.selection_anchor = None; + } + } + + fn copy_selection(&mut self) { + if let Some((start, end)) = self.get_selection() { + let cut_slice = &self.line_buffer.get_buffer()[start..end]; + self.cut_buffer.set(cut_slice, ClipboardMode::Normal); + } + } + + /// If a selection is active returns the selected range, otherwise None. + /// The range is guaranteed to be ascending. + pub fn get_selection(&self) -> Option<(usize, usize)> { + self.selection_anchor.map(|selection_anchor| { + if self.insertion_point() > selection_anchor { + (selection_anchor, self.insertion_point()) + } else { + (self.insertion_point(), selection_anchor) + } + }) + } + + fn delete_selection(&mut self) { + if let Some((start, end)) = self.get_selection() { + self.line_buffer.clear_range_safe(start, end); + self.selection_anchor = None; + } + } + + fn backspace(&mut self) { + if self.selection_anchor.is_some() { + self.delete_selection(); + } else { + self.line_buffer.delete_left_grapheme(); + } + } + + fn delete(&mut self) { + if self.selection_anchor.is_some() { + self.delete_selection(); + } else { + self.line_buffer.delete_right_grapheme(); + } + } + + fn move_word_left(&mut self, select: bool) { + self.move_to_position(self.line_buffer.word_left_index(), select); + } + + fn move_big_word_left(&mut self, select: bool) { + self.move_to_position(self.line_buffer.big_word_left_index(), select); + } + + fn move_word_right(&mut self, select: bool) { + self.move_to_position(self.line_buffer.word_right_index(), select); + } + + fn move_word_right_start(&mut self, select: bool) { + self.move_to_position(self.line_buffer.word_right_start_index(), select); + } + + fn move_big_word_right_start(&mut self, select: bool) { + self.move_to_position(self.line_buffer.big_word_right_start_index(), select); + } + + fn move_word_right_end(&mut self, select: bool) { + self.move_to_position(self.line_buffer.word_right_end_index(), select); + } + + fn move_big_word_right_end(&mut self, select: bool) { + self.move_to_position(self.line_buffer.big_word_right_end_index(), select); + } + + fn insert_char(&mut self, c: char) { + self.delete_selection(); + self.line_buffer.insert_char(c); + } + + fn insert_str(&mut self, str: &str) { + self.delete_selection(); + self.line_buffer.insert_str(str); + } + + fn insert_newline(&mut self) { + self.delete_selection(); + self.line_buffer.insert_newline(); + } } #[cfg(test)] diff --git a/src/core_editor/line_buffer.rs b/src/core_editor/line_buffer.rs index 01759ba7..6c929a63 100644 --- a/src/core_editor/line_buffer.rs +++ b/src/core_editor/line_buffer.rs @@ -396,6 +396,27 @@ impl LineBuffer { self.insertion_point = 0; } + /// Clear all contents between `start` and `end` and change insertion point if necessary. + /// + /// If the cursor is located between `start` and `end` it is adjusted to `start`. + /// If the cursor is located after `end` it is adjusted to stay at its current char boundary. + pub fn clear_range_safe(&mut self, start: usize, end: usize) { + let (start, end) = if start > end { + (end, start) + } else { + (start, end) + }; + if self.insertion_point <= start { + // No action necessary + } else if self.insertion_point < end { + self.insertion_point = start; + } else { + // Insertion point after end + self.insertion_point -= end - start; + } + self.clear_range(start..end); + } + /// Clear text covered by `range` in the current line /// /// Safety: Does not change the insertion point/offset and is thus not unicode safe! diff --git a/src/edit_mode/emacs.rs b/src/edit_mode/emacs.rs index 35aa6db7..99498306 100644 --- a/src/edit_mode/emacs.rs +++ b/src/edit_mode/emacs.rs @@ -2,7 +2,7 @@ use crate::{ edit_mode::{ keybindings::{ add_common_control_bindings, add_common_edit_bindings, add_common_navigation_bindings, - edit_bind, Keybindings, + add_common_selection_bindings, edit_bind, Keybindings, }, EditMode, }, @@ -21,6 +21,7 @@ pub fn default_emacs_keybindings() -> Keybindings { add_common_control_bindings(&mut kb); add_common_navigation_bindings(&mut kb); add_common_edit_bindings(&mut kb); + add_common_selection_bindings(&mut kb); // This could be in common, but in Vi it also changes the mode kb.add_binding(KM::NONE, KC::Enter, ReedlineEvent::Enter); @@ -53,27 +54,36 @@ pub fn default_emacs_keybindings() -> Keybindings { kb.add_binding(KM::CONTROL, KC::Char('w'), edit_bind(EC::CutWordLeft)); kb.add_binding(KM::CONTROL, KC::Char('k'), edit_bind(EC::CutToEnd)); kb.add_binding(KM::CONTROL, KC::Char('u'), edit_bind(EC::CutFromStart)); + kb.add_binding(KM::ALT, KC::Char('d'), edit_bind(EC::CutWordRight)); // Edits kb.add_binding(KM::CONTROL, KC::Char('t'), edit_bind(EC::SwapGraphemes)); // *** ALT *** // Moves - kb.add_binding(KM::ALT, KC::Left, edit_bind(EC::MoveWordLeft)); + kb.add_binding( + KM::ALT, + KC::Left, + edit_bind(EC::MoveWordLeft { select: false }), + ); kb.add_binding( KM::ALT, KC::Right, ReedlineEvent::UntilFound(vec![ ReedlineEvent::HistoryHintWordComplete, - edit_bind(EC::MoveWordRight), + edit_bind(EC::MoveWordRight { select: false }), ]), ); - kb.add_binding(KM::ALT, KC::Char('b'), edit_bind(EC::MoveWordLeft)); + kb.add_binding( + KM::ALT, + KC::Char('b'), + edit_bind(EC::MoveWordLeft { select: false }), + ); kb.add_binding( KM::ALT, KC::Char('f'), ReedlineEvent::UntilFound(vec![ ReedlineEvent::HistoryHintWordComplete, - edit_bind(EC::MoveWordRight), + edit_bind(EC::MoveWordRight { select: false }), ]), ); // Edits @@ -84,8 +94,6 @@ pub fn default_emacs_keybindings() -> Keybindings { KC::Char('m'), ReedlineEvent::Edit(vec![EditCommand::BackspaceWord]), ); - // Cutting - kb.add_binding(KM::ALT, KC::Char('d'), edit_bind(EC::CutWordRight)); // Case changes kb.add_binding(KM::ALT, KC::Char('u'), edit_bind(EC::UppercaseWord)); kb.add_binding(KM::ALT, KC::Char('l'), edit_bind(EC::LowercaseWord)); diff --git a/src/edit_mode/keybindings.rs b/src/edit_mode/keybindings.rs index 772e4199..c3b3a670 100644 --- a/src/edit_mode/keybindings.rs +++ b/src/edit_mode/keybindings.rs @@ -136,24 +136,36 @@ pub fn add_common_navigation_bindings(kb: &mut Keybindings) { ); // Ctrl Left and Right - kb.add_binding(KM::CONTROL, KC::Left, edit_bind(EC::MoveWordLeft)); + kb.add_binding( + KM::CONTROL, + KC::Left, + edit_bind(EC::MoveWordLeft { select: false }), + ); kb.add_binding( KM::CONTROL, KC::Right, ReedlineEvent::UntilFound(vec![ ReedlineEvent::HistoryHintWordComplete, - edit_bind(EC::MoveWordRight), + edit_bind(EC::MoveWordRight { select: false }), ]), ); // Home/End & ctrl+a/ctrl+e - kb.add_binding(KM::NONE, KC::Home, edit_bind(EC::MoveToLineStart)); - kb.add_binding(KM::CONTROL, KC::Char('a'), edit_bind(EC::MoveToLineStart)); + kb.add_binding( + KM::NONE, + KC::Home, + edit_bind(EC::MoveToLineStart { select: false }), + ); + kb.add_binding( + KM::CONTROL, + KC::Char('a'), + edit_bind(EC::MoveToLineStart { select: false }), + ); kb.add_binding( KM::NONE, KC::End, ReedlineEvent::UntilFound(vec![ ReedlineEvent::HistoryHintComplete, - edit_bind(EC::MoveToLineEnd), + edit_bind(EC::MoveToLineEnd { select: false }), ]), ); kb.add_binding( @@ -161,12 +173,20 @@ pub fn add_common_navigation_bindings(kb: &mut Keybindings) { KC::Char('e'), ReedlineEvent::UntilFound(vec![ ReedlineEvent::HistoryHintComplete, - edit_bind(EC::MoveToLineEnd), + edit_bind(EC::MoveToLineEnd { select: false }), ]), ); // Ctrl Home/End - kb.add_binding(KM::CONTROL, KC::Home, edit_bind(EC::MoveToStart)); - kb.add_binding(KM::CONTROL, KC::End, edit_bind(EC::MoveToEnd)); + kb.add_binding( + KM::CONTROL, + KC::Home, + edit_bind(EC::MoveToStart { select: false }), + ); + kb.add_binding( + KM::CONTROL, + KC::End, + edit_bind(EC::MoveToEnd { select: false }), + ); // EMACS arrows kb.add_binding( KM::CONTROL, @@ -194,4 +214,59 @@ pub fn add_common_edit_bindings(kb: &mut Keybindings) { // Base commands should not affect cut buffer kb.add_binding(KM::CONTROL, KC::Char('h'), edit_bind(EC::Backspace)); kb.add_binding(KM::CONTROL, KC::Char('w'), edit_bind(EC::BackspaceWord)); + kb.add_binding(KM::CONTROL, KC::Char('x'), edit_bind(EC::CutSelection)); + kb.add_binding(KM::CONTROL, KC::Char('c'), edit_bind(EC::CopySelection)); + kb.add_binding( + KM::CONTROL, + KC::Char('v'), + edit_bind(EC::PasteCutBufferBefore), + ); +} + +pub fn add_common_selection_bindings(kb: &mut Keybindings) { + use EditCommand as EC; + use KeyCode as KC; + use KeyModifiers as KM; + + kb.add_binding( + KM::SHIFT, + KC::Left, + edit_bind(EC::MoveLeft { select: true }), + ); + kb.add_binding( + KM::SHIFT, + KC::Right, + edit_bind(EC::MoveRight { select: true }), + ); + kb.add_binding( + KM::SHIFT | KM::CONTROL, + KC::Left, + edit_bind(EC::MoveWordLeft { select: true }), + ); + kb.add_binding( + KM::SHIFT | KM::CONTROL, + KC::Right, + edit_bind(EC::MoveWordRight { select: true }), + ); + kb.add_binding( + KM::SHIFT, + KC::End, + edit_bind(EC::MoveToLineEnd { select: true }), + ); + kb.add_binding( + KM::SHIFT | KM::CONTROL, + KC::End, + edit_bind(EC::MoveToEnd { select: true }), + ); + kb.add_binding( + KM::SHIFT, + KC::Home, + edit_bind(EC::MoveToLineStart { select: true }), + ); + kb.add_binding( + KM::SHIFT | KM::CONTROL, + KC::Home, + edit_bind(EC::MoveToStart { select: true }), + ); + kb.add_binding(KM::CONTROL, KC::Char('a'), edit_bind(EC::SelectAll)); } diff --git a/src/edit_mode/vi/command.rs b/src/edit_mode/vi/command.rs index 7159acb9..145e3dd0 100644 --- a/src/edit_mode/vi/command.rs +++ b/src/edit_mode/vi/command.rs @@ -125,14 +125,20 @@ impl Command { pub fn to_reedline(&self, vi_state: &mut Vi) -> Vec { match self { Self::EnterViInsert => vec![ReedlineOption::Event(ReedlineEvent::Repaint)], - Self::EnterViAppend => vec![ReedlineOption::Edit(EditCommand::MoveRight)], + Self::EnterViAppend => vec![ReedlineOption::Edit(EditCommand::MoveRight { + select: false, + })], Self::PasteAfter => vec![ReedlineOption::Edit(EditCommand::PasteCutBufferAfter)], Self::PasteBefore => vec![ReedlineOption::Edit(EditCommand::PasteCutBufferBefore)], Self::Undo => vec![ReedlineOption::Edit(EditCommand::Undo)], Self::ChangeToLineEnd => vec![ReedlineOption::Edit(EditCommand::ClearToLineEnd)], Self::DeleteToEnd => vec![ReedlineOption::Edit(EditCommand::CutToLineEnd)], - Self::AppendToEnd => vec![ReedlineOption::Edit(EditCommand::MoveToLineEnd)], - Self::PrependToStart => vec![ReedlineOption::Edit(EditCommand::MoveToLineStart)], + Self::AppendToEnd => vec![ReedlineOption::Edit(EditCommand::MoveToLineEnd { + select: false, + })], + Self::PrependToStart => vec![ReedlineOption::Edit(EditCommand::MoveToLineStart { + select: false, + })], Self::RewriteCurrentLine => vec![ReedlineOption::Edit(EditCommand::CutCurrentLine)], Self::DeleteChar => vec![ReedlineOption::Edit(EditCommand::CutChar)], Self::ReplaceChar(c) => { @@ -207,7 +213,7 @@ impl Command { let op = match motion { Motion::End => Some(vec![ReedlineOption::Edit(EditCommand::ClearToLineEnd)]), Motion::Line => Some(vec![ - ReedlineOption::Edit(EditCommand::MoveToStart), + ReedlineOption::Edit(EditCommand::MoveToStart { select: false }), ReedlineOption::Edit(EditCommand::ClearToLineEnd), ]), Motion::NextWord => Some(vec![ReedlineOption::Edit(EditCommand::CutWordRight)]), diff --git a/src/edit_mode/vi/motion.rs b/src/edit_mode/vi/motion.rs index 90822a52..2095b100 100644 --- a/src/edit_mode/vi/motion.rs +++ b/src/edit_mode/vi/motion.rs @@ -160,30 +160,60 @@ impl Motion { ReedlineEvent::MenuDown, ReedlineEvent::Down, ]))], - Motion::NextWord => vec![ReedlineOption::Edit(EditCommand::MoveWordRightStart)], - Motion::NextBigWord => vec![ReedlineOption::Edit(EditCommand::MoveBigWordRightStart)], - Motion::NextWordEnd => vec![ReedlineOption::Edit(EditCommand::MoveWordRightEnd)], - Motion::NextBigWordEnd => vec![ReedlineOption::Edit(EditCommand::MoveBigWordRightEnd)], - Motion::PreviousWord => vec![ReedlineOption::Edit(EditCommand::MoveWordLeft)], - Motion::PreviousBigWord => vec![ReedlineOption::Edit(EditCommand::MoveBigWordLeft)], + Motion::NextWord => vec![ReedlineOption::Edit(EditCommand::MoveWordRightStart { + select: false, + })], + Motion::NextBigWord => vec![ReedlineOption::Edit(EditCommand::MoveBigWordRightStart { + select: false, + })], + Motion::NextWordEnd => vec![ReedlineOption::Edit(EditCommand::MoveWordRightEnd { + select: false, + })], + Motion::NextBigWordEnd => { + vec![ReedlineOption::Edit(EditCommand::MoveBigWordRightEnd { + select: false, + })] + } + Motion::PreviousWord => vec![ReedlineOption::Edit(EditCommand::MoveWordLeft { + select: false, + })], + Motion::PreviousBigWord => vec![ReedlineOption::Edit(EditCommand::MoveBigWordLeft { + select: false, + })], Motion::Line => vec![], // Placeholder as unusable standalone motion - Motion::Start => vec![ReedlineOption::Edit(EditCommand::MoveToLineStart)], - Motion::End => vec![ReedlineOption::Edit(EditCommand::MoveToLineEnd)], + Motion::Start => vec![ReedlineOption::Edit(EditCommand::MoveToLineStart { + select: false, + })], + Motion::End => vec![ReedlineOption::Edit(EditCommand::MoveToLineEnd { + select: false, + })], Motion::RightUntil(ch) => { vi_state.last_char_search = Some(ViCharSearch::ToRight(*ch)); - vec![ReedlineOption::Edit(EditCommand::MoveRightUntil(*ch))] + vec![ReedlineOption::Edit(EditCommand::MoveRightUntil { + c: *ch, + select: false, + })] } Motion::RightBefore(ch) => { vi_state.last_char_search = Some(ViCharSearch::TillRight(*ch)); - vec![ReedlineOption::Edit(EditCommand::MoveRightBefore(*ch))] + vec![ReedlineOption::Edit(EditCommand::MoveRightBefore { + c: *ch, + select: false, + })] } Motion::LeftUntil(ch) => { vi_state.last_char_search = Some(ViCharSearch::ToLeft(*ch)); - vec![ReedlineOption::Edit(EditCommand::MoveLeftUntil(*ch))] + vec![ReedlineOption::Edit(EditCommand::MoveLeftUntil { + c: *ch, + select: false, + })] } Motion::LeftBefore(ch) => { vi_state.last_char_search = Some(ViCharSearch::TillLeft(*ch)); - vec![ReedlineOption::Edit(EditCommand::MoveLeftBefore(*ch))] + vec![ReedlineOption::Edit(EditCommand::MoveLeftBefore { + c: *ch, + select: false, + })] } Motion::ReplayCharSearch => { if let Some(char_search) = vi_state.last_char_search.as_ref() { @@ -229,10 +259,22 @@ impl ViCharSearch { pub fn to_move(&self) -> EditCommand { match self { - ViCharSearch::ToRight(c) => EditCommand::MoveRightUntil(*c), - ViCharSearch::ToLeft(c) => EditCommand::MoveLeftUntil(*c), - ViCharSearch::TillRight(c) => EditCommand::MoveRightBefore(*c), - ViCharSearch::TillLeft(c) => EditCommand::MoveLeftBefore(*c), + ViCharSearch::ToRight(c) => EditCommand::MoveRightUntil { + c: *c, + select: false, + }, + ViCharSearch::ToLeft(c) => EditCommand::MoveLeftUntil { + c: *c, + select: false, + }, + ViCharSearch::TillRight(c) => EditCommand::MoveRightBefore { + c: *c, + select: false, + }, + ViCharSearch::TillLeft(c) => EditCommand::MoveLeftBefore { + c: *c, + select: false, + }, } } diff --git a/src/edit_mode/vi/parser.rs b/src/edit_mode/vi/parser.rs index e04bd5a6..777d7cba 100644 --- a/src/edit_mode/vi/parser.rs +++ b/src/edit_mode/vi/parser.rs @@ -418,9 +418,9 @@ mod tests { ReedlineEvent::Up, ])]))] #[case(&['w'], - ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveWordRightStart])]))] + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveWordRightStart{select:false}])]))] #[case(&['W'], - ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveBigWordRightStart])]))] + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveBigWordRightStart{select:false}])]))] #[case(&['2', 'l'], ReedlineEvent::Multiple(vec![ ReedlineEvent::UntilFound(vec![ ReedlineEvent::HistoryHintComplete, @@ -436,8 +436,8 @@ mod tests { ReedlineEvent::MenuRight, ReedlineEvent::Right, ])]))] - #[case(&['0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveToLineStart])]))] - #[case(&['$'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveToLineEnd])]))] + #[case(&['0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveToLineStart{select:false}])]))] + #[case(&['$'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveToLineEnd{select:false}])]))] #[case(&['i'], ReedlineEvent::Multiple(vec![ReedlineEvent::Repaint]))] #[case(&['p'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::PasteCutBufferAfter])]))] #[case(&['2', 'p'], ReedlineEvent::Multiple(vec![ diff --git a/src/edit_mode/vi/vi_keybindings.rs b/src/edit_mode/vi/vi_keybindings.rs index 1498f3ac..20cca81a 100644 --- a/src/edit_mode/vi/vi_keybindings.rs +++ b/src/edit_mode/vi/vi_keybindings.rs @@ -4,7 +4,7 @@ use crate::{ edit_mode::{ keybindings::{ add_common_control_bindings, add_common_edit_bindings, add_common_navigation_bindings, - edit_bind, + add_common_selection_bindings, edit_bind, }, Keybindings, }, @@ -20,8 +20,13 @@ pub fn default_vi_normal_keybindings() -> Keybindings { add_common_control_bindings(&mut kb); add_common_navigation_bindings(&mut kb); + add_common_selection_bindings(&mut kb); // Replicate vi's default behavior for Backspace and delete - kb.add_binding(KM::NONE, KC::Backspace, edit_bind(EC::MoveLeft)); + kb.add_binding( + KM::NONE, + KC::Backspace, + edit_bind(EC::MoveLeft { select: false }), + ); kb.add_binding(KM::NONE, KC::Delete, edit_bind(EC::Delete)); kb @@ -34,6 +39,7 @@ pub fn default_vi_insert_keybindings() -> Keybindings { add_common_control_bindings(&mut kb); add_common_navigation_bindings(&mut kb); add_common_edit_bindings(&mut kb); + add_common_selection_bindings(&mut kb); kb } diff --git a/src/engine.rs b/src/engine.rs index 30bb43bc..8e51af06 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use itertools::Itertools; +use nu_ansi_term::{Color, Style}; use crate::{enums::ReedlineRawEvent, CursorConfig}; #[cfg(feature = "bashisms")] @@ -127,6 +128,9 @@ pub struct Reedline { // Highlight the edit buffer highlighter: Box, + // Style used for visual selection + visual_selection_style: Style, + // Showcase hints based on various strategies (history, language-completion, spellcheck, etc) hinter: Option>, hide_hints: bool, @@ -183,6 +187,7 @@ impl Reedline { let history = Box::::default(); let painter = Painter::new(std::io::BufWriter::new(std::io::stderr())); let buffer_highlighter = Box::::default(); + let visual_selection_style = Style::new().on(Color::LightGray); let completer = Box::::default(); let hinter = None; let validator = None; @@ -209,6 +214,7 @@ impl Reedline { quick_completions: false, partial_completions: false, highlighter: buffer_highlighter, + visual_selection_style, hinter, hide_hints: false, validator, @@ -371,6 +377,13 @@ impl Reedline { self } + /// A builder that configures the style used for visual selection + #[must_use] + pub fn with_visual_selection_style(mut self, style: Style) -> Self { + self.visual_selection_style = style; + self + } + /// A builder which configures the history for your instance of the Reedline engine /// # Example /// ```rust,no_run @@ -1104,7 +1117,7 @@ impl Reedline { match commands.first() { Some(&EditCommand::Backspace) | Some(&EditCommand::BackspaceWord) - | Some(&EditCommand::MoveToLineStart) => { + | Some(&EditCommand::MoveToLineStart { select: false }) => { menu.menu_event(MenuEvent::Deactivate) } _ => { @@ -1166,11 +1179,11 @@ impl Reedline { Ok(EventStatus::Handled) } ReedlineEvent::Left => { - self.run_edit_commands(&[EditCommand::MoveLeft]); + self.run_edit_commands(&[EditCommand::MoveLeft { select: false }]); Ok(EventStatus::Handled) } ReedlineEvent::Right => { - self.run_edit_commands(&[EditCommand::MoveRight]); + self.run_edit_commands(&[EditCommand::MoveRight { select: false }]); Ok(EventStatus::Handled) } ReedlineEvent::SearchHistory => { @@ -1248,9 +1261,12 @@ impl Reedline { .expect("todo: error handling"); } self.update_buffer_from_history(); - self.editor.move_to_start(UndoBehavior::HistoryNavigation); + self.editor.move_to_start(false); + self.editor + .update_undo_state(UndoBehavior::HistoryNavigation); + self.editor.move_to_line_end(false); self.editor - .move_to_line_end(UndoBehavior::HistoryNavigation); + .update_undo_state(UndoBehavior::HistoryNavigation); } fn next_history(&mut self) { @@ -1282,7 +1298,9 @@ impl Reedline { self.input_mode = InputMode::Regular; } self.update_buffer_from_history(); - self.editor.move_to_end(UndoBehavior::HistoryNavigation); + self.editor.move_to_end(false); + self.editor + .update_undo_state(UndoBehavior::HistoryNavigation) } /// Enable the search and navigation through the history from the line buffer prompt @@ -1545,7 +1563,10 @@ impl Reedline { if let Some((start, size, history)) = history_result { let edits = vec![ - EditCommand::MoveToPosition(start), + EditCommand::MoveToPosition { + position: start, + select: false, + }, EditCommand::ReplaceChars(size, history), ]; @@ -1638,14 +1659,18 @@ impl Reedline { let cursor_position_in_buffer = self.editor.insertion_point(); let buffer_to_paint = self.editor.get_buffer(); - let (before_cursor, after_cursor) = self + let mut styled_text = self .highlighter - .highlight(buffer_to_paint, cursor_position_in_buffer) - .render_around_insertion_point( - cursor_position_in_buffer, - prompt, - self.use_ansi_coloring, - ); + .highlight(buffer_to_paint, cursor_position_in_buffer); + if let Some((from, to)) = self.editor.get_selection() { + styled_text.style_range(from, to, self.visual_selection_style); + } + + let (before_cursor, after_cursor) = styled_text.render_around_insertion_point( + cursor_position_in_buffer, + prompt, + self.use_ansi_coloring, + ); let hint: String = if self.hints_active() { self.hinter.as_mut().map_or_else(String::new, |hinter| { diff --git a/src/enums.rs b/src/enums.rs index 210dac8e..7ebaa343 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -20,46 +20,90 @@ pub enum Signal { #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, EnumIter)] pub enum EditCommand { /// Move to the start of the buffer - MoveToStart, + MoveToStart { + /// Select the text between the current cursor position and destination + select: bool, + }, /// Move to the start of the current line - MoveToLineStart, + MoveToLineStart { + /// Select the text between the current cursor position and destination + select: bool, + }, /// Move to the end of the buffer - MoveToEnd, + MoveToEnd { + /// Select the text between the current cursor position and destination + select: bool, + }, /// Move to the end of the current line - MoveToLineEnd, + MoveToLineEnd { + /// Select the text between the current cursor position and destination + select: bool, + }, /// Move one character to the left - MoveLeft, + MoveLeft { + /// Select the text between the current cursor position and destination + select: bool, + }, /// Move one character to the right - MoveRight, + MoveRight { + /// Select the text between the current cursor position and destination + select: bool, + }, /// Move one word to the left - MoveWordLeft, + MoveWordLeft { + /// Select the text between the current cursor position and destination + select: bool, + }, /// Move one WORD to the left - MoveBigWordLeft, + MoveBigWordLeft { + /// Select the text between the current cursor position and destination + select: bool, + }, /// Move one word to the right - MoveWordRight, + MoveWordRight { + /// Select the text between the current cursor position and destination + select: bool, + }, /// Move one word to the right, stop at start of word - MoveWordRightStart, + MoveWordRightStart { + /// Select the text between the current cursor position and destination + select: bool, + }, /// Move one WORD to the right, stop at start of WORD - MoveBigWordRightStart, + MoveBigWordRightStart { + /// Select the text between the current cursor position and destination + select: bool, + }, /// Move one word to the right, stop at end of word - MoveWordRightEnd, + MoveWordRightEnd { + /// Select the text between the current cursor position and destination + select: bool, + }, /// Move one WORD to the right, stop at end of WORD - MoveBigWordRightEnd, + MoveBigWordRightEnd { + /// Select the text between the current cursor position and destination + select: bool, + }, /// Move to position - MoveToPosition(usize), + MoveToPosition { + /// Position to move to + position: usize, + /// Select the text between the current cursor position and destination + select: bool, + }, /// Insert a character at the current insertion point InsertChar(char), @@ -173,10 +217,20 @@ pub enum EditCommand { CutRightBefore(char), /// CutUntil right until char - MoveRightUntil(char), + MoveRightUntil { + /// Char to move towards + c: char, + /// Select the text between the current cursor position and destination + select: bool, + }, /// CutUntil right before char - MoveRightBefore(char), + MoveRightBefore { + /// Char to move towards + c: char, + /// Select the text between the current cursor position and destination + select: bool, + }, /// CutUntil left until char CutLeftUntil(char), @@ -184,30 +238,73 @@ pub enum EditCommand { /// CutUntil left before char CutLeftBefore(char), - /// CutUntil left until char - MoveLeftUntil(char), - - /// CutUntil left before char - MoveLeftBefore(char), + /// Move left until char + MoveLeftUntil { + /// Char to move towards + c: char, + /// Select the text between the current cursor position and destination + select: bool, + }, + + /// Move left before char + MoveLeftBefore { + /// Char to move towards + c: char, + /// Select the text between the current cursor position and destination + select: bool, + }, + + /// Select whole input buffer + SelectAll, + + /// Cut selection + CutSelection, + + /// Copy selection + CopySelection, } impl Display for EditCommand { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { match self { - EditCommand::MoveToStart => write!(f, "MoveToStart"), - EditCommand::MoveToLineStart => write!(f, "MoveToLineStart"), - EditCommand::MoveToEnd => write!(f, "MoveToEnd"), - EditCommand::MoveToLineEnd => write!(f, "MoveToLineEnd"), - EditCommand::MoveLeft => write!(f, "MoveLeft"), - EditCommand::MoveRight => write!(f, "MoveRight"), - EditCommand::MoveWordLeft => write!(f, "MoveWordLeft"), - EditCommand::MoveBigWordLeft => write!(f, "MoveBigWordLeft"), - EditCommand::MoveWordRight => write!(f, "MoveWordRight"), - EditCommand::MoveWordRightEnd => write!(f, "MoveWordRightEnd"), - EditCommand::MoveBigWordRightEnd => write!(f, "MoveBigWordRightEnd"), - EditCommand::MoveWordRightStart => write!(f, "MoveWordRightStart"), - EditCommand::MoveBigWordRightStart => write!(f, "MoveBigWordRightStart"), - EditCommand::MoveToPosition(_) => write!(f, "MoveToPosition Value: "), + EditCommand::MoveToStart { .. } => write!(f, "MoveToStart Optional[select: ]"), + EditCommand::MoveToLineStart { .. } => { + write!(f, "MoveToLineStart Optional[select: ]") + } + EditCommand::MoveToEnd { .. } => write!(f, "MoveToEnd Optional[select: ]"), + EditCommand::MoveToLineEnd { .. } => { + write!(f, "MoveToLineEnd Optional[select: ]") + } + EditCommand::MoveLeft { .. } => write!(f, "MoveLeft Optional[select: ]"), + EditCommand::MoveRight { .. } => write!(f, "MoveRight Optional[select: ]"), + EditCommand::MoveWordLeft { .. } => write!(f, "MoveWordLeft Optional[select: ]"), + EditCommand::MoveBigWordLeft { .. } => { + write!(f, "MoveBigWordLeft Optional[select: ]") + } + EditCommand::MoveWordRight { .. } => { + write!(f, "MoveWordRight Optional[select: ]") + } + EditCommand::MoveWordRightEnd { .. } => { + write!(f, "MoveWordRightEnd Optional[select: ]") + } + EditCommand::MoveBigWordRightEnd { .. } => { + write!(f, "MoveBigWordRightEnd Optional[select: ]") + } + EditCommand::MoveWordRightStart { .. } => { + write!(f, "MoveWordRightStart Optional[select: ]") + } + EditCommand::MoveBigWordRightStart { .. } => { + write!(f, "MoveBigWordRightStart Optional[select: ]") + } + EditCommand::MoveToPosition { .. } => { + write!(f, "MoveToPosition Value: , Optional[select: ]") + } + EditCommand::MoveLeftUntil { .. } => { + write!(f, "MoveLeftUntil Value: , Optional[select: ]") + } + EditCommand::MoveLeftBefore { .. } => { + write!(f, "MoveLeftBefore Value: , Optional[select: ]") + } EditCommand::InsertChar(_) => write!(f, "InsertChar Value: "), EditCommand::InsertString(_) => write!(f, "InsertString Value: "), EditCommand::InsertNewline => write!(f, "InsertNewline"), @@ -244,12 +341,13 @@ impl Display for EditCommand { EditCommand::Redo => write!(f, "Redo"), EditCommand::CutRightUntil(_) => write!(f, "CutRightUntil Value: "), EditCommand::CutRightBefore(_) => write!(f, "CutRightBefore Value: "), - EditCommand::MoveRightUntil(_) => write!(f, "MoveRightUntil Value: "), - EditCommand::MoveRightBefore(_) => write!(f, "MoveRightBefore Value: "), + EditCommand::MoveRightUntil { .. } => write!(f, "MoveRightUntil Value: "), + EditCommand::MoveRightBefore { .. } => write!(f, "MoveRightBefore Value: "), EditCommand::CutLeftUntil(_) => write!(f, "CutLeftUntil Value: "), EditCommand::CutLeftBefore(_) => write!(f, "CutLeftBefore Value: "), - EditCommand::MoveLeftUntil(_) => write!(f, "MoveLeftUntil Value: "), - EditCommand::MoveLeftBefore(_) => write!(f, "MoveLeftBefore Value: "), + EditCommand::SelectAll => write!(f, "SelectAll"), + EditCommand::CutSelection => write!(f, "CutSelection"), + EditCommand::CopySelection => write!(f, "CopySelection"), } } } @@ -260,24 +358,28 @@ impl EditCommand { pub fn edit_type(&self) -> EditType { match self { // Cursor moves - EditCommand::MoveToStart - | EditCommand::MoveToEnd - | EditCommand::MoveToLineStart - | EditCommand::MoveToLineEnd - | EditCommand::MoveToPosition(_) - | EditCommand::MoveLeft - | EditCommand::MoveRight - | EditCommand::MoveWordLeft - | EditCommand::MoveBigWordLeft - | EditCommand::MoveWordRight - | EditCommand::MoveWordRightStart - | EditCommand::MoveBigWordRightStart - | EditCommand::MoveWordRightEnd - | EditCommand::MoveBigWordRightEnd - | EditCommand::MoveRightUntil(_) - | EditCommand::MoveRightBefore(_) - | EditCommand::MoveLeftUntil(_) - | EditCommand::MoveLeftBefore(_) => EditType::MoveCursor, + EditCommand::MoveToStart { select, .. } + | EditCommand::MoveToEnd { select, .. } + | EditCommand::MoveToLineStart { select, .. } + | EditCommand::MoveToLineEnd { select, .. } + | EditCommand::MoveToPosition { select, .. } + | EditCommand::MoveLeft { select, .. } + | EditCommand::MoveRight { select, .. } + | EditCommand::MoveWordLeft { select, .. } + | EditCommand::MoveBigWordLeft { select, .. } + | EditCommand::MoveWordRight { select, .. } + | EditCommand::MoveWordRightStart { select, .. } + | EditCommand::MoveBigWordRightStart { select, .. } + | EditCommand::MoveWordRightEnd { select, .. } + | EditCommand::MoveBigWordRightEnd { select, .. } + | EditCommand::MoveRightUntil { select, .. } + | EditCommand::MoveRightBefore { select, .. } + | EditCommand::MoveLeftUntil { select, .. } + | EditCommand::MoveLeftBefore { select, .. } => { + EditType::MoveCursor { select: *select } + } + + EditCommand::SelectAll => EditType::MoveCursor { select: true }, // Text edits EditCommand::InsertChar(_) @@ -315,9 +417,12 @@ impl EditCommand { | EditCommand::CutRightUntil(_) | EditCommand::CutRightBefore(_) | EditCommand::CutLeftUntil(_) - | EditCommand::CutLeftBefore(_) => EditType::EditText, + | EditCommand::CutLeftBefore(_) + | EditCommand::CutSelection => EditType::EditText, EditCommand::Undo | EditCommand::Redo => EditType::UndoRedo, + + EditCommand::CopySelection => EditType::NoOp, } } } @@ -327,11 +432,13 @@ impl EditCommand { #[derive(PartialEq, Eq)] pub enum EditType { /// Cursor movement commands - MoveCursor, + MoveCursor { select: bool }, /// Undo/Redo commands UndoRedo, /// Text editing commands EditText, + /// No effect on line buffer + NoOp, } /// Every line change should come with an `UndoBehavior` tag, which can be used to diff --git a/src/painting/styled_text.rs b/src/painting/styled_text.rs index eeb88071..f6a5afa0 100644 --- a/src/painting/styled_text.rs +++ b/src/painting/styled_text.rs @@ -5,6 +5,7 @@ use crate::Prompt; use super::utils::strip_ansi; /// A representation of a buffer with styling, used for doing syntax highlighting +#[derive(Clone)] pub struct StyledText { /// The component, styled parts of the text pub buffer: Vec<(Style, String)>, @@ -27,6 +28,67 @@ impl StyledText { self.buffer.push(styled_string); } + /// Style range with the provided style + pub fn style_range(&mut self, from: usize, to: usize, new_style: Style) { + let (from, to) = if from > to { (to, from) } else { (from, to) }; + let mut current_idx = 0; + let mut pair_idx = 0; + while pair_idx < self.buffer.len() { + let pair = &mut self.buffer[pair_idx]; + let end_idx = current_idx + pair.1.len(); + enum Position { + Before, + In, + After, + } + let start_position = if current_idx < from { + Position::Before + } else if current_idx >= to { + Position::After + } else { + Position::In + }; + let end_position = if end_idx < from { + Position::Before + } else if end_idx > to { + Position::After + } else { + Position::In + }; + match (start_position, end_position) { + (Position::Before, Position::After) => { + let mut in_range = pair.1.split_off(from - current_idx); + let after_range = in_range.split_off(to - current_idx - from); + let in_range = (new_style, in_range); + let after_range = (pair.0, after_range); + self.buffer.insert(pair_idx + 1, in_range); + self.buffer.insert(pair_idx + 2, after_range); + break; + } + (Position::Before, Position::In) => { + let in_range = pair.1.split_off(from - current_idx); + pair_idx += 1; // Additional increment for the split pair, since the new insertion is already correctly styled and can be skipped next iteration + self.buffer.insert(pair_idx, (new_style, in_range)); + } + (Position::In, Position::After) => { + let after_range = pair.1.split_off(to - current_idx); + let old_style = pair.0; + pair.0 = new_style; + if !after_range.is_empty() { + self.buffer.insert(pair_idx + 1, (old_style, after_range)); + } + break; + } + (Position::In, Position::In) => pair.0 = new_style, + + (Position::After, _) => break, + _ => (), + } + current_idx = end_idx; + pair_idx += 1; + } + } + /// Render the styled string. We use the insertion point to render around so that /// we can properly write out the styled string to the screen and find the correct /// place to put the cursor. This assumes a logic that prints the first part of the @@ -109,3 +171,88 @@ fn render_as_string( } rendered } + +#[cfg(test)] +mod test { + use nu_ansi_term::{Color, Style}; + + use crate::StyledText; + + fn get_styled_text_template() -> (super::StyledText, Style, Style) { + let before_style = Style::new().on(Color::Black); + let after_style = Style::new().on(Color::Red); + ( + super::StyledText { + buffer: vec![ + (before_style, "aaa".into()), + (before_style, "bbb".into()), + (before_style, "ccc".into()), + ], + }, + before_style, + after_style, + ) + } + #[test] + fn style_range_partial_update_one_part() { + let (styled_text_template, before_style, after_style) = get_styled_text_template(); + let mut styled_text = styled_text_template.clone(); + styled_text.style_range(0, 1, after_style); + assert_eq!(styled_text.buffer[0], (after_style, "a".into())); + assert_eq!(styled_text.buffer[1], (before_style, "aa".into())); + assert_eq!(styled_text.buffer[2], (before_style, "bbb".into())); + assert_eq!(styled_text.buffer[3], (before_style, "ccc".into())); + } + #[test] + fn style_range_complete_update_one_part() { + let (styled_text_template, before_style, after_style) = get_styled_text_template(); + let mut styled_text = styled_text_template.clone(); + styled_text.style_range(0, 3, after_style); + assert_eq!(styled_text.buffer[0], (after_style, "aaa".into())); + assert_eq!(styled_text.buffer[1], (before_style, "bbb".into())); + assert_eq!(styled_text.buffer[2], (before_style, "ccc".into())); + assert_eq!(styled_text.buffer.len(), 3); + } + #[test] + fn style_range_update_over_boundary() { + let (styled_text_template, before_style, after_style) = get_styled_text_template(); + let mut styled_text = styled_text_template; + styled_text.style_range(0, 5, after_style); + assert_eq!(styled_text.buffer[0], (after_style, "aaa".into())); + assert_eq!(styled_text.buffer[1], (after_style, "bb".into())); + assert_eq!(styled_text.buffer[2], (before_style, "b".into())); + assert_eq!(styled_text.buffer[3], (before_style, "ccc".into())); + } + #[test] + fn style_range_update_over_part() { + let (styled_text_template, before_style, after_style) = get_styled_text_template(); + let mut styled_text = styled_text_template; + styled_text.style_range(1, 7, after_style); + assert_eq!(styled_text.buffer[0], (before_style, "a".into())); + assert_eq!(styled_text.buffer[1], (after_style, "aa".into())); + assert_eq!(styled_text.buffer[2], (after_style, "bbb".into())); + assert_eq!(styled_text.buffer[3], (after_style, "c".into())); + assert_eq!(styled_text.buffer[4], (before_style, "cc".into())); + } + #[test] + fn style_range_last_letter() { + let (_, before_style, after_style) = get_styled_text_template(); + let mut styled_text = StyledText { + buffer: vec![(before_style, "asdf".into())], + }; + styled_text.style_range(3, 4, after_style); + assert_eq!(styled_text.buffer[0], (before_style, "asd".into())); + assert_eq!(styled_text.buffer[1], (after_style, "f".into())); + } + #[test] + fn style_range_from_second_to_last() { + let (_, before_style, after_style) = get_styled_text_template(); + let mut styled_text = StyledText { + buffer: vec![(before_style, "asdf".into())], + }; + styled_text.style_range(2, 3, after_style); + assert_eq!(styled_text.buffer[0], (before_style, "as".into())); + assert_eq!(styled_text.buffer[1], (after_style, "d".into())); + assert_eq!(styled_text.buffer[2], (before_style, "f".into())); + } +}