diff --git a/src/components/diff_view.rs b/src/components/diff_view.rs index 4942a03..62cd5fc 100644 --- a/src/components/diff_view.rs +++ b/src/components/diff_view.rs @@ -1,5 +1,5 @@ use crate::{ - data::{FileDiff, VersionDiff}, + data::{ChunkInfo, FileDiff, VersionDiff}, syntax::{highlight_changes, infer_syntax_for_file, syntect_style_to_css}, }; use bytes::Bytes; @@ -16,7 +16,7 @@ struct DiffGroupInfo { /// The actual changes group: Vec<(ChangeTag, Vec<(Style, bytes::Bytes)>)>, /// What range of lines the group covers (used as a Yew list key) - range: std::ops::Range, + range: ChunkInfo, /// Whether the group contains an actual diff (and therefore shows some context) in_context: bool, } @@ -70,12 +70,18 @@ pub fn DiffView(props: &DiffViewProps) -> Html { // Group contiguous lines by whether they contain an actual diff +/- some context buffer. let mut cursor = 0; let mut stack: Vec = vec![]; + for next_range in ranges { // out of context lines - if next_range.start != 0 { + if next_range.start() != 0 { stack.push(DiffGroupInfo { - group: changes.by_ref().take(next_range.start - cursor).collect(), - range: cursor..next_range.start, + group: changes.by_ref().take(next_range.start() - cursor).collect(), + range: ChunkInfo { + range: cursor..next_range.start(), + left_start: (next_range.left_start + cursor).saturating_sub(next_range.start()), + right_start: (next_range.right_start + cursor) + .saturating_sub(next_range.start()), + }, in_context: false, }); } @@ -83,18 +89,23 @@ pub fn DiffView(props: &DiffViewProps) -> Html { stack.push(DiffGroupInfo { group: changes .by_ref() - .take(next_range.end - next_range.start) + .take(next_range.end() - next_range.start()) .collect(), range: next_range.clone(), in_context: true, }); - cursor = next_range.end; + cursor = next_range.end(); } if changes.len() > 0 { // Trailing unchanged lines at the end of a file stack.push(DiffGroupInfo { group: changes.by_ref().collect(), - range: cursor..file_diff.changes.len(), + range: ChunkInfo { + range: cursor..file_diff.changes.len(), + left_start: (cursor).saturating_sub(file_diff.summary.added as usize), + right_start: (cursor).saturating_sub(file_diff.summary.removed as usize), + }, + // When comparing a version of the crate to itself, this group will // always contain the full text of the file. Don't collapse it. in_context: is_identical_version, @@ -136,10 +147,10 @@ pub fn UnifiedDiffView(props: &AnyDiffViewProps) -> Html { .map(|DiffGroupInfo {group, range, in_context}| { let res = html!{ }; overall_index += group.len(); @@ -192,7 +203,7 @@ pub fn SplitDiffView(props: &AnyDiffViewProps) -> Html { key={format!("{:?}", range)} group={group.clone()} {in_context} - group_start_index={overall_index} + group_start_index={(overall_index, range.left_start, range.right_start)} /> }; overall_index += group.len(); @@ -218,7 +229,7 @@ fn ExpandIcon() -> Html { pub struct DiffLineGroupProps { group: Vec<(ChangeTag, Vec<(Style, bytes::Bytes)>)>, in_context: bool, - group_start_index: usize, + group_start_index: (usize, usize, usize), } #[derive(Properties, PartialEq)] @@ -234,8 +245,20 @@ pub fn DiffLineGroup(props: &DiffLineGroupProps) -> Html { let folded = folded.clone(); Callback::from(move |_| folded.set(!*folded)) }; - let group_start_index = props.group_start_index + 1; - let end_index = group_start_index + props.group.len() - 1; + + // go from 0-indexed to 1-indexed + let start_index = ( + props.group_start_index.0 + 1, + props.group_start_index.1 + 1, + props.group_start_index.2 + 1, + ); + + // use the fact that folded sections never contain changes + let end_index = ( + start_index.0 + props.group.len() - 1, + start_index.1 + props.group.len() - 1, + start_index.2 + props.group.len() - 1, + ); if *folded { html! { @@ -244,33 +267,45 @@ pub fn DiffLineGroup(props: &DiffLineGroupProps) -> Html { } } else { + let (mut left_idx, mut right_idx) = (start_index.1, start_index.2); html! { <> if !props.in_context { } { - props.group.iter().enumerate().map(|(index, (tag, change))| { - let overall_index = group_start_index + index; - let (sign, class) = match tag { - ChangeTag::Delete => ("-", "deletion"), - ChangeTag::Insert => ("+", "insertion"), - ChangeTag::Equal => (" ", "unchanged"), + props.group.iter().map(|(tag, change)| { + let (sign, class, left, right) = match tag { + ChangeTag::Delete => ("-", "deletion", left_idx.to_string(), String::new()), + ChangeTag::Insert => ("+", "insertion", String::new(), right_idx.to_string()), + ChangeTag::Equal => (" ", "unchanged", left_idx.to_string(), right_idx.to_string()), + }; + (left_idx, right_idx) = match tag { + ChangeTag::Delete => (left_idx + 1, right_idx), + ChangeTag::Insert => (left_idx, right_idx + 1), + ChangeTag::Equal => (left_idx + 1, right_idx + 1), }; + html! {
-
+
{ - format!("{overall_index}") + format!("{left}") }
{ - format!("{overall_index}") + format!("{right}") }
diff --git a/src/data.rs b/src/data.rs index 92a14a4..65a7fb8 100644 --- a/src/data.rs +++ b/src/data.rs @@ -390,7 +390,9 @@ pub struct FileDiff { /// Diff in this file pub changes: Vec<(ChangeTag, Bytes)>, /// Ranges of lines to show for each file - pub context_ranges: Vec>, + pub context_ranges: Vec, + // Redundant - alternativly take from files + pub summary: Changes, } /// Precomputed diff data @@ -473,37 +475,46 @@ impl VersionDiff { match tag { ChangeTag::Equal => {} ChangeTag::Delete => { + // cnt for determining start idx of hunk, wanna start before this line, so do not count current line + offsets.push((index, insertions, deletions)); deletions += 1; - offsets.push(index); } ChangeTag::Insert => { + offsets.push((index, insertions, deletions)); insertions += 1; - offsets.push(index); } } } // compute ranges to show let mut ranges = vec![]; - let mut last_hunk = 0..0; - - for offset in offsets.iter() { - let hunk = offset.saturating_sub(CONTEXT_LINES)..*offset + CONTEXT_LINES + 1; - let overlaps_with_last_hunk = - hunk.start.max(last_hunk.start) <= hunk.end.min(last_hunk.end); + let mut last_hunk = (0..0, 0, 0); + + for (offset, ins, del) in offsets.iter() { + let hunk_start = offset.saturating_sub(CONTEXT_LINES); + let left_start = hunk_start.saturating_sub(*ins); + let right_start = hunk_start.saturating_sub(*del); + + let hunk = ( + hunk_start..*offset + CONTEXT_LINES + 1, + left_start, + right_start, + ); + let overlaps_with_last_hunk = hunk.0.start.max(last_hunk.0.start) + <= hunk.0.end.min(last_hunk.0.end) + CONTEXT_LINES; if overlaps_with_last_hunk { - last_hunk = last_hunk.start..hunk.end; + last_hunk = (last_hunk.0.start..hunk.0.end, last_hunk.1, last_hunk.2); } else { - if last_hunk.end != 0 { - ranges.push(last_hunk.clone()); + if last_hunk.0.end != 0 { + ranges.push(last_hunk.clone().into()); } last_hunk = hunk; } } // Push the last hunk we've computed if any - if last_hunk.end != 0 { - ranges.push(last_hunk) + if last_hunk.0.end != 0 { + ranges.push(last_hunk.into()) } // compute additions @@ -526,6 +537,10 @@ impl VersionDiff { FileDiff { changes, context_ranges: ranges, + summary: Changes { + added: insertions as u64, + removed: deletions as u64, + }, }, ); } @@ -540,6 +555,32 @@ impl VersionDiff { } } +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct ChunkInfo { + pub range: Range, + pub left_start: usize, + pub right_start: usize, +} + +impl From<(Range, usize, usize)> for ChunkInfo { + fn from((range, left_start, right_start): (Range, usize, usize)) -> Self { + ChunkInfo { + range, + left_start, + right_start, + } + } +} + +impl ChunkInfo { + pub fn start(&self) -> usize { + self.range.start + } + pub fn end(&self) -> usize { + self.range.end + } +} + #[derive(Default, Clone, Copy, PartialEq, Eq, Debug)] pub struct Changes { pub added: u64,