holon 0.14.1

A headless, event-driven runtime for long-lived agents
Documentation
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(super) struct ComposerState {
    text: String,
    cursor: usize,
}

impl ComposerState {
    pub(super) fn new() -> Self {
        Self::default()
    }

    pub(super) fn as_str(&self) -> &str {
        &self.text
    }

    pub(super) fn cursor(&self) -> usize {
        self.cursor
    }

    pub(super) fn is_empty(&self) -> bool {
        self.text.is_empty()
    }

    pub(super) fn clear(&mut self) {
        self.text.clear();
        self.cursor = 0;
    }

    pub(super) fn insert_char(&mut self, ch: char) {
        self.text.insert(self.cursor, ch);
        self.cursor += ch.len_utf8();
    }

    pub(super) fn insert_str(&mut self, text: &str) {
        self.text.insert_str(self.cursor, text);
        self.cursor += text.len();
    }

    pub(super) fn insert_newline(&mut self) {
        self.insert_char('\n');
    }

    pub(super) fn move_left(&mut self) {
        if let Some(index) = self.previous_boundary(self.cursor) {
            self.cursor = index;
        }
    }

    pub(super) fn move_right(&mut self) {
        if let Some(index) = self.next_boundary(self.cursor) {
            self.cursor = index;
        }
    }

    pub(super) fn move_home(&mut self) {
        self.cursor = self.current_line_start();
    }

    pub(super) fn move_end(&mut self) {
        self.cursor = self.current_line_end();
    }

    pub(super) fn backspace(&mut self) {
        if let Some(start) = self.previous_boundary(self.cursor) {
            self.text.drain(start..self.cursor);
            self.cursor = start;
        }
    }

    pub(super) fn delete(&mut self) {
        if let Some(end) = self.next_boundary(self.cursor) {
            self.text.drain(self.cursor..end);
        }
    }
    pub(super) fn move_to_start(&mut self) {
        self.cursor = 0;
    }

    pub(super) fn move_to_end(&mut self) {
        self.cursor = self.text.len();
    }

    pub(super) fn delete_to_end(&mut self) {
        let end = self.current_line_end();
        self.text.truncate(end);
        if self.cursor > end {
            self.cursor = end;
        }
    }

    pub(super) fn delete_to_start(&mut self) {
        let start = self.current_line_start();
        self.text.drain(start..self.cursor);
        self.cursor = start;
    }

    pub(super) fn delete_word(&mut self) {
        let start = self.find_word_start();
        self.text.drain(start..self.cursor);
        self.cursor = start;
    }

    fn find_word_start(&self) -> usize {
        let before = &self.text[..self.cursor];
        let trimmed_end = before.trim_end().len();
        let word_end = if trimmed_end == 0 {
            before.len()
        } else {
            trimmed_end
        };
        before[..word_end]
            .rfind(|c: char| c.is_whitespace())
            .map(|i| {
                // Find the byte after the whitespace character
                i + before[i..]
                    .chars()
                    .next()
                    .map(|c| c.len_utf8())
                    .unwrap_or(1)
            })
            .unwrap_or(0)
    }

    fn previous_boundary(&self, from: usize) -> Option<usize> {
        self.text[..from]
            .char_indices()
            .last()
            .map(|(index, _)| index)
    }

    fn next_boundary(&self, from: usize) -> Option<usize> {
        if from >= self.text.len() {
            return None;
        }
        self.text[from..]
            .chars()
            .next()
            .map(|ch| from + ch.len_utf8())
    }

    fn current_line_start(&self) -> usize {
        self.text[..self.cursor]
            .rfind('\n')
            .map(|index| index + 1)
            .unwrap_or(0)
    }

    fn current_line_end(&self) -> usize {
        self.text[self.cursor..]
            .find('\n')
            .map(|index| self.cursor + index)
            .unwrap_or(self.text.len())
    }
}

impl From<&str> for ComposerState {
    fn from(value: &str) -> Self {
        Self {
            text: value.to_string(),
            cursor: value.len(),
        }
    }
}

impl From<String> for ComposerState {
    fn from(value: String) -> Self {
        let cursor = value.len();
        Self {
            text: value,
            cursor,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::ComposerState;

    #[test]
    fn inserts_and_moves_inside_existing_text() {
        let mut composer = ComposerState::from("hlo");
        composer.move_left();
        composer.move_left();
        composer.insert_char('e');
        assert_eq!(composer.as_str(), "helo");
        assert_eq!(composer.cursor(), 2);
    }

    #[test]
    fn inserts_pasted_text_at_cursor() {
        let mut composer = ComposerState::from("ab");
        composer.move_left();
        composer.insert_str("x\ny");
        assert_eq!(composer.as_str(), "ax\nyb");
        assert_eq!(composer.cursor(), "ax\ny".len());
    }

    #[test]
    fn backspace_and_delete_respect_utf8_boundaries() {
        let mut composer = ComposerState::from("你好吗");
        composer.move_left();
        composer.backspace();
        assert_eq!(composer.as_str(), "你吗");
        composer.move_home();
        composer.delete();
        assert_eq!(composer.as_str(), "");
    }

    #[test]
    fn home_and_end_stay_within_current_line() {
        let mut composer = ComposerState::from("first\nsecond\nthird");
        composer.move_home();
        assert_eq!(composer.cursor(), "first\nsecond\n".len());
        composer.move_left();
        composer.move_left();
        composer.move_home();
        assert_eq!(composer.cursor(), "first\n".len());
        composer.move_end();
        assert_eq!(composer.cursor(), "first\nsecond".len());
    }
}

#[test]
fn move_to_start_moves_cursor_to_beginning() {
    let mut composer = ComposerState::from("some text");
    composer.move_to_start();
    assert_eq!(composer.cursor(), 0);
}

#[test]
fn move_to_end_moves_cursor_to_end() {
    let mut composer = ComposerState::from("some text");
    composer.move_to_start();
    composer.move_to_end();
    assert_eq!(composer.cursor(), 9);
}

#[test]
fn delete_to_end_deletes_from_cursor_to_line_end() {
    let mut composer = ComposerState::from("first line\nsecond line");
    // Move to start of "first line"
    composer.move_to_start();
    // Move to position after "first"
    for _ in 0..5 {
        composer.move_right();
    }
    composer.delete_to_end();
    assert_eq!(composer.as_str(), "first line");
    assert_eq!(composer.cursor(), 5);
}

#[test]
fn delete_to_start_deletes_from_line_start_to_cursor() {
    let mut composer = ComposerState::from("first line\nsecond line");
    // Move to start of "first line"
    composer.move_to_start();
    // Move to position after "first "
    for _ in 0..6 {
        composer.move_right();
    }
    composer.delete_to_start();
    assert_eq!(composer.as_str(), "line\nsecond line");
    assert_eq!(composer.cursor(), 0);
}

#[test]
fn delete_to_end_on_last_line_deletes_to_end_of_buffer() {
    let mut composer = ComposerState::from("first\nsecond");
    // Move to start of "second"
    composer.move_to_start();
    for _ in 0..7 {
        composer.move_right();
    }
    composer.delete_to_end();
    assert_eq!(composer.as_str(), "first\nsecond");
    // Cursor should be at end since there's no newline after
    assert_eq!(composer.cursor(), 7);
}

#[test]
fn delete_word_deletes_previous_word() {
    let mut composer = ComposerState::from("hello world test");
    composer.delete_word();
    assert_eq!(composer.as_str(), "hello world ");
    assert_eq!(composer.cursor(), 12);
}

#[test]
fn delete_word_handles_multiple_spaces() {
    let mut composer = ComposerState::from("hello   world");
    composer.delete_word();
    assert_eq!(composer.as_str(), "hello   ");
    assert_eq!(composer.cursor(), 8);
}

#[test]
fn delete_word_handles_tabs_and_newlines() {
    let mut composer = ComposerState::from("hello\tworld\nthere");
    composer.delete_word();
    // Should delete "there" and stop at whitespace
    assert_eq!(composer.as_str(), "hello\tworld\n");
    assert_eq!(composer.cursor(), 12);
}