use super::{edit_stack::EditStack, CaretGeometry, Clipboard, Cursor, LineBuffer, Movement};
#[cfg(feature = "system_clipboard")]
use crate::core_editor::get_system_clipboard;
use crate::core_editor::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
use crate::core_editor::{commit, line, operator_span, resolve_motion, RestPolicy};
use crate::enums::{EditType, TextObject, TextObjectScope, TextObjectType, UndoBehavior};
use crate::prompt::PromptEditMode;
use crate::{core_editor::get_local_clipboard, EditCommand};
use crate::{Direction, Granularity, MotionTarget, WordEdge, WordKind};
use std::cmp::{max, min};
use std::ops::{DerefMut, Range};
pub struct Editor {
line_buffer: LineBuffer,
cut_buffer: Box<dyn Clipboard>,
#[cfg(feature = "system_clipboard")]
system_clipboard: Box<dyn Clipboard>,
edit_stack: EditStack<LineBuffer>,
last_undo_behavior: UndoBehavior,
edit_mode: PromptEditMode,
policy_unsettled: bool,
cross_line_cursor: bool,
}
enum OperatorVerb {
Cut,
Copy,
Change,
Erase,
}
fn word_target(kind: WordKind, edge: WordEdge, direction: Direction) -> MotionTarget {
MotionTarget::Word {
kind,
edge,
direction,
}
}
impl Default for Editor {
fn default() -> Self {
Editor {
line_buffer: LineBuffer::new(),
cut_buffer: get_local_clipboard(),
#[cfg(feature = "system_clipboard")]
system_clipboard: get_system_clipboard(),
edit_stack: EditStack::new(),
last_undo_behavior: UndoBehavior::CreateUndoPoint,
edit_mode: PromptEditMode::Default,
policy_unsettled: false,
cross_line_cursor: true,
}
}
}
impl Editor {
pub const fn line_buffer(&self) -> &LineBuffer {
&self.line_buffer
}
pub(crate) fn set_line_buffer(&mut self, line_buffer: LineBuffer, undo_behavior: UndoBehavior) {
self.line_buffer = line_buffer;
self.update_undo_state(undo_behavior);
}
pub(crate) fn run_edit_command(&mut self, command: &EditCommand) {
match command {
EditCommand::MoveToStart { select } => self.move_to_start(*select),
EditCommand::MoveToLineStart { select } => self.move_to_line_start(*select),
EditCommand::MoveToLineNonBlankStart { select } => {
self.move_to_line_non_blank_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_head_to(*position, *select)
}
EditCommand::MoveLineUp { select } => self.move_line_up(*select),
EditCommand::MoveLineDown { select } => self.move_line_down(*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::Move(t) => {
let head = self.resolve_head(*t);
self.move_head_to(head, false);
}
EditCommand::Extend(t) => {
let head = self.resolve_head(*t);
self.move_head_to(head, true);
}
EditCommand::Cut {
target,
granularity,
} => {
let sel = operator_span(
self.get_buffer(),
self.insertion_point(),
*target,
self.caret_geometry(),
);
self.operate(sel, OperatorVerb::Cut, *granularity);
}
EditCommand::Copy {
target,
granularity,
} => {
let sel = operator_span(
self.get_buffer(),
self.insertion_point(),
*target,
self.caret_geometry(),
);
self.operate(sel, OperatorVerb::Copy, *granularity);
}
EditCommand::Change {
target,
granularity,
} => {
let sel = operator_span(
self.get_buffer(),
self.insertion_point(),
*target,
self.caret_geometry(),
);
self.operate(sel, OperatorVerb::Change, *granularity);
}
EditCommand::Erase(t) => {
let sel = operator_span(
self.get_buffer(),
self.insertion_point(),
*t,
self.caret_geometry(),
);
self.operate(sel, OperatorVerb::Erase, Granularity::CharWise);
}
EditCommand::InsertChar(c) => self.insert_char(*c),
EditCommand::Complete => {}
EditCommand::InsertString(str) => self.insert_str(str),
EditCommand::InsertNewline => self.insert_newline(),
EditCommand::InsertNewlineAbove => self.insert_newline_above(),
EditCommand::InsertNewlineBelow => self.insert_newline_below(),
EditCommand::ReplaceChar(chr) => self.replace_char(*chr),
EditCommand::ReplaceChars(n_chars, str) => self.replace_chars(*n_chars, str),
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(),
EditCommand::Clear => self.line_buffer.clear(),
EditCommand::ClearToLineEnd => self.line_buffer.clear_to_line_end(),
EditCommand::CutCurrentLine => self.cut_current_line(),
EditCommand::CutFromStart => self.cut_from_start(),
EditCommand::CutFromStartLinewise { leave_blank_line } => {
self.cut_from_start_linewise(*leave_blank_line)
}
EditCommand::CutFromLineStart => self.cut_from_line_start(),
EditCommand::CutFromLineNonBlankStart => self.cut_from_line_non_blank_start(),
EditCommand::CutToEnd => self.cut_from_end(),
EditCommand::CutToEndLinewise { leave_blank_line } => {
self.cut_from_end_linewise(*leave_blank_line)
}
EditCommand::CutToLineEnd => self.cut_to_line_end(),
EditCommand::KillLine => self.kill_line(),
EditCommand::CutWordLeft => self.cut_word_left(),
EditCommand::CutBigWordLeft => self.cut_big_word_left(),
EditCommand::CutWordRight => self.cut_word_right(),
EditCommand::CutBigWordRight => self.cut_big_word_right(),
EditCommand::CutWordRightToNext => self.cut_word_right_to_next(),
EditCommand::CutBigWordRightToNext => self.cut_big_word_right_to_next(),
EditCommand::PasteCutBufferBefore => self.insert_cut_buffer_before(),
EditCommand::PasteCutBufferAfter => self.insert_cut_buffer_after(),
EditCommand::UppercaseWord => self.line_buffer.uppercase_word(),
EditCommand::LowercaseWord => self.line_buffer.lowercase_word(),
EditCommand::SwitchcaseChar => self.line_buffer.switchcase_char(),
EditCommand::CapitalizeChar => self.line_buffer.capitalize_char(),
EditCommand::SwapWords => self.line_buffer.swap_words(),
EditCommand::SwapGraphemes => self.line_buffer.swap_graphemes(),
EditCommand::Undo => self.undo(),
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, 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, 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_to_cut_buffer(),
EditCommand::CopySelection => self.copy_selection_to_cut_buffer(),
EditCommand::Paste => self.paste_cut_buffer(),
EditCommand::CopyFromStart => self.copy_from_start(),
EditCommand::CopyFromStartLinewise => self.copy_from_start_linewise(),
EditCommand::CopyFromLineStart => self.copy_from_line_start(),
EditCommand::CopyFromLineNonBlankStart => self.copy_from_line_non_blank_start(),
EditCommand::CopyToEnd => self.copy_from_end(),
EditCommand::CopyToEndLinewise => self.copy_from_end_linewise(),
EditCommand::CopyToLineEnd => self.copy_to_line_end(),
EditCommand::CopyWordLeft => self.copy_word_left(),
EditCommand::CopyBigWordLeft => self.copy_big_word_left(),
EditCommand::CopyWordRight => self.copy_word_right(),
EditCommand::CopyBigWordRight => self.copy_big_word_right(),
EditCommand::CopyWordRightToNext => self.copy_word_right_to_next(),
EditCommand::CopyBigWordRightToNext => self.copy_big_word_right_to_next(),
EditCommand::CopyRightUntil(c) => self.copy_right_until_char(*c, false, true),
EditCommand::CopyRightBefore(c) => self.copy_right_until_char(*c, true, true),
EditCommand::CopyLeftUntil(c) => self.copy_left_until_char(*c, false, true),
EditCommand::CopyLeftBefore(c) => self.copy_left_until_char(*c, true, true),
EditCommand::CopyCurrentLine => {
let range = self.line_buffer.current_line_range();
let copy_slice = &self.line_buffer.get_buffer()[range];
if !copy_slice.is_empty() {
self.cut_buffer.set(copy_slice, Granularity::LineWise);
}
}
EditCommand::CopyLeft => {
let insertion_offset = self.line_buffer.insertion_point();
if insertion_offset > 0 {
let left_index = self.line_buffer.grapheme_left_index();
let copy_range = left_index..insertion_offset;
self.cut_buffer.set(
&self.line_buffer.get_buffer()[copy_range],
Granularity::CharWise,
);
}
}
EditCommand::CopyRight => {
let insertion_offset = self.line_buffer.insertion_point();
let right_index = self.line_buffer.grapheme_right_index();
if right_index > insertion_offset {
let copy_range = insertion_offset..right_index;
self.cut_buffer.set(
&self.line_buffer.get_buffer()[copy_range],
Granularity::CharWise,
);
}
}
EditCommand::SwapCursorAndAnchor => self
.line_buffer
.set_cursor(self.line_buffer.cursor().flip()),
#[cfg(feature = "system_clipboard")]
EditCommand::CutSelectionSystem => self.cut_selection_to_system(),
#[cfg(feature = "system_clipboard")]
EditCommand::CopySelectionSystem => self.copy_selection_to_system(),
#[cfg(feature = "system_clipboard")]
EditCommand::PasteSystem => self.paste_from_system(),
EditCommand::CutInsidePair { left, right } => self.cut_inside_pair(*left, *right),
EditCommand::CopyInsidePair { left, right } => self.copy_inside_pair(*left, *right),
EditCommand::CutAroundPair { left, right } => self.cut_around_pair(*left, *right),
EditCommand::CopyAroundPair { left, right } => self.copy_around_pair(*left, *right),
EditCommand::CutTextObject { text_object } => self.cut_text_object(*text_object),
EditCommand::CopyTextObject { text_object } => self.copy_text_object(*text_object),
}
if !matches!(command.edit_type(), EditType::MoveCursor { select: true }) {
self.clear_selection();
}
self.commit_cursor();
let new_undo_behavior = match (command, command.edit_type()) {
(_, 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();
UndoBehavior::Delete(deleted_char)
}
(EditCommand::Backspace, EditType::EditText) => {
let deleted_char = self.edit_stack.current().grapheme_left().chars().next();
UndoBehavior::Backspace(deleted_char)
}
(_, EditType::UndoRedo | EditType::NoOp) => UndoBehavior::NoOp,
(_, _) => UndoBehavior::CreateUndoPoint,
};
self.update_undo_state(new_undo_behavior);
}
pub(crate) fn clear_selection(&mut self) {
let caret = self.line_buffer.insertion_point();
self.line_buffer.set_cursor(Cursor::point(caret));
}
fn operate(&mut self, selection: Cursor, verb: OperatorVerb, granularity: Granularity) {
let (register, delete) = match granularity {
Granularity::CharWise => {
let r = selection.start()..selection.end();
(r.clone(), r)
}
Granularity::LineWise => {
let buf = self.get_buffer();
let s = line::start_of_line(buf, selection.start());
match verb {
OperatorVerb::Change => {
let r = s..line::end_of_line(buf, selection.end());
(r.clone(), r)
}
_ => {
let e = line::start_of_next_line(buf, selection.end()).unwrap_or(buf.len());
let delete_start = if e == buf.len() && s > 0 {
if buf[..s].ends_with("\r\n") {
s - 2
} else {
s - 1
}
} else {
s
};
(s..e, delete_start..e)
}
}
}
};
match verb {
OperatorVerb::Cut => {
self.copy_range_with(register, granularity);
self.line_buffer.clear_range_safe(delete.clone());
self.line_buffer.set_insertion_point(delete.start);
}
OperatorVerb::Change => self.cut_range_with(delete, granularity),
OperatorVerb::Copy => self.copy_range_with(register, granularity),
OperatorVerb::Erase => {
self.line_buffer.clear_range_safe(delete.clone());
self.line_buffer.set_insertion_point(delete.start);
}
}
}
#[cfg(test)]
fn update_selection_anchor(&mut self, select: bool) {
if select {
if self.line_buffer.selection_anchor().is_none() {
self.line_buffer
.set_selection_anchor(Some(self.insertion_point()));
}
} else {
self.clear_selection();
}
}
pub fn set_edit_mode(&mut self, mode: PromptEditMode) {
let policy_changed = mode.rest_policy() != self.edit_mode.rest_policy();
self.edit_mode = mode;
if policy_changed || self.policy_unsettled {
self.commit_cursor();
}
}
pub(crate) fn policy_unsettled(&self) -> bool {
self.policy_unsettled
}
pub(crate) fn set_cross_line_cursor(&mut self, cross: bool) {
self.cross_line_cursor = cross;
}
pub fn sync_edit_mode(&mut self, mode: PromptEditMode) {
if mode.rest_policy() != self.edit_mode.rest_policy() {
self.policy_unsettled = true;
}
self.edit_mode = mode;
}
pub(crate) fn commit_cursor(&mut self) {
let committed = commit(
self.line_buffer.get_buffer(),
self.line_buffer.cursor(),
self.edit_mode.rest_policy(),
);
self.line_buffer.set_cursor(committed);
self.policy_unsettled = false;
}
#[cfg(test)]
fn move_to_position(&mut self, position: usize, select: bool) {
self.line_buffer.move_head(position, select);
}
fn resolve_head(&self, target: MotionTarget) -> usize {
let buf = self.line_buffer.get_buffer();
let origin = self.insertion_point();
let head = resolve_motion(buf, origin, target, self.caret_geometry()).head;
if let MotionTarget::Grapheme(direction) = target {
if self.caret_geometry() == CaretGeometry::Block {
return self.grapheme_line_policy(buf, origin, head, direction);
}
}
head
}
fn grapheme_line_policy(
&self,
buf: &str,
origin: usize,
head: usize,
direction: Direction,
) -> usize {
if !self.cross_line_cursor {
return match direction {
Direction::Backward => head.max(line::start_of_line(buf, origin)),
Direction::Forward => head.min(line::end_of_line(buf, origin)),
};
}
let is_terminator = |pos: usize| buf[pos..].starts_with(['\r', '\n']);
if !is_terminator(head) {
return head;
}
match direction {
Direction::Forward => next_grapheme_boundary(buf, head),
Direction::Backward => {
let back = prev_grapheme_boundary(buf, head);
if is_terminator(back) {
head } else {
back
}
}
}
}
fn caret_geometry(&self) -> CaretGeometry {
if self.edit_mode.rest_policy() == RestPolicy::Between {
CaretGeometry::Bar
} else {
CaretGeometry::Block
}
}
fn move_head_to(&mut self, target: usize, select: bool) {
let next = self.line_buffer.cursor().put_cursor(
self.line_buffer.get_buffer(),
target,
Movement::select(select),
self.caret_geometry(),
);
self.line_buffer.set_cursor(next);
self.commit_cursor();
}
fn apply_move(&mut self, target: MotionTarget, select: bool) {
let head = self.resolve_head(target);
self.move_head_to(head, select);
}
fn apply_operator(&mut self, target: MotionTarget, verb: OperatorVerb) {
let sel = operator_span(
self.get_buffer(),
self.insertion_point(),
target,
self.caret_geometry(),
);
self.operate(sel, verb, Granularity::CharWise);
}
pub(crate) fn move_line_up(&mut self, select: bool) {
if let Some(target) = self.line_buffer.line_up_target() {
self.move_head_to(target, select);
}
self.update_undo_state(UndoBehavior::MoveCursor);
}
pub(crate) fn move_line_down(&mut self, select: bool) {
if let Some(target) = self.line_buffer.line_down_target() {
self.move_head_to(target, select);
}
self.update_undo_state(UndoBehavior::MoveCursor);
}
pub fn get_buffer(&self) -> &str {
self.line_buffer.get_buffer()
}
pub fn edit_buffer<F>(&mut self, func: F, undo_behavior: UndoBehavior)
where
F: FnOnce(&mut LineBuffer),
{
self.update_undo_state(undo_behavior);
func(&mut self.line_buffer);
}
pub(crate) fn set_buffer(&mut self, buffer: String, undo_behavior: UndoBehavior) {
self.line_buffer.set_buffer(buffer);
self.commit_cursor();
self.update_undo_state(undo_behavior);
}
pub(crate) fn insertion_point(&self) -> usize {
let cursor = self.line_buffer.cursor();
if self.edit_mode.rest_policy() == RestPolicy::Between {
cursor.head()
} else {
cursor.caret(self.line_buffer.get_buffer())
}
}
pub(crate) fn is_empty(&self) -> bool {
self.line_buffer.is_empty()
}
pub(crate) fn is_cursor_at_first_line(&self) -> bool {
self.line_buffer.is_cursor_at_first_line()
}
pub(crate) fn is_cursor_at_last_line(&self) -> bool {
self.line_buffer.is_cursor_at_last_line()
}
pub(crate) fn is_cursor_at_buffer_end(&self) -> bool {
let buf = self.get_buffer();
let cursor = self.line_buffer.cursor();
if !cursor.is_empty() {
return false;
}
if self.caret_geometry() == CaretGeometry::Block {
next_grapheme_boundary(buf, cursor.head()) == buf.len()
} else {
cursor.head() == buf.len()
}
}
pub(crate) fn reset_undo_stack(&mut self) {
self.edit_stack.reset();
}
pub(crate) fn move_to_start(&mut self, select: bool) {
self.move_head_to(0, select);
}
pub(crate) fn move_to_end(&mut self, select: bool) {
self.move_head_to(self.line_buffer.len(), select);
}
pub(crate) fn prepare_append_at_buffer_end(&mut self) {
self.line_buffer.set_insertion_point(self.line_buffer.len());
}
pub(crate) fn move_to_line_start(&mut self, select: bool) {
self.move_head_to(self.line_buffer.line_start_index(), select);
}
pub(crate) fn move_to_line_non_blank_start(&mut self, select: bool) {
self.move_head_to(self.line_buffer.line_non_blank_start_index(), select);
}
pub(crate) fn move_to_line_end(&mut self, select: bool) {
self.move_head_to(self.line_buffer.find_current_line_end(), select);
}
fn undo(&mut self) {
let val = self.edit_stack.undo();
self.line_buffer = val.clone();
}
fn redo(&mut self) {
let val = self.edit_stack.redo();
self.line_buffer = val.clone();
}
pub(crate) fn update_undo_state(&mut self, undo_behavior: UndoBehavior) {
if matches!(undo_behavior, UndoBehavior::NoOp) {
self.last_undo_behavior = UndoBehavior::NoOp;
return;
}
if !undo_behavior.create_undo_point_after(&self.last_undo_behavior) {
self.edit_stack.undo();
}
self.edit_stack.insert(self.line_buffer.clone());
self.last_undo_behavior = undo_behavior;
}
fn cut_current_line(&mut self) {
let deletion_range = self.line_buffer.current_line_range();
let cut_slice = &self.line_buffer.get_buffer()[deletion_range.clone()];
if !cut_slice.is_empty() {
self.cut_buffer.set(cut_slice, Granularity::LineWise);
self.line_buffer.set_insertion_point(deletion_range.start);
self.line_buffer.clear_range(deletion_range);
}
}
fn cut_from_start(&mut self) {
let insertion_offset = self.line_buffer.insertion_point();
if insertion_offset > 0 {
self.cut_buffer.set(
&self.line_buffer.get_buffer()[..insertion_offset],
Granularity::CharWise,
);
self.line_buffer.clear_to_insertion_point();
}
}
fn cut_from_start_linewise(&mut self, leave_blank_line: bool) {
let insertion_offset = self.line_buffer.insertion_point();
let end_offset = self.line_buffer.get_buffer()[insertion_offset..]
.find('\n')
.map_or(self.line_buffer.len(), |offset| {
if leave_blank_line {
insertion_offset + offset
} else {
insertion_offset + offset + 1
}
});
if end_offset > 0 {
self.cut_buffer.set(
&self.line_buffer.get_buffer()[..end_offset],
Granularity::LineWise,
);
self.line_buffer.clear_range(..end_offset);
self.line_buffer.move_to_start();
}
}
fn cut_from_line_start(&mut self) {
let previous_offset = self.line_buffer.insertion_point();
self.line_buffer.move_to_line_start();
let deletion_range = self.line_buffer.insertion_point()..previous_offset;
let cut_slice = &self.line_buffer.get_buffer()[deletion_range.clone()];
if !cut_slice.is_empty() {
self.cut_buffer.set(cut_slice, Granularity::CharWise);
self.line_buffer.clear_range(deletion_range);
}
}
fn cut_from_line_non_blank_start(&mut self) {
let cursor_pos = self.line_buffer.insertion_point();
self.line_buffer.move_to_line_non_blank_start();
let other_pos = self.line_buffer.insertion_point();
let deletion_range = min(cursor_pos, other_pos)..max(cursor_pos, other_pos);
self.cut_range(deletion_range);
}
fn cut_from_end(&mut self) {
let cut_slice = &self.line_buffer.get_buffer()[self.line_buffer.insertion_point()..];
if !cut_slice.is_empty() {
self.cut_buffer.set(cut_slice, Granularity::CharWise);
self.line_buffer.clear_to_end();
}
}
fn cut_from_end_linewise(&mut self, leave_blank_line: bool) {
let buf = self.line_buffer.get_buffer();
let len = buf.len();
let nl = buf[..self.line_buffer.insertion_point()].rfind('\n');
let register_start = nl.map_or(0, |offset| offset + 1);
let delete_start = nl.map_or(0, |offset| {
if leave_blank_line {
offset + 1
} else if buf[..offset].ends_with('\r') {
offset - 1
} else {
offset
}
});
if delete_start < len {
let register_slice = &self.line_buffer.get_buffer()[register_start..];
if !register_slice.is_empty() {
self.cut_buffer.set(register_slice, Granularity::LineWise);
}
self.line_buffer.set_insertion_point(delete_start);
self.line_buffer.clear_to_end();
}
}
fn cut_to_line_end(&mut self) {
let cut_slice = &self.line_buffer.get_buffer()
[self.line_buffer.insertion_point()..self.line_buffer.find_current_line_end()];
if !cut_slice.is_empty() {
self.cut_buffer.set(cut_slice, Granularity::CharWise);
self.line_buffer.clear_to_line_end();
}
}
fn kill_line(&mut self) {
if self.line_buffer.insertion_point() == self.line_buffer.find_current_line_end() {
self.cut_char()
} else {
self.cut_to_line_end()
}
}
fn cut_word_left(&mut self) {
self.apply_operator(
word_target(WordKind::Unicode, WordEdge::Start, Direction::Backward),
OperatorVerb::Cut,
);
}
fn cut_big_word_left(&mut self) {
self.apply_operator(
word_target(WordKind::LongWord, WordEdge::Start, Direction::Backward),
OperatorVerb::Cut,
);
}
fn cut_word_right(&mut self) {
self.apply_operator(
word_target(WordKind::Unicode, WordEdge::End, Direction::Forward),
OperatorVerb::Cut,
);
}
fn cut_big_word_right(&mut self) {
self.apply_operator(
word_target(WordKind::LongWord, WordEdge::End, Direction::Forward),
OperatorVerb::Cut,
);
}
fn cut_word_right_to_next(&mut self) {
self.apply_operator(
word_target(WordKind::Unicode, WordEdge::Start, Direction::Forward),
OperatorVerb::Cut,
);
}
fn cut_big_word_right_to_next(&mut self) {
self.apply_operator(
word_target(WordKind::LongWord, WordEdge::Start, Direction::Forward),
OperatorVerb::Cut,
);
}
fn cut_char(&mut self) {
if self.line_buffer.selection_anchor().is_some() {
self.cut_selection_to_cut_buffer();
} else {
let insertion_offset = self.line_buffer.insertion_point();
let next_char = self.line_buffer.grapheme_right_index();
self.cut_range(insertion_offset..next_char);
}
}
fn insert_cut_buffer_before(&mut self) {
self.delete_selection();
insert_clipboard_content_before(&mut self.line_buffer, self.cut_buffer.deref_mut())
}
fn insert_cut_buffer_after(&mut self) {
let had_selection = self.line_buffer.selection_anchor().is_some();
self.delete_selection();
match self.cut_buffer.get() {
(content, Granularity::CharWise) => {
if !had_selection {
self.line_buffer.move_right();
}
self.line_buffer.insert_str(&content);
}
(mut content, Granularity::LineWise) => {
if !content.ends_with('\n') {
content.push('\n');
}
let ip = self.line_buffer.insertion_point();
match line::start_of_next_line(self.line_buffer.get_buffer(), ip) {
Some(next) => {
self.line_buffer.set_insertion_point(next);
self.line_buffer.insert_str(&content);
}
None => {
let trimmed = content.strip_suffix('\n').unwrap_or(&content);
if self.line_buffer.is_empty() {
self.line_buffer.insert_str(trimmed);
} else {
self.line_buffer.set_insertion_point(self.line_buffer.len());
self.line_buffer.insert_str(&format!("\n{trimmed}"));
}
}
}
}
}
}
fn move_right_until_char(
&mut self,
c: char,
before_char: bool,
current_line: bool,
select: bool,
) {
let Some(found) = self.line_buffer.find_char_right(c, current_line) else {
if !select {
self.clear_selection();
}
return;
};
let target = if before_char {
self.line_buffer.grapheme_left_index_from_pos(found)
} else {
found
};
self.move_head_to(target, select);
}
fn move_left_until_char(
&mut self,
c: char,
before_char: bool,
current_line: bool,
select: bool,
) {
let Some(found) = self.line_buffer.find_char_left(c, current_line) else {
if !select {
self.clear_selection();
}
return;
};
let target = if before_char {
found + c.len_utf8()
} else {
found
};
self.move_head_to(target, select);
}
fn cut_right_until_char(&mut self, c: char, before_char: bool, current_line: bool) {
if let Some(index) = self.line_buffer.find_char_right(c, current_line) {
let extra = if before_char { 0 } else { c.len_utf8() };
let cut_slice =
&self.line_buffer.get_buffer()[self.line_buffer.insertion_point()..index + extra];
if !cut_slice.is_empty() {
self.cut_buffer.set(cut_slice, Granularity::CharWise);
if before_char {
self.line_buffer.delete_right_before_char(c, current_line);
} else {
self.line_buffer.delete_right_until_char(c, current_line);
}
}
}
}
fn cut_left_until_char(&mut self, c: char, before_char: bool, current_line: bool) {
if let Some(index) = self.line_buffer.find_char_left(c, current_line) {
let extra = if before_char { c.len_utf8() } else { 0 };
let cut_slice =
&self.line_buffer.get_buffer()[index + extra..self.line_buffer.insertion_point()];
if !cut_slice.is_empty() {
self.cut_buffer.set(cut_slice, Granularity::CharWise);
if before_char {
self.line_buffer.delete_left_before_char(c, current_line);
} else {
self.line_buffer.delete_left_until_char(c, current_line);
}
}
}
}
fn replace_char(&mut self, character: char) {
if let Some((start, end)) = self.get_selection() {
use unicode_segmentation::UnicodeSegmentation;
let replacement: String = self.line_buffer.get_buffer()[start..end]
.graphemes(true)
.map(|g| {
if g == "\n" || g == "\r\n" || g == "\r" {
g.to_string()
} else {
character.to_string()
}
})
.collect();
self.line_buffer.replace_range(start..end, &replacement);
self.line_buffer.set_cursor(Cursor::point(start));
return;
}
self.line_buffer.collapse_to_caret();
let insertion_point = self.line_buffer.insertion_point();
self.line_buffer.delete_right_grapheme();
self.line_buffer.insert_char(character);
self.line_buffer.set_insertion_point(insertion_point);
}
fn replace_chars(&mut self, n_chars: usize, string: &str) {
self.line_buffer.collapse_to_caret();
for _ in 0..n_chars {
self.line_buffer.delete_right_grapheme();
}
self.line_buffer.insert_str(string);
}
fn move_left(&mut self, select: bool) {
let head = self.resolve_head(MotionTarget::Grapheme(Direction::Backward));
self.move_head_to(head, select);
}
fn move_right(&mut self, select: bool) {
let head = self.resolve_head(MotionTarget::Grapheme(Direction::Forward));
self.move_head_to(head, select);
}
fn select_all(&mut self) {
let end = self.line_buffer.len();
self.line_buffer.set_cursor(Cursor::new(0, end));
}
#[cfg(feature = "system_clipboard")]
fn cut_selection_to_system(&mut self) {
if let Some((start, end)) = self.get_selection() {
let cut_slice = &self.line_buffer.get_buffer()[start..end];
self.system_clipboard.set(cut_slice, Granularity::CharWise);
self.cut_range(start..end);
self.clear_selection();
}
}
fn cut_selection_to_cut_buffer(&mut self) {
if let Some((start, end)) = self.get_selection() {
self.cut_range(start..end);
self.clear_selection();
}
}
#[cfg(feature = "system_clipboard")]
fn copy_selection_to_system(&mut self) {
if let Some((start, end)) = self.get_selection() {
let cut_slice = &self.line_buffer.get_buffer()[start..end];
self.system_clipboard.set(cut_slice, Granularity::CharWise);
}
}
fn copy_selection_to_cut_buffer(&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, Granularity::CharWise);
}
}
pub fn get_selection(&self) -> Option<(usize, usize)> {
self.line_buffer.selection_anchor()?;
let cursor = self.line_buffer.cursor();
Some((cursor.start(), cursor.end().min(self.line_buffer.len())))
}
fn delete_selection(&mut self) {
if let Some((start, end)) = self.get_selection() {
self.line_buffer.clear_range_safe(start..end);
self.clear_selection();
}
}
fn backspace(&mut self) {
if self.line_buffer.selection_anchor().is_some() {
self.delete_selection();
} else {
self.line_buffer.delete_left_grapheme();
}
}
fn delete(&mut self) {
if self.line_buffer.selection_anchor().is_some() {
self.delete_selection();
} else {
self.line_buffer.delete_right_grapheme();
}
}
fn move_word_left(&mut self, select: bool) {
self.apply_move(
word_target(WordKind::Unicode, WordEdge::Start, Direction::Backward),
select,
);
}
fn move_big_word_left(&mut self, select: bool) {
self.apply_move(
word_target(WordKind::LongWord, WordEdge::Start, Direction::Backward),
select,
);
}
fn move_word_right(&mut self, select: bool) {
self.apply_move(
word_target(WordKind::Unicode, WordEdge::End, Direction::Forward),
select,
);
}
fn move_word_right_start(&mut self, select: bool) {
self.apply_move(
word_target(WordKind::Unicode, WordEdge::Start, Direction::Forward),
select,
);
}
fn move_big_word_right_start(&mut self, select: bool) {
self.apply_move(
word_target(WordKind::LongWord, WordEdge::Start, Direction::Forward),
select,
);
}
fn move_word_right_end(&mut self, select: bool) {
self.move_head_to(self.word_end_on_grapheme(WordKind::Unicode), select);
}
fn move_big_word_right_end(&mut self, select: bool) {
self.move_head_to(self.word_end_on_grapheme(WordKind::LongWord), select);
}
fn word_end_on_grapheme(&self, kind: WordKind) -> usize {
let target = word_target(kind, WordEdge::End, Direction::Forward);
resolve_motion(
self.get_buffer(),
self.insertion_point(),
target,
CaretGeometry::Block,
)
.head
}
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();
}
fn insert_newline_above(&mut self) {
let index = self.line_buffer.find_char_left('\n', false).unwrap_or(0);
self.line_buffer.set_insertion_point(index);
self.line_buffer.insert_newline();
}
fn insert_newline_below(&mut self) {
let index = self
.line_buffer
.find_char_right('\n', false)
.unwrap_or(self.line_buffer.len());
self.line_buffer.set_insertion_point(index);
self.line_buffer.insert_newline();
}
#[cfg(feature = "system_clipboard")]
fn paste_from_system(&mut self) {
self.delete_selection();
insert_clipboard_content_before(&mut self.line_buffer, self.system_clipboard.deref_mut());
}
fn paste_cut_buffer(&mut self) {
self.delete_selection();
insert_clipboard_content_before(&mut self.line_buffer, self.cut_buffer.deref_mut());
}
fn cut_range(&mut self, range: Range<usize>) {
self.cut_range_with(range, Granularity::CharWise);
}
fn cut_range_with(&mut self, range: Range<usize>, granularity: Granularity) {
if range.start <= range.end {
self.copy_range_with(range.clone(), granularity);
self.line_buffer.clear_range_safe(range.clone());
self.line_buffer.set_insertion_point(range.start);
}
}
fn copy_range(&mut self, range: Range<usize>) {
self.copy_range_with(range, Granularity::CharWise);
}
fn copy_range_with(&mut self, range: Range<usize>, granularity: Granularity) {
if range.start < range.end {
let slice = &self.line_buffer.get_buffer()[range];
self.cut_buffer.set(slice, granularity);
}
}
fn cut_inside_pair(&mut self, open_char: char, close_char: char) {
if let Some(range) = self
.line_buffer
.range_inside_current_pair(open_char, close_char)
.or_else(|| {
self.line_buffer
.range_inside_next_pair(open_char, close_char)
})
{
self.cut_range(range)
}
}
fn word_text_object_range(&self, text_object_scope: TextObjectScope) -> Range<usize> {
self.line_buffer
.current_whitespace_range()
.unwrap_or_else(|| {
let word_range = self.line_buffer.current_word_range();
match text_object_scope {
TextObjectScope::Inner => word_range,
TextObjectScope::Around => {
self.line_buffer.expand_range_with_whitespace(word_range)
}
}
})
}
fn big_word_text_object_range(&self, text_object_scope: TextObjectScope) -> Range<usize> {
self.line_buffer
.current_whitespace_range()
.unwrap_or_else(|| {
let big_word_range = self.line_buffer.current_big_word_range();
match text_object_scope {
TextObjectScope::Inner => big_word_range,
TextObjectScope::Around => self
.line_buffer
.expand_range_with_whitespace(big_word_range),
}
})
}
fn matching_pair_group_text_object_range(
&self,
text_object_scope: TextObjectScope,
matching_pair_group: &[(char, char)],
) -> Option<Range<usize>> {
self.line_buffer
.range_inside_current_pair_in_group(matching_pair_group)
.or_else(|| {
self.line_buffer
.range_inside_next_pair_in_group(matching_pair_group)
})
.and_then(|pair_range| match text_object_scope {
TextObjectScope::Inner => Some(pair_range),
TextObjectScope::Around => self.expand_range_to_include_pair(pair_range),
})
}
fn bracket_text_object_range(
&self,
text_object_scope: TextObjectScope,
) -> Option<Range<usize>> {
const BRACKET_PAIRS: &[(char, char)] = &[('(', ')'), ('[', ']'), ('{', '}')];
self.matching_pair_group_text_object_range(text_object_scope, BRACKET_PAIRS)
}
fn quote_text_object_range(&self, text_object_scope: TextObjectScope) -> Option<Range<usize>> {
const QUOTE_PAIRS: &[(char, char)] = &[('"', '"'), ('\'', '\''), ('`', '`')];
self.matching_pair_group_text_object_range(text_object_scope, QUOTE_PAIRS)
}
fn text_object_range(&self, text_object: TextObject) -> Option<Range<usize>> {
match text_object.object_type {
TextObjectType::Word => Some(self.word_text_object_range(text_object.scope)),
TextObjectType::BigWord => Some(self.big_word_text_object_range(text_object.scope)),
TextObjectType::Brackets => self.bracket_text_object_range(text_object.scope),
TextObjectType::Quote => self.quote_text_object_range(text_object.scope),
}
}
fn cut_text_object(&mut self, text_object: TextObject) {
if let Some(range) = self.text_object_range(text_object) {
self.cut_range(range);
}
}
fn copy_text_object(&mut self, text_object: TextObject) {
if let Some(range) = self.text_object_range(text_object) {
self.copy_range(range);
}
}
pub(crate) fn copy_from_start(&mut self) {
let insertion_offset = self.line_buffer.insertion_point();
if insertion_offset > 0 {
self.cut_buffer.set(
&self.line_buffer.get_buffer()[..insertion_offset],
Granularity::CharWise,
);
}
}
pub(crate) fn copy_from_start_linewise(&mut self) {
let insertion_point = self.line_buffer.insertion_point();
let end_offset = self.line_buffer.get_buffer()[insertion_point..]
.find('\n')
.map_or(self.line_buffer.len(), |offset| insertion_point + offset);
if end_offset > 0 {
self.cut_buffer.set(
&self.line_buffer.get_buffer()[..end_offset],
Granularity::LineWise,
);
}
self.line_buffer.move_to_start();
}
pub(crate) fn copy_from_line_start(&mut self) {
let previous_offset = self.line_buffer.insertion_point();
let start_offset = {
let temp_pos = self.line_buffer.insertion_point();
self.line_buffer.move_to_line_start();
let start = self.line_buffer.insertion_point();
self.line_buffer.set_insertion_point(temp_pos);
start
};
let copy_range = start_offset..previous_offset;
self.copy_range(copy_range);
}
pub(crate) fn copy_from_line_non_blank_start(&mut self) {
let cursor_pos = self.line_buffer.insertion_point();
self.line_buffer.move_to_line_non_blank_start();
let other_pos = self.line_buffer.insertion_point();
self.line_buffer.set_insertion_point(cursor_pos);
let copy_range = min(cursor_pos, other_pos)..max(cursor_pos, other_pos);
self.copy_range(copy_range);
}
pub(crate) fn copy_from_end(&mut self) {
let copy_range = self.line_buffer.insertion_point()..self.line_buffer.len();
self.copy_range(copy_range);
}
pub(crate) fn copy_from_end_linewise(&mut self) {
self.line_buffer.move_to_line_start();
let copy_range = self.line_buffer.insertion_point()..self.line_buffer.len();
if copy_range.start < copy_range.end {
let slice = &self.line_buffer.get_buffer()[copy_range];
self.cut_buffer.set(slice, Granularity::LineWise);
}
}
pub(crate) fn copy_to_line_end(&mut self) {
let copy_range =
self.line_buffer.insertion_point()..self.line_buffer.find_current_line_end();
self.copy_range(copy_range);
}
pub(crate) fn copy_word_left(&mut self) {
self.apply_operator(
word_target(WordKind::Unicode, WordEdge::Start, Direction::Backward),
OperatorVerb::Copy,
);
}
pub(crate) fn copy_big_word_left(&mut self) {
self.apply_operator(
word_target(WordKind::LongWord, WordEdge::Start, Direction::Backward),
OperatorVerb::Copy,
);
}
pub(crate) fn copy_word_right(&mut self) {
self.apply_operator(
word_target(WordKind::Unicode, WordEdge::End, Direction::Forward),
OperatorVerb::Copy,
);
}
pub(crate) fn copy_big_word_right(&mut self) {
self.apply_operator(
word_target(WordKind::LongWord, WordEdge::End, Direction::Forward),
OperatorVerb::Copy,
);
}
pub(crate) fn copy_word_right_to_next(&mut self) {
self.apply_operator(
word_target(WordKind::Unicode, WordEdge::Start, Direction::Forward),
OperatorVerb::Copy,
);
}
pub(crate) fn copy_big_word_right_to_next(&mut self) {
self.apply_operator(
word_target(WordKind::LongWord, WordEdge::Start, Direction::Forward),
OperatorVerb::Copy,
);
}
pub(crate) fn copy_right_until_char(&mut self, c: char, before_char: bool, current_line: bool) {
if let Some(index) = self.line_buffer.find_char_right(c, current_line) {
let extra = if before_char { 0 } else { c.len_utf8() };
let copy_range = self.line_buffer.insertion_point()..index + extra;
self.copy_range(copy_range);
}
}
pub(crate) fn copy_left_until_char(&mut self, c: char, before_char: bool, current_line: bool) {
if let Some(index) = self.line_buffer.find_char_left(c, current_line) {
let extra = if before_char { c.len_utf8() } else { 0 };
let copy_range = index + extra..self.line_buffer.insertion_point();
self.copy_range(copy_range);
}
}
fn copy_inside_pair(&mut self, open_char: char, close_char: char) {
if let Some(range) = self
.line_buffer
.range_inside_current_pair(open_char, close_char)
.or_else(|| {
self.line_buffer
.range_inside_next_pair(open_char, close_char)
})
{
self.copy_range(range);
}
}
fn expand_range_to_include_pair(&self, range: Range<usize>) -> Option<Range<usize>> {
let start = self.line_buffer.grapheme_left_index_from_pos(range.start);
let end = self.line_buffer.grapheme_right_index_from_pos(range.end);
Some(start..end)
}
fn cut_around_pair(&mut self, open_char: char, close_char: char) {
if let Some(around_range) = self
.line_buffer
.range_inside_current_pair(open_char, close_char)
.or_else(|| {
self.line_buffer
.range_inside_next_pair(open_char, close_char)
})
.and_then(|range| self.expand_range_to_include_pair(range))
{
self.cut_range(around_range);
}
}
fn copy_around_pair(&mut self, open_char: char, close_char: char) {
if let Some(around_range) = self
.line_buffer
.range_inside_current_pair(open_char, close_char)
.or_else(|| {
self.line_buffer
.range_inside_next_pair(open_char, close_char)
})
.and_then(|range| self.expand_range_to_include_pair(range))
{
self.copy_range(around_range);
}
}
}
fn insert_clipboard_content_before(line_buffer: &mut LineBuffer, clipboard: &mut dyn Clipboard) {
match clipboard.get() {
(content, Granularity::CharWise) => {
line_buffer.insert_str(&content);
}
(mut content, Granularity::LineWise) => {
line_buffer.move_to_line_start();
line_buffer.move_line_up();
if !content.ends_with('\n') {
content.push('\n');
}
line_buffer.insert_str(&content);
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::prompt::PromptViMode;
use crate::{Direction, FindStop, WordEdge, WordKind};
use pretty_assertions::assert_eq;
use rstest::rstest;
fn editor_with(buffer: &str) -> Editor {
let mut editor = Editor::default();
editor.set_buffer(buffer.to_string(), UndoBehavior::CreateUndoPoint);
editor
}
fn vi_editor(buffer: &str, vi_mode: PromptViMode) -> Editor {
let mut editor = editor_with(buffer);
editor.set_edit_mode(PromptEditMode::Vi(vi_mode));
editor
}
#[test]
fn vi_normal_clamps_cursor_off_the_end() {
let mut editor = vi_editor("hello", PromptViMode::Normal);
editor.run_edit_command(&EditCommand::MoveToEnd { select: false });
assert_eq!(editor.insertion_point(), 4);
}
#[test]
fn vi_normal_clamps_to_line_end() {
let mut editor = vi_editor("hello", PromptViMode::Normal);
editor.run_edit_command(&EditCommand::MoveToLineEnd { select: false });
assert_eq!(editor.insertion_point(), 4);
}
#[test]
fn vi_insert_does_not_clamp_off_the_end() {
let mut editor = vi_editor("hello", PromptViMode::Insert);
editor.run_edit_command(&EditCommand::MoveToEnd { select: false });
assert_eq!(editor.insertion_point(), 5);
}
#[test]
fn vi_normal_empty_buffer_stays_at_zero() {
let mut editor = vi_editor("", PromptViMode::Normal);
editor.run_edit_command(&EditCommand::MoveToEnd { select: false });
assert_eq!(editor.insertion_point(), 0);
}
#[test]
fn vi_normal_within_bounds_is_unchanged() {
let mut editor = vi_editor("hello", PromptViMode::Normal);
editor.run_edit_command(&EditCommand::MoveToPosition {
position: 2,
select: false,
});
assert_eq!(editor.insertion_point(), 2);
}
#[test]
fn vi_normal_clamps_onto_multibyte_grapheme() {
let mut editor = vi_editor("café", PromptViMode::Normal);
editor.run_edit_command(&EditCommand::MoveToEnd { select: false });
assert_eq!(editor.insertion_point(), "caf".len());
}
#[rstest]
#[case(PromptViMode::Insert, "hello", 5)] #[case(PromptViMode::Normal, "hello", 4)] #[case(PromptViMode::Insert, "café", 5)] #[case(PromptViMode::Normal, "café", 3)] #[case(PromptViMode::Insert, "", 0)]
#[case(PromptViMode::Normal, "", 0)]
fn net_insertion_point_at_end(
#[case] mode: PromptViMode,
#[case] buf: &str,
#[case] expect: usize,
) {
let mut editor = vi_editor(buf, mode);
editor.run_edit_command(&EditCommand::MoveToEnd { select: false });
assert_eq!(editor.insertion_point(), expect);
}
#[test]
fn net_insertion_point_emacs_rests_at_len() {
let mut editor = editor_with("hello");
editor.run_edit_command(&EditCommand::MoveToEnd { select: false });
assert_eq!(editor.insertion_point(), 5);
}
#[test]
fn net_get_selection_none_without_anchor() {
let editor = vi_editor("hello", PromptViMode::Normal);
assert_eq!(editor.get_selection(), None);
}
#[test]
fn net_get_selection_backward_is_ordered() {
let mut editor = vi_editor("hello", PromptViMode::Normal);
editor.move_to_position(3, false);
editor.run_edit_command(&EditCommand::MoveLeft { select: true });
editor.run_edit_command(&EditCommand::MoveLeft { select: true });
assert_eq!(editor.get_selection(), Some((1, 4)));
}
#[test]
fn net_deliberate_selection_reports_a_range() {
let mut editor = vi_editor("hello", PromptViMode::Normal);
editor.move_to_position(1, false);
editor.run_edit_command(&EditCommand::MoveRight { select: true });
assert_eq!(editor.get_selection(), Some((1, 3)));
}
fn esc_back_from_insert(buffer: &str, at: usize) -> Editor {
let mut editor = vi_editor(buffer, PromptViMode::Insert);
editor.set_cross_line_cursor(false);
editor.line_buffer.set_insertion_point(at);
editor.sync_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
editor.run_edit_command(&EditCommand::Move(MotionTarget::Grapheme(
Direction::Backward,
)));
editor
}
#[test]
fn esc_back_steps_one_within_line() {
let editor = esc_back_from_insert("aa bb cc", 7);
assert_eq!(editor.insertion_point(), 6);
}
#[test]
fn esc_back_at_line_end_does_not_double_step() {
let editor = esc_back_from_insert("aa bb cc", 8);
assert_eq!(editor.insertion_point(), 7);
}
#[test]
fn esc_back_at_line_start_stays_in_line() {
let editor = esc_back_from_insert("ab\ncd", 3);
assert_eq!(editor.insertion_point(), 3);
}
#[test]
fn esc_back_on_trailing_empty_line_stays() {
let editor = esc_back_from_insert("a\n\n", 3);
assert_eq!(editor.insertion_point(), 3);
}
#[test]
fn vi_normal_set_buffer_clamps_cursor() {
let mut editor = vi_editor("", PromptViMode::Normal);
editor.set_buffer("hello".to_string(), UndoBehavior::CreateUndoPoint);
assert_eq!(editor.insertion_point(), 4);
}
#[test]
fn vi_normal_set_buffer_clamps_multibyte() {
let mut editor = vi_editor("", PromptViMode::Normal);
editor.set_buffer("café".to_string(), UndoBehavior::CreateUndoPoint);
assert_eq!(editor.insertion_point(), "caf".len());
}
#[test]
fn entering_vi_normal_clamps_cursor() {
let mut editor = vi_editor("hello", PromptViMode::Insert);
editor.run_edit_command(&EditCommand::MoveToEnd { select: false });
assert_eq!(editor.insertion_point(), 5);
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
assert_eq!(editor.insertion_point(), 4);
}
#[test]
fn entering_vi_insert_does_not_move_cursor() {
let mut editor = vi_editor("hello", PromptViMode::Normal);
editor.run_edit_command(&EditCommand::MoveToPosition {
position: 4,
select: false,
});
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Insert));
assert_eq!(editor.insertion_point(), 4);
}
#[test]
fn vi_normal_selection_cut_is_inclusive() {
let mut editor = vi_editor("hello", PromptViMode::Normal);
editor.run_edit_command(&EditCommand::MoveToPosition {
position: 0,
select: false,
});
for _ in 0..2 {
editor.run_edit_command(&EditCommand::MoveRight { select: true });
}
assert_eq!(editor.get_selection(), Some((0, 3)));
editor.run_edit_command(&EditCommand::CutSelection);
assert_eq!(editor.get_buffer(), "lo");
assert_eq!(editor.cut_buffer.get().0, "hel");
}
#[test]
fn vi_normal_selection_cut_spans_multibyte_grapheme() {
let mut editor = vi_editor("caféx", PromptViMode::Normal);
editor.run_edit_command(&EditCommand::MoveToPosition {
position: 0,
select: false,
});
for _ in 0..3 {
editor.run_edit_command(&EditCommand::MoveRight { select: true });
}
assert_eq!(editor.get_selection(), Some((0, 5)));
editor.run_edit_command(&EditCommand::CutSelection);
assert_eq!(editor.get_buffer(), "x");
assert_eq!(editor.cut_buffer.get().0, "café");
}
#[test]
fn shift_select_grabs_one_grapheme_per_step() {
let mut editor = editor_with("hello");
editor.move_to_position(0, false);
editor.run_edit_command(&EditCommand::MoveRight { select: true });
assert_eq!(editor.get_selection(), Some((0, 1)));
editor.run_edit_command(&EditCommand::MoveRight { select: true });
assert_eq!(editor.get_selection(), Some((0, 2)));
}
#[test]
fn shift_select_one_grapheme_over_multibyte() {
let mut editor = editor_with("café"); editor.move_to_position(3, false);
editor.run_edit_command(&EditCommand::MoveRight { select: true });
assert_eq!(editor.get_selection(), Some((3, 5))); }
#[test]
fn select_all_captures_inclusivity_at_plant_time() {
let mut editor = vi_editor("hello", PromptViMode::Normal);
editor.run_edit_command(&EditCommand::SelectAll);
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Insert));
assert_eq!(editor.get_selection(), Some((0, 5)));
}
fn linewise_editor() -> Editor {
let mut editor = editor_with("aaa\nbbb\nccc");
editor.move_to_position(5, false);
editor
}
#[test]
fn cut_current_line_is_linewise() {
let mut editor = linewise_editor();
editor.run_edit_command(&EditCommand::CutCurrentLine);
assert_eq!(editor.get_buffer(), "aaa\nccc");
assert_eq!(editor.insertion_point(), 4);
let (content, mode) = editor.cut_buffer.get();
assert_eq!(content, "bbb\n");
assert!(matches!(mode, Granularity::LineWise));
}
#[test]
fn cut_lineedge_linewise_matches_current_line() {
let mut editor = linewise_editor();
editor.run_edit_command(&EditCommand::Cut {
target: MotionTarget::LineEdge(Direction::Forward),
granularity: Granularity::LineWise,
});
assert_eq!(editor.get_buffer(), "aaa\nccc");
assert_eq!(editor.insertion_point(), 4);
let (content, gran) = editor.cut_buffer.get();
assert_eq!(content, "bbb\n");
assert_eq!(gran, Granularity::LineWise);
}
#[test]
fn cut_bufferedge_back_linewise_cuts_through_current_line() {
let mut editor = linewise_editor();
editor.run_edit_command(&EditCommand::Cut {
target: MotionTarget::BufferEdge(Direction::Backward),
granularity: Granularity::LineWise,
});
assert_eq!(editor.get_buffer(), "ccc");
assert_eq!(editor.insertion_point(), 0);
let (content, gran) = editor.cut_buffer.get();
assert_eq!(content, "aaa\nbbb\n");
assert_eq!(gran, Granularity::LineWise);
}
#[test]
fn cut_bufferedge_fwd_linewise_eats_leading_newline() {
let mut editor = linewise_editor();
editor.run_edit_command(&EditCommand::Cut {
target: MotionTarget::BufferEdge(Direction::Forward),
granularity: Granularity::LineWise,
});
assert_eq!(editor.get_buffer(), "aaa");
assert_eq!(editor.insertion_point(), 3);
let (content, gran) = editor.cut_buffer.get();
assert_eq!(content, "bbb\nccc");
assert_eq!(gran, Granularity::LineWise);
}
#[test]
fn copy_lineedge_linewise_tags_register_nondestructively() {
let mut editor = linewise_editor();
editor.run_edit_command(&EditCommand::Copy {
target: MotionTarget::LineEdge(Direction::Forward),
granularity: Granularity::LineWise,
});
assert_eq!(editor.get_buffer(), "aaa\nbbb\nccc"); let (content, gran) = editor.cut_buffer.get();
assert_eq!(content, "bbb\n");
assert_eq!(gran, Granularity::LineWise);
}
#[test]
fn cut_lineedge_charwise_stays_charwise() {
let mut editor = linewise_editor(); editor.run_edit_command(&EditCommand::Cut {
target: MotionTarget::LineEdge(Direction::Forward),
granularity: Granularity::CharWise,
});
assert_eq!(editor.get_buffer(), "aaa\nb\nccc"); let (content, gran) = editor.cut_buffer.get();
assert_eq!(content, "bb");
assert_eq!(gran, Granularity::CharWise);
}
#[test]
fn cut_line_down_linewise_deletes_current_and_next() {
let mut editor = linewise_editor();
editor.run_edit_command(&EditCommand::Cut {
target: MotionTarget::Line(Direction::Forward),
granularity: Granularity::LineWise,
});
assert_eq!(editor.get_buffer(), "aaa");
let (content, gran) = editor.cut_buffer.get();
assert_eq!(content, "bbb\nccc");
assert_eq!(gran, Granularity::LineWise);
}
#[test]
fn cut_line_up_linewise_deletes_current_and_prev() {
let mut editor = linewise_editor();
editor.run_edit_command(&EditCommand::Cut {
target: MotionTarget::Line(Direction::Backward),
granularity: Granularity::LineWise,
});
assert_eq!(editor.get_buffer(), "ccc");
assert_eq!(editor.insertion_point(), 0);
let (content, gran) = editor.cut_buffer.get();
assert_eq!(content, "aaa\nbbb\n");
assert_eq!(gran, Granularity::LineWise);
}
#[test]
fn cut_line_down_on_last_line_cuts_only_that_line() {
let mut editor = editor_with("aaa\nbbb\nccc");
editor.move_to_position(9, false); editor.run_edit_command(&EditCommand::Cut {
target: MotionTarget::Line(Direction::Forward),
granularity: Granularity::LineWise,
});
assert_eq!(editor.get_buffer(), "aaa\nbbb");
let (content, gran) = editor.cut_buffer.get();
assert_eq!(content, "ccc");
assert_eq!(gran, Granularity::LineWise);
}
#[test]
fn dd_on_last_line_then_paste_leaves_no_blank_line() {
let mut editor = vi_editor("ab\ncd", PromptViMode::Normal);
editor.line_buffer.set_insertion_point(3); editor.run_edit_command(&EditCommand::Cut {
target: MotionTarget::LineEdge(Direction::Forward),
granularity: Granularity::LineWise,
});
assert_eq!(editor.get_buffer(), "ab");
assert_eq!(editor.cut_buffer.get().0, "cd"); editor.run_edit_command(&EditCommand::PasteCutBufferBefore);
assert_eq!(editor.get_buffer(), "cd\nab"); }
#[test]
fn word_operator_never_splits_a_combining_grapheme() {
let mut editor = vi_editor("ae\u{0301}", PromptViMode::Normal);
editor.line_buffer.set_insertion_point(0);
editor.run_edit_command(&EditCommand::Cut {
target: MotionTarget::Word {
kind: WordKind::Word,
edge: WordEdge::Start,
direction: Direction::Forward,
},
granularity: Granularity::CharWise,
});
let buf = editor.get_buffer();
assert!(
!buf.starts_with('\u{0301}'),
"word operator orphaned a combining mark: {buf:?}"
);
}
#[test]
fn paste_after_over_selection_does_not_skip_a_grapheme() {
let mut editor = vi_editor("hello", PromptViMode::Normal);
editor.cut_buffer.set("xyz", Granularity::CharWise);
editor.run_edit_command(&EditCommand::MoveToPosition {
position: 0,
select: false,
});
for _ in 0..2 {
editor.run_edit_command(&EditCommand::MoveRight { select: true });
}
assert_eq!(editor.get_selection(), Some((0, 3))); editor.run_edit_command(&EditCommand::PasteCutBufferAfter);
assert_eq!(editor.get_buffer(), "xyzlo");
}
#[test]
fn paste_after_linewise_on_last_line_lands_below() {
let mut editor = editor_with("ab");
editor.cut_buffer.set("cd", Granularity::LineWise);
editor.line_buffer.set_insertion_point(0);
editor.run_edit_command(&EditCommand::PasteCutBufferAfter);
assert_eq!(editor.get_buffer(), "ab\ncd"); }
#[test]
fn paste_after_linewise_into_empty_buffer_has_no_blank_line() {
let mut editor = editor_with("");
editor.cut_buffer.set("ab", Granularity::LineWise);
editor.run_edit_command(&EditCommand::PasteCutBufferAfter);
assert_eq!(editor.get_buffer(), "ab");
}
#[test]
fn paste_after_linewise_middle_line_lands_below() {
let mut editor = editor_with("a\nb");
editor.cut_buffer.set("X", Granularity::LineWise);
editor.line_buffer.set_insertion_point(0); editor.run_edit_command(&EditCommand::PasteCutBufferAfter);
assert_eq!(editor.get_buffer(), "a\nX\nb");
}
#[test]
fn visual_replace_char_replaces_whole_selection() {
let mut editor = vi_editor("hello", PromptViMode::Normal);
editor.run_edit_command(&EditCommand::MoveToPosition {
position: 0,
select: false,
});
for _ in 0..2 {
editor.run_edit_command(&EditCommand::MoveRight { select: true });
}
assert_eq!(editor.get_selection(), Some((0, 3))); editor.run_edit_command(&EditCommand::ReplaceChar('x'));
assert_eq!(editor.get_buffer(), "xxxlo");
}
#[test]
fn cut_line_up_on_first_line_cuts_only_that_line() {
let mut editor = editor_with("aaa\nbbb\nccc");
editor.move_to_position(1, false);
editor.run_edit_command(&EditCommand::Cut {
target: MotionTarget::Line(Direction::Backward),
granularity: Granularity::LineWise,
});
assert_eq!(editor.get_buffer(), "bbb\nccc");
assert_eq!(editor.insertion_point(), 0);
let (content, gran) = editor.cut_buffer.get();
assert_eq!(content, "aaa\n");
assert_eq!(gran, Granularity::LineWise);
}
#[test]
fn cut_line_down_on_single_line_buffer_empties_it() {
let mut editor = editor_with("aaa");
editor.move_to_position(1, false);
editor.run_edit_command(&EditCommand::Cut {
target: MotionTarget::Line(Direction::Forward),
granularity: Granularity::LineWise,
});
assert_eq!(editor.get_buffer(), "");
let (content, gran) = editor.cut_buffer.get();
assert_eq!(content, "aaa");
assert_eq!(gran, Granularity::LineWise);
}
#[test]
fn change_lineedge_linewise_blanks_current_line() {
let mut editor = linewise_editor();
editor.run_edit_command(&EditCommand::Change {
target: MotionTarget::LineEdge(Direction::Forward),
granularity: Granularity::LineWise,
});
assert_eq!(editor.get_buffer(), "aaa\n\nccc");
assert_eq!(editor.insertion_point(), 4);
let (content, gran) = editor.cut_buffer.get();
assert_eq!(content, "bbb");
assert_eq!(gran, Granularity::LineWise);
}
#[test]
fn change_line_down_blanks_current_and_next() {
let mut editor = linewise_editor();
editor.run_edit_command(&EditCommand::Change {
target: MotionTarget::Line(Direction::Forward),
granularity: Granularity::LineWise,
});
assert_eq!(editor.get_buffer(), "aaa\n");
assert_eq!(editor.insertion_point(), 4);
let (content, gran) = editor.cut_buffer.get();
assert_eq!(content, "bbb\nccc");
assert_eq!(gran, Granularity::LineWise);
}
#[test]
fn change_line_up_blanks_current_and_prev() {
let mut editor = linewise_editor();
editor.run_edit_command(&EditCommand::Change {
target: MotionTarget::Line(Direction::Backward),
granularity: Granularity::LineWise,
});
assert_eq!(editor.get_buffer(), "\nccc");
assert_eq!(editor.insertion_point(), 0);
let (content, gran) = editor.cut_buffer.get();
assert_eq!(content, "aaa\nbbb");
assert_eq!(gran, Granularity::LineWise);
}
#[test]
fn change_bufferedge_back_matches_legacy_leave_blank_command() {
let mut editor = linewise_editor();
editor.run_edit_command(&EditCommand::Change {
target: MotionTarget::BufferEdge(Direction::Backward),
granularity: Granularity::LineWise,
});
assert_eq!(editor.get_buffer(), "\nccc");
assert_eq!(editor.insertion_point(), 0);
let (content, gran) = editor.cut_buffer.get();
assert_eq!(content, "aaa\nbbb");
assert_eq!(gran, Granularity::LineWise);
}
#[test]
fn change_bufferedge_fwd_matches_legacy_leave_blank_command() {
let mut editor = linewise_editor();
editor.run_edit_command(&EditCommand::Change {
target: MotionTarget::BufferEdge(Direction::Forward),
granularity: Granularity::LineWise,
});
assert_eq!(editor.get_buffer(), "aaa\n");
assert_eq!(editor.insertion_point(), 4);
let (content, gran) = editor.cut_buffer.get();
assert_eq!(content, "bbb\nccc");
assert_eq!(gran, Granularity::LineWise);
}
#[test]
fn change_charwise_behaves_like_cut() {
let mut editor = linewise_editor(); editor.run_edit_command(&EditCommand::Change {
target: MotionTarget::LineEdge(Direction::Forward),
granularity: Granularity::CharWise,
});
assert_eq!(editor.get_buffer(), "aaa\nb\nccc"); let (content, gran) = editor.cut_buffer.get();
assert_eq!(content, "bb");
assert_eq!(gran, Granularity::CharWise);
}
#[test]
fn cut_from_start_linewise_cuts_through_current_line() {
let mut editor = linewise_editor();
editor.run_edit_command(&EditCommand::CutFromStartLinewise {
leave_blank_line: false,
});
assert_eq!(editor.get_buffer(), "ccc");
assert_eq!(editor.insertion_point(), 0);
let (content, mode) = editor.cut_buffer.get();
assert_eq!(content, "aaa\nbbb\n");
assert!(matches!(mode, Granularity::LineWise));
}
#[test]
fn cut_from_start_linewise_leave_blank_keeps_empty_line() {
let mut editor = linewise_editor();
editor.run_edit_command(&EditCommand::CutFromStartLinewise {
leave_blank_line: true,
});
assert_eq!(editor.get_buffer(), "\nccc");
assert_eq!(editor.insertion_point(), 0);
let (content, mode) = editor.cut_buffer.get();
assert_eq!(content, "aaa\nbbb");
assert!(matches!(mode, Granularity::LineWise));
}
#[test]
fn cut_to_end_linewise_cuts_from_current_line() {
let mut editor = linewise_editor();
editor.run_edit_command(&EditCommand::CutToEndLinewise {
leave_blank_line: false,
});
assert_eq!(editor.get_buffer(), "aaa");
assert_eq!(editor.insertion_point(), 3);
let (content, mode) = editor.cut_buffer.get();
assert_eq!(content, "bbb\nccc");
assert!(matches!(mode, Granularity::LineWise));
}
#[test]
fn copy_current_line_is_linewise_and_nondestructive() {
let mut editor = linewise_editor();
editor.run_edit_command(&EditCommand::CopyCurrentLine);
assert_eq!(editor.get_buffer(), "aaa\nbbb\nccc"); let (content, mode) = editor.cut_buffer.get();
assert_eq!(content, "bbb\n");
assert!(matches!(mode, Granularity::LineWise));
}
#[rstest]
#[case("abc def ghi", 11, "abc def ")]
#[case("abc def-ghi", 11, "abc def-")]
#[case("abc def.ghi", 11, "abc ")]
fn test_cut_word_left(#[case] input: &str, #[case] position: usize, #[case] expected: &str) {
let mut editor = editor_with(input);
editor.line_buffer.set_insertion_point(position);
editor.cut_word_left();
assert_eq!(editor.get_buffer(), expected);
}
#[rstest]
#[case("abc def ghi", 11, "abc def ")]
#[case("abc def-ghi", 11, "abc ")]
#[case("abc def.ghi", 11, "abc ")]
#[case("abc def gh ", 11, "abc def ")]
fn test_cut_big_word_left(
#[case] input: &str,
#[case] position: usize,
#[case] expected: &str,
) {
let mut editor = editor_with(input);
editor.line_buffer.set_insertion_point(position);
editor.cut_big_word_left();
assert_eq!(editor.get_buffer(), expected);
}
#[rstest]
#[case("hello world", 0, 'l', 1, false, "lo world")]
#[case("hello world", 0, 'l', 1, true, "llo world")]
#[ignore = "Deleting two consecutive chars is not implemented correctly and needs the multiplier explicitly."]
#[case("hello world", 0, 'l', 2, false, "o world")]
#[case("hello world", 0, 'h', 1, false, "hello world")]
#[case("hello world", 0, 'l', 3, true, "ld")]
#[case("hello world", 4, 'o', 1, true, "hellorld")]
#[case("hello world", 4, 'w', 1, false, "hellorld")]
#[case("hello world", 4, 'o', 1, false, "hellrld")]
fn test_cut_right_until_char(
#[case] input: &str,
#[case] position: usize,
#[case] search_char: char,
#[case] repeat: usize,
#[case] before_char: bool,
#[case] expected: &str,
) {
let mut editor = editor_with(input);
editor.line_buffer.set_insertion_point(position);
for _ in 0..repeat {
editor.cut_right_until_char(search_char, before_char, true);
}
assert_eq!(editor.get_buffer(), expected);
}
#[rstest]
#[case("abc", 1, 'X', "aXc")]
#[case("abc", 1, '🔄', "a🔄c")]
#[case("a🔄c", 1, 'X', "aXc")]
#[case("a🔄c", 1, '🔀', "a🔀c")]
fn test_replace_char(
#[case] input: &str,
#[case] position: usize,
#[case] replacement: char,
#[case] expected: &str,
) {
let mut editor = editor_with(input);
editor.line_buffer.set_insertion_point(position);
editor.replace_char(replacement);
assert_eq!(editor.get_buffer(), expected);
}
#[test]
fn visual_replace_char_replaces_only_the_caret_grapheme() {
let mut editor = vi_editor("hello", PromptViMode::Visual);
editor.run_edit_command(&EditCommand::MoveToStart { select: false });
editor.update_selection_anchor(true); editor.replace_char('x');
assert_eq!(editor.get_buffer(), "xello");
}
#[test]
fn visual_move_line_does_not_panic_across_line_boundary() {
let mut editor = vi_editor("a\nbc", PromptViMode::Visual);
editor.run_edit_command(&EditCommand::MoveToStart { select: false });
editor.update_selection_anchor(true);
editor.run_edit_command(&EditCommand::MoveLineDown { select: true });
editor.run_edit_command(&EditCommand::MoveLineDown { select: true });
editor.run_edit_command(&EditCommand::MoveLineUp { select: true });
}
#[test]
fn linewise_cut_last_line_eats_whole_crlf_terminator() {
let mut editor = editor_with("ab\r\ncd");
editor.operate(
Cursor::point(5), OperatorVerb::Cut,
Granularity::LineWise,
);
assert_eq!(editor.get_buffer(), "ab");
let mut editor = editor_with("ab\ncd");
editor.operate(Cursor::point(4), OperatorVerb::Cut, Granularity::LineWise);
assert_eq!(editor.get_buffer(), "ab");
}
#[test]
fn cut_to_end_linewise_eats_whole_crlf_terminator() {
let mut editor = editor_with("x\r\ncd");
editor.line_buffer.set_insertion_point(3); editor.cut_from_end_linewise(false);
assert_eq!(editor.get_buffer(), "x");
assert!(!editor.get_buffer().contains('\r'));
}
fn selected_text(editor: &Editor) -> String {
let c = editor.line_buffer().cursor();
editor.get_buffer()[c.start()..c.end()].to_string()
}
#[test]
fn visual_move_word_right_end_select_covers_last_grapheme() {
let mut editor = vi_editor("foo bar", PromptViMode::Visual);
editor.run_edit_command(&EditCommand::MoveToStart { select: false });
editor.run_edit_command(&EditCommand::MoveWordRightEnd { select: true });
assert_eq!(selected_text(&editor), "foo");
}
#[test]
fn visual_move_to_line_start_after_end_keeps_anchor_grapheme() {
let mut editor = vi_editor("abc def", PromptViMode::Visual);
editor.run_edit_command(&EditCommand::MoveToPosition {
position: 4, select: false,
});
editor.run_edit_command(&EditCommand::MoveToLineEnd { select: true });
editor.run_edit_command(&EditCommand::MoveToLineStart { select: true });
assert_eq!(selected_text(&editor), "abc d");
}
#[test]
fn visual_line_jk_keeps_anchor_grapheme_covered() {
let mut editor = vi_editor("x\ny\nz", PromptViMode::Visual);
editor.run_edit_command(&EditCommand::MoveToPosition {
position: 2, select: false,
});
editor.update_selection_anchor(true);
editor.run_edit_command(&EditCommand::MoveLineDown { select: true });
editor.run_edit_command(&EditCommand::MoveLineUp { select: true });
editor.run_edit_command(&EditCommand::MoveLineUp { select: true });
let c = editor.line_buffer().cursor();
assert!(
c.start() <= 2 && 2 < c.end(),
"byte 2 ('y', the anchor) must stay covered; selection was {:?}",
c.start()..c.end()
);
}
#[test]
fn visual_line_jk_preserves_caret_column() {
let mut editor = vi_editor("ab cd\nef gh", PromptViMode::Visual);
editor.run_edit_command(&EditCommand::MoveToPosition {
position: 1, select: false,
});
editor.update_selection_anchor(true);
for _ in 0..2 {
editor.run_edit_command(&EditCommand::MoveLineDown { select: true });
editor.run_edit_command(&EditCommand::MoveLineUp { select: true });
assert_eq!(
editor.insertion_point(),
1,
"caret drifted off 'b' after a j/k round-trip"
);
}
}
#[test]
fn select_until_char_in_bar_mode_opens_selection() {
let mut editor = editor_with("This is a test!"); editor.line_buffer.set_insertion_point(0);
editor.run_edit_command(&EditCommand::MoveRightUntil {
c: 's',
select: true,
});
assert_eq!(editor.get_selection(), Some((0, 3)));
let mut editor = editor_with("This is a test!");
editor
.line_buffer
.set_insertion_point(editor.line_buffer.len());
editor.run_edit_command(&EditCommand::MoveLeftUntil {
c: 'T',
select: true,
});
assert!(editor.get_selection().is_some());
}
fn str_to_edit_commands(s: &str) -> Vec<EditCommand> {
s.chars().map(EditCommand::InsertChar).collect()
}
#[test]
fn test_undo_insert_works_on_work_boundaries() {
let mut editor = editor_with("This is a");
for cmd in str_to_edit_commands(" test") {
editor.run_edit_command(&cmd);
}
assert_eq!(editor.get_buffer(), "This is a test");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "This is a");
editor.run_edit_command(&EditCommand::Redo);
assert_eq!(editor.get_buffer(), "This is a test");
}
#[test]
fn test_undo_backspace_works_on_word_boundaries() {
let mut editor = editor_with("This is a test");
for _ in 0..6 {
editor.run_edit_command(&EditCommand::Backspace);
}
assert_eq!(editor.get_buffer(), "This is ");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "This is a");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "This is a test");
}
#[test]
fn test_undo_delete_works_on_word_boundaries() {
let mut editor = editor_with("This is a test");
editor.line_buffer.set_insertion_point(0);
for _ in 0..7 {
editor.run_edit_command(&EditCommand::Delete);
}
assert_eq!(editor.get_buffer(), "s a test");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "is a test");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "This is a test");
}
#[test]
fn test_undo_insert_with_newline() {
let mut editor = editor_with("This is a");
for cmd in str_to_edit_commands(" \n test") {
editor.run_edit_command(&cmd);
}
assert_eq!(editor.get_buffer(), "This is a \n test");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "This is a \n");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "This is a");
}
#[test]
fn test_undo_backspace_with_newline() {
let mut editor = editor_with("This is a \n test");
for _ in 0..8 {
editor.run_edit_command(&EditCommand::Backspace);
}
assert_eq!(editor.get_buffer(), "This is ");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "This is a");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "This is a \n");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "This is a \n test");
}
#[test]
fn test_undo_backspace_with_crlf() {
let mut editor = editor_with("This is a \r\n test");
for _ in 0..8 {
editor.run_edit_command(&EditCommand::Backspace);
}
assert_eq!(editor.get_buffer(), "This is ");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "This is a");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "This is a \r\n");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "This is a \r\n test");
}
#[test]
fn test_undo_delete_with_newline() {
let mut editor = editor_with("This \n is a test");
editor.line_buffer.set_insertion_point(0);
for _ in 0..8 {
editor.run_edit_command(&EditCommand::Delete);
}
assert_eq!(editor.get_buffer(), "s a test");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "is a test");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "\n is a test");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "This \n is a test");
}
#[test]
fn test_undo_delete_with_crlf() {
let mut editor = editor_with("This \r\n is a test");
editor.line_buffer.set_insertion_point(0);
for _ in 0..8 {
editor.run_edit_command(&EditCommand::Delete);
}
assert_eq!(editor.get_buffer(), "s a test");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "is a test");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "\r\n is a test");
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "This \r\n is a test");
}
#[test]
fn test_swap_cursor_and_anchor() {
let mut editor = editor_with("This is some test content");
editor.line_buffer.set_insertion_point(0);
editor.update_selection_anchor(true);
for _ in 0..3 {
editor.run_edit_command(&EditCommand::MoveRight { select: true });
}
assert_eq!(editor.line_buffer().selection_anchor(), Some(0));
assert_eq!(editor.insertion_point(), 3);
assert_eq!(editor.get_selection(), Some((0, 3)));
editor.run_edit_command(&EditCommand::SwapCursorAndAnchor);
assert_eq!(editor.line_buffer().selection_anchor(), Some(3));
assert_eq!(editor.insertion_point(), 0);
assert_eq!(editor.get_selection(), Some((0, 3)));
editor.run_edit_command(&EditCommand::SwapCursorAndAnchor);
assert_eq!(editor.line_buffer().selection_anchor(), Some(0));
assert_eq!(editor.insertion_point(), 3);
assert_eq!(editor.get_selection(), Some((0, 3)));
}
#[cfg(test)]
fn normal_mode_step(buf: &str, start: usize, cross: bool, cmd: &EditCommand) -> usize {
let mut e = editor_with(buf);
e.set_cross_line_cursor(cross);
e.line_buffer.set_insertion_point(start);
e.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
e.run_edit_command(cmd);
e.insertion_point()
}
#[test]
fn cross_line_cursor_on_crosses_newline() {
let r = &EditCommand::MoveRight { select: false };
let l = &EditCommand::MoveLeft { select: false };
assert_eq!(normal_mode_step("ab\ncd", 1, true, r), 3);
assert_eq!(normal_mode_step("ab\ncd", 3, true, l), 1);
assert_eq!(normal_mode_step("ab\r\ncd", 1, true, r), 4);
assert_eq!(normal_mode_step("ab\r\ncd", 4, true, l), 1);
}
#[test]
fn cross_line_cursor_off_clamps_to_line() {
let r = &EditCommand::MoveRight { select: false };
let l = &EditCommand::MoveLeft { select: false };
assert_ne!(normal_mode_step("ab\ncd", 1, false, r), 3);
assert_ne!(normal_mode_step("ab\ncd", 3, false, l), 1);
}
#[test]
fn append_at_buffer_end_appends_past_block_caret() {
let mut editor = editor_with("abc");
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
editor.run_edit_command(&EditCommand::MoveToLineEnd { select: false });
editor.prepare_append_at_buffer_end();
editor.run_edit_command(&EditCommand::InsertString("def".into()));
assert_eq!(editor.get_buffer(), "abcdef");
let mut editor = editor_with("caf\u{e9}");
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
editor.run_edit_command(&EditCommand::MoveToLineEnd { select: false });
editor.prepare_append_at_buffer_end();
editor.run_edit_command(&EditCommand::InsertString("X".into()));
assert_eq!(editor.get_buffer(), "caf\u{e9}X");
}
#[test]
fn cursor_at_buffer_end_holds_on_last_grapheme_in_normal_mode() {
let mut editor = editor_with("abc");
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
editor.run_edit_command(&EditCommand::MoveToLineEnd { select: false });
assert!(editor.is_cursor_at_buffer_end());
let mut editor = editor_with("caf\u{e9}");
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
editor.run_edit_command(&EditCommand::MoveToLineEnd { select: false });
assert!(editor.is_cursor_at_buffer_end());
let mut editor = editor_with("abc");
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
editor.run_edit_command(&EditCommand::MoveToLineStart { select: false });
assert!(!editor.is_cursor_at_buffer_end());
let mut editor = editor_with("abc");
editor.line_buffer.set_insertion_point(0);
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
editor.update_selection_anchor(true);
for _ in 0..3 {
editor.run_edit_command(&EditCommand::MoveRight { select: true });
}
assert!(!editor.is_cursor_at_buffer_end());
}
#[test]
fn test_vi_normal_mode_inclusive_selection() {
let mut editor = editor_with("This is some test content");
editor.line_buffer.set_insertion_point(0);
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
editor.update_selection_anchor(true);
for _ in 0..3 {
editor.run_edit_command(&EditCommand::MoveRight { select: true });
}
assert_eq!(editor.line_buffer().selection_anchor(), Some(0));
assert_eq!(editor.insertion_point(), 3);
assert_eq!(editor.get_selection(), Some((0, 4)));
}
#[test]
fn test_vi_normal_mode_inclusive_selection_backward() {
let mut editor = editor_with("This is some test content");
editor.line_buffer.set_insertion_point(4); editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
editor.update_selection_anchor(true);
for _ in 0..3 {
editor.run_edit_command(&EditCommand::MoveLeft { select: true });
}
assert_eq!(editor.line_buffer().selection_anchor(), Some(5));
assert_eq!(editor.insertion_point(), 1); assert_eq!(editor.get_selection(), Some((1, 5)));
}
#[test]
fn test_vi_normal_mode_cut_selection_backward() {
let mut editor = editor_with("This is some test content");
editor.line_buffer.set_insertion_point(4); editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
editor.update_selection_anchor(true);
for _ in 0..3 {
editor.run_edit_command(&EditCommand::MoveLeft { select: true });
}
assert_eq!(editor.get_selection(), Some((1, 5)));
editor.run_edit_command(&EditCommand::CutSelection);
assert_eq!(editor.get_buffer(), "Tis some test content");
assert_eq!(editor.insertion_point(), 1); }
#[test]
fn test_vi_visual_mode_c_command() {
let mut editor = editor_with("hello world");
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
editor.line_buffer.set_insertion_point(0);
editor.update_selection_anchor(true);
for _ in 0..4 {
editor.run_edit_command(&EditCommand::MoveRight { select: true });
}
assert_eq!(editor.get_selection(), Some((0, 5)));
editor.run_edit_command(&EditCommand::CutSelection);
assert_eq!(editor.get_buffer(), " world");
assert_eq!(editor.insertion_point(), 0);
}
#[test]
fn test_vi_normal_mode_c_command_with_selection() {
let mut editor = editor_with("hello world");
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
editor.line_buffer.set_insertion_point(0);
editor.update_selection_anchor(true);
for _ in 0..4 {
editor.run_edit_command(&EditCommand::MoveRight { select: true });
}
assert_eq!(editor.get_selection(), Some((0, 5)));
editor.run_edit_command(&EditCommand::CutSelection);
assert_eq!(editor.get_buffer(), " world");
assert_eq!(editor.insertion_point(), 0);
}
#[cfg(feature = "system_clipboard")]
mod without_system_clipboard {
use super::*;
#[test]
fn test_cut_selection_system() {
let mut editor = editor_with("This is a test!");
editor.line_buffer.set_insertion_point(0);
editor
.line_buffer
.set_selection_anchor(Some(editor.line_buffer.len()));
editor.run_edit_command(&EditCommand::CutSelectionSystem);
assert!(editor.line_buffer.get_buffer().is_empty());
}
#[test]
fn test_copypaste_selection_system() {
let s = "This is a test!";
let mut editor = editor_with(s);
editor.line_buffer.set_insertion_point(0);
editor
.line_buffer
.set_selection_anchor(Some(editor.line_buffer.len()));
editor.run_edit_command(&EditCommand::CopySelectionSystem);
editor.run_edit_command(&EditCommand::PasteSystem);
pretty_assertions::assert_eq!(editor.line_buffer.len(), s.len() * 2);
}
}
#[test]
fn test_cut_inside_brackets() {
let mut editor = editor_with("foo(bar)baz");
editor.move_to_position(5, false); editor.cut_inside_pair('(', ')');
assert_eq!(editor.get_buffer(), "foo()baz");
assert_eq!(editor.insertion_point(), 4);
assert_eq!(editor.cut_buffer.get().0, "bar");
let mut editor = editor_with("foo(bar)baz");
editor.move_to_position(0, false);
editor.cut_inside_pair('(', ')');
assert_eq!(editor.get_buffer(), "foo()baz");
assert_eq!(editor.insertion_point(), 4);
assert_eq!(editor.cut_buffer.get().0, "bar");
let mut editor = editor_with("foo bar baz");
editor.move_to_position(4, false);
editor.cut_inside_pair('(', ')');
assert_eq!(editor.get_buffer(), "foo bar baz");
assert_eq!(editor.insertion_point(), 4);
assert_eq!(editor.cut_buffer.get().0, "");
}
#[test]
fn test_cut_inside_quotes() {
let mut editor = editor_with("foo\"bar\"baz");
editor.move_to_position(5, false); editor.cut_inside_pair('"', '"');
assert_eq!(editor.get_buffer(), "foo\"\"baz");
assert_eq!(editor.insertion_point(), 4);
assert_eq!(editor.cut_buffer.get().0, "bar");
let mut editor = editor_with("foo\"bar\"baz");
editor.move_to_position(0, false);
editor.cut_inside_pair('"', '"');
assert_eq!(editor.get_buffer(), "foo\"\"baz");
assert_eq!(editor.insertion_point(), 4);
assert_eq!(editor.cut_buffer.get().0, "bar");
let mut editor = editor_with("foo bar baz");
editor.move_to_position(4, false);
editor.cut_inside_pair('"', '"');
assert_eq!(editor.get_buffer(), "foo bar baz");
assert_eq!(editor.insertion_point(), 4);
}
#[test]
fn test_cut_inside_nested() {
let mut editor = editor_with("foo(bar(baz)qux)quux");
editor.move_to_position(8, false); editor.cut_inside_pair('(', ')');
assert_eq!(editor.get_buffer(), "foo(bar()qux)quux");
assert_eq!(editor.insertion_point(), 8);
assert_eq!(editor.cut_buffer.get().0, "baz");
editor.move_to_position(4, false); editor.cut_inside_pair('(', ')');
assert_eq!(editor.get_buffer(), "foo()quux");
assert_eq!(editor.insertion_point(), 4);
assert_eq!(editor.cut_buffer.get().0, "bar()qux");
}
#[test]
fn test_yank_inside_brackets() {
let mut editor = editor_with("foo(bar)baz");
editor.move_to_position(5, false); editor.copy_inside_pair('(', ')');
assert_eq!(editor.get_buffer(), "foo(bar)baz"); assert_eq!(editor.insertion_point(), 5);
editor.paste_cut_buffer();
assert_eq!(editor.get_buffer(), "foo(bbarar)baz");
let mut editor = editor_with("foo(bar)baz");
editor.move_to_position(0, false);
editor.copy_inside_pair('(', ')');
assert_eq!(editor.get_buffer(), "foo(bar)baz");
assert_eq!(editor.insertion_point(), 0);
}
#[test]
fn test_yank_inside_quotes() {
let mut editor = editor_with("foo\"bar\"baz");
editor.move_to_position(5, false); editor.copy_inside_pair('"', '"');
assert_eq!(editor.get_buffer(), "foo\"bar\"baz"); assert_eq!(editor.insertion_point(), 5); assert_eq!(editor.cut_buffer.get().0, "bar");
let mut editor = editor_with("foo bar baz");
editor.move_to_position(4, false);
editor.copy_inside_pair('"', '"');
assert_eq!(editor.get_buffer(), "foo bar baz");
assert_eq!(editor.insertion_point(), 4);
assert_eq!(editor.cut_buffer.get().0, "");
}
#[test]
fn test_yank_inside_nested() {
let mut editor = editor_with("foo(bar(baz)qux)quux");
editor.move_to_position(8, false); editor.copy_inside_pair('(', ')');
assert_eq!(editor.get_buffer(), "foo(bar(baz)qux)quux"); assert_eq!(editor.insertion_point(), 8);
assert_eq!(editor.cut_buffer.get().0, "baz");
editor.paste_cut_buffer();
assert_eq!(editor.get_buffer(), "foo(bar(bazbaz)qux)quux");
editor.move_to_position(4, false); editor.copy_inside_pair('(', ')');
assert_eq!(editor.get_buffer(), "foo(bar(bazbaz)qux)quux");
assert_eq!(editor.insertion_point(), 4);
assert_eq!(editor.cut_buffer.get().0, "bar(bazbaz)qux");
}
#[test]
fn test_kill_line() {
let mut editor = editor_with("foo\nbar");
editor.move_to_position(1, false);
editor.kill_line();
assert_eq!(editor.get_buffer(), "f\nbar"); assert_eq!(editor.insertion_point(), 1); assert_eq!(editor.cut_buffer.get().0, "oo");
editor.kill_line();
assert_eq!(editor.get_buffer(), "fbar"); assert_eq!(editor.insertion_point(), 1);
assert_eq!(editor.cut_buffer.get().0, "\n");
let mut editor = editor_with("foo\nbar");
editor.move_to_position(3, false);
editor.kill_line();
assert_eq!(editor.get_buffer(), "foobar"); assert_eq!(editor.insertion_point(), 3); assert_eq!(editor.cut_buffer.get().0, "\n");
editor.kill_line();
assert_eq!(editor.get_buffer(), "foo"); assert_eq!(editor.insertion_point(), 3);
assert_eq!(editor.cut_buffer.get().0, "bar");
editor.kill_line();
assert_eq!(editor.get_buffer(), "foo");
assert_eq!(editor.insertion_point(), 3);
assert_eq!(editor.cut_buffer.get().0, "bar");
}
#[test]
fn test_kill_line_with_windows_newline() {
let mut editor = editor_with("foo\r\nbar");
editor.move_to_position(1, false);
editor.kill_line();
assert_eq!(editor.get_buffer(), "f\r\nbar"); assert_eq!(editor.insertion_point(), 1); assert_eq!(editor.cut_buffer.get().0, "oo");
editor.kill_line();
assert_eq!(editor.get_buffer(), "fbar"); assert_eq!(editor.insertion_point(), 1);
assert_eq!(editor.cut_buffer.get().0, "\r\n");
let mut editor = editor_with("foo\r\nbar");
editor.move_to_position(3, false);
editor.kill_line();
assert_eq!(editor.get_buffer(), "foobar"); assert_eq!(editor.insertion_point(), 3); assert_eq!(editor.cut_buffer.get().0, "\r\n");
}
#[test]
fn test_vi_normal_mode_shift_select_right_c_command() {
let mut editor = editor_with("hello world");
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
editor.line_buffer.set_insertion_point(0);
editor.update_selection_anchor(true);
for _ in 0..4 {
editor.run_edit_command(&EditCommand::MoveRight { select: true });
}
assert_eq!(editor.insertion_point(), 4);
assert_eq!(editor.line_buffer().selection_anchor(), Some(0));
assert_eq!(editor.get_selection(), Some((0, 5)));
editor.run_edit_command(&EditCommand::CutSelection);
assert_eq!(editor.get_buffer(), " world");
assert_eq!(editor.insertion_point(), 0);
assert_eq!(editor.cut_buffer.get().0, "hello");
}
#[test]
fn test_vi_mode_selection_calculation_bug() {
let mut editor = editor_with("hello world");
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
editor.line_buffer.set_insertion_point(0);
editor.update_selection_anchor(true);
for _ in 0..4 {
editor.run_edit_command(&EditCommand::MoveRight { select: true });
}
assert_eq!(editor.get_selection(), Some((0, 5)));
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Insert));
assert_eq!(editor.get_selection(), Some((0, 5))); }
#[test]
fn test_vi_c_command_mode_switch_bug_fix() {
let mut editor = editor_with("hello world");
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
editor.line_buffer.set_insertion_point(0);
editor.update_selection_anchor(true);
for _ in 0..4 {
editor.run_edit_command(&EditCommand::MoveRight { select: true });
}
assert_eq!(editor.get_selection(), Some((0, 5)));
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Insert));
editor.run_edit_command(&EditCommand::CutSelection);
assert_eq!(editor.get_buffer(), " world");
assert_eq!(editor.insertion_point(), 0);
assert_eq!(editor.cut_buffer.get().0, "hello");
}
#[test]
fn test_vi_x_command_with_shift_selection() {
let mut editor = editor_with("hello world");
editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal));
editor.line_buffer.set_insertion_point(0);
editor.update_selection_anchor(true);
for _ in 0..4 {
editor.run_edit_command(&EditCommand::MoveRight { select: true });
}
assert_eq!(editor.get_selection(), Some((0, 5)));
editor.run_edit_command(&EditCommand::CutChar);
assert_eq!(editor.get_buffer(), " world");
assert_eq!(editor.insertion_point(), 0);
assert_eq!(editor.cut_buffer.get().0, "hello");
}
#[rstest]
#[case("hello world test", 7, "hello test", 6, "world")] #[case("hello world test", 6, "hello test", 6, "world")] #[case("hello world test", 10, "hello test", 6, "world")] fn test_cut_inside_word(
#[case] input: &str,
#[case] cursor_pos: usize,
#[case] expected_buffer: &str,
#[case] expected_cursor: usize,
#[case] expected_cut: &str,
) {
let mut editor = editor_with(input);
editor.move_to_position(cursor_pos, false);
editor.cut_text_object(TextObject {
scope: TextObjectScope::Inner,
object_type: TextObjectType::Word,
});
assert_eq!(editor.get_buffer(), expected_buffer);
assert_eq!(editor.insertion_point(), expected_cursor);
assert_eq!(editor.cut_buffer.get().0, expected_cut);
}
#[rstest]
#[case("hello world test", 7, "world")] #[case("hello world test", 6, "world")] #[case("hello world test", 10, "world")] fn test_yank_inside_word(
#[case] input: &str,
#[case] cursor_pos: usize,
#[case] expected_yank: &str,
) {
let mut editor = editor_with(input);
editor.move_to_position(cursor_pos, false);
editor.copy_text_object(TextObject {
scope: TextObjectScope::Inner,
object_type: TextObjectType::Word,
});
assert_eq!(editor.get_buffer(), input); assert_eq!(editor.insertion_point(), cursor_pos); assert_eq!(editor.cut_buffer.get().0, expected_yank);
}
#[rstest]
#[case("hello world test", 7, "hello test", 6, "world ")] #[case("hello world", 7, "hello", 5, " world")] #[case("word test", 2, "test", 0, "word ")] #[case("hello word", 7, "hello", 5, " word")] #[case("word", 2, "", 0, "word")] #[case(" word", 2, "", 0, " word")] #[case("word.", 2, ".", 0, "word")] #[case(".word", 2, ".", 1, "word")] #[case("(word)", 2, "()", 1, "word")] #[case("hello,world", 2, ",world", 0, "hello")] #[case("hello,world", 7, "hello,", 6, "world")] fn test_cut_around_word(
#[case] input: &str,
#[case] cursor_pos: usize,
#[case] expected_buffer: &str,
#[case] expected_cursor: usize,
#[case] expected_cut: &str,
) {
let mut editor = editor_with(input);
editor.move_to_position(cursor_pos, false);
editor.cut_text_object(TextObject {
scope: TextObjectScope::Around,
object_type: TextObjectType::Word,
});
assert_eq!(editor.get_buffer(), expected_buffer);
assert_eq!(editor.insertion_point(), expected_cursor);
assert_eq!(editor.cut_buffer.get().0, expected_cut);
}
#[rstest]
#[case("hello world test", 7, "world ")] #[case("hello world", 7, " world")] #[case("word test", 2, "word ")] fn test_yank_around_word(
#[case] input: &str,
#[case] cursor_pos: usize,
#[case] expected_yank: &str,
) {
let mut editor = editor_with(input);
editor.move_to_position(cursor_pos, false);
editor.copy_text_object(TextObject {
scope: TextObjectScope::Around,
object_type: TextObjectType::Word,
});
assert_eq!(editor.get_buffer(), input); assert_eq!(editor.insertion_point(), cursor_pos); assert_eq!(editor.cut_buffer.get().0, expected_yank);
}
#[rstest]
#[case("hello big-word test", 10, "hello test", 6, "big-word")] #[case("hello BIGWORD test", 10, "hello test", 6, "BIGWORD")] #[case("test@example.com file", 8, " file", 0, "test@example.com")] #[case("test@example.com file", 17, "test@example.com ", 17, "file")] fn test_cut_inside_big_word(
#[case] input: &str,
#[case] cursor_pos: usize,
#[case] expected_buffer: &str,
#[case] expected_cursor: usize,
#[case] expected_cut: &str,
) {
let mut editor = editor_with(input);
editor.move_to_position(cursor_pos, false);
editor.cut_text_object(TextObject {
scope: TextObjectScope::Inner,
object_type: TextObjectType::BigWord,
});
assert_eq!(editor.get_buffer(), expected_buffer);
assert_eq!(editor.insertion_point(), expected_cursor);
assert_eq!(editor.cut_buffer.get().0, expected_cut);
}
#[rstest]
#[case("hello-world test", 2, "-world test", 0, "hello")] #[case("hello-world test", 5, "helloworld test", 5, "-")] #[case("hello-world test", 8, "hello- test", 6, "world")] #[case("a-b-c test", 0, "-b-c test", 0, "a")] #[case("a-b-c test", 2, "a--c test", 2, "b")] fn test_cut_inside_word_with_punctuation(
#[case] input: &str,
#[case] cursor_pos: usize,
#[case] expected_buffer: &str,
#[case] expected_cursor: usize,
#[case] expected_cut: &str,
) {
let mut editor = editor_with(input);
editor.move_to_position(cursor_pos, false);
editor.cut_text_object(TextObject {
scope: TextObjectScope::Inner,
object_type: TextObjectType::Word,
});
assert_eq!(editor.get_buffer(), expected_buffer);
assert_eq!(editor.insertion_point(), expected_cursor);
assert_eq!(editor.cut_buffer.get().0, expected_cut);
}
#[rstest]
#[case("hello-world test", 2, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "-world test", "hello")] #[case("hello-world test", 2, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::BigWord }, " test", "hello-world")] #[case("test@example.com", 6, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "test@", "example.com")] #[case("test@example.com", 6, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::BigWord }, "", "test@example.com")] fn test_word_vs_big_word_comparison(
#[case] input: &str,
#[case] cursor_pos: usize,
#[case] text_object: TextObject,
#[case] expected_buffer: &str,
#[case] expected_cut: &str,
) {
let mut editor = editor_with(input);
editor.move_to_position(cursor_pos, false);
editor.cut_text_object(text_object);
assert_eq!(editor.get_buffer(), expected_buffer);
assert_eq!(editor.cut_buffer.get().0, expected_cut);
}
#[rstest]
#[case("hello world", 0, "hello")] #[case("hello world", 4, "hello")] #[case("hello world", 6, "world")] #[case("hello world", 10, "world")] #[case("hello-world", 4, "hello")] #[case("hello-world", 5, "-")] #[case("hello-world", 6, "world")] fn test_cut_inside_word_boundaries(
#[case] input: &str,
#[case] cursor_pos: usize,
#[case] expected_cut: &str,
) {
let mut editor = editor_with(input);
editor.move_to_position(cursor_pos, false);
editor.cut_text_object(TextObject {
scope: TextObjectScope::Inner,
object_type: TextObjectType::Word,
});
assert_eq!(editor.cut_buffer.get().0, expected_cut);
}
#[rstest]
#[case("hello world", 0, "hello ")] #[case("hello world", 4, "hello ")] #[case("hello world", 6, " world")] #[case("hello world", 10, " world")] #[case("word", 0, "word")] #[case("word ", 0, "word ")] #[case(" word", 1, " word")] fn test_cut_around_word_boundaries(
#[case] input: &str,
#[case] cursor_pos: usize,
#[case] expected_cut: &str,
) {
let mut editor = editor_with(input);
editor.move_to_position(cursor_pos, false);
editor.cut_text_object(TextObject {
scope: TextObjectScope::Around,
object_type: TextObjectType::Word,
});
assert_eq!(editor.cut_buffer.get().0, expected_cut);
}
#[rstest]
fn test_cut_text_object_unicode_safety() {
let mut editor = editor_with("hello 🦀end");
editor.move_to_position(10, false); editor.move_to_position(6, false);
editor.cut_text_object(TextObject {
scope: TextObjectScope::Inner,
object_type: TextObjectType::Word,
});
assert!(editor.line_buffer.is_valid()); }
#[rstest]
#[case("hello world test", 5, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "helloworld test", 5, " ")] #[case("hello world", 6, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "helloworld", 5, " ")] #[case("hello world", 7, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "helloworld", 5, " ")] #[case(" hello", 1, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "hello", 0, " ")] #[case("hello ", 7, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "hello", 5, " ")] #[case("hello\tworld", 5, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "helloworld", 5, "\t")] #[case("hello\nworld", 5, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "helloworld", 5, "\n")] #[case("hello world test", 5, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::BigWord }, "helloworld test", 5, " ")] #[case("hello world", 6, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::BigWord }, "helloworld", 5, " ")] #[case(" ", 0, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "", 0, " ")] #[case(" ", 1, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "", 0, " ")] #[case("hello ", 5, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "hello", 5, " ")] #[case(" hello", 0, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "hello", 0, " ")] fn test_text_object_in_whitespace(
#[case] input: &str,
#[case] cursor_pos: usize,
#[case] text_object: TextObject,
#[case] expected_buffer: &str,
#[case] expected_cursor: usize,
#[case] expected_cut: &str,
) {
let mut editor = editor_with(input);
editor.move_to_position(cursor_pos, false);
editor.cut_text_object(text_object);
assert_eq!(editor.get_buffer(), expected_buffer);
assert_eq!(editor.insertion_point(), expected_cursor);
assert_eq!(editor.cut_buffer.get().0, expected_cut);
}
#[rstest]
#[case(r#"foo()bar"#, 4, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Brackets }, "foo()bar", 4, "")] #[case(r#"foo""bar"#, 4, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Quote }, "foo\"\"bar", 4, "")] #[case(r#"foo ()bar"#, 2, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Brackets }, "foo ()bar", 5, "")] #[case(r#"foo ""bar"#, 2, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Quote }, "foo \"\"bar", 5, "")] #[case(r#"foo (content)bar"#, 2, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Brackets }, "foo ()bar", 5, "content")] #[case(r#"foo "content"bar"#, 2, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Quote }, "foo \"\"bar", 5, "content")] #[case(r#"(first) (second)"#, 8, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Brackets }, "(first) ()", 9, "second")] #[case(r#""first" "second""#, 8, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Quote }, "\"first\"\"second\"", 7, " ")] #[case(r#"foo (bar)"#, 2, TextObject { scope: TextObjectScope::Around, object_type: TextObjectType::Brackets }, "foo ", 4, "(bar)")] #[case(r#"foo "bar""#, 2, TextObject { scope: TextObjectScope::Around, object_type: TextObjectType::Quote }, "foo ", 4, "\"bar\"")] fn test_text_object_jumping_behavior(
#[case] input: &str,
#[case] cursor_pos: usize,
#[case] text_object: TextObject,
#[case] expected_buffer: &str,
#[case] expected_cursor: usize,
#[case] expected_cut: &str,
) {
let mut editor = editor_with(input);
editor.move_to_position(cursor_pos, false);
editor.cut_text_object(text_object);
assert_eq!(editor.get_buffer(), expected_buffer);
assert_eq!(editor.insertion_point(), expected_cursor);
assert_eq!(editor.cut_buffer.get().0, expected_cut);
}
#[rstest]
#[case("foo(bar)baz", 5, TextObjectScope::Inner, Some(4..7))] #[case("foo[bar]baz", 5, TextObjectScope::Inner, Some(4..7))] #[case("foo{bar}baz", 5, TextObjectScope::Inner, Some(4..7))] #[case("foo()bar", 4, TextObjectScope::Inner, Some(4..4))] #[case("(nested[inner]outer)", 8, TextObjectScope::Inner, Some(8..13))] #[case("(nested[mixed{inner}brackets]outer)", 8, TextObjectScope::Inner, Some(8..28))] #[case("next(nested[mixed{inner}brackets]outer)", 0, TextObjectScope::Inner, Some(5..38))] #[case("foo (bar)baz", 0, TextObjectScope::Inner, Some(5..8))] #[case(" (bar)baz", 1, TextObjectScope::Inner, Some(5..8))] #[case("foo(bar)baz", 2, TextObjectScope::Inner, Some(4..7))] #[case("foo(bar\nbaz)qux", 8, TextObjectScope::Inner, Some(4..11))] #[case("foo\n(bar\nbaz)qux", 0, TextObjectScope::Inner, Some(5..12))] #[case("foo\n(bar\nbaz)qux", 3, TextObjectScope::Around, Some(4..13))] #[case("{hello}", 3, TextObjectScope::Around, Some(0..7))] #[case("foo()bar", 4, TextObjectScope::Around, Some(3..5))] #[case("(nested(inner)outer)", 8, TextObjectScope::Around, Some(7..14))] #[case("start(nested(inner)outer)", 2, TextObjectScope::Around, Some(5..25))] #[case("(mixed{nested)brackets", 1, TextObjectScope::Inner, Some(1..13))] #[case("(unclosed(nested)brackets", 1, TextObjectScope::Inner, Some(10..16))] #[case("no brackets here", 5, TextObjectScope::Inner, None)] #[case("(unclosed", 1, TextObjectScope::Inner, None)] #[case("(mismatched}", 1, TextObjectScope::Inner, None)] fn test_bracket_text_object_range(
#[case] input: &str,
#[case] cursor_pos: usize,
#[case] scope: TextObjectScope,
#[case] expected: Option<std::ops::Range<usize>>,
) {
let mut editor = editor_with(input);
editor.move_to_position(cursor_pos, false);
let result = editor.bracket_text_object_range(scope);
assert_eq!(result, expected);
}
#[rstest]
#[case(r#"foo"bar"baz"#, 5, TextObjectScope::Inner, Some(4..7))] #[case("foo'bar'baz", 5, TextObjectScope::Inner, Some(4..7))] #[case("foo`bar`baz", 5, TextObjectScope::Inner, Some(4..7))] #[case(r#"foo""bar"#, 4, TextObjectScope::Inner, Some(4..4))] #[case(r#""nested'inner'outer""#, 8, TextObjectScope::Inner, Some(8..13))] #[case(r#""nested`mixed'inner'backticks`outer""#, 8, TextObjectScope::Inner, Some(8..29))] #[case(r#"next"nested'mixed`inner`quotes'outer""#, 0, TextObjectScope::Inner, Some(5..36))] #[case(r#"foo "bar"baz"#, 0, TextObjectScope::Inner, Some(5..8))] #[case(r#"foo"bar"baz"#, 2, TextObjectScope::Inner, Some(4..7))] #[case(r#"foo"bar"baz"#, 4, TextObjectScope::Around, Some(3..8))] #[case(r#"foo"bar"baz"#, 3, TextObjectScope::Around, Some(3..8))] #[case(r#"foo"bar"baz"#, 2, TextObjectScope::Around, Some(3..8))] #[case(r#"foo""bar"#, 4, TextObjectScope::Around, Some(3..5))] #[case(r#"foo""bar"#, 1, TextObjectScope::Around, Some(3..5))] #[case(r#""nested"inner"outer""#, 8, TextObjectScope::Around, Some(7..14))] #[case(r#"start"nested'inner'outer""#, 2, TextObjectScope::Around, Some(5..25))] #[case("no quotes here", 5, TextObjectScope::Inner, None)] #[case(r#"foo"bar"#, 1, TextObjectScope::Inner, None)] #[case("foo'bar\nbaz'qux", 5, TextObjectScope::Inner, None)] #[case("foo'bar\nbaz'qux", 0, TextObjectScope::Inner, None)] #[case("foobar\n`baz`qux", 6, TextObjectScope::Inner, None)] #[case("foo\n(bar\nbaz)qux", 0, TextObjectScope::Inner, None)] #[case("foo\n(bar\nbaz)qux", 3, TextObjectScope::Around, None)] fn test_quote_text_object_range(
#[case] input: &str,
#[case] cursor_pos: usize,
#[case] scope: TextObjectScope,
#[case] expected: Option<std::ops::Range<usize>>,
) {
let mut editor = editor_with(input);
editor.line_buffer.set_insertion_point(cursor_pos);
let result = editor.quote_text_object_range(scope);
assert_eq!(result, expected);
}
#[rstest]
#[case("", 0, TextObjectScope::Inner, None, None)] #[case("a", 0, TextObjectScope::Inner, None, None)] #[case("()", 1, TextObjectScope::Inner, Some(1..1), None)] #[case(r#""""#, 1, TextObjectScope::Inner, None, Some(1..1))] #[case("([{}])", 3, TextObjectScope::Inner, Some(3..3), None)] #[case(r#""'`text`'""#, 5, TextObjectScope::Inner, None, Some(3..7))] #[case("(text) and [more]", 5, TextObjectScope::Around, Some(0..6), None)] #[case(r#""text" and 'more'"#, 5, TextObjectScope::Around, None, Some(0..6))] fn test_text_object_edge_cases(
#[case] input: &str,
#[case] cursor_pos: usize,
#[case] scope: TextObjectScope,
#[case] expected_bracket: Option<std::ops::Range<usize>>,
#[case] expected_quote: Option<std::ops::Range<usize>>,
) {
let mut editor = editor_with(input);
editor.move_to_position(cursor_pos, false);
let bracket_result = editor.bracket_text_object_range(scope);
let quote_result = editor.quote_text_object_range(scope);
assert_eq!(bracket_result, expected_bracket);
assert_eq!(quote_result, expected_quote);
}
fn word_start_fwd() -> MotionTarget {
MotionTarget::Word {
kind: WordKind::Word,
edge: WordEdge::Start,
direction: Direction::Forward,
}
}
#[test]
fn move_word_forward_lands_on_next_word_start() {
let mut editor = editor_with("foo bar baz");
editor.move_to_position(0, false);
editor.run_edit_command(&EditCommand::Move(word_start_fwd()));
assert_eq!(editor.insertion_point(), 4);
assert_eq!(editor.get_selection(), None); }
#[test]
fn move_grapheme_right_steps_over_multibyte() {
let mut editor = editor_with("café"); editor.move_to_position(3, false);
editor.run_edit_command(&EditCommand::Move(MotionTarget::Grapheme(
Direction::Forward,
)));
assert_eq!(editor.insertion_point(), 5); }
#[test]
fn extend_word_forward_keeps_anchor_at_origin() {
let mut editor = editor_with("foo bar baz");
editor.move_to_position(0, false);
editor.run_edit_command(&EditCommand::Extend(word_start_fwd()));
assert_eq!(editor.insertion_point(), 4);
assert_eq!(editor.get_selection(), Some((0, 4))); }
#[test]
fn cut_word_forward_removes_range_and_yanks() {
let mut editor = editor_with("foo bar baz");
editor.move_to_position(0, false);
editor.run_edit_command(&EditCommand::Cut {
target: word_start_fwd(),
granularity: Granularity::CharWise,
});
assert_eq!(editor.get_buffer(), "bar baz");
assert_eq!(editor.insertion_point(), 0);
assert_eq!(editor.cut_buffer.get().0, "foo ");
}
#[test]
fn cut_word_backward_removes_preceding_word() {
let mut editor = editor_with("foo bar");
editor.move_to_position(7, false); editor.run_edit_command(&EditCommand::Cut {
target: MotionTarget::Word {
kind: WordKind::Word,
edge: WordEdge::Start,
direction: Direction::Backward,
},
granularity: Granularity::CharWise,
});
assert_eq!(editor.get_buffer(), "foo ");
assert_eq!(editor.insertion_point(), 4); assert_eq!(editor.cut_buffer.get().0, "bar");
}
#[test]
fn copy_word_forward_yanks_without_editing() {
let mut editor = editor_with("foo bar");
editor.move_to_position(0, false);
editor.run_edit_command(&EditCommand::Copy {
target: word_start_fwd(),
granularity: Granularity::CharWise,
});
assert_eq!(editor.get_buffer(), "foo bar"); assert_eq!(editor.insertion_point(), 0); assert_eq!(editor.cut_buffer.get().0, "foo ");
}
#[test]
fn erase_word_forward_deletes_without_touching_register() {
let mut editor = editor_with("foo bar baz");
editor.move_to_position(0, false);
editor.run_edit_command(&EditCommand::Erase(word_start_fwd()));
assert_eq!(editor.get_buffer(), "bar baz");
assert_eq!(editor.insertion_point(), 0);
assert_eq!(editor.cut_buffer.get().0, ""); }
#[test]
fn erase_find_forward_is_inclusive() {
let mut editor = editor_with("foo bar baz");
editor.move_to_position(0, false);
editor.run_edit_command(&EditCommand::Erase(find(
'b',
Direction::Forward,
FindStop::On,
)));
assert_eq!(editor.get_buffer(), "ar baz"); assert_eq!(editor.insertion_point(), 0);
assert_eq!(editor.cut_buffer.get().0, ""); }
#[test]
fn erase_grapheme_backward_over_multibyte() {
let mut editor = editor_with("café"); editor.move_to_position(5, false);
editor.run_edit_command(&EditCommand::Erase(MotionTarget::Grapheme(
Direction::Backward,
)));
assert_eq!(editor.get_buffer(), "caf");
assert_eq!(editor.insertion_point(), 3);
assert_eq!(editor.cut_buffer.get().0, ""); }
fn word_end_fwd() -> MotionTarget {
MotionTarget::Word {
kind: WordKind::Word,
edge: WordEdge::End,
direction: Direction::Forward,
}
}
#[test]
fn move_word_end_landing_follows_caret_geometry() {
let mut block = vi_editor("foo bar", PromptViMode::Normal);
block.move_to_position(0, false);
block.run_edit_command(&EditCommand::Move(word_end_fwd()));
assert_eq!(block.insertion_point(), 2);
let mut bar = editor_with("foo bar"); bar.move_to_position(0, false);
bar.run_edit_command(&EditCommand::Move(word_end_fwd()));
assert_eq!(bar.insertion_point(), 3); }
#[test]
fn cut_word_end_is_inclusive_of_last_char() {
let mut editor = editor_with("foo bar");
editor.move_to_position(0, false);
editor.run_edit_command(&EditCommand::Cut {
target: word_end_fwd(),
granularity: Granularity::CharWise,
});
assert_eq!(editor.get_buffer(), " bar");
assert_eq!(editor.cut_buffer.get().0, "foo");
}
#[test]
fn word_commands_match_legacy_spec_at_every_position() {
let buffers = [
"foo bar baz",
"foo.bar baz", "can't stop now", "café résumé", " lead trail ", "a",
"",
];
for buf in buffers {
for pos in (0..=buf.len()).filter(|p| buf.is_char_boundary(*p)) {
fn wb(
lb: &LineBuffer,
kind: WordKind,
edge: WordEdge,
fwd: bool,
block: bool,
) -> usize {
let buf = lb.get_buffer();
let origin = lb.insertion_point();
use crate::core_editor::graphemes::{
next_grapheme_boundary, prev_grapheme_boundary,
};
use crate::core_editor::word;
let dir = if fwd {
Direction::Forward
} else {
Direction::Backward
};
if block && fwd && edge == WordEdge::End {
let probe = next_grapheme_boundary(buf, origin);
prev_grapheme_boundary(buf, word::locate_word(buf, probe, kind, edge, dir))
} else {
word::locate_word(buf, origin, kind, edge, dir)
}
}
#[allow(clippy::type_complexity)]
let moves: &[(EditCommand, fn(&LineBuffer) -> usize)] = &[
(EditCommand::MoveWordLeft { select: false }, |lb| {
wb(lb, WordKind::Unicode, WordEdge::Start, false, false)
}),
(EditCommand::MoveBigWordLeft { select: false }, |lb| {
wb(lb, WordKind::LongWord, WordEdge::Start, false, false)
}),
(EditCommand::MoveWordRight { select: false }, |lb| {
wb(lb, WordKind::Unicode, WordEdge::End, true, false)
}),
(EditCommand::MoveWordRightStart { select: false }, |lb| {
wb(lb, WordKind::Unicode, WordEdge::Start, true, false)
}),
(EditCommand::MoveBigWordRightStart { select: false }, |lb| {
wb(lb, WordKind::LongWord, WordEdge::Start, true, false)
}),
(EditCommand::MoveWordRightEnd { select: false }, |lb| {
wb(lb, WordKind::Unicode, WordEdge::End, true, true)
}),
(EditCommand::MoveBigWordRightEnd { select: false }, |lb| {
wb(lb, WordKind::LongWord, WordEdge::End, true, true)
}),
];
for (cmd, legacy) in moves {
let mut got = editor_with(buf);
got.move_to_position(pos, false);
got.run_edit_command(cmd);
let mut spec = editor_with(buf);
spec.move_to_position(pos, false);
let target = legacy(&spec.line_buffer);
assert_eq!(
got.insertion_point(),
target,
"{cmd:?} at pos {pos} of {buf:?}"
);
}
#[allow(clippy::type_complexity)]
let ops: &[(
EditCommand,
EditCommand,
fn(&LineBuffer, usize) -> (usize, usize),
)] = &[
(
EditCommand::CutWordLeft,
EditCommand::CopyWordLeft,
|lb, ip| (wb(lb, WordKind::Unicode, WordEdge::Start, false, false), ip),
),
(
EditCommand::CutBigWordLeft,
EditCommand::CopyBigWordLeft,
|lb, ip| {
(
wb(lb, WordKind::LongWord, WordEdge::Start, false, false),
ip,
)
},
),
(
EditCommand::CutWordRight,
EditCommand::CopyWordRight,
|lb, ip| (ip, wb(lb, WordKind::Unicode, WordEdge::End, true, false)),
),
(
EditCommand::CutBigWordRight,
EditCommand::CopyBigWordRight,
|lb, ip| (ip, wb(lb, WordKind::LongWord, WordEdge::End, true, false)),
),
(
EditCommand::CutWordRightToNext,
EditCommand::CopyWordRightToNext,
|lb, ip| (ip, wb(lb, WordKind::Unicode, WordEdge::Start, true, false)),
),
(
EditCommand::CutBigWordRightToNext,
EditCommand::CopyBigWordRightToNext,
|lb, ip| (ip, wb(lb, WordKind::LongWord, WordEdge::Start, true, false)),
),
];
for (cut_cmd, copy_cmd, legacy) in ops {
let mut got = editor_with(buf);
got.move_to_position(pos, false);
got.run_edit_command(cut_cmd);
let mut spec = editor_with(buf);
spec.move_to_position(pos, false);
let (lo, hi) = legacy(&spec.line_buffer, pos);
spec.cut_range(lo..hi);
let got_pair = (got.get_buffer().to_string(), got.cut_buffer.get().0);
let spec_pair = (spec.get_buffer().to_string(), spec.cut_buffer.get().0);
assert_eq!(got_pair, spec_pair, "{cut_cmd:?} at pos {pos} of {buf:?}");
let mut got = editor_with(buf);
got.move_to_position(pos, false);
got.run_edit_command(copy_cmd);
assert_eq!(got.get_buffer(), buf, "{copy_cmd:?} touched buffer");
let expect = buf.get(lo..hi).unwrap_or("");
assert_eq!(
got.cut_buffer.get().0,
expect,
"{copy_cmd:?} at pos {pos} of {buf:?}"
);
}
}
}
}
#[test]
fn move_word_right_rests_after_word_like_emacs_meta_f() {
let mut editor = editor_with("foo bar");
editor.move_to_position(0, false);
editor.run_edit_command(&EditCommand::MoveWordRight { select: false });
assert_eq!(editor.insertion_point(), 3);
}
#[test]
fn move_word_right_keeps_contraction_whole() {
let mut editor = editor_with("can't stop");
editor.move_to_position(0, false);
editor.run_edit_command(&EditCommand::MoveWordRight { select: false });
assert_eq!(editor.insertion_point(), 5);
}
#[test]
fn cut_word_right_consumes_whole_contraction() {
let mut editor = editor_with("can't stop");
editor.move_to_position(0, false);
editor.run_edit_command(&EditCommand::CutWordRight);
assert_eq!(editor.get_buffer(), " stop");
assert_eq!(editor.cut_buffer.get().0, "can't");
}
#[test]
fn move_word_right_with_select_extends_anchor() {
let mut editor = editor_with("foo bar");
editor.move_to_position(0, false);
editor.run_edit_command(&EditCommand::MoveWordRight { select: true });
assert_eq!(editor.insertion_point(), 3);
assert_eq!(editor.get_selection(), Some((0, 3)));
}
#[test]
fn move_word_right_from_midword_completes_current_word() {
let mut editor = editor_with("foo bar");
editor.move_to_position(2, false);
editor.run_edit_command(&EditCommand::MoveWordRight { select: false });
assert_eq!(editor.insertion_point(), 3);
}
#[test]
fn cut_word_right_from_midword_consumes_rest_of_word() {
let mut editor = editor_with("foo bar");
editor.move_to_position(2, false);
editor.run_edit_command(&EditCommand::CutWordRight);
assert_eq!(editor.get_buffer(), "fo bar");
assert_eq!(editor.cut_buffer.get().0, "o");
}
#[test]
fn cut_big_word_right_from_midword_consumes_rest_of_word() {
let mut editor = editor_with("foo.bar baz");
editor.move_to_position(2, false); editor.run_edit_command(&EditCommand::CutBigWordRight);
assert_eq!(editor.get_buffer(), "fo baz");
assert_eq!(editor.cut_buffer.get().0, "o.bar");
}
fn outcome(
buffer: &str,
cursor: usize,
cmd: &EditCommand,
) -> (String, usize, Option<(usize, usize)>, String) {
let mut editor = editor_with(buffer);
editor.move_to_position(cursor, false);
editor.run_edit_command(cmd);
(
editor.get_buffer().to_string(),
editor.insertion_point(),
editor.get_selection(),
editor.cut_buffer.get().0,
)
}
fn equivalent(buffer: &str, cursor: usize, new: &EditCommand, old: &EditCommand) {
assert_eq!(outcome(buffer, cursor, new), outcome(buffer, cursor, old));
}
fn find(ch: char, direction: Direction, stop: FindStop) -> MotionTarget {
MotionTarget::Find {
ch,
direction,
stop,
}
}
#[test]
fn cut_line_edge_matches_dedicated_line_cuts() {
equivalent(
"foo bar",
2,
&EditCommand::Cut {
target: MotionTarget::LineEdge(Direction::Forward),
granularity: Granularity::CharWise,
},
&EditCommand::CutToLineEnd,
);
equivalent(
"foo bar",
4,
&EditCommand::Cut {
target: MotionTarget::LineEdge(Direction::Backward),
granularity: Granularity::CharWise,
},
&EditCommand::CutFromLineStart,
);
}
#[test]
fn copy_line_edge_matches_dedicated_line_copies() {
equivalent(
"foo bar",
2,
&EditCommand::Copy {
target: MotionTarget::LineEdge(Direction::Forward),
granularity: Granularity::CharWise,
},
&EditCommand::CopyToLineEnd,
);
equivalent(
"foo bar",
4,
&EditCommand::Copy {
target: MotionTarget::LineEdge(Direction::Backward),
granularity: Granularity::CharWise,
},
&EditCommand::CopyFromLineStart,
);
}
#[test]
fn cut_line_edge_forward_stops_at_newline() {
equivalent(
"ab\ncd",
0,
&EditCommand::Cut {
target: MotionTarget::LineEdge(Direction::Forward),
granularity: Granularity::CharWise,
},
&EditCommand::CutToLineEnd,
);
let (buffer, cursor, _selection, cut) = outcome(
"ab\ncd",
0,
&EditCommand::Cut {
target: MotionTarget::LineEdge(Direction::Forward),
granularity: Granularity::CharWise,
},
);
assert_eq!(buffer, "\ncd");
assert_eq!(cursor, 0);
assert_eq!(cut, "ab");
}
#[test]
fn move_buffer_edge_matches_move_to_start_end() {
equivalent(
"foo bar",
3,
&EditCommand::Move(MotionTarget::BufferEdge(Direction::Backward)),
&EditCommand::MoveToStart { select: false },
);
equivalent(
"foo bar",
3,
&EditCommand::Move(MotionTarget::BufferEdge(Direction::Forward)),
&EditCommand::MoveToEnd { select: false },
);
}
#[test]
fn extend_buffer_edge_matches_move_to_start_end_selecting() {
equivalent(
"foo bar",
3,
&EditCommand::Extend(MotionTarget::BufferEdge(Direction::Backward)),
&EditCommand::MoveToStart { select: true },
);
equivalent(
"foo bar",
3,
&EditCommand::Extend(MotionTarget::BufferEdge(Direction::Forward)),
&EditCommand::MoveToEnd { select: true },
);
}
#[test]
fn buffer_edge_spans_lines() {
equivalent(
"ab\ncd",
4,
&EditCommand::Move(MotionTarget::BufferEdge(Direction::Backward)),
&EditCommand::MoveToStart { select: false },
);
equivalent(
"ab\ncd",
0,
&EditCommand::Move(MotionTarget::BufferEdge(Direction::Forward)),
&EditCommand::MoveToEnd { select: false },
);
}
#[test]
fn cut_find_forward_on_matches_cut_right_until() {
equivalent(
"foo bar baz",
0,
&EditCommand::Cut {
target: find('b', Direction::Forward, FindStop::On),
granularity: Granularity::CharWise,
},
&EditCommand::CutRightUntil('b'),
);
}
#[test]
fn cut_find_forward_before_matches_cut_right_before() {
equivalent(
"foo bar baz",
0,
&EditCommand::Cut {
target: find('b', Direction::Forward, FindStop::Before),
granularity: Granularity::CharWise,
},
&EditCommand::CutRightBefore('b'),
);
}
#[test]
fn cut_find_backward_on_matches_cut_left_until() {
equivalent(
"foo bar baz",
11,
&EditCommand::Cut {
target: find('o', Direction::Backward, FindStop::On),
granularity: Granularity::CharWise,
},
&EditCommand::CutLeftUntil('o'),
);
}
#[test]
fn cut_find_backward_before_matches_cut_left_before() {
equivalent(
"foo bar baz",
11,
&EditCommand::Cut {
target: find('o', Direction::Backward, FindStop::Before),
granularity: Granularity::CharWise,
},
&EditCommand::CutLeftBefore('o'),
);
}
#[test]
fn cut_find_absent_char_is_noop() {
equivalent(
"foo bar",
0,
&EditCommand::Cut {
target: find('z', Direction::Forward, FindStop::On),
granularity: Granularity::CharWise,
},
&EditCommand::CutRightUntil('z'),
);
}
#[test]
fn copy_find_forward_matches_copy_right_until() {
equivalent(
"foo bar baz",
0,
&EditCommand::Copy {
target: find('b', Direction::Forward, FindStop::On),
granularity: Granularity::CharWise,
},
&EditCommand::CopyRightUntil('b'),
);
}
#[test]
fn move_find_forward_matches_move_right_until() {
equivalent(
"foo bar baz",
0,
&EditCommand::Move(find('b', Direction::Forward, FindStop::On)),
&EditCommand::MoveRightUntil {
c: 'b',
select: false,
},
);
}
#[test]
fn move_find_forward_before_matches_move_right_before() {
equivalent(
"foo bar baz",
0,
&EditCommand::Move(find('b', Direction::Forward, FindStop::Before)),
&EditCommand::MoveRightBefore {
c: 'b',
select: false,
},
);
}
#[test]
fn move_find_backward_on_matches_move_left_until() {
equivalent(
"foo bar baz",
11,
&EditCommand::Move(find('o', Direction::Backward, FindStop::On)),
&EditCommand::MoveLeftUntil {
c: 'o',
select: false,
},
);
}
#[test]
fn move_find_backward_before_matches_move_left_before() {
equivalent(
"foo bar baz",
11,
&EditCommand::Move(find('o', Direction::Backward, FindStop::Before)),
&EditCommand::MoveLeftBefore {
c: 'o',
select: false,
},
);
}
}