Skip to content

Commit

Permalink
Add ability to select and cut text in the input buffer (#689)
Browse files Browse the repository at this point in the history
* Add ability to select and cut text in the input buffer

* Add visual selection effect

* Add SelectMoveWord<Left/Right> command on Shift + Ctrl + Arrow

* Add ability to delete selection with EditCommands 'Delete' and 'Backspace'

* Make selection an option on every move EditCommand

* Add display text for optional 'select' parameter to move EditCommands
  • Loading branch information
Tastaturtaste authored Jan 17, 2024
1 parent dc27ed8 commit 2f3eb3e
Show file tree
Hide file tree
Showing 11 changed files with 745 additions and 151 deletions.
231 changes: 194 additions & 37 deletions src/core_editor/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ use crate::{core_editor::get_default_clipboard, EditCommand};
pub struct Editor {
line_buffer: LineBuffer,
cut_buffer: Box<dyn Clipboard>,

edit_stack: EditStack<LineBuffer>,
last_undo_behavior: UndoBehavior,
selection_anchor: Option<usize>,
}

impl Default for Editor {
Expand All @@ -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,
}
}
}
Expand All @@ -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(),
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down Expand 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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -393,15 +427,29 @@ 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 {
self.line_buffer.move_right_until(c, current_line);
}
}

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 {
Expand Down Expand Up @@ -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)]
Expand Down
21 changes: 21 additions & 0 deletions src/core_editor/line_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
Loading

0 comments on commit 2f3eb3e

Please sign in to comment.