use std::num::NonZeroU64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Revisions {
current: NonZeroU64,
saved: Option<NonZeroU64>,
needles: Option<NonZeroU64>,
}
impl Revisions {
pub fn new() -> Self {
let one = NonZeroU64::new(1).unwrap();
Self {
current: one,
saved: Some(one),
needles: None,
}
}
pub fn current(&self) -> NonZeroU64 {
self.current
}
pub fn bump(&mut self) {
let next = self.current.get().wrapping_add(1);
self.current = NonZeroU64::new(next).unwrap_or(NonZeroU64::new(1).unwrap());
}
pub fn rev_from_gen(content_gen: u64) -> NonZeroU64 {
NonZeroU64::new(content_gen.saturating_add(1)).unwrap_or(NonZeroU64::new(1).unwrap())
}
pub fn adopt(&mut self, rev: NonZeroU64) {
self.current = rev;
}
pub fn is_dirty(&self) -> bool {
self.saved != Some(self.current)
}
pub fn mark_saved_at(&mut self, rev: NonZeroU64) -> bool {
if rev != self.current {
return false;
}
self.saved = Some(rev);
true
}
pub fn mark_saved_current(&mut self) {
self.saved = Some(self.current);
}
pub fn mark_diverged(&mut self) {
self.saved = None;
}
pub fn arm_needles(&mut self) {
self.needles = Some(self.current);
}
pub fn needles_stale(&self) -> bool {
self.needles.is_some_and(|r| r != self.current)
}
pub fn disarm_needles(&mut self) {
self.needles = None;
}
}
impl Default for Revisions {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn nz(v: u64) -> NonZeroU64 {
NonZeroU64::new(v).unwrap()
}
#[test]
fn fresh_buffer_is_clean_at_revision_one() {
let r = Revisions::new();
assert_eq!(r.current(), nz(1));
assert!(!r.is_dirty());
assert!(!r.needles_stale());
}
#[test]
fn bump_dirties_and_mark_saved_current_cleans() {
let mut r = Revisions::new();
r.bump();
assert_eq!(r.current(), nz(2));
assert!(r.is_dirty());
r.mark_saved_current();
assert!(!r.is_dirty());
}
#[test]
fn stale_save_completion_does_not_apply() {
let mut r = Revisions::new();
r.bump(); let issued = r.current();
r.bump(); assert!(!r.mark_saved_at(issued), "stale completion must not apply");
assert!(r.is_dirty(), "buffer stays dirty after a stale completion");
}
#[test]
fn current_save_completion_applies_and_cleans() {
let mut r = Revisions::new();
r.bump();
let issued = r.current();
assert!(r.mark_saved_at(issued));
assert!(!r.is_dirty());
}
#[test]
fn diverged_save_stays_dirty_until_next_save() {
let mut r = Revisions::new();
r.mark_diverged();
assert!(r.is_dirty());
r.mark_saved_current();
assert!(!r.is_dirty());
}
#[test]
fn adopt_moves_the_clock_and_preserves_dirty_semantics() {
let mut r = Revisions::new();
r.mark_saved_current(); r.adopt(nz(7)); assert_eq!(r.current(), nz(7));
assert!(r.is_dirty(), "saved snapshot (1) no longer matches");
r.adopt(nz(7)); assert_eq!(r.current(), nz(7));
}
#[test]
fn bump_skips_zero_on_wraparound() {
let mut r = Revisions::new();
r.adopt(nz(u64::MAX));
r.bump();
assert_eq!(r.current(), nz(1), "wrap-around must substitute 1, never 0");
}
#[test]
fn rev_from_gen_maps_the_backend_counter_into_the_nonzero_domain() {
assert_eq!(Revisions::rev_from_gen(0), nz(1));
assert_eq!(Revisions::rev_from_gen(6), nz(7));
assert_eq!(Revisions::rev_from_gen(u64::MAX), nz(u64::MAX));
}
#[test]
fn needles_die_on_first_edit() {
let mut r = Revisions::new();
r.arm_needles();
assert!(!r.needles_stale(), "just armed — not stale");
r.bump();
assert!(r.needles_stale(), "an edit outlives the needles");
r.disarm_needles();
assert!(!r.needles_stale(), "disarmed needles are never stale");
}
}