//! An open document: the document, its undo history, its cursor and its //! marks. [`Buffer::apply`] is the one place where a document changes. use std::collections::BTreeMap; use super::action::{CursorAfter, DocAction, Message}; use crate::model::{Applied, Document, History, NodeId}; /// An open document, with what goes with it while it's open. #[derive(Debug)] pub struct Buffer { doc: Document, history: History, cursor: NodeId, marks: BTreeMap, /// Where the cursor was before the last jump, for `''`. before_jump: Option, } impl Buffer { /// A buffer for a document, with the cursor on its root. pub fn new(doc: Document) -> Buffer { Buffer { cursor: doc.root(), doc, history: History::default(), marks: BTreeMap::new(), before_jump: None, } } pub fn doc(&self) -> &Document { &self.doc } /// The node the cursor is on (always one that exists). pub fn cursor(&self) -> NodeId { self.cursor } pub fn history(&self) -> &History { &self.history } /// Carries out an action on the document or its cursor, and says how it /// went when there's something to say. pub fn apply(&mut self, action: DocAction) -> Option { match action { DocAction::MoveCursor { to, jump } => self.move_cursor(to, jump), DocAction::Edit { op, cursor, group } => { let (applied, change) = match self.doc.apply(&op) { Ok(done) => done, Err(error) => return Some(Message::problem(error.to_string())), }; match group { Some(group) => self.history.record_in(change, group), None => self.history.record(change), } self.cursor = match (cursor, applied) { (CursorAfter::Added, Applied::Added(id)) | (CursorAfter::To(id), _) => id, _ => self.cursor, }; self.keep_cursor(); match applied { Applied::Removed(count) => { Some(Message::done(format!("deleted {} · u to undo", nodes(count)))) } Applied::Added(_) | Applied::Changed => None, } } DocAction::Undo(times) => self.undo_or_redo(times, true), DocAction::Redo(times) => self.undo_or_redo(times, false), DocAction::SetMark(name) => { self.marks.insert(name, self.cursor); let title = self.doc.node(self.cursor).map(|node| node.title().to_string()).unwrap_or_default(); Some(Message::done(format!("mark {name} is on \"{title}\": '{name} comes back here"))) } DocAction::JumpToMark('\'' | '`') => match self.before_jump { Some(id) if self.doc.contains(id) => self.move_cursor(id, true), _ => Some(Message::info("no jump to go back from yet")), }, DocAction::JumpToMark(name) => match self.marks.get(&name) { Some(&id) if self.doc.contains(id) => self.move_cursor(id, true), Some(_) => Some(Message::problem(format!("the node marked {name} is gone"))), None => Some(Message::info(format!("no mark {name} yet: m{name} sets it"))), }, } } fn move_cursor(&mut self, to: NodeId, jump: bool) -> Option { if !self.doc.contains(to) { return Some(Message::problem("that node is gone")); } if jump && to != self.cursor { self.before_jump = Some(self.cursor); } self.cursor = to; None } /// Undoes (or redoes) up to `times` changes; the cursor goes where the /// last one happened. fn undo_or_redo(&mut self, times: u32, undo: bool) -> Option { let mut landed = None; for _ in 0..times { let change = if undo { self.history.undo(&mut self.doc) } else { self.history.redo(&mut self.doc) }; let Some(change) = change else { break }; landed = Some(change.nodes().find(|id| self.doc.contains(*id))); } match landed { None => Some(Message::info(if undo { "nothing to undo" } else { "nothing to redo" })), Some(target) => { self.cursor = target.unwrap_or(self.cursor); self.keep_cursor(); None } } } /// The cursor's node may be gone after an edit or an undo: then it goes /// to the root. fn keep_cursor(&mut self) { if !self.doc.contains(self.cursor) { self.cursor = self.doc.root(); } } } /// "1 node", "3 nodes". fn nodes(count: usize) -> String { if count == 1 { "1 node".to_string() } else { format!("{count} nodes") } } #[cfg(test)] mod tests { use super::*; use crate::model::test_support::{garden, idea}; use crate::model::{EditOp, Spot}; fn edit(op: EditOp, cursor: CursorAfter) -> DocAction { DocAction::Edit { op: Box::new(op), cursor, group: None } } #[test] fn edits_move_the_cursor_and_undo_brings_it_back() { let (doc, g) = garden(); let mut buffer = Buffer::new(doc); buffer.apply(edit( EditOp::Add { node: idea("new"), spot: Spot::LastChildOf(g.soil) }, CursorAfter::Added, )); let added = buffer.cursor(); assert_eq!(buffer.doc().parent(added), Some(g.soil)); let message = buffer.apply(edit(EditOp::RemoveBranches(vec![g.soil]), CursorAfter::To(g.beds))); assert_eq!(message, Some(Message::done("deleted 2 nodes · u to undo"))); assert_eq!(buffer.cursor(), g.beds); buffer.apply(DocAction::Undo(1)); assert_eq!(buffer.cursor(), g.soil, "back where the change was"); assert_eq!(buffer.apply(DocAction::Undo(5)), None); assert_eq!(buffer.apply(DocAction::Undo(1)), Some(Message::info("nothing to undo"))); } #[test] fn marks_and_jumping_back() { let (doc, g) = garden(); let mut buffer = Buffer::new(doc); buffer.apply(DocAction::MoveCursor { to: g.cedar, jump: false }); buffer.apply(DocAction::SetMark('a')); buffer.apply(DocAction::MoveCursor { to: g.compost, jump: true }); buffer.apply(DocAction::JumpToMark('a')); assert_eq!(buffer.cursor(), g.cedar); buffer.apply(DocAction::JumpToMark('\'')); assert_eq!(buffer.cursor(), g.compost, "'' goes back to before the jump"); assert_eq!( buffer.apply(DocAction::JumpToMark('z')), Some(Message::info("no mark z yet: mz sets it")) ); } #[test] fn the_cursor_never_rests_on_a_deleted_node() { let (doc, g) = garden(); let mut buffer = Buffer::new(doc); buffer.apply(DocAction::MoveCursor { to: g.cedar, jump: false }); buffer.apply(edit(EditOp::RemoveBranches(vec![g.beds]), CursorAfter::Stay)); assert_eq!(buffer.cursor(), buffer.doc().root()); } }