Implement Undo and Redo (#354)

* Implement undo and redo

* Create more save points and hook up menu entry

* Fix operation ordering

* Remove debug statement

* Fix folder changed order

* Don't store overlays in the history chain

* Keep selection
This commit is contained in:
TrueDoctor 2021-08-26 12:20:00 +02:00 committed by Keavon Chambers
parent 47fbb8d0fa
commit 0ccb181e2c
6 changed files with 133 additions and 38 deletions

View File

@ -19,6 +19,8 @@ use std::collections::VecDeque;
use super::movement_handler::{MovementMessage, MovementMessageHandler}; use super::movement_handler::{MovementMessage, MovementMessageHandler};
type DocumentSave = (InternalDocument, HashMap<Vec<LayerId>, LayerData>);
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
pub enum FlipAxis { pub enum FlipAxis {
X, X,
@ -42,7 +44,8 @@ pub enum AlignAggregate {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct DocumentMessageHandler { pub struct DocumentMessageHandler {
pub document: InternalDocument, pub document: InternalDocument,
pub document_backup: Option<InternalDocument>, pub document_history: Vec<DocumentSave>,
pub document_redo_history: Vec<DocumentSave>,
pub name: String, pub name: String,
pub layer_data: HashMap<Vec<LayerId>, LayerData>, pub layer_data: HashMap<Vec<LayerId>, LayerData>,
movement_handler: MovementMessageHandler, movement_handler: MovementMessageHandler,
@ -52,7 +55,8 @@ impl Default for DocumentMessageHandler {
fn default() -> Self { fn default() -> Self {
Self { Self {
document: InternalDocument::default(), document: InternalDocument::default(),
document_backup: None, document_history: Vec::new(),
document_redo_history: Vec::new(),
name: String::from("Untitled Document"), name: String::from("Untitled Document"),
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(), layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
movement_handler: MovementMessageHandler::default(), movement_handler: MovementMessageHandler::default(),
@ -90,6 +94,10 @@ pub enum DocumentMessage {
SaveDocument, SaveDocument,
RenderDocument, RenderDocument,
Undo, Undo,
Redo,
DocumentHistoryBackward,
DocumentHistoryForward,
ClearOverlays,
NudgeSelectedLayers(f64, f64), NudgeSelectedLayers(f64, f64),
AlignSelectedLayers(AlignAxis, AlignAggregate), AlignSelectedLayers(AlignAxis, AlignAggregate),
MoveSelectedLayersTo { MoveSelectedLayersTo {
@ -195,7 +203,8 @@ impl DocumentMessageHandler {
pub fn with_name(name: String) -> Self { pub fn with_name(name: String) -> Self {
Self { Self {
document: InternalDocument::default(), document: InternalDocument::default(),
document_backup: None, document_history: Vec::new(),
document_redo_history: Vec::new(),
name, name,
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(), layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
movement_handler: MovementMessageHandler::default(), movement_handler: MovementMessageHandler::default(),
@ -219,18 +228,42 @@ impl DocumentMessageHandler {
} }
pub fn backup(&mut self) { pub fn backup(&mut self) {
self.document_backup = Some(self.document.clone()) self.document_redo_history.clear();
let new_layer_data = self
.layer_data
.iter()
.filter_map(|(key, value)| (!self.document.layer(key).unwrap().overlay).then(|| (key.clone(), *value)))
.collect();
self.document_history.push((self.document.clone_without_overlays(), new_layer_data))
} }
pub fn rollback(&mut self) -> Result<(), EditorError> { pub fn rollback(&mut self) -> Result<(), EditorError> {
self.backup(); self.backup();
self.reset() self.undo()
} }
pub fn reset(&mut self) -> Result<(), EditorError> { pub fn undo(&mut self) -> Result<(), EditorError> {
match self.document_backup.take() { match self.document_history.pop() {
Some(backup) => { Some((document, layer_data)) => {
self.document = backup; let document = std::mem::replace(&mut self.document, document);
let layer_data = std::mem::replace(&mut self.layer_data, layer_data);
self.document_redo_history.push((document, layer_data));
Ok(())
}
None => Err(EditorError::NoTransactionInProgress),
}
}
pub fn redo(&mut self) -> Result<(), EditorError> {
match self.document_redo_history.pop() {
Some((document, layer_data)) => {
let document = std::mem::replace(&mut self.document, document);
let layer_data = std::mem::replace(&mut self.layer_data, layer_data);
let new_layer_data = layer_data
.iter()
.filter_map(|(key, value)| (!self.document.layer(key).unwrap().overlay).then(|| (key.clone(), *value)))
.collect();
self.document_history.push((document.clone_without_overlays(), new_layer_data));
Ok(()) Ok(())
} }
None => Err(EditorError::NoTransactionInProgress), None => Err(EditorError::NoTransactionInProgress),
@ -284,10 +317,10 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]); responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]);
} }
AbortTransaction => { AbortTransaction => {
self.reset().unwrap_or_else(|e| log::warn!("{}", e)); self.undo().unwrap_or_else(|e| log::warn!("{}", e));
responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]); responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]);
} }
CommitTransaction => self.document_backup = None, CommitTransaction => (),
ExportDocument => { ExportDocument => {
let bbox = self.document.visible_layers_bounding_box().unwrap_or([DVec2::ZERO, ipp.viewport_bounds.size()]); let bbox = self.document.visible_layers_bounding_box().unwrap_or([DVec2::ZERO, ipp.viewport_bounds.size()]);
let size = bbox[1] - bbox[0]; let size = bbox[1] - bbox[0];
@ -325,11 +358,13 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
) )
} }
SetBlendModeForSelectedLayers(blend_mode) => { SetBlendModeForSelectedLayers(blend_mode) => {
self.backup();
for path in self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) { for path in self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into()); responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into());
} }
} }
SetOpacityForSelectedLayers(opacity) => { SetOpacityForSelectedLayers(opacity) => {
self.backup();
let opacity = opacity.clamp(0., 1.); let opacity = opacity.clamp(0., 1.);
for path in self.selected_layers().cloned() { for path in self.selected_layers().cloned() {
@ -345,12 +380,20 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
} }
SelectionChanged => responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()), SelectionChanged => responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()),
DeleteSelectedLayers => { DeleteSelectedLayers => {
self.backup();
responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into());
for path in self.selected_layers().cloned() { for path in self.selected_layers().cloned() {
responses.push_back(DocumentOperation::DeleteLayer { path }.into()) responses.push_front(DocumentOperation::DeleteLayer { path }.into())
}
}
ClearOverlays => {
responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into());
for path in self.layer_data.keys().filter(|path| self.document.layer(path).unwrap().overlay).cloned() {
responses.push_front(DocumentOperation::DeleteLayer { path }.into())
} }
responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into());
} }
DuplicateSelectedLayers => { DuplicateSelectedLayers => {
self.backup();
for path in self.selected_layers_sorted() { for path in self.selected_layers_sorted() {
responses.push_back(DocumentOperation::DuplicateLayer { path }.into()) responses.push_back(DocumentOperation::DuplicateLayer { path }.into())
} }
@ -365,7 +408,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
} }
// TODO: Correctly update layer panel in clear_selection instead of here // TODO: Correctly update layer panel in clear_selection instead of here
responses.extend(self.handle_folder_changed(Vec::new())); responses.extend(self.handle_folder_changed(Vec::new()));
responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into());
} }
SelectAllLayers => { SelectAllLayers => {
let all_layer_paths = self let all_layer_paths = self
@ -374,16 +417,26 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
.filter(|path| !path.is_empty() && !self.document.layer(path).unwrap().overlay) .filter(|path| !path.is_empty() && !self.document.layer(path).unwrap().overlay)
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
responses.push_back(SetSelectedLayers(all_layer_paths).into()); responses.push_front(SetSelectedLayers(all_layer_paths).into());
} }
DeselectAllLayers => { DeselectAllLayers => {
responses.push_back(SetSelectedLayers(vec![]).into()); responses.push_front(SetSelectedLayers(vec![]).into());
} }
DocumentHistoryBackward => self.undo().unwrap_or_else(|e| log::warn!("{}", e)),
DocumentHistoryForward => self.redo().unwrap_or_else(|e| log::warn!("{}", e)),
Undo => { Undo => {
// this is a temporary fix and will be addressed by #123 responses.push_back(SelectMessage::Abort.into());
if let Some(id) = self.document.root.as_folder().unwrap().list_layers().last() { responses.push_back(DocumentHistoryBackward.into());
responses.push_back(DocumentOperation::DeleteLayer { path: vec![*id] }.into()) responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into());
} responses.push_back(RenderDocument.into());
responses.push_back(FolderChanged(vec![]).into());
}
Redo => {
responses.push_back(SelectMessage::Abort.into());
responses.push_back(DocumentHistoryForward.into());
responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into());
responses.push_back(RenderDocument.into());
responses.push_back(FolderChanged(vec![]).into());
} }
FolderChanged(path) => responses.extend(self.handle_folder_changed(path)), FolderChanged(path) => responses.extend(self.handle_folder_changed(path)),
DispatchOperation(op) => match self.document.handle_operation(&op) { DispatchOperation(op) => match self.document.handle_operation(&op) {
@ -446,6 +499,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
} }
NudgeSelectedLayers(x, y) => { NudgeSelectedLayers(x, y) => {
self.backup();
for path in self.selected_layers().cloned() { for path in self.selected_layers().cloned() {
let operation = DocumentOperation::TransformLayerInViewport { let operation = DocumentOperation::TransformLayerInViewport {
path, path,
@ -461,6 +515,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
responses.push_back(DocumentsMessage::PasteLayers { path, insert_index }.into()); responses.push_back(DocumentsMessage::PasteLayers { path, insert_index }.into());
} }
ReorderSelectedLayers(relative_position) => { ReorderSelectedLayers(relative_position) => {
self.backup();
let all_layer_paths = self.all_layers_sorted(); let all_layer_paths = self.all_layers_sorted();
let selected_layers = self.selected_layers_sorted(); let selected_layers = self.selected_layers_sorted();
if let Some(pivot) = match relative_position.signum() { if let Some(pivot) = match relative_position.signum() {
@ -491,6 +546,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
} }
} }
FlipSelectedLayers(axis) => { FlipSelectedLayers(axis) => {
self.backup();
let scale = match axis { let scale = match axis {
FlipAxis::X => DVec2::new(-1., 1.), FlipAxis::X => DVec2::new(-1., 1.),
FlipAxis::Y => DVec2::new(1., -1.), FlipAxis::Y => DVec2::new(1., -1.),
@ -512,6 +568,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
} }
} }
AlignSelectedLayers(axis, aggregate) => { AlignSelectedLayers(axis, aggregate) => {
self.backup();
let (paths, boxes): (Vec<_>, Vec<_>) = self.selected_layers().filter_map(|path| self.document.viewport_bounding_box(path).ok()?.map(|b| (path, b))).unzip(); let (paths, boxes): (Vec<_>, Vec<_>) = self.selected_layers().filter_map(|path| self.document.viewport_bounding_box(path).ok()?.map(|b| (path, b))).unzip();
let axis = match axis { let axis = match axis {
@ -550,6 +607,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
fn actions(&self) -> ActionList { fn actions(&self) -> ActionList {
let mut common = actions!(DocumentMessageDiscriminant; let mut common = actions!(DocumentMessageDiscriminant;
Undo, Undo,
Redo,
SelectAllLayers, SelectAllLayers,
DeselectAllLayers, DeselectAllLayers,
RenderDocument, RenderDocument,

View File

@ -185,6 +185,7 @@ impl Default for Mapping {
// Editor Actions // Editor Actions
entry! {action=FrontendMessage::OpenDocumentBrowse, key_down=KeyO, modifiers=[KeyControl]}, entry! {action=FrontendMessage::OpenDocumentBrowse, key_down=KeyO, modifiers=[KeyControl]},
// Document Actions // Document Actions
entry! {action=DocumentMessage::Redo, key_down=KeyZ, modifiers=[KeyControl, KeyShift]},
entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]}, entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]},
entry! {action=DocumentMessage::DeselectAllLayers, key_down=KeyA, modifiers=[KeyControl, KeyAlt]}, entry! {action=DocumentMessage::DeselectAllLayers, key_down=KeyA, modifiers=[KeyControl, KeyAlt]},
entry! {action=DocumentMessage::SelectAllLayers, key_down=KeyA, modifiers=[KeyControl]}, entry! {action=DocumentMessage::SelectAllLayers, key_down=KeyA, modifiers=[KeyControl]},

View File

@ -89,9 +89,9 @@ impl SelectToolData {
} }
} }
fn add_boundnig_box(responses: &mut VecDeque<Message>) -> Vec<LayerId> { fn add_boundnig_box(responses: &mut Vec<Message>) -> Vec<LayerId> {
let path = vec![generate_uuid()]; let path = vec![generate_uuid()];
responses.push_back( responses.push(
Operation::AddBoundingBox { Operation::AddBoundingBox {
path: path.clone(), path: path.clone(),
transform: DAffine2::ZERO.to_cols_array(), transform: DAffine2::ZERO.to_cols_array(),
@ -124,22 +124,25 @@ impl Fsm for SelectToolFsmState {
if let ToolMessage::Select(event) = event { if let ToolMessage::Select(event) = event {
match (self, event) { match (self, event) {
(_, UpdateSelectionBoundingBox) => { (_, UpdateSelectionBoundingBox) => {
let mut buffer = Vec::new();
let response = match (document.selected_layers_bounding_box(), data.bounding_box_id.take()) { let response = match (document.selected_layers_bounding_box(), data.bounding_box_id.take()) {
(None, Some(path)) => Operation::DeleteLayer { path }.into(), (None, Some(path)) => Operation::DeleteLayer { path }.into(),
(Some([pos1, pos2]), path) => { (Some([pos1, pos2]), path) => {
let path = path.unwrap_or_else(|| add_boundnig_box(responses)); let path = path.unwrap_or_else(|| add_boundnig_box(&mut buffer));
data.bounding_box_id = Some(path.clone()); data.bounding_box_id = Some(path.clone());
let transform = transform_from_box(pos1, pos2); let transform = transform_from_box(pos1, pos2);
Operation::SetLayerTransformInViewport { path, transform }.into() Operation::SetLayerTransformInViewport { path, transform }.into()
} }
(_, _) => Message::NoOp, (_, _) => Message::NoOp,
}; };
responses.push_back(response); responses.push_front(response);
buffer.into_iter().rev().for_each(|message| responses.push_front(message));
self self
} }
(Ready, DragStart { add_to_selection }) => { (Ready, DragStart { add_to_selection }) => {
data.drag_start = input.mouse.position; data.drag_start = input.mouse.position;
data.drag_current = input.mouse.position; data.drag_current = input.mouse.position;
let mut buffer = Vec::new();
let mut selected: Vec<_> = document.selected_layers().cloned().collect(); let mut selected: Vec<_> = document.selected_layers().cloned().collect();
let quad = data.selection_quad(); let quad = data.selection_quad();
let intersection = document.document.intersects_quad_root(quad); let intersection = document.document.intersects_quad_root(quad);
@ -147,25 +150,29 @@ impl Fsm for SelectToolFsmState {
if selected.is_empty() { if selected.is_empty() {
if let Some(layer) = intersection.last() { if let Some(layer) = intersection.last() {
selected.push(layer.clone()); selected.push(layer.clone());
responses.push_back(DocumentMessage::SetSelectedLayers(selected.clone()).into()); buffer.push(DocumentMessage::SetSelectedLayers(selected.clone()).into());
} }
} }
// If the user clicks on a layer that is in their current selection, go into the dragging mode. // If the user clicks on a layer that is in their current selection, go into the dragging mode.
// Otherwise enter the box select mode // Otherwise enter the box select mode
if selected.iter().any(|path| intersection.contains(path)) { let state = if selected.iter().any(|path| intersection.contains(path)) {
buffer.push(DocumentMessage::StartTransaction.into());
data.layers_dragging = selected; data.layers_dragging = selected;
Dragging Dragging
} else { } else {
if !input.keyboard.get(add_to_selection as usize) { if !input.keyboard.get(add_to_selection as usize) {
responses.push_back(DocumentMessage::DeselectAllLayers.into()); buffer.push(DocumentMessage::DeselectAllLayers.into());
} }
data.drag_box_id = Some(add_boundnig_box(responses)); data.drag_box_id = Some(add_boundnig_box(&mut buffer));
DrawingBox DrawingBox
} };
buffer.into_iter().rev().for_each(|message| responses.push_front(message));
state
} }
(Dragging, MouseMove) => { (Dragging, MouseMove) => {
responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into());
for path in data.layers_dragging.iter() { for path in data.layers_dragging.iter() {
responses.push_back( responses.push_front(
Operation::TransformLayerInViewport { Operation::TransformLayerInViewport {
path: path.clone(), path: path.clone(),
transform: DAffine2::from_translation(input.mouse.position - data.drag_current).to_cols_array(), transform: DAffine2::from_translation(input.mouse.position - data.drag_current).to_cols_array(),
@ -173,7 +180,6 @@ impl Fsm for SelectToolFsmState {
.into(), .into(),
); );
} }
responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into());
data.drag_current = input.mouse.position; data.drag_current = input.mouse.position;
Dragging Dragging
} }
@ -183,7 +189,7 @@ impl Fsm for SelectToolFsmState {
let start = data.drag_start + half_pixel_offset; let start = data.drag_start + half_pixel_offset;
let size = data.drag_current - start + half_pixel_offset; let size = data.drag_current - start + half_pixel_offset;
responses.push_back( responses.push_front(
Operation::SetLayerTransformInViewport { Operation::SetLayerTransformInViewport {
path: data.drag_box_id.clone().unwrap(), path: data.drag_box_id.clone().unwrap(),
transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(), transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(),
@ -192,21 +198,27 @@ impl Fsm for SelectToolFsmState {
); );
DrawingBox DrawingBox
} }
(Dragging, DragStop) => Ready, (Dragging, DragStop) => {
let response = match input.mouse.position.distance(data.drag_start) < 10. * f64::EPSILON {
true => DocumentMessage::Undo,
false => DocumentMessage::CommitTransaction,
};
responses.push_front(response.into());
Ready
}
(DrawingBox, DragStop) => { (DrawingBox, DragStop) => {
let quad = data.selection_quad(); let quad = data.selection_quad();
responses.push_back(DocumentMessage::AddSelectedLayers(document.document.intersects_quad_root(quad)).into()); responses.push_front(DocumentMessage::AddSelectedLayers(document.document.intersects_quad_root(quad)).into());
responses.push_back( responses.push_front(
Operation::DeleteLayer { Operation::DeleteLayer {
path: data.drag_box_id.take().unwrap(), path: data.drag_box_id.take().unwrap(),
} }
.into(), .into(),
); );
data.drag_box_id = None;
Ready Ready
} }
(_, Abort) => { (_, Abort) => {
let mut delete = |path: &mut Option<Vec<LayerId>>| path.take().map(|path| responses.push_back(Operation::DeleteLayer { path }.into())); let mut delete = |path: &mut Option<Vec<LayerId>>| path.take().map(|path| responses.push_front(Operation::DeleteLayer { path }.into()));
delete(&mut data.drag_box_id); delete(&mut data.drag_box_id);
delete(&mut data.bounding_box_id); delete(&mut data.bounding_box_id);
Ready Ready

View File

@ -108,7 +108,7 @@ const menuEntries: MenuListEntries = [
children: [ children: [
[ [
{ label: "Undo", shortcut: ["Ctrl", "Z"], action: async () => (await wasm).undo() }, { label: "Undo", shortcut: ["Ctrl", "Z"], action: async () => (await wasm).undo() },
{ label: "Redo", shortcut: ["Ctrl", "⇧", "Z"] }, { label: "Redo", shortcut: ["Ctrl", "⇧", "Z"], action: async () => (await wasm).redo() },
], ],
[ [
{ label: "Cut", shortcut: ["Ctrl", "X"] }, { label: "Cut", shortcut: ["Ctrl", "X"] },

View File

@ -212,6 +212,12 @@ pub fn undo() -> Result<(), JsValue> {
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::Undo)).map_err(convert_error) EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::Undo)).map_err(convert_error)
} }
/// Redo history one step
#[wasm_bindgen]
pub fn redo() -> Result<(), JsValue> {
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::Redo)).map_err(convert_error)
}
/// Select all layers /// Select all layers
#[wasm_bindgen] #[wasm_bindgen]
pub fn select_all_layers() -> Result<(), JsValue> { pub fn select_all_layers() -> Result<(), JsValue> {

View File

@ -272,6 +272,24 @@ impl Document {
self.set_transform_relative_to_scope(layer, None, transform) self.set_transform_relative_to_scope(layer, None, transform)
} }
fn remove_overlays(&mut self, path: &mut Vec<LayerId>) {
if self.layer(path).unwrap().overlay {
self.delete(path).unwrap()
}
let ids = self.folder(path).map(|folder| folder.layer_ids.clone()).unwrap_or_default();
for id in ids {
path.push(id);
self.remove_overlays(path);
path.pop();
}
}
pub fn clone_without_overlays(&self) -> Self {
let mut document = self.clone();
document.remove_overlays(&mut vec![]);
document
}
/// Mutate the document by applying the `operation` to it. If the operation necessitates a /// Mutate the document by applying the `operation` to it. If the operation necessitates a
/// reaction from the frontend, responses may be returned. /// reaction from the frontend, responses may be returned.
pub fn handle_operation(&mut self, operation: &Operation) -> Result<Option<Vec<DocumentResponse>>, DocumentError> { pub fn handle_operation(&mut self, operation: &Operation) -> Result<Option<Vec<DocumentResponse>>, DocumentError> {