Skip to main content

open_gpui_canvas/
mutation.rs

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}