Skip to content

Commit

Permalink
feat: Bookmark and Clipboard commands, deep copying of nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
justinpombrio committed Apr 1, 2024
1 parent 39c9576 commit 2f2e102
Show file tree
Hide file tree
Showing 4 changed files with 299 additions and 85 deletions.
213 changes: 152 additions & 61 deletions src/engine/doc.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use super::doc_command::{
DocCommand, EdCommand, NavCommand, TextEdCommand, TextNavCommand, TreeEdCommand, TreeNavCommand,
BookmarkCommand, ClipboardCommand, DocCommand, EdCommand, NavCommand, TextEdCommand,
TextNavCommand, TreeEdCommand, TreeNavCommand,
};
use crate::language::Storage;
use crate::tree::{Bookmark, Location, Mode, Node};
use crate::util::{bug_assert, SynlessBug};
use std::collections::HashMap;
use std::mem;

/// A set of changes that can be undone/redone all at once.
Expand Down Expand Up @@ -32,17 +34,20 @@ pub enum DocError {
BookmarkNotFound,
#[error("Cannot delete character here")]
CannotDeleteChar,
#[error("No node there to delete")]
CannotDeleteNode,
#[error("Cannot insert that node here")]
CannotInsertNode,
#[error("Cannot place that node here")]
CannotPlaceNode,
#[error("No node to act on here")]
NoNodeHere,
#[error("Clipboard is empty")]
EmptyClipboard,
}

pub struct Doc {
cursor: Location,
recent: Option<UndoGroup>,
undo_stack: Vec<UndoGroup>,
redo_stack: Vec<UndoGroup>,
bookmarks: HashMap<char, Bookmark>,
}

impl Doc {
Expand All @@ -52,45 +57,41 @@ impl Doc {
recent: None,
undo_stack: Vec::new(),
redo_stack: Vec::new(),
bookmarks: HashMap::new(),
}
}

pub fn mode(&self) -> Mode {
self.cursor.mode()
}

/// Get a Bookmark pointing to the current cursor.
pub fn bookmark(&self) -> Bookmark {
self.cursor.bookmark()
}

/// Move the cursor to the bookmark's location, if it's in this document.
pub fn goto_bookmark(&mut self, s: &Storage, bookmark: Bookmark) -> Result<(), DocError> {
if let Some(new_loc) = self.cursor.validate_bookmark(s, bookmark) {
self.cursor = new_loc;
Ok(())
} else {
Err(DocError::BookmarkNotFound)
}
}

