Skip to content
This repository was archived by the owner on Oct 9, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 24 additions & 91 deletions crates/diffr-core/src/present.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! lines that stay visible.
use crate::hash::DftHashSet;
use crate::pairing::Pairing;
use crate::protocol::{Diff, LineRange, Node, Region, Source, StructuralChanges, Visibility};
use crate::protocol::{Diff, LineCounts, LineRange, Node, Region, Source, Visibility};

/// Run the plugins on a diff and recount what stays visible. `shape` runs
/// the plugins on a text diff's sides. A hidden file runs no plugin: its
Expand All @@ -27,27 +27,17 @@ pub async fn present(
}),
None => shape(sides).await?,
};
let coverage = change_coverage(&sides);
stats.visible = coverage.initially_visible.counts();
Ok(Diff::Text {
sides,
stats,
structural_changes: coverage.all,
})
stats.visible = visible_changes(&sides);
Ok(Diff::Text { sides, stats })
}
Diff::Binary { sides } => Ok(Diff::Binary { sides }),
}
}

/// Collect complete and default-visible coverage together. A paired leaf counts
/// only lines carrying changed spans; every line of an unpaired leaf counts,
/// including blank lines. Visibility never removes lines from `all`.
struct ChangeCoverage {
all: StructuralChanges,
initially_visible: StructuralChanges,
}

fn change_coverage(sides: &Pairing<Source>) -> ChangeCoverage {
/// Count the changed lines that start visible. A paired leaf counts only
/// lines carrying changed spans; every line of an unpaired leaf counts,
/// including blank lines.
fn visible_changes(sides: &Pairing<Source>) -> LineCounts {
fn alignments(regions: &[Region], out: &mut DftHashSet<u32>) {
for region in regions {
match &region.node {
Expand All @@ -62,61 +52,54 @@ fn change_coverage(sides: &Pairing<Source>) -> ChangeCoverage {
regions: &[Region],
other: &DftHashSet<u32>,
hidden: bool,
all: &mut Vec<LineRange>,
visible: &mut Vec<LineRange>,
) {
for region in regions {
let hidden = hidden || region.visibility.collapsed;
match &region.node {
Node::Leaf { .. } if hidden => {}
Node::Leaf {
alignment_id,
changed,
} => {
let start = all.len();
if other.contains(alignment_id) {
all.extend(changed.iter().map(|span| [span.line, span.line + 1]));
visible.extend(changed.iter().map(|span| [span.line, span.line + 1]));
} else {
let lines = region.range.lines();
all.push([lines.start, lines.end]);
}
if !hidden {
visible.extend_from_slice(&all[start..]);
visible.push([lines.start, lines.end]);
}
}
Node::Fold { children, .. } => collect(children, other, hidden, all, visible),
Node::Fold { children, .. } => collect(children, other, hidden, visible),
}
}
}
fn side(source: Option<&Source>, other: Option<&Source>) -> (Vec<LineRange>, Vec<LineRange>) {
fn side(source: Option<&Source>, other: Option<&Source>) -> u32 {
let mut paired = DftHashSet::default();
if let Some(other) = other {
alignments(std::slice::from_ref(&other.root), &mut paired);
}
let (mut all, mut visible) = (Vec::new(), Vec::new());
let mut visible = Vec::new();
if let Some(source) = source {
collect(
std::slice::from_ref(&source.root),
&paired,
false,
&mut all,
&mut visible,
);
}
(coalesce(all), coalesce(visible))
coalesce(visible)
.iter()
.map(|[start, end]| end - start)
.sum()
}
let (lhs, rhs) = match sides {
Pairing::Both { lhs, rhs } => (Some(lhs), Some(rhs)),
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);
ChangeCoverage {
all: StructuralChanges { base, head },
initially_visible: StructuralChanges {
base: visible_base,
head: visible_head,
},
LineCounts {
added: side(rhs, lhs),
removed: side(lhs, rhs),
}
}

Expand Down Expand Up @@ -241,64 +224,14 @@ 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 });
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();
let counts = visible_changes(&Pairing::Both { lhs, rhs });
assert_eq!(counts.added, 2 + 2 + 3);
assert_eq!(counts.removed, 1);
}

#[test]
fn changing_fold_visibility_never_changes_complete_coverage() {
let lhs = source(vec![leaf(1, 7, (0, 3), &[], false)]);
let rhs = source(vec![fold(
2,
(0, 3),
true,
vec![fold(
3,
(0, 3),
false,
vec![leaf(4, 7, (0, 3), &[2, 0, 0], false)],
)],
)]);
let mut sides = Pairing::Both { lhs, rhs };
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.
assert_eq!(hidden.initially_visible.counts().added, 0);
if let Pairing::Both { rhs, .. } = &mut sides {
let Node::Fold { children, .. } = &mut rhs.root.node else {
unreachable!("a root is a fold");
};
children[0].visibility.collapsed = false;
}
let opened = change_coverage(&sides);
assert_eq!(opened.all, hidden.all);
assert_eq!(opened.initially_visible, opened.all);
}

#[test]
fn deleted_blank_lines_and_empty_files_have_complete_coverage() {
let lhs = source(vec![leaf(1, 0, (0, 2), &[], true)]);
let deleted = change_coverage(&Pairing::LeftOnly { lhs });
assert_eq!(deleted.all.base, vec![[0, 2]]);
assert!(deleted.all.head.is_empty());
assert_eq!(deleted.initially_visible.counts().removed, 0);
let empty = change_coverage(&Pairing::RightOnly {
rhs: source(vec![]),
});
assert_eq!(empty.all, StructuralChanges::default());
}

#[test]
fn a_missing_side_counts_nothing() {
let rhs = source(vec![leaf(0, 1, (0, 1), &[0], false)]);
let counts = change_coverage(&Pairing::RightOnly { rhs })
.initially_visible
.counts();
assert_eq!(counts.added, 1);
assert_eq!(counts.removed, 0);
fn every_line_of_a_deleted_file_counts_even_blank_ones() {
let lhs = source(vec![leaf(1, 0, (0, 2), &[], false)]);
assert_eq!(visible_changes(&Pairing::LeftOnly { lhs }).removed, 2);
}
}
26 changes: 0 additions & 26 deletions crates/diffr-core/src/protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,6 @@ pub enum Diff {
#[serde(flatten)]
sides: Pairing<Source>,
stats: Stats,
/// All structurally changed lines, including content hidden by folds.
structural_changes: StructuralChanges,
},
/// Either side being binary makes the whole diff binary.
Binary {
Expand Down Expand Up @@ -356,25 +354,6 @@ pub struct SourcePos {
/// Zero-based, half-open source line interval: `[start, end)` on the wire.
pub type LineRange = [u32; 2];

/// Structural change coverage, independent of visibility. Ranges are sorted,
/// nonempty, disjoint, and coalesced when adjacent. A missing side has no ranges.
/// `base` refers to `lhs`; `head` refers to `rhs`.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct StructuralChanges {
pub base: Vec<LineRange>,
pub head: Vec<LineRange>,
}

impl StructuralChanges {
pub fn counts(&self) -> LineCounts {
let count = |ranges: &[LineRange]| ranges.iter().map(|[start, end]| end - start).sum();
LineCounts {
added: count(&self.head),
removed: count(&self.base),
}
}
}

/// Line counts for one file. `fallback` is present exactly when the AST
/// match did not run and the alignment is a line diff, carrying why:
/// `too_complex`, `too_large`, `unsupported_language`, `parse_error`,
Expand Down Expand Up @@ -486,10 +465,6 @@ mod tests {
}],
),
},
structural_changes: StructuralChanges {
base: vec![],
head: vec![[1, 2]],
},
stats: Stats {
textual: LineCounts {
added: 1,
Expand Down Expand Up @@ -543,7 +518,6 @@ mod tests {
"rhs": {"text": "fn f() {\n 1 + 2\n}\n",
"root": root(10, region(5, json!([{"line": 1, "start_column": 5, "end_column": 9}])))},
"stats": {"textual": {"added": 1, "removed": 1}, "visible": {"added": 1, "removed": 1}},
"structural_changes": {"base": [], "head": [[1, 2]]},
},
});
assert_eq!(serde_json::to_value(example_file()).unwrap(), expected);
Expand Down
2 changes: 0 additions & 2 deletions crates/diffr-core/src/protocol/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,6 @@ pub fn diff(result: &DiffResult, inputs: Inputs<'_>) -> Diff {
Diff::Text {
sides,
stats: stats(result, lhs_src, rhs_src),
// Filled together with visible counts after plugins shape the trees.
structural_changes: Default::default(),
}
}

Expand Down
33 changes: 0 additions & 33 deletions diffr-ts/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,42 +158,10 @@ export const StructuralVisibilitySchema: z.ZodType<StructuralVisibility> =
}),
);

