magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

#[derive(Clone)]
pub(super) struct PromptSnapshot {
    pub(super) visible: String,
    pub(super) atoms: Vec<PasteAtom>,
    pub(super) next_paste_id: u64,
    pub(super) cursor: usize,
}

#[derive(Clone)]
pub(super) struct SemanticHistory {
    pub(super) snapshots: Vec<PromptSnapshot>,
    pub(super) position: usize,
    pub(super) max_edits: usize,
}

impl SemanticHistory {
    pub(super) fn new(max_edits: usize, snapshot: PromptSnapshot) -> Self {
        Self {
            snapshots: vec![snapshot],
            position: 0,
            max_edits,
        }
    }
    pub(super) fn from_snapshots(max_edits: usize, snapshots: Vec<PromptSnapshot>) -> Self {
        let position = snapshots.len().saturating_sub(1);
        Self {
            snapshots,
            position,
            max_edits,
        }
    }

    pub(super) fn reset(&mut self, snapshot: PromptSnapshot) {
        self.snapshots.clear();
        self.snapshots.push(snapshot);
        self.position = 0;
    }

    pub(super) fn update_current_cursor(&mut self, cursor: usize) {
        if let Some(snapshot) = self.snapshots.get_mut(self.position) {
            snapshot.cursor = cursor;
        }
    }

    pub(super) fn record(&mut self, snapshot: PromptSnapshot) {
        self.snapshots.truncate(self.position.saturating_add(1));
        self.snapshots.push(snapshot);
        self.position = self.snapshots.len().saturating_sub(1);

        let max_states = self.max_edits.saturating_add(1);
        if self.snapshots.len() > max_states {
            let remove = self.snapshots.len() - max_states;
            self.snapshots.drain(..remove);
            self.position = self.position.saturating_sub(remove);
        }
    }

    pub(super) fn undo_target(&mut self) -> Option<PromptSnapshot> {
        if self.position == 0 {
            return None;
        }
        self.position -= 1;
        self.snapshots.get(self.position).cloned()
    }

    pub(super) fn redo_target(&mut self) -> Option<PromptSnapshot> {
        if self.position + 1 >= self.snapshots.len() {
            return None;
        }
        self.position += 1;
        self.snapshots.get(self.position).cloned()
    }
}

impl PromptEditor {
    pub(crate) fn undo(&mut self, _visible_rows: u16, _wrap_width: u16) -> PromptEditResult {
        if !self.semantic_history_active {
            return self.edit_without_atom_tracking(|textarea| textarea.undo());
        }
        let Some(snapshot) = self.semantic_history.undo_target() else {
            return PromptEditResult::Unchanged;
        };
        let before = self.text();
        self.restore_semantic_snapshot(snapshot);
        if before != self.text() {
            self.bump_content_revision();
        }
        self.pending_submission = None;
        self.cursor_visible = true;
        PromptEditResult::Changed
    }

    pub(crate) fn redo(&mut self, _visible_rows: u16, _wrap_width: u16) -> PromptEditResult {
        if !self.semantic_history_active {
            return self.edit_without_atom_tracking(|textarea| textarea.redo());
        }
        let Some(snapshot) = self.semantic_history.redo_target() else {
            return PromptEditResult::Unchanged;
        };
        let before = self.text();
        self.restore_semantic_snapshot(snapshot);
        if before != self.text() {
            self.bump_content_revision();
        }
        self.pending_submission = None;
        self.cursor_visible = true;
        PromptEditResult::Changed
    }

    pub(super) fn semantic_snapshot(&self) -> PromptSnapshot {
        PromptSnapshot {
            visible: self.visible_text(),
            atoms: self.paste_atoms.clone(),
            next_paste_id: self.next_paste_id,
            cursor: self.visible_cursor(),
        }
    }

    pub(super) fn restore_semantic_snapshot(&mut self, snapshot: PromptSnapshot) {
        let cursor = snapshot.cursor.min(snapshot.visible.len());
        self.textarea.borrow_mut().set_lines(
            split_lines(&snapshot.visible),
            cursor_for_byte_offset(&snapshot.visible, cursor),
        );
        self.paste_atoms = snapshot.atoms;
        self.next_paste_id = snapshot.next_paste_id;
        self.refresh_atomic_ranges();
    }

    pub(super) fn activate_semantic_history(&mut self) {
        if !self.semantic_history_active {
            self.semantic_history = self.semantic_history_from_native();
            self.semantic_history_active = true;
        }
    }

    pub(super) fn semantic_history_from_native(&self) -> SemanticHistory {
        let (max_edits, mut probe) = {
            let textarea = self.textarea.borrow();
            (textarea.max_histories(), textarea.clone())
        };
        let mut snapshots = vec![self.semantic_snapshot()];
        while probe.undo() {
            snapshots.push(PromptSnapshot {
                visible: probe.lines().join("\n"),
                atoms: Vec::new(),
                next_paste_id: 1,
                cursor: byte_offset_for_cursor(probe.lines(), probe.cursor()),
            });
        }
        snapshots.reverse();
        SemanticHistory::from_snapshots(max_edits, snapshots)
    }

    pub(super) fn prepare_semantic_edit(&mut self) {
        if self.semantic_history_active {
            self.semantic_history
                .update_current_cursor(self.visible_cursor());
        }
    }

    pub(super) fn finish_content_edit(&mut self) {
        if self.semantic_history_active {
            self.semantic_history.record(self.semantic_snapshot());
        }
        self.bump_content_revision();
        self.pending_submission = None;
        self.cursor_visible = true;
    }
}