diff --git a/diffr/crates/diffr-core/src/plugin/cursor.rs b/diffr/crates/diffr-core/src/plugin/cursor.rs index 8b846ca41..194896ef0 100644 --- a/diffr/crates/diffr-core/src/plugin/cursor.rs +++ b/diffr/crates/diffr-core/src/plugin/cursor.rs @@ -2,6 +2,7 @@ //! host walks the trees, positioning the cursor on each node before calling //! the plugin; the plugin reads and edits through it, and edits apply //! immediately. +use crate::hash::DftHashMap; use crate::pairing::Pairing; use crate::protocol::{FileChange, Node, Region, Source, SourcePos, SourceRange, Span, Visibility}; use std::collections::BTreeSet; @@ -13,6 +14,14 @@ pub enum Side { Rhs, } +/// A region's side, its parent fold, and its path from the side's root. +#[derive(Clone)] +struct Place { + side: Side, + parent: Option, + path: Vec, +} + /// One region as a plugin sees it: shallow, with its parent and its /// children's ids rather than nested subtrees. #[derive(Clone, Debug, PartialEq, Eq)] @@ -79,13 +88,16 @@ pub enum MoveError { /// A file's trees, the next unused IDs, and where the current walk stands. pub struct Cursor { pub file: FileChange, - pub sides: Pairing, + sides: Pairing, /// The region the cursor is on: the one the current callback visits. pub id: u32, /// IDs from here up were made during the current walk, which skips them. limit: u32, next_region_id: u32, next_alignment_id: u32, + places: DftHashMap, + /// The regions in each fold state. + states: DftHashMap>, } impl Cursor { @@ -98,14 +110,25 @@ impl Cursor { }; let mut next_region_id = 1; let mut next_alignment_id = 0; + let mut states: DftHashMap> = DftHashMap::default(); for source in sides.sides() { walk(top(source), &mut |region| { + states + .entry(region.fold_state_id) + .or_default() + .push(region.id); next_region_id = next_region_id.max(region.id + 1); if let Some(alignment) = region.alignment_id() { next_alignment_id = next_alignment_id.max(alignment + 1); } }); } + let mut places = DftHashMap::default(); + for (side, source) in [(Side::Lhs, sides.lhs()), (Side::Rhs, sides.rhs())] { + if let Some(source) = source { + place(top(source), 0, None, side, &[], &mut places); + } + } Ok(Self { file, sides, @@ -113,9 +136,16 @@ impl Cursor { limit: next_region_id, next_region_id, next_alignment_id, + places, + states, }) } + /// The edited sides, once the plugins are done. + pub fn into_sides(self) -> Pairing { + self.sides + } + /// Start a walk over the nodes that exist now, from the first region. pub fn rewind(&mut self) { self.id = self.top_level()[0]; @@ -160,52 +190,22 @@ impl Cursor { /// One region and its immediate children. pub fn get(&self, id: u32) -> Result { - fn find( - regions: &[Region], - parent: Option, - id: u32, - side: Side, - ) -> Option { - for region in regions { - if region.id == id { - let children = match ®ion.node { - Node::Leaf { .. } => vec![], - Node::Fold { children, .. } => { - children.iter().map(|child| child.id).collect() - } - }; - return Some(view(region, parent, side, children)); - } - if let Node::Fold { children, .. } = ®ion.node { - if let Some(node) = find(children, Some(region.id), id, side) { - return Some(node); - } - } - } - None - } - for (side, source) in [(Side::Lhs, self.sides.lhs()), (Side::Rhs, self.sides.rhs())] { - if let Some(node) = source.and_then(|source| find(top(source), None, id, side)) { - return Ok(node); - } - } - Err(MoveError::NoRegion(id)) + let place = self.place(id)?; + let region = self.region(id)?; + let children = region.children().iter().map(|child| child.id).collect(); + Ok(view(region, place.parent, place.side, children)) } /// Original text covered by this region's whole-line range. pub fn text(&self, id: u32) -> Result { - for source in self.sides.sides() { - if let Some(region) = find(top(source), id) { - let range = region.range.lines(); - return Ok(source - .text - .split_inclusive('\n') - .skip(range.start as usize) - .take(range.len()) - .collect()); - } - } - Err(MoveError::NoRegion(id)) + let range = self.region(id)?.range.lines(); + Ok(self + .source_on(self.place(id)?.side) + .text + .split_inclusive('\n') + .skip(range.start as usize) + .take(range.len()) + .collect()) } /// Summary of visible collapsed rows and open-line runs beneath a node. @@ -256,43 +256,36 @@ impl Cursor { } } } - Ok(summarize(region_of(&self.sides, id)?)) + Ok(summarize(self.region(id)?)) } /// Resolve an ordered sibling sequence across sides. None means conflicting /// alignment; an empty sequence means all members are one-sided. pub fn matching_siblings(&self, ids: &[u32]) -> Result>, MoveError> { check_regions(ids, Grouping::Join)?; - let own = self - .sides - .sides() - .into_iter() - .find(|s| find(top(s), ids[0]).is_some()) - .ok_or(MoveError::NoRegion(ids[0]))?; - let nodes = ids - .iter() - .map(|id| find(top(own), *id).ok_or(MoveError::NoRegion(*id))) - .collect::, _>>()?; - let path = path_of(top(own), ids[0]).expect("the region was found above"); - let siblings = if path.len() == 1 { - top(own) - } else { - let Node::Fold { children, .. } = &at(top(own), &path[..path.len() - 1]).node else { - unreachable!("a region's parent is a fold") - }; - children + let first = self.place(ids[0])?; + let mut nodes = Vec::with_capacity(ids.len()); + for &id in ids { + // Every region must sit on the first one's side. + if self.place(id)?.side != first.side { + return Err(MoveError::NoRegion(id)); + } + nodes.push(self.region(id)?); + } + let siblings: Vec<_> = match first.parent { + Some(parent) => self + .region(parent)? + .children() + .iter() + .map(|region| region.id) + .collect(), + None => vec![ids[0]], }; - let siblings: Vec<_> = siblings.iter().map(|region| region.id).collect(); let start = siblings.iter().position(|id| *id == ids[0]).unwrap(); if siblings.get(start..start + ids.len()) != Some(ids) { return Err(MoveError::NotSiblings(ids.to_vec())); } - let other = if self.sides.lhs().is_some_and(|s| std::ptr::eq(s, own)) { - self.sides.rhs() - } else { - self.sides.lhs() - }; - let Some(other) = other else { + let Some(other) = side_of(&self.sides, first.side.other()) else { return Ok(Some(Vec::new())); }; fn matches(a: &Region, b: &Region) -> bool { @@ -357,17 +350,10 @@ impl Cursor { /// Whether this region contains changed bytes or an unpaired leaf on its own side. pub fn has_changes(&self, id: u32) -> Result { - let target = region_of(&self.sides, id)?; - let alignments = self.opposite_alignments(id); let mut changes = false; - walk(std::slice::from_ref(target), &mut |region| { - if let Node::Leaf { - alignment_id, - changed, - .. - } = ®ion.node - { - changes |= !changed.is_empty() || !alignments.contains(alignment_id); + walk(std::slice::from_ref(self.region(id)?), &mut |region| { + if let Node::Leaf { pair, changed, .. } = ®ion.node { + changes |= !changed.is_empty() || pair.is_none(); } }); Ok(changes) @@ -375,43 +361,29 @@ impl Cursor { /// Opposite-side leaf with the same alignment; folds and unmatched leaves return None. pub fn paired_leaf(&self, id: u32) -> Result, MoveError> { - let Some(alignment) = region_of(&self.sides, id)?.alignment_id() else { - return Ok(None); - }; - let mut paired = None; - if let Some(other) = self.opposite(id) { - walk(top(other), &mut |region| { - if region.alignment_id() == Some(alignment) { - paired = Some(region.id); - } - }); - } - Ok(paired) + Ok(match self.region(id)?.node { + Node::Leaf { pair, .. } => pair, + Node::Fold { .. } => None, + }) } /// All regions sharing this region's collapse state, including itself. pub fn linked_regions(&self, id: u32) -> Result, MoveError> { - let state = region_of(&self.sides, id)?.fold_state_id; - let mut ids = Vec::new(); - for tree in trees_ref(&self.sides) { - walk(tree, &mut |region| { - if region.fold_state_id == state { - ids.push(region.id); - } - }); - } + let state = self.region(id)?.fold_state_id; + let mut ids = self.states[&state].clone(); + // The lhs before the rhs, each in preorder. + ids.sort_by(|a, b| { + let (a, b) = (&self.places[a], &self.places[b]); + (a.side == Side::Rhs, &a.path).cmp(&(b.side == Side::Rhs, &b.path)) + }); Ok(ids) } /// No leaf in this subtree has an opposite-side match. pub fn is_one_sided(&self, id: u32) -> Result { - let target = region_of(&self.sides, id)?; - let alignments = self.opposite_alignments(id); let mut paired = false; - walk(std::slice::from_ref(target), &mut |region| { - paired |= region - .alignment_id() - .is_some_and(|id| alignments.contains(&id)); + walk(std::slice::from_ref(self.region(id)?), &mut |region| { + paired |= matches!(region.node, Node::Leaf { pair: Some(_), .. }); }); Ok(!paired) } @@ -450,113 +422,63 @@ impl Cursor { /// Split a leaf and its paired leaf at a relative line offset. Returns the new tails. pub fn cut(&mut self, id: u32, offset: u32) -> Result { - let Cursor { - sides, - next_region_id, - next_alignment_id, - .. - } = self; - let Some((side, path)) = trees(sides) - .into_iter() - .enumerate() - .find_map(|(side, tree)| Some((side, path_of(tree, id)?))) - else { - return Err(MoveError::NoRegion(id)); - }; - let (alignment, len) = { - let leaf = at(trees(sides).swap_remove(side), &path); - let Some(alignment) = leaf.alignment_id() else { - return Err(MoveError::CutFold(id)); - }; - (alignment, leaf.range.lines().len() as u32) + let leaf = self.region(id)?; + let Node::Leaf { pair, .. } = leaf.node else { + return Err(MoveError::CutFold(id)); }; + let len = leaf.range.lines().len() as u32; if !(0 < offset && offset < len) { return Err(MoveError::CutOutside { id, offset, len }); } // Validate both sides before allocating IDs or changing either tree. - let paths = trees_ref(sides) - .into_iter() - .enumerate() - .map(|(tree_side, tree)| { - let path = if tree_side == side { - Some(path.clone()) - } else { - path_where(tree, &|region| region.alignment_id() == Some(alignment)) - }; - if let Some(path) = &path { - if at(tree, path).range.lines().len() as u32 != len { - return Err(MoveError::UnevenSides(id)); - } - } - Ok(path) - }) - .collect::, MoveError>>()?; - let piece_alignment = *next_alignment_id; - *next_alignment_id += 1; - // Both tails' ids first, so each can name the other as its pair. - let piece_ids: Vec> = paths - .iter() - .map(|path| { - path.as_ref().map(|_| { - let id = *next_region_id; - *next_region_id += 1; - id - }) - }) - .collect(); - let piece_state = piece_ids.iter().flatten().next().copied(); - let has_lhs = sides.lhs().is_some(); + let mut cuts = vec![self.place(id)?.clone()]; + if let Some(pair) = pair { + if self.region(pair)?.range.lines().len() as u32 != len { + return Err(MoveError::UnevenSides(id)); + } + cuts.push(self.place(pair)?.clone()); + } + // The lhs tail is numbered first, and its id is the tails' fold state. + cuts.sort_by_key(|place| place.side == Side::Rhs); + let piece_alignment = self.next_alignment_id; + self.next_alignment_id += 1; + let tails: Vec = (self.next_region_id..).take(cuts.len()).collect(); + self.next_region_id += cuts.len() as u32; let (mut lhs, mut rhs) = (None, None); - for (tree_side, (tree, path)) in trees(sides).into_iter().zip(paths).enumerate() { - let (Some(path), Some(piece_id), Some(piece_state)) = - (path, piece_ids[tree_side], piece_state) - else { - continue; - }; - let partner = - piece_ids - .iter() - .enumerate() - .find_map(|(other, id)| if other == tree_side { None } else { *id }); - let (index, parent) = path.split_last().expect("a path is never empty"); - let list = siblings(tree, parent); - if tree_side == 0 && has_lhs { - lhs = Some(piece_id); - } else { - rhs = Some(piece_id); + for (Place { side, parent, path }, &tail) in cuts.into_iter().zip(&tails) { + let partner = tails.iter().copied().find(|&other| other != tail); + let (&index, holder) = path.split_last().expect("a path is never empty"); + let list = siblings(tree_mut(&mut self.sides, side), holder); + let leaf = list.remove(index); + let pieces = split(leaf, offset, tail, piece_alignment, tails[0], partner); + list.splice(index..index, pieces); + // The tail and every later sibling moved one index along. + place(list, index + 1, parent, side, holder, &mut self.places); + match side { + Side::Lhs => lhs = Some(tail), + Side::Rhs => rhs = Some(tail), } - let leaf = list.remove(*index); - let pieces = split( - leaf, - offset, - piece_id, - piece_alignment, - piece_state, - partner, - ); - list.splice(*index..*index, pieces); } + self.states.insert(tails[0], tails); Ok(region_ids(lhs, rhs)) } /// Wrap consecutive siblings on each side in new open, unlabelled folds. pub fn join(&mut self, ids: &[u32]) -> Result { - let Cursor { - sides, - next_region_id, - .. - } = self; check_regions(ids, Grouping::Join)?; - for id in ids { - region_of(sides, *id)?; - } + let located = ids + .iter() + .map(|id| self.place(*id)) + .collect::, MoveError>>()?; // Compute and validate every sibling range before draining either side. let mut groups = Vec::new(); - for tree in trees_ref(sides) { - let mut paths: Vec> = - ids.iter().filter_map(|id| path_of(tree, *id)).collect(); + for side in [Side::Lhs, Side::Rhs] { + let mut paths: Vec<&Vec> = located + .iter() + .filter(|place| place.side == side) + .map(|place| &place.path) + .collect(); if paths.is_empty() { - groups.push(None); continue; } if paths.len() < 2 { @@ -574,33 +496,32 @@ impl Cursor { if !adjacent { return Err(MoveError::NotSiblings(ids.to_vec())); } - groups.push(Some((parent.to_vec(), first, paths.len()))); + groups.push((side, parent.to_vec(), first, paths.len())); } let mut state = None; - let has_lhs = sides.lhs().is_some(); let (mut lhs, mut rhs) = (None, None); - for (side, (tree, group)) in trees(sides).into_iter().zip(groups).enumerate() { - let Some((parent, first, count)) = group else { - continue; - }; + for (side, parent, first, count) in groups { + let tree = tree_mut(&mut self.sides, side); // A joined fold sits in its parent's body. - let Node::Fold { indent, .. } = at(tree, &parent).node else { + let holder = at(tree, &parent); + let Node::Fold { indent, .. } = holder.node else { unreachable!("a path descends through folds"); }; + let holder = holder.id; let list = siblings(tree, &parent); let children: Vec = list.drain(first..first + count).collect(); let range = SourceRange { start: children[0].range.start, end: children[children.len() - 1].range.end, }; - let id = *next_region_id; - *next_region_id += 1; - if side == 0 && has_lhs { - lhs = Some(id); - } else { - rhs = Some(id); + let id = self.next_region_id; + self.next_region_id += 1; + match side { + Side::Lhs => lhs = Some(id), + Side::Rhs => rhs = Some(id), } let fold_state_id = *state.get_or_insert(id); + self.states.entry(fold_state_id).or_default().push(id); list.insert( first, Region { @@ -616,71 +537,110 @@ impl Cursor { }, }, ); + // The new fold, its children, and every later sibling moved. + place(list, first, Some(holder), side, &parent, &mut self.places); } Ok(region_ids(lhs, rhs)) } /// Merge the fold states of `ids` into the first's; all collapse if any was. pub fn link(&mut self, ids: &[u32]) -> Result<(), MoveError> { - let sides = &mut self.sides; check_regions(ids, Grouping::Link)?; - let state = region_of(sides, ids[0])?.fold_state_id; + let state = self.region(ids[0])?.fold_state_id; let mut collapsed = false; for &id in ids { - collapsed |= region_of(sides, id)?.visibility.collapsed; + collapsed |= self.region(id)?.visibility.collapsed; } - let states = ids + let merged = ids .iter() - .map(|id| Ok(region_of(sides, *id)?.fold_state_id)) + .map(|id| Ok(self.region(*id)?.fold_state_id)) .collect::, MoveError>>()?; - for tree in trees(sides) { - walk_mut(tree, &mut |region| { - if states.contains(®ion.fold_state_id) { - region.fold_state_id = state; - region.visibility.collapsed = collapsed; - } - }); + let mut members = Vec::new(); + for old in merged { + members.extend( + self.states + .remove(&old) + .expect("every fold state is indexed"), + ); + } + let Cursor { sides, places, .. } = self; + for id in &members { + let place = &places[id]; + let region = at_mut(tree_mut(sides, place.side), &place.path); + region.fold_state_id = state; + region.visibility.collapsed = collapsed; } + self.states.insert(state, members); Ok(()) } /// Set the shared collapsed state. pub fn set_collapsed(&mut self, region: u32, collapsed: bool) -> Result<(), MoveError> { - let state = region_of(&self.sides, region)?.fold_state_id; - for tree in trees(&mut self.sides) { - walk_mut(tree, &mut |region| { - if region.fold_state_id == state { - region.visibility.collapsed = collapsed; - } - }); + let state = self.region(region)?.fold_state_id; + let Cursor { + sides, + places, + states, + .. + } = self; + for id in &states[&state] { + let place = &places[id]; + at_mut(tree_mut(sides, place.side), &place.path) + .visibility + .collapsed = collapsed; } Ok(()) } /// Set or clear a region's label. pub fn set_label(&mut self, region: u32, label: Option) -> Result<(), MoveError> { - region_mut(&mut self.sides, region)?.visibility.label = label.unwrap_or_default(); + self.region_mut(region)?.visibility.label = label.unwrap_or_default(); Ok(()) } - /// The side not holding `id`, if the file has one. - fn opposite(&self, id: u32) -> Option<&Source> { - if self.sides.lhs().is_some_and(|s| find(top(s), id).is_some()) { - self.sides.rhs() - } else { - self.sides.lhs() - } + fn place(&self, id: u32) -> Result<&Place, MoveError> { + self.places.get(&id).ok_or(MoveError::NoRegion(id)) } - /// Every leaf alignment on the side not holding `id`. - fn opposite_alignments(&self, id: u32) -> BTreeSet { - let mut alignments = BTreeSet::new(); - if let Some(other) = self.opposite(id) { - walk(top(other), &mut |region| { - alignments.extend(region.alignment_id()); - }); + /// The region with this `id`, on whichever side holds it. + fn region(&self, id: u32) -> Result<&Region, MoveError> { + let place = self.place(id)?; + Ok(at(top(self.source_on(place.side)), &place.path)) + } + + fn region_mut(&mut self, id: u32) -> Result<&mut Region, MoveError> { + let place = self.places.get(&id).ok_or(MoveError::NoRegion(id))?; + Ok(at_mut(tree_mut(&mut self.sides, place.side), &place.path)) + } + + fn source_on(&self, side: Side) -> &Source { + side_of(&self.sides, side).expect("an indexed region's side exists") + } +} + +impl Side { + fn other(self) -> Self { + match self { + Self::Lhs => Self::Rhs, + Self::Rhs => Self::Lhs, } - alignments + } +} + +/// Index `regions[start..]` and everything beneath them: the children of +/// `parent`, which sits at `prefix`. +fn place( + regions: &[Region], + start: usize, + parent: Option, + side: Side, + prefix: &[usize], + places: &mut DftHashMap, +) { + for (index, region) in regions.iter().enumerate().skip(start) { + let path = [prefix, &[index]].concat(); + place(region.children(), 0, Some(region.id), side, &path, places); + places.insert(region.id, Place { side, parent, path }); } } @@ -759,55 +719,19 @@ fn walk(regions: &[Region], visit: &mut impl FnMut(&Region)) { } } -fn walk_mut(regions: &mut [Region], visit: &mut impl FnMut(&mut Region)) { - for region in regions { - visit(region); - if let Node::Fold { children, .. } = &mut region.node { - walk_mut(children, visit); - } - } -} - -/// Each side's tree, as a one-region list holding its root. -fn trees(sides: &mut Pairing) -> Vec<&mut [Region]> { - match sides { - Pairing::Both { lhs, rhs } => vec![ - std::slice::from_mut(&mut lhs.root), - std::slice::from_mut(&mut rhs.root), - ], - Pairing::LeftOnly { lhs } => vec![std::slice::from_mut(&mut lhs.root)], - Pairing::RightOnly { rhs } => vec![std::slice::from_mut(&mut rhs.root)], - } -} - -fn trees_ref(sides: &Pairing) -> Vec<&[Region]> { - sides.sides().into_iter().map(top).collect() -} - /// A side's tree, as a one-region list holding its root. fn top(source: &Source) -> &[Region] { std::slice::from_ref(&source.root) } -/// Child indices from the root down to the first region `is` accepts. -fn path_where(regions: &[Region], is: &impl Fn(&Region) -> bool) -> Option> { - for (index, region) in regions.iter().enumerate() { - if is(region) { - return Some(vec![index]); - } - if let Node::Fold { children, .. } = ®ion.node { - if let Some(mut path) = path_where(children, is) { - path.insert(0, index); - return Some(path); - } - } - } - None -} - -/// Child indices from the root down to the region with this `id`. -fn path_of(regions: &[Region], id: u32) -> Option> { - path_where(regions, &|region| region.id == id) +/// A side's tree, as a one-region list holding its root. +fn tree_mut(sides: &mut Pairing, side: Side) -> &mut [Region] { + let source = match (side, sides) { + (Side::Lhs, Pairing::Both { lhs, .. } | Pairing::LeftOnly { lhs }) => lhs, + (Side::Rhs, Pairing::Both { rhs, .. } | Pairing::RightOnly { rhs }) => rhs, + _ => unreachable!("an indexed region's side exists"), + }; + std::slice::from_mut(&mut source.root) } /// The region at a path. @@ -820,6 +744,17 @@ fn at<'a>(regions: &'a [Region], path: &[usize]) -> &'a Region { } } +fn at_mut<'a>(regions: &'a mut [Region], path: &[usize]) -> &'a mut Region { + let (&index, rest) = path.split_first().expect("a path is never empty"); + if rest.is_empty() { + return &mut regions[index]; + } + match &mut regions[index].node { + Node::Fold { children, .. } => at_mut(children, rest), + Node::Leaf { .. } => unreachable!("a path descends through folds"), + } +} + /// The children of the fold at `parent`: the sibling list a path's last /// index points into. The root has no siblings, so `parent` is never empty. fn siblings<'a>(regions: &'a mut [Region], parent: &[usize]) -> &'a mut Vec { @@ -834,47 +769,6 @@ fn siblings<'a>(regions: &'a mut [Region], parent: &[usize]) -> &'a mut Vec, id: u32) -> Result<&Region, MoveError> { - trees_ref(sides) - .into_iter() - .find_map(|tree| find(tree, id)) - .ok_or(MoveError::NoRegion(id)) -} - -fn region_mut(sides: &mut Pairing, id: u32) -> Result<&mut Region, MoveError> { - trees(sides) - .into_iter() - .find_map(|tree| find_mut(tree, id)) - .ok_or(MoveError::NoRegion(id)) -} - -fn find(regions: &[Region], id: u32) -> Option<&Region> { - regions.iter().find_map(|region| { - if region.id == id { - return Some(region); - } - match ®ion.node { - Node::Fold { children, .. } => find(children, id), - Node::Leaf { .. } => None, - } - }) -} - -fn find_mut(regions: &mut [Region], id: u32) -> Option<&mut Region> { - for region in regions { - if region.id == id { - return Some(region); - } - if let Node::Fold { children, .. } = &mut region.node { - if let Some(found) = find_mut(children, id) { - return Some(found); - } - } - } - None -} - /// A leaf split at relative line `offset`. The first piece keeps the leaf's /// identity and pair; the second takes `id`, `alignment_id`, /// `fold_state_id` and `pair`, the other side's new tail. @@ -967,4 +861,4 @@ fn region_ids(lhs: Option, rhs: Option) -> RegionIds { mod mutations; #[cfg(test)] -mod tests; +pub(crate) mod tests; diff --git a/diffr/crates/diffr-core/src/plugin/cursor/mutations.rs b/diffr/crates/diffr-core/src/plugin/cursor/mutations.rs index 7d088f1ea..84a8eb2c8 100644 --- a/diffr/crates/diffr-core/src/plugin/cursor/mutations.rs +++ b/diffr/crates/diffr-core/src/plugin/cursor/mutations.rs @@ -1,3 +1,4 @@ +use super::tests::name_pairs; use super::*; use crate::protocol::Span; use crate::protocol::{FileRef, FileStatus}; @@ -96,14 +97,12 @@ fn entry() -> FileChange { } fn both(lhs: Vec, rhs: Vec) -> Cursor { - Cursor::new( - entry(), - Pairing::Both { - lhs: source(100, lhs), - rhs: source(101, rhs), - }, - ) - .expect("a region") + let mut sides = Pairing::Both { + lhs: source(100, lhs), + rhs: source(101, rhs), + }; + name_pairs(&mut sides); + Cursor::new(entry(), sides).expect("a region") } type Shape = (u32, Option, u32, u32, u32, bool, String); diff --git a/diffr/crates/diffr-core/src/plugin/cursor/tests.rs b/diffr/crates/diffr-core/src/plugin/cursor/tests.rs index f2a57253a..dea564202 100644 --- a/diffr/crates/diffr-core/src/plugin/cursor/tests.rs +++ b/diffr/crates/diffr-core/src/plugin/cursor/tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::hash::DftHashMap; use crate::protocol::{FileRef, FileStatus}; /// A test side's root. Its id stays clear of the regions' ids and differs @@ -8,6 +9,35 @@ pub(crate) fn test_root(regions: Vec) -> Region { Region::root(id, regions) } +/// Set each leaf's `pair` from the fixture's alignment ids. +pub(crate) fn name_pairs(sides: &mut Pairing) { + fn name(regions: &mut [Region], other: &DftHashMap) { + for region in regions { + match &mut region.node { + Node::Leaf { + alignment_id, pair, .. + } => *pair = other.get(alignment_id).copied(), + Node::Fold { children, .. } => name(children, other), + } + } + } + let Pairing::Both { lhs, rhs } = sides else { + return; + }; + let ids = |source: &Source| { + let mut ids = DftHashMap::default(); + walk(top(source), &mut |region| { + if let Some(alignment) = region.alignment_id() { + ids.insert(alignment, region.id); + } + }); + ids + }; + let (lhs_ids, rhs_ids) = (ids(lhs), ids(rhs)); + name(std::slice::from_mut(&mut lhs.root), &rhs_ids); + name(std::slice::from_mut(&mut rhs.root), &lhs_ids); +} + fn data(view: RegionView) -> RegionView { view } @@ -37,7 +67,8 @@ fn first(ids: &RegionIds) -> u32 { } /// A cursor over `sides`, with a manifest entry naming the same sides. -fn cursor(sides: Pairing) -> Cursor { +fn cursor(mut sides: Pairing) -> Cursor { + name_pairs(&mut sides); let file = match &sides { Pairing::Both { .. } => Pairing::Both { lhs: file_ref(), diff --git a/diffr/crates/diffr-core/src/present.rs b/diffr/crates/diffr-core/src/present.rs index 61df8df31..3d2675a51 100644 --- a/diffr/crates/diffr-core/src/present.rs +++ b/diffr/crates/diffr-core/src/present.rs @@ -1,6 +1,5 @@ //! What a diff looks like once the plugins have shaped it: the changed //! lines that stay visible. -use crate::hash::DftHashSet; use crate::pairing::Pairing; use crate::protocol::{Diff, LineRange, Node, Region, Source, StructuralChanges, Visibility}; @@ -48,19 +47,8 @@ struct ChangeCoverage { } fn change_coverage(sides: &Pairing) -> ChangeCoverage { - fn alignments(regions: &[Region], out: &mut DftHashSet) { - for region in regions { - match ®ion.node { - Node::Leaf { alignment_id, .. } => { - out.insert(*alignment_id); - } - Node::Fold { children, .. } => alignments(children, out), - } - } - } fn collect( regions: &[Region], - other: &DftHashSet, hidden: bool, all: &mut Vec, visible: &mut Vec, @@ -68,13 +56,9 @@ fn change_coverage(sides: &Pairing) -> ChangeCoverage { for region in regions { let hidden = hidden || region.visibility.collapsed; match ®ion.node { - Node::Leaf { - alignment_id, - changed, - .. - } => { + Node::Leaf { pair, changed, .. } => { let start = all.len(); - if other.contains(alignment_id) { + if pair.is_some() { all.extend(changed.iter().map(|span| [span.line, span.line + 1])); } else { let lines = region.range.lines(); @@ -84,20 +68,15 @@ fn change_coverage(sides: &Pairing) -> ChangeCoverage { visible.extend_from_slice(&all[start..]); } } - Node::Fold { children, .. } => collect(children, other, hidden, all, visible), + Node::Fold { children, .. } => collect(children, hidden, all, visible), } } } - fn side(source: Option<&Source>, other: Option<&Source>) -> (Vec, Vec) { - let mut paired = DftHashSet::default(); - if let Some(other) = other { - alignments(std::slice::from_ref(&other.root), &mut paired); - } + fn side(source: Option<&Source>) -> (Vec, Vec) { let (mut all, mut visible) = (Vec::new(), Vec::new()); if let Some(source) = source { collect( std::slice::from_ref(&source.root), - &paired, false, &mut all, &mut visible, @@ -110,8 +89,8 @@ fn change_coverage(sides: &Pairing) -> ChangeCoverage { Pairing::LeftOnly { lhs } => (Some(lhs), None), Pairing::RightOnly { rhs } => (None, Some(rhs)), }; - let (base, visible_base) = side(lhs, rhs); - let (head, visible_head) = side(rhs, lhs); + let (base, visible_base) = side(lhs); + let (head, visible_head) = side(rhs); ChangeCoverage { all: StructuralChanges { base, head }, initially_visible: StructuralChanges { @@ -143,6 +122,7 @@ fn coalesce(mut ranges: Vec) -> Vec { #[cfg(test)] mod visible_tests { use super::*; + use crate::plugin::cursor::tests::name_pairs; use crate::protocol::{SourcePos, SourceRange, Span}; fn test_root(regions: Vec) -> Region { @@ -243,7 +223,9 @@ mod visible_tests { leaf(9, 9, (3, 4), &[], false), leaf(10, 8, (4, 7), &[4, 5], true), ]); - let coverage = change_coverage(&Pairing::Both { lhs, rhs }); + let mut sides = Pairing::Both { lhs, rhs }; + name_pairs(&mut sides); + let coverage = change_coverage(&sides); assert_eq!(coverage.all.head, vec![[0, 2], [4, 15]]); assert_eq!(coverage.all.base, vec![[0, 1], [4, 7]]); let counts = coverage.initially_visible.counts(); @@ -266,6 +248,7 @@ mod visible_tests { )], )]); let mut sides = Pairing::Both { lhs, rhs }; + name_pairs(&mut sides); let hidden = change_coverage(&sides); assert_eq!(hidden.all.head, vec![[0, 1], [2, 3]]); assert!(hidden.all.base.is_empty()); // Added tokens do not imply removed tokens. diff --git a/diffr/src/plugin/tests/context.rs b/diffr/src/plugin/tests/context.rs index 741d7ce0e..d9cdbc763 100644 --- a/diffr/src/plugin/tests/context.rs +++ b/diffr/src/plugin/tests/context.rs @@ -299,6 +299,7 @@ fn a_fold_whose_matched_partner_holds_changes_stays_open() { ], ), }; + name_pairs(&mut sides); let (file, _) = project("a.py", "", ""); run("context", json!({"lines": 1}), &file, &mut sides); let collapsed = |source: &Source| { diff --git a/diffr/src/plugin/tests/deleted_bodies.rs b/diffr/src/plugin/tests/deleted_bodies.rs index c56448a85..32e85668c 100644 --- a/diffr/src/plugin/tests/deleted_bodies.rs +++ b/diffr/src/plugin/tests/deleted_bodies.rs @@ -80,6 +80,7 @@ fn deleted_bodies_skip_folds_with_paired_content() { lhs: source(vec![rewritten, removed]), rhs: source(vec![leaf(5, 0, 0, 20)]), }; + name_pairs(&mut sides); run( "deleted-bodies", json!({"min_lines": 3}), @@ -113,6 +114,7 @@ fn a_matched_function_whose_lines_all_went_away_is_a_removal() { lhs: source(vec![moved, kept]), rhs: source(vec![kept_rhs, moved_rhs]), }; + name_pairs(&mut sides); run( "deleted-bodies", json!({"min_lines": 3}), diff --git a/diffr/src/plugin/tests/mod.rs b/diffr/src/plugin/tests/mod.rs index 177c41b5c..0502c5514 100644 --- a/diffr/src/plugin/tests/mod.rs +++ b/diffr/src/plugin/tests/mod.rs @@ -13,6 +13,7 @@ use crate::options::DiffOptions; use crate::pairing::Pairing; use crate::protocol::{self, project, Diff, FileChange, FileRef, FileStatus, Node, Region, Source}; use serde_json::json; +use std::collections::BTreeMap; use std::num::NonZeroUsize; /// A test side's root. Its id stays clear of the regions' ids and differs @@ -21,6 +22,35 @@ pub(crate) fn test_root(regions: Vec) -> Region { let id = 1000 + regions.iter().map(|region| region.id).min().unwrap_or(0); Region::root(id, regions) } + +/// Set each leaf's `pair` from the fixture's alignment ids. +pub(crate) fn name_pairs(sides: &mut Pairing) { + fn name(regions: &mut [Region], other: &BTreeMap) { + for region in regions { + match &mut region.node { + Node::Leaf { + alignment_id, pair, .. + } => *pair = other.get(alignment_id).copied(), + Node::Fold { children, .. } => name(children, other), + } + } + } + let Pairing::Both { lhs, rhs } = sides else { + return; + }; + let ids = |source: &Source| { + let mut ids = BTreeMap::new(); + walk(std::slice::from_ref(&source.root), &mut |region| { + if let Some(alignment) = region.alignment_id() { + ids.insert(alignment, region.id); + } + }); + ids + }; + let (lhs_ids, rhs_ids) = (ids(lhs), ids(rhs)); + name(std::slice::from_mut(&mut lhs.root), &rhs_ids); + name(std::slice::from_mut(&mut rhs.root), &lhs_ids); +} use std::path::Path; pub(crate) fn walk(regions: &[Region], visit: &mut impl FnMut(&Region)) { diff --git a/diffr/src/plugin/tests/removed_runs.rs b/diffr/src/plugin/tests/removed_runs.rs index 36d4900ad..84e5fb0e3 100644 --- a/diffr/src/plugin/tests/removed_runs.rs +++ b/diffr/src/plugin/tests/removed_runs.rs @@ -190,6 +190,7 @@ fn removed_runs_skip_paired_leaves_and_collapsed_ancestors() { ]), rhs: source(vec![paired_rhs]), }; + name_pairs(&mut sides); run( "removed-runs", json!({"min_lines": 5}), @@ -248,6 +249,7 @@ fn removed_runs_stay_open_under_a_paired_function() { lhs: source(vec![rewritten, removed]), rhs: source(vec![removed_leaf(6, 3, 0, 10, &[])]), }; + name_pairs(&mut sides); run( "removed-runs", json!({"min_lines": 5}), diff --git a/diffr/src/plugin/wasm.rs b/diffr/src/plugin/wasm.rs index 2fedbaf19..bd6a8a04c 100644 --- a/diffr/src/plugin/wasm.rs +++ b/diffr/src/plugin/wasm.rs @@ -528,7 +528,7 @@ impl Pipeline { mailbox.send(job).await.map_err(|_| trapped())?; } let cursor = result.await.map_err(|_| trapped())??; - Ok(cursor.sides) + Ok(cursor.into_sides()) } /// Give the file to the least-loaded worker whose mailbox has room. When