1use crate::{
2 CanvasDocument, CanvasDocumentDiff, CanvasKindRegistry, CanvasRecord, CanvasRecordChange,
3 CanvasRecordId, CanvasRecordOperationBatch, CanvasRecordRelation, CanvasRelationChange,
4 CanvasRelationOperationBatch, CanvasTransaction, DocumentCommand, DocumentError,
5};
6
7#[derive(Clone, Debug, PartialEq)]
8pub struct CanvasCommittedMutation {
9 transaction: CanvasTransaction,
10 inverse: CanvasTransaction,
11 diff: CanvasDocumentDiff,
12 record_changes: Vec<CanvasRecordChange>,
13 relation_changes: Vec<CanvasRelationChange>,
14}
15
16impl CanvasCommittedMutation {
17 pub fn transaction(&self) -> &CanvasTransaction {
18 &self.transaction
19 }
20
21 pub fn inverse(&self) -> &CanvasTransaction {
22 &self.inverse
23 }
24
25 pub fn diff(&self) -> &CanvasDocumentDiff {
26 &self.diff
27 }
28
29 pub fn record_changes(&self) -> &[CanvasRecordChange] {
30 &self.record_changes
31 }
32
33 pub fn relation_changes(&self) -> &[CanvasRelationChange] {
34 &self.relation_changes
35 }
36
37 pub fn record_operation_batch(&self, transaction_sequence: u64) -> CanvasRecordOperationBatch {
38 CanvasRecordOperationBatch::from_record_changes(
39 transaction_sequence,
40 self.transaction.metadata.clone(),
41 self.record_changes.clone(),
42 )
43 }
44
45 pub fn relation_operation_batch(
46 &self,
47 transaction_sequence: u64,
48 ) -> CanvasRelationOperationBatch {
49 CanvasRelationOperationBatch::from_relation_changes(
50 transaction_sequence,
51 self.transaction.metadata.clone(),
52 self.relation_changes.clone(),
53 )
54 }
55
56 pub fn into_diff(self) -> CanvasDocumentDiff {
57 self.diff
58 }
59
60 pub fn into_inverse(self) -> CanvasTransaction {
61 self.inverse
62 }
63}
64
65#[derive(Clone, Debug, PartialEq)]
66pub struct CanvasPreparedMutation {
67 document: CanvasDocument,
68 committed: CanvasCommittedMutation,
69}
70
71impl CanvasPreparedMutation {
72 pub fn committed(&self) -> &CanvasCommittedMutation {
73 &self.committed
74 }
75
76 pub(crate) fn apply_to(self, document: &mut CanvasDocument) -> CanvasCommittedMutation {
77 *document = self.document;
78 self.committed
79 }
80}
81
82pub(crate) struct CanvasMutationJournal;
83
84impl CanvasMutationJournal {
85 pub fn prepare_with_kind_registry(
86 document: &CanvasDocument,
87 transaction: CanvasTransaction,
88 kind_registry: &CanvasKindRegistry,
89 ) -> Result<CanvasPreparedMutation, DocumentError> {
90 let transaction = kind_registry.normalize_transaction(transaction)?;
91 let mut inverse = document.invert_transaction(&transaction)?;
92 let mut draft = document.clone();
93
94 for command in transaction.commands.iter().cloned() {
95 draft.apply(command)?;
96 }
97 draft.prune_missing_relations();
98 draft.validate_relations()?;
99 kind_registry.validate_document(&draft)?;
100 complete_inverse_relation_commands(document, &draft, &mut inverse)?;
101
102 let diff = draft.diff_against(document);
103 let record_changes = record_changes_from_diff(&draft, &diff);
104 let relation_changes = relation_changes_from_diff(&draft, document);
105 let committed = CanvasCommittedMutation {
106 transaction,
107 inverse,
108 diff,
109 record_changes,
110 relation_changes,
111 };
112
113 Ok(CanvasPreparedMutation {
114 document: draft,
115 committed,
116 })
117 }
118
119 pub fn commit(
120 document: &mut CanvasDocument,
121 transaction: CanvasTransaction,
122 ) -> Result<CanvasCommittedMutation, DocumentError> {
123 Self::commit_with_kind_registry(document, transaction, &CanvasKindRegistry::open())
124 }
125
126 pub fn commit_with_kind_registry(
127 document: &mut CanvasDocument,
128 transaction: CanvasTransaction,
129 kind_registry: &CanvasKindRegistry,
130 ) -> Result<CanvasCommittedMutation, DocumentError> {
131 let prepared = Self::prepare_with_kind_registry(document, transaction, kind_registry)?;
132 Ok(prepared.apply_to(document))
133 }
134}
135
136fn record_changes_from_diff(
137 document: &CanvasDocument,
138 diff: &CanvasDocumentDiff,
139) -> Vec<CanvasRecordChange> {
140 let mut changes =
141 Vec::with_capacity(diff.inserted.len() + diff.updated.len() + diff.removed.len());
142
143 changes.extend(
144 diff.inserted
145 .iter()
146 .filter_map(|id| record_from_document(document, id))
147 .map(CanvasRecordChange::Upsert),
148 );
149 changes.extend(
150 diff.updated
151 .iter()
152 .filter_map(|id| record_from_document(document, id))
153 .map(CanvasRecordChange::Upsert),
154 );
155 changes.extend(diff.removed.iter().cloned().map(CanvasRecordChange::Delete));
156
157 changes
158}
159
160fn relation_changes_from_diff(
161 document: &CanvasDocument,
162 previous: &CanvasDocument,
163) -> Vec<CanvasRelationChange> {
164 let current = document.relations();
165 let previous = previous.relations();
166 let mut changes = Vec::new();
167
168 for relation in previous.parents() {
169 if current.parent_of(&relation.child) != Some(&relation.parent) {
170 changes.push(CanvasRelationChange::Delete(CanvasRecordRelation::Parent(
171 relation.clone(),
172 )));
173 }
174 }
175
176 for relation in current.parents() {
177 if previous.parent_of(&relation.child) != Some(&relation.parent) {
178 changes.push(CanvasRelationChange::Upsert(CanvasRecordRelation::Parent(
179 relation.clone(),
180 )));
181 }
182 }
183
184 for relation in previous.groups() {
185 if !current.contains_relation(&CanvasRecordRelation::Group(relation.clone())) {
186 changes.push(CanvasRelationChange::Delete(CanvasRecordRelation::Group(
187 relation.clone(),
188 )));
189 }
190 }
191
192 for relation in current.groups() {
193 if !previous.contains_relation(&CanvasRecordRelation::Group(relation.clone())) {
194 changes.push(CanvasRelationChange::Upsert(CanvasRecordRelation::Group(
195 relation.clone(),
196 )));
197 }
198 }
199
200 for relation in previous.bindings() {
201 if current.binding(&relation.id) != Some(relation) {
202 changes.push(CanvasRelationChange::Delete(CanvasRecordRelation::Binding(
203 relation.clone(),
204 )));
205 }
206 }
207
208 for relation in current.bindings() {
209 if previous.binding(&relation.id) != Some(relation) {
210 changes.push(CanvasRelationChange::Upsert(CanvasRecordRelation::Binding(
211 relation.clone(),
212 )));
213 }
214 }
215
216 changes
217}
218
219fn complete_inverse_relation_commands(
220 previous: &CanvasDocument,
221 current: &CanvasDocument,
222 inverse: &mut CanvasTransaction,
223) -> Result<(), DocumentError> {
224 let mut restored = current.clone();
225 for command in inverse.commands.iter().cloned() {
226 restored.apply(command)?;
227 }
228 restored.prune_missing_relations();
229 restored.validate_relations()?;
230
231 let commands = relation_changes_from_diff(previous, &restored)
232 .into_iter()
233 .map(relation_change_to_command)
234 .collect::<Vec<_>>();
235 if commands.is_empty() {
236 return Ok(());
237 }
238
239 for command in commands.iter().cloned() {
240 restored.apply(command)?;
241 }
242 restored.prune_missing_relations();
243 restored.validate_relations()?;
244 inverse.commands.extend(commands);
245 Ok(())
246}
247
248fn relation_change_to_command(change: CanvasRelationChange) -> DocumentCommand {
249 match change {
250 CanvasRelationChange::Upsert(CanvasRecordRelation::Parent(relation)) => {
251 DocumentCommand::SetRecordParent {
252 child: relation.child,
253 parent: relation.parent,
254 }
255 }
256 CanvasRelationChange::Delete(CanvasRecordRelation::Parent(relation)) => {
257 DocumentCommand::ClearRecordParent {
258 child: relation.child,
259 }
260 }
261 CanvasRelationChange::Upsert(CanvasRecordRelation::Group(relation)) => {
262 DocumentCommand::AddRecordToGroup {
263 group: relation.group,
264 member: relation.member,
265 }
266 }
267 CanvasRelationChange::Delete(CanvasRecordRelation::Group(relation)) => {
268 DocumentCommand::RemoveRecordFromGroup {
269 group: relation.group,
270 member: relation.member,
271 }
272 }
273 CanvasRelationChange::Upsert(CanvasRecordRelation::Binding(relation)) => {
274 DocumentCommand::SetRecordBinding(relation)
275 }
276 CanvasRelationChange::Delete(CanvasRecordRelation::Binding(relation)) => {
277 DocumentCommand::RemoveRecordBinding { id: relation.id }
278 }
279 }
280}
281
282fn record_from_document(document: &CanvasDocument, id: &CanvasRecordId) -> Option<CanvasRecord> {
283 match id {
284 CanvasRecordId::Node(id) => document.node(id).cloned().map(CanvasRecord::Node),
285 CanvasRecordId::Edge(id) => document.edge(id).cloned().map(CanvasRecord::Edge),
286 CanvasRecordId::Shape(id) => document.shape(id).cloned().map(CanvasRecord::Shape),
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use crate::test_support::{child_frame_fixture, connected_pair_fixture, document_fixture};
294 use crate::{
295 CanvasEdge, CanvasEndpoint, CanvasNode, CanvasRecordBindingRelation,
296 CanvasRecordGroupRelation, CanvasRecordId, CanvasRecordParentRelation, DocumentCommand,
297 EdgeId, NodeId, ShapeId,
298 };
299 use open_gpui::{point, px, size};
300
301 #[test]
302 fn committed_mutation_reports_actual_incident_edge_deletes() {
303 let mut document = connected_document();
304 let committed = document
305 .commit_transaction(CanvasTransaction::single(DocumentCommand::RemoveNode(
306 NodeId::from("a"),
307 )))
308 .unwrap();
309
310 assert_eq!(
311 committed.record_changes(),
312 &[
313 CanvasRecordChange::Delete(CanvasRecordId::Node(NodeId::from("a"))),
314 CanvasRecordChange::Delete(CanvasRecordId::Edge(EdgeId::from("a-b"))),
315 ]
316 );
317 assert_eq!(
318 committed
319 .record_operation_batch(9)
320 .operations
321 .iter()
322 .map(|operation| (
323 operation.transaction_sequence,
324 operation.operation_index,
325 operation.id()
326 ))
327 .collect::<Vec<_>>(),
328 vec![
329 (9, 0, CanvasRecordId::Node(NodeId::from("a"))),
330 (9, 1, CanvasRecordId::Edge(EdgeId::from("a-b"))),
331 ]
332 );
333 }
334
335 #[test]
336 fn committed_mutation_prunes_relations_for_actual_deleted_records() {
337 let mut document = connected_document();
338 document
339 .apply_transaction(CanvasTransaction::single(
340 DocumentCommand::AddRecordToGroup {
341 group: CanvasRecordId::Node(NodeId::from("b")),
342 member: CanvasRecordId::Edge(EdgeId::from("a-b")),
343 },
344 ))
345 .unwrap();
346
347 let committed = document
348 .commit_transaction(CanvasTransaction::single(DocumentCommand::RemoveNode(
349 NodeId::from("a"),
350 )))
351 .unwrap();
352
353 assert!(committed.diff().relations_changed);
354 assert!(document.relations().is_empty());
355 assert_eq!(
356 committed.record_changes(),
357 &[
358 CanvasRecordChange::Delete(CanvasRecordId::Node(NodeId::from("a"))),
359 CanvasRecordChange::Delete(CanvasRecordId::Edge(EdgeId::from("a-b"))),
360 ]
361 );
362 assert_eq!(
363 committed.relation_changes(),
364 &[CanvasRelationChange::Delete(CanvasRecordRelation::Group(
365 CanvasRecordGroupRelation::new(NodeId::from("b"), EdgeId::from("a-b"))
366 ))]
367 );
368 }
369
370 #[test]
371 fn committed_mutation_reports_relation_only_changes() {
372 let mut document = child_frame_fixture().build();
373 let binding = CanvasRecordBindingRelation::new(
374 "binding",
375 CanvasRecordId::Node(NodeId::from("child")),
376 CanvasRecordId::Shape(ShapeId::from("frame")),
377 );
378 let mut transaction = CanvasTransaction::new([
379 DocumentCommand::SetRecordParent {
380 child: CanvasRecordId::Node(NodeId::from("child")),
381 parent: CanvasRecordId::Shape(ShapeId::from("frame")),
382 },
383 DocumentCommand::AddRecordToGroup {
384 group: CanvasRecordId::Shape(ShapeId::from("frame")),
385 member: CanvasRecordId::Node(NodeId::from("child")),
386 },
387 DocumentCommand::SetRecordBinding(binding.clone()),
388 ]);
389 transaction
390 .metadata
391 .insert("origin".into(), serde_json::json!("test"));
392
393 let committed = document.commit_transaction(transaction).unwrap();
394
395 assert!(committed.diff().relations_changed);
396 assert!(committed.record_changes().is_empty());
397 assert_eq!(
398 committed.relation_changes(),
399 &[
400 CanvasRelationChange::Upsert(CanvasRecordRelation::Parent(
401 CanvasRecordParentRelation::new(NodeId::from("child"), ShapeId::from("frame"))
402 )),
403 CanvasRelationChange::Upsert(CanvasRecordRelation::Group(
404 CanvasRecordGroupRelation::new(ShapeId::from("frame"), NodeId::from("child"))
405 )),
406 CanvasRelationChange::Upsert(CanvasRecordRelation::Binding(binding)),
407 ]
408 );
409
410 let batch = committed.relation_operation_batch(7);
411 assert_eq!(batch.transaction_sequence, 7);
412 assert_eq!(
413 batch.transaction_metadata.get("origin"),
414 Some(&serde_json::json!("test"))
415 );
416 assert_eq!(
417 batch
418 .operations
419 .iter()
420 .map(|operation| operation.operation_index)
421 .collect::<Vec<_>>(),
422 vec![0, 1, 2]
423 );
424 }
425
426 #[test]
427 fn committed_mutation_reports_binding_replacement_as_delete_and_upsert() {
428 let mut document = child_frame_fixture().build();
429 document
430 .insert_shape(crate::CanvasShape::new(
431 "other-frame",
432 open_gpui::Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
433 ))
434 .unwrap();
435 document
436 .commit_transaction(CanvasTransaction::single(
437 DocumentCommand::SetRecordBinding(CanvasRecordBindingRelation::new(
438 "binding",
439 CanvasRecordId::Node(NodeId::from("child")),
440 CanvasRecordId::Shape(ShapeId::from("frame")),
441 )),
442 ))
443 .unwrap();
444
445 let committed = document
446 .commit_transaction(CanvasTransaction::single(
447 DocumentCommand::SetRecordBinding(CanvasRecordBindingRelation::new(
448 "binding",
449 CanvasRecordId::Node(NodeId::from("child")),
450 CanvasRecordId::Shape(ShapeId::from("other-frame")),
451 )),
452 ))
453 .unwrap();
454
455 assert_eq!(
456 committed.relation_changes(),
457 &[
458 CanvasRelationChange::Delete(CanvasRecordRelation::Binding(
459 CanvasRecordBindingRelation::new(
460 "binding",
461 CanvasRecordId::Node(NodeId::from("child")),
462 CanvasRecordId::Shape(ShapeId::from("frame")),
463 )
464 )),
465 CanvasRelationChange::Upsert(CanvasRecordRelation::Binding(
466 CanvasRecordBindingRelation::new(
467 "binding",
468 CanvasRecordId::Node(NodeId::from("child")),
469 CanvasRecordId::Shape(ShapeId::from("other-frame")),
470 )
471 )),
472 ]
473 );
474 }
475
476 #[test]
477 fn committed_mutation_reports_parent_replacement_as_delete_and_upsert() {
478 let mut document = document_fixture()
479 .node(CanvasNode::new(
480 "child",
481 point(px(0.0), px(0.0)),
482 size(px(10.0), px(10.0)),
483 ))
484 .build();
485 for id in ["old-frame", "new-frame"] {
486 document
487 .insert_shape(crate::CanvasShape::new(
488 id,
489 open_gpui::Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
490 ))
491 .unwrap();
492 }
493 document
494 .apply_transaction(CanvasTransaction::single(
495 DocumentCommand::SetRecordParent {
496 child: CanvasRecordId::Node(NodeId::from("child")),
497 parent: CanvasRecordId::Shape(ShapeId::from("old-frame")),
498 },
499 ))
500 .unwrap();
501
502 let committed = document
503 .commit_transaction(CanvasTransaction::single(
504 DocumentCommand::SetRecordParent {
505 child: CanvasRecordId::Node(NodeId::from("child")),
506 parent: CanvasRecordId::Shape(ShapeId::from("new-frame")),
507 },
508 ))
509 .unwrap();
510
511 assert_eq!(
512 committed.relation_changes(),
513 &[
514 CanvasRelationChange::Delete(CanvasRecordRelation::Parent(
515 CanvasRecordParentRelation::new(
516 NodeId::from("child"),
517 ShapeId::from("old-frame")
518 )
519 )),
520 CanvasRelationChange::Upsert(CanvasRecordRelation::Parent(
521 CanvasRecordParentRelation::new(
522 NodeId::from("child"),
523 ShapeId::from("new-frame")
524 )
525 )),
526 ]
527 );
528 }
529
530 #[test]
531 fn committed_mutation_omits_noop_relation_changes() {
532 let mut document = child_frame_fixture().build();
533 let relation_transaction = CanvasTransaction::new([
534 DocumentCommand::SetRecordParent {
535 child: CanvasRecordId::Node(NodeId::from("child")),
536 parent: CanvasRecordId::Shape(ShapeId::from("frame")),
537 },
538 DocumentCommand::AddRecordToGroup {
539 group: CanvasRecordId::Shape(ShapeId::from("frame")),
540 member: CanvasRecordId::Node(NodeId::from("child")),
541 },
542 ]);
543 document
544 .commit_transaction(relation_transaction.clone())
545 .unwrap();
546
547 let committed = document.commit_transaction(relation_transaction).unwrap();
548
549 assert!(committed.diff().is_empty());
550 assert!(committed.record_changes().is_empty());
551 assert!(committed.relation_changes().is_empty());
552 }
553
554 #[test]
555 fn failed_transaction_leaves_document_unchanged() {
556 let mut document = document_fixture().build();
557 let before = document.clone();
558 let transaction = CanvasTransaction::new([
559 DocumentCommand::InsertNode(CanvasNode::new(
560 "a",
561 point(px(0.0), px(0.0)),
562 size(px(10.0), px(10.0)),
563 )),
564 DocumentCommand::InsertEdge(CanvasEdge::new(
565 "bad",
566 CanvasEndpoint::new("a", None::<&str>),
567 CanvasEndpoint::new("missing", None::<&str>),
568 )),
569 ]);
570
571 let err = document.commit_transaction(transaction).unwrap_err();
572
573 assert_eq!(err, DocumentError::MissingNode(NodeId::from("missing")));
574 assert_eq!(document, before);
575 }
576
577 #[test]
578 fn committed_mutation_preserves_transaction_metadata() {
579 let mut document = document_fixture().build();
580 let mut transaction = CanvasTransaction::single(DocumentCommand::InsertNode(
581 CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(10.0), px(10.0))),
582 ));
583 transaction
584 .metadata
585 .insert("origin".into(), serde_json::json!("test"));
586
587 let committed = document.commit_transaction(transaction).unwrap();
588 let batch = committed.record_operation_batch(3);
589
590 assert_eq!(
591 batch.transaction_metadata.get("origin"),
592 Some(&serde_json::json!("test"))
593 );
594 }
595
596 #[test]
597 fn committed_inverse_restores_previous_document() {
598 let mut document = connected_document();
599 let before = document.clone();
600 let committed = document
601 .commit_transaction(CanvasTransaction::single(DocumentCommand::RemoveNode(
602 NodeId::from("a"),
603 )))
604 .unwrap();
605
606 assert_ne!(document, before);
607
608 document
609 .commit_transaction(committed.inverse().clone())
610 .unwrap();
611 assert_eq!(document, before);
612 }
613
614 #[test]
615 fn committed_inverse_restores_relations_pruned_by_deleted_records() {
616 let mut document = connected_document();
617 let edge = CanvasRecordId::Edge(EdgeId::from("a-b"));
618 let group = CanvasRecordId::Node(NodeId::from("b"));
619 document
620 .commit_transaction(CanvasTransaction::new([
621 DocumentCommand::SetRecordParent {
622 child: edge.clone(),
623 parent: group.clone(),
624 },
625 DocumentCommand::AddRecordToGroup {
626 group: group.clone(),
627 member: edge.clone(),
628 },
629 DocumentCommand::SetRecordBinding(CanvasRecordBindingRelation::new(
630 "binding",
631 edge.clone(),
632 group.clone(),
633 )),
634 ]))
635 .unwrap();
636 let before = document.clone();
637
638 let committed = document
639 .commit_transaction(CanvasTransaction::single(DocumentCommand::RemoveNode(
640 NodeId::from("a"),
641 )))
642 .unwrap();
643
644 assert!(document.relations().is_empty());
645
646 document
647 .commit_transaction(committed.inverse().clone())
648 .unwrap();
649 assert_eq!(document, before);
650 assert_eq!(document.relations().parent_of(&edge), Some(&group));
651 assert_eq!(
652 document
653 .relations()
654 .members_of(&group)
655 .cloned()
656 .collect::<Vec<_>>(),
657 vec![edge.clone()]
658 );
659 assert_eq!(
660 document
661 .relations()
662 .binding(&crate::BindingId::from("binding")),
663 Some(&CanvasRecordBindingRelation::new("binding", edge, group))
664 );
665 }
666
667 fn connected_document() -> CanvasDocument {
668 connected_pair_fixture().build()
669 }
670}