/// 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, s: &mut Storage, cmd: DocCommand) -> Result<(), DocError> {
match cmd {
DocCommand::Ed(cmd) => {
self.redo_stack.clear();
let restore_loc = self.cursor;
let undos = execute_ed(s, cmd, &mut self.cursor)?;
if let Some(recent) = &mut self.recent {
recent.commands.extend(undos);
} else {
self.recent = Some(UndoGroup::new(restore_loc, undos));
}
Ok(())
pub fn execute(
&mut self,
s: &mut Storage,
cmd: DocCommand,
clipboard: &mut Vec<Node>,
) -> Result<(), DocError> {
let restore_loc = self.cursor;
let undos = match cmd {
DocCommand::Ed(cmd) => execute_ed(s, cmd, &mut self.cursor)?,
DocCommand::Clipboard(cmd) => execute_clipboard(s, cmd, &mut self.cursor, clipboard)?,
DocCommand::Nav(cmd) => {
execute_nav(s, cmd, &mut self.cursor, &mut self.bookmarks, clipboard)?;
Vec::new()
}
DocCommand::Nav(cmd) => execute_nav(s, cmd, &mut self.cursor),
};
if undos.is_empty() {
return Ok(());
}
self.clear_redos(s);
if let Some(recent) = &mut self.recent {
recent.commands.extend(undos);
} else {
self.recent = Some(UndoGroup::new(restore_loc, undos));
}
Ok(())
}

/// Groups together all editing commands that have been `.execute()`ed since the last call to
Expand All @@ -109,28 +110,28 @@ impl Doc {
pub fn undo(&mut self, s: &mut Storage) -> Result<(), DocError> {
self.end_undo_group();

if let Some(undo_group) = self.undo_stack.pop() {
let redo_group = undo_group.execute(s, &mut self.cursor);
self.redo_stack.push(redo_group);
Ok(())
} else {
Err(DocError::NothingToUndo)
}
let undo_group = self.undo_stack.pop().ok_or(DocError::NothingToUndo)?;
let redo_group = undo_group.execute(s, &mut self.cursor);
self.redo_stack.push(redo_group);
Ok(())
}

/// Redoes the last undo group on the redo stack and moves it to the undo stack.
/// Returns DocError::NothingToRedo if the redo stack is empty.
pub fn redo(&mut self, s: &mut Storage) -> Result<(), DocError> {
if let Some(redo_group) = self.redo_stack.pop() {
bug_assert!(
self.recent.is_none(),
"redo: recent edits should have cleared the redo stack"
);
let undo_group = redo_group.execute(s, &mut self.cursor);
self.undo_stack.push(undo_group);
Ok(())
} else {
Err(DocError::NothingToRedo)
let redo_group = self.redo_stack.pop().ok_or(DocError::NothingToRedo)?;
bug_assert!(
self.recent.is_none(),
"redo: recent edits should have cleared the redo stack"
);
let undo_group = redo_group.execute(s, &mut self.cursor);
self.undo_stack.push(undo_group);
Ok(())
}

fn clear_redos(&mut self, s: &mut Storage) {
for group in self.redo_stack.drain(..) {
group.delete_trees(s);
}
}
}
Expand Down Expand Up @@ -158,6 +159,12 @@ impl UndoGroup {
jump_to(s, cursor, self.restore_loc);
UndoGroup::new(redo_restore_loc.bug(), redos)
}

fn delete_trees(self, s: &mut Storage) {
for (_loc, cmd) in self.commands {
cmd.delete_trees(s);
}
}
}

fn jump_to(s: &Storage, cursor: &mut Location, loc: Location) {
Expand All @@ -179,10 +186,17 @@ fn execute_ed(
}
}

fn execute_nav(s: &Storage, cmd: NavCommand, cursor: &mut Location) -> Result<(), DocError> {
fn execute_nav(
s: &Storage,
cmd: NavCommand,
cursor: &mut Location,
bookmarks: &mut HashMap<char, Bookmark>,
clipboard: &mut Vec<Node>,

Check failure on line 194 in src/engine/doc.rs

View workflow job for this annotation

GitHub Actions / build

writing `&mut Vec` instead of `&mut [_]` involves a new object where a slice will do
) -> Result<(), DocError> {
match cmd {
NavCommand::Tree(cmd) => execute_tree_nav(s, cmd, cursor),
NavCommand::Text(cmd) => execute_text_nav(s, cmd, cursor),
NavCommand::Bookmark(cmd) => execute_bookmark(s, cmd, cursor, bookmarks),
}
}

Expand All @@ -203,21 +217,27 @@ fn execute_tree_ed(
Ok(Some(detached_node)) => {
Ok(vec![(cursor.prev(s).bug(), Insert(detached_node).into())])
}
Err(()) => Err(DocError::CannotInsertNode),
Err(()) => Err(DocError::CannotPlaceNode),
},
Backspace => {
if let Some(old_node) = cursor.delete_neighbor(s, true) {
Ok(vec![(*cursor, Insert(old_node).into())])
Replace(new_node) => {
let old_node = cursor.left_node(s).ok_or(DocError::NoNodeHere)?;
if old_node.swap(s, new_node) {
Ok(vec![(*cursor, Replace(old_node).into())])
} else {
Err(DocError::CannotDeleteNode)
Err(DocError::CannotPlaceNode)
}
}
Backspace => {
let old_node = cursor
.delete_neighbor(s, true)
.ok_or(DocError::NoNodeHere)?;
Ok(vec![(*cursor, Insert(old_node).into())])
}
Delete => {
if let Some(old_node) = cursor.delete_neighbor(s, false) {
Ok(vec![(*cursor, Insert(old_node).into())])
} else {
Err(DocError::CannotDeleteNode)
}
let old_node = cursor
.delete_neighbor(s, false)
.ok_or(DocError::NoNodeHere)?;
Ok(vec![(*cursor, Insert(old_node).into())])
}
}
}
Expand Down Expand Up @@ -257,6 +277,52 @@ fn execute_text_ed(
}
}

fn execute_clipboard(
s: &mut Storage,
cmd: ClipboardCommand,
cursor: &mut Location,
clipboard: &mut Vec<Node>,
) -> Result<Vec<(Location, EdCommand)>, DocError> {
use ClipboardCommand::*;

match cmd {
Copy => {
let node = cursor.left_node(s).ok_or(DocError::NoNodeHere)?;
clipboard.push(node.deep_copy(s));
Ok(Vec::new())
}
Paste => {
let node = clipboard.pop().ok_or(DocError::EmptyClipboard)?;
let result = execute_tree_ed(s, TreeEdCommand::Insert(node), cursor);
if result.is_err() {
clipboard.push(node);
}
result
}
PasteSwap => {
let clip_node = clipboard.pop().ok_or(DocError::EmptyClipboard)?;
let doc_node = cursor.left_node(s).ok_or(DocError::NoNodeHere)?;
if doc_node.swap(s, clip_node) {
clipboard.push(doc_node.deep_copy(s));
Ok(vec![(*cursor, TreeEdCommand::Replace(doc_node).into())])
} else {
clipboard.push(clip_node);
Err(DocError::CannotPlaceNode)
}
}
Dup => {
let clip_node = clipboard.last().ok_or(DocError::EmptyClipboard)?;
clipboard.push(clip_node.deep_copy(s));
Ok(Vec::new())
}
Pop => {
let clip_node = clipboard.pop().ok_or(DocError::EmptyClipboard)?;
clip_node.delete_root(s);
Ok(Vec::new())
}
}
}

fn execute_tree_nav(
s: &Storage,
cmd: TreeNavCommand,
Expand Down Expand Up @@ -321,3 +387,28 @@ fn execute_text_nav(
}
Ok(())
}

fn execute_bookmark(
s: &Storage,
cmd: BookmarkCommand,
cursor: &mut Location,
bookmarks: &mut HashMap<char, Bookmark>,
) -> Result<(), DocError> {
match cmd {
BookmarkCommand::Save(letter) => {
bookmarks.insert(letter, cursor.bookmark());
Ok(())
}
BookmarkCommand::Goto(letter) => {
if let Some(loc) = bookmarks
.get(&letter)
.and_then(|bookmark| cursor.validate_bookmark(s, *bookmark))
{
*cursor = loc;
Ok(())
} else {
Err(DocError::BookmarkNotFound)
}
}
}
}
Loading

0 comments on commit 2f2e102

Please sign in to comment.