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