1use ahash::{HashSet, HashSetExt};
13use blockworx_doc::{
14 commit::CommitBuilder,
15 document::{Document, IndexedDocument},
16 id::{AreaId, BlockId, ImageId, PinId, RouteId, RouteLabelId, TextId},
17 opcode::{Crud, OpCodes},
18};
19
20use crate::edit::lock::MaterialPin;
21use crate::path::Scope;
22
23#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
26pub enum Target {
27 Block(BlockId),
28 Pin(PinId),
29 Text(TextId),
30 Area(AreaId),
31 Image(ImageId),
32 Route(RouteId),
33}
34
35#[derive(Debug)]
44pub(crate) struct Closure {
45 pub(crate) blocks: Vec<BlockId>,
46 pub(crate) pins: Vec<PinId>,
47 pub(crate) routes: Vec<RouteId>,
48 pub(crate) labels: Vec<RouteLabelId>,
49 pub(crate) texts: Vec<TextId>,
50 pub(crate) areas: Vec<AreaId>,
51 pub(crate) images: Vec<ImageId>,
52}
53
54impl Closure {
55 pub(crate) fn of(indexed: &IndexedDocument<'_>, roots: &[Target]) -> Self {
56 let mut blocks = HashSet::new();
57 let mut pins = HashSet::new();
58 let mut routes = HashSet::new();
59 let mut texts = HashSet::new();
60 let mut areas = HashSet::new();
61 let mut images = HashSet::new();
62 let mut pending: Vec<BlockId> = Vec::new();
63
64 for root in roots.iter().copied().filter(|&r| held(indexed, r)) {
65 match root {
66 Target::Block(id) => pending.push(id),
67 Target::Pin(id) => {
68 pins.insert(id);
69 }
70 Target::Text(id) => {
71 texts.insert(id);
72 }
73 Target::Area(id) => {
74 areas.insert(id);
75 }
76 Target::Image(id) => {
77 images.insert(id);
78 }
79 Target::Route(id) => {
80 routes.insert(id);
81 }
82 }
83 }
84 while let Some(id) = pending.pop() {
85 let Some(entry) = indexed.index.blocks.get(&id) else {
86 continue;
87 };
88 if !blocks.insert(id) {
89 continue;
90 }
91 pending.extend(&entry.children);
92 pins.extend(&entry.pins);
93 routes.extend(&entry.routes);
94 texts.extend(&entry.texts);
95 areas.extend(&entry.areas);
96 images.extend(&entry.images);
97 }
98 routes.extend(
99 pins.iter()
100 .filter_map(|pin| indexed.index.routes_by_endpoint.get(pin))
101 .flatten(),
102 );
103 let labels = routes
104 .iter()
105 .filter_map(|route| indexed.index.routes.get(route))
106 .flat_map(|entry| &entry.labels)
107 .copied()
108 .collect();
109 Closure {
110 blocks: by_depth(indexed.doc, blocks),
111 pins: sorted(pins),
112 routes: sorted(routes),
113 labels: sorted(labels),
114 texts: sorted(texts),
115 areas: sorted(areas),
116 images: sorted(images),
117 }
118 }
119
120 pub(crate) fn push_deletes(&self, builder: &mut CommitBuilder) {
125 builder.extend(
126 self.labels
127 .iter()
128 .map(|&id| OpCodes::RouteLabel(id, Crud::Delete)),
129 );
130 builder.extend(
131 self.routes
132 .iter()
133 .map(|&id| OpCodes::Route(id, Crud::Delete)),
134 );
135 builder.extend(self.pins.iter().map(|&id| OpCodes::Pin(id, Crud::Delete)));
136 builder.extend(self.texts.iter().map(|&id| OpCodes::Text(id, Crud::Delete)));
137 builder.extend(self.areas.iter().map(|&id| OpCodes::Area(id, Crud::Delete)));
138 builder.extend(
139 self.images
140 .iter()
141 .map(|&id| OpCodes::Image(id, Crud::Delete)),
142 );
143 builder.extend(
144 self.blocks
145 .iter()
146 .rev()
147 .map(|&id| OpCodes::Block(id, Crud::Delete)),
148 );
149 }
150}
151
152fn held(indexed: &IndexedDocument<'_>, target: Target) -> bool {
156 match target {
157 Target::Block(id) => indexed.index.holds_block(id),
158 Target::Route(id) => indexed.index.routes.contains_key(&id),
159 Target::Pin(id) => indexed.doc.pin(&id).is_some(),
160 Target::Text(id) => indexed.doc.text(&id).is_some(),
161 Target::Area(id) => indexed.doc.area(&id).is_some(),
162 Target::Image(id) => indexed.doc.image(&id).is_some(),
163 }
164}
165
166fn deletable(doc: &Document, target: Target) -> bool {
170 match target {
171 Target::Pin(id) => MaterialPin::of(doc, id).is_some(),
172 Target::Block(_)
173 | Target::Text(_)
174 | Target::Area(_)
175 | Target::Image(_)
176 | Target::Route(_) => true,
177 }
178}
179
180fn sorted<T: Ord>(ids: HashSet<T>) -> Vec<T> {
181 let mut ids: Vec<T> = ids.into_iter().collect();
182 ids.sort_unstable();
183 ids
184}
185
186fn by_depth(doc: &Document, blocks: HashSet<BlockId>) -> Vec<BlockId> {
189 let mut blocks: Vec<BlockId> = blocks.into_iter().collect();
190 blocks.sort_by_key(|&id| (depth(doc, id), id));
191 blocks
192}
193
194fn depth(doc: &Document, block: BlockId) -> usize {
197 let mut depth = 0;
198 let mut current = block;
199 while let Some(parent) = doc
200 .block(¤t)
201 .map(|live| live.parent)
202 .filter(|&parent| Scope::from_wire(parent) != Scope::Root)
203 {
204 current = parent;
205 depth += 1;
206 }
207 depth
208}
209
210pub(crate) fn closure(indexed: &IndexedDocument<'_>, targets: &[Target]) -> Closure {
214 let roots: Vec<Target> = targets
215 .iter()
216 .copied()
217 .filter(|&target| deletable(indexed.doc, target))
218 .collect();
219 Closure::of(indexed, &roots)
220}
221
222pub fn selection(indexed: &IndexedDocument<'_>, targets: &[Target], builder: &mut CommitBuilder) {
227 closure(indexed, targets).push_deletes(builder);
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use crate::edit::harness::{
234 area_create, block_create, fold, image_create, pin_create, route_create,
235 route_label_create, seals_to_nothing, text_create, wired,
236 };
237 use blockworx_doc::document::DocIndex;
238 use blockworx_doc::{
239 block_model::BlockUpdate,
240 fixtures::{area_id, block_id, image_id, pin_id, route_id, route_label_id, text_id},
241 };
242
243 fn reparent(child: u32, parent: u32) -> OpCodes {
244 OpCodes::Block(
245 block_id(child),
246 Crud::Update(BlockUpdate::Parent(block_id(parent))),
247 )
248 }
249
250 fn lock(id: u32) -> OpCodes {
251 OpCodes::Block(block_id(id), Crud::Update(BlockUpdate::Locked(true)))
252 }
253
254 fn standing(doc: &Document, target: Target) -> bool {
257 match target {
258 Target::Block(id) => doc.block(&id).is_some(),
259 Target::Pin(id) => doc.pin(&id).is_some(),
260 Target::Text(id) => doc.text(&id).is_some(),
261 Target::Area(id) => doc.area(&id).is_some(),
262 Target::Image(id) => doc.image(&id).is_some(),
263 Target::Route(id) => doc.route(&id).is_some(),
264 }
265 }
266
267 fn label_standing(doc: &Document, label: u32) -> bool {
268 doc.route_label(&route_label_id(label)).is_some()
269 }
270
271 fn assert_standing(doc: &Document, targets: &[Target], labels: &[u32], why: &str) {
272 for &target in targets {
273 assert!(standing(doc, target), "{target:?}: {why}");
274 }
275 for &label in labels {
276 assert!(label_standing(doc, label), "label {label}: {why}");
277 }
278 }
279
280 fn assert_gone(doc: &Document, targets: &[Target], labels: &[u32], why: &str) {
281 for &target in targets {
282 assert!(!standing(doc, target), "{target:?}: {why}");
283 }
284 for &label in labels {
285 assert!(!label_standing(doc, label), "label {label}: {why}");
286 }
287 }
288
289 fn subtree() -> Vec<Target> {
292 vec![
293 Target::Block(block_id(1)),
294 Target::Block(block_id(2)),
295 Target::Pin(pin_id(3)),
296 Target::Pin(pin_id(4)),
297 Target::Pin(pin_id(10)),
298 Target::Pin(pin_id(11)),
299 Target::Route(route_id(5)),
300 Target::Route(route_id(12)),
301 Target::Route(route_id(13)),
302 Target::Route(route_id(38)),
303 Target::Text(text_id(7)),
304 Target::Text(text_id(14)),
305 Target::Area(area_id(8)),
306 Target::Area(area_id(15)),
307 Target::Image(image_id(9)),
308 ]
309 }
310
311 fn sibling() -> Vec<Target> {
314 vec![
315 Target::Block(block_id(30)),
316 Target::Pin(pin_id(31)),
317 Target::Pin(pin_id(32)),
318 Target::Route(route_id(33)),
319 Target::Text(text_id(35)),
320 Target::Area(area_id(36)),
321 Target::Image(image_id(37)),
322 ]
323 }
324
325 fn scene() -> Document {
333 let doc = wired();
334 let mut builder = CommitBuilder::new("Furnished two levels and a sibling");
335 builder.extend([
336 block_create(2),
337 reparent(2, 1),
338 pin_create(10, 2),
339 pin_create(11, 2),
340 route_create(12, 2, 10, 11),
341 route_create(13, 1, 3, 10),
342 text_create(14, 2),
343 area_create(15, 2),
344 image_create(9, 2),
345 route_label_create(20, 5),
346 route_label_create(21, 12),
347 route_label_create(22, 13),
348 block_create(30),
349 pin_create(31, 30),
350 pin_create(32, 30),
351 route_create(33, 30, 31, 32),
352 route_label_create(34, 33),
353 text_create(35, 30),
354 area_create(36, 30),
355 image_create(37, 30),
356 route_create(38, 30, 31, 10),
357 route_label_create(39, 38),
358 ]);
359 let doc = fold(builder, &doc);
360
361 let index = DocIndex::of(&doc);
362 assert!(
363 index.blocks[&block_id(1)].children.contains(&block_id(2)),
364 "precondition: block 2 nests inside block 1"
365 );
366 assert!(
367 index.blocks[&Scope::Root.wire_id()]
368 .children
369 .contains(&block_id(30)),
370 "precondition: the sibling is a top-level block of its own"
371 );
372 let crossing = doc.route(&route_id(38)).expect("the crossing wire exists");
373 assert_eq!(
374 (crossing.owner, crossing.to),
375 (block_id(30), pin_id(10)),
376 "precondition: wire 38 is owned outside the subtree and lands inside it"
377 );
378 assert_standing(
379 &doc,
380 &subtree(),
381 &[20, 21, 22, 39],
382 "the scene starts whole",
383 );
384 assert_standing(&doc, &sibling(), &[34], "the scene starts whole");
385 doc
386 }
387
388 #[test]
389 fn deleting_a_block_takes_its_subtree_and_leaves_the_sibling_standing() {
390 let doc = scene();
391 let mut index = DocIndex::default();
392
393 let mut builder = CommitBuilder::new("Deleted a block");
394 selection(
395 &index.view(&doc),
396 &[Target::Block(block_id(1))],
397 &mut builder,
398 );
399 let doc = fold(builder, &doc);
400
401 assert_gone(
402 &doc,
403 &subtree(),
404 &[20, 21, 22, 39],
405 "the cascade takes the whole closure",
406 );
407 assert_standing(
408 &doc,
409 &sibling(),
410 &[34],
411 "an unrelated block keeps everything of its own",
412 );
413 }
414
415 #[test]
418 fn a_cascade_emits_each_entity_before_whatever_owned_it() {
419 let doc = scene();
420 let mut index = DocIndex::default();
421
422 let mut builder = CommitBuilder::new("Deleted a block");
423 selection(
424 &index.view(&doc),
425 &[Target::Block(block_id(2))],
426 &mut builder,
427 );
428 let ops = builder.seal().expect("the cascade produced ops");
429
430 assert_eq!(
431 ops.ops(),
432 [
433 OpCodes::RouteLabel(route_label_id(21), Crud::Delete),
434 OpCodes::RouteLabel(route_label_id(22), Crud::Delete),
435 OpCodes::RouteLabel(route_label_id(39), Crud::Delete),
436 OpCodes::Route(route_id(12), Crud::Delete),
437 OpCodes::Route(route_id(13), Crud::Delete),
438 OpCodes::Route(route_id(38), Crud::Delete),
439 OpCodes::Pin(pin_id(10), Crud::Delete),
440 OpCodes::Pin(pin_id(11), Crud::Delete),
441 OpCodes::Text(text_id(14), Crud::Delete),
442 OpCodes::Area(area_id(15), Crud::Delete),
443 OpCodes::Image(image_id(9), Crud::Delete),
444 OpCodes::Block(block_id(2), Crud::Delete),
445 ]
446 );
447 }
448
449 #[test]
452 fn a_nested_cascade_emits_children_before_parents() {
453 let doc = scene();
454 let mut index = DocIndex::default();
455
456 let mut builder = CommitBuilder::new("Deleted a block");
457 selection(
458 &index.view(&doc),
459 &[Target::Block(block_id(1))],
460 &mut builder,
461 );
462 let ops = builder.seal().expect("the cascade produced ops");
463
464 let blocks: Vec<&OpCodes> = ops
465 .ops()
466 .iter()
467 .filter(|op| matches!(op, OpCodes::Block(..)))
468 .collect();
469 assert_eq!(
470 blocks,
471 [
472 &OpCodes::Block(block_id(2), Crud::Delete),
473 &OpCodes::Block(block_id(1), Crud::Delete),
474 ]
475 );
476 }
477
478 #[test]
479 fn deleting_pins_takes_exactly_the_wires_that_land_on_them() {
480 let doc = scene();
481 let mut index = DocIndex::default();
482 assert_eq!(
483 DocIndex::of(&doc).routes_by_endpoint[&pin_id(10)]
484 .iter()
485 .copied()
486 .collect::<HashSet<RouteId>>(),
487 [route_id(12), route_id(13), route_id(38)]
488 .into_iter()
489 .collect::<HashSet<RouteId>>(),
490 "precondition: three wires at two levels land on the pin"
491 );
492
493 let mut builder = CommitBuilder::new("Deleted a pin");
494 selection(&index.view(&doc), &[Target::Pin(pin_id(10))], &mut builder);
495 let doc = fold(builder, &doc);
496
497 assert_gone(
498 &doc,
499 &[
500 Target::Pin(pin_id(10)),
501 Target::Route(route_id(12)),
502 Target::Route(route_id(13)),
503 Target::Route(route_id(38)),
504 ],
505 &[21, 22, 39],
506 "a pin takes every wire landing on it, and each wire its labels",
507 );
508 assert_standing(
509 &doc,
510 &[
511 Target::Block(block_id(2)),
512 Target::Pin(pin_id(11)),
513 Target::Text(text_id(14)),
514 Target::Image(image_id(9)),
515 Target::Route(route_id(5)),
516 ],
517 &[20],
518 "the pin's owner and its neighbours are untouched",
519 );
520 }
521
522 #[test]
523 fn a_locked_owner_declines_its_own_pins_and_no_others() {
524 let doc = scene();
525 let mut index = DocIndex::default();
526 let mut builder = CommitBuilder::new("Locked a block");
527 builder.push(lock(2));
528 let doc = fold(builder, &doc);
529 assert!(
530 doc.block(&block_id(2))
531 .expect("the locked block exists")
532 .locked,
533 "precondition: the child block's interface is frozen"
534 );
535
536 let mut builder = CommitBuilder::new("Deleted the frozen pins");
537 selection(
538 &index.view(&doc),
539 &[Target::Pin(pin_id(10)), Target::Pin(pin_id(11))],
540 &mut builder,
541 );
542 seals_to_nothing(builder);
543
544 let mut builder = CommitBuilder::new("Deleted a mixed pin selection");
545 selection(
546 &index.view(&doc),
547 &[Target::Pin(pin_id(3)), Target::Pin(pin_id(10))],
548 &mut builder,
549 );
550 let doc = fold(builder, &doc);
551
552 assert_gone(
553 &doc,
554 &[
555 Target::Pin(pin_id(3)),
556 Target::Route(route_id(5)),
557 Target::Route(route_id(13)),
558 ],
559 &[20, 22],
560 "the unfrozen pin goes, wires and labels included",
561 );
562 assert_standing(
563 &doc,
564 &[
565 Target::Pin(pin_id(10)),
566 Target::Route(route_id(12)),
567 Target::Route(route_id(38)),
568 ],
569 &[21, 39],
570 "the frozen pin declines alone — the rest of the gesture still lands",
571 );
572 }
573
574 #[test]
577 fn a_locked_block_still_deletes_with_its_frozen_pins() {
578 let doc = scene();
579 let mut index = DocIndex::default();
580 let mut builder = CommitBuilder::new("Locked a block");
581 builder.push(lock(2));
582 let doc = fold(builder, &doc);
583
584 let mut builder = CommitBuilder::new("Deleted a locked block");
585 selection(
586 &index.view(&doc),
587 &[Target::Block(block_id(2))],
588 &mut builder,
589 );
590 let doc = fold(builder, &doc);
591
592 assert_gone(
593 &doc,
594 &[
595 Target::Block(block_id(2)),
596 Target::Pin(pin_id(10)),
597 Target::Pin(pin_id(11)),
598 ],
599 &[],
600 "a locked block is deletable, interface and all",
601 );
602 }
603
604 #[test]
605 fn deleting_a_route_takes_its_labels_and_leaves_its_endpoints() {
606 let doc = scene();
607 let mut index = DocIndex::default();
608
609 let mut builder = CommitBuilder::new("Deleted a wire");
610 selection(
611 &index.view(&doc),
612 &[Target::Route(route_id(13))],
613 &mut builder,
614 );
615 let doc = fold(builder, &doc);
616
617 assert_gone(&doc, &[Target::Route(route_id(13))], &[22], "the wire dies");
618 assert_standing(
619 &doc,
620 &[
621 Target::Pin(pin_id(3)),
622 Target::Pin(pin_id(10)),
623 Target::Route(route_id(12)),
624 ],
625 &[21],
626 "a wire's endpoints outlive it",
627 );
628 }
629
630 #[test]
631 fn deleting_an_annotation_has_no_fallout() {
632 let doc = scene();
633 let mut index = DocIndex::default();
634
635 let mut builder = CommitBuilder::new("Deleted the annotations");
636 selection(
637 &index.view(&doc),
638 &[Target::Text(text_id(14))],
639 &mut builder,
640 );
641 selection(
642 &index.view(&doc),
643 &[Target::Area(area_id(15))],
644 &mut builder,
645 );
646 selection(
647 &index.view(&doc),
648 &[Target::Image(image_id(9))],
649 &mut builder,
650 );
651 let doc = fold(builder, &doc);
652
653 assert_gone(
654 &doc,
655 &[
656 Target::Text(text_id(14)),
657 Target::Area(area_id(15)),
658 Target::Image(image_id(9)),
659 ],
660 &[],
661 "each annotation dies",
662 );
663 assert_standing(
664 &doc,
665 &[
666 Target::Block(block_id(2)),
667 Target::Pin(pin_id(10)),
668 Target::Route(route_id(12)),
669 ],
670 &[21],
671 "an annotation takes nothing with it",
672 );
673 }
674
675 #[test]
678 fn a_selection_holding_a_block_and_its_own_child_deletes_each_thing_once() {
679 let doc = scene();
680 let mut index = DocIndex::default();
681
682 let mut nested = CommitBuilder::new("Deleted a selection");
683 selection(
684 &index.view(&doc),
685 &[
686 Target::Block(block_id(2)),
687 Target::Block(block_id(1)),
688 Target::Pin(pin_id(10)),
689 Target::Text(text_id(7)),
690 Target::Route(route_id(13)),
691 ],
692 &mut nested,
693 );
694 let mut whole = CommitBuilder::new("Deleted a selection");
695 selection(&index.view(&doc), &[Target::Block(block_id(1))], &mut whole);
696
697 let nested = nested.seal().expect("the selection produced ops");
698 let whole = whole.seal().expect("the cascade produced ops");
699 assert_eq!(
700 nested.ops(),
701 whole.ops(),
702 "roots already inside the closure add nothing to it"
703 );
704 }
705
706 #[test]
707 fn a_selection_deletes_unrelated_targets_together() {
708 let doc = scene();
709 let mut index = DocIndex::default();
710
711 let mut builder = CommitBuilder::new("Deleted a selection");
712 selection(
713 &index.view(&doc),
714 &[Target::Block(block_id(2)), Target::Text(text_id(35))],
715 &mut builder,
716 );
717 let doc = fold(builder, &doc);
718
719 assert_gone(
720 &doc,
721 &[Target::Block(block_id(2)), Target::Text(text_id(35))],
722 &[],
723 "both roots of the selection die in one commit",
724 );
725 assert_standing(
726 &doc,
727 &[Target::Block(block_id(30)), Target::Route(route_id(33))],
728 &[34],
729 "the sibling keeps everything the selection did not name",
730 );
731 }
732
733 #[test]
737 fn deleting_what_is_already_gone_pushes_nothing() {
738 let doc = scene();
739 let mut index = DocIndex::default();
740 let mut builder = CommitBuilder::new("Deleted a block");
741 selection(
742 &index.view(&doc),
743 &[Target::Block(block_id(2))],
744 &mut builder,
745 );
746 let doc = fold(builder, &doc);
747 assert!(
748 !standing(&doc, Target::Block(block_id(2))),
749 "precondition: the delete removed the block"
750 );
751
752 let redelete = blockworx_doc::fixtures::commit(
753 "Deleted it again by hand",
754 vec![OpCodes::Block(block_id(2), Crud::Delete)],
755 );
756 assert!(
757 doc.try_apply(&redelete).is_err(),
758 "precondition: the fold refuses a delete at an absent target"
759 );
760
761 let mut builder = CommitBuilder::new("Deleted it again");
762 selection(
763 &index.view(&doc),
764 &[
765 Target::Block(block_id(2)),
766 Target::Pin(pin_id(10)),
767 Target::Route(route_id(12)),
768 Target::Text(text_id(14)),
769 Target::Area(area_id(15)),
770 Target::Image(image_id(9)),
771 ],
772 &mut builder,
773 );
774 seals_to_nothing(builder);
775 }
776
777 #[test]
780 fn absent_targets_push_nothing() {
781 let doc = scene();
782 let mut index = DocIndex::default();
783 assert!(
784 doc.block(&block_id(99)).is_none(),
785 "precondition: the strangers really are absent"
786 );
787
788 let mut builder = CommitBuilder::new("Deleted strangers");
789 selection(
790 &index.view(&doc),
791 &[
792 Target::Block(block_id(99)),
793 Target::Pin(pin_id(98)),
794 Target::Route(route_id(97)),
795 Target::Text(text_id(96)),
796 Target::Area(area_id(95)),
797 Target::Image(image_id(94)),
798 ],
799 &mut builder,
800 );
801 seals_to_nothing(builder);
802 }
803}