Skip to content

Commit 2f3eb3e

Browse files
Add ability to select and cut text in the input buffer (#689)
* 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
1 parent dc27ed8 commit 2f3eb3e

11 files changed

+745
-151
lines changed

src/core_editor/editor.rs

+194-37
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ use crate::{core_editor::get_default_clipboard, EditCommand};
99
pub struct Editor {
1010
line_buffer: LineBuffer,
1111
cut_buffer: Box<dyn Clipboard>,
12-
1312
edit_stack: EditStack<LineBuffer>,
1413
last_undo_behavior: UndoBehavior,
14+
selection_anchor: Option<usize>,
1515
}
1616

1717
impl Default for Editor {
@@ -21,6 +21,7 @@ impl Default for Editor {
2121
cut_buffer: Box::new(get_default_clipboard()),
2222
edit_stack: EditStack::new(),
2323
last_undo_behavior: UndoBehavior::CreateUndoPoint,
24+
selection_anchor: None,
2425
}
2526
}
2627
}
@@ -40,28 +41,32 @@ impl Editor {
4041

4142
pub(crate) fn run_edit_command(&mut self, command: &EditCommand) {
4243
match command {
43-
EditCommand::MoveToStart => self.line_buffer.move_to_start(),
44-
EditCommand::MoveToLineStart => self.line_buffer.move_to_line_start(),
45-
EditCommand::MoveToEnd => self.line_buffer.move_to_end(),
46-
EditCommand::MoveToLineEnd => self.line_buffer.move_to_line_end(),
47-
EditCommand::MoveToPosition(pos) => self.line_buffer.set_insertion_point(*pos),
48-
EditCommand::MoveLeft => self.line_buffer.move_left(),
49-
EditCommand::MoveRight => self.line_buffer.move_right(),
50-
EditCommand::MoveWordLeft => self.line_buffer.move_word_left(),
51-
EditCommand::MoveBigWordLeft => self.line_buffer.move_big_word_left(),
52-
EditCommand::MoveWordRight => self.line_buffer.move_word_right(),
53-
EditCommand::MoveWordRightStart => self.line_buffer.move_word_right_start(),
54-
EditCommand::MoveBigWordRightStart => self.line_buffer.move_big_word_right_start(),
55-
EditCommand::MoveWordRightEnd => self.line_buffer.move_word_right_end(),
56-
EditCommand::MoveBigWordRightEnd => self.line_buffer.move_big_word_right_end(),
57-
EditCommand::InsertChar(c) => self.line_buffer.insert_char(*c),
44+
EditCommand::MoveToStart { select } => self.move_to_start(*select),
45+
EditCommand::MoveToLineStart { select } => self.move_to_line_start(*select),
46+
EditCommand::MoveToEnd { select } => self.move_to_end(*select),
47+
EditCommand::MoveToLineEnd { select } => self.move_to_line_end(*select),
48+
EditCommand::MoveToPosition { position, select } => {
49+
self.move_to_position(*position, *select)
50+
}
51+
EditCommand::MoveLeft { select } => self.move_left(*select),
52+
EditCommand::MoveRight { select } => self.move_right(*select),
53+
EditCommand::MoveWordLeft { select } => self.move_word_left(*select),
54+
EditCommand::MoveBigWordLeft { select } => self.move_big_word_left(*select),
55+
EditCommand::MoveWordRight { select } => self.move_word_right(*select),
56+
EditCommand::MoveWordRightStart { select } => self.move_word_right_start(*select),
57+
EditCommand::MoveBigWordRightStart { select } => {
58+
self.move_big_word_right_start(*select)
59+
}
60+
EditCommand::MoveWordRightEnd { select } => self.move_word_right_end(*select),
61+
EditCommand::MoveBigWordRightEnd { select } => self.move_big_word_right_end(*select),
62+
EditCommand::InsertChar(c) => self.insert_char(*c),
5863
EditCommand::Complete => {}
59-
EditCommand::InsertString(str) => self.line_buffer.insert_str(str),
60-
EditCommand::InsertNewline => self.line_buffer.insert_newline(),
64+
EditCommand::InsertString(str) => self.insert_str(str),
65+
EditCommand::InsertNewline => self.insert_newline(),
6166
EditCommand::ReplaceChar(chr) => self.replace_char(*chr),
6267
EditCommand::ReplaceChars(n_chars, str) => self.replace_chars(*n_chars, str),
63-
EditCommand::Backspace => self.line_buffer.delete_left_grapheme(),
64-
EditCommand::Delete => self.line_buffer.delete_right_grapheme(),
68+
EditCommand::Backspace => self.backspace(),
69+
EditCommand::Delete => self.delete(),
6570
EditCommand::CutChar => self.cut_char(),
6671
EditCommand::BackspaceWord => self.line_buffer.delete_word_left(),
6772
EditCommand::DeleteWord => self.line_buffer.delete_word_right(),
@@ -90,16 +95,31 @@ impl Editor {
9095
EditCommand::Redo => self.redo(),
9196
EditCommand::CutRightUntil(c) => self.cut_right_until_char(*c, false, true),
9297
EditCommand::CutRightBefore(c) => self.cut_right_until_char(*c, true, true),
93-
EditCommand::MoveRightUntil(c) => self.move_right_until_char(*c, false, true),
94-
EditCommand::MoveRightBefore(c) => self.move_right_until_char(*c, true, true),
98+
EditCommand::MoveRightUntil { c, select } => {
99+
self.move_right_until_char(*c, false, true, *select)
100+
}
101+
EditCommand::MoveRightBefore { c, select } => {
102+
self.move_right_until_char(*c, true, true, *select)
103+
}
95104
EditCommand::CutLeftUntil(c) => self.cut_left_until_char(*c, false, true),
96105
EditCommand::CutLeftBefore(c) => self.cut_left_until_char(*c, true, true),
97-
EditCommand::MoveLeftUntil(c) => self.move_left_until_char(*c, false, true),
98-
EditCommand::MoveLeftBefore(c) => self.move_left_until_char(*c, true, true),
106+
EditCommand::MoveLeftUntil { c, select } => {
107+
self.move_left_until_char(*c, false, true, *select)
108+
}
109+
EditCommand::MoveLeftBefore { c, select } => {
110+
self.move_left_until_char(*c, true, true, *select)
111+
}
112+
EditCommand::SelectAll => self.select_all(),
113+
EditCommand::CutSelection => self.cut_selection(),
114+
EditCommand::CopySelection => self.copy_selection(),
99115
}
116+
if !matches!(command.edit_type(), EditType::MoveCursor { select: true }) {
117+
self.selection_anchor = None;
118+
}
119+
if let EditType::MoveCursor { select: true } = command.edit_type() {}
100120

101121
let new_undo_behavior = match (command, command.edit_type()) {
102-
(_, EditType::MoveCursor) => UndoBehavior::MoveCursor,
122+
(_, EditType::MoveCursor { .. }) => UndoBehavior::MoveCursor,
103123
(EditCommand::InsertChar(c), EditType::EditText) => UndoBehavior::InsertCharacter(*c),
104124
(EditCommand::Delete, EditType::EditText) => {
105125
let deleted_char = self.edit_stack.current().grapheme_right().chars().next();
@@ -112,8 +132,21 @@ impl Editor {
112132
(_, EditType::UndoRedo) => UndoBehavior::UndoRedo,
113133
(_, _) => UndoBehavior::CreateUndoPoint,
114134
};
135+
115136
self.update_undo_state(new_undo_behavior);
116137
}
138+
fn update_selection_anchor(&mut self, select: bool) {
139+
self.selection_anchor = if select {
140+
self.selection_anchor
141+
.or_else(|| Some(self.insertion_point()))
142+
} else {
143+
None
144+
};
145+
}
146+
fn move_to_position(&mut self, position: usize, select: bool) {
147+
self.update_selection_anchor(select);
148+
self.line_buffer.set_insertion_point(position)
149+
}
117150

118151
pub(crate) fn move_line_up(&mut self) {
119152
self.line_buffer.move_line_up();
@@ -170,25 +203,24 @@ impl Editor {
170203
self.edit_stack.reset();
171204
}
172205

173-
pub(crate) fn move_to_start(&mut self, undo_behavior: UndoBehavior) {
206+
pub(crate) fn move_to_start(&mut self, select: bool) {
207+
self.update_selection_anchor(select);
174208
self.line_buffer.move_to_start();
175-
self.update_undo_state(undo_behavior);
176209
}
177210

178-
pub(crate) fn move_to_end(&mut self, undo_behavior: UndoBehavior) {
211+
pub(crate) fn move_to_end(&mut self, select: bool) {
212+
self.update_selection_anchor(select);
179213
self.line_buffer.move_to_end();
180-
self.update_undo_state(undo_behavior);
181214
}
182215

183-
#[allow(dead_code)]
184-
pub(crate) fn move_to_line_start(&mut self, undo_behavior: UndoBehavior) {
216+
pub(crate) fn move_to_line_start(&mut self, select: bool) {
217+
self.update_selection_anchor(select);
185218
self.line_buffer.move_to_line_start();
186-
self.update_undo_state(undo_behavior);
187219
}
188220

189-
pub(crate) fn move_to_line_end(&mut self, undo_behavior: UndoBehavior) {
221+
pub(crate) fn move_to_line_end(&mut self, select: bool) {
222+
self.update_selection_anchor(select);
190223
self.line_buffer.move_to_line_end();
191-
self.update_undo_state(undo_behavior);
192224
}
193225

194226
fn undo(&mut self) {
@@ -201,7 +233,7 @@ impl Editor {
201233
self.line_buffer = val.clone();
202234
}
203235

204-
fn update_undo_state(&mut self, undo_behavior: UndoBehavior) {
236+
pub(crate) fn update_undo_state(&mut self, undo_behavior: UndoBehavior) {
205237
if matches!(undo_behavior, UndoBehavior::UndoRedo) {
206238
self.last_undo_behavior = UndoBehavior::UndoRedo;
207239
return;
@@ -357,6 +389,7 @@ impl Editor {
357389
}
358390

359391
fn insert_cut_buffer_before(&mut self) {
392+
self.delete_selection();
360393
match self.cut_buffer.get() {
361394
(content, ClipboardMode::Normal) => {
362395
self.line_buffer.insert_str(&content);
@@ -375,6 +408,7 @@ impl Editor {
375408
}
376409

377410
fn insert_cut_buffer_after(&mut self) {
411+
self.delete_selection();
378412
match self.cut_buffer.get() {
379413
(content, ClipboardMode::Normal) => {
380414
self.line_buffer.move_right();
@@ -393,15 +427,29 @@ impl Editor {
393427
}
394428
}
395429

396-
fn move_right_until_char(&mut self, c: char, before_char: bool, current_line: bool) {
430+
fn move_right_until_char(
431+
&mut self,
432+
c: char,
433+
before_char: bool,
434+
current_line: bool,
435+
select: bool,
436+
) {
437+
self.update_selection_anchor(select);
397438
if before_char {
398439
self.line_buffer.move_right_before(c, current_line);
399440
} else {
400441
self.line_buffer.move_right_until(c, current_line);
401442
}
402443
}
403444

404-
fn move_left_until_char(&mut self, c: char, before_char: bool, current_line: bool) {
445+
fn move_left_until_char(
446+
&mut self,
447+
c: char,
448+
before_char: bool,
449+
current_line: bool,
450+
select: bool,
451+
) {
452+
self.update_selection_anchor(select);
405453
if before_char {
406454
self.line_buffer.move_left_before(c, current_line);
407455
} else {
@@ -462,6 +510,115 @@ impl Editor {
462510

463511
self.line_buffer.insert_str(string);
464512
}
513+
514+
fn move_left(&mut self, select: bool) {
515+
self.update_selection_anchor(select);
516+
self.line_buffer.move_left();
517+
}
518+
519+
fn move_right(&mut self, select: bool) {
520+
self.update_selection_anchor(select);
521+
self.line_buffer.move_right();
522+
}
523+
524+
fn select_all(&mut self) {
525+
self.selection_anchor = Some(0);
526+
self.line_buffer.move_to_end();
527+
}
528+
529+
fn cut_selection(&mut self) {
530+
if let Some((start, end)) = self.get_selection() {
531+
let cut_slice = &self.line_buffer.get_buffer()[start..end];
532+
self.cut_buffer.set(cut_slice, ClipboardMode::Normal);
533+
self.line_buffer.clear_range_safe(start, end);
534+
self.selection_anchor = None;
535+
}
536+
}
537+
538+
fn copy_selection(&mut self) {
539+
if let Some((start, end)) = self.get_selection() {
540+
let cut_slice = &self.line_buffer.get_buffer()[start..end];
541+
self.cut_buffer.set(cut_slice, ClipboardMode::Normal);
542+
}
543+
}
544+
545+
/// If a selection is active returns the selected range, otherwise None.
546+
/// The range is guaranteed to be ascending.
547+
pub fn get_selection(&self) -> Option<(usize, usize)> {
548+
self.selection_anchor.map(|selection_anchor| {
549+
if self.insertion_point() > selection_anchor {
550+
(selection_anchor, self.insertion_point())
551+
} else {
552+
(self.insertion_point(), selection_anchor)
553+
}
554+
})
555+
}
556+
557+
fn delete_selection(&mut self) {
558+
if let Some((start, end)) = self.get_selection() {
559+
self.line_buffer.clear_range_safe(start, end);
560+
self.selection_anchor = None;
561+
}
562+
}
563+
564+
fn backspace(&mut self) {
565+
if self.selection_anchor.is_some() {
566+
self.delete_selection();
567+
} else {
568+
self.line_buffer.delete_left_grapheme();
569+
}
570+
}
571+
572+
fn delete(&mut self) {
573+
if self.selection_anchor.is_some() {
574+
self.delete_selection();
575+
} else {
576+
self.line_buffer.delete_right_grapheme();
577+
}
578+
}
579+
580+
fn move_word_left(&mut self, select: bool) {
581+
self.move_to_position(self.line_buffer.word_left_index(), select);
582+
}
583+
584+
fn move_big_word_left(&mut self, select: bool) {
585+
self.move_to_position(self.line_buffer.big_word_left_index(), select);
586+
}
587+
588+
fn move_word_right(&mut self, select: bool) {
589+
self.move_to_position(self.line_buffer.word_right_index(), select);
590+
}
591+
592+
fn move_word_right_start(&mut self, select: bool) {
593+
self.move_to_position(self.line_buffer.word_right_start_index(), select);
594+
}
595+
596+
fn move_big_word_right_start(&mut self, select: bool) {
597+
self.move_to_position(self.line_buffer.big_word_right_start_index(), select);
598+
}
599+
600+
fn move_word_right_end(&mut self, select: bool) {
601+
self.move_to_position(self.line_buffer.word_right_end_index(), select);
602+
}
603+
604+
fn move_big_word_right_end(&mut self, select: bool) {
605+
self.move_to_position(self.line_buffer.big_word_right_end_index(), select);
606+
}
607+
608+
fn insert_char(&mut self, c: char) {
609+
self.delete_selection();
610+
self.line_buffer.insert_char(c);
611+
}
612+
613+
fn insert_str(&mut self, str: &str) {
614+
self.delete_selection();
615+
self.line_buffer.insert_str(str);
616+
}
617+
618+
fn insert_newline(&mut self) {
619+
self.delete_selection();
620+
self.line_buffer.insert_newline();
621+
}
465622
}
466623

467624
#[cfg(test)]

src/core_editor/line_buffer.rs

+21
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,27 @@ impl LineBuffer {
396396
self.insertion_point = 0;
397397
}
398398

399+
/// Clear all contents between `start` and `end` and change insertion point if necessary.
400+
///
401+
/// If the cursor is located between `start` and `end` it is adjusted to `start`.
402+
/// If the cursor is located after `end` it is adjusted to stay at its current char boundary.
403+
pub fn clear_range_safe(&mut self, start: usize, end: usize) {
404+
let (start, end) = if start > end {
405+
(end, start)
406+
} else {
407+
(start, end)
408+
};
409+
if self.insertion_point <= start {
410+
// No action necessary
411+
} else if self.insertion_point < end {
412+
self.insertion_point = start;
413+
} else {
414+
// Insertion point after end
415+
self.insertion_point -= end - start;
416+
}
417+
self.clear_range(start..end);
418+
}
419+
399420
/// Clear text covered by `range` in the current line
400421
///
401422
/// Safety: Does not change the insertion point/offset and is thus not unicode safe!

0 commit comments

Comments
 (0)