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 match &entry.reverse.kind {
269 EditKind::Insert { text } => {
270 self.rope.insert(a, text);
271 }
272 EditKind::Delete => {
273 self.rope.remove(a..b);
274 }
275 EditKind::Replace { text } => {
276 self.rope.remove(a..b);
277 self.rope.insert(a, text);
278 }
279 }
280 self.modified = true;
281 Ok(entry)
282 }
283
284 pub fn redo(&mut self) -> Result<UndoEntry, BufferError> {
285 let entry = self.undo.pop_redo().ok_or(BufferError::NothingToRedo)?;
286 let r = entry.applied.range.normalized();
287 let a = self.position_to_char(r.start)?;
288 let b = self.position_to_char(r.end)?;
289 match &entry.applied.kind {
290 EditKind::Insert { text } => {
291 self.rope.insert(a, text);
292 }
293 EditKind::Delete => {
294 self.rope.remove(a..b);
295 }
296 EditKind::Replace { text } => {
297 self.rope.remove(a..b);
298 self.rope.insert(a, text);
299 }
300 }
301 self.modified = true;
302 Ok(entry)
303 }
304
305 #[must_use]
306 pub fn to_string(&self) -> String {
307 self.rope.to_string()
308 }
309}
310
311#[derive(Debug, Default, Clone)]
313pub struct BufferSet {
314 buffers: HashMap<BufferId, Buffer>,
315 next_id: u64,
316}
317
318impl BufferSet {
319 #[must_use]
320 pub fn new() -> Self {
321 Self::default()
322 }
323
324 pub fn next_id(&mut self) -> BufferId {
325 self.next_id += 1;
326 BufferId(self.next_id)
327 }
328
329 pub fn open(&mut self, path: impl AsRef<Path>) -> Result<BufferId, BufferError> {
330 let id = self.next_id();
331 let buf = Buffer::open(id, path)?;
332 self.buffers.insert(id, buf);
333 Ok(id)
334 }
335
336 pub fn scratch(&mut self, src: &str) -> BufferId {
337 let id = self.next_id();
338 self.buffers.insert(id, Buffer::from_str(id, src));
339 id
340 }
341
342 #[must_use]
343 pub fn get(&self, id: BufferId) -> Option<&Buffer> {
344 self.buffers.get(&id)
345 }
346
347 pub fn get_mut(&mut self, id: BufferId) -> Option<&mut Buffer> {
348 self.buffers.get_mut(&id)
349 }
350
351 #[must_use]
352 pub fn ids(&self) -> Vec<BufferId> {
353 let mut v: Vec<_> = self.buffers.keys().copied().collect();
354 v.sort();
355 v
356 }
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct BufferSummary {
361 pub id: BufferId,
362 pub path: Option<PathBuf>,
363 pub line_count: u32,
364 pub modified: bool,
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use escriba_core::{Edit, Position, Range};
371
372 fn buf(src: &str) -> Buffer {
373 Buffer::from_str(BufferId(1), src)
374 }
375
376 #[test]
377 fn empty_has_one_line() {
378 let b = Buffer::empty(BufferId(1));
379 assert_eq!(b.line_count(), 1);
380 }
381
382 #[test]
383 fn line_counts() {
384 let b = buf("a\nb\nc\n");
385 assert_eq!(b.line_count(), 4); }
387
388 #[test]
389 fn position_char_round_trip() {
390 let b = buf("hello\nworld\n");
391 let p = Position::new(1, 3);
392 let c = b.position_to_char(p).unwrap();
393 assert_eq!(b.char_to_position(c), p);
394 }
395
396 #[test]
397 fn insert_then_undo() {
398 let mut b = buf("hello");
399 let e = Edit::insert(Position::new(0, 5), " world");
400 b.apply(&e).unwrap();
401 assert_eq!(b.to_string(), "hello world");
402 b.undo().unwrap();
403 assert_eq!(b.to_string(), "hello");
404 }
405
406 #[test]
407 fn delete_then_redo() {
408 let mut b = buf("hello world");
409 let e = Edit::delete(Range::new(Position::new(0, 5), Position::new(0, 11)));
410 b.apply(&e).unwrap();
411 assert_eq!(b.to_string(), "hello");
412 b.undo().unwrap();
413 assert_eq!(b.to_string(), "hello world");
414 b.redo().unwrap();
415 assert_eq!(b.to_string(), "hello");
416 }
417
418 #[test]
419 fn replace_is_delete_plus_insert() {
420 let mut b = buf("hello world");
421 let e = Edit::replace(
422 Range::new(Position::new(0, 6), Position::new(0, 11)),
423 "tatara",
424 );
425 b.apply(&e).unwrap();
426 assert_eq!(b.to_string(), "hello tatara");
427 b.undo().unwrap();
428 assert_eq!(b.to_string(), "hello world");
429 }
430
431 #[test]
432 fn clamp_constrains_position() {
433 let b = buf("ab\ncd");
434 assert_eq!(b.line_count(), 2);
435 assert_eq!(b.clamp(Position::new(0, 99)), Position::new(0, 2));
436 assert_eq!(b.clamp(Position::new(99, 0)), Position::new(1, 0));
437 }
438
439 #[test]
440 fn slice_returns_text() {
441 let b = buf("hello world");
442 let s = b
443 .slice(Range::new(Position::new(0, 6), Position::new(0, 11)))
444 .unwrap();
445 assert_eq!(s, "world");
446 }
447
448 #[test]
449 fn save_round_trip() {
450 let dir = tempfile::tempdir().unwrap();
451 let path = dir.path().join("demo.txt");
452 let mut b = Buffer::from_str(BufferId(1), "hello\n");
453 b.save_as(&path).unwrap();
454 let b2 = Buffer::open(BufferId(2), &path).unwrap();
455 assert_eq!(b2.to_string(), "hello\n");
456 }
457
458 #[test]
459 fn buffer_set_tracks_ids() {
460 let mut set = BufferSet::new();
461 let a = set.scratch("one");
462 let b = set.scratch("two");
463 assert_ne!(a, b);
464 assert_eq!(set.ids().len(), 2);
465 assert_eq!(set.get(a).unwrap().to_string(), "one");
466 }
467}
468
469#[cfg(test)]
470mod text_rev_tests {
471 use super::*;
472 use escriba_core::{Edit, Position, Range};
473
474 fn buf(src: &str) -> Buffer {
475 Buffer::from_str(BufferId(0), src)
476 }
477
478 #[test]
479 fn a_fresh_buffer_starts_at_revision_zero() {
480 assert_eq!(buf("hello").text_rev(), TextRev(0));
481 }
482
483 #[test]
484 fn an_applied_edit_advances_the_revision() {
485 let mut b = buf("hello");
486 let before = b.text_rev();
487 b.apply(&Edit::insert(Position::new(0, 0), "X".to_string()))
488 .expect("insert applies");
489 assert_ne!(
490 b.text_rev(),
491 before,
492 "a text change must expire old offsets"
493 );
494 }
495
496 #[test]
497 fn each_edit_advances_it_again() {
498 let mut b = buf("hello");
499 let mut seen = vec![b.text_rev()];
500 for _ in 0..3 {
501 b.apply(&Edit::insert(Position::new(0, 0), "X".to_string()))
502 .expect("insert applies");
503 let now = b.text_rev();
504 assert!(!seen.contains(&now), "revisions must not repeat: {now:?}");
505 seen.push(now);
506 }
507 }
508
509 #[test]
510 fn a_rejected_edit_does_not_advance_the_revision() {
511 let mut b = buf("hello");
514 let before = b.text_rev();
515 let out_of_range = Range {
516 start: Position::new(99, 0),
517 end: Position::new(99, 1),
518 };
519 assert!(
520 b.apply(&Edit::delete(out_of_range)).is_err(),
521 "the edit must fail"
522 );
523 assert_eq!(b.text_rev(), before, "a failed edit changed no text");
524 }
525
526 #[test]
527 fn reading_the_buffer_does_not_advance_the_revision() {
528 let b = buf("hello");
531 let before = b.text_rev();
532 let _ = b.to_string();
533 let _ = b.text_rev();
534 assert_eq!(b.text_rev(), before);
535 }
536}