strop_core/buffer/
seed.rs1use serde::{Deserialize, Serialize};
6
7use super::Buffer;
8use crate::history::History;
9use crate::id::BufferRevision;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct BufferSeed {
16 pub text: String,
17 pub revision: BufferRevision,
18 pub history: History,
19 #[serde(with = "crate::path_serde::option")]
20 pub path: Option<std::path::PathBuf>,
21 pub name: Option<String>,
22 pub dirty: bool,
23 pub readonly: bool,
24 #[serde(default)]
27 pub readonly_reason: Option<super::ReadonlyReason>,
28 pub disk_stamp: Option<std::time::SystemTime>,
29 #[serde(with = "crate::path_serde::option")]
30 pub file_identity: Option<std::path::PathBuf>,
31}
32
33impl Buffer {
34 pub fn seed(&self) -> BufferSeed {
35 BufferSeed {
36 text: self.rope.to_string(),
37 revision: self.revision(),
38 history: self.history.clone(),
39 path: self.path.clone(),
40 name: self.name.clone(),
41 dirty: self.dirty,
42 readonly: self.readonly,
43 readonly_reason: self.readonly_reason,
44 disk_stamp: self.disk_stamp,
45 file_identity: self.file_identity.clone(),
46 }
47 }
48}
49
50#[derive(Serialize)]
55pub struct BufferWitness {
56 pub text_digest: String,
57 pub text_bytes: usize,
58 pub revision: BufferRevision,
59 pub history: History,
60 #[serde(with = "crate::path_serde::option")]
61 pub path: Option<std::path::PathBuf>,
62 pub name: Option<String>,
63 pub dirty: bool,
64 pub readonly: bool,
65 pub readonly_reason: Option<super::ReadonlyReason>,
66 pub disk_stamp: Option<std::time::SystemTime>,
67 #[serde(with = "crate::path_serde::option")]
68 pub file_identity: Option<std::path::PathBuf>,
69}
70
71impl Buffer {
72 pub fn witness(&self) -> BufferWitness {
75 BufferWitness {
76 text_digest: self.text_digest(),
77 text_bytes: self.len_bytes(),
78 revision: self.revision(),
79 history: self.history.clone(),
80 path: self.path.clone(),
81 name: self.name.clone(),
82 dirty: self.dirty,
83 readonly: self.readonly,
84 readonly_reason: self.readonly_reason,
85 disk_stamp: self.disk_stamp,
86 file_identity: self.file_identity.clone(),
87 }
88 }
89
90 pub fn text_digest(&self) -> String {
92 use sha2::{Digest, Sha256};
93 use std::fmt::Write as _;
94 let mut hasher = Sha256::new();
95 for chunk in self.rope.chunks() {
96 hasher.update(chunk.as_bytes());
97 }
98 let digest: [u8; 32] = hasher.finalize().into();
99 let mut text = String::with_capacity(64);
100 let _ = digest
101 .iter()
102 .try_for_each(|byte| write!(text, "{byte:02x}"));
103 text
104 }
105}
106
107impl BufferSeed {
108 pub fn into_buffer(self) -> Result<Buffer, crate::history::HistoryError> {
111 let mut buffer = Buffer::from_text(&self.text);
112 buffer.restore_history(self.history)?;
113 buffer.epoch = self.revision.get();
114 buffer.path = self.path;
115 buffer.name = self.name;
116 buffer.dirty = self.dirty;
117 buffer.readonly = self.readonly;
118 buffer.readonly_reason = self.readonly_reason;
119 buffer.disk_stamp = self.disk_stamp;
120 buffer.file_identity = self.file_identity;
121 Ok(buffer)
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn seed_round_trips_content_history_and_disk_baseline() {
131 let mut buffer = Buffer::from_text("line\nline two\n");
132 buffer.begin_undo_group();
133 buffer
134 .edit()
135 .replace(crate::Range::charwise(5, 8), "XY")
136 .unwrap();
137 buffer.commit_undo_group();
138 buffer.dirty = true;
139 buffer.disk_stamp = Some(std::time::UNIX_EPOCH);
140 let seed = buffer.seed();
141 let mut restored = seed.into_buffer().unwrap();
142 assert_eq!(restored.text().to_string(), "line\nXYe two\n");
143 restored.undo().unwrap();
145 assert_eq!(restored.text().to_string(), "line\nline two\n");
146 }
147
148 #[test]
149 fn history_that_disagrees_with_text_is_rejected() {
150 let mut buffer = Buffer::from_text("abc\n");
151 buffer.begin_undo_group();
152 buffer
153 .edit()
154 .replace(crate::Range::charwise(0, 1), "z")
155 .unwrap();
156 buffer.commit_undo_group();
157 let mut seed = buffer.seed();
158 seed.text = "different bytes\n".into();
159 assert!(seed.into_buffer().is_err());
160 }
161
162 #[test]
163 fn witness_digest_tracks_text_exactly() {
164 let buffer = Buffer::from_text("abc\n");
165 let digest = buffer.text_digest();
166 assert_eq!(Buffer::from_text("abc\n").text_digest(), digest);
168 let mut edited = Buffer::from_text("abc\n");
169 edited
170 .edit()
171 .replace(crate::Range::charwise(0, 1), "z")
172 .unwrap();
173 assert_ne!(edited.text_digest(), digest);
174 let witness = edited.witness();
176 assert_eq!(witness.text_bytes, 4);
177 assert_eq!(witness.text_digest.len(), 64);
178 assert_eq!(witness.revision, edited.revision());
179 }
180}