use jagged::index::RowIndex;
use super::Execute;
use crate::{
actions::motion::CharacterClass,
clipboard::ClipboardTrait,
helper::{is_out_of_bounds, max_col_insert, skip_whitespace, skip_whitespace_rev},
state::selection::Selection,
EditorState, Index2, Lines,
};
#[derive(Clone, Debug, Copy)]
pub struct RemoveChar(pub usize);
impl Execute for RemoveChar {
fn execute(&mut self, state: &mut EditorState) {
state.capture();
state.clamp_column();
for _ in 0..self.0 {
let lines = &mut state.lines;
let index = &mut state.cursor;
if is_out_of_bounds(lines, index) {
return;
}
let _ = lines.remove(*index);
index.col = index.col.min(
lines
.len_col(index.row)
.unwrap_or_default()
.saturating_sub(1),
);
}
}
}
#[derive(Clone, Debug, Copy)]
pub struct ReplaceChar(pub char);
impl Execute for ReplaceChar {
fn execute(&mut self, state: &mut EditorState) {
let index = state.cursor;
if is_out_of_bounds(&state.lines, &index) {
return;
}
state.capture();
if let Some(ch) = state.lines.get_mut(index) {
*ch = self.0;
};
}
}
#[derive(Clone, Debug, Copy)]
pub struct DeleteChar(pub usize);
impl Execute for DeleteChar {
fn execute(&mut self, state: &mut EditorState) {
state.capture();
for _ in 0..self.0 {
delete_char(&mut state.lines, &mut state.cursor);
}
}
}
fn delete_char(lines: &mut Lines, index: &mut Index2) {
fn move_left(lines: &Lines, index: &mut Index2) {
if index.col > 0 {
index.col -= 1;
} else if index.row > 0 {
index.row -= 1;
index.col = lines.len_col(index.row).unwrap_or_default();
}
}
let len_col = lines.len_col(index.row).unwrap_or_default();
if len_col == 0 && index.row == 0 {
return;
}
if index.col > len_col {
index.col = len_col;
}
if index.col == 0 {
let mut rest = lines.split_off(*index);
move_left(lines, index);
lines.merge(&mut rest);
} else {
let max_col = max_col_insert(lines, index);
index.col = index.col.min(max_col);
move_left(lines, index);
let _ = lines.remove(*index);
}
}
#[derive(Clone, Debug, Copy)]
pub struct DeleteCharForward(pub usize);
impl Execute for DeleteCharForward {
fn execute(&mut self, state: &mut EditorState) {
state.capture();
state.clamp_column();
for _ in 0..self.0 {
delete_char_forward(&mut state.lines, &mut state.cursor);
}
}
}
fn delete_char_forward(lines: &mut Lines, index: &mut Index2) {
let Some(row) = lines.get(RowIndex::new(index.row)) else {
return;
};
let row_len = row.len();
if index.col >= row_len {
if index.row + 1 >= lines.len() {
return;
}
lines.join_lines(index.row);
return;
}
let _ = lines.remove(*index);
}
#[derive(Clone, Debug, Copy)]
pub struct DeleteWordForward(pub usize);
impl Execute for DeleteWordForward {
fn execute(&mut self, state: &mut EditorState) {
if state.lines.is_empty() {
return;
}
state.capture();
for _ in 0..self.0 {
delete_word_forward(state);
}
}
}
fn delete_word_forward(state: &mut EditorState) {
let start = state.cursor;
let start_char = state.lines.get(start);
if start_char.is_none() {
if state.cursor.row + 1 < state.lines.len() {
state.lines.join_lines(state.cursor.row);
}
return;
}
let mut end = start;
let start_class = CharacterClass::from(start_char);
for (ch, idx) in state.lines.iter().from(start) {
if CharacterClass::from(ch) != start_class {
break;
}
end = idx;
}
end.col += 1;
skip_whitespace(&state.lines, &mut end);
delete_range(&mut state.lines, start, end, &mut state.clip);
}
#[derive(Clone, Debug, Copy)]
pub struct DeleteWordBackward(pub usize);
impl Execute for DeleteWordBackward {
fn execute(&mut self, state: &mut EditorState) {
if state.lines.is_empty() {
return;
}
state.capture();
for _ in 0..self.0 {
delete_word_backward(state);
}
}
}
fn delete_word_backward(state: &mut EditorState) {
let end = state.cursor;
if end.row == 0 && end.col == 0 {
return;
}
if end.col == 0 {
state.cursor.row -= 1;
state.cursor.col = state.lines.len_col(state.cursor.row).unwrap_or(0);
state.lines.join_lines(state.cursor.row);
return;
}
let mut start = Index2::new(end.row, end.col.saturating_sub(1));
skip_whitespace_rev(&state.lines, &mut start);
let start_class = CharacterClass::from(state.lines.get(start));
for (ch, idx) in state.lines.iter().from(start).rev() {
if idx.col == 0 {
start = idx;
break;
}
if CharacterClass::from(ch) != start_class {
break;
}
start = idx;
}
delete_range(&mut state.lines, start, end, &mut state.clip);
state.cursor = start;
}
fn delete_range(
lines: &mut Lines,
start: Index2,
end: Index2,
clip: &mut crate::clipboard::Clipboard,
) {
if start.row != end.row || start.col >= end.col {
return;
}
let Some(row) = lines.get_mut(RowIndex::new(start.row)) else {
return;
};
let end_col = end.col.min(row.len());
let start_col = start.col.min(end_col);
let deleted: String = row.drain(start_col..end_col).collect();
clip.set_text(deleted);
}
#[derive(Clone, Debug, Copy)]
pub struct DeleteLine(pub usize);
impl Execute for DeleteLine {
fn execute(&mut self, state: &mut EditorState) {
state.capture();
for _ in 0..self.0 {
if state.cursor.row >= state.lines.len() {
break;
}
let row_index = RowIndex::new(state.cursor.row);
let deleted_line = state.lines.remove(row_index).iter().collect::<String>();
state.clip.set_text(String::from('\n') + &deleted_line);
state.cursor.col = 0;
state.cursor.row = state.cursor.row.min(state.lines.len().saturating_sub(1));
}
}
}
#[derive(Clone, Debug, Copy)]
pub struct DeleteToFirstCharOfLine;
impl Execute for DeleteToFirstCharOfLine {
fn execute(&mut self, state: &mut EditorState) {
state.capture();
let row_index = RowIndex::new(state.cursor.row);
let Some(row) = state.lines.get_mut(row_index) else {
return;
};
let col = state.cursor.col;
let first_char = row
.iter()
.position(|c| !c.is_whitespace())
.unwrap_or(row.len());
let anchor = if col <= first_char { 0 } else { first_char };
if anchor < col && col <= row.len() {
let deleted = row.drain(anchor..col).collect();
state.clip.set_text(deleted);
}
state.cursor.col = anchor;
}
}
#[derive(Clone, Debug, Copy)]
pub struct DeleteToEndOfLine;
impl Execute for DeleteToEndOfLine {
fn execute(&mut self, state: &mut EditorState) {
if is_out_of_bounds(&state.lines, &state.cursor) {
return;
}
state.capture();
let Some(row) = state.lines.get_mut(RowIndex::new(state.cursor.row)) else {
return;
};
let deleted_chars = row.drain(state.cursor.col..);
state.cursor.col = state.cursor.col.saturating_sub(1);
state.clip.set_text(deleted_chars.collect());
}
}
#[derive(Clone, Debug)]
pub struct DeleteSelection;
impl Execute for DeleteSelection {
fn execute(&mut self, state: &mut EditorState) {
if let Some(selection) = state.selection.take() {
state.capture();
let drained = delete_selection(state, &selection);
state.clip.set_text(drained.into());
}
state.selection = None;
}
}
pub(crate) fn delete_selection(state: &mut EditorState, selection: &Selection) -> Lines {
state.cursor = selection.start();
state.clamp_column();
selection.extract_from(&mut state.lines)
}
#[derive(Clone, Debug, Copy)]
pub struct JoinLineWithLineBelow;
impl Execute for JoinLineWithLineBelow {
fn execute(&mut self, state: &mut EditorState) {
if state.cursor.row + 1 >= state.lines.len() {
return;
}
state.capture();
state.lines.join_lines(state.cursor.row);
}
}
#[cfg(test)]
mod tests {
use crate::state::selection::Selection;
use crate::EditorMode;
use crate::Index2;
use crate::Lines;
use super::*;
fn test_state() -> EditorState {
EditorState::new(Lines::from("Hello World!\n\n123."))
}
#[test]
fn test_remove_char() {
let mut state = test_state();
state.cursor = Index2::new(0, 4);
RemoveChar(1).execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 4));
assert_eq!(state.lines, Lines::from("Hell World!\n\n123."));
state.cursor = Index2::new(0, 10);
RemoveChar(1).execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 9));
assert_eq!(state.lines, Lines::from("Hell World\n\n123."));
}
#[test]
fn test_replace_char() {
let mut state = test_state();
state.cursor = Index2::new(0, 4);
ReplaceChar('x').execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 4));
assert_eq!(state.lines, Lines::from("Hellx World!\n\n123."));
state.cursor = Index2::new(1, 0);
ReplaceChar('x').execute(&mut state);
assert_eq!(state.cursor, Index2::new(1, 0));
assert_eq!(state.lines, Lines::from("Hellx World!\n\n123."));
state.cursor = Index2::new(99, 0);
ReplaceChar('x').execute(&mut state);
assert_eq!(state.cursor, Index2::new(99, 0));
assert_eq!(state.lines, Lines::from("Hellx World!\n\n123."));
}
#[test]
fn test_delete_char() {
let mut state = test_state();
state.cursor = Index2::new(0, 5);
DeleteChar(1).execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 4));
assert_eq!(state.lines, Lines::from("Hell World!\n\n123."));
state.cursor = Index2::new(0, 11);
DeleteChar(1).execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 10));
assert_eq!(state.lines, Lines::from("Hell World\n\n123."));
}
#[test]
fn test_delete_char_empty_line() {
let mut state = test_state();
state.cursor = Index2::new(1, 99);
DeleteChar(1).execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 12));
assert_eq!(state.lines, Lines::from("Hello World!\n123."));
let mut state = EditorState::new(Lines::from("\nb"));
state.cursor = Index2::new(0, 1);
DeleteChar(1).execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 1));
assert_eq!(state.lines, Lines::from("\nb"));
}
#[test]
fn test_delete_line() {
let mut state = test_state();
state.cursor = Index2::new(2, 3);
DeleteLine(1).execute(&mut state);
assert_eq!(state.cursor, Index2::new(1, 0));
assert_eq!(state.lines, Lines::from("Hello World!\n"));
DeleteLine(1).execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 0));
assert_eq!(state.lines, Lines::from("Hello World!"));
DeleteLine(1).execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 0));
assert_eq!(state.lines, Lines::from(""));
}
#[test]
fn test_delete_to_first_char_of_line() {
let mut state = EditorState::new(Lines::from(" Hello World!"));
state.cursor = Index2::new(0, 4);
DeleteToFirstCharOfLine.execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 2));
assert_eq!(state.lines, Lines::from(" llo World!"));
state.cursor = Index2::new(0, 2);
DeleteToFirstCharOfLine.execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 0));
assert_eq!(state.lines, Lines::from("llo World!"));
}
#[test]
fn test_delete_to_end_of_line() {
let mut state = test_state();
state.cursor = Index2::new(0, 3);
DeleteToEndOfLine.execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 2));
assert_eq!(state.lines, Lines::from("Hel\n\n123."));
}
#[test]
fn test_delete_selection() {
let mut state = test_state();
let st = Index2::new(0, 1);
let en = Index2::new(2, 0);
state.selection = Some(Selection::new(st, en));
DeleteSelection.execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 1));
assert_eq!(state.lines, Lines::from("H23."));
}
#[test]
fn test_delete_selection_out_of_bounds() {
let mut state = EditorState::new(Lines::from("123.\nHello World!\n456."));
let st = Index2::new(0, 5);
let en = Index2::new(2, 10);
state.selection = Some(Selection::new(st, en));
DeleteSelection.execute(&mut state);
assert_eq!(state.lines, Lines::from("123."));
}
#[test]
fn test_delete_char_forward() {
let mut state = EditorState::new(Lines::from("Hello World!\nNext line"));
state.mode = EditorMode::Insert;
state.cursor = Index2::new(0, 0);
DeleteCharForward(1).execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 0));
assert_eq!(state.lines, Lines::from("ello World!\nNext line"));
state.cursor = Index2::new(0, 0);
DeleteCharForward(1).execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 0));
assert_eq!(state.lines, Lines::from("llo World!\nNext line"));
state.cursor = Index2::new(0, 10);
DeleteCharForward(1).execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 10));
assert_eq!(state.lines, Lines::from("llo World!Next line"));
}
#[test]
fn test_delete_char_forward_at_end() {
let mut state = EditorState::new(Lines::from("Hello\nWorld"));
state.mode = EditorMode::Insert;
state.cursor = Index2::new(0, 5);
DeleteCharForward(1).execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 5));
assert_eq!(state.lines, Lines::from("HelloWorld"));
state.cursor = Index2::new(0, 10);
DeleteCharForward(1).execute(&mut state);
assert_eq!(state.cursor, Index2::new(0, 10));
assert_eq!(state.lines, Lines::from("HelloWorld"));
}
#[test]
fn test_delete_word_forward() {
let mut state = EditorState::new(Lines::from("Hello World Test"));
state.mode = EditorMode::Insert;
state.cursor = Index2::new(0, 0);
DeleteWordForward(1).execute(&mut state);
assert_eq!(state.lines.to_string(), "World Test");
assert_eq!(state.cursor, Index2::new(0, 0));
DeleteWordForward(1).execute(&mut state);
assert_eq!(state.lines.to_string(), "Test");
}
#[test]
fn test_delete_word_forward_mid_word() {
let mut state = EditorState::new(Lines::from("Hello World"));
state.mode = EditorMode::Insert;
state.cursor = Index2::new(0, 2);
DeleteWordForward(1).execute(&mut state);
assert_eq!(state.lines.to_string(), "HeWorld");
}
#[test]
fn test_delete_word_backward() {
let mut state = EditorState::new(Lines::from("Hello World Test"));
state.mode = EditorMode::Insert;
state.cursor = Index2::new(0, 12);
DeleteWordBackward(1).execute(&mut state);
assert_eq!(state.lines.to_string(), "Hello Test");
assert_eq!(state.cursor, Index2::new(0, 6));
}
#[test]
fn test_delete_word_backward_mid_word() {
let mut state = EditorState::new(Lines::from("Hello World"));
state.mode = EditorMode::Insert;
state.cursor = Index2::new(0, 7);
DeleteWordBackward(1).execute(&mut state);
assert_eq!(state.lines.to_string(), "Hello orld");
assert_eq!(state.cursor, Index2::new(0, 6));
}
#[test]
fn test_delete_word_backward_at_word_start() {
let mut state = EditorState::new(Lines::from("Hello World"));
state.mode = EditorMode::Insert;
state.cursor = Index2::new(0, 6);
DeleteWordBackward(1).execute(&mut state);
assert_eq!(state.lines.to_string(), "World");
assert_eq!(state.cursor, Index2::new(0, 0));
}
}