/** Structural changed lines, independent of presentation and fold state. */
export type StructuralChanges = {
base: [number, number][];
head: [number, number][];
};

const structuralRangesSchema = z
.array(z.tuple([structuralU32, structuralU32]))
.refine(
(ranges) =>
ranges.every(
([start, end], index) =>
start < end && (index === 0 || ranges[index - 1][1] < start),
),
"Expected sorted, coalesced, nonempty half-open ranges",
);

export const StructuralChangesSchema: z.ZodType<StructuralChanges> = z.object({
base: structuralRangesSchema,
head: structuralRangesSchema,
});

export function structuralChangeCounts(
changes: StructuralChanges,
): StructuralLineCounts {
const count = (ranges: [number, number][]) =>
ranges.reduce((sum, [start, end]) => sum + end - start, 0);

return { added: count(changes.head), removed: count(changes.base) };
}

export type StructuralDiff =
| ({
type: "text";
stats: StructuralStats;
structural_changes: StructuralChanges;
} & StructuralPairing<StructuralSource>)
| ({ type: "binary" } & StructuralPairing<StructuralBinaryRef>);

Expand All @@ -203,7 +171,6 @@ export const StructuralDiffSchema: z.ZodType<StructuralDiff> = z.lazy(() =>
.object({
type: z.literal("text"),
stats: StructuralStatsSchema,
structural_changes: StructuralChangesSchema,
})
.and(structuralPairingSchema(StructuralSourceSchema)),
z
Expand Down
Loading
Loading