Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to select and cut text in the input buffer #689

Merged
merged 6 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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