#[derive(Debug, Clone)]
pub struct InputBuffer {
text: String,
cursor: usize,
selection_anchor: Option<usize>,
}
pub fn new() -> InputBuffer {
InputBuffer {
text: String::new(),
cursor: 0,
selection_anchor: None,
}
}
pub fn text(buf: &InputBuffer) -> &str {
&buf.text
}
pub fn cursor(buf: &InputBuffer) -> usize {
buf.cursor
}
pub fn selection_range(buf: &InputBuffer) -> Option<(usize, usize)> {
buf.selection_anchor.map(|anchor| {
let start = anchor.min(buf.cursor);
let end = anchor.max(buf.cursor);
(start, end)
})
}
pub fn has_selection(buf: &InputBuffer) -> bool {
buf.selection_anchor.is_some()
}
pub fn insert_char(buf: &mut InputBuffer, c: char) {
delete_selection_if_active(buf);
buf.text.insert(byte_offset(&buf.text, buf.cursor), c);
buf.cursor += 1;
}
pub fn backspace(buf: &mut InputBuffer) {
if has_selection(buf) {
delete_selection_if_active(buf);
return;
}
if buf.cursor == 0 {
return;
}
let offset = byte_offset(&buf.text, buf.cursor - 1);
buf.text.remove(offset);
buf.cursor -= 1;
}
pub fn delete(buf: &mut InputBuffer) {
if has_selection(buf) {
delete_selection_if_active(buf);
return;
}
let len = char_count(&buf.text);
if buf.cursor >= len {
return;
}
let offset = byte_offset(&buf.text, buf.cursor);
buf.text.remove(offset);
}
pub fn move_left(buf: &mut InputBuffer) {
buf.selection_anchor = None;
if buf.cursor > 0 {
buf.cursor -= 1;
}
}
pub fn move_right(buf: &mut InputBuffer) {
buf.selection_anchor = None;
let len = char_count(&buf.text);
if buf.cursor < len {
buf.cursor += 1;
}
}
pub fn move_home(buf: &mut InputBuffer) {
buf.selection_anchor = None;
buf.cursor = 0;
}
pub fn move_end(buf: &mut InputBuffer) {
buf.selection_anchor = None;
buf.cursor = char_count(&buf.text);
}
pub fn select_left(buf: &mut InputBuffer) {
if buf.cursor == 0 {
return;
}
if buf.selection_anchor.is_none() {
buf.selection_anchor = Some(buf.cursor);
}
buf.cursor -= 1;
collapse_if_empty(buf);
}
pub fn select_right(buf: &mut InputBuffer) {
let len = char_count(&buf.text);
if buf.cursor >= len {
return;
}
if buf.selection_anchor.is_none() {
buf.selection_anchor = Some(buf.cursor);
}
buf.cursor += 1;
collapse_if_empty(buf);
}
pub fn select_home(buf: &mut InputBuffer) {
if buf.cursor == 0 {
return;
}
if buf.selection_anchor.is_none() {
buf.selection_anchor = Some(buf.cursor);
}
buf.cursor = 0;
collapse_if_empty(buf);
}
pub fn select_end(buf: &mut InputBuffer) {
let len = char_count(&buf.text);
if buf.cursor >= len {
return;
}
if buf.selection_anchor.is_none() {
buf.selection_anchor = Some(buf.cursor);
}
buf.cursor = len;
collapse_if_empty(buf);
}
pub fn select_all(buf: &mut InputBuffer) {
let len = char_count(&buf.text);
if len == 0 {
return;
}
buf.selection_anchor = Some(0);
buf.cursor = len;
}
pub fn move_word_left(buf: &mut InputBuffer) {
buf.selection_anchor = None;
buf.cursor = prev_word_boundary(&buf.text, buf.cursor);
}
pub fn move_word_right(buf: &mut InputBuffer) {
buf.selection_anchor = None;
buf.cursor = next_word_boundary(&buf.text, buf.cursor);
}
pub fn select_word_left(buf: &mut InputBuffer) {
let target = prev_word_boundary(&buf.text, buf.cursor);
if target == buf.cursor {
return;
}
if buf.selection_anchor.is_none() {
buf.selection_anchor = Some(buf.cursor);
}
buf.cursor = target;
collapse_if_empty(buf);
}
pub fn select_word_right(buf: &mut InputBuffer) {
let target = next_word_boundary(&buf.text, buf.cursor);
if target == buf.cursor {
return;
}
if buf.selection_anchor.is_none() {
buf.selection_anchor = Some(buf.cursor);
}
buf.cursor = target;
collapse_if_empty(buf);
}
fn delete_selection_if_active(buf: &mut InputBuffer) {
let Some(anchor) = buf.selection_anchor.take() else {
return;
};
let start = anchor.min(buf.cursor);
let end = anchor.max(buf.cursor);
let start_byte = byte_offset(&buf.text, start);
let end_byte = byte_offset(&buf.text, end);
buf.text.replace_range(start_byte..end_byte, "");
buf.cursor = start;
}
fn collapse_if_empty(buf: &mut InputBuffer) {
if buf.selection_anchor == Some(buf.cursor) {
buf.selection_anchor = None;
}
}
fn byte_offset(text: &str, char_idx: usize) -> usize {
text
.char_indices()
.nth(char_idx)
.map(|(i, _)| i)
.unwrap_or(text.len())
}
fn char_count(text: &str) -> usize {
text.chars().count()
}
fn prev_word_boundary(text: &str, cursor: usize) -> usize {
if cursor == 0 {
return 0;
}
let chars: Vec<char> = text.chars().collect();
let mut pos = cursor - 1;
while pos > 0 && !chars[pos].is_alphanumeric() {
pos -= 1;
}
while pos > 0 && chars[pos - 1].is_alphanumeric() {
pos -= 1;
}
pos
}
fn next_word_boundary(text: &str, cursor: usize) -> usize {
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
if cursor >= len {
return len;
}
let mut pos = cursor;
while pos < len && !chars[pos].is_alphanumeric() {
pos += 1;
}
while pos < len && chars[pos].is_alphanumeric() {
pos += 1;
}
pos
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn insert_appends_at_end() {
let mut buf = new();
insert_char(&mut buf, 'h');
insert_char(&mut buf, 'i');
assert_eq!(text(&buf), "hi");
assert_eq!(cursor(&buf), 2);
}
#[test]
fn insert_at_cursor_position() {
let mut buf = new();
insert_char(&mut buf, 'a');
insert_char(&mut buf, 'c');
move_left(&mut buf);
insert_char(&mut buf, 'b');
assert_eq!(text(&buf), "abc");
assert_eq!(cursor(&buf), 2);
}
#[test]
fn backspace_removes_before_cursor() {
let mut buf = new();
insert_char(&mut buf, 'a');
insert_char(&mut buf, 'b');
insert_char(&mut buf, 'c');
move_left(&mut buf);
backspace(&mut buf);
assert_eq!(text(&buf), "ac");
assert_eq!(cursor(&buf), 1);
}
#[test]
fn backspace_at_start_does_nothing() {
let mut buf = new();
insert_char(&mut buf, 'a');
move_home(&mut buf);
backspace(&mut buf);
assert_eq!(text(&buf), "a");
assert_eq!(cursor(&buf), 0);
}
#[test]
fn delete_removes_at_cursor() {
let mut buf = new();
insert_char(&mut buf, 'a');
insert_char(&mut buf, 'b');
insert_char(&mut buf, 'c');
move_home(&mut buf);
delete(&mut buf);
assert_eq!(text(&buf), "bc");
assert_eq!(cursor(&buf), 0);
}
#[test]
fn delete_at_end_does_nothing() {
let mut buf = new();
insert_char(&mut buf, 'a');
delete(&mut buf);
assert_eq!(text(&buf), "a");
}
#[test]
fn move_left_right_navigates() {
let mut buf = new();
insert_char(&mut buf, 'a');
insert_char(&mut buf, 'b');
insert_char(&mut buf, 'c');
move_left(&mut buf);
assert_eq!(cursor(&buf), 2);
move_left(&mut buf);
assert_eq!(cursor(&buf), 1);
move_right(&mut buf);
assert_eq!(cursor(&buf), 2);
}
#[test]
fn move_left_clamps_at_zero() {
let mut buf = new();
move_left(&mut buf);
assert_eq!(cursor(&buf), 0);
}
#[test]
fn move_right_clamps_at_end() {
let mut buf = new();
insert_char(&mut buf, 'a');
move_right(&mut buf);
assert_eq!(cursor(&buf), 1);
}
#[test]
fn home_and_end() {
let mut buf = new();
insert_char(&mut buf, 'a');
insert_char(&mut buf, 'b');
insert_char(&mut buf, 'c');
move_home(&mut buf);
assert_eq!(cursor(&buf), 0);
move_end(&mut buf);
assert_eq!(cursor(&buf), 3);
}
#[test]
fn select_left_creates_selection() {
let mut buf = new();
insert_char(&mut buf, 'a');
insert_char(&mut buf, 'b');
insert_char(&mut buf, 'c');
select_left(&mut buf);
select_left(&mut buf);
assert_eq!(selection_range(&buf), Some((1, 3)));
assert_eq!(cursor(&buf), 1);
}
#[test]
fn select_right_creates_selection() {
let mut buf = new();
insert_char(&mut buf, 'a');
insert_char(&mut buf, 'b');
insert_char(&mut buf, 'c');
move_home(&mut buf);
select_right(&mut buf);
assert_eq!(selection_range(&buf), Some((0, 1)));
assert_eq!(cursor(&buf), 1);
}
#[test]
fn select_all_selects_entire_text() {
let mut buf = new();
insert_char(&mut buf, 'h');
insert_char(&mut buf, 'i');
select_all(&mut buf);
assert_eq!(selection_range(&buf), Some((0, 2)));
}
#[test]
fn typing_replaces_selection() {
let mut buf = new();
insert_char(&mut buf, 'h');
insert_char(&mut buf, 'e');
insert_char(&mut buf, 'l');
insert_char(&mut buf, 'l');
insert_char(&mut buf, 'o');
select_all(&mut buf);
insert_char(&mut buf, 'x');
assert_eq!(text(&buf), "x");
assert_eq!(cursor(&buf), 1);
assert!(!has_selection(&buf));
}
#[test]
fn backspace_deletes_selection() {
let mut buf = new();
insert_char(&mut buf, 'a');
insert_char(&mut buf, 'b');
insert_char(&mut buf, 'c');
select_left(&mut buf);
select_left(&mut buf);
backspace(&mut buf);
assert_eq!(text(&buf), "a");
assert_eq!(cursor(&buf), 1);
}
#[test]
fn move_clears_selection() {
let mut buf = new();
insert_char(&mut buf, 'a');
insert_char(&mut buf, 'b');
select_left(&mut buf);
move_right(&mut buf);
assert!(!has_selection(&buf));
}
#[test]
fn select_home_from_end() {
let mut buf = new();
insert_char(&mut buf, 'a');
insert_char(&mut buf, 'b');
insert_char(&mut buf, 'c');
select_home(&mut buf);
assert_eq!(selection_range(&buf), Some((0, 3)));
assert_eq!(cursor(&buf), 0);
}
#[test]
fn select_end_from_start() {
let mut buf = new();
insert_char(&mut buf, 'a');
insert_char(&mut buf, 'b');
move_home(&mut buf);
select_end(&mut buf);
assert_eq!(selection_range(&buf), Some((0, 2)));
assert_eq!(cursor(&buf), 2);
}
#[test]
fn word_boundaries_navigate_words() {
let mut buf = new();
for c in "hello world foo".chars() {
insert_char(&mut buf, c);
}
move_home(&mut buf);
move_word_right(&mut buf);
assert_eq!(cursor(&buf), 5);
move_word_right(&mut buf);
assert_eq!(cursor(&buf), 11);
move_word_left(&mut buf);
assert_eq!(cursor(&buf), 6);
move_word_left(&mut buf);
assert_eq!(cursor(&buf), 0);
}
#[test]
fn select_word_left_selects_word() {
let mut buf = new();
for c in "hello world".chars() {
insert_char(&mut buf, c);
}
select_word_left(&mut buf);
assert_eq!(selection_range(&buf), Some((6, 11)));
}
#[test]
fn select_word_right_selects_word() {
let mut buf = new();
for c in "hello world".chars() {
insert_char(&mut buf, c);
}
move_home(&mut buf);
select_word_right(&mut buf);
assert_eq!(selection_range(&buf), Some((0, 5)));
}
#[test]
fn multibyte_chars_handled_correctly() {
let mut buf = new();
for c in "café".chars() {
insert_char(&mut buf, c);
}
assert_eq!(cursor(&buf), 4);
move_left(&mut buf);
insert_char(&mut buf, 'x');
assert_eq!(text(&buf), "cafxé");
assert_eq!(cursor(&buf), 4);
}
#[test]
fn selection_collapes_when_cursor_meets_anchor() {
let mut buf = new();
insert_char(&mut buf, 'a');
insert_char(&mut buf, 'b');
select_left(&mut buf);
select_right(&mut buf);
assert!(!has_selection(&buf));
}
#[test]
fn delete_with_selection_removes_selected() {
let mut buf = new();
for c in "abc".chars() {
insert_char(&mut buf, c);
}
move_home(&mut buf);
select_right(&mut buf);
select_right(&mut buf);
delete(&mut buf);
assert_eq!(text(&buf), "c");
}
#[test]
fn select_left_at_zero_is_noop() {
let mut buf = new();
insert_char(&mut buf, 'a');
move_home(&mut buf);
select_left(&mut buf);
assert_eq!(cursor(&buf), 0);
assert!(!has_selection(&buf));
}
#[test]
fn select_right_at_end_is_noop() {
let mut buf = new();
insert_char(&mut buf, 'a');
select_right(&mut buf);
assert_eq!(cursor(&buf), 1);
assert!(!has_selection(&buf));
}
#[test]
fn select_home_at_zero_is_noop() {
let mut buf = new();
select_home(&mut buf);
assert_eq!(cursor(&buf), 0);
assert!(!has_selection(&buf));
}
#[test]
fn select_end_at_end_is_noop() {
let mut buf = new();
insert_char(&mut buf, 'a');
select_end(&mut buf);
assert_eq!(cursor(&buf), 1);
assert!(!has_selection(&buf));
}
#[test]
fn select_all_on_empty_is_noop() {
let mut buf = new();
select_all(&mut buf);
assert!(!has_selection(&buf));
}
#[test]
fn select_word_left_at_boundary_is_noop() {
let mut buf = new();
insert_char(&mut buf, 'a');
move_home(&mut buf);
select_word_left(&mut buf);
assert_eq!(cursor(&buf), 0);
assert!(!has_selection(&buf));
}
#[test]
fn select_word_right_at_boundary_is_noop() {
let mut buf = new();
insert_char(&mut buf, 'a');
select_word_right(&mut buf);
assert_eq!(cursor(&buf), 1);
assert!(!has_selection(&buf));
}
#[test]
fn prev_word_boundary_at_zero_returns_zero() {
assert_eq!(prev_word_boundary("hello", 0), 0);
}
#[test]
fn next_word_boundary_at_end_returns_len() {
assert_eq!(next_word_boundary("hello", 5), 5);
}
}