#[derive(Debug, Clone, Default)]
pub struct TextEdit {
chars: Vec<char>,
cursor: usize,
}
impl TextEdit {
pub fn new(text: &str) -> Self {
let chars: Vec<char> = text.chars().collect();
let cursor = chars.len();
Self { chars, cursor }
}
pub fn text(&self) -> String {
self.chars.iter().collect()
}
pub fn is_empty(&self) -> bool {
self.chars.is_empty()
}
pub fn len(&self) -> usize {
self.chars.len()
}
pub fn insert(&mut self, s: &str) {
for c in s.chars() {
self.chars.insert(self.cursor, c);
self.cursor += 1;
}
}
pub fn backspace(&mut self) -> bool {
if self.cursor == 0 {
return false;
}
self.cursor -= 1;
self.chars.remove(self.cursor);
true
}
pub fn delete(&mut self) -> bool {
if self.cursor >= self.chars.len() {
return false;
}
self.chars.remove(self.cursor);
true
}
pub fn left(&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
pub fn right(&mut self) {
if self.cursor < self.chars.len() {
self.cursor += 1;
}
}
pub fn home(&mut self) {
self.cursor = 0;
}
pub fn end(&mut self) {
self.cursor = self.chars.len();
}
pub fn word_left(&mut self) {
while self.cursor > 0 && !is_word(self.chars[self.cursor - 1]) {
self.cursor -= 1;
}
while self.cursor > 0 && is_word(self.chars[self.cursor - 1]) {
self.cursor -= 1;
}
}
pub fn word_right(&mut self) {
let n = self.chars.len();
while self.cursor < n && !is_word(self.chars[self.cursor]) {
self.cursor += 1;
}
while self.cursor < n && is_word(self.chars[self.cursor]) {
self.cursor += 1;
}
}
pub fn delete_word_back(&mut self) -> bool {
let end = self.cursor;
self.word_left();
if self.cursor < end {
self.chars.drain(self.cursor..end);
true
} else {
false
}
}
pub fn delete_word_forward(&mut self) -> bool {
let start = self.cursor;
let n = self.chars.len();
let mut end = self.cursor;
while end < n && !is_word(self.chars[end]) {
end += 1;
}
while end < n && is_word(self.chars[end]) {
end += 1;
}
if end > start {
self.chars.drain(start..end);
true
} else {
false
}
}
pub fn delete_to_start(&mut self) -> bool {
if self.cursor == 0 {
return false;
}
self.chars.drain(0..self.cursor);
self.cursor = 0;
true
}
pub fn delete_to_end(&mut self) -> bool {
if self.cursor >= self.chars.len() {
return false;
}
self.chars.truncate(self.cursor);
true
}
pub fn split(&self) -> (String, String) {
(
self.chars[..self.cursor].iter().collect(),
self.chars[self.cursor..].iter().collect(),
)
}
pub fn up(&mut self) {
self.vmove(-1);
}
pub fn down(&mut self) {
self.vmove(1);
}
fn line_col(&self) -> (usize, usize) {
let mut line = 0;
let mut col = 0;
for &c in &self.chars[..self.cursor] {
if c == '\n' {
line += 1;
col = 0;
} else {
col += 1;
}
}
(line, col)
}
fn line_bounds(&self) -> Vec<(usize, usize)> {
let mut out = Vec::new();
let mut start = 0;
let mut len = 0;
for (i, &c) in self.chars.iter().enumerate() {
if c == '\n' {
out.push((start, len));
start = i + 1;
len = 0;
} else {
len += 1;
}
}
out.push((start, len));
out
}
fn vmove(&mut self, dir: isize) {
let (line, col) = self.line_col();
let bounds = self.line_bounds();
let target = line as isize + dir;
if target < 0 || target as usize >= bounds.len() {
return;
}
let (start, len) = bounds[target as usize];
self.cursor = start + col.min(len);
}
}
fn is_word(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_places_cursor_at_end() {
let e = TextEdit::new("abc");
assert_eq!(e.split(), ("abc".into(), "".into()));
}
#[test]
fn insert_at_cursor() {
let mut e = TextEdit::new("ac");
e.left();
e.insert("b");
assert_eq!(e.text(), "abc");
assert_eq!(e.split(), ("ab".into(), "c".into()));
}
#[test]
fn backspace_and_delete() {
let mut e = TextEdit::new("abc");
assert!(e.backspace());
assert_eq!(e.text(), "ab");
e.home();
assert!(e.delete());
assert_eq!(e.text(), "b");
e.home();
assert!(!e.backspace());
e.end();
assert!(!e.delete());
}
#[test]
fn handles_unicode() {
let mut e = TextEdit::new("café");
assert!(e.backspace());
assert_eq!(e.text(), "caf");
e.insert("é");
assert_eq!(e.text(), "café");
}
#[test]
fn vertical_movement_keeps_column() {
let mut e = TextEdit::new("hello\nhi");
e.up();
assert_eq!(e.split().0, "he");
e.down();
assert_eq!(e.split(), ("hello\nhi".into(), "".into()));
}
#[test]
fn vertical_movement_stops_at_edges() {
let mut e = TextEdit::new("a\nb");
e.home(); e.up(); assert_eq!(e.split().0, "");
}
#[test]
fn word_navigation() {
let mut e = TextEdit::new("foo bar baz");
e.word_left();
assert_eq!(e.split(), ("foo bar ".into(), "baz".into()));
e.word_left();
assert_eq!(e.split(), ("foo ".into(), "bar baz".into()));
e.word_right();
assert_eq!(e.split(), ("foo bar".into(), " baz".into()));
}
#[test]
fn delete_word_back_and_forward() {
let mut e = TextEdit::new("foo bar baz");
assert!(e.delete_word_back());
assert_eq!(e.text(), "foo bar ");
e.home();
assert!(e.delete_word_forward());
assert_eq!(e.text(), " bar ");
e.home();
assert!(!e.delete_word_back());
}
#[test]
fn delete_to_line_edges() {
let mut e = TextEdit::new("hello world");
e.home();
e.right();
e.right();
assert!(e.delete_to_start());
assert_eq!(e.text(), "llo world");
assert!(e.delete_to_end());
assert_eq!(e.text(), "");
assert!(!e.delete_to_end());
}
}