const DEFAULT_CAPACITY: usize = 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Key {
Char(char),
Backspace,
MoveLeft,
MoveRight,
WordLeft,
WordRight,
LineStart,
LineEnd,
Reset,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WordAtCaret {
pub word: String,
pub trailing: String,
pub chars_before_caret: usize,
pub chars_after_caret: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Sentence {
pub sentence: String,
pub buffer_byte_start: usize,
pub buffer_byte_end: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SentenceAtCaret {
pub sentence: String,
pub trailing: String,
pub chars_before_caret: usize,
pub chars_after_caret: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NearbyWord {
pub word: String,
pub byte_start: usize,
pub byte_end: usize,
pub caret_offset_chars: i32,
}
#[derive(Debug)]
pub struct Buffer {
text: String,
caret: usize,
capacity: usize,
}
impl Default for Buffer {
fn default() -> Self {
Self::with_capacity(DEFAULT_CAPACITY)
}
}
impl Buffer {
pub fn with_capacity(capacity: usize) -> Self {
Self {
text: String::new(),
caret: 0,
capacity: capacity.max(1),
}
}
pub fn push(&mut self, key: Key) {
match key {
Key::Char(c) => {
self.text.insert(self.caret, c);
self.caret += c.len_utf8();
self.trim_to_capacity();
}
Key::Backspace => {
if self.caret == 0 {
return;
}
let prev = prev_char_boundary(&self.text, self.caret);
self.text.drain(prev..self.caret);
self.caret = prev;
}
Key::MoveLeft => {
if self.caret == 0 {
return;
}
self.caret = prev_char_boundary(&self.text, self.caret);
}
Key::MoveRight => {
if self.caret >= self.text.len() {
return;
}
self.caret = next_char_boundary(&self.text, self.caret);
}
Key::WordLeft => {
self.caret = prev_word_boundary(&self.text, self.caret);
}
Key::WordRight => {
self.caret = next_word_boundary(&self.text, self.caret);
}
Key::LineStart => {
self.caret = 0;
}
Key::LineEnd => {
self.caret = self.text.len();
}
Key::Reset => {
self.text.clear();
self.caret = 0;
}
}
}
pub fn clear(&mut self) {
self.text.clear();
self.caret = 0;
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
pub fn text(&self) -> &str {
&self.text
}
pub fn text_before_caret(&self) -> &str {
&self.text[..self.caret]
}
pub fn sentence_at_caret(&self) -> Option<SentenceAtCaret> {
let caret = self.caret;
let s = self.sentence_containing(caret)?;
let caret_in_range = caret.clamp(s.buffer_byte_start, s.buffer_byte_end);
let chars_before = self.text[s.buffer_byte_start..caret_in_range]
.chars()
.count();
let chars_after = self.text[caret_in_range..s.buffer_byte_end].chars().count();
let trailing = if caret > s.buffer_byte_end {
self.text[s.buffer_byte_end..caret].to_string()
} else {
String::new()
};
Some(SentenceAtCaret {
sentence: s.sentence,
trailing,
chars_before_caret: chars_before,
chars_after_caret: chars_after,
})
}
pub fn sentence_containing(&self, byte_offset: usize) -> Option<Sentence> {
let text = &self.text;
if text.is_empty() {
return None;
}
let mut ranges: Vec<(usize, usize)> = Vec::new();
let mut start = 0;
for (i, c) in text.char_indices() {
if matches!(c, '.' | '!' | '?') {
ranges.push((start, i + c.len_utf8()));
start = i + c.len_utf8();
}
}
if start < text.len() {
ranges.push((start, text.len()));
}
if ranges.is_empty() {
return None;
}
let mut idx = ranges
.iter()
.rposition(|&(s, e)| s <= byte_offset && byte_offset <= e)?;
if text[ranges[idx].0..ranges[idx].1].trim().is_empty() && idx > 0 {
idx -= 1;
}
let (range_start, range_end) = ranges[idx];
let raw = &text[range_start..range_end];
let leading_ws = raw.len() - raw.trim_start().len();
let sentence_start = range_start + leading_ws;
let sentence_end = range_start + raw.trim_end().len();
if sentence_start >= sentence_end {
return None;
}
Some(Sentence {
sentence: text[sentence_start..sentence_end].to_string(),
buffer_byte_start: sentence_start,
buffer_byte_end: sentence_end,
})
}
pub fn word_at_caret(&self) -> Option<WordAtCaret> {
let caret = self.caret;
let text = &self.text;
let prev_is_word = text[..caret].chars().next_back().is_some_and(is_word_char);
if prev_is_word {
let right_span: usize = text[caret..]
.chars()
.take_while(|&c| is_word_char(c))
.map(char::len_utf8)
.sum();
let left_span: usize = text[..caret]
.chars()
.rev()
.take_while(|&c| is_word_char(c))
.map(char::len_utf8)
.sum();
let word_start = caret - left_span;
let word_end = caret + right_span;
if word_start == word_end {
return None;
}
return Some(WordAtCaret {
word: text[word_start..word_end].to_string(),
trailing: String::new(),
chars_before_caret: text[word_start..caret].chars().count(),
chars_after_caret: text[caret..word_end].chars().count(),
});
}
let before = &text[..caret];
let trimmed_right = before.trim_end_matches(|c: char| !is_word_char(c));
if trimmed_right.is_empty() {
return None;
}
let word_chars: usize = trimmed_right
.chars()
.rev()
.take_while(|&c| is_word_char(c))
.map(char::len_utf8)
.sum();
if word_chars == 0 {
return None;
}
let word_end = trimmed_right.len();
let word_start = word_end - word_chars;
Some(WordAtCaret {
word: text[word_start..word_end].to_string(),
trailing: text[word_end..caret].to_string(),
chars_before_caret: text[word_start..word_end].chars().count(),
chars_after_caret: 0,
})
}
pub fn apply_around_caret(&mut self, backspaces: usize, deletes: usize, insert: &str) {
for _ in 0..backspaces {
if self.caret == 0 {
break;
}
let prev = prev_char_boundary(&self.text, self.caret);
self.text.drain(prev..self.caret);
self.caret = prev;
}
for _ in 0..deletes {
if self.caret >= self.text.len() {
break;
}
let next = next_char_boundary(&self.text, self.caret);
self.text.drain(self.caret..next);
}
self.text.insert_str(self.caret, insert);
self.caret += insert.len();
self.trim_to_capacity();
}
pub fn apply(&mut self, backspaces: usize, insert: &str) {
self.apply_around_caret(backspaces, 0, insert);
}
pub fn caret(&self) -> usize {
self.caret
}
pub fn apply_at_word(&mut self, byte_start: usize, byte_end: usize, insert: &str) {
debug_assert!(byte_start <= byte_end && byte_end <= self.text.len());
self.text.replace_range(byte_start..byte_end, insert);
self.caret = byte_start + insert.len();
self.trim_to_capacity();
}
pub fn words_near_caret(&self) -> Vec<NearbyWord> {
let text = &self.text;
let caret = self.caret;
let mut out: Vec<NearbyWord> = Vec::new();
let mut current_start: Option<usize> = None;
for (i, c) in text.char_indices() {
if is_word_char(c) {
if current_start.is_none() {
current_start = Some(i);
}
} else if let Some(start) = current_start.take() {
out.push(make_nearby_word(text, caret, start, i));
}
}
if let Some(start) = current_start {
out.push(make_nearby_word(text, caret, start, text.len()));
}
out.sort_by_key(|nw| nw.caret_offset_chars.abs());
out
}
fn trim_to_capacity(&mut self) {
while self.text.chars().count() > self.capacity {
let first = self.text.chars().next().map_or(0, char::len_utf8);
self.text.drain(..first);
self.caret = self.caret.saturating_sub(first);
}
}
}
fn prev_char_boundary(s: &str, pos: usize) -> usize {
s[..pos].char_indices().next_back().map_or(0, |(i, _)| i)
}
fn next_char_boundary(s: &str, pos: usize) -> usize {
s[pos..].chars().next().map_or(pos, |c| pos + c.len_utf8())
}
fn make_nearby_word(text: &str, caret: usize, start: usize, end: usize) -> NearbyWord {
let caret_offset_chars = if end >= caret {
text[caret..end].chars().count() as i32
} else {
-(text[end..caret].chars().count() as i32)
};
NearbyWord {
word: text[start..end].to_string(),
byte_start: start,
byte_end: end,
caret_offset_chars,
}
}
fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '\''
}
fn is_nav_word_char(c: char) -> bool {
is_word_char(c) || matches!(c, '.' | '-' | '_')
}
fn prev_word_boundary(s: &str, from: usize) -> usize {
let left = &s[..from];
let trim: usize = left
.chars()
.rev()
.take_while(|&c| !is_nav_word_char(c))
.map(char::len_utf8)
.sum();
let trimmed_end = left.len() - trim;
let word_chars: usize = left[..trimmed_end]
.chars()
.rev()
.take_while(|&c| is_nav_word_char(c))
.map(char::len_utf8)
.sum();
trimmed_end - word_chars
}
fn next_word_boundary(s: &str, from: usize) -> usize {
let right = &s[from..];
let skip: usize = right
.chars()
.take_while(|&c| !is_nav_word_char(c))
.map(char::len_utf8)
.sum();
let word_chars: usize = right[skip..]
.chars()
.take_while(|&c| is_nav_word_char(c))
.map(char::len_utf8)
.sum();
from + skip + word_chars
}
#[cfg(test)]
mod tests {
use super::*;
fn type_str(buf: &mut Buffer, s: &str) {
for c in s.chars() {
buf.push(Key::Char(c));
}
}
#[test]
fn empty_buffer_has_no_word() {
let buf = Buffer::default();
assert!(buf.is_empty());
assert_eq!(buf.word_at_caret(), None);
}
#[test]
fn word_at_caret_at_end_of_word() {
let mut buf = Buffer::default();
type_str(&mut buf, "vernuer");
let at = buf.word_at_caret().unwrap();
assert_eq!(at.word, "vernuer");
assert_eq!(at.trailing, "");
assert_eq!(at.chars_before_caret, 7);
assert_eq!(at.chars_after_caret, 0);
}
#[test]
fn word_at_caret_in_trailing_whitespace_picks_the_left_word() {
let mut buf = Buffer::default();
type_str(&mut buf, "vernuer ");
let at = buf.word_at_caret().unwrap();
assert_eq!(at.word, "vernuer");
assert_eq!(at.trailing, " ");
assert_eq!(at.chars_before_caret, 7);
assert_eq!(at.chars_after_caret, 0);
}
#[test]
fn word_at_caret_inside_word_expands_both_directions() {
let mut buf = Buffer::default();
type_str(&mut buf, "vernuer");
for _ in 0..4 {
buf.push(Key::MoveLeft);
}
let at = buf.word_at_caret().unwrap();
assert_eq!(at.word, "vernuer");
assert_eq!(at.chars_before_caret, 3);
assert_eq!(at.chars_after_caret, 4);
}
#[test]
fn word_at_caret_picks_the_final_word() {
let mut buf = Buffer::default();
type_str(&mut buf, "the quick vernuer ");
let at = buf.word_at_caret().unwrap();
assert_eq!(at.word, "vernuer");
}
#[test]
fn all_whitespace_has_no_word_at_caret() {
let mut buf = Buffer::default();
type_str(&mut buf, " ");
assert_eq!(buf.word_at_caret(), None);
}
#[test]
fn word_at_caret_handles_multibyte_chars() {
let mut buf = Buffer::default();
type_str(&mut buf, "café ");
let at = buf.word_at_caret().unwrap();
assert_eq!(at.word, "café");
assert_eq!(at.chars_before_caret, 4);
}
#[test]
fn backspace_removes_the_last_character() {
let mut buf = Buffer::default();
type_str(&mut buf, "vernuer");
buf.push(Key::Backspace);
assert_eq!(buf.text(), "vernue");
}
#[test]
fn backspace_on_empty_buffer_is_a_no_op() {
let mut buf = Buffer::default();
buf.push(Key::Backspace);
assert!(buf.is_empty());
}
#[test]
fn reset_clears_the_buffer() {
let mut buf = Buffer::default();
type_str(&mut buf, "vernuer ");
buf.push(Key::Reset);
assert!(buf.is_empty());
assert_eq!(buf.word_at_caret(), None);
}
#[test]
fn buffer_is_bounded_by_capacity() {
let mut buf = Buffer::with_capacity(5);
type_str(&mut buf, "abcdefgh");
assert_eq!(buf.text(), "defgh");
}
#[test]
fn sentence_at_caret_after_an_ender() {
let mut buf = Buffer::default();
type_str(&mut buf, "Hello there. how are you ");
let at = buf.sentence_at_caret().unwrap();
assert_eq!(at.sentence, "how are you");
assert_eq!(at.trailing, " ");
assert_eq!(at.chars_after_caret, 0);
}
#[test]
fn sentence_at_caret_when_no_ender_yet() {
let mut buf = Buffer::default();
type_str(&mut buf, "the quick brown fox");
let at = buf.sentence_at_caret().unwrap();
assert_eq!(at.sentence, "the quick brown fox");
assert_eq!(at.trailing, "");
}
#[test]
fn sentence_at_caret_with_multiple_enders_picks_current() {
let mut buf = Buffer::default();
type_str(&mut buf, "Hi! Hello there. How are yu");
let at = buf.sentence_at_caret().unwrap();
assert_eq!(at.sentence, "How are yu");
}
#[test]
fn sentence_at_caret_includes_the_trailing_ender() {
let mut buf = Buffer::default();
type_str(&mut buf, "Hello there.");
let at = buf.sentence_at_caret().unwrap();
assert_eq!(at.sentence, "Hello there.");
}
#[test]
fn sentence_at_caret_picks_the_final_of_multiple_complete_sentences() {
let mut buf = Buffer::default();
type_str(&mut buf, "First sentence. Second sentence!");
let at = buf.sentence_at_caret().unwrap();
assert_eq!(at.sentence, "Second sentence!");
}
#[test]
fn sentence_at_caret_after_complete_one_then_trailing_ws() {
let mut buf = Buffer::default();
type_str(&mut buf, "Hello there. ");
let at = buf.sentence_at_caret().unwrap();
assert_eq!(at.sentence, "Hello there.");
assert_eq!(at.trailing, " ");
}
#[test]
fn sentence_at_caret_returns_none_for_whitespace_only() {
let mut buf = Buffer::default();
type_str(&mut buf, " ");
assert!(buf.sentence_at_caret().is_none());
}
#[test]
fn sentence_at_caret_in_middle_spans_both_sides() {
let mut buf = Buffer::default();
type_str(&mut buf, "the quick brown fox jumps");
for _ in 0..10 {
buf.push(Key::MoveLeft);
}
let at = buf.sentence_at_caret().unwrap();
assert_eq!(at.sentence, "the quick brown fox jumps");
assert_eq!(at.chars_before_caret, 15);
assert_eq!(at.chars_after_caret, 10);
}
#[test]
fn sentence_containing_returns_the_active_sentence_with_buffer_offsets() {
let mut buf = Buffer::default();
type_str(&mut buf, "Hello world. The quick brown fox.");
let fox_buf_start = buf.text().find("fox").unwrap();
let fox_buf_end = fox_buf_start + "fox".len();
let s = buf.sentence_containing(fox_buf_start).unwrap();
assert_eq!(s.sentence, "The quick brown fox.");
assert_eq!(s.buffer_byte_start, 13);
assert_eq!(s.buffer_byte_end, 33);
let start_in_sentence = fox_buf_start - s.buffer_byte_start;
let end_in_sentence = fox_buf_end - s.buffer_byte_start;
assert_eq!(&s.sentence[start_in_sentence..end_in_sentence], "fox");
}
#[test]
fn sentence_containing_picks_first_sentence_for_offset_in_it() {
let mut buf = Buffer::default();
type_str(&mut buf, "Hello world. The quick brown fox.");
let s = buf.sentence_containing(3).unwrap();
assert_eq!(s.sentence, "Hello world.");
assert_eq!(s.buffer_byte_start, 0);
assert_eq!(s.buffer_byte_end, 12);
}
#[test]
fn sentence_containing_returns_none_for_whitespace_only_buffer() {
let mut buf = Buffer::default();
type_str(&mut buf, " ");
assert!(buf.sentence_containing(1).is_none());
}
#[test]
fn sentence_containing_steps_back_from_inter_sentence_whitespace() {
let mut buf = Buffer::default();
type_str(&mut buf, "Hello world. ");
let s = buf.sentence_containing(13).unwrap();
assert_eq!(s.sentence, "Hello world.");
}
#[test]
fn apply_mirrors_a_correction_at_end() {
let mut buf = Buffer::default();
type_str(&mut buf, "vernuer ");
buf.apply(8, "veneer ");
assert_eq!(buf.text(), "veneer ");
assert_eq!(buf.word_at_caret().unwrap().word, "veneer");
}
#[test]
fn apply_around_caret_mirrors_a_mid_word_fix() {
let mut buf = Buffer::default();
type_str(&mut buf, "vernuer trailing");
for _ in 0..9 {
buf.push(Key::MoveLeft);
}
for _ in 0..4 {
buf.push(Key::MoveLeft);
}
buf.apply_around_caret(3, 4, "veneer");
assert_eq!(buf.text(), "veneer trailing");
}
#[test]
fn move_left_walks_caret_back_without_clearing_text() {
let mut buf = Buffer::default();
type_str(&mut buf, "hello world");
for _ in 0..6 {
buf.push(Key::MoveLeft);
}
assert_eq!(buf.text(), "hello world");
assert_eq!(buf.text_before_caret(), "hello");
}
#[test]
fn typing_after_move_left_inserts_at_caret() {
let mut buf = Buffer::default();
type_str(&mut buf, "helloworld");
for _ in 0..5 {
buf.push(Key::MoveLeft);
}
type_str(&mut buf, " ");
assert_eq!(buf.text(), "hello world");
}
#[test]
fn backspace_after_move_left_removes_the_char_before_caret() {
let mut buf = Buffer::default();
type_str(&mut buf, "hello world");
for _ in 0..6 {
buf.push(Key::MoveLeft);
}
buf.push(Key::Backspace);
assert_eq!(buf.text(), "hell world");
}
#[test]
fn move_right_at_end_is_a_no_op() {
let mut buf = Buffer::default();
type_str(&mut buf, "abc");
buf.push(Key::MoveRight);
assert_eq!(buf.text_before_caret(), "abc");
}
#[test]
fn move_left_at_start_is_a_no_op() {
let mut buf = Buffer::default();
type_str(&mut buf, "abc");
for _ in 0..10 {
buf.push(Key::MoveLeft);
}
assert_eq!(buf.text_before_caret(), "");
assert_eq!(buf.text(), "abc");
}
#[test]
fn line_start_and_line_end_jump_to_the_edges() {
let mut buf = Buffer::default();
type_str(&mut buf, "hello world");
buf.push(Key::LineStart);
assert_eq!(buf.text_before_caret(), "");
buf.push(Key::LineEnd);
assert_eq!(buf.text_before_caret(), "hello world");
}
#[test]
fn word_left_jumps_to_previous_word_start() {
let mut buf = Buffer::default();
type_str(&mut buf, "the quick brown fox");
buf.push(Key::WordLeft);
assert_eq!(buf.text_before_caret(), "the quick brown ");
buf.push(Key::WordLeft);
assert_eq!(buf.text_before_caret(), "the quick ");
buf.push(Key::WordLeft);
assert_eq!(buf.text_before_caret(), "the ");
buf.push(Key::WordLeft);
assert_eq!(buf.text_before_caret(), "");
}
#[test]
fn word_right_jumps_to_next_word_end() {
let mut buf = Buffer::default();
type_str(&mut buf, "the quick brown fox");
buf.push(Key::LineStart);
buf.push(Key::WordRight);
assert_eq!(buf.text_before_caret(), "the");
buf.push(Key::WordRight);
assert_eq!(buf.text_before_caret(), "the quick");
}
#[test]
fn word_left_from_mid_word_lands_at_word_start() {
let mut buf = Buffer::default();
type_str(&mut buf, "hello world");
for _ in 0..3 {
buf.push(Key::MoveLeft);
}
assert_eq!(buf.text_before_caret(), "hello wo");
buf.push(Key::WordLeft);
assert_eq!(buf.text_before_caret(), "hello ");
}
#[test]
fn ctrl_left_skips_commas_like_a_typical_editor() {
let mut buf = Buffer::default();
type_str(&mut buf, "hello, world");
buf.push(Key::WordLeft);
assert_eq!(buf.text_before_caret(), "hello, ");
buf.push(Key::WordLeft);
assert_eq!(buf.text_before_caret(), "");
}
#[test]
fn word_at_caret_excludes_trailing_punctuation() {
let mut buf = Buffer::default();
type_str(&mut buf, "recieve,");
let at = buf.word_at_caret().expect("word at caret");
assert_eq!(at.word, "recieve");
assert_eq!(at.trailing, ",");
}
#[test]
fn word_at_caret_keeps_apostrophes_for_contractions() {
let mut buf = Buffer::default();
type_str(&mut buf, "don't");
let at = buf.word_at_caret().expect("word at caret");
assert_eq!(at.word, "don't");
}
#[test]
fn ctrl_right_skips_dot_runs_as_one_nav_word() {
let mut buf = Buffer::default();
type_str(&mut buf, "deal...do you");
buf.push(Key::LineStart);
buf.push(Key::WordRight);
assert_eq!(buf.text_before_caret(), "deal...do");
buf.push(Key::WordRight);
assert_eq!(buf.text_before_caret(), "deal...do you");
}
#[test]
fn word_at_caret_does_not_pull_dot_neighbors_in() {
let mut buf = Buffer::default();
type_str(&mut buf, "deal...do");
let at = buf.word_at_caret().expect("word at caret");
assert_eq!(at.word, "do");
}
#[test]
fn words_near_caret_orders_by_char_distance() {
let mut buf = Buffer::default();
type_str(&mut buf, "the quick brown fox");
let nearby = buf.words_near_caret();
let just_words: Vec<&str> = nearby.iter().map(|nw| nw.word.as_str()).collect();
assert_eq!(just_words, vec!["fox", "brown", "quick", "the"]);
assert_eq!(nearby[0].caret_offset_chars, 0);
assert_eq!(nearby[1].caret_offset_chars, -4);
}
#[test]
fn words_near_caret_walks_outward_from_mid_buffer_caret() {
let mut buf = Buffer::default();
type_str(&mut buf, "the quick brown fox");
for _ in 0..("brown fox".len()) {
buf.push(Key::MoveLeft);
}
assert_eq!(buf.text_before_caret(), "the quick ");
let nearby = buf.words_near_caret();
assert_eq!(nearby[0].word, "quick");
assert_eq!(nearby[0].caret_offset_chars, -1);
assert_eq!(nearby[1].word, "brown");
assert_eq!(nearby[1].caret_offset_chars, 5);
}
#[test]
fn apply_at_word_replaces_and_sets_caret() {
let mut buf = Buffer::default();
type_str(&mut buf, "the quick brown fox");
buf.apply_at_word(10, 15, "red");
assert_eq!(buf.text(), "the quick red fox");
assert_eq!(buf.caret(), 13);
}
}