use std::io;
use std::path::Path;
use std::rc::Rc;
use crossterm::event::KeyEvent;
use rizz::RizzError;
use rizz::runtime::Value;
use crate::{
action::Action,
buffer::{Buffer, BufferKind},
keymap::KeymapRegistry,
lisp::{EditorGuard, LispRuntime, init_script_path},
mode::EditingMode,
position::Position,
render::{CursorStyle, Renderer, StateSnapshot},
render_ratatui::RatatuiRenderer,
slots::SlotRegistry,
styling::ThemeCell,
window::{SplitDir, WindowTree},
};
const STATUS_LINE_ROWS: u16 = 1;
const MINIBUFFER_ROWS: u16 = 1;
pub struct Config {
pub renderer: Box<dyn Renderer>,
}
impl Config {
pub fn new() -> io::Result<Self> {
Ok(Self {
renderer: Box::new(RatatuiRenderer::new()?),
})
}
}
pub struct State {
bufs: Vec<Buffer>,
windows: WindowTree,
focus_minibuffer: bool,
minibuffer: usize,
quit: bool,
keymap: KeymapRegistry,
renderer: Box<dyn Renderer>,
keyevent: Option<KeyEvent>,
lisp: Option<LispRuntime>,
theme: ThemeCell,
slots: SlotRegistry,
}
impl State {
pub fn new() -> io::Result<Self> {
Self::with_config(Config::new()?)
}
pub fn with_config(config: Config) -> io::Result<Self> {
let mut state = Self {
bufs: vec![Buffer::minibuffer(), Buffer::new()],
windows: WindowTree::new(1),
focus_minibuffer: false,
minibuffer: 0,
quit: false,
keymap: KeymapRegistry::new(),
renderer: config.renderer,
keyevent: None,
lisp: Some(LispRuntime::new()),
theme: ThemeCell::default(),
slots: SlotRegistry::new(),
};
state.refresh_viewport();
if let Err(e) = state.eval_lisp_script(include_str!("../default.lisp")) {
eprintln!("default.lisp eval failed: {e}");
}
if let Err(e) = state.eval_lisp_script(include_str!("../default-style.lisp")) {
eprintln!("default-style.lisp eval failed: {e}");
}
if let Some(path) = init_script_path()
&& let Ok(src) = std::fs::read_to_string(&path)
&& let Err(e) = state.eval_lisp_script(&src)
{
eprintln!("init.lisp ({}) eval failed: {e}", path.display());
}
Ok(state)
}
pub fn eval_lisp(&mut self, src: &str) -> Result<Rc<Value>, RizzError> {
let mut lisp = self
.lisp
.take()
.expect("recursive eval_lisp is not supported");
let result = {
let _guard = EditorGuard::new(self);
lisp.eval_str(src)
};
self.lisp = Some(lisp);
result
}
pub fn eval_lisp_value(&mut self, form: Rc<Value>) -> Result<Rc<Value>, RizzError> {
let mut lisp = self
.lisp
.take()
.expect("recursive eval_lisp is not supported");
let result = {
let _guard = EditorGuard::new(self);
lisp.eval_value(form)
};
self.lisp = Some(lisp);
result
}
pub fn eval_lisp_script(&mut self, src: &str) -> Result<(), RizzError> {
let mut lisp = self
.lisp
.take()
.expect("recursive eval_lisp is not supported");
let result = {
let _guard = EditorGuard::new(self);
lisp.eval_script(src)
};
self.lisp = Some(lisp);
result
}
pub(crate) fn focused_buf(&self) -> &Buffer {
let i = self.focused_bufno();
&self.bufs[i]
}
pub(crate) fn theme(&self) -> &ThemeCell {
&self.theme
}
pub(crate) fn slots_mut(&mut self) -> &mut SlotRegistry {
&mut self.slots
}
pub(crate) fn set_minibuffer_message(&mut self, msg: &str) {
let b = &mut self.bufs[self.minibuffer];
b.clear();
for c in msg.chars() {
b.insert_char(c);
}
}
pub(crate) fn take_minibuffer_command(&mut self) -> String {
let cmd = self.bufs[self.minibuffer].text();
self.exit_minibuffer();
cmd
}
fn focused_bufno(&self) -> usize {
if self.focus_minibuffer {
self.minibuffer
} else {
self.windows.focused_bufno()
}
}
fn refresh_viewport(&mut self) {
let Ok((cols, rows)) = crossterm::terminal::size() else {
return;
};
let editor_h = rows.saturating_sub(STATUS_LINE_ROWS + MINIBUFFER_ROWS);
let editor_area = ratatui::layout::Rect::new(0, 0, cols, editor_h);
for leaf in self.windows.layout(editor_area) {
if let Some(buf) = self.bufs.get_mut(leaf.bufno) {
buf.viewport = Position::new(leaf.area.width, leaf.area.height);
}
}
self.bufs[self.minibuffer].viewport = Position::new(cols, MINIBUFFER_ROWS);
}
pub fn quit_requested(&self) -> bool {
self.quit
}
pub fn handle_key_event(&mut self, event: KeyEvent) -> io::Result<()> {
self.keyevent = Some(event);
let mode = self.bufs[self.focused_bufno()].mode();
if let Some(action) = self.keymap.resolve(mode, event.into()) {
self.apply(&action)?;
}
self.refresh_viewport();
let focused = self.focused_bufno();
self.bufs[focused].clamp_cursor();
self.render()
}
pub fn apply(&mut self, actions: &[Rc<Action>]) -> io::Result<()> {
for action in actions {
match action.as_ref() {
Action::Noop => {}
Action::Quit => self.quit = true,
Action::SetMode(m) => self.set_mode(*m),
Action::InsertChar(c) => {
let f = self.focused_bufno();
self.bufs[f].insert_char(*c);
}
Action::InsertNewline => {
let f = self.focused_bufno();
self.bufs[f].insert_char('\n');
}
Action::DeleteChar => {
let f = self.focused_bufno();
self.bufs[f].delete_char();
}
Action::MoveCursor(m) => {
let f = self.focused_bufno();
self.bufs[f].move_cursor(*m);
}
Action::CommandCancel => self.exit_minibuffer(),
Action::BufCreate { path, set_active } => {
self.create_buf(*set_active, path.clone())?;
}
Action::BufDelete => {
let editor = self.windows.focused_bufno();
self.delete_buf(editor);
}
Action::BufNext => self.next_buffer(),
Action::BufPrev => self.previous_buffer(),
Action::BufEdit(path) => {
self.edit_buf(path.clone())?;
}
Action::BufWrite(path) => self.write_buf(path.clone())?,
Action::WindowSplit(dir) => self.window_split(*dir),
Action::WindowClose => self.window_close(),
Action::WindowFocusNext => self.windows.focus_next(),
Action::WindowFocus(d) => self.windows.focus_dir(*d),
Action::KeymapSet { mode, lhs, rhs } => {
self.keymap.set(*mode, lhs, rhs.clone());
}
Action::KeymapRemove { mode, lhs } => {
self.keymap.remove(*mode, lhs);
}
Action::EvalLisp(form) => {
if let Err(e) = self.eval_lisp_value(form.clone()) {
self.set_minibuffer_message(&e.to_string());
}
}
}
}
Ok(())
}
fn set_mode(&mut self, mode: EditingMode) {
if mode == EditingMode::Command {
self.bufs[self.minibuffer].clear();
self.bufs[self.minibuffer].set_mode(EditingMode::Command);
self.focus_minibuffer = true;
} else {
let f = self.focused_bufno();
self.bufs[f].set_mode(mode);
}
}
fn exit_minibuffer(&mut self) {
self.bufs[self.minibuffer].clear();
self.bufs[self.minibuffer].set_mode(EditingMode::Command);
self.focus_minibuffer = false;
let editor = self.windows.focused_bufno();
self.bufs[editor].set_mode(EditingMode::Normal);
}
fn create_buf(&mut self, set_active: bool, path: Option<Rc<Path>>) -> io::Result<usize> {
let buf = match path {
Some(ref p) => self
.bufs
.iter()
.find(|b| b.fs_path == path)
.cloned()
.unwrap_or_else(|| Buffer::with_path(p.clone())),
None => Buffer::new(),
};
self.bufs.push(buf);
let bufno = self.bufs.len() - 1;
if set_active {
self.windows.set_focused_bufno(bufno);
}
Ok(bufno)
}
fn edit_buf(&mut self, path: Rc<Path>) -> io::Result<usize> {
let idx = self
.bufs
.iter()
.position(|b| b.fs_path.as_ref() == Some(&path));
match idx {
Some(idx) => {
self.windows.set_focused_bufno(idx);
Ok(idx)
}
None => {
self.bufs.push(Buffer::with_path(path));
let idx = self.bufs.len() - 1;
self.windows.set_focused_bufno(idx);
Ok(idx)
}
}
}
fn write_buf(&mut self, path: Option<Rc<Path>>) -> io::Result<()> {
let editor = self.windows.focused_bufno();
self.bufs[editor].write(path)
}
fn delete_buf(&mut self, bufno: usize) {
if bufno >= self.bufs.len() || self.bufs[bufno].kind() == BufferKind::Minibuffer {
return;
}
if self.file_buf_count() == 1 {
self.bufs[bufno] = Buffer::new();
self.windows.for_each_leaf_mut(|b| *b = bufno);
return;
}
self.bufs.remove(bufno);
if self.minibuffer > bufno {
self.minibuffer -= 1;
}
let first = self.first_file_buf();
self.windows.for_each_leaf_mut(|b| {
if *b == bufno {
*b = first;
} else if *b > bufno {
*b -= 1;
}
});
}
fn window_split(&mut self, dir: SplitDir) {
self.bufs.push(Buffer::new());
let new_bufno = self.bufs.len() - 1;
self.windows.split(dir, new_bufno);
}
fn window_close(&mut self) {
self.windows.close_focused();
}
fn file_buf_count(&self) -> usize {
self.bufs
.iter()
.filter(|b| b.kind() != BufferKind::Minibuffer)
.count()
}
fn first_file_buf(&self) -> usize {
self.bufs
.iter()
.position(|b| b.kind() != BufferKind::Minibuffer)
.expect("at least one file buffer always exists")
}
fn previous_buffer(&mut self) {
let n = self.bufs.len();
let mut i = self.windows.focused_bufno();
for _ in 0..n {
i = if i == 0 { n - 1 } else { i - 1 };
if self.bufs[i].kind() != BufferKind::Minibuffer {
self.windows.set_focused_bufno(i);
return;
}
}
}
fn next_buffer(&mut self) {
let n = self.bufs.len();
let mut i = self.windows.focused_bufno();
for _ in 0..n {
i = if i + 1 >= n { 0 } else { i + 1 };
if self.bufs[i].kind() != BufferKind::Minibuffer {
self.windows.set_focused_bufno(i);
return;
}
}
}
pub fn render(&mut self) -> io::Result<()> {
let focused = self.focused_bufno();
let (frame, error_msg) = self.precompute_frame();
let snap = StateSnapshot {
bufs: &self.bufs,
windows: &self.windows,
minibuffer: &self.bufs[self.minibuffer],
focus_minibuffer: self.focus_minibuffer,
bufno: self.windows.focused_bufno(),
keyevent: self.keyevent.map(|e| e.into()),
cursor_style: match self.bufs[focused].mode() {
EditingMode::Insert | EditingMode::Command => CursorStyle::Bar,
_ => CursorStyle::Block,
},
};
let result = self.renderer.render(snap, &frame);
if let Some(msg) = error_msg {
self.set_minibuffer_message(&msg);
}
result
}
pub(crate) fn precompute_frame(&mut self) -> (crate::render::RenderedFrame, Option<String>) {
use crate::render::{RenderedBottom, RenderedBuffer, RenderedFrame};
use crate::slots::{
SegmentSide, produce_bottom, produce_decorator, produce_gutter, produce_status_segment,
};
let lisp = self.lisp.take().expect("recursive render is not supported");
let _editor_guard = crate::lisp::EditorGuard::new(self);
let _phase_guard = crate::lisp::RenderPhaseGuard::enter();
let theme = self.theme.borrow().clone();
let env = lisp.env().clone();
let mut error_chunks: Vec<String> = Vec::new();
let record = |chunks: &mut Vec<String>, slot_name: &str, err: rizz::RizzError| {
if chunks.len() < 3 {
chunks.push(format!("[{slot_name}] {err}"));
}
};
let snap = StateSnapshot {
bufs: &self.bufs,
windows: &self.windows,
minibuffer: &self.bufs[self.minibuffer],
focus_minibuffer: self.focus_minibuffer,
bufno: self.windows.focused_bufno(),
keyevent: self.keyevent.map(|e| e.into()),
cursor_style: CursorStyle::Block, };
let mut status_left = Vec::new();
for s in self.slots.status_segments(SegmentSide::Left) {
match produce_status_segment(s, &snap, &theme, &env) {
Ok(spans) => status_left.extend(spans),
Err(e) => record(&mut error_chunks, &s.name, e),
}
}
let mut status_right = Vec::new();
for s in self.slots.status_segments(SegmentSide::Right) {
match produce_status_segment(s, &snap, &theme, &env) {
Ok(spans) => status_right.extend(spans),
Err(e) => record(&mut error_chunks, &s.name, e),
}
}
let mut bottom_extra = Vec::new();
for s in self.slots.bottom() {
match produce_bottom(s, &snap, &theme, &env) {
Ok(lines) => bottom_extra.push(RenderedBottom { lines }),
Err(e) => record(&mut error_chunks, &s.name, e),
}
}
let mut per_buf = Vec::with_capacity(self.bufs.len());
for (i, buf) in self.bufs.iter().enumerate() {
if i == self.minibuffer {
per_buf.push(RenderedBuffer::default());
continue;
}
let mut rb = RenderedBuffer::default();
for s in self.slots.gutters() {
match produce_gutter(s, buf, &theme, &env) {
Ok(g) => rb.gutters.push(g),
Err(e) => record(&mut error_chunks, &s.name, e),
}
}
for s in self.slots.decorators() {
match produce_decorator(s, buf, &theme, &env) {
Ok(d) => rb.decorators.push(d),
Err(e) => record(&mut error_chunks, &s.name, e),
}
}
per_buf.push(rb);
}
drop(_phase_guard);
drop(_editor_guard);
self.lisp = Some(lisp);
let error_msg = if error_chunks.is_empty() {
None
} else {
Some(error_chunks.join("; "))
};
(
RenderedFrame {
status_left,
status_right,
bottom_extra,
per_buf,
},
error_msg,
)
}
}
#[cfg(test)]
pub(crate) mod test_support {
use super::*;
pub struct NullRenderer;
impl Renderer for NullRenderer {
fn render(
&mut self,
_snap: StateSnapshot<'_>,
_frame: &crate::render::RenderedFrame,
) -> io::Result<()> {
Ok(())
}
}
pub fn test_state() -> State {
State::with_config(Config {
renderer: Box::new(NullRenderer),
})
.unwrap()
}
}
#[cfg(test)]
mod tests {
use super::test_support::test_state;
use super::*;
#[test]
fn render_does_not_panic_on_empty_buffer() {
let mut s = test_state();
s.render().unwrap();
}
#[test]
fn split_then_close_returns_to_single_window() {
let mut s = test_state();
s.apply(&[Rc::new(Action::WindowSplit(SplitDir::Horizontal))])
.unwrap();
s.apply(&[Rc::new(Action::WindowClose)]).unwrap();
s.render().unwrap();
}
#[test]
fn default_precompute_produces_expected_slots() {
let mut s = test_state();
let (frame, err) = s.precompute_frame();
assert!(err.is_none(), "no slot errors expected: {err:?}");
assert!(!frame.status_left.is_empty(), "expected left segments");
assert!(!frame.status_right.is_empty(), "expected right segments");
let bufno = s.windows.focused_bufno();
let bf = &frame.per_buf[bufno];
assert!(!bf.gutters.is_empty());
assert!(bf.decorators.len() >= 3);
assert!(!frame.bottom_extra.is_empty());
}
}