Skip to main content

diffr_plugin_sdk/
apply.rs

1//! Carry out moves, in order. diffr carries every plugin's moves out with
2//! this applier, and [`crate::Draft`] runs the same code on a copy, so a
3//! plugin predicts exactly the trees and ids its moves leave.
4//!
5//! A move names a region by `id`, on the one side that holds it, or the file
6//! by [`ROOT`]. An unknown `id` is an error.
7//!
8//! - `Cut { region, at }` splits a leaf into the lines before `at`, relative
9//!   to its first line, and the lines from `at` on; `at` must fall strictly
10//!   inside it. The leaf on the other side with its `alignment_id`, which
11//!   has the same length, is cut at the same offset. The first piece on each
12//!   side keeps its leaf's ids. The second piece takes a fresh `id` on each
13//!   side and a fresh `alignment_id` shared by the two, and its
14//!   `fold_state_id` is the `id` of the lhs piece, or of its only piece.
15//!   Pieces keep the leaf's tags and visibility and the `changed` spans on
16//!   their lines. A fold, or the file, cannot be cut.
17//! - `JoinFolds { regions }` needs two or more region ids. Each side wraps the
18//!   ones it holds, which must be two or more consecutive siblings under one
19//!   parent, in a new fold spanning them, with a fresh `id`, no tags, and an
20//!   open, unlabelled visibility. When both sides wrap, the two new folds
21//!   share the lhs fold's `fold_state_id`, its `id`, so a plugin joins a run
22//!   and the run matched with it on the other side in one move by listing
23//!   both runs' ids. A side holding exactly one of the ids is an error, and
24//!   so is an id no side holds.
25//! - `LinkFoldState { regions }` needs two or more region ids. Every region
26//!   in any of their fold states, on either side, takes the first region's
27//!   `fold_state_id` and whether it starts collapsed.
28//! - `SetCollapsed { region, collapsed }` sets whether every region sharing
29//!   the region's `fold_state_id` starts collapsed, on both sides: they open
30//!   and close together. On [`ROOT`] it sets whether the file starts hidden.
31//! - `SetLabel { region, label }` sets the label of that region alone, or of
32//!   the file on [`ROOT`]; `None` clears it.
33//! - `SetTags { region, tags }` replaces that region's tags. The file's tags
34//!   are its manifest entry's, fixed before any diff runs, so [`ROOT`] is an
35//!   error.
36//!
37//! Fresh `id`s start above the largest `id` in the file (and above
38//! [`ROOT`]) when a plugin's moves begin, and fresh `alignment_id`s above the
39//! largest leaf `alignment_id`; each is handed out in the order the moves
40//! need them, lhs before rhs.
41use 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
46/// Carries out one plugin's moves, handing out fresh ids as they need them.
47pub struct Applier {
48    fresh: Fresh,
49}
50
51impl Applier {
52    /// An applier for moves on `sides` as they are now.
53    pub fn new(sides: &Pairing<Source>) -> Self {
54        Self {
55            fresh: Fresh::of(sides),
56        }
57    }
58
59    /// Carry out one move on `sides` and `file`, the file's own visibility.
60    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, &regions, &mut self.fresh),
69            Move::LinkFoldState(regions) => link(sides, &regions),
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
104/// Carry out `moves` in order.
105pub 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/// The next unused `id` and leaf `alignment_id` in the file: what a plugin's
134/// moves hand out, in order. A plugin that does not use [`crate::Draft`]
135/// predicts the ids its cuts and joins create with it.
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub struct Fresh {
138    pub id: u32,
139    pub alignment: u32,
140}
141
142impl Fresh {
143    /// One above every `id`, and [`ROOT`], and one above every leaf
144    /// `alignment_id`, in the file.
145    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    /// Take the next `id`.
162    pub fn id(&mut self) -> u32 {
163        let id = self.id;
164        self.id += 1;
165        id
166    }
167
168    /// Take the next `alignment_id`.
169    pub fn alignment(&mut self) -> u32 {
170        let alignment = self.alignment;
171        self.alignment += 1;
172        alignment
173    }
174}
175
176/// Child indices from the root down to the first region `is` accepts.
177fn 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 } = &region.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
192/// Child indices from the root down to the region with this `id`.
193fn path_of(regions: &[Region], id: u32) -> Option<Vec<usize>> {
194    path_where(regions, &|region| region.id == id)
195}
196
197/// The region at a path.
198fn 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(), &regions[index].node) {
201        (true, _) => &regions[index],
202        (false, Node::Fold { children }) => at(children, rest),
203        (false, Node::Leaf { .. }) => unreachable!("a path descends through folds"),
204    }
205}
206
207/// The sibling list the last index of a path points into.
208fn 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
218/// The region with this `id`, on whichever side holds it.
219fn 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 &region.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    // The second pieces' fold state: the id of the first side's.
281    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
306/// A leaf split at relative line `offset`. The second piece takes `id`,
307/// `alignment_id` and `fold_state_id`.
308fn 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
355/// Two or more distinct region ids, none of them the file.
356fn 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(&region.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        // Child-index paths sort in document order.
401        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    /// A leaf whose `fold_state_id` is its `id`.
456    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    /// A fold whose `fold_state_id` is its `id`.
478    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    /// The region sharing another's fold state: the second of a pair.
496    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    /// `(id, alignment_id, fold_state_id, start, end, collapsed, label)` in
524    /// preorder.
525    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        // Leaves 2 (lhs) and 3 (rhs) are paired: alignment 1, fold state 2.
551        // Cutting either side cuts both the same way.
552        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        // Folds 2 (lhs) and 4 (rhs) are matched.
622        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        // Folds 1 (lhs) and 5 (rhs) are a matched pair, as are 3 (lhs) and
664        // 8 (rhs).
665        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        // Folds 1 and 3 on the lhs are matched with 5 and 6 on the rhs; the
698        // one-line leaves 2 (lhs) and 7 (rhs) between them are paired.
699        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}