refactor: migrate monolithic crate to Cargo Workspace with Clean Architecture
Transform the single binary crate into a 9-crate workspace monorepo: - Root Cargo.toml as [workspace] manager with resolver = "2" - zesdex-entities: Domain entity types (session, settings, store, message, etc.) - zesdex-utils: Pure utility functions (error, logger, pagination, slug, clipboard) - zesdex-dto: Data Transfer Objects for LLM provider API communication - zesdex-ipc: Unix-socket IPC layer (client/server/framing/protocol) - zesdex-iam: Identity & Access Management (Clean Architecture: domain/application/infrastructure) - zesdex-cms: Content Management (Clean Architecture: domain/application/infrastructure) - zesdex-middleware: HTTP middleware (Auth, CORS, Rate Limiting) - zesdex-libs: Composition root (AppContext, DB init, JWT, Argon2) - zesdex-backend: Main binary entry point + seed/migrate binaries - DevOps: Dockerfile, docker-compose, Nix (flake/shell/default), CI/CD updates - Remove dead root src/ and src-misc/ directories All crate re-exports maintain backward compatibility with original crate::model::*, crate::dto::*, crate::ipc::* module paths. Feature crates enforce strict layer separation: domain -> application -> infrastructure with generic trait-based dependency injection.
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
//! 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<String>,
|
||||
pub undo_stack: Vec<Vec<String>>,
|
||||
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<Vec<String>>) -> 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_or(0, std::string::String::len),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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: &str) {
|
||||
let editor = &mut state.misc.editor;
|
||||
let Some(ed) = editor.as_mut() else {
|
||||
return;
|
||||
};
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user