use serde::{Deserialize, Serialize};
use super::Buffer;
use crate::history::History;
use crate::id::BufferRevision;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BufferSeed {
pub text: String,
pub revision: BufferRevision,
pub history: History,
#[serde(with = "crate::path_serde::option")]
pub path: Option<std::path::PathBuf>,
pub name: Option<String>,
pub dirty: bool,
pub readonly: bool,
pub disk_stamp: Option<std::time::SystemTime>,
#[serde(with = "crate::path_serde::option")]
pub file_identity: Option<std::path::PathBuf>,
}
impl Buffer {
pub fn seed(&self) -> BufferSeed {
BufferSeed {
text: self.rope.to_string(),
revision: self.revision(),
history: self.history.clone(),
path: self.path.clone(),
name: self.name.clone(),
dirty: self.dirty,
readonly: self.readonly,
disk_stamp: self.disk_stamp,
file_identity: self.file_identity.clone(),
}
}
}
#[derive(Serialize)]
pub struct BufferWitness {
pub text_digest: String,
pub text_bytes: usize,
pub revision: BufferRevision,
pub history: History,
#[serde(with = "crate::path_serde::option")]
pub path: Option<std::path::PathBuf>,
pub name: Option<String>,
pub dirty: bool,
pub readonly: bool,
pub disk_stamp: Option<std::time::SystemTime>,
#[serde(with = "crate::path_serde::option")]
pub file_identity: Option<std::path::PathBuf>,
}
impl Buffer {
pub fn witness(&self) -> BufferWitness {
BufferWitness {
text_digest: self.text_digest(),
text_bytes: self.len_bytes(),
revision: self.revision(),
history: self.history.clone(),
path: self.path.clone(),
name: self.name.clone(),
dirty: self.dirty,
readonly: self.readonly,
disk_stamp: self.disk_stamp,
file_identity: self.file_identity.clone(),
}
}
pub fn text_digest(&self) -> String {
use sha2::{Digest, Sha256};
use std::fmt::Write as _;
let mut hasher = Sha256::new();
for chunk in self.rope.chunks() {
hasher.update(chunk.as_bytes());
}
let digest: [u8; 32] = hasher.finalize().into();
let mut text = String::with_capacity(64);
let _ = digest
.iter()
.try_for_each(|byte| write!(text, "{byte:02x}"));
text
}
}
impl BufferSeed {
pub fn into_buffer(self) -> Result<Buffer, crate::history::HistoryError> {
let mut buffer = Buffer::from_text(&self.text);
buffer.restore_history(self.history)?;
buffer.epoch = self.revision.get();
buffer.path = self.path;
buffer.name = self.name;
buffer.dirty = self.dirty;
buffer.readonly = self.readonly;
buffer.disk_stamp = self.disk_stamp;
buffer.file_identity = self.file_identity;
Ok(buffer)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn seed_round_trips_content_history_and_disk_baseline() {
let mut buffer = Buffer::from_text("line\nline two\n");
buffer.begin_undo_group();
buffer
.edit()
.replace(crate::Range::charwise(5, 8), "XY")
.unwrap();
buffer.commit_undo_group();
buffer.dirty = true;
buffer.disk_stamp = Some(std::time::UNIX_EPOCH);
let seed = buffer.seed();
let mut restored = seed.into_buffer().unwrap();
assert_eq!(restored.text().to_string(), "line\nXYe two\n");
restored.undo().unwrap();
assert_eq!(restored.text().to_string(), "line\nline two\n");
}
#[test]
fn history_that_disagrees_with_text_is_rejected() {
let mut buffer = Buffer::from_text("abc\n");
buffer.begin_undo_group();
buffer
.edit()
.replace(crate::Range::charwise(0, 1), "z")
.unwrap();
buffer.commit_undo_group();
let mut seed = buffer.seed();
seed.text = "different bytes\n".into();
assert!(seed.into_buffer().is_err());
}
#[test]
fn witness_digest_tracks_text_exactly() {
let buffer = Buffer::from_text("abc\n");
let digest = buffer.text_digest();
assert_eq!(Buffer::from_text("abc\n").text_digest(), digest);
let mut edited = Buffer::from_text("abc\n");
edited
.edit()
.replace(crate::Range::charwise(0, 1), "z")
.unwrap();
assert_ne!(edited.text_digest(), digest);
let witness = edited.witness();
assert_eq!(witness.text_bytes, 4);
assert_eq!(witness.text_digest.len(), 64);
assert_eq!(witness.revision, edited.revision());
}
}