use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct EditBuffer {
text: String,
cursor: usize,
}
impl EditBuffer {
pub fn new(text: impl Into<String>) -> Self {
let text = text.into();
let cursor = text.len();
Self { text, cursor }
}
pub fn text(&self) -> &str {
&self.text
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
pub fn set_text(&mut self, text: impl Into<String>) {
self.text = text.into();
self.set_cursor(self.cursor);
}
pub fn set_cursor(&mut self, cursor: usize) {
let mut cursor = cursor.min(self.text.len());
while !self.text.is_char_boundary(cursor) {
cursor -= 1;
}
self.cursor = cursor;
}
pub fn insert_char(&mut self, character: char) {
self.text.insert(self.cursor, character);
self.cursor += character.len_utf8();
}
pub fn insert_str(&mut self, text: &str) {
self.text.insert_str(self.cursor, text);
self.cursor += text.len();
}
pub fn insert_paste(&mut self, text: &str) {
for character in
text.chars().filter(|character| !character.is_control() || *character == '\n' || *character == '\t')
{
self.insert_char(character);
}
}
pub fn backspace(&mut self) -> bool {
let Some(previous) = self.previous_boundary() else {
return false;
};
self.text.replace_range(previous..self.cursor, "");
self.cursor = previous;
true
}
pub fn delete(&mut self) -> bool {
let Some(next) = self.next_boundary() else {
return false;
};
self.text.replace_range(self.cursor..next, "");
true
}
pub fn move_left(&mut self) -> bool {
self.previous_boundary().is_some_and(|previous| {
self.cursor = previous;
true
})
}
pub fn move_right(&mut self) -> bool {
self.next_boundary().is_some_and(|next| {
self.cursor = next;
true
})
}
pub fn move_line_start(&mut self) {
self.cursor = self.line_start();
}
pub fn move_line_end(&mut self) {
self.cursor = self.line_end();
}
pub fn replace_range(&mut self, range: std::ops::Range<usize>, replacement: &str) {
let cursor = range.start + replacement.len();
self.text.replace_range(range, replacement);
self.set_cursor(cursor);
}
pub fn take(&mut self) -> String {
self.cursor = 0;
std::mem::take(&mut self.text)
}
pub fn line_start(&self) -> usize {
self.text[..self.cursor].rfind('\n').map_or(0, |index| index + 1)
}
fn line_end(&self) -> usize {
self.text[self.cursor..].find('\n').map_or(self.text.len(), |index| self.cursor + index)
}
fn previous_boundary(&self) -> Option<usize> {
self.text[..self.cursor].chars().next_back().map(|character| self.cursor - character.len_utf8())
}
fn next_boundary(&self) -> Option<usize> {
self.text[self.cursor..].chars().next().map(|character| self.cursor + character.len_utf8())
}
}
pub fn apply_edit_key(buffer: &mut EditBuffer, key: KeyEvent) -> bool {
match key.code {
KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => buffer.move_line_start(),
KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => buffer.move_line_end(),
KeyCode::Char(character) if !key.modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) => {
buffer.insert_char(character);
}
KeyCode::Backspace => {
buffer.backspace();
}
KeyCode::Delete => {
buffer.delete();
}
KeyCode::Left => {
buffer.move_left();
}
KeyCode::Right => {
buffer.move_right();
}
KeyCode::Home => buffer.move_line_start(),
KeyCode::End => buffer.move_line_end(),
_ => return false,
}
true
}
impl From<String> for EditBuffer {
fn from(text: String) -> Self {
Self::new(text)
}
}
impl From<&str> for EditBuffer {
fn from(text: &str) -> Self {
Self::new(text)
}
}
impl std::ops::Deref for EditBuffer {
type Target = str;
fn deref(&self) -> &Self::Target {
self.text()
}
}
impl PartialEq<str> for EditBuffer {
fn eq(&self, other: &str) -> bool {
self.text() == other
}
}
#[cfg(test)]
mod tests {
use super::{EditBuffer, KeyCode, KeyEvent, KeyModifiers, apply_edit_key};
#[test]
fn edit_keys_cover_the_shared_table_and_reject_the_rest() {
let mut buffer = EditBuffer::new("ab");
assert!(apply_edit_key(&mut buffer, KeyEvent::new(KeyCode::Home, KeyModifiers::NONE)));
assert_eq!(buffer.cursor(), 0);
assert!(apply_edit_key(&mut buffer, KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE)));
assert_eq!(buffer.text(), "Xab");
assert!(apply_edit_key(&mut buffer, KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL)));
assert_eq!(buffer.cursor(), 3);
assert!(
!apply_edit_key(&mut buffer, KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL)),
"control chords that are not editing keys must not be typed into the buffer"
);
assert!(!apply_edit_key(&mut buffer, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)));
assert_eq!(buffer.text(), "Xab");
}
#[test]
fn edits_multi_byte_characters_on_char_boundaries() {
let mut buffer = EditBuffer::new("a界");
assert!(buffer.move_left());
buffer.insert_char('🙂');
assert_eq!(buffer.text(), "a🙂界");
assert!(buffer.backspace());
assert_eq!(buffer.text(), "a界");
}
#[test]
fn refuses_to_move_past_either_end() {
let mut buffer = EditBuffer::new("ab");
assert!(!buffer.move_right());
assert!(!buffer.delete());
buffer.set_cursor(0);
assert!(!buffer.move_left());
assert!(!buffer.backspace());
}
#[test]
fn take_empties_the_buffer_and_resets_the_cursor() {
let mut buffer = EditBuffer::new("draft");
assert_eq!(buffer.take(), "draft");
assert!(buffer.is_empty());
assert_eq!(buffer.cursor(), 0);
}
#[test]
fn replace_range_leaves_the_cursor_after_the_replacement() {
let mut buffer = EditBuffer::new("say @fo here");
buffer.replace_range(4..7, "@foo.rs ");
assert_eq!(buffer.text(), "say @foo.rs here");
assert_eq!(buffer.cursor(), 12);
}
}