#[derive(Debug, Default)]
pub struct LineEdit {
text: String,
cursor: usize,
}
impl LineEdit {
pub fn new() -> Self {
Self::default()
}
pub fn with_text(text: &str, cursor: usize) -> Self {
let mut edit = LineEdit {
text: text.to_string(),
cursor: 0,
};
edit.cursor = cursor.min(edit.char_count());
edit
}
pub fn text(&self) -> &str {
&self.text
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn split(&self) -> (&str, &str, &str) {
let head = self.byte_of(self.cursor());
let under = self.text[head..].chars().next();
let tail = head + under.map_or(0, char::len_utf8);
(
&self.text[..head],
&self.text[head..tail],
&self.text[tail..],
)
}
pub fn insert(&mut self, c: char) {
let at = self.byte_of(self.cursor);
self.text.insert(at, c);
self.cursor += 1;
}
pub fn backspace(&mut self) {
if self.cursor == 0 {
return;
}
self.cursor -= 1;
let at = self.byte_of(self.cursor);
self.text.remove(at);
}
pub fn delete(&mut self) {
if self.cursor >= self.char_count() {
return;
}
let at = self.byte_of(self.cursor);
self.text.remove(at);
}
pub fn left(&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
pub fn right(&mut self) {
self.cursor = (self.cursor + 1).min(self.char_count());
}
pub fn home(&mut self) {
self.cursor = 0;
}
pub fn end(&mut self) {
self.cursor = self.char_count();
}
fn char_count(&self) -> usize {
self.text.chars().count()
}
fn byte_of(&self, chars: usize) -> usize {
self.text
.char_indices()
.nth(chars)
.map_or(self.text.len(), |(at, _)| at)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn state(edit: &LineEdit) -> (&str, usize) {
(edit.text(), edit.cursor())
}
#[test]
fn an_empty_buffer_has_exactly_one_position() {
let mut edit = LineEdit::new();
assert_eq!(state(&edit), ("", 0));
assert_eq!(edit.split(), ("", "", ""));
edit.left();
edit.right();
edit.home();
edit.end();
edit.backspace();
edit.delete();
assert_eq!(state(&edit), ("", 0));
}
#[test]
fn typing_inserts_at_the_cursor_and_advances_it() {
let mut edit = LineEdit::new();
for c in "abc".chars() {
edit.insert(c);
}
assert_eq!(state(&edit), ("abc", 3));
edit.home();
edit.right();
edit.insert('X');
assert_eq!(state(&edit), ("aXbc", 2));
}
#[test]
fn backspace_removes_behind_the_cursor_and_stops_at_the_start() {
let mut edit = LineEdit::with_text("abc", 3);
edit.backspace();
assert_eq!(state(&edit), ("ab", 2));
edit.home();
edit.backspace();
assert_eq!(state(&edit), ("ab", 0));
}
#[test]
fn delete_removes_under_the_cursor_and_stops_at_the_end() {
let mut edit = LineEdit::with_text("abc", 1);
edit.delete();
assert_eq!(state(&edit), ("ac", 1));
edit.end();
edit.delete();
assert_eq!(state(&edit), ("ac", 2));
}
#[test]
fn the_cursor_stops_at_both_ends() {
let mut edit = LineEdit::with_text("ab", 0);
edit.left();
assert_eq!(edit.cursor(), 0);
edit.right();
edit.right();
assert_eq!(edit.cursor(), 2);
edit.right();
assert_eq!(edit.cursor(), 2);
edit.insert('c');
assert_eq!(state(&edit), ("abc", 3));
}
#[test]
fn home_and_end_jump_to_the_ends() {
let mut edit = LineEdit::with_text("notes.md", 5);
edit.home();
assert_eq!(edit.cursor(), 0);
edit.end();
assert_eq!(edit.cursor(), 8);
}
#[test]
fn a_prefilled_cursor_past_the_end_is_clamped_rather_than_fatal() {
let edit = LineEdit::with_text("ab", 99);
assert_eq!(state(&edit), ("ab", 2));
assert_eq!(state(&LineEdit::with_text("notes.md", 5)), ("notes.md", 5));
}
#[test]
fn split_names_the_character_under_the_cursor() {
let edit = LineEdit::with_text("notes.md", 5);
assert_eq!(edit.split(), ("notes", ".", "md"));
assert_eq!(LineEdit::with_text("ab", 0).split(), ("", "a", "b"));
assert_eq!(LineEdit::with_text("ab", 2).split(), ("ab", "", ""));
}
#[test]
fn multibyte_names_move_and_edit_a_whole_character_at_a_time() {
let mut edit = LineEdit::with_text("café.txt", 8);
assert_eq!(edit.cursor(), 8);
edit.home();
for _ in 0..3 {
edit.right();
}
assert_eq!(edit.split(), ("caf", "é", ".txt"));
edit.delete();
assert_eq!(state(&edit), ("caf.txt", 3));
let mut edit = LineEdit::with_text("café", 4);
edit.backspace();
assert_eq!(state(&edit), ("caf", 3));
}
#[test]
fn an_emoji_is_one_position_and_survives_an_edit_beside_it() {
let mut edit = LineEdit::with_text("a🎉b", 3);
assert_eq!(edit.split(), ("a🎉b", "", ""));
edit.left();
assert_eq!(edit.split(), ("a🎉", "b", ""));
edit.left();
assert_eq!(edit.split(), ("a", "🎉", "b"));
edit.insert('-');
assert_eq!(state(&edit), ("a-🎉b", 2));
edit.backspace();
assert_eq!(state(&edit), ("a🎉b", 1));
edit.delete();
assert_eq!(state(&edit), ("ab", 1));
}
#[test]
fn typing_a_multibyte_character_advances_one_position() {
let mut edit = LineEdit::new();
edit.insert('n');
edit.insert('ö');
edit.insert('t');
assert_eq!(state(&edit), ("nöt", 3));
assert_eq!(edit.text().len(), 4); edit.left();
edit.left();
assert_eq!(edit.split(), ("n", "ö", "t"));
}
}