xei-editor 0.2.9

xei (晴) — a modern terminal text editor in Rust with Vim-modal editing, syntax highlighting, file explorer, built-in terminal, and theming
use crate::buffer::BufferSnapshot;

pub struct Xlc {
    pub open: bool,
    pub input: String,
    pub output: Vec<String>,
    pub history: Vec<String>,
    pub history_index: usize,
    pub scroll_offset: usize,
}

impl Default for Xlc {
    fn default() -> Self {
        Self {
            open: false,
            input: String::new(),
            output: vec!["xei Line Command — :help for commands".to_string()],
            history: Vec::new(),
            history_index: 0,
            scroll_offset: 0,
        }
    }
}

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

    pub fn open_panel(&mut self, prompt: Option<&str>) {
        self.open = true;
        self.input.clear();
        self.history_index = self.history.len();
        self.scroll_offset = 0;
        if let Some(p) = prompt {
            self.input = p.to_string();
        }
    }

    pub fn close(&mut self) {
        self.open = false;
        self.input.clear();
    }

    pub fn push_char(&mut self, c: char) {
        self.input.push(c);
    }

    pub fn pop_char(&mut self) {
        self.input.pop();
    }

    pub fn history_up(&mut self) {
        if self.history.is_empty() {
            return;
        }
        if self.history_index > 0 {
            self.history_index -= 1;
            self.input = self.history[self.history_index].clone();
        }
    }

    pub fn history_down(&mut self) {
        if self.history_index < self.history.len() {
            self.history_index += 1;
            if self.history_index < self.history.len() {
                self.input = self.history[self.history_index].clone();
            } else {
                self.input.clear();
            }
        }
    }

    pub fn execute(&mut self) -> XlcCmd {
        let cmd = self.input.trim().to_string();
        if !cmd.is_empty() {
            self.history.push(cmd.clone());
        }
        self.history_index = self.history.len();

        self.output.push(format!("> {}", cmd));

        let result = parse_command(&cmd);
        self.input.clear();
        result
    }

    pub fn add_output(&mut self, msg: &str) {
        self.output.push(msg.to_string());
    }

    pub fn scroll_up(&mut self, amount: usize) {
        self.scroll_offset = self
            .scroll_offset
            .saturating_add(amount)
            .min(self.output.len().saturating_sub(1));
    }

    pub fn scroll_down(&mut self, amount: usize) {
        self.scroll_offset = self.scroll_offset.saturating_sub(amount);
    }

    #[allow(dead_code)]
    pub fn scroll_reset(&mut self) {
        self.scroll_offset = 0;
    }

    pub fn scroll_to_top(&mut self) {
        self.scroll_offset = self.output.len().saturating_sub(1);
    }

    pub fn scroll_to_bottom(&mut self) {
        self.scroll_offset = 0;
    }
}

#[derive(Debug)]
pub enum XlcCmd {
    None,
    Save,
    SaveAs(String),
    Quit,
    ForceQuit,
    Open(String),
    Move(String),
    Rename(String),
    DeleteFile,
    Pwd,
    Ls,
    Help,
    Search(String),
    Theme(String),
}

fn parse_command(input: &str) -> XlcCmd {
    let parts: Vec<&str> = input.splitn(2, ' ').collect();
    let cmd = parts[0];
    let arg = parts.get(1).map(|s| s.trim()).unwrap_or("");

    match cmd {
        "w" | "save" => {
            if arg.is_empty() {
                XlcCmd::Save
            } else {
                XlcCmd::SaveAs(arg.to_string())
            }
        }
        "q" | "quit" => XlcCmd::Quit,
        "q!" | "quit!" => XlcCmd::ForceQuit,
        "wq" | "x" => XlcCmd::Save,
        "e" | "open" if !arg.is_empty() => XlcCmd::Open(arg.to_string()),
        "mv" | "move" if !arg.is_empty() => XlcCmd::Move(arg.to_string()),
        "rename" if !arg.is_empty() => XlcCmd::Rename(arg.to_string()),
        "rm" => XlcCmd::DeleteFile,
        "pwd" => XlcCmd::Pwd,
        "ls" => XlcCmd::Ls,
        "help" | "h" | "?" => XlcCmd::Help,
        "find" | "/" if !arg.is_empty() => XlcCmd::Search(arg.to_string()),
        "theme" => XlcCmd::Theme(arg.to_string()),
        _ => XlcCmd::None,
    }
}

pub struct UndoStack {
    snapshots: Vec<BufferSnapshot>,
    index: usize,
}

impl Default for UndoStack {
    fn default() -> Self {
        Self {
            snapshots: Vec::new(),
            index: 0,
        }
    }
}

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

    pub fn push(&mut self, snapshot: BufferSnapshot) {
        self.snapshots.truncate(self.index);
        self.snapshots.push(snapshot);
        self.index += 1;
    }

    pub fn undo(&mut self) -> Option<&BufferSnapshot> {
        if self.index > 1 {
            self.index -= 1;
            Some(&self.snapshots[self.index - 1])
        } else if self.index == 1 {
            self.index -= 1;
            Some(&self.snapshots[self.index])
        } else {
            None
        }
    }
}