//! Editor mode: a minimal in-TUI line editor for viewing/modifying a file, //! with bounded undo history. use crate::app::state::rest::AppStateRest; use crate::app::state::types::Overlay; /// State for the built-in line editor overlay: buffer contents, cursor /// position, and a bounded undo stack. #[derive(Debug, Clone)] pub struct EditorState { pub path: String, pub content: Vec, pub undo_stack: Vec>, pub cursor_line: usize, pub cursor_col: usize, } impl Default for EditorState { fn default() -> Self { EditorState { path: String::new(), content: vec![String::new()], undo_stack: Vec::new(), cursor_line: 0, cursor_col: 0, } } } impl EditorState { /// Create a fresh editor state for `path`, seeded with existing content /// (or a single empty line for a new file). pub fn open(path: String, existing_content: Option>) -> Self { let content = existing_content.unwrap_or_else(|| vec![String::new()]); EditorState { path, content, ..Default::default() } } /// Insert a new empty line immediately after the cursor line. /// /// Why: snapshots content to the undo stack first, matching every other /// mutating method here. pub fn insert_line_after(&mut self) { self.save_undo(); let pos = (self.cursor_line + 1).min(self.content.len()); self.content.insert(pos, String::new()); } /// Push a snapshot of the current content onto the undo stack, capped at 50 entries. /// /// Why: `remove(0)` on overflow bounds memory use at the cost of O(n) /// shifting; the cap (50) keeps that cost negligible in practice. fn save_undo(&mut self) { self.undo_stack.push(self.content.clone()); if self.undo_stack.len() > 50 { self.undo_stack.remove(0); } } /// Move the cursor down one line, clamping the column to the new line's length. pub fn cursor_down(&mut self) { if self.cursor_line + 1 < self.content.len() { self.cursor_line += 1; } self.cursor_col = self.cursor_col.min( self.content.get(self.cursor_line).map(|l| l.len()).unwrap_or(0), ); } /// Insert a character at the cursor and advance the cursor past it. pub fn insert_char(&mut self, c: char) { self.save_undo(); if let Some(line) = self.content.get_mut(self.cursor_line) { line.insert(self.cursor_col, c); self.cursor_col += 1; } } /// Delete the character before the cursor (backspace). /// /// Flow: if not at column 0, remove the preceding char on this line → /// otherwise (start of line, not the first line) merge this line into /// the previous one, joining at the old line's end. pub fn delete_left(&mut self) { self.save_undo(); if let Some(line) = self.content.get_mut(self.cursor_line) { if self.cursor_col > 0 { self.cursor_col -= 1; line.remove(self.cursor_col); } else if self.cursor_line > 0 { let prev_len = self.content[self.cursor_line - 1].len(); let rest = self.content.remove(self.cursor_line); self.cursor_line -= 1; self.cursor_col = prev_len; self.content[self.cursor_line].push_str(&rest); } } } /// Join all lines with `\n` into the full file contents, for saving. pub fn as_string(&self) -> String { self.content.join("\n") } } /// Feed a chunk of typed text into the active editor, translating newlines /// and tabs into editor operations. /// /// Flow: no-op if no editor is open → for each char: `\n`/`\r` inserts a /// line and moves down, `\t` inserts two spaces, everything else inserts /// the char directly → mark state dirty. pub fn handle_editor_input(state: &mut AppStateRest, text: String) { let editor = &mut state.misc.editor; if editor.is_none() { return; } let ed = editor.as_mut().unwrap(); for c in text.chars() { match c { '\n' | '\r' => { ed.insert_line_after(); ed.cursor_down(); ed.cursor_col = 0; } '\t' => { ed.insert_char(' '); ed.insert_char(' '); } _ => { ed.insert_char(c); } } } state.dirty = true; } /// Close the editor overlay without saving, clearing editor state. pub fn handle_editor_dismiss(state: &mut AppStateRest) { state.misc.editor = None; state.misc.overlay = Overlay::None; state.dirty = true; }