Skip to main content

diffr_plugin_sdk/
draft.rs

1//! A plugin's moves, carried out on a copy of the region trees as the plugin
2//! makes them. The copy lets a plugin name what its own moves create (the
3//! piece a cut leaves, the fold a join adds) with the ids the applier will
4//! give them, since both hand out ids the same way.
5//!
6//! The helpers here are the moves the bundled plugins make together:
7//! collapsing a region under a label, linking regions, cutting lines out of
8//! a leaf, and grouping siblings under a labelled fold.
9use crate::apply::{find, trees_ref, Applier};
10use crate::tree::{siblings_of, walk, Node, Pairing, Region, Source};
11use crate::types::{Cut, Move, Visibility};
12use anyhow::{anyhow, ensure};
13
14pub struct Draft {
15    /// The trees as the moves so far leave them. Only regions are copied.
16    sides: Pairing<Source>,
17    file: Visibility,
18    applier: Applier,
19    moves: Vec<Move>,
20}
21
22impl Draft {
23    pub fn new(sides: &Pairing<Source>) -> Self {
24        let sides = sides.clone().map(|source| Source {
25            text: String::new(),
26            regions: source.regions,
27        });
28        Self {
29            applier: Applier::new(&sides),
30            sides,
31            file: Visibility::default(),
32            moves: Vec::new(),
33        }
34    }
35
36    /// Carry out `next` on the copy and keep it. A move the applier refuses
37    /// is an error, as it would be in the pipeline.
38    pub fn push(&mut self, next: Move) -> anyhow::Result<()> {
39        self.applier
40            .apply(next.clone(), &mut self.sides, &mut self.file)?;
41        self.moves.push(next);
42        Ok(())
43    }
44
45    pub fn into_moves(self) -> Vec<Move> {
46        self.moves
47    }
48
49    fn region(&self, id: u32) -> anyhow::Result<&Region> {
50        trees_ref(&self.sides)
51            .into_iter()
52            .find_map(|tree| find(tree, id))
53            .ok_or_else(|| anyhow!("no region {id}"))
54    }
55
56    fn regions(&self, visit: &mut impl FnMut(&Region)) {
57        for tree in trees_ref(&self.sides) {
58            walk(tree, visit);
59        }
60    }
61
62    /// The leaf on the other side whose rows pair with the leaf `id`.
63    pub fn paired_leaf(&self, id: u32) -> anyhow::Result<Option<u32>> {
64        let alignment = self.region(id)?.alignment_id();
65        let mut paired = None;
66        self.regions(&mut |region| {
67            if region.id != id && alignment.is_some() && region.alignment_id() == alignment {
68                paired = Some(region.id);
69            }
70        });
71        Ok(paired)
72    }
73
74    /// Start `id` collapsed behind `label`, and with it every region in its
75    /// fold state. The leaf paired with a leaf `id` takes the label too. Any
76    /// other region in the fold state that was open loses its label, so it
77    /// reads as part of `id`'s row rather than as its own placeholder.
78    pub fn collapse(&mut self, id: u32, label: String) -> anyhow::Result<()> {
79        let target = self.region(id)?;
80        let (state, alignment) = (target.fold_state_id, target.alignment_id());
81        let (mut named, mut cleared) = (vec![id], Vec::new());
82        self.regions(&mut |region| {
83            if region.fold_state_id != state || region.id == id {
84                return;
85            }
86            if alignment.is_some() && region.alignment_id() == alignment {
87                named.push(region.id);
88            } else if !region.visibility.collapsed && !region.visibility.label.is_empty() {
89                cleared.push(region.id);
90            }
91        });
92        self.push(Move::SetCollapsed((id, true)))?;
93        for region in named {
94            self.push(Move::SetLabel((region, Some(label.clone()))))?;
95        }
96        for region in cleared {
97            self.push(Move::SetLabel((region, None)))?;
98        }
99        Ok(())
100    }
101
102    /// Open and close `ids` together, collapsed if any region in their fold
103    /// states starts collapsed. A region that was open and so starts
104    /// collapsed loses its label, as in [`Draft::collapse`].
105    pub fn link(&mut self, ids: &[u32]) -> anyhow::Result<()> {
106        let states = ids
107            .iter()
108            .map(|id| Ok(self.region(*id)?.fold_state_id))
109            .collect::<anyhow::Result<Vec<u32>>>()?;
110        let first_collapsed = self.region(ids[0])?.visibility.collapsed;
111        let (mut collapsed, mut cleared) = (false, Vec::new());
112        self.regions(&mut |region| {
113            if !states.contains(&region.fold_state_id) {
114                return;
115            }
116            collapsed |= region.visibility.collapsed;
117            if !region.visibility.collapsed && !region.visibility.label.is_empty() {
118                cleared.push(region.id);
119            }
120        });
121        self.push(Move::LinkFoldState(ids.to_vec()))?;
122        if !collapsed {
123            return Ok(());
124        }
125        if !first_collapsed {
126            self.push(Move::SetCollapsed((ids[0], true)))?;
127        }
128        for region in cleared {
129            self.push(Move::SetLabel((region, None)))?;
130        }
131        Ok(())
132    }
133
134    /// Cut the lines `start..end`, relative to the leaf `id`'s first line
135    /// and half-open, out of it (and so out of the leaf paired with it),
136    /// returning the `id` of the piece on `id`'s side that holds them. The
137    /// leaf keeps its `id` for the lines before `start`.
138    pub fn cut_lines(&mut self, id: u32, start: u32, end: u32) -> anyhow::Result<u32> {
139        let leaf = self.region(id)?;
140        ensure!(
141            leaf.alignment_id().is_some(),
142            "region {id} is a fold; only a leaf can be cut to lines"
143        );
144        let len = leaf.range.lines().len() as u32;
145        ensure!(
146            start < end && end <= len,
147            "lines {start}..{end} are outside region {id}, which has {len} lines"
148        );
149        let mut piece = id;
150        if start > 0 {
151            self.push(Move::Cut(Cut {
152                region: id,
153                at: start,
154            }))?;
155            piece = self.next_sibling(id)?;
156        }
157        if end < len {
158            self.push(Move::Cut(Cut {
159                region: piece,
160                at: end - start,
161            }))?;
162        }
163        Ok(piece)
164    }
165
166    /// The `id` of the region just after `id` among its siblings.
167    fn next_sibling(&self, id: u32) -> anyhow::Result<u32> {
168        trees_ref(&self.sides)
169            .into_iter()
170            .find_map(|tree| {
171                let siblings = siblings_of(tree, id)?;
172                let index = siblings.iter().position(|region| region.id == id)?;
173                siblings.get(index + 1).map(|region| region.id)
174            })
175            .ok_or_else(|| anyhow!("region {id} has no next sibling"))
176    }
177
178    /// Wrap `ids`, consecutive siblings on each side that holds them, in a
179    /// new fold on each such side, starting collapsed behind `label`.
180    pub fn group(&mut self, ids: Vec<u32>, label: String) -> anyhow::Result<()> {
181        self.push(Move::JoinFolds(ids.clone()))?;
182        let mut folds = Vec::new();
183        for tree in trees_ref(&self.sides) {
184            let mut parent = None;
185            walk(tree, &mut |region| {
186                if let Node::Fold { children } = &region.node {
187                    if children.iter().any(|child| ids.contains(&child.id)) {
188                        parent = Some(region.id);
189                    }
190                }
191            });
192            folds.extend(parent);
193        }
194        let first = *folds.first().expect("a join adds a fold on some side");
195        self.push(Move::SetCollapsed((first, true)))?;
196        for region in folds {
197            self.push(Move::SetLabel((region, Some(label.clone()))))?;
198        }
199        Ok(())
200    }
201}