diff --git a/src/core_editor/editor.rs b/src/core_editor/editor.rs index 3cabe1ae..5bff2856 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, } } } @@ -96,6 +97,11 @@ impl Editor { 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::SelectMoveLeft => self.select_move_left(), + EditCommand::SelectMoveRight => self.select_move_right(), + EditCommand::SelectAll => self.select_all(), + EditCommand::CutSelection => self.cut_selection(), + EditCommand::CopySelection => self.copy_selection(), } let new_undo_behavior = match (command, command.edit_type()) { @@ -112,6 +118,12 @@ impl Editor { (_, EditType::UndoRedo) => UndoBehavior::UndoRedo, (_, _) => UndoBehavior::CreateUndoPoint, }; + if !matches!( + command, + EditCommand::SelectMoveLeft | EditCommand::SelectMoveRight | EditCommand::SelectAll + ) { + self.reset_selection(); + } self.update_undo_state(new_undo_behavior); } @@ -462,6 +474,65 @@ impl Editor { self.line_buffer.insert_str(string); } + + fn select_move_left(&mut self) { + if self.selection_anchor.is_none() { + self.selection_anchor = Some(self.insertion_point()); + } + self.line_buffer.move_left(); + } + + fn select_move_right(&mut self) { + if self.selection_anchor.is_none() { + self.selection_anchor = Some(self.insertion_point()); + } + 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(selection_anchor) = self.selection_anchor { + let (start, end) = if self.insertion_point() > selection_anchor { + (selection_anchor, self.insertion_point()) + } else { + (self.insertion_point(), selection_anchor) + }; + let cut_slice = &self.line_buffer.get_buffer()[start..end]; + self.cut_buffer.set(cut_slice, ClipboardMode::Normal); + if self.insertion_point() <= start { + // No action necessary + } else if self.insertion_point() < end { + // Insertion point is in selected area + self.line_buffer.set_insertion_point(start); + } else { + // Insertion point after end + self.line_buffer + .set_insertion_point(self.insertion_point() - (end - start)); + } + self.line_buffer + .clear_range_safe(self.insertion_point(), selection_anchor); + } + } + + fn copy_selection(&mut self) { + if let Some(selection_anchor) = self.selection_anchor { + let (start, end) = if self.insertion_point() > selection_anchor { + (selection_anchor, self.insertion_point()) + } else { + (self.insertion_point(), selection_anchor) + }; + let cut_slice = &self.line_buffer.get_buffer()[start..end]; + self.cut_buffer.set(cut_slice, ClipboardMode::Normal); + } + } + + fn reset_selection(&mut self) { + self.selection_anchor = None; + } } #[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..0b513b4c 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,6 +54,7 @@ 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)); @@ -84,8 +86,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..7cd3bae1 100644 --- a/src/edit_mode/keybindings.rs +++ b/src/edit_mode/keybindings.rs @@ -194,4 +194,21 @@ 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::SelectMoveLeft)); + kb.add_binding(KM::SHIFT, KC::Right, edit_bind(EC::SelectMoveRight)); + kb.add_binding(KM::CONTROL, KC::Char('a'), edit_bind(EC::SelectAll)); } diff --git a/src/edit_mode/vi/vi_keybindings.rs b/src/edit_mode/vi/vi_keybindings.rs index 1498f3ac..0a08be23 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,6 +20,7 @@ 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::Delete, edit_bind(EC::Delete)); @@ -34,6 +35,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/enums.rs b/src/enums.rs index 210dac8e..188cea39 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -189,6 +189,21 @@ pub enum EditCommand { /// CutUntil left before char MoveLeftBefore(char), + + /// Select and move left + SelectMoveLeft, + + /// Select and move right + SelectMoveRight, + + /// Select whole input buffer + SelectAll, + + /// Cut selection + CutSelection, + + /// Copy selection + CopySelection, } impl Display for EditCommand { @@ -250,6 +265,11 @@ impl Display for EditCommand { EditCommand::CutLeftBefore(_) => write!(f, "CutLeftBefore Value: "), EditCommand::MoveLeftUntil(_) => write!(f, "MoveLeftUntil Value: "), EditCommand::MoveLeftBefore(_) => write!(f, "MoveLeftBefore Value: "), + EditCommand::SelectMoveLeft => write!(f, "SelectMoveLeft"), + EditCommand::SelectMoveRight => write!(f, "SelectMoveRight"), + EditCommand::SelectAll => write!(f, "SelectAll"), + EditCommand::CutSelection => write!(f, "CutSelection"), + EditCommand::CopySelection => write!(f, "CopySelection"), } } } @@ -277,7 +297,10 @@ impl EditCommand { | EditCommand::MoveRightUntil(_) | EditCommand::MoveRightBefore(_) | EditCommand::MoveLeftUntil(_) - | EditCommand::MoveLeftBefore(_) => EditType::MoveCursor, + | EditCommand::MoveLeftBefore(_) + | EditCommand::SelectMoveLeft + | EditCommand::SelectMoveRight + | EditCommand::SelectAll => EditType::MoveCursor, // Text edits EditCommand::InsertChar(_) @@ -315,9 +338,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, } } } @@ -332,6 +358,8 @@ pub enum EditType { 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