1use crate::io::{current_timestamp, find_char_boundary};
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct Buffer {
25 pub id: Option<i64>,
27
28 pub name: Option<String>,
30
31 pub source: Option<PathBuf>,
33
34 pub content: String,
36
37 pub metadata: BufferMetadata,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
43pub struct BufferMetadata {
44 pub content_type: Option<String>,
46
47 pub created_at: i64,
49
50 pub updated_at: i64,
52
53 pub size: usize,
55
56 pub line_count: Option<usize>,
58
59 pub chunk_count: Option<usize>,
61
62 pub content_hash: Option<String>,
64}
65
66impl Buffer {
67 #[must_use]
83 pub fn from_content(content: String) -> Self {
84 let size = content.len();
85 let now = current_timestamp();
86 Self {
87 id: None,
88 name: None,
89 source: None,
90 content,
91 metadata: BufferMetadata {
92 size,
93 created_at: now,
94 updated_at: now,
95 ..Default::default()
96 },
97 }
98 }
99
100 #[must_use]
120 pub fn from_file(path: PathBuf, content: String) -> Self {
121 let size = content.len();
122 let content_type = infer_content_type(&path);
123 let name = path
124 .file_name()
125 .and_then(|n| n.to_str())
126 .map(ToString::to_string);
127 let now = current_timestamp();
128
129 Self {
130 id: None,
131 name,
132 source: Some(path),
133 content,
134 metadata: BufferMetadata {
135 content_type,
136 size,
137 created_at: now,
138 updated_at: now,
139 ..Default::default()
140 },
141 }
142 }
143
144 #[must_use]
151 pub fn from_named(name: String, content: String) -> Self {
152 let mut buffer = Self::from_content(content);
153 buffer.name = Some(name);
154 buffer
155 }
156
157 #[must_use]
159 pub const fn size(&self) -> usize {
160 self.content.len()
161 }
162
163 pub fn line_count(&mut self) -> usize {
167 if let Some(count) = self.metadata.line_count {
168 return count;
169 }
170 let count = self.content.lines().count();
171 self.metadata.line_count = Some(count);
172 count
173 }
174
175 #[must_use]
186 pub fn slice(&self, start: usize, end: usize) -> Option<&str> {
187 if start <= end && end <= self.content.len() {
188 self.content.get(start..end)
189 } else {
190 None
191 }
192 }
193
194 #[must_use]
200 pub fn peek(&self, len: usize) -> &str {
201 let end = len.min(self.content.len());
202 let end = find_char_boundary(&self.content, end);
204 &self.content[..end]
205 }
206
207 #[must_use]
213 pub fn peek_end(&self, len: usize) -> &str {
214 let start = self.content.len().saturating_sub(len);
215 let start = find_char_boundary(&self.content, start);
217 &self.content[start..]
218 }
219
220 #[must_use]
222 pub const fn is_empty(&self) -> bool {
223 self.content.is_empty()
224 }
225
226 #[must_use]
228 pub fn display_name(&self) -> String {
229 if let Some(ref name) = self.name {
230 return name.clone();
231 }
232 if let Some(ref path) = self.source
233 && let Some(name) = path.file_name()
234 && let Some(s) = name.to_str()
235 {
236 return s.to_string();
237 }
238 if let Some(id) = self.id {
239 return format!("buffer-{id}");
240 }
241 "unnamed".to_string()
242 }
243
244 pub fn set_chunk_count(&mut self, count: usize) {
246 self.metadata.chunk_count = Some(count);
247 self.metadata.updated_at = current_timestamp();
248 }
249
250 pub fn compute_hash(&mut self) {
252 use std::collections::hash_map::DefaultHasher;
253 use std::hash::{Hash, Hasher};
254
255 let mut hasher = DefaultHasher::new();
256 self.content.hash(&mut hasher);
257 self.metadata.content_hash = Some(format!("{:016x}", hasher.finish()));
258 }
259}
260
261fn infer_content_type(path: &std::path::Path) -> Option<String> {
263 path.extension()
264 .and_then(|ext| ext.to_str())
265 .map(str::to_lowercase)
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn test_buffer_from_content() {
274 let buffer = Buffer::from_content("Hello, world!".to_string());
275 assert!(buffer.id.is_none());
276 assert!(buffer.source.is_none());
277 assert_eq!(buffer.size(), 13);
278 assert!(!buffer.is_empty());
279 }
280
281 #[test]
282 fn test_buffer_from_file() {
283 let buffer = Buffer::from_file(PathBuf::from("test.txt"), "content".to_string());
284 assert_eq!(buffer.source, Some(PathBuf::from("test.txt")));
285 assert_eq!(buffer.metadata.content_type, Some("txt".to_string()));
286 assert_eq!(buffer.name, Some("test.txt".to_string()));
287 }
288
289 #[test]
290 fn test_buffer_from_named() {
291 let buffer = Buffer::from_named("my-buffer".to_string(), "content".to_string());
292 assert_eq!(buffer.name, Some("my-buffer".to_string()));
293 }
294
295 #[test]
296 fn test_buffer_slice() {
297 let buffer = Buffer::from_content("Hello, world!".to_string());
298 assert_eq!(buffer.slice(0, 5), Some("Hello"));
299 assert_eq!(buffer.slice(7, 12), Some("world"));
300 assert_eq!(buffer.slice(0, 100), None); assert_eq!(buffer.slice(10, 5), None); }
303
304 #[test]
305 fn test_buffer_peek() {
306 let buffer = Buffer::from_content("Hello, world!".to_string());
307 assert_eq!(buffer.peek(5), "Hello");
308 assert_eq!(buffer.peek(100), "Hello, world!"); }
310
311 #[test]
312 fn test_buffer_peek_end() {
313 let buffer = Buffer::from_content("Hello, world!".to_string());
314 assert_eq!(buffer.peek_end(6), "world!");
315 assert_eq!(buffer.peek_end(100), "Hello, world!"); }
317
318 #[test]
319 fn test_buffer_line_count() {
320 let mut buffer = Buffer::from_content("line1\nline2\nline3".to_string());
321 assert_eq!(buffer.line_count(), 3);
322 assert_eq!(buffer.line_count(), 3);
324 assert_eq!(buffer.metadata.line_count, Some(3));
325 }
326
327 #[test]
328 fn test_buffer_display_name() {
329 let buffer1 = Buffer::from_named("named".to_string(), String::new());
330 assert_eq!(buffer1.display_name(), "named");
331
332 let buffer2 = Buffer::from_file(PathBuf::from("/path/to/file.txt"), String::new());
333 assert_eq!(buffer2.display_name(), "file.txt");
334
335 let mut buffer3 = Buffer::from_content(String::new());
336 buffer3.id = Some(42);
337 assert_eq!(buffer3.display_name(), "buffer-42");
338
339 let buffer4 = Buffer::from_content(String::new());
340 assert_eq!(buffer4.display_name(), "unnamed");
341 }
342
343 #[test]
344 fn test_buffer_display_name_source_without_name() {
345 let mut buffer = Buffer::from_content(String::new());
347 buffer.source = Some(PathBuf::from("/some/path/to/document.md"));
348 assert_eq!(buffer.display_name(), "document.md");
350 }
351
352 #[test]
353 fn test_buffer_hash() {
354 let mut buffer = Buffer::from_content("Hello".to_string());
355 buffer.compute_hash();
356 assert!(buffer.metadata.content_hash.is_some());
357
358 let mut buffer2 = Buffer::from_content("Hello".to_string());
359 buffer2.compute_hash();
360 assert_eq!(buffer.metadata.content_hash, buffer2.metadata.content_hash);
361 }
362
363 #[test]
364 fn test_buffer_empty() {
365 let buffer = Buffer::from_content(String::new());
366 assert!(buffer.is_empty());
367 assert_eq!(buffer.size(), 0);
368 }
369
370 #[test]
371 fn test_buffer_serialization() {
372 let buffer = Buffer::from_named("test".to_string(), "content".to_string());
373 let json = serde_json::to_string(&buffer);
374 assert!(json.is_ok());
375
376 let deserialized: Result<Buffer, _> = serde_json::from_str(&json.unwrap());
377 assert!(deserialized.is_ok());
378 assert_eq!(deserialized.unwrap().content, "content");
379 }
380}