1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use escriba_core::{BufferId, Edit, EditKind, Position, Range};
5use ropey::Rope;
6use serde::{Deserialize, Serialize};
7
8use crate::encoding::Encoding;
9use crate::error::BufferError;
10use crate::line_ending::LineEnding;
11use crate::undo::{UndoEntry, UndoTree};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
26pub struct TextRev(pub u64);
27
28impl TextRev {
29 #[must_use]
30 pub const fn next(self) -> Self {
31 Self(self.0.wrapping_add(1))
32 }
33}
34
35#[derive(Debug, Clone)]
36pub struct Buffer {
37 pub id: BufferId,
38 pub path: Option<PathBuf>,
39 pub rope: Rope,
40 pub modified: bool,
41 pub encoding: Encoding,
42 pub line_ending: LineEnding,
43 pub undo: UndoTree,
44 text_rev: TextRev,
48}
49
50impl Buffer {
51 #[must_use]
52 pub fn empty(id: BufferId) -> Self {
53 Self {
54 id,
55 path: None,
56 rope: Rope::new(),
57 modified: false,
58 encoding: Encoding::default(),
59 line_ending: LineEnding::default(),
60 undo: UndoTree::new(),
61 text_rev: TextRev::default(),
62 }
63 }
64
65 #[must_use]
68 pub const fn text_rev(&self) -> TextRev {
69 self.text_rev
70 }
71
72 #[must_use]
73 pub fn from_str(id: BufferId, src: &str) -> Self {
74 let line_ending = LineEnding::detect(src);
75 Self {
76 id,
77 path: None,
78 rope: Rope::from_str(src),
79 modified: false,
80 encoding: Encoding::default(),
81 line_ending,
82 undo: UndoTree::new(),
83 text_rev: TextRev::default(),
84 }
85 }
86
87 pub fn open(id: BufferId, path: impl AsRef<Path>) -> Result<Self, BufferError> {
88 let path = path.as_ref().to_path_buf();
89 let src = if path.exists() {
90 std::fs::read_to_string(&path)?
91 } else {
92 String::new()
93 };
94 let line_ending = LineEnding::detect(&src);
95 Ok(Self {
96 id,
97 path: Some(path),
98 rope: Rope::from_str(&src),
99 modified: false,
100 encoding: Encoding::default(),
101 line_ending,
102 undo: UndoTree::new(),
103 text_rev: TextRev::default(),
104 })
105 }
106
107 pub fn save(&mut self) -> Result<(), BufferError> {
108 let path = self.path.clone().ok_or(BufferError::NoPath)?;
109 let mut src = String::new();
110 for line in self.rope.lines() {
111 src.push_str(&line.to_string());
112 }
113 std::fs::write(&path, src)?;
114 self.modified = false;
115 Ok(())
116 }
117
118 pub fn save_as(&mut self, path: impl AsRef<Path>) -> Result<(), BufferError> {
119 self.path = Some(path.as_ref().to_path_buf());
120 self.save()
121 }
122
123 #[must_use]
126 pub fn line_count(&self) -> u32 {
127 u32::try_from(self.rope.len_lines()).unwrap_or(u32::MAX)
128 }
129
130 #[must_use]
131 pub fn byte_count(&self) -> usize {
132 self.rope.len_bytes()
133 }
134
135 #[must_use]
136 pub fn char_count(&self) -> usize {
137 self.rope.len_chars()
138 }
139
140 pub fn line(&self, n: u32) -> Option<String> {
141 let n = n as usize;
142 if n >= self.rope.len_lines() {
143 return None;
144 }
145 Some(self.rope.line(n).to_string())
146 }
147
148 pub fn line_len_chars(&self, n: u32) -> u32 {
149 let n = n as usize;
150 if n >= self.rope.len_lines() {
151 return 0;
152 }
153 let line = self.rope.line(n);
154 let mut len = line.len_chars();
155 if line.chars().last().is_some_and(|c| c == '\n' || c == '\r') {
157 len = len.saturating_sub(1);
158 }
159 u32::try_from(len).unwrap_or(u32::MAX)
160 }
161
162 #[must_use]
164 pub fn clamp(&self, pos: Position) -> Position {
165 let line = pos.line.min(self.line_count().saturating_sub(1));
166 let col = pos.column.min(self.line_len_chars(line));
167 Position::new(line, col)
168 }
169
170 pub fn position_to_char(&self, pos: Position) -> Result<usize, BufferError> {
171 let line = pos.line as usize;
172 if line >= self.rope.len_lines() {
173 return Err(BufferError::InvalidPosition {
174 line: pos.line,
175 column: pos.column,
176 total_lines: self.line_count(),
177 });
178 }
179 let line_start = self.rope.line_to_char(line);
180 let line_slice = self.rope.line(line);
181 let max_col = line_slice.len_chars();
182 let col = (pos.column as usize).min(max_col);
183 Ok(line_start + col)
184 }
185
186 #[must_use]
187 pub fn char_to_position(&self, ch: usize) -> Position {
188 let line = self.rope.char_to_line(ch.min(self.rope.len_chars()));
189 let line_start = self.rope.line_to_char(line);
190 let col = ch.saturating_sub(line_start);
191 Position::new(
192 u32::try_from(line).unwrap_or(u32::MAX),
193 u32::try_from(col).unwrap_or(u32::MAX),
194 )
195 }
196
197 pub fn slice(&self, range: Range) -> Result<String, BufferError> {
198 let r = range.normalized();
199 let a = self.position_to_char(r.start)?;
200 let b = self.position_to_char(r.end)?;
201 Ok(self.rope.slice(a..b).to_string())
202 }
203
204 pub fn apply(&mut self, edit: &Edit) -> Result<UndoEntry, BufferError> {
207 let range = edit.range.normalized();
208 let start_char = self.position_to_char(range.start)?;
209 let end_char = self.position_to_char(range.end)?;
210 let previous_text = self.rope.slice(start_char..end_char).to_string();
211
212 self.text_rev = self.text_rev.next();
216
217 let (inserted_len_chars, reverse_kind) = match &edit.kind {
218 EditKind::Insert { text } => {
219 self.rope.insert(start_char, text);
220 (text.chars().count(), EditKind::Delete)
221 }
222 EditKind::Delete => {
223 self.rope.remove(start_char..end_char);
224 (
225 0usize,
226 EditKind::Insert {
227 text: previous_text.clone(),
228 },
229 )
230 }
231 EditKind::Replace { text } => {
232 self.rope.remove(start_char..end_char);
233 self.rope.insert(start_char, text);
234 (
235 text.chars().count(),
236 EditKind::Replace {
237 text: previous_text.clone(),
238 },
239 )
240 }
241 };
242 self.modified = true;
243
244 let reverse_end_char = start_char + inserted_len_chars;
246 let reverse_range = Range::new(
247 self.char_to_position(start_char),
248 self.char_to_position(reverse_end_char),
249 );
250 let reverse_edit = Edit {
251 range: reverse_range,
252 kind: reverse_kind,
253 };
254 let entry = UndoEntry {
255 applied: edit.clone(),
256 reverse: reverse_edit,
257 };
258 self.undo.push(entry.clone());
259 Ok(entry)
260 }
261
262 pub fn undo(&mut self) -> Result<UndoEntry, BufferError> {
263 let entry = self.undo.pop_undo().ok_or(BufferError::NothingToUndo)?;
264 let r = entry.reverse.range.normalized();
266 let a = self.position_to_char(r.start)?;
267 let b = self.position_to_char(r.end)?;
268 self.text_rev = self.text_rev.next();
278 match &entry.reverse.kind {
279 EditKind::Insert { text } => {
280 self.rope.insert(a, text);
281 }
282 EditKind::Delete => {
283 self.rope.remove(a..b);
284 }
285 EditKind::Replace { text } => {
286 self.rope.remove(a..b);
287 self.rope.insert(a, text);
288 }
289 }
290 self.modified = true;
291 Ok(entry)
292 }
293
294 pub fn redo(&mut self) -> Result<UndoEntry, BufferError> {
295 let entry = self.undo.pop_redo().ok_or(BufferError::NothingToRedo)?;
296 let r = entry.applied.range.normalized();
297 let a = self.position_to_char(r.start)?;
298 let b = self.position_to_char(r.end)?;
299 self.text_rev = self.text_rev.next();
309 match &entry.applied.kind {
310 EditKind::Insert { text } => {
311 self.rope.insert(a, text);
312 }
313 EditKind::Delete => {
314 self.rope.remove(a..b);
315 }
316 EditKind::Replace { text } => {
317 self.rope.remove(a..b);
318 self.rope.insert(a, text);
319 }
320 }
321 self.modified = true;
322 Ok(entry)
323 }
324
325 #[must_use]
326 pub fn to_string(&self) -> String {
327 self.rope.to_string()
328 }
329}
330
331#[derive(Debug, Default, Clone)]
333pub struct BufferSet {
334 buffers: HashMap<BufferId, Buffer>,
335 next_id: u64,
336}
337
338impl BufferSet {
339 #[must_use]
340 pub fn new() -> Self {
341 Self::default()
342 }
343
344 pub fn next_id(&mut self) -> BufferId {
345 self.next_id += 1;
346 BufferId(self.next_id)
347 }
348
349 pub fn open(&mut self, path: impl AsRef<Path>) -> Result<BufferId, BufferError> {
350 let id = self.next_id();
351 let buf = Buffer::open(id, path)?;
352 self.buffers.insert(id, buf);
353 Ok(id)
354 }
355
356 pub fn scratch(&mut self, src: &str) -> BufferId {
357 let id = self.next_id();
358 self.buffers.insert(id, Buffer::from_str(id, src));
359 id
360 }
361
362 #[must_use]
363 pub fn get(&self, id: BufferId) -> Option<&Buffer> {
364 self.buffers.get(&id)
365 }
366
367 pub fn get_mut(&mut self, id: BufferId) -> Option<&mut Buffer> {
368 self.buffers.get_mut(&id)
369 }
370
371 #[must_use]
372 pub fn ids(&self) -> Vec<BufferId> {
373 let mut v: Vec<_> = self.buffers.keys().copied().collect();
374 v.sort();
375 v
376 }
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct BufferSummary {
381 pub id: BufferId,
382 pub path: Option<PathBuf>,
383 pub line_count: u32,
384 pub modified: bool,
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use escriba_core::{Edit, Position, Range};
391
392 fn buf(src: &str) -> Buffer {
393 Buffer::from_str(BufferId(1), src)
394 }
395
396 #[test]
397 fn empty_has_one_line() {
398 let b = Buffer::empty(BufferId(1));
399 assert_eq!(b.line_count(), 1);
400 }
401
402 #[test]
403 fn line_counts() {
404 let b = buf("a\nb\nc\n");
405 assert_eq!(b.line_count(), 4); }
407
408 #[test]
409 fn position_char_round_trip() {
410 let b = buf("hello\nworld\n");
411 let p = Position::new(1, 3);
412 let c = b.position_to_char(p).unwrap();
413 assert_eq!(b.char_to_position(c), p);
414 }
415
416 #[test]
417 fn insert_then_undo() {
418 let mut b = buf("hello");
419 let e = Edit::insert(Position::new(0, 5), " world");
420 b.apply(&e).unwrap();
421 assert_eq!(b.to_string(), "hello world");
422 b.undo().unwrap();
423 assert_eq!(b.to_string(), "hello");
424 }
425
426 #[test]
427 fn delete_then_redo() {
428 let mut b = buf("hello world");
429 let e = Edit::delete(Range::new(Position::new(0, 5), Position::new(0, 11)));
430 b.apply(&e).unwrap();
431 assert_eq!(b.to_string(), "hello");
432 b.undo().unwrap();
433 assert_eq!(b.to_string(), "hello world");
434 b.redo().unwrap();
435 assert_eq!(b.to_string(), "hello");
436 }
437
438 #[test]
439 fn replace_is_delete_plus_insert() {
440 let mut b = buf("hello world");
441 let e = Edit::replace(
442 Range::new(Position::new(0, 6), Position::new(0, 11)),
443 "tatara",
444 );
445 b.apply(&e).unwrap();
446 assert_eq!(b.to_string(), "hello tatara");
447 b.undo().unwrap();
448 assert_eq!(b.to_string(), "hello world");
449 }
450
451 #[test]
452 fn clamp_constrains_position() {
453 let b = buf("ab\ncd");
454 assert_eq!(b.line_count(), 2);
455 assert_eq!(b.clamp(Position::new(0, 99)), Position::new(0, 2));
456 assert_eq!(b.clamp(Position::new(99, 0)), Position::new(1, 0));
457 }
458
459 #[test]
460 fn slice_returns_text() {
461 let b = buf("hello world");
462 let s = b
463 .slice(Range::new(Position::new(0, 6), Position::new(0, 11)))
464 .unwrap();
465 assert_eq!(s, "world");
466 }
467
468 #[test]
469 fn save_round_trip() {
470 let dir = tempfile::tempdir().unwrap();
471 let path = dir.path().join("demo.txt");
472 let mut b = Buffer::from_str(BufferId(1), "hello\n");
473 b.save_as(&path).unwrap();
474 let b2 = Buffer::open(BufferId(2), &path).unwrap();
475 assert_eq!(b2.to_string(), "hello\n");
476 }
477
478 #[test]
479 fn buffer_set_tracks_ids() {
480 let mut set = BufferSet::new();
481 let a = set.scratch("one");
482 let b = set.scratch("two");
483 assert_ne!(a, b);
484 assert_eq!(set.ids().len(), 2);
485 assert_eq!(set.get(a).unwrap().to_string(), "one");
486 }
487}
488
489#[cfg(test)]
490mod text_rev_tests {
491 use super::*;
492 use escriba_core::{Edit, Position, Range};
493
494 fn buf(src: &str) -> Buffer {
495 Buffer::from_str(BufferId(0), src)
496 }
497
498 #[test]
499 fn a_fresh_buffer_starts_at_revision_zero() {
500 assert_eq!(buf("hello").text_rev(), TextRev(0));
501 }
502
503 #[test]
504 fn an_applied_edit_advances_the_revision() {
505 let mut b = buf("hello");
506 let before = b.text_rev();
507 b.apply(&Edit::insert(Position::new(0, 0), "X".to_string()))
508 .expect("insert applies");
509 assert_ne!(
510 b.text_rev(),
511 before,
512 "a text change must expire old offsets"
513 );
514 }
515
516 #[test]
517 fn each_edit_advances_it_again() {
518 let mut b = buf("hello");
519 let mut seen = vec![b.text_rev()];
520 for _ in 0..3 {
521 b.apply(&Edit::insert(Position::new(0, 0), "X".to_string()))
522 .expect("insert applies");
523 let now = b.text_rev();
524 assert!(!seen.contains(&now), "revisions must not repeat: {now:?}");
525 seen.push(now);
526 }
527 }
528
529 #[test]
530 fn a_rejected_edit_does_not_advance_the_revision() {
531 let mut b = buf("hello");
534 let before = b.text_rev();
535 let out_of_range = Range {
536 start: Position::new(99, 0),
537 end: Position::new(99, 1),
538 };
539 assert!(
540 b.apply(&Edit::delete(out_of_range)).is_err(),
541 "the edit must fail"
542 );
543 assert_eq!(b.text_rev(), before, "a failed edit changed no text");
544 }
545
546 #[test]
547 fn reading_the_buffer_does_not_advance_the_revision() {
548 let b = buf("hello");
551 let before = b.text_rev();
552 let _ = b.to_string();
553 let _ = b.text_rev();
554 assert_eq!(b.text_rev(), before);
555 }
556}
557
558#[cfg(test)]
559mod undo_rev_tests {
560 use super::*;
561 use escriba_core::{Edit, Position};
562
563 #[test]
564 fn undo_and_redo_advance_the_revision() {
565 let mut b = Buffer::from_str(BufferId(0), "hello");
569 b.apply(&Edit::insert(Position::new(0, 0), "X".to_string()))
570 .expect("insert applies");
571 let after_edit = b.text_rev();
572
573 b.undo().expect("undo applies");
574 assert_ne!(b.text_rev(), after_edit, "undo must advance the revision");
575 let after_undo = b.text_rev();
576
577 b.redo().expect("redo applies");
578 assert_ne!(b.text_rev(), after_undo, "redo must advance it again");
579 }
580
581 #[test]
582 fn a_rejected_undo_does_not_advance_the_revision() {
583 let mut b = Buffer::from_str(BufferId(0), "hello");
584 let before = b.text_rev();
585 assert!(b.undo().is_err(), "nothing to undo");
586 assert_eq!(b.text_rev(), before, "a failed undo changed no text");
587 }
588}