Skip to main content

diffr_plugin_sdk/
tree.rs

1//! Region trees: each side's flat preorder list rebuilt as a tree, the
2//! records rebuilt from a tree, and the helpers plugins read trees with.
3use crate::types::{self, Range, Span, Visibility, ROOT};
4use std::collections::BTreeSet;
5
6/// Which sides a thing exists on.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum Pairing<T> {
9    Both { lhs: T, rhs: T },
10    LeftOnly { lhs: T },
11    RightOnly { rhs: T },
12}
13
14impl<T> Pairing<T> {
15    /// The same sides, each value transformed by `f`.
16    pub fn map<U>(self, mut f: impl FnMut(T) -> U) -> Pairing<U> {
17        match self {
18            Self::Both { lhs, rhs } => Pairing::Both {
19                lhs: f(lhs),
20                rhs: f(rhs),
21            },
22            Self::LeftOnly { lhs } => Pairing::LeftOnly { lhs: f(lhs) },
23            Self::RightOnly { rhs } => Pairing::RightOnly { rhs: f(rhs) },
24        }
25    }
26
27    pub fn lhs(&self) -> Option<&T> {
28        match self {
29            Self::Both { lhs, .. } | Self::LeftOnly { lhs } => Some(lhs),
30            Self::RightOnly { .. } => None,
31        }
32    }
33
34    pub fn rhs(&self) -> Option<&T> {
35        match self {
36            Self::Both { rhs, .. } | Self::RightOnly { rhs } => Some(rhs),
37            Self::LeftOnly { .. } => None,
38        }
39    }
40
41    /// Every side that exists, lhs first.
42    pub fn sides(&self) -> Vec<&T> {
43        match self {
44            Self::Both { lhs, rhs } => vec![lhs, rhs],
45            Self::LeftOnly { lhs } => vec![lhs],
46            Self::RightOnly { rhs } => vec![rhs],
47        }
48    }
49
50    /// Every side that exists, lhs first.
51    pub fn sides_mut(&mut self) -> Vec<&mut T> {
52        match self {
53            Self::Both { lhs, rhs } => vec![lhs, rhs],
54            Self::LeftOnly { lhs } => vec![lhs],
55            Self::RightOnly { rhs } => vec![rhs],
56        }
57    }
58}
59
60/// One region of a side's tree.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Region {
63    pub id: u32,
64    pub fold_state_id: u32,
65    pub range: Range,
66    pub tags: Vec<String>,
67    pub visibility: Visibility,
68    pub node: Node,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum Node {
73    Leaf {
74        alignment_id: u32,
75        changed: Vec<Span>,
76    },
77    Fold {
78        children: Vec<Region>,
79    },
80}
81
82impl Region {
83    /// A leaf's row alignment; a fold has none.
84    pub fn alignment_id(&self) -> Option<u32> {
85        match self.node {
86            Node::Leaf { alignment_id, .. } => Some(alignment_id),
87            Node::Fold { .. } => None,
88        }
89    }
90}
91
92/// One side of the diffed file as a tree.
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct Source {
95    pub text: String,
96    /// The largest regions, in order.
97    pub regions: Vec<Region>,
98}
99
100impl Source {
101    /// Rebuild a side's tree from its preorder list with parent ids.
102    pub fn from_record(side: &types::Source) -> anyhow::Result<Self> {
103        fn children(
104            regions: &mut std::iter::Peekable<std::slice::Iter<'_, types::Region>>,
105            parent: u32,
106        ) -> Vec<Region> {
107            let mut out = Vec::new();
108            while let Some(region) = regions.next_if(|region| region.parent == parent) {
109                let node = match &region.kind {
110                    types::Kind::Leaf(leaf) => Node::Leaf {
111                        alignment_id: leaf.alignment_id,
112                        changed: leaf.changed.clone(),
113                    },
114                    types::Kind::Fold => Node::Fold {
115                        children: children(regions, region.id),
116                    },
117                };
118                out.push(Region {
119                    id: region.id,
120                    fold_state_id: region.fold_state_id,
121                    range: region.range,
122                    tags: region.tags.clone(),
123                    visibility: region.visibility.clone(),
124                    node,
125                });
126            }
127            out
128        }
129        let mut regions = side.regions.iter().peekable();
130        let tree = children(&mut regions, ROOT);
131        if let Some(stray) = regions.next() {
132            anyhow::bail!(
133                "region {} names parent {}, which does not precede it",
134                stray.id,
135                stray.parent
136            );
137        }
138        Ok(Self {
139            text: side.text.clone(),
140            regions: tree,
141        })
142    }
143
144    /// The side as a record: the tree flattened in preorder with parent ids.
145    pub fn to_record(&self) -> types::Source {
146        fn flatten(regions: &[Region], parent: u32, out: &mut Vec<types::Region>) {
147            for region in regions {
148                let kind = match &region.node {
149                    Node::Leaf {
150                        alignment_id,
151                        changed,
152                    } => types::Kind::Leaf(types::Leaf {
153                        alignment_id: *alignment_id,
154                        changed: changed.clone(),
155                    }),
156                    Node::Fold { .. } => types::Kind::Fold,
157                };
158                out.push(types::Region {
159                    id: region.id,
160                    parent,
161                    fold_state_id: region.fold_state_id,
162                    range: region.range,
163                    tags: region.tags.clone(),
164                    visibility: region.visibility.clone(),
165                    kind,
166                });
167                if let Node::Fold { children } = &region.node {
168                    flatten(children, region.id, out);
169                }
170            }
171        }
172        let mut regions = Vec::new();
173        flatten(&self.regions, ROOT, &mut regions);
174        types::Source {
175            text: self.text.clone(),
176            regions,
177        }
178    }
179}
180
181/// The contract's sides as the trees the SDK works on. diffr calls this once
182/// on the way in, so a plugin is handed the pairing rather than the record.
183pub fn sides(sides: &types::SourceSides) -> anyhow::Result<Pairing<Source>> {
184    Ok(match sides {
185        types::SourceSides::Both((lhs, rhs)) => Pairing::Both {
186            lhs: Source::from_record(lhs)?,
187            rhs: Source::from_record(rhs)?,
188        },
189        types::SourceSides::LeftOnly(lhs) => Pairing::LeftOnly {
190            lhs: Source::from_record(lhs)?,
191        },
192        types::SourceSides::RightOnly(rhs) => Pairing::RightOnly {
193            rhs: Source::from_record(rhs)?,
194        },
195    })
196}
197
198// ── region helpers ────────────────────────────────────────────────────────
199
200pub fn walk(regions: &[Region], visit: &mut impl FnMut(&Region)) {
201    for region in regions {
202        visit(region);
203        if let Node::Fold { children } = &region.node {
204            walk(children, visit);
205        }
206    }
207}
208
209pub fn walk_mut(regions: &mut [Region], visit: &mut impl FnMut(&mut Region)) {
210    for region in regions {
211        visit(region);
212        if let Node::Fold { children } = &mut region.node {
213            walk_mut(children, visit);
214        }
215    }
216}
217
218/// What the other side holds, to tell one side's paired regions from
219/// one-sided ones. A leaf is paired when the other side holds its
220/// `alignment_id`: its rows line up with a leaf there. A fold is paired when
221/// a region on the other side shares its `fold_state_id`: the fold the
222/// matcher paired it with, or a region linked to that fold. Only the other
223/// side counts, since a link merges fold states within a side as well.
224#[derive(Default)]
225pub struct OtherSide {
226    leaf_ids: BTreeSet<u32>,
227    fold_state_ids: BTreeSet<u32>,
228}
229
230impl OtherSide {
231    /// What the other side, whose regions are `other`, holds.
232    pub fn of(other: &[Region]) -> Self {
233        let mut side = Self::default();
234        walk(other, &mut |region| {
235            if let Some(alignment_id) = region.alignment_id() {
236                side.leaf_ids.insert(alignment_id);
237            }
238            side.fold_state_ids.insert(region.fold_state_id);
239        });
240        side
241    }
242
243    /// Whether `region`, on the side opposite this one, is paired.
244    pub fn pairs(&self, region: &Region) -> bool {
245        match region.node {
246            Node::Leaf { alignment_id, .. } => self.leaf_ids.contains(&alignment_id),
247            Node::Fold { .. } => self.fold_state_ids.contains(&region.fold_state_id),
248        }
249    }
250}
251
252/// True when no leaf under `region`, itself included, is paired with the
253/// other side.
254///
255/// Newness is the leaves' answer. A fold covers its body alone — the
256/// header line that opens it belongs to the leaf before it — so the leaves
257/// inside a body fold are the body, and a body grown in place, whose lines
258/// still align, is a rewrite rather than a removal. A fold state, on the
259/// other hand, says only whether the matcher paired the two nodes, and a
260/// file diffed by line pairs no folds at all while its lines still align.
261/// So a fold is new, or deleted, exactly when nothing inside it lines up
262/// with the other side.
263pub fn one_sided(region: &Region, other: &OtherSide) -> bool {
264    let mut paired = false;
265    walk(std::slice::from_ref(region), &mut |inner| {
266        paired |= matches!(inner.node, Node::Leaf { .. }) && other.pairs(inner);
267    });
268    !paired
269}
270
271/// The lines a region spans: for a fold, exactly the lines collapsing it
272/// hides. A body fold spans its body alone, starting the line after the
273/// `{` or `:` that opens it.
274pub fn line_count(region: &Region) -> usize {
275    region.range.lines().len()
276}
277
278pub fn is_fold(region: &Region) -> bool {
279    matches!(region.node, Node::Fold { .. })
280}
281
282pub fn has_tag(region: &Region, tag: &str) -> bool {
283    region.tags.iter().any(|own| own == tag)
284}
285
286/// How many lines (attributes and the signature) may sit between a
287/// docstring and the line its function's body opens on.
288const MAX_SIGNATURE_LINES: usize = 12;
289
290/// The `id` of the docstring of `body`, a function body fold on `side`, as
291/// `plugin`'s queries tag it (`<plugin>:docstring`, from
292/// `plugins/shared/queries/<language>-docstrings.scm`): the body's first
293/// fold child when it starts where the body does (Python), or else the fold
294/// just before the body in document order, with
295/// only a signature's worth of lines between them. The search runs back
296/// through the body's siblings and then out through the folds that enclose
297/// it, so a fold wrapping the whole function (another plugin's scope) does
298/// not hide the docstring above it; it stops at a fold carrying one of
299/// `plugin`'s own tags, such as the body of an enclosing function. A plugin
300/// that collapses a body links it to its docstring, so the two open and
301/// close together.
302pub fn docstring_of(side: &Source, body: &Region, plugin: &str) -> Option<u32> {
303    let tag = format!("{plugin}:docstring");
304    let own = format!("{plugin}:");
305    if let Node::Fold { children } = &body.node {
306        if let Some(first) = children
307            .iter()
308            .find(|child| is_fold(child))
309            .filter(|first| has_tag(first, &tag) && first.range.start.line == body.range.start.line)
310        {
311            return Some(first.id);
312        }
313    }
314    let path = path_to(&side.regions, body.id).expect("the body is on this side");
315    let mut between = 0;
316    // From the body's own siblings outwards: at each level, the regions
317    // before the one on the path.
318    let mut level: &[Region] = &side.regions;
319    let mut levels = Vec::new();
320    for &index in &path {
321        levels.push((level, index));
322        if let Node::Fold { children } = &level[index].node {
323            level = children;
324        }
325    }
326    for (depth, (siblings, index)) in levels.iter().enumerate().rev() {
327        if depth + 1 < path.len() {
328            // Leaving the fold at `siblings[index]` for the regions before it.
329            let parent = &siblings[*index];
330            if parent.tags.iter().any(|tag| tag.starts_with(&own)) {
331                return None;
332            }
333        }
334        for region in siblings[..*index].iter().rev() {
335            if is_fold(region) {
336                return has_tag(region, &tag).then_some(region.id);
337            }
338            between += line_count(region);
339            if between > MAX_SIGNATURE_LINES {
340                return None;
341            }
342        }
343    }
344    None
345}
346
347/// Child indices from the root down to the region with this `id`.
348pub fn path_to(regions: &[Region], id: u32) -> Option<Vec<usize>> {
349    for (index, region) in regions.iter().enumerate() {
350        if region.id == id {
351            return Some(vec![index]);
352        }
353        if let Node::Fold { children } = &region.node {
354            if let Some(mut path) = path_to(children, id) {
355                path.insert(0, index);
356                return Some(path);
357            }
358        }
359    }
360    None
361}
362
363/// The sibling list holding the region with this `id`.
364pub fn siblings_of(regions: &[Region], id: u32) -> Option<&[Region]> {
365    if regions.iter().any(|region| region.id == id) {
366        return Some(regions);
367    }
368    regions.iter().find_map(|region| match &region.node {
369        Node::Fold { children } => siblings_of(children, id),
370        Node::Leaf { .. } => None,
371    })
372}
373
374/// The before side, and what the after side holds.
375pub fn before_and_after_ids(sides: &Pairing<Source>) -> Option<(&Source, OtherSide)> {
376    match sides {
377        Pairing::Both { lhs, rhs } => Some((lhs, OtherSide::of(&rhs.regions))),
378        Pairing::LeftOnly { lhs } => Some((lhs, OtherSide::default())),
379        Pairing::RightOnly { .. } => None,
380    }
381}
382
383/// Each side that exists, with what the other side holds.
384pub fn sides_with_other_ids(sides: &Pairing<Source>) -> Vec<(&Source, OtherSide)> {
385    match sides {
386        Pairing::Both { lhs, rhs } => vec![
387            (lhs, OtherSide::of(&rhs.regions)),
388            (rhs, OtherSide::of(&lhs.regions)),
389        ],
390        Pairing::LeftOnly { lhs } => vec![(lhs, OtherSide::default())],
391        Pairing::RightOnly { rhs } => vec![(rhs, OtherSide::default())],
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use crate::types::Position;
399
400    /// A one-line leaf, or a fold holding one, with `id`. A leaf's
401    /// `alignment_id` is `alignment`; the fold's child takes ids of its own.
402    fn region(id: u32, alignment: u32, fold_state_id: u32, line: u32, fold: bool) -> Region {
403        let range = Range {
404            start: Position { line, column: 0 },
405            end: Position {
406                line: line + 1,
407                column: 0,
408            },
409        };
410        let leaf = Region {
411            id,
412            fold_state_id,
413            range,
414            tags: vec![],
415            visibility: Visibility::default(),
416            node: Node::Leaf {
417                alignment_id: alignment,
418                changed: vec![],
419            },
420        };
421        if !fold {
422            return leaf;
423        }
424        Region {
425            node: Node::Fold {
426                children: vec![Region {
427                    id: id + 100,
428                    fold_state_id: id + 100,
429                    node: Node::Leaf {
430                        alignment_id: alignment + 100,
431                        changed: vec![],
432                    },
433                    ..leaf.clone()
434                }],
435            },
436            ..leaf
437        }
438    }
439
440    #[test]
441    fn leaves_pair_by_alignment_id_and_folds_by_fold_state_on_the_other_side() {
442        // lhs: a paired leaf (1), a matched fold (2), and an unmatched fold
443        // (3) linked on this side with a leaf (4). rhs: the leaf paired with
444        // 1, which shares its alignment id (5), and the fold matched with 2,
445        // which shares its fold state (6).
446        let lhs = vec![
447            region(1, 1, 1, 0, false),
448            region(2, 2, 2, 1, true),
449            region(3, 3, 3, 2, true),
450            region(4, 4, 3, 3, false),
451        ];
452        let rhs = vec![region(5, 1, 1, 0, false), region(6, 6, 2, 1, true)];
453        let from_lhs = OtherSide::of(&rhs);
454        let from_rhs = OtherSide::of(&lhs);
455        assert!(from_lhs.pairs(&lhs[0]));
456        assert!(from_lhs.pairs(&lhs[1]), "the matched fold is paired");
457        assert!(
458            !from_lhs.pairs(&lhs[2]),
459            "a fold state shared only on its own side does not pair"
460        );
461        assert!(
462            !from_lhs.pairs(&lhs[3]),
463            "a leaf pairs by alignment id alone"
464        );
465        assert!(from_rhs.pairs(&rhs[0]));
466        assert!(from_rhs.pairs(&rhs[1]));
467    }
468
469    #[test]
470    fn newness_comes_from_the_leaves_not_the_fold_state() {
471        // A fold matched across sides (both in fold state 2) whose leaves
472        // align with nothing there: nothing inside it survived, so it is
473        // one-sided, whatever the matcher made of the two nodes.
474        let moved = region(2, 2, 2, 1, true);
475        let elsewhere = region(6, 6, 2, 1, true);
476        let other = OtherSide::of(std::slice::from_ref(&elsewhere));
477        assert!(other.pairs(&moved), "the fold itself is matched");
478        assert!(one_sided(&moved, &other));
479        // The same fold, with a leaf inside it that does align: a rewrite.
480        let kept = region(6, 2, 9, 1, true);
481        let other = OtherSide::of(std::slice::from_ref(&kept));
482        assert!(!other.pairs(&moved), "the folds are not matched");
483        assert!(!one_sided(&moved, &other));
484    }
485
486    #[test]
487    fn a_tree_survives_the_round_trip_through_its_record() {
488        let source = Source {
489            text: "a\nb\nc\n".to_owned(),
490            regions: vec![region(1, 1, 1, 0, false), region(2, 2, 2, 1, true)],
491        };
492        let record = source.to_record();
493        assert_eq!(
494            record
495                .regions
496                .iter()
497                .map(|region| (region.id, region.parent))
498                .collect::<Vec<_>>(),
499            [(1, ROOT), (2, ROOT), (102, 2)]
500        );
501        assert_eq!(Source::from_record(&record).unwrap(), source);
502    }
503}