1use crate::tree::{walk, walk_mut, Node, Pairing, Region, Source};
42use crate::types::{Cut, Move, Position, Range, Visibility, ROOT};
43use anyhow::{bail, ensure};
44use std::collections::BTreeSet;
45
46pub struct Applier {
48 fresh: Fresh,
49}
50
51impl Applier {
52 pub fn new(sides: &Pairing<Source>) -> Self {
54 Self {
55 fresh: Fresh::of(sides),
56 }
57 }
58
59 pub fn apply(
61 &mut self,
62 next: Move,
63 sides: &mut Pairing<Source>,
64 file: &mut Visibility,
65 ) -> anyhow::Result<()> {
66 match next {
67 Move::Cut(Cut { region, at }) => cut(sides, region, at, &mut self.fresh),
68 Move::JoinFolds(regions) => join(sides, ®ions, &mut self.fresh),
69 Move::LinkFoldState(regions) => link(sides, ®ions),
70 Move::SetCollapsed((ROOT, collapsed)) => {
71 file.collapsed = collapsed;
72 Ok(())
73 }
74 Move::SetCollapsed((region, collapsed)) => {
75 let state = region_of(sides, region)?.fold_state_id;
76 for tree in trees(sides) {
77 walk_mut(tree, &mut |region| {
78 if region.fold_state_id == state {
79 region.visibility.collapsed = collapsed;
80 }
81 });
82 }
83 Ok(())
84 }
85 Move::SetLabel((ROOT, label)) => {
86 file.label = label.unwrap_or_default();
87 Ok(())
88 }
89 Move::SetLabel((region, label)) => {
90 region_mut(sides, region)?.visibility.label = label.unwrap_or_default();
91 Ok(())
92 }
93 Move::SetTags((ROOT, _)) => {
94 bail!("the file's tags are its manifest entry's; only a region's tags can be set")
95 }
96 Move::SetTags((region, tags)) => {
97 region_mut(sides, region)?.tags = tags;
98 Ok(())
99 }
100 }
101 }
102}
103
104pub fn apply(
106 moves: Vec<Move>,
107 sides: &mut Pairing<Source>,
108 file: &mut Visibility,
109) -> anyhow::Result<()> {
110 let mut applier = Applier::new(sides);
111 for next in moves {
112 applier.apply(next, sides, file)?;
113 }
114 Ok(())
115}
116
117fn trees(sides: &mut Pairing<Source>) -> Vec<&mut Vec<Region>> {
118 match sides {
119 Pairing::Both { lhs, rhs } => vec![&mut lhs.regions, &mut rhs.regions],
120 Pairing::LeftOnly { lhs } => vec![&mut lhs.regions],
121 Pairing::RightOnly { rhs } => vec![&mut rhs.regions],
122 }
123}
124
125pub fn trees_ref(sides: &Pairing<Source>) -> Vec<&[Region]> {
126 match sides {
127 Pairing::Both { lhs, rhs } => vec![&lhs.regions, &rhs.regions],
128 Pairing::LeftOnly { lhs } => vec![&lhs.regions],
129 Pairing::RightOnly { rhs } => vec![&rhs.regions],
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub struct Fresh {
138 pub id: u32,
139 pub alignment: u32,
140}
141
142impl Fresh {
143 pub fn of(sides: &Pairing<Source>) -> Self {
146 let mut fresh = Self {
147 id: ROOT + 1,
148 alignment: 0,
149 };
150 for tree in trees_ref(sides) {
151 walk(tree, &mut |region| {
152 fresh.id = fresh.id.max(region.id + 1);
153 if let Some(alignment_id) = region.alignment_id() {
154 fresh.alignment = fresh.alignment.max(alignment_id + 1);
155 }
156 });
157 }
158 fresh
159 }
160
161 pub fn id(&mut self) -> u32 {
163 let id = self.id;
164 self.id += 1;
165 id
166 }
167
168 pub fn alignment(&mut self) -> u32 {
170 let alignment = self.alignment;
171 self.alignment += 1;
172 alignment
173 }
174}
175
176fn path_where(regions: &[Region], is: &impl Fn(&Region) -> bool) -> Option<Vec<usize>> {
178 for (index, region) in regions.iter().enumerate() {
179 if is(region) {
180 return Some(vec![index]);
181 }
182 if let Node::Fold { children } = ®ion.node {
183 if let Some(mut path) = path_where(children, is) {
184 path.insert(0, index);
185 return Some(path);
186 }
187 }
188 }
189 None
190}
191
192fn path_of(regions: &[Region], id: u32) -> Option<Vec<usize>> {
194 path_where(regions, &|region| region.id == id)
195}
196
197fn at<'a>(regions: &'a [Region], path: &[usize]) -> &'a Region {
199 let (&index, rest) = path.split_first().expect("a path is never empty");
200 match (rest.is_empty(), ®ions[index].node) {
201 (true, _) => ®ions[index],
202 (false, Node::Fold { children }) => at(children, rest),
203 (false, Node::Leaf { .. }) => unreachable!("a path descends through folds"),
204 }
205}
206
207fn siblings<'a>(regions: &'a mut Vec<Region>, parent: &[usize]) -> &'a mut Vec<Region> {
209 match parent.split_first() {
210 None => regions,
211 Some((&index, rest)) => match &mut regions[index].node {
212 Node::Fold { children } => siblings(children, rest),
213 Node::Leaf { .. } => unreachable!("a path descends through folds"),
214 },
215 }
216}
217
218fn region_of(sides: &Pairing<Source>, id: u32) -> anyhow::Result<&Region> {
220 trees_ref(sides)
221 .into_iter()
222 .find_map(|tree| find(tree, id))
223 .ok_or_else(|| anyhow::anyhow!("no region {id}"))
224}
225
226fn region_mut(sides: &mut Pairing<Source>, id: u32) -> anyhow::Result<&mut Region> {
227 trees(sides)
228 .into_iter()
229 .find_map(|tree| find_mut(tree, id))
230 .ok_or_else(|| anyhow::anyhow!("no region {id}"))
231}
232
233pub fn find(regions: &[Region], id: u32) -> Option<&Region> {
234 regions.iter().find_map(|region| {
235 if region.id == id {
236 return Some(region);
237 }
238 match ®ion.node {
239 Node::Fold { children } => find(children, id),
240 Node::Leaf { .. } => None,
241 }
242 })
243}
244
245fn find_mut(regions: &mut [Region], id: u32) -> Option<&mut Region> {
246 for region in regions {
247 if region.id == id {
248 return Some(region);
249 }
250 if let Node::Fold { children } = &mut region.node {
251 if let Some(found) = find_mut(children, id) {
252 return Some(found);
253 }
254 }
255 }
256 None
257}
258
259fn cut(sides: &mut Pairing<Source>, id: u32, offset: u32, fresh: &mut Fresh) -> anyhow::Result<()> {
260 ensure!(id != ROOT, "the file cannot be cut; only a leaf can");
261 let Some((side, path)) = trees(sides)
262 .into_iter()
263 .enumerate()
264 .find_map(|(side, tree)| Some((side, path_of(tree, id)?)))
265 else {
266 bail!("no region {id}");
267 };
268 let (alignment, len) = {
269 let leaf = at(trees(sides).swap_remove(side), &path);
270 let Some(alignment) = leaf.alignment_id() else {
271 bail!("region {id} is a fold; only a leaf can be cut");
272 };
273 (alignment, leaf.range.lines().len() as u32)
274 };
275 ensure!(
276 0 < offset && offset < len,
277 "line {offset} is not inside region {id}, which has {len} lines"
278 );
279 let piece_alignment = fresh.alignment();
280 let mut state = None;
282 for (tree_side, tree) in trees(sides).into_iter().enumerate() {
283 let path = if tree_side == side {
284 path.clone()
285 } else {
286 match path_where(tree, &|region| region.alignment_id() == Some(alignment)) {
287 Some(path) => path,
288 None => continue,
289 }
290 };
291 let (index, parent) = path.split_last().expect("a path is never empty");
292 let list = siblings(tree, parent);
293 ensure!(
294 list[*index].range.lines().len() as u32 == len,
295 "region {id} has a different length on each side"
296 );
297 let piece_id = fresh.id();
298 let piece_state = *state.get_or_insert(piece_id);
299 let leaf = list.remove(*index);
300 let pieces = split(leaf, offset, piece_id, piece_alignment, piece_state);
301 list.splice(*index..*index, pieces);
302 }
303 Ok(())
304}
305
306fn split(leaf: Region, offset: u32, id: u32, alignment_id: u32, fold_state_id: u32) -> [Region; 2] {
309 let Node::Leaf { changed, .. } = &leaf.node else {
310 unreachable!("only leaves are cut");
311 };
312 let boundary = Position {
313 line: leaf.range.start.line + offset,
314 column: 0,
315 };
316 let piece = |range: Range, id: u32, alignment_id: u32, fold_state_id: u32| {
317 let lines = range.lines();
318 Region {
319 id,
320 fold_state_id,
321 range,
322 tags: leaf.tags.clone(),
323 visibility: leaf.visibility.clone(),
324 node: Node::Leaf {
325 alignment_id,
326 changed: changed
327 .iter()
328 .copied()
329 .filter(|span| lines.contains(&span.line))
330 .collect(),
331 },
332 }
333 };
334 let head = piece(
335 Range {
336 start: leaf.range.start,
337 end: boundary,
338 },
339 leaf.id,
340 leaf.alignment_id().expect("a leaf"),
341 leaf.fold_state_id,
342 );
343 let tail = piece(
344 Range {
345 start: boundary,
346 end: leaf.range.end,
347 },
348 id,
349 alignment_id,
350 fold_state_id,
351 );
352 [head, tail]
353}
354
355fn check_regions(ids: &[u32], what: &str) -> anyhow::Result<()> {
357 ensure!(ids.len() >= 2, "{what} needs at least two regions");
358 ensure!(!ids.contains(&ROOT), "{what} cannot include the file");
359 ensure!(
360 ids.iter().collect::<BTreeSet<_>>().len() == ids.len(),
361 "{what} lists a region twice: {ids:?}"
362 );
363 Ok(())
364}
365
366fn link(sides: &mut Pairing<Source>, ids: &[u32]) -> anyhow::Result<()> {
367 check_regions(ids, "a link")?;
368 let first = region_of(sides, ids[0])?;
369 let (state, collapsed) = (first.fold_state_id, first.visibility.collapsed);
370 let states = ids
371 .iter()
372 .map(|id| Ok(region_of(sides, *id)?.fold_state_id))
373 .collect::<anyhow::Result<BTreeSet<u32>>>()?;
374 for tree in trees(sides) {
375 walk_mut(tree, &mut |region| {
376 if states.contains(®ion.fold_state_id) {
377 region.fold_state_id = state;
378 region.visibility.collapsed = collapsed;
379 }
380 });
381 }
382 Ok(())
383}
384
385fn join(sides: &mut Pairing<Source>, ids: &[u32], fresh: &mut Fresh) -> anyhow::Result<()> {
386 check_regions(ids, "a join")?;
387 for id in ids {
388 region_of(sides, *id)?;
389 }
390 let mut state = None;
391 for tree in trees(sides) {
392 let mut paths: Vec<Vec<usize>> = ids.iter().filter_map(|id| path_of(tree, *id)).collect();
393 if paths.is_empty() {
394 continue;
395 }
396 ensure!(
397 paths.len() >= 2,
398 "a side holds only one of the joined regions {ids:?}"
399 );
400 paths.sort();
402 let parent = &paths[0][..paths[0].len() - 1];
403 let first = paths[0][paths[0].len() - 1];
404 let adjacent = paths.iter().enumerate().all(|(offset, path)| {
405 path.len() == paths[0].len()
406 && &path[..path.len() - 1] == parent
407 && path[path.len() - 1] == first + offset
408 });
409 ensure!(
410 adjacent,
411 "the joined regions {ids:?} are not consecutive siblings"
412 );
413 let parent = parent.to_vec();
414 let list = siblings(tree, &parent);
415 let children: Vec<Region> = list.drain(first..first + paths.len()).collect();
416 let range = Range {
417 start: children[0].range.start,
418 end: children[children.len() - 1].range.end,
419 };
420 let id = fresh.id();
421 let fold_state_id = *state.get_or_insert(id);
422 list.insert(
423 first,
424 Region {
425 id,
426 fold_state_id,
427 range,
428 tags: Vec::new(),
429 visibility: Visibility::default(),
430 node: Node::Fold { children },
431 },
432 );
433 }
434 Ok(())
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440 use crate::types::Span;
441
442 fn range(start: u32, end: u32) -> Range {
443 Range {
444 start: Position {
445 line: start,
446 column: 0,
447 },
448 end: Position {
449 line: end,
450 column: 0,
451 },
452 }
453 }
454
455 fn leaf(id: u32, alignment: u32, start: u32, end: u32, changed: &[u32]) -> Region {
457 Region {
458 id,
459 fold_state_id: id,
460 range: range(start, end),
461 tags: vec![],
462 visibility: Visibility::default(),
463 node: Node::Leaf {
464 alignment_id: alignment,
465 changed: changed
466 .iter()
467 .map(|&line| Span {
468 line,
469 start_column: 0,
470 end_column: 1,
471 })
472 .collect(),
473 },
474 }
475 }
476
477 fn fold(id: u32, collapsed: bool, children: Vec<Region>) -> Region {
479 Region {
480 id,
481 fold_state_id: id,
482 range: Range {
483 start: children[0].range.start,
484 end: children[children.len() - 1].range.end,
485 },
486 tags: vec![],
487 visibility: Visibility {
488 collapsed,
489 label: String::new(),
490 },
491 node: Node::Fold { children },
492 }
493 }
494
495 fn in_state(mut region: Region, state: u32) -> Region {
497 region.fold_state_id = state;
498 region
499 }
500
501 fn source(regions: Vec<Region>) -> Source {
502 Source {
503 text: String::new(),
504 regions,
505 }
506 }
507
508 fn both(lhs: Vec<Region>, rhs: Vec<Region>) -> Pairing<Source> {
509 Pairing::Both {
510 lhs: source(lhs),
511 rhs: source(rhs),
512 }
513 }
514
515 fn run(moves: Vec<Move>, sides: &mut Pairing<Source>) -> anyhow::Result<Visibility> {
516 let mut visibility = Visibility::default();
517 apply(moves, sides, &mut visibility)?;
518 Ok(visibility)
519 }
520
521 type Shape = (u32, Option<u32>, u32, u32, u32, bool, String);
522
523 fn shape(regions: &[Region]) -> Vec<Shape> {
526 let mut out = Vec::new();
527 walk(regions, &mut |region| {
528 out.push((
529 region.id,
530 region.alignment_id(),
531 region.fold_state_id,
532 region.range.start.line,
533 region.range.end.line,
534 region.visibility.collapsed,
535 region.visibility.label.clone(),
536 ))
537 });
538 out
539 }
540
541 fn sides_of(sides: &Pairing<Source>) -> (&Source, &Source) {
542 let Pairing::Both { lhs, rhs } = sides else {
543 panic!("both sides");
544 };
545 (lhs, rhs)
546 }
547
548 #[test]
549 fn cutting_a_paired_leaf_cuts_both_sides_with_fresh_ids_and_a_shared_alignment() {
550 for target in [2, 3] {
553 let mut sides = both(
554 vec![leaf(1, 0, 0, 2, &[]), leaf(2, 1, 2, 8, &[3, 6])],
555 vec![
556 in_state(leaf(3, 1, 0, 6, &[1, 4]), 2),
557 leaf(4, 2, 6, 7, &[]),
558 ],
559 );
560 let moves = vec![
561 Move::Cut(Cut {
562 region: target,
563 at: 2,
564 }),
565 Move::Cut(Cut { region: 5, at: 2 }),
566 ];
567 run(moves, &mut sides).unwrap();
568 let (lhs, rhs) = sides_of(&sides);
569 let open = String::new;
570 assert_eq!(
571 shape(&lhs.regions),
572 [
573 (1, Some(0), 1, 0, 2, false, open()),
574 (2, Some(1), 2, 2, 4, false, open()),
575 (5, Some(3), 5, 4, 6, false, open()),
576 (7, Some(4), 7, 6, 8, false, open()),
577 ],
578 "target {target}"
579 );
580 assert_eq!(
581 shape(&rhs.regions),
582 [
583 (3, Some(1), 2, 0, 2, false, open()),
584 (6, Some(3), 5, 2, 4, false, open()),
585 (8, Some(4), 7, 4, 6, false, open()),
586 (4, Some(2), 4, 6, 7, false, open()),
587 ],
588 "target {target}"
589 );
590 let Node::Leaf { changed, .. } = &lhs.regions[3].node else {
591 panic!("a leaf");
592 };
593 assert_eq!(
594 changed.iter().map(|span| span.line).collect::<Vec<_>>(),
595 [6]
596 );
597 }
598 }
599
600 #[test]
601 fn cutting_a_one_sided_leaf_takes_fresh_ids() {
602 let mut sides = both(
603 vec![leaf(1, 0, 0, 1, &[]), leaf(2, 1, 1, 6, &[])],
604 vec![in_state(leaf(3, 0, 0, 1, &[]), 1)],
605 );
606 run(vec![Move::Cut(Cut { region: 2, at: 1 })], &mut sides).unwrap();
607 let (lhs, rhs) = sides_of(&sides);
608 assert_eq!(
609 shape(&lhs.regions),
610 [
611 (1, Some(0), 1, 0, 1, false, String::new()),
612 (2, Some(1), 2, 1, 2, false, String::new()),
613 (4, Some(2), 4, 2, 6, false, String::new()),
614 ]
615 );
616 assert_eq!(rhs.regions.len(), 1);
617 }
618
619 #[test]
620 fn set_collapsed_reaches_every_region_in_the_fold_state_and_labels_reach_one() {
621 let mut sides = both(
623 vec![
624 leaf(1, 0, 0, 2, &[]),
625 fold(2, false, vec![leaf(3, 1, 2, 5, &[])]),
626 ],
627 vec![in_state(fold(4, false, vec![leaf(5, 2, 0, 4, &[])]), 2)],
628 );
629 let moves = vec![
630 Move::LinkFoldState(vec![2, 1]),
631 Move::SetCollapsed((4, true)),
632 Move::SetLabel((2, Some("summary".to_owned()))),
633 ];
634 run(moves, &mut sides).unwrap();
635 let (lhs, rhs) = sides_of(&sides);
636 assert_eq!(
637 shape(&lhs.regions),
638 [
639 (1, Some(0), 2, 0, 2, true, String::new()),
640 (2, None, 2, 2, 5, true, "summary".to_owned()),
641 (3, Some(1), 3, 2, 5, false, String::new()),
642 ]
643 );
644 assert_eq!(
645 shape(&rhs.regions)[0],
646 (4, None, 2, 0, 4, true, String::new())
647 );
648 run(
649 vec![Move::SetCollapsed((1, false)), Move::SetLabel((2, None))],
650 &mut sides,
651 )
652 .unwrap();
653 let (lhs, rhs) = sides_of(&sides);
654 assert!(lhs
655 .regions
656 .iter()
657 .all(|region| region.visibility == Visibility::default()));
658 assert!(!rhs.regions[0].visibility.collapsed);
659 }
660
661 #[test]
662 fn a_link_takes_the_first_regions_fold_state_and_collapsed_state() {
663 let tree = || {
666 both(
667 vec![
668 fold(1, false, vec![leaf(2, 0, 0, 3, &[])]),
669 fold(3, true, vec![leaf(4, 1, 3, 6, &[])]),
670 ],
671 vec![
672 in_state(fold(8, true, vec![leaf(7, 2, 0, 3, &[])]), 3),
673 in_state(fold(5, false, vec![leaf(6, 3, 3, 6, &[])]), 1),
674 ],
675 )
676 };
677 let mut sides = tree();
678 run(vec![Move::LinkFoldState(vec![7, 5])], &mut sides).unwrap();
679 let (lhs, rhs) = sides_of(&sides);
680 assert_eq!(lhs.regions[0].fold_state_id, 7, "the pair stays together");
681 assert_eq!(rhs.regions[1].fold_state_id, 7);
682 assert_eq!(rhs.regions[0].fold_state_id, 3);
683
684 let mut sides = tree();
685 run(vec![Move::LinkFoldState(vec![8, 1])], &mut sides).unwrap();
686 let (lhs, rhs) = sides_of(&sides);
687 for region in [&lhs.regions[0], &lhs.regions[1], &rhs.regions[1]] {
688 assert_eq!(
689 (region.fold_state_id, region.visibility.collapsed),
690 (3, true)
691 );
692 }
693 }
694
695 #[test]
696 fn a_join_listing_both_sides_runs_wraps_each_with_one_fold_state() {
697 let mut sides = both(
700 vec![
701 fold(1, true, vec![leaf(10, 0, 0, 3, &[])]),
702 leaf(2, 1, 3, 4, &[]),
703 fold(3, true, vec![leaf(11, 2, 4, 7, &[])]),
704 ],
705 vec![
706 leaf(4, 3, 0, 1, &[]),
707 in_state(fold(5, true, vec![leaf(12, 4, 1, 4, &[])]), 1),
708 in_state(leaf(7, 1, 4, 5, &[]), 2),
709 in_state(fold(6, true, vec![leaf(13, 5, 5, 8, &[])]), 3),
710 ],
711 );
712 let moves = vec![Move::JoinFolds(vec![1, 2, 3, 5, 7, 6])];
713 run(moves, &mut sides).unwrap();
714 let (lhs, rhs) = sides_of(&sides);
715 assert_eq!(lhs.regions.len(), 1);
716 assert_eq!(
717 shape(&lhs.regions)[0],
718 (14, None, 14, 0, 7, false, String::new())
719 );
720 assert_eq!(rhs.regions.len(), 2);
721 assert_eq!(
722 shape(&rhs.regions[1..])[0],
723 (15, None, 14, 1, 8, false, String::new())
724 );
725 }
726
727 #[test]
728 fn a_join_wraps_consecutive_siblings_on_each_side_that_holds_them() {
729 let mut sides = both(
730 vec![
731 leaf(1, 0, 0, 1, &[]),
732 fold(2, false, vec![leaf(3, 1, 1, 4, &[])]),
733 leaf(4, 2, 4, 5, &[]),
734 fold(5, true, vec![leaf(6, 3, 5, 8, &[])]),
735 ],
736 vec![in_state(leaf(7, 0, 0, 1, &[]), 1)],
737 );
738 let moves = vec![Move::JoinFolds(vec![2, 4, 5])];
739 run(moves, &mut sides).unwrap();
740 let (lhs, rhs) = sides_of(&sides);
741 assert_eq!(lhs.regions.len(), 2);
742 assert_eq!(
743 shape(&lhs.regions[1..])[0],
744 (8, None, 8, 1, 8, false, String::new())
745 );
746 assert_eq!(rhs.regions.len(), 1);
747 }
748
749 #[test]
750 fn moves_that_cannot_be_carried_out_are_errors() {
751 let tree = || {
752 both(
753 vec![
754 leaf(1, 0, 0, 1, &[]),
755 fold(2, false, vec![leaf(3, 1, 1, 4, &[])]),
756 leaf(4, 2, 4, 5, &[]),
757 ],
758 vec![
759 in_state(leaf(5, 0, 0, 1, &[]), 1),
760 in_state(leaf(6, 2, 1, 2, &[]), 4),
761 ],
762 )
763 };
764 let error = |next: Move| run(vec![next], &mut tree()).unwrap_err().to_string();
765 assert!(error(Move::SetCollapsed((9, true))).contains("no region 9"));
766 assert!(error(Move::Cut(Cut { region: 3, at: 3 })).contains("not inside region 3"));
767 assert!(error(Move::Cut(Cut { region: 3, at: 0 })).contains("not inside region 3"));
768 assert!(error(Move::Cut(Cut { region: 2, at: 1 })).contains("is a fold"));
769 assert!(error(Move::Cut(Cut {
770 region: ROOT,
771 at: 1
772 }))
773 .contains("the file cannot be cut"));
774 assert!(error(Move::JoinFolds(vec![1, 4])).contains("not consecutive"));
775 assert!(error(Move::JoinFolds(vec![1, 2, 99])).contains("no region 99"));
776 assert!(error(Move::JoinFolds(vec![ROOT, 1])).contains("cannot include the file"));
777 assert!(error(Move::JoinFolds(vec![1, 2, 5])).contains("holds only one"));
778 assert!(error(Move::LinkFoldState(vec![1])).contains("at least two"));
779 assert!(error(Move::LinkFoldState(vec![1, 1])).contains("twice"));
780 assert!(error(Move::SetTags((ROOT, vec![]))).contains("manifest"));
781 }
782
783 #[test]
784 fn the_root_is_the_file() {
785 let mut sides = both(
786 vec![leaf(1, 0, 0, 1, &[])],
787 vec![in_state(leaf(2, 0, 0, 1, &[]), 1)],
788 );
789 let visibility = run(
790 vec![
791 Move::SetLabel((ROOT, Some("first".to_owned()))),
792 Move::SetCollapsed((ROOT, true)),
793 Move::SetLabel((ROOT, Some("Generated file".to_owned()))),
794 Move::SetTags((2, vec!["mine:tag".to_owned()])),
795 ],
796 &mut sides,
797 )
798 .unwrap();
799 assert_eq!(
800 visibility,
801 Visibility {
802 collapsed: true,
803 label: "Generated file".to_owned()
804 }
805 );
806 let (lhs, rhs) = sides_of(&sides);
807 assert_eq!(lhs.regions[0].visibility, Visibility::default());
808 assert_eq!(rhs.regions[0].tags, ["mine:tag"]);
809 }
810}