illumitech-forge-cli 1.5.0

Evidence-backed agent execution CLI
Documentation
use super::{KeyCode, KeyEvent, KeyModifiers, display};

#[derive(Default)]
pub(super) struct Editor {
    pub text: String,
    pub cursor: usize,
    pub saved: String,
    history_index: Option<usize>,
    history_draft: String,
}
impl Editor {
    pub fn set(&mut self, text: String) {
        self.text = text;
        self.cursor = self.text.len();
    }
    pub fn clear(&mut self) {
        self.text.clear();
        self.cursor = 0;
        self.history_index = None;
    }
    pub fn insert(&mut self, text: &str) {
        let text = display(text);
        if self.text.len() + text.len() > 64 * 1024 {
            return;
        }
        self.text.insert_str(self.cursor, &text);
        self.cursor += text.len();
    }
    fn previous(&self) -> usize {
        self.text[..self.cursor]
            .char_indices()
            .next_back()
            .map_or(0, |(i, _)| i)
    }
    fn next(&self) -> usize {
        self.cursor
            + self.text[self.cursor..]
                .chars()
                .next()
                .map_or(0, char::len_utf8)
    }
    fn line_start(&self) -> usize {
        self.text[..self.cursor].rfind('\n').map_or(0, |i| i + 1)
    }
    fn line_end(&self) -> usize {
        self.cursor
            + self.text[self.cursor..]
                .find('\n')
                .unwrap_or(self.text.len() - self.cursor)
    }
    fn kill(&mut self, start: usize, end: usize) {
        if start == end {
            return;
        }
        self.saved = self.text.drain(start..end).collect();
        self.cursor = start;
    }
    pub fn clear_recoverable(&mut self) {
        self.saved = self.text.clone();
        self.clear();
    }
    pub fn history(&mut self, entries: &[String], older: bool) {
        if entries.is_empty() {
            return;
        }
        if older {
            if self.history_index.is_none() {
                self.history_draft = self.text.clone();
            }
            let index = self
                .history_index
                .map_or(entries.len() - 1, |i| i.saturating_sub(1));
            self.history_index = Some(index);
            self.set(entries[index].clone());
        } else if let Some(index) = self.history_index {
            if index + 1 < entries.len() {
                self.history_index = Some(index + 1);
                self.set(entries[index + 1].clone());
            } else {
                self.history_index = None;
                self.set(self.history_draft.clone());
            }
        }
    }
    pub fn edit(&mut self, key: KeyEvent) {
        let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
        if ctrl {
            match key.code {
                KeyCode::Char('a') => self.cursor = self.line_start(),
                KeyCode::Char('e') => self.cursor = self.line_end(),
                KeyCode::Char('u') => self.kill(self.line_start(), self.cursor),
                KeyCode::Char('k') => self.kill(self.cursor, self.line_end()),
                KeyCode::Char('w') => {
                    let mut start = self.cursor;
                    while start > 0
                        && self.text[..start]
                            .chars()
                            .next_back()
                            .unwrap()
                            .is_whitespace()
                    {
                        start -= self.text[..start].chars().next_back().unwrap().len_utf8();
                    }
                    while start > 0
                        && !self.text[..start]
                            .chars()
                            .next_back()
                            .unwrap()
                            .is_whitespace()
                    {
                        start -= self.text[..start].chars().next_back().unwrap().len_utf8();
                    }
                    self.kill(start, self.cursor);
                }
                KeyCode::Char('y') => self.insert(&self.saved.clone()),
                KeyCode::Char('j') => self.insert("\n"),
                _ => {}
            }
            return;
        }
        match key.code {
            KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::ALT) => {
                self.insert(&c.to_string())
            }
            KeyCode::Backspace => self.kill_without_yank(self.previous(), self.cursor),
            KeyCode::Delete => self.kill_without_yank(self.cursor, self.next()),
            KeyCode::Left => self.cursor = self.previous(),
            KeyCode::Right => self.cursor = self.next(),
            KeyCode::Home => self.cursor = self.line_start(),
            KeyCode::End => self.cursor = self.line_end(),
            KeyCode::Up | KeyCode::Down => {
                let column = self.text[self.line_start()..self.cursor].chars().count();
                let start = if key.code == KeyCode::Up {
                    let end = self.line_start().saturating_sub(1);
                    self.text[..end].rfind('\n').map_or(0, |i| i + 1)
                } else {
                    let end = self.line_end();
                    if end == self.text.len() {
                        return;
                    }
                    end + 1
                };
                let line = &self.text[start..];
                let count = line
                    .split('\n')
                    .next()
                    .unwrap_or("")
                    .chars()
                    .take(column)
                    .map(char::len_utf8)
                    .sum::<usize>();
                self.cursor = start + count;
            }
            _ => {}
        }
    }
    fn kill_without_yank(&mut self, start: usize, end: usize) {
        self.text.drain(start..end);
        self.cursor = start;
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    fn key(c: char) -> KeyEvent {
        KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL)
    }
    #[test]
    fn readline_unicode_kill_yank_and_multiline_keep_boundaries() {
        let mut editor = Editor::default();
        editor.insert("ação 🚀");
        editor.edit(key('w'));
        assert_eq!(editor.text, "ação ");
        editor.edit(key('y'));
        assert_eq!(editor.text, "ação 🚀");
        editor.edit(key('j'));
        editor.insert("segunda linha");
        editor.edit(key('a'));
        editor.edit(key('k'));
        assert_eq!(editor.text, "ação 🚀\n");
        editor.edit(key('y'));
        assert_eq!(editor.text, "ação 🚀\nsegunda linha");
        editor.clear_recoverable();
        editor.edit(key('y'));
        assert_eq!(editor.text, "ação 🚀\nsegunda linha");
    }
    #[test]
    fn history_restores_unsent_draft_and_does_not_persist() {
        let mut editor = Editor::default();
        editor.insert("rascunho");
        let history = vec!["um".into(), "dois".into()];
        editor.history(&history, true);
        assert_eq!(editor.text, "dois");
        editor.history(&history, true);
        assert_eq!(editor.text, "um");
        editor.history(&history, false);
        editor.history(&history, false);
        assert_eq!(editor.text, "rascunho");
    }
}