argv-todo 0.1.0

A Vim-style terminal todo manager with nested tasks, priorities, search, and local SQLite storage
use super::{EditAction, EditTarget, Editor, Motion, VimAction, VimMode};
use unicode_segmentation::UnicodeSegmentation;

#[derive(Clone)]
pub(super) struct Snapshot {
    pub(super) text: String,
    pub(super) cursor: usize,
}

#[derive(Clone)]
pub(super) enum RecordedAction {
    Key(VimAction),
    Paste(String),
}

pub(super) struct Change {
    pub(super) before: Snapshot,
    pub(super) actions: Vec<RecordedAction>,
}

impl Editor {
    pub(crate) fn prepare_submit(&mut self) {
        self.finish_change();
    }

    fn snapshot(&self) -> Snapshot {
        Snapshot {
            text: self.text.clone(),
            cursor: self.cursor,
        }
    }

    pub(crate) fn apply(&mut self, action: VimAction) {
        match action {
            VimAction::Input(EditAction::Normal) => {
                self.finish_change();
                if self.mode.typing() {
                    self.cursor = self.previous_at(self.cursor);
                }
                self.mode = VimMode::Normal;
                self.anchor = None;
                self.replace_history.clear();
                self.normalize_cursor();
            }
            VimAction::Input(EditAction::Undo(count)) => self.restore_history(false, count),
            VimAction::Input(EditAction::Redo(count)) => self.restore_history(true, count),
            VimAction::Input(EditAction::Repeat(count)) => self.repeat_change(count),
            _ => self.record(RecordedAction::Key(action)),
        }
    }

    pub(crate) fn insert(&mut self, text: &str) {
        if text.is_empty() {
            return;
        }
        self.record(RecordedAction::Paste(text.to_owned()));
    }

    fn record(&mut self, action: RecordedAction) {
        let before = self.snapshot();
        let mode = self.mode;
        let repeat_action = self.repeatable_action(&action);
        self.replay_action(&action);
        let changed = before.text != self.text;
        if let Some(change) = &mut self.change {
            change.actions.push(action);
        } else if self.mode.typing() && (changed || self.mode != mode) {
            self.change = Some(Change {
                before,
                actions: vec![repeat_action],
            });
        } else if changed {
            self.undo.push(before);
            self.redo.clear();
            self.last_change = vec![repeat_action];
        }
    }

    fn repeatable_action(&self, action: &RecordedAction) -> RecordedAction {
        let Some(range) = self.selection() else {
            return action.clone();
        };
        let count = self.text[range].graphemes(true).count();
        match action {
            RecordedAction::Key(VimAction::Input(EditAction::Operate(
                operator,
                EditTarget::Selection,
                _,
            ))) => {
                let target = if self.mode == VimMode::VisualLine {
                    EditTarget::Line
                } else {
                    EditTarget::Motion(Motion::Right)
                };
                RecordedAction::Key(VimAction::Input(EditAction::Operate(
                    *operator, target, count,
                )))
            }
            RecordedAction::Key(VimAction::Input(EditAction::Replace(character, _))) => {
                RecordedAction::Key(VimAction::Input(EditAction::Replace(*character, count)))
            }
            _ => action.clone(),
        }
    }

    fn replay_action(&mut self, action: &RecordedAction) {
        match action {
            RecordedAction::Key(action) => self.apply_inner(*action),
            RecordedAction::Paste(text) => self.paste_inner(text),
        }
        self.normalize_cursor();
    }

    fn finish_change(&mut self) {
        let Some(mut change) = self.change.take() else {
            return;
        };
        if self.insert_count > 1 {
            let typing = if matches!(
                change.actions.first(),
                Some(RecordedAction::Key(VimAction::Input(EditAction::Insert(
                    ..
                ))))
            ) {
                change.actions[1..].to_vec()
            } else {
                change.actions.clone()
            };
            for _ in 1..self.insert_count {
                for action in &typing {
                    self.replay_action(action);
                }
                change.actions.extend_from_slice(&typing);
            }
        }
        self.insert_count = 1;
        if self.text != change.before.text {
            self.undo.push(change.before);
            self.redo.clear();
            change
                .actions
                .push(RecordedAction::Key(VimAction::Input(EditAction::Normal)));
            self.last_change = change.actions;
        }
    }

    fn restore_history(&mut self, redo: bool, count: usize) {
        self.finish_change();
        self.mode = VimMode::Normal;
        self.anchor = None;
        for _ in 0..count {
            let snapshot = if redo {
                self.redo.pop()
            } else {
                self.undo.pop()
            };
            let Some(snapshot) = snapshot else {
                break;
            };
            let current = self.snapshot();
            if redo {
                self.undo.push(current);
            } else {
                self.redo.push(current);
            }
            self.text = snapshot.text;
            self.cursor = snapshot.cursor;
            self.normalize_cursor();
        }
    }

    fn repeat_change(&mut self, count: usize) {
        let actions = self.last_change.clone();
        if actions.is_empty() {
            return;
        }
        let before = self.snapshot();
        for _ in 0..count {
            for action in &actions {
                self.replay_action(action);
            }
        }
        self.insert_count = 1;
        if self.text != before.text {
            self.undo.push(before);
            self.redo.clear();
        }
    }
}