use alloc::{collections::VecDeque, string::String};
use crate::{
Edit,
text::{next_unit, prev_unit, text_width, word_left, word_right},
};
#[derive(Clone, Debug)]
pub struct Editor {
line: String,
cursor: usize,
kill: String,
submitted: String,
history: VecDeque<String>,
capacity: usize,
browsing: Option<usize>,
draft: String,
}
impl Default for Editor {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl Editor {
pub const DEFAULT_HISTORY: usize = 1000;
#[must_use]
pub fn new() -> Self {
Self::with_history(Self::DEFAULT_HISTORY)
}
#[must_use]
pub fn with_history(capacity: usize) -> Self {
Self {
line: String::new(),
cursor: 0,
kill: String::new(),
submitted: String::new(),
history: VecDeque::new(),
capacity,
browsing: None,
draft: String::new(),
}
}
#[inline]
#[must_use]
pub fn line(&self) -> &str {
&self.line
}
#[inline]
#[must_use]
pub fn cursor(&self) -> usize {
self.cursor
}
#[must_use]
pub fn column(&self) -> usize {
text_width(&self.line[..self.cursor])
}
pub fn apply(&mut self, edit: Edit) -> bool {
match edit {
Edit::Insert(c) => self.insert_char(c),
Edit::Backspace => self.backspace(),
Edit::Delete => self.delete(),
Edit::Left => self.move_to(prev_unit(&self.line, self.cursor)),
Edit::Right => self.move_to(next_unit(&self.line, self.cursor)),
Edit::WordLeft => self.move_to(word_left(&self.line, self.cursor)),
Edit::WordRight => self.move_to(word_right(&self.line, self.cursor)),
Edit::Home => self.move_to(0),
Edit::End => self.move_to(self.line.len()),
Edit::KillToStart => self.kill_range(0, self.cursor),
Edit::KillToEnd => self.kill_range(self.cursor, self.line.len()),
Edit::KillWordLeft => self.kill_range(word_left(&self.line, self.cursor), self.cursor),
Edit::KillWordRight => {
self.kill_range(self.cursor, word_right(&self.line, self.cursor))
}
Edit::Yank => self.yank(),
Edit::HistoryPrev => self.history_prev(),
Edit::HistoryNext => self.history_next(),
}
}
pub fn insert(&mut self, text: &str) -> bool {
let before = self.line.len();
for segment in text.split(char::is_control) {
self.line.insert_str(self.cursor, segment);
self.cursor += segment.len();
}
self.line.len() != before
}
pub fn clear(&mut self) -> bool {
self.forget_draft();
let changed = !self.line.is_empty();
self.line.clear();
self.cursor = 0;
changed
}
pub fn submit(&mut self) -> &str {
self.forget_draft();
core::mem::swap(&mut self.line, &mut self.submitted);
self.line.clear();
self.cursor = 0;
record(
&mut self.history,
self.capacity,
&self.submitted,
&mut self.browsing,
);
&self.submitted
}
pub fn add_history(&mut self, line: &str) {
if line.contains(char::is_control) {
let clean: String = line.chars().filter(|c| !c.is_control()).collect();
record(&mut self.history, self.capacity, &clean, &mut self.browsing);
} else {
record(&mut self.history, self.capacity, line, &mut self.browsing);
}
}
pub fn history(&self) -> impl DoubleEndedIterator<Item = &str> + ExactSizeIterator + '_ {
self.history.iter().map(String::as_str)
}
#[inline]
fn move_to(&mut self, pos: usize) -> bool {
let moved = pos != self.cursor;
self.cursor = pos;
moved
}
fn insert_char(&mut self, c: char) -> bool {
if c.is_control() {
return false;
}
self.line.insert(self.cursor, c);
self.cursor += c.len_utf8();
true
}
fn backspace(&mut self) -> bool {
if self.cursor == 0 {
return false;
}
let start = prev_unit(&self.line, self.cursor);
self.line.replace_range(start..self.cursor, "");
self.cursor = start;
true
}
fn delete(&mut self) -> bool {
let end = next_unit(&self.line, self.cursor);
if end == self.cursor {
return false;
}
self.line.replace_range(self.cursor..end, "");
true
}
fn kill_range(&mut self, start: usize, end: usize) -> bool {
if start == end {
return false;
}
self.kill.clear();
self.kill.push_str(&self.line[start..end]);
self.line.replace_range(start..end, "");
self.cursor = start;
true
}
fn yank(&mut self) -> bool {
if self.kill.is_empty() {
return false;
}
self.line.insert_str(self.cursor, &self.kill);
self.cursor += self.kill.len();
true
}
fn history_prev(&mut self) -> bool {
let target = match self.browsing {
None => match self.history.len().checked_sub(1) {
Some(newest) => {
core::mem::swap(&mut self.line, &mut self.draft);
newest
}
None => return false,
},
Some(0) => return false,
Some(index) => index - 1,
};
self.load(target);
true
}
fn history_next(&mut self) -> bool {
match self.browsing {
None => false,
Some(index) if index + 1 < self.history.len() => {
self.load(index + 1);
true
}
Some(_) => {
self.restore_draft();
true
}
}
}
fn load(&mut self, index: usize) {
self.line.clear();
if let Some(entry) = self.history.get(index) {
self.line.push_str(entry);
}
self.cursor = self.line.len();
self.browsing = Some(index);
}
fn restore_draft(&mut self) {
if self.browsing.take().is_some() {
core::mem::swap(&mut self.line, &mut self.draft);
self.draft.clear();
self.cursor = self.line.len();
}
}
fn forget_draft(&mut self) {
self.browsing = None;
self.draft.clear();
}
}
fn record(
history: &mut VecDeque<String>,
capacity: usize,
line: &str,
browsing: &mut Option<usize>,
) {
if capacity == 0
|| line.trim().is_empty()
|| history.back().is_some_and(|newest| newest == line)
{
return;
}
let mut slot = if history.len() >= capacity {
*browsing = browsing.map(|index| index.saturating_sub(1));
history.pop_front().unwrap_or_default()
} else {
String::new()
};
slot.clear();
slot.push_str(line);
history.push_back(slot);
}
#[cfg(test)]
#[allow(unused_results)]
mod tests {
use super::*;
fn typed(text: &str) -> Editor {
let mut editor = Editor::new();
assert!(editor.insert(text));
editor
}
#[test]
fn test_insert_char_advances_cursor_by_utf8_len() {
let mut editor = Editor::new();
assert!(editor.apply(Edit::Insert('é')));
assert_eq!(editor.cursor(), 2);
assert!(editor.apply(Edit::Insert('世')));
assert_eq!(editor.cursor(), 5);
assert_eq!(editor.line(), "é世");
}
#[test]
fn test_insert_control_char_is_refused() {
let mut editor = Editor::new();
for c in ['\n', '\r', '\t', '\u{1b}', '\u{7f}', '\u{85}'] {
assert!(!editor.apply(Edit::Insert(c)), "{c:?} was accepted");
}
assert_eq!(editor.line(), "");
}
#[test]
fn test_insert_in_middle_keeps_tail() {
let mut editor = typed("ac");
assert!(editor.apply(Edit::Left));
assert!(editor.apply(Edit::Insert('b')));
assert_eq!(editor.line(), "abc");
assert_eq!(editor.cursor(), 2);
}
#[test]
fn test_insert_str_only_controls_returns_false() {
let mut editor = Editor::new();
assert!(!editor.insert("\n\t\r"));
assert!(!editor.insert(""));
}
#[test]
fn test_backspace_at_start_returns_false() {
let mut editor = typed("x");
assert!(editor.apply(Edit::Home));
assert!(!editor.apply(Edit::Backspace));
assert_eq!(editor.line(), "x");
}
#[test]
fn test_backspace_removes_whole_combining_unit() {
let mut editor = typed("ae\u{0301}");
assert!(editor.apply(Edit::Backspace));
assert_eq!(editor.line(), "a");
assert_eq!(editor.cursor(), 1);
}
#[test]
fn test_delete_at_end_returns_false() {
let mut editor = typed("x");
assert!(!editor.apply(Edit::Delete));
}
#[test]
fn test_delete_removes_unit_under_cursor() {
let mut editor = typed("世界");
assert!(editor.apply(Edit::Home));
assert!(editor.apply(Edit::Delete));
assert_eq!(editor.line(), "界");
assert_eq!(editor.cursor(), 0);
}
#[test]
fn test_left_right_at_bounds_return_false() {
let mut editor = typed("ab");
assert!(!editor.apply(Edit::Right));
assert!(!editor.apply(Edit::End));
assert!(editor.apply(Edit::Home));
assert!(!editor.apply(Edit::Left));
assert!(!editor.apply(Edit::Home));
}
#[test]
fn test_column_counts_wide_and_zero_width() {
let editor = typed("a世e\u{0301}");
assert_eq!(editor.column(), 4);
}
#[test]
fn test_kill_to_end_then_yank_round_trips() {
let mut editor = typed("abc def");
assert!(editor.apply(Edit::WordLeft));
assert!(editor.apply(Edit::KillToEnd));
assert_eq!(editor.line(), "abc ");
assert!(!editor.apply(Edit::KillToEnd)); assert!(editor.apply(Edit::Yank));
assert_eq!(editor.line(), "abc def");
}
#[test]
fn test_empty_kill_keeps_previous_kill_buffer() {
let mut editor = typed("one two");
assert!(editor.apply(Edit::KillWordLeft));
assert!(!editor.apply(Edit::KillWordRight)); assert!(editor.apply(Edit::Yank));
assert_eq!(editor.line(), "one two");
}
#[test]
fn test_kill_word_right_from_start() {
let mut editor = typed(" alpha beta");
assert!(editor.apply(Edit::Home));
assert!(editor.apply(Edit::KillWordRight));
assert_eq!(editor.line(), " beta");
assert_eq!(editor.cursor(), 0);
}
#[test]
fn test_yank_with_empty_kill_buffer_returns_false() {
let mut editor = Editor::new();
assert!(!editor.apply(Edit::Yank));
}
#[test]
fn test_submit_returns_line_and_resets() {
let mut editor = typed("print 1");
assert_eq!(editor.submit(), "print 1");
assert_eq!(editor.line(), "");
assert_eq!(editor.cursor(), 0);
assert!(editor.history().eq(["print 1"]));
}
#[test]
fn test_submit_skips_blank_and_duplicate() {
let mut editor = Editor::new();
for line in ["a", "a", "", " ", "b", "a"] {
editor.insert(line);
editor.submit();
}
assert!(editor.history().eq(["a", "b", "a"]));
}
#[test]
fn test_history_capacity_zero_records_nothing() {
let mut editor = Editor::with_history(0);
editor.insert("x");
assert_eq!(editor.submit(), "x");
assert_eq!(editor.history().len(), 0);
assert!(!editor.apply(Edit::HistoryPrev));
}
#[test]
fn test_add_history_strips_control_characters() {
let mut editor = Editor::new();
editor.add_history("a\tb\u{1b}[0m");
editor.add_history("\n\t"); assert!(editor.history().eq(["ab[0m"]));
assert!(editor.apply(Edit::HistoryPrev));
assert!(!editor.line().contains(char::is_control));
}
#[test]
fn test_history_ring_drops_oldest_at_capacity() {
let mut editor = Editor::with_history(3);
for line in ["1", "2", "3", "4", "5"] {
editor.add_history(line);
}
assert!(editor.history().eq(["3", "4", "5"]));
}
#[test]
fn test_history_prev_next_restores_draft() {
let mut editor = Editor::new();
editor.add_history("old");
editor.add_history("new");
editor.insert("draft");
assert!(editor.apply(Edit::HistoryPrev));
assert_eq!(editor.line(), "new");
assert!(editor.apply(Edit::HistoryPrev));
assert_eq!(editor.line(), "old");
assert!(!editor.apply(Edit::HistoryPrev)); assert!(editor.apply(Edit::HistoryNext));
assert_eq!(editor.line(), "new");
assert!(editor.apply(Edit::HistoryNext));
assert_eq!(editor.line(), "draft");
assert_eq!(editor.cursor(), 5);
assert!(!editor.apply(Edit::HistoryNext)); }
#[test]
fn test_history_prev_on_empty_history_returns_false() {
let mut editor = typed("x");
assert!(!editor.apply(Edit::HistoryPrev));
assert_eq!(editor.line(), "x");
}
#[test]
fn test_editing_recalled_entry_leaves_history_intact() {
let mut editor = Editor::new();
editor.add_history("abc");
assert!(editor.apply(Edit::HistoryPrev));
assert!(editor.apply(Edit::Backspace));
assert_eq!(editor.line(), "ab");
assert!(editor.history().eq(["abc"]));
assert_eq!(editor.submit(), "ab");
assert!(editor.history().eq(["abc", "ab"]));
}
#[test]
fn test_submit_while_browsing_submits_recalled_line() {
let mut editor = Editor::new();
editor.add_history("again");
editor.insert("draft");
assert!(editor.apply(Edit::HistoryPrev));
assert_eq!(editor.submit(), "again");
assert_eq!(editor.line(), "");
assert!(editor.history().eq(["again"]));
}
#[test]
fn test_add_history_while_browsing_at_capacity_keeps_entry() {
let mut editor = Editor::with_history(2);
editor.add_history("a");
editor.add_history("b");
assert!(editor.apply(Edit::HistoryPrev)); editor.add_history("c"); assert!(editor.apply(Edit::HistoryNext));
assert_eq!(editor.line(), "c");
}
#[test]
fn test_clear_ends_browse_and_empties_line() {
let mut editor = Editor::new();
editor.add_history("x");
editor.insert("draft");
assert!(editor.apply(Edit::HistoryPrev));
assert!(editor.clear());
assert_eq!(editor.line(), "");
assert!(!editor.apply(Edit::HistoryNext));
}
#[test]
fn test_submit_steady_state_reuses_buffers() {
let mut editor = Editor::with_history(2);
for line in ["aaaa", "bbbb", "cccc"] {
editor.insert(line);
editor.submit();
}
let before = editor
.history()
.map(str::as_ptr)
.collect::<alloc::vec::Vec<_>>();
editor.insert("dddd");
editor.submit();
let after = editor
.history()
.map(str::as_ptr)
.collect::<alloc::vec::Vec<_>>();
assert_eq!(after[0], before[1]);
assert_eq!(after[1], before[0]);
}
}