use crate::{DiffSide, FileDiff, Fingerprint, PatchLine, PatchLineKind, RepoPath};
use serde::{Deserialize, Serialize};
const CONTEXT_RADIUS: usize = 2;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct LineAnchor {
pub path: RepoPath,
pub side: DiffSide,
pub old_line_no: Option<usize>,
pub new_line_no: Option<usize>,
pub kind: PatchLineKind,
pub fingerprint: Fingerprint,
pub content_fingerprint: Fingerprint,
}
impl LineAnchor {
#[must_use]
pub fn for_line(file: &FileDiff, side: DiffSide, hunk: usize, line: usize) -> Option<Self> {
let lines = &file.hunks.get(hunk)?.lines;
let anchored = lines.get(line)?;
anchored.line_number(side)?;
let start = line.saturating_sub(CONTEXT_RADIUS);
let end = (line + CONTEXT_RADIUS + 1).min(lines.len());
Some(Self {
path: file.path.clone(),
side,
old_line_no: anchored.old_line_no,
new_line_no: anchored.new_line_no,
kind: anchored.kind,
fingerprint: context_fingerprint(&file.path, side, anchored, &lines[start..end]),
content_fingerprint: Self::content_fingerprint_of(side, anchored),
})
}
#[must_use]
pub fn content_fingerprint_of(side: DiffSide, line: &PatchLine) -> Fingerprint {
Fingerprint::of([side.as_str(), line.kind.as_str(), line.text.as_ref()])
}
#[must_use]
pub const fn line_number(&self) -> Option<usize> {
match self.side {
DiffSide::Old => self.old_line_no,
DiffSide::New => self.new_line_no,
}
}
#[must_use]
pub fn addresses_same_side(&self, other: &Self) -> bool {
self.path == other.path && self.side == other.side
}
}
fn context_fingerprint(
path: &RepoPath,
side: DiffSide,
line: &PatchLine,
nearby: &[PatchLine],
) -> Fingerprint {
Fingerprint::of(
[
path.as_str(),
side.as_str(),
line.text.as_ref(),
line.kind.as_str(),
]
.into_iter()
.chain(nearby.iter().map(|context| context.text.as_ref())),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FileDiff;
#[test]
fn deterministic_and_side_sensitive() {
let file = FileDiff::from_texts("a.rs", "x\n", "y\n").unwrap();
let old = LineAnchor::for_line(&file, DiffSide::Old, 0, 0).unwrap();
let new = LineAnchor::for_line(&file, DiffSide::New, 0, 1).unwrap();
assert_ne!(old.fingerprint, new.fingerprint);
assert_eq!(
old,
LineAnchor::for_line(&file, DiffSide::Old, 0, 0).unwrap()
);
assert!(!old.addresses_same_side(&new));
}
#[test]
fn cheap_content_digest_matches_the_stored_one() {
let file = FileDiff::from_texts("a.rs", "x\n", "y\n").unwrap();
let anchor = LineAnchor::for_line(&file, DiffSide::New, 0, 1).unwrap();
let line = file.line(0, 1).unwrap();
assert_eq!(
anchor.content_fingerprint,
LineAnchor::content_fingerprint_of(DiffSide::New, line)
);
}
#[test]
fn rejects_lines_without_a_number_on_the_requested_side() {
let file = FileDiff::from_texts("a.rs", "x\n", "y\n").unwrap();
assert!(LineAnchor::for_line(&file, DiffSide::Old, 0, 1).is_none());
assert!(LineAnchor::for_line(&file, DiffSide::New, 9, 0).is_none());
}
}