diff --git a/src/engine/doc.rs b/src/engine/doc.rs index 399f679..b7df195 100644 --- a/src/engine/doc.rs +++ b/src/engine/doc.rs @@ -35,10 +35,10 @@ pub enum DocError { #[error("No node there to delete")] CannotDeleteNode, #[error("Cannot insert that node here")] - CannotInsert, + CannotInsertNode, } -struct Doc { +pub struct Doc { cursor: Location, recent: Option, undo_stack: Vec, @@ -64,7 +64,7 @@ impl Doc { self.cursor.bookmark() } - /// Move the cursor to the bookmark's location. + /// Move the cursor to the bookmark's location, if it's in this document. pub fn goto_bookmark(&mut self, bookmark: Bookmark, s: &Storage) -> Result<(), DocError> { if let Some(new_loc) = self.cursor.validate_bookmark(bookmark, s) { self.cursor = new_loc; @@ -74,6 +74,28 @@ impl Doc { } } + /// Executes a single command. Clears the redo stack if it was an editing command (but not if + /// it was a navigation command). + pub fn execute(&mut self, cmd: DocCommand, s: &mut Storage) -> Result<(), DocError> { + match cmd { + DocCommand::Ed(cmd) => { + self.redo_stack.clear(); + let restore_loc = self.cursor; + let undos = execute_ed(cmd, &mut self.cursor, s)?; + if let Some(recent) = &mut self.recent { + recent.commands.extend(undos); + } else { + self.recent = Some(UndoGroup::new(restore_loc, undos)); + } + Ok(()) + } + DocCommand::Nav(cmd) => execute_nav(cmd, &mut self.cursor, s), + } + } + + /// Groups together all editing commands that have been `.execute()`ed since the last call to + /// `.end_undo_group()`. They will be treated as a single unit ("undo group") by calls to + /// `.undo()` and `.redo()`. pub fn end_undo_group(&mut self) { if let Some(recent) = self.recent.take() { self.undo_stack.push(recent); @@ -81,7 +103,7 @@ impl Doc { } /// Undoes the last undo group on the undo stack and moves it to the redo stack. - /// Returns DocError::NothingToUndo if the undo stack is empty. + /// Returns `Err(DocError::NothingToUndo)` if the undo stack is empty. /// If there were recent edits _not_ completed with a call to end_undo_group(), /// the group is automatically ended and then undone. pub fn undo(&mut self, s: &mut Storage) -> Result<(), DocError> { @@ -111,23 +133,6 @@ impl Doc { Err(DocError::NothingToRedo) } } - - pub fn execute(&mut self, cmd: DocCommand, s: &mut Storage) -> Result<(), DocError> { - match cmd { - DocCommand::Ed(cmd) => { - self.redo_stack.clear(); - let restore_loc = self.cursor; - let undos = execute_ed(cmd, &mut self.cursor, s)?; - if let Some(recent) = &mut self.recent { - recent.commands.extend(undos); - } else { - self.recent = Some(UndoGroup::new(restore_loc, undos)); - } - Ok(()) - } - DocCommand::Nav(cmd) => execute_nav(cmd, &mut self.cursor, s), - } - } } impl UndoGroup { @@ -140,17 +145,17 @@ impl UndoGroup { } fn execute(self, cursor: &mut Location, s: &mut Storage) -> UndoGroup { - let undo_restore_loc = self.restore_loc; let mut redo_restore_loc = None; let mut redos = Vec::new(); - for (loc, cmd) in self.commands { + for (loc, cmd) in self.commands.into_iter().rev() { if redo_restore_loc.is_none() { redo_restore_loc = Some(loc); } jump_to(cursor, loc, s); redos.extend(execute_ed(cmd, cursor, s).bug_msg("Failed to undo/redo")); } - jump_to(cursor, undo_restore_loc, s); + + jump_to(cursor, self.restore_loc, s); UndoGroup::new(redo_restore_loc.bug(), redos) } } @@ -188,13 +193,17 @@ fn execute_tree_ed( ) -> Result, DocError> { use TreeEdCommand::*; + if cursor.mode() != Mode::Tree { + return Err(DocError::NotInTreeMode); + } + match cmd { Insert(node) => match cursor.insert(node, s) { Ok(None) => Ok(vec![(*cursor, Backspace.into())]), Ok(Some(detached_node)) => { Ok(vec![(cursor.prev(s).bug(), Insert(detached_node).into())]) } - Err(()) => Err(DocError::CannotDeleteNode), + Err(()) => Err(DocError::CannotInsertNode), }, Backspace => { if let Some(old_node) = cursor.delete_neighbor(true, s) { @@ -218,31 +227,32 @@ fn execute_text_ed( cursor: &mut Location, s: &mut Storage, ) -> Result, DocError> { - let original_loc = *cursor; + use TextEdCommand::{Backspace, Delete, Insert}; + let (node, char_index) = cursor.text_pos_mut().ok_or(DocError::NotInTextMode)?; let text = node.text_mut(s).bug(); match cmd { - TextEdCommand::Insert(ch) => { + Insert(ch) => { text.insert(*char_index, ch); *char_index += 1; - Ok(vec![(*cursor, TextEdCommand::Backspace.into())]) + Ok(vec![(*cursor, Backspace.into())]) } - TextEdCommand::Backspace => { + Backspace => { if *char_index == 0 { return Err(DocError::CannotDeleteChar); } let ch = text.delete(*char_index - 1); *char_index -= 1; - Ok(vec![(*cursor, TextEdCommand::Insert(ch).into())]) + Ok(vec![(*cursor, Insert(ch).into())]) } - TextEdCommand::Delete => { + Delete => { let text_len = text.num_chars(); if *char_index == text_len { return Err(DocError::CannotDeleteChar); } let ch = text.delete(*char_index); - Ok(vec![(*cursor, TextEdCommand::Insert(ch).into())]) + Ok(vec![(*cursor, Insert(ch).into())]) } } } @@ -269,7 +279,9 @@ fn execute_tree_nav( .and_then(|node| Location::after_children(node, s)), InorderNext => cursor.inorder_next(s), InorderPrev => cursor.inorder_prev(s), - EnterText => cursor.enter_text(s), + EnterText => cursor + .left_node(s) + .and_then(|node| Location::end_of_text(node, s)), }; if let Some(new_loc) = new_loc { @@ -301,7 +313,7 @@ fn execute_text_nav( if *char_index >= text.num_chars() { return Err(DocError::CannotMove); } - *char_index -= 1; + *char_index += 1; } Beginning => *char_index = 0, End => *char_index = text.num_chars(), diff --git a/src/tree/location.rs b/src/tree/location.rs index d69ffcd..a18876e 100644 --- a/src/tree/location.rs +++ b/src/tree/location.rs @@ -69,6 +69,21 @@ impl Location { } } + /// If the node is texty, returns the location at the start of its text, otherwise returns `None`. + pub fn start_of_text(node: Node, s: &Storage) -> Option { + if node.is_texty(s) { + Some(Location(LocationInner::InText(node, 0))) + } else { + None + } + } + + /// If the node is texty, returns the location at the end of its text, otherwise returns `None`. + pub fn end_of_text(node: Node, s: &Storage) -> Option { + let text_len = node.text(s)?.num_chars(); + Some(Location(LocationInner::InText(node, text_len))) + } + /************* * Accessors * *************/ @@ -104,10 +119,8 @@ impl Location { use LocationInner::{AfterNode, BeforeNode, BelowNode, InText}; match self.0 { - InText(_, _) => None, AfterNode(node) => Some(Location::before(node, s)), - BeforeNode(_) => None, - BelowNode(_) => None, + InText(_, _) | BeforeNode(_) | BelowNode(_) => None, } } @@ -115,10 +128,9 @@ impl Location { use LocationInner::{AfterNode, BeforeNode, BelowNode, InText}; match self.0 { - InText(_, _) => None, AfterNode(node) => Some(Location::after(node.next_sibling(s)?, s)), BeforeNode(node) => Some(Location::after(node, s)), - BelowNode(_) => None, + InText(_, _) | BelowNode(_) => None, } } @@ -176,19 +188,6 @@ impl Location { } } - /// Returns the location at the end of the texty node that is before the current location. - pub fn enter_text(self, s: &Storage) -> Option { - use LocationInner::{AfterNode, BeforeNode, BelowNode, InText}; - - match self.0 { - AfterNode(node) => { - let text_len = node.text(s)?.num_chars(); - Some(Location(LocationInner::InText(node, text_len))) - } - InText(_, _) | BeforeNode(_) | BelowNode(_) => None, - } - } - /// If the location is in text, returns the location after that text node. pub fn exit_text(self) -> Option { if let LocationInner::InText(node, _) = self.0 { @@ -207,10 +206,8 @@ impl Location { use LocationInner::{AfterNode, BeforeNode, BelowNode, InText}; match self.0 { - InText(_, _) => None, AfterNode(node) => Some(node), - BeforeNode(_) => None, - BelowNode(_) => None, + InText(_, _) | BeforeNode(_) | BelowNode(_) => None, } } @@ -218,10 +215,9 @@ impl Location { use LocationInner::{AfterNode, BeforeNode, BelowNode, InText}; match self.0 { - InText(_, _) => None, AfterNode(node) => node.next_sibling(s), BeforeNode(node) => Some(node), - BelowNode(_) => None, + InText(_, _) | BelowNode(_) => None, } } @@ -230,20 +226,30 @@ impl Location { match self.0 { InText(node, _) => None, - AfterNode(node) => node.parent(s), - BeforeNode(node) => node.parent(s), + BeforeNode(node) | AfterNode(node) => node.parent(s), BelowNode(node) => Some(node), } } pub fn root_node(self, s: &Storage) -> Node { - self.0.node().root(s) + self.0.reference_node().root(s) } /************ * Mutation * ************/ + /// In a listy sequence, inserts `new_node` at this location and returns `Ok(None)`. In a fixed + /// sequence, replaces the node after this location with `new_node` and returns + /// `Ok(Some(old_node))`. Either way, moves `self` to after the new node. + /// + /// If we cannot insert, returns `Err(())` and does not modify `self`. This can happen for any + /// of the following reasons: + /// + /// - This location is in text. + /// - This location is before or after a root node. + /// - This location is after the last node in a fixed sequence. + /// - The new node does not match the required sort. #[allow(clippy::result_unit_err)] pub fn insert(&mut self, new_node: Node, s: &mut Storage) -> Result, ()> { use LocationInner::*; @@ -263,7 +269,7 @@ impl Location { } Arity::Listy(_) => { let success = match self.0 { - InText(_, _) => false, + InText(_, _) => bug!("insert: bug in textiness check"), AfterNode(left_node) => left_node.insert_after(s, new_node), BeforeNode(right_node) => right_node.insert_before(s, new_node), BelowNode(_) => parent.insert_last_child(s, new_node), @@ -278,25 +284,43 @@ impl Location { } } + /// In a listy sequence, delete the node before (after) the cursor. In a fixed sequence, + /// replace the node before (after) the cursor with a hole, and move the cursor before (after) + /// it. #[must_use] - pub fn delete_neighbor(&mut self, on_left: bool, s: &mut Storage) -> Option { + pub fn delete_neighbor(&mut self, delete_before: bool, s: &mut Storage) -> Option { let parent = self.parent_node(s)?; - let node = if on_left { + let node = if delete_before { self.left_node(s)? } else { self.right_node(s)? }; + match parent.arity(s) { Arity::Fixed(_) => { - let hole = Node::new_hole(s, parent.language(s)); + // NOTE: Think about what language the hole should be in once we start supporting + // multi-language docs + let hole = Node::new_hole(s, node.language(s)); if node.swap(s, hole) { + *self = if delete_before { + Location::before(hole, s) + } else { + Location::after(hole, s) + }; Some(node) } else { None } } Arity::Listy(_) => { + let prev_node = node.prev_sibling(s); + let next_node = node.next_sibling(s); if node.detach(s) { + *self = match (prev_node, next_node) { + (Some(prev), _) => Location(LocationInner::AfterNode(prev)), + (None, Some(next)) => Location(LocationInner::BeforeNode(next)), + (None, None) => Location(LocationInner::BelowNode(parent)), + }; Some(node) } else { None @@ -322,7 +346,7 @@ impl Location { /// has since been deleted, or if it is currently located in a /// different tree. pub fn validate_bookmark(self, mark: Bookmark, s: &Storage) -> Option { - let mark_node = mark.0.node(); + let mark_node = mark.0.reference_node(); if mark_node.is_valid(s) && mark_node.root(s) == self.root_node(s) { Some(Location(mark.0.normalize(s))) } else { @@ -346,7 +370,9 @@ impl LocationInner { } } - fn node(self) -> Node { + /// Get the node this location is defined relative to. May be before, after, or above this + /// location! + fn reference_node(self) -> Node { use LocationInner::{AfterNode, BeforeNode, BelowNode, InText}; match